@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.
- package/README.md +185 -189
- 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 +40 -0
- package/dist/core/helpers.d.ts +40 -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 +61 -80
- 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
|
|
72
|
+
|
|
73
|
+
Pass work to next or previous navigation when an operation must succeed before movement:
|
|
75
74
|
|
|
76
75
|
```ts
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
+
}));
|
|
83
83
|
},
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (!result.ok) {
|
|
87
|
+
console.error(result.reason, result.error);
|
|
86
88
|
}
|
|
87
89
|
```
|
|
88
90
|
|
|
89
|
-
|
|
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.
|
|
90
94
|
|
|
91
|
-
|
|
95
|
+
`goToPreviousStep()` accepts the same work contract.
|
|
92
96
|
|
|
93
|
-
|
|
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")]
|
|
101
|
-
}
|
|
102
|
-
});
|
|
97
|
+
## Async state
|
|
103
98
|
|
|
104
|
-
|
|
105
|
-
const definition = build({
|
|
106
|
-
initial: "login",
|
|
107
|
-
context: { role: "user" },
|
|
108
|
-
steps: [loginStep, adminStep, dashboardStep]
|
|
109
|
-
});
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
Use the **factory form** when you need `event.payload` narrowed to the specific event type:
|
|
99
|
+
Use `snapshot.transition.pending` as the normal UI-level loading flag:
|
|
113
100
|
|
|
114
101
|
```ts
|
|
115
|
-
|
|
102
|
+
const snapshot = checkout.getSnapshot();
|
|
103
|
+
continueButton.disabled = snapshot.transition.pending;
|
|
116
104
|
```
|
|
117
105
|
|
|
118
|
-
|
|
119
|
-
|
|
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.
|
|
109
|
+
|
|
110
|
+
Only one navigation settles at a time. Concurrent calls resolve with
|
|
111
|
+
`{ ok: false, reason: "transitioning" }`.
|
|
120
112
|
|
|
121
|
-
|
|
113
|
+
## Lifecycle effects
|
|
122
114
|
|
|
123
|
-
|
|
115
|
+
`onLeave` and `onEnter` are awaited post-commit effects. They cannot cancel or roll back movement:
|
|
124
116
|
|
|
125
117
|
```ts
|
|
126
|
-
const machine =
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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),
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "receipt",
|
|
127
|
+
onEnter: async ({ snapshot, updateContext }) => {
|
|
128
|
+
const receipt = await loadReceipt(snapshot.context);
|
|
129
|
+
updateContext((context) => ({ ...context, receiptId: receipt.id }));
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
],
|
|
131
133
|
});
|
|
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();
|
|
138
134
|
```
|
|
139
135
|
|
|
140
|
-
|
|
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.
|
|
141
138
|
|
|
142
|
-
|
|
139
|
+
## Navigation and history
|
|
143
140
|
|
|
144
|
-
|
|
141
|
+
```ts
|
|
142
|
+
await checkout.navigate.goToNextStep();
|
|
143
|
+
await checkout.navigate.goToPreviousStep();
|
|
144
|
+
await checkout.navigate.goToPreviousStep(2);
|
|
145
|
+
await checkout.navigate.goToLastVisitedStep();
|
|
146
|
+
```
|
|
145
147
|
|
|
146
|
-
|
|
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.
|
|
147
150
|
|
|
148
|
-
|
|
151
|
+
`goToStepById(id)` is an intentional escape hatch for occasional direct jumps in an otherwise
|
|
152
|
+
ordered flow:
|
|
149
153
|
|
|
150
154
|
```ts
|
|
151
|
-
|
|
152
|
-
context: { username: "" },
|
|
153
|
-
steps: {
|
|
154
|
-
login: {
|
|
155
|
-
onLeave: ({ context }) => analytics.track("login_left", { user: context.username })
|
|
156
|
-
},
|
|
157
|
-
dashboard: {
|
|
158
|
-
onEnter: ({ context }) => analytics.track("dashboard_entered"),
|
|
159
|
-
onLeave: ({ context }) => console.log("leaving dashboard")
|
|
160
|
-
}
|
|
161
|
-
},
|
|
162
|
-
transitions: ["login", "dashboard"]
|
|
163
|
-
});
|
|
155
|
+
await checkout.navigate.goToStepById("review");
|
|
164
156
|
```
|
|
165
157
|
|
|
166
|
-
|
|
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.
|
|
167
160
|
|
|
168
|
-
|
|
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
|
-
});
|
|
174
|
-
```
|
|
161
|
+
## Completion and termination
|
|
175
162
|
|
|
176
|
-
|
|
163
|
+
Reaching the last linear step does not complete the machine. A final screen is a position;
|
|
164
|
+
completion is an explicit product outcome:
|
|
177
165
|
|
|
178
|
-
```
|
|
179
|
-
|
|
166
|
+
```ts
|
|
167
|
+
checkout.controls.complete({ orderId: "order-42" });
|
|
180
168
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
onEnter: ({ context }) => analytics.track("dashboard_entered"),
|
|
184
|
-
onLeave: ({ context }) => console.log("leaving dashboard")
|
|
185
|
-
});
|
|
186
|
-
// ...
|
|
187
|
-
}
|
|
169
|
+
checkout.getSnapshot().machine.outcome;
|
|
170
|
+
// { type: "completed", payload: { orderId: "order-42" } }
|
|
188
171
|
```
|
|
189
172
|
|
|
190
|
-
|
|
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.
|
|
191
175
|
|
|
192
|
-
##
|
|
176
|
+
## Snapshot and subscriptions
|
|
193
177
|
|
|
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
|
|
178
|
+
All changing state lives in one immutable snapshot:
|
|
202
179
|
|
|
203
|
-
|
|
180
|
+
```ts
|
|
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
|
+
```
|
|
192
|
+
|
|
193
|
+
Subscribe to every committed snapshot, or to a named lifecycle event:
|
|
204
194
|
|
|
205
195
|
```ts
|
|
206
|
-
|
|
196
|
+
const stop = checkout.subscriptions.subscribe(() => render(checkout.getSnapshot().currentStep?.id));
|
|
207
197
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
createPersistencePlugin({
|
|
211
|
-
key: "checkout",
|
|
212
|
-
version: 2,
|
|
213
|
-
blockList: ["payment.cardNumber"]
|
|
214
|
-
})
|
|
215
|
-
]
|
|
198
|
+
checkout.subscriptions.subscribeEvent("navigationBlocked", ({ reason, error }) => {
|
|
199
|
+
reportNavigationFailure(reason, error);
|
|
216
200
|
});
|
|
201
|
+
|
|
202
|
+
stop();
|
|
217
203
|
```
|
|
218
204
|
|
|
219
|
-
|
|
205
|
+
## Graph mode
|
|
220
206
|
|
|
221
|
-
|
|
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.
|
|
222
210
|
|
|
223
|
-
|
|
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.
|
|
224
216
|
|
|
225
|
-
|
|
226
|
-
|
|
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.dev/docs/core/usage/graph).
|
|
227
220
|
|
|
228
|
-
|
|
229
|
-
plugins: [
|
|
230
|
-
createAutosavePlugin({
|
|
231
|
-
key: "checkout-draft",
|
|
232
|
-
debounceMs: 300,
|
|
233
|
-
allowList: ["profile", "shipping"]
|
|
234
|
-
})
|
|
235
|
-
]
|
|
236
|
-
});
|
|
221
|
+
## Connectors
|
|
237
222
|
|
|
238
|
-
|
|
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.dev/docs/core/connectors/immer) for replacement recipes,
|
|
244
|
+
freezing, draftability, and transactional behavior.
|
|
242
245
|
|
|
243
|
-
##
|
|
246
|
+
## Plugins
|
|
247
|
+
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
]
|
|
257
|
+
track: (event) => analytics.track(event.name, event.payload),
|
|
258
|
+
}),
|
|
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
|
-
- [
|
|
271
|
-
- [
|
|
272
|
-
- [
|
|
273
|
-
- [
|
|
274
|
-
- [
|
|
275
|
-
- [
|
|
276
|
-
- [
|
|
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)
|
|
269
|
+
- [Quickstart](https://rxova.dev/docs/core/getting-started)
|
|
270
|
+
- [Linear journeys](https://rxova.dev/docs/core/usage/linear)
|
|
271
|
+
- [Async behavior](https://rxova.dev/docs/core/async)
|
|
272
|
+
- [Machine API](https://rxova.dev/docs/core/api/machine-api)
|
|
273
|
+
- [Snapshot](https://rxova.dev/docs/core/snapshot)
|
|
274
|
+
- [Pre-1.0 migration](https://rxova.dev/docs/core/pre-1-0-migration)
|
|
275
|
+
- [Stability contract](https://rxova.dev/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;
|