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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +182 -186
  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 +46 -0
  13. package/dist/core/helpers.d.ts +46 -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 -79
  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
package/README.md CHANGED
@@ -1,282 +1,278 @@
1
1
  # @rxova/journey-core
2
2
 
3
- Typed state machine for multi-step UI flows.
3
+ Typed, framework-independent state machines for multi-step product flows.
4
4
 
5
5
  <p>
6
6
  <a href="https://www.npmjs.com/package/@rxova/journey-core">
7
7
  <img src="https://img.shields.io/npm/v/@rxova/journey-core?color=0f8f6a" alt="npm" />
8
8
  </a>
9
- <img src="https://img.shields.io/badge/7.58%20kB-brotli-0f8f6a" alt="size" />
10
- <img src="https://img.shields.io/badge/zero%20dependencies-black" alt="zero deps" />
9
+ <img src="https://img.shields.io/badge/zero%20dependencies-black" alt="zero dependencies" />
11
10
  <img src="https://img.shields.io/badge/TypeScript-strict-3178C6?logo=typescript&logoColor=white" alt="TypeScript" />
12
11
  </p>
13
12
 
14
- `@rxova/journey-core` is approaching a `1.0.0-rc` contract freeze. The current public model is:
15
-
16
- - JSON-only runtime `context`
17
- - static step `meta`
18
- - transition-side state updates through `updateContext(...)`
19
- - documented lifecycle callbacks and events as the supported extension surface
13
+ Journey owns current position, context, realized history, lifecycle status, terminal outcomes, and
14
+ observable async transition state. Linear and graph definitions run on the same small runtime.
20
15
 
21
16
  ## Install
22
17
 
23
18
  ```bash
24
- npm i @rxova/journey-core
19
+ npm install @rxova/journey-core
25
20
  ```
26
21
 
27
- ## Quickstart
22
+ ## Linear quickstart
28
23
 
29
24
  ```ts
30
- import { createJourneyMachine } from "@rxova/journey-core";
31
-
32
- const machine = createJourneyMachine({
33
- initial: "account",
34
- context: { name: "" },
35
- steps: { account: {}, details: {}, review: {} },
36
- transitions: ["account", "details", "review"]
25
+ import { createLinearJourney } from "@rxova/journey-core";
26
+
27
+ type CheckoutStepId = "account" | "shipping" | "review";
28
+ type CheckoutContext = {
29
+ email: string;
30
+ shippingId: string | null;
31
+ };
32
+ type CheckoutTerminationPayloads = {
33
+ complete: { orderId: string };
34
+ terminate: { reason: "cancelled" | "expired" };
35
+ };
36
+
37
+ const checkout = createLinearJourney<CheckoutStepId, CheckoutContext, CheckoutTerminationPayloads>({
38
+ steps: [
39
+ { id: "account", metadata: { title: "Account" } },
40
+ { id: "shipping", metadata: { title: "Shipping" } },
41
+ { id: "review", metadata: { title: "Review" } }
42
+ ],
43
+ context: {
44
+ email: "",
45
+ shippingId: null
46
+ }
37
47
  });
38
48
 
39
- machine.startJourney();
40
-
41
- await machine.send({ type: "goToNextStep" }); // account → details
42
- await machine.send({ type: "goToNextStep" }); // details → review
43
- await machine.goToPreviousStep(); // review → details
49
+ checkout.context.update((context) => ({
50
+ ...context,
51
+ email: "ada@example.com"
52
+ }));
44
53
 
45
- const snap = machine.getSnapshot();
46
- console.log(snap.currentStepId); // "details"
47
- console.log(snap.history.timeline); // ["account", "details"]
48
- console.log(snap.status); // "running"
54
+ await checkout.navigate.goToNextStep();
49
55
  ```
50
56
 
51
- ## Three Transition Modes
52
-
53
- Reserved step ids: `*`, `global`, `COMPLETE`, and `TERMINATED`. They are used by the runtime and cannot be used as real step names.
54
-
55
- ### Linear
56
-
57
- Array shorthand for sequential flows. Steps can carry synchronous `updateContext` transitions and timeouts.
57
+ The first declared step is initial. String steps are shorthand when metadata and hooks are not
58
+ needed:
58
59
 
59
60
  ```ts
60
- transitions: [
61
- "account",
62
- {
63
- step: "details",
64
- updateContext: ({ context }) => ({ ...context, step: 2 }),
65
- timeoutMs: 5000
66
- },
67
- "payment",
68
- "review"
69
- ];
61
+ createLinearJourney({
62
+ steps: ["account", "shipping", "review"],
63
+ context: {}
64
+ });
70
65
  ```
71
66
 
72
- ### Graph
67
+ The optional third generic groups payloads passed to `controls.complete` and
68
+ `controls.terminate`. It also types the corresponding `snapshot.machine.outcome`. Omit it when
69
+ terminal payload typing is not needed.
73
70
 
74
- Step-keyed transitions with guards, `updateContext`, and branching.
71
+ ## Transactional navigation
75
72
 
76
- ```ts
77
- transitions: {
78
- login: {
79
- goToNextStep: [
80
- { to: "admin", when: ({ context }) => context.role === "admin" },
81
- { to: "dashboard" }
82
- ]
83
- },
84
- admin: { completeJourney: true },
85
- dashboard: { completeJourney: true }
86
- }
87
- ```
88
-
89
- #### Graph Builder
90
-
91
- For larger flows, `createJourneyBuilder` lets each step declare its own transitions co-located with its component. It compiles to the same `JourneyDefinition` — no new runtime concepts.
73
+ Pass work to next or previous navigation when an operation must succeed before movement:
92
74
 
93
75
  ```ts
94
- // builder.ts — typed singleton
95
- const { createStep, to, build } = createJourneyBuilder<Context, StepId, EventMap>();
96
-
97
- // steps/login.step.ts — co-located with Login.tsx
98
- export const loginStep = createStep("login", {
99
- on: {
100
- submit: [to("admin").when(({ context }) => context.role === "admin"), to("dashboard")]
76
+ const result = await checkout.navigate.goToNextStep({
77
+ run: async ({ snapshot }) => submitShipping(snapshot.context),
78
+ commit: ({ result: shipping, updateContext }) => {
79
+ updateContext((context) => ({
80
+ ...context,
81
+ shippingId: shipping.id
82
+ }));
101
83
  }
102
84
  });
103
85
 
104
- // journey.ts — one-screen assembly
105
- const definition = build({
106
- initial: "login",
107
- context: { role: "user" },
108
- steps: [loginStep, adminStep, dashboardStep]
109
- });
86
+ if (!result.ok) {
87
+ console.error(result.reason, result.error);
88
+ }
110
89
  ```
111
90
 
112
- Use the **factory form** when you need `event.payload` narrowed to the specific event type:
113
-
114
- ```ts
115
- submit: ({ to }) => [to("admin").when(({ context, event }) => event.payload?.username !== "")];
116
- ```
91
+ `run` is awaited while the source remains current. If it throws, rejects, or times out, neither
92
+ position nor context changes. `commit` is synchronous; its context updates publish atomically with
93
+ the destination.
117
94
 
118
- Each transition modifier is single-use at the type level. Calling `.when()`, `.updateContext()`,
119
- `.onEnter()`, `.onLeave()`, `.label()`, or `.timeoutMs()` twice on the same builder is a TypeScript error. If you bypass the type system, runtime keeps the existing last-call-wins behavior.
95
+ `goToPreviousStep()` accepts the same work contract.
120
96
 
121
- ### Headless
97
+ ## Async state
122
98
 
123
- Omit `transitions` entirely. The machine holds state, history, and context, but the caller decides where to go. Useful for custom renderers, server-driven flows, or when the navigation logic lives outside the definition.
99
+ Use `snapshot.transition.pending` as the normal UI-level loading flag:
124
100
 
125
101
  ```ts
126
- const machine = createJourneyMachine({
127
- initial: "start",
128
- context: {},
129
- steps: { start: {}, configure: {}, confirm: {} }
130
- // no transitions — the caller drives the flow
131
- });
132
-
133
- machine.startJourney();
134
- await machine.goToStepById("configure");
135
- await machine.goToStepById("confirm");
136
- await machine.goToPreviousStep(); // back to configure
137
- await machine.completeJourney();
102
+ const snapshot = checkout.getSnapshot();
103
+ continueButton.disabled = snapshot.transition.pending;
138
104
  ```
139
105
 
140
- In headless mode, `goToStepById(...)` is the navigation primitive. `goToNextStep()` and custom events are explicit no-ops until you define transitions and move into linear or graph mode.
141
-
142
- After `dispose()`, send-style APIs resolve with `transitioned: false` and `error: JourneyDisposedError`. Control APIs such as `startJourney()` and `updateContext()` stay no-op and emit a development warning.
106
+ For diagnostics and richer feedback, `snapshot.transition` identifies the `working`, `leaving`, or
107
+ `entering` phase, source, and destination. `snapshot.currentStep.async` records loading, success,
108
+ and error state for the current entry.
143
109
 
144
- `updateContext()` is the ordered context-write API. It runs through the same queue as `send()`, so it applies against the latest committed snapshot when it executes.
110
+ Only one navigation settles at a time. Concurrent calls resolve with
111
+ `{ ok: false, reason: "transitioning" }`.
145
112
 
146
- ## Step Lifecycle
113
+ ## Lifecycle effects
147
114
 
148
- Run side effects when a step is entered or left by attaching `onEnter` / `onLeave` directly to the step definition. Both callbacks receive the current `context` and are observational. They do not change transition selection or mutate context. Use transition `updateContext` for synchronous state commits that belong to the transition itself.
115
+ `onLeave` and `onEnter` are awaited post-commit effects. They cannot cancel or roll back movement:
149
116
 
150
117
  ```ts
151
- const machine = createJourneyMachine({
152
- context: { username: "" },
153
- steps: {
154
- login: {
155
- onLeave: ({ context }) => analytics.track("login_left", { user: context.username })
118
+ const machine = createLinearJourney({
119
+ context: { receiptId: null as string | null },
120
+ steps: [
121
+ {
122
+ id: "payment",
123
+ onLeave: ({ snapshot }) => analytics.track("payment_left", snapshot.context)
156
124
  },
157
- dashboard: {
158
- onEnter: ({ context }) => analytics.track("dashboard_entered"),
159
- onLeave: ({ context }) => console.log("leaving dashboard")
125
+ {
126
+ id: "receipt",
127
+ onEnter: async ({ snapshot, updateContext }) => {
128
+ const receipt = await loadReceipt(snapshot.context);
129
+ updateContext((context) => ({ ...context, receiptId: receipt.id }));
130
+ }
160
131
  }
161
- },
162
- transitions: ["login", "dashboard"]
132
+ ]
163
133
  });
164
134
  ```
165
135
 
166
- With the graph builder, callbacks sit alongside transitions on the step:
136
+ Use navigation work for validation or submission that must prevent movement. Use hooks for cleanup,
137
+ analytics, and destination setup after the move has committed.
138
+
139
+ ## Navigation and history
167
140
 
168
141
  ```ts
169
- const dashboardStep = createStep("dashboard", {
170
- onEnter: ({ context }) => analytics.track("dashboard_entered"),
171
- onLeave: ({ context }) => console.log("leaving dashboard"),
172
- on: { submit: [to("review")] }
173
- });
142
+ await checkout.navigate.goToNextStep();
143
+ await checkout.navigate.goToPreviousStep();
144
+ await checkout.navigate.goToPreviousStep(2);
145
+ await checkout.navigate.goToLastVisitedStep();
174
146
  ```
175
147
 
176
- In React, use `useJourneyStepLifecycle` when the callback needs access to component state or React context:
148
+ Linear history behaves like a browser timeline. Going back preserves the future; appending a new
149
+ destination from an older position replaces the abandoned future.
177
150
 
178
- ```tsx
179
- const { useJourneyStepLifecycle } = createJourney(definition);
151
+ `goToStepById(id)` is an intentional escape hatch for occasional direct jumps in an otherwise
152
+ ordered flow:
180
153
 
181
- function Dashboard() {
182
- useJourneyStepLifecycle("dashboard", {
183
- onEnter: ({ context }) => analytics.track("dashboard_entered"),
184
- onLeave: ({ context }) => console.log("leaving dashboard")
185
- });
186
- // ...
187
- }
154
+ ```ts
155
+ await checkout.navigate.goToStepById("review");
188
156
  ```
189
157
 
190
- The hook always calls the latest version of your callbacks without re-subscribing.
191
-
192
- ## Features
158
+ It is ungated and can reach any declared linear step. When named jumps, branching, or guarded
159
+ destinations become normal flow behavior, move the definition to graph mode.
193
160
 
194
- - **Async guards** — `when` can be async, with per-transition `timeoutMs` and `AbortSignal`-based cancellation. Step async state is tracked in `snapshot.async.byStep[stepId]` (`idle`, `evaluating-when`, `error`)
195
- - **Timeline history** — `goToPreviousStep()`, `goToLastVisitedStep()`, deterministic back/forward. Snapshot exposes `history.timeline` and `history.index` so you always know where the user has been
196
- - **Computed state** — `getComputed()` returns derived flags: `isFirstStep`, `isLastStep`, `isLoading`, `isComplete`, `isTerminated`, `activeStepIndex`, `stepCount`, `mode` (`linear` | `graph` | `headless`)
197
- - **Observability** — `subscribeEvent()` streams typed lifecycle events: `journey.start`, `transition.start`, `transition.success`, `step.enter`, `step.exit`, `journey.completed`, `journey.terminated`, and more
198
- - **Step metadata** — attach typed metadata per step and read it with `getStepMeta(stepId)`. Metadata is definition data, not mutable runtime state
199
- - **Terminal events** — `completeJourney()` and `terminateJourney()` with typed payloads. Once terminal, all navigation no-ops until `resetJourney()`
200
- - **Snapshot** — one object holds everything that changes at runtime: `currentStepId`, `context`, `history`, `visited`, `status` (`idled`, `running`, `completed`, `terminated`), and `async`
201
- - **Global transitions** — cross-cutting handlers via the `global` key, useful for close-confirmation or abort flows
161
+ ## Completion and termination
202
162
 
203
- ## Persistence
163
+ Reaching the last linear step does not complete the machine. A final screen is a position;
164
+ completion is an explicit product outcome:
204
165
 
205
166
  ```ts
206
- import { createPersistencePlugin } from "@rxova/journey-core/persistence";
167
+ checkout.controls.complete({ orderId: "order-42" });
207
168
 
208
- const machine = createJourneyMachine(definition, {
209
- plugins: [
210
- createPersistencePlugin({
211
- key: "checkout",
212
- version: 2,
213
- blockList: ["payment.cardNumber"]
214
- })
215
- ]
216
- });
169
+ checkout.getSnapshot().machine.outcome;
170
+ // { type: "completed", payload: { orderId: "order-42" } }
217
171
  ```
218
172
 
219
- Versioned snapshot storage with migrations, context allow/block lists, and configurable reset behavior. Defaults to `localStorage` when available.
173
+ Use `controls.terminate(payload?)` for an unsuccessful or cancelled terminal outcome. Navigation is
174
+ rejected after either terminal state until `controls.restart()` begins a fresh run.
220
175
 
221
- If `migrate(...)` returns data that no longer matches a valid snapshot, Journey reports the error through `onError` (or a development warning when `onError` is omitted) and falls back to the initial snapshot.
176
+ ## Snapshot and subscriptions
222
177
 
223
- ## Autosave
178
+ All changing state lives in one immutable snapshot:
224
179
 
225
180
  ```ts
226
- import { createAutosavePlugin } from "@rxova/journey-core/autosave";
181
+ const snapshot = checkout.getSnapshot();
182
+
183
+ snapshot.status; // "idle" | "running" | "paused" | "completed" | "terminated"
184
+ snapshot.context;
185
+ snapshot.currentStep?.id;
186
+ snapshot.currentStep?.index;
187
+ snapshot.history.timeline;
188
+ snapshot.history.canGoBack;
189
+ snapshot.transition.pending;
190
+ snapshot.machine.outcome;
191
+ ```
227
192
 
228
- const machine = createJourneyMachine(definition, {
229
- plugins: [
230
- createAutosavePlugin({
231
- key: "checkout-draft",
232
- debounceMs: 300,
233
- allowList: ["profile", "shipping"]
234
- })
235
- ]
193
+ Subscribe to every committed snapshot, or to a named lifecycle event:
194
+
195
+ ```ts
196
+ const stop = checkout.subscriptions.subscribe(() => render(checkout.getSnapshot().currentStep?.id));
197
+
198
+ checkout.subscriptions.subscribeEvent("navigationBlocked", ({ reason, error }) => {
199
+ reportNavigationFailure(reason, error);
236
200
  });
237
201
 
238
- await machine.flushAutosave();
202
+ stop();
203
+ ```
204
+
205
+ ## Graph mode
206
+
207
+ Use `createGraphJourney` when named events, guards, or branches choose destinations. Graph machines
208
+ add typed `send(type, payload?)`, available events and targets in the snapshot, and transition-level
209
+ post-commit effects.
210
+
211
+ Candidates take an optional `label`, which names the edge in timeout and error messages, in the
212
+ `transition` argument every step hook receives, and in the structure view plugins read — several
213
+ candidates on one event differ only by guard, so `event` and `to` do not tell them apart. They also
214
+ take an optional `timeoutMs`, as does a declared work entry, overriding `defaultTimeoutMs` for that
215
+ one edge so a single slow third-party call does not set the budget for the whole graph.
216
+
217
+ For larger graphs, `withGraphTypes<Bag>()` pins the types once and `GraphStep<Bag>` lets each step live in its own file, and
218
+ `defineWork<Bag, "EVENT">()` reads a work entry's result type off its `run` instead of restating it
219
+ in the bag. See the [Graph guide](https://rxova.org/docs/core/usage/graph).
220
+
221
+ ## Connectors
222
+
223
+ Connectors adapt optional third-party libraries to existing Core primitives without attaching a
224
+ plugin to the machine. The Immer connector turns a synchronous producer into a context updater:
225
+
226
+ ```bash
227
+ pnpm add immer
228
+ ```
229
+
230
+ ```ts
231
+ import { immerConnector } from "@rxova/journey-core/connectors/immer";
232
+
233
+ machine.context.update(
234
+ immerConnector<CheckoutContext>((draft) => {
235
+ draft.cart.items.push(item);
236
+ draft.cart.total += item.price;
237
+ })
238
+ );
239
239
  ```
240
240
 
241
- Autosave adds debounced draft persistence, hydration, `getAutosaveState()`, `flushAutosave()`, and `clearAutosave()` without requiring the persistence plugin as a separate public dependency.
241
+ It also works with the `updateContext` passed to hooks and transactional commits. Immer remains an
242
+ optional peer, so consumers that do not import this connector do not install or bundle it. See the
243
+ [Immer connector guide](https://rxova.org/docs/core/connectors/immer) for replacement recipes,
244
+ freezing, draftability, and transactional behavior.
245
+
246
+ ## Plugins
242
247
 
243
- ## Analytics
248
+ Built-in plugins are separately imported and observe the machine through a read-only host:
244
249
 
245
250
  ```ts
246
- import { createAnalyticsPlugin } from "@rxova/journey-core/analytics";
251
+ import { createLinearJourney } from "@rxova/journey-core";
252
+ import { createAnalyticsPlugin } from "@rxova/journey-core/plugins";
247
253
 
248
- const machine = createJourneyMachine(definition, {
254
+ const machine = createLinearJourney(definition, {
249
255
  plugins: [
250
256
  createAnalyticsPlugin({
251
- machineId: "checkout",
252
257
  track: (event) => analytics.track(event.name, event.payload)
253
258
  })
254
259
  ]
255
260
  });
256
261
  ```
257
262
 
258
- Analytics normalizes Journey lifecycle events into a stable event envelope and adds `trackAnalyticsEvent(...)` for custom markers.
259
-
260
- ## DevTools
261
-
262
- ```ts
263
- import { attachJourneyDevtools } from "@rxova/journey-devtools-bridge";
264
-
265
- const detach = attachJourneyDevtools(machine, { label: "Checkout" });
266
- ```
263
+ Persistence, analytics, replay, and execution paths each have dedicated export paths and
264
+ documentation. `analyzeStructure(definition)` checks a graph for unreachable steps, shadowed
265
+ transitions and cycles without creating a machine.
267
266
 
268
267
  ## Documentation
269
268
 
270
- - [Pre-1.0 Migration](https://rxova.org/docs/core/pre-1-0-migration)
271
- - [Stability Contract](https://rxova.org/docs/core/stability)
272
269
  - [Quickstart](https://rxova.org/docs/core/getting-started)
273
- - [Usage Modes](https://rxova.org/docs/core/usage)
274
- - [Async Lifecycle](https://rxova.org/docs/core/async)
275
- - [Persistence](https://rxova.org/docs/core/persistence)
276
- - [Autosave](https://rxova.org/docs/core/autosave)
277
- - [Analytics Plugin](https://rxova.org/docs/core/plugins/analytics-plugin)
278
- - [Plugins](https://rxova.org/docs/core/plugins/overview)
279
- - [API Reference](https://rxova.org/docs/core/api/overview)
270
+ - [Linear journeys](https://rxova.org/docs/core/usage/linear)
271
+ - [Async behavior](https://rxova.org/docs/core/async)
272
+ - [Machine API](https://rxova.org/docs/core/api/machine-api)
273
+ - [Snapshot](https://rxova.org/docs/core/snapshot)
274
+ - [Pre-1.0 migration](https://rxova.org/docs/core/pre-1-0-migration)
275
+ - [Stability contract](https://rxova.org/docs/core/stability)
280
276
 
281
277
  ## License
282
278
 
@@ -0,0 +1,2 @@
1
+ "use strict";var m=Object.defineProperty;var i=Object.getOwnPropertyDescriptor;var x=Object.getOwnPropertyNames;var C=Object.prototype.hasOwnProperty;var c=(t,e)=>{for(var r in e)m(t,r,{get:e[r],enumerable:!0})},y=(t,e,r,p)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of x(e))!C.call(t,o)&&o!==r&&m(t,o,{get:()=>e[o],enumerable:!(p=i(e,o))||p.enumerable});return t};var f=t=>y(m({},"__esModule",{value:!0}),t);var s={};c(s,{immerConnector:()=>d});module.exports=f(s);var n=require("immer");function d(t){return e=>(0,n.produce)(e,t)}0&&(module.exports={immerConnector});
2
+ //# sourceMappingURL=immer.cjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/connectors/immer/immer.ts"],
4
+ "sourcesContent": ["import { produce } from \"immer\";\nimport type { ContextUpdater } from \"../../core/types\";\nimport type { ImmerContextRecipe } from \"./immer.types\";\n\nexport type { ImmerContextRecipe } from \"./immer.types\";\n\n/**\n * Converts a synchronous Immer recipe into a Journey context updater.\n *\n * Pass the result to `machine.context.update`, a hook's `updateContext`, or a\n * transactional work commit. Immer supplies structural sharing and applies its\n * normal draftability and auto-freezing behavior.\n */\nexport function immerConnector<TContext>(\n recipe: ImmerContextRecipe<TContext>\n): ContextUpdater<TContext> {\n return (previous) => produce(previous, recipe);\n}\n"],
5
+ "mappings": "yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,oBAAAE,IAAA,eAAAC,EAAAH,GAAA,IAAAI,EAAwB,iBAajB,SAASF,EACdG,EAC0B,CAC1B,OAAQC,MAAa,WAAQA,EAAUD,CAAM,CAC/C",
6
+ "names": ["immer_exports", "__export", "immerConnector", "__toCommonJS", "import_immer", "recipe", "previous"]
7
+ }
@@ -0,0 +1,11 @@
1
+ import type { ContextUpdater } from "../../core/types.js";
2
+ import type { ImmerContextRecipe } from "./immer.types.js";
3
+ export type { ImmerContextRecipe } from "./immer.types.js";
4
+ /**
5
+ * Converts a synchronous Immer recipe into a Journey context updater.
6
+ *
7
+ * Pass the result to `machine.context.update`, a hook's `updateContext`, or a
8
+ * transactional work commit. Immer supplies structural sharing and applies its
9
+ * normal draftability and auto-freezing behavior.
10
+ */
11
+ export declare function immerConnector<TContext>(recipe: ImmerContextRecipe<TContext>): ContextUpdater<TContext>;
@@ -0,0 +1,11 @@
1
+ import type { ContextUpdater } from "../../core/types.js";
2
+ import type { ImmerContextRecipe } from "./immer.types.js";
3
+ export type { ImmerContextRecipe } from "./immer.types.js";
4
+ /**
5
+ * Converts a synchronous Immer recipe into a Journey context updater.
6
+ *
7
+ * Pass the result to `machine.context.update`, a hook's `updateContext`, or a
8
+ * transactional work commit. Immer supplies structural sharing and applies its
9
+ * normal draftability and auto-freezing behavior.
10
+ */
11
+ export declare function immerConnector<TContext>(recipe: ImmerContextRecipe<TContext>): ContextUpdater<TContext>;
@@ -0,0 +1,2 @@
1
+ import{produce as o}from"immer";function m(e){return t=>o(t,e)}export{m as immerConnector};
2
+ //# sourceMappingURL=immer.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/connectors/immer/immer.ts"],
4
+ "sourcesContent": ["import { produce } from \"immer\";\nimport type { ContextUpdater } from \"../../core/types\";\nimport type { ImmerContextRecipe } from \"./immer.types\";\n\nexport type { ImmerContextRecipe } from \"./immer.types\";\n\n/**\n * Converts a synchronous Immer recipe into a Journey context updater.\n *\n * Pass the result to `machine.context.update`, a hook's `updateContext`, or a\n * transactional work commit. Immer supplies structural sharing and applies its\n * normal draftability and auto-freezing behavior.\n */\nexport function immerConnector<TContext>(\n recipe: ImmerContextRecipe<TContext>\n): ContextUpdater<TContext> {\n return (previous) => produce(previous, recipe);\n}\n"],
5
+ "mappings": "AAAA,OAAS,WAAAA,MAAe,QAajB,SAASC,EACdC,EAC0B,CAC1B,OAAQC,GAAaH,EAAQG,EAAUD,CAAM,CAC/C",
6
+ "names": ["produce", "immerConnector", "recipe", "previous"]
7
+ }
@@ -0,0 +1,9 @@
1
+ import type { Producer } from "immer";
2
+ /**
3
+ * Synchronous Immer recipe adapted into a Journey context updater.
4
+ *
5
+ * A recipe may mutate its draft or return a replacement context, following
6
+ * Immer's producer rules. Returning `undefined` without draft mutations is a
7
+ * no-op; use Immer's `nothing` sentinel when an undefined context is intended.
8
+ */
9
+ export type ImmerContextRecipe<TContext> = Producer<TContext>;
@@ -0,0 +1,9 @@
1
+ import type { Producer } from "immer";
2
+ /**
3
+ * Synchronous Immer recipe adapted into a Journey context updater.
4
+ *
5
+ * A recipe may mutate its draft or return a replacement context, following
6
+ * Immer's producer rules. Returning `undefined` without draft mutations is a
7
+ * no-op; use Immer's `nothing` sentinel when an undefined context is intended.
8
+ */
9
+ export type ImmerContextRecipe<TContext> = Producer<TContext>;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Machine-readable reasons a journey can refuse to build or operate.
3
+ *
4
+ * Closed on purpose: consumers should be able to `switch` over it exhaustively.
5
+ * Adding a member is a minor change; removing or repurposing one is not done
6
+ * within a major.
7
+ */
8
+ export type JourneyErrorCode =
9
+ /** A definition declared no steps. */
10
+ "empty-definition"
11
+ /** The same step id was declared twice. */
12
+ | "duplicate-step-id"
13
+ /** A step id was referenced that the definition does not declare. */
14
+ | "unknown-step"
15
+ /** `initial` names a step the definition does not declare. */
16
+ | "unknown-initial-step"
17
+ /** A transition's `from` or `to` names a step that does not exist. */
18
+ | "dangling-transition"
19
+ /** Two plugins claimed the same `name`. */
20
+ | "duplicate-plugin-name"
21
+ /** Persistence was requested but no usable storage could be resolved. */
22
+ | "storage-unavailable"
23
+ /** Navigation work's `commit` returned a promise; it must be synchronous. */
24
+ | "async-commit"
25
+ /** A transition or work entry declared `label` as something other than a non-empty string. */
26
+ | "invalid-label"
27
+ /** A transition or work entry declared `timeoutMs` as something other than a positive finite number. */
28
+ | "invalid-timeout";
29
+ export type JourneyErrorDetails = {
30
+ /** The offending step id, where one caused the failure. */
31
+ readonly stepId?: string;
32
+ /** The offending event name, for transition failures. */
33
+ readonly event?: string;
34
+ /** The offending plugin name, for plugin failures. */
35
+ readonly pluginName?: string;
36
+ };
37
+ /**
38
+ * Every error core throws itself.
39
+ *
40
+ * Before this existed, distinguishing "duplicate plugin name" from "unknown step
41
+ * in transition" meant matching on the message text — which quietly made every
42
+ * message a compatibility promise. `code` is the stable contract; `message` is
43
+ * for humans and may be reworded in any release.
44
+ *
45
+ * Errors surfaced through `NavigationResult.error` and the `error` subscription
46
+ * event stay `unknown`: those carry whatever the caller's own work threw, which
47
+ * core cannot constrain.
48
+ */
49
+ export declare class JourneyError extends Error {
50
+ readonly code: JourneyErrorCode;
51
+ readonly stepId: string | undefined;
52
+ readonly event: string | undefined;
53
+ readonly pluginName: string | undefined;
54
+ constructor(code: JourneyErrorCode, message: string, details?: JourneyErrorDetails, cause?: unknown);
55
+ }
56
+ /** Narrowing helper, so consumers need not import the class to test for it. */
57
+ export declare const isJourneyError: (value: unknown) => value is JourneyError;