Skip to content

Commit 2c67924

Browse files
Implement static transition topology (#125)
1 parent 75c5014 commit 2c67924

103 files changed

Lines changed: 7620 additions & 3703 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@typeonce/effect-machine": minor
3+
---
4+
5+
Require `Machine.transition` for every machine transition and capture each possible target as static machine topology. Direct transitions declare `target` and `resolve`; conditional transitions declare titled `cases` whose `when` functions return `Option`, plus an explicit `otherwise` branch. The selected target builder and conditional match value are inferred in each resolver.
6+
7+
Initial state construction now uses the same `target` and `resolve` shape, restricted to the machine's declared initial state. Replace process logic previously created with `Machine.transition` by `Machine.logic`, and replace function handlers, target upper-bound lists, and `States.initial` construction with the explicit transition and initial target selectors.

README.md

Lines changed: 71 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -66,19 +66,31 @@ const CounterDefinition = Machine.make({
6666
id: "Counter",
6767
states: States.states,
6868
events: CounterEvent,
69-
initial: () => States.initial.Idle.from()
69+
initial: {
70+
target: (to) => to.Idle(),
71+
resolve: ({ target }) => target.from()
72+
}
7073
})
7174

7275
const Counter = CounterDefinition.handle({
7376
Idle: {
7477
on: {
75-
Start: ({ target }) => target.full.Running.from({ count: 0 })
78+
Start: Machine.transition({
79+
target: (to) => to.full.Running(),
80+
resolve: ({ target }) => target.from({ count: 0 })
81+
})
7682
}
7783
},
7884
Running: {
7985
on: {
80-
Increment: ({ state, target }) => target.full.Running.from({ count: state.count + 1 }),
81-
Stop: ({ target }) => target.full.Idle.from()
86+
Increment: Machine.transition({
87+
target: (to) => to.full.Running(),
88+
resolve: ({ state, target }) => target.from({ count: state.count + 1 })
89+
}),
90+
Stop: Machine.transition({
91+
target: (to) => to.full.Idle(),
92+
resolve: ({ target }) => target.from()
93+
})
8294
}
8395
}
8496
})
@@ -111,8 +123,7 @@ Use this order to preserve inference and keep boundaries explicit:
111123
Use `.from(...)` when constructing a new state from fields:
112124

113125
```ts
114-
target.local.Saving.from({ draft: event.draft })
115-
States.initial.Form.from({ draft: "" }, (form) => form.Editing.from())
126+
target.from({ draft: event.draft })
116127
```
117128

118129
The machine runs these inputs through the state schema while planning. Schema
@@ -124,10 +135,12 @@ When sibling states share fields, remove the source discriminator and pass the
124135
remaining fields through the target schema:
125136

126137
```ts
127-
Submit: ;
128-
;(({ state, target }) => {
129-
const { _tag: _, ...fields } = state
130-
return target.local.Saving.from({ ...fields, attempt: 1 })
138+
Submit: Machine.transition({
139+
target: (to) => to.local.Saving(),
140+
resolve: ({ state, target }) => {
141+
const { _tag: _, ...fields } = state
142+
return target.from({ ...fields, attempt: 1 })
143+
}
131144
})
132145
```
133146

@@ -144,7 +157,10 @@ const States = Machine.defineStates({
144157
}
145158
})
146159

147-
States.initial.Form.from((form) => form.Editing.from())
160+
initial: {
161+
target: (to) => to.Form.initial(),
162+
resolve: ({ target }) => target((form) => form.Editing.from())
163+
}
148164
```
149165

150166
Schema-less states remain active, targetable, matchable, and visible through
@@ -181,7 +197,10 @@ const definition = Machine.make({
181197
events: CommandEvent,
182198
internalEvents: InternalEvent,
183199
emittedEvents: Emissions,
184-
initial: () => States.initial.Idle.from()
200+
initial: {
201+
target: (to) => to.Idle(),
202+
resolve: ({ target }) => target.from()
203+
}
185204
})
186205
```
187206

@@ -293,16 +312,22 @@ const child = Machine.make({
293312
states: ChildStates.states,
294313
events: ChildEvents,
295314
parentEvents: ParentEvents,
296-
initial: () => ChildStates.initial.Working.from()
315+
initial: {
316+
target: (to) => to.Working(),
317+
resolve: ({ target }) => target.from()
318+
}
297319
}).handle({
298320
Working: {
299321
on: {
300-
Finish: ({ parent, target }, enqueue) => {
301-
if (parent !== undefined) {
302-
enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" }))
322+
Finish: Machine.transition({
323+
target: (to) => to.full.Done(),
324+
resolve: ({ parent, target }, enqueue) => {
325+
if (parent !== undefined) {
326+
enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" }))
327+
}
328+
return target.from()
303329
}
304-
return target.full.Done.from()
305-
}
330+
})
306331
}
307332
},
308333
Done: {}
@@ -377,16 +402,25 @@ Loading: {
377402
invoke: Machine.invoke({
378403
id: "save-document",
379404
effect: () => saveDocument,
380-
onDone: ({ output, target }) => target.full.Saved({ id: output.id }),
381-
onFailure: ({ error, target }) => target.full.Failed({ message: String(error) })
405+
onDone: Machine.transition({
406+
target: (to) => to.full.Saved(),
407+
resolve: ({ output, target }) => target.from({ id: output.id })
408+
}),
409+
onFailure: Machine.transition({
410+
target: (to) => to.full.Failed(),
411+
resolve: ({ error, target }) => target.from({ message: String(error) })
412+
})
382413
})
383414
}
384415

385416
Waiting: {
386417
invoke: Machine.invoke({
387418
id: "save-timeout",
388419
after: "3 seconds",
389-
onDone: ({ target }) => target.full.Failed({ message: "Timed out" })
420+
onDone: Machine.transition({
421+
target: (to) => to.full.Failed(),
422+
resolve: ({ target }) => target.from({ message: "Timed out" })
423+
})
390424
})
391425
}
392426
```
@@ -401,8 +435,14 @@ for state-dependent Effects:
401435
invoke: Machine.invoke({
402436
id: "load-document",
403437
effect: ({ state }) => loadDocument(state.documentId),
404-
onDone: ({ output, target }) => target.full.Ready({ document: output }),
405-
onFailure: ({ error, target }) => target.full.Failed({ message: error.message })
438+
onDone: Machine.transition({
439+
target: (to) => to.full.Ready(),
440+
resolve: ({ output, target }) => target.from({ document: output })
441+
}),
442+
onFailure: Machine.transition({
443+
target: (to) => to.full.Failed(),
444+
resolve: ({ error, target }) => target.from({ message: error.message })
445+
})
406446
})
407447
```
408448

@@ -428,8 +468,14 @@ const machine = definition.handle({
428468
parent === undefined
429469
? Effect.void
430470
: parent.send(ParentEvents.SaveStarted()),
431-
onDone: ({ target }) => target.none(),
432-
onFailure: ({ target }) => target.none()
471+
onDone: Machine.transition({
472+
target: (to) => to.none(),
473+
resolve: () => undefined
474+
}),
475+
onFailure: Machine.transition({
476+
target: (to) => to.none(),
477+
resolve: () => undefined
478+
})
433479
})
434480
}
435481
})

0 commit comments

Comments
 (0)