Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/brave-machines-observe.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
Add an Effect-native `@typeonce/effect-machine/testing` entrypoint with schema-derived scenarios, replayable planner traces, independent statechart law and finite-model verification for compound, parallel, and history states, trace coverage, observed Effect graphs, and live runtime command models.

Expose retained planner transition evidence and correct reentry boundaries, simultaneous parallel target application, and recorded nested-history restoration through inactive ancestors.

Model finite transitions through one discriminated `event | always | done` trigger representation, with independent stabilization, completion ordering, cycle detection, generated mixed-trigger models, managed-runtime differential checks, and activity-lifecycle command verification. Hand-authored finite models now migrate event transitions from `{ source, event, ... }` to `{ source, trigger: { type: "event", event }, ... }`.
2 changes: 2 additions & 0 deletions .changeset/quiet-spiders-inspect.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
---

Add public getters for inspecting compiled state nodes, registered transition handlers, declared transition targets, and the active state configuration of a snapshot. Event, eventless, and completion handlers may declare an upper bound of target paths that is checked against inferred and runtime results.

Represent compiled state-node inspection as a six-way discriminated union so atomic, compound, parallel, final, history, and choice metadata narrow without impossible field combinations.
7 changes: 7 additions & 0 deletions .changeset/strong-machines-guard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@typeonce/effect-machine": patch
---

Harden machine execution and definitions while preserving the existing Effect-native API. Self-stop now uses supervisor-owned terminal arbitration without initialization or worker deadlocks; execution adapters consistently reject incomplete output, history, and choice implementations; finite-union event tags narrow correctly; and machine guards verify the runtime brand value.

State definitions now reject unknown node properties and unsafe state keys at compile time and runtime with path-local diagnostics. Add deterministic lifecycle, adversarial snapshot-codec, type-performance, activity-lifecycle, and planner-versus-runtime verification coverage.
51 changes: 51 additions & 0 deletions perf/types/adapter-readiness-control.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Machine } from "@typeonce/effect-machine"
import { Schema } from "effect"

export const Flow = Schema.TaggedStruct("Flow", {})
export const Idle = Schema.TaggedStruct("Idle", {})
export const Ready = Schema.TaggedStruct("Ready", {})

export const States = Machine.defineStates({
Flow: {
schema: Flow,
initial: "Idle",
states: {
Idle,
Route: {
type: "choice"
},
recent: {
type: "history",
history: "deep"
}
}
},
Ready
})

export const snapshot = States.initial.Ready(Ready.make({}))

export const machine = Machine.make({
id: "perf-readiness",
states: States.states,
events: [],
initial: () => snapshot
}).handle({
Flow: {
history: {
recent: {
default: ({ target }) => target.Flow(Flow.make({}), (flow) => flow.Idle(Idle.make({})))
}
},
states: {
Idle: {},
Route: {
choice: {
targets: ["Ready"],
transition: ({ target }) => target.full.Ready(Ready.make({}))
}
}
}
},
Ready: {}
})
38 changes: 38 additions & 0 deletions perf/types/adapter-readiness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Machine } from "@typeonce/effect-machine"
import { ClusterMachine } from "@typeonce/effect-machine/cluster"
import { AtomMachine } from "@typeonce/effect-machine/reactivity"
import { Atom } from "effect/unstable/reactivity"
import { machine, snapshot } from "./adapter-readiness-control.js"

type Equal<Left, Right> = (<Type>() => Type extends Left ? 1 : 2) extends (<Type>() => Type extends Right ? 1 : 2) ?
true :
false
type Expect<Value extends true> = Value
type IsAny<Value> = 0 extends 1 & Value ? true : false

const child = Machine.child("ready-child", machine)
const atom = AtomMachine.make(machine)
const resumedAtom = AtomMachine.resume(machine, snapshot)
const cluster = ClusterMachine.make("ReadyEntity", machine, { version: "1" })

declare const bound: AtomMachine.Bound<never>
const boundAtom = bound.make(machine)
const boundResumedAtom = bound.resume(machine, snapshot)

type StateIsNotAny = Expect<Equal<IsAny<Machine.Machine.States<typeof machine>>, false>>
type ErrorIsNotAny = Expect<Equal<IsAny<Machine.Machine.Error<typeof machine>>, false>>
type AtomEventIsExact = Expect<
Equal<typeof atom.send extends Atom.Writable<any, infer Event> ? Event : unknown, never>
>
type BoundAtomEventIsExact = Expect<
Equal<typeof boundAtom.send extends Atom.Writable<any, infer Event> ? Event : unknown, never>
>

void Machine.planInitial(machine)
void Machine.start(machine)
void Machine.resume(machine, snapshot)
void Machine.invokeMachine({ child })
void resumedAtom
void cluster
void boundResumedAtom
export type { AtomEventIsExact, BoundAtomEventIsExact, ErrorIsNotAny, StateIsNotAny }
79 changes: 79 additions & 0 deletions perf/types/composition-control.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Machine } from "@typeonce/effect-machine"
import { Schema } from "effect"

export const App = Schema.TaggedStruct("App", {})
export const Workspace = Schema.TaggedStruct("Workspace", {})
export const Editor = Schema.TaggedStruct("Editor", {})
export const Editing = Schema.TaggedStruct("Editing", {})
export const EditorDone = Schema.TaggedStruct("EditorDone", { value: Schema.String })
export const Sync = Schema.TaggedStruct("Sync", {})
export const SyncIdle = Schema.TaggedStruct("SyncIdle", {})
export const SyncDone = Schema.TaggedStruct("SyncDone", { value: Schema.Number })

export const WorkspaceOutput = Schema.Struct({
Editor: Schema.String,
Sync: Schema.Number
})

export const States = Machine.defineStates({
App: {
schema: App,
initial: "Workspace",
states: {
Workspace: {
schema: Workspace,
type: "parallel",
output: WorkspaceOutput,
states: {
Editor: {
schema: Editor,
initial: "Editing",
states: {
Editing,
Done: {
schema: EditorDone,
type: "final",
output: Schema.String
}
}
},
Sync: {
schema: Sync,
initial: "Idle",
states: {
Idle: SyncIdle,
Done: {
schema: SyncDone,
type: "final",
output: Schema.Number
}
}
},
recent: {
type: "history",
history: "deep"
}
}
},
Route: {
type: "choice"
}
}
}
})

export const initialWorkspace = () =>
States.initial.App(App.make({}), (app) =>
app.Workspace(
Workspace.make({}),
(workspace) =>
workspace
.Editor(Editor.make({}), (editor) => editor.Editing(Editing.make({})))
.Sync(Sync.make({}), (sync) => sync.Idle(SyncIdle.make({})))
))

export const machine = Machine.make({
states: States.states,
events: [],
initial: initialWorkspace
})
99 changes: 99 additions & 0 deletions perf/types/composition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Machine } from "@typeonce/effect-machine"
import { Context, Data, Effect } from "effect"
import {
App,
Editing,
Editor,
EditorDone,
initialWorkspace,
machine,
States,
Sync,
SyncDone,
SyncIdle,
Workspace
} from "./composition-control.js"

type Equal<Left, Right> = (<Type>() => Type extends Left ? 1 : 2) extends (<Type>() => Type extends Right ? 1 : 2) ?
true :
false
type Expect<Value extends true> = Value
type IsAny<Value> = 0 extends 1 & Value ? true : false

class CompositionService extends Context.Service<CompositionService, string>()(
"perf/composition/CompositionService"
) {}
class CompositionFailure extends Data.TaggedError("CompositionFailure")<{}> {}

const handled = machine.handle({
App: {
states: {
Workspace: {
history: {
recent: {
default: () => initialWorkspace()
}
},
output: ({ outputs }) => ({
Editor: outputs.Editor,
Sync: outputs.Sync
}),
states: {
Editor: {
states: {
Editing: {
entry: () =>
Effect.flatMap(
CompositionService,
() => Math.random() > 2 ? Effect.fail(new CompositionFailure()) : Effect.void
)
},
Done: {
output: ({ state }) => state.value
}
}
},
Sync: {
states: {
Idle: {},
Done: {
output: ({ state }) => state.value
}
}
}
}
},
Route: {
choice: {
targets: ["App"],
transition: ({ target }) =>
target.full.App(App.make({}), (app) =>
app.Workspace(
Workspace.make({}),
(workspace) =>
workspace
.Editor(Editor.make({}), (editor) => editor.Editing(Editing.make({})))
.Sync(Sync.make({}), (sync) => sync.Idle(SyncIdle.make({})))
))
}
}
}
}
})

type ErrorIsExact = Expect<Equal<Machine.Machine.Error<typeof handled>, CompositionFailure>>
type ServicesAreExact = Expect<Equal<Machine.Machine.Services<typeof handled>, CompositionService>>
type OutputIsExact = Expect<
Equal<
Machine.Machine.OutputByIdentifier<typeof States.states, "App.Workspace">,
{ readonly Editor: string; readonly Sync: number }
>
>
type EveryStateIsHandled = Expect<Equal<Machine.Machine.UnhandledStates<typeof handled>, never>>
type ErrorIsNotAny = Expect<Equal<IsAny<Machine.Machine.Error<typeof handled>>, false>>
type ServicesAreNotAny = Expect<Equal<IsAny<Machine.Machine.Services<typeof handled>>, false>>

void EditorDone
void SyncDone
void Machine.planInitial(handled)
export type { ErrorIsExact, ErrorIsNotAny, EveryStateIsHandled, OutputIsExact, ServicesAreExact, ServicesAreNotAny }
34 changes: 34 additions & 0 deletions perf/types/exact-channels-control.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Machine } from "@typeonce/effect-machine"
import { Context, Data, Effect, Schema } from "effect"

export const Idle = Schema.TaggedStruct("Idle", { value: Schema.Number })
export const Done = Schema.TaggedStruct("Done", { value: Schema.String })
export const Start = Schema.TaggedStruct("Start", { value: Schema.String })
export const Loaded = Schema.TaggedStruct("Loaded", { value: Schema.String })
export const Notice = Schema.TaggedStruct("Notice", { value: Schema.String })
export const Input = Schema.Struct({ seed: Schema.Number })

export class InitialService extends Context.Service<InitialService, string>()("perf/channels/InitialService") {}
export class InitialFailure extends Data.TaggedError("InitialFailure")<{}> {}

export const States = Machine.defineStates({
Idle,
Done: {
schema: Done,
type: "final",
output: Schema.String
}
})

export const machine = Machine.make({
states: States.states,
events: [Start],
internalEvents: [Loaded],
emits: [Notice],
input: Input,
initial: (input) =>
Effect.flatMap(InitialService, () =>
input.seed < 0
? Effect.fail(new InitialFailure())
: Effect.succeed(States.initial.Idle(Idle.make({ value: input.seed }))))
})
Loading