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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +182 -186
  2. package/dist/connectors/immer/immer.cjs +2 -0
  3. package/dist/connectors/immer/immer.cjs.map +7 -0
  4. package/dist/connectors/immer/immer.d.cts +11 -0
  5. package/dist/connectors/immer/immer.d.ts +11 -0
  6. package/dist/connectors/immer/immer.js +2 -0
  7. package/dist/connectors/immer/immer.js.map +7 -0
  8. package/dist/connectors/immer/immer.types.d.cts +9 -0
  9. package/dist/connectors/immer/immer.types.d.ts +9 -0
  10. package/dist/core/errors.d.cts +57 -0
  11. package/dist/core/errors.d.ts +57 -0
  12. package/dist/core/helpers.d.cts +46 -0
  13. package/dist/core/helpers.d.ts +46 -0
  14. package/dist/core/machine.d.cts +9 -0
  15. package/dist/core/machine.d.ts +9 -0
  16. package/dist/core/runtime.d.cts +115 -0
  17. package/dist/core/runtime.d.ts +115 -0
  18. package/dist/core/runtime.types.d.cts +112 -0
  19. package/dist/core/runtime.types.d.ts +112 -0
  20. package/dist/core/store.d.cts +31 -0
  21. package/dist/core/store.d.ts +31 -0
  22. package/dist/core/types.d.cts +481 -0
  23. package/dist/core/types.d.ts +481 -0
  24. package/dist/diagnostics/diagnostics.helpers.d.cts +17 -0
  25. package/dist/diagnostics/diagnostics.helpers.d.ts +17 -0
  26. package/dist/diagnostics/diagnostics.types.d.cts +22 -0
  27. package/dist/diagnostics/diagnostics.types.d.ts +22 -0
  28. package/dist/graph/bag.types.d.cts +91 -0
  29. package/dist/graph/bag.types.d.ts +91 -0
  30. package/dist/graph/graph.d.cts +63 -0
  31. package/dist/graph/graph.d.ts +63 -0
  32. package/dist/graph/graph.types.d.cts +212 -0
  33. package/dist/graph/graph.types.d.ts +212 -0
  34. package/dist/graph/work.d.cts +81 -0
  35. package/dist/graph/work.d.ts +81 -0
  36. package/dist/index.cjs +1 -1
  37. package/dist/index.cjs.map +4 -4
  38. package/dist/index.d.cts +13 -5
  39. package/dist/index.d.ts +13 -5
  40. package/dist/index.js +1 -1
  41. package/dist/index.js.map +4 -4
  42. package/dist/linear/linear.d.cts +33 -0
  43. package/dist/linear/linear.d.ts +33 -0
  44. package/dist/linear/linear.types.d.cts +28 -0
  45. package/dist/linear/linear.types.d.ts +28 -0
  46. package/dist/plugins/analytics/analytics.d.cts +8 -0
  47. package/dist/plugins/analytics/analytics.d.ts +8 -0
  48. package/dist/plugins/analytics/analytics.helpers.d.cts +2 -0
  49. package/dist/plugins/analytics/analytics.helpers.d.ts +2 -0
  50. package/dist/plugins/analytics/analytics.types.d.cts +27 -0
  51. package/dist/plugins/analytics/analytics.types.d.ts +27 -0
  52. package/dist/plugins/execution-paths/execution-paths.d.cts +7 -4
  53. package/dist/plugins/execution-paths/execution-paths.d.ts +7 -4
  54. package/dist/plugins/execution-paths/execution-paths.types.d.cts +21 -0
  55. package/dist/plugins/execution-paths/execution-paths.types.d.ts +21 -0
  56. package/dist/plugins/index.cjs +2 -0
  57. package/dist/plugins/index.cjs.map +7 -0
  58. package/dist/plugins/index.d.cts +19 -0
  59. package/dist/plugins/index.d.ts +19 -0
  60. package/dist/plugins/index.js +2 -0
  61. package/dist/plugins/index.js.map +7 -0
  62. package/dist/plugins/persistence/persistence.d.cts +16 -0
  63. package/dist/plugins/persistence/persistence.d.ts +16 -0
  64. package/dist/plugins/persistence/persistence.helpers.d.cts +34 -0
  65. package/dist/plugins/persistence/persistence.helpers.d.ts +34 -0
  66. package/dist/plugins/persistence/persistence.types.d.cts +63 -0
  67. package/dist/plugins/persistence/persistence.types.d.ts +63 -0
  68. package/dist/plugins/replay/replay.d.cts +7 -0
  69. package/dist/plugins/replay/replay.d.ts +7 -0
  70. package/dist/plugins/replay/replay.helpers.d.cts +22 -0
  71. package/dist/plugins/replay/replay.helpers.d.ts +22 -0
  72. package/dist/plugins/replay/replay.types.d.cts +28 -0
  73. package/dist/plugins/replay/replay.types.d.ts +28 -0
  74. package/package.json +61 -79
  75. package/dist/journey-builder/index.d.cts +0 -5
  76. package/dist/journey-builder/index.d.ts +0 -5
  77. package/dist/journey-builder/types.d.cts +0 -126
  78. package/dist/journey-builder/types.d.ts +0 -126
  79. package/dist/journey-machine/async-state.d.cts +0 -12
  80. package/dist/journey-machine/async-state.d.ts +0 -12
  81. package/dist/journey-machine/computed.d.cts +0 -2
  82. package/dist/journey-machine/computed.d.ts +0 -2
  83. package/dist/journey-machine/controls.d.cts +0 -17
  84. package/dist/journey-machine/controls.d.ts +0 -17
  85. package/dist/journey-machine/helpers.d.cts +0 -52
  86. package/dist/journey-machine/helpers.d.ts +0 -52
  87. package/dist/journey-machine/index.d.cts +0 -3
  88. package/dist/journey-machine/index.d.ts +0 -3
  89. package/dist/journey-machine/navigation.d.cts +0 -32
  90. package/dist/journey-machine/navigation.d.ts +0 -32
  91. package/dist/journey-machine/plugin-controller.d.cts +0 -15
  92. package/dist/journey-machine/plugin-controller.d.ts +0 -15
  93. package/dist/journey-machine/resolve-journey-definition.d.cts +0 -2
  94. package/dist/journey-machine/resolve-journey-definition.d.ts +0 -2
  95. package/dist/journey-machine/runtime.d.cts +0 -33
  96. package/dist/journey-machine/runtime.d.ts +0 -33
  97. package/dist/journey-machine/send.d.cts +0 -9
  98. package/dist/journey-machine/send.d.ts +0 -9
  99. package/dist/plugins/analytics/index.cjs +0 -2
  100. package/dist/plugins/analytics/index.cjs.map +0 -7
  101. package/dist/plugins/analytics/index.d.cts +0 -25
  102. package/dist/plugins/analytics/index.d.ts +0 -25
  103. package/dist/plugins/analytics/index.js +0 -2
  104. package/dist/plugins/analytics/index.js.map +0 -7
  105. package/dist/plugins/autosave/index.cjs +0 -2
  106. package/dist/plugins/autosave/index.cjs.map +0 -7
  107. package/dist/plugins/autosave/index.d.cts +0 -17
  108. package/dist/plugins/autosave/index.d.ts +0 -17
  109. package/dist/plugins/autosave/index.js +0 -2
  110. package/dist/plugins/autosave/index.js.map +0 -7
  111. package/dist/plugins/diagnostics/diagnostics.d.cts +0 -3
  112. package/dist/plugins/diagnostics/diagnostics.d.ts +0 -3
  113. package/dist/plugins/diagnostics/index.cjs +0 -2
  114. package/dist/plugins/diagnostics/index.cjs.map +0 -7
  115. package/dist/plugins/diagnostics/index.d.cts +0 -15
  116. package/dist/plugins/diagnostics/index.d.ts +0 -15
  117. package/dist/plugins/diagnostics/index.js +0 -2
  118. package/dist/plugins/diagnostics/index.js.map +0 -7
  119. package/dist/plugins/execution-paths/index.cjs +0 -2
  120. package/dist/plugins/execution-paths/index.cjs.map +0 -7
  121. package/dist/plugins/execution-paths/index.d.cts +0 -18
  122. package/dist/plugins/execution-paths/index.d.ts +0 -18
  123. package/dist/plugins/execution-paths/index.js +0 -2
  124. package/dist/plugins/execution-paths/index.js.map +0 -7
  125. package/dist/plugins/persistence/controller.d.cts +0 -17
  126. package/dist/plugins/persistence/controller.d.ts +0 -17
  127. package/dist/plugins/persistence/index.cjs +0 -2
  128. package/dist/plugins/persistence/index.cjs.map +0 -7
  129. package/dist/plugins/persistence/index.d.cts +0 -12
  130. package/dist/plugins/persistence/index.d.ts +0 -12
  131. package/dist/plugins/persistence/index.js +0 -2
  132. package/dist/plugins/persistence/index.js.map +0 -7
  133. package/dist/plugins/replay/index.cjs +0 -2
  134. package/dist/plugins/replay/index.cjs.map +0 -7
  135. package/dist/plugins/replay/index.d.cts +0 -27
  136. package/dist/plugins/replay/index.d.ts +0 -27
  137. package/dist/plugins/replay/index.js +0 -2
  138. package/dist/plugins/replay/index.js.map +0 -7
  139. package/dist/types/analytics.types.d.cts +0 -33
  140. package/dist/types/analytics.types.d.ts +0 -33
  141. package/dist/types/autosave.types.d.cts +0 -23
  142. package/dist/types/autosave.types.d.ts +0 -23
  143. package/dist/types/diagnostics.types.d.cts +0 -38
  144. package/dist/types/diagnostics.types.d.ts +0 -38
  145. package/dist/types/index.d.cts +0 -9
  146. package/dist/types/index.d.ts +0 -9
  147. package/dist/types/journey.types.d.cts +0 -191
  148. package/dist/types/journey.types.d.ts +0 -191
  149. package/dist/types/machine.types.d.cts +0 -104
  150. package/dist/types/machine.types.d.ts +0 -104
  151. package/dist/types/observation.types.d.cts +0 -83
  152. package/dist/types/observation.types.d.ts +0 -83
  153. package/dist/types/persistence.types.d.cts +0 -34
  154. package/dist/types/persistence.types.d.ts +0 -34
  155. package/dist/types/replay.types.d.cts +0 -35
  156. package/dist/types/replay.types.d.ts +0 -35
  157. package/dist/types/transitions.types.d.cts +0 -122
  158. package/dist/types/transitions.types.d.ts +0 -122
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/plugins/analytics/analytics.helpers.ts", "../../src/plugins/analytics/analytics.ts", "../../src/plugins/execution-paths/execution-paths.ts", "../../../common/src/dev.ts", "../../src/core/errors.ts", "../../src/plugins/persistence/persistence.helpers.ts", "../../src/plugins/persistence/persistence.ts", "../../src/plugins/replay/replay.helpers.ts", "../../src/plugins/replay/replay.ts"],
4
+ "sourcesContent": ["/** Ring-buffer capacity for the recent-events buffer. */\nexport const RECENT_EVENT_CAPACITY = 100;\n", "import { RECENT_EVENT_CAPACITY } from \"./analytics.helpers\";\n\nexport { RECENT_EVENT_CAPACITY } from \"./analytics.helpers\";\nimport type {\n AnalyticsApi,\n AnalyticsPluginOptions,\n AnalyticsRecentEvent,\n AnalyticsTrackedEvent\n} from \"./analytics.types\";\nimport type { JourneyPlugin } from \"../../core/types\";\n\n/**\n * Converts journey observations (transitions, lifecycle, blocked navigations,\n * errors) into analytics envelopes delivered to the configured sink.\n */\nexport function createAnalyticsPlugin(\n options: AnalyticsPluginOptions\n): JourneyPlugin<\"analytics\", AnalyticsApi, never> {\n const now = options.now ?? Date.now;\n\n return {\n name: \"analytics\",\n // Per-instance state lives inside `setup()` (called once per machine) so a\n // single plugin instance reused across machines never shares its buffer.\n setup(host) {\n let recent: AnalyticsRecentEvent[] = [];\n\n const record = (entry: AnalyticsRecentEvent) => {\n recent.push(entry);\n if (recent.length > RECENT_EVENT_CAPACITY) {\n recent = recent.slice(recent.length - RECENT_EVENT_CAPACITY);\n }\n };\n\n const trackSafely = (\n source: AnalyticsRecentEvent[\"source\"],\n name: string,\n payload: Record<string, unknown>\n ): AnalyticsTrackedEvent => {\n const tracked: AnalyticsTrackedEvent = {\n name,\n timestamp: now(),\n stepId: host.getSnapshot().currentStep?.id ?? null,\n payload\n };\n try {\n options.track(tracked);\n record({ source, tracked, success: true });\n } catch (error) {\n record({ source, tracked, success: false, error });\n // The error handler sits inside the guard: a throwing onError used to\n // escape trackSafely, which made \"the sink failed\" indistinguishable\n // from \"your error handler failed\" at the isolation boundary.\n try {\n options.onError?.(error, tracked);\n } catch (handlerError) {\n host.reportError(handlerError);\n }\n }\n return tracked;\n };\n\n host.onTransition(({ from, to }) => {\n trackSafely(\"lifecycle\", \"journey.transition\", { from, to });\n });\n host.onStatusChange(({ previous, current }) => {\n trackSafely(\"lifecycle\", `journey.${current}`, { previous });\n });\n host.onNavigationBlocked(({ reason, from, to }) => {\n trackSafely(\"lifecycle\", \"journey.navigationBlocked\", { reason, from, to });\n });\n host.onError(({ phase, stepId, error }) => {\n trackSafely(\"lifecycle\", \"journey.error\", { phase, stepId, error });\n });\n\n return {\n api: {\n trackAnalyticsEvent: (name, payload = {}) => trackSafely(\"custom\", name, payload),\n getRecentEvents: () => [...recent],\n clearRecentEvents: () => {\n recent = [];\n }\n }\n };\n }\n };\n}\n", "import type {\n ExecutionPathsApi,\n ExecutionPathsPluginOptions,\n ExecutionPathsSnapshot\n} from \"./execution-paths.types\";\nimport type { JourneyPlugin } from \"../../core/types\";\n\nexport type {\n ExecutionPathsApi,\n ExecutionPathsPluginOptions,\n ExecutionPathsSnapshot\n} from \"./execution-paths.types\";\n\n/** Retained finished runs. Diagnostic history, so a modest bound is enough. */\nconst DEFAULT_MAX_PATHS = 50;\n\n/**\n * First-party plugin: tracks execution paths through the journey. Observes\n * transitions via the plugin host and contributes\n * `machine.plugins[\"execution-paths\"]` and `snapshot.plugins[\"execution-paths\"]`.\n */\nexport function createExecutionPathsPlugin(\n options: ExecutionPathsPluginOptions = {}\n): JourneyPlugin<\"execution-paths\", ExecutionPathsApi, ExecutionPathsSnapshot> {\n const maxPaths = options.maxPaths ?? DEFAULT_MAX_PATHS;\n return {\n name: \"execution-paths\",\n setup(host) {\n let currentPath: string[] = [];\n let completedPaths: (readonly string[])[] = [];\n\n host.onTransition(({ to }) => {\n currentPath.push(to);\n });\n host.onStatusChange(({ current }) => {\n if ((current === \"completed\" || current === \"terminated\") && currentPath.length > 0) {\n completedPaths.push(Object.freeze([...currentPath]));\n // Newest kept: a machine that completes and restarts on a loop would\n // otherwise retain one frozen array per run, forever.\n if (completedPaths.length > maxPaths) {\n completedPaths = completedPaths.slice(completedPaths.length - maxPaths);\n }\n currentPath = [];\n }\n });\n\n return {\n api: {\n getCurrentPath: () => Object.freeze([...currentPath]),\n getCompletedPaths: () => Object.freeze([...completedPaths]),\n clearCompletedPaths: () => {\n completedPaths = [];\n }\n },\n deriveSnapshot: (_snapshot, previous) => {\n if (\n previous &&\n previous.currentPath.length === currentPath.length &&\n previous.completedPaths.length === completedPaths.length\n ) {\n return previous;\n }\n return Object.freeze({\n currentPath: Object.freeze([...currentPath]),\n completedPaths: Object.freeze([...completedPaths])\n });\n }\n };\n }\n };\n}\n", "/**\n * Environment detection for diagnostics that must not survive into production.\n *\n * Two resolvers live here and they answer deliberately different questions.\n * They are not interchangeable, and the difference is the point:\n *\n * - {@link isDevelopmentEnvironment} asks *\"should I print a developer\n * warning?\"* and is **permissive** \u2014 an unset `NODE_ENV` counts as\n * development, because an unconfigured environment is someone's machine and\n * the cost of a stray warning there is far lower than the cost of a silently\n * swallowed one.\n * - {@link resolveNonProductionEnvironment} asks *\"should I switch on a feature\n * that has a runtime cost?\"* and is **conservative** \u2014 an unset environment\n * counts as production, because auto-enabling a devtools bridge in an\n * unconfigured deployment leaks internal state to anything listening.\n *\n * Permissive for output, conservative for behaviour. Changing either default to\n * match the other would break one of those two properties.\n */\n\nimport { isRecord } from \"./predicates\";\n\ntype DiagnosticGlobal = typeof globalThis & {\n __DEV__?: unknown;\n process?: {\n env?: {\n NODE_ENV?: string;\n };\n };\n};\n\n/**\n * Reports whether developer-facing diagnostics should be emitted.\n *\n * A boolean `__DEV__` global wins outright \u2014 bundlers inline it, and test\n * suites set it to exercise warning paths that `NODE_ENV=test` would otherwise\n * suppress. Otherwise an unset or `\"development\"` `NODE_ENV` is development,\n * and anything else (including `\"test\"` and `\"production\"`) is not.\n *\n * @returns `true` when warnings and errors should reach the console.\n */\nexport const isDevelopmentEnvironment = (): boolean => {\n const diagnosticGlobal = globalThis as DiagnosticGlobal;\n if (typeof diagnosticGlobal.__DEV__ === \"boolean\") {\n return diagnosticGlobal.__DEV__;\n }\n\n const nodeEnv = diagnosticGlobal.process?.env?.NODE_ENV;\n return nodeEnv === undefined || nodeEnv === \"development\";\n};\n\n/**\n * Logs a warning, but only where {@link isDevelopmentEnvironment} holds.\n *\n * The `detail` argument is forwarded as a second console argument rather than\n * interpolated, so objects stay inspectable in devtools instead of collapsing\n * to `[object Object]`. Omitting it logs the message alone, avoiding a trailing\n * `undefined` in the output.\n *\n * @param message - The warning text.\n * @param detail - Optional structured context to log alongside the message.\n */\nexport const warnInDevelopment = (message: string, detail?: unknown): void => {\n if (!isDevelopmentEnvironment() || typeof console === \"undefined\") {\n return;\n }\n\n if (detail === undefined) {\n console.warn(message);\n return;\n }\n\n // `message` goes through a constant `%s` rather than standing as the format\n // string itself: callers build it by interpolation, so a `%` in an\n // interpolated value would otherwise be read as a directive and consume\n // `detail`.\n console.warn(\"%s\", message, detail);\n};\n\n/**\n * The subset of a bundler's `import.meta.env` this module reads.\n *\n * Callers pass `import.meta.env` in rather than this module reading it,\n * because `import.meta` is a syntax error in a CommonJS build \u2014 keeping it at\n * the call site lets the bundler that understands it do the substitution.\n */\nexport type NonProductionBundlerEnv = {\n /** Vite-style development flag. */\n DEV?: unknown;\n /** Vite-style production flag. Takes precedence over {@link NonProductionBundlerEnv.DEV}. */\n PROD?: unknown;\n};\n\ntype ProcessLike = { env?: { NODE_ENV?: string } };\n\nconst resolveImportMetaEnvironment = (\n bundlerEnv: NonProductionBundlerEnv | null | undefined\n): boolean | null => {\n if (!isRecord(bundlerEnv)) {\n return null;\n }\n if (bundlerEnv.PROD === true) {\n return false;\n }\n if (bundlerEnv.DEV === true) {\n return true;\n }\n return null;\n};\n\nconst resolveNodeEnvironment = (nodeEnv: string | undefined): boolean | null => {\n if (typeof nodeEnv !== \"string\") {\n return null;\n }\n return nodeEnv !== \"production\";\n};\n\nconst readAmbientNodeEnv = (): string | undefined => {\n const processLike = (globalThis as typeof globalThis & { process?: ProcessLike }).process;\n return processLike?.env?.NODE_ENV;\n};\n\n/**\n * Decides whether the current environment is something other than production,\n * for features that should not switch themselves on in a shipped app.\n *\n * Sources are consulted in order \u2014 bundler flags, then `NODE_ENV` \u2014 and the\n * first that gives a definite answer wins. When none does, the answer is\n * `false`: an environment that never said it was safe is treated as production.\n *\n * Passing `nodeEnv` explicitly, *including as `undefined`*, opts out of reading\n * the ambient `process.env.NODE_ENV`. That lets a caller say \"I have looked and\n * there is no value\" and get the conservative answer, rather than having this\n * function reach for a global the caller deliberately ignored.\n *\n * @param options - Environment sources. Omit entirely to read ambient `NODE_ENV`.\n * @param options.bundlerEnv - A bundler's `import.meta.env`, if available.\n * @param options.nodeEnv - An explicit `NODE_ENV`; presence of the key disables the ambient read.\n * @returns `true` only when a source positively indicates a non-production environment.\n */\nexport const resolveNonProductionEnvironment = (\n options: {\n bundlerEnv?: NonProductionBundlerEnv | null | undefined;\n nodeEnv?: string | undefined;\n } = {}\n): boolean => {\n const resolvedNodeEnv = \"nodeEnv\" in options ? options.nodeEnv : readAmbientNodeEnv();\n\n return (\n resolveImportMetaEnvironment(options.bundlerEnv ?? null) ??\n resolveNodeEnvironment(resolvedNodeEnv) ??\n false\n );\n};\n", "/**\n * Machine-readable reasons a journey can refuse to build or operate.\n *\n * Closed on purpose: consumers should be able to `switch` over it exhaustively.\n * Adding a member is a minor change; removing or repurposing one is not done\n * within a major.\n */\nexport type JourneyErrorCode =\n /** A definition declared no steps. */\n | \"empty-definition\"\n /** The same step id was declared twice. */\n | \"duplicate-step-id\"\n /** A step id was referenced that the definition does not declare. */\n | \"unknown-step\"\n /** `initial` names a step the definition does not declare. */\n | \"unknown-initial-step\"\n /** A transition's `from` or `to` names a step that does not exist. */\n | \"dangling-transition\"\n /** Two plugins claimed the same `name`. */\n | \"duplicate-plugin-name\"\n /** Persistence was requested but no usable storage could be resolved. */\n | \"storage-unavailable\"\n /** Navigation work's `commit` returned a promise; it must be synchronous. */\n | \"async-commit\"\n /** A transition or work entry declared `label` as something other than a non-empty string. */\n | \"invalid-label\"\n /** A transition or work entry declared `timeoutMs` as something other than a positive finite number. */\n | \"invalid-timeout\";\n\nexport type JourneyErrorDetails = {\n /** The offending step id, where one caused the failure. */\n readonly stepId?: string;\n /** The offending event name, for transition failures. */\n readonly event?: string;\n /** The offending plugin name, for plugin failures. */\n readonly pluginName?: string;\n};\n\n/**\n * Every error core throws itself.\n *\n * Before this existed, distinguishing \"duplicate plugin name\" from \"unknown step\n * in transition\" meant matching on the message text \u2014 which quietly made every\n * message a compatibility promise. `code` is the stable contract; `message` is\n * for humans and may be reworded in any release.\n *\n * Errors surfaced through `NavigationResult.error` and the `error` subscription\n * event stay `unknown`: those carry whatever the caller's own work threw, which\n * core cannot constrain.\n */\nexport class JourneyError extends Error {\n readonly code: JourneyErrorCode;\n readonly stepId: string | undefined;\n readonly event: string | undefined;\n readonly pluginName: string | undefined;\n\n constructor(\n code: JourneyErrorCode,\n message: string,\n details: JourneyErrorDetails = {},\n cause?: unknown\n ) {\n super(`journey: ${message}`, cause === undefined ? undefined : { cause });\n this.name = \"JourneyError\";\n this.code = code;\n this.stepId = details.stepId;\n this.event = details.event;\n this.pluginName = details.pluginName;\n }\n}\n\n/** Narrowing helper, so consumers need not import the class to test for it. */\nexport const isJourneyError = (value: unknown): value is JourneyError =>\n value instanceof JourneyError;\n", "import { JourneyError } from \"../../core/errors\";\nimport type { JourneyPersistedState, JourneyStorage, PersistenceReason } from \"./persistence.types\";\nimport type { JourneyPersistOption, JourneySnapshot } from \"../../core/types\";\n\n/** Every observation kind a save can be scheduled on. */\nexport const DEFAULT_SAVE_REASONS: readonly PersistenceReason[] = [\n \"context\",\n \"transition\",\n \"status\"\n];\n\n/** Omitted means \"write immediately\"; anything else is clamped to a whole >= 0. */\nexport function normalizeDebounceMs(value: number | undefined): number {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return 0;\n return Math.max(0, Math.trunc(value));\n}\n\nexport function buildPersistedState(snapshot: JourneySnapshot, now: number): JourneyPersistedState {\n return {\n status: snapshot.status,\n context: snapshot.context,\n timeline: snapshot.history.timeline,\n currentIndex: snapshot.history.currentIndex,\n savedAt: now\n };\n}\n\nconst JOURNEY_STATUSES = new Set([\n \"idle\",\n \"running\",\n \"paused\",\n \"completed\",\n \"terminated\"\n]) as ReadonlySet<string>;\n\n/**\n * Keys that must never survive `JSON.parse` into machine state.\n *\n * `JSON.parse` creates `__proto__` as an ordinary own property rather than\n * reassigning the prototype, so the parsed object is safe in isolation. It stops\n * being safe the moment application code spreads or `Object.assign`s it into a\n * fresh object \u2014 the own key is then copied as a *prototype assignment*. Storage\n * is attacker-reachable, so the shape is scrubbed before it can reach a context.\n */\nconst UNSAFE_KEYS = [\"__proto__\", \"constructor\", \"prototype\"] as const;\n\nfunction scrub(value: unknown, depth = 0): unknown {\n if (depth > 50 || value === null || typeof value !== \"object\") return value;\n if (Array.isArray(value)) return value.map((item) => scrub(item, depth + 1));\n\n const output: Record<string, unknown> = {};\n for (const [key, item] of Object.entries(value)) {\n if ((UNSAFE_KEYS as readonly string[]).includes(key)) continue;\n output[key] = scrub(item, depth + 1);\n }\n return output;\n}\n\n/**\n * Parses a stored value; malformed or foreign payloads yield `null`.\n *\n * Validation is deliberately total: this is the only gate between an untrusted\n * storage entry and machine state, and `readPersisted()` hands the result\n * straight to application code typed as `JourneyPersistedState`.\n */\nexport function parsePersistedState(raw: string | null): JourneyPersistedState | null {\n if (raw === null) return null;\n let value: unknown;\n try {\n value = JSON.parse(raw);\n } catch {\n return null;\n }\n if (typeof value !== \"object\" || value === null) return null;\n const candidate = value as Record<string, unknown>;\n if (\n typeof candidate.status !== \"string\" ||\n !JOURNEY_STATUSES.has(candidate.status) ||\n !Array.isArray(candidate.timeline) ||\n !candidate.timeline.every((id) => typeof id === \"string\") ||\n typeof candidate.currentIndex !== \"number\" ||\n !Number.isInteger(candidate.currentIndex) ||\n typeof candidate.savedAt !== \"number\" ||\n !Number.isFinite(candidate.savedAt)\n ) {\n return null;\n }\n return {\n status: candidate.status,\n context: scrub(candidate.context),\n timeline: candidate.timeline,\n currentIndex: candidate.currentIndex,\n savedAt: candidate.savedAt\n } as JourneyPersistedState;\n}\n\n/**\n * Resolves the `persist` option's storage; throws when none is available.\n *\n * Reading `globalThis.localStorage` can itself throw \u2014 a third-party iframe\n * with storage blocked, or Safari's Lockdown Mode \u2014 rather than returning\n * undefined. Unguarded, that surfaced as a raw `SecurityError` out of\n * `createLinearJourney`, which reads as a library crash rather than an\n * environment that cannot persist. The failure stays loud (silently disabling\n * persistence loses data with no signal) but is now a `journey:` error naming\n * the fix, with the original kept as `cause`.\n */\nexport function resolvePersistStorage(option: JourneyPersistOption): JourneyStorage {\n if (option.storage) return option.storage;\n\n let ambient: JourneyStorage | undefined;\n try {\n ambient = globalThis.localStorage as JourneyStorage | undefined;\n } catch (error) {\n throw new JourneyError(\n \"storage-unavailable\",\n \"localStorage access was blocked by the environment; pass persist.storage explicitly\",\n {},\n error\n );\n }\n\n if (!ambient) {\n throw new JourneyError(\n \"storage-unavailable\",\n \"persist.storage is required when localStorage is unavailable\"\n );\n }\n return ambient;\n}\n\n/**\n * Reads a persisted record that can seed this machine, or `null`. Restore is\n * best-effort by design: terminal-status records, records whose timeline\n * mentions a step the current definition no longer declares (definition\n * drift), and malformed/foreign payloads are all ignored rather than thrown.\n */\nexport function readRestorableState(\n option: JourneyPersistOption,\n isDeclaredStep: (id: string) => boolean\n): JourneyPersistedState | null {\n let raw: string | null;\n try {\n raw = resolvePersistStorage(option).getItem(option.key);\n } catch {\n return null;\n }\n const record = parsePersistedState(raw);\n if (!record) return null;\n if (record.status !== \"running\" && record.status !== \"paused\") return null;\n if (!Number.isInteger(record.currentIndex)) return null;\n if (record.currentIndex < 0 || record.currentIndex >= record.timeline.length) return null;\n for (const id of record.timeline) {\n if (typeof id !== \"string\" || !isDeclaredStep(id)) return null;\n }\n return record;\n}\n", "import { warnInDevelopment } from \"@rxova/journey-common/dev\";\nimport {\n buildPersistedState,\n DEFAULT_SAVE_REASONS,\n normalizeDebounceMs,\n parsePersistedState,\n resolvePersistStorage\n} from \"./persistence.helpers\";\nimport type {\n JourneyPersistedState,\n PersistenceApi,\n PersistencePluginOptions,\n PersistenceReason,\n PersistenceState\n} from \"./persistence.types\";\nimport type { JourneyPersistOption, JourneyPlugin } from \"../../core/types\";\n\nexport {\n buildPersistedState,\n DEFAULT_SAVE_REASONS,\n normalizeDebounceMs,\n parsePersistedState\n} from \"./persistence.helpers\";\n\nconst IDLE_STATE: PersistenceState = { status: \"idle\", lastSavedAt: null, error: null };\n\n/**\n * Persists a serializable slice of machine state (status, context, timeline) on\n * every transition, status change, and context change. `saveOn` narrows which\n * of those trigger a write; `debounceMs` collapses a burst of them into one.\n *\n * The plugin itself is save-side only (plugins observe, they cannot seed the\n * runtime). Restore happens through the creation-time `persist` option, which\n * reads a restorable record before the machine is built; `readPersisted()`\n * exposes the saved state for callers wiring this plugin explicitly.\n */\nexport function createPersistencePlugin(\n options: PersistencePluginOptions\n): JourneyPlugin<\"persistence\", PersistenceApi, PersistenceState> {\n const now = options.now ?? Date.now;\n const debounceMs = normalizeDebounceMs(options.debounceMs);\n const saveReasons = new Set<PersistenceReason>(options.saveOn ?? DEFAULT_SAVE_REASONS);\n // Per plugin *instance*, not per machine: mutable state is scoped to setup(),\n // but `options` is not \u2014 two machines sharing one instance write the same key\n // and silently clobber each other.\n let setupCount = 0;\n return {\n name: \"persistence\",\n setup(host) {\n setupCount += 1;\n if (setupCount > 1) {\n warnInDevelopment(\n `journey: persistence plugin instance shared by ${setupCount} machines; they overwrite key \"${options.key}\". Create one per machine.`\n );\n }\n let lastWritten: JourneyPersistedState | null = null;\n let state: PersistenceState = IDLE_STATE;\n let timer: ReturnType<typeof setTimeout> | null = null;\n let disposed = false;\n\n const succeeded = (written: JourneyPersistedState) => {\n lastWritten = written;\n state = { status: \"saved\", lastSavedAt: written.savedAt, error: null };\n };\n\n const failed = (error: unknown) => {\n state = { ...state, status: \"error\", error };\n host.reportError(error);\n };\n\n /**\n * Writes now. Returns a promise only so `flushPersisted` can be awaited;\n * the immediate path stays synchronous up to the `setItem` call, which is\n * what lets a throwing adapter surface through the runtime's listener\n * isolation exactly as it always did.\n */\n const save = (): void | Promise<void> => {\n const written = buildPersistedState(host.getSnapshot(), now());\n state = { ...state, status: \"saving\" };\n let result: void | Promise<void>;\n try {\n result = options.storage.setItem(options.key, JSON.stringify(written));\n } catch (error) {\n // Recorded here, but rethrown so the runtime's listener isolation\n // reports it exactly as it always did.\n state = { ...state, status: \"error\", error };\n throw error;\n }\n // `setItem` may be async. Discarding that promise turned a rejecting\n // adapter into an unhandled rejection, which terminates the process\n // under Node's default `--unhandled-rejections=throw`.\n if (result !== undefined) {\n return result.then(() => succeeded(written), failed);\n }\n succeeded(written);\n };\n\n const cancelTimer = () => {\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n };\n\n /**\n * A debounced write happens on a timer, so there is no synchronous caller\n * left to throw to \u2014 its failures land in `state` and `reportError`\n * instead. The immediate path keeps throwing.\n */\n const runDebounced = () => {\n timer = null;\n try {\n const result = save();\n if (result !== undefined) void result;\n } catch (error) {\n failed(error);\n }\n };\n\n const onObservation = (reason: PersistenceReason) => {\n if (disposed || !saveReasons.has(reason)) return;\n if (debounceMs === 0) {\n void save();\n return;\n }\n cancelTimer();\n state = { ...state, status: \"pending\" };\n timer = setTimeout(runDebounced, debounceMs);\n };\n\n host.onContextChange(() => onObservation(\"context\"));\n host.onTransition(() => onObservation(\"transition\"));\n host.onStatusChange(({ current }) => {\n if (current === \"terminated\" && options.clearOnTerminate) {\n cancelTimer();\n lastWritten = null;\n state = IDLE_STATE;\n options.storage.removeItem(options.key);\n return;\n }\n onObservation(\"status\");\n });\n host.onDispose(() => {\n disposed = true;\n cancelTimer();\n });\n\n return {\n api: {\n inspectPersistedState: () => lastWritten,\n getPersistenceState: () => state,\n readPersisted: () => parsePersistedState(options.storage.getItem(options.key)),\n clearPersisted: () => {\n cancelTimer();\n lastWritten = null;\n state = IDLE_STATE;\n // Called directly by user code; record the failure here rather than\n // throwing out of an API whose sibling writes are all contained.\n try {\n options.storage.removeItem(options.key);\n } catch (error) {\n failed(error);\n }\n },\n flushPersisted: async () => {\n cancelTimer();\n try {\n await save();\n } catch (error) {\n failed(error);\n }\n }\n },\n deriveSnapshot: (_snapshot, previous) => (previous === state ? previous : state)\n };\n }\n };\n}\n\n/** Expands the creation-time `persist` option into the persistence plugin. */\nexport function persistOptionToPlugin(\n option: JourneyPersistOption\n): JourneyPlugin<\"persistence\", PersistenceApi, PersistenceState> {\n return createPersistencePlugin({ key: option.key, storage: resolvePersistStorage(option) });\n}\n", "import type { ReplayExportOptions, ReplaySession } from \"./replay.types\";\n\nexport function normalizeMaxEntries(value: number | undefined): number {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return 500;\n return Math.max(1, Math.trunc(value));\n}\n\n/**\n * Depth past which nesting is truncated instead of walked. Guards the call\n * stack: a long parent/child chain used to overflow it, and the resulting\n * RangeError was swallowed by listener isolation, silently dropping the entry.\n */\nconst DEFAULT_MAX_DEPTH = 100;\n\nconst CIRCULAR = \"[circular]\";\nconst TRUNCATED = \"[max-depth]\";\n\ntype WalkState = {\n /** Completed conversions, so a shared subtree is walked exactly once. */\n readonly memo: Map<object, unknown>;\n /** Objects on the current path \u2014 these, and only these, are cycles. */\n readonly inProgress: Set<object>;\n readonly maxDepth: number;\n};\n\nfunction walk(value: unknown, state: WalkState, depth: number): unknown {\n if (\n value === null ||\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"boolean\"\n ) {\n return value;\n }\n if (typeof value === \"bigint\") return value.toString();\n if (typeof value === \"undefined\") return null;\n if (typeof value === \"function\" || typeof value === \"symbol\") {\n return `[unsupported:${typeof value}]`;\n }\n if (value instanceof Error) {\n return {\n name: value.name,\n message: value.message,\n ...(value.stack ? { stack: value.stack } : {})\n };\n }\n if (value instanceof Date) return value.toISOString();\n if (typeof value !== \"object\") return String(value);\n\n const object = value as object;\n if (state.memo.has(object)) return state.memo.get(object);\n if (state.inProgress.has(object)) return CIRCULAR;\n if (depth >= state.maxDepth) return TRUNCATED;\n\n state.inProgress.add(object);\n let result: unknown;\n if (Array.isArray(value)) {\n result = value.map((item) => walk(item, state, depth + 1));\n } else {\n const output: Record<string, unknown> = {};\n for (const [key, item] of Object.entries(value)) {\n output[key] = walk(item, state, depth + 1);\n }\n result = output;\n }\n state.inProgress.delete(object);\n state.memo.set(object, result);\n return result;\n}\n\n/**\n * Converts any value into a JSON-safe structure (circular refs, errors, dates).\n *\n * Memoized by reference, which is what keeps it linear in the number of\n * distinct objects. The previous walk removed each node from its `seen` set on\n * the way back up \u2014 correct for cycles, but it meant a shared subtree was\n * re-traversed once per path reaching it, so a context with diamond-shaped\n * sharing cost 2^N. Since the replay plugin serializes the whole snapshot on\n * every transition by default, that ran on the hot path: measured 21 ms at\n * depth 14, 241 ms at 18, 932 ms at 20, and no completion at 26.\n *\n * Arrays are now cycle-tracked too. They were checked before the object branch\n * and never entered `seen`, so a self-referencing array recursed until the\n * stack gave out.\n */\nexport function toSerializable(value: unknown, options: { maxDepth?: number } = {}): unknown {\n return walk(\n value,\n {\n memo: new Map<object, unknown>(),\n inProgress: new Set<object>(),\n maxDepth: options.maxDepth ?? DEFAULT_MAX_DEPTH\n },\n 0\n );\n}\n\n/** Serializes a replay session into a JSON string safe for logging or export. */\nexport function serializeReplaySession(\n session: ReplaySession,\n options?: ReplayExportOptions\n): string {\n return JSON.stringify(toSerializable(session), null, options?.pretty ? 2 : undefined);\n}\n", "import { normalizeMaxEntries, serializeReplaySession, toSerializable } from \"./replay.helpers\";\nimport type { ReplayApi, ReplayEntry, ReplayEntryKind, ReplayPluginOptions } from \"./replay.types\";\nimport type { JourneyPlugin } from \"../../core/types\";\n\nexport { normalizeMaxEntries, serializeReplaySession, toSerializable } from \"./replay.helpers\";\n\n/** Records snapshot and lifecycle activity into an exportable replay session. */\nexport function createReplayPlugin(\n options: ReplayPluginOptions = {}\n): JourneyPlugin<\"replay\", ReplayApi, { entryCount: number }> {\n const maxEntries = normalizeMaxEntries(options.maxEntries);\n const captureSnapshots = options.captureSnapshots ?? true;\n const now = options.now ?? Date.now;\n\n return {\n name: \"replay\",\n setup(host) {\n let startedAt = now();\n let entries: ReplayEntry[] = [];\n\n const record = (kind: ReplayEntryKind, data: unknown) => {\n const entry: ReplayEntry = {\n at: now(),\n kind,\n data: toSerializable(data),\n ...(captureSnapshots ? { snapshot: toSerializable(host.getSnapshot()) } : {})\n };\n entries.push(entry);\n if (entries.length > maxEntries) {\n entries = entries.slice(entries.length - maxEntries);\n }\n };\n\n host.onTransition(({ from, to }) => record(\"transition\", { from, to }));\n host.onStatusChange(({ previous, current }) => record(\"status\", { previous, current }));\n host.onContextChange(({ previous, current }) => record(\"context\", { previous, current }));\n host.onNavigationBlocked(({ reason, from, to }) =>\n record(\"navigationBlocked\", { reason, from, to })\n );\n host.onError(({ phase, stepId, error }) => record(\"error\", { phase, stepId, error }));\n\n return {\n api: {\n getReplaySession: () => ({ startedAt, entries: [...entries] }),\n clearReplaySession: () => {\n startedAt = now();\n entries = [];\n },\n exportReplaySession: (exportOptions) =>\n serializeReplaySession({ startedAt, entries: [...entries] }, exportOptions)\n },\n deriveSnapshot: (_snapshot, previous) =>\n previous?.entryCount === entries.length ? previous : { entryCount: entries.length }\n };\n }\n };\n}\n"],
5
+ "mappings": "AACO,IAAMA,EAAwB,ICc9B,SAASC,EACdC,EACiD,CACjD,IAAMC,EAAMD,EAAQ,KAAO,KAAK,IAEhC,MAAO,CACL,KAAM,YAGN,MAAME,EAAM,CACV,IAAIC,EAAiC,CAAC,EAEhCC,EAAUC,GAAgC,CAC9CF,EAAO,KAAKE,CAAK,EACbF,EAAO,OAAS,MAClBA,EAASA,EAAO,MAAMA,EAAO,OAAS,GAAqB,EAE/D,EAEMG,EAAc,CAClBC,EACAC,EACAC,IAC0B,CAC1B,IAAMC,EAAiC,CACrC,KAAAF,EACA,UAAWP,EAAI,EACf,OAAQC,EAAK,YAAY,EAAE,aAAa,IAAM,KAC9C,QAAAO,CACF,EACA,GAAI,CACFT,EAAQ,MAAMU,CAAO,EACrBN,EAAO,CAAE,OAAAG,EAAQ,QAAAG,EAAS,QAAS,EAAK,CAAC,CAC3C,OAASC,EAAO,CACdP,EAAO,CAAE,OAAAG,EAAQ,QAAAG,EAAS,QAAS,GAAO,MAAAC,CAAM,CAAC,EAIjD,GAAI,CACFX,EAAQ,UAAUW,EAAOD,CAAO,CAClC,OAASE,EAAc,CACrBV,EAAK,YAAYU,CAAY,CAC/B,CACF,CACA,OAAOF,CACT,EAEA,OAAAR,EAAK,aAAa,CAAC,CAAE,KAAAW,EAAM,GAAAC,CAAG,IAAM,CAClCR,EAAY,YAAa,qBAAsB,CAAE,KAAAO,EAAM,GAAAC,CAAG,CAAC,CAC7D,CAAC,EACDZ,EAAK,eAAe,CAAC,CAAE,SAAAa,EAAU,QAAAC,CAAQ,IAAM,CAC7CV,EAAY,YAAa,WAAWU,CAAO,GAAI,CAAE,SAAAD,CAAS,CAAC,CAC7D,CAAC,EACDb,EAAK,oBAAoB,CAAC,CAAE,OAAAe,EAAQ,KAAAJ,EAAM,GAAAC,CAAG,IAAM,CACjDR,EAAY,YAAa,4BAA6B,CAAE,OAAAW,EAAQ,KAAAJ,EAAM,GAAAC,CAAG,CAAC,CAC5E,CAAC,EACDZ,EAAK,QAAQ,CAAC,CAAE,MAAAgB,EAAO,OAAAC,EAAQ,MAAAR,CAAM,IAAM,CACzCL,EAAY,YAAa,gBAAiB,CAAE,MAAAY,EAAO,OAAAC,EAAQ,MAAAR,CAAM,CAAC,CACpE,CAAC,EAEM,CACL,IAAK,CACH,oBAAqB,CAACH,EAAMC,EAAU,CAAC,IAAMH,EAAY,SAAUE,EAAMC,CAAO,EAChF,gBAAiB,IAAM,CAAC,GAAGN,CAAM,EACjC,kBAAmB,IAAM,CACvBA,EAAS,CAAC,CACZ,CACF,CACF,CACF,CACF,CACF,CCjEO,SAASiB,EACdC,EAAuC,CAAC,EACqC,CAC7E,IAAMC,EAAWD,EAAQ,UAAY,GACrC,MAAO,CACL,KAAM,kBACN,MAAME,EAAM,CACV,IAAIC,EAAwB,CAAC,EACzBC,EAAwC,CAAC,EAE7C,OAAAF,EAAK,aAAa,CAAC,CAAE,GAAAG,CAAG,IAAM,CAC5BF,EAAY,KAAKE,CAAE,CACrB,CAAC,EACDH,EAAK,eAAe,CAAC,CAAE,QAAAI,CAAQ,IAAM,EAC9BA,IAAY,aAAeA,IAAY,eAAiBH,EAAY,OAAS,IAChFC,EAAe,KAAK,OAAO,OAAO,CAAC,GAAGD,CAAW,CAAC,CAAC,EAG/CC,EAAe,OAASH,IAC1BG,EAAiBA,EAAe,MAAMA,EAAe,OAASH,CAAQ,GAExEE,EAAc,CAAC,EAEnB,CAAC,EAEM,CACL,IAAK,CACH,eAAgB,IAAM,OAAO,OAAO,CAAC,GAAGA,CAAW,CAAC,EACpD,kBAAmB,IAAM,OAAO,OAAO,CAAC,GAAGC,CAAc,CAAC,EAC1D,oBAAqB,IAAM,CACzBA,EAAiB,CAAC,CACpB,CACF,EACA,eAAgB,CAACG,EAAWC,IAExBA,GACAA,EAAS,YAAY,SAAWL,EAAY,QAC5CK,EAAS,eAAe,SAAWJ,EAAe,OAE3CI,EAEF,OAAO,OAAO,CACnB,YAAa,OAAO,OAAO,CAAC,GAAGL,CAAW,CAAC,EAC3C,eAAgB,OAAO,OAAO,CAAC,GAAGC,CAAc,CAAC,CACnD,CAAC,CAEL,CACF,CACF,CACF,CC7BO,IAAMK,EAA2B,IAAe,CACrD,IAAMC,EAAmB,WACzB,GAAI,OAAOA,EAAiB,SAAY,UACtC,OAAOA,EAAiB,QAG1B,IAAMC,EAAUD,EAAiB,SAAS,KAAK,SAC/C,OAAOC,IAAY,QAAaA,IAAY,aAC9C,EAaaC,EAAoB,CAACC,EAAiBC,IAA2B,CAC5E,GAAI,GAACL,EAAyB,GAAK,OAAO,QAAY,KAItD,IAAIK,IAAW,OAAW,CACxB,QAAQ,KAAKD,CAAO,EACpB,MACF,CAMA,QAAQ,KAAK,KAAMA,EAASC,CAAM,EACpC,EC3BO,IAAMC,EAAN,cAA2B,KAAM,CAMtC,YACEC,EACAC,EACAC,EAA+B,CAAC,EAChCC,EACA,CACA,MAAM,YAAYF,CAAO,GAAIE,IAAU,OAAY,OAAY,CAAE,MAAAA,CAAM,CAAC,EACxE,KAAK,KAAO,eACZ,KAAK,KAAOH,EACZ,KAAK,OAASE,EAAQ,OACtB,KAAK,MAAQA,EAAQ,MACrB,KAAK,WAAaA,EAAQ,UAC5B,CACF,EChEO,IAAME,EAAqD,CAChE,UACA,aACA,QACF,EAGO,SAASC,EAAoBC,EAAmC,CACrE,OAAI,OAAOA,GAAU,UAAY,CAAC,OAAO,SAASA,CAAK,EAAU,EAC1D,KAAK,IAAI,EAAG,KAAK,MAAMA,CAAK,CAAC,CACtC,CAEO,SAASC,EAAoBC,EAA2BC,EAAoC,CACjG,MAAO,CACL,OAAQD,EAAS,OACjB,QAASA,EAAS,QAClB,SAAUA,EAAS,QAAQ,SAC3B,aAAcA,EAAS,QAAQ,aAC/B,QAASC,CACX,CACF,CAEA,IAAMC,EAAmB,IAAI,IAAI,CAC/B,OACA,UACA,SACA,YACA,YACF,CAAC,EAWKC,EAAc,CAAC,YAAa,cAAe,WAAW,EAE5D,SAASC,EAAMN,EAAgBO,EAAQ,EAAY,CACjD,GAAIA,EAAQ,IAAMP,IAAU,MAAQ,OAAOA,GAAU,SAAU,OAAOA,EACtE,GAAI,MAAM,QAAQA,CAAK,EAAG,OAAOA,EAAM,IAAKQ,GAASF,EAAME,EAAMD,EAAQ,CAAC,CAAC,EAE3E,IAAME,EAAkC,CAAC,EACzC,OAAW,CAACC,EAAKF,CAAI,IAAK,OAAO,QAAQR,CAAK,EACvCK,EAAkC,SAASK,CAAG,IACnDD,EAAOC,CAAG,EAAIJ,EAAME,EAAMD,EAAQ,CAAC,GAErC,OAAOE,CACT,CASO,SAASE,EAAoBC,EAAkD,CACpF,GAAIA,IAAQ,KAAM,OAAO,KACzB,IAAIZ,EACJ,GAAI,CACFA,EAAQ,KAAK,MAAMY,CAAG,CACxB,MAAQ,CACN,OAAO,IACT,CACA,GAAI,OAAOZ,GAAU,UAAYA,IAAU,KAAM,OAAO,KACxD,IAAMa,EAAYb,EAClB,OACE,OAAOa,EAAU,QAAW,UAC5B,CAACT,EAAiB,IAAIS,EAAU,MAAM,GACtC,CAAC,MAAM,QAAQA,EAAU,QAAQ,GACjC,CAACA,EAAU,SAAS,MAAOC,GAAO,OAAOA,GAAO,QAAQ,GACxD,OAAOD,EAAU,cAAiB,UAClC,CAAC,OAAO,UAAUA,EAAU,YAAY,GACxC,OAAOA,EAAU,SAAY,UAC7B,CAAC,OAAO,SAASA,EAAU,OAAO,EAE3B,KAEF,CACL,OAAQA,EAAU,OAClB,QAASP,EAAMO,EAAU,OAAO,EAChC,SAAUA,EAAU,SACpB,aAAcA,EAAU,aACxB,QAASA,EAAU,OACrB,CACF,CAaO,SAASE,EAAsBC,EAA8C,CAClF,GAAIA,EAAO,QAAS,OAAOA,EAAO,QAElC,IAAIC,EACJ,GAAI,CACFA,EAAU,WAAW,YACvB,OAASC,EAAO,CACd,MAAM,IAAIC,EACR,sBACA,sFACA,CAAC,EACDD,CACF,CACF,CAEA,GAAI,CAACD,EACH,MAAM,IAAIE,EACR,sBACA,8DACF,EAEF,OAAOF,CACT,CCzGA,IAAMG,EAA+B,CAAE,OAAQ,OAAQ,YAAa,KAAM,MAAO,IAAK,EAY/E,SAASC,EACdC,EACgE,CAChE,IAAMC,EAAMD,EAAQ,KAAO,KAAK,IAC1BE,EAAaC,EAAoBH,EAAQ,UAAU,EACnDI,EAAc,IAAI,IAAuBJ,EAAQ,QAAUK,CAAoB,EAIjFC,EAAa,EACjB,MAAO,CACL,KAAM,cACN,MAAMC,EAAM,CACVD,GAAc,EACVA,EAAa,GACfE,EACE,kDAAkDF,CAAU,kCAAkCN,EAAQ,GAAG,4BAC3G,EAEF,IAAIS,EAA4C,KAC5CC,EAA0BZ,EAC1Ba,EAA8C,KAC9CC,EAAW,GAETC,EAAaC,GAAmC,CACpDL,EAAcK,EACdJ,EAAQ,CAAE,OAAQ,QAAS,YAAaI,EAAQ,QAAS,MAAO,IAAK,CACvE,EAEMC,EAAUC,GAAmB,CACjCN,EAAQ,CAAE,GAAGA,EAAO,OAAQ,QAAS,MAAAM,CAAM,EAC3CT,EAAK,YAAYS,CAAK,CACxB,EAQMC,EAAO,IAA4B,CACvC,IAAMH,EAAUI,EAAoBX,EAAK,YAAY,EAAGN,EAAI,CAAC,EAC7DS,EAAQ,CAAE,GAAGA,EAAO,OAAQ,QAAS,EACrC,IAAIS,EACJ,GAAI,CACFA,EAASnB,EAAQ,QAAQ,QAAQA,EAAQ,IAAK,KAAK,UAAUc,CAAO,CAAC,CACvE,OAASE,EAAO,CAGd,MAAAN,EAAQ,CAAE,GAAGA,EAAO,OAAQ,QAAS,MAAAM,CAAM,EACrCA,CACR,CAIA,GAAIG,IAAW,OACb,OAAOA,EAAO,KAAK,IAAMN,EAAUC,CAAO,EAAGC,CAAM,EAErDF,EAAUC,CAAO,CACnB,EAEMM,EAAc,IAAM,CACpBT,IAAU,OACZ,aAAaA,CAAK,EAClBA,EAAQ,KAEZ,EAOMU,EAAe,IAAM,CACzBV,EAAQ,KACR,GAAI,CACF,IAAMQ,EAASF,EAAK,CAEtB,OAASD,EAAO,CACdD,EAAOC,CAAK,CACd,CACF,EAEMM,EAAiBC,GAA8B,CACnD,GAAI,EAAAX,GAAY,CAACR,EAAY,IAAImB,CAAM,GACvC,IAAIrB,IAAe,EAAG,CACfe,EAAK,EACV,MACF,CACAG,EAAY,EACZV,EAAQ,CAAE,GAAGA,EAAO,OAAQ,SAAU,EACtCC,EAAQ,WAAWU,EAAcnB,CAAU,EAC7C,EAEA,OAAAK,EAAK,gBAAgB,IAAMe,EAAc,SAAS,CAAC,EACnDf,EAAK,aAAa,IAAMe,EAAc,YAAY,CAAC,EACnDf,EAAK,eAAe,CAAC,CAAE,QAAAiB,CAAQ,IAAM,CACnC,GAAIA,IAAY,cAAgBxB,EAAQ,iBAAkB,CACxDoB,EAAY,EACZX,EAAc,KACdC,EAAQZ,EACRE,EAAQ,QAAQ,WAAWA,EAAQ,GAAG,EACtC,MACF,CACAsB,EAAc,QAAQ,CACxB,CAAC,EACDf,EAAK,UAAU,IAAM,CACnBK,EAAW,GACXQ,EAAY,CACd,CAAC,EAEM,CACL,IAAK,CACH,sBAAuB,IAAMX,EAC7B,oBAAqB,IAAMC,EAC3B,cAAe,IAAMe,EAAoBzB,EAAQ,QAAQ,QAAQA,EAAQ,GAAG,CAAC,EAC7E,eAAgB,IAAM,CACpBoB,EAAY,EACZX,EAAc,KACdC,EAAQZ,EAGR,GAAI,CACFE,EAAQ,QAAQ,WAAWA,EAAQ,GAAG,CACxC,OAASgB,EAAO,CACdD,EAAOC,CAAK,CACd,CACF,EACA,eAAgB,SAAY,CAC1BI,EAAY,EACZ,GAAI,CACF,MAAMH,EAAK,CACb,OAASD,EAAO,CACdD,EAAOC,CAAK,CACd,CACF,CACF,EACA,eAAgB,CAACU,EAAWC,IAAcA,IAAajB,EAAQiB,EAAWjB,CAC5E,CACF,CACF,CACF,CAGO,SAASkB,EACdC,EACgE,CAChE,OAAO9B,EAAwB,CAAE,IAAK8B,EAAO,IAAK,QAASC,EAAsBD,CAAM,CAAE,CAAC,CAC5F,CCtLO,SAASE,EAAoBC,EAAmC,CACrE,OAAI,OAAOA,GAAU,UAAY,CAAC,OAAO,SAASA,CAAK,EAAU,IAC1D,KAAK,IAAI,EAAG,KAAK,MAAMA,CAAK,CAAC,CACtC,CAOA,IAAMC,EAAoB,IAEpBC,EAAW,aACXC,EAAY,cAUlB,SAASC,EAAKJ,EAAgBK,EAAkBC,EAAwB,CACtE,GACEN,IAAU,MACV,OAAOA,GAAU,UACjB,OAAOA,GAAU,UACjB,OAAOA,GAAU,UAEjB,OAAOA,EAET,GAAI,OAAOA,GAAU,SAAU,OAAOA,EAAM,SAAS,EACrD,GAAI,OAAOA,EAAU,IAAa,OAAO,KACzC,GAAI,OAAOA,GAAU,YAAc,OAAOA,GAAU,SAClD,MAAO,gBAAgB,OAAOA,CAAK,IAErC,GAAIA,aAAiB,MACnB,MAAO,CACL,KAAMA,EAAM,KACZ,QAASA,EAAM,QACf,GAAIA,EAAM,MAAQ,CAAE,MAAOA,EAAM,KAAM,EAAI,CAAC,CAC9C,EAEF,GAAIA,aAAiB,KAAM,OAAOA,EAAM,YAAY,EACpD,GAAI,OAAOA,GAAU,SAAU,OAAO,OAAOA,CAAK,EAElD,IAAMO,EAASP,EACf,GAAIK,EAAM,KAAK,IAAIE,CAAM,EAAG,OAAOF,EAAM,KAAK,IAAIE,CAAM,EACxD,GAAIF,EAAM,WAAW,IAAIE,CAAM,EAAG,OAAOL,EACzC,GAAII,GAASD,EAAM,SAAU,OAAOF,EAEpCE,EAAM,WAAW,IAAIE,CAAM,EAC3B,IAAIC,EACJ,GAAI,MAAM,QAAQR,CAAK,EACrBQ,EAASR,EAAM,IAAKS,GAASL,EAAKK,EAAMJ,EAAOC,EAAQ,CAAC,CAAC,MACpD,CACL,IAAMI,EAAkC,CAAC,EACzC,OAAW,CAACC,EAAKF,CAAI,IAAK,OAAO,QAAQT,CAAK,EAC5CU,EAAOC,CAAG,EAAIP,EAAKK,EAAMJ,EAAOC,EAAQ,CAAC,EAE3CE,EAASE,CACX,CACA,OAAAL,EAAM,WAAW,OAAOE,CAAM,EAC9BF,EAAM,KAAK,IAAIE,EAAQC,CAAM,EACtBA,CACT,CAiBO,SAASI,EAAeZ,EAAgBa,EAAiC,CAAC,EAAY,CAC3F,OAAOT,EACLJ,EACA,CACE,KAAM,IAAI,IACV,WAAY,IAAI,IAChB,SAAUa,EAAQ,UAAYZ,CAChC,EACA,CACF,CACF,CAGO,SAASa,EACdC,EACAF,EACQ,CACR,OAAO,KAAK,UAAUD,EAAeG,CAAO,EAAG,KAAMF,GAAS,OAAS,EAAI,MAAS,CACtF,CChGO,SAASG,EACdC,EAA+B,CAAC,EAC4B,CAC5D,IAAMC,EAAaC,EAAoBF,EAAQ,UAAU,EACnDG,EAAmBH,EAAQ,kBAAoB,GAC/CI,EAAMJ,EAAQ,KAAO,KAAK,IAEhC,MAAO,CACL,KAAM,SACN,MAAMK,EAAM,CACV,IAAIC,EAAYF,EAAI,EAChBG,EAAyB,CAAC,EAExBC,EAAS,CAACC,EAAuBC,IAAkB,CACvD,IAAMC,EAAqB,CACzB,GAAIP,EAAI,EACR,KAAAK,EACA,KAAMG,EAAeF,CAAI,EACzB,GAAIP,EAAmB,CAAE,SAAUS,EAAeP,EAAK,YAAY,CAAC,CAAE,EAAI,CAAC,CAC7E,EACAE,EAAQ,KAAKI,CAAK,EACdJ,EAAQ,OAASN,IACnBM,EAAUA,EAAQ,MAAMA,EAAQ,OAASN,CAAU,EAEvD,EAEA,OAAAI,EAAK,aAAa,CAAC,CAAE,KAAAQ,EAAM,GAAAC,CAAG,IAAMN,EAAO,aAAc,CAAE,KAAAK,EAAM,GAAAC,CAAG,CAAC,CAAC,EACtET,EAAK,eAAe,CAAC,CAAE,SAAAU,EAAU,QAAAC,CAAQ,IAAMR,EAAO,SAAU,CAAE,SAAAO,EAAU,QAAAC,CAAQ,CAAC,CAAC,EACtFX,EAAK,gBAAgB,CAAC,CAAE,SAAAU,EAAU,QAAAC,CAAQ,IAAMR,EAAO,UAAW,CAAE,SAAAO,EAAU,QAAAC,CAAQ,CAAC,CAAC,EACxFX,EAAK,oBAAoB,CAAC,CAAE,OAAAY,EAAQ,KAAAJ,EAAM,GAAAC,CAAG,IAC3CN,EAAO,oBAAqB,CAAE,OAAAS,EAAQ,KAAAJ,EAAM,GAAAC,CAAG,CAAC,CAClD,EACAT,EAAK,QAAQ,CAAC,CAAE,MAAAa,EAAO,OAAAC,EAAQ,MAAAC,CAAM,IAAMZ,EAAO,QAAS,CAAE,MAAAU,EAAO,OAAAC,EAAQ,MAAAC,CAAM,CAAC,CAAC,EAE7E,CACL,IAAK,CACH,iBAAkB,KAAO,CAAE,UAAAd,EAAW,QAAS,CAAC,GAAGC,CAAO,CAAE,GAC5D,mBAAoB,IAAM,CACxBD,EAAYF,EAAI,EAChBG,EAAU,CAAC,CACb,EACA,oBAAsBc,GACpBC,EAAuB,CAAE,UAAAhB,EAAW,QAAS,CAAC,GAAGC,CAAO,CAAE,EAAGc,CAAa,CAC9E,EACA,eAAgB,CAACE,EAAWR,IAC1BA,GAAU,aAAeR,EAAQ,OAASQ,EAAW,CAAE,WAAYR,EAAQ,MAAO,CACtF,CACF,CACF,CACF",
6
+ "names": ["RECENT_EVENT_CAPACITY", "createAnalyticsPlugin", "options", "now", "host", "recent", "record", "entry", "trackSafely", "source", "name", "payload", "tracked", "error", "handlerError", "from", "to", "previous", "current", "reason", "phase", "stepId", "createExecutionPathsPlugin", "options", "maxPaths", "host", "currentPath", "completedPaths", "to", "current", "_snapshot", "previous", "isDevelopmentEnvironment", "diagnosticGlobal", "nodeEnv", "warnInDevelopment", "message", "detail", "JourneyError", "code", "message", "details", "cause", "DEFAULT_SAVE_REASONS", "normalizeDebounceMs", "value", "buildPersistedState", "snapshot", "now", "JOURNEY_STATUSES", "UNSAFE_KEYS", "scrub", "depth", "item", "output", "key", "parsePersistedState", "raw", "candidate", "id", "resolvePersistStorage", "option", "ambient", "error", "JourneyError", "IDLE_STATE", "createPersistencePlugin", "options", "now", "debounceMs", "normalizeDebounceMs", "saveReasons", "DEFAULT_SAVE_REASONS", "setupCount", "host", "warnInDevelopment", "lastWritten", "state", "timer", "disposed", "succeeded", "written", "failed", "error", "save", "buildPersistedState", "result", "cancelTimer", "runDebounced", "onObservation", "reason", "current", "parsePersistedState", "_snapshot", "previous", "persistOptionToPlugin", "option", "resolvePersistStorage", "normalizeMaxEntries", "value", "DEFAULT_MAX_DEPTH", "CIRCULAR", "TRUNCATED", "walk", "state", "depth", "object", "result", "item", "output", "key", "toSerializable", "options", "serializeReplaySession", "session", "createReplayPlugin", "options", "maxEntries", "normalizeMaxEntries", "captureSnapshots", "now", "host", "startedAt", "entries", "record", "kind", "data", "entry", "toSerializable", "from", "to", "previous", "current", "reason", "phase", "stepId", "error", "exportOptions", "serializeReplaySession", "_snapshot"]
7
+ }
@@ -0,0 +1,16 @@
1
+ import type { PersistenceApi, PersistencePluginOptions, PersistenceState } from "./persistence.types.js";
2
+ import type { JourneyPersistOption, JourneyPlugin } from "../../core/types.js";
3
+ export { buildPersistedState, DEFAULT_SAVE_REASONS, normalizeDebounceMs, parsePersistedState } from "./persistence.helpers.js";
4
+ /**
5
+ * Persists a serializable slice of machine state (status, context, timeline) on
6
+ * every transition, status change, and context change. `saveOn` narrows which
7
+ * of those trigger a write; `debounceMs` collapses a burst of them into one.
8
+ *
9
+ * The plugin itself is save-side only (plugins observe, they cannot seed the
10
+ * runtime). Restore happens through the creation-time `persist` option, which
11
+ * reads a restorable record before the machine is built; `readPersisted()`
12
+ * exposes the saved state for callers wiring this plugin explicitly.
13
+ */
14
+ export declare function createPersistencePlugin(options: PersistencePluginOptions): JourneyPlugin<"persistence", PersistenceApi, PersistenceState>;
15
+ /** Expands the creation-time `persist` option into the persistence plugin. */
16
+ export declare function persistOptionToPlugin(option: JourneyPersistOption): JourneyPlugin<"persistence", PersistenceApi, PersistenceState>;
@@ -0,0 +1,16 @@
1
+ import type { PersistenceApi, PersistencePluginOptions, PersistenceState } from "./persistence.types.js";
2
+ import type { JourneyPersistOption, JourneyPlugin } from "../../core/types.js";
3
+ export { buildPersistedState, DEFAULT_SAVE_REASONS, normalizeDebounceMs, parsePersistedState } from "./persistence.helpers.js";
4
+ /**
5
+ * Persists a serializable slice of machine state (status, context, timeline) on
6
+ * every transition, status change, and context change. `saveOn` narrows which
7
+ * of those trigger a write; `debounceMs` collapses a burst of them into one.
8
+ *
9
+ * The plugin itself is save-side only (plugins observe, they cannot seed the
10
+ * runtime). Restore happens through the creation-time `persist` option, which
11
+ * reads a restorable record before the machine is built; `readPersisted()`
12
+ * exposes the saved state for callers wiring this plugin explicitly.
13
+ */
14
+ export declare function createPersistencePlugin(options: PersistencePluginOptions): JourneyPlugin<"persistence", PersistenceApi, PersistenceState>;
15
+ /** Expands the creation-time `persist` option into the persistence plugin. */
16
+ export declare function persistOptionToPlugin(option: JourneyPersistOption): JourneyPlugin<"persistence", PersistenceApi, PersistenceState>;
@@ -0,0 +1,34 @@
1
+ import type { JourneyPersistedState, JourneyStorage, PersistenceReason } from "./persistence.types.js";
2
+ import type { JourneyPersistOption, JourneySnapshot } from "../../core/types.js";
3
+ /** Every observation kind a save can be scheduled on. */
4
+ export declare const DEFAULT_SAVE_REASONS: readonly PersistenceReason[];
5
+ /** Omitted means "write immediately"; anything else is clamped to a whole >= 0. */
6
+ export declare function normalizeDebounceMs(value: number | undefined): number;
7
+ export declare function buildPersistedState(snapshot: JourneySnapshot, now: number): JourneyPersistedState;
8
+ /**
9
+ * Parses a stored value; malformed or foreign payloads yield `null`.
10
+ *
11
+ * Validation is deliberately total: this is the only gate between an untrusted
12
+ * storage entry and machine state, and `readPersisted()` hands the result
13
+ * straight to application code typed as `JourneyPersistedState`.
14
+ */
15
+ export declare function parsePersistedState(raw: string | null): JourneyPersistedState | null;
16
+ /**
17
+ * Resolves the `persist` option's storage; throws when none is available.
18
+ *
19
+ * Reading `globalThis.localStorage` can itself throw — a third-party iframe
20
+ * with storage blocked, or Safari's Lockdown Mode — rather than returning
21
+ * undefined. Unguarded, that surfaced as a raw `SecurityError` out of
22
+ * `createLinearJourney`, which reads as a library crash rather than an
23
+ * environment that cannot persist. The failure stays loud (silently disabling
24
+ * persistence loses data with no signal) but is now a `journey:` error naming
25
+ * the fix, with the original kept as `cause`.
26
+ */
27
+ export declare function resolvePersistStorage(option: JourneyPersistOption): JourneyStorage;
28
+ /**
29
+ * Reads a persisted record that can seed this machine, or `null`. Restore is
30
+ * best-effort by design: terminal-status records, records whose timeline
31
+ * mentions a step the current definition no longer declares (definition
32
+ * drift), and malformed/foreign payloads are all ignored rather than thrown.
33
+ */
34
+ export declare function readRestorableState(option: JourneyPersistOption, isDeclaredStep: (id: string) => boolean): JourneyPersistedState | null;
@@ -0,0 +1,34 @@
1
+ import type { JourneyPersistedState, JourneyStorage, PersistenceReason } from "./persistence.types.js";
2
+ import type { JourneyPersistOption, JourneySnapshot } from "../../core/types.js";
3
+ /** Every observation kind a save can be scheduled on. */
4
+ export declare const DEFAULT_SAVE_REASONS: readonly PersistenceReason[];
5
+ /** Omitted means "write immediately"; anything else is clamped to a whole >= 0. */
6
+ export declare function normalizeDebounceMs(value: number | undefined): number;
7
+ export declare function buildPersistedState(snapshot: JourneySnapshot, now: number): JourneyPersistedState;
8
+ /**
9
+ * Parses a stored value; malformed or foreign payloads yield `null`.
10
+ *
11
+ * Validation is deliberately total: this is the only gate between an untrusted
12
+ * storage entry and machine state, and `readPersisted()` hands the result
13
+ * straight to application code typed as `JourneyPersistedState`.
14
+ */
15
+ export declare function parsePersistedState(raw: string | null): JourneyPersistedState | null;
16
+ /**
17
+ * Resolves the `persist` option's storage; throws when none is available.
18
+ *
19
+ * Reading `globalThis.localStorage` can itself throw — a third-party iframe
20
+ * with storage blocked, or Safari's Lockdown Mode — rather than returning
21
+ * undefined. Unguarded, that surfaced as a raw `SecurityError` out of
22
+ * `createLinearJourney`, which reads as a library crash rather than an
23
+ * environment that cannot persist. The failure stays loud (silently disabling
24
+ * persistence loses data with no signal) but is now a `journey:` error naming
25
+ * the fix, with the original kept as `cause`.
26
+ */
27
+ export declare function resolvePersistStorage(option: JourneyPersistOption): JourneyStorage;
28
+ /**
29
+ * Reads a persisted record that can seed this machine, or `null`. Restore is
30
+ * best-effort by design: terminal-status records, records whose timeline
31
+ * mentions a step the current definition no longer declares (definition
32
+ * drift), and malformed/foreign payloads are all ignored rather than thrown.
33
+ */
34
+ export declare function readRestorableState(option: JourneyPersistOption, isDeclaredStep: (id: string) => boolean): JourneyPersistedState | null;
@@ -0,0 +1,63 @@
1
+ import type { JourneyStatus } from "../../core/types.js";
2
+ /** localStorage-compatible adapter; `setItem` may be async. */
3
+ export type JourneyStorage = {
4
+ getItem(key: string): string | null;
5
+ setItem(key: string, value: string): void | Promise<void>;
6
+ removeItem(key: string): void;
7
+ };
8
+ /** The serializable slice of machine state persisted to storage. */
9
+ export type JourneyPersistedState = {
10
+ readonly status: JourneyStatus;
11
+ readonly context: unknown;
12
+ readonly timeline: readonly string[];
13
+ readonly currentIndex: number;
14
+ readonly savedAt: number;
15
+ };
16
+ /** The observations that trigger a save. */
17
+ export type PersistenceReason = "context" | "transition" | "status";
18
+ export type PersistencePluginOptions = {
19
+ storage: JourneyStorage;
20
+ key: string;
21
+ /**
22
+ * Wait this many milliseconds after an observation before writing, collapsing
23
+ * a burst of changes into one write. Omitted or `0` writes immediately.
24
+ *
25
+ * This subsumes what used to be a separate autosave plugin. It was the same
26
+ * plugin with a timer: the same serializer, the same storage adapter, the same
27
+ * key — so having two of them meant choosing between "persist" and "persist,
28
+ * but later", which is a parameter, not a plugin.
29
+ */
30
+ debounceMs?: number;
31
+ /** Which observations schedule a save. Defaults to all of them. */
32
+ saveOn?: readonly PersistenceReason[];
33
+ /** Remove the persisted entry when the journey terminates. Defaults to `false`. */
34
+ clearOnTerminate?: boolean;
35
+ /** Injectable clock, mainly for tests. */
36
+ now?: () => number;
37
+ };
38
+ export type PersistenceState = {
39
+ /**
40
+ * `"pending"` only ever appears when `debounceMs` is set — an immediate write
41
+ * has no window in which to be pending.
42
+ */
43
+ readonly status: "idle" | "pending" | "saving" | "saved" | "error";
44
+ /** When the last **confirmed** write landed, not when one was attempted. */
45
+ readonly lastSavedAt: number | null;
46
+ /** The most recent write failure, cleared by the next successful write. */
47
+ readonly error: unknown | null;
48
+ };
49
+ export type PersistenceApi = {
50
+ /**
51
+ * The last state this run confirmed to storage (not re-read from it), or
52
+ * `null` if nothing has been written yet.
53
+ */
54
+ inspectPersistedState(): JourneyPersistedState | null;
55
+ /** Last write status, confirmed write time, and last write failure. */
56
+ getPersistenceState(): PersistenceState;
57
+ /** Re-reads and parses storage; `null` when absent, malformed, or foreign. */
58
+ readPersisted(): JourneyPersistedState | null;
59
+ /** Cancels any pending debounced save and removes the persisted entry. */
60
+ clearPersisted(): void;
61
+ /** Cancels the debounce and writes now. Resolves once the write settles. */
62
+ flushPersisted(): Promise<void>;
63
+ };
@@ -0,0 +1,63 @@
1
+ import type { JourneyStatus } from "../../core/types.js";
2
+ /** localStorage-compatible adapter; `setItem` may be async. */
3
+ export type JourneyStorage = {
4
+ getItem(key: string): string | null;
5
+ setItem(key: string, value: string): void | Promise<void>;
6
+ removeItem(key: string): void;
7
+ };
8
+ /** The serializable slice of machine state persisted to storage. */
9
+ export type JourneyPersistedState = {
10
+ readonly status: JourneyStatus;
11
+ readonly context: unknown;
12
+ readonly timeline: readonly string[];
13
+ readonly currentIndex: number;
14
+ readonly savedAt: number;
15
+ };
16
+ /** The observations that trigger a save. */
17
+ export type PersistenceReason = "context" | "transition" | "status";
18
+ export type PersistencePluginOptions = {
19
+ storage: JourneyStorage;
20
+ key: string;
21
+ /**
22
+ * Wait this many milliseconds after an observation before writing, collapsing
23
+ * a burst of changes into one write. Omitted or `0` writes immediately.
24
+ *
25
+ * This subsumes what used to be a separate autosave plugin. It was the same
26
+ * plugin with a timer: the same serializer, the same storage adapter, the same
27
+ * key — so having two of them meant choosing between "persist" and "persist,
28
+ * but later", which is a parameter, not a plugin.
29
+ */
30
+ debounceMs?: number;
31
+ /** Which observations schedule a save. Defaults to all of them. */
32
+ saveOn?: readonly PersistenceReason[];
33
+ /** Remove the persisted entry when the journey terminates. Defaults to `false`. */
34
+ clearOnTerminate?: boolean;
35
+ /** Injectable clock, mainly for tests. */
36
+ now?: () => number;
37
+ };
38
+ export type PersistenceState = {
39
+ /**
40
+ * `"pending"` only ever appears when `debounceMs` is set — an immediate write
41
+ * has no window in which to be pending.
42
+ */
43
+ readonly status: "idle" | "pending" | "saving" | "saved" | "error";
44
+ /** When the last **confirmed** write landed, not when one was attempted. */
45
+ readonly lastSavedAt: number | null;
46
+ /** The most recent write failure, cleared by the next successful write. */
47
+ readonly error: unknown | null;
48
+ };
49
+ export type PersistenceApi = {
50
+ /**
51
+ * The last state this run confirmed to storage (not re-read from it), or
52
+ * `null` if nothing has been written yet.
53
+ */
54
+ inspectPersistedState(): JourneyPersistedState | null;
55
+ /** Last write status, confirmed write time, and last write failure. */
56
+ getPersistenceState(): PersistenceState;
57
+ /** Re-reads and parses storage; `null` when absent, malformed, or foreign. */
58
+ readPersisted(): JourneyPersistedState | null;
59
+ /** Cancels any pending debounced save and removes the persisted entry. */
60
+ clearPersisted(): void;
61
+ /** Cancels the debounce and writes now. Resolves once the write settles. */
62
+ flushPersisted(): Promise<void>;
63
+ };
@@ -0,0 +1,7 @@
1
+ import type { ReplayApi, ReplayPluginOptions } from "./replay.types.js";
2
+ import type { JourneyPlugin } from "../../core/types.js";
3
+ export { normalizeMaxEntries, serializeReplaySession, toSerializable } from "./replay.helpers.js";
4
+ /** Records snapshot and lifecycle activity into an exportable replay session. */
5
+ export declare function createReplayPlugin(options?: ReplayPluginOptions): JourneyPlugin<"replay", ReplayApi, {
6
+ entryCount: number;
7
+ }>;
@@ -0,0 +1,7 @@
1
+ import type { ReplayApi, ReplayPluginOptions } from "./replay.types.js";
2
+ import type { JourneyPlugin } from "../../core/types.js";
3
+ export { normalizeMaxEntries, serializeReplaySession, toSerializable } from "./replay.helpers.js";
4
+ /** Records snapshot and lifecycle activity into an exportable replay session. */
5
+ export declare function createReplayPlugin(options?: ReplayPluginOptions): JourneyPlugin<"replay", ReplayApi, {
6
+ entryCount: number;
7
+ }>;
@@ -0,0 +1,22 @@
1
+ import type { ReplayExportOptions, ReplaySession } from "./replay.types.js";
2
+ export declare function normalizeMaxEntries(value: number | undefined): number;
3
+ /**
4
+ * Converts any value into a JSON-safe structure (circular refs, errors, dates).
5
+ *
6
+ * Memoized by reference, which is what keeps it linear in the number of
7
+ * distinct objects. The previous walk removed each node from its `seen` set on
8
+ * the way back up — correct for cycles, but it meant a shared subtree was
9
+ * re-traversed once per path reaching it, so a context with diamond-shaped
10
+ * sharing cost 2^N. Since the replay plugin serializes the whole snapshot on
11
+ * every transition by default, that ran on the hot path: measured 21 ms at
12
+ * depth 14, 241 ms at 18, 932 ms at 20, and no completion at 26.
13
+ *
14
+ * Arrays are now cycle-tracked too. They were checked before the object branch
15
+ * and never entered `seen`, so a self-referencing array recursed until the
16
+ * stack gave out.
17
+ */
18
+ export declare function toSerializable(value: unknown, options?: {
19
+ maxDepth?: number;
20
+ }): unknown;
21
+ /** Serializes a replay session into a JSON string safe for logging or export. */
22
+ export declare function serializeReplaySession(session: ReplaySession, options?: ReplayExportOptions): string;
@@ -0,0 +1,22 @@
1
+ import type { ReplayExportOptions, ReplaySession } from "./replay.types.js";
2
+ export declare function normalizeMaxEntries(value: number | undefined): number;
3
+ /**
4
+ * Converts any value into a JSON-safe structure (circular refs, errors, dates).
5
+ *
6
+ * Memoized by reference, which is what keeps it linear in the number of
7
+ * distinct objects. The previous walk removed each node from its `seen` set on
8
+ * the way back up — correct for cycles, but it meant a shared subtree was
9
+ * re-traversed once per path reaching it, so a context with diamond-shaped
10
+ * sharing cost 2^N. Since the replay plugin serializes the whole snapshot on
11
+ * every transition by default, that ran on the hot path: measured 21 ms at
12
+ * depth 14, 241 ms at 18, 932 ms at 20, and no completion at 26.
13
+ *
14
+ * Arrays are now cycle-tracked too. They were checked before the object branch
15
+ * and never entered `seen`, so a self-referencing array recursed until the
16
+ * stack gave out.
17
+ */
18
+ export declare function toSerializable(value: unknown, options?: {
19
+ maxDepth?: number;
20
+ }): unknown;
21
+ /** Serializes a replay session into a JSON string safe for logging or export. */
22
+ export declare function serializeReplaySession(session: ReplaySession, options?: ReplayExportOptions): string;
@@ -0,0 +1,28 @@
1
+ export type ReplayEntryKind = "status" | "transition" | "context" | "navigationBlocked" | "error";
2
+ export type ReplayEntry = {
3
+ readonly at: number;
4
+ readonly kind: ReplayEntryKind;
5
+ readonly data: unknown;
6
+ /** Present when `captureSnapshots` is enabled. */
7
+ readonly snapshot?: unknown;
8
+ };
9
+ export type ReplaySession = {
10
+ readonly startedAt: number;
11
+ readonly entries: readonly ReplayEntry[];
12
+ };
13
+ export type ReplayPluginOptions = {
14
+ /** Ring-buffer capacity. Defaults to 500; clamped to >= 1. */
15
+ maxEntries?: number;
16
+ /** Attach a serialized snapshot to each entry. Defaults to `true`. */
17
+ captureSnapshots?: boolean;
18
+ /** Injectable clock, mainly for tests. */
19
+ now?: () => number;
20
+ };
21
+ export type ReplayExportOptions = {
22
+ pretty?: boolean;
23
+ };
24
+ export type ReplayApi = {
25
+ getReplaySession(): ReplaySession;
26
+ clearReplaySession(): void;
27
+ exportReplaySession(options?: ReplayExportOptions): string;
28
+ };
@@ -0,0 +1,28 @@
1
+ export type ReplayEntryKind = "status" | "transition" | "context" | "navigationBlocked" | "error";
2
+ export type ReplayEntry = {
3
+ readonly at: number;
4
+ readonly kind: ReplayEntryKind;
5
+ readonly data: unknown;
6
+ /** Present when `captureSnapshots` is enabled. */
7
+ readonly snapshot?: unknown;
8
+ };
9
+ export type ReplaySession = {
10
+ readonly startedAt: number;
11
+ readonly entries: readonly ReplayEntry[];
12
+ };
13
+ export type ReplayPluginOptions = {
14
+ /** Ring-buffer capacity. Defaults to 500; clamped to >= 1. */
15
+ maxEntries?: number;
16
+ /** Attach a serialized snapshot to each entry. Defaults to `true`. */
17
+ captureSnapshots?: boolean;
18
+ /** Injectable clock, mainly for tests. */
19
+ now?: () => number;
20
+ };
21
+ export type ReplayExportOptions = {
22
+ pretty?: boolean;
23
+ };
24
+ export type ReplayApi = {
25
+ getReplaySession(): ReplaySession;
26
+ clearReplaySession(): void;
27
+ exportReplaySession(options?: ReplayExportOptions): string;
28
+ };