@shware/workflow 4.1.1 → 5.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 (118) hide show
  1. package/README.md +16 -38
  2. package/dist/aws/index.cjs +2 -3
  3. package/dist/aws/index.cjs.map +1 -1
  4. package/dist/aws/index.d.cts +1 -1
  5. package/dist/aws/index.d.mts +1 -1
  6. package/dist/aws/index.mjs +2 -3
  7. package/dist/aws/index.mjs.map +1 -1
  8. package/dist/aws/instances.cjs +1 -1
  9. package/dist/aws/instances.cjs.map +1 -1
  10. package/dist/aws/instances.d.cts +1 -1
  11. package/dist/aws/instances.d.mts +1 -1
  12. package/dist/aws/instances.mjs +1 -1
  13. package/dist/aws/instances.mjs.map +1 -1
  14. package/dist/cloudflare/bindings.cjs.map +1 -1
  15. package/dist/cloudflare/bindings.d.cts +0 -19
  16. package/dist/cloudflare/bindings.d.cts.map +1 -1
  17. package/dist/cloudflare/bindings.d.mts +0 -19
  18. package/dist/cloudflare/bindings.d.mts.map +1 -1
  19. package/dist/cloudflare/bindings.mjs.map +1 -1
  20. package/dist/cloudflare/index.cjs +0 -5
  21. package/dist/cloudflare/index.d.cts +4 -6
  22. package/dist/cloudflare/index.d.mts +4 -6
  23. package/dist/cloudflare/index.mjs +2 -4
  24. package/dist/cloudflare/runner.cjs +3 -4
  25. package/dist/cloudflare/runner.cjs.map +1 -1
  26. package/dist/cloudflare/runner.d.cts +3 -3
  27. package/dist/cloudflare/runner.d.cts.map +1 -1
  28. package/dist/cloudflare/runner.d.mts +3 -3
  29. package/dist/cloudflare/runner.d.mts.map +1 -1
  30. package/dist/cloudflare/runner.mjs +3 -4
  31. package/dist/cloudflare/runner.mjs.map +1 -1
  32. package/dist/config.cjs.map +1 -1
  33. package/dist/config.d.cts +1 -1
  34. package/dist/config.d.mts +1 -1
  35. package/dist/config.mjs.map +1 -1
  36. package/dist/engine/condition.cjs +2 -2
  37. package/dist/engine/condition.cjs.map +1 -1
  38. package/dist/engine/condition.d.cts +1 -1
  39. package/dist/engine/condition.d.mts +1 -1
  40. package/dist/engine/condition.mjs +2 -2
  41. package/dist/engine/condition.mjs.map +1 -1
  42. package/dist/engine/ports.d.cts +1 -1
  43. package/dist/engine/ports.d.mts +1 -1
  44. package/dist/host/channels.d.cts +1 -1
  45. package/dist/host/channels.d.mts +1 -1
  46. package/dist/host/execute.d.cts +1 -1
  47. package/dist/host/execute.d.mts +1 -1
  48. package/dist/host/http.cjs +3 -4
  49. package/dist/host/http.cjs.map +1 -1
  50. package/dist/host/http.d.cts +2 -2
  51. package/dist/host/http.d.cts.map +1 -1
  52. package/dist/host/http.d.mts +2 -2
  53. package/dist/host/http.d.mts.map +1 -1
  54. package/dist/host/http.mjs +3 -4
  55. package/dist/host/http.mjs.map +1 -1
  56. package/dist/host/ingest.cjs +11 -22
  57. package/dist/host/ingest.cjs.map +1 -1
  58. package/dist/host/ingest.d.cts +8 -13
  59. package/dist/host/ingest.d.cts.map +1 -1
  60. package/dist/host/ingest.d.mts +8 -13
  61. package/dist/host/ingest.d.mts.map +1 -1
  62. package/dist/host/ingest.mjs +12 -22
  63. package/dist/host/ingest.mjs.map +1 -1
  64. package/dist/host/journey-host.cjs +2 -4
  65. package/dist/host/journey-host.cjs.map +1 -1
  66. package/dist/host/journey-host.d.cts +6 -6
  67. package/dist/host/journey-host.d.cts.map +1 -1
  68. package/dist/host/journey-host.d.mts +6 -6
  69. package/dist/host/journey-host.d.mts.map +1 -1
  70. package/dist/host/journey-host.mjs +2 -4
  71. package/dist/host/journey-host.mjs.map +1 -1
  72. package/dist/host/platform.cjs.map +1 -1
  73. package/dist/host/platform.d.cts +1 -7
  74. package/dist/host/platform.d.cts.map +1 -1
  75. package/dist/host/platform.d.mts +1 -7
  76. package/dist/host/platform.d.mts.map +1 -1
  77. package/dist/host/platform.mjs.map +1 -1
  78. package/dist/store/facts.cjs +1 -1
  79. package/dist/store/facts.cjs.map +1 -1
  80. package/dist/store/facts.mjs +1 -1
  81. package/dist/store/facts.mjs.map +1 -1
  82. package/dist/store/index.cjs +0 -2
  83. package/dist/store/index.cjs.map +1 -1
  84. package/dist/store/index.d.cts +21 -26
  85. package/dist/store/index.d.cts.map +1 -1
  86. package/dist/store/index.d.mts +21 -26
  87. package/dist/store/index.d.mts.map +1 -1
  88. package/dist/store/index.mjs +1 -2
  89. package/dist/store/index.mjs.map +1 -1
  90. package/dist/store/postgres.cjs +2 -3
  91. package/dist/store/postgres.cjs.map +1 -1
  92. package/dist/store/postgres.d.cts +2 -3
  93. package/dist/store/postgres.d.cts.map +1 -1
  94. package/dist/store/postgres.d.mts +2 -3
  95. package/dist/store/postgres.d.mts.map +1 -1
  96. package/dist/store/postgres.mjs +2 -3
  97. package/dist/store/postgres.mjs.map +1 -1
  98. package/dist/transports/cloudflare-email.d.cts +1 -1
  99. package/dist/transports/cloudflare-email.d.mts +1 -1
  100. package/package.json +1 -3
  101. package/dist/cloudflare/d1-env.cjs +0 -17
  102. package/dist/cloudflare/d1-env.cjs.map +0 -1
  103. package/dist/cloudflare/d1-env.d.cts +0 -12
  104. package/dist/cloudflare/d1-env.d.cts.map +0 -1
  105. package/dist/cloudflare/d1-env.d.mts +0 -12
  106. package/dist/cloudflare/d1-env.d.mts.map +0 -1
  107. package/dist/cloudflare/d1-env.mjs +0 -16
  108. package/dist/cloudflare/d1-env.mjs.map +0 -1
  109. package/dist/cloudflare/schema.sql +0 -148
  110. package/dist/store/d1.cjs +0 -245
  111. package/dist/store/d1.cjs.map +0 -1
  112. package/dist/store/d1.d.cts +0 -73
  113. package/dist/store/d1.d.cts.map +0 -1
  114. package/dist/store/d1.d.mts +0 -73
  115. package/dist/store/d1.d.mts.map +0 -1
  116. package/dist/store/d1.mjs +0 -244
  117. package/dist/store/d1.mjs.map +0 -1
  118. package/src/cloudflare/schema.sql +0 -148
@@ -1 +1 @@
1
- {"version":3,"file":"instances.mjs","names":[],"sources":["../../src/aws/instances.ts"],"sourcesContent":["import { murmur3 } from '../engine/bucket';\nimport {\n InstanceNotFoundError,\n type JourneyParams,\n type WorkflowBindingLike,\n type WorkflowInstanceLike,\n} from '../host/platform';\nimport type {\n ExecutionRecord,\n ExecutionRegistry,\n ExecutionStart,\n JourneyStore,\n} from '../store/index';\nimport type { LambdaApi } from './lambda-api';\n\n/**\n * The router's instance port over Lambda durable executions: an instance is one execution of\n * this same function, started by an asynchronous self-invoke under a stable execution name;\n * a wake completes the callback its current wait parks on; stopping stops the execution.\n */\n\n/** The top-level shape of an internal run: what the self-invoke sends, never an HTTP body. */\nexport const RUN_EVENT_TYPE = '$workflow.run';\n\nexport interface RunInput {\n type: typeof RUN_EVENT_TYPE;\n instanceId: string;\n params: JourneyParams;\n /** When the instance was created (as Cloudflare's event timestamp): its entry anchor, fixed with the start. */\n enteredAtMs: number;\n}\n\nexport function isRunInput(event: unknown): event is RunInput {\n if (typeof event !== 'object' || event === null) return false;\n const candidate = event as {\n type?: unknown;\n instanceId?: unknown;\n enteredAtMs?: unknown;\n params?: unknown;\n requestContext?: unknown;\n };\n return (\n candidate.type === RUN_EVENT_TYPE &&\n candidate.requestContext === undefined &&\n typeof candidate.instanceId === 'string' &&\n typeof candidate.enteredAtMs === 'number' &&\n typeof candidate.params === 'object' &&\n candidate.params !== null\n );\n}\n\nconst utf8 = new TextEncoder();\n\n/**\n * The durable execution name for one start of an instance: the instance id, cut to fit, with a\n * fingerprint of the whole id (so a cut never makes two instances collide) and the attempt's\n * token. Every retry of that start reuses the recorded name; a new attempt gets a new token.\n */\nexport function executionName(instanceId: string, attempt: string): string {\n const safe = instanceId.replace(/[^a-zA-Z0-9_-]/g, '-');\n const token = attempt.replace(/[^a-zA-Z0-9]/g, '').slice(0, 8);\n const fingerprint = `${murmur3(utf8.encode(instanceId)).toString(36)}${murmur3(utf8.encode(instanceId), 0x9747b28c).toString(36)}`;\n const suffix = `-${fingerprint}-${token}`;\n return `${safe.slice(0, 64 - suffix.length)}${suffix}`;\n}\n\nexport interface AwsInstancesOptions {\n store: JourneyStore;\n executions: ExecutionRegistry;\n lambda: LambdaApi;\n /**\n * The qualified function ARN an execution pinned to `contentHash` must run on; `version` is\n * the code version whose routes named it.\n */\n target(contentHash: string, version?: string): Promise<string>;\n /**\n * How long an unconfirmed start keeps being retried. It must stay shorter than the durable\n * execution retention: past that the platform forgets the name, and a retry would start a\n * second execution. Twelve hours by default.\n */\n startRetryMs?: number;\n /** How stale a running instance's last check must be before maintenance looks again. */\n reconcileEveryMs?: number;\n /**\n * How long after a stop an invocation already running may still act: Lambda cannot interrupt\n * one, and it runs on to its next checkpoint. An erasure waits this long before it erases.\n * The function's timeout is enough; 15 minutes (Lambda's maximum) by default.\n */\n stopDrainMs?: number;\n now?: () => number;\n}\n\nconst MINUTE = 60_000;\n\nexport class AwsInstances implements WorkflowBindingLike {\n private readonly now: () => number;\n private readonly startRetryMs: number;\n private readonly reconcileEveryMs: number;\n private readonly stopDrainMs: number;\n\n constructor(private readonly options: AwsInstancesOptions) {\n this.now = options.now ?? Date.now;\n this.startRetryMs = options.startRetryMs ?? 12 * 60 * MINUTE;\n this.reconcileEveryMs = options.reconcileEveryMs ?? 15 * MINUTE;\n this.stopDrainMs = options.stopDrainMs ?? 15 * MINUTE;\n }\n\n /**\n * A start, planned for one entry attempt. Its name is new every time: a failed entry reclaimed\n * under the same instance id (an event replayed with its ts) must start a new execution — the\n * old name would return the closed one. Retries of this attempt reuse the recorded name.\n */\n async plan(options: {\n id: string;\n params: JourneyParams;\n ts: number;\n version?: string | undefined;\n }): Promise<ExecutionStart> {\n const input: RunInput = {\n type: RUN_EVENT_TYPE,\n instanceId: options.id,\n params: options.params,\n // The moment the instance is created, like Cloudflare's: an event's own ts may be long past\n enteredAtMs: this.now(),\n };\n return {\n name: executionName(options.id, crypto.randomUUID().slice(0, 8)),\n target: await this.options.target(options.params.contentHash, options.version),\n input: JSON.stringify(input),\n };\n }\n\n async create(options: { id: string; params: unknown }): Promise<WorkflowInstanceLike> {\n const record = await this.options.executions.get(options.id);\n if (record === undefined) {\n throw new Error(`no start is recorded for ${options.id}: the entry must be planned first`);\n }\n await this.submit(record);\n return this.instance(options.id);\n }\n\n async get(id: string): Promise<WorkflowInstanceLike> {\n // A read of the registry alone: a Lambda failure must never read as \"no such instance\"\n if ((await this.options.executions.get(id)) === undefined) {\n throw new InstanceNotFoundError(id);\n }\n return this.instance(id);\n }\n\n async resume(entry: { workflow: string; userId: string }): Promise<void> {\n for (const record of await this.options.executions.pendingStarts({ ...entry, limit: 1 })) {\n if (!(await this.giveUpIfStale(record))) await this.submit(record);\n }\n }\n\n async maintain(): Promise<void> {\n const now = this.now();\n const failures: unknown[] = [];\n // Starts whose submit never confirmed: retried with the recorded name and payload\n for (const record of await this.options.executions.pendingStarts({\n createdBefore: now - MINUTE,\n limit: 25,\n })) {\n try {\n if (!(await this.giveUpIfStale(record))) await this.submit(record);\n } catch (error) {\n failures.push(error);\n }\n }\n // Executions the platform ended without the run finalizing (a timeout, a stop, a crash)\n for (const record of await this.options.executions.toReconcile(\n 25,\n now - this.reconcileEveryMs\n )) {\n try {\n await this.reconcile(record, now);\n } catch (error) {\n failures.push(error);\n }\n }\n if (failures.length > 0) throw failures[0];\n }\n\n private async submit(record: ExecutionRecord): Promise<void> {\n const { arn } = await this.options.lambda.startExecution({\n target: record.target,\n name: record.name,\n payload: record.input,\n });\n await this.options.executions.markSubmitted(record.instanceId, arn, this.now());\n }\n\n /** Past the retry budget a start is unresolved for good: no retry could tell a duplicate apart. */\n private async giveUpIfStale(record: ExecutionRecord): Promise<boolean> {\n if (this.now() - record.createdAt <= this.startRetryMs) return false;\n await this.options.executions.markUnresolved(record.instanceId, this.now());\n console.error(\n `[workflow] the start of ${record.instanceId} (${record.name}) was never confirmed: left unresolved`\n );\n return true;\n }\n\n private async arnOf(record: ExecutionRecord): Promise<string | undefined> {\n if (record.arn !== undefined) return record.arn;\n const arn = await this.options.lambda.findExecution(record.target, record.name);\n if (arn !== undefined) await this.options.executions.setArn(record.instanceId, arn);\n return arn;\n }\n\n /**\n * Past the start retry budget, an execution still nowhere to be seen never started: any queued\n * invoke has expired by then. Before that, not finding it says nothing.\n */\n private vanished(record: ExecutionRecord): boolean {\n return this.now() - record.createdAt > this.startRetryMs;\n }\n\n /** Stopped so recently that an invocation running at the time may still be acting. */\n private draining(record: ExecutionRecord): boolean {\n return record.stoppedAt !== undefined && this.now() - record.stoppedAt < this.stopDrainMs;\n }\n\n /**\n * The one judgment every path makes of an execution the platform reports stopped — by\n * `terminate`, by an operator, or seen by `reconcile` before the stop was recorded: the first\n * sighting is recorded (`markStopped` keeps an earlier time, so no poll ever extends the\n * drain), and the record is read again so the verdict rests on what is committed now, not on\n * the snapshot a caller read before its API calls. A write or read that fails throws: an\n * unknown stop time is never taken for \"safely stopped\". Undefined means the record is gone —\n * the erasure finished with it meanwhile.\n */\n private async observeStop(instanceId: string): Promise<ExecutionRecord | undefined> {\n await this.options.executions.markStopped(instanceId, this.now());\n return this.options.executions.get(instanceId);\n }\n\n private async reconcile(record: ExecutionRecord, now: number): Promise<void> {\n const arn = await this.arnOf(record);\n const state = arn === undefined ? undefined : await this.options.lambda.executionState(arn);\n // Only a state that will never run another step — or one missing for good — ends the entry;\n // a run still queued, or not visible yet, is left alone\n const ended =\n state === 'failed' ||\n state === 'stopped' ||\n state === 'timed_out' ||\n (state === undefined && this.vanished(record));\n if (ended) {\n // The verdict is taken on the record as it is now: `toReconcile` handed over a snapshot,\n // and a stop may have been recorded since — by `terminate`, or here, at its first sighting\n const fresh =\n state === 'stopped'\n ? await this.observeStop(record.instanceId)\n : await this.options.executions.get(record.instanceId);\n if (fresh === undefined) return;\n if (this.draining(fresh)) {\n // Stopped, but an invocation running at the stop may still be acting: the entry stays\n // running until the drain has passed, so an erasure keeps waiting for it (`status`\n // says running too), and the next look finalizes it\n await this.options.executions.markChecked(record.instanceId, now);\n return;\n }\n // Ended without its finalize step: the ledger must not show it running forever\n const { store } = this.options;\n await store.setEntryStatus(record.instanceId, 'failed');\n await store.reporting?.instanceFinished({\n workflow: record.workflow,\n instanceId: record.instanceId,\n userId: record.userId,\n status: 'failed',\n at: now,\n });\n console.warn(\n `[workflow] ${record.instanceId} ended on the platform (${state ?? 'unknown'}) without finishing: recorded as failed`\n );\n }\n await this.options.executions.markChecked(record.instanceId, now);\n }\n\n private instance(id: string): WorkflowInstanceLike {\n const { executions, lambda } = this.options;\n const current = async () => {\n const record = await executions.get(id);\n if (record === undefined) throw new InstanceNotFoundError(id);\n return record;\n };\n return {\n id,\n sendEvent: async (event) => {\n // The wait parked now, read at the moment of the wake: a newer wait's callback replaces\n // an older one, and a wake for none (the instance is between waits) is nothing to do —\n // its next wait re-reads the facts before parking\n const { callbackId } = await current();\n if (callbackId === undefined) return;\n await lambda.completeCallback(callbackId, JSON.stringify(event.payload ?? {}));\n },\n terminate: async () => {\n const record = await current();\n // Stopped before: done once an invocation that was running then has had time to end\n if (record.stoppedAt !== undefined) {\n if (this.draining(record)) {\n throw new Error(`${id} was stopped; its running invocation may still be finishing`);\n }\n return;\n }\n if (record.submittedAt === undefined && record.unresolvedAt === undefined) {\n // No retry may start it after this\n await executions.markUnresolved(id, this.now());\n }\n const arn = await this.arnOf(record);\n if (arn === undefined) {\n // Not visible yet is not stopped: the erasure stays pending, and the sweep retries\n if (this.vanished(record)) return;\n throw new Error(`cannot stop ${id} yet: its execution is not visible`);\n }\n // One that finished on its own has no invocation left to drain\n const state = await lambda.executionState(arn);\n if (state === 'succeeded' || state === 'failed' || state === 'timed_out') return;\n // A stop does not interrupt an invocation already running: it goes on to its next\n // checkpoint, side effects included. The erasure waits (see stopDrainMs). One already\n // stopped (by an operator, or a stop whose record did not land) is not stopped again:\n // its first sighting is recorded and the same drain judged\n if (state !== 'stopped' && !(await lambda.stopExecution(arn))) {\n throw new Error(`stopping ${id} failed`);\n }\n const fresh = await this.observeStop(id);\n if (fresh !== undefined && this.draining(fresh)) {\n throw new Error(`${id} is stopped; its running invocation may still be finishing`);\n }\n },\n status: async () => {\n const record = await current();\n if (this.draining(record)) return { status: 'running' };\n const arn = await this.arnOf(record);\n // Not visible yet may still run: only one missing past the budget is gone\n if (arn === undefined) return { status: this.vanished(record) ? 'terminated' : 'running' };\n const state = await lambda.executionState(arn);\n // In the router's vocabulary (Cloudflare's): what will never run another step\n if (state === 'succeeded') return { status: 'complete' };\n if (state === 'failed') return { status: 'errored' };\n if (state === 'running') return { status: 'running' };\n if (state === 'stopped') {\n // Stopped, seen here first or recorded since this read: the drain is the judgment\n const fresh = await this.observeStop(id);\n if (fresh !== undefined && this.draining(fresh)) return { status: 'running' };\n }\n return { status: 'terminated' };\n },\n };\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"instances.mjs","names":[],"sources":["../../src/aws/instances.ts"],"sourcesContent":["import { murmur3 } from '../engine/bucket';\nimport {\n InstanceNotFoundError,\n type JourneyParams,\n type WorkflowBindingLike,\n type WorkflowInstanceLike,\n} from '../host/platform';\nimport type {\n ExecutionRecord,\n ExecutionRegistry,\n ExecutionStart,\n JourneyStore,\n} from '../store/index';\nimport type { LambdaApi } from './lambda-api';\n\n/**\n * The router's instance port over Lambda durable executions: an instance is one execution of\n * this same function, started by an asynchronous self-invoke under a stable execution name;\n * a wake completes the callback its current wait parks on; stopping stops the execution.\n */\n\n/** The top-level shape of an internal run: what the self-invoke sends, never an HTTP body. */\nexport const RUN_EVENT_TYPE = '$workflow.run';\n\nexport interface RunInput {\n type: typeof RUN_EVENT_TYPE;\n instanceId: string;\n params: JourneyParams;\n /** When the instance was created (as Cloudflare's event timestamp): its entry anchor, fixed with the start. */\n enteredAtMs: number;\n}\n\nexport function isRunInput(event: unknown): event is RunInput {\n if (typeof event !== 'object' || event === null) return false;\n const candidate = event as {\n type?: unknown;\n instanceId?: unknown;\n enteredAtMs?: unknown;\n params?: unknown;\n requestContext?: unknown;\n };\n return (\n candidate.type === RUN_EVENT_TYPE &&\n candidate.requestContext === undefined &&\n typeof candidate.instanceId === 'string' &&\n typeof candidate.enteredAtMs === 'number' &&\n typeof candidate.params === 'object' &&\n candidate.params !== null\n );\n}\n\nconst utf8 = new TextEncoder();\n\n/**\n * The durable execution name for one start of an instance: the instance id, cut to fit, with a\n * fingerprint of the whole id (so a cut never makes two instances collide) and the attempt's\n * token. Every retry of that start reuses the recorded name; a new attempt gets a new token.\n */\nexport function executionName(instanceId: string, attempt: string): string {\n const safe = instanceId.replace(/[^a-zA-Z0-9_-]/g, '-');\n const token = attempt.replace(/[^a-zA-Z0-9]/g, '').slice(0, 8);\n const fingerprint = `${murmur3(utf8.encode(instanceId)).toString(36)}${murmur3(utf8.encode(instanceId), 0x9747b28c).toString(36)}`;\n const suffix = `-${fingerprint}-${token}`;\n return `${safe.slice(0, 64 - suffix.length)}${suffix}`;\n}\n\nexport interface AwsInstancesOptions {\n store: JourneyStore;\n executions: ExecutionRegistry;\n lambda: LambdaApi;\n /**\n * The qualified function ARN an execution pinned to `contentHash` must run on; `version` is\n * the code version whose routes named it.\n */\n target(contentHash: string, version?: string): Promise<string>;\n /**\n * How long an unconfirmed start keeps being retried. It must stay shorter than the durable\n * execution retention: past that the platform forgets the name, and a retry would start a\n * second execution. Twelve hours by default.\n */\n startRetryMs?: number;\n /** How stale a running instance's last check must be before maintenance looks again. */\n reconcileEveryMs?: number;\n /**\n * How long after a stop an invocation already running may still act: Lambda cannot interrupt\n * one, and it runs on to its next checkpoint. An erasure waits this long before it erases.\n * The function's timeout is enough; 15 minutes (Lambda's maximum) by default.\n */\n stopDrainMs?: number;\n now?: () => number;\n}\n\nconst MINUTE = 60_000;\n\nexport class AwsInstances implements WorkflowBindingLike {\n private readonly now: () => number;\n private readonly startRetryMs: number;\n private readonly reconcileEveryMs: number;\n private readonly stopDrainMs: number;\n\n constructor(private readonly options: AwsInstancesOptions) {\n this.now = options.now ?? Date.now;\n this.startRetryMs = options.startRetryMs ?? 12 * 60 * MINUTE;\n this.reconcileEveryMs = options.reconcileEveryMs ?? 15 * MINUTE;\n this.stopDrainMs = options.stopDrainMs ?? 15 * MINUTE;\n }\n\n /**\n * A start, planned for one entry attempt. Its name is new every time: a failed entry reclaimed\n * under the same instance id (an event replayed with its ts) must start a new execution — the\n * old name would return the closed one. Retries of this attempt reuse the recorded name.\n */\n async plan(options: {\n id: string;\n params: JourneyParams;\n ts: number;\n version?: string | undefined;\n }): Promise<ExecutionStart> {\n const input: RunInput = {\n type: RUN_EVENT_TYPE,\n instanceId: options.id,\n params: options.params,\n // The moment the instance is created, like Cloudflare's: an event's own ts may be long past\n enteredAtMs: this.now(),\n };\n return {\n name: executionName(options.id, crypto.randomUUID().slice(0, 8)),\n target: await this.options.target(options.params.contentHash, options.version),\n input: JSON.stringify(input),\n };\n }\n\n async create(options: { id: string; params: unknown }): Promise<WorkflowInstanceLike> {\n const record = await this.options.executions.get(options.id);\n if (record === undefined) {\n throw new Error(`no start is recorded for ${options.id}: the entry must be planned first`);\n }\n await this.submit(record);\n return this.instance(options.id);\n }\n\n async get(id: string): Promise<WorkflowInstanceLike> {\n // A read of the registry alone: a Lambda failure must never read as \"no such instance\"\n if ((await this.options.executions.get(id)) === undefined) {\n throw new InstanceNotFoundError(id);\n }\n return this.instance(id);\n }\n\n async resume(entry: { workflow: string; userId: string }): Promise<void> {\n for (const record of await this.options.executions.pendingStarts({ ...entry, limit: 1 })) {\n if (!(await this.giveUpIfStale(record))) await this.submit(record);\n }\n }\n\n async maintain(): Promise<void> {\n const now = this.now();\n const failures: unknown[] = [];\n // Starts whose submit never confirmed: retried with the recorded name and payload\n for (const record of await this.options.executions.pendingStarts({\n createdBefore: now - MINUTE,\n limit: 25,\n })) {\n try {\n if (!(await this.giveUpIfStale(record))) await this.submit(record);\n } catch (error) {\n failures.push(error);\n }\n }\n // Executions the platform ended without the run finalizing (a timeout, a stop, a crash)\n for (const record of await this.options.executions.toReconcile(\n 25,\n now - this.reconcileEveryMs\n )) {\n try {\n await this.reconcile(record, now);\n } catch (error) {\n failures.push(error);\n }\n }\n if (failures.length > 0) throw failures[0];\n }\n\n private async submit(record: ExecutionRecord): Promise<void> {\n const { arn } = await this.options.lambda.startExecution({\n target: record.target,\n name: record.name,\n payload: record.input,\n });\n await this.options.executions.markSubmitted(record.instanceId, arn, this.now());\n }\n\n /** Past the retry budget a start is unresolved for good: no retry could tell a duplicate apart. */\n private async giveUpIfStale(record: ExecutionRecord): Promise<boolean> {\n if (this.now() - record.createdAt <= this.startRetryMs) return false;\n await this.options.executions.markUnresolved(record.instanceId, this.now());\n console.error(\n `[workflow] the start of ${record.instanceId} (${record.name}) was never confirmed: left unresolved`\n );\n return true;\n }\n\n private async arnOf(record: ExecutionRecord): Promise<string | undefined> {\n if (record.arn !== undefined) return record.arn;\n const arn = await this.options.lambda.findExecution(record.target, record.name);\n if (arn !== undefined) await this.options.executions.setArn(record.instanceId, arn);\n return arn;\n }\n\n /**\n * Past the start retry budget, an execution still nowhere to be seen never started: any queued\n * invoke has expired by then. Before that, not finding it says nothing.\n */\n private vanished(record: ExecutionRecord): boolean {\n return this.now() - record.createdAt > this.startRetryMs;\n }\n\n /** Stopped so recently that an invocation running at the time may still be acting. */\n private draining(record: ExecutionRecord): boolean {\n return record.stoppedAt !== undefined && this.now() - record.stoppedAt < this.stopDrainMs;\n }\n\n /**\n * The one judgment every path makes of an execution the platform reports stopped — by\n * `terminate`, by an operator, or seen by `reconcile` before the stop was recorded: the first\n * sighting is recorded (`markStopped` keeps an earlier time, so no poll ever extends the\n * drain), and the record is read again so the verdict rests on what is committed now, not on\n * the snapshot a caller read before its API calls. A write or read that fails throws: an\n * unknown stop time is never taken for \"safely stopped\". Undefined means the record is gone —\n * the erasure finished with it meanwhile.\n */\n private async observeStop(instanceId: string): Promise<ExecutionRecord | undefined> {\n await this.options.executions.markStopped(instanceId, this.now());\n return this.options.executions.get(instanceId);\n }\n\n private async reconcile(record: ExecutionRecord, now: number): Promise<void> {\n const arn = await this.arnOf(record);\n const state = arn === undefined ? undefined : await this.options.lambda.executionState(arn);\n // Only a state that will never run another step — or one missing for good — ends the entry;\n // a run still queued, or not visible yet, is left alone\n const ended =\n state === 'failed' ||\n state === 'stopped' ||\n state === 'timed_out' ||\n (state === undefined && this.vanished(record));\n if (ended) {\n // The verdict is taken on the record as it is now: `toReconcile` handed over a snapshot,\n // and a stop may have been recorded since — by `terminate`, or here, at its first sighting\n const fresh =\n state === 'stopped'\n ? await this.observeStop(record.instanceId)\n : await this.options.executions.get(record.instanceId);\n if (fresh === undefined) return;\n if (this.draining(fresh)) {\n // Stopped, but an invocation running at the stop may still be acting: the entry stays\n // running until the drain has passed, so an erasure keeps waiting for it (`status`\n // says running too), and the next look finalizes it\n await this.options.executions.markChecked(record.instanceId, now);\n return;\n }\n // Ended without its finalize step: the ledger must not show it running forever\n const { store } = this.options;\n await store.setEntryStatus(record.instanceId, 'failed');\n await store.reporting.instanceFinished({\n workflow: record.workflow,\n instanceId: record.instanceId,\n userId: record.userId,\n status: 'failed',\n at: now,\n });\n console.warn(\n `[workflow] ${record.instanceId} ended on the platform (${state ?? 'unknown'}) without finishing: recorded as failed`\n );\n }\n await this.options.executions.markChecked(record.instanceId, now);\n }\n\n private instance(id: string): WorkflowInstanceLike {\n const { executions, lambda } = this.options;\n const current = async () => {\n const record = await executions.get(id);\n if (record === undefined) throw new InstanceNotFoundError(id);\n return record;\n };\n return {\n id,\n sendEvent: async (event) => {\n // The wait parked now, read at the moment of the wake: a newer wait's callback replaces\n // an older one, and a wake for none (the instance is between waits) is nothing to do —\n // its next wait re-reads the facts before parking\n const { callbackId } = await current();\n if (callbackId === undefined) return;\n await lambda.completeCallback(callbackId, JSON.stringify(event.payload ?? {}));\n },\n terminate: async () => {\n const record = await current();\n // Stopped before: done once an invocation that was running then has had time to end\n if (record.stoppedAt !== undefined) {\n if (this.draining(record)) {\n throw new Error(`${id} was stopped; its running invocation may still be finishing`);\n }\n return;\n }\n if (record.submittedAt === undefined && record.unresolvedAt === undefined) {\n // No retry may start it after this\n await executions.markUnresolved(id, this.now());\n }\n const arn = await this.arnOf(record);\n if (arn === undefined) {\n // Not visible yet is not stopped: the erasure stays pending, and the sweep retries\n if (this.vanished(record)) return;\n throw new Error(`cannot stop ${id} yet: its execution is not visible`);\n }\n // One that finished on its own has no invocation left to drain\n const state = await lambda.executionState(arn);\n if (state === 'succeeded' || state === 'failed' || state === 'timed_out') return;\n // A stop does not interrupt an invocation already running: it goes on to its next\n // checkpoint, side effects included. The erasure waits (see stopDrainMs). One already\n // stopped (by an operator, or a stop whose record did not land) is not stopped again:\n // its first sighting is recorded and the same drain judged\n if (state !== 'stopped' && !(await lambda.stopExecution(arn))) {\n throw new Error(`stopping ${id} failed`);\n }\n const fresh = await this.observeStop(id);\n if (fresh !== undefined && this.draining(fresh)) {\n throw new Error(`${id} is stopped; its running invocation may still be finishing`);\n }\n },\n status: async () => {\n const record = await current();\n if (this.draining(record)) return { status: 'running' };\n const arn = await this.arnOf(record);\n // Not visible yet may still run: only one missing past the budget is gone\n if (arn === undefined) return { status: this.vanished(record) ? 'terminated' : 'running' };\n const state = await lambda.executionState(arn);\n // In the router's vocabulary (Cloudflare's): what will never run another step\n if (state === 'succeeded') return { status: 'complete' };\n if (state === 'failed') return { status: 'errored' };\n if (state === 'running') return { status: 'running' };\n if (state === 'stopped') {\n // Stopped, seen here first or recorded since this read: the drain is the judgment\n const fresh = await this.observeStop(id);\n if (fresh !== undefined && this.draining(fresh)) return { status: 'running' };\n }\n return { status: 'terminated' };\n },\n };\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"bindings.cjs","names":["InstanceNotFoundError"],"sources":["../../src/cloudflare/bindings.ts"],"sourcesContent":["import type { EmailAddress } from '../email';\nimport { InstanceNotFoundError, type WorkflowBindingLike } from '../host/platform';\n\n/**\n * A minimal structural interface over the Cloudflare bindings.\n *\n * @cloudflare/workers-types is deliberately not used: the global types it\n * injects clash with the DOM lib this package's react side needs. A structural\n * subset plus the ambient cloudflare:workers declaration is enough to compile,\n * and the real bindings satisfy it structurally at runtime.\n */\n\n/** Structural subset of D1Result: `meta.changes` is how INSERT OR IGNORE reports whether it inserted. */\nexport interface D1RunResultLike {\n meta?: { changes?: number };\n}\n\nexport interface D1PreparedStatementLike {\n bind(...values: unknown[]): D1PreparedStatementLike;\n first<T = Record<string, unknown>>(): Promise<T | null>;\n all<T = Record<string, unknown>>(): Promise<{ results: T[] }>;\n run(): Promise<D1RunResultLike>;\n}\n\nexport interface D1DatabaseLike {\n prepare(sql: string): D1PreparedStatementLike;\n /** Statements run in order inside one transaction (real D1 semantics) — deploy relies on the atomicity. */\n batch(statements: D1PreparedStatementLike[]): Promise<unknown>;\n}\n\n/**\n * Cloudflare Email Service's send_email binding (structural subset).\n *\n * `idempotencyKey` is part of the call because the send happens inside a\n * `step.do`: a step body re-runs after its fn resolved but before the\n * checkpoint committed, so a binding that drops the key mails the user twice.\n * The binding is app-supplied glue (nothing here can de-duplicate — there is\n * no state between two runs of the same step), which is why the port makes the\n * key impossible to miss rather than merely available.\n */\nexport interface EmailBindingLike {\n send(message: {\n from: EmailAddress;\n to: EmailAddress;\n replyTo?: EmailAddress;\n subject: string;\n html: string;\n /** List-Unsubscribe and List-Unsubscribe-Post, when the email has an unsubscribe link. */\n headers?: Readonly<Record<string, string>>;\n /** `${instanceId}:${nodeId}`: stable across replays and retries — drop a send whose key was already delivered. */\n idempotencyKey: string;\n }): Promise<unknown>;\n}\n\n/** Cloudflare's Workflow binding, as far as the host uses it. */\nexport type CfWorkflowBinding = Pick<WorkflowBindingLike, 'create' | 'get'>;\n\n/** How Cloudflare's `get` reports an unknown instance id. */\nconst CF_NOT_FOUND = 'instance.not_found';\n\n/**\n * The Workflow binding as the host's runtime port: `get` answers an unknown id with\n * `InstanceNotFoundError`, as the port promises, instead of Cloudflare's own error. Everything\n * else is forwarded as is.\n */\nexport function cloudflareJourneyBinding(binding: CfWorkflowBinding): WorkflowBindingLike {\n return {\n create: (options) => binding.create(options),\n async get(id) {\n try {\n return await binding.get(id);\n } catch (error) {\n if (String(error).includes(CF_NOT_FOUND)) {\n throw new InstanceNotFoundError(id, { cause: error });\n }\n throw error;\n }\n },\n };\n}\n\n/**\n * Slack on the deadline when deciding whether a thrown wait was a timeout: the\n * platform may surface the expiry a moment early, and a wait shorter than this\n * has no room to tell the two apart anyway.\n */\nexport const WAKE_TIMEOUT_TOLERANCE_MS = 5_000;\n\n/**\n * Did a wait that threw actually reach its deadline?\n *\n * CF signals a wait timeout by throwing, and so does every other failure in\n * there — a broken binding, an evicted instance, a malformed timeout. Reading\n * them all as timeouts would make an infrastructure fault indistinguishable\n * from \"the user never did it\" (the journey would take its onTimeout branch,\n * early and silently). Only the clock can tell them apart, and only against\n * the absolute deadline: measured from when the wait was parked, a replay\n * after an eviction would start the measure over and read a\n * genuine expiry as a failure.\n */\nexport function wakeExpired(deadlineMs: number, now: number): boolean {\n return now >= deadlineMs - WAKE_TIMEOUT_TOLERANCE_MS;\n}\n"],"mappings":";;;;AA0DA,MAAM,eAAe;;;;;;AAOrB,SAAgB,yBAAyB,SAAiD;CACxF,OAAO;EACL,SAAS,YAAY,QAAQ,OAAO,OAAO;EAC3C,MAAM,IAAI,IAAI;GACZ,IAAI;IACF,OAAO,MAAM,QAAQ,IAAI,EAAE;GAC7B,SAAS,OAAO;IACd,IAAI,OAAO,KAAK,CAAC,CAAC,SAAS,YAAY,GACrC,MAAM,IAAIA,sBAAAA,sBAAsB,IAAI,EAAE,OAAO,MAAM,CAAC;IAEtD,MAAM;GACR;EACF;CACF;AACF;;;;;;AAOA,MAAa,4BAA4B;;;;;;;;;;;;;AAczC,SAAgB,YAAY,YAAoB,KAAsB;CACpE,OAAO,OAAO,aAAa;AAC7B"}
1
+ {"version":3,"file":"bindings.cjs","names":["InstanceNotFoundError"],"sources":["../../src/cloudflare/bindings.ts"],"sourcesContent":["import type { EmailAddress } from '../email';\nimport { InstanceNotFoundError, type WorkflowBindingLike } from '../host/platform';\n\n/**\n * A minimal structural interface over the Cloudflare bindings.\n *\n * @cloudflare/workers-types is deliberately not used: the global types it\n * injects clash with the DOM lib this package's react side needs. A structural\n * subset plus the ambient cloudflare:workers declaration is enough to compile,\n * and the real bindings satisfy it structurally at runtime.\n */\n\n/**\n * Cloudflare Email Service's send_email binding (structural subset).\n *\n * `idempotencyKey` is part of the call because the send happens inside a\n * `step.do`: a step body re-runs after its fn resolved but before the\n * checkpoint committed, so a binding that drops the key mails the user twice.\n * The binding is app-supplied glue (nothing here can de-duplicate — there is\n * no state between two runs of the same step), which is why the port makes the\n * key impossible to miss rather than merely available.\n */\nexport interface EmailBindingLike {\n send(message: {\n from: EmailAddress;\n to: EmailAddress;\n replyTo?: EmailAddress;\n subject: string;\n html: string;\n /** List-Unsubscribe and List-Unsubscribe-Post, when the email has an unsubscribe link. */\n headers?: Readonly<Record<string, string>>;\n /** `${instanceId}:${nodeId}`: stable across replays and retries — drop a send whose key was already delivered. */\n idempotencyKey: string;\n }): Promise<unknown>;\n}\n\n/** Cloudflare's Workflow binding, as far as the host uses it. */\nexport type CfWorkflowBinding = Pick<WorkflowBindingLike, 'create' | 'get'>;\n\n/** How Cloudflare's `get` reports an unknown instance id. */\nconst CF_NOT_FOUND = 'instance.not_found';\n\n/**\n * The Workflow binding as the host's runtime port: `get` answers an unknown id with\n * `InstanceNotFoundError`, as the port promises, instead of Cloudflare's own error. Everything\n * else is forwarded as is.\n */\nexport function cloudflareJourneyBinding(binding: CfWorkflowBinding): WorkflowBindingLike {\n return {\n create: (options) => binding.create(options),\n async get(id) {\n try {\n return await binding.get(id);\n } catch (error) {\n if (String(error).includes(CF_NOT_FOUND)) {\n throw new InstanceNotFoundError(id, { cause: error });\n }\n throw error;\n }\n },\n };\n}\n\n/**\n * Slack on the deadline when deciding whether a thrown wait was a timeout: the\n * platform may surface the expiry a moment early, and a wait shorter than this\n * has no room to tell the two apart anyway.\n */\nexport const WAKE_TIMEOUT_TOLERANCE_MS = 5_000;\n\n/**\n * Did a wait that threw actually reach its deadline?\n *\n * CF signals a wait timeout by throwing, and so does every other failure in\n * there — a broken binding, an evicted instance, a malformed timeout. Reading\n * them all as timeouts would make an infrastructure fault indistinguishable\n * from \"the user never did it\" (the journey would take its onTimeout branch,\n * early and silently). Only the clock can tell them apart, and only against\n * the absolute deadline: measured from when the wait was parked, a replay\n * after an eviction would start the measure over and read a\n * genuine expiry as a failure.\n */\nexport function wakeExpired(deadlineMs: number, now: number): boolean {\n return now >= deadlineMs - WAKE_TIMEOUT_TOLERANCE_MS;\n}\n"],"mappings":";;;;AAwCA,MAAM,eAAe;;;;;;AAOrB,SAAgB,yBAAyB,SAAiD;CACxF,OAAO;EACL,SAAS,YAAY,QAAQ,OAAO,OAAO;EAC3C,MAAM,IAAI,IAAI;GACZ,IAAI;IACF,OAAO,MAAM,QAAQ,IAAI,EAAE;GAC7B,SAAS,OAAO;IACd,IAAI,OAAO,KAAK,CAAC,CAAC,SAAS,YAAY,GACrC,MAAM,IAAIA,sBAAAA,sBAAsB,IAAI,EAAE,OAAO,MAAM,CAAC;IAEtD,MAAM;GACR;EACF;CACF;AACF;;;;;;AAOA,MAAa,4BAA4B;;;;;;;;;;;;;AAczC,SAAgB,YAAY,YAAoB,KAAsB;CACpE,OAAO,OAAO,aAAa;AAC7B"}
@@ -9,25 +9,6 @@ import { WorkflowBindingLike } from "../host/platform.cjs";
9
9
  * subset plus the ambient cloudflare:workers declaration is enough to compile,
10
10
  * and the real bindings satisfy it structurally at runtime.
11
11
  */
12
- /** Structural subset of D1Result: `meta.changes` is how INSERT OR IGNORE reports whether it inserted. */
13
- export interface D1RunResultLike {
14
- meta?: {
15
- changes?: number;
16
- };
17
- }
18
- export interface D1PreparedStatementLike {
19
- bind(...values: unknown[]): D1PreparedStatementLike;
20
- first<T = Record<string, unknown>>(): Promise<T | null>;
21
- all<T = Record<string, unknown>>(): Promise<{
22
- results: T[];
23
- }>;
24
- run(): Promise<D1RunResultLike>;
25
- }
26
- export interface D1DatabaseLike {
27
- prepare(sql: string): D1PreparedStatementLike;
28
- /** Statements run in order inside one transaction (real D1 semantics) — deploy relies on the atomicity. */
29
- batch(statements: D1PreparedStatementLike[]): Promise<unknown>;
30
- }
31
12
  /**
32
13
  * Cloudflare Email Service's send_email binding (structural subset).
33
14
  *
@@ -1 +1 @@
1
- {"version":3,"file":"bindings.d.cts","names":[],"sources":["../../src/cloudflare/bindings.ts"],"mappings":";;;;;;;;;;;;iBAaiB;EACf;IAAS;;;iBAGM;EACf,QAAQ,oBAAoB;EAC5B,MAAM,IAAI,4BAA4B,QAAQ;EAC9C,IAAI,IAAI,4BAA4B;IAAU,SAAS;;EACvD,OAAO,QAAQ;;iBAGA;EACf,QAAQ,cAAc;;EAEtB,MAAM,YAAY,4BAA4B;;;;;;;;;;;;iBAa/B;EACf,KAAK;IACH,MAAM;IACN,IAAI;IACJ,UAAU;IACV;IACA;;IAEA,UAAU,SAAS;;IAEnB;MACE;;;YAIM,oBAAoB,KAAK;;;;;;wBAUrB,yBAAyB,SAAS,oBAAoB;;;;;;qBAqBzD;;;;;;;;;;;;;wBAcG,YAAY,oBAAoB"}
1
+ {"version":3,"file":"bindings.d.cts","names":[],"sources":["../../src/cloudflare/bindings.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;iBAsBiB;EACf,KAAK;IACH,MAAM;IACN,IAAI;IACJ,UAAU;IACV;IACA;;IAEA,UAAU,SAAS;;IAEnB;MACE;;;YAIM,oBAAoB,KAAK;;;;;;wBAUrB,yBAAyB,SAAS,oBAAoB;;;;;;qBAqBzD;;;;;;;;;;;;;wBAcG,YAAY,oBAAoB"}
@@ -9,25 +9,6 @@ import { WorkflowBindingLike } from "../host/platform.mjs";
9
9
  * subset plus the ambient cloudflare:workers declaration is enough to compile,
10
10
  * and the real bindings satisfy it structurally at runtime.
11
11
  */
12
- /** Structural subset of D1Result: `meta.changes` is how INSERT OR IGNORE reports whether it inserted. */
13
- export interface D1RunResultLike {
14
- meta?: {
15
- changes?: number;
16
- };
17
- }
18
- export interface D1PreparedStatementLike {
19
- bind(...values: unknown[]): D1PreparedStatementLike;
20
- first<T = Record<string, unknown>>(): Promise<T | null>;
21
- all<T = Record<string, unknown>>(): Promise<{
22
- results: T[];
23
- }>;
24
- run(): Promise<D1RunResultLike>;
25
- }
26
- export interface D1DatabaseLike {
27
- prepare(sql: string): D1PreparedStatementLike;
28
- /** Statements run in order inside one transaction (real D1 semantics) — deploy relies on the atomicity. */
29
- batch(statements: D1PreparedStatementLike[]): Promise<unknown>;
30
- }
31
12
  /**
32
13
  * Cloudflare Email Service's send_email binding (structural subset).
33
14
  *
@@ -1 +1 @@
1
- {"version":3,"file":"bindings.d.mts","names":[],"sources":["../../src/cloudflare/bindings.ts"],"mappings":";;;;;;;;;;;;iBAaiB;EACf;IAAS;;;iBAGM;EACf,QAAQ,oBAAoB;EAC5B,MAAM,IAAI,4BAA4B,QAAQ;EAC9C,IAAI,IAAI,4BAA4B;IAAU,SAAS;;EACvD,OAAO,QAAQ;;iBAGA;EACf,QAAQ,cAAc;;EAEtB,MAAM,YAAY,4BAA4B;;;;;;;;;;;;iBAa/B;EACf,KAAK;IACH,MAAM;IACN,IAAI;IACJ,UAAU;IACV;IACA;;IAEA,UAAU,SAAS;;IAEnB;MACE;;;YAIM,oBAAoB,KAAK;;;;;;wBAUrB,yBAAyB,SAAS,oBAAoB;;;;;;qBAqBzD;;;;;;;;;;;;;wBAcG,YAAY,oBAAoB"}
1
+ {"version":3,"file":"bindings.d.mts","names":[],"sources":["../../src/cloudflare/bindings.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;iBAsBiB;EACf,KAAK;IACH,MAAM;IACN,IAAI;IACJ,UAAU;IACV;IACA;;IAEA,UAAU,SAAS;;IAEnB;MACE;;;YAIM,oBAAoB,KAAK;;;;;;wBAUrB,yBAAyB,SAAS,oBAAoB;;;;;;qBAqBzD;;;;;;;;;;;;;wBAcG,YAAY,oBAAoB"}
@@ -1 +1 @@
1
- {"version":3,"file":"bindings.mjs","names":[],"sources":["../../src/cloudflare/bindings.ts"],"sourcesContent":["import type { EmailAddress } from '../email';\nimport { InstanceNotFoundError, type WorkflowBindingLike } from '../host/platform';\n\n/**\n * A minimal structural interface over the Cloudflare bindings.\n *\n * @cloudflare/workers-types is deliberately not used: the global types it\n * injects clash with the DOM lib this package's react side needs. A structural\n * subset plus the ambient cloudflare:workers declaration is enough to compile,\n * and the real bindings satisfy it structurally at runtime.\n */\n\n/** Structural subset of D1Result: `meta.changes` is how INSERT OR IGNORE reports whether it inserted. */\nexport interface D1RunResultLike {\n meta?: { changes?: number };\n}\n\nexport interface D1PreparedStatementLike {\n bind(...values: unknown[]): D1PreparedStatementLike;\n first<T = Record<string, unknown>>(): Promise<T | null>;\n all<T = Record<string, unknown>>(): Promise<{ results: T[] }>;\n run(): Promise<D1RunResultLike>;\n}\n\nexport interface D1DatabaseLike {\n prepare(sql: string): D1PreparedStatementLike;\n /** Statements run in order inside one transaction (real D1 semantics) — deploy relies on the atomicity. */\n batch(statements: D1PreparedStatementLike[]): Promise<unknown>;\n}\n\n/**\n * Cloudflare Email Service's send_email binding (structural subset).\n *\n * `idempotencyKey` is part of the call because the send happens inside a\n * `step.do`: a step body re-runs after its fn resolved but before the\n * checkpoint committed, so a binding that drops the key mails the user twice.\n * The binding is app-supplied glue (nothing here can de-duplicate — there is\n * no state between two runs of the same step), which is why the port makes the\n * key impossible to miss rather than merely available.\n */\nexport interface EmailBindingLike {\n send(message: {\n from: EmailAddress;\n to: EmailAddress;\n replyTo?: EmailAddress;\n subject: string;\n html: string;\n /** List-Unsubscribe and List-Unsubscribe-Post, when the email has an unsubscribe link. */\n headers?: Readonly<Record<string, string>>;\n /** `${instanceId}:${nodeId}`: stable across replays and retries — drop a send whose key was already delivered. */\n idempotencyKey: string;\n }): Promise<unknown>;\n}\n\n/** Cloudflare's Workflow binding, as far as the host uses it. */\nexport type CfWorkflowBinding = Pick<WorkflowBindingLike, 'create' | 'get'>;\n\n/** How Cloudflare's `get` reports an unknown instance id. */\nconst CF_NOT_FOUND = 'instance.not_found';\n\n/**\n * The Workflow binding as the host's runtime port: `get` answers an unknown id with\n * `InstanceNotFoundError`, as the port promises, instead of Cloudflare's own error. Everything\n * else is forwarded as is.\n */\nexport function cloudflareJourneyBinding(binding: CfWorkflowBinding): WorkflowBindingLike {\n return {\n create: (options) => binding.create(options),\n async get(id) {\n try {\n return await binding.get(id);\n } catch (error) {\n if (String(error).includes(CF_NOT_FOUND)) {\n throw new InstanceNotFoundError(id, { cause: error });\n }\n throw error;\n }\n },\n };\n}\n\n/**\n * Slack on the deadline when deciding whether a thrown wait was a timeout: the\n * platform may surface the expiry a moment early, and a wait shorter than this\n * has no room to tell the two apart anyway.\n */\nexport const WAKE_TIMEOUT_TOLERANCE_MS = 5_000;\n\n/**\n * Did a wait that threw actually reach its deadline?\n *\n * CF signals a wait timeout by throwing, and so does every other failure in\n * there — a broken binding, an evicted instance, a malformed timeout. Reading\n * them all as timeouts would make an infrastructure fault indistinguishable\n * from \"the user never did it\" (the journey would take its onTimeout branch,\n * early and silently). Only the clock can tell them apart, and only against\n * the absolute deadline: measured from when the wait was parked, a replay\n * after an eviction would start the measure over and read a\n * genuine expiry as a failure.\n */\nexport function wakeExpired(deadlineMs: number, now: number): boolean {\n return now >= deadlineMs - WAKE_TIMEOUT_TOLERANCE_MS;\n}\n"],"mappings":";;;AA0DA,MAAM,eAAe;;;;;;AAOrB,SAAgB,yBAAyB,SAAiD;CACxF,OAAO;EACL,SAAS,YAAY,QAAQ,OAAO,OAAO;EAC3C,MAAM,IAAI,IAAI;GACZ,IAAI;IACF,OAAO,MAAM,QAAQ,IAAI,EAAE;GAC7B,SAAS,OAAO;IACd,IAAI,OAAO,KAAK,CAAC,CAAC,SAAS,YAAY,GACrC,MAAM,IAAI,sBAAsB,IAAI,EAAE,OAAO,MAAM,CAAC;IAEtD,MAAM;GACR;EACF;CACF;AACF;;;;;;AAOA,MAAa,4BAA4B;;;;;;;;;;;;;AAczC,SAAgB,YAAY,YAAoB,KAAsB;CACpE,OAAO,OAAO,aAAa;AAC7B"}
1
+ {"version":3,"file":"bindings.mjs","names":[],"sources":["../../src/cloudflare/bindings.ts"],"sourcesContent":["import type { EmailAddress } from '../email';\nimport { InstanceNotFoundError, type WorkflowBindingLike } from '../host/platform';\n\n/**\n * A minimal structural interface over the Cloudflare bindings.\n *\n * @cloudflare/workers-types is deliberately not used: the global types it\n * injects clash with the DOM lib this package's react side needs. A structural\n * subset plus the ambient cloudflare:workers declaration is enough to compile,\n * and the real bindings satisfy it structurally at runtime.\n */\n\n/**\n * Cloudflare Email Service's send_email binding (structural subset).\n *\n * `idempotencyKey` is part of the call because the send happens inside a\n * `step.do`: a step body re-runs after its fn resolved but before the\n * checkpoint committed, so a binding that drops the key mails the user twice.\n * The binding is app-supplied glue (nothing here can de-duplicate — there is\n * no state between two runs of the same step), which is why the port makes the\n * key impossible to miss rather than merely available.\n */\nexport interface EmailBindingLike {\n send(message: {\n from: EmailAddress;\n to: EmailAddress;\n replyTo?: EmailAddress;\n subject: string;\n html: string;\n /** List-Unsubscribe and List-Unsubscribe-Post, when the email has an unsubscribe link. */\n headers?: Readonly<Record<string, string>>;\n /** `${instanceId}:${nodeId}`: stable across replays and retries — drop a send whose key was already delivered. */\n idempotencyKey: string;\n }): Promise<unknown>;\n}\n\n/** Cloudflare's Workflow binding, as far as the host uses it. */\nexport type CfWorkflowBinding = Pick<WorkflowBindingLike, 'create' | 'get'>;\n\n/** How Cloudflare's `get` reports an unknown instance id. */\nconst CF_NOT_FOUND = 'instance.not_found';\n\n/**\n * The Workflow binding as the host's runtime port: `get` answers an unknown id with\n * `InstanceNotFoundError`, as the port promises, instead of Cloudflare's own error. Everything\n * else is forwarded as is.\n */\nexport function cloudflareJourneyBinding(binding: CfWorkflowBinding): WorkflowBindingLike {\n return {\n create: (options) => binding.create(options),\n async get(id) {\n try {\n return await binding.get(id);\n } catch (error) {\n if (String(error).includes(CF_NOT_FOUND)) {\n throw new InstanceNotFoundError(id, { cause: error });\n }\n throw error;\n }\n },\n };\n}\n\n/**\n * Slack on the deadline when deciding whether a thrown wait was a timeout: the\n * platform may surface the expiry a moment early, and a wait shorter than this\n * has no room to tell the two apart anyway.\n */\nexport const WAKE_TIMEOUT_TOLERANCE_MS = 5_000;\n\n/**\n * Did a wait that threw actually reach its deadline?\n *\n * CF signals a wait timeout by throwing, and so does every other failure in\n * there — a broken binding, an evicted instance, a malformed timeout. Reading\n * them all as timeouts would make an infrastructure fault indistinguishable\n * from \"the user never did it\" (the journey would take its onTimeout branch,\n * early and silently). Only the clock can tell them apart, and only against\n * the absolute deadline: measured from when the wait was parked, a replay\n * after an eviction would start the measure over and read a\n * genuine expiry as a failure.\n */\nexport function wakeExpired(deadlineMs: number, now: number): boolean {\n return now >= deadlineMs - WAKE_TIMEOUT_TOLERANCE_MS;\n}\n"],"mappings":";;;AAwCA,MAAM,eAAe;;;;;;AAOrB,SAAgB,yBAAyB,SAAiD;CACxF,OAAO;EACL,SAAS,YAAY,QAAQ,OAAO,OAAO;EAC3C,MAAM,IAAI,IAAI;GACZ,IAAI;IACF,OAAO,MAAM,QAAQ,IAAI,EAAE;GAC7B,SAAS,OAAO;IACd,IAAI,OAAO,KAAK,CAAC,CAAC,SAAS,YAAY,GACrC,MAAM,IAAI,sBAAsB,IAAI,EAAE,OAAO,MAAM,CAAC;IAEtD,MAAM;GACR;EACF;CACF;AACF;;;;;;AAOA,MAAa,4BAA4B;;;;;;;;;;;;;AAczC,SAAgB,YAAY,YAAoB,KAAsB;CACpE,OAAO,OAAO,aAAa;AAC7B"}
@@ -4,8 +4,6 @@ const require_store_erasure = require("../store/erasure.cjs");
4
4
  const require_store_postgres_reporting = require("../store/postgres-reporting.cjs");
5
5
  const require_store_postgres = require("../store/postgres.cjs");
6
6
  const require_store_facts = require("../store/facts.cjs");
7
- const require_store_d1 = require("../store/d1.cjs");
8
- const require_cloudflare_d1_env = require("./d1-env.cjs");
9
7
  const require_store_index = require("../store/index.cjs");
10
8
  const require_host_platform = require("../host/platform.cjs");
11
9
  const require_host_ingest = require("../host/ingest.cjs");
@@ -22,7 +20,6 @@ const require_cloudflare_runner = require("./runner.cjs");
22
20
  const require_cloudflare_worker = require("./worker.cjs");
23
21
  exports.BURST_WINDOW_MS = require_reporting_classify.BURST_WINDOW_MS;
24
22
  exports.BundleIntegrityError = require_host_ingest.BundleIntegrityError;
25
- exports.D1JourneyStore = require_store_d1.D1JourneyStore;
26
23
  exports.DEFAULT_QUICK_MS = require_reporting_classify.DEFAULT_QUICK_MS;
27
24
  exports.EmailSender = require_email_sender.EmailSender;
28
25
  exports.EnvValidationError = require_host_journey_host.EnvValidationError;
@@ -34,7 +31,6 @@ exports.PostgresJourneyStore = require_store_postgres.PostgresJourneyStore;
34
31
  exports.PostgresReporting = require_store_postgres_reporting.PostgresReporting;
35
32
  exports.SWEEP_PAGE_SIZE = require_host_ingest.SWEEP_PAGE_SIZE;
36
33
  exports.SchemaMismatchError = require_store_index.SchemaMismatchError;
37
- exports.UnversionedStoreError = require_host_ingest.UnversionedStoreError;
38
34
  exports.UserErasedError = require_store_erasure.UserErasedError;
39
35
  exports.WAKE_EVENT_TYPE = require_host_platform.WAKE_EVENT_TYPE;
40
36
  exports.WAKE_TIMEOUT_TOLERANCE_MS = require_cloudflare_bindings.WAKE_TIMEOUT_TOLERANCE_MS;
@@ -45,7 +41,6 @@ exports.channelsFromConfig = require_host_channels.channelsFromConfig;
45
41
  exports.classifyClick = require_reporting_classify.classifyClick;
46
42
  exports.classifyOpen = require_reporting_classify.classifyOpen;
47
43
  exports.cloudflareJourneyBinding = require_cloudflare_bindings.cloudflareJourneyBinding;
48
- exports.d1Store = require_cloudflare_d1_env.d1Store;
49
44
  exports.deployBundle = require_host_ingest.deployBundle;
50
45
  exports.forgetUser = require_host_ingest.forgetUser;
51
46
  exports.handleRequest = require_host_http.handleRequest;
@@ -1,9 +1,6 @@
1
1
  import { JourneyObserver, LinkPurpose, MessageTracking, OutboundMessage, PushDevice, SendResult } from "../engine/ports.cjs";
2
2
  import { EmailAddress } from "../email.cjs";
3
3
  import { UserErasedError } from "../store/erasure.cjs";
4
- import { InstanceNotFoundError, JourneyEnv, JourneyParams, WAKE_EVENT_TYPE, WorkflowBindingLike, WorkflowInstanceLike } from "../host/platform.cjs";
5
- import { D1DatabaseLike, EmailBindingLike, WAKE_TIMEOUT_TOLERANCE_MS, cloudflareJourneyBinding, wakeExpired } from "./bindings.cjs";
6
- import { D1JourneyStore } from "../store/d1.cjs";
7
4
  import { JourneyFactSource } from "../store/facts.cjs";
8
5
  import { SqlClientLike } from "../store/sql.cjs";
9
6
  import { PostgresJourneyStore, PostgresJourneyStoreOptions } from "../store/postgres.cjs";
@@ -11,9 +8,10 @@ import { PostgresReporting } from "../store/postgres-reporting.cjs";
11
8
  import { BundleVersion, Engagement, EntryInput, EntryOutcome, EventInput, JourneyReporting, JourneyStore, MessageRecord, ProfileProps, SchemaMismatchError, SegmentTriggerRoute, StoreLease, TriggerRoute } from "../store/index.cjs";
12
9
  import { EmailMessage, EmailRenderer, EmailSender, EmailSenderOptions, EmailTransport, RenderedEmail, trackEmail } from "../email-sender.cjs";
13
10
  import { Authorize, authorized, bearerMatches, timingSafeEqual } from "../host/auth.cjs";
14
- import { BundleIntegrityError, DeployResult, DeviceRegistration, IdentifyResult, IngestInput, IngestResult, SWEEP_PAGE_SIZE, SweepPage, SweepResult, UnversionedStoreError, deployBundle, forgetUser, identifyUser, ingestEvent, registerDevice, removeDevice, sweepMaintenance, sweepSegments, sweepSegmentsPaged, unbindDevice } from "../host/ingest.cjs";
11
+ import { EmailBindingLike, WAKE_TIMEOUT_TOLERANCE_MS, cloudflareJourneyBinding, wakeExpired } from "./bindings.cjs";
12
+ import { InstanceNotFoundError, JourneyEnv, JourneyParams, WAKE_EVENT_TYPE, WorkflowBindingLike, WorkflowInstanceLike } from "../host/platform.cjs";
13
+ import { BundleIntegrityError, DeployResult, DeviceRegistration, IdentifyResult, IngestInput, IngestResult, SWEEP_PAGE_SIZE, SweepPage, SweepResult, deployBundle, forgetUser, identifyUser, ingestEvent, registerDevice, removeDevice, sweepMaintenance, sweepSegments, sweepSegmentsPaged, unbindDevice } from "../host/ingest.cjs";
15
14
  import { EnvValidationError } from "../host/journey-host.cjs";
16
- import { d1Store } from "./d1-env.cjs";
17
15
  import { TrackingOptions, handleTracking, recordPushOpen } from "../reporting/tracking.cjs";
18
16
  import { IngestValidation, handleRequest } from "../host/http.cjs";
19
17
  import { JourneyRunner } from "./runner.cjs";
@@ -22,4 +20,4 @@ import { ChannelsConfig, channelsFromConfig, journeyMode, runtimeFromConfig } fr
22
20
  import { reportingObserver } from "../reporting/observer.cjs";
23
21
  import { BURST_WINDOW_MS, ClassifyOptions, ClickFacts, DEFAULT_QUICK_MS, RequestFacts, Verdict, classifyClick, classifyOpen } from "../reporting/classify.cjs";
24
22
  import { ExecutionContextLike, JourneyWorker, JourneyWorkerOptions, journeyWorker } from "./worker.cjs";
25
- export { type Authorize, BURST_WINDOW_MS, BundleIntegrityError, type BundleVersion, type ChannelsConfig, type ClassifyOptions, type ClickFacts, type D1DatabaseLike, D1JourneyStore, DEFAULT_QUICK_MS, type DeployResult, type DeviceRegistration, type EmailAddress, type EmailBindingLike, type EmailMessage, type EmailRenderer, EmailSender, type EmailSenderOptions, type EmailTransport, type Engagement, type EntryInput, type EntryOutcome, EnvValidationError, type EventInput, type ExecutionContextLike, type IdentifyResult, type IngestInput, type IngestResult, type IngestValidation, InstanceNotFoundError, type JourneyEnv, JourneyFactSource, type JourneyObserver, type JourneyParams, type JourneyReporting, JourneyRunner, type JourneyStore, type JourneyWorker, type JourneyWorkerOptions, type LinkPurpose, LogMessageSender, type MessageRecord, type MessageTracking, type OutboundMessage, PostgresJourneyStore, type PostgresJourneyStoreOptions, PostgresReporting, type ProfileProps, type PushDevice, type RenderedEmail, type RequestFacts, SWEEP_PAGE_SIZE, SchemaMismatchError, type SegmentTriggerRoute, type SendResult, type SqlClientLike, type StoreLease, type SweepPage, type SweepResult, type TrackingOptions, type TriggerRoute, UnversionedStoreError, UserErasedError, type Verdict, WAKE_EVENT_TYPE, WAKE_TIMEOUT_TOLERANCE_MS, WebhookMessageSender, type WorkflowBindingLike, type WorkflowInstanceLike, authorized, bearerMatches, channelsFromConfig, classifyClick, classifyOpen, cloudflareJourneyBinding, d1Store, deployBundle, forgetUser, handleRequest, handleTracking, identifyUser, ingestEvent, journeyMode, journeyWorker, recordPushOpen, registerDevice, removeDevice, reportingObserver, routeByChannel, routeByPlatform, runtimeFromConfig, sweepMaintenance, sweepSegments, sweepSegmentsPaged, timingSafeEqual, trackEmail, unbindDevice, wakeExpired };
23
+ export { type Authorize, BURST_WINDOW_MS, BundleIntegrityError, type BundleVersion, type ChannelsConfig, type ClassifyOptions, type ClickFacts, DEFAULT_QUICK_MS, type DeployResult, type DeviceRegistration, type EmailAddress, type EmailBindingLike, type EmailMessage, type EmailRenderer, EmailSender, type EmailSenderOptions, type EmailTransport, type Engagement, type EntryInput, type EntryOutcome, EnvValidationError, type EventInput, type ExecutionContextLike, type IdentifyResult, type IngestInput, type IngestResult, type IngestValidation, InstanceNotFoundError, type JourneyEnv, JourneyFactSource, type JourneyObserver, type JourneyParams, type JourneyReporting, JourneyRunner, type JourneyStore, type JourneyWorker, type JourneyWorkerOptions, type LinkPurpose, LogMessageSender, type MessageRecord, type MessageTracking, type OutboundMessage, PostgresJourneyStore, type PostgresJourneyStoreOptions, PostgresReporting, type ProfileProps, type PushDevice, type RenderedEmail, type RequestFacts, SWEEP_PAGE_SIZE, SchemaMismatchError, type SegmentTriggerRoute, type SendResult, type SqlClientLike, type StoreLease, type SweepPage, type SweepResult, type TrackingOptions, type TriggerRoute, UserErasedError, type Verdict, WAKE_EVENT_TYPE, WAKE_TIMEOUT_TOLERANCE_MS, WebhookMessageSender, type WorkflowBindingLike, type WorkflowInstanceLike, authorized, bearerMatches, channelsFromConfig, classifyClick, classifyOpen, cloudflareJourneyBinding, deployBundle, forgetUser, handleRequest, handleTracking, identifyUser, ingestEvent, journeyMode, journeyWorker, recordPushOpen, registerDevice, removeDevice, reportingObserver, routeByChannel, routeByPlatform, runtimeFromConfig, sweepMaintenance, sweepSegments, sweepSegmentsPaged, timingSafeEqual, trackEmail, unbindDevice, wakeExpired };
@@ -1,9 +1,6 @@
1
1
  import { JourneyObserver, LinkPurpose, MessageTracking, OutboundMessage, PushDevice, SendResult } from "../engine/ports.mjs";
2
2
  import { EmailAddress } from "../email.mjs";
3
3
  import { UserErasedError } from "../store/erasure.mjs";
4
- import { InstanceNotFoundError, JourneyEnv, JourneyParams, WAKE_EVENT_TYPE, WorkflowBindingLike, WorkflowInstanceLike } from "../host/platform.mjs";
5
- import { D1DatabaseLike, EmailBindingLike, WAKE_TIMEOUT_TOLERANCE_MS, cloudflareJourneyBinding, wakeExpired } from "./bindings.mjs";
6
- import { D1JourneyStore } from "../store/d1.mjs";
7
4
  import { JourneyFactSource } from "../store/facts.mjs";
8
5
  import { SqlClientLike } from "../store/sql.mjs";
9
6
  import { PostgresJourneyStore, PostgresJourneyStoreOptions } from "../store/postgres.mjs";
@@ -11,9 +8,10 @@ import { PostgresReporting } from "../store/postgres-reporting.mjs";
11
8
  import { BundleVersion, Engagement, EntryInput, EntryOutcome, EventInput, JourneyReporting, JourneyStore, MessageRecord, ProfileProps, SchemaMismatchError, SegmentTriggerRoute, StoreLease, TriggerRoute } from "../store/index.mjs";
12
9
  import { EmailMessage, EmailRenderer, EmailSender, EmailSenderOptions, EmailTransport, RenderedEmail, trackEmail } from "../email-sender.mjs";
13
10
  import { Authorize, authorized, bearerMatches, timingSafeEqual } from "../host/auth.mjs";
14
- import { BundleIntegrityError, DeployResult, DeviceRegistration, IdentifyResult, IngestInput, IngestResult, SWEEP_PAGE_SIZE, SweepPage, SweepResult, UnversionedStoreError, deployBundle, forgetUser, identifyUser, ingestEvent, registerDevice, removeDevice, sweepMaintenance, sweepSegments, sweepSegmentsPaged, unbindDevice } from "../host/ingest.mjs";
11
+ import { EmailBindingLike, WAKE_TIMEOUT_TOLERANCE_MS, cloudflareJourneyBinding, wakeExpired } from "./bindings.mjs";
12
+ import { InstanceNotFoundError, JourneyEnv, JourneyParams, WAKE_EVENT_TYPE, WorkflowBindingLike, WorkflowInstanceLike } from "../host/platform.mjs";
13
+ import { BundleIntegrityError, DeployResult, DeviceRegistration, IdentifyResult, IngestInput, IngestResult, SWEEP_PAGE_SIZE, SweepPage, SweepResult, deployBundle, forgetUser, identifyUser, ingestEvent, registerDevice, removeDevice, sweepMaintenance, sweepSegments, sweepSegmentsPaged, unbindDevice } from "../host/ingest.mjs";
15
14
  import { EnvValidationError } from "../host/journey-host.mjs";
16
- import { d1Store } from "./d1-env.mjs";
17
15
  import { TrackingOptions, handleTracking, recordPushOpen } from "../reporting/tracking.mjs";
18
16
  import { IngestValidation, handleRequest } from "../host/http.mjs";
19
17
  import { JourneyRunner } from "./runner.mjs";
@@ -22,4 +20,4 @@ import { ChannelsConfig, channelsFromConfig, journeyMode, runtimeFromConfig } fr
22
20
  import { reportingObserver } from "../reporting/observer.mjs";
23
21
  import { BURST_WINDOW_MS, ClassifyOptions, ClickFacts, DEFAULT_QUICK_MS, RequestFacts, Verdict, classifyClick, classifyOpen } from "../reporting/classify.mjs";
24
22
  import { ExecutionContextLike, JourneyWorker, JourneyWorkerOptions, journeyWorker } from "./worker.mjs";
25
- export { type Authorize, BURST_WINDOW_MS, BundleIntegrityError, type BundleVersion, type ChannelsConfig, type ClassifyOptions, type ClickFacts, type D1DatabaseLike, D1JourneyStore, DEFAULT_QUICK_MS, type DeployResult, type DeviceRegistration, type EmailAddress, type EmailBindingLike, type EmailMessage, type EmailRenderer, EmailSender, type EmailSenderOptions, type EmailTransport, type Engagement, type EntryInput, type EntryOutcome, EnvValidationError, type EventInput, type ExecutionContextLike, type IdentifyResult, type IngestInput, type IngestResult, type IngestValidation, InstanceNotFoundError, type JourneyEnv, JourneyFactSource, type JourneyObserver, type JourneyParams, type JourneyReporting, JourneyRunner, type JourneyStore, type JourneyWorker, type JourneyWorkerOptions, type LinkPurpose, LogMessageSender, type MessageRecord, type MessageTracking, type OutboundMessage, PostgresJourneyStore, type PostgresJourneyStoreOptions, PostgresReporting, type ProfileProps, type PushDevice, type RenderedEmail, type RequestFacts, SWEEP_PAGE_SIZE, SchemaMismatchError, type SegmentTriggerRoute, type SendResult, type SqlClientLike, type StoreLease, type SweepPage, type SweepResult, type TrackingOptions, type TriggerRoute, UnversionedStoreError, UserErasedError, type Verdict, WAKE_EVENT_TYPE, WAKE_TIMEOUT_TOLERANCE_MS, WebhookMessageSender, type WorkflowBindingLike, type WorkflowInstanceLike, authorized, bearerMatches, channelsFromConfig, classifyClick, classifyOpen, cloudflareJourneyBinding, d1Store, deployBundle, forgetUser, handleRequest, handleTracking, identifyUser, ingestEvent, journeyMode, journeyWorker, recordPushOpen, registerDevice, removeDevice, reportingObserver, routeByChannel, routeByPlatform, runtimeFromConfig, sweepMaintenance, sweepSegments, sweepSegmentsPaged, timingSafeEqual, trackEmail, unbindDevice, wakeExpired };
23
+ export { type Authorize, BURST_WINDOW_MS, BundleIntegrityError, type BundleVersion, type ChannelsConfig, type ClassifyOptions, type ClickFacts, DEFAULT_QUICK_MS, type DeployResult, type DeviceRegistration, type EmailAddress, type EmailBindingLike, type EmailMessage, type EmailRenderer, EmailSender, type EmailSenderOptions, type EmailTransport, type Engagement, type EntryInput, type EntryOutcome, EnvValidationError, type EventInput, type ExecutionContextLike, type IdentifyResult, type IngestInput, type IngestResult, type IngestValidation, InstanceNotFoundError, type JourneyEnv, JourneyFactSource, type JourneyObserver, type JourneyParams, type JourneyReporting, JourneyRunner, type JourneyStore, type JourneyWorker, type JourneyWorkerOptions, type LinkPurpose, LogMessageSender, type MessageRecord, type MessageTracking, type OutboundMessage, PostgresJourneyStore, type PostgresJourneyStoreOptions, PostgresReporting, type ProfileProps, type PushDevice, type RenderedEmail, type RequestFacts, SWEEP_PAGE_SIZE, SchemaMismatchError, type SegmentTriggerRoute, type SendResult, type SqlClientLike, type StoreLease, type SweepPage, type SweepResult, type TrackingOptions, type TriggerRoute, UserErasedError, type Verdict, WAKE_EVENT_TYPE, WAKE_TIMEOUT_TOLERANCE_MS, WebhookMessageSender, type WorkflowBindingLike, type WorkflowInstanceLike, authorized, bearerMatches, channelsFromConfig, classifyClick, classifyOpen, cloudflareJourneyBinding, deployBundle, forgetUser, handleRequest, handleTracking, identifyUser, ingestEvent, journeyMode, journeyWorker, recordPushOpen, registerDevice, removeDevice, reportingObserver, routeByChannel, routeByPlatform, runtimeFromConfig, sweepMaintenance, sweepSegments, sweepSegmentsPaged, timingSafeEqual, trackEmail, unbindDevice, wakeExpired };
@@ -3,11 +3,9 @@ import { UserErasedError } from "../store/erasure.mjs";
3
3
  import { PostgresReporting } from "../store/postgres-reporting.mjs";
4
4
  import { PostgresJourneyStore } from "../store/postgres.mjs";
5
5
  import { JourneyFactSource } from "../store/facts.mjs";
6
- import { D1JourneyStore } from "../store/d1.mjs";
7
- import { d1Store } from "./d1-env.mjs";
8
6
  import { SchemaMismatchError } from "../store/index.mjs";
9
7
  import { InstanceNotFoundError, WAKE_EVENT_TYPE } from "../host/platform.mjs";
10
- import { BundleIntegrityError, SWEEP_PAGE_SIZE, UnversionedStoreError, deployBundle, forgetUser, identifyUser, ingestEvent, registerDevice, removeDevice, sweepMaintenance, sweepSegments, sweepSegmentsPaged, unbindDevice } from "../host/ingest.mjs";
8
+ import { BundleIntegrityError, SWEEP_PAGE_SIZE, deployBundle, forgetUser, identifyUser, ingestEvent, registerDevice, removeDevice, sweepMaintenance, sweepSegments, sweepSegmentsPaged, unbindDevice } from "../host/ingest.mjs";
11
9
  import { reportingObserver } from "../reporting/observer.mjs";
12
10
  import { LogMessageSender, WebhookMessageSender, routeByChannel, routeByPlatform } from "../host/senders.mjs";
13
11
  import { channelsFromConfig, journeyMode, runtimeFromConfig } from "../host/channels.mjs";
@@ -19,4 +17,4 @@ import { EnvValidationError } from "../host/journey-host.mjs";
19
17
  import { WAKE_TIMEOUT_TOLERANCE_MS, cloudflareJourneyBinding, wakeExpired } from "./bindings.mjs";
20
18
  import { JourneyRunner } from "./runner.mjs";
21
19
  import { journeyWorker } from "./worker.mjs";
22
- export { BURST_WINDOW_MS, BundleIntegrityError, D1JourneyStore, DEFAULT_QUICK_MS, EmailSender, EnvValidationError, InstanceNotFoundError, JourneyFactSource, JourneyRunner, LogMessageSender, PostgresJourneyStore, PostgresReporting, SWEEP_PAGE_SIZE, SchemaMismatchError, UnversionedStoreError, UserErasedError, WAKE_EVENT_TYPE, WAKE_TIMEOUT_TOLERANCE_MS, WebhookMessageSender, authorized, bearerMatches, channelsFromConfig, classifyClick, classifyOpen, cloudflareJourneyBinding, d1Store, deployBundle, forgetUser, handleRequest, handleTracking, identifyUser, ingestEvent, journeyMode, journeyWorker, recordPushOpen, registerDevice, removeDevice, reportingObserver, routeByChannel, routeByPlatform, runtimeFromConfig, sweepMaintenance, sweepSegments, sweepSegmentsPaged, timingSafeEqual, trackEmail, unbindDevice, wakeExpired };
20
+ export { BURST_WINDOW_MS, BundleIntegrityError, DEFAULT_QUICK_MS, EmailSender, EnvValidationError, InstanceNotFoundError, JourneyFactSource, JourneyRunner, LogMessageSender, PostgresJourneyStore, PostgresReporting, SWEEP_PAGE_SIZE, SchemaMismatchError, UserErasedError, WAKE_EVENT_TYPE, WAKE_TIMEOUT_TOLERANCE_MS, WebhookMessageSender, authorized, bearerMatches, channelsFromConfig, classifyClick, classifyOpen, cloudflareJourneyBinding, deployBundle, forgetUser, handleRequest, handleTracking, identifyUser, ingestEvent, journeyMode, journeyWorker, recordPushOpen, registerDevice, removeDevice, reportingObserver, routeByChannel, routeByPlatform, runtimeFromConfig, sweepMaintenance, sweepSegments, sweepSegmentsPaged, timingSafeEqual, trackEmail, unbindDevice, wakeExpired };
@@ -1,7 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_engine_retry = require("../engine/retry.cjs");
3
3
  const require_runtime = require("../runtime.cjs");
4
- const require_cloudflare_d1_env = require("./d1-env.cjs");
5
4
  const require_host_platform = require("../host/platform.cjs");
6
5
  const require_host_execute = require("../host/execute.cjs");
7
6
  const require_engine_actions = require("../engine/actions.cjs");
@@ -79,13 +78,13 @@ var CfEngineStep = class {
79
78
  */
80
79
  var JourneyRunner = class extends cloudflare_workers.WorkflowEntrypoint {
81
80
  /**
82
- * The data plane. D1 from the env by default; an app on Postgres
83
- * overrides this (and `closeStore` if its client needs closing):
81
+ * The data plane: `journeyWorker` opens it through the host's store lease. A runner subclass
82
+ * of its own overrides this (and `closeStore` if its client needs closing):
84
83
  *
85
84
  * protected override createStore() { return new PostgresJourneyStore(client(this.env)); }
86
85
  */
87
86
  createStore() {
88
- return require_cloudflare_d1_env.d1Store(this.env);
87
+ throw new Error("JourneyRunner: override createStore, or build the runner with journeyWorker");
89
88
  }
90
89
  /** Called once `run` is over, success or not — release whatever `createStore` opened. */
91
90
  async closeStore(_store) {}
@@ -1 +1 @@
1
- {"version":3,"file":"runner.cjs","names":["isRetryable","NonRetryableError","WAKE_EVENT_TYPE","wakeExpired","WorkflowEntrypoint","d1Store","cloudflareJourneyBinding","resolveRuntime","WebhookMessageSender","LogMessageSender","RegistryActionInvoker","executeJourney"],"sources":["../../src/cloudflare/runner.ts"],"sourcesContent":["import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from 'cloudflare:workers';\nimport { NonRetryableError } from 'cloudflare:workflows';\nimport type { Contract } from '../contract';\nimport { type RegisteredAction, RegistryActionInvoker } from '../engine/actions';\nimport type {\n ActionInvoker,\n EngineStep,\n JourneyObserver,\n JourneyOutcome,\n MessageSender,\n MessageValidator,\n} from '../engine/ports';\nimport { isRetryable } from '../engine/retry';\nimport { type JourneyRunParts, executeJourney } from '../host/execute';\nimport { type JourneyEnv, type JourneyParams, WAKE_EVENT_TYPE } from '../host/platform';\nimport { LogMessageSender, WebhookMessageSender } from '../host/senders';\nimport { type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport type { JourneyStore } from '../store/index';\nimport { cloudflareJourneyBinding, wakeExpired } from './bindings';\nimport { d1Store } from './d1-env';\n\n/**\n * Adapts CF's WorkflowStep to EngineStep.\n * subscribe/unsubscribe write the subscription table inside durable steps (the\n * router wakes instances from that table); waitForWake parks on CF's\n * waitForEvent, which signals a timeout by throwing — and so does every other\n * failure in there, so only the clock, against the checkpointed deadline, can\n * tell them apart (see wakeExpired).\n */\nclass CfEngineStep implements EngineStep {\n constructor(\n private readonly step: WorkflowStep,\n private readonly store: JourneyStore,\n private readonly userId: string,\n private readonly instanceId: string\n ) {}\n\n /**\n * A failure a retry would only repeat (`isRetryable`: a template refusing its props, the\n * contract refusing an emitted event, a provider rejecting the request) ends the step at\n * once instead of burning the retry budget. The engine recognizes a NonRetryableError by its\n * `name`, so the original's name is not written over it — the original travels as `cause`.\n */\n do<T>(name: string, fn: () => Promise<T>): Promise<T> {\n return this.step.do(name, async () => {\n try {\n return await fn();\n } catch (error) {\n if (error instanceof Error && !isRetryable(error)) {\n const permanent = new NonRetryableError(error.message);\n permanent.cause = error;\n throw permanent;\n }\n throw error;\n }\n });\n }\n\n sleep(name: string, ms: number): Promise<void> {\n return this.step.sleep(name, ms);\n }\n\n subscribe(name: string, events: readonly string[]): Promise<void> {\n // Idempotent in the store: the interpreter re-subscribes on every wait\n // attempt (one-shot-callback contract) and registrations persist here.\n return this.step.do(name, () =>\n this.store.subscribe(this.instanceId, this.userId, events, Date.now())\n );\n }\n\n unsubscribe(name: string): Promise<void> {\n return this.step.do(name, () => this.store.unsubscribe(this.instanceId));\n }\n\n async waitForWake(\n name: string,\n { timeoutMs, deadlineMs }: { timeoutMs: number; deadlineMs: number }\n ): Promise<'event' | 'timeout'> {\n try {\n await this.step.waitForEvent(name, { type: WAKE_EVENT_TYPE, timeout: timeoutMs });\n return 'event';\n } catch (error) {\n /*\n * Only the deadline can tell a real timeout from a failure (see wakeExpired). The\n * deadline is the interpreter's checkpoint, so the verdict is the same on the attempt\n * that parked and on a replay after an eviction — which is where the wait usually\n * resurfaces: the instance is rebuilt, the wait is already over, and the first thing to\n * run is this step. The verdict is checkpointed too, so later replays reuse it.\n */\n const timedOut = await this.step.do(`${name}:timed-out`, async () =>\n wakeExpired(deadlineMs, Date.now())\n );\n if (timedOut) return 'timeout';\n throw error;\n }\n }\n}\n\n/**\n * The generic journey executor: one class runs every workflow, because IR is\n * data. It is loaded from the store by the contentHash pinned at entry —\n * content addressing makes it immutable, so reading it outside a step is\n * replay-safe.\n */\nexport class JourneyRunner extends WorkflowEntrypoint<JourneyEnv, JourneyParams> {\n /**\n * The data plane. D1 from the env by default; an app on Postgres\n * overrides this (and `closeStore` if its client needs closing):\n *\n * protected override createStore() { return new PostgresJourneyStore(client(this.env)); }\n */\n protected createStore(): JourneyStore {\n return d1Store(this.env);\n }\n\n /** Called once `run` is over, success or not — release whatever `createStore` opened. */\n protected async closeStore(_store: JourneyStore): Promise<void> {}\n\n private adapted: JourneyEnv | undefined;\n\n /**\n * The env a run ingests its emitted events with: the Workflow binding behind the host's port,\n * so an instance the platform no longer knows answers `InstanceNotFoundError` (what lets the\n * host drop its wakes). Built once per instance.\n */\n protected hostEnv(): JourneyEnv {\n return (this.adapted ??= { ...this.env, JOURNEY: cloudflareJourneyBinding(this.env.JOURNEY) });\n }\n\n /**\n * Runtime options for this instance — typed configuration, resolved by the app (typically\n * `resolveRuntime(config.runtime, mode)` over its workflow.config.ts, with the mode coming from\n * an ENVIRONMENT var). The instance params are passed so the decision can also be per user:\n * a tester account in production can run in fast-forward while everyone else runs in real\n * time. Defaults: real time, quiet.\n *\n * protected override runtime({ userId }) {\n * return resolveRuntime(workflowConfig.runtime, isTester(userId) ? 'test' : this.env.ENVIRONMENT);\n * }\n */\n protected runtime(_params: JourneyParams): JourneyRuntimeOptions {\n return resolveRuntime(undefined, 'production');\n }\n\n /**\n * Message outlet; an app overrides this to plug in real channels (say\n * an EmailSender over a transport with a react-email renderer). By default it picks the webhook\n * or logging sender based on the environment.\n */\n protected createMessageSender(): MessageSender {\n return this.env.MESSAGE_WEBHOOK_URL\n ? new WebhookMessageSender(this.env.MESSAGE_WEBHOOK_URL)\n : new LogMessageSender();\n }\n\n /**\n * Custom-action registry; an app overrides this returning the same\n * `action(...)` objects its workflows reference (an ActionRef is a\n * RegisteredAction structurally) — the code plane of the dual-plane model:\n *\n * protected override actions() { return [syncCrm, issueCoupon]; }\n */\n protected actions(): readonly RegisteredAction[] {\n return [];\n }\n\n /**\n * The contract events this journey emits (`.sendEvent()`) are checked\n * against, exactly like ingest; journeyWorker passes its config's.\n */\n protected contract(): Contract | undefined {\n return undefined;\n }\n\n /**\n * Template props check before every send; an app overrides this with\n * `registryValidator({ emails, pushes })` (journeyWorker does it from its\n * registries). By default props go out as resolved.\n */\n protected createMessageValidator(): MessageValidator | undefined {\n return undefined;\n }\n\n /**\n * Reporting hooks: node arrivals, every copy sent, how each instance ended. The host stores\n * what it needs for its dashboards; by default nothing is reported.\n */\n protected createObserver(_store: JourneyStore): JourneyObserver | undefined {\n return undefined;\n }\n\n /** Override for a different version policy ('warn' by default) or a fully custom invoker (e.g. webhook degradation for hosted tenants). */\n protected createActionInvoker(): ActionInvoker {\n return new RegistryActionInvoker(this.actions());\n }\n\n /**\n * Everything one run is wired to. By default it is composed from the methods above; a host\n * that assembles the whole set itself (journeyWorker) overrides this one instead.\n */\n protected createRunParts(store: JourneyStore, params: JourneyParams): JourneyRunParts {\n return {\n store,\n env: this.hostEnv(),\n runtime: this.runtime(params),\n messages: this.createMessageSender(),\n actions: this.createActionInvoker(),\n contract: this.contract(),\n validator: this.createMessageValidator(),\n observer: this.createObserver(store),\n };\n }\n\n async run(event: WorkflowEvent<JourneyParams>, step: WorkflowStep): Promise<JourneyOutcome> {\n const store = this.createStore();\n try {\n const { userId } = event.payload;\n return await executeJourney(\n {\n instanceId: event.instanceId,\n params: event.payload,\n // Trigger-event creation time: replay-stable, unlike Date.now() here\n enteredAtMs: event.timestamp.getTime(),\n step: new CfEngineStep(step, store, userId, event.instanceId),\n },\n this.createRunParts(store, event.payload)\n );\n } finally {\n await this.closeStore(store);\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"runner.cjs","names":["isRetryable","NonRetryableError","WAKE_EVENT_TYPE","wakeExpired","WorkflowEntrypoint","cloudflareJourneyBinding","resolveRuntime","WebhookMessageSender","LogMessageSender","RegistryActionInvoker","executeJourney"],"sources":["../../src/cloudflare/runner.ts"],"sourcesContent":["import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from 'cloudflare:workers';\nimport { NonRetryableError } from 'cloudflare:workflows';\nimport type { Contract } from '../contract';\nimport { type RegisteredAction, RegistryActionInvoker } from '../engine/actions';\nimport type {\n ActionInvoker,\n EngineStep,\n JourneyObserver,\n JourneyOutcome,\n MessageSender,\n MessageValidator,\n} from '../engine/ports';\nimport { isRetryable } from '../engine/retry';\nimport { type JourneyRunParts, executeJourney } from '../host/execute';\nimport { type JourneyEnv, type JourneyParams, WAKE_EVENT_TYPE } from '../host/platform';\nimport { LogMessageSender, WebhookMessageSender } from '../host/senders';\nimport { type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport type { JourneyStore } from '../store/index';\nimport { cloudflareJourneyBinding, wakeExpired } from './bindings';\n\n/**\n * Adapts CF's WorkflowStep to EngineStep.\n * subscribe/unsubscribe write the subscription table inside durable steps (the\n * router wakes instances from that table); waitForWake parks on CF's\n * waitForEvent, which signals a timeout by throwing — and so does every other\n * failure in there, so only the clock, against the checkpointed deadline, can\n * tell them apart (see wakeExpired).\n */\nclass CfEngineStep implements EngineStep {\n constructor(\n private readonly step: WorkflowStep,\n private readonly store: JourneyStore,\n private readonly userId: string,\n private readonly instanceId: string\n ) {}\n\n /**\n * A failure a retry would only repeat (`isRetryable`: a template refusing its props, the\n * contract refusing an emitted event, a provider rejecting the request) ends the step at\n * once instead of burning the retry budget. The engine recognizes a NonRetryableError by its\n * `name`, so the original's name is not written over it — the original travels as `cause`.\n */\n do<T>(name: string, fn: () => Promise<T>): Promise<T> {\n return this.step.do(name, async () => {\n try {\n return await fn();\n } catch (error) {\n if (error instanceof Error && !isRetryable(error)) {\n const permanent = new NonRetryableError(error.message);\n permanent.cause = error;\n throw permanent;\n }\n throw error;\n }\n });\n }\n\n sleep(name: string, ms: number): Promise<void> {\n return this.step.sleep(name, ms);\n }\n\n subscribe(name: string, events: readonly string[]): Promise<void> {\n // Idempotent in the store: the interpreter re-subscribes on every wait\n // attempt (one-shot-callback contract) and registrations persist here.\n return this.step.do(name, () =>\n this.store.subscribe(this.instanceId, this.userId, events, Date.now())\n );\n }\n\n unsubscribe(name: string): Promise<void> {\n return this.step.do(name, () => this.store.unsubscribe(this.instanceId));\n }\n\n async waitForWake(\n name: string,\n { timeoutMs, deadlineMs }: { timeoutMs: number; deadlineMs: number }\n ): Promise<'event' | 'timeout'> {\n try {\n await this.step.waitForEvent(name, { type: WAKE_EVENT_TYPE, timeout: timeoutMs });\n return 'event';\n } catch (error) {\n /*\n * Only the deadline can tell a real timeout from a failure (see wakeExpired). The\n * deadline is the interpreter's checkpoint, so the verdict is the same on the attempt\n * that parked and on a replay after an eviction — which is where the wait usually\n * resurfaces: the instance is rebuilt, the wait is already over, and the first thing to\n * run is this step. The verdict is checkpointed too, so later replays reuse it.\n */\n const timedOut = await this.step.do(`${name}:timed-out`, async () =>\n wakeExpired(deadlineMs, Date.now())\n );\n if (timedOut) return 'timeout';\n throw error;\n }\n }\n}\n\n/**\n * The generic journey executor: one class runs every workflow, because IR is\n * data. It is loaded from the store by the contentHash pinned at entry —\n * content addressing makes it immutable, so reading it outside a step is\n * replay-safe.\n */\nexport class JourneyRunner extends WorkflowEntrypoint<JourneyEnv, JourneyParams> {\n /**\n * The data plane: `journeyWorker` opens it through the host's store lease. A runner subclass\n * of its own overrides this (and `closeStore` if its client needs closing):\n *\n * protected override createStore() { return new PostgresJourneyStore(client(this.env)); }\n */\n protected createStore(): JourneyStore {\n throw new Error('JourneyRunner: override createStore, or build the runner with journeyWorker');\n }\n\n /** Called once `run` is over, success or not — release whatever `createStore` opened. */\n protected async closeStore(_store: JourneyStore): Promise<void> {}\n\n private adapted: JourneyEnv | undefined;\n\n /**\n * The env a run ingests its emitted events with: the Workflow binding behind the host's port,\n * so an instance the platform no longer knows answers `InstanceNotFoundError` (what lets the\n * host drop its wakes). Built once per instance.\n */\n protected hostEnv(): JourneyEnv {\n return (this.adapted ??= { ...this.env, JOURNEY: cloudflareJourneyBinding(this.env.JOURNEY) });\n }\n\n /**\n * Runtime options for this instance — typed configuration, resolved by the app (typically\n * `resolveRuntime(config.runtime, mode)` over its workflow.config.ts, with the mode coming from\n * an ENVIRONMENT var). The instance params are passed so the decision can also be per user:\n * a tester account in production can run in fast-forward while everyone else runs in real\n * time. Defaults: real time, quiet.\n *\n * protected override runtime({ userId }) {\n * return resolveRuntime(workflowConfig.runtime, isTester(userId) ? 'test' : this.env.ENVIRONMENT);\n * }\n */\n protected runtime(_params: JourneyParams): JourneyRuntimeOptions {\n return resolveRuntime(undefined, 'production');\n }\n\n /**\n * Message outlet; an app overrides this to plug in real channels (say\n * an EmailSender over a transport with a react-email renderer). By default it picks the webhook\n * or logging sender based on the environment.\n */\n protected createMessageSender(): MessageSender {\n return this.env.MESSAGE_WEBHOOK_URL\n ? new WebhookMessageSender(this.env.MESSAGE_WEBHOOK_URL)\n : new LogMessageSender();\n }\n\n /**\n * Custom-action registry; an app overrides this returning the same\n * `action(...)` objects its workflows reference (an ActionRef is a\n * RegisteredAction structurally) — the code plane of the dual-plane model:\n *\n * protected override actions() { return [syncCrm, issueCoupon]; }\n */\n protected actions(): readonly RegisteredAction[] {\n return [];\n }\n\n /**\n * The contract events this journey emits (`.sendEvent()`) are checked\n * against, exactly like ingest; journeyWorker passes its config's.\n */\n protected contract(): Contract | undefined {\n return undefined;\n }\n\n /**\n * Template props check before every send; an app overrides this with\n * `registryValidator({ emails, pushes })` (journeyWorker does it from its\n * registries). By default props go out as resolved.\n */\n protected createMessageValidator(): MessageValidator | undefined {\n return undefined;\n }\n\n /**\n * Reporting hooks: node arrivals, every copy sent, how each instance ended. The host stores\n * what it needs for its dashboards; by default nothing is reported.\n */\n protected createObserver(_store: JourneyStore): JourneyObserver | undefined {\n return undefined;\n }\n\n /** Override for a different version policy ('warn' by default) or a fully custom invoker (e.g. webhook degradation for hosted tenants). */\n protected createActionInvoker(): ActionInvoker {\n return new RegistryActionInvoker(this.actions());\n }\n\n /**\n * Everything one run is wired to. By default it is composed from the methods above; a host\n * that assembles the whole set itself (journeyWorker) overrides this one instead.\n */\n protected createRunParts(store: JourneyStore, params: JourneyParams): JourneyRunParts {\n return {\n store,\n env: this.hostEnv(),\n runtime: this.runtime(params),\n messages: this.createMessageSender(),\n actions: this.createActionInvoker(),\n contract: this.contract(),\n validator: this.createMessageValidator(),\n observer: this.createObserver(store),\n };\n }\n\n async run(event: WorkflowEvent<JourneyParams>, step: WorkflowStep): Promise<JourneyOutcome> {\n const store = this.createStore();\n try {\n const { userId } = event.payload;\n return await executeJourney(\n {\n instanceId: event.instanceId,\n params: event.payload,\n // Trigger-event creation time: replay-stable, unlike Date.now() here\n enteredAtMs: event.timestamp.getTime(),\n step: new CfEngineStep(step, store, userId, event.instanceId),\n },\n this.createRunParts(store, event.payload)\n );\n } finally {\n await this.closeStore(store);\n }\n }\n}\n"],"mappings":"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"}
@@ -1,9 +1,9 @@
1
1
  import { ActionInvoker, JourneyObserver, JourneyOutcome, MessageSender, MessageValidator } from "../engine/ports.cjs";
2
2
  import { RegisteredAction } from "../engine/actions.cjs";
3
- import { JourneyEnv, JourneyParams } from "../host/platform.cjs";
4
3
  import { JourneyStore } from "../store/index.cjs";
5
4
  import { Contract } from "../contract.cjs";
6
5
  import { JourneyRuntimeOptions } from "../runtime.cjs";
6
+ import { JourneyEnv, JourneyParams } from "../host/platform.cjs";
7
7
  import { JourneyRunParts } from "../host/execute.cjs";
8
8
  import { WorkflowEntrypoint, WorkflowEvent, WorkflowStep } from "cloudflare:workers";
9
9
  //#region src/cloudflare/runner.d.ts
@@ -15,8 +15,8 @@ import { WorkflowEntrypoint, WorkflowEvent, WorkflowStep } from "cloudflare:work
15
15
  */
16
16
  export declare class JourneyRunner extends WorkflowEntrypoint<JourneyEnv, JourneyParams> {
17
17
  /**
18
- * The data plane. D1 from the env by default; an app on Postgres
19
- * overrides this (and `closeStore` if its client needs closing):
18
+ * The data plane: `journeyWorker` opens it through the host's store lease. A runner subclass
19
+ * of its own overrides this (and `closeStore` if its client needs closing):
20
20
  *
21
21
  * protected override createStore() { return new PostgresJourneyStore(client(this.env)); }
22
22
  */
@@ -1 +1 @@
1
- {"version":3,"file":"runner.d.cts","names":[],"sources":["../../src/cloudflare/runner.ts"],"mappings":";;;;;;;;;;;;;;;qBAwGa,sBAAsB,mBAAmB,YAAY;;;;;;;YAOtD,eAAe;;YAKT,WAAW,QAAQ,eAAe;UAE1C;;;;;;YAOE,WAAW;;;;;;;;;;;;YAeX,QAAQ,SAAS,gBAAgB;;;;;;YASjC,uBAAuB;;;;;;;;YAavB,oBAAoB;;;;;YAQpB,YAAY;;;;;;YASZ,0BAA0B;;;;;YAQ1B,eAAe,QAAQ,eAAe;;YAKtC,uBAAuB;;;;;YAQvB,eAAe,OAAO,cAAc,QAAQ,gBAAgB;EAahE,IAAI,OAAO,cAAc,gBAAgB,MAAM,eAAe,QAAQ"}
1
+ {"version":3,"file":"runner.d.cts","names":[],"sources":["../../src/cloudflare/runner.ts"],"mappings":";;;;;;;;;;;;;;;qBAuGa,sBAAsB,mBAAmB,YAAY;;;;;;;YAOtD,eAAe;;YAKT,WAAW,QAAQ,eAAe;UAE1C;;;;;;YAOE,WAAW;;;;;;;;;;;;YAeX,QAAQ,SAAS,gBAAgB;;;;;;YASjC,uBAAuB;;;;;;;;YAavB,oBAAoB;;;;;YAQpB,YAAY;;;;;;YASZ,0BAA0B;;;;;YAQ1B,eAAe,QAAQ,eAAe;;YAKtC,uBAAuB;;;;;YAQvB,eAAe,OAAO,cAAc,QAAQ,gBAAgB;EAahE,IAAI,OAAO,cAAc,gBAAgB,MAAM,eAAe,QAAQ"}
@@ -1,9 +1,9 @@
1
1
  import { ActionInvoker, JourneyObserver, JourneyOutcome, MessageSender, MessageValidator } from "../engine/ports.mjs";
2
2
  import { RegisteredAction } from "../engine/actions.mjs";
3
- import { JourneyEnv, JourneyParams } from "../host/platform.mjs";
4
3
  import { JourneyStore } from "../store/index.mjs";
5
4
  import { Contract } from "../contract.mjs";
6
5
  import { JourneyRuntimeOptions } from "../runtime.mjs";
6
+ import { JourneyEnv, JourneyParams } from "../host/platform.mjs";
7
7
  import { JourneyRunParts } from "../host/execute.mjs";
8
8
  import { WorkflowEntrypoint, WorkflowEvent, WorkflowStep } from "cloudflare:workers";
9
9
  //#region src/cloudflare/runner.d.ts
@@ -15,8 +15,8 @@ import { WorkflowEntrypoint, WorkflowEvent, WorkflowStep } from "cloudflare:work
15
15
  */
16
16
  export declare class JourneyRunner extends WorkflowEntrypoint<JourneyEnv, JourneyParams> {
17
17
  /**
18
- * The data plane. D1 from the env by default; an app on Postgres
19
- * overrides this (and `closeStore` if its client needs closing):
18
+ * The data plane: `journeyWorker` opens it through the host's store lease. A runner subclass
19
+ * of its own overrides this (and `closeStore` if its client needs closing):
20
20
  *
21
21
  * protected override createStore() { return new PostgresJourneyStore(client(this.env)); }
22
22
  */
@@ -1 +1 @@
1
- {"version":3,"file":"runner.d.mts","names":[],"sources":["../../src/cloudflare/runner.ts"],"mappings":";;;;;;;;;;;;;;;qBAwGa,sBAAsB,mBAAmB,YAAY;;;;;;;YAOtD,eAAe;;YAKT,WAAW,QAAQ,eAAe;UAE1C;;;;;;YAOE,WAAW;;;;;;;;;;;;YAeX,QAAQ,SAAS,gBAAgB;;;;;;YASjC,uBAAuB;;;;;;;;YAavB,oBAAoB;;;;;YAQpB,YAAY;;;;;;YASZ,0BAA0B;;;;;YAQ1B,eAAe,QAAQ,eAAe;;YAKtC,uBAAuB;;;;;YAQvB,eAAe,OAAO,cAAc,QAAQ,gBAAgB;EAahE,IAAI,OAAO,cAAc,gBAAgB,MAAM,eAAe,QAAQ"}
1
+ {"version":3,"file":"runner.d.mts","names":[],"sources":["../../src/cloudflare/runner.ts"],"mappings":";;;;;;;;;;;;;;;qBAuGa,sBAAsB,mBAAmB,YAAY;;;;;;;YAOtD,eAAe;;YAKT,WAAW,QAAQ,eAAe;UAE1C;;;;;;YAOE,WAAW;;;;;;;;;;;;YAeX,QAAQ,SAAS,gBAAgB;;;;;;YASjC,uBAAuB;;;;;;;;YAavB,oBAAoB;;;;;YAQpB,YAAY;;;;;;YASZ,0BAA0B;;;;;YAQ1B,eAAe,QAAQ,eAAe;;YAKtC,uBAAuB;;;;;YAQvB,eAAe,OAAO,cAAc,QAAQ,gBAAgB;EAahE,IAAI,OAAO,cAAc,gBAAgB,MAAM,eAAe,QAAQ"}
@@ -1,6 +1,5 @@
1
1
  import { isRetryable } from "../engine/retry.mjs";
2
2
  import { resolveRuntime } from "../runtime.mjs";
3
- import { d1Store } from "./d1-env.mjs";
4
3
  import { WAKE_EVENT_TYPE } from "../host/platform.mjs";
5
4
  import { executeJourney } from "../host/execute.mjs";
6
5
  import { RegistryActionInvoker } from "../engine/actions.mjs";
@@ -78,13 +77,13 @@ var CfEngineStep = class {
78
77
  */
79
78
  var JourneyRunner = class extends WorkflowEntrypoint {
80
79
  /**
81
- * The data plane. D1 from the env by default; an app on Postgres
82
- * overrides this (and `closeStore` if its client needs closing):
80
+ * The data plane: `journeyWorker` opens it through the host's store lease. A runner subclass
81
+ * of its own overrides this (and `closeStore` if its client needs closing):
83
82
  *
84
83
  * protected override createStore() { return new PostgresJourneyStore(client(this.env)); }
85
84
  */
86
85
  createStore() {
87
- return d1Store(this.env);
86
+ throw new Error("JourneyRunner: override createStore, or build the runner with journeyWorker");
88
87
  }
89
88
  /** Called once `run` is over, success or not — release whatever `createStore` opened. */
90
89
  async closeStore(_store) {}
@@ -1 +1 @@
1
- {"version":3,"file":"runner.mjs","names":[],"sources":["../../src/cloudflare/runner.ts"],"sourcesContent":["import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from 'cloudflare:workers';\nimport { NonRetryableError } from 'cloudflare:workflows';\nimport type { Contract } from '../contract';\nimport { type RegisteredAction, RegistryActionInvoker } from '../engine/actions';\nimport type {\n ActionInvoker,\n EngineStep,\n JourneyObserver,\n JourneyOutcome,\n MessageSender,\n MessageValidator,\n} from '../engine/ports';\nimport { isRetryable } from '../engine/retry';\nimport { type JourneyRunParts, executeJourney } from '../host/execute';\nimport { type JourneyEnv, type JourneyParams, WAKE_EVENT_TYPE } from '../host/platform';\nimport { LogMessageSender, WebhookMessageSender } from '../host/senders';\nimport { type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport type { JourneyStore } from '../store/index';\nimport { cloudflareJourneyBinding, wakeExpired } from './bindings';\nimport { d1Store } from './d1-env';\n\n/**\n * Adapts CF's WorkflowStep to EngineStep.\n * subscribe/unsubscribe write the subscription table inside durable steps (the\n * router wakes instances from that table); waitForWake parks on CF's\n * waitForEvent, which signals a timeout by throwing — and so does every other\n * failure in there, so only the clock, against the checkpointed deadline, can\n * tell them apart (see wakeExpired).\n */\nclass CfEngineStep implements EngineStep {\n constructor(\n private readonly step: WorkflowStep,\n private readonly store: JourneyStore,\n private readonly userId: string,\n private readonly instanceId: string\n ) {}\n\n /**\n * A failure a retry would only repeat (`isRetryable`: a template refusing its props, the\n * contract refusing an emitted event, a provider rejecting the request) ends the step at\n * once instead of burning the retry budget. The engine recognizes a NonRetryableError by its\n * `name`, so the original's name is not written over it — the original travels as `cause`.\n */\n do<T>(name: string, fn: () => Promise<T>): Promise<T> {\n return this.step.do(name, async () => {\n try {\n return await fn();\n } catch (error) {\n if (error instanceof Error && !isRetryable(error)) {\n const permanent = new NonRetryableError(error.message);\n permanent.cause = error;\n throw permanent;\n }\n throw error;\n }\n });\n }\n\n sleep(name: string, ms: number): Promise<void> {\n return this.step.sleep(name, ms);\n }\n\n subscribe(name: string, events: readonly string[]): Promise<void> {\n // Idempotent in the store: the interpreter re-subscribes on every wait\n // attempt (one-shot-callback contract) and registrations persist here.\n return this.step.do(name, () =>\n this.store.subscribe(this.instanceId, this.userId, events, Date.now())\n );\n }\n\n unsubscribe(name: string): Promise<void> {\n return this.step.do(name, () => this.store.unsubscribe(this.instanceId));\n }\n\n async waitForWake(\n name: string,\n { timeoutMs, deadlineMs }: { timeoutMs: number; deadlineMs: number }\n ): Promise<'event' | 'timeout'> {\n try {\n await this.step.waitForEvent(name, { type: WAKE_EVENT_TYPE, timeout: timeoutMs });\n return 'event';\n } catch (error) {\n /*\n * Only the deadline can tell a real timeout from a failure (see wakeExpired). The\n * deadline is the interpreter's checkpoint, so the verdict is the same on the attempt\n * that parked and on a replay after an eviction — which is where the wait usually\n * resurfaces: the instance is rebuilt, the wait is already over, and the first thing to\n * run is this step. The verdict is checkpointed too, so later replays reuse it.\n */\n const timedOut = await this.step.do(`${name}:timed-out`, async () =>\n wakeExpired(deadlineMs, Date.now())\n );\n if (timedOut) return 'timeout';\n throw error;\n }\n }\n}\n\n/**\n * The generic journey executor: one class runs every workflow, because IR is\n * data. It is loaded from the store by the contentHash pinned at entry —\n * content addressing makes it immutable, so reading it outside a step is\n * replay-safe.\n */\nexport class JourneyRunner extends WorkflowEntrypoint<JourneyEnv, JourneyParams> {\n /**\n * The data plane. D1 from the env by default; an app on Postgres\n * overrides this (and `closeStore` if its client needs closing):\n *\n * protected override createStore() { return new PostgresJourneyStore(client(this.env)); }\n */\n protected createStore(): JourneyStore {\n return d1Store(this.env);\n }\n\n /** Called once `run` is over, success or not — release whatever `createStore` opened. */\n protected async closeStore(_store: JourneyStore): Promise<void> {}\n\n private adapted: JourneyEnv | undefined;\n\n /**\n * The env a run ingests its emitted events with: the Workflow binding behind the host's port,\n * so an instance the platform no longer knows answers `InstanceNotFoundError` (what lets the\n * host drop its wakes). Built once per instance.\n */\n protected hostEnv(): JourneyEnv {\n return (this.adapted ??= { ...this.env, JOURNEY: cloudflareJourneyBinding(this.env.JOURNEY) });\n }\n\n /**\n * Runtime options for this instance — typed configuration, resolved by the app (typically\n * `resolveRuntime(config.runtime, mode)` over its workflow.config.ts, with the mode coming from\n * an ENVIRONMENT var). The instance params are passed so the decision can also be per user:\n * a tester account in production can run in fast-forward while everyone else runs in real\n * time. Defaults: real time, quiet.\n *\n * protected override runtime({ userId }) {\n * return resolveRuntime(workflowConfig.runtime, isTester(userId) ? 'test' : this.env.ENVIRONMENT);\n * }\n */\n protected runtime(_params: JourneyParams): JourneyRuntimeOptions {\n return resolveRuntime(undefined, 'production');\n }\n\n /**\n * Message outlet; an app overrides this to plug in real channels (say\n * an EmailSender over a transport with a react-email renderer). By default it picks the webhook\n * or logging sender based on the environment.\n */\n protected createMessageSender(): MessageSender {\n return this.env.MESSAGE_WEBHOOK_URL\n ? new WebhookMessageSender(this.env.MESSAGE_WEBHOOK_URL)\n : new LogMessageSender();\n }\n\n /**\n * Custom-action registry; an app overrides this returning the same\n * `action(...)` objects its workflows reference (an ActionRef is a\n * RegisteredAction structurally) — the code plane of the dual-plane model:\n *\n * protected override actions() { return [syncCrm, issueCoupon]; }\n */\n protected actions(): readonly RegisteredAction[] {\n return [];\n }\n\n /**\n * The contract events this journey emits (`.sendEvent()`) are checked\n * against, exactly like ingest; journeyWorker passes its config's.\n */\n protected contract(): Contract | undefined {\n return undefined;\n }\n\n /**\n * Template props check before every send; an app overrides this with\n * `registryValidator({ emails, pushes })` (journeyWorker does it from its\n * registries). By default props go out as resolved.\n */\n protected createMessageValidator(): MessageValidator | undefined {\n return undefined;\n }\n\n /**\n * Reporting hooks: node arrivals, every copy sent, how each instance ended. The host stores\n * what it needs for its dashboards; by default nothing is reported.\n */\n protected createObserver(_store: JourneyStore): JourneyObserver | undefined {\n return undefined;\n }\n\n /** Override for a different version policy ('warn' by default) or a fully custom invoker (e.g. webhook degradation for hosted tenants). */\n protected createActionInvoker(): ActionInvoker {\n return new RegistryActionInvoker(this.actions());\n }\n\n /**\n * Everything one run is wired to. By default it is composed from the methods above; a host\n * that assembles the whole set itself (journeyWorker) overrides this one instead.\n */\n protected createRunParts(store: JourneyStore, params: JourneyParams): JourneyRunParts {\n return {\n store,\n env: this.hostEnv(),\n runtime: this.runtime(params),\n messages: this.createMessageSender(),\n actions: this.createActionInvoker(),\n contract: this.contract(),\n validator: this.createMessageValidator(),\n observer: this.createObserver(store),\n };\n }\n\n async run(event: WorkflowEvent<JourneyParams>, step: WorkflowStep): Promise<JourneyOutcome> {\n const store = this.createStore();\n try {\n const { userId } = event.payload;\n return await executeJourney(\n {\n instanceId: event.instanceId,\n params: event.payload,\n // Trigger-event creation time: replay-stable, unlike Date.now() here\n enteredAtMs: event.timestamp.getTime(),\n step: new CfEngineStep(step, store, userId, event.instanceId),\n },\n this.createRunParts(store, event.payload)\n );\n } finally {\n await this.closeStore(store);\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA6BA,IAAM,eAAN,MAAyC;CAEpB;CACA;CACA;CACA;CAJnB,YACE,MACA,OACA,QACA,YACA;EAJiB,KAAA,OAAA;EACA,KAAA,QAAA;EACA,KAAA,SAAA;EACA,KAAA,aAAA;CAChB;;;;;;;CAQH,GAAM,MAAc,IAAkC;EACpD,OAAO,KAAK,KAAK,GAAG,MAAM,YAAY;GACpC,IAAI;IACF,OAAO,MAAM,GAAG;GAClB,SAAS,OAAO;IACd,IAAI,iBAAiB,SAAS,CAAC,YAAY,KAAK,GAAG;KACjD,MAAM,YAAY,IAAI,kBAAkB,MAAM,OAAO;KACrD,UAAU,QAAQ;KAClB,MAAM;IACR;IACA,MAAM;GACR;EACF,CAAC;CACH;CAEA,MAAM,MAAc,IAA2B;EAC7C,OAAO,KAAK,KAAK,MAAM,MAAM,EAAE;CACjC;CAEA,UAAU,MAAc,QAA0C;EAGhE,OAAO,KAAK,KAAK,GAAG,YAClB,KAAK,MAAM,UAAU,KAAK,YAAY,KAAK,QAAQ,QAAQ,KAAK,IAAI,CAAC,CACvE;CACF;CAEA,YAAY,MAA6B;EACvC,OAAO,KAAK,KAAK,GAAG,YAAY,KAAK,MAAM,YAAY,KAAK,UAAU,CAAC;CACzE;CAEA,MAAM,YACJ,MACA,EAAE,WAAW,cACiB;EAC9B,IAAI;GACF,MAAM,KAAK,KAAK,aAAa,MAAM;IAAE,MAAM;IAAiB,SAAS;GAAU,CAAC;GAChF,OAAO;EACT,SAAS,OAAO;GAWd,IAAI,MAHmB,KAAK,KAAK,GAAG,GAAG,KAAK,aAAa,YACvD,YAAY,YAAY,KAAK,IAAI,CAAC,CACpC,GACc,OAAO;GACrB,MAAM;EACR;CACF;AACF;;;;;;;AAQA,IAAa,gBAAb,cAAmC,mBAA8C;;;;;;;CAO/E,cAAsC;EACpC,OAAO,QAAQ,KAAK,GAAG;CACzB;;CAGA,MAAgB,WAAW,QAAqC,CAAC;CAEjE;;;;;;CAOA,UAAgC;EAC9B,OAAQ,KAAK,YAAY;GAAE,GAAG,KAAK;GAAK,SAAS,yBAAyB,KAAK,IAAI,OAAO;EAAE;CAC9F;;;;;;;;;;;;CAaA,QAAkB,SAA+C;EAC/D,OAAO,eAAe,KAAA,GAAW,YAAY;CAC/C;;;;;;CAOA,sBAA+C;EAC7C,OAAO,KAAK,IAAI,sBACZ,IAAI,qBAAqB,KAAK,IAAI,mBAAmB,IACrD,IAAI,iBAAiB;CAC3B;;;;;;;;CASA,UAAiD;EAC/C,OAAO,CAAC;CACV;;;;;CAMA,WAA2C,CAE3C;;;;;;CAOA,yBAAiE,CAEjE;;;;;CAMA,eAAyB,QAAmD,CAE5E;;CAGA,sBAA+C;EAC7C,OAAO,IAAI,sBAAsB,KAAK,QAAQ,CAAC;CACjD;;;;;CAMA,eAAyB,OAAqB,QAAwC;EACpF,OAAO;GACL;GACA,KAAK,KAAK,QAAQ;GAClB,SAAS,KAAK,QAAQ,MAAM;GAC5B,UAAU,KAAK,oBAAoB;GACnC,SAAS,KAAK,oBAAoB;GAClC,UAAU,KAAK,SAAS;GACxB,WAAW,KAAK,uBAAuB;GACvC,UAAU,KAAK,eAAe,KAAK;EACrC;CACF;CAEA,MAAM,IAAI,OAAqC,MAA6C;EAC1F,MAAM,QAAQ,KAAK,YAAY;EAC/B,IAAI;GACF,MAAM,EAAE,WAAW,MAAM;GACzB,OAAO,MAAM,eACX;IACE,YAAY,MAAM;IAClB,QAAQ,MAAM;IAEd,aAAa,MAAM,UAAU,QAAQ;IACrC,MAAM,IAAI,aAAa,MAAM,OAAO,QAAQ,MAAM,UAAU;GAC9D,GACA,KAAK,eAAe,OAAO,MAAM,OAAO,CAC1C;EACF,UAAU;GACR,MAAM,KAAK,WAAW,KAAK;EAC7B;CACF;AACF"}
1
+ {"version":3,"file":"runner.mjs","names":[],"sources":["../../src/cloudflare/runner.ts"],"sourcesContent":["import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from 'cloudflare:workers';\nimport { NonRetryableError } from 'cloudflare:workflows';\nimport type { Contract } from '../contract';\nimport { type RegisteredAction, RegistryActionInvoker } from '../engine/actions';\nimport type {\n ActionInvoker,\n EngineStep,\n JourneyObserver,\n JourneyOutcome,\n MessageSender,\n MessageValidator,\n} from '../engine/ports';\nimport { isRetryable } from '../engine/retry';\nimport { type JourneyRunParts, executeJourney } from '../host/execute';\nimport { type JourneyEnv, type JourneyParams, WAKE_EVENT_TYPE } from '../host/platform';\nimport { LogMessageSender, WebhookMessageSender } from '../host/senders';\nimport { type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport type { JourneyStore } from '../store/index';\nimport { cloudflareJourneyBinding, wakeExpired } from './bindings';\n\n/**\n * Adapts CF's WorkflowStep to EngineStep.\n * subscribe/unsubscribe write the subscription table inside durable steps (the\n * router wakes instances from that table); waitForWake parks on CF's\n * waitForEvent, which signals a timeout by throwing — and so does every other\n * failure in there, so only the clock, against the checkpointed deadline, can\n * tell them apart (see wakeExpired).\n */\nclass CfEngineStep implements EngineStep {\n constructor(\n private readonly step: WorkflowStep,\n private readonly store: JourneyStore,\n private readonly userId: string,\n private readonly instanceId: string\n ) {}\n\n /**\n * A failure a retry would only repeat (`isRetryable`: a template refusing its props, the\n * contract refusing an emitted event, a provider rejecting the request) ends the step at\n * once instead of burning the retry budget. The engine recognizes a NonRetryableError by its\n * `name`, so the original's name is not written over it — the original travels as `cause`.\n */\n do<T>(name: string, fn: () => Promise<T>): Promise<T> {\n return this.step.do(name, async () => {\n try {\n return await fn();\n } catch (error) {\n if (error instanceof Error && !isRetryable(error)) {\n const permanent = new NonRetryableError(error.message);\n permanent.cause = error;\n throw permanent;\n }\n throw error;\n }\n });\n }\n\n sleep(name: string, ms: number): Promise<void> {\n return this.step.sleep(name, ms);\n }\n\n subscribe(name: string, events: readonly string[]): Promise<void> {\n // Idempotent in the store: the interpreter re-subscribes on every wait\n // attempt (one-shot-callback contract) and registrations persist here.\n return this.step.do(name, () =>\n this.store.subscribe(this.instanceId, this.userId, events, Date.now())\n );\n }\n\n unsubscribe(name: string): Promise<void> {\n return this.step.do(name, () => this.store.unsubscribe(this.instanceId));\n }\n\n async waitForWake(\n name: string,\n { timeoutMs, deadlineMs }: { timeoutMs: number; deadlineMs: number }\n ): Promise<'event' | 'timeout'> {\n try {\n await this.step.waitForEvent(name, { type: WAKE_EVENT_TYPE, timeout: timeoutMs });\n return 'event';\n } catch (error) {\n /*\n * Only the deadline can tell a real timeout from a failure (see wakeExpired). The\n * deadline is the interpreter's checkpoint, so the verdict is the same on the attempt\n * that parked and on a replay after an eviction — which is where the wait usually\n * resurfaces: the instance is rebuilt, the wait is already over, and the first thing to\n * run is this step. The verdict is checkpointed too, so later replays reuse it.\n */\n const timedOut = await this.step.do(`${name}:timed-out`, async () =>\n wakeExpired(deadlineMs, Date.now())\n );\n if (timedOut) return 'timeout';\n throw error;\n }\n }\n}\n\n/**\n * The generic journey executor: one class runs every workflow, because IR is\n * data. It is loaded from the store by the contentHash pinned at entry —\n * content addressing makes it immutable, so reading it outside a step is\n * replay-safe.\n */\nexport class JourneyRunner extends WorkflowEntrypoint<JourneyEnv, JourneyParams> {\n /**\n * The data plane: `journeyWorker` opens it through the host's store lease. A runner subclass\n * of its own overrides this (and `closeStore` if its client needs closing):\n *\n * protected override createStore() { return new PostgresJourneyStore(client(this.env)); }\n */\n protected createStore(): JourneyStore {\n throw new Error('JourneyRunner: override createStore, or build the runner with journeyWorker');\n }\n\n /** Called once `run` is over, success or not — release whatever `createStore` opened. */\n protected async closeStore(_store: JourneyStore): Promise<void> {}\n\n private adapted: JourneyEnv | undefined;\n\n /**\n * The env a run ingests its emitted events with: the Workflow binding behind the host's port,\n * so an instance the platform no longer knows answers `InstanceNotFoundError` (what lets the\n * host drop its wakes). Built once per instance.\n */\n protected hostEnv(): JourneyEnv {\n return (this.adapted ??= { ...this.env, JOURNEY: cloudflareJourneyBinding(this.env.JOURNEY) });\n }\n\n /**\n * Runtime options for this instance — typed configuration, resolved by the app (typically\n * `resolveRuntime(config.runtime, mode)` over its workflow.config.ts, with the mode coming from\n * an ENVIRONMENT var). The instance params are passed so the decision can also be per user:\n * a tester account in production can run in fast-forward while everyone else runs in real\n * time. Defaults: real time, quiet.\n *\n * protected override runtime({ userId }) {\n * return resolveRuntime(workflowConfig.runtime, isTester(userId) ? 'test' : this.env.ENVIRONMENT);\n * }\n */\n protected runtime(_params: JourneyParams): JourneyRuntimeOptions {\n return resolveRuntime(undefined, 'production');\n }\n\n /**\n * Message outlet; an app overrides this to plug in real channels (say\n * an EmailSender over a transport with a react-email renderer). By default it picks the webhook\n * or logging sender based on the environment.\n */\n protected createMessageSender(): MessageSender {\n return this.env.MESSAGE_WEBHOOK_URL\n ? new WebhookMessageSender(this.env.MESSAGE_WEBHOOK_URL)\n : new LogMessageSender();\n }\n\n /**\n * Custom-action registry; an app overrides this returning the same\n * `action(...)` objects its workflows reference (an ActionRef is a\n * RegisteredAction structurally) — the code plane of the dual-plane model:\n *\n * protected override actions() { return [syncCrm, issueCoupon]; }\n */\n protected actions(): readonly RegisteredAction[] {\n return [];\n }\n\n /**\n * The contract events this journey emits (`.sendEvent()`) are checked\n * against, exactly like ingest; journeyWorker passes its config's.\n */\n protected contract(): Contract | undefined {\n return undefined;\n }\n\n /**\n * Template props check before every send; an app overrides this with\n * `registryValidator({ emails, pushes })` (journeyWorker does it from its\n * registries). By default props go out as resolved.\n */\n protected createMessageValidator(): MessageValidator | undefined {\n return undefined;\n }\n\n /**\n * Reporting hooks: node arrivals, every copy sent, how each instance ended. The host stores\n * what it needs for its dashboards; by default nothing is reported.\n */\n protected createObserver(_store: JourneyStore): JourneyObserver | undefined {\n return undefined;\n }\n\n /** Override for a different version policy ('warn' by default) or a fully custom invoker (e.g. webhook degradation for hosted tenants). */\n protected createActionInvoker(): ActionInvoker {\n return new RegistryActionInvoker(this.actions());\n }\n\n /**\n * Everything one run is wired to. By default it is composed from the methods above; a host\n * that assembles the whole set itself (journeyWorker) overrides this one instead.\n */\n protected createRunParts(store: JourneyStore, params: JourneyParams): JourneyRunParts {\n return {\n store,\n env: this.hostEnv(),\n runtime: this.runtime(params),\n messages: this.createMessageSender(),\n actions: this.createActionInvoker(),\n contract: this.contract(),\n validator: this.createMessageValidator(),\n observer: this.createObserver(store),\n };\n }\n\n async run(event: WorkflowEvent<JourneyParams>, step: WorkflowStep): Promise<JourneyOutcome> {\n const store = this.createStore();\n try {\n const { userId } = event.payload;\n return await executeJourney(\n {\n instanceId: event.instanceId,\n params: event.payload,\n // Trigger-event creation time: replay-stable, unlike Date.now() here\n enteredAtMs: event.timestamp.getTime(),\n step: new CfEngineStep(step, store, userId, event.instanceId),\n },\n this.createRunParts(store, event.payload)\n );\n } finally {\n await this.closeStore(store);\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA4BA,IAAM,eAAN,MAAyC;CAEpB;CACA;CACA;CACA;CAJnB,YACE,MACA,OACA,QACA,YACA;EAJiB,KAAA,OAAA;EACA,KAAA,QAAA;EACA,KAAA,SAAA;EACA,KAAA,aAAA;CAChB;;;;;;;CAQH,GAAM,MAAc,IAAkC;EACpD,OAAO,KAAK,KAAK,GAAG,MAAM,YAAY;GACpC,IAAI;IACF,OAAO,MAAM,GAAG;GAClB,SAAS,OAAO;IACd,IAAI,iBAAiB,SAAS,CAAC,YAAY,KAAK,GAAG;KACjD,MAAM,YAAY,IAAI,kBAAkB,MAAM,OAAO;KACrD,UAAU,QAAQ;KAClB,MAAM;IACR;IACA,MAAM;GACR;EACF,CAAC;CACH;CAEA,MAAM,MAAc,IAA2B;EAC7C,OAAO,KAAK,KAAK,MAAM,MAAM,EAAE;CACjC;CAEA,UAAU,MAAc,QAA0C;EAGhE,OAAO,KAAK,KAAK,GAAG,YAClB,KAAK,MAAM,UAAU,KAAK,YAAY,KAAK,QAAQ,QAAQ,KAAK,IAAI,CAAC,CACvE;CACF;CAEA,YAAY,MAA6B;EACvC,OAAO,KAAK,KAAK,GAAG,YAAY,KAAK,MAAM,YAAY,KAAK,UAAU,CAAC;CACzE;CAEA,MAAM,YACJ,MACA,EAAE,WAAW,cACiB;EAC9B,IAAI;GACF,MAAM,KAAK,KAAK,aAAa,MAAM;IAAE,MAAM;IAAiB,SAAS;GAAU,CAAC;GAChF,OAAO;EACT,SAAS,OAAO;GAWd,IAAI,MAHmB,KAAK,KAAK,GAAG,GAAG,KAAK,aAAa,YACvD,YAAY,YAAY,KAAK,IAAI,CAAC,CACpC,GACc,OAAO;GACrB,MAAM;EACR;CACF;AACF;;;;;;;AAQA,IAAa,gBAAb,cAAmC,mBAA8C;;;;;;;CAO/E,cAAsC;EACpC,MAAM,IAAI,MAAM,6EAA6E;CAC/F;;CAGA,MAAgB,WAAW,QAAqC,CAAC;CAEjE;;;;;;CAOA,UAAgC;EAC9B,OAAQ,KAAK,YAAY;GAAE,GAAG,KAAK;GAAK,SAAS,yBAAyB,KAAK,IAAI,OAAO;EAAE;CAC9F;;;;;;;;;;;;CAaA,QAAkB,SAA+C;EAC/D,OAAO,eAAe,KAAA,GAAW,YAAY;CAC/C;;;;;;CAOA,sBAA+C;EAC7C,OAAO,KAAK,IAAI,sBACZ,IAAI,qBAAqB,KAAK,IAAI,mBAAmB,IACrD,IAAI,iBAAiB;CAC3B;;;;;;;;CASA,UAAiD;EAC/C,OAAO,CAAC;CACV;;;;;CAMA,WAA2C,CAE3C;;;;;;CAOA,yBAAiE,CAEjE;;;;;CAMA,eAAyB,QAAmD,CAE5E;;CAGA,sBAA+C;EAC7C,OAAO,IAAI,sBAAsB,KAAK,QAAQ,CAAC;CACjD;;;;;CAMA,eAAyB,OAAqB,QAAwC;EACpF,OAAO;GACL;GACA,KAAK,KAAK,QAAQ;GAClB,SAAS,KAAK,QAAQ,MAAM;GAC5B,UAAU,KAAK,oBAAoB;GACnC,SAAS,KAAK,oBAAoB;GAClC,UAAU,KAAK,SAAS;GACxB,WAAW,KAAK,uBAAuB;GACvC,UAAU,KAAK,eAAe,KAAK;EACrC;CACF;CAEA,MAAM,IAAI,OAAqC,MAA6C;EAC1F,MAAM,QAAQ,KAAK,YAAY;EAC/B,IAAI;GACF,MAAM,EAAE,WAAW,MAAM;GACzB,OAAO,MAAM,eACX;IACE,YAAY,MAAM;IAClB,QAAQ,MAAM;IAEd,aAAa,MAAM,UAAU,QAAQ;IACrC,MAAM,IAAI,aAAa,MAAM,OAAO,QAAQ,MAAM,UAAU;GAC9D,GACA,KAAK,eAAe,OAAO,MAAM,OAAO,CAC1C;EACF,UAAU;GACR,MAAM,KAAK,WAAW,KAAK;EAC7B;CACF;AACF"}