xstate 6.0.0-alpha.40 → 6.0.0-alpha.41

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.
Files changed (55) hide show
  1. package/dist/{StateMachine-1464d5e4.cjs.js → StateMachine-8e73d782.cjs.js} +65 -4
  2. package/dist/{StateMachine-76ee1d6e.development.cjs.js → StateMachine-db4e6d4b.development.cjs.js} +65 -4
  3. package/dist/{StateMachine-6082c3cf.esm.js → StateMachine-ee89d49f.esm.js} +65 -4
  4. package/dist/{StateMachine-f8c7feca.development.esm.js → StateMachine-f60abe05.development.esm.js} +65 -4
  5. package/dist/{createActor-c0f78b8a.development.cjs.js → createActor-20615484.development.cjs.js} +787 -62
  6. package/dist/{createActor-a211c276.esm.js → createActor-a3f7750f.esm.js} +757 -55
  7. package/dist/{createActor-2a03d4b2.development.esm.js → createActor-c97c04c9.development.esm.js} +767 -62
  8. package/dist/{createActor-d5305cc9.cjs.js → createActor-f3afd774.cjs.js} +777 -55
  9. package/dist/declarations/src/actors/logic.d.ts +0 -3
  10. package/dist/declarations/src/createActor.d.ts +41 -6
  11. package/dist/declarations/src/durable/index.d.ts +75 -8
  12. package/dist/declarations/src/effectDescriptor.d.ts +81 -0
  13. package/dist/declarations/src/fsm.d.ts +1 -0
  14. package/dist/declarations/src/index.d.ts +3 -1
  15. package/dist/declarations/src/inspection.d.ts +11 -1
  16. package/dist/declarations/src/machineVersion.types.d.ts +1 -0
  17. package/dist/declarations/src/runtimeHelpers.d.ts +35 -0
  18. package/dist/declarations/src/system.d.ts +38 -2
  19. package/dist/declarations/src/transitionActions.d.ts +1 -1
  20. package/dist/declarations/src/types.d.ts +18 -0
  21. package/dist/{transition-e8a13c00.esm.js → effectDescriptor-57ae11fa.esm.js} +110 -2
  22. package/dist/{transition-f581ffa6.development.esm.js → effectDescriptor-b7c48ceb.development.esm.js} +110 -2
  23. package/dist/{transition-4ba58b71.development.cjs.js → effectDescriptor-ba9e14f0.development.cjs.js} +110 -1
  24. package/dist/{transition-2dcabac8.cjs.js → effectDescriptor-e2bb61e2.cjs.js} +110 -1
  25. package/dist/{index-21d08fff.cjs.js → index-7fd92683.cjs.js} +9 -53
  26. package/dist/{index-7020402e.esm.js → index-a8ce72f1.esm.js} +6 -50
  27. package/dist/{index-17f889df.development.esm.js → index-b5b1b2bc.development.esm.js} +6 -50
  28. package/dist/{index-3d1368f7.development.cjs.js → index-d42efc07.development.cjs.js} +9 -53
  29. package/dist/xstate-actors.cjs.js +2 -2
  30. package/dist/xstate-actors.development.cjs.js +2 -2
  31. package/dist/xstate-actors.development.esm.js +2 -2
  32. package/dist/xstate-actors.esm.js +2 -2
  33. package/dist/xstate-actors.umd.min.js +1 -1
  34. package/dist/xstate-actors.umd.min.js.map +1 -1
  35. package/dist/xstate-durable.cjs.js +245 -24
  36. package/dist/xstate-durable.development.cjs.js +245 -24
  37. package/dist/xstate-durable.development.esm.js +243 -22
  38. package/dist/xstate-durable.esm.js +243 -22
  39. package/dist/xstate-durable.umd.min.js +1 -1
  40. package/dist/xstate-durable.umd.min.js.map +1 -1
  41. package/dist/xstate-graph.cjs.js +3 -3
  42. package/dist/xstate-graph.development.cjs.js +3 -3
  43. package/dist/xstate-graph.development.esm.js +3 -3
  44. package/dist/xstate-graph.esm.js +3 -3
  45. package/dist/xstate-graph.umd.min.js +1 -1
  46. package/dist/xstate-graph.umd.min.js.map +1 -1
  47. package/dist/xstate.cjs.js +20 -9
  48. package/dist/xstate.cjs.mjs +5 -0
  49. package/dist/xstate.development.cjs.js +20 -9
  50. package/dist/xstate.development.cjs.mjs +5 -0
  51. package/dist/xstate.development.esm.js +12 -6
  52. package/dist/xstate.esm.js +12 -6
  53. package/dist/xstate.umd.min.js +1 -1
  54. package/dist/xstate.umd.min.js.map +1 -1
  55. package/package.json +1 -1
@@ -58,6 +58,9 @@ const XSTATE_SPAWN = '@xstate.spawn';
58
58
  const XSTATE_STOP = '@xstate.stop';
59
59
  const XSTATE_TERMINATE = '@xstate.terminate';
60
60
  const XSTATE_TIMER = 'xstate.timer';
61
+ const XSTATE_LOGIC_EFFECT_RESOLVE = 'xstate.logic.effect.resolve';
62
+ const XSTATE_LOGIC_EFFECT_REJECT = 'xstate.logic.effect.reject';
63
+ const XSTATE_LOGIC_EFFECT_START = 'xstate.logic.effect.start';
61
64
 
62
65
  /**
63
66
  * Returns an event that represents an implicit event that is sent after the
@@ -180,6 +183,118 @@ function reportUnhandledError(err) {
180
183
 
181
184
  const symbolObservable = (() => typeof Symbol === 'function' && Symbol.observable || '@@observable')();
182
185
 
186
+ const emptySubscription = {
187
+ unsubscribe() {}
188
+ };
189
+ const restoreHint = 'or restore this snapshot with embedded children on the runtime that owns them.';
190
+
191
+ /** @internal */
192
+ function isRemoteActorRef(actorRef) {
193
+ return actorRef._remote === true;
194
+ }
195
+
196
+ /**
197
+ * The lifecycle-only snapshot a remote handle exposes. `active` is accurate
198
+ * by construction: a completion removes the child from its parent's
199
+ * `children`, so a handle only exists while the child is presumed active,
200
+ * and the terminal result arrives as a completion event instead of a
201
+ * snapshot. Full snapshots are a co-location capability.
202
+ */
203
+ const remoteSnapshot = Object.freeze({
204
+ status: 'active',
205
+ output: undefined,
206
+ error: undefined
207
+ });
208
+
209
+ /**
210
+ * Creates a location-transparent handle to an actor whose state lives with
211
+ * another runtime. The handle is constructed from identity alone — no lookup
212
+ * — and every send routes through the system's runtime; its state is not
213
+ * synchronously readable.
214
+ *
215
+ * The surface is deliberately minimal: it covers every member the runtime
216
+ * reaches for a restored child (send/_send, lifecycle via start/_stop and
217
+ * system.stopActor, persistence, subscribe/on as inert subscriptions).
218
+ * Members that only make sense for a co-located actor (public stop(),
219
+ * select(), trigger) throw a descriptive error rather than being absent, so
220
+ * calling one reports what went wrong instead of a bare TypeError. Observable
221
+ * interop and _processingStatus are omitted entirely.
222
+ *
223
+ * @internal
224
+ */
225
+ function createRemoteActorRef(system, options) {
226
+ const handle = {
227
+ _remote: true,
228
+ // Self-reference so context persistence recognizes the handle as an
229
+ // actor reference instead of recursing into it.
230
+ ref: undefined,
231
+ id: options.id,
232
+ address: options.address,
233
+ src: options.src,
234
+ registryKey: options.registryKey,
235
+ // A remote handle does not know the child's incarnation; the runtime that
236
+ // owns the child is the authority on completion staleness — unless the
237
+ // host supplied an incarnation token, which lets this side drop stale
238
+ // completions and lets journaled sends name the intended incarnation.
239
+ sessionId: undefined,
240
+ _incarnation: options.incarnation,
241
+ system,
242
+ _parent: options.parent,
243
+ // Round-trips through persistence verbatim (undefined stays undefined,
244
+ // so re-persisting is byte-stable); a remote handle cannot act on it
245
+ // locally.
246
+ _syncSnapshot: options.syncSnapshot,
247
+ send(event) {
248
+ void system.sendEvent(undefined, ref, event);
249
+ },
250
+ _send(event) {
251
+ throw new Error(`Remote actor '${options.address}' has no local mailbox to receive "${event.type}". Its state lives with another runtime; install a runtime that can reach it (via \`createDurable\`'s adapter runtime operations, or \`system.runtime\`) before sending, ${restoreHint}`);
252
+ },
253
+ getSnapshot() {
254
+ return remoteSnapshot;
255
+ },
256
+ getPersistedSnapshot() {
257
+ throw new Error(`Cannot persist remote actor '${options.address}' from here: its state lives with the runtime that owns it. Persist it there, ${restoreHint}`);
258
+ },
259
+ start() {},
260
+ _stop() {},
261
+ stop() {
262
+ throw new Error(`Cannot stop remote actor '${options.address}' directly: stopping is a co-located operation. Stop it through the system runtime that owns it (\`system.stopActor(ref)\`), ${restoreHint}`);
263
+ },
264
+ select() {
265
+ throw new Error(`Cannot select from remote actor '${options.address}': its snapshot is not synchronously readable because its state lives with another runtime. Read it on the runtime that owns it, ${restoreHint}`);
266
+ },
267
+ get trigger() {
268
+ throw new Error(`Remote actor '${options.address}' has no \`trigger\` shorthand: it requires a co-located actor. Use \`send(...)\`, which routes through the system runtime.`);
269
+ },
270
+ // Observation is a co-location capability: a remote handle never emits,
271
+ // so subscriptions are inert rather than errors — generic observers may
272
+ // attach to any ref.
273
+ subscribe() {
274
+ return emptySubscription;
275
+ },
276
+ on() {
277
+ return emptySubscription;
278
+ },
279
+ _isRunning() {
280
+ return false;
281
+ },
282
+ // The same shape Actor.toJSON produces, so serialized snapshots carry
283
+ // one actor-reference marker whether a child is co-located or remote.
284
+ toJSON() {
285
+ return {
286
+ xstate$type: ACTOR_REF_TYPE,
287
+ id: options.id,
288
+ address: options.address,
289
+ src: options.src
290
+ };
291
+ }
292
+ };
293
+ const ref = handle;
294
+ handle.ref = ref;
295
+ return ref;
296
+ }
297
+
183
298
  function assertSendToEvent(event) {
184
299
  if (typeof event === 'string') {
185
300
  throw new Error(// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
@@ -223,7 +338,12 @@ const subscriptions = /* #__PURE__ */new WeakMap();
223
338
  * Internal factory for actor logic that attaches to another actor on start
224
339
  * (`enq.listen()` / `enq.subscribeTo()`) and detaches when stopped.
225
340
  */
226
- function createAttachedLogic(attach) {
341
+ function createAttachedLogic(attach,
342
+ /**
343
+ * Names the logic so its generated actor ids get their own prefix, out of
344
+ * the id space that user sources allocate from.
345
+ */
346
+ id) {
227
347
  const initialTransition = (input, _) => [{
228
348
  status: 'active',
229
349
  output: undefined,
@@ -231,6 +351,7 @@ function createAttachedLogic(attach) {
231
351
  input
232
352
  }, []];
233
353
  return {
354
+ id,
234
355
  start: (state, {
235
356
  self,
236
357
  system
@@ -329,7 +450,7 @@ function createListenerLogic() {
329
450
  }
330
451
  relayMappedToParent(self, system, () => mapper(emittedEvent));
331
452
  });
332
- });
453
+ }, 'xstate.listener');
333
454
  }
334
455
 
335
456
  // Singleton logic instance
@@ -371,7 +492,7 @@ function createSubscriptionLogic() {
371
492
  // Actor completed without output (stopped); no action needed
372
493
  }
373
494
  });
374
- });
495
+ }, 'xstate.subscription');
375
496
  }
376
497
 
377
498
  // Singleton logic instance
@@ -570,6 +691,17 @@ function pushBuiltInAction(actions, action, ...args) {
570
691
  actions.push(actionRecord);
571
692
  return actionRecord;
572
693
  }
694
+
695
+ /** @internal Max-merges generated-id counter records, copy-on-write. */
696
+ function mergeActorIdCounters(current, update) {
697
+ const merged = {
698
+ ...current
699
+ };
700
+ for (const key of Object.keys(update)) {
701
+ merged[key] = Math.max(merged[key] ?? 0, update[key]);
702
+ }
703
+ return merged;
704
+ }
573
705
  function applyChildUpdate(snapshot, update, actorScope) {
574
706
  if (update.type === 'add') {
575
707
  return {
@@ -577,7 +709,10 @@ function applyChildUpdate(snapshot, update, actorScope) {
577
709
  children: {
578
710
  ...snapshot.children,
579
711
  [update.id]: update.actor
580
- }
712
+ },
713
+ ...(update.counters && {
714
+ _nextActorIds: mergeActorIdCounters(snapshot._nextActorIds, update.counters)
715
+ })
581
716
  };
582
717
  }
583
718
  const children = {
@@ -605,15 +740,187 @@ function getTransitionActionRecord(action) {
605
740
  }
606
741
  return undefined;
607
742
  }
608
- function pushSpawnedChild(actions, actor, id) {
743
+ function pushSpawnedChild(actions, actor, id, counters) {
609
744
  const action = pushBuiltInAction(actions, builtInActions['@xstate.spawn'], actor);
610
745
  action.childUpdate = {
611
746
  type: 'add',
612
747
  actor,
613
- id: id ?? actor.id
748
+ id,
749
+ counters
614
750
  };
615
751
  }
752
+
753
+ /**
754
+ * Spawn-allocation state shared by every enqueue object of one transition, so
755
+ * generated ids stay unique and resolvable across action functions and
756
+ * microsteps of the same event.
757
+ */
758
+
759
+ const spawnAllocations = new WeakMap();
760
+ const createSpawnAllocation = () => ({
761
+ counters: new Map(),
762
+ explicitIds: new Set(),
763
+ stoppedIds: new Set()
764
+ });
765
+
766
+ /**
767
+ * Starts a fresh spawn-allocation transaction for one logical transition.
768
+ * Called at every transition boundary so pure replays from the same snapshot
769
+ * allocate identical ids.
770
+ *
771
+ * @internal
772
+ */
773
+ function beginSpawnAllocation(actorScope) {
774
+ spawnAllocations.set(actorScope, createSpawnAllocation());
775
+ }
776
+
777
+ // Read the raw snapshot: getSnapshot() throws while the actor initializes,
778
+ // and entry actions run before any snapshot exists.
779
+ function getWorkingSnapshotOf(actorScope) {
780
+ return actorScope.self._snapshot;
781
+ }
782
+ const srcKeyCaches = new WeakMap();
783
+ function getRegisteredSrcKey(actorScope, logic) {
784
+ const registeredActors = actorScope.self.logic?.sources?.actors;
785
+ if (!registeredActors) {
786
+ return undefined;
787
+ }
788
+ let cache = srcKeyCaches.get(registeredActors);
789
+ if (!cache) {
790
+ cache = new Map();
791
+ for (const key of Object.keys(registeredActors)) {
792
+ cache.set(registeredActors[key], key);
793
+ }
794
+ srcKeyCaches.set(registeredActors, cache);
795
+ }
796
+ return cache.get(logic);
797
+ }
798
+
799
+ /**
800
+ * Internal helper actors (listeners, subscriptions) number their ids from
801
+ * system-level counters under `xstate.`-prefixed names; snapshot-owned
802
+ * children number from per-snapshot counters. The two spaces only stay
803
+ * collision-free because their prefixes are disjoint, so user sources must
804
+ * not enter the reserved namespace.
805
+ */
806
+ function assertUnreservedPrefix(prefix) {
807
+ if (prefix.startsWith('xstate.')) {
808
+ throw new Error(`Child actor ids with the "xstate." prefix are reserved for internal actors; rename the "${prefix}" source or logic id.`);
809
+ }
810
+ }
811
+
812
+ /**
813
+ * The next free index for a prefix. The parent snapshot's persisted counter
814
+ * is always a floor, so a freed id is never handed out again — not even when
815
+ * an explicit id reserved a lower one earlier in the transition.
816
+ */
817
+ function nextChildIndex(actorScope, allocation, prefix) {
818
+ return Math.max(allocation.counters.get(prefix) ?? 0, getWorkingSnapshotOf(actorScope)?._nextActorIds?.[prefix] ?? 0);
819
+ }
820
+
821
+ /**
822
+ * Allocates the next generated child id for the transition's allocation
823
+ * transaction, seeded from the parent snapshot's own persisted counters. This
824
+ * is the single allocator for snapshot-owned children (`enq.spawn` and
825
+ * context-factory spawns); a correct allocation cannot collide.
826
+ *
827
+ * @internal
828
+ */
829
+ function allocateChildId(actorScope, src, localAllocation) {
830
+ if (!spawnAllocations.get(actorScope)) {
831
+ console.warn('A child id was generated outside a spawn-allocation transaction; ids may repeat across enqueue objects. Transition entry points must call beginSpawnAllocation().');
832
+ }
833
+ const allocation = spawnAllocations.get(actorScope) ?? localAllocation ?? createSpawnAllocation();
834
+ const prefix = getActorIdPrefix(src);
835
+ assertUnreservedPrefix(prefix);
836
+ const next = nextChildIndex(actorScope, allocation, prefix);
837
+ allocation.counters.set(prefix, next + 1);
838
+ return {
839
+ id: `${prefix}:${next}`,
840
+ counters: {
841
+ [prefix]: next + 1
842
+ }
843
+ };
844
+ }
845
+
846
+ /**
847
+ * Requires an explicit child id to be unoccupied before a spawn or invoke
848
+ * claims it: an address must name at most one live actor. An id is occupied
849
+ * when a child of this parent already holds it and this transition has not
850
+ * stopped that child, or when an earlier spawn of this transition claimed it.
851
+ *
852
+ * @internal
853
+ */
854
+ function assertChildIdFree(actorScope, id, localAllocation) {
855
+ const allocation = spawnAllocations.get(actorScope) ?? localAllocation ?? createSpawnAllocation();
856
+ const existing = getWorkingSnapshotOf(actorScope)?.children?.[id];
857
+ // A terminated child no longer occupies its id: it relayed its completion
858
+ // and is removed from `children` right after the transition handling it,
859
+ // so the supervisor pattern — respawn under the same name while handling
860
+ // the child's done/error event — must not conflict with the outgoing
861
+ // entry. Remote handles never expose a terminal status locally; the
862
+ // completion event that removes one is the owning runtime's business.
863
+ const occupied = existing !== undefined && !allocation.stoppedIds.has(id) && existing.getSnapshot().status === 'active';
864
+ if (allocation.explicitIds.has(id) || occupied) {
865
+ throw new Error(`Cannot spawn child actor with id '${id}': the id is already in use by another child of '${actorScope.self.id}'. Stop the existing child before reusing its id.` );
866
+ }
867
+ allocation.explicitIds.add(id);
868
+ }
869
+
870
+ /**
871
+ * Records that this transition stopped a child, freeing its id for a later
872
+ * spawn or invoke of the same transition (the invoke restart pattern).
873
+ *
874
+ * @internal
875
+ */
876
+ function recordStoppedChild(actorScope, actor) {
877
+ const allocation = spawnAllocations.get(actorScope);
878
+ if (allocation) {
879
+ allocation.stoppedIds.add(actor.id);
880
+ allocation.explicitIds.delete(actor.id);
881
+ }
882
+ }
883
+
884
+ /**
885
+ * Reserves an explicit generated-shaped id (`worker:5`) in the transaction
886
+ * and the parent snapshot, so live runs and pure replays allocate the same
887
+ * later ids even after this child is removed.
888
+ *
889
+ * @internal
890
+ */
891
+ function reserveChildId(actorScope, id, localAllocation) {
892
+ const generated = parseGeneratedActorId(id);
893
+ if (!generated) {
894
+ return undefined;
895
+ }
896
+ assertUnreservedPrefix(generated.prefix);
897
+ const allocation = spawnAllocations.get(actorScope) ?? localAllocation ?? createSpawnAllocation();
898
+ // The requested id is used as asked, but numbering continues from the
899
+ // highest reservation: an explicit low id never rewinds the counter.
900
+ const next = Math.max(nextChildIndex(actorScope, allocation, generated.prefix), generated.index + 1);
901
+ allocation.counters.set(generated.prefix, next);
902
+ return {
903
+ [generated.prefix]: next
904
+ };
905
+ }
906
+
907
+ /**
908
+ * The counters allocated so far in the transition's transaction, for
909
+ * committing into a snapshot built outside `applyChildUpdate` (the
910
+ * pre-initial snapshot with context-factory spawns).
911
+ *
912
+ * @internal
913
+ */
914
+ function takeSpawnAllocationCounters(actorScope) {
915
+ const counters = spawnAllocations.get(actorScope)?.counters;
916
+ if (!counters?.size) {
917
+ return undefined;
918
+ }
919
+ return Object.fromEntries(counters);
920
+ }
616
921
  function createTransitionEnqueue(actorScope, actions, internalEvents, actorSubscriptions = false, createActors = true) {
922
+ // Paths that never begin a transaction keep a per-enqueue scope.
923
+ const localAllocation = spawnAllocations.get(actorScope) ?? createSpawnAllocation();
617
924
  const props = {
618
925
  cancel: id => {
619
926
  pushBuiltInAction(actions, builtInActions['@xstate.cancel'], actorScope, id);
@@ -641,11 +948,33 @@ function createTransitionEnqueue(actorScope, actions, internalEvents, actorSubsc
641
948
  id: options?.id ?? options?.registryKey ?? logic.id
642
949
  };
643
950
  }
951
+ // Recover the registered source key for setup-provided logic so the
952
+ // spawned child persists (and gets a deterministic id prefix) by key
953
+ // instead of by inline logic reference.
954
+ const src = getRegisteredSrcKey(actorScope, logic);
955
+ // Generated ids allocate from the parent snapshot's own counters
956
+ // through the transition's allocation transaction; explicit
957
+ // generated-shaped ids reserve their numbering the same way.
958
+ let id = options?.id;
959
+ let counters;
960
+ if (id === undefined) {
961
+ ({
962
+ id,
963
+ counters
964
+ } = allocateChildId(actorScope, src ?? logic, localAllocation));
965
+ } else {
966
+ assertChildIdFree(actorScope, id, localAllocation);
967
+ counters = reserveChildId(actorScope, id, localAllocation);
968
+ }
644
969
  const actor = actorScope.system.createActorRef(logic, {
645
970
  ...options,
971
+ ...(src !== undefined && {
972
+ src
973
+ }),
974
+ id,
646
975
  parent: actorScope.self
647
976
  });
648
- pushSpawnedChild(actions, actor, options?.id);
977
+ pushSpawnedChild(actions, actor, id, counters);
649
978
  return actor;
650
979
  },
651
980
  sendTo: (actor, event, options) => {
@@ -665,6 +994,7 @@ function createTransitionEnqueue(actorScope, actions, internalEvents, actorSubsc
665
994
  type: 'remove',
666
995
  actor
667
996
  };
997
+ recordStoppedChild(actorScope, actor);
668
998
  }
669
999
  }
670
1000
  };
@@ -1401,6 +1731,10 @@ function createMaterializedInertActorScope(actorLogic, sourceRef, sourceChildren
1401
1731
  });
1402
1732
  if (previousSelf?._parent) {
1403
1733
  self._parent = previousSelf._parent;
1734
+ // `address` memoizes on first read assuming `_parent` is final; nothing
1735
+ // reads it between construction and this assignment, but drop any memo so
1736
+ // a future construction-time read cannot pin a root-shaped address.
1737
+ self._address = undefined;
1404
1738
  }
1405
1739
  if (currentSnapshot) {
1406
1740
  self._snapshot = currentSnapshot;
@@ -1672,7 +2006,17 @@ function getEventDescriptorKey(event) {
1672
2006
  return legacyType ? `${event.type}|${legacyType}` : event.type;
1673
2007
  }
1674
2008
  function matchesActorSession(event, snapshot, actorId) {
1675
- return !snapshot.children[actorId] || !('sessionId' in event) || snapshot.children[actorId]?.sessionId === event.sessionId;
2009
+ const child = snapshot.children[actorId];
2010
+ if (!child || !('sessionId' in event)) {
2011
+ return true;
2012
+ }
2013
+ if (isRemoteActorRef(child)) {
2014
+ // Without a host-supplied incarnation token the runtime that owns the
2015
+ // child is the authority on staleness; with one, a completion from a
2016
+ // different incarnation of the same address is dropped here.
2017
+ return child._incarnation === undefined || child._incarnation === event.sessionId;
2018
+ }
2019
+ return child.sessionId === event.sessionId;
1676
2020
  }
1677
2021
  function normalizeLegacyInternalEvent(event, machine) {
1678
2022
  if (event.type === 'xstate.done.actor' || event.type === 'xstate.error.actor' || event.type === 'xstate.snapshot.actor' || event.type === 'xstate.done.state' || event.type === 'xstate.after' || event.type === 'xstate.timeout' || event.type === 'xstate.timeout.actor') {
@@ -2258,9 +2602,17 @@ function narrowParallelDomain(domain, targetStates) {
2258
2602
  function getTransitionDomain(transition, snapshot, resolveTransition) {
2259
2603
  const targetStates = getEffectiveTargetStates(transition, snapshot, resolveTransition);
2260
2604
  const {
2605
+ targets,
2261
2606
  reenter
2262
2607
  } = resolveTransition(transition);
2263
- if (targetStates.every(target => target === transition.source || isDescendant(target, transition.source))) {
2608
+
2609
+ // A history target that restores the source itself must exit and reenter
2610
+ // the source (SCXML domain = the source's ancestor), unlike a plain
2611
+ // non-reentering self-target: the enter set restores the stored
2612
+ // configuration from outside the source, so the exit set must match or the
2613
+ // source's invoked actors are re-created without being stopped.
2614
+ const restoresSourceViaHistory = targets?.some(isHistoryNode) && targetStates.some(target => target === transition.source);
2615
+ if (!restoresSourceViaHistory && targetStates.every(target => target === transition.source || isDescendant(target, transition.source))) {
2264
2616
  // Targets are contained within the source. A reentering transition
2265
2617
  // exits and reenters the source itself; otherwise the domain narrows
2266
2618
  // past parallel nodes so sibling regions are left untouched.
@@ -2629,6 +2981,7 @@ function microstep(transitions, currentSnapshot, actorScope, event, isInitial, i
2629
2981
  event,
2630
2982
  output: getEventOutput(event)
2631
2983
  }) : invokeDef.input;
2984
+ assertChildIdFree(actorScope, invokeDef.id);
2632
2985
  const actor = actorScope.system.createActorRef(logic, {
2633
2986
  ...invokeDef,
2634
2987
  input,
@@ -2864,7 +3217,17 @@ function macrostep(snapshot, event, actorScope, internalQueue, initialMicrosteps
2864
3217
  return;
2865
3218
  }
2866
3219
  const child = nextSnapshot.children[actorId];
2867
- if (!child || child.sessionId !== sessionId) {
3220
+ if (!child) {
3221
+ return;
3222
+ }
3223
+ // The same staleness rule matchesActorSession applies to transition
3224
+ // selection: a completion from a different incarnation must not remove
3225
+ // the still-running child either.
3226
+ if (isRemoteActorRef(child)) {
3227
+ if (child._incarnation !== undefined && child._incarnation !== sessionId) {
3228
+ return;
3229
+ }
3230
+ } else if (child.sessionId !== sessionId) {
2868
3231
  return;
2869
3232
  }
2870
3233
  const children = {
@@ -3236,6 +3599,7 @@ function createMachineSnapshot(config, machine, actorRef) {
3236
3599
  historyValue: compactSnapshotRecord(config.historyValue),
3237
3600
  _stateInputs: compactSnapshotRecord(config._stateInputs),
3238
3601
  _nextTimerId: config._nextTimerId ?? 0,
3602
+ _nextActorIds: config._nextActorIds,
3239
3603
  matches: machineSnapshotMatches,
3240
3604
  hasTag: machineSnapshotHasTag,
3241
3605
  can: machineSnapshotCan,
@@ -3268,6 +3632,7 @@ function cloneMachineSnapshot(snapshot, config = {}) {
3268
3632
  historyValue: compactSnapshotRecord(configWithSnapshot.historyValue),
3269
3633
  _stateInputs: compactSnapshotRecord(configWithSnapshot._stateInputs),
3270
3634
  _nextTimerId: configWithSnapshot._nextTimerId ?? 0,
3635
+ _nextActorIds: configWithSnapshot._nextActorIds,
3271
3636
  matches: machineSnapshotMatches,
3272
3637
  hasTag: machineSnapshotHasTag,
3273
3638
  can: machineSnapshotCan,
@@ -3316,18 +3681,46 @@ function getPersistedSnapshot(snapshot, options) {
3316
3681
  } = snapshot;
3317
3682
  const childrenJson = {};
3318
3683
  const timersJson = {};
3684
+ const embedChildren = options?.embedChildren !== false;
3319
3685
  for (const id in children) {
3320
3686
  const child = children[id];
3321
3687
  if (typeof child.src !== 'string' && (!options || !('__unsafeAllowInlineActors' in options))) {
3322
3688
  throw new Error('An inline child actor cannot be persisted.');
3323
3689
  }
3690
+ // Children are referenced by their logical address. Embedding the child
3691
+ // state is the co-locating runtime's whole-tree checkpoint capability;
3692
+ // remote handles never embed — their state lives with another runtime.
3693
+ const embed = embedChildren && !isRemoteActorRef(child);
3694
+ if (!embed && typeof child.src !== 'string') {
3695
+ // Fail where it is actionable: a by-address reference without a
3696
+ // registered source key could be persisted but never restored.
3697
+ throw new Error(`Unable to persist child '${id}' by address: it requires a registered source key.`);
3698
+ }
3324
3699
  childrenJson[id] = {
3325
- snapshot: child.getPersistedSnapshot(options),
3700
+ address: child.address,
3701
+ // The explicit marker disambiguates a by-address reference from a child
3702
+ // whose own persisted snapshot happens to be undefined.
3703
+ ...(embed ? {
3704
+ snapshot: child.getPersistedSnapshot(options)
3705
+ } : {
3706
+ remote: true,
3707
+ // Round-trips verbatim: the host that owns the child injects the
3708
+ // token; XState never stamps one (a local sessionId would make
3709
+ // persisted snapshots nondeterministic across replays).
3710
+ incarnation: child._incarnation
3711
+ }),
3326
3712
  src: child.src,
3327
3713
  registryKey: child.registryKey,
3328
3714
  syncSnapshot: child._syncSnapshot
3329
3715
  };
3330
3716
  }
3717
+ const snapshotActor = getSnapshotActorRef(snapshot)?.actor;
3718
+ // Only the wall clock is stamped into persisted timers: a custom clock's
3719
+ // readings (a simulated clock, a monotonic counter) are meaningless in any
3720
+ // other process, and restoring them under the wall clock would fire every
3721
+ // pending delay instantly. Pure-transition snapshots have no local
3722
+ // schedule and persist no timestamp.
3723
+ const scheduledTimers = snapshotActor?.system?._clock?.now ? undefined : snapshotActor?.system?._snapshot?._scheduledTimers;
3331
3724
  for (const id in timers) {
3332
3725
  const timer = timers[id];
3333
3726
  let event = timer.event;
@@ -3352,17 +3745,28 @@ function getPersistedSnapshot(snapshot, options) {
3352
3745
  throw new Error(`Unable to persist timer '${id}': target actor '${timer.target.id}' is no longer addressable from this snapshot.`);
3353
3746
  }
3354
3747
  }
3748
+ // A live runtime knows when the timer is due; persist the wall-clock
3749
+ // start (dueAt - declared delay) so restore can honor the absolute
3750
+ // deadline. Derived from dueAt, not the scheduling moment, so repeated
3751
+ // persist/restore cycles keep the same deadline. Without a live schedule
3752
+ // (a restored-but-never-started actor, a pure-transition snapshot) the
3753
+ // timer's carried-in start — which only a wall-clock actor ever stamped —
3754
+ // passes through, so re-persisting cannot push the deadline back.
3755
+ const scheduled = scheduledTimers?.[`${snapshotActor.sessionId}.${id}`];
3756
+ const startedAt = scheduled ? scheduled.dueAt - timer.delay : timer.startedAt;
3355
3757
  timersJson[id] = {
3356
3758
  id: timer.id,
3357
3759
  delay: timer.delay,
3358
3760
  type: timer.type,
3359
3761
  event,
3360
- target
3762
+ target,
3763
+ ...(startedAt !== undefined && {
3764
+ startedAt
3765
+ })
3361
3766
  };
3362
3767
  }
3363
3768
  const persisted = {
3364
3769
  ...jsonValues,
3365
- _nextActorId: getSnapshotActorRef(snapshot)?.systemState.snapshot._nextActorId,
3366
3770
  context: persistContext(context),
3367
3771
  children: childrenJson,
3368
3772
  timers: timersJson,
@@ -3390,7 +3794,7 @@ function persistContext(contextPart) {
3390
3794
  ...contextPart
3391
3795
  };
3392
3796
  copy[key] = {
3393
- xstate$$type: $$ACTOR_TYPE,
3797
+ xstate$type: ACTOR_REF_TYPE,
3394
3798
  id: value.id
3395
3799
  };
3396
3800
  } else {
@@ -3657,6 +4061,106 @@ function matchesEventDescriptor(eventType, descriptor) {
3657
4061
  return true;
3658
4062
  }
3659
4063
 
4064
+ function getStepEffect(actor, key) {
4065
+ return actor.getSnapshot().effects?.[key];
4066
+ }
4067
+ function waitForStep(actor, key) {
4068
+ return new Promise((resolve, reject) => {
4069
+ const subscription = actor.subscribe(snapshot => {
4070
+ const effect = snapshot.effects?.[key];
4071
+ if (effect?.status === 'done') {
4072
+ subscription.unsubscribe();
4073
+ resolve(effect.output);
4074
+ } else if (effect?.status === 'error') {
4075
+ subscription.unsubscribe();
4076
+ reject(effect.error);
4077
+ }
4078
+ });
4079
+ });
4080
+ }
4081
+
4082
+ /**
4083
+ * Runs one keyed step of an async actor with the built-in journal: the
4084
+ * result is memoized in the actor's own snapshot (`effects[key]`), so a
4085
+ * restored actor skips completed steps when its logic function re-runs.
4086
+ * Custom runtimes that implement `runStep` use this for the default local
4087
+ * behavior, the same way `deliverEvent` exposes local delivery.
4088
+ *
4089
+ * @experimental
4090
+ */
4091
+ async function runStep(actor, key, exec, sendSelf = event => void actor.system.sendEvent(actor, actor, event)) {
4092
+ const effect = getStepEffect(actor, key);
4093
+ if (effect?.status === 'done') {
4094
+ return effect.output;
4095
+ }
4096
+ if (effect?.status === 'error') {
4097
+ throw effect.error;
4098
+ }
4099
+ if (effect?.status === 'active') {
4100
+ return waitForStep(actor, key);
4101
+ }
4102
+ sendSelf({
4103
+ type: XSTATE_LOGIC_EFFECT_START,
4104
+ key
4105
+ });
4106
+ try {
4107
+ const output = await exec();
4108
+ sendSelf({
4109
+ type: XSTATE_LOGIC_EFFECT_RESOLVE,
4110
+ key,
4111
+ output
4112
+ });
4113
+ return output;
4114
+ } catch (error) {
4115
+ sendSelf({
4116
+ type: XSTATE_LOGIC_EFFECT_REJECT,
4117
+ key,
4118
+ error
4119
+ });
4120
+ throw error;
4121
+ }
4122
+ }
4123
+
4124
+ /**
4125
+ * Delivers an event directly to a target actor, bypassing any installed host
4126
+ * runtime. Custom runtimes use this for the default local delivery behavior
4127
+ * inside their own `sendEvent` implementations.
4128
+ *
4129
+ * @experimental
4130
+ */
4131
+ function deliverEvent(source, target, event) {
4132
+ const runtimeTarget = target;
4133
+ // The same guard local delivery has always applied: internal events may
4134
+ // only originate from the actor itself, whichever runtime delivers them.
4135
+ if (source !== target && runtimeTarget.logic?.isInternalEventType?.(event.type)) {
4136
+ throw new Error(`Internal event "${event.type}" cannot be sent to actor "${target.id}" from outside.`);
4137
+ }
4138
+ runtimeTarget._lastSourceRef = source;
4139
+ runtimeTarget._send(event);
4140
+ }
4141
+
4142
+ /**
4143
+ * Publishes an actor's terminal result, notifying its parent and observers.
4144
+ * Custom runtimes use this for the default local behavior inside their own
4145
+ * `terminateActor` implementations.
4146
+ *
4147
+ * @experimental
4148
+ */
4149
+ function terminateActor(actor, termination) {
4150
+ actor._terminate(termination);
4151
+ }
4152
+
4153
+ /**
4154
+ * Stops an actor without producing a completion result. Custom runtimes use
4155
+ * this for the default local behavior inside their own `stopActor`
4156
+ * implementations.
4157
+ *
4158
+ * @experimental
4159
+ */
4160
+ function stopActor(actor) {
4161
+ actor._stop();
4162
+ }
4163
+
3660
4164
  let systemIdPrefix;
3661
4165
  let nextSystemId = 0;
3662
4166
 
@@ -3694,23 +4198,104 @@ function createSystemId() {
3694
4198
  return `${systemIdPrefix}:${(nextSystemId++).toString(36)}`;
3695
4199
  }
3696
4200
 
4201
+ /**
4202
+ * Derives the deterministic id prefix for a generated actor id from its actor
4203
+ * source: the registered source key when the source is a string, the logic's
4204
+ * own `id` otherwise, with `x` as the last-resort prefix.
4205
+ *
4206
+ * @internal
4207
+ */
4208
+ function getActorIdPrefix(src) {
4209
+ if (typeof src === 'string') {
4210
+ return src;
4211
+ }
4212
+ const logicId = src?.id;
4213
+ // Anonymous machines get the placeholder id '(machine)'; only named logic
4214
+ // earns a named id prefix.
4215
+ return typeof logicId === 'string' && logicId.length && logicId !== '(machine)' ? logicId : 'x';
4216
+ }
4217
+
4218
+ /**
4219
+ * Encodes one actor id as an address segment. `/` separates segments, so it
4220
+ * is percent-encoded, and the escape character `%` is escaped first so the
4221
+ * encoding stays injective: the ids 'a/b' and 'a%2Fb' must not produce the
4222
+ * same address.
4223
+ *
4224
+ * @internal
4225
+ */
4226
+ function encodeAddressSegment(id) {
4227
+ return id.includes('/') || id.includes('%') ? id.replaceAll('%', '%25').replaceAll('/', '%2F') : id;
4228
+ }
4229
+
4230
+ /**
4231
+ * Parses a generated-shaped actor id (`prefix:<n>`). This single definition is
4232
+ * the determinism contract for id reservation: live allocation, replay, and
4233
+ * restore must all agree on what counts as generated-shaped. Deliberately
4234
+ * broader than ids a generated allocation could produce — over-reserving
4235
+ * skips numbers, which is harmless, while under-reserving could collide.
4236
+ *
4237
+ * @internal
4238
+ */
4239
+ function parseGeneratedActorId(id) {
4240
+ const separator = id.lastIndexOf(':');
4241
+ if (separator <= 0 || separator === id.length - 1) {
4242
+ return undefined;
4243
+ }
4244
+ const index = Number(id.slice(separator + 1));
4245
+ return Number.isSafeInteger(index) && index >= 0 ? {
4246
+ prefix: id.slice(0, separator),
4247
+ index
4248
+ } : undefined;
4249
+ }
4250
+
4251
+ /**
4252
+ * The id (and address root segment) of the first parentless actor of a logic:
4253
+ * the logic's own name, or `x:0` for anonymous logic.
4254
+ *
4255
+ * @internal
4256
+ */
4257
+ function getRootActorId(src) {
4258
+ const prefix = getActorIdPrefix(src);
4259
+ return prefix === 'x' ? 'x:0' : prefix;
4260
+ }
4261
+ function getActorIdCounterKey(parent, prefix) {
4262
+ return `${parent ? parent.address : ''}|${prefix}`;
4263
+ }
4264
+ function bumpActorIdCounter(system, counterKey, next) {
4265
+ const counters = system._snapshot._nextActorIds;
4266
+ if ((counters[counterKey] ?? 0) >= next) {
4267
+ return;
4268
+ }
4269
+ // Copy-on-write: snapshot systems share this record by shallow `_snapshot`
4270
+ // copies, so branches must not observe each other's allocations.
4271
+ system._snapshot._nextActorIds = {
4272
+ ...counters,
4273
+ [counterKey]: next
4274
+ };
4275
+ markSystemSnapshotDirty(system);
4276
+ }
4277
+
3697
4278
  /** @internal */
3698
- function resolveActorId(system, requestedId) {
4279
+ function resolveActorId(system, requestedId, options) {
3699
4280
  if (requestedId !== undefined) {
3700
- const match = /^x:(\d+)$/.exec(requestedId);
3701
- const reservedId = match ? Number(match[1]) : undefined;
3702
- if (reservedId !== undefined && Number.isSafeInteger(reservedId)) {
3703
- const nextActorId = Math.max(system._snapshot._nextActorId, reservedId + 1);
3704
- if (nextActorId !== system._snapshot._nextActorId) {
3705
- system._snapshot._nextActorId = nextActorId;
3706
- markSystemSnapshotDirty(system);
3707
- }
4281
+ const generated = parseGeneratedActorId(requestedId);
4282
+ if (generated) {
4283
+ bumpActorIdCounter(system, getActorIdCounterKey(options?.parent, generated.prefix), generated.index + 1);
4284
+ } else if (!options?.parent) {
4285
+ // Reserve a restored root's bare name so a later parentless actor of
4286
+ // the same logic in this system numbers past it.
4287
+ bumpActorIdCounter(system, getActorIdCounterKey(undefined, requestedId), 1);
3708
4288
  }
3709
4289
  return requestedId;
3710
4290
  }
3711
- const id = `x:${system._snapshot._nextActorId++}`;
3712
- markSystemSnapshotDirty(system);
3713
- return id;
4291
+ const prefix = getActorIdPrefix(options?.src);
4292
+ const counterKey = getActorIdCounterKey(options?.parent, prefix);
4293
+ const counter = system._snapshot._nextActorIds[counterKey] ?? 0;
4294
+ bumpActorIdCounter(system, counterKey, counter + 1);
4295
+ // The first parentless actor of a logic gets the logic's own name; later
4296
+ // parentless actors of the same logic in a shared system get numbered so
4297
+ // addresses stay unique.
4298
+ return !options?.parent && prefix !== 'x' && counter === 0 ? prefix : `${prefix}:${counter}`;
3714
4299
  }
3715
4300
 
3716
4301
  /** @internal */
@@ -3721,10 +4306,18 @@ function bookSessionId(system) {
3721
4306
  /**
3722
4307
  * Runtime operations used to execute effects.
3723
4308
  *
3724
- * An external interpreter can override these operations while the default actor
3725
- * system provides the local in-memory implementation.
4309
+ * XState calculates pure transitions; a runtime executes their effects. The
4310
+ * built-in local runtime is one implementation at the same level as any
4311
+ * other: install a different one via `system.runtime` (for durable hosts,
4312
+ * through `createDurable`'s adapter runtime operations).
3726
4313
  */
3727
4314
 
4315
+ /** @internal Every operation of `ActorSystemRuntime`, for runtime wrappers. */
4316
+ const RUNTIME_OPERATIONS = ['spawnActor', 'startActor', 'stopActor', 'terminateActor', 'emitEvent', 'scheduleTimer', 'cancelTimer', 'cancelAllTimers', 'deadLetter'
4317
+ // `runStep` and `sendEvent` are deliberately absent: durable executions
4318
+ // wire them separately, since a step awaits other runtime operations and
4319
+ // root-addressed sends are captured per batch.
4320
+ ];
3728
4321
  const emptyScheduledTimers = Object.freeze({});
3729
4322
  function createScheduledTimerId(actor, id) {
3730
4323
  return `${actor.sessionId}.${id}`;
@@ -3785,7 +4378,10 @@ class RuntimeSystem {
3785
4378
  this.createActorRef = options.createActorRef;
3786
4379
  this._snapshot = {
3787
4380
  _scheduledTimers: restoredSnapshot?.scheduler ?? emptyScheduledTimers,
3788
- _nextActorId: restoredSnapshot?._nextActorId ?? 0
4381
+ // System-level counters are process-local backstops; per-actor
4382
+ // counters persist on each machine snapshot, and restored explicit ids
4383
+ // reserve their numbering here via `resolveActorId`.
4384
+ _nextActorIds: {}
3789
4385
  };
3790
4386
  }
3791
4387
 
@@ -3869,16 +4465,7 @@ class RuntimeSystem {
3869
4465
  // Delivers an event to the target actor. Used by both `_relay` (which also
3870
4466
  // records the send) and the scheduler's timer (which already recorded it).
3871
4467
  _deliver(source, target, event) {
3872
- const runtimeTarget = target;
3873
- const targetMachine = runtimeTarget.logic;
3874
- const isInternalEvent = typeof targetMachine?.isInternalEventType === 'function' && targetMachine.isInternalEventType(event.type);
3875
- if (isInternalEvent && source !== target) {
3876
- throw new Error(`Internal event "${event.type}" cannot be sent to actor "${target.id}" from outside.`);
3877
- }
3878
-
3879
- // remember the last source for unified transition inspect event
3880
- runtimeTarget._lastSourceRef = source;
3881
- target._send(event);
4468
+ deliverEvent(source, target, event);
3882
4469
  }
3883
4470
  _register(sessionId, actor) {
3884
4471
  if (actor === this._rootActor) {
@@ -3891,11 +4478,13 @@ class RuntimeSystem {
3891
4478
  }
3892
4479
  _unregister(actor) {
3893
4480
  let changed;
4481
+ // Remote handles have no sessionId and are never in the session map;
4482
+ // their registry cleanup happens through the keyed-actor path below.
3894
4483
  if (actor === this._rootActor) {
3895
4484
  changed = this._getRootActor() !== undefined;
3896
- changed = (this._children?.delete(actor.sessionId) ?? false) || changed;
4485
+ changed = actor.sessionId !== undefined && (this._children?.delete(actor.sessionId) ?? false) || changed;
3897
4486
  } else {
3898
- changed = this._children?.delete(actor.sessionId) ?? false;
4487
+ changed = actor.sessionId !== undefined && (this._children?.delete(actor.sessionId) ?? false);
3899
4488
  }
3900
4489
  const registryKey = this._reverseKeyedActors?.get(actor);
3901
4490
  if (registryKey !== undefined) {
@@ -3948,37 +4537,109 @@ class RuntimeSystem {
3948
4537
  };
3949
4538
  this._inspectionObservers.forEach(observer => observer.next?.(resolvedInspectionEvent));
3950
4539
  }
3951
- spawnActor() {}
4540
+
4541
+ // Unlike the other operations, spawn has no local work: local actors
4542
+ // register with the system when they are constructed, so this only
4543
+ // notifies a host runtime.
4544
+ spawnActor(source, actor) {
4545
+ return this.runtime?.spawnActor?.(source, actor);
4546
+ }
3952
4547
  startActor(actor) {
4548
+ const override = this.runtime?.startActor;
4549
+ if (override) {
4550
+ return override(actor);
4551
+ }
3953
4552
  actor.start();
3954
4553
  }
3955
4554
  stopActor(actor) {
3956
- actor._stop();
4555
+ const override = this.runtime?.stopActor;
4556
+ if (override) {
4557
+ return override(actor);
4558
+ }
4559
+ stopActor(actor);
3957
4560
  }
3958
4561
  terminateActor(actor, termination) {
3959
- actor._terminate(termination);
4562
+ const override = this.runtime?.terminateActor;
4563
+ if (override) {
4564
+ return override(actor, termination);
4565
+ }
4566
+ terminateActor(actor, termination);
3960
4567
  }
3961
4568
  sendEvent(source, target, event) {
4569
+ // Record for the inspection `sent[]` facet regardless of which runtime
4570
+ // delivers, so host runtimes keep inspection parity.
3962
4571
  this._recordSent(source, target, event);
4572
+ const override = this.runtime?.sendEvent;
4573
+ if (override) {
4574
+ return override(source, target, event);
4575
+ }
3963
4576
  this._deliver(source, target, event);
3964
4577
  }
3965
4578
  emitEvent(source, event) {
4579
+ const override = this.runtime?.emitEvent;
4580
+ if (override) {
4581
+ return override(source, event);
4582
+ }
3966
4583
  source._emit(event);
3967
4584
  }
4585
+ deadLetter(source, target, event, reason) {
4586
+ this._sendInspectionEvent({
4587
+ type: '@xstate.deadletter',
4588
+ actorRef: target,
4589
+ sourceRef: source,
4590
+ event,
4591
+ reason
4592
+ });
4593
+ const override = this.runtime?.deadLetter;
4594
+ if (override) {
4595
+ return override(source, target, event, reason);
4596
+ }
4597
+ {
4598
+ console.warn(`Event "${event.type}" to actor "${target.id}" was not delivered (${reason}).`);
4599
+ }
4600
+ }
4601
+ runStep(actor, key, exec) {
4602
+ const override = this.runtime?.runStep;
4603
+ if (override) {
4604
+ return override(actor, key, exec);
4605
+ }
4606
+ return runStep(actor, key, exec);
4607
+ }
3968
4608
  scheduleTimer(source, id, delay) {
4609
+ const override = this.runtime?.scheduleTimer;
4610
+ if (override) {
4611
+ // The local scheduler records delayed sends for inspection inside
4612
+ // `schedule`; keep that parity for host runtimes.
4613
+ const timer = source.getSnapshot()?.timers?.[id];
4614
+ if (timer) {
4615
+ const target = timer.target === 'self' ? source : timer.target;
4616
+ this._recordSent(source, target, timer.event, delay, id);
4617
+ }
4618
+ return override(source, id, delay);
4619
+ }
3969
4620
  this.schedule(source, id, delay);
3970
4621
  }
3971
4622
  cancelTimer(source, id) {
4623
+ const override = this.runtime?.cancelTimer;
4624
+ if (override) {
4625
+ return override(source, id);
4626
+ }
3972
4627
  this.cancel(source, id);
3973
4628
  }
3974
4629
  cancelAllTimers(source) {
4630
+ const override = this.runtime?.cancelAllTimers;
4631
+ if (override) {
4632
+ return override(source);
4633
+ }
3975
4634
  this.cancelAll(source);
3976
4635
  }
3977
4636
  _relay(source, target, event) {
3978
4637
  if (transitionEffectTargets.length && transitionEffectTargets.includes(target)) {
3979
4638
  throw transitionEffectSignal;
3980
4639
  }
3981
- this.sendEvent(source, target, event);
4640
+ // Returned so callers that can observe a host runtime's asynchronous
4641
+ // delivery do; the built-in runtime delivers synchronously.
4642
+ return this.sendEvent(source, target, event);
3982
4643
  }
3983
4644
  getSnapshot() {
3984
4645
  return {
@@ -4014,7 +4675,12 @@ function createRuntimeSystem(rootActor, options) {
4014
4675
  // oxlint-enable no-unused-vars
4015
4676
 
4016
4677
  let executingCustomAction = false;
4017
- const $$ACTOR_TYPE = 1;
4678
+
4679
+ /**
4680
+ * Marks a serialized object as an actor reference (`xstate$type` in JSON
4681
+ * output and persisted context).
4682
+ */
4683
+ const ACTOR_REF_TYPE = 'actorRef';
4018
4684
  const emptyInspectionRecords = Object.freeze([]);
4019
4685
 
4020
4686
  // those values are currently used by @xstate/react directly so it's important to keep the assigned values in sync
@@ -4065,6 +4731,23 @@ function createActorRef(logic, options) {
4065
4731
  * or read snapshots.
4066
4732
  */
4067
4733
  class Actor {
4734
+ /**
4735
+ * The deterministic logical address of this actor within its system: the
4736
+ * `/`-joined path of actor ids from the root. Stable across persistence and
4737
+ * restore, unlike `sessionId`, which identifies one incarnation.
4738
+ *
4739
+ * `/` separates segments, so an id containing one (a state name with a
4740
+ * slash reaches its invoked child's id) is percent-encoded to keep the
4741
+ * path unambiguous.
4742
+ */
4743
+ get address() {
4744
+ // `id` and `_parent` never change after construction, so the whole chain
4745
+ // memoizes to O(1) amortized.
4746
+ return this._address ??= this._parent ? `${this._parent.address}/${encodeAddressSegment(this.id)}` : encodeAddressSegment(this.id);
4747
+ }
4748
+
4749
+ /** The system to which this actor belongs. */
4750
+
4068
4751
  get trigger() {
4069
4752
  return this._trigger ??= new Proxy({}, {
4070
4753
  get: (_, eventType) => {
@@ -4127,7 +4810,7 @@ class Actor {
4127
4810
  this.registryKey = void 0;
4128
4811
  /** The globally unique process ID for this invocation. */
4129
4812
  this.sessionId = void 0;
4130
- /** The system to which this actor belongs. */
4813
+ this._address = void 0;
4131
4814
  this.system = void 0;
4132
4815
  this._trigger = void 0;
4133
4816
  this._boundSend = void 0;
@@ -4162,7 +4845,10 @@ class Actor {
4162
4845
  this.system.inspect(toObserver(inspect));
4163
4846
  }
4164
4847
  this.sessionId = resolvedOptions._sessionId ?? bookSessionId(this.system);
4165
- this.id = resolveActorId(this.system, id);
4848
+ this.id = resolveActorId(this.system, id, {
4849
+ parent,
4850
+ src: resolvedOptions.src ?? logic
4851
+ });
4166
4852
  this.logger = options?.logger ?? this.system._logger;
4167
4853
  this.clock = options?.clock ?? this.system._clock;
4168
4854
  this._parent = parent;
@@ -4630,8 +5316,20 @@ class Actor {
4630
5316
  }
4631
5317
  if (this._restored) {
4632
5318
  const timers = this._snapshot.timers ?? {};
5319
+ // startedAt is only persisted from — and only meaningful under — the
5320
+ // wall clock; a custom clock (a simulated clock, a monotonic counter)
5321
+ // restores every timer with its declared delay. The clamp bounds
5322
+ // remaining time by the declared delay in case the wall clock moved
5323
+ // backwards between persist and restore.
5324
+ const wallClock = !this.system._clock.now;
5325
+ const now = Date.now();
4633
5326
  for (const timer of Object.values(timers)) {
4634
- this.system.scheduleTimer(this, timer.id, timer.delay);
5327
+ // A timer persisted from a live runtime carries its wall-clock start;
5328
+ // honor the absolute deadline instead of restarting the full delay.
5329
+ // Without a start (a pure-transition snapshot, or an older snapshot)
5330
+ // the declared delay is all there is.
5331
+ const delay = wallClock && timer.startedAt !== undefined ? Math.min(timer.delay, Math.max(0, timer.startedAt + timer.delay - now)) : timer.delay;
5332
+ this.system.scheduleTimer(this, timer.id, delay);
4635
5333
  }
4636
5334
  this._restored = false;
4637
5335
  }
@@ -4791,13 +5489,7 @@ class Actor {
4791
5489
  /** @internal */
4792
5490
  _send(event) {
4793
5491
  if (this._processingStatus === ProcessingStatus.Stopped) {
4794
- // do nothing
4795
- {
4796
- // TODO: circular serialization issues
4797
- // const eventString = ''; //JSON.stringify(event);
4798
-
4799
- console.warn(`Event "${event.type}" was sent to stopped actor "${this.id} (${this.sessionId})". This actor has already reached its final state, and will not transition.`);
4800
- }
5492
+ this.system.deadLetter(this._lastSourceRef, this, event, 'stopped');
4801
5493
  return;
4802
5494
  }
4803
5495
  let mailbox = this.mailbox;
@@ -4822,10 +5514,17 @@ class Actor {
4822
5514
  }
4823
5515
  this.system._relay(undefined, this, event);
4824
5516
  }
5517
+
5518
+ /**
5519
+ * Returns the actor's serializable logical identity: its `id`, `address`,
5520
+ * and registered source key (when the actor was created from one).
5521
+ */
4825
5522
  toJSON() {
4826
5523
  return {
4827
- xstate$$type: $$ACTOR_TYPE,
4828
- id: this.id
5524
+ xstate$type: ACTOR_REF_TYPE,
5525
+ id: this.id,
5526
+ address: this.address,
5527
+ src: typeof this.src === 'string' ? this.src : undefined
4829
5528
  };
4830
5529
  }
4831
5530
 
@@ -4854,10 +5553,16 @@ class Actor {
4854
5553
  * Read an actor’s snapshot synchronously.
4855
5554
  *
4856
5555
  * @remarks
4857
- * The snapshot represent an actor's last emitted value.
5556
+ * The snapshot is the last value the actor published, not a query of its
5557
+ * private state: the actor emits a snapshot when it transitions, and this
5558
+ * returns that cached last emission. The read is coherent within one
5559
+ * synchronous transition turn because event processing runs to completion;
5560
+ * across asynchronous boundaries it may be stale, like any observed value.
4858
5561
  *
4859
- * When an actor receives an event, its internal state may change. An actor
4860
- * may emit a snapshot when a state transition occurs.
5562
+ * Only co-located actors publish full snapshots. A location-transparent
5563
+ * remote handle exposes lifecycle only ({ status, output?, error? }): while
5564
+ * the handle exists among its parent's children the child is presumed
5565
+ * `active`, and its terminal result arrives as a completion event.
4861
5566
  *
4862
5567
  * Note that some actors, such as callback actors generated with
4863
5568
  * `createCallbackLogic`, will not emit snapshots.
@@ -4914,14 +5619,21 @@ function createActor(logic, options) {
4914
5619
  return new Actor(logic, options);
4915
5620
  }
4916
5621
 
4917
- exports.$$ACTOR_TYPE = $$ACTOR_TYPE;
5622
+ exports.ACTOR_REF_TYPE = ACTOR_REF_TYPE;
4918
5623
  exports.Actor = Actor;
4919
5624
  exports.NULL_EVENT = NULL_EVENT;
5625
+ exports.RUNTIME_OPERATIONS = RUNTIME_OPERATIONS;
4920
5626
  exports.STATE_DELIMITER = STATE_DELIMITER;
4921
5627
  exports.XSTATE_INIT = XSTATE_INIT;
5628
+ exports.XSTATE_LOGIC_EFFECT_REJECT = XSTATE_LOGIC_EFFECT_REJECT;
5629
+ exports.XSTATE_LOGIC_EFFECT_RESOLVE = XSTATE_LOGIC_EFFECT_RESOLVE;
5630
+ exports.XSTATE_LOGIC_EFFECT_START = XSTATE_LOGIC_EFFECT_START;
4922
5631
  exports.XSTATE_STOP = XSTATE_STOP;
4923
5632
  exports.XSTATE_TIMER = XSTATE_TIMER;
5633
+ exports.allocateChildId = allocateChildId;
5634
+ exports.assertChildIdFree = assertChildIdFree;
4924
5635
  exports.attachSnapshotActorRef = attachSnapshotActorRef;
5636
+ exports.beginSpawnAllocation = beginSpawnAllocation;
4925
5637
  exports.builtInActions = builtInActions;
4926
5638
  exports.checkStateIn = checkStateIn;
4927
5639
  exports.cloneMachineSnapshot = cloneMachineSnapshot;
@@ -4936,16 +5648,20 @@ exports.createInvokeTimeoutEvent = createInvokeTimeoutEvent;
4936
5648
  exports.createInvokeTimeoutEventId = createInvokeTimeoutEventId;
4937
5649
  exports.createListenerLogic = createListenerLogic;
4938
5650
  exports.createMachineSnapshot = createMachineSnapshot;
5651
+ exports.createRemoteActorRef = createRemoteActorRef;
4939
5652
  exports.createSendToEffect = createSendToEffect;
4940
5653
  exports.createSpawnEffect = createSpawnEffect;
4941
5654
  exports.createSubscriptionLogic = createSubscriptionLogic;
4942
5655
  exports.createTransitionEnqueue = createTransitionEnqueue;
5656
+ exports.deliverEvent = deliverEvent;
4943
5657
  exports.deriveDeferredStarts = deriveDeferredStarts;
5658
+ exports.encodeAddressSegment = encodeAddressSegment;
4944
5659
  exports.evaluateCandidate = evaluateCandidate;
4945
5660
  exports.executeEffects = executeEffects;
4946
5661
  exports.finalizeTransitionResult = finalizeTransitionResult;
4947
5662
  exports.formatRouteTransitions = formatRouteTransitions;
4948
5663
  exports.formatTransition = formatTransition;
5664
+ exports.getActorIdPrefix = getActorIdPrefix;
4949
5665
  exports.getAllOwnEventDescriptors = getAllOwnEventDescriptors;
4950
5666
  exports.getAllOwnEvents = getAllOwnEvents;
4951
5667
  exports.getAllStateNodes = getAllStateNodes;
@@ -4956,6 +5672,8 @@ exports.getInitialSnapshot = getInitialSnapshot;
4956
5672
  exports.getNextSnapshot = getNextSnapshot;
4957
5673
  exports.getPersistedSnapshot = getPersistedSnapshot;
4958
5674
  exports.getProperAncestors = getProperAncestors;
5675
+ exports.getRootActorId = getRootActorId;
5676
+ exports.getSnapshotActorRef = getSnapshotActorRef;
4959
5677
  exports.getStateNodeByPath = getStateNodeByPath;
4960
5678
  exports.getStateNodes = getStateNodes;
4961
5679
  exports.getTransitionResult = getTransitionResult;
@@ -4975,14 +5693,21 @@ exports.matchesActorSession = matchesActorSession;
4975
5693
  exports.matchesEvent = matchesEvent;
4976
5694
  exports.matchesEventDescriptor = matchesEventDescriptor;
4977
5695
  exports.matchesState = matchesState;
5696
+ exports.mergeActorIdCounters = mergeActorIdCounters;
4978
5697
  exports.parseDelayToMilliseconds = parseDelayToMilliseconds;
5698
+ exports.parseGeneratedActorId = parseGeneratedActorId;
4979
5699
  exports.pathToStateValue = pathToStateValue;
5700
+ exports.reserveChildId = reserveChildId;
4980
5701
  exports.resolveActionsWithContext = resolveActionsWithContext;
4981
5702
  exports.resolveReferencedActor = resolveReferencedActor;
4982
5703
  exports.resolveStateValue = resolveStateValue;
5704
+ exports.runStep = runStep;
4983
5705
  exports.setInertActorScopeSnapshot = setInertActorScopeSnapshot;
4984
5706
  exports.setSnapshotActorRef = setSnapshotActorRef;
5707
+ exports.stopActor = stopActor;
4985
5708
  exports.subscriptionLogic = subscriptionLogic;
5709
+ exports.takeSpawnAllocationCounters = takeSpawnAllocationCounters;
5710
+ exports.terminateActor = terminateActor;
4986
5711
  exports.toArray = toArray;
4987
5712
  exports.toObserver = toObserver;
4988
5713
  exports.toStatePath = toStatePath;