@shware/workflow 3.0.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -4
- package/dist/aws/engine-step.cjs +2 -3
- package/dist/aws/engine-step.cjs.map +1 -1
- package/dist/aws/engine-step.d.cts +10 -0
- package/dist/aws/engine-step.d.cts.map +1 -1
- package/dist/aws/engine-step.d.mts +10 -0
- package/dist/aws/engine-step.d.mts.map +1 -1
- package/dist/aws/engine-step.mjs +2 -3
- package/dist/aws/engine-step.mjs.map +1 -1
- package/dist/aws/index.cjs +14 -6
- package/dist/aws/index.cjs.map +1 -1
- package/dist/aws/index.d.cts +12 -0
- package/dist/aws/index.d.cts.map +1 -1
- package/dist/aws/index.d.mts +12 -0
- package/dist/aws/index.d.mts.map +1 -1
- package/dist/aws/index.mjs +14 -6
- package/dist/aws/index.mjs.map +1 -1
- package/dist/aws/instances.cjs +29 -9
- package/dist/aws/instances.cjs.map +1 -1
- package/dist/aws/instances.d.cts +24 -5
- package/dist/aws/instances.d.cts.map +1 -1
- package/dist/aws/instances.d.mts +24 -5
- package/dist/aws/instances.d.mts.map +1 -1
- package/dist/aws/instances.mjs +29 -9
- package/dist/aws/instances.mjs.map +1 -1
- package/dist/cloudflare/bindings.cjs.map +1 -1
- package/dist/cloudflare/bindings.d.cts +2 -0
- package/dist/cloudflare/bindings.d.cts.map +1 -1
- package/dist/cloudflare/bindings.d.mts +2 -0
- package/dist/cloudflare/bindings.d.mts.map +1 -1
- package/dist/cloudflare/bindings.mjs.map +1 -1
- package/dist/cloudflare/runner.cjs +4 -5
- package/dist/cloudflare/runner.cjs.map +1 -1
- package/dist/cloudflare/runner.d.cts.map +1 -1
- package/dist/cloudflare/runner.d.mts.map +1 -1
- package/dist/cloudflare/runner.mjs +4 -5
- package/dist/cloudflare/runner.mjs.map +1 -1
- package/dist/engine/duration.cjs +34 -0
- package/dist/engine/duration.cjs.map +1 -0
- package/dist/engine/duration.d.cts +5 -0
- package/dist/engine/duration.d.cts.map +1 -0
- package/dist/engine/duration.d.mts +5 -0
- package/dist/engine/duration.d.mts.map +1 -0
- package/dist/engine/duration.mjs +33 -0
- package/dist/engine/duration.mjs.map +1 -0
- package/dist/engine/index.cjs +5 -0
- package/dist/engine/index.d.cts +3 -1
- package/dist/engine/index.d.mts +3 -1
- package/dist/engine/index.mjs +3 -1
- package/dist/engine/retry.cjs +29 -0
- package/dist/engine/retry.cjs.map +1 -0
- package/dist/engine/retry.d.cts +17 -0
- package/dist/engine/retry.d.cts.map +1 -0
- package/dist/engine/retry.d.mts +17 -0
- package/dist/engine/retry.d.mts.map +1 -0
- package/dist/engine/retry.mjs +27 -0
- package/dist/engine/retry.mjs.map +1 -0
- package/dist/host/execute.cjs +6 -7
- package/dist/host/execute.cjs.map +1 -1
- package/dist/host/execute.d.cts +0 -4
- package/dist/host/execute.d.cts.map +1 -1
- package/dist/host/execute.d.mts +0 -4
- package/dist/host/execute.d.mts.map +1 -1
- package/dist/host/execute.mjs +6 -7
- package/dist/host/execute.mjs.map +1 -1
- package/dist/host/journey-host.cjs +15 -0
- package/dist/host/journey-host.cjs.map +1 -1
- package/dist/host/journey-host.d.cts.map +1 -1
- package/dist/host/journey-host.d.mts.map +1 -1
- package/dist/host/journey-host.mjs +15 -0
- package/dist/host/journey-host.mjs.map +1 -1
- package/dist/host/router.cjs +7 -5
- package/dist/host/router.cjs.map +1 -1
- package/dist/host/router.d.cts.map +1 -1
- package/dist/host/router.d.mts.map +1 -1
- package/dist/host/router.mjs +7 -5
- package/dist/host/router.mjs.map +1 -1
- package/dist/http2-client.cjs +7 -5
- package/dist/http2-client.cjs.map +1 -1
- package/dist/http2-client.d.cts +5 -2
- package/dist/http2-client.d.cts.map +1 -1
- package/dist/http2-client.d.mts +5 -2
- package/dist/http2-client.d.mts.map +1 -1
- package/dist/http2-client.mjs +7 -5
- package/dist/http2-client.mjs.map +1 -1
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +11 -0
- package/dist/store/index.d.cts.map +1 -1
- package/dist/store/index.d.mts +11 -0
- package/dist/store/index.d.mts.map +1 -1
- package/dist/store/index.mjs.map +1 -1
- package/dist/store/postgres.cjs +37 -13
- package/dist/store/postgres.cjs.map +1 -1
- package/dist/store/postgres.d.cts +6 -0
- package/dist/store/postgres.d.cts.map +1 -1
- package/dist/store/postgres.d.mts +6 -0
- package/dist/store/postgres.d.mts.map +1 -1
- package/dist/store/postgres.mjs +37 -13
- package/dist/store/postgres.mjs.map +1 -1
- package/dist/store/tables.cjs +6 -3
- package/dist/store/tables.cjs.map +1 -1
- package/dist/store/tables.d.cts.map +1 -1
- package/dist/store/tables.d.mts.map +1 -1
- package/dist/store/tables.mjs +6 -3
- package/dist/store/tables.mjs.map +1 -1
- package/dist/transports/cloudflare-email.cjs +4 -0
- package/dist/transports/cloudflare-email.cjs.map +1 -1
- package/dist/transports/cloudflare-email.d.cts +2 -0
- package/dist/transports/cloudflare-email.d.cts.map +1 -1
- package/dist/transports/cloudflare-email.d.mts +2 -0
- package/dist/transports/cloudflare-email.d.mts.map +1 -1
- package/dist/transports/cloudflare-email.mjs +4 -0
- package/dist/transports/cloudflare-email.mjs.map +1 -1
- package/dist/transports/ses.cjs +4 -0
- package/dist/transports/ses.cjs.map +1 -1
- package/dist/transports/ses.d.cts +2 -0
- package/dist/transports/ses.d.cts.map +1 -1
- package/dist/transports/ses.d.mts +2 -0
- package/dist/transports/ses.d.mts.map +1 -1
- package/dist/transports/ses.mjs +4 -0
- package/dist/transports/ses.mjs.map +1 -1
- package/dist/transports/sigv4.cjs +6 -1
- package/dist/transports/sigv4.cjs.map +1 -1
- package/dist/transports/sigv4.d.cts.map +1 -1
- package/dist/transports/sigv4.d.mts.map +1 -1
- package/dist/transports/sigv4.mjs +6 -1
- package/dist/transports/sigv4.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bindings.mjs","names":[],"sources":["../../src/cloudflare/bindings.ts"],"sourcesContent":["import type { EmailAddress } from '../email';\nimport type { ExecutionStart } from '../store/index';\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\nexport interface KVNamespaceLike {\n get(key: string): Promise<string | null>;\n put(key: string, value: string): Promise<void>;\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\nexport interface WorkflowInstanceLike {\n id: string;\n sendEvent(event: { type: string; payload?: unknown }): Promise<void>;\n /** Stop the instance for good (Cloudflare Workflows' `terminate`). */\n terminate?(): Promise<void>;\n /** Where it stands (`complete`, `errored`, `terminated`, `running`, …). */\n status?(): Promise<{ status: string }>;\n}\n\n/**\n * How the router starts, wakes and stops instances: Cloudflare's workflow binding, or the AWS\n * adapter's equivalent over durable executions.\n */\nexport interface WorkflowBindingLike {\n create(options: { id: string; params: unknown }): Promise<WorkflowInstanceLike>;\n get(id: string): Promise<WorkflowInstanceLike>;\n /**\n * For a runtime whose start is a submit that can be lost in flight (AWS): the start to record\n * with the entry, in one transaction. A failed `create` then keeps the entry — its recorded\n * start is retried by the next ingest for the pair (`resume`) or by maintenance, never rolled\n * back and re-entered as a second execution.\n */\n plan?(options: {
|
|
1
|
+
{"version":3,"file":"bindings.mjs","names":[],"sources":["../../src/cloudflare/bindings.ts"],"sourcesContent":["import type { EmailAddress } from '../email';\nimport type { ExecutionStart } from '../store/index';\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\nexport interface KVNamespaceLike {\n get(key: string): Promise<string | null>;\n put(key: string, value: string): Promise<void>;\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\nexport interface WorkflowInstanceLike {\n id: string;\n sendEvent(event: { type: string; payload?: unknown }): Promise<void>;\n /** Stop the instance for good (Cloudflare Workflows' `terminate`). */\n terminate?(): Promise<void>;\n /** Where it stands (`complete`, `errored`, `terminated`, `running`, …). */\n status?(): Promise<{ status: string }>;\n}\n\n/**\n * How the router starts, wakes and stops instances: Cloudflare's workflow binding, or the AWS\n * adapter's equivalent over durable executions.\n */\nexport interface WorkflowBindingLike {\n create(options: { id: string; params: unknown }): Promise<WorkflowInstanceLike>;\n get(id: string): Promise<WorkflowInstanceLike>;\n /**\n * For a runtime whose start is a submit that can be lost in flight (AWS): the start to record\n * with the entry, in one transaction. A failed `create` then keeps the entry — its recorded\n * start is retried by the next ingest for the pair (`resume`) or by maintenance, never rolled\n * back and re-entered as a second execution.\n */\n plan?(options: {\n id: string;\n params: JourneyParams;\n ts: number;\n /** The code version whose routes named `params.contentHash` (absent after a forced deploy). */\n version?: string | undefined;\n }): Promise<ExecutionStart>;\n /** Retry the recorded, unconfirmed start of the (workflow, user) entry still running, if any. */\n resume?(entry: { workflow: string; userId: string }): Promise<void>;\n /**\n * Maintenance the sweep runs first: old unconfirmed starts retried (or given up on), and\n * executions the platform ended on its own (timeout, stop, crash) reconciled into the ledger.\n */\n maintain?(): Promise<void>;\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 /** `${instanceId}:${nodeId}`: stable across replays and retries — drop a send whose key was already delivered. */\n idempotencyKey: string;\n }): Promise<unknown>;\n}\n\n/** Every binding the journey engine needs (names match the wrangler config). */\nexport interface JourneyEnv {\n /**\n * BundleIR storage for the default D1 store (`wf:${contentHash}` → WorkflowIR JSON). Optional:\n * a Worker on another JourneyStore (Postgres, say) does not declare it.\n */\n WORKFLOW_KV?: KVNamespaceLike;\n /** Tables of the default D1 store (events / profiles / segments / triggers / entries / subscriptions). Optional, as above. */\n DB?: D1DatabaseLike;\n /** JourneyRunner's workflow binding (creating instances and waking them). */\n JOURNEY: WorkflowBindingLike;\n /** Optional message-delivery webhook (defaults to the console logging sender). */\n MESSAGE_WEBHOOK_URL?: string;\n /**\n * Bearer token required on every mutating HTTP endpoint (`Authorization:\n * Bearer <token>`). Unset = open — acceptable only for local dev; /deploy in\n * particular rewrites the whole routing table, so production must set this.\n */\n API_TOKEN?: string;\n /**\n * This Worker's public origin. With a store that keeps reporting (Postgres), journey emails'\n * links and open pixel point at its `/t/...` routes; without it emails go out untracked.\n */\n WORKFLOW_ORIGIN?: string;\n /** Which `workflow.config.ts` entry applies (`development`, `production`, …); production when unset. */\n ENVIRONMENT?: string;\n /** Cloudflare Email Sending, when the config has `emails`. */\n EMAIL?: EmailBindingLike;\n /** The APNs auth key (`.p8`, base64) named by the config's `apns.keyId`. */\n APNS_PRIVATE_KEY_BASE64?: string;\n /** The Firebase service account (base64 JSON) the config's `fcm` sends with. */\n GOOGLE_APPLICATION_CREDENTIALS_BASE64?: string;\n /** The VAPID key pair web pushes are signed with (`generateVapidKeys`); the config's `webPush` turns the channel on. */\n VAPID_PUBLIC_KEY?: string;\n VAPID_PRIVATE_KEY?: string;\n /**\n * Cloudflare's version metadata binding (wrangler `\"version_metadata\": { \"binding\":\n * \"CF_VERSION_METADATA\" }`). With it, and a store that records the live version (Postgres),\n * journeyWorker makes the bundle compiled into each new version live by itself: `wrangler\n * deploy` is the whole deploy.\n */\n CF_VERSION_METADATA?: { id: string; tag?: string; timestamp: string };\n /** Development only: the HTTP/2 relay `wrangler dev` sends APNs requests through (see `@shware/workflow/apns-relay`). */\n APNS_RELAY_ORIGIN?: string;\n}\n\n/** The runtime does not know the instance: the only failure that lets the router drop its wakes. */\nexport class InstanceNotFoundError extends Error {\n override readonly name = 'InstanceNotFoundError';\n constructor(readonly instanceId: string) {\n super(`no such instance: ${instanceId}`);\n }\n}\n\n/**\n * Does this lookup failure say the instance is gone, rather than that the lookup failed?\n * Cloudflare reports an unknown id as `instance.not_found`.\n */\nexport function isInstanceNotFound(error: unknown): boolean {\n if (error instanceof InstanceNotFoundError) return true;\n return /not[_ ]found|no such instance/i.test(String(error));\n}\n\n/** The event type the router uses to wake waiting instances (sendEvent's `type`). */\nexport const WAKE_EVENT_TYPE = 'wake';\n\n/** Journey instance parameters, passed by the ingest router when it creates one. */\nexport interface JourneyParams {\n workflowId: string;\n /** The IR version pinned at entry. */\n contentHash: string;\n userId: string;\n trigger: { event: string; payload: Record<string, unknown> };\n [key: string]: unknown;\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. The\n * adapter used to read them all as timeouts, which made an infrastructure\n * fault indistinguishable from \"the user never did it\": the journey took its\n * onTimeout branch, early and silently. Only the clock can tell them apart.\n */\nexport function wakeExpired(startedAt: number, timeoutMs: number, now: number): boolean {\n return now - startedAt >= Math.max(0, timeoutMs - WAKE_TIMEOUT_TOLERANCE_MS);\n}\n"],"mappings":";;AA8IA,IAAa,wBAAb,cAA2C,MAAM;CAE1B;CADrB,OAAyB;CACzB,YAAY,YAA6B;EACvC,MAAM,qBAAqB,YAAY;EADpB,KAAA,aAAA;CAErB;AACF;;;;;AAMA,SAAgB,mBAAmB,OAAyB;CAC1D,IAAI,iBAAiB,uBAAuB,OAAO;CACnD,OAAO,iCAAiC,KAAK,OAAO,KAAK,CAAC;AAC5D;;AAGA,MAAa,kBAAkB;;;;;;AAiB/B,MAAa,4BAA4B;;;;;;;;;;AAWzC,SAAgB,YAAY,WAAmB,WAAmB,KAAsB;CACtF,OAAO,MAAM,aAAa,KAAK,IAAI,GAAG,YAAY,yBAAyB;AAC7E"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_runtime = require("../runtime.cjs");
|
|
3
|
+
const require_engine_retry = require("../engine/retry.cjs");
|
|
3
4
|
const require_cloudflare_bindings = require("./bindings.cjs");
|
|
4
5
|
const require_cloudflare_d1_env = require("./d1-env.cjs");
|
|
5
6
|
const require_host_execute = require("../host/execute.cjs");
|
|
@@ -15,8 +16,6 @@ let cloudflare_workflows = require("cloudflare:workflows");
|
|
|
15
16
|
* waitForEvent, which signals a timeout by throwing — and so does every other
|
|
16
17
|
* failure in there, so only the clock can tell them apart (see wakeExpired).
|
|
17
18
|
*/
|
|
18
|
-
/** Refusals a retry would only repeat: a template's props, an emitted event's payload. */
|
|
19
|
-
const NON_RETRYABLE = /* @__PURE__ */ new Set(["TemplateValidationError", "ContractValidationError"]);
|
|
20
19
|
var CfEngineStep = class {
|
|
21
20
|
step;
|
|
22
21
|
store;
|
|
@@ -29,8 +28,8 @@ var CfEngineStep = class {
|
|
|
29
28
|
this.instanceId = instanceId;
|
|
30
29
|
}
|
|
31
30
|
/**
|
|
32
|
-
* A
|
|
33
|
-
*
|
|
31
|
+
* A failure a retry would only repeat (`isRetryable`: a template refusing its props, the
|
|
32
|
+
* contract refusing an emitted event, a provider rejecting the request) ends the step at
|
|
34
33
|
* once instead of burning the retry budget.
|
|
35
34
|
*/
|
|
36
35
|
do(name, fn) {
|
|
@@ -38,7 +37,7 @@ var CfEngineStep = class {
|
|
|
38
37
|
try {
|
|
39
38
|
return await fn();
|
|
40
39
|
} catch (error) {
|
|
41
|
-
if (error instanceof Error &&
|
|
40
|
+
if (error instanceof Error && !require_engine_retry.isRetryable(error)) throw new cloudflare_workflows.NonRetryableError(error.message, error.name);
|
|
42
41
|
throw error;
|
|
43
42
|
}
|
|
44
43
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.cjs","names":["NonRetryableError","WAKE_EVENT_TYPE","wakeExpired","WorkflowEntrypoint","d1Store","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 { type JourneyRunParts, executeJourney } from '../host/execute';\nimport { LogMessageSender, WebhookMessageSender } from '../host/senders';\nimport { type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport type { JourneyStore } from '../store/index';\nimport { type JourneyEnv, type JourneyParams, WAKE_EVENT_TYPE, 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 can tell them apart (see wakeExpired).\n */\n/** Refusals a retry would only repeat: a template's props, an emitted event's payload. */\nconst NON_RETRYABLE = new Set(['TemplateValidationError', 'ContractValidationError']);\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 template refusing its props, or the contract refusing an emitted event,\n * is deterministic — a retry hits the same refusal — so it ends the step at\n * once instead of burning the retry budget.\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 && NON_RETRYABLE.has(error.name)) {\n throw new NonRetryableError(error.message, error.name);\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 sleepUntil(name: string, timestampMs: number): Promise<void> {\n return this.step.sleepUntil(name, timestampMs);\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(name: string, timeoutMs: number): Promise<'event' | 'timeout'> {\n const startedAt = Date.now();\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\n * wakeExpired). `startedAt` is not replay-stable, but it is read only\n * inside the step body below, and a step body runs on the live attempt\n * alone — a replay returns the persisted verdict without touching the\n * clock.\n */\n const timedOut = await this.step.do(`${name}:timed-out`, async () =>\n wakeExpired(startedAt, timeoutMs, 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 + KV 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 /**\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.env,\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","d1Store","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 { LogMessageSender, WebhookMessageSender } from '../host/senders';\nimport { type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport type { JourneyStore } from '../store/index';\nimport { type JourneyEnv, type JourneyParams, WAKE_EVENT_TYPE, 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 can 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.\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 throw new NonRetryableError(error.message, error.name);\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 sleepUntil(name: string, timestampMs: number): Promise<void> {\n return this.step.sleepUntil(name, timestampMs);\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(name: string, timeoutMs: number): Promise<'event' | 'timeout'> {\n const startedAt = Date.now();\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\n * wakeExpired). `startedAt` is not replay-stable, but it is read only\n * inside the step body below, and a step body runs on the live attempt\n * alone — a replay returns the persisted verdict without touching the\n * clock.\n */\n const timedOut = await this.step.do(`${name}:timed-out`, async () =>\n wakeExpired(startedAt, timeoutMs, 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 + KV 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 /**\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.env,\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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.cts","names":[],"sources":["../../src/cloudflare/runner.ts"],"mappings":";;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"runner.d.cts","names":[],"sources":["../../src/cloudflare/runner.ts"],"mappings":";;;;;;;;;;;;;;;qBAqGa,sBAAsB,mBAAmB,YAAY;;;;;;;YAOtD,eAAe;;YAKT,WAAW,QAAQ,eAAe;;;;;;;;;;;;YAaxC,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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.mts","names":[],"sources":["../../src/cloudflare/runner.ts"],"mappings":";;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"runner.d.mts","names":[],"sources":["../../src/cloudflare/runner.ts"],"mappings":";;;;;;;;;;;;;;;qBAqGa,sBAAsB,mBAAmB,YAAY;;;;;;;YAOtD,eAAe;;YAKT,WAAW,QAAQ,eAAe;;;;;;;;;;;;YAaxC,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,4 +1,5 @@
|
|
|
1
1
|
import { resolveRuntime } from "../runtime.mjs";
|
|
2
|
+
import { isRetryable } from "../engine/retry.mjs";
|
|
2
3
|
import { WAKE_EVENT_TYPE, wakeExpired } from "./bindings.mjs";
|
|
3
4
|
import { d1Store } from "./d1-env.mjs";
|
|
4
5
|
import { executeJourney } from "../host/execute.mjs";
|
|
@@ -14,8 +15,6 @@ import { NonRetryableError } from "cloudflare:workflows";
|
|
|
14
15
|
* waitForEvent, which signals a timeout by throwing — and so does every other
|
|
15
16
|
* failure in there, so only the clock can tell them apart (see wakeExpired).
|
|
16
17
|
*/
|
|
17
|
-
/** Refusals a retry would only repeat: a template's props, an emitted event's payload. */
|
|
18
|
-
const NON_RETRYABLE = /* @__PURE__ */ new Set(["TemplateValidationError", "ContractValidationError"]);
|
|
19
18
|
var CfEngineStep = class {
|
|
20
19
|
step;
|
|
21
20
|
store;
|
|
@@ -28,8 +27,8 @@ var CfEngineStep = class {
|
|
|
28
27
|
this.instanceId = instanceId;
|
|
29
28
|
}
|
|
30
29
|
/**
|
|
31
|
-
* A
|
|
32
|
-
*
|
|
30
|
+
* A failure a retry would only repeat (`isRetryable`: a template refusing its props, the
|
|
31
|
+
* contract refusing an emitted event, a provider rejecting the request) ends the step at
|
|
33
32
|
* once instead of burning the retry budget.
|
|
34
33
|
*/
|
|
35
34
|
do(name, fn) {
|
|
@@ -37,7 +36,7 @@ var CfEngineStep = class {
|
|
|
37
36
|
try {
|
|
38
37
|
return await fn();
|
|
39
38
|
} catch (error) {
|
|
40
|
-
if (error instanceof Error &&
|
|
39
|
+
if (error instanceof Error && !isRetryable(error)) throw new NonRetryableError(error.message, error.name);
|
|
41
40
|
throw error;
|
|
42
41
|
}
|
|
43
42
|
});
|
|
@@ -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 { type JourneyRunParts, executeJourney } from '../host/execute';\nimport { LogMessageSender, WebhookMessageSender } from '../host/senders';\nimport { type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport type { JourneyStore } from '../store/index';\nimport { type JourneyEnv, type JourneyParams, WAKE_EVENT_TYPE, 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 can tell them apart (see wakeExpired).\n */\n/** Refusals a retry would only repeat: a template's props, an emitted event's payload. */\nconst NON_RETRYABLE = new Set(['TemplateValidationError', 'ContractValidationError']);\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 template refusing its props, or the contract refusing an emitted event,\n * is deterministic — a retry hits the same refusal — so it ends the step at\n * once instead of burning the retry budget.\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 && NON_RETRYABLE.has(error.name)) {\n throw new NonRetryableError(error.message, error.name);\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 sleepUntil(name: string, timestampMs: number): Promise<void> {\n return this.step.sleepUntil(name, timestampMs);\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(name: string, timeoutMs: number): Promise<'event' | 'timeout'> {\n const startedAt = Date.now();\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\n * wakeExpired). `startedAt` is not replay-stable, but it is read only\n * inside the step body below, and a step body runs on the live attempt\n * alone — a replay returns the persisted verdict without touching the\n * clock.\n */\n const timedOut = await this.step.do(`${name}:timed-out`, async () =>\n wakeExpired(startedAt, timeoutMs, 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 + KV 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 /**\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.env,\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":";;;;;;;;;;;;;;;;;AA2BA,MAAM,gCAAgB,IAAI,IAAI,CAAC,2BAA2B,yBAAyB,CAAC;AAEpF,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;;;;;;CAOH,GAAM,MAAc,IAAkC;EACpD,OAAO,KAAK,KAAK,GAAG,MAAM,YAAY;GACpC,IAAI;IACF,OAAO,MAAM,GAAG;GAClB,SAAS,OAAO;IACd,IAAI,iBAAiB,SAAS,cAAc,IAAI,MAAM,IAAI,GACxD,MAAM,IAAI,kBAAkB,MAAM,SAAS,MAAM,IAAI;IAEvD,MAAM;GACR;EACF,CAAC;CACH;CAEA,MAAM,MAAc,IAA2B;EAC7C,OAAO,KAAK,KAAK,MAAM,MAAM,EAAE;CACjC;CAEA,WAAW,MAAc,aAAoC;EAC3D,OAAO,KAAK,KAAK,WAAW,MAAM,WAAW;CAC/C;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,YAAY,MAAc,WAAiD;EAC/E,MAAM,YAAY,KAAK,IAAI;EAC3B,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,WAAW,WAAW,KAAK,IAAI,CAAC,CAC9C,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;;;;;;;;;;;;CAajE,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;GACV,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 { LogMessageSender, WebhookMessageSender } from '../host/senders';\nimport { type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport type { JourneyStore } from '../store/index';\nimport { type JourneyEnv, type JourneyParams, WAKE_EVENT_TYPE, 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 can 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.\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 throw new NonRetryableError(error.message, error.name);\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 sleepUntil(name: string, timestampMs: number): Promise<void> {\n return this.step.sleepUntil(name, timestampMs);\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(name: string, timeoutMs: number): Promise<'event' | 'timeout'> {\n const startedAt = Date.now();\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\n * wakeExpired). `startedAt` is not replay-stable, but it is read only\n * inside the step body below, and a step body runs on the live attempt\n * alone — a replay returns the persisted verdict without touching the\n * clock.\n */\n const timedOut = await this.step.do(`${name}:timed-out`, async () =>\n wakeExpired(startedAt, timeoutMs, 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 + KV 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 /**\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.env,\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":";;;;;;;;;;;;;;;;;AA2BA,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;;;;;;CAOH,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,GAC9C,MAAM,IAAI,kBAAkB,MAAM,SAAS,MAAM,IAAI;IAEvD,MAAM;GACR;EACF,CAAC;CACH;CAEA,MAAM,MAAc,IAA2B;EAC7C,OAAO,KAAK,KAAK,MAAM,MAAM,EAAE;CACjC;CAEA,WAAW,MAAc,aAAoC;EAC3D,OAAO,KAAK,KAAK,WAAW,MAAM,WAAW;CAC/C;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,YAAY,MAAc,WAAiD;EAC/E,MAAM,YAAY,KAAK,IAAI;EAC3B,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,WAAW,WAAW,KAAK,IAAI,CAAC,CAC9C,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;;;;;;;;;;;;CAajE,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;GACV,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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/engine/duration.ts
|
|
3
|
+
/**
|
|
4
|
+
* The longest an instance of a workflow can live, from its IR: every delay at its longest, every
|
|
5
|
+
* wait to its timeout, a time window as a full week (the furthest the next allowed slot can be),
|
|
6
|
+
* the longest arm of a branch or cohort — and at least the goal's window, which keeps an instance
|
|
7
|
+
* watching after its last step. Message, action and event steps count as instant. A runtime that
|
|
8
|
+
* caps an instance's lifetime (a Lambda durable execution's `ExecutionTimeout`) checks its
|
|
9
|
+
* workflows against it, rather than finding out when the platform stops one midway.
|
|
10
|
+
*/
|
|
11
|
+
const WEEK_MS = 6048e5;
|
|
12
|
+
function flowMs(nodes) {
|
|
13
|
+
let total = 0;
|
|
14
|
+
for (const node of nodes) total += nodeMs(node);
|
|
15
|
+
return total;
|
|
16
|
+
}
|
|
17
|
+
function nodeMs(node) {
|
|
18
|
+
switch (node.type) {
|
|
19
|
+
case "delay": return node.duration.ms;
|
|
20
|
+
case "random_delay": return Math.max(node.min.ms, node.max.ms);
|
|
21
|
+
case "time_window": return WEEK_MS;
|
|
22
|
+
case "wait_until": return node.timeout.ms + (Array.isArray(node.onTimeout) ? flowMs(node.onTimeout) : 0);
|
|
23
|
+
case "branch": return Math.max(flowMs(node.otherwise ?? []), ...node.cases.map((branch) => flowMs(branch.flow)));
|
|
24
|
+
case "cohort": return Math.max(0, ...node.arms.map((arm) => flowMs(arm.flow)));
|
|
25
|
+
default: return 0;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function longestJourneyMs(ir) {
|
|
29
|
+
return Math.max(flowMs(ir.flow), ir.goal?.within?.ms ?? 0);
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
exports.longestJourneyMs = longestJourneyMs;
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=duration.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duration.cjs","names":[],"sources":["../../src/engine/duration.ts"],"sourcesContent":["import type { NodeIR, WorkflowIR } from '../ir';\n\n/**\n * The longest an instance of a workflow can live, from its IR: every delay at its longest, every\n * wait to its timeout, a time window as a full week (the furthest the next allowed slot can be),\n * the longest arm of a branch or cohort — and at least the goal's window, which keeps an instance\n * watching after its last step. Message, action and event steps count as instant. A runtime that\n * caps an instance's lifetime (a Lambda durable execution's `ExecutionTimeout`) checks its\n * workflows against it, rather than finding out when the platform stops one midway.\n */\n\nconst WEEK_MS = 7 * 24 * 3_600_000;\n\nfunction flowMs(nodes: readonly NodeIR[]): number {\n let total = 0;\n for (const node of nodes) total += nodeMs(node);\n return total;\n}\n\nfunction nodeMs(node: NodeIR): number {\n switch (node.type) {\n case 'delay':\n return node.duration.ms;\n case 'random_delay':\n return Math.max(node.min.ms, node.max.ms);\n case 'time_window':\n return WEEK_MS;\n case 'wait_until':\n return node.timeout.ms + (Array.isArray(node.onTimeout) ? flowMs(node.onTimeout) : 0);\n case 'branch':\n return Math.max(\n flowMs(node.otherwise ?? []),\n ...node.cases.map((branch) => flowMs(branch.flow))\n );\n case 'cohort':\n return Math.max(0, ...node.arms.map((arm) => flowMs(arm.flow)));\n default:\n return 0;\n }\n}\n\nexport function longestJourneyMs(ir: Pick<WorkflowIR, 'flow' | 'goal'>): number {\n return Math.max(flowMs(ir.flow), ir.goal?.within?.ms ?? 0);\n}\n"],"mappings":";;;;;;;;;;AAWA,MAAM,UAAU;AAEhB,SAAS,OAAO,OAAkC;CAChD,IAAI,QAAQ;CACZ,KAAK,MAAM,QAAQ,OAAO,SAAS,OAAO,IAAI;CAC9C,OAAO;AACT;AAEA,SAAS,OAAO,MAAsB;CACpC,QAAQ,KAAK,MAAb;EACE,KAAK,SACH,OAAO,KAAK,SAAS;EACvB,KAAK,gBACH,OAAO,KAAK,IAAI,KAAK,IAAI,IAAI,KAAK,IAAI,EAAE;EAC1C,KAAK,eACH,OAAO;EACT,KAAK,cACH,OAAO,KAAK,QAAQ,MAAM,MAAM,QAAQ,KAAK,SAAS,IAAI,OAAO,KAAK,SAAS,IAAI;EACrF,KAAK,UACH,OAAO,KAAK,IACV,OAAO,KAAK,aAAa,CAAC,CAAC,GAC3B,GAAG,KAAK,MAAM,KAAK,WAAW,OAAO,OAAO,IAAI,CAAC,CACnD;EACF,KAAK,UACH,OAAO,KAAK,IAAI,GAAG,GAAG,KAAK,KAAK,KAAK,QAAQ,OAAO,IAAI,IAAI,CAAC,CAAC;EAChE,SACE,OAAO;CACX;AACF;AAEA,SAAgB,iBAAiB,IAA+C;CAC9E,OAAO,KAAK,IAAI,OAAO,GAAG,IAAI,GAAG,GAAG,MAAM,QAAQ,MAAM,CAAC;AAC3D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duration.d.cts","names":[],"sources":["../../src/engine/duration.ts"],"mappings":";;wBAyCgB,iBAAiB,IAAI,KAAK"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duration.d.mts","names":[],"sources":["../../src/engine/duration.ts"],"mappings":";;wBAyCgB,iBAAiB,IAAI,KAAK"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/engine/duration.ts
|
|
2
|
+
/**
|
|
3
|
+
* The longest an instance of a workflow can live, from its IR: every delay at its longest, every
|
|
4
|
+
* wait to its timeout, a time window as a full week (the furthest the next allowed slot can be),
|
|
5
|
+
* the longest arm of a branch or cohort — and at least the goal's window, which keeps an instance
|
|
6
|
+
* watching after its last step. Message, action and event steps count as instant. A runtime that
|
|
7
|
+
* caps an instance's lifetime (a Lambda durable execution's `ExecutionTimeout`) checks its
|
|
8
|
+
* workflows against it, rather than finding out when the platform stops one midway.
|
|
9
|
+
*/
|
|
10
|
+
const WEEK_MS = 6048e5;
|
|
11
|
+
function flowMs(nodes) {
|
|
12
|
+
let total = 0;
|
|
13
|
+
for (const node of nodes) total += nodeMs(node);
|
|
14
|
+
return total;
|
|
15
|
+
}
|
|
16
|
+
function nodeMs(node) {
|
|
17
|
+
switch (node.type) {
|
|
18
|
+
case "delay": return node.duration.ms;
|
|
19
|
+
case "random_delay": return Math.max(node.min.ms, node.max.ms);
|
|
20
|
+
case "time_window": return WEEK_MS;
|
|
21
|
+
case "wait_until": return node.timeout.ms + (Array.isArray(node.onTimeout) ? flowMs(node.onTimeout) : 0);
|
|
22
|
+
case "branch": return Math.max(flowMs(node.otherwise ?? []), ...node.cases.map((branch) => flowMs(branch.flow)));
|
|
23
|
+
case "cohort": return Math.max(0, ...node.arms.map((arm) => flowMs(arm.flow)));
|
|
24
|
+
default: return 0;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function longestJourneyMs(ir) {
|
|
28
|
+
return Math.max(flowMs(ir.flow), ir.goal?.within?.ms ?? 0);
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { longestJourneyMs };
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=duration.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duration.mjs","names":[],"sources":["../../src/engine/duration.ts"],"sourcesContent":["import type { NodeIR, WorkflowIR } from '../ir';\n\n/**\n * The longest an instance of a workflow can live, from its IR: every delay at its longest, every\n * wait to its timeout, a time window as a full week (the furthest the next allowed slot can be),\n * the longest arm of a branch or cohort — and at least the goal's window, which keeps an instance\n * watching after its last step. Message, action and event steps count as instant. A runtime that\n * caps an instance's lifetime (a Lambda durable execution's `ExecutionTimeout`) checks its\n * workflows against it, rather than finding out when the platform stops one midway.\n */\n\nconst WEEK_MS = 7 * 24 * 3_600_000;\n\nfunction flowMs(nodes: readonly NodeIR[]): number {\n let total = 0;\n for (const node of nodes) total += nodeMs(node);\n return total;\n}\n\nfunction nodeMs(node: NodeIR): number {\n switch (node.type) {\n case 'delay':\n return node.duration.ms;\n case 'random_delay':\n return Math.max(node.min.ms, node.max.ms);\n case 'time_window':\n return WEEK_MS;\n case 'wait_until':\n return node.timeout.ms + (Array.isArray(node.onTimeout) ? flowMs(node.onTimeout) : 0);\n case 'branch':\n return Math.max(\n flowMs(node.otherwise ?? []),\n ...node.cases.map((branch) => flowMs(branch.flow))\n );\n case 'cohort':\n return Math.max(0, ...node.arms.map((arm) => flowMs(arm.flow)));\n default:\n return 0;\n }\n}\n\nexport function longestJourneyMs(ir: Pick<WorkflowIR, 'flow' | 'goal'>): number {\n return Math.max(flowMs(ir.flow), ir.goal?.within?.ms ?? 0);\n}\n"],"mappings":";;;;;;;;;AAWA,MAAM,UAAU;AAEhB,SAAS,OAAO,OAAkC;CAChD,IAAI,QAAQ;CACZ,KAAK,MAAM,QAAQ,OAAO,SAAS,OAAO,IAAI;CAC9C,OAAO;AACT;AAEA,SAAS,OAAO,MAAsB;CACpC,QAAQ,KAAK,MAAb;EACE,KAAK,SACH,OAAO,KAAK,SAAS;EACvB,KAAK,gBACH,OAAO,KAAK,IAAI,KAAK,IAAI,IAAI,KAAK,IAAI,EAAE;EAC1C,KAAK,eACH,OAAO;EACT,KAAK,cACH,OAAO,KAAK,QAAQ,MAAM,MAAM,QAAQ,KAAK,SAAS,IAAI,OAAO,KAAK,SAAS,IAAI;EACrF,KAAK,UACH,OAAO,KAAK,IACV,OAAO,KAAK,aAAa,CAAC,CAAC,GAC3B,GAAG,KAAK,MAAM,KAAK,WAAW,OAAO,OAAO,IAAI,CAAC,CACnD;EACF,KAAK,UACH,OAAO,KAAK,IAAI,GAAG,GAAG,KAAK,KAAK,KAAK,QAAQ,OAAO,IAAI,IAAI,CAAC,CAAC;EAChE,SACE,OAAO;CACX;AACF;AAEA,SAAgB,iBAAiB,IAA+C;CAC9E,OAAO,KAAK,IAAI,OAAO,GAAG,IAAI,GAAG,GAAG,MAAM,QAAQ,MAAM,CAAC;AAC3D"}
|
package/dist/engine/index.cjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_engine_retry = require("./retry.cjs");
|
|
3
|
+
const require_engine_duration = require("./duration.cjs");
|
|
2
4
|
const require_engine_bucket = require("./bucket.cjs");
|
|
3
5
|
const require_engine_condition = require("./condition.cjs");
|
|
4
6
|
const require_engine_time_window = require("./time-window.cjs");
|
|
@@ -10,6 +12,9 @@ exports.PROFILE_UPDATED_EVENT = require_engine_condition.PROFILE_UPDATED_EVENT;
|
|
|
10
12
|
exports.RegistryActionInvoker = require_engine_actions.RegistryActionInvoker;
|
|
11
13
|
exports.evaluateCondition = require_engine_condition.evaluateCondition;
|
|
12
14
|
exports.hashToBucket = require_engine_bucket.hashToBucket;
|
|
15
|
+
exports.isRetryable = require_engine_retry.isRetryable;
|
|
16
|
+
exports.isRetryableStatus = require_engine_retry.isRetryableStatus;
|
|
17
|
+
exports.longestJourneyMs = require_engine_duration.longestJourneyMs;
|
|
13
18
|
exports.matchesWhere = require_engine_condition.matchesWhere;
|
|
14
19
|
exports.murmur3 = require_engine_bucket.murmur3;
|
|
15
20
|
exports.nextWindowStart = require_engine_time_window.nextWindowStart;
|
package/dist/engine/index.d.cts
CHANGED
|
@@ -5,4 +5,6 @@ import { DEVICES_UPDATED_EVENT, EvaluateOptions, PROFILE_UPDATED_EVENT, evaluate
|
|
|
5
5
|
import { runJourney } from "./interpreter.cjs";
|
|
6
6
|
import { scaleDurations, scaleSegmentWindows } from "./time-scale.cjs";
|
|
7
7
|
import { nextWindowStart, resolveTimeZone } from "./time-window.cjs";
|
|
8
|
-
|
|
8
|
+
import { longestJourneyMs } from "./duration.cjs";
|
|
9
|
+
import { isRetryable, isRetryableStatus } from "./retry.cjs";
|
|
10
|
+
export { type ActionInvocation, type ActionInvoker, type ActionVersionPolicy, DEVICES_UPDATED_EVENT, type EngineStep, type EvaluateOptions, type EventSink, type FactSource, type JourneyContext, type JourneyObserver, type JourneyOutcome, type LinkPurpose, type MessageSender, type MessageTracking, type OutboundMessage, PROFILE_UPDATED_EVENT, type PushDevice, type RegisteredAction, RegistryActionInvoker, type SendResult, evaluateCondition, hashToBucket, isRetryable, isRetryableStatus, longestJourneyMs, matchesWhere, murmur3, nextWindowStart, relevantEvents, resolveTimeZone, runJourney, scaleDurations, scaleSegmentWindows };
|
package/dist/engine/index.d.mts
CHANGED
|
@@ -5,4 +5,6 @@ import { DEVICES_UPDATED_EVENT, EvaluateOptions, PROFILE_UPDATED_EVENT, evaluate
|
|
|
5
5
|
import { runJourney } from "./interpreter.mjs";
|
|
6
6
|
import { scaleDurations, scaleSegmentWindows } from "./time-scale.mjs";
|
|
7
7
|
import { nextWindowStart, resolveTimeZone } from "./time-window.mjs";
|
|
8
|
-
|
|
8
|
+
import { longestJourneyMs } from "./duration.mjs";
|
|
9
|
+
import { isRetryable, isRetryableStatus } from "./retry.mjs";
|
|
10
|
+
export { type ActionInvocation, type ActionInvoker, type ActionVersionPolicy, DEVICES_UPDATED_EVENT, type EngineStep, type EvaluateOptions, type EventSink, type FactSource, type JourneyContext, type JourneyObserver, type JourneyOutcome, type LinkPurpose, type MessageSender, type MessageTracking, type OutboundMessage, PROFILE_UPDATED_EVENT, type PushDevice, type RegisteredAction, RegistryActionInvoker, type SendResult, evaluateCondition, hashToBucket, isRetryable, isRetryableStatus, longestJourneyMs, matchesWhere, murmur3, nextWindowStart, relevantEvents, resolveTimeZone, runJourney, scaleDurations, scaleSegmentWindows };
|
package/dist/engine/index.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { isRetryable, isRetryableStatus } from "./retry.mjs";
|
|
2
|
+
import { longestJourneyMs } from "./duration.mjs";
|
|
1
3
|
import { hashToBucket, murmur3 } from "./bucket.mjs";
|
|
2
4
|
import { DEVICES_UPDATED_EVENT, PROFILE_UPDATED_EVENT, evaluateCondition, matchesWhere, relevantEvents } from "./condition.mjs";
|
|
3
5
|
import { nextWindowStart, resolveTimeZone } from "./time-window.mjs";
|
|
4
6
|
import { runJourney } from "./interpreter.mjs";
|
|
5
7
|
import { scaleDurations, scaleSegmentWindows } from "./time-scale.mjs";
|
|
6
8
|
import { RegistryActionInvoker } from "./actions.mjs";
|
|
7
|
-
export { DEVICES_UPDATED_EVENT, PROFILE_UPDATED_EVENT, RegistryActionInvoker, evaluateCondition, hashToBucket, matchesWhere, murmur3, nextWindowStart, relevantEvents, resolveTimeZone, runJourney, scaleDurations, scaleSegmentWindows };
|
|
9
|
+
export { DEVICES_UPDATED_EVENT, PROFILE_UPDATED_EVENT, RegistryActionInvoker, evaluateCondition, hashToBucket, isRetryable, isRetryableStatus, longestJourneyMs, matchesWhere, murmur3, nextWindowStart, relevantEvents, resolveTimeZone, runJourney, scaleDurations, scaleSegmentWindows };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/engine/retry.ts
|
|
3
|
+
/**
|
|
4
|
+
* Which step failures a retry would only repeat. A durable step retries what it throws; an
|
|
5
|
+
* error that is the same on every attempt — a template refusing its props, the contract
|
|
6
|
+
* refusing an emitted event, a provider rejecting the request itself — ends the step at once
|
|
7
|
+
* instead of burning the retry budget and delaying the journey. Every runtime adapter asks this
|
|
8
|
+
* one rule.
|
|
9
|
+
*/
|
|
10
|
+
/** Errors the engine raises that are deterministic by construction. */
|
|
11
|
+
const DETERMINISTIC = /* @__PURE__ */ new Set(["TemplateValidationError", "ContractValidationError"]);
|
|
12
|
+
/**
|
|
13
|
+
* False for an error a retry cannot fix: one of the engine's deterministic refusals, or any
|
|
14
|
+
* error that says so with `retryable: false` (the transports' 4xx answers).
|
|
15
|
+
*/
|
|
16
|
+
function isRetryable(error) {
|
|
17
|
+
if (!(error instanceof Error)) return true;
|
|
18
|
+
if (DETERMINISTIC.has(error.name)) return false;
|
|
19
|
+
return error.retryable !== false;
|
|
20
|
+
}
|
|
21
|
+
/** An HTTP answer a retry can change: a timeout, throttling, or the provider's own fault. */
|
|
22
|
+
function isRetryableStatus(status) {
|
|
23
|
+
return status === 408 || status === 429 || status >= 500;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
exports.isRetryable = isRetryable;
|
|
27
|
+
exports.isRetryableStatus = isRetryableStatus;
|
|
28
|
+
|
|
29
|
+
//# sourceMappingURL=retry.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.cjs","names":[],"sources":["../../src/engine/retry.ts"],"sourcesContent":["/**\n * Which step failures a retry would only repeat. A durable step retries what it throws; an\n * error that is the same on every attempt — a template refusing its props, the contract\n * refusing an emitted event, a provider rejecting the request itself — ends the step at once\n * instead of burning the retry budget and delaying the journey. Every runtime adapter asks this\n * one rule.\n */\n\n/** Errors the engine raises that are deterministic by construction. */\nconst DETERMINISTIC = new Set(['TemplateValidationError', 'ContractValidationError']);\n\n/**\n * False for an error a retry cannot fix: one of the engine's deterministic refusals, or any\n * error that says so with `retryable: false` (the transports' 4xx answers).\n */\nexport function isRetryable(error: unknown): boolean {\n if (!(error instanceof Error)) return true;\n if (DETERMINISTIC.has(error.name)) return false;\n return (error as { retryable?: unknown }).retryable !== false;\n}\n\n/** An HTTP answer a retry can change: a timeout, throttling, or the provider's own fault. */\nexport function isRetryableStatus(status: number): boolean {\n return status === 408 || status === 429 || status >= 500;\n}\n"],"mappings":";;;;;;;;;;AASA,MAAM,gCAAgB,IAAI,IAAI,CAAC,2BAA2B,yBAAyB,CAAC;;;;;AAMpF,SAAgB,YAAY,OAAyB;CACnD,IAAI,EAAE,iBAAiB,QAAQ,OAAO;CACtC,IAAI,cAAc,IAAI,MAAM,IAAI,GAAG,OAAO;CAC1C,OAAQ,MAAkC,cAAc;AAC1D;;AAGA,SAAgB,kBAAkB,QAAyB;CACzD,OAAO,WAAW,OAAO,WAAW,OAAO,UAAU;AACvD"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/engine/retry.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Which step failures a retry would only repeat. A durable step retries what it throws; an
|
|
4
|
+
* error that is the same on every attempt — a template refusing its props, the contract
|
|
5
|
+
* refusing an emitted event, a provider rejecting the request itself — ends the step at once
|
|
6
|
+
* instead of burning the retry budget and delaying the journey. Every runtime adapter asks this
|
|
7
|
+
* one rule.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* False for an error a retry cannot fix: one of the engine's deterministic refusals, or any
|
|
11
|
+
* error that says so with `retryable: false` (the transports' 4xx answers).
|
|
12
|
+
*/
|
|
13
|
+
export declare function isRetryable(error: unknown): boolean;
|
|
14
|
+
/** An HTTP answer a retry can change: a timeout, throttling, or the provider's own fault. */
|
|
15
|
+
export declare function isRetryableStatus(status: number): boolean;
|
|
16
|
+
//#endregion
|
|
17
|
+
//# sourceMappingURL=retry.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.d.cts","names":[],"sources":["../../src/engine/retry.ts"],"mappings":";;;;;;;;;;;;wBAegB,YAAY;;wBAOZ,kBAAkB"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/engine/retry.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Which step failures a retry would only repeat. A durable step retries what it throws; an
|
|
4
|
+
* error that is the same on every attempt — a template refusing its props, the contract
|
|
5
|
+
* refusing an emitted event, a provider rejecting the request itself — ends the step at once
|
|
6
|
+
* instead of burning the retry budget and delaying the journey. Every runtime adapter asks this
|
|
7
|
+
* one rule.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* False for an error a retry cannot fix: one of the engine's deterministic refusals, or any
|
|
11
|
+
* error that says so with `retryable: false` (the transports' 4xx answers).
|
|
12
|
+
*/
|
|
13
|
+
export declare function isRetryable(error: unknown): boolean;
|
|
14
|
+
/** An HTTP answer a retry can change: a timeout, throttling, or the provider's own fault. */
|
|
15
|
+
export declare function isRetryableStatus(status: number): boolean;
|
|
16
|
+
//#endregion
|
|
17
|
+
//# sourceMappingURL=retry.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.d.mts","names":[],"sources":["../../src/engine/retry.ts"],"mappings":";;;;;;;;;;;;wBAegB,YAAY;;wBAOZ,kBAAkB"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/engine/retry.ts
|
|
2
|
+
/**
|
|
3
|
+
* Which step failures a retry would only repeat. A durable step retries what it throws; an
|
|
4
|
+
* error that is the same on every attempt — a template refusing its props, the contract
|
|
5
|
+
* refusing an emitted event, a provider rejecting the request itself — ends the step at once
|
|
6
|
+
* instead of burning the retry budget and delaying the journey. Every runtime adapter asks this
|
|
7
|
+
* one rule.
|
|
8
|
+
*/
|
|
9
|
+
/** Errors the engine raises that are deterministic by construction. */
|
|
10
|
+
const DETERMINISTIC = /* @__PURE__ */ new Set(["TemplateValidationError", "ContractValidationError"]);
|
|
11
|
+
/**
|
|
12
|
+
* False for an error a retry cannot fix: one of the engine's deterministic refusals, or any
|
|
13
|
+
* error that says so with `retryable: false` (the transports' 4xx answers).
|
|
14
|
+
*/
|
|
15
|
+
function isRetryable(error) {
|
|
16
|
+
if (!(error instanceof Error)) return true;
|
|
17
|
+
if (DETERMINISTIC.has(error.name)) return false;
|
|
18
|
+
return error.retryable !== false;
|
|
19
|
+
}
|
|
20
|
+
/** An HTTP answer a retry can change: a timeout, throttling, or the provider's own fault. */
|
|
21
|
+
function isRetryableStatus(status) {
|
|
22
|
+
return status === 408 || status === 429 || status >= 500;
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { isRetryable, isRetryableStatus };
|
|
26
|
+
|
|
27
|
+
//# sourceMappingURL=retry.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.mjs","names":[],"sources":["../../src/engine/retry.ts"],"sourcesContent":["/**\n * Which step failures a retry would only repeat. A durable step retries what it throws; an\n * error that is the same on every attempt — a template refusing its props, the contract\n * refusing an emitted event, a provider rejecting the request itself — ends the step at once\n * instead of burning the retry budget and delaying the journey. Every runtime adapter asks this\n * one rule.\n */\n\n/** Errors the engine raises that are deterministic by construction. */\nconst DETERMINISTIC = new Set(['TemplateValidationError', 'ContractValidationError']);\n\n/**\n * False for an error a retry cannot fix: one of the engine's deterministic refusals, or any\n * error that says so with `retryable: false` (the transports' 4xx answers).\n */\nexport function isRetryable(error: unknown): boolean {\n if (!(error instanceof Error)) return true;\n if (DETERMINISTIC.has(error.name)) return false;\n return (error as { retryable?: unknown }).retryable !== false;\n}\n\n/** An HTTP answer a retry can change: a timeout, throttling, or the provider's own fault. */\nexport function isRetryableStatus(status: number): boolean {\n return status === 408 || status === 429 || status >= 500;\n}\n"],"mappings":";;;;;;;;;AASA,MAAM,gCAAgB,IAAI,IAAI,CAAC,2BAA2B,yBAAyB,CAAC;;;;;AAMpF,SAAgB,YAAY,OAAyB;CACnD,IAAI,EAAE,iBAAiB,QAAQ,OAAO;CACtC,IAAI,cAAc,IAAI,MAAM,IAAI,GAAG,OAAO;CAC1C,OAAQ,MAAkC,cAAc;AAC1D;;AAGA,SAAgB,kBAAkB,QAAyB;CACzD,OAAO,WAAW,OAAO,WAAW,OAAO,UAAU;AACvD"}
|
package/dist/host/execute.cjs
CHANGED
|
@@ -13,18 +13,11 @@ function logging(sender) {
|
|
|
13
13
|
return sender.send(message);
|
|
14
14
|
} };
|
|
15
15
|
}
|
|
16
|
-
/**
|
|
17
|
-
* The IR is read outside a step: content addressing makes it immutable, so the read is
|
|
18
|
-
* replay-safe.
|
|
19
|
-
*/
|
|
20
16
|
async function executeJourney(input, parts) {
|
|
21
17
|
const { instanceId, params, step } = input;
|
|
22
18
|
const { workflowId, contentHash, userId } = params;
|
|
23
19
|
const { store, env, observer, contract } = parts;
|
|
24
|
-
const stored = await store.getWorkflowIR(contentHash);
|
|
25
|
-
if (stored === void 0) throw new Error(`WorkflowIR not found: ${workflowId}@${contentHash}`);
|
|
26
20
|
const runtime = require_runtime.resolveRuntime({ current: parts.runtime }, "current");
|
|
27
|
-
const ir = require_engine_time_scale.scaleDurations(stored, runtime.timeScale);
|
|
28
21
|
const finished = (status, convertedAt) => observer?.finished?.({
|
|
29
22
|
workflow: workflowId,
|
|
30
23
|
instanceId,
|
|
@@ -50,6 +43,12 @@ async function executeJourney(input, parts) {
|
|
|
50
43
|
} };
|
|
51
44
|
let outcome;
|
|
52
45
|
try {
|
|
46
|
+
const stored = await step.do("$workflow:ir", async () => {
|
|
47
|
+
const found = await store.getWorkflowIR(contentHash);
|
|
48
|
+
if (found === void 0) throw new Error(`WorkflowIR not found: ${workflowId}@${contentHash}`);
|
|
49
|
+
return found;
|
|
50
|
+
});
|
|
51
|
+
const ir = require_engine_time_scale.scaleDurations(stored, runtime.timeScale);
|
|
53
52
|
outcome = await require_engine_interpreter.runJourney(ir, {
|
|
54
53
|
userId,
|
|
55
54
|
instanceId,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"execute.cjs","names":["resolveRuntime","
|
|
1
|
+
{"version":3,"file":"execute.cjs","names":["resolveRuntime","checkEvent","ContractValidationError","ingestEvent","scaleDurations","runJourney","JourneyFactSource"],"sources":["../../src/host/execute.ts"],"sourcesContent":["import type { JourneyEnv, JourneyParams } from '../cloudflare/bindings';\nimport { type Contract, ContractValidationError, checkEvent } from '../contract';\nimport { runJourney } from '../engine/interpreter';\nimport type {\n ActionInvoker,\n EngineStep,\n EventSink,\n JourneyObserver,\n JourneyOutcome,\n MessageSender,\n MessageValidator,\n} from '../engine/ports';\nimport { scaleDurations } from '../engine/time-scale';\nimport { type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport { JourneyFactSource } from '../store/facts';\nimport type { JourneyStore } from '../store/index';\nimport { ingestEvent } from './router';\n\n/**\n * One journey instance run to its end, on whatever durable runtime supplies the `EngineStep`:\n * the IR loaded by the hash pinned at entry, the outlets wired, the interpreter run, and the\n * ledger finalized. Cloudflare's runner and the Lambda handler both come through here; only\n * their EngineStep differs.\n */\n\n/** Everything a run is wired to, assembled by the host. */\nexport interface JourneyRunParts {\n store: JourneyStore;\n /**\n * The env the emitted events are ingested with: its `JOURNEY` starts and wakes instances on\n * this runtime.\n */\n env: JourneyEnv;\n runtime: JourneyRuntimeOptions;\n messages: MessageSender;\n actions: ActionInvoker;\n contract?: Contract | undefined;\n validator?: MessageValidator | undefined;\n observer?: JourneyObserver | undefined;\n}\n\nexport interface JourneyRunInput {\n instanceId: string;\n params: JourneyParams;\n /** When the trigger happened: replay-stable, never Date.now() at run time. */\n enteredAtMs: number;\n step: EngineStep;\n}\n\n/** Development aid: echo each outbound message before handing it to the real sender. */\nfunction logging(sender: MessageSender): MessageSender {\n return {\n async send(message) {\n console.log(\n `[journey] ${message.channel} ${message.template} → ${message.recipient ?? message.userId} ${JSON.stringify(message.props)}`\n );\n return sender.send(message);\n },\n };\n}\n\nexport async function executeJourney(\n input: JourneyRunInput,\n parts: JourneyRunParts\n): Promise<JourneyOutcome> {\n const { instanceId, params, step } = input;\n const { workflowId, contentHash, userId } = params;\n const { store, env, observer, contract } = parts;\n\n const runtime = resolveRuntime({ current: parts.runtime }, 'current');\n\n const finished = (status: JourneyOutcome['status'] | 'failed', convertedAt?: number) =>\n observer?.finished?.({\n workflow: workflowId,\n instanceId,\n userId,\n status,\n at: Date.now(),\n ...(convertedAt === undefined ? {} : { convertedAt }),\n });\n\n const messages = runtime.logMessages ? logging(parts.messages) : parts.messages;\n\n // send_event feeds straight back into the router logic — an in-process call\n // forming the event edge between workflows. The step's identity travels as\n // the event id, so a retried emit records the occurrence once.\n // An emitted event passes the same contract check as one arriving over HTTP.\n const events: EventSink = {\n emit: async (name, payload, id) => {\n let accepted: Record<string, unknown> = payload;\n if (contract !== undefined) {\n const verdict = checkEvent(contract, name, payload);\n if (!verdict.ok) throw new ContractValidationError(name, verdict.issues);\n accepted = verdict.data;\n }\n await ingestEvent(env, { id, userId, event: name, payload: accepted }, store);\n },\n };\n\n let outcome: JourneyOutcome;\n try {\n // In a step: a read that fails is retried like any other, and one that keeps failing ends\n // the instance through the failure path below rather than leaving its entry 'running'.\n // Content addressing makes the IR immutable, so the checkpointed copy is the one to replay\n const stored = await step.do('$workflow:ir', async () => {\n const found = await store.getWorkflowIR(contentHash);\n if (found === undefined)\n throw new Error(`WorkflowIR not found: ${workflowId}@${contentHash}`);\n return found;\n });\n // Development runs the journey in fast-forward; the pinned hash still names the version.\n const ir = scaleDurations(stored, runtime.timeScale);\n outcome = await runJourney(ir, {\n userId,\n instanceId,\n enteredAtMs: input.enteredAtMs,\n step,\n facts: new JourneyFactSource(store, userId),\n messages,\n events,\n actions: parts.actions,\n validator: parts.validator,\n observer,\n });\n } catch (error) {\n /*\n * An error escaping run() ends the instance, and the ledger is the only\n * place anyone looks afterwards: leaving it at 'running' meant a dead\n * journey stayed indistinguishable from a waiting one, in the dashboard\n * and in the once-policy, forever.\n *\n * Subscriptions are deliberately left in place. They are cleaned up on\n * every ordinary ending, but here the wake rows are the only evidence of\n * what the instance was waiting for, and dropping them would also break\n * the instance if the platform does re-run it.\n *\n * A paused instance never gets here: a durable wait suspends the run\n * without throwing through it.\n */\n await step.do('finalize:failed', async () => {\n await store.setEntryStatus(instanceId, 'failed');\n await finished('failed');\n });\n throw error;\n }\n\n await step.do('finalize', async () => {\n await store.setEntryStatus(instanceId, outcome.status);\n await store.unsubscribe(instanceId);\n await finished(outcome.status, outcome.convertedAt);\n });\n\n return outcome;\n}\n"],"mappings":";;;;;;;;;AAkDA,SAAS,QAAQ,QAAsC;CACrD,OAAO,EACL,MAAM,KAAK,SAAS;EAClB,QAAQ,IACN,aAAa,QAAQ,QAAQ,GAAG,QAAQ,SAAS,KAAK,QAAQ,aAAa,QAAQ,OAAO,GAAG,KAAK,UAAU,QAAQ,KAAK,GAC3H;EACA,OAAO,OAAO,KAAK,OAAO;CAC5B,EACF;AACF;AAEA,eAAsB,eACpB,OACA,OACyB;CACzB,MAAM,EAAE,YAAY,QAAQ,SAAS;CACrC,MAAM,EAAE,YAAY,aAAa,WAAW;CAC5C,MAAM,EAAE,OAAO,KAAK,UAAU,aAAa;CAE3C,MAAM,UAAUA,gBAAAA,eAAe,EAAE,SAAS,MAAM,QAAQ,GAAG,SAAS;CAEpE,MAAM,YAAY,QAA6C,gBAC7D,UAAU,WAAW;EACnB,UAAU;EACV;EACA;EACA;EACA,IAAI,KAAK,IAAI;EACb,GAAI,gBAAgB,KAAA,IAAY,CAAC,IAAI,EAAE,YAAY;CACrD,CAAC;CAEH,MAAM,WAAW,QAAQ,cAAc,QAAQ,MAAM,QAAQ,IAAI,MAAM;CAMvE,MAAM,SAAoB,EACxB,MAAM,OAAO,MAAM,SAAS,OAAO;EACjC,IAAI,WAAoC;EACxC,IAAI,aAAa,KAAA,GAAW;GAC1B,MAAM,UAAUC,iBAAAA,WAAW,UAAU,MAAM,OAAO;GAClD,IAAI,CAAC,QAAQ,IAAI,MAAM,IAAIC,iBAAAA,wBAAwB,MAAM,QAAQ,MAAM;GACvE,WAAW,QAAQ;EACrB;EACA,MAAMC,oBAAAA,YAAY,KAAK;GAAE;GAAI;GAAQ,OAAO;GAAM,SAAS;EAAS,GAAG,KAAK;CAC9E,EACF;CAEA,IAAI;CACJ,IAAI;EAIF,MAAM,SAAS,MAAM,KAAK,GAAG,gBAAgB,YAAY;GACvD,MAAM,QAAQ,MAAM,MAAM,cAAc,WAAW;GACnD,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MAAM,yBAAyB,WAAW,GAAG,aAAa;GACtE,OAAO;EACT,CAAC;EAED,MAAM,KAAKC,0BAAAA,eAAe,QAAQ,QAAQ,SAAS;EACnD,UAAU,MAAMC,2BAAAA,WAAW,IAAI;GAC7B;GACA;GACA,aAAa,MAAM;GACnB;GACA,OAAO,IAAIC,oBAAAA,kBAAkB,OAAO,MAAM;GAC1C;GACA;GACA,SAAS,MAAM;GACf,WAAW,MAAM;GACjB;EACF,CAAC;CACH,SAAS,OAAO;EAed,MAAM,KAAK,GAAG,mBAAmB,YAAY;GAC3C,MAAM,MAAM,eAAe,YAAY,QAAQ;GAC/C,MAAM,SAAS,QAAQ;EACzB,CAAC;EACD,MAAM;CACR;CAEA,MAAM,KAAK,GAAG,YAAY,YAAY;EACpC,MAAM,MAAM,eAAe,YAAY,QAAQ,MAAM;EACrD,MAAM,MAAM,YAAY,UAAU;EAClC,MAAM,SAAS,QAAQ,QAAQ,QAAQ,WAAW;CACpD,CAAC;CAED,OAAO;AACT"}
|
package/dist/host/execute.d.cts
CHANGED
|
@@ -32,10 +32,6 @@ export interface JourneyRunInput {
|
|
|
32
32
|
enteredAtMs: number;
|
|
33
33
|
step: EngineStep;
|
|
34
34
|
}
|
|
35
|
-
/**
|
|
36
|
-
* The IR is read outside a step: content addressing makes it immutable, so the read is
|
|
37
|
-
* replay-safe.
|
|
38
|
-
*/
|
|
39
35
|
export declare function executeJourney(input: JourneyRunInput, parts: JourneyRunParts): Promise<JourneyOutcome>;
|
|
40
36
|
//#endregion
|
|
41
37
|
//# sourceMappingURL=execute.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"execute.d.cts","names":[],"sources":["../../src/host/execute.ts"],"mappings":";;;;;;;;;;;;;iBA0BiB;EACf,OAAO;;;;;EAKP,KAAK;EACL,SAAS;EACT,UAAU;EACV,SAAS;EACT,WAAW;EACX,YAAY;EACZ,WAAW;;iBAGI;EACf;EACA,QAAQ;;EAER;EACA,MAAM
|
|
1
|
+
{"version":3,"file":"execute.d.cts","names":[],"sources":["../../src/host/execute.ts"],"mappings":";;;;;;;;;;;;;iBA0BiB;EACf,OAAO;;;;;EAKP,KAAK;EACL,SAAS;EACT,UAAU;EACV,SAAS;EACT,WAAW;EACX,YAAY;EACZ,WAAW;;iBAGI;EACf;EACA,QAAQ;;EAER;EACA,MAAM;;wBAec,eACpB,OAAO,iBACP,OAAO,kBACN,QAAQ"}
|