@rxova/journey-core 1.0.0-rc.2 → 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.
- package/README.md +182 -186
- package/dist/connectors/immer/immer.cjs +2 -0
- package/dist/connectors/immer/immer.cjs.map +7 -0
- package/dist/connectors/immer/immer.d.cts +11 -0
- package/dist/connectors/immer/immer.d.ts +11 -0
- package/dist/connectors/immer/immer.js +2 -0
- package/dist/connectors/immer/immer.js.map +7 -0
- package/dist/connectors/immer/immer.types.d.cts +9 -0
- package/dist/connectors/immer/immer.types.d.ts +9 -0
- package/dist/core/errors.d.cts +57 -0
- package/dist/core/errors.d.ts +57 -0
- package/dist/core/helpers.d.cts +46 -0
- package/dist/core/helpers.d.ts +46 -0
- package/dist/core/machine.d.cts +9 -0
- package/dist/core/machine.d.ts +9 -0
- package/dist/core/runtime.d.cts +115 -0
- package/dist/core/runtime.d.ts +115 -0
- package/dist/core/runtime.types.d.cts +112 -0
- package/dist/core/runtime.types.d.ts +112 -0
- package/dist/core/store.d.cts +31 -0
- package/dist/core/store.d.ts +31 -0
- package/dist/core/types.d.cts +481 -0
- package/dist/core/types.d.ts +481 -0
- package/dist/diagnostics/diagnostics.helpers.d.cts +17 -0
- package/dist/diagnostics/diagnostics.helpers.d.ts +17 -0
- package/dist/diagnostics/diagnostics.types.d.cts +22 -0
- package/dist/diagnostics/diagnostics.types.d.ts +22 -0
- package/dist/graph/bag.types.d.cts +91 -0
- package/dist/graph/bag.types.d.ts +91 -0
- package/dist/graph/graph.d.cts +63 -0
- package/dist/graph/graph.d.ts +63 -0
- package/dist/graph/graph.types.d.cts +212 -0
- package/dist/graph/graph.types.d.ts +212 -0
- package/dist/graph/work.d.cts +81 -0
- package/dist/graph/work.d.ts +81 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.cts +13 -5
- package/dist/index.d.ts +13 -5
- package/dist/index.js +1 -1
- package/dist/index.js.map +4 -4
- package/dist/linear/linear.d.cts +33 -0
- package/dist/linear/linear.d.ts +33 -0
- package/dist/linear/linear.types.d.cts +28 -0
- package/dist/linear/linear.types.d.ts +28 -0
- package/dist/plugins/analytics/analytics.d.cts +8 -0
- package/dist/plugins/analytics/analytics.d.ts +8 -0
- package/dist/plugins/analytics/analytics.helpers.d.cts +2 -0
- package/dist/plugins/analytics/analytics.helpers.d.ts +2 -0
- package/dist/plugins/analytics/analytics.types.d.cts +27 -0
- package/dist/plugins/analytics/analytics.types.d.ts +27 -0
- package/dist/plugins/execution-paths/execution-paths.d.cts +7 -4
- package/dist/plugins/execution-paths/execution-paths.d.ts +7 -4
- package/dist/plugins/execution-paths/execution-paths.types.d.cts +21 -0
- package/dist/plugins/execution-paths/execution-paths.types.d.ts +21 -0
- package/dist/plugins/index.cjs +2 -0
- package/dist/plugins/index.cjs.map +7 -0
- package/dist/plugins/index.d.cts +19 -0
- package/dist/plugins/index.d.ts +19 -0
- package/dist/plugins/index.js +2 -0
- package/dist/plugins/index.js.map +7 -0
- package/dist/plugins/persistence/persistence.d.cts +16 -0
- package/dist/plugins/persistence/persistence.d.ts +16 -0
- package/dist/plugins/persistence/persistence.helpers.d.cts +34 -0
- package/dist/plugins/persistence/persistence.helpers.d.ts +34 -0
- package/dist/plugins/persistence/persistence.types.d.cts +63 -0
- package/dist/plugins/persistence/persistence.types.d.ts +63 -0
- package/dist/plugins/replay/replay.d.cts +7 -0
- package/dist/plugins/replay/replay.d.ts +7 -0
- package/dist/plugins/replay/replay.helpers.d.cts +22 -0
- package/dist/plugins/replay/replay.helpers.d.ts +22 -0
- package/dist/plugins/replay/replay.types.d.cts +28 -0
- package/dist/plugins/replay/replay.types.d.ts +28 -0
- package/package.json +78 -79
- package/dist/journey-builder/index.d.cts +0 -5
- package/dist/journey-builder/index.d.ts +0 -5
- package/dist/journey-builder/types.d.cts +0 -126
- package/dist/journey-builder/types.d.ts +0 -126
- package/dist/journey-machine/async-state.d.cts +0 -12
- package/dist/journey-machine/async-state.d.ts +0 -12
- package/dist/journey-machine/computed.d.cts +0 -2
- package/dist/journey-machine/computed.d.ts +0 -2
- package/dist/journey-machine/controls.d.cts +0 -17
- package/dist/journey-machine/controls.d.ts +0 -17
- package/dist/journey-machine/helpers.d.cts +0 -52
- package/dist/journey-machine/helpers.d.ts +0 -52
- package/dist/journey-machine/index.d.cts +0 -3
- package/dist/journey-machine/index.d.ts +0 -3
- package/dist/journey-machine/navigation.d.cts +0 -32
- package/dist/journey-machine/navigation.d.ts +0 -32
- package/dist/journey-machine/plugin-controller.d.cts +0 -15
- package/dist/journey-machine/plugin-controller.d.ts +0 -15
- package/dist/journey-machine/resolve-journey-definition.d.cts +0 -2
- package/dist/journey-machine/resolve-journey-definition.d.ts +0 -2
- package/dist/journey-machine/runtime.d.cts +0 -33
- package/dist/journey-machine/runtime.d.ts +0 -33
- package/dist/journey-machine/send.d.cts +0 -9
- package/dist/journey-machine/send.d.ts +0 -9
- package/dist/plugins/analytics/index.cjs +0 -2
- package/dist/plugins/analytics/index.cjs.map +0 -7
- package/dist/plugins/analytics/index.d.cts +0 -25
- package/dist/plugins/analytics/index.d.ts +0 -25
- package/dist/plugins/analytics/index.js +0 -2
- package/dist/plugins/analytics/index.js.map +0 -7
- package/dist/plugins/autosave/index.cjs +0 -2
- package/dist/plugins/autosave/index.cjs.map +0 -7
- package/dist/plugins/autosave/index.d.cts +0 -17
- package/dist/plugins/autosave/index.d.ts +0 -17
- package/dist/plugins/autosave/index.js +0 -2
- package/dist/plugins/autosave/index.js.map +0 -7
- package/dist/plugins/diagnostics/diagnostics.d.cts +0 -3
- package/dist/plugins/diagnostics/diagnostics.d.ts +0 -3
- package/dist/plugins/diagnostics/index.cjs +0 -2
- package/dist/plugins/diagnostics/index.cjs.map +0 -7
- package/dist/plugins/diagnostics/index.d.cts +0 -15
- package/dist/plugins/diagnostics/index.d.ts +0 -15
- package/dist/plugins/diagnostics/index.js +0 -2
- package/dist/plugins/diagnostics/index.js.map +0 -7
- package/dist/plugins/execution-paths/index.cjs +0 -2
- package/dist/plugins/execution-paths/index.cjs.map +0 -7
- package/dist/plugins/execution-paths/index.d.cts +0 -18
- package/dist/plugins/execution-paths/index.d.ts +0 -18
- package/dist/plugins/execution-paths/index.js +0 -2
- package/dist/plugins/execution-paths/index.js.map +0 -7
- package/dist/plugins/persistence/controller.d.cts +0 -17
- package/dist/plugins/persistence/controller.d.ts +0 -17
- package/dist/plugins/persistence/index.cjs +0 -2
- package/dist/plugins/persistence/index.cjs.map +0 -7
- package/dist/plugins/persistence/index.d.cts +0 -12
- package/dist/plugins/persistence/index.d.ts +0 -12
- package/dist/plugins/persistence/index.js +0 -2
- package/dist/plugins/persistence/index.js.map +0 -7
- package/dist/plugins/replay/index.cjs +0 -2
- package/dist/plugins/replay/index.cjs.map +0 -7
- package/dist/plugins/replay/index.d.cts +0 -27
- package/dist/plugins/replay/index.d.ts +0 -27
- package/dist/plugins/replay/index.js +0 -2
- package/dist/plugins/replay/index.js.map +0 -7
- package/dist/types/analytics.types.d.cts +0 -33
- package/dist/types/analytics.types.d.ts +0 -33
- package/dist/types/autosave.types.d.cts +0 -23
- package/dist/types/autosave.types.d.ts +0 -23
- package/dist/types/diagnostics.types.d.cts +0 -38
- package/dist/types/diagnostics.types.d.ts +0 -38
- package/dist/types/index.d.cts +0 -9
- package/dist/types/index.d.ts +0 -9
- package/dist/types/journey.types.d.cts +0 -191
- package/dist/types/journey.types.d.ts +0 -191
- package/dist/types/machine.types.d.cts +0 -104
- package/dist/types/machine.types.d.ts +0 -104
- package/dist/types/observation.types.d.cts +0 -83
- package/dist/types/observation.types.d.ts +0 -83
- package/dist/types/persistence.types.d.cts +0 -34
- package/dist/types/persistence.types.d.ts +0 -34
- package/dist/types/replay.types.d.cts +0 -35
- package/dist/types/replay.types.d.ts +0 -35
- package/dist/types/transitions.types.d.cts +0 -122
- 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
|
|
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/
|
|
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
|
-
|
|
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
|
|
19
|
+
npm install @rxova/journey-core
|
|
25
20
|
```
|
|
26
21
|
|
|
27
|
-
##
|
|
22
|
+
## Linear quickstart
|
|
28
23
|
|
|
29
24
|
```ts
|
|
30
|
-
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
await machine.goToPreviousStep(); // review → details
|
|
49
|
+
checkout.context.update((context) => ({
|
|
50
|
+
...context,
|
|
51
|
+
email: "ada@example.com"
|
|
52
|
+
}));
|
|
44
53
|
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
"account",
|
|
62
|
-
{
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
+
## Transactional navigation
|
|
75
72
|
|
|
76
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
97
|
+
## Async state
|
|
122
98
|
|
|
123
|
-
|
|
99
|
+
Use `snapshot.transition.pending` as the normal UI-level loading flag:
|
|
124
100
|
|
|
125
101
|
```ts
|
|
126
|
-
const
|
|
127
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
110
|
+
Only one navigation settles at a time. Concurrent calls resolve with
|
|
111
|
+
`{ ok: false, reason: "transitioning" }`.
|
|
145
112
|
|
|
146
|
-
##
|
|
113
|
+
## Lifecycle effects
|
|
147
114
|
|
|
148
|
-
|
|
115
|
+
`onLeave` and `onEnter` are awaited post-commit effects. They cannot cancel or roll back movement:
|
|
149
116
|
|
|
150
117
|
```ts
|
|
151
|
-
const machine =
|
|
152
|
-
context: {
|
|
153
|
-
steps:
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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
|
-
|
|
179
|
-
|
|
151
|
+
`goToStepById(id)` is an intentional escape hatch for occasional direct jumps in an otherwise
|
|
152
|
+
ordered flow:
|
|
180
153
|
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
+
checkout.controls.complete({ orderId: "order-42" });
|
|
207
168
|
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
+
## Snapshot and subscriptions
|
|
222
177
|
|
|
223
|
-
|
|
178
|
+
All changing state lives in one immutable snapshot:
|
|
224
179
|
|
|
225
180
|
```ts
|
|
226
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
248
|
+
Built-in plugins are separately imported and observe the machine through a read-only host:
|
|
244
249
|
|
|
245
250
|
```ts
|
|
246
|
-
import {
|
|
251
|
+
import { createLinearJourney } from "@rxova/journey-core";
|
|
252
|
+
import { createAnalyticsPlugin } from "@rxova/journey-core/plugins";
|
|
247
253
|
|
|
248
|
-
const machine =
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
- [
|
|
274
|
-
- [Async
|
|
275
|
-
- [
|
|
276
|
-
- [
|
|
277
|
-
- [
|
|
278
|
-
- [
|
|
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,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;
|