@shware/workflow 0.1.0 → 0.3.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 (68) hide show
  1. package/README.md +688 -10
  2. package/dist/cloudflare/bindings.cjs.map +1 -1
  3. package/dist/cloudflare/bindings.d.cts +11 -0
  4. package/dist/cloudflare/bindings.d.cts.map +1 -1
  5. package/dist/cloudflare/bindings.d.mts +11 -0
  6. package/dist/cloudflare/bindings.d.mts.map +1 -1
  7. package/dist/cloudflare/bindings.mjs.map +1 -1
  8. package/dist/cloudflare/index.cjs +1 -0
  9. package/dist/cloudflare/index.d.cts +3 -3
  10. package/dist/cloudflare/index.d.mts +3 -3
  11. package/dist/cloudflare/index.mjs +2 -2
  12. package/dist/cloudflare/router.cjs +21 -4
  13. package/dist/cloudflare/router.cjs.map +1 -1
  14. package/dist/cloudflare/router.d.cts +18 -3
  15. package/dist/cloudflare/router.d.cts.map +1 -1
  16. package/dist/cloudflare/router.d.mts +18 -3
  17. package/dist/cloudflare/router.d.mts.map +1 -1
  18. package/dist/cloudflare/router.mjs +21 -5
  19. package/dist/cloudflare/router.mjs.map +1 -1
  20. package/dist/cloudflare/worker.cjs +37 -1
  21. package/dist/cloudflare/worker.cjs.map +1 -1
  22. package/dist/cloudflare/worker.d.cts.map +1 -1
  23. package/dist/cloudflare/worker.d.mts.map +1 -1
  24. package/dist/cloudflare/worker.mjs +37 -1
  25. package/dist/cloudflare/worker.mjs.map +1 -1
  26. package/dist/dsl/condition.cjs +2 -0
  27. package/dist/dsl/condition.cjs.map +1 -1
  28. package/dist/dsl/condition.d.cts +2 -0
  29. package/dist/dsl/condition.d.cts.map +1 -1
  30. package/dist/dsl/condition.d.mts +2 -0
  31. package/dist/dsl/condition.d.mts.map +1 -1
  32. package/dist/dsl/condition.mjs +2 -0
  33. package/dist/dsl/condition.mjs.map +1 -1
  34. package/dist/dsl/flow.cjs +52 -8
  35. package/dist/dsl/flow.cjs.map +1 -1
  36. package/dist/dsl/flow.d.cts +39 -5
  37. package/dist/dsl/flow.d.cts.map +1 -1
  38. package/dist/dsl/flow.d.mts +39 -5
  39. package/dist/dsl/flow.d.mts.map +1 -1
  40. package/dist/dsl/flow.mjs +52 -8
  41. package/dist/dsl/flow.mjs.map +1 -1
  42. package/dist/dsl/index.d.cts +2 -2
  43. package/dist/dsl/index.d.mts +2 -2
  44. package/dist/index.d.cts +2 -2
  45. package/dist/index.d.mts +2 -2
  46. package/dist/store/index.cjs.map +1 -1
  47. package/dist/store/index.d.cts +21 -0
  48. package/dist/store/index.d.cts.map +1 -1
  49. package/dist/store/index.d.mts +21 -0
  50. package/dist/store/index.d.mts.map +1 -1
  51. package/dist/store/index.mjs.map +1 -1
  52. package/dist/store/postgres.cjs +45 -20
  53. package/dist/store/postgres.cjs.map +1 -1
  54. package/dist/store/postgres.d.cts +4 -1
  55. package/dist/store/postgres.d.cts.map +1 -1
  56. package/dist/store/postgres.d.mts +4 -1
  57. package/dist/store/postgres.d.mts.map +1 -1
  58. package/dist/store/postgres.mjs +45 -20
  59. package/dist/store/postgres.mjs.map +1 -1
  60. package/dist/store/tables.cjs +11 -0
  61. package/dist/store/tables.cjs.map +1 -1
  62. package/dist/store/tables.d.cts +1 -0
  63. package/dist/store/tables.d.cts.map +1 -1
  64. package/dist/store/tables.d.mts +1 -0
  65. package/dist/store/tables.d.mts.map +1 -1
  66. package/dist/store/tables.mjs +11 -0
  67. package/dist/store/tables.mjs.map +1 -1
  68. package/package.json +1 -1
@@ -328,6 +328,13 @@ async function removeDevice(env, token, store = d1Store(env)) {
328
328
  woke: owner === void 0 ? 0 : await wakeSubscribers(env, store, owner, DEVICES_UPDATED_EVENT)
329
329
  };
330
330
  }
331
+ /** A versioned deploy on a store that cannot tell which version is live (D1). */
332
+ var UnversionedStoreError = class extends Error {
333
+ name = "UnversionedStoreError";
334
+ constructor() {
335
+ super("this store does not record which version is live: deploy with POST /bundle/deploy");
336
+ }
337
+ };
331
338
  /** A bundle whose declared hashes do not describe its contents — rejected before anything is written. */
332
339
  var BundleIntegrityError = class extends Error {
333
340
  constructor(message) {
@@ -356,21 +363,30 @@ function verifyBundleHashes(parsed) {
356
363
  for (const segment of parsed.segments) if (semanticHash(segment.condition) !== segment.contentHash) mismatched.push(`segment '${segment.id}'`);
357
364
  if (mismatched.length > 0) throw new BundleIntegrityError(`contentHash does not match content for ${mismatched.join(", ")} — recompile the bundle instead of editing it by hand`);
358
365
  }
359
- /** Bundle deploy, terraform-apply style: workflow bodies first (content-addressed, so a failed deploy leaves only harmless orphans), then the routing swap. */
360
- async function deployBundle(env, bundle, store = d1Store(env)) {
366
+ /**
367
+ * Bundle deploy, terraform-apply style: workflow bodies first (content-addressed, so a failed
368
+ * deploy leaves only harmless orphans), then the routing swap. With a `version` the swap happens
369
+ * only over an older live version (`replaceRoutesIfNewer`) — how journeyWorker applies the bundle
370
+ * compiled into it; without one it is forced, whatever is live.
371
+ */
372
+ async function deployBundle(env, bundle, store = d1Store(env), version) {
373
+ if (version !== void 0 && store.replaceRoutesIfNewer === void 0) throw new UnversionedStoreError();
361
374
  const parsed = BundleIR.parse(bundle);
362
375
  verifyBundleHashes(parsed);
363
376
  const problems = await store.checkSchema?.() ?? [];
364
377
  if (problems.length > 0) throw new SchemaMismatchError(problems);
365
378
  for (const workflow of parsed.workflows) await store.putWorkflowIR(workflow);
366
- await store.replaceRoutes(parsed);
379
+ let applied = true;
380
+ if (version === void 0) await store.replaceRoutes(parsed);
381
+ else applied = await store.replaceRoutesIfNewer?.(parsed, version) ?? false;
367
382
  const unrouted = parsed.workflows.filter((workflow) => workflow.trigger.type === "date" || workflow.trigger.type === "webhook").map((workflow) => ({
368
383
  workflow: workflow.id,
369
384
  trigger: workflow.trigger.type
370
385
  }));
371
386
  return {
372
387
  workflows: parsed.workflows.map((w) => `${w.id}@${w.contentHash.slice(0, 8)}`),
373
- unrouted
388
+ unrouted,
389
+ applied
374
390
  };
375
391
  }
376
392
  /**
@@ -498,6 +514,6 @@ async function handleRequest(request, env, store = d1Store(env), validation = {}
498
514
  }
499
515
  }
500
516
  //#endregion
501
- export { BundleIntegrityError, deployBundle, forgetUser, handleRequest, identifyUser, ingestEvent, refreshSegmentTriggers, registerDevice, removeDevice, sweepSegments, unbindDevice };
517
+ export { BundleIntegrityError, UnversionedStoreError, deployBundle, forgetUser, handleRequest, identifyUser, ingestEvent, refreshSegmentTriggers, registerDevice, removeDevice, sweepSegments, unbindDevice };
502
518
 
503
519
  //# sourceMappingURL=router.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"router.mjs","names":[],"sources":["../../src/cloudflare/router.ts"],"sourcesContent":["import * as z from 'zod/mini';\nimport { type Contract, type ContractResult, checkEvent } from '../contract';\nimport { murmur3 } from '../engine/bucket';\nimport {\n DEVICES_UPDATED_EVENT,\n PROFILE_UPDATED_EVENT,\n evaluateCondition,\n matchesWhere,\n} from '../engine/condition';\nimport { semanticHash } from '../hash';\nimport { BundleIR, type ConditionIR, type TriggerIR } from '../ir';\nimport { recordPushOpen } from '../reporting/tracking';\nimport { JourneyFactSource } from '../store/facts';\nimport {\n type JourneyStore,\n type ProfileProps,\n SchemaMismatchError,\n type SegmentTriggerRoute,\n} from '../store/index';\nimport {\n type JourneyEnv,\n type JourneyParams,\n WAKE_EVENT_TYPE,\n type WorkflowInstanceLike,\n} from './bindings';\nimport { d1Store } from './d1-env';\n\n/**\n * Ingest router. An incoming event does four things: it is stored, it wakes\n * waiting instances, it is matched against event triggers, and it re-evaluates\n * segment-trigger membership. Plus /deploy (persist a bundle) and /identify\n * (merge a profile — which wakes property-condition waits via\n * PROFILE_UPDATED_EVENT and re-evaluates segment triggers too).\n *\n * Storage goes through a JourneyStore; the `store` argument defaults to the\n * D1 + KV store built from the env's bindings, so a Worker that only has those\n * calls these functions exactly as before.\n */\n\nexport interface IngestInput {\n userId: string;\n event: string;\n payload?: Record<string, unknown>;\n ts?: number;\n /**\n * This occurrence's identity, and with it the idempotency of sending it.\n *\n * Only the sender can know whether two deliveries describe one occurrence or\n * two, so the engine does not decide: an id it has already stored makes the\n * second delivery a no-op. That matters because ingest is reachable from two\n * retrying callers — an HTTP client retrying a 500, and the interpreter's\n * send_event step being retried after it already committed its row — and a\n * second copy of an event silently changes every count- and window-based\n * condition that reads the log.\n *\n * Callers that retry should send an id they can reproduce. Omitting it gets\n * a fresh one, which records the event as its own occurrence.\n */\n id?: string;\n}\n\nexport interface IngestResult {\n /** The occurrence's identity: the caller's `id`, or the one minted for it. */\n id: string;\n /** False when that id was already in the log: nothing was written this time. */\n stored: boolean;\n woke: number;\n started: string[];\n}\n\nexport async function ingestEvent(\n env: JourneyEnv,\n input: IngestInput,\n store: JourneyStore = d1Store(env)\n): Promise<IngestResult> {\n if (input.event.startsWith('$')) {\n throw new Error(`event name '${input.event}' is reserved ('$' prefix is internal)`);\n }\n const ts = input.ts ?? Date.now();\n const payload = input.payload ?? {};\n // No id means the sender is not claiming this is a repeat of anything, which\n // a fresh one states truthfully — unlike a key derived from the contents,\n // which would silently collapse two identical events a second apart.\n const id = input.id ?? crypto.randomUUID();\n\n const stored = await store.insertEvent({\n id,\n userId: input.userId,\n event: input.event,\n ts,\n payload,\n });\n\n /*\n * The rest runs even for a duplicate. A retry arrives precisely because the\n * first attempt did not finish — it committed the row and then failed — so\n * returning early would strand the journeys the retry exists to start. Both\n * halves are independently idempotent: waking an instance that already moved\n * on is a no-op, and the entry ledger refuses a second entry.\n */\n const woke = await wakeSubscribers(env, store, input.userId, input.event);\n const started = await startTriggeredJourneys(env, store, { ...input, payload }, ts);\n started.push(...(await refreshSegmentTriggers(env, store, input.userId, ts)));\n return { id, stored, woke, started };\n}\n\nasync function wakeSubscribers(\n env: JourneyEnv,\n store: JourneyStore,\n userId: string,\n event: string\n): Promise<number> {\n let woke = 0;\n for (const handle of await store.findWakeHandles(userId, event)) {\n let instance: WorkflowInstanceLike;\n try {\n instance = await env.JOURNEY.get(handle);\n } catch {\n // Unknown instance (errored out without cleanup): drop the dangling subscription\n await store.unsubscribe(handle);\n continue;\n }\n try {\n await instance.sendEvent({ type: WAKE_EVENT_TYPE, payload: { event } });\n woke++;\n } catch {\n // Transient send failure (or an instance finishing right now): keep the\n // subscription — a later event retries it, and the wait's own timeout\n // bounds the damage. Deleting here would silence all future wake-ups.\n }\n }\n return woke;\n}\n\nasync function startTriggeredJourneys(\n env: JourneyEnv,\n store: JourneyStore,\n input: IngestInput & { payload: Record<string, unknown> },\n ts: number\n): Promise<string[]> {\n const started: string[] = [];\n for (const route of await store.findTriggers(input.event)) {\n // where: the payload gate — evaluated against the incoming event first\n if (route.where !== null && !matchesWhere(input.payload, route.where)) continue;\n\n // filter: the profile gate\n if (route.filter !== null) {\n const facts = new JourneyFactSource(store, input.userId);\n if (!(await evaluateCondition(route.filter, facts, ts))) continue;\n }\n\n const instanceId = await startJourney(\n env,\n store,\n route.workflow,\n route.hash,\n input.userId,\n ts,\n {\n event: input.event,\n payload: input.payload,\n }\n );\n if (instanceId !== null) started.push(instanceId);\n }\n return started;\n}\n\n/**\n * Segment-entry triggers, maintained lazily: on each ingest/identify for a\n * user, re-evaluate every trigger-routed segment for that user and act on the\n * membership transition (enter → start journeys, leave → drop membership so a\n * later re-entry is observable). Purely time-driven drift (an inactivity\n * segment turning true by clock alone) has no event to ride on: the scheduled\n * sweep catches it (`sweepSegments`).\n */\nexport async function refreshSegmentTriggers(\n env: JourneyEnv,\n store: JourneyStore,\n userId: string,\n ts: number\n): Promise<string[]> {\n return refreshSegments(env, store, userId, ts, await store.listSegmentTriggers());\n}\n\n/**\n * Re-evaluate segments for one user — the routed ones only, or with `segments` every one named —\n * and act on each membership transition: entering records the membership and starts the\n * segment's routed workflows, leaving drops it. A segment trigger therefore starts journeys for\n * users who enter after it is deployed; a user already a member is not entered again.\n */\nasync function refreshSegments(\n env: JourneyEnv,\n store: JourneyStore,\n userId: string,\n ts: number,\n routes: readonly SegmentTriggerRoute[],\n segments: readonly string[] = [],\n // A sweep loads each definition once for all its users, not once per user\n definitions = new Map<string, ConditionIR | undefined>()\n): Promise<string[]> {\n const bySegment = new Map<string, { workflow: string; hash: string }[]>(\n segments.map((name) => [name, []])\n );\n for (const route of routes) {\n const list = bySegment.get(route.segment) ?? [];\n list.push({ workflow: route.workflow, hash: route.hash });\n bySegment.set(route.segment, list);\n }\n if (bySegment.size === 0) return [];\n\n const facts = new JourneyFactSource(store, userId);\n const started: string[] = [];\n const failures: unknown[] = [];\n for (const [segmentId, segmentRoutes] of bySegment) {\n if (!definitions.has(segmentId)) {\n definitions.set(segmentId, await store.getSegmentCondition(segmentId));\n }\n const definition = definitions.get(segmentId);\n if (definition === undefined) continue; // route without a definition: broken deploy, skip\n\n const matches = await evaluateCondition(definition, facts, ts);\n const member = await store.isSegmentMember(segmentId, userId);\n\n if (matches && !member) {\n // Entry transition: the membership and the starts it owes, recorded together\n await store.addSegmentMember(\n segmentId,\n userId,\n ts,\n segmentRoutes.map((route) => route.workflow)\n );\n } else if (!matches && member) {\n await store.removeSegmentMember(segmentId, userId);\n continue;\n }\n if (!matches) continue;\n\n // Every start the entry still owes — a fresh entry's, or one an earlier evaluation failed on.\n // Each is crossed off once its instance exists, so one workflow failing neither blocks the\n // others nor gets lost: the next evaluation (the sweep, the user's next event) retries it.\n const pending = new Set(await store.pendingSegmentStarts(segmentId, userId));\n for (const route of segmentRoutes) {\n if (!pending.has(route.workflow)) continue;\n try {\n const instanceId = await startJourney(env, store, route.workflow, route.hash, userId, ts, {\n event: '$segment_entry',\n payload: { segment: segmentId },\n });\n if (instanceId !== null) started.push(instanceId);\n await store.completeSegmentStart(segmentId, userId, route.workflow);\n } catch (error) {\n failures.push(error);\n }\n }\n }\n // Surface a failure to the caller (an ingest answers 500) once every start had its chance\n if (failures.length > 0) throw failures[0];\n return started;\n}\n\nexport interface SweepResult {\n users: number;\n /** Journeys the segments' entries started. */\n started: string[];\n /** Each deployed segment's size after the sweep (empty when the store keeps no reporting). */\n sizes: { segment: string; size: number }[];\n}\n\n/**\n * The scheduled sweep: every deployed segment evaluated for every known user — the evaluator a\n * journey's conditions run on, so a member is exactly who a journey would see in it. Membership\n * follows (joiners keep the time they joined, leavers are removed), segment triggers start the\n * journeys of whoever entered by the clock alone, and the store records each segment's size.\n * It walks users one by one: fine for thousands, a large audience wants SQL-side evaluation.\n */\nexport async function sweepSegments(\n env: JourneyEnv,\n store: JourneyStore,\n now = Date.now()\n): Promise<SweepResult> {\n // Erasures left unfinished (an instance would not stop) first: their users are not evaluated\n const erasing = new Set<string>();\n for (const userId of await store.pendingErasures()) {\n try {\n const { erased } = await eraseUser(env, userId, store);\n if (!erased) erasing.add(userId);\n } catch (error) {\n // Still pending: the next sweep tries again, and the rest of this one goes on\n erasing.add(userId);\n console.error(`[workflow] erasure of ${userId} failed`, error);\n }\n }\n const segments = (await store.listSegments()).map((segment) => segment.id);\n const routes = await store.listSegmentTriggers();\n const users = (await store.listUserIds()).filter((userId) => !erasing.has(userId));\n const started: string[] = [];\n const definitions = new Map<string, ConditionIR | undefined>();\n for (const userId of users) {\n try {\n started.push(\n ...(await refreshSegments(env, store, userId, now, routes, segments, definitions))\n );\n } catch (error) {\n // One user's failed start is kept for the next sweep; it must not stop everyone else's\n console.error(`[workflow] segment refresh for ${userId} failed`, error);\n }\n }\n const sizes = (await store.reporting?.snapshotSegments(now)) ?? [];\n return { users: users.length, started, sizes };\n}\n\n/**\n * Create one journey instance behind the entry ledger. Returns null when the\n * once-policy blocks the entry (the store's attempt is atomic, so concurrent\n * ingests cannot double-enter). A pair whose previous instance is recorded as\n * 'failed' is reclaimed rather than barred forever over one outage.\n */\nasync function startJourney(\n env: JourneyEnv,\n store: JourneyStore,\n workflow: string,\n hash: string,\n userId: string,\n ts: number,\n trigger: { event: string; payload: Record<string, unknown> }\n): Promise<string | null> {\n const instanceId = buildInstanceId(workflow, hash, userId, ts);\n const params: JourneyParams = { workflowId: workflow, contentHash: hash, userId, trigger };\n\n const outcome = await store.enterJourney({ workflow, userId, instanceId, hash, ts });\n if (outcome === null) return null; // already entered, and still alive\n\n try {\n await env.JOURNEY.create({ id: instanceId, params });\n } catch (error) {\n if (outcome === 'reclaimed') {\n // Put the corpse back rather than deleting it: the entry is still a\n // failure, and dropping the row would erase that from the ledger.\n await store.setEntryStatus(instanceId, 'failed');\n } else {\n // Roll the ledger back so a retried ingest can enter — otherwise the\n // once-policy would permanently record an entry that never ran.\n await store.removeEntry(instanceId);\n }\n throw error;\n }\n return instanceId;\n}\n\nconst utf8 = new TextEncoder();\n\n/**\n * Instance id: addressable (the router gets it directly to wake it), ≤100\n * chars, legal charset. The murmur3 fingerprint keeps ids distinct when the\n * sanitized/truncated userId alone would collide.\n */\nfunction buildInstanceId(workflow: string, hash: string, userId: string, ts: number): string {\n const uid = userId.replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 24);\n const fingerprint = murmur3(utf8.encode(userId)).toString(36);\n return `${workflow.slice(0, 32)}-${hash.slice(0, 8)}-${uid}-${fingerprint}-${ts.toString(36)}`;\n}\n\n/* --------------------------------- identify --------------------------------- */\n\nexport interface IdentifyResult {\n ok: true;\n props: ProfileProps;\n woke: number;\n started: string[];\n}\n\n/**\n * Merge-patch profile properties (a null removes one), then wake property waits\n * and re-evaluate segment triggers. The merge is one statement in the store, so\n * two identifies landing together cannot lose each other's fields.\n */\nexport async function identifyUser(\n env: JourneyEnv,\n userId: string,\n props: ProfileProps,\n store: JourneyStore = d1Store(env)\n): Promise<IdentifyResult> {\n const ts = Date.now();\n const merged = await store.mergeProfile(userId, props);\n // Property-condition waits subscribe to this reserved event (see relevantEvents)\n const woke = await wakeSubscribers(env, store, userId, PROFILE_UPDATED_EVENT);\n // A profile change can move the user into (or out of) a trigger-routed segment\n const started = await refreshSegmentTriggers(env, store, userId, ts);\n return { ok: true, props: merged, woke, started };\n}\n\nexport interface DeviceRegistration {\n userId: string;\n token: string;\n platform: string;\n ts?: number;\n}\n\n/**\n * A push token registered for the user (or moved to them from another account: a token belongs\n * to one user at a time). Wakes the waits that read devices (`reachable('push')` inside\n * `waitUntil`), for the new owner and the previous one.\n */\nexport async function registerDevice(\n env: JourneyEnv,\n device: DeviceRegistration,\n store: JourneyStore = d1Store(env)\n): Promise<{ ok: true; woke: number }> {\n const previous = await store.removeDevice(device.token);\n await store.upsertDevice({ ...device, ts: device.ts ?? Date.now() });\n let woke = await wakeSubscribers(env, store, device.userId, DEVICES_UPDATED_EVENT);\n if (previous !== undefined && previous !== device.userId) {\n woke += await wakeSubscribers(env, store, previous, DEVICES_UPDATED_EVENT);\n }\n return { ok: true, woke };\n}\n\n/**\n * A deleted account: everything the engine keeps about the user is erased and the journeys still\n * running for them are terminated, so nothing more is sent. Idempotent.\n */\n/** Workflow instance states that will never run another step. */\nconst STOPPED_STATUSES = new Set(['complete', 'errored', 'terminated']);\n\n/**\n * Stop one instance for good; true once it is known not to run again. An instance the platform\n * no longer knows, or one already finished (terminate refuses those), counts as stopped. Any other\n * failure is not taken for \"already gone\": it answers false, and the erasure waits for a retry.\n */\nasync function stopInstance(env: JourneyEnv, instanceId: string): Promise<boolean> {\n let instance: WorkflowInstanceLike;\n try {\n instance = await env.JOURNEY.get(instanceId);\n } catch (error) {\n if (/not[_ ]found/i.test(String(error))) return true;\n console.warn(`[workflow] looking up ${instanceId} to stop it failed`, error);\n return false;\n }\n if (instance.terminate === undefined) return false;\n try {\n await instance.terminate();\n return true;\n } catch (error) {\n const status = await instance.status?.().catch(() => undefined);\n if (status !== undefined && STOPPED_STATUSES.has(status.status)) return true;\n console.warn(`[workflow] terminating ${instanceId} failed`, error);\n return false;\n }\n}\n\n/**\n * Carry out a recorded erasure: stop every running instance of the user, and only once all have\n * stopped erase their data (the request with it). Stopping first means no step can write rows\n * back after the erase; erasing nothing until then keeps the entries that name the instances\n * still to stop, so a retry can finish the job.\n */\nasync function eraseUser(\n env: JourneyEnv,\n userId: string,\n store: JourneyStore\n): Promise<{ erased: boolean; pending: string[] }> {\n const pending: string[] = [];\n for (const instanceId of await store.runningInstances(userId)) {\n if (!(await stopInstance(env, instanceId))) pending.push(instanceId);\n }\n if (pending.length > 0) return { erased: false, pending };\n await store.forgetUser(userId);\n return { erased: true, pending };\n}\n\n/**\n * Forget a user (a deleted account): the request is recorded first, then carried out. An\n * instance that will not stop leaves it pending — nothing erased yet — and the scheduled sweep\n * retries it; `erased: false` says so.\n */\nexport async function forgetUser(\n env: JourneyEnv,\n userId: string,\n store: JourneyStore = d1Store(env),\n now = Date.now()\n): Promise<{ ok: true; erased: boolean; pending: string[] }> {\n await store.requestErasure(userId, now);\n return { ok: true, ...(await eraseUser(env, userId, store)) };\n}\n\n/**\n * The user signed out on the device: the token stays registered, bound to nobody, so the next\n * sign-in only has to bind it again. Wakes the former owner's device waits (`reachable('push')`\n * may no longer hold).\n */\nexport async function unbindDevice(\n env: JourneyEnv,\n token: string,\n store: JourneyStore = d1Store(env)\n): Promise<{ ok: true; woke: number }> {\n const owner = await store.unbindDevice(token);\n const woke =\n owner === undefined ? 0 : await wakeSubscribers(env, store, owner, DEVICES_UPDATED_EVENT);\n return { ok: true, woke };\n}\n\n/** A push token forgotten for good (the push service reports it gone); wakes its owner's device waits. */\nexport async function removeDevice(\n env: JourneyEnv,\n token: string,\n store: JourneyStore = d1Store(env)\n): Promise<{ ok: true; woke: number }> {\n const owner = await store.removeDevice(token);\n const woke =\n owner === undefined ? 0 : await wakeSubscribers(env, store, owner, DEVICES_UPDATED_EVENT);\n return { ok: true, woke };\n}\n\n/* ---------------------------------- deploy ---------------------------------- */\n\nexport interface DeployResult {\n workflows: string[];\n /**\n * Workflows whose trigger type has no runtime routing yet — they deploy (the\n * IR is stored and versioned) but will never start until the routing lands.\n * Currently: date (TODO — needs cron + Queues, instance creation is\n * rate-limited and a date trigger fans out to the whole audience at once)\n * and webhook (TODO — needs endpoint allocation).\n */\n unrouted: { workflow: string; trigger: TriggerIR['type'] }[];\n}\n\n/** A bundle whose declared hashes do not describe its contents — rejected before anything is written. */\nexport class BundleIntegrityError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'BundleIntegrityError';\n }\n}\n\n/**\n * Recompute every declared contentHash and refuse the bundle if one disagrees.\n *\n * The contentHash is the address an in-flight journey reads its IR from, so\n * a hash that does not describe its own content is a way to change what a\n * pinned instance executes: submit an edited flow under the old hash and the\n * next replay picks it up. The compiler always computes the hash it ships, so\n * a mismatch means a hand-rolled payload or a stale client — neither should be\n * able to overwrite a version that journeys are running.\n *\n * Each hash is recomputed exactly the way its compiler produced it: a workflow\n * hashes its whole IR (contentHash strips itself), a segment hashes only its\n * condition. Mirroring them is load-bearing — recomputing a segment over the\n * whole record would reject every honest deploy.\n */\nfunction verifyBundleHashes(parsed: BundleIR): void {\n const mismatched: string[] = [];\n for (const workflow of parsed.workflows) {\n if (semanticHash(workflow) !== workflow.contentHash) {\n mismatched.push(`workflow '${workflow.id}'`);\n }\n }\n for (const segment of parsed.segments) {\n if (semanticHash(segment.condition) !== segment.contentHash) {\n mismatched.push(`segment '${segment.id}'`);\n }\n }\n if (mismatched.length > 0) {\n throw new BundleIntegrityError(\n `contentHash does not match content for ${mismatched.join(', ')} — recompile the bundle instead of editing it by hand`\n );\n }\n}\n\n/** Bundle deploy, terraform-apply style: workflow bodies first (content-addressed, so a failed deploy leaves only harmless orphans), then the routing swap. */\nexport async function deployBundle(\n env: JourneyEnv,\n bundle: unknown,\n store: JourneyStore = d1Store(env)\n): Promise<DeployResult> {\n const parsed = BundleIR.parse(bundle);\n // Before any write: a bad hash must not reach the store, where it would address content it does not describe\n verifyBundleHashes(parsed);\n // Nor a bundle the database cannot hold: the host migrates, the engine only checks\n const problems = (await store.checkSchema?.()) ?? [];\n if (problems.length > 0) throw new SchemaMismatchError(problems);\n\n for (const workflow of parsed.workflows) await store.putWorkflowIR(workflow);\n await store.replaceRoutes(parsed);\n\n const unrouted = parsed.workflows\n .filter((workflow) => workflow.trigger.type === 'date' || workflow.trigger.type === 'webhook')\n .map((workflow) => ({ workflow: workflow.id, trigger: workflow.trigger.type }));\n\n return {\n workflows: parsed.workflows.map((w) => `${w.id}@${w.contentHash.slice(0, 8)}`),\n unrouted,\n };\n}\n\n/* ----------------------------------- http ----------------------------------- */\n\n/**\n * Request bodies are untrusted JSON. They are read as unknown fields and\n * checked, never cast into the shape they are supposed to have: a cast only\n * silences the compiler, and what arrives here reaches SQL parameters.\n */\nasync function readBody(request: Request): Promise<Record<string, unknown>> {\n const body: unknown = await request.json();\n return isPlainObject(body) ? body : {};\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.length > 0;\n}\n\n/**\n * How the ingest API applies a contract: 'enforce' refuses an invalid event or\n * patch with a 400 before anything is stored, woken or started; 'report' logs\n * the issues (event id, name, field paths — never the payload) and accepts it,\n * for checking real producers before turning enforcement on.\n */\nexport interface IngestValidation {\n contract?: Contract | undefined;\n mode?: 'enforce' | 'report' | undefined;\n}\n\nexport async function handleRequest(\n request: Request,\n env: JourneyEnv,\n store: JourneyStore = d1Store(env),\n validation: IngestValidation = {}\n): Promise<Response> {\n const url = new URL(request.url);\n const json = (body: unknown, status = 200): Response =>\n new Response(JSON.stringify(body), {\n status,\n headers: { 'content-type': 'application/json' },\n });\n const { contract } = validation;\n const enforce = validation.mode !== 'report';\n /** The contract's verdict: the value to use, or the response refusing the request. */\n const checked = <T>(\n result: ContractResult<T>,\n original: T,\n what: string\n ): { value: T } | { refused: Response } => {\n if (result.ok) return { value: result.data };\n const issues = result.issues.map((i) => `${i.path || '(root)'}: ${i.message}`);\n if (!enforce) {\n console.warn(`[workflow router] contract would refuse ${what}: ${issues.join('; ')}`);\n return { value: original };\n }\n return {\n refused: json({ error: `${what} refused by the contract`, issues: result.issues }, 400),\n };\n };\n\n if (request.method === 'GET' && url.pathname === '/health') {\n return json({ ok: true });\n }\n\n // Bearer auth on everything mutating. An unset token means an open dev\n // instance (see JourneyEnv.API_TOKEN) — production must configure it.\n if (env.API_TOKEN !== undefined) {\n if (request.headers.get('authorization') !== `Bearer ${env.API_TOKEN}`) {\n return json({ error: 'unauthorized' }, 401);\n }\n }\n\n try {\n if (request.method === 'POST' && url.pathname === '/events') {\n const { id, userId, event, payload, ts } = await readBody(request);\n if (!isNonEmptyString(userId) || !isNonEmptyString(event)) {\n return json({ error: 'userId and event required' }, 400);\n }\n if (event.startsWith('$')) {\n return json({ error: 'event names starting with $ are reserved' }, 400);\n }\n // An empty id is rejected rather than treated as absent: a caller that\n // sent one meant to identify the occurrence, and silently minting a\n // different one would turn their retry into a duplicate.\n if (id !== undefined && !isNonEmptyString(id)) {\n return json({ error: 'id must be a non-empty string' }, 400);\n }\n if (ts !== undefined && typeof ts !== 'number') {\n return json({ error: 'ts must be a number' }, 400);\n }\n if (payload !== undefined && !isPlainObject(payload)) {\n return json({ error: 'payload must be an object' }, 400);\n }\n let accepted = payload;\n if (contract !== undefined) {\n const verdict = checked(\n checkEvent(contract, event, payload ?? {}),\n accepted ?? {},\n `event '${event}'${typeof id === 'string' ? ` (${id})` : ''}`\n );\n if ('refused' in verdict) return verdict.refused;\n accepted = verdict.value;\n }\n return json(await ingestEvent(env, { id, userId, event, payload: accepted, ts }, store));\n }\n\n if (request.method === 'POST' && url.pathname === '/identify') {\n const { userId, props } = await readBody(request);\n if (!isNonEmptyString(userId)) return json({ error: 'userId required' }, 400);\n if (!isPlainObject(props)) return json({ error: 'props must be an object' }, 400);\n let patch = props as ProfileProps;\n if (contract !== undefined) {\n const verdict = checked(contract.parseProfilePatch(props), patch, 'identify');\n if ('refused' in verdict) return verdict.refused;\n patch = verdict.value as ProfileProps;\n }\n return json(await identifyUser(env, userId, patch, store));\n }\n\n if (request.method === 'POST' && url.pathname === '/devices') {\n const { userId, token, platform, ts } = await readBody(request);\n if (!isNonEmptyString(userId)) return json({ error: 'userId required' }, 400);\n if (!isNonEmptyString(token)) return json({ error: 'token required' }, 400);\n if (platform !== 'ios' && platform !== 'android' && platform !== 'web') {\n return json({ error: \"platform must be 'ios', 'android' or 'web'\" }, 400);\n }\n if (ts !== undefined && typeof ts !== 'number')\n return json({ error: 'ts must be a number' }, 400);\n return json(\n await registerDevice(\n env,\n { userId, token, platform, ...(ts === undefined ? {} : { ts }) },\n store\n )\n );\n }\n\n if (request.method === 'POST' && url.pathname === '/messages/opened') {\n const { userId, messageId } = await readBody(request);\n if (!isNonEmptyString(userId)) return json({ error: 'userId required' }, 400);\n if (!isNonEmptyString(messageId)) return json({ error: 'messageId required' }, 400);\n const recorded =\n store.reporting !== undefined && (await recordPushOpen(store.reporting, userId, messageId));\n return json({ ok: true, recorded });\n }\n\n if (request.method === 'POST' && url.pathname === '/users/forget') {\n const { userId } = await readBody(request);\n if (!isNonEmptyString(userId)) return json({ error: 'userId required' }, 400);\n return json(await forgetUser(env, userId, store));\n }\n\n if (request.method === 'POST' && url.pathname === '/devices/unbind') {\n const { token } = await readBody(request);\n if (!isNonEmptyString(token)) return json({ error: 'token required' }, 400);\n return json(await unbindDevice(env, token, store));\n }\n\n if (request.method === 'POST' && url.pathname === '/devices/remove') {\n const { token } = await readBody(request);\n if (!isNonEmptyString(token)) return json({ error: 'token required' }, 400);\n return json(await removeDevice(env, token, store));\n }\n\n if (request.method === 'POST' && url.pathname === '/deploy') {\n return json(await deployBundle(env, await request.json(), store));\n }\n\n return json({ error: 'not found' }, 404);\n } catch (error) {\n // Validation problems are the caller's to fix; anything else stays out of\n // the response body (internals are logged, not leaked).\n if (error instanceof z.core.$ZodError) {\n return json({ error: `invalid payload: ${error.message}` }, 400);\n }\n // The caller sent a self-inconsistent bundle: their problem to fix, so say what is wrong\n if (error instanceof BundleIntegrityError) {\n return json({ error: error.message }, 400);\n }\n // The operator's to fix (run the migrations), so the response says what is missing\n if (error instanceof SchemaMismatchError) {\n return json({ error: error.message, problems: error.problems }, 503);\n }\n console.error('[workflow router]', error);\n return json({ error: 'internal error' }, 500);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"router.mjs","names":[],"sources":["../../src/cloudflare/router.ts"],"sourcesContent":["import * as z from 'zod/mini';\nimport { type Contract, type ContractResult, checkEvent } from '../contract';\nimport { murmur3 } from '../engine/bucket';\nimport {\n DEVICES_UPDATED_EVENT,\n PROFILE_UPDATED_EVENT,\n evaluateCondition,\n matchesWhere,\n} from '../engine/condition';\nimport { semanticHash } from '../hash';\nimport { BundleIR, type ConditionIR, type TriggerIR } from '../ir';\nimport { recordPushOpen } from '../reporting/tracking';\nimport { JourneyFactSource } from '../store/facts';\nimport {\n type BundleVersion,\n type JourneyStore,\n type ProfileProps,\n SchemaMismatchError,\n type SegmentTriggerRoute,\n} from '../store/index';\nimport {\n type JourneyEnv,\n type JourneyParams,\n WAKE_EVENT_TYPE,\n type WorkflowInstanceLike,\n} from './bindings';\nimport { d1Store } from './d1-env';\n\n/**\n * Ingest router. An incoming event does four things: it is stored, it wakes\n * waiting instances, it is matched against event triggers, and it re-evaluates\n * segment-trigger membership. Plus /deploy (persist a bundle) and /identify\n * (merge a profile — which wakes property-condition waits via\n * PROFILE_UPDATED_EVENT and re-evaluates segment triggers too).\n *\n * Storage goes through a JourneyStore; the `store` argument defaults to the\n * D1 + KV store built from the env's bindings, so a Worker that only has those\n * calls these functions exactly as before.\n */\n\nexport interface IngestInput {\n userId: string;\n event: string;\n payload?: Record<string, unknown>;\n ts?: number;\n /**\n * This occurrence's identity, and with it the idempotency of sending it.\n *\n * Only the sender can know whether two deliveries describe one occurrence or\n * two, so the engine does not decide: an id it has already stored makes the\n * second delivery a no-op. That matters because ingest is reachable from two\n * retrying callers — an HTTP client retrying a 500, and the interpreter's\n * send_event step being retried after it already committed its row — and a\n * second copy of an event silently changes every count- and window-based\n * condition that reads the log.\n *\n * Callers that retry should send an id they can reproduce. Omitting it gets\n * a fresh one, which records the event as its own occurrence.\n */\n id?: string;\n}\n\nexport interface IngestResult {\n /** The occurrence's identity: the caller's `id`, or the one minted for it. */\n id: string;\n /** False when that id was already in the log: nothing was written this time. */\n stored: boolean;\n woke: number;\n started: string[];\n}\n\nexport async function ingestEvent(\n env: JourneyEnv,\n input: IngestInput,\n store: JourneyStore = d1Store(env)\n): Promise<IngestResult> {\n if (input.event.startsWith('$')) {\n throw new Error(`event name '${input.event}' is reserved ('$' prefix is internal)`);\n }\n const ts = input.ts ?? Date.now();\n const payload = input.payload ?? {};\n // No id means the sender is not claiming this is a repeat of anything, which\n // a fresh one states truthfully — unlike a key derived from the contents,\n // which would silently collapse two identical events a second apart.\n const id = input.id ?? crypto.randomUUID();\n\n const stored = await store.insertEvent({\n id,\n userId: input.userId,\n event: input.event,\n ts,\n payload,\n });\n\n /*\n * The rest runs even for a duplicate. A retry arrives precisely because the\n * first attempt did not finish — it committed the row and then failed — so\n * returning early would strand the journeys the retry exists to start. Both\n * halves are independently idempotent: waking an instance that already moved\n * on is a no-op, and the entry ledger refuses a second entry.\n */\n const woke = await wakeSubscribers(env, store, input.userId, input.event);\n const started = await startTriggeredJourneys(env, store, { ...input, payload }, ts);\n started.push(...(await refreshSegmentTriggers(env, store, input.userId, ts)));\n return { id, stored, woke, started };\n}\n\nasync function wakeSubscribers(\n env: JourneyEnv,\n store: JourneyStore,\n userId: string,\n event: string\n): Promise<number> {\n let woke = 0;\n for (const handle of await store.findWakeHandles(userId, event)) {\n let instance: WorkflowInstanceLike;\n try {\n instance = await env.JOURNEY.get(handle);\n } catch {\n // Unknown instance (errored out without cleanup): drop the dangling subscription\n await store.unsubscribe(handle);\n continue;\n }\n try {\n await instance.sendEvent({ type: WAKE_EVENT_TYPE, payload: { event } });\n woke++;\n } catch {\n // Transient send failure (or an instance finishing right now): keep the\n // subscription — a later event retries it, and the wait's own timeout\n // bounds the damage. Deleting here would silence all future wake-ups.\n }\n }\n return woke;\n}\n\nasync function startTriggeredJourneys(\n env: JourneyEnv,\n store: JourneyStore,\n input: IngestInput & { payload: Record<string, unknown> },\n ts: number\n): Promise<string[]> {\n const started: string[] = [];\n for (const route of await store.findTriggers(input.event)) {\n // where: the payload gate — evaluated against the incoming event first\n if (route.where !== null && !matchesWhere(input.payload, route.where)) continue;\n\n // filter: the profile gate\n if (route.filter !== null) {\n const facts = new JourneyFactSource(store, input.userId);\n if (!(await evaluateCondition(route.filter, facts, ts))) continue;\n }\n\n const instanceId = await startJourney(\n env,\n store,\n route.workflow,\n route.hash,\n input.userId,\n ts,\n {\n event: input.event,\n payload: input.payload,\n }\n );\n if (instanceId !== null) started.push(instanceId);\n }\n return started;\n}\n\n/**\n * Segment-entry triggers, maintained lazily: on each ingest/identify for a\n * user, re-evaluate every trigger-routed segment for that user and act on the\n * membership transition (enter → start journeys, leave → drop membership so a\n * later re-entry is observable). Purely time-driven drift (an inactivity\n * segment turning true by clock alone) has no event to ride on: the scheduled\n * sweep catches it (`sweepSegments`).\n */\nexport async function refreshSegmentTriggers(\n env: JourneyEnv,\n store: JourneyStore,\n userId: string,\n ts: number\n): Promise<string[]> {\n return refreshSegments(env, store, userId, ts, await store.listSegmentTriggers());\n}\n\n/**\n * Re-evaluate segments for one user — the routed ones only, or with `segments` every one named —\n * and act on each membership transition: entering records the membership and starts the\n * segment's routed workflows, leaving drops it. A segment trigger therefore starts journeys for\n * users who enter after it is deployed; a user already a member is not entered again.\n */\nasync function refreshSegments(\n env: JourneyEnv,\n store: JourneyStore,\n userId: string,\n ts: number,\n routes: readonly SegmentTriggerRoute[],\n segments: readonly string[] = [],\n // A sweep loads each definition once for all its users, not once per user\n definitions = new Map<string, ConditionIR | undefined>()\n): Promise<string[]> {\n const bySegment = new Map<string, { workflow: string; hash: string }[]>(\n segments.map((name) => [name, []])\n );\n for (const route of routes) {\n const list = bySegment.get(route.segment) ?? [];\n list.push({ workflow: route.workflow, hash: route.hash });\n bySegment.set(route.segment, list);\n }\n if (bySegment.size === 0) return [];\n\n const facts = new JourneyFactSource(store, userId);\n const started: string[] = [];\n const failures: unknown[] = [];\n for (const [segmentId, segmentRoutes] of bySegment) {\n if (!definitions.has(segmentId)) {\n definitions.set(segmentId, await store.getSegmentCondition(segmentId));\n }\n const definition = definitions.get(segmentId);\n if (definition === undefined) continue; // route without a definition: broken deploy, skip\n\n const matches = await evaluateCondition(definition, facts, ts);\n const member = await store.isSegmentMember(segmentId, userId);\n\n if (matches && !member) {\n // Entry transition: the membership and the starts it owes, recorded together\n await store.addSegmentMember(\n segmentId,\n userId,\n ts,\n segmentRoutes.map((route) => route.workflow)\n );\n } else if (!matches && member) {\n await store.removeSegmentMember(segmentId, userId);\n continue;\n }\n if (!matches) continue;\n\n // Every start the entry still owes — a fresh entry's, or one an earlier evaluation failed on.\n // Each is crossed off once its instance exists, so one workflow failing neither blocks the\n // others nor gets lost: the next evaluation (the sweep, the user's next event) retries it.\n const pending = new Set(await store.pendingSegmentStarts(segmentId, userId));\n for (const route of segmentRoutes) {\n if (!pending.has(route.workflow)) continue;\n try {\n const instanceId = await startJourney(env, store, route.workflow, route.hash, userId, ts, {\n event: '$segment_entry',\n payload: { segment: segmentId },\n });\n if (instanceId !== null) started.push(instanceId);\n await store.completeSegmentStart(segmentId, userId, route.workflow);\n } catch (error) {\n failures.push(error);\n }\n }\n }\n // Surface a failure to the caller (an ingest answers 500) once every start had its chance\n if (failures.length > 0) throw failures[0];\n return started;\n}\n\nexport interface SweepResult {\n users: number;\n /** Journeys the segments' entries started. */\n started: string[];\n /** Each deployed segment's size after the sweep (empty when the store keeps no reporting). */\n sizes: { segment: string; size: number }[];\n}\n\n/**\n * The scheduled sweep: every deployed segment evaluated for every known user — the evaluator a\n * journey's conditions run on, so a member is exactly who a journey would see in it. Membership\n * follows (joiners keep the time they joined, leavers are removed), segment triggers start the\n * journeys of whoever entered by the clock alone, and the store records each segment's size.\n * It walks users one by one: fine for thousands, a large audience wants SQL-side evaluation.\n */\nexport async function sweepSegments(\n env: JourneyEnv,\n store: JourneyStore,\n now = Date.now()\n): Promise<SweepResult> {\n // Erasures left unfinished (an instance would not stop) first: their users are not evaluated\n const erasing = new Set<string>();\n for (const userId of await store.pendingErasures()) {\n try {\n const { erased } = await eraseUser(env, userId, store);\n if (!erased) erasing.add(userId);\n } catch (error) {\n // Still pending: the next sweep tries again, and the rest of this one goes on\n erasing.add(userId);\n console.error(`[workflow] erasure of ${userId} failed`, error);\n }\n }\n const segments = (await store.listSegments()).map((segment) => segment.id);\n const routes = await store.listSegmentTriggers();\n const users = (await store.listUserIds()).filter((userId) => !erasing.has(userId));\n const started: string[] = [];\n const definitions = new Map<string, ConditionIR | undefined>();\n for (const userId of users) {\n try {\n started.push(\n ...(await refreshSegments(env, store, userId, now, routes, segments, definitions))\n );\n } catch (error) {\n // One user's failed start is kept for the next sweep; it must not stop everyone else's\n console.error(`[workflow] segment refresh for ${userId} failed`, error);\n }\n }\n const sizes = (await store.reporting?.snapshotSegments(now)) ?? [];\n return { users: users.length, started, sizes };\n}\n\n/**\n * Create one journey instance behind the entry ledger. Returns null when the\n * once-policy blocks the entry (the store's attempt is atomic, so concurrent\n * ingests cannot double-enter). A pair whose previous instance is recorded as\n * 'failed' is reclaimed rather than barred forever over one outage.\n */\nasync function startJourney(\n env: JourneyEnv,\n store: JourneyStore,\n workflow: string,\n hash: string,\n userId: string,\n ts: number,\n trigger: { event: string; payload: Record<string, unknown> }\n): Promise<string | null> {\n const instanceId = buildInstanceId(workflow, hash, userId, ts);\n const params: JourneyParams = { workflowId: workflow, contentHash: hash, userId, trigger };\n\n const outcome = await store.enterJourney({ workflow, userId, instanceId, hash, ts });\n if (outcome === null) return null; // already entered, and still alive\n\n try {\n await env.JOURNEY.create({ id: instanceId, params });\n } catch (error) {\n if (outcome === 'reclaimed') {\n // Put the corpse back rather than deleting it: the entry is still a\n // failure, and dropping the row would erase that from the ledger.\n await store.setEntryStatus(instanceId, 'failed');\n } else {\n // Roll the ledger back so a retried ingest can enter — otherwise the\n // once-policy would permanently record an entry that never ran.\n await store.removeEntry(instanceId);\n }\n throw error;\n }\n return instanceId;\n}\n\nconst utf8 = new TextEncoder();\n\n/**\n * Instance id: addressable (the router gets it directly to wake it), ≤100\n * chars, legal charset. The murmur3 fingerprint keeps ids distinct when the\n * sanitized/truncated userId alone would collide.\n */\nfunction buildInstanceId(workflow: string, hash: string, userId: string, ts: number): string {\n const uid = userId.replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 24);\n const fingerprint = murmur3(utf8.encode(userId)).toString(36);\n return `${workflow.slice(0, 32)}-${hash.slice(0, 8)}-${uid}-${fingerprint}-${ts.toString(36)}`;\n}\n\n/* --------------------------------- identify --------------------------------- */\n\nexport interface IdentifyResult {\n ok: true;\n props: ProfileProps;\n woke: number;\n started: string[];\n}\n\n/**\n * Merge-patch profile properties (a null removes one), then wake property waits\n * and re-evaluate segment triggers. The merge is one statement in the store, so\n * two identifies landing together cannot lose each other's fields.\n */\nexport async function identifyUser(\n env: JourneyEnv,\n userId: string,\n props: ProfileProps,\n store: JourneyStore = d1Store(env)\n): Promise<IdentifyResult> {\n const ts = Date.now();\n const merged = await store.mergeProfile(userId, props);\n // Property-condition waits subscribe to this reserved event (see relevantEvents)\n const woke = await wakeSubscribers(env, store, userId, PROFILE_UPDATED_EVENT);\n // A profile change can move the user into (or out of) a trigger-routed segment\n const started = await refreshSegmentTriggers(env, store, userId, ts);\n return { ok: true, props: merged, woke, started };\n}\n\nexport interface DeviceRegistration {\n userId: string;\n token: string;\n platform: string;\n ts?: number;\n}\n\n/**\n * A push token registered for the user (or moved to them from another account: a token belongs\n * to one user at a time). Wakes the waits that read devices (`reachable('push')` inside\n * `waitUntil`), for the new owner and the previous one.\n */\nexport async function registerDevice(\n env: JourneyEnv,\n device: DeviceRegistration,\n store: JourneyStore = d1Store(env)\n): Promise<{ ok: true; woke: number }> {\n const previous = await store.removeDevice(device.token);\n await store.upsertDevice({ ...device, ts: device.ts ?? Date.now() });\n let woke = await wakeSubscribers(env, store, device.userId, DEVICES_UPDATED_EVENT);\n if (previous !== undefined && previous !== device.userId) {\n woke += await wakeSubscribers(env, store, previous, DEVICES_UPDATED_EVENT);\n }\n return { ok: true, woke };\n}\n\n/**\n * A deleted account: everything the engine keeps about the user is erased and the journeys still\n * running for them are terminated, so nothing more is sent. Idempotent.\n */\n/** Workflow instance states that will never run another step. */\nconst STOPPED_STATUSES = new Set(['complete', 'errored', 'terminated']);\n\n/**\n * Stop one instance for good; true once it is known not to run again. An instance the platform\n * no longer knows, or one already finished (terminate refuses those), counts as stopped. Any other\n * failure is not taken for \"already gone\": it answers false, and the erasure waits for a retry.\n */\nasync function stopInstance(env: JourneyEnv, instanceId: string): Promise<boolean> {\n let instance: WorkflowInstanceLike;\n try {\n instance = await env.JOURNEY.get(instanceId);\n } catch (error) {\n if (/not[_ ]found/i.test(String(error))) return true;\n console.warn(`[workflow] looking up ${instanceId} to stop it failed`, error);\n return false;\n }\n if (instance.terminate === undefined) return false;\n try {\n await instance.terminate();\n return true;\n } catch (error) {\n const status = await instance.status?.().catch(() => undefined);\n if (status !== undefined && STOPPED_STATUSES.has(status.status)) return true;\n console.warn(`[workflow] terminating ${instanceId} failed`, error);\n return false;\n }\n}\n\n/**\n * Carry out a recorded erasure: stop every running instance of the user, and only once all have\n * stopped erase their data (the request with it). Stopping first means no step can write rows\n * back after the erase; erasing nothing until then keeps the entries that name the instances\n * still to stop, so a retry can finish the job.\n */\nasync function eraseUser(\n env: JourneyEnv,\n userId: string,\n store: JourneyStore\n): Promise<{ erased: boolean; pending: string[] }> {\n const pending: string[] = [];\n for (const instanceId of await store.runningInstances(userId)) {\n if (!(await stopInstance(env, instanceId))) pending.push(instanceId);\n }\n if (pending.length > 0) return { erased: false, pending };\n await store.forgetUser(userId);\n return { erased: true, pending };\n}\n\n/**\n * Forget a user (a deleted account): the request is recorded first, then carried out. An\n * instance that will not stop leaves it pending — nothing erased yet — and the scheduled sweep\n * retries it; `erased: false` says so.\n */\nexport async function forgetUser(\n env: JourneyEnv,\n userId: string,\n store: JourneyStore = d1Store(env),\n now = Date.now()\n): Promise<{ ok: true; erased: boolean; pending: string[] }> {\n await store.requestErasure(userId, now);\n return { ok: true, ...(await eraseUser(env, userId, store)) };\n}\n\n/**\n * The user signed out on the device: the token stays registered, bound to nobody, so the next\n * sign-in only has to bind it again. Wakes the former owner's device waits (`reachable('push')`\n * may no longer hold).\n */\nexport async function unbindDevice(\n env: JourneyEnv,\n token: string,\n store: JourneyStore = d1Store(env)\n): Promise<{ ok: true; woke: number }> {\n const owner = await store.unbindDevice(token);\n const woke =\n owner === undefined ? 0 : await wakeSubscribers(env, store, owner, DEVICES_UPDATED_EVENT);\n return { ok: true, woke };\n}\n\n/** A push token forgotten for good (the push service reports it gone); wakes its owner's device waits. */\nexport async function removeDevice(\n env: JourneyEnv,\n token: string,\n store: JourneyStore = d1Store(env)\n): Promise<{ ok: true; woke: number }> {\n const owner = await store.removeDevice(token);\n const woke =\n owner === undefined ? 0 : await wakeSubscribers(env, store, owner, DEVICES_UPDATED_EVENT);\n return { ok: true, woke };\n}\n\n/* ---------------------------------- deploy ---------------------------------- */\n\nexport interface DeployResult {\n workflows: string[];\n /**\n * Workflows whose trigger type has no runtime routing yet — they deploy (the\n * IR is stored and versioned) but will never start until the routing lands.\n * Currently: date (TODO — needs cron + Queues, instance creation is\n * rate-limited and a date trigger fans out to the whole audience at once)\n * and webhook (TODO — needs endpoint allocation).\n */\n unrouted: { workflow: string; trigger: TriggerIR['type'] }[];\n /**\n * Whether the routes now point at this bundle: always for a forced deploy, false for a\n * versioned one when a version as new or newer is already live (nothing was swapped).\n */\n applied: boolean;\n}\n\n/** A versioned deploy on a store that cannot tell which version is live (D1). */\nexport class UnversionedStoreError extends Error {\n override readonly name = 'UnversionedStoreError';\n constructor() {\n super('this store does not record which version is live: deploy with POST /bundle/deploy');\n }\n}\n\n/** A bundle whose declared hashes do not describe its contents — rejected before anything is written. */\nexport class BundleIntegrityError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'BundleIntegrityError';\n }\n}\n\n/**\n * Recompute every declared contentHash and refuse the bundle if one disagrees.\n *\n * The contentHash is the address an in-flight journey reads its IR from, so\n * a hash that does not describe its own content is a way to change what a\n * pinned instance executes: submit an edited flow under the old hash and the\n * next replay picks it up. The compiler always computes the hash it ships, so\n * a mismatch means a hand-rolled payload or a stale client — neither should be\n * able to overwrite a version that journeys are running.\n *\n * Each hash is recomputed exactly the way its compiler produced it: a workflow\n * hashes its whole IR (contentHash strips itself), a segment hashes only its\n * condition. Mirroring them is load-bearing — recomputing a segment over the\n * whole record would reject every honest deploy.\n */\nfunction verifyBundleHashes(parsed: BundleIR): void {\n const mismatched: string[] = [];\n for (const workflow of parsed.workflows) {\n if (semanticHash(workflow) !== workflow.contentHash) {\n mismatched.push(`workflow '${workflow.id}'`);\n }\n }\n for (const segment of parsed.segments) {\n if (semanticHash(segment.condition) !== segment.contentHash) {\n mismatched.push(`segment '${segment.id}'`);\n }\n }\n if (mismatched.length > 0) {\n throw new BundleIntegrityError(\n `contentHash does not match content for ${mismatched.join(', ')} — recompile the bundle instead of editing it by hand`\n );\n }\n}\n\n/**\n * Bundle deploy, terraform-apply style: workflow bodies first (content-addressed, so a failed\n * deploy leaves only harmless orphans), then the routing swap. With a `version` the swap happens\n * only over an older live version (`replaceRoutesIfNewer`) — how journeyWorker applies the bundle\n * compiled into it; without one it is forced, whatever is live.\n */\nexport async function deployBundle(\n env: JourneyEnv,\n bundle: unknown,\n store: JourneyStore = d1Store(env),\n version?: BundleVersion\n): Promise<DeployResult> {\n if (version !== undefined && store.replaceRoutesIfNewer === undefined) {\n throw new UnversionedStoreError();\n }\n const parsed = BundleIR.parse(bundle);\n // Before any write: a bad hash must not reach the store, where it would address content it does not describe\n verifyBundleHashes(parsed);\n // Nor a bundle the database cannot hold: the host migrates, the engine only checks\n const problems = (await store.checkSchema?.()) ?? [];\n if (problems.length > 0) throw new SchemaMismatchError(problems);\n\n for (const workflow of parsed.workflows) await store.putWorkflowIR(workflow);\n let applied = true;\n if (version === undefined) await store.replaceRoutes(parsed);\n else applied = (await store.replaceRoutesIfNewer?.(parsed, version)) ?? false;\n\n const unrouted = parsed.workflows\n .filter((workflow) => workflow.trigger.type === 'date' || workflow.trigger.type === 'webhook')\n .map((workflow) => ({ workflow: workflow.id, trigger: workflow.trigger.type }));\n\n return {\n workflows: parsed.workflows.map((w) => `${w.id}@${w.contentHash.slice(0, 8)}`),\n unrouted,\n applied,\n };\n}\n\n/* ----------------------------------- http ----------------------------------- */\n\n/**\n * Request bodies are untrusted JSON. They are read as unknown fields and\n * checked, never cast into the shape they are supposed to have: a cast only\n * silences the compiler, and what arrives here reaches SQL parameters.\n */\nasync function readBody(request: Request): Promise<Record<string, unknown>> {\n const body: unknown = await request.json();\n return isPlainObject(body) ? body : {};\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.length > 0;\n}\n\n/**\n * How the ingest API applies a contract: 'enforce' refuses an invalid event or\n * patch with a 400 before anything is stored, woken or started; 'report' logs\n * the issues (event id, name, field paths — never the payload) and accepts it,\n * for checking real producers before turning enforcement on.\n */\nexport interface IngestValidation {\n contract?: Contract | undefined;\n mode?: 'enforce' | 'report' | undefined;\n}\n\nexport async function handleRequest(\n request: Request,\n env: JourneyEnv,\n store: JourneyStore = d1Store(env),\n validation: IngestValidation = {}\n): Promise<Response> {\n const url = new URL(request.url);\n const json = (body: unknown, status = 200): Response =>\n new Response(JSON.stringify(body), {\n status,\n headers: { 'content-type': 'application/json' },\n });\n const { contract } = validation;\n const enforce = validation.mode !== 'report';\n /** The contract's verdict: the value to use, or the response refusing the request. */\n const checked = <T>(\n result: ContractResult<T>,\n original: T,\n what: string\n ): { value: T } | { refused: Response } => {\n if (result.ok) return { value: result.data };\n const issues = result.issues.map((i) => `${i.path || '(root)'}: ${i.message}`);\n if (!enforce) {\n console.warn(`[workflow router] contract would refuse ${what}: ${issues.join('; ')}`);\n return { value: original };\n }\n return {\n refused: json({ error: `${what} refused by the contract`, issues: result.issues }, 400),\n };\n };\n\n if (request.method === 'GET' && url.pathname === '/health') {\n return json({ ok: true });\n }\n\n // Bearer auth on everything mutating. An unset token means an open dev\n // instance (see JourneyEnv.API_TOKEN) — production must configure it.\n if (env.API_TOKEN !== undefined) {\n if (request.headers.get('authorization') !== `Bearer ${env.API_TOKEN}`) {\n return json({ error: 'unauthorized' }, 401);\n }\n }\n\n try {\n if (request.method === 'POST' && url.pathname === '/events') {\n const { id, userId, event, payload, ts } = await readBody(request);\n if (!isNonEmptyString(userId) || !isNonEmptyString(event)) {\n return json({ error: 'userId and event required' }, 400);\n }\n if (event.startsWith('$')) {\n return json({ error: 'event names starting with $ are reserved' }, 400);\n }\n // An empty id is rejected rather than treated as absent: a caller that\n // sent one meant to identify the occurrence, and silently minting a\n // different one would turn their retry into a duplicate.\n if (id !== undefined && !isNonEmptyString(id)) {\n return json({ error: 'id must be a non-empty string' }, 400);\n }\n if (ts !== undefined && typeof ts !== 'number') {\n return json({ error: 'ts must be a number' }, 400);\n }\n if (payload !== undefined && !isPlainObject(payload)) {\n return json({ error: 'payload must be an object' }, 400);\n }\n let accepted = payload;\n if (contract !== undefined) {\n const verdict = checked(\n checkEvent(contract, event, payload ?? {}),\n accepted ?? {},\n `event '${event}'${typeof id === 'string' ? ` (${id})` : ''}`\n );\n if ('refused' in verdict) return verdict.refused;\n accepted = verdict.value;\n }\n return json(await ingestEvent(env, { id, userId, event, payload: accepted, ts }, store));\n }\n\n if (request.method === 'POST' && url.pathname === '/identify') {\n const { userId, props } = await readBody(request);\n if (!isNonEmptyString(userId)) return json({ error: 'userId required' }, 400);\n if (!isPlainObject(props)) return json({ error: 'props must be an object' }, 400);\n let patch = props as ProfileProps;\n if (contract !== undefined) {\n const verdict = checked(contract.parseProfilePatch(props), patch, 'identify');\n if ('refused' in verdict) return verdict.refused;\n patch = verdict.value as ProfileProps;\n }\n return json(await identifyUser(env, userId, patch, store));\n }\n\n if (request.method === 'POST' && url.pathname === '/devices') {\n const { userId, token, platform, ts } = await readBody(request);\n if (!isNonEmptyString(userId)) return json({ error: 'userId required' }, 400);\n if (!isNonEmptyString(token)) return json({ error: 'token required' }, 400);\n if (platform !== 'ios' && platform !== 'android' && platform !== 'web') {\n return json({ error: \"platform must be 'ios', 'android' or 'web'\" }, 400);\n }\n if (ts !== undefined && typeof ts !== 'number')\n return json({ error: 'ts must be a number' }, 400);\n return json(\n await registerDevice(\n env,\n { userId, token, platform, ...(ts === undefined ? {} : { ts }) },\n store\n )\n );\n }\n\n if (request.method === 'POST' && url.pathname === '/messages/opened') {\n const { userId, messageId } = await readBody(request);\n if (!isNonEmptyString(userId)) return json({ error: 'userId required' }, 400);\n if (!isNonEmptyString(messageId)) return json({ error: 'messageId required' }, 400);\n const recorded =\n store.reporting !== undefined && (await recordPushOpen(store.reporting, userId, messageId));\n return json({ ok: true, recorded });\n }\n\n if (request.method === 'POST' && url.pathname === '/users/forget') {\n const { userId } = await readBody(request);\n if (!isNonEmptyString(userId)) return json({ error: 'userId required' }, 400);\n return json(await forgetUser(env, userId, store));\n }\n\n if (request.method === 'POST' && url.pathname === '/devices/unbind') {\n const { token } = await readBody(request);\n if (!isNonEmptyString(token)) return json({ error: 'token required' }, 400);\n return json(await unbindDevice(env, token, store));\n }\n\n if (request.method === 'POST' && url.pathname === '/devices/remove') {\n const { token } = await readBody(request);\n if (!isNonEmptyString(token)) return json({ error: 'token required' }, 400);\n return json(await removeDevice(env, token, store));\n }\n\n if (request.method === 'POST' && url.pathname === '/deploy') {\n return json(await deployBundle(env, await request.json(), store));\n }\n\n return json({ error: 'not found' }, 404);\n } catch (error) {\n // Validation problems are the caller's to fix; anything else stays out of\n // the response body (internals are logged, not leaked).\n if (error instanceof z.core.$ZodError) {\n return json({ error: `invalid payload: ${error.message}` }, 400);\n }\n // The caller sent a self-inconsistent bundle: their problem to fix, so say what is wrong\n if (error instanceof BundleIntegrityError) {\n return json({ error: error.message }, 400);\n }\n // The operator's to fix (run the migrations), so the response says what is missing\n if (error instanceof SchemaMismatchError) {\n return json({ error: error.message, problems: error.problems }, 503);\n }\n console.error('[workflow router]', error);\n return json({ error: 'internal error' }, 500);\n }\n}\n"],"mappings":"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"}
@@ -110,6 +110,34 @@ function journeyWorker(options) {
110
110
  });
111
111
  const authorized = (request, env) => env.API_TOKEN === void 0 || request.headers.get("authorization") === `Bearer ${env.API_TOKEN}`;
112
112
  const preview = emails === void 0 ? void 0 : require_react_email.registryRenderer(emails);
113
+ let applying;
114
+ let retryAt = 0;
115
+ const applyOwnBundle = (env) => {
116
+ const metadata = env.CF_VERSION_METADATA;
117
+ if (metadata === void 0 || metadata.id === "") return void 0;
118
+ const version = {
119
+ id: metadata.id,
120
+ createdAt: Date.parse(metadata.timestamp)
121
+ };
122
+ if (Number.isNaN(version.createdAt)) return void 0;
123
+ if (applying !== void 0 || Date.now() < retryAt) return void 0;
124
+ applying = (async () => {
125
+ const { store, close } = lease(env);
126
+ try {
127
+ if (store.liveBundleVersion === void 0) return;
128
+ const live = await store.liveBundleVersion();
129
+ if (live !== void 0 && (live.id === version.id || live.createdAt >= version.createdAt)) return;
130
+ if ((await require_cloudflare_router.deployBundle(env, bundle(), store, version)).applied) console.log(`[workflow] version ${version.id} is live`);
131
+ } finally {
132
+ await close();
133
+ }
134
+ })().catch((error) => {
135
+ applying = void 0;
136
+ retryAt = Date.now() + 6e4;
137
+ console.error(`[workflow] could not make version ${version.id} live`, error);
138
+ });
139
+ return applying;
140
+ };
113
141
  const fetch = async (request, rawEnv, ctx) => {
114
142
  let env;
115
143
  try {
@@ -119,6 +147,11 @@ function journeyWorker(options) {
119
147
  console.error(`[workflow] ${error.message}`);
120
148
  return json({ error: error.message }, 500);
121
149
  }
150
+ const pending = applyOwnBundle(env);
151
+ if (pending !== void 0) {
152
+ if (ctx === void 0) await pending;
153
+ else ctx.waitUntil(pending);
154
+ }
122
155
  const url = new URL(request.url);
123
156
  const { store, close } = lease(env);
124
157
  try {
@@ -212,7 +245,10 @@ function journeyWorker(options) {
212
245
  }
213
246
  };
214
247
  const scheduled = async (_controller, env, ctx) => {
215
- ctx.waitUntil(sweep(env));
248
+ ctx.waitUntil((async () => {
249
+ await applyOwnBundle(checkEnv(env));
250
+ await sweep(env);
251
+ })());
216
252
  };
217
253
  return {
218
254
  Runner,
@@ -1 +1 @@
1
- {"version":3,"file":"worker.cjs","names":["z","compileConfig","d1Store","runtimeFromConfig","scaleSegmentWindows","JourneyRunner","registryValidator","channelsFromConfig","reportingObserver","registryRenderer","handleTracking","checkEvent","ContractValidationError","ingestEvent","sweepSegments","deployBundle","SchemaMismatchError","TemplateValidationError","handleRequest"],"sources":["../../src/cloudflare/worker.ts"],"sourcesContent":["import * as z from 'zod/mini';\nimport type * as core from 'zod/v4/core';\nimport { type JourneyConfig, compileConfig } from '../config';\nimport { type Contract, ContractValidationError, checkEvent } from '../contract';\nimport type { RegisteredAction } from '../engine/actions';\nimport type { JourneyObserver, MessageSender, MessageValidator } from '../engine/ports';\nimport { scaleSegmentWindows } from '../engine/time-scale';\nimport type { ScalarIR } from '../ir';\nimport { registryRenderer } from '../react-email';\nimport { reportingObserver } from '../reporting/observer';\nimport { handleTracking } from '../reporting/tracking';\nimport type { JourneyRuntimeOptions } from '../runtime';\nimport { type JourneyStore, SchemaMismatchError, type StoreLease } from '../store/index';\nimport { DEFAULT_SCHEMA } from '../store/tables';\nimport { TemplateValidationError, registryValidator } from '../templates';\nimport type { JourneyEnv, JourneyParams } from './bindings';\nimport { channelsFromConfig, runtimeFromConfig } from './channels';\nimport { d1Store } from './d1-env';\nimport {\n type SweepResult,\n deployBundle,\n handleRequest,\n ingestEvent,\n sweepSegments,\n} from './router';\nimport { JourneyRunner } from './runner';\n\n/** The fetch handler's execution context (the subset used). */\nexport interface ExecutionContextLike {\n waitUntil(promise: Promise<unknown>): void;\n}\n\nexport interface JourneyWorkerOptions<Env extends JourneyEnv> {\n /**\n * The project's `workflow.config.ts` (`defineConfig`): what the Worker\n * deploys (`GET /bundle`, `POST /bundle/deploy` serve `compileConfig`), the\n * template registries the senders, the validator and `/preview` render from,\n * the contract every `/events` and `/identify` is checked against, the email\n * envelope, actions, and the runtime / APNs / FCM settings. Secrets come\n * from the env under the names `JourneyEnv` documents.\n */\n config: JourneyConfig;\n /**\n * The data plane, opened per request and per workflow run. Return a\n * `StoreLease` when the client must be released afterwards (a Postgres\n * connection); defaults to D1 + KV from the env's bindings. `database` is\n * the config's (`schema` defaulting to `workflow`), for a Postgres store:\n *\n * store: (env, database) => postgresJsStore(env.HYPERDRIVE.connectionString, database)\n */\n store?: (env: Env, database: { schema: string }) => JourneyStore | StoreLease;\n /**\n * Reporting hooks: node arrivals, every copy sent (with the tracking it should carry — click\n * links and an open pixel for email, payload data for a push), and how each instance ended.\n * By default a store that keeps reporting (Postgres) records them itself, and the Worker\n * serves the email tracking routes (`/t/...`, on `WORKFLOW_ORIGIN`); override to report\n * elsewhere.\n */\n observer?: (env: Env, store: JourneyStore) => JourneyObserver;\n /** Message outlet override (tests, a custom gateway); by default the channels the config describes. */\n messages?: (env: Env, store: JourneyStore) => MessageSender;\n /** Per-instance runtime options override (a tester in fast-forward); by default `config.runtime` for `ENVIRONMENT`. */\n runtime?: (env: Env, params: JourneyParams) => JourneyRuntimeOptions;\n /**\n * The host's environment, as a zod schema over its vars and secrets (bindings pass through\n * untouched): checked the first time the Worker runs, so a missing or malformed variable fails\n * there — every problem listed — not at the send that needs it. Webhook templates read their\n * URL from the parsed values: `webhook: (env: Env) => env.WEBHOOK_DISCORD_ALERTS`.\n */\n env?: core.$ZodType<object>;\n /** 'enforce' (default) answers an event or patch the contract refuses with a 400; 'report' only logs it. */\n contractMode?: 'enforce' | 'report';\n}\n\nexport interface JourneyWorker<Env extends JourneyEnv> {\n /** Export this under the `class_name` of the wrangler `workflows` binding. */\n Runner: new (...args: never[]) => JourneyRunner;\n /** The Worker's fetch handler: `/health`, `/events`, `/identify`, `/devices`, `/messages/opened`, `/segments/sweep`, `/t/*` (email tracking), `/deploy`, `/bundle`, `/bundle/deploy` and `/preview/*`. */\n fetch: (request: Request, env: Env, ctx?: ExecutionContextLike) => Promise<Response>;\n /**\n * The segment sweep (`sweepSegments`): membership of every deployed segment for every known\n * user, the journeys of segments entered by the clock alone (`not(performed(e.login, { within:\n * '7 days' }))` has no event to ride on), and each segment's size. Also served as\n * `POST /segments/sweep`.\n */\n sweep: (env: Env) => Promise<SweepResult>;\n /** The cron trigger's handler: runs the sweep. Export it beside `fetch` with a `triggers.crons` entry. */\n scheduled: (controller: unknown, env: Env, ctx: ExecutionContextLike) => Promise<void>;\n}\n\n/**\n * A complete journey host in one call: the runner class with the app's store,\n * outlets and runtime plugged in, and the fetch handler that serves the ingest\n * API around them.\n *\n * const journeys = journeyWorker<Env>({\n * config, // workflow.config.ts (defineConfig)\n * store: (env, database) => postgresJsStore(env.HYPERDRIVE.connectionString, database),\n * });\n * export const MyRunner = journeys.Runner;\n * export default { fetch: journeys.fetch };\n */\n/** The Worker's environment failed its schema (`journeyWorker({ env })`). */\nexport class EnvValidationError extends Error {\n override readonly name = 'EnvValidationError';\n constructor(readonly issues: readonly string[]) {\n super(`the Worker's environment is invalid:\\n ${issues.join('\\n ')}`);\n }\n}\n\nexport function journeyWorker<Env extends JourneyEnv>(\n options: JourneyWorkerOptions<Env>\n): JourneyWorker<Env> {\n const { config } = options;\n // Parsed once per env object: the runtime hands the same one to every request of an isolate\n const checked = new WeakMap<object, Env>();\n const checkEnv = (env: Env): Env => {\n if (options.env === undefined) return env;\n const cached = checked.get(env);\n if (cached !== undefined) return cached;\n const result = z.safeParse(options.env, env);\n if (!result.success) {\n // Names and reasons only: the values are secrets\n throw new EnvValidationError(\n result.error.issues.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)\n );\n }\n const merged: Env = { ...env, ...result.data };\n checked.set(env, merged);\n return merged;\n };\n const bundle = () => compileConfig(config);\n const emails = config.templates?.email;\n const database = { schema: config.database?.schema ?? DEFAULT_SCHEMA };\n // In fast-forward, segment windows scale with the journeys reading them (time-scale.ts)\n const lease = (env: Env): Required<StoreLease> => {\n const opened = options.store === undefined ? d1Store(env) : options.store(env, database);\n const noop = async () => {};\n const factor = runtimeFromConfig(config, env).timeScale;\n return 'store' in opened\n ? { store: scaleSegmentWindows(opened.store, factor), close: opened.close ?? noop }\n : { store: scaleSegmentWindows(opened, factor), close: noop };\n };\n\n class Runner extends JourneyRunner {\n private readonly leases = new WeakMap<JourneyStore, () => Promise<void>>();\n private current: JourneyStore | undefined;\n\n protected override createStore(): JourneyStore {\n // A run fails at its start on an invalid environment, not at the send that needs it\n const { store, close } = lease(checkEnv(this.env as Env));\n this.leases.set(store, close);\n this.current = store;\n return store;\n }\n\n protected override async closeStore(store: JourneyStore): Promise<void> {\n await this.leases.get(store)?.();\n }\n\n protected override contract(): Contract | undefined {\n return config.contract;\n }\n\n protected override createMessageValidator(): MessageValidator {\n return registryValidator(config.templates ?? {});\n }\n\n protected override createMessageSender(): MessageSender {\n if (this.current === undefined) return super.createMessageSender();\n if (options.messages !== undefined) {\n return options.messages(checkEnv(this.env as Env), this.current);\n }\n return channelsFromConfig(config, checkEnv(this.env as Env), { store: this.current });\n }\n\n protected override createObserver(store: JourneyStore): JourneyObserver | undefined {\n if (options.observer !== undefined) return options.observer(checkEnv(this.env as Env), store);\n return store.reporting === undefined\n ? undefined\n : reportingObserver(store.reporting, checkEnv(this.env as Env).WORKFLOW_ORIGIN);\n }\n\n protected override runtime(params: JourneyParams): JourneyRuntimeOptions {\n const env = checkEnv(this.env as Env);\n if (options.runtime !== undefined) return options.runtime(env, params);\n return runtimeFromConfig(config, env);\n }\n\n protected override actions(): readonly RegisteredAction[] {\n // An action ref is a registered action structurally: the same object carries name, code and hash\n return config.actions ?? [];\n }\n }\n\n const json = (body: unknown, status = 200): Response =>\n new Response(JSON.stringify(body), {\n status,\n headers: { 'content-type': 'application/json' },\n });\n\n const authorized = (request: Request, env: Env): boolean =>\n env.API_TOKEN === undefined ||\n request.headers.get('authorization') === `Bearer ${env.API_TOKEN}`;\n\n const preview = emails === undefined ? undefined : registryRenderer(emails);\n\n const fetch = async (request: Request, rawEnv: Env, ctx?: ExecutionContextLike) => {\n // Everything below reads the validated environment (the schema's defaults applied)\n let env: Env;\n try {\n env = checkEnv(rawEnv);\n } catch (error) {\n if (!(error instanceof EnvValidationError)) throw error;\n console.error(`[workflow] ${error.message}`);\n return json({ error: error.message }, 500);\n }\n const url = new URL(request.url);\n const { store, close } = lease(env);\n try {\n // Opened by recipients, so no bearer: the routes only record, and redirect to recorded URLs\n if (url.pathname.startsWith('/t/') && store.reporting !== undefined) {\n const openedEvent = config.webPush?.openedEvent;\n const tracked = await handleTracking(request, store.reporting, {\n unsubscribe: config.emails?.unsubscribe,\n product: config.title,\n quickMs: config.emails?.tracking?.quickClickMs,\n pushOpened:\n openedEvent === undefined\n ? undefined\n : async ({ messageId, userId, at }) => {\n const payload: Record<string, unknown> = { message_id: messageId };\n const contract = config.contract;\n const verdict =\n contract === undefined ? undefined : checkEvent(contract, openedEvent, payload);\n if (verdict !== undefined && !verdict.ok) {\n throw new ContractValidationError(openedEvent, verdict.issues);\n }\n // Keyed by the copy: a second tap of the same notification is the same event\n await ingestEvent(\n env,\n {\n id: `${openedEvent}:${messageId}`,\n userId,\n event: openedEvent,\n payload: verdict?.data ?? payload,\n ts: at,\n },\n store\n );\n },\n });\n if (tracked !== undefined) return tracked;\n }\n if (url.pathname === '/segments/sweep') {\n if (!authorized(request, env)) return json({ error: 'unauthorized' }, 401);\n if (request.method !== 'POST') return json({ error: 'method not allowed' }, 405);\n return json(await sweepSegments(env, store));\n }\n if (url.pathname === '/bundle') {\n if (!authorized(request, env)) return json({ error: 'unauthorized' }, 401);\n if (request.method !== 'GET') return json({ error: 'method not allowed' }, 405);\n return json(bundle());\n }\n if (url.pathname === '/bundle/deploy') {\n if (!authorized(request, env)) return json({ error: 'unauthorized' }, 401);\n if (request.method !== 'POST') return json({ error: 'method not allowed' }, 405);\n try {\n return json(await deployBundle(env, bundle(), store));\n } catch (error) {\n if (!(error instanceof SchemaMismatchError)) throw error;\n return json({ error: error.message, problems: error.problems }, 503);\n }\n }\n if (emails !== undefined && preview !== undefined && url.pathname.startsWith('/preview/')) {\n if (!authorized(request, env)) return json({ error: 'unauthorized' }, 401);\n if (request.method !== 'GET') return json({ error: 'method not allowed' }, 405);\n const key = url.pathname.slice('/preview/'.length);\n if (!Object.hasOwn(emails, key))\n return json({ error: `unknown email template: ${key}` }, 404);\n // `?props=<JSON object>`: JSON keeps numbers and booleans typed, which query strings cannot\n let props: unknown = {};\n try {\n props = JSON.parse(url.searchParams.get('props') ?? '{}');\n } catch {\n return json({ error: 'props must be a JSON object' }, 400);\n }\n if (typeof props !== 'object' || props === null || Array.isArray(props)) {\n return json({ error: 'props must be a JSON object' }, 400);\n }\n try {\n const { subject, html } = await preview(key, props as Record<string, ScalarIR>);\n return new Response(html, {\n headers: { 'content-type': 'text/html; charset=utf-8', 'x-subject': subject },\n });\n } catch (error) {\n // The query string is the props: a refusal is the caller's to fix, with the field paths\n if (error instanceof TemplateValidationError) {\n return json({ error: error.message, issues: error.issues }, 400);\n }\n throw error;\n }\n }\n return await handleRequest(request, env, store, {\n contract: config.contract,\n mode: options.contractMode,\n });\n } finally {\n // After the response when the runtime offers waitUntil, inline otherwise (tests).\n if (ctx === undefined) await close();\n else ctx.waitUntil(close());\n }\n };\n\n const sweep = async (rawEnv: Env) => {\n const env = checkEnv(rawEnv);\n const { store, close } = lease(env);\n try {\n return await sweepSegments(env, store);\n } finally {\n await close();\n }\n };\n\n const scheduled = async (_controller: unknown, env: Env, ctx: ExecutionContextLike) => {\n ctx.waitUntil(sweep(env));\n };\n\n return { Runner, fetch, sweep, scheduled };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"worker.cjs","names":["z","compileConfig","d1Store","runtimeFromConfig","scaleSegmentWindows","JourneyRunner","registryValidator","channelsFromConfig","reportingObserver","registryRenderer","deployBundle","handleTracking","checkEvent","ContractValidationError","ingestEvent","sweepSegments","SchemaMismatchError","TemplateValidationError","handleRequest"],"sources":["../../src/cloudflare/worker.ts"],"sourcesContent":["import * as z from 'zod/mini';\nimport type * as core from 'zod/v4/core';\nimport { type JourneyConfig, compileConfig } from '../config';\nimport { type Contract, ContractValidationError, checkEvent } from '../contract';\nimport type { RegisteredAction } from '../engine/actions';\nimport type { JourneyObserver, MessageSender, MessageValidator } from '../engine/ports';\nimport { scaleSegmentWindows } from '../engine/time-scale';\nimport type { ScalarIR } from '../ir';\nimport { registryRenderer } from '../react-email';\nimport { reportingObserver } from '../reporting/observer';\nimport { handleTracking } from '../reporting/tracking';\nimport type { JourneyRuntimeOptions } from '../runtime';\nimport {\n type BundleVersion,\n type JourneyStore,\n SchemaMismatchError,\n type StoreLease,\n} from '../store/index';\nimport { DEFAULT_SCHEMA } from '../store/tables';\nimport { TemplateValidationError, registryValidator } from '../templates';\nimport type { JourneyEnv, JourneyParams } from './bindings';\nimport { channelsFromConfig, runtimeFromConfig } from './channels';\nimport { d1Store } from './d1-env';\nimport {\n type SweepResult,\n deployBundle,\n handleRequest,\n ingestEvent,\n sweepSegments,\n} from './router';\nimport { JourneyRunner } from './runner';\n\n/** The fetch handler's execution context (the subset used). */\nexport interface ExecutionContextLike {\n waitUntil(promise: Promise<unknown>): void;\n}\n\nexport interface JourneyWorkerOptions<Env extends JourneyEnv> {\n /**\n * The project's `workflow.config.ts` (`defineConfig`): what the Worker\n * deploys (`GET /bundle`, `POST /bundle/deploy` serve `compileConfig`), the\n * template registries the senders, the validator and `/preview` render from,\n * the contract every `/events` and `/identify` is checked against, the email\n * envelope, actions, and the runtime / APNs / FCM settings. Secrets come\n * from the env under the names `JourneyEnv` documents.\n */\n config: JourneyConfig;\n /**\n * The data plane, opened per request and per workflow run. Return a\n * `StoreLease` when the client must be released afterwards (a Postgres\n * connection); defaults to D1 + KV from the env's bindings. `database` is\n * the config's (`schema` defaulting to `workflow`), for a Postgres store:\n *\n * store: (env, database) => postgresJsStore(env.HYPERDRIVE.connectionString, database)\n */\n store?: (env: Env, database: { schema: string }) => JourneyStore | StoreLease;\n /**\n * Reporting hooks: node arrivals, every copy sent (with the tracking it should carry — click\n * links and an open pixel for email, payload data for a push), and how each instance ended.\n * By default a store that keeps reporting (Postgres) records them itself, and the Worker\n * serves the email tracking routes (`/t/...`, on `WORKFLOW_ORIGIN`); override to report\n * elsewhere.\n */\n observer?: (env: Env, store: JourneyStore) => JourneyObserver;\n /** Message outlet override (tests, a custom gateway); by default the channels the config describes. */\n messages?: (env: Env, store: JourneyStore) => MessageSender;\n /** Per-instance runtime options override (a tester in fast-forward); by default `config.runtime` for `ENVIRONMENT`. */\n runtime?: (env: Env, params: JourneyParams) => JourneyRuntimeOptions;\n /**\n * The host's environment, as a zod schema over its vars and secrets (bindings pass through\n * untouched): checked the first time the Worker runs, so a missing or malformed variable fails\n * there — every problem listed — not at the send that needs it. Webhook templates read their\n * URL from the parsed values: `webhook: (env: Env) => env.WEBHOOK_DISCORD_ALERTS`.\n */\n env?: core.$ZodType<object>;\n /** 'enforce' (default) answers an event or patch the contract refuses with a 400; 'report' only logs it. */\n contractMode?: 'enforce' | 'report';\n}\n\nexport interface JourneyWorker<Env extends JourneyEnv> {\n /** Export this under the `class_name` of the wrangler `workflows` binding. */\n Runner: new (...args: never[]) => JourneyRunner;\n /** The Worker's fetch handler: `/health`, `/events`, `/identify`, `/devices`, `/messages/opened`, `/segments/sweep`, `/t/*` (email tracking), `/deploy`, `/bundle`, `/bundle/deploy` and `/preview/*`. */\n fetch: (request: Request, env: Env, ctx?: ExecutionContextLike) => Promise<Response>;\n /**\n * The segment sweep (`sweepSegments`): membership of every deployed segment for every known\n * user, the journeys of segments entered by the clock alone (`not(performed(e.login, { within:\n * '7 days' }))` has no event to ride on), and each segment's size. Also served as\n * `POST /segments/sweep`.\n */\n sweep: (env: Env) => Promise<SweepResult>;\n /** The cron trigger's handler: runs the sweep. Export it beside `fetch` with a `triggers.crons` entry. */\n scheduled: (controller: unknown, env: Env, ctx: ExecutionContextLike) => Promise<void>;\n}\n\n/**\n * A complete journey host in one call: the runner class with the app's store,\n * outlets and runtime plugged in, and the fetch handler that serves the ingest\n * API around them.\n *\n * const journeys = journeyWorker<Env>({\n * config, // workflow.config.ts (defineConfig)\n * store: (env, database) => postgresJsStore(env.HYPERDRIVE.connectionString, database),\n * });\n * export const MyRunner = journeys.Runner;\n * export default { fetch: journeys.fetch };\n */\n/** The Worker's environment failed its schema (`journeyWorker({ env })`). */\nexport class EnvValidationError extends Error {\n override readonly name = 'EnvValidationError';\n constructor(readonly issues: readonly string[]) {\n super(`the Worker's environment is invalid:\\n ${issues.join('\\n ')}`);\n }\n}\n\nexport function journeyWorker<Env extends JourneyEnv>(\n options: JourneyWorkerOptions<Env>\n): JourneyWorker<Env> {\n const { config } = options;\n // Parsed once per env object: the runtime hands the same one to every request of an isolate\n const checked = new WeakMap<object, Env>();\n const checkEnv = (env: Env): Env => {\n if (options.env === undefined) return env;\n const cached = checked.get(env);\n if (cached !== undefined) return cached;\n const result = z.safeParse(options.env, env);\n if (!result.success) {\n // Names and reasons only: the values are secrets\n throw new EnvValidationError(\n result.error.issues.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)\n );\n }\n const merged: Env = { ...env, ...result.data };\n checked.set(env, merged);\n return merged;\n };\n const bundle = () => compileConfig(config);\n const emails = config.templates?.email;\n const database = { schema: config.database?.schema ?? DEFAULT_SCHEMA };\n // In fast-forward, segment windows scale with the journeys reading them (time-scale.ts)\n const lease = (env: Env): Required<StoreLease> => {\n const opened = options.store === undefined ? d1Store(env) : options.store(env, database);\n const noop = async () => {};\n const factor = runtimeFromConfig(config, env).timeScale;\n return 'store' in opened\n ? { store: scaleSegmentWindows(opened.store, factor), close: opened.close ?? noop }\n : { store: scaleSegmentWindows(opened, factor), close: noop };\n };\n\n class Runner extends JourneyRunner {\n private readonly leases = new WeakMap<JourneyStore, () => Promise<void>>();\n private current: JourneyStore | undefined;\n\n protected override createStore(): JourneyStore {\n // A run fails at its start on an invalid environment, not at the send that needs it\n const { store, close } = lease(checkEnv(this.env as Env));\n this.leases.set(store, close);\n this.current = store;\n return store;\n }\n\n protected override async closeStore(store: JourneyStore): Promise<void> {\n await this.leases.get(store)?.();\n }\n\n protected override contract(): Contract | undefined {\n return config.contract;\n }\n\n protected override createMessageValidator(): MessageValidator {\n return registryValidator(config.templates ?? {});\n }\n\n protected override createMessageSender(): MessageSender {\n if (this.current === undefined) return super.createMessageSender();\n if (options.messages !== undefined) {\n return options.messages(checkEnv(this.env as Env), this.current);\n }\n return channelsFromConfig(config, checkEnv(this.env as Env), { store: this.current });\n }\n\n protected override createObserver(store: JourneyStore): JourneyObserver | undefined {\n if (options.observer !== undefined) return options.observer(checkEnv(this.env as Env), store);\n return store.reporting === undefined\n ? undefined\n : reportingObserver(store.reporting, checkEnv(this.env as Env).WORKFLOW_ORIGIN);\n }\n\n protected override runtime(params: JourneyParams): JourneyRuntimeOptions {\n const env = checkEnv(this.env as Env);\n if (options.runtime !== undefined) return options.runtime(env, params);\n return runtimeFromConfig(config, env);\n }\n\n protected override actions(): readonly RegisteredAction[] {\n // An action ref is a registered action structurally: the same object carries name, code and hash\n return config.actions ?? [];\n }\n }\n\n const json = (body: unknown, status = 200): Response =>\n new Response(JSON.stringify(body), {\n status,\n headers: { 'content-type': 'application/json' },\n });\n\n const authorized = (request: Request, env: Env): boolean =>\n env.API_TOKEN === undefined ||\n request.headers.get('authorization') === `Bearer ${env.API_TOKEN}`;\n\n const preview = emails === undefined ? undefined : registryRenderer(emails);\n\n // The bundle compiled into this version goes live on its own, once per isolate: the first\n // request or cron run reads which version's bundle is live (one row) and applies this one only\n // over an older version, so one still serving during a rollout can never put its routes back.\n // A failure (say the migrations have not run) is logged and retried a minute later. Only the\n // request that starts the check gets its promise back: a request context cannot wait on\n // another's I/O.\n let applying: Promise<void> | undefined;\n let retryAt = 0;\n const applyOwnBundle = (env: Env): Promise<void> | undefined => {\n const metadata = env.CF_VERSION_METADATA;\n if (metadata === undefined || metadata.id === '') return undefined;\n const version: BundleVersion = { id: metadata.id, createdAt: Date.parse(metadata.timestamp) };\n if (Number.isNaN(version.createdAt)) return undefined;\n if (applying !== undefined || Date.now() < retryAt) return undefined;\n applying = (async () => {\n const { store, close } = lease(env);\n try {\n if (store.liveBundleVersion === undefined) return; // D1: POST /bundle/deploy\n const live = await store.liveBundleVersion();\n if (live !== undefined && (live.id === version.id || live.createdAt >= version.createdAt))\n return;\n const result = await deployBundle(env, bundle(), store, version);\n if (result.applied) console.log(`[workflow] version ${version.id} is live`);\n } finally {\n await close();\n }\n })().catch((error: unknown) => {\n applying = undefined;\n retryAt = Date.now() + 60_000;\n console.error(`[workflow] could not make version ${version.id} live`, error);\n });\n return applying;\n };\n\n const fetch = async (request: Request, rawEnv: Env, ctx?: ExecutionContextLike) => {\n // Everything below reads the validated environment (the schema's defaults applied)\n let env: Env;\n try {\n env = checkEnv(rawEnv);\n } catch (error) {\n if (!(error instanceof EnvValidationError)) throw error;\n console.error(`[workflow] ${error.message}`);\n return json({ error: error.message }, 500);\n }\n const pending = applyOwnBundle(env);\n // In the background where the runtime has waitUntil; inline otherwise (tests)\n if (pending !== undefined) {\n if (ctx === undefined) await pending;\n else ctx.waitUntil(pending);\n }\n const url = new URL(request.url);\n const { store, close } = lease(env);\n try {\n // Opened by recipients, so no bearer: the routes only record, and redirect to recorded URLs\n if (url.pathname.startsWith('/t/') && store.reporting !== undefined) {\n const openedEvent = config.webPush?.openedEvent;\n const tracked = await handleTracking(request, store.reporting, {\n unsubscribe: config.emails?.unsubscribe,\n product: config.title,\n quickMs: config.emails?.tracking?.quickClickMs,\n pushOpened:\n openedEvent === undefined\n ? undefined\n : async ({ messageId, userId, at }) => {\n const payload: Record<string, unknown> = { message_id: messageId };\n const contract = config.contract;\n const verdict =\n contract === undefined ? undefined : checkEvent(contract, openedEvent, payload);\n if (verdict !== undefined && !verdict.ok) {\n throw new ContractValidationError(openedEvent, verdict.issues);\n }\n // Keyed by the copy: a second tap of the same notification is the same event\n await ingestEvent(\n env,\n {\n id: `${openedEvent}:${messageId}`,\n userId,\n event: openedEvent,\n payload: verdict?.data ?? payload,\n ts: at,\n },\n store\n );\n },\n });\n if (tracked !== undefined) return tracked;\n }\n if (url.pathname === '/segments/sweep') {\n if (!authorized(request, env)) return json({ error: 'unauthorized' }, 401);\n if (request.method !== 'POST') return json({ error: 'method not allowed' }, 405);\n return json(await sweepSegments(env, store));\n }\n if (url.pathname === '/bundle') {\n if (!authorized(request, env)) return json({ error: 'unauthorized' }, 401);\n if (request.method !== 'GET') return json({ error: 'method not allowed' }, 405);\n return json(bundle());\n }\n if (url.pathname === '/bundle/deploy') {\n if (!authorized(request, env)) return json({ error: 'unauthorized' }, 401);\n if (request.method !== 'POST') return json({ error: 'method not allowed' }, 405);\n try {\n return json(await deployBundle(env, bundle(), store));\n } catch (error) {\n if (!(error instanceof SchemaMismatchError)) throw error;\n return json({ error: error.message, problems: error.problems }, 503);\n }\n }\n if (emails !== undefined && preview !== undefined && url.pathname.startsWith('/preview/')) {\n if (!authorized(request, env)) return json({ error: 'unauthorized' }, 401);\n if (request.method !== 'GET') return json({ error: 'method not allowed' }, 405);\n const key = url.pathname.slice('/preview/'.length);\n if (!Object.hasOwn(emails, key))\n return json({ error: `unknown email template: ${key}` }, 404);\n // `?props=<JSON object>`: JSON keeps numbers and booleans typed, which query strings cannot\n let props: unknown = {};\n try {\n props = JSON.parse(url.searchParams.get('props') ?? '{}');\n } catch {\n return json({ error: 'props must be a JSON object' }, 400);\n }\n if (typeof props !== 'object' || props === null || Array.isArray(props)) {\n return json({ error: 'props must be a JSON object' }, 400);\n }\n try {\n const { subject, html } = await preview(key, props as Record<string, ScalarIR>);\n return new Response(html, {\n headers: { 'content-type': 'text/html; charset=utf-8', 'x-subject': subject },\n });\n } catch (error) {\n // The query string is the props: a refusal is the caller's to fix, with the field paths\n if (error instanceof TemplateValidationError) {\n return json({ error: error.message, issues: error.issues }, 400);\n }\n throw error;\n }\n }\n return await handleRequest(request, env, store, {\n contract: config.contract,\n mode: options.contractMode,\n });\n } finally {\n // After the response when the runtime offers waitUntil, inline otherwise (tests).\n if (ctx === undefined) await close();\n else ctx.waitUntil(close());\n }\n };\n\n const sweep = async (rawEnv: Env) => {\n const env = checkEnv(rawEnv);\n const { store, close } = lease(env);\n try {\n return await sweepSegments(env, store);\n } finally {\n await close();\n }\n };\n\n const scheduled = async (_controller: unknown, env: Env, ctx: ExecutionContextLike) => {\n // A version with no traffic yet goes live here; the sweep then reads its segments\n ctx.waitUntil(\n (async () => {\n await applyOwnBundle(checkEnv(env));\n await sweep(env);\n })()\n );\n };\n\n return { Runner, fetch, sweep, scheduled };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"worker.d.cts","names":[],"sources":["../../src/cloudflare/worker.ts"],"mappings":";;;;;;;;;;iBA4BiB;EACf,UAAU,SAAS;;iBAGJ,qBAAqB,YAAY;;;;;;;;;EAShD,QAAQ;;;;;;;;;EASR,SAAS,KAAK,KAAK;IAAY;QAAqB,eAAe;;;;;;;;EAQnE,YAAY,KAAK,KAAK,OAAO,iBAAiB;;EAE9C,YAAY,KAAK,KAAK,OAAO,iBAAiB;;EAE9C,WAAW,KAAK,KAAK,QAAQ,kBAAkB;;;;;;;EAO/C,MAAM,KAAK;;EAEX;;iBAGe,cAAc,YAAY;;EAEzC,gBAAgB,kBAAkB;;EAElC,QAAQ,SAAS,SAAS,KAAK,KAAK,MAAM,yBAAyB,QAAQ;;;;;;;EAO3E,QAAQ,KAAK,QAAQ,QAAQ;;EAE7B,YAAY,qBAAqB,KAAK,KAAK,KAAK,yBAAyB;;;;;;;;;;;;;;;qBAgB9D,2BAA2B;WAEjB;WADH;EAClB,YAAqB;;wBAKP,cAAc,YAAY,YACxC,SAAS,qBAAqB,OAC7B,cAAc"}
1
+ {"version":3,"file":"worker.d.cts","names":[],"sources":["../../src/cloudflare/worker.ts"],"mappings":";;;;;;;;;;iBAiCiB;EACf,UAAU,SAAS;;iBAGJ,qBAAqB,YAAY;;;;;;;;;EAShD,QAAQ;;;;;;;;;EASR,SAAS,KAAK,KAAK;IAAY;QAAqB,eAAe;;;;;;;;EAQnE,YAAY,KAAK,KAAK,OAAO,iBAAiB;;EAE9C,YAAY,KAAK,KAAK,OAAO,iBAAiB;;EAE9C,WAAW,KAAK,KAAK,QAAQ,kBAAkB;;;;;;;EAO/C,MAAM,KAAK;;EAEX;;iBAGe,cAAc,YAAY;;EAEzC,gBAAgB,kBAAkB;;EAElC,QAAQ,SAAS,SAAS,KAAK,KAAK,MAAM,yBAAyB,QAAQ;;;;;;;EAO3E,QAAQ,KAAK,QAAQ,QAAQ;;EAE7B,YAAY,qBAAqB,KAAK,KAAK,KAAK,yBAAyB;;;;;;;;;;;;;;;qBAgB9D,2BAA2B;WAEjB;WADH;EAClB,YAAqB;;wBAKP,cAAc,YAAY,YACxC,SAAS,qBAAqB,OAC7B,cAAc"}
@@ -1 +1 @@
1
- {"version":3,"file":"worker.d.mts","names":[],"sources":["../../src/cloudflare/worker.ts"],"mappings":";;;;;;;;;;iBA4BiB;EACf,UAAU,SAAS;;iBAGJ,qBAAqB,YAAY;;;;;;;;;EAShD,QAAQ;;;;;;;;;EASR,SAAS,KAAK,KAAK;IAAY;QAAqB,eAAe;;;;;;;;EAQnE,YAAY,KAAK,KAAK,OAAO,iBAAiB;;EAE9C,YAAY,KAAK,KAAK,OAAO,iBAAiB;;EAE9C,WAAW,KAAK,KAAK,QAAQ,kBAAkB;;;;;;;EAO/C,MAAM,KAAK;;EAEX;;iBAGe,cAAc,YAAY;;EAEzC,gBAAgB,kBAAkB;;EAElC,QAAQ,SAAS,SAAS,KAAK,KAAK,MAAM,yBAAyB,QAAQ;;;;;;;EAO3E,QAAQ,KAAK,QAAQ,QAAQ;;EAE7B,YAAY,qBAAqB,KAAK,KAAK,KAAK,yBAAyB;;;;;;;;;;;;;;;qBAgB9D,2BAA2B;WAEjB;WADH;EAClB,YAAqB;;wBAKP,cAAc,YAAY,YACxC,SAAS,qBAAqB,OAC7B,cAAc"}
1
+ {"version":3,"file":"worker.d.mts","names":[],"sources":["../../src/cloudflare/worker.ts"],"mappings":";;;;;;;;;;iBAiCiB;EACf,UAAU,SAAS;;iBAGJ,qBAAqB,YAAY;;;;;;;;;EAShD,QAAQ;;;;;;;;;EASR,SAAS,KAAK,KAAK;IAAY;QAAqB,eAAe;;;;;;;;EAQnE,YAAY,KAAK,KAAK,OAAO,iBAAiB;;EAE9C,YAAY,KAAK,KAAK,OAAO,iBAAiB;;EAE9C,WAAW,KAAK,KAAK,QAAQ,kBAAkB;;;;;;;EAO/C,MAAM,KAAK;;EAEX;;iBAGe,cAAc,YAAY;;EAEzC,gBAAgB,kBAAkB;;EAElC,QAAQ,SAAS,SAAS,KAAK,KAAK,MAAM,yBAAyB,QAAQ;;;;;;;EAO3E,QAAQ,KAAK,QAAQ,QAAQ;;EAE7B,YAAY,qBAAqB,KAAK,KAAK,KAAK,yBAAyB;;;;;;;;;;;;;;;qBAgB9D,2BAA2B;WAEjB;WADH;EAClB,YAAqB;;wBAKP,cAAc,YAAY,YACxC,SAAS,qBAAqB,OAC7B,cAAc"}
@@ -107,6 +107,34 @@ function journeyWorker(options) {
107
107
  });
108
108
  const authorized = (request, env) => env.API_TOKEN === void 0 || request.headers.get("authorization") === `Bearer ${env.API_TOKEN}`;
109
109
  const preview = emails === void 0 ? void 0 : registryRenderer(emails);
110
+ let applying;
111
+ let retryAt = 0;
112
+ const applyOwnBundle = (env) => {
113
+ const metadata = env.CF_VERSION_METADATA;
114
+ if (metadata === void 0 || metadata.id === "") return void 0;
115
+ const version = {
116
+ id: metadata.id,
117
+ createdAt: Date.parse(metadata.timestamp)
118
+ };
119
+ if (Number.isNaN(version.createdAt)) return void 0;
120
+ if (applying !== void 0 || Date.now() < retryAt) return void 0;
121
+ applying = (async () => {
122
+ const { store, close } = lease(env);
123
+ try {
124
+ if (store.liveBundleVersion === void 0) return;
125
+ const live = await store.liveBundleVersion();
126
+ if (live !== void 0 && (live.id === version.id || live.createdAt >= version.createdAt)) return;
127
+ if ((await deployBundle(env, bundle(), store, version)).applied) console.log(`[workflow] version ${version.id} is live`);
128
+ } finally {
129
+ await close();
130
+ }
131
+ })().catch((error) => {
132
+ applying = void 0;
133
+ retryAt = Date.now() + 6e4;
134
+ console.error(`[workflow] could not make version ${version.id} live`, error);
135
+ });
136
+ return applying;
137
+ };
110
138
  const fetch = async (request, rawEnv, ctx) => {
111
139
  let env;
112
140
  try {
@@ -116,6 +144,11 @@ function journeyWorker(options) {
116
144
  console.error(`[workflow] ${error.message}`);
117
145
  return json({ error: error.message }, 500);
118
146
  }
147
+ const pending = applyOwnBundle(env);
148
+ if (pending !== void 0) {
149
+ if (ctx === void 0) await pending;
150
+ else ctx.waitUntil(pending);
151
+ }
119
152
  const url = new URL(request.url);
120
153
  const { store, close } = lease(env);
121
154
  try {
@@ -209,7 +242,10 @@ function journeyWorker(options) {
209
242
  }
210
243
  };
211
244
  const scheduled = async (_controller, env, ctx) => {
212
- ctx.waitUntil(sweep(env));
245
+ ctx.waitUntil((async () => {
246
+ await applyOwnBundle(checkEnv(env));
247
+ await sweep(env);
248
+ })());
213
249
  };
214
250
  return {
215
251
  Runner,