Skip to content

Commit 881575b

Browse files
Decompose machine semantic internals (#79)
1 parent 12845fe commit 881575b

19 files changed

Lines changed: 2579 additions & 2336 deletions

.changeset/calm-machines-divide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@typeonce/effect-machine": patch
3+
---
4+
5+
Decompose machine topology, schema protocols, configurations, snapshot serialization, command execution, semantic planning, and compiled execution plans into explicit internal modules without changing runtime behavior or public types.

scripts/check-architecture.mjs

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const ruleDescriptions = {
77
ARCH002: "Public modules may only reach internals through their designated implementation seam",
88
ARCH003: "Core internals may only refer back to Machine through type-only imports",
99
ARCH004: "The planner may not depend on process or runtime execution",
10+
ARCH005: "Machine semantic layers may only depend inward",
1011
ARCH006: "The runtime may not depend on machine semantics or process orchestration",
1112
ARCH007: "Production modules may not depend on testing internals",
1213
ARCH008: "Black-box tests may not depend on implementation internals",
@@ -219,6 +220,65 @@ export const checkArchitecture = ({
219220
["src/unstable/reactivity/AtomMachine.ts", "src/internal/machine/atom.ts"],
220221
["src/unstable/cluster/ClusterMachine.ts", "src/internal/machine/cluster.ts"]
221222
])
223+
const forbiddenSemanticDependencies = new Map([
224+
["src/internal/machine/topology.ts", new Set([
225+
"src/internal/machine/protocol.ts",
226+
"src/internal/machine/configuration.ts",
227+
"src/internal/machine/serialization.ts",
228+
"src/internal/machine/planner.ts",
229+
"src/internal/machine/command.ts",
230+
"src/internal/machine/executionPlan.ts",
231+
"src/internal/machine/commandRuntime.ts",
232+
"src/internal/machine/process.ts",
233+
"src/internal/machine/runtime.ts"
234+
])],
235+
["src/internal/machine/protocol.ts", new Set([
236+
"src/internal/machine/configuration.ts",
237+
"src/internal/machine/serialization.ts",
238+
"src/internal/machine/planner.ts",
239+
"src/internal/machine/command.ts",
240+
"src/internal/machine/executionPlan.ts",
241+
"src/internal/machine/commandRuntime.ts",
242+
"src/internal/machine/process.ts",
243+
"src/internal/machine/runtime.ts"
244+
])],
245+
["src/internal/machine/configuration.ts", new Set([
246+
"src/internal/machine/serialization.ts",
247+
"src/internal/machine/planner.ts",
248+
"src/internal/machine/command.ts",
249+
"src/internal/machine/executionPlan.ts",
250+
"src/internal/machine/commandRuntime.ts",
251+
"src/internal/machine/process.ts",
252+
"src/internal/machine/runtime.ts"
253+
])],
254+
["src/internal/machine/serialization.ts", new Set([
255+
"src/internal/machine/planner.ts",
256+
"src/internal/machine/command.ts",
257+
"src/internal/machine/executionPlan.ts",
258+
"src/internal/machine/commandRuntime.ts",
259+
"src/internal/machine/process.ts",
260+
"src/internal/machine/runtime.ts"
261+
])],
262+
["src/internal/machine/command.ts", new Set([
263+
"src/internal/machine/executionPlan.ts",
264+
"src/internal/machine/commandRuntime.ts",
265+
"src/internal/machine/process.ts",
266+
"src/internal/machine/runtime.ts"
267+
])],
268+
["src/internal/machine/planner.ts", new Set([
269+
"src/internal/machine/serialization.ts",
270+
"src/internal/machine/executionPlan.ts",
271+
"src/internal/machine/commandRuntime.ts",
272+
"src/internal/machine/process.ts",
273+
"src/internal/machine/runtime.ts"
274+
])],
275+
["src/internal/machine/executionPlan.ts", new Set([
276+
"src/internal/machine/serialization.ts",
277+
"src/internal/machine/commandRuntime.ts",
278+
"src/internal/machine/process.ts",
279+
"src/internal/machine/runtime.ts"
280+
])]
281+
])
222282

223283
for (const edge of edges) {
224284
if (entrypoints.has(edge.source) && edge.target.includes("/internal/")) {
@@ -261,7 +321,12 @@ export const checkArchitecture = ({
261321
if (
262322
edge.source === "src/internal/machine/planner.ts" &&
263323
!edge.typeOnly &&
264-
(edge.target === "src/internal/machine/process.ts" || edge.target === "src/internal/machine/runtime.ts")
324+
[
325+
"src/internal/machine/commandRuntime.ts",
326+
"src/internal/machine/executionPlan.ts",
327+
"src/internal/machine/process.ts",
328+
"src/internal/machine/runtime.ts"
329+
].includes(edge.target)
265330
) {
266331
diagnostics.push(diagnostic(
267332
"ARCH004",
@@ -271,10 +336,20 @@ export const checkArchitecture = ({
271336
`Planner has a runtime dependency on ${edge.target}`
272337
))
273338
}
339+
if (forbiddenSemanticDependencies.get(edge.source)?.has(edge.target)) {
340+
diagnostics.push(diagnostic(
341+
"ARCH005",
342+
edge.sourceFile,
343+
edge.node,
344+
edge.source,
345+
`Semantic layer depends outward on ${edge.target}`
346+
))
347+
}
274348
if (
275349
edge.source === "src/internal/machine/runtime.ts" &&
276350
[
277-
"src/internal/machine/model.ts",
351+
"src/internal/machine/configuration.ts",
352+
"src/internal/machine/executionPlan.ts",
278353
"src/internal/machine/planner.ts",
279354
"src/internal/machine/process.ts"
280355
].includes(edge.target)

scripts/check-architecture.test.mjs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,15 @@ test("rejects value back-edges and execution-layer inversions", () => {
8080
"src/internal/testing/machine/arbitrary.ts": "export const arbitrary = 1",
8181
"src/consumer.ts": 'import { arbitrary } from "./internal/testing/machine/arbitrary.js"\nvoid arbitrary'
8282
})
83-
assert.deepEqual(rules(root), ["ARCH007", "ARCH003", "ARCH004", "ARCH006"])
83+
assert.deepEqual(rules(root), ["ARCH007", "ARCH003", "ARCH004", "ARCH005", "ARCH006"])
84+
})
85+
86+
test("rejects outward machine semantic dependencies", () => {
87+
const root = makeProject({
88+
"src/internal/machine/topology.ts": 'import { decode } from "./protocol.js"\nexport const topology = decode',
89+
"src/internal/machine/protocol.ts": "export const decode = 1"
90+
})
91+
assert.deepEqual(rules(root), ["ARCH005"])
8492
})
8593

8694
test("detects runtime cycles while permitting type-only cycles", () => {

src/Machine.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ import type {
2626
} from "./internal/machine/machine.js"
2727
import * as internal from "./internal/machine/machine.js"
2828
import { InitialEventTypeId } from "./internal/machine/machine.js"
29-
import type * as Model from "./internal/machine/model.js"
3029
import type { EnsureExecutable } from "./internal/machine/readiness.js"
3130
import type * as internalRuntime from "./internal/machine/runtime.js"
3231
import type * as StateDefinition from "./internal/machine/stateDefinition.js"
32+
import type * as Topology from "./internal/machine/topology.js"
3333

3434
/**
3535
* String literal type used as the runtime type identifier for `Machine`
@@ -3394,7 +3394,7 @@ export declare namespace Machine {
33943394
* @since 4.0.0
33953395
*/
33963396
export interface StateConstruction<Result> {
3397-
readonly [Model.StateConstructionTypeId]: Result
3397+
readonly [Topology.StateConstructionTypeId]: Result
33983398
}
33993399

34003400
/**
@@ -3407,8 +3407,8 @@ export declare namespace Machine {
34073407
States extends StateSchemas,
34083408
StateId extends StateIdentifier<States>
34093409
> {
3410-
readonly [Model.TargetTypeId]: typeof Model.TargetTypeId
3411-
readonly [Model.TargetSnapshotTypeId]?: SnapshotByIdentifier<States, StateId>
3410+
readonly [Topology.TargetTypeId]: typeof Topology.TargetTypeId
3411+
readonly [Topology.TargetSnapshotTypeId]?: SnapshotByIdentifier<States, StateId>
34123412
readonly path: StateId
34133413
readonly value: StateByIdentifier<States, StateId>
34143414
readonly values?: Partial<
@@ -3432,14 +3432,14 @@ export declare namespace Machine {
34323432
States extends StateSchemas,
34333433
HistoryId extends HistoryIdentifier<States>
34343434
> {
3435-
readonly [Model.HistoryTargetTypeId]: typeof Model.HistoryTargetTypeId
3435+
readonly [Topology.HistoryTargetTypeId]: typeof Topology.HistoryTargetTypeId
34363436
readonly path: HistoryId
34373437
readonly parent: Extract<ParentPath<HistoryId>, StateIdentifier<States>>
34383438
}
34393439

34403440
/** Branded transient target instruction used while constructing initial states. */
34413441
export interface ChoiceTargetInstruction<ChoiceId extends string = string> {
3442-
readonly [Model.ChoiceTargetTypeId]: typeof Model.ChoiceTargetTypeId
3442+
readonly [Topology.ChoiceTargetTypeId]: typeof Topology.ChoiceTargetTypeId
34433443
readonly path: ChoiceId
34443444
readonly parent: ParentPath<ChoiceId>
34453445
readonly values?: Readonly<Record<string, unknown>>

src/internal/machine/atom.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import { AsyncResult, Atom, type AtomRegistry } from "effect/unstable/reactivity
1515
import type * as Machine from "../../Machine.js"
1616
import type { Bound, ChildMachineAtom, ChildOf, MachineAtom } from "../../unstable/reactivity/AtomMachine.js"
1717
import * as internalMachine from "./machine.js"
18-
import * as Model from "./model.js"
1918
import type { EnsureExecutable } from "./readiness.js"
19+
import * as Topology from "./topology.js"
2020

2121
export class NotReadyError extends Data.TaggedError("NotReadyError") {}
2222

@@ -448,7 +448,7 @@ const selectSnapshot = <
448448
snapshot: State,
449449
path: Path
450450
): Option.Option<SnapshotValueByIdentifier<State, Path>> =>
451-
Model.getSnapshotByPath(snapshot, path).pipe(
451+
Topology.getSnapshotByPath(snapshot, path).pipe(
452452
Option.map((snapshot) => snapshot.value)
453453
) as Option.Option<SnapshotValueByIdentifier<State, Path>>
454454

@@ -498,7 +498,7 @@ export const matches = <
498498
self: MachineAtom<State, Event, Error, Output, StartError>,
499499
path: Path
500500
): Atom.Atom<AsyncResult.AsyncResult<boolean, StartError | Error>> =>
501-
Atom.mapResult(self.result, (snapshot) => Option.isSome(Model.getSnapshotByPath(snapshot, path))).pipe(
501+
Atom.mapResult(self.result, (snapshot) => Option.isSome(Topology.getSnapshotByPath(snapshot, path))).pipe(
502502
Atom.withEquality(Equal.equals)
503503
)
504504

@@ -514,7 +514,7 @@ export const matchesChild = <
514514
> =>
515515
Atom.mapResult(
516516
self.result,
517-
Option.exists((snapshot) => Option.isSome(Model.getSnapshotByPath(snapshot, path)))
517+
Option.exists((snapshot) => Option.isSome(Topology.getSnapshotByPath(snapshot, path)))
518518
).pipe(Atom.withEquality(Equal.equals))
519519

520520
const BoundRequirementsTypeId = "~effect/reactivity/AtomMachine/BoundRequirements"

src/internal/machine/command.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* Internal machine command collection.
3+
*
4+
* @since 4.0.0
5+
*/
6+
7+
import type { Command, Enqueue, Machine } from "../../Machine.js"
8+
import { decodeEmitSync, decodeEventSync } from "./protocol.js"
9+
10+
export type RuntimeCommand = Command
11+
12+
export interface Collected<Event> {
13+
readonly enqueue: Enqueue<Event, unknown>
14+
readonly commands: Array<RuntimeCommand>
15+
readonly raisedEvents: Array<Event>
16+
readonly emittedEvents: Array<unknown>
17+
}
18+
19+
const targetBuilderCache = new WeakMap<object, Map<string, unknown>>()
20+
21+
export const getTargetBuilder = (machine: Machine.Any, path: string): any => {
22+
let byPath = targetBuilderCache.get(machine)
23+
if (byPath === undefined) {
24+
byPath = new Map()
25+
targetBuilderCache.set(machine, byPath)
26+
}
27+
if (byPath.has(path)) {
28+
return byPath.get(path)
29+
}
30+
const builder = machine.makeTargetBuilder(path as any)
31+
byPath.set(path, builder)
32+
return builder
33+
}
34+
35+
export const makeCollector = <Event>(machine: Machine.Any): Collected<Event> => {
36+
const commands: Array<RuntimeCommand> = []
37+
const raisedEvents: Array<Event> = []
38+
const emittedEvents: Array<unknown> = []
39+
return {
40+
commands,
41+
raisedEvents,
42+
emittedEvents,
43+
enqueue: {
44+
raise: (event) => {
45+
raisedEvents.push(decodeEventSync(machine, event) as Event)
46+
},
47+
emit: (event) => {
48+
emittedEvents.push(decodeEmitSync(machine, event))
49+
},
50+
sendTo: (child: unknown, event: unknown) => {
51+
commands.push({ _tag: "SendTo", child: child as any, event })
52+
},
53+
stop: (child: unknown) => {
54+
commands.push({ _tag: "Stop", child: child as any })
55+
}
56+
}
57+
}
58+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Internal process-side machine command execution.
3+
*
4+
* @since 4.0.0
5+
*/
6+
7+
import * as Effect from "effect/Effect"
8+
import type { Machine, Runtime } from "../../Machine.js"
9+
import type { RuntimeCommand } from "./command.js"
10+
import { decodeEmit, decodeEvent } from "./protocol.js"
11+
import type { ProcessScope } from "./runtime.js"
12+
13+
export const makeLiveRuntime = <Events, Emits>(
14+
machine: Machine.Any,
15+
scope: ProcessScope<Events>
16+
): Runtime<Events, Emits> => ({
17+
raise: (event) =>
18+
decodeEvent(machine, event).pipe(
19+
Effect.flatMap((event) => scope.self.send(event as Events))
20+
),
21+
sendParent: (event) =>
22+
decodeEmit(machine, event).pipe(
23+
Effect.flatMap((event) => scope.sendParent(event))
24+
)
25+
})
26+
27+
export const runCommands = <Event>(
28+
commands: Iterable<RuntimeCommand>,
29+
scope: ProcessScope<Event>
30+
) =>
31+
Effect.forEach(commands, (command) =>
32+
command._tag === "SendTo"
33+
? scope.sendTo(command.child as never, command.event)
34+
: scope.stopChild(command.child as never), { discard: true })
35+
36+
export const runEmittedEvents = <Events, Emits>(
37+
events: Iterable<Emits>,
38+
runtime: Runtime<Events, Emits>
39+
) =>
40+
Effect.all(
41+
Array.from(events, (event) => runtime.sendParent(event)),
42+
{ discard: true }
43+
)

0 commit comments

Comments
 (0)