@rxova/journey-core 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
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": "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",
3
+ "sources": ["../../src/plugins/analytics/analytics.helpers.ts", "../../src/plugins/analytics/analytics.ts", "../../src/plugins/execution-paths/execution-paths.ts", "../../src/internal/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 * Developer diagnostics that must not survive into production.\n *\n * {@link isDevelopmentEnvironment} asks *\"should I print a developer\n * warning?\"* and is **permissive**: an unset `NODE_ENV` counts as development,\n * because an unconfigured environment is someone's machine and the cost of a\n * stray warning there is far lower than the cost of a silently swallowed one.\n * `NODE_ENV=test` is not development, so test runs stay quiet unless a test\n * opts in through `__DEV__`.\n *\n * Kept here rather than taken from `@rxova/ts-utils`: its `isDevelopment`\n * treats `NODE_ENV=test` as development and reads `process.env.NODE_ENV`\n * literally, which a consumer's bundler rewrites. This reads it through\n * `globalThis`, as the shipped builds always have.\n */\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 * 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;\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 \"../../internal/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 // Unreachable for the built-in `typeof` results, all handled above; kept as a\n // defensive fallback for anything a host adds.\n // eslint-disable-next-line @typescript-eslint/no-base-to-string\n if (typeof value !== \"object\") return String(value);\n\n const object = value;\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": "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",
6
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
7
  }
@@ -1,6 +1,6 @@
1
1
  import type { PersistenceApi, PersistencePluginOptions, PersistenceState } from "./persistence.types.js";
2
2
  import type { JourneyPersistOption, JourneyPlugin } from "../../core/types.js";
3
- export { buildPersistedState, DEFAULT_SAVE_REASONS, normalizeDebounceMs, parsePersistedState } from "./persistence.helpers.js";
3
+ export { buildPersistedState, DEFAULT_SAVE_REASONS, normalizeDebounceMs, parsePersistedState, } from "./persistence.helpers.js";
4
4
  /**
5
5
  * Persists a serializable slice of machine state (status, context, timeline) on
6
6
  * every transition, status change, and context change. `saveOn` narrows which
@@ -1,6 +1,6 @@
1
1
  import type { PersistenceApi, PersistencePluginOptions, PersistenceState } from "./persistence.types.js";
2
2
  import type { JourneyPersistOption, JourneyPlugin } from "../../core/types.js";
3
- export { buildPersistedState, DEFAULT_SAVE_REASONS, normalizeDebounceMs, parsePersistedState } from "./persistence.helpers.js";
3
+ export { buildPersistedState, DEFAULT_SAVE_REASONS, normalizeDebounceMs, parsePersistedState, } from "./persistence.helpers.js";
4
4
  /**
5
5
  * Persists a serializable slice of machine state (status, context, timeline) on
6
6
  * every transition, status change, and context change. `saveOn` narrows which
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rxova/journey-core",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Journey core state machine.",
5
5
  "keywords": [
6
6
  "journey",
@@ -82,7 +82,6 @@
82
82
  "sideEffects": false,
83
83
  "devDependencies": {
84
84
  "@arethetypeswrong/cli": "^0.18.5",
85
- "@rxova/journey-common": "0.0.0",
86
85
  "immer": "^11.1.15",
87
86
  "publint": "^0.3.24"
88
87
  },
@@ -145,12 +144,12 @@
145
144
  }
146
145
  ],
147
146
  "scripts": {
148
- "build": "pnpm run clean && node --import tsx ./scripts/build.ts && tsc -p tsconfig.build.json && node --import tsx ../common/tooling/copy-types.ts dist",
149
- "clean": "node --import tsx ../common/tooling/clean-paths.ts dist tsconfig.build.tsbuildinfo",
150
- "coverage": "JOURNEY_COVERAGE_INCLUDE=packages/core/src/** JOURNEY_COVERAGE_DIR=coverage/core pnpm --workspace-root vitest run packages/core/src --coverage --coverage.reporter=text-summary",
151
- "test": "pnpm --workspace-root vitest run packages/core/src --coverage.enabled=false",
152
- "typecheck": "tsc --noEmit -p tsconfig.json",
147
+ "build": "pnpm run clean && node --import tsx ./scripts/build.ts && tsc -p tsconfig.build.json && node --import tsx ../../scripts/packages/clean-paths.ts dist/internal && node --import tsx ../../scripts/packages/copy-types.ts dist",
148
+ "clean": "node --import tsx ../../scripts/packages/clean-paths.ts dist tsconfig.build.tsbuildinfo",
149
+ "test": "vitest run --coverage",
150
+ "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p scripts/tsconfig.json",
153
151
  "check:exports": "publint --strict",
152
+ "pack:smoke": "rxova-repo-config pack-smoke",
154
153
  "attw": "attw --pack . --profile node16",
155
154
  "size": "size-limit"
156
155
  }