@typeonce/effect-machine 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -5
- package/dist/Machine.d.ts +188 -52
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +6 -5
- package/dist/Machine.js.map +1 -1
- package/dist/internal/machine/executionPlan.d.ts.map +1 -1
- package/dist/internal/machine/executionPlan.js +50 -6
- package/dist/internal/machine/executionPlan.js.map +1 -1
- package/dist/internal/machine/initialization.d.ts.map +1 -1
- package/dist/internal/machine/initialization.js +7 -2
- package/dist/internal/machine/initialization.js.map +1 -1
- package/dist/internal/machine/machine.d.ts.map +1 -1
- package/dist/internal/machine/machine.js +132 -22
- package/dist/internal/machine/machine.js.map +1 -1
- package/dist/internal/machine/planner.d.ts +16 -2
- package/dist/internal/machine/planner.d.ts.map +1 -1
- package/dist/internal/machine/planner.js +67 -13
- package/dist/internal/machine/planner.js.map +1 -1
- package/dist/internal/machine/topology.d.ts +19 -2
- package/dist/internal/machine/topology.d.ts.map +1 -1
- package/dist/internal/machine/topology.js +17 -2
- package/dist/internal/machine/topology.js.map +1 -1
- package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -1
- package/dist/internal/testing/machine/finiteModel.js +3 -3
- package/dist/internal/testing/machine/finiteModel.js.map +1 -1
- package/dist/internal/testing/machine/verification.d.ts.map +1 -1
- package/dist/internal/testing/machine/verification.js +17 -3
- package/dist/internal/testing/machine/verification.js.map +1 -1
- package/dist/testing/MachineTest.d.ts +5 -3
- package/dist/testing/MachineTest.d.ts.map +1 -1
- package/dist/testing/MachineTest.js +3 -3
- package/dist/testing/MachineTest.js.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.d.ts +1 -1
- package/dist/unstable/reactivity/AtomMachine.js +1 -1
- package/docs/agent-guide.md +51 -3
- package/package.json +1 -1
- package/src/Machine.ts +532 -107
- package/src/internal/machine/executionPlan.ts +62 -6
- package/src/internal/machine/initialization.ts +10 -2
- package/src/internal/machine/machine.ts +188 -24
- package/src/internal/machine/planner.ts +86 -15
- package/src/internal/machine/topology.ts +40 -3
- package/src/internal/testing/machine/finiteModel.ts +7 -3
- package/src/internal/testing/machine/verification.ts +24 -3
- package/src/testing/MachineTest.ts +5 -3
- package/src/unstable/reactivity/AtomMachine.ts +1 -1
|
@@ -47,7 +47,15 @@ import {
|
|
|
47
47
|
validateDeclaredTransitionTarget
|
|
48
48
|
} from "./planner.js"
|
|
49
49
|
import { decodeEmitSync, decodeEventSync, decodeInputSync, decodeStateValueSync } from "./protocol.js"
|
|
50
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
isCombinedTarget,
|
|
52
|
+
isInitialTarget,
|
|
53
|
+
isNoTarget,
|
|
54
|
+
isSnapshot,
|
|
55
|
+
isStateUpdate,
|
|
56
|
+
isTarget,
|
|
57
|
+
TargetSnapshotTypeId
|
|
58
|
+
} from "./topology.js"
|
|
51
59
|
|
|
52
60
|
interface IndexedExecutionDescriptor {
|
|
53
61
|
readonly flat: boolean
|
|
@@ -548,7 +556,28 @@ const collectIndexedEvaluatedTransition = (
|
|
|
548
556
|
selection.context,
|
|
549
557
|
selection.transition.evaluate
|
|
550
558
|
)
|
|
551
|
-
const
|
|
559
|
+
const combined = isCombinedTarget(transitionResult.state) ? transitionResult.state : undefined
|
|
560
|
+
const transitionState = combined?.target ?? transitionResult.state
|
|
561
|
+
const stateUpdate = combined?.update ?? (isStateUpdate(transitionState) ? transitionState : undefined)
|
|
562
|
+
const update = stateUpdate !== undefined
|
|
563
|
+
? (() => {
|
|
564
|
+
const index = descriptor.indexByPath.get(stateUpdate.path)
|
|
565
|
+
const node = index === undefined ? undefined : descriptor.nodes[index]
|
|
566
|
+
if (
|
|
567
|
+
index === undefined || node === undefined || state.active[index] !== 1 || node.schema === undefined ||
|
|
568
|
+
(node.type !== "compound" && node.type !== "parallel")
|
|
569
|
+
) {
|
|
570
|
+
throw new Error(
|
|
571
|
+
`Machine state update owner "${stateUpdate.path}" must be an active valued compound or parallel state`
|
|
572
|
+
)
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
path: node.path,
|
|
576
|
+
value: decodeStateValueSync(machine, node, stateUpdate.value)
|
|
577
|
+
}
|
|
578
|
+
})()
|
|
579
|
+
: undefined
|
|
580
|
+
const unresolvedTarget = transitionState === undefined || isStateUpdate(transitionState) ? undefined : transitionState
|
|
552
581
|
validateDeclaredTransitionTarget(
|
|
553
582
|
selection.sourcePath,
|
|
554
583
|
selection.trigger,
|
|
@@ -567,10 +596,24 @@ const collectIndexedEvaluatedTransition = (
|
|
|
567
596
|
if (target !== undefined && !isTarget(target) && !isSnapshot(target)) {
|
|
568
597
|
throw new Error("Machine expected indexed transition target to be a snapshot or target builder result")
|
|
569
598
|
}
|
|
570
|
-
|
|
571
|
-
? state
|
|
599
|
+
let next = target === undefined
|
|
600
|
+
? update === undefined ? state : (() => {
|
|
601
|
+
const next = copyOwnedIndexedState(state)
|
|
602
|
+
next.values[descriptor.indexByPath.get(update.path)!] = update.value
|
|
603
|
+
return next
|
|
604
|
+
})()
|
|
572
605
|
: normalizeIndexedTargetStateSync(machine, descriptor, state, target as any, selection.leafIndex)
|
|
606
|
+
if (combined !== undefined && update !== undefined) {
|
|
607
|
+
const ownerIndex = descriptor.indexByPath.get(update.path)!
|
|
608
|
+
if (next.active[ownerIndex] !== 1) {
|
|
609
|
+
throw new Error(`Machine combined target exited its updating owner "${update.path}"`)
|
|
610
|
+
}
|
|
611
|
+
const values = next.values.slice()
|
|
612
|
+
values[ownerIndex] = update.value
|
|
613
|
+
next = { ...next, values }
|
|
614
|
+
}
|
|
573
615
|
const changed = selection.transition.reenter || !hasSameIndexedActive(state, next)
|
|
616
|
+
const stabilize = changed || update !== undefined
|
|
574
617
|
if (!changed) {
|
|
575
618
|
return {
|
|
576
619
|
selection,
|
|
@@ -578,11 +621,13 @@ const collectIndexedEvaluatedTransition = (
|
|
|
578
621
|
branchKey: transitionResult.branchKey,
|
|
579
622
|
unresolvedTarget: unresolvedTarget as any,
|
|
580
623
|
target: target as any,
|
|
624
|
+
update,
|
|
581
625
|
next,
|
|
582
626
|
commands: [...transitionResult.commands, ...(initialResolution?.commands ?? [])],
|
|
583
627
|
raisedEvents: [...transitionResult.raisedEvents, ...(initialResolution?.raisedEvents ?? [])],
|
|
584
628
|
emittedEvents: [...transitionResult.emittedEvents, ...(initialResolution?.emittedEvents ?? [])],
|
|
585
629
|
changed: false,
|
|
630
|
+
stabilize,
|
|
586
631
|
exitPaths: [],
|
|
587
632
|
entryPaths: [],
|
|
588
633
|
choiceTransitions: initialResolution?.transitions ?? []
|
|
@@ -603,11 +648,13 @@ const collectIndexedEvaluatedTransition = (
|
|
|
603
648
|
branchKey: transitionResult.branchKey,
|
|
604
649
|
unresolvedTarget: unresolvedTarget as any,
|
|
605
650
|
target: target as any,
|
|
651
|
+
update,
|
|
606
652
|
next,
|
|
607
653
|
commands: [...transitionResult.commands, ...(initialResolution?.commands ?? [])],
|
|
608
654
|
raisedEvents: [...transitionResult.raisedEvents, ...(initialResolution?.raisedEvents ?? [])],
|
|
609
655
|
emittedEvents: [...transitionResult.emittedEvents, ...(initialResolution?.emittedEvents ?? [])],
|
|
610
656
|
changed: true,
|
|
657
|
+
stabilize,
|
|
611
658
|
exitPaths: getExitPaths(machine, activeConfigurationFromIndexedState(descriptor, state), boundary),
|
|
612
659
|
entryPaths: getEntryPaths(machine, activeConfigurationFromIndexedState(descriptor, next), boundary),
|
|
613
660
|
choiceTransitions: initialResolution?.transitions ?? []
|
|
@@ -629,7 +676,8 @@ const indexedMicrostep = (
|
|
|
629
676
|
branchIndex: transition.branchIndex,
|
|
630
677
|
branchKey: transition.branchKey,
|
|
631
678
|
target: transition.unresolvedTarget === undefined ? undefined : getTargetNodePath(transition.unresolvedTarget),
|
|
632
|
-
resolvedTarget: transition.target === undefined ? undefined : getTargetNodePath(transition.target)
|
|
679
|
+
resolvedTarget: transition.target === undefined ? undefined : getTargetNodePath(transition.target),
|
|
680
|
+
updates: transition.update === undefined ? [] : [transition.update.path]
|
|
633
681
|
})
|
|
634
682
|
if (selections.length === 1) {
|
|
635
683
|
const transition = collectIndexedEvaluatedTransition(machine, descriptor, state, selections[0]!)
|
|
@@ -663,6 +711,13 @@ const indexedMicrostep = (
|
|
|
663
711
|
if (transitions.length === 1) {
|
|
664
712
|
next = transitions[0]!.next
|
|
665
713
|
} else {
|
|
714
|
+
for (const transition of transitions) {
|
|
715
|
+
if (transition.update !== undefined) {
|
|
716
|
+
const values = next.values.slice()
|
|
717
|
+
values[descriptor.indexByPath.get(transition.update.path)!] = transition.update.value
|
|
718
|
+
next = { ...next, values }
|
|
719
|
+
}
|
|
720
|
+
}
|
|
666
721
|
const applicationOrder = [
|
|
667
722
|
...transitions.filter((transition) => !transition.changed),
|
|
668
723
|
...transitions.filter((transition) => transition.changed)
|
|
@@ -815,7 +870,8 @@ const planIndexedFlatState = (
|
|
|
815
870
|
branchIndex: transitionResult.branchIndex,
|
|
816
871
|
branchKey: transitionResult.branchKey,
|
|
817
872
|
target: target === undefined ? undefined : getTargetNodePath(target as any),
|
|
818
|
-
resolvedTarget: target === undefined ? undefined : getTargetNodePath(target as any)
|
|
873
|
+
resolvedTarget: target === undefined ? undefined : getTargetNodePath(target as any),
|
|
874
|
+
updates: []
|
|
819
875
|
}]
|
|
820
876
|
}
|
|
821
877
|
: undefined),
|
|
@@ -14,11 +14,19 @@ const getNode = (machine: Machine.Any, path: string): Machine.StateNode => {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const withFrom = <Method extends (value: unknown) => unknown>(method: Method) => {
|
|
17
|
-
|
|
17
|
+
const builder = {}
|
|
18
|
+
Object.defineProperty(builder, "decoded", {
|
|
19
|
+
value: method,
|
|
20
|
+
enumerable: false
|
|
21
|
+
})
|
|
22
|
+
Object.defineProperty(builder, "from", {
|
|
18
23
|
value: (...args: ReadonlyArray<unknown>) => method(Topology.makeStateInput(args.length === 0 ? {} : args[0])),
|
|
19
24
|
enumerable: false
|
|
20
25
|
})
|
|
21
|
-
return
|
|
26
|
+
return builder as {
|
|
27
|
+
readonly decoded: Method
|
|
28
|
+
readonly from: (...args: ReadonlyArray<unknown>) => unknown
|
|
29
|
+
}
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
const makeCompletion = (values: Readonly<Record<string, unknown>>): object => {
|
|
@@ -191,7 +191,7 @@ type InitialBuilderDescriptor = {
|
|
|
191
191
|
const plainTargetSelection = (selection: Topology.TargetSelection): Topology.TargetSelection =>
|
|
192
192
|
selection.kind === "none"
|
|
193
193
|
? Topology.noneTargetSelection
|
|
194
|
-
: Topology.makeTargetSelection(selection.kind, selection.path, selection.scope)
|
|
194
|
+
: Topology.makeTargetSelection(selection.kind, selection.path, selection.scope, selection.updatePath)
|
|
195
195
|
|
|
196
196
|
const transitionOptions = (options: unknown): { readonly reenter: boolean; readonly declinable: boolean } => {
|
|
197
197
|
const configuration = typeof options === "object" && options !== null
|
|
@@ -224,9 +224,33 @@ const decorateTransitionSelection = (selection: Topology.TargetSelection): Topol
|
|
|
224
224
|
...selection,
|
|
225
225
|
resolve: (resolve: (context: any, enqueue: unknown) => unknown, options?: unknown) =>
|
|
226
226
|
makeDirectTransitionDescriptor(selection, resolve, options),
|
|
227
|
-
reenter: () => makeDirectTransitionDescriptor(selection, undefined, { reenter: true })
|
|
227
|
+
reenter: () => makeDirectTransitionDescriptor(selection, undefined, { reenter: true }),
|
|
228
|
+
updating: (owner: unknown) => {
|
|
229
|
+
if (typeof owner !== "function") {
|
|
230
|
+
throw new Error("Machine updating owner must be a state selector")
|
|
231
|
+
}
|
|
232
|
+
const ownerSelection = owner()
|
|
233
|
+
if (
|
|
234
|
+
!Topology.isTargetSelection(ownerSelection) || ownerSelection.kind !== "state" ||
|
|
235
|
+
ownerSelection.scope !== "branch"
|
|
236
|
+
) {
|
|
237
|
+
throw new Error("Machine updating owner must be addressed by one branch state selector")
|
|
238
|
+
}
|
|
239
|
+
return decorateTransitionSelection(
|
|
240
|
+
Topology.makeTargetSelection(selection.kind, selection.path, selection.scope, ownerSelection.path)
|
|
241
|
+
)
|
|
242
|
+
}
|
|
228
243
|
})
|
|
229
244
|
|
|
245
|
+
const decorateStateUpdateSelection = (selection: Topology.TargetSelection): Topology.TargetSelection => {
|
|
246
|
+
const update = (
|
|
247
|
+
resolve: (context: any, enqueue: unknown) => unknown,
|
|
248
|
+
options?: unknown
|
|
249
|
+
) => makeDirectTransitionDescriptor(selection, resolve, options)
|
|
250
|
+
Object.assign(update, selection)
|
|
251
|
+
return Object.freeze(update) as unknown as Topology.TargetSelection
|
|
252
|
+
}
|
|
253
|
+
|
|
230
254
|
const noneTransitionSelection = decorateTransitionSelection(Topology.noneTargetSelection)
|
|
231
255
|
|
|
232
256
|
const makeInitialBuilderDescriptor = (
|
|
@@ -268,6 +292,7 @@ const decorateInitialSelectorNode = (node: unknown): unknown => {
|
|
|
268
292
|
|
|
269
293
|
const decorateTransitionSelectorNode = (node: unknown): unknown => {
|
|
270
294
|
if (Topology.isTargetSelection(node)) {
|
|
295
|
+
if (node.kind === "update") return decorateStateUpdateSelection(node)
|
|
271
296
|
return node === Topology.noneTargetSelection ? noneTransitionSelection : decorateTransitionSelection(node)
|
|
272
297
|
}
|
|
273
298
|
if (typeof node === "function") {
|
|
@@ -350,6 +375,11 @@ const transitionTargetSelection = (
|
|
|
350
375
|
scope: selection.scope
|
|
351
376
|
})
|
|
352
377
|
|
|
378
|
+
const selectionUpdates = (selection: Topology.TargetSelection): ReadonlyArray<string> =>
|
|
379
|
+
selection.updatePath === undefined ?
|
|
380
|
+
selection.kind === "update" && selection.path !== undefined ? [selection.path] : []
|
|
381
|
+
: [selection.updatePath]
|
|
382
|
+
|
|
353
383
|
const makeSelectionMethod = (
|
|
354
384
|
kind: Topology.TargetSelectionKind,
|
|
355
385
|
path: string | undefined,
|
|
@@ -363,22 +393,29 @@ const makeSelectionValue = (
|
|
|
363
393
|
scope: Topology.TargetSelectionScope
|
|
364
394
|
): Topology.TargetSelection => Topology.makeTargetSelection(kind, path, scope)
|
|
365
395
|
|
|
396
|
+
const makeStateUpdateSelection = (
|
|
397
|
+
path: string,
|
|
398
|
+
scope: "local" | "branch"
|
|
399
|
+
): Topology.TargetSelection => Topology.makeTargetSelection("update", path, scope)
|
|
400
|
+
|
|
366
401
|
const addSelectionChildren = (
|
|
367
402
|
builder: Record<string, unknown>,
|
|
368
403
|
stateNodes: Machine.StateNodes,
|
|
369
404
|
parent: string,
|
|
370
|
-
scope: "local" | "branch"
|
|
405
|
+
scope: "local" | "branch",
|
|
406
|
+
source?: string
|
|
371
407
|
): void => {
|
|
372
408
|
for (const node of stateNodes.byPath.values()) {
|
|
373
409
|
if (node.parent !== parent || node.type === "history") continue
|
|
374
|
-
builder[node.key] = makeSelectionNode(stateNodes, node.path, scope)
|
|
410
|
+
builder[node.key] = makeSelectionNode(stateNodes, node.path, scope, source)
|
|
375
411
|
}
|
|
376
412
|
}
|
|
377
413
|
|
|
378
414
|
const makeSelectionNode = (
|
|
379
415
|
stateNodes: Machine.StateNodes,
|
|
380
416
|
path: string,
|
|
381
|
-
scope: Topology.TargetSelectionScope
|
|
417
|
+
scope: Topology.TargetSelectionScope,
|
|
418
|
+
source?: string
|
|
382
419
|
): unknown => {
|
|
383
420
|
const node = getTargetBuilderNode(stateNodes, path)
|
|
384
421
|
const kind: Topology.TargetSelectionKind = node.type === "choice" ? "choice" : "state"
|
|
@@ -389,7 +426,17 @@ const makeSelectionNode = (
|
|
|
389
426
|
enumerable: true
|
|
390
427
|
})
|
|
391
428
|
if (scope === "local" || scope === "branch") {
|
|
392
|
-
addSelectionChildren(method, stateNodes, path, scope)
|
|
429
|
+
addSelectionChildren(method, stateNodes, path, scope, source)
|
|
430
|
+
}
|
|
431
|
+
if (
|
|
432
|
+
scope === "branch" && source !== undefined && node.schema !== undefined &&
|
|
433
|
+
(source === path || source.startsWith(`${path}.`)) &&
|
|
434
|
+
getTargetBuilderNode(stateNodes, source).type !== "choice"
|
|
435
|
+
) {
|
|
436
|
+
Object.defineProperty(method, "update", {
|
|
437
|
+
value: makeStateUpdateSelection(path, "branch"),
|
|
438
|
+
enumerable: true
|
|
439
|
+
})
|
|
393
440
|
}
|
|
394
441
|
}
|
|
395
442
|
return method
|
|
@@ -424,13 +471,16 @@ const makeTargetSelector = (
|
|
|
424
471
|
}
|
|
425
472
|
const branch: Record<string, unknown> = {}
|
|
426
473
|
const root = getTargetBuilderNode(stateNodes, source.split(".")[0]!)
|
|
427
|
-
branch[root.key] = makeSelectionNode(stateNodes, root.path, "branch")
|
|
474
|
+
branch[root.key] = makeSelectionNode(stateNodes, root.path, "branch", source)
|
|
428
475
|
const local: Record<string, unknown> = {}
|
|
429
476
|
const localScope = getLocalTargetScope(stateNodes, source)
|
|
430
477
|
if (localScope !== undefined) {
|
|
431
478
|
const localScopeNode = getTargetBuilderNode(stateNodes, localScope)
|
|
432
479
|
if (localScopeNode.schema !== undefined) {
|
|
433
480
|
local.with = makeSelectionValue("state", localScope, "local")
|
|
481
|
+
if (getTargetBuilderNode(stateNodes, source).type !== "choice") {
|
|
482
|
+
local.update = makeStateUpdateSelection(localScope, "local")
|
|
483
|
+
}
|
|
434
484
|
}
|
|
435
485
|
addSelectionChildren(local, stateNodes, localScope, "local")
|
|
436
486
|
}
|
|
@@ -446,6 +496,7 @@ const makeTargetSelector = (
|
|
|
446
496
|
const captureDefinitionBranch = (
|
|
447
497
|
branch: unknown,
|
|
448
498
|
selector: unknown,
|
|
499
|
+
stateNodes: Machine.StateNodes,
|
|
449
500
|
path: string,
|
|
450
501
|
trigger: PropertyKey
|
|
451
502
|
): CapturedBranch => {
|
|
@@ -459,9 +510,58 @@ const captureDefinitionBranch = (
|
|
|
459
510
|
if (!Topology.isTargetSelection(selection)) {
|
|
460
511
|
throw new Error(`Machine transition for state "${path}" on "${String(trigger)}" must select exactly one target`)
|
|
461
512
|
}
|
|
513
|
+
if (selection.updatePath !== undefined) {
|
|
514
|
+
const owner = stateNodes.byPath.get(selection.updatePath)
|
|
515
|
+
if (
|
|
516
|
+
selection.kind !== "state" || (selection.scope !== "local" && selection.scope !== "branch") ||
|
|
517
|
+
selection.path === undefined || owner === undefined || owner.schema === undefined ||
|
|
518
|
+
(owner.type !== "compound" && owner.type !== "parallel") ||
|
|
519
|
+
!Configuration.isDescendantOf(path, owner.path) ||
|
|
520
|
+
!Configuration.isDescendantOf(selection.path, owner.path)
|
|
521
|
+
) {
|
|
522
|
+
throw new Error(
|
|
523
|
+
`Machine updating owner "${selection.updatePath}" must be a valued ancestor retained by source "${path}" and target "${selection.path}"`
|
|
524
|
+
)
|
|
525
|
+
}
|
|
526
|
+
}
|
|
462
527
|
return { ...(branch as DefinitionBranch), selection }
|
|
463
528
|
}
|
|
464
529
|
|
|
530
|
+
const makeUpdatingConstruction = (
|
|
531
|
+
target: unknown,
|
|
532
|
+
ownerPath: string
|
|
533
|
+
): { readonly update: (update: unknown) => Topology.CombinedTarget } =>
|
|
534
|
+
Object.freeze({
|
|
535
|
+
update: (update: unknown) => {
|
|
536
|
+
if (!Topology.isStateUpdate(update) || update.path !== ownerPath) {
|
|
537
|
+
throw new Error(`Machine combined target must update its declared owner "${ownerPath}"`)
|
|
538
|
+
}
|
|
539
|
+
return Topology.makeCombinedTarget(target, update)
|
|
540
|
+
}
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
const makeUpdatingTargetBuilder = (
|
|
544
|
+
builder: unknown,
|
|
545
|
+
ownerPath: string
|
|
546
|
+
): unknown => {
|
|
547
|
+
if (typeof builder !== "object" || builder === null) {
|
|
548
|
+
throw new Error("Machine combined target requires a state construction builder")
|
|
549
|
+
}
|
|
550
|
+
const updating: Record<PropertyKey, unknown> = {}
|
|
551
|
+
for (const property of Reflect.ownKeys(builder)) {
|
|
552
|
+
const descriptor = Object.getOwnPropertyDescriptor(builder, property)
|
|
553
|
+
if (descriptor === undefined) continue
|
|
554
|
+
if (
|
|
555
|
+
(property === "from" || property === "decoded") && "value" in descriptor && typeof descriptor.value === "function"
|
|
556
|
+
) {
|
|
557
|
+
const construct = descriptor.value
|
|
558
|
+
descriptor.value = (...args: ReadonlyArray<unknown>) => makeUpdatingConstruction(construct(...args), ownerPath)
|
|
559
|
+
}
|
|
560
|
+
Object.defineProperty(updating, property, descriptor)
|
|
561
|
+
}
|
|
562
|
+
return Object.freeze(updating)
|
|
563
|
+
}
|
|
564
|
+
|
|
465
565
|
const getSelectionBuilder = (
|
|
466
566
|
target: Record<string, any>,
|
|
467
567
|
selection: Topology.TargetSelection,
|
|
@@ -469,6 +569,13 @@ const getSelectionBuilder = (
|
|
|
469
569
|
source: string
|
|
470
570
|
): unknown => {
|
|
471
571
|
if (selection.kind === "none") return target.none
|
|
572
|
+
if (selection.kind === "update") {
|
|
573
|
+
return withFrom(
|
|
574
|
+
(value: unknown) => Topology.makeStateUpdate(selection.path!, value),
|
|
575
|
+
"leaf",
|
|
576
|
+
true
|
|
577
|
+
)
|
|
578
|
+
}
|
|
472
579
|
let builder: any
|
|
473
580
|
let parts = selection.path!.split(".")
|
|
474
581
|
if (selection.kind === "history") {
|
|
@@ -497,7 +604,7 @@ const getSelectionBuilder = (
|
|
|
497
604
|
) {
|
|
498
605
|
throw new Error(`Machine could not construct selected transition target "${selection.path}"`)
|
|
499
606
|
}
|
|
500
|
-
return builder
|
|
607
|
+
return selection.updatePath === undefined ? builder : makeUpdatingTargetBuilder(builder, selection.updatePath)
|
|
501
608
|
}
|
|
502
609
|
|
|
503
610
|
const constructSelectedTarget = (builder: any): unknown =>
|
|
@@ -514,10 +621,24 @@ const validateResolvedSelection = (
|
|
|
514
621
|
}
|
|
515
622
|
return
|
|
516
623
|
}
|
|
624
|
+
if (selection.kind === "update") {
|
|
625
|
+
if (!Topology.isStateUpdate(result) || result.path !== selection.path) {
|
|
626
|
+
throw new Error(`Machine state update for "${selection.path}" must return its selected update builder`)
|
|
627
|
+
}
|
|
628
|
+
return
|
|
629
|
+
}
|
|
630
|
+
if (selection.updatePath !== undefined) {
|
|
631
|
+
if (!Topology.isCombinedTarget(result) || result.update.path !== selection.updatePath) {
|
|
632
|
+
throw new Error(`Machine target updating "${selection.updatePath}" must return target construction .update(...)`)
|
|
633
|
+
}
|
|
634
|
+
} else if (Topology.isCombinedTarget(result)) {
|
|
635
|
+
throw new Error("Machine combined target requires an updating owner declaration")
|
|
636
|
+
}
|
|
517
637
|
if (result === undefined) return
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
638
|
+
const target = Topology.isCombinedTarget(result) ? result.target : result
|
|
639
|
+
const resultPath = typeof target === "object" && target !== null && hasProperty(target, "path") &&
|
|
640
|
+
typeof target.path === "string"
|
|
641
|
+
? target.path
|
|
521
642
|
: undefined
|
|
522
643
|
const selectedNode = selection.path === undefined ? undefined : stateNodes.byPath.get(selection.path)
|
|
523
644
|
const acceptsDescendant = (selection.scope === "local" || selection.scope === "branch") &&
|
|
@@ -544,6 +665,26 @@ const runCapturedBranch = (
|
|
|
544
665
|
const resolverContext = { ...context }
|
|
545
666
|
if (branch.selection.kind === "none") delete resolverContext.target
|
|
546
667
|
else resolverContext.target = selectedTarget
|
|
668
|
+
if (branch.selection.kind === "update") {
|
|
669
|
+
const ownerPath = branch.selection.path!
|
|
670
|
+
delete resolverContext.target
|
|
671
|
+
resolverContext.current = context.ancestors[ownerPath] ?? context.state
|
|
672
|
+
resolverContext.owner = getSelectionBuilder(
|
|
673
|
+
context.target,
|
|
674
|
+
makeStateUpdateSelection(ownerPath, branch.selection.scope === "local" ? "local" : "branch"),
|
|
675
|
+
stateNodes,
|
|
676
|
+
source
|
|
677
|
+
)
|
|
678
|
+
} else if (branch.selection.updatePath !== undefined) {
|
|
679
|
+
const ownerPath = branch.selection.updatePath
|
|
680
|
+
resolverContext.current = context.ancestors[ownerPath]
|
|
681
|
+
resolverContext.owner = getSelectionBuilder(
|
|
682
|
+
context.target,
|
|
683
|
+
makeStateUpdateSelection(ownerPath, "branch"),
|
|
684
|
+
stateNodes,
|
|
685
|
+
source
|
|
686
|
+
)
|
|
687
|
+
}
|
|
547
688
|
if (branch.declinable === true) resolverContext.decline = Topology.makeDeclined
|
|
548
689
|
const resolved = branch.resolve(resolverContext, enqueue)
|
|
549
690
|
if (Topology.isDeclined(resolved)) {
|
|
@@ -556,6 +697,9 @@ const runCapturedBranch = (
|
|
|
556
697
|
return resolved === undefined ? constructSelectedTarget(selectedTarget) : resolved
|
|
557
698
|
}
|
|
558
699
|
|
|
700
|
+
const topologyTargetPath = (selection: Topology.TargetSelection): string | undefined =>
|
|
701
|
+
selection.kind === "update" ? undefined : selection.path
|
|
702
|
+
|
|
559
703
|
const isArrayIndexKey = (key: string): boolean => {
|
|
560
704
|
const index = Number(key)
|
|
561
705
|
return Number.isInteger(index) && index >= 0 && index < 0xffff_ffff && String(index) === key
|
|
@@ -590,6 +734,9 @@ const captureNamedBranches = (
|
|
|
590
734
|
if (!Topology.isTargetSelection(target)) {
|
|
591
735
|
throw new Error(`Machine transition branch "${key}" must select exactly one target`)
|
|
592
736
|
}
|
|
737
|
+
if (target.updatePath !== undefined) {
|
|
738
|
+
throw new Error(`Machine transition branch "${key}" cannot declare an updating target`)
|
|
739
|
+
}
|
|
593
740
|
if (title !== undefined && (typeof title !== "string" || title.length === 0)) {
|
|
594
741
|
throw new Error(`Machine transition branch "${key}" title must be a non-empty string`)
|
|
595
742
|
}
|
|
@@ -738,7 +885,9 @@ const captureTransition = (
|
|
|
738
885
|
declinable,
|
|
739
886
|
targets: [
|
|
740
887
|
...new Set(
|
|
741
|
-
branches.flatMap((branch) =>
|
|
888
|
+
branches.flatMap((branch) =>
|
|
889
|
+
topologyTargetPath(branch.selection) === undefined ? [] : [branch.selection.path!]
|
|
890
|
+
)
|
|
742
891
|
)
|
|
743
892
|
],
|
|
744
893
|
branches: branches.map((branch) =>
|
|
@@ -746,15 +895,16 @@ const captureTransition = (
|
|
|
746
895
|
type: "branch" as const,
|
|
747
896
|
key: branch.key,
|
|
748
897
|
title: branch.title,
|
|
749
|
-
target: branch.selection
|
|
750
|
-
selection: transitionTargetSelection(branch.selection)
|
|
898
|
+
target: topologyTargetPath(branch.selection),
|
|
899
|
+
selection: transitionTargetSelection(branch.selection),
|
|
900
|
+
updates: selectionUpdates(branch.selection)
|
|
751
901
|
})
|
|
752
902
|
),
|
|
753
903
|
evaluate,
|
|
754
904
|
transition: (context: Record<string, any>, enqueue: unknown) => evaluate(context, enqueue).result
|
|
755
905
|
}
|
|
756
906
|
}
|
|
757
|
-
const branch = captureDefinitionBranch(transition, selector, path, trigger)
|
|
907
|
+
const branch = captureDefinitionBranch(transition, selector, stateNodes, path, trigger)
|
|
758
908
|
const evaluate = (context: Record<string, any>, enqueue: unknown) => ({
|
|
759
909
|
result: runCapturedBranch(branch, context, enqueue, stateNodes, path),
|
|
760
910
|
branchIndex: 0,
|
|
@@ -763,11 +913,12 @@ const captureTransition = (
|
|
|
763
913
|
return {
|
|
764
914
|
reenter,
|
|
765
915
|
declinable,
|
|
766
|
-
targets: branch.selection
|
|
916
|
+
targets: topologyTargetPath(branch.selection) === undefined ? [] : [branch.selection.path!],
|
|
767
917
|
branches: [{
|
|
768
918
|
type: "direct" as const,
|
|
769
|
-
target: branch.selection
|
|
770
|
-
selection: transitionTargetSelection(branch.selection)
|
|
919
|
+
target: topologyTargetPath(branch.selection),
|
|
920
|
+
selection: transitionTargetSelection(branch.selection),
|
|
921
|
+
updates: selectionUpdates(branch.selection)
|
|
771
922
|
}],
|
|
772
923
|
evaluate,
|
|
773
924
|
transition: (context: Record<string, any>, enqueue: unknown) => evaluate(context, enqueue).result
|
|
@@ -974,8 +1125,18 @@ const withFrom = <Method extends (value: unknown, ...args: ReadonlyArray<any>) =
|
|
|
974
1125
|
method: Method,
|
|
975
1126
|
kind: FromMethodKind,
|
|
976
1127
|
valued: boolean
|
|
977
|
-
):
|
|
978
|
-
|
|
1128
|
+
): {
|
|
1129
|
+
readonly decoded?: Method
|
|
1130
|
+
readonly from: (...args: ReadonlyArray<any>) => unknown
|
|
1131
|
+
} => {
|
|
1132
|
+
const builder: Record<string, unknown> = {}
|
|
1133
|
+
if (valued) {
|
|
1134
|
+
Object.defineProperty(builder, "decoded", {
|
|
1135
|
+
value: method,
|
|
1136
|
+
enumerable: false
|
|
1137
|
+
})
|
|
1138
|
+
}
|
|
1139
|
+
Object.defineProperty(builder, "from", {
|
|
979
1140
|
value: (...args: ReadonlyArray<any>) => {
|
|
980
1141
|
if (!valued) {
|
|
981
1142
|
return method(undefined, ...args)
|
|
@@ -987,7 +1148,10 @@ const withFrom = <Method extends (value: unknown, ...args: ReadonlyArray<any>) =
|
|
|
987
1148
|
},
|
|
988
1149
|
enumerable: false
|
|
989
1150
|
})
|
|
990
|
-
return
|
|
1151
|
+
return builder as {
|
|
1152
|
+
readonly decoded?: Method
|
|
1153
|
+
readonly from: (...args: ReadonlyArray<any>) => unknown
|
|
1154
|
+
}
|
|
991
1155
|
}
|
|
992
1156
|
|
|
993
1157
|
const withInitial = <Builder extends object>(
|
|
@@ -1470,13 +1634,13 @@ const makeInitialSelector = (stateNodes: Machine.StateNodes): unknown => {
|
|
|
1470
1634
|
const getInitialSelectionBuilder = (
|
|
1471
1635
|
initialBuilder: Record<string, any>,
|
|
1472
1636
|
selection: Topology.TargetSelection
|
|
1473
|
-
):
|
|
1637
|
+
): Record<string, any> => {
|
|
1474
1638
|
const path = selection.path
|
|
1475
1639
|
if (path === undefined || path.includes(".")) {
|
|
1476
1640
|
throw new Error("Machine initial target must select one top-level state")
|
|
1477
1641
|
}
|
|
1478
1642
|
const builder = initialBuilder[path]
|
|
1479
|
-
if (typeof builder !== "function") {
|
|
1643
|
+
if (typeof builder !== "object" || builder === null || typeof builder.from !== "function") {
|
|
1480
1644
|
throw new Error(`Machine could not construct selected initial state "${path}"`)
|
|
1481
1645
|
}
|
|
1482
1646
|
return builder
|
|
@@ -1489,7 +1653,7 @@ const captureInitialBranch = (
|
|
|
1489
1653
|
): {
|
|
1490
1654
|
readonly selection: Topology.TargetSelection
|
|
1491
1655
|
readonly resolve?: (context: any) => unknown
|
|
1492
|
-
readonly builder:
|
|
1656
|
+
readonly builder: Record<string, any>
|
|
1493
1657
|
} => {
|
|
1494
1658
|
if (typeof definition !== "function") {
|
|
1495
1659
|
throw new Error("Machine initial definition must be a target-first callback")
|