@flowsterix/core 0.1.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/dist/__tests__/events.test.d.ts +2 -0
- package/dist/__tests__/events.test.d.ts.map +1 -0
- package/dist/__tests__/flowEvents.test.d.ts +2 -0
- package/dist/__tests__/flowEvents.test.d.ts.map +1 -0
- package/dist/__tests__/storage.test.d.ts +2 -0
- package/dist/__tests__/storage.test.d.ts.map +1 -0
- package/dist/__tests__/validation.test.d.ts +2 -0
- package/dist/__tests__/validation.test.d.ts.map +1 -0
- package/dist/__tests__/version.test.d.ts +2 -0
- package/dist/__tests__/version.test.d.ts.map +1 -0
- package/dist/createFlow.d.ts +3 -0
- package/dist/createFlow.d.ts.map +1 -0
- package/dist/events.d.ts +12 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/index.cjs +1056 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +1017 -0
- package/dist/state.d.ts +15 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/storage.d.ts +51 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/types.d.ts +289 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/validation.d.ts +90 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/version.d.ts +41 -0
- package/dist/version.d.ts.map +1 -0
- package/package.json +42 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1056 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
MemoryStorageAdapter: () => MemoryStorageAdapter,
|
|
24
|
+
buildStepIdMap: () => buildStepIdMap,
|
|
25
|
+
compareVersions: () => compareVersions,
|
|
26
|
+
createApiStorageAdapter: () => createApiStorageAdapter,
|
|
27
|
+
createEventBus: () => createEventBus,
|
|
28
|
+
createFlow: () => createFlow,
|
|
29
|
+
createFlowStore: () => createFlowStore,
|
|
30
|
+
createLocalStorageAdapter: () => createLocalStorageAdapter,
|
|
31
|
+
flowDefinitionSchema: () => flowDefinitionSchema,
|
|
32
|
+
parseVersion: () => parseVersion,
|
|
33
|
+
resolveMaybePromise: () => resolveMaybePromise,
|
|
34
|
+
serializeVersion: () => serializeVersion,
|
|
35
|
+
validateFlowDefinition: () => validateFlowDefinition
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(index_exports);
|
|
38
|
+
|
|
39
|
+
// src/validation.ts
|
|
40
|
+
var import_zod = require("zod");
|
|
41
|
+
var targetSchema = import_zod.z.union([
|
|
42
|
+
import_zod.z.literal("screen"),
|
|
43
|
+
import_zod.z.object({
|
|
44
|
+
selector: import_zod.z.string().optional(),
|
|
45
|
+
getNode: import_zod.z.custom(
|
|
46
|
+
(value) => typeof value === "function",
|
|
47
|
+
{
|
|
48
|
+
message: "getNode must be a function returning an Element"
|
|
49
|
+
}
|
|
50
|
+
).optional(),
|
|
51
|
+
description: import_zod.z.string().optional()
|
|
52
|
+
}).refine((value) => Boolean(value.selector || value.getNode), {
|
|
53
|
+
message: "target requires either a selector or getNode definition"
|
|
54
|
+
})
|
|
55
|
+
]).describe("Step target definition");
|
|
56
|
+
var maskSchema = import_zod.z.union([
|
|
57
|
+
import_zod.z.literal("hole"),
|
|
58
|
+
import_zod.z.literal("none"),
|
|
59
|
+
import_zod.z.object({
|
|
60
|
+
padding: import_zod.z.number().nonnegative().optional(),
|
|
61
|
+
radius: import_zod.z.number().nonnegative().optional()
|
|
62
|
+
})
|
|
63
|
+
]);
|
|
64
|
+
var controlStateSchema = import_zod.z.union([
|
|
65
|
+
import_zod.z.literal("auto"),
|
|
66
|
+
import_zod.z.literal("hidden"),
|
|
67
|
+
import_zod.z.literal("disabled")
|
|
68
|
+
]);
|
|
69
|
+
var advanceRuleSchema = import_zod.z.union([
|
|
70
|
+
import_zod.z.object({ type: import_zod.z.literal("manual") }),
|
|
71
|
+
import_zod.z.object({
|
|
72
|
+
type: import_zod.z.literal("event"),
|
|
73
|
+
event: import_zod.z.string(),
|
|
74
|
+
on: import_zod.z.union([import_zod.z.literal("target"), import_zod.z.string()]).optional()
|
|
75
|
+
}),
|
|
76
|
+
import_zod.z.object({
|
|
77
|
+
type: import_zod.z.literal("predicate"),
|
|
78
|
+
pollMs: import_zod.z.number().positive().optional(),
|
|
79
|
+
timeoutMs: import_zod.z.number().positive().optional(),
|
|
80
|
+
check: import_zod.z.custom(
|
|
81
|
+
(value) => typeof value === "function",
|
|
82
|
+
{
|
|
83
|
+
message: "predicate check must be a function"
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
}),
|
|
87
|
+
import_zod.z.object({ type: import_zod.z.literal("delay"), ms: import_zod.z.number().nonnegative() }),
|
|
88
|
+
import_zod.z.object({
|
|
89
|
+
type: import_zod.z.literal("route"),
|
|
90
|
+
to: import_zod.z.union([import_zod.z.string(), import_zod.z.instanceof(RegExp)]).optional()
|
|
91
|
+
})
|
|
92
|
+
]);
|
|
93
|
+
var waitForSchema = import_zod.z.object({
|
|
94
|
+
selector: import_zod.z.string().optional(),
|
|
95
|
+
timeout: import_zod.z.number().positive().optional(),
|
|
96
|
+
predicate: import_zod.z.custom(
|
|
97
|
+
(value) => typeof value === "function",
|
|
98
|
+
{
|
|
99
|
+
message: "waitFor.predicate must be a function"
|
|
100
|
+
}
|
|
101
|
+
).optional(),
|
|
102
|
+
pollMs: import_zod.z.number().positive().optional(),
|
|
103
|
+
subscribe: import_zod.z.custom(
|
|
104
|
+
(value) => typeof value === "function",
|
|
105
|
+
{
|
|
106
|
+
message: "waitFor.subscribe must be a function"
|
|
107
|
+
}
|
|
108
|
+
).optional()
|
|
109
|
+
}).partial();
|
|
110
|
+
var scrollMarginSchema = import_zod.z.union([
|
|
111
|
+
import_zod.z.number().nonnegative(),
|
|
112
|
+
import_zod.z.object({
|
|
113
|
+
top: import_zod.z.number().nonnegative().optional(),
|
|
114
|
+
bottom: import_zod.z.number().nonnegative().optional(),
|
|
115
|
+
left: import_zod.z.number().nonnegative().optional(),
|
|
116
|
+
right: import_zod.z.number().nonnegative().optional()
|
|
117
|
+
}).strict()
|
|
118
|
+
]);
|
|
119
|
+
var hudPopoverSchema = import_zod.z.object({
|
|
120
|
+
offset: import_zod.z.number().optional(),
|
|
121
|
+
role: import_zod.z.string().optional(),
|
|
122
|
+
ariaLabel: import_zod.z.string().optional(),
|
|
123
|
+
ariaDescribedBy: import_zod.z.string().optional(),
|
|
124
|
+
ariaModal: import_zod.z.boolean().optional()
|
|
125
|
+
});
|
|
126
|
+
var hudBackdropSchema = import_zod.z.object({
|
|
127
|
+
interaction: import_zod.z.union([import_zod.z.literal("block"), import_zod.z.literal("passthrough")]).optional()
|
|
128
|
+
});
|
|
129
|
+
var hudBehaviorSchema = import_zod.z.object({
|
|
130
|
+
lockBodyScroll: import_zod.z.boolean().optional()
|
|
131
|
+
});
|
|
132
|
+
var hudSchema = import_zod.z.object({
|
|
133
|
+
render: import_zod.z.union([import_zod.z.literal("default"), import_zod.z.literal("none")]).optional(),
|
|
134
|
+
popover: hudPopoverSchema.optional(),
|
|
135
|
+
backdrop: hudBackdropSchema.optional(),
|
|
136
|
+
behavior: hudBehaviorSchema.optional(),
|
|
137
|
+
tokens: import_zod.z.record(import_zod.z.string(), import_zod.z.any()).optional()
|
|
138
|
+
});
|
|
139
|
+
var resumeStrategySchema = import_zod.z.union([
|
|
140
|
+
import_zod.z.literal("chain"),
|
|
141
|
+
import_zod.z.literal("current")
|
|
142
|
+
]);
|
|
143
|
+
var flowVersionSchema = import_zod.z.object({
|
|
144
|
+
major: import_zod.z.number().nonnegative().int(),
|
|
145
|
+
minor: import_zod.z.number().nonnegative().int()
|
|
146
|
+
});
|
|
147
|
+
var migrateFnSchema = import_zod.z.custom(
|
|
148
|
+
(value) => typeof value === "function",
|
|
149
|
+
{ message: "migrate must be a function" }
|
|
150
|
+
).optional();
|
|
151
|
+
var placementSchema = import_zod.z.union([
|
|
152
|
+
import_zod.z.literal("auto"),
|
|
153
|
+
import_zod.z.literal("top"),
|
|
154
|
+
import_zod.z.literal("bottom"),
|
|
155
|
+
import_zod.z.literal("left"),
|
|
156
|
+
import_zod.z.literal("right"),
|
|
157
|
+
import_zod.z.literal("auto-start"),
|
|
158
|
+
import_zod.z.literal("auto-end"),
|
|
159
|
+
import_zod.z.literal("top-start"),
|
|
160
|
+
import_zod.z.literal("top-end"),
|
|
161
|
+
import_zod.z.literal("bottom-start"),
|
|
162
|
+
import_zod.z.literal("bottom-end"),
|
|
163
|
+
import_zod.z.literal("left-start"),
|
|
164
|
+
import_zod.z.literal("left-end"),
|
|
165
|
+
import_zod.z.literal("right-start"),
|
|
166
|
+
import_zod.z.literal("right-end")
|
|
167
|
+
]);
|
|
168
|
+
var stepSchema = import_zod.z.object({
|
|
169
|
+
id: import_zod.z.string().min(1),
|
|
170
|
+
target: targetSchema,
|
|
171
|
+
route: import_zod.z.union([import_zod.z.string(), import_zod.z.instanceof(RegExp)]).optional(),
|
|
172
|
+
placement: placementSchema.optional(),
|
|
173
|
+
mask: maskSchema.optional(),
|
|
174
|
+
targetBehavior: import_zod.z.object({
|
|
175
|
+
hidden: import_zod.z.union([import_zod.z.literal("screen"), import_zod.z.literal("skip")]).optional(),
|
|
176
|
+
hiddenDelayMs: import_zod.z.number().nonnegative().optional(),
|
|
177
|
+
scrollMargin: scrollMarginSchema.optional(),
|
|
178
|
+
scrollMode: import_zod.z.union([import_zod.z.literal("preserve"), import_zod.z.literal("start"), import_zod.z.literal("center")]).optional()
|
|
179
|
+
}).optional(),
|
|
180
|
+
content: import_zod.z.any(),
|
|
181
|
+
advance: import_zod.z.array(advanceRuleSchema).optional(),
|
|
182
|
+
waitFor: waitForSchema.optional(),
|
|
183
|
+
onResume: import_zod.z.custom((value) => typeof value === "function").optional(),
|
|
184
|
+
onEnter: import_zod.z.custom((value) => typeof value === "function").optional(),
|
|
185
|
+
onExit: import_zod.z.custom((value) => typeof value === "function").optional(),
|
|
186
|
+
controls: import_zod.z.object({
|
|
187
|
+
back: controlStateSchema.optional(),
|
|
188
|
+
next: controlStateSchema.optional()
|
|
189
|
+
}).optional()
|
|
190
|
+
});
|
|
191
|
+
var flowDefinitionSchema = import_zod.z.object({
|
|
192
|
+
id: import_zod.z.string().min(1),
|
|
193
|
+
version: flowVersionSchema,
|
|
194
|
+
steps: import_zod.z.array(stepSchema).min(1),
|
|
195
|
+
hud: hudSchema.optional(),
|
|
196
|
+
resumeStrategy: resumeStrategySchema.optional(),
|
|
197
|
+
autoStart: import_zod.z.boolean().optional(),
|
|
198
|
+
metadata: import_zod.z.record(import_zod.z.string(), import_zod.z.any()).optional(),
|
|
199
|
+
migrate: migrateFnSchema
|
|
200
|
+
});
|
|
201
|
+
var validateFlowDefinition = (definition) => {
|
|
202
|
+
return flowDefinitionSchema.parse(definition);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// src/createFlow.ts
|
|
206
|
+
var createFlow = (definition) => {
|
|
207
|
+
const parsed = flowDefinitionSchema.parse(
|
|
208
|
+
definition
|
|
209
|
+
);
|
|
210
|
+
return {
|
|
211
|
+
...parsed,
|
|
212
|
+
steps: parsed.steps.map((step) => ({ ...step }))
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// src/events.ts
|
|
217
|
+
var createEventBus = () => {
|
|
218
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
219
|
+
const on = (event, handler) => {
|
|
220
|
+
const set = listeners.get(event) ?? /* @__PURE__ */ new Set();
|
|
221
|
+
set.add(handler);
|
|
222
|
+
listeners.set(event, set);
|
|
223
|
+
return () => off(event, handler);
|
|
224
|
+
};
|
|
225
|
+
const once = (event, handler) => {
|
|
226
|
+
const wrapper = (payload) => {
|
|
227
|
+
off(event, wrapper);
|
|
228
|
+
handler(payload);
|
|
229
|
+
};
|
|
230
|
+
return on(event, wrapper);
|
|
231
|
+
};
|
|
232
|
+
const off = (event, handler) => {
|
|
233
|
+
const set = listeners.get(event);
|
|
234
|
+
if (!set) return;
|
|
235
|
+
set.delete(handler);
|
|
236
|
+
if (set.size === 0) {
|
|
237
|
+
listeners.delete(event);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
const emit = (event, payload) => {
|
|
241
|
+
const set = listeners.get(event);
|
|
242
|
+
if (!set) return;
|
|
243
|
+
for (const handler of Array.from(set)) {
|
|
244
|
+
;
|
|
245
|
+
handler(payload);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
const clear = () => {
|
|
249
|
+
listeners.clear();
|
|
250
|
+
};
|
|
251
|
+
return { on, once, off, emit, clear };
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// src/storage.ts
|
|
255
|
+
var resolveMaybePromise = async (value) => await value;
|
|
256
|
+
var MemoryStorageAdapter = class {
|
|
257
|
+
store = /* @__PURE__ */ new Map();
|
|
258
|
+
listeners = /* @__PURE__ */ new Set();
|
|
259
|
+
get = (key) => {
|
|
260
|
+
return this.store.get(key) ?? null;
|
|
261
|
+
};
|
|
262
|
+
set = (key, snapshot) => {
|
|
263
|
+
this.store.set(key, snapshot);
|
|
264
|
+
this.emit();
|
|
265
|
+
};
|
|
266
|
+
remove = (key) => {
|
|
267
|
+
this.store.delete(key);
|
|
268
|
+
this.emit();
|
|
269
|
+
};
|
|
270
|
+
subscribe = (listener) => {
|
|
271
|
+
this.listeners.add(listener);
|
|
272
|
+
return () => this.listeners.delete(listener);
|
|
273
|
+
};
|
|
274
|
+
emit() {
|
|
275
|
+
for (const listener of Array.from(this.listeners)) {
|
|
276
|
+
listener();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
var safeGlobal = typeof globalThis !== "undefined" ? globalThis : void 0;
|
|
281
|
+
var isStorageSnapshotShape = (value) => {
|
|
282
|
+
if (typeof value !== "object" || value === null) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
const snapshot = value;
|
|
286
|
+
const hasValidVersion = typeof snapshot.version === "string" || typeof snapshot.version === "number";
|
|
287
|
+
return hasValidVersion && typeof snapshot.updatedAt === "number" && "value" in snapshot;
|
|
288
|
+
};
|
|
289
|
+
var createLocalStorageAdapter = (storage = getLocalStorage()) => {
|
|
290
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
291
|
+
const get = (key) => {
|
|
292
|
+
if (!storage) return null;
|
|
293
|
+
const raw = storage.getItem(key);
|
|
294
|
+
if (!raw) return null;
|
|
295
|
+
try {
|
|
296
|
+
const parsed = JSON.parse(raw);
|
|
297
|
+
if (!isStorageSnapshotShape(parsed)) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
return parsed;
|
|
301
|
+
} catch (error) {
|
|
302
|
+
console.warn(
|
|
303
|
+
"[tour][storage] Failed to parse snapshot for key",
|
|
304
|
+
key,
|
|
305
|
+
error
|
|
306
|
+
);
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
const set = (key, snapshot) => {
|
|
311
|
+
if (!storage) return;
|
|
312
|
+
storage.setItem(key, JSON.stringify(snapshot));
|
|
313
|
+
emit();
|
|
314
|
+
};
|
|
315
|
+
const remove = (key) => {
|
|
316
|
+
if (!storage) return;
|
|
317
|
+
storage.removeItem(key);
|
|
318
|
+
emit();
|
|
319
|
+
};
|
|
320
|
+
const subscribe = (listener) => {
|
|
321
|
+
listeners.add(listener);
|
|
322
|
+
return () => listeners.delete(listener);
|
|
323
|
+
};
|
|
324
|
+
const emit = () => {
|
|
325
|
+
for (const listener of Array.from(listeners)) {
|
|
326
|
+
listener();
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
return { get, set, remove, subscribe };
|
|
330
|
+
};
|
|
331
|
+
function getLocalStorage() {
|
|
332
|
+
try {
|
|
333
|
+
if (!safeGlobal) return null;
|
|
334
|
+
if ("localStorage" in safeGlobal) {
|
|
335
|
+
return safeGlobal.localStorage;
|
|
336
|
+
}
|
|
337
|
+
} catch (error) {
|
|
338
|
+
console.warn("[tour][storage] localStorage unavailable", error);
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
function createApiStorageAdapter(options) {
|
|
343
|
+
const {
|
|
344
|
+
baseUrl,
|
|
345
|
+
getHeaders = () => ({}),
|
|
346
|
+
fetch: fetchFn = globalThis.fetch
|
|
347
|
+
} = options;
|
|
348
|
+
const buildUrl = (key) => `${baseUrl.replace(/\/$/, "")}/${encodeURIComponent(key)}`;
|
|
349
|
+
const get = async (key) => {
|
|
350
|
+
try {
|
|
351
|
+
const headers = await Promise.resolve(getHeaders());
|
|
352
|
+
const res = await fetchFn(buildUrl(key), { headers });
|
|
353
|
+
if (res.status === 404) return null;
|
|
354
|
+
if (!res.ok) {
|
|
355
|
+
console.warn("[tour][storage] API get failed", res.status, res.statusText);
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
const data = await res.json();
|
|
359
|
+
if (!isStorageSnapshotShape(data)) {
|
|
360
|
+
console.warn("[tour][storage] Invalid snapshot shape from API");
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
return data;
|
|
364
|
+
} catch (error) {
|
|
365
|
+
console.warn("[tour][storage] API get error", error);
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
const set = async (key, snapshot) => {
|
|
370
|
+
try {
|
|
371
|
+
const headers = await Promise.resolve(getHeaders());
|
|
372
|
+
const res = await fetchFn(buildUrl(key), {
|
|
373
|
+
method: "PUT",
|
|
374
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
375
|
+
body: JSON.stringify(snapshot)
|
|
376
|
+
});
|
|
377
|
+
if (!res.ok) {
|
|
378
|
+
console.warn("[tour][storage] API set failed", res.status, res.statusText);
|
|
379
|
+
}
|
|
380
|
+
} catch (error) {
|
|
381
|
+
console.warn("[tour][storage] API set error", error);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
const remove = async (key) => {
|
|
385
|
+
try {
|
|
386
|
+
const headers = await Promise.resolve(getHeaders());
|
|
387
|
+
const res = await fetchFn(buildUrl(key), {
|
|
388
|
+
method: "DELETE",
|
|
389
|
+
headers
|
|
390
|
+
});
|
|
391
|
+
if (!res.ok && res.status !== 404) {
|
|
392
|
+
console.warn("[tour][storage] API remove failed", res.status, res.statusText);
|
|
393
|
+
}
|
|
394
|
+
} catch (error) {
|
|
395
|
+
console.warn("[tour][storage] API remove error", error);
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
return { get, set, remove };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/version.ts
|
|
402
|
+
var serializeVersion = (version) => `${version.major}.${version.minor}`;
|
|
403
|
+
var parseVersion = (versionStr) => {
|
|
404
|
+
const parts = versionStr.split(".");
|
|
405
|
+
if (parts.length === 2) {
|
|
406
|
+
const major = parseInt(parts[0], 10);
|
|
407
|
+
const minor = parseInt(parts[1], 10);
|
|
408
|
+
if (!isNaN(major) && !isNaN(minor)) {
|
|
409
|
+
return { major, minor };
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
console.warn(`[tour][version] Invalid version string: "${versionStr}", defaulting to 0.0`);
|
|
413
|
+
return { major: 0, minor: 0 };
|
|
414
|
+
};
|
|
415
|
+
var compareVersions = (stored, current) => {
|
|
416
|
+
if (stored.major === current.major && stored.minor === current.minor) {
|
|
417
|
+
return "same";
|
|
418
|
+
}
|
|
419
|
+
if (stored.major !== current.major) {
|
|
420
|
+
return "major";
|
|
421
|
+
}
|
|
422
|
+
return "minor";
|
|
423
|
+
};
|
|
424
|
+
var buildStepIdMap = (definition) => {
|
|
425
|
+
const map = /* @__PURE__ */ new Map();
|
|
426
|
+
definition.steps.forEach((step, index) => {
|
|
427
|
+
map.set(step.id, index);
|
|
428
|
+
});
|
|
429
|
+
return map;
|
|
430
|
+
};
|
|
431
|
+
var handleVersionMismatch = (params) => {
|
|
432
|
+
const {
|
|
433
|
+
storedState,
|
|
434
|
+
storedVersion,
|
|
435
|
+
definition,
|
|
436
|
+
currentVersion,
|
|
437
|
+
stepIdMap,
|
|
438
|
+
now
|
|
439
|
+
} = params;
|
|
440
|
+
const comparison = compareVersions(storedVersion, currentVersion);
|
|
441
|
+
if (comparison === "same") {
|
|
442
|
+
return {
|
|
443
|
+
state: storedState,
|
|
444
|
+
action: "continued"
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
if (storedState.status === "completed" || storedState.status === "cancelled") {
|
|
448
|
+
return {
|
|
449
|
+
state: {
|
|
450
|
+
...storedState,
|
|
451
|
+
version: serializeVersion(currentVersion)
|
|
452
|
+
},
|
|
453
|
+
action: "continued"
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
if (storedState.status === "idle" || storedState.stepIndex < 0) {
|
|
457
|
+
return {
|
|
458
|
+
state: {
|
|
459
|
+
...storedState,
|
|
460
|
+
version: serializeVersion(currentVersion),
|
|
461
|
+
stepIndex: -1
|
|
462
|
+
},
|
|
463
|
+
action: "continued"
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
if (comparison === "minor") {
|
|
467
|
+
const resolvedState = resolveMinorMismatch({
|
|
468
|
+
storedState,
|
|
469
|
+
currentVersion,
|
|
470
|
+
stepIdMap,
|
|
471
|
+
definition
|
|
472
|
+
});
|
|
473
|
+
if (resolvedState) {
|
|
474
|
+
return { state: resolvedState, action: "continued" };
|
|
475
|
+
}
|
|
476
|
+
return {
|
|
477
|
+
state: createResetState(currentVersion, now),
|
|
478
|
+
action: "reset"
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
if (definition.migrate) {
|
|
482
|
+
const migrationCtx = {
|
|
483
|
+
oldState: storedState,
|
|
484
|
+
oldVersion: storedVersion,
|
|
485
|
+
newVersion: currentVersion,
|
|
486
|
+
stepIdMap,
|
|
487
|
+
definition
|
|
488
|
+
};
|
|
489
|
+
try {
|
|
490
|
+
const migrated = definition.migrate(migrationCtx);
|
|
491
|
+
if (migrated) {
|
|
492
|
+
return {
|
|
493
|
+
state: {
|
|
494
|
+
...migrated,
|
|
495
|
+
version: serializeVersion(currentVersion)
|
|
496
|
+
},
|
|
497
|
+
action: "migrated"
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
} catch (error) {
|
|
501
|
+
console.warn("[tour][version] Migration function threw error", error);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return {
|
|
505
|
+
state: createResetState(currentVersion, now),
|
|
506
|
+
action: "reset"
|
|
507
|
+
};
|
|
508
|
+
};
|
|
509
|
+
var resolveMinorMismatch = (params) => {
|
|
510
|
+
const { storedState, currentVersion, stepIdMap, definition } = params;
|
|
511
|
+
if (storedState.stepId) {
|
|
512
|
+
const newIndex = stepIdMap.get(storedState.stepId);
|
|
513
|
+
if (newIndex !== void 0) {
|
|
514
|
+
return {
|
|
515
|
+
...storedState,
|
|
516
|
+
stepIndex: newIndex,
|
|
517
|
+
version: serializeVersion(currentVersion)
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (storedState.stepIndex >= 0 && storedState.stepIndex < definition.steps.length) {
|
|
522
|
+
return {
|
|
523
|
+
...storedState,
|
|
524
|
+
version: serializeVersion(currentVersion),
|
|
525
|
+
stepId: definition.steps[storedState.stepIndex].id
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
return null;
|
|
529
|
+
};
|
|
530
|
+
var createResetState = (version, now) => ({
|
|
531
|
+
status: "idle",
|
|
532
|
+
stepIndex: -1,
|
|
533
|
+
version: serializeVersion(version),
|
|
534
|
+
updatedAt: now()
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// src/state.ts
|
|
538
|
+
var DEFAULT_STORAGE_PREFIX = "tour";
|
|
539
|
+
var clampIndex = (value, max) => {
|
|
540
|
+
if (value < 0) return 0;
|
|
541
|
+
if (value > max) return max;
|
|
542
|
+
return value;
|
|
543
|
+
};
|
|
544
|
+
var cloneState = (state) => ({ ...state });
|
|
545
|
+
var getStepDirection = (previousIndex, currentIndex) => {
|
|
546
|
+
if (currentIndex > previousIndex) return "forward";
|
|
547
|
+
if (currentIndex < previousIndex) return "backward";
|
|
548
|
+
return "none";
|
|
549
|
+
};
|
|
550
|
+
var getStepEnterReason = (previousState, currentState, direction) => {
|
|
551
|
+
if (previousState.status === "idle" && currentState.status === "running") {
|
|
552
|
+
return "start";
|
|
553
|
+
}
|
|
554
|
+
if (previousState.status === "paused" && currentState.status === "running") {
|
|
555
|
+
return "resume";
|
|
556
|
+
}
|
|
557
|
+
if (direction === "forward") return "advance";
|
|
558
|
+
if (direction === "backward") return "back";
|
|
559
|
+
return "jump";
|
|
560
|
+
};
|
|
561
|
+
var getStepExitReason = (currentState, direction) => {
|
|
562
|
+
if (currentState.status === "paused") return "pause";
|
|
563
|
+
if (currentState.status === "cancelled") return "cancel";
|
|
564
|
+
if (currentState.status === "completed") return "complete";
|
|
565
|
+
if (direction === "forward") return "advance";
|
|
566
|
+
if (direction === "backward") return "back";
|
|
567
|
+
return "unknown";
|
|
568
|
+
};
|
|
569
|
+
var getStepCompleteReason = (currentState) => {
|
|
570
|
+
if (currentState.status === "completed") {
|
|
571
|
+
return "flowComplete";
|
|
572
|
+
}
|
|
573
|
+
return "advance";
|
|
574
|
+
};
|
|
575
|
+
var createInitialState = (definition, now) => ({
|
|
576
|
+
status: "idle",
|
|
577
|
+
stepIndex: -1,
|
|
578
|
+
version: serializeVersion(definition.version),
|
|
579
|
+
updatedAt: now()
|
|
580
|
+
});
|
|
581
|
+
var getStep = (definition, index) => {
|
|
582
|
+
if (index < 0) return null;
|
|
583
|
+
return definition.steps[index] ?? null;
|
|
584
|
+
};
|
|
585
|
+
var snapshotFromState = (state) => ({
|
|
586
|
+
version: state.version,
|
|
587
|
+
value: cloneState(state),
|
|
588
|
+
updatedAt: state.updatedAt
|
|
589
|
+
});
|
|
590
|
+
var isFlowStateSnapshot = (snapshot) => {
|
|
591
|
+
if (!snapshot) return false;
|
|
592
|
+
const { value } = snapshot;
|
|
593
|
+
if (typeof value !== "object" || value === null) {
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
const flowValue = value;
|
|
597
|
+
const hasValidVersion = typeof flowValue.version === "string" || typeof flowValue.version === "number";
|
|
598
|
+
return typeof flowValue.status === "string" && typeof flowValue.stepIndex === "number" && hasValidVersion;
|
|
599
|
+
};
|
|
600
|
+
var createFlowStore = (definition, flowOptions = {}) => {
|
|
601
|
+
const {
|
|
602
|
+
storageAdapter,
|
|
603
|
+
storageKey = `${DEFAULT_STORAGE_PREFIX}:${definition.id}`,
|
|
604
|
+
eventBus = createEventBus(),
|
|
605
|
+
now = () => Date.now(),
|
|
606
|
+
persistOnChange = true,
|
|
607
|
+
analytics,
|
|
608
|
+
onVersionMismatch
|
|
609
|
+
} = flowOptions;
|
|
610
|
+
const currentVersion = definition.version;
|
|
611
|
+
const stepIdMap = buildStepIdMap(definition);
|
|
612
|
+
let state = createInitialState(definition, now);
|
|
613
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
614
|
+
let destroyed = false;
|
|
615
|
+
let unsubscribeStorage = null;
|
|
616
|
+
let isHydrating = false;
|
|
617
|
+
let hasHydrated = !storageAdapter;
|
|
618
|
+
let pendingStartOptions;
|
|
619
|
+
const callAnalytics = (event, payload) => {
|
|
620
|
+
if (!analytics) return;
|
|
621
|
+
const eventName = String(event);
|
|
622
|
+
const handlerKey = `on${eventName.charAt(0).toUpperCase()}${eventName.slice(
|
|
623
|
+
1
|
|
624
|
+
)}`;
|
|
625
|
+
const handler = analytics[handlerKey];
|
|
626
|
+
handler?.(payload);
|
|
627
|
+
};
|
|
628
|
+
const emitEvent = (event, payload) => {
|
|
629
|
+
eventBus.emit(event, payload);
|
|
630
|
+
callAnalytics(event, payload);
|
|
631
|
+
};
|
|
632
|
+
const emitFlowError = (code, error, meta) => {
|
|
633
|
+
emitEvent("flowError", {
|
|
634
|
+
flow: definition,
|
|
635
|
+
state,
|
|
636
|
+
code,
|
|
637
|
+
error,
|
|
638
|
+
meta
|
|
639
|
+
});
|
|
640
|
+
};
|
|
641
|
+
const scheduleMicrotask = (fn) => {
|
|
642
|
+
if (typeof queueMicrotask === "function") {
|
|
643
|
+
queueMicrotask(fn);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
Promise.resolve().then(fn).catch((error) => {
|
|
647
|
+
console.warn("[tour][flow] async scheduling failed", error);
|
|
648
|
+
emitFlowError("async.schedule_failed", error, {
|
|
649
|
+
operation: "queueMicrotaskFallback"
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
};
|
|
653
|
+
const runPendingStart = () => {
|
|
654
|
+
if (!pendingStartOptions) return;
|
|
655
|
+
const pending = pendingStartOptions;
|
|
656
|
+
pendingStartOptions = void 0;
|
|
657
|
+
scheduleMicrotask(() => {
|
|
658
|
+
start(pending);
|
|
659
|
+
});
|
|
660
|
+
};
|
|
661
|
+
const notifySubscribers = () => {
|
|
662
|
+
for (const listener of Array.from(subscribers)) {
|
|
663
|
+
listener(state);
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
const persistState = (nextState) => {
|
|
667
|
+
if (!storageAdapter || !persistOnChange) return;
|
|
668
|
+
if (isHydrating) return;
|
|
669
|
+
const result = storageAdapter.set(storageKey, snapshotFromState(nextState));
|
|
670
|
+
if (result instanceof Promise) {
|
|
671
|
+
result.catch((error) => {
|
|
672
|
+
console.warn("[tour][storage] Failed to persist flow state", error);
|
|
673
|
+
emitFlowError("storage.persist_failed", error, { key: storageKey });
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
const emitLifecycleEvents = (previousState, currentState, lifecycleOptions) => {
|
|
678
|
+
if (previousState.status !== currentState.status) {
|
|
679
|
+
if (currentState.status === "running") {
|
|
680
|
+
const payload = { flow: definition, state: currentState };
|
|
681
|
+
if (previousState.status === "paused") {
|
|
682
|
+
emitEvent("flowResume", payload);
|
|
683
|
+
} else {
|
|
684
|
+
emitEvent("flowStart", payload);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (currentState.status === "paused") {
|
|
688
|
+
emitEvent("flowPause", { flow: definition, state: currentState });
|
|
689
|
+
}
|
|
690
|
+
if (currentState.status === "cancelled") {
|
|
691
|
+
emitEvent("flowCancel", {
|
|
692
|
+
flow: definition,
|
|
693
|
+
state: currentState,
|
|
694
|
+
reason: lifecycleOptions?.cancelReason
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
if (currentState.status === "completed") {
|
|
698
|
+
emitEvent("flowComplete", { flow: definition, state: currentState });
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
const previousStep = getStep(definition, previousState.stepIndex);
|
|
702
|
+
const currentStep = getStep(definition, currentState.stepIndex);
|
|
703
|
+
const direction = getStepDirection(
|
|
704
|
+
previousState.stepIndex,
|
|
705
|
+
currentState.stepIndex
|
|
706
|
+
);
|
|
707
|
+
const shouldEmitStepExit = previousStep !== null && (direction !== "none" || currentState.status !== "running");
|
|
708
|
+
if (shouldEmitStepExit) {
|
|
709
|
+
const exitedStep = previousStep;
|
|
710
|
+
emitEvent("stepExit", {
|
|
711
|
+
flow: definition,
|
|
712
|
+
state: currentState,
|
|
713
|
+
currentStep,
|
|
714
|
+
currentStepIndex: currentState.stepIndex,
|
|
715
|
+
previousStep: exitedStep,
|
|
716
|
+
previousStepIndex: previousState.stepIndex,
|
|
717
|
+
direction,
|
|
718
|
+
reason: getStepExitReason(currentState, direction)
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
const shouldEmitStepComplete = previousStep !== null && (direction === "forward" || currentState.status === "completed" && previousState.status !== "completed");
|
|
722
|
+
if (shouldEmitStepComplete) {
|
|
723
|
+
const completedStep = previousStep;
|
|
724
|
+
emitEvent("stepComplete", {
|
|
725
|
+
flow: definition,
|
|
726
|
+
state: currentState,
|
|
727
|
+
currentStep,
|
|
728
|
+
currentStepIndex: currentState.stepIndex,
|
|
729
|
+
previousStep: completedStep,
|
|
730
|
+
previousStepIndex: previousState.stepIndex,
|
|
731
|
+
direction,
|
|
732
|
+
reason: getStepCompleteReason(currentState)
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
const shouldEmitStepEnter = currentState.status === "running" && currentStep !== null && (direction !== "none" || previousState.status !== "running");
|
|
736
|
+
if (shouldEmitStepEnter) {
|
|
737
|
+
const enteredStep = currentStep;
|
|
738
|
+
emitEvent("stepEnter", {
|
|
739
|
+
flow: definition,
|
|
740
|
+
state: currentState,
|
|
741
|
+
currentStep: enteredStep,
|
|
742
|
+
currentStepIndex: currentState.stepIndex,
|
|
743
|
+
previousStep,
|
|
744
|
+
previousStepIndex: previousState.stepIndex,
|
|
745
|
+
direction,
|
|
746
|
+
reason: getStepEnterReason(previousState, currentState, direction)
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
if (previousState.stepIndex !== currentState.stepIndex || previousState.status !== currentState.status) {
|
|
750
|
+
emitEvent("stepChange", {
|
|
751
|
+
flow: definition,
|
|
752
|
+
state: currentState,
|
|
753
|
+
step: currentStep,
|
|
754
|
+
previousStep
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
emitEvent("stateChange", { flow: definition, state: currentState });
|
|
758
|
+
};
|
|
759
|
+
const commit = (nextState, commitConfig = {}) => {
|
|
760
|
+
const {
|
|
761
|
+
persist: shouldPersist = true,
|
|
762
|
+
emitLifecycle = true,
|
|
763
|
+
preserveTimestamp = false,
|
|
764
|
+
cancelReason
|
|
765
|
+
} = commitConfig;
|
|
766
|
+
if (destroyed) {
|
|
767
|
+
const error = new Error("Flow store has been destroyed");
|
|
768
|
+
emitFlowError("flow.store_destroyed", error, { operation: "commit" });
|
|
769
|
+
throw error;
|
|
770
|
+
}
|
|
771
|
+
const previousState = state;
|
|
772
|
+
const timestamp = preserveTimestamp ? nextState.updatedAt : now();
|
|
773
|
+
const currentStepId = nextState.stepIndex >= 0 ? definition.steps[nextState.stepIndex]?.id : void 0;
|
|
774
|
+
const hydratedNext = {
|
|
775
|
+
...nextState,
|
|
776
|
+
version: serializeVersion(currentVersion),
|
|
777
|
+
stepId: currentStepId,
|
|
778
|
+
updatedAt: timestamp
|
|
779
|
+
};
|
|
780
|
+
const changed = previousState.status !== hydratedNext.status || previousState.stepIndex !== hydratedNext.stepIndex || previousState.updatedAt !== hydratedNext.updatedAt;
|
|
781
|
+
if (!changed) {
|
|
782
|
+
return state;
|
|
783
|
+
}
|
|
784
|
+
state = hydratedNext;
|
|
785
|
+
if (shouldPersist) {
|
|
786
|
+
persistState(state);
|
|
787
|
+
}
|
|
788
|
+
if (emitLifecycle) {
|
|
789
|
+
emitLifecycleEvents(previousState, state, { cancelReason });
|
|
790
|
+
}
|
|
791
|
+
notifySubscribers();
|
|
792
|
+
return state;
|
|
793
|
+
};
|
|
794
|
+
const hydrateFromStorage = async () => {
|
|
795
|
+
if (!storageAdapter || isHydrating) return;
|
|
796
|
+
isHydrating = true;
|
|
797
|
+
try {
|
|
798
|
+
const snapshot = await resolveMaybePromise(
|
|
799
|
+
storageAdapter.get(storageKey)
|
|
800
|
+
);
|
|
801
|
+
if (!snapshot || !isFlowStateSnapshot(snapshot)) return;
|
|
802
|
+
const storedVersion = parseVersion(snapshot.version);
|
|
803
|
+
const storedState = {
|
|
804
|
+
...snapshot.value,
|
|
805
|
+
// Normalize version to string format if it was legacy number
|
|
806
|
+
version: typeof snapshot.value.version === "number" ? serializeVersion({ major: snapshot.value.version, minor: 0 }) : snapshot.value.version
|
|
807
|
+
};
|
|
808
|
+
const { state: resolvedState, action } = handleVersionMismatch({
|
|
809
|
+
storedState,
|
|
810
|
+
storedVersion,
|
|
811
|
+
definition,
|
|
812
|
+
currentVersion,
|
|
813
|
+
stepIdMap,
|
|
814
|
+
now
|
|
815
|
+
});
|
|
816
|
+
if (action !== "continued" || serializeVersion(storedVersion) !== serializeVersion(currentVersion)) {
|
|
817
|
+
const mismatchInfo = {
|
|
818
|
+
flowId: definition.id,
|
|
819
|
+
oldVersion: storedVersion,
|
|
820
|
+
newVersion: currentVersion,
|
|
821
|
+
action,
|
|
822
|
+
resolvedStepId: resolvedState.stepIndex >= 0 ? definition.steps[resolvedState.stepIndex]?.id : void 0,
|
|
823
|
+
resolvedStepIndex: resolvedState.stepIndex >= 0 ? resolvedState.stepIndex : void 0
|
|
824
|
+
};
|
|
825
|
+
emitEvent("versionMismatch", { ...mismatchInfo, flow: definition });
|
|
826
|
+
onVersionMismatch?.(mismatchInfo);
|
|
827
|
+
if (action === "reset") {
|
|
828
|
+
state = resolvedState;
|
|
829
|
+
notifySubscribers();
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
commit(resolvedState, {
|
|
834
|
+
persist: action !== "continued",
|
|
835
|
+
// Persist if we migrated
|
|
836
|
+
preserveTimestamp: action === "continued"
|
|
837
|
+
});
|
|
838
|
+
} catch (error) {
|
|
839
|
+
console.warn("[tour][storage] failed to hydrate flow state", error);
|
|
840
|
+
emitFlowError("storage.hydrate_failed", error, { key: storageKey });
|
|
841
|
+
} finally {
|
|
842
|
+
isHydrating = false;
|
|
843
|
+
hasHydrated = true;
|
|
844
|
+
runPendingStart();
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
if (storageAdapter) {
|
|
848
|
+
void hydrateFromStorage();
|
|
849
|
+
if (storageAdapter.subscribe) {
|
|
850
|
+
unsubscribeStorage = storageAdapter.subscribe(() => {
|
|
851
|
+
void hydrateFromStorage();
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
const start = (startConfig) => {
|
|
856
|
+
if (!hasHydrated) {
|
|
857
|
+
pendingStartOptions = startConfig ?? {};
|
|
858
|
+
if (!isHydrating) {
|
|
859
|
+
void hydrateFromStorage();
|
|
860
|
+
}
|
|
861
|
+
return state;
|
|
862
|
+
}
|
|
863
|
+
const { fromStepId, fromStepIndex, resume } = startConfig ?? {};
|
|
864
|
+
if (resume && (state.status === "paused" || state.status === "running")) {
|
|
865
|
+
return resumeFlow();
|
|
866
|
+
}
|
|
867
|
+
let targetIndex = 0;
|
|
868
|
+
if (typeof fromStepIndex === "number") {
|
|
869
|
+
targetIndex = clampIndex(fromStepIndex, definition.steps.length - 1);
|
|
870
|
+
} else if (fromStepId) {
|
|
871
|
+
const index = definition.steps.findIndex((step) => step.id === fromStepId);
|
|
872
|
+
if (index === -1) {
|
|
873
|
+
const error = new Error(
|
|
874
|
+
`Step with id "${fromStepId}" not found in flow ${definition.id}`
|
|
875
|
+
);
|
|
876
|
+
emitFlowError("flow.step_not_found", error, {
|
|
877
|
+
stepId: fromStepId,
|
|
878
|
+
operation: "start"
|
|
879
|
+
});
|
|
880
|
+
throw error;
|
|
881
|
+
}
|
|
882
|
+
targetIndex = index;
|
|
883
|
+
} else if (state.stepIndex >= 0) {
|
|
884
|
+
const canResumeFromStoredStep = state.status === "running" || state.status === "paused" || state.status === "cancelled" && state.cancelReason === "keyboard";
|
|
885
|
+
if (canResumeFromStoredStep) {
|
|
886
|
+
targetIndex = clampIndex(state.stepIndex, definition.steps.length - 1);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return commit({
|
|
890
|
+
status: "running",
|
|
891
|
+
stepIndex: targetIndex,
|
|
892
|
+
version: state.version,
|
|
893
|
+
updatedAt: state.updatedAt
|
|
894
|
+
});
|
|
895
|
+
};
|
|
896
|
+
const next = () => {
|
|
897
|
+
if (state.status !== "running") {
|
|
898
|
+
return state;
|
|
899
|
+
}
|
|
900
|
+
if (state.stepIndex >= definition.steps.length - 1) {
|
|
901
|
+
return complete();
|
|
902
|
+
}
|
|
903
|
+
return commit({
|
|
904
|
+
status: "running",
|
|
905
|
+
stepIndex: state.stepIndex + 1,
|
|
906
|
+
version: state.version,
|
|
907
|
+
updatedAt: state.updatedAt
|
|
908
|
+
});
|
|
909
|
+
};
|
|
910
|
+
const back = () => {
|
|
911
|
+
if (state.status !== "running") {
|
|
912
|
+
return state;
|
|
913
|
+
}
|
|
914
|
+
if (state.stepIndex <= 0) {
|
|
915
|
+
return state;
|
|
916
|
+
}
|
|
917
|
+
return commit({
|
|
918
|
+
status: "running",
|
|
919
|
+
stepIndex: state.stepIndex - 1,
|
|
920
|
+
version: state.version,
|
|
921
|
+
updatedAt: state.updatedAt
|
|
922
|
+
});
|
|
923
|
+
};
|
|
924
|
+
const goToStep = (step) => {
|
|
925
|
+
if (state.status !== "running") {
|
|
926
|
+
return state;
|
|
927
|
+
}
|
|
928
|
+
let targetIndex;
|
|
929
|
+
if (typeof step === "number") {
|
|
930
|
+
targetIndex = clampIndex(step, definition.steps.length - 1);
|
|
931
|
+
} else {
|
|
932
|
+
const index = definition.steps.findIndex((item) => item.id === step);
|
|
933
|
+
if (index === -1) {
|
|
934
|
+
const error = new Error(
|
|
935
|
+
`Step with id "${step}" not found in flow ${definition.id}`
|
|
936
|
+
);
|
|
937
|
+
emitFlowError("flow.step_not_found", error, {
|
|
938
|
+
stepId: step,
|
|
939
|
+
operation: "goToStep"
|
|
940
|
+
});
|
|
941
|
+
throw error;
|
|
942
|
+
}
|
|
943
|
+
targetIndex = index;
|
|
944
|
+
}
|
|
945
|
+
return commit({
|
|
946
|
+
status: "running",
|
|
947
|
+
stepIndex: targetIndex,
|
|
948
|
+
version: state.version,
|
|
949
|
+
updatedAt: state.updatedAt
|
|
950
|
+
});
|
|
951
|
+
};
|
|
952
|
+
const pause = () => {
|
|
953
|
+
if (state.status !== "running") {
|
|
954
|
+
return state;
|
|
955
|
+
}
|
|
956
|
+
return commit({
|
|
957
|
+
status: "paused",
|
|
958
|
+
stepIndex: state.stepIndex,
|
|
959
|
+
version: state.version,
|
|
960
|
+
updatedAt: state.updatedAt
|
|
961
|
+
});
|
|
962
|
+
};
|
|
963
|
+
const resumeFlow = () => {
|
|
964
|
+
if (state.status !== "paused") {
|
|
965
|
+
return state;
|
|
966
|
+
}
|
|
967
|
+
const index = clampIndex(
|
|
968
|
+
state.stepIndex < 0 ? 0 : state.stepIndex,
|
|
969
|
+
definition.steps.length - 1
|
|
970
|
+
);
|
|
971
|
+
return commit({
|
|
972
|
+
status: "running",
|
|
973
|
+
stepIndex: index,
|
|
974
|
+
version: state.version,
|
|
975
|
+
updatedAt: state.updatedAt
|
|
976
|
+
});
|
|
977
|
+
};
|
|
978
|
+
const cancel = (reason) => {
|
|
979
|
+
if (state.status === "cancelled") {
|
|
980
|
+
return state;
|
|
981
|
+
}
|
|
982
|
+
return commit(
|
|
983
|
+
{
|
|
984
|
+
status: "cancelled",
|
|
985
|
+
stepIndex: state.stepIndex,
|
|
986
|
+
version: state.version,
|
|
987
|
+
updatedAt: state.updatedAt,
|
|
988
|
+
cancelReason: reason
|
|
989
|
+
},
|
|
990
|
+
{ cancelReason: reason }
|
|
991
|
+
);
|
|
992
|
+
};
|
|
993
|
+
const complete = () => {
|
|
994
|
+
if (state.status === "completed") {
|
|
995
|
+
return state;
|
|
996
|
+
}
|
|
997
|
+
return commit({
|
|
998
|
+
status: "completed",
|
|
999
|
+
stepIndex: clampIndex(state.stepIndex, definition.steps.length - 1),
|
|
1000
|
+
version: state.version,
|
|
1001
|
+
updatedAt: state.updatedAt
|
|
1002
|
+
});
|
|
1003
|
+
};
|
|
1004
|
+
const subscribe = (listener) => {
|
|
1005
|
+
if (destroyed) {
|
|
1006
|
+
const error = new Error("Cannot subscribe to a destroyed flow store");
|
|
1007
|
+
emitFlowError("flow.store_destroyed", error, { operation: "subscribe" });
|
|
1008
|
+
throw error;
|
|
1009
|
+
}
|
|
1010
|
+
subscribers.add(listener);
|
|
1011
|
+
listener(state);
|
|
1012
|
+
return () => {
|
|
1013
|
+
subscribers.delete(listener);
|
|
1014
|
+
};
|
|
1015
|
+
};
|
|
1016
|
+
const destroy = () => {
|
|
1017
|
+
if (destroyed) return;
|
|
1018
|
+
destroyed = true;
|
|
1019
|
+
subscribers.clear();
|
|
1020
|
+
eventBus.clear();
|
|
1021
|
+
if (unsubscribeStorage) {
|
|
1022
|
+
unsubscribeStorage();
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
return {
|
|
1026
|
+
definition,
|
|
1027
|
+
events: eventBus,
|
|
1028
|
+
getState: () => state,
|
|
1029
|
+
start,
|
|
1030
|
+
next,
|
|
1031
|
+
back,
|
|
1032
|
+
goToStep,
|
|
1033
|
+
pause,
|
|
1034
|
+
resume: resumeFlow,
|
|
1035
|
+
cancel,
|
|
1036
|
+
complete,
|
|
1037
|
+
subscribe,
|
|
1038
|
+
destroy
|
|
1039
|
+
};
|
|
1040
|
+
};
|
|
1041
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1042
|
+
0 && (module.exports = {
|
|
1043
|
+
MemoryStorageAdapter,
|
|
1044
|
+
buildStepIdMap,
|
|
1045
|
+
compareVersions,
|
|
1046
|
+
createApiStorageAdapter,
|
|
1047
|
+
createEventBus,
|
|
1048
|
+
createFlow,
|
|
1049
|
+
createFlowStore,
|
|
1050
|
+
createLocalStorageAdapter,
|
|
1051
|
+
flowDefinitionSchema,
|
|
1052
|
+
parseVersion,
|
|
1053
|
+
resolveMaybePromise,
|
|
1054
|
+
serializeVersion,
|
|
1055
|
+
validateFlowDefinition
|
|
1056
|
+
});
|