@rxova/journey-core 1.0.0-rc.3 → 1.0.1

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 (158) hide show
  1. package/README.md +185 -189
  2. package/dist/connectors/immer/immer.cjs +2 -0
  3. package/dist/connectors/immer/immer.cjs.map +7 -0
  4. package/dist/connectors/immer/immer.d.cts +11 -0
  5. package/dist/connectors/immer/immer.d.ts +11 -0
  6. package/dist/connectors/immer/immer.js +2 -0
  7. package/dist/connectors/immer/immer.js.map +7 -0
  8. package/dist/connectors/immer/immer.types.d.cts +9 -0
  9. package/dist/connectors/immer/immer.types.d.ts +9 -0
  10. package/dist/core/errors.d.cts +57 -0
  11. package/dist/core/errors.d.ts +57 -0
  12. package/dist/core/helpers.d.cts +40 -0
  13. package/dist/core/helpers.d.ts +40 -0
  14. package/dist/core/machine.d.cts +9 -0
  15. package/dist/core/machine.d.ts +9 -0
  16. package/dist/core/runtime.d.cts +115 -0
  17. package/dist/core/runtime.d.ts +115 -0
  18. package/dist/core/runtime.types.d.cts +112 -0
  19. package/dist/core/runtime.types.d.ts +112 -0
  20. package/dist/core/store.d.cts +31 -0
  21. package/dist/core/store.d.ts +31 -0
  22. package/dist/core/types.d.cts +481 -0
  23. package/dist/core/types.d.ts +481 -0
  24. package/dist/diagnostics/diagnostics.helpers.d.cts +17 -0
  25. package/dist/diagnostics/diagnostics.helpers.d.ts +17 -0
  26. package/dist/diagnostics/diagnostics.types.d.cts +22 -0
  27. package/dist/diagnostics/diagnostics.types.d.ts +22 -0
  28. package/dist/graph/bag.types.d.cts +91 -0
  29. package/dist/graph/bag.types.d.ts +91 -0
  30. package/dist/graph/graph.d.cts +63 -0
  31. package/dist/graph/graph.d.ts +63 -0
  32. package/dist/graph/graph.types.d.cts +212 -0
  33. package/dist/graph/graph.types.d.ts +212 -0
  34. package/dist/graph/work.d.cts +81 -0
  35. package/dist/graph/work.d.ts +81 -0
  36. package/dist/index.cjs +1 -1
  37. package/dist/index.cjs.map +4 -4
  38. package/dist/index.d.cts +13 -5
  39. package/dist/index.d.ts +13 -5
  40. package/dist/index.js +1 -1
  41. package/dist/index.js.map +4 -4
  42. package/dist/linear/linear.d.cts +33 -0
  43. package/dist/linear/linear.d.ts +33 -0
  44. package/dist/linear/linear.types.d.cts +28 -0
  45. package/dist/linear/linear.types.d.ts +28 -0
  46. package/dist/plugins/analytics/analytics.d.cts +8 -0
  47. package/dist/plugins/analytics/analytics.d.ts +8 -0
  48. package/dist/plugins/analytics/analytics.helpers.d.cts +2 -0
  49. package/dist/plugins/analytics/analytics.helpers.d.ts +2 -0
  50. package/dist/plugins/analytics/analytics.types.d.cts +27 -0
  51. package/dist/plugins/analytics/analytics.types.d.ts +27 -0
  52. package/dist/plugins/execution-paths/execution-paths.d.cts +7 -4
  53. package/dist/plugins/execution-paths/execution-paths.d.ts +7 -4
  54. package/dist/plugins/execution-paths/execution-paths.types.d.cts +21 -0
  55. package/dist/plugins/execution-paths/execution-paths.types.d.ts +21 -0
  56. package/dist/plugins/index.cjs +2 -0
  57. package/dist/plugins/index.cjs.map +7 -0
  58. package/dist/plugins/index.d.cts +19 -0
  59. package/dist/plugins/index.d.ts +19 -0
  60. package/dist/plugins/index.js +2 -0
  61. package/dist/plugins/index.js.map +7 -0
  62. package/dist/plugins/persistence/persistence.d.cts +16 -0
  63. package/dist/plugins/persistence/persistence.d.ts +16 -0
  64. package/dist/plugins/persistence/persistence.helpers.d.cts +34 -0
  65. package/dist/plugins/persistence/persistence.helpers.d.ts +34 -0
  66. package/dist/plugins/persistence/persistence.types.d.cts +63 -0
  67. package/dist/plugins/persistence/persistence.types.d.ts +63 -0
  68. package/dist/plugins/replay/replay.d.cts +7 -0
  69. package/dist/plugins/replay/replay.d.ts +7 -0
  70. package/dist/plugins/replay/replay.helpers.d.cts +22 -0
  71. package/dist/plugins/replay/replay.helpers.d.ts +22 -0
  72. package/dist/plugins/replay/replay.types.d.cts +28 -0
  73. package/dist/plugins/replay/replay.types.d.ts +28 -0
  74. package/package.json +61 -80
  75. package/dist/journey-builder/index.d.cts +0 -5
  76. package/dist/journey-builder/index.d.ts +0 -5
  77. package/dist/journey-builder/types.d.cts +0 -126
  78. package/dist/journey-builder/types.d.ts +0 -126
  79. package/dist/journey-machine/async-state.d.cts +0 -12
  80. package/dist/journey-machine/async-state.d.ts +0 -12
  81. package/dist/journey-machine/computed.d.cts +0 -2
  82. package/dist/journey-machine/computed.d.ts +0 -2
  83. package/dist/journey-machine/controls.d.cts +0 -17
  84. package/dist/journey-machine/controls.d.ts +0 -17
  85. package/dist/journey-machine/helpers.d.cts +0 -52
  86. package/dist/journey-machine/helpers.d.ts +0 -52
  87. package/dist/journey-machine/index.d.cts +0 -3
  88. package/dist/journey-machine/index.d.ts +0 -3
  89. package/dist/journey-machine/navigation.d.cts +0 -32
  90. package/dist/journey-machine/navigation.d.ts +0 -32
  91. package/dist/journey-machine/plugin-controller.d.cts +0 -15
  92. package/dist/journey-machine/plugin-controller.d.ts +0 -15
  93. package/dist/journey-machine/resolve-journey-definition.d.cts +0 -2
  94. package/dist/journey-machine/resolve-journey-definition.d.ts +0 -2
  95. package/dist/journey-machine/runtime.d.cts +0 -33
  96. package/dist/journey-machine/runtime.d.ts +0 -33
  97. package/dist/journey-machine/send.d.cts +0 -9
  98. package/dist/journey-machine/send.d.ts +0 -9
  99. package/dist/plugins/analytics/index.cjs +0 -2
  100. package/dist/plugins/analytics/index.cjs.map +0 -7
  101. package/dist/plugins/analytics/index.d.cts +0 -25
  102. package/dist/plugins/analytics/index.d.ts +0 -25
  103. package/dist/plugins/analytics/index.js +0 -2
  104. package/dist/plugins/analytics/index.js.map +0 -7
  105. package/dist/plugins/autosave/index.cjs +0 -2
  106. package/dist/plugins/autosave/index.cjs.map +0 -7
  107. package/dist/plugins/autosave/index.d.cts +0 -17
  108. package/dist/plugins/autosave/index.d.ts +0 -17
  109. package/dist/plugins/autosave/index.js +0 -2
  110. package/dist/plugins/autosave/index.js.map +0 -7
  111. package/dist/plugins/diagnostics/diagnostics.d.cts +0 -3
  112. package/dist/plugins/diagnostics/diagnostics.d.ts +0 -3
  113. package/dist/plugins/diagnostics/index.cjs +0 -2
  114. package/dist/plugins/diagnostics/index.cjs.map +0 -7
  115. package/dist/plugins/diagnostics/index.d.cts +0 -15
  116. package/dist/plugins/diagnostics/index.d.ts +0 -15
  117. package/dist/plugins/diagnostics/index.js +0 -2
  118. package/dist/plugins/diagnostics/index.js.map +0 -7
  119. package/dist/plugins/execution-paths/index.cjs +0 -2
  120. package/dist/plugins/execution-paths/index.cjs.map +0 -7
  121. package/dist/plugins/execution-paths/index.d.cts +0 -18
  122. package/dist/plugins/execution-paths/index.d.ts +0 -18
  123. package/dist/plugins/execution-paths/index.js +0 -2
  124. package/dist/plugins/execution-paths/index.js.map +0 -7
  125. package/dist/plugins/persistence/controller.d.cts +0 -17
  126. package/dist/plugins/persistence/controller.d.ts +0 -17
  127. package/dist/plugins/persistence/index.cjs +0 -2
  128. package/dist/plugins/persistence/index.cjs.map +0 -7
  129. package/dist/plugins/persistence/index.d.cts +0 -12
  130. package/dist/plugins/persistence/index.d.ts +0 -12
  131. package/dist/plugins/persistence/index.js +0 -2
  132. package/dist/plugins/persistence/index.js.map +0 -7
  133. package/dist/plugins/replay/index.cjs +0 -2
  134. package/dist/plugins/replay/index.cjs.map +0 -7
  135. package/dist/plugins/replay/index.d.cts +0 -27
  136. package/dist/plugins/replay/index.d.ts +0 -27
  137. package/dist/plugins/replay/index.js +0 -2
  138. package/dist/plugins/replay/index.js.map +0 -7
  139. package/dist/types/analytics.types.d.cts +0 -33
  140. package/dist/types/analytics.types.d.ts +0 -33
  141. package/dist/types/autosave.types.d.cts +0 -23
  142. package/dist/types/autosave.types.d.ts +0 -23
  143. package/dist/types/diagnostics.types.d.cts +0 -38
  144. package/dist/types/diagnostics.types.d.ts +0 -38
  145. package/dist/types/index.d.cts +0 -9
  146. package/dist/types/index.d.ts +0 -9
  147. package/dist/types/journey.types.d.cts +0 -191
  148. package/dist/types/journey.types.d.ts +0 -191
  149. package/dist/types/machine.types.d.cts +0 -104
  150. package/dist/types/machine.types.d.ts +0 -104
  151. package/dist/types/observation.types.d.cts +0 -83
  152. package/dist/types/observation.types.d.ts +0 -83
  153. package/dist/types/persistence.types.d.cts +0 -34
  154. package/dist/types/persistence.types.d.ts +0 -34
  155. package/dist/types/replay.types.d.cts +0 -35
  156. package/dist/types/replay.types.d.ts +0 -35
  157. package/dist/types/transitions.types.d.cts +0 -122
  158. package/dist/types/transitions.types.d.ts +0 -122
@@ -0,0 +1,481 @@
1
+ /**
2
+ * Shared core contracts for `@rxova/journey-core`.
3
+ *
4
+ * The machine object holds only stable methods; every piece of changing state
5
+ * lives in an immutable {@link JourneySnapshot} rebuilt at defined emission
6
+ * points (navigation accepted → commit → settle, plus lifecycle and context
7
+ * changes) and delivered to subscribers.
8
+ */
9
+ import type { JourneyStorage } from "../plugins/persistence/persistence.types.js";
10
+ /** Journey lifecycle status (meta-state-machine, enforced in core). */
11
+ export type JourneyStatus = "idle" | "running" | "paused" | "completed" | "terminated";
12
+ /** An event declaration: discriminated union member `{ type; payload? }`. */
13
+ export type JourneyEventObject = {
14
+ readonly type: string;
15
+ readonly payload?: unknown;
16
+ };
17
+ /** Extracts the payload type of event `TType` from a declared event union. */
18
+ export type JourneyEventPayload<TEvents extends JourneyEventObject, TType extends TEvents["type"]> = Extract<TEvents, {
19
+ type: TType;
20
+ }> extends {
21
+ payload: infer P;
22
+ } ? P : undefined;
23
+ /**
24
+ * Why a navigation was rejected.
25
+ *
26
+ * - `error` — navigation work threw, rejected, timed out, or failed to commit.
27
+ * - `transitioning` — another navigation's hook chain is still pending.
28
+ * - `not-running` — the machine is idle, paused, completed, or terminated.
29
+ * - `invalid-target` — unknown step id, or (graph) no transition targets it.
30
+ * - `no-enabled-transition` — the event matched no enabled candidate.
31
+ * - `out-of-bounds` — timeline move past its edge (back at index 0, forward at tip).
32
+ * - `no-op` — navigation to the current position.
33
+ * - `disposed` — the machine was disposed; all methods are safe no-ops.
34
+ */
35
+ export type NavigationFailureReason = "error" | "transitioning" | "not-running" | "invalid-target" | "no-enabled-transition" | "out-of-bounds" | "no-op" | "disposed";
36
+ /**
37
+ * Result of every navigation verb (and of graph `send`).
38
+ *
39
+ * The failure arm carries `from`/`to` so a caller awaiting the result can log
40
+ * *which* target was rejected without also subscribing to `navigationBlocked`.
41
+ * `to` is null when the machine never resolved one — a graph `send` with no
42
+ * enabled candidate, for instance.
43
+ */
44
+ export type NavigationResult<TStepId extends string = string> = {
45
+ readonly ok: true;
46
+ readonly from: TStepId | null;
47
+ readonly to: TStepId;
48
+ } | {
49
+ readonly ok: false;
50
+ readonly reason: NavigationFailureReason;
51
+ readonly from: TStepId | null;
52
+ readonly to: TStepId | null;
53
+ readonly error?: unknown;
54
+ };
55
+ /** Journey outcome recorded by `complete(payload?)` / `terminate(payload?)`. */
56
+ export type JourneyOutcome<TCompletePayload = unknown, TTerminatePayload = unknown> = {
57
+ readonly type: "completed";
58
+ readonly payload: TCompletePayload | undefined;
59
+ } | {
60
+ readonly type: "terminated";
61
+ readonly payload: TTerminatePayload | undefined;
62
+ };
63
+ /** Machine-level async transition state (a snapshot source of truth). */
64
+ export type TransitionState<TStepId extends string = string> = {
65
+ readonly pending: boolean;
66
+ readonly phase: "working" | "leaving" | "entering" | null;
67
+ readonly from: TStepId | null;
68
+ readonly to: TStepId | null;
69
+ };
70
+ /** Async state of the current navigation work or lifecycle effect chain. */
71
+ export type StepAsyncState = {
72
+ readonly isLoading: boolean;
73
+ readonly isSuccess: boolean;
74
+ readonly isError: boolean;
75
+ readonly error: unknown | null;
76
+ };
77
+ /** Fields present on `currentStep` for every journey kind. */
78
+ export type CurrentStepBase<TStepId extends string, TMeta> = {
79
+ readonly id: TStepId;
80
+ readonly metadata: TMeta;
81
+ /** True while this is the first ever entry of the step in this run. */
82
+ readonly isFirstTimeVisit: boolean;
83
+ readonly async: StepAsyncState;
84
+ };
85
+ /** Browser-like timeline state shared by every journey kind. */
86
+ export type JourneyHistoryState<TStepId extends string> = {
87
+ readonly timeline: readonly TStepId[];
88
+ /** Pointer into `timeline`; -1 while idle. */
89
+ readonly currentIndex: number;
90
+ readonly visited: Readonly<Record<TStepId, boolean>>;
91
+ readonly canGoBack: boolean;
92
+ readonly canGoForward: boolean;
93
+ };
94
+ /**
95
+ * How the run ended, once it has.
96
+ *
97
+ * This used to also carry six derived booleans — `isLoading` and one per
98
+ * status. They were each another spelling of something the snapshot already
99
+ * said: the five status flags were `status === x`, and `isLoading` was
100
+ * `transition.pending` computed a second time. `outcome` is the only value here
101
+ * that is not derivable from another field.
102
+ */
103
+ export type MachineState<TCompletePayload = unknown, TTerminatePayload = unknown> = {
104
+ readonly outcome: JourneyOutcome<TCompletePayload, TTerminatePayload> | null;
105
+ };
106
+ /** Fields shared by every snapshot kind (discriminated on `type`). */
107
+ export type JourneySnapshotBase<TContext, TStepId extends string, TMeta, TCompletePayload = unknown, TTerminatePayload = unknown> = {
108
+ readonly status: JourneyStatus;
109
+ readonly context: TContext;
110
+ readonly transition: TransitionState<TStepId>;
111
+ readonly history: JourneyHistoryState<TStepId>;
112
+ readonly machine: MachineState<TCompletePayload, TTerminatePayload>;
113
+ readonly plugins: Readonly<Record<string, unknown>>;
114
+ readonly currentStep: CurrentStepBase<TStepId, TMeta> | null;
115
+ };
116
+ /** Linear snapshots carry declared-order derivations; graph fields don't exist. */
117
+ export type LinearSnapshot<TContext, TStepId extends string, TMeta, TCompletePayload = unknown, TTerminatePayload = unknown> = Omit<JourneySnapshotBase<TContext, TStepId, TMeta, TCompletePayload, TTerminatePayload>, "currentStep"> & {
118
+ readonly type: "linear";
119
+ readonly currentStep: (CurrentStepBase<TStepId, TMeta> & {
120
+ /** Index in declared order. */
121
+ readonly index: number;
122
+ readonly isFirstStep: boolean;
123
+ readonly isLastStep: boolean;
124
+ }) | null;
125
+ readonly steps: {
126
+ readonly totalSteps: number;
127
+ readonly stepOrder: readonly TStepId[];
128
+ readonly visitedStepCount: number;
129
+ };
130
+ };
131
+ /** Guard evaluation recorded for an outgoing graph transition candidate. */
132
+ export type GraphGuardState = "none" | "passed" | "failed";
133
+ /** Serializable routing state for one candidate declared from the current graph step. */
134
+ export type GraphTransitionSnapshot<TStepId extends string, TEventType extends string> = {
135
+ /** Event that evaluates this candidate. */
136
+ readonly event: TEventType;
137
+ /** Candidate destination. */
138
+ readonly to: TStepId;
139
+ /** Zero-based declaration order among candidates for the same event. */
140
+ readonly priority: number;
141
+ /** Current result of the candidate's guard, or `none` when no guard is declared. */
142
+ readonly guard: GraphGuardState;
143
+ /** Whether the candidate's guard currently permits routing. */
144
+ readonly enabled: boolean;
145
+ /** Whether sending this event would select this candidate under first-enabled semantics. */
146
+ readonly selected: boolean;
147
+ };
148
+ /** Graph snapshots carry declared and enabled transition derivations; order fields don't exist. */
149
+ export type GraphSnapshot<TContext, TStepId extends string, TMeta, TEvents extends JourneyEventObject = JourneyEventObject, TCompletePayload = unknown, TTerminatePayload = unknown> = Omit<JourneySnapshotBase<TContext, TStepId, TMeta, TCompletePayload, TTerminatePayload>, "currentStep"> & {
150
+ readonly type: "graph";
151
+ readonly currentStep: (CurrentStepBase<TStepId, TMeta> & {
152
+ /** No outgoing transitions are defined from this step. */
153
+ readonly isTerminal: boolean;
154
+ }) | null;
155
+ readonly steps: {
156
+ readonly totalSteps: number;
157
+ readonly visitedStepCount: number;
158
+ };
159
+ /** Event names declared by outgoing candidates from the current step. */
160
+ readonly declaredEvents: readonly TEvents["type"][];
161
+ /** Events with at least one enabled candidate from the current step. */
162
+ readonly availableEvents: readonly TEvents["type"][];
163
+ /** Targets of enabled candidates from the current step. */
164
+ readonly availableSteps: readonly TStepId[];
165
+ /** Guard and selection state for every outgoing candidate from the current step. */
166
+ readonly outgoingTransitions: readonly GraphTransitionSnapshot<TStepId, TEvents["type"]>[];
167
+ };
168
+ export type JourneySnapshot<TContext = unknown, TStepId extends string = string, TMeta = unknown, TEvents extends JourneyEventObject = JourneyEventObject, TCompletePayload = unknown, TTerminatePayload = unknown> = LinearSnapshot<TContext, TStepId, TMeta, TCompletePayload, TTerminatePayload> | GraphSnapshot<TContext, TStepId, TMeta, TEvents, TCompletePayload, TTerminatePayload>;
169
+ /** Context updater used by `machine.context.update` and hook args. */
170
+ export type ContextUpdater<TContext> = (previous: TContext) => TContext;
171
+ /**
172
+ * Which edge a hook is running for.
173
+ *
174
+ * `event` and `to` alone do not identify it: several candidates on one event
175
+ * can share a target and differ only by guard. `label` is the edge's declared
176
+ * name; `index` is its position among the candidates the origin step declares
177
+ * for that event, so an anonymous edge is still identifiable.
178
+ */
179
+ export type TransitionInfo = {
180
+ readonly event: string;
181
+ readonly from: string;
182
+ readonly to: string;
183
+ readonly label: string | null;
184
+ readonly index: number;
185
+ };
186
+ /**
187
+ * Arguments passed to step hooks.
188
+ *
189
+ * `event` is the graph event that caused the transition; `null` for timeline
190
+ * moves, linear navigation, and the initial entry on `start()`.
191
+ * Hooks run after navigation commits. Context updates made inside hooks apply
192
+ * immediately and are side effects; hook failures never roll navigation back.
193
+ */
194
+ export type StepHookArgs<TContext, TStepId extends string, TEvents extends JourneyEventObject = never, TSnap = JourneySnapshot<TContext, TStepId>> = {
195
+ readonly snapshot: TSnap;
196
+ readonly from: TStepId | null;
197
+ readonly to: TStepId;
198
+ readonly event: TEvents | null;
199
+ /**
200
+ * The edge that caused this move, or `null` for timeline moves, linear
201
+ * navigation, and the initial entry. Present on every step hook, not just
202
+ * `onTransition`, so `onEnter` can tell which candidate routed into it.
203
+ */
204
+ readonly transition: TransitionInfo | null;
205
+ readonly updateContext: (updater: ContextUpdater<TContext>) => void;
206
+ /**
207
+ * Queues an event for processing after the current transition fully settles
208
+ * (FIFO). Only meaningful on graph journeys; linear hooks receive a no-op.
209
+ */
210
+ readonly raise: (event: TEvents) => void;
211
+ };
212
+ /** `onLeave` is an awaited post-commit side effect and cannot block navigation. */
213
+ export type OnLeaveHook<TContext, TStepId extends string, TEvents extends JourneyEventObject = never, TSnap = JourneySnapshot<TContext, TStepId>> = (args: StepHookArgs<TContext, TStepId, TEvents, TSnap>) => void | Promise<void>;
214
+ /** `onEnter` is an awaited post-commit side effect and cannot block navigation. */
215
+ export type OnEnterHook<TContext, TStepId extends string, TEvents extends JourneyEventObject = never, TSnap = JourneySnapshot<TContext, TStepId>> = (args: StepHookArgs<TContext, TStepId, TEvents, TSnap>) => void | Promise<void>;
216
+ /**
217
+ * Completion and termination payload types supplied to a journey factory.
218
+ *
219
+ * Shared by both tiers: linear takes it as a generic parameter, graph names it
220
+ * through the definition's `$payloads` phantom carrier.
221
+ */
222
+ export type JourneyTerminationPayloads = {
223
+ readonly complete?: unknown;
224
+ readonly terminate?: unknown;
225
+ };
226
+ export type CompletePayloadOf<TTypes extends JourneyTerminationPayloads> = TTypes extends {
227
+ readonly complete: infer TPayload;
228
+ } ? TPayload : unknown;
229
+ export type TerminatePayloadOf<TTypes extends JourneyTerminationPayloads> = TTypes extends {
230
+ readonly terminate: infer TPayload;
231
+ } ? TPayload : unknown;
232
+ /** Subscription event names. */
233
+ export type JourneySubscriptionEvent = "stepEnter" | "stepLeave" | "statusChange" | "contextChange" | "navigationBlocked" | "error";
234
+ export type JourneyEventPayloads<TContext, TStepId extends string, TSnap = JourneySnapshot<TContext, TStepId>> = {
235
+ stepEnter: {
236
+ readonly snapshot: TSnap;
237
+ readonly from: TStepId | null;
238
+ readonly to: TStepId;
239
+ readonly direction: StepEnterDirection;
240
+ };
241
+ stepLeave: {
242
+ readonly snapshot: TSnap;
243
+ readonly from: TStepId;
244
+ readonly to: TStepId;
245
+ };
246
+ statusChange: {
247
+ readonly snapshot: TSnap;
248
+ readonly previous: JourneyStatus;
249
+ readonly current: JourneyStatus;
250
+ };
251
+ contextChange: {
252
+ readonly snapshot: TSnap;
253
+ readonly previous: TContext;
254
+ readonly current: TContext;
255
+ };
256
+ navigationBlocked: {
257
+ readonly snapshot: TSnap;
258
+ readonly reason: NavigationFailureReason;
259
+ readonly from: TStepId | null;
260
+ readonly to: TStepId | null;
261
+ readonly error?: unknown;
262
+ };
263
+ error: {
264
+ readonly snapshot: TSnap;
265
+ readonly error: unknown;
266
+ /** Which pipeline stage threw. */
267
+ readonly phase: "work" | "leave" | "enter" | "transition" | "raise";
268
+ readonly stepId: TStepId | null;
269
+ };
270
+ };
271
+ export type Unsubscribe = () => void;
272
+ export type JourneySubscriptions<TContext, TStepId extends string, TSnap = JourneySnapshot<TContext, TStepId>> = {
273
+ /**
274
+ * Fires on every committed snapshot; read the current one with
275
+ * `getSnapshot()`. Deriving a slice and skipping unchanged values belongs to
276
+ * the caller — React's `useSelector` owns that comparison because it must
277
+ * anyway, to keep render identity stable.
278
+ */
279
+ subscribe(listener: () => void): Unsubscribe;
280
+ subscribeEvent<TEvent extends JourneySubscriptionEvent>(event: TEvent, listener: (payload: JourneyEventPayloads<TContext, TStepId, TSnap>[TEvent]) => void): Unsubscribe;
281
+ };
282
+ /**
283
+ * Lifecycle verbs. Each returns `true` when the lifecycle change applied and
284
+ * `false` when rejected (wrong source status, pending transition, disposed).
285
+ */
286
+ export type JourneyControls<TCompletePayload = unknown, TTerminatePayload = unknown> = {
287
+ /** idle → running; enters the first/initial step. */
288
+ start(): boolean;
289
+ /** running → paused; navigation is rejected while paused. */
290
+ pause(): boolean;
291
+ /** paused → running. */
292
+ resume(): boolean;
293
+ /** running → completed. Explicit only — never fired by navigation. */
294
+ complete(payload?: TCompletePayload): boolean;
295
+ /** any → terminated. Wins over a pending transition. */
296
+ terminate(payload?: TTerminatePayload): boolean;
297
+ /** completed | terminated → running; resets timeline + context to initial. */
298
+ restart(): boolean;
299
+ };
300
+ export type NavigationDirection = "forward" | "backward";
301
+ /**
302
+ * How a step was entered, by intent rather than index math: only
303
+ * `goToNextStep`/`goToPreviousStep` report `"forward"`/`"backward"`; the
304
+ * initial entry, absolute verbs (`goToStepById`, `goToStepByIndex`,
305
+ * `goToLastVisitedStep`), and graph `send` report `"jump"`.
306
+ */
307
+ export type StepEnterDirection = NavigationDirection | "jump";
308
+ /** Read-only arguments for work that must succeed before navigation commits. */
309
+ export type NavigationWorkArgs<TStepId extends string, TSnap> = {
310
+ readonly snapshot: TSnap;
311
+ readonly from: TStepId;
312
+ readonly to: TStepId;
313
+ readonly direction: NavigationDirection;
314
+ };
315
+ /**
316
+ * Transactional work attached to next/previous navigation.
317
+ *
318
+ * `run` is awaited before movement. If it throws, rejects, or times out, the
319
+ * machine stays on the current step. `commit` runs synchronously after `run`
320
+ * succeeds; its context updates are staged and published atomically with the
321
+ * position change.
322
+ */
323
+ export type NavigationWork<TContext, TStepId extends string, TSnap, TResult = void> = {
324
+ readonly run: (args: NavigationWorkArgs<TStepId, TSnap>) => TResult | Promise<TResult>;
325
+ readonly commit?: (args: NavigationWorkArgs<TStepId, TSnap> & {
326
+ readonly result: TResult;
327
+ readonly updateContext: (updater: ContextUpdater<TContext>) => void;
328
+ }) => void;
329
+ /**
330
+ * Budget for this `run`, overriding `defaultTimeoutMs`. Left unset, the
331
+ * global default applies; set it when this one call is slower than the rest.
332
+ */
333
+ readonly timeoutMs?: number;
334
+ };
335
+ export type JourneyNavigation<TContext, TStepId extends string, TSnap> = {
336
+ goToStepById(id: TStepId): Promise<NavigationResult<TStepId>>;
337
+ /** Timeline pointer back `n`; clamps to start; fails only at index 0. */
338
+ goToPreviousStep(n?: number): Promise<NavigationResult<TStepId>>;
339
+ /** Timeline forward; linear falls back to next-in-declared-order at tip. */
340
+ goToNextStep<TResult = void>(work?: NavigationWork<TContext, TStepId, TSnap, TResult>): Promise<NavigationResult<TStepId>>;
341
+ /** Pointer → timeline tip; fails if already there. */
342
+ goToLastVisitedStep(): Promise<NavigationResult<TStepId>>;
343
+ };
344
+ /** Machine surface shared by linear and graph journeys. */
345
+ export type JourneyMachineBase<TContext, TStepId extends string, TSnap = JourneySnapshot<TContext, TStepId>, TCompletePayload = unknown, TTerminatePayload = unknown> = {
346
+ getSnapshot(): TSnap;
347
+ controls: JourneyControls<TCompletePayload, TTerminatePayload>;
348
+ navigate: JourneyNavigation<TContext, TStepId, TSnap>;
349
+ subscriptions: JourneySubscriptions<TContext, TStepId, TSnap>;
350
+ context: {
351
+ update(updater: ContextUpdater<TContext>): void;
352
+ };
353
+ async: {
354
+ clearError(): void;
355
+ };
356
+ /** Irreversible teardown: drops listeners; all methods become safe no-ops. */
357
+ dispose(): void;
358
+ };
359
+ /** A read-only structural view of the running definition, for analysis plugins. */
360
+ export type JourneyStructure = {
361
+ readonly kind: "linear" | "graph";
362
+ readonly stepIds: readonly string[];
363
+ readonly initial: string;
364
+ /** Flattened transitions in declaration order (empty for linear). */
365
+ readonly transitions: readonly {
366
+ readonly event: string;
367
+ readonly from: string;
368
+ readonly to: string;
369
+ readonly guarded: boolean;
370
+ /** The edge's declared name, or `null` when it was left anonymous. */
371
+ readonly label: string | null;
372
+ /** Position among the candidates this step declares for this event. */
373
+ readonly index: number;
374
+ }[];
375
+ };
376
+ /** Observe-only taps + reads — no interception in v1. */
377
+ export type PluginHost<TContext = unknown, TStepId extends string = string> = {
378
+ getSnapshot(): JourneySnapshot<TContext, TStepId>;
379
+ readonly structure: JourneyStructure;
380
+ /** Fires after commit + settle of every successful navigation (incl. initial entry). */
381
+ onTransition(callback: (info: {
382
+ readonly from: TStepId | null;
383
+ readonly to: TStepId;
384
+ readonly snapshot: JourneySnapshot<TContext, TStepId>;
385
+ }) => void): Unsubscribe;
386
+ onNavigationBlocked(callback: (info: JourneyEventPayloads<TContext, TStepId>["navigationBlocked"]) => void): Unsubscribe;
387
+ onStatusChange(callback: (info: JourneyEventPayloads<TContext, TStepId>["statusChange"]) => void): Unsubscribe;
388
+ onContextChange(callback: (info: JourneyEventPayloads<TContext, TStepId>["contextChange"]) => void): Unsubscribe;
389
+ onError(callback: (info: JourneyEventPayloads<TContext, TStepId>["error"]) => void): Unsubscribe;
390
+ onDispose(callback: () => void): void;
391
+ /**
392
+ * Routes a plugin's own failure to the machine's `onListenerError`.
393
+ *
394
+ * A plugin tap that throws synchronously is already isolated and reported.
395
+ * Work that outlives the tap — an awaited storage write, a debounced flush —
396
+ * has no such path, and would otherwise have to choose between an unhandled
397
+ * rejection and a `console.error` that ignores the configured reporter.
398
+ */
399
+ reportError(error: unknown): void;
400
+ };
401
+ /**
402
+ * A journey plugin: observes via {@link PluginHost} taps and extends the
403
+ * machine (`machine.plugins[name]`) and snapshot (`snapshot.plugins[name]`).
404
+ * Extensions are always namespaced, never merged into core fields.
405
+ */
406
+ export type JourneyPlugin<TName extends string = string, TApi = unknown, TSnapExt = unknown> = {
407
+ readonly name: TName;
408
+ setup(host: PluginHost): {
409
+ api?: TApi;
410
+ deriveSnapshot?: (snapshot: JourneySnapshot, previousExtension: TSnapExt | undefined) => TSnapExt;
411
+ };
412
+ };
413
+ /**
414
+ * Variance-friendly plugin bound: `deriveSnapshot`'s `previousExtension`
415
+ * parameter is contravariant, so concrete `JourneyPlugin<N, Api, Ext>` types
416
+ * are not assignable to bare `JourneyPlugin`. Constraints use this instead.
417
+ */
418
+ export type AnyJourneyPlugin = {
419
+ readonly name: string;
420
+ setup(host: PluginHost): {
421
+ api?: unknown;
422
+ deriveSnapshot?: (snapshot: JourneySnapshot, previousExtension: never) => unknown;
423
+ };
424
+ };
425
+ /** Maps a plugin tuple to the `machine.plugins` record. */
426
+ export type PluginApis<TPlugins extends readonly AnyJourneyPlugin[]> = {
427
+ readonly [P in TPlugins[number] as P["name"]]: P extends JourneyPlugin<string, infer TApi, infer TSnapExt> ? TApi : never;
428
+ };
429
+ /** Sugar over the persistence plugin, expanded at creation. */
430
+ export type JourneyPersistOption = {
431
+ readonly key: string;
432
+ /** Defaults to `globalThis.localStorage`; creation throws when neither is available. */
433
+ readonly storage?: JourneyStorage;
434
+ };
435
+ /** Runtime options shared by linear and graph journeys. */
436
+ export type JourneyRuntimeOptions<TPlugins extends readonly AnyJourneyPlugin[] = readonly [], TStepId extends string = string> = {
437
+ /**
438
+ * Defaults to `true`: creating a journey starts it, so the common case needs
439
+ * no second call.
440
+ *
441
+ * The trade this makes, stated plainly because it cannot be worked around:
442
+ * starting happens inside the constructor and the initial entry commits
443
+ * synchronously, so the first `stepEnter` has already fired by the time the
444
+ * factory returns. A subscriber attached afterwards cannot see it.
445
+ *
446
+ * Pass `false` when that first event matters — it is the subscribe-then-start
447
+ * order, and saying so out loud is better than a default that silently
448
+ * assumes it:
449
+ *
450
+ * ```ts
451
+ * const machine = createLinearJourney(definition, { autoStart: false });
452
+ * machine.subscriptions.subscribeEvent("stepEnter", onEnter);
453
+ * machine.controls.start();
454
+ * ```
455
+ */
456
+ autoStart?: boolean;
457
+ /**
458
+ * Start (and restart) directly at this step instead of the first/initial
459
+ * one: only the target's `onEnter` fires, earlier steps are neither entered
460
+ * nor visited, and the timeline begins as `[startAt]`. An unknown id throws
461
+ * at creation.
462
+ */
463
+ startAt?: TStepId;
464
+ /**
465
+ * Expands into the persistence plugin, prepended to `plugins`. Combining it
466
+ * with an explicit persistence plugin fails as a duplicate plugin name.
467
+ */
468
+ persist?: JourneyPersistOption;
469
+ /**
470
+ * Applied to navigation work and every async hook invocation. Work timeouts
471
+ * block movement; post-commit hook timeouts surface as step errors.
472
+ */
473
+ defaultTimeoutMs?: number;
474
+ /**
475
+ * Called when a subscriber (selector or event listener) throws. Listener
476
+ * failures are always isolated — this hook only routes the report. Defaults
477
+ * to a `console.error`; a throwing reporter falls back to that default.
478
+ */
479
+ onListenerError?: (error: unknown) => void;
480
+ plugins?: TPlugins;
481
+ };
@@ -0,0 +1,17 @@
1
+ import type { DiagnosticsResult } from "./diagnostics.types.js";
2
+ /**
3
+ * Structural analysis of a graph definition: unreachable steps, shadowed
4
+ * transitions (an earlier unguarded candidate on the same `(from, event)`
5
+ * always wins), cycles, and terminal-path existence. Completion is explicit in
6
+ * this model, so terminal steps are reported as facts in the summary, not as
7
+ * dead-end issues.
8
+ *
9
+ * A plain function over the definition rather than a plugin over a machine.
10
+ * Nothing here needs a runtime — it is the definition that is being checked —
11
+ * so this runs in a test or a build step, before any machine exists. Linear
12
+ * journeys have no transition graph and so nothing to analyze.
13
+ */
14
+ export declare function analyzeStructure(definition: {
15
+ readonly steps: object;
16
+ readonly initial: string;
17
+ }): DiagnosticsResult;
@@ -0,0 +1,17 @@
1
+ import type { DiagnosticsResult } from "./diagnostics.types.js";
2
+ /**
3
+ * Structural analysis of a graph definition: unreachable steps, shadowed
4
+ * transitions (an earlier unguarded candidate on the same `(from, event)`
5
+ * always wins), cycles, and terminal-path existence. Completion is explicit in
6
+ * this model, so terminal steps are reported as facts in the summary, not as
7
+ * dead-end issues.
8
+ *
9
+ * A plain function over the definition rather than a plugin over a machine.
10
+ * Nothing here needs a runtime — it is the definition that is being checked —
11
+ * so this runs in a test or a build step, before any machine exists. Linear
12
+ * journeys have no transition graph and so nothing to analyze.
13
+ */
14
+ export declare function analyzeStructure(definition: {
15
+ readonly steps: object;
16
+ readonly initial: string;
17
+ }): DiagnosticsResult;
@@ -0,0 +1,22 @@
1
+ export type DiagnosticsIssue = {
2
+ readonly code: "unreachable-step" | "shadowed-transition" | "cycle-detected" | "no-terminal-path";
3
+ readonly severity: "warning" | "error";
4
+ readonly message: string;
5
+ readonly stepId?: string;
6
+ readonly from?: string;
7
+ readonly event?: string;
8
+ readonly steps?: readonly string[];
9
+ };
10
+ export type DiagnosticsResult = {
11
+ readonly issues: readonly DiagnosticsIssue[];
12
+ readonly summary: {
13
+ readonly stepCount: number;
14
+ readonly reachableStepCount: number;
15
+ readonly unreachableStepCount: number;
16
+ /** Steps with no defined outgoing transitions (legal rest points). */
17
+ readonly terminalStepIds: readonly string[];
18
+ readonly cycleCount: number;
19
+ readonly shadowedTransitionCount: number;
20
+ readonly terminalPathExists: boolean;
21
+ };
22
+ };
@@ -0,0 +1,22 @@
1
+ export type DiagnosticsIssue = {
2
+ readonly code: "unreachable-step" | "shadowed-transition" | "cycle-detected" | "no-terminal-path";
3
+ readonly severity: "warning" | "error";
4
+ readonly message: string;
5
+ readonly stepId?: string;
6
+ readonly from?: string;
7
+ readonly event?: string;
8
+ readonly steps?: readonly string[];
9
+ };
10
+ export type DiagnosticsResult = {
11
+ readonly issues: readonly DiagnosticsIssue[];
12
+ readonly summary: {
13
+ readonly stepCount: number;
14
+ readonly reachableStepCount: number;
15
+ readonly unreachableStepCount: number;
16
+ /** Steps with no defined outgoing transitions (legal rest points). */
17
+ readonly terminalStepIds: readonly string[];
18
+ readonly cycleCount: number;
19
+ readonly shadowedTransitionCount: number;
20
+ readonly terminalPathExists: boolean;
21
+ };
22
+ };
@@ -0,0 +1,91 @@
1
+ import type { GraphOnEntry, GraphStepConfig } from "./graph.types.js";
2
+ import type { JourneyEventObject } from "../core/types.js";
3
+ /**
4
+ * One declaration point for a journey's types.
5
+ *
6
+ * Two things need it. Steps authored in separate files have to name the shape
7
+ * they satisfy, and `withTypes` needs somewhere to pin types that the
8
+ * definition cannot infer on its own. Everything except `context`, `stepId`
9
+ * and `events` is optional and falls back.
10
+ */
11
+ export type Bag = {
12
+ context: unknown;
13
+ stepId: string;
14
+ events: JourneyEventObject;
15
+ meta?: unknown;
16
+ handlers?: unknown;
17
+ /**
18
+ * Result type per event name, for events declaring `run`.
19
+ *
20
+ * Load-bearing rather than a convenience: a declared `run` sits at a property
21
+ * position, which is not an inference site, so `commit`'s `result` is
22
+ * `unknown` unless it is pinned here. One result type per event name — not
23
+ * per (step, event) pair.
24
+ */
25
+ results?: Record<string, unknown>;
26
+ };
27
+ /**
28
+ * `meta`, `handlers` and `results` are optional on the bag, so these must infer
29
+ * from an optional property. Matching `{ meta: infer T }` — a *required*
30
+ * property — silently skipped anyone who mirrored the constraint and wrote
31
+ * `meta?: MyMeta`: they got the fallback instead of their own type, and the
32
+ * eventual error pointed nowhere near the bag declaration. `NonNullable` strips
33
+ * the `undefined` that optionality adds.
34
+ */
35
+ export type MetaOf<TBag extends Bag> = TBag extends {
36
+ meta?: infer TMeta;
37
+ } ? [TMeta] extends [undefined] ? Record<string, unknown> : NonNullable<TMeta> : Record<string, unknown>;
38
+ export type HandlersOf<TBag extends Bag> = TBag extends {
39
+ handlers?: infer THandlers;
40
+ } ? [THandlers] extends [undefined] ? Record<string, never> : NonNullable<THandlers> : Record<string, never>;
41
+ export type ResultsOf<TBag extends Bag> = TBag extends {
42
+ results?: infer TResults;
43
+ } ? [TResults] extends [undefined] ? Record<string, never> : NonNullable<TResults> : Record<string, never>;
44
+ /**
45
+ * The declared result for one event, or `unknown` when the bag pins none.
46
+ *
47
+ * The empty-default check comes first and is load-bearing: `ResultsOf` falls
48
+ * back to `Record<string, never>`, whose `keyof` is `string`, so without it
49
+ * every event name "matches" and resolves to `never` — which rejects every
50
+ * `run` rather than accepting any.
51
+ */
52
+ export type ResultOf<TBag extends Bag, TType extends string> = [ResultsOf<TBag>] extends [
53
+ Record<string, never>
54
+ ] ? unknown : TType extends keyof ResultsOf<TBag> ? ResultsOf<TBag>[TType] : unknown;
55
+ /**
56
+ * One step's config for a pinned bag — what a step authored in its own file
57
+ * annotates itself with.
58
+ *
59
+ * ```ts
60
+ * export const loginStep: GraphStep<AuthBag> = {
61
+ * metadata: { label: "Login" },
62
+ * on: { submit: "twofa" }
63
+ * };
64
+ * ```
65
+ */
66
+ export type GraphStep<TBag extends Bag> = Omit<GraphStepConfig<TBag["context"], TBag["stepId"], TBag["events"], MetaOf<TBag>, HandlersOf<TBag>>, "on"> & {
67
+ readonly on?: {
68
+ readonly [TType in TBag["events"]["type"]]?: GraphOnEntry<TBag["context"], TBag["stepId"], TBag["events"], HandlersOf<TBag>, MetaOf<TBag>, ResultOf<TBag, TType>, Extract<TBag["events"], {
69
+ type: TType;
70
+ }>>;
71
+ };
72
+ };
73
+ /**
74
+ * A whole graph definition for a pinned bag, for the case `withTypes` cannot
75
+ * express: a standalone definition passed to the factory more than once, with
76
+ * different options each time.
77
+ *
78
+ * ```ts
79
+ * export const definition = {
80
+ * initial: "login",
81
+ * context: initialContext,
82
+ * steps: { login: { on: { submit: "twofa" } }, twofa: {} }
83
+ * } satisfies GraphDefinition<AuthBag>;
84
+ * ```
85
+ */
86
+ export type GraphDefinition<TBag extends Bag> = {
87
+ readonly initial: TBag["stepId"];
88
+ readonly context: TBag["context"];
89
+ readonly handlers?: HandlersOf<TBag>;
90
+ readonly steps: Readonly<Record<TBag["stepId"], GraphStep<TBag>>>;
91
+ };