@shware/workflow 9.0.1 → 9.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/engine/time-scale.cjs +33 -11
- package/dist/engine/time-scale.cjs.map +1 -1
- package/dist/engine/time-scale.d.cts +12 -6
- package/dist/engine/time-scale.d.cts.map +1 -1
- package/dist/engine/time-scale.d.mts +12 -6
- package/dist/engine/time-scale.d.mts.map +1 -1
- package/dist/engine/time-scale.mjs +33 -11
- package/dist/engine/time-scale.mjs.map +1 -1
- package/dist/host/ingest.cjs +86 -3
- package/dist/host/ingest.cjs.map +1 -1
- package/dist/host/ingest.d.cts +30 -3
- package/dist/host/ingest.d.cts.map +1 -1
- package/dist/host/ingest.d.mts +30 -3
- package/dist/host/ingest.d.mts.map +1 -1
- package/dist/host/ingest.mjs +85 -4
- package/dist/host/ingest.mjs.map +1 -1
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +20 -0
- package/dist/store/index.d.cts.map +1 -1
- package/dist/store/index.d.mts +20 -0
- package/dist/store/index.d.mts.map +1 -1
- package/dist/store/index.mjs.map +1 -1
- package/dist/store/postgres.cjs +14 -0
- package/dist/store/postgres.cjs.map +1 -1
- package/dist/store/postgres.d.cts +3 -1
- package/dist/store/postgres.d.cts.map +1 -1
- package/dist/store/postgres.d.mts +3 -1
- package/dist/store/postgres.d.mts.map +1 -1
- package/dist/store/postgres.mjs +14 -0
- package/dist/store/postgres.mjs.map +1 -1
- package/package.json +1 -1
package/dist/store/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/store/index.ts"],"sourcesContent":["import type { PushDevice } from '../engine/ports';\nimport type { BundleIR, ConditionIR, ScalarIR, WorkflowIR } from '../ir';\n\n/**\n * The journey engine's data plane. Postgres is the store (`PostgresJourneyStore`); this\n * interface is the port the host, the runner and the fact source use to persist or look\n * anything up, and what a test stub implements in its place. Execution (durable steps,\n * sleeps, wake-ups) is the workflow runtime's job and stays out of here.\n *\n * Timestamps are epoch milliseconds throughout; a store may persist them in\n * its native type as long as `sinceMs` comparisons behave.\n */\n\n/**\n * A profile patch or a stored profile. On the way in, `null` means \"remove\n * this property\" (RFC 7386 merge-patch, the only way the API has to unset\n * one); a stored profile never contains null.\n */\nexport type ProfileProps = Record<string, ScalarIR | null | undefined>;\n\nexport interface EventInput {\n /** The occurrence's identity, chosen by the sender; a second insert with the same id is a no-op. */\n id: string;\n userId: string;\n event: string;\n ts: number;\n payload: Record<string, unknown>;\n}\n\nexport interface TriggerRoute {\n workflow: string;\n hash: string;\n /** Payload gate (payload-only condition tree), or null when the trigger has none. */\n where: ConditionIR | null;\n /** Profile gate, or null. */\n filter: ConditionIR | null;\n /** Whether a visitor not signed in may enter. */\n visitors: boolean;\n /**\n * The code version that applied these routes; absent after a hand-posted `POST /deploy`. Read\n * in the same row as the hash, so the pair is always one deploy's.\n */\n version?: string | undefined;\n}\n\nexport interface SegmentTriggerRoute {\n workflow: string;\n hash: string;\n segment: string;\n /** As `TriggerRoute.version`. */\n version?: string | undefined;\n}\n\n/**\n * A Worker version, as Cloudflare's version metadata binding names it: the bundle compiled into\n * that version goes live only over an older one (`replaceRoutesIfNewer`).\n */\nexport interface BundleVersion {\n id: string;\n /** When the version was created (epoch ms): later versions are newer. */\n createdAt: number;\n}\n\nexport interface EntryInput {\n workflow: string;\n userId: string;\n instanceId: string;\n hash: string;\n ts: number;\n /**\n * The instance's start, recorded in the same transaction as the entry — for a runtime whose\n * start is an asynchronous submit (AWS) that may have to be retried.\n */\n execution?: ExecutionStart | undefined;\n}\n\n/** How an instance is started on a runtime that submits it: replayed as is on every retry. */\nexport interface ExecutionStart {\n /** The runtime's idempotency key (the durable execution name). */\n name: string;\n /** What is invoked: a qualified function ARN, its version included. */\n target: string;\n /** The exact payload: a retried submit must send the same bytes, or the runtime refuses it. */\n input: string;\n}\n\nexport interface ExecutionRecord extends ExecutionStart {\n instanceId: string;\n workflow: string;\n userId: string;\n createdAt: number;\n submittedAt?: number | undefined;\n arn?: string | undefined;\n unresolvedAt?: number | undefined;\n callbackId?: string | undefined;\n /** Bumped by every wait the instance arms: a wake or a cleanup of an older one leaves it alone. */\n generation: number;\n deadlineAt?: number | undefined;\n checkedAt?: number | undefined;\n stoppedAt?: number | undefined;\n}\n\n/**\n * What a runtime that submits its starts and parks waits on one-shot callbacks (AWS) keeps\n * beside the entry ledger. The runtime holds the execution history; this holds only the\n * mapping from the engine's instance id to it.\n */\nexport interface ExecutionRegistry {\n get(instanceId: string): Promise<ExecutionRecord | undefined>;\n /**\n * Starts not yet confirmed and not given up on, oldest first: of one (workflow, user) entry\n * still running, or every one created before `createdBefore`.\n */\n pendingStarts(filter: {\n workflow?: string;\n userId?: string;\n createdBefore?: number;\n limit: number;\n }): Promise<ExecutionRecord[]>;\n markSubmitted(instanceId: string, arn: string | undefined, at: number): Promise<void>;\n markUnresolved(instanceId: string, at: number): Promise<void>;\n setArn(instanceId: string, arn: string): Promise<void>;\n /** Record the callback a wait parks on; false when a newer generation is already armed. */\n armCallback(\n instanceId: string,\n callback: { id: string; generation: number; deadlineAt?: number | undefined }\n ): Promise<boolean>;\n /** Forget the callback — only if it is still that generation's. */\n clearCallback(instanceId: string, generation: number): Promise<void>;\n /** Submitted executions whose entry is still running, least recently checked first. */\n toReconcile(limit: number, checkedBefore: number): Promise<ExecutionRecord[]>;\n markChecked(instanceId: string, at: number): Promise<void>;\n /** Record the first stop; a later one keeps it. */\n markStopped(instanceId: string, at: number): Promise<void>;\n}\n\n/**\n * How an entry attempt ended: a fresh row, a 'failed' row of a dead instance\n * taken over (the once policy does not bar a user over one outage), or\n * refused because the pair already entered and is still alive.\n */\nexport type EntryOutcome = 'entered' | 'reclaimed' | null;\n\n/** A device registered for pushes: its provider token belongs to the user who registered it last. */\nexport interface DeviceInput {\n deviceId?: string | undefined;\n /** Null until the device's visitor signs in. */\n userId: string | null;\n visitorId?: string | undefined;\n token: string;\n /** `ios` (APNs) or `android` (FCM): which provider the token belongs to. */\n platform: string;\n at: number;\n}\n\n/** What linking a visitor to a user did; null when it was already linked, or to another user. */\nexport interface VisitorMerge {\n /** Instances dropped because the user is already in the same workflow: to stop. */\n dropped: string[];\n /** The event names and profile properties the user gained: their waits must wake. */\n events: string[];\n props: string[];\n}\n\n/** An instance of a user still in the ledger, and whether its runner was admitted (see `admitInstance`). */\nexport interface RunningInstance {\n instanceId: string;\n /** False until the runner admitted itself: nothing has run, and the tombstone keeps it that way. */\n admitted: boolean;\n}\n\n/** One (workflow, user) entry: the attempt it records and how far that attempt got. */\nexport interface EntryState {\n instanceId: string;\n /** 'running' until the run finalizes it ('completed', 'goal', 'failed', …). */\n status: string;\n /** The runner admitted itself (`admitInstance`): the instance did reach the runtime. */\n admitted: boolean;\n}\n\n/** An entry still waiting for its first admission (`unadmittedEntries`). */\nexport interface UnadmittedEntry {\n instanceId: string;\n workflow: string;\n userId: string;\n}\n\n/** A store plus how to release what opening it acquired (a connection, say). */\nexport interface StoreLease {\n store: JourneyStore;\n close?: () => Promise<void>;\n}\n\nimport type { ErasureState } from './erasure';\n\nexport { type ErasureState, UserErasedError } from './erasure';\nexport { instanceOwner, isVisitorSubject, visitorIdOf, visitorSubject } from './identity';\n\n/**\n * The live database lacks tables or columns the engine reads (an SDK upgrade added them, or the\n * host never ran the migration). Raised before a deploy writes anything.\n */\nexport class SchemaMismatchError extends Error {\n override readonly name = 'SchemaMismatchError';\n\n constructor(readonly problems: readonly string[]) {\n super(\n `the database does not match @shware/workflow's tables — regenerate the drizzle schema (workflow-ui generate) and run your migrations:\\n ${problems.join('\\n ')}`\n );\n }\n}\n\n/** A message copy as reporting records it, before it goes out. */\nexport interface MessageRecord {\n idempotencyKey: string;\n workflow: string;\n instanceId: string;\n nodeId: string;\n template: string;\n /** Where the copy lands: `email`, `push_ios`, `push_android`. */\n channel: string;\n userId: string;\n /** An email's subscription topic; null for a transactional email and for every other channel. */\n topic: string | null;\n sentAt: number;\n}\n\n/** What is known of a copy without its engagement: whose it is, where it went, its topic. */\nexport interface MessageSummary {\n userId: string;\n channel: string;\n topic: string | null;\n}\n\n/** One opt-out to set or clear: a topic of the channel, or the whole channel (`topic: null`). */\nexport interface SuppressionChange {\n topic: string | null;\n suppressed: boolean;\n}\n\n/** What a click is judged by. */\nexport interface ClickContext {\n url: string;\n deliveredAt: number;\n linkCount: number;\n /** The copy's clicks in the window before this one. */\n recent: { position: number; at: number }[];\n}\n\n/** An open or a click, with the verdict on who made it and where it came from. */\nexport interface Engagement {\n at: number;\n machine: boolean;\n /** The rule that called it a machine; null for a person. */\n reason: string | null;\n userAgent: string | null;\n /** The network the request came from (Cloudflare's `cf.asn`, `cf.asOrganization`, `cf.country`), never the address. */\n asn: number | null;\n organization: string | null;\n country: string | null;\n}\n\n/**\n * Journey reporting: where each instance stands, every copy and how it was engaged with, segment\n * sizes. The store exposes it as `reporting`; the host records it and serves email tracking.\n * Every write is idempotent — a retried step reports again.\n */\n/** Bounds on when an event happened, both inclusive (epoch ms); an absent side is open. */\nexport interface EventWindow {\n sinceMs?: number;\n untilMs?: number;\n}\n\n/**\n * Where a page of events ended: the last row's timestamp and id, the keyset `(ts, id)` the next\n * page starts after. Opaque to callers; `ts` is the store's own rendering of the timestamp, so a\n * round trip loses no precision.\n */\nexport interface EventCursor {\n ts: string;\n id: string;\n}\n\nexport interface JourneyReporting {\n /** The instance entered: its row exists from here, so one a guard ends at once still counts. */\n instanceStarted(input: {\n workflow: string;\n instanceId: string;\n userId: string;\n at: number;\n }): Promise<void>;\n nodeReached(input: {\n workflow: string;\n instanceId: string;\n userId: string;\n nodeId: string;\n at: number;\n }): Promise<void>;\n instanceFinished(input: {\n workflow: string;\n instanceId: string;\n userId: string;\n status: string;\n at: number;\n convertedAt?: number;\n }): Promise<void>;\n /** The copy's row id; the row a retry already wrote, when there is one. */\n recordMessage(message: MessageRecord): Promise<string>;\n /**\n * Whether the copy was settled (`settleMessage`: delivered, or settled undelivered) — read from\n * the database itself, so a replayed send step never trusts a cache that missed the settle.\n */\n messageSettled(idempotencyKey: string): Promise<boolean>;\n settleMessage(\n idempotencyKey: string,\n result: { delivered: true } | { delivered: false; reason: string },\n at: number\n ): Promise<void>;\n /** An email's tracked links, in document order. */\n recordLinks(messageId: string, urls: readonly string[]): Promise<void>;\n /**\n * Record an open of the copy, judged by `judge` against the copy (its recipient and when it was\n * delivered — when it was sent, before the transport answered); `undefined` from the judge\n * records nothing. Read and written in one transaction. False for an unknown copy.\n */\n recordOpen(\n messageId: string,\n judge: (message: { userId: string; deliveredAt: number }) => Engagement | undefined\n ): Promise<boolean>;\n /**\n * Record a click on the copy's link at `position`, judged by `judge` against the link, how\n * many links the copy has and its clicks of the last `windowMs`. The copy is locked for the\n * transaction, so clicks arriving together (a scanner following every link at once) are\n * judged one after another, each seeing the ones before. When the judge asks to `backdate`,\n * the window's earlier clicks are reclassified with this one's verdict. A click implies an\n * open. Undefined for an unknown link.\n */\n recordClick(\n messageId: string,\n position: number,\n at: number,\n windowMs: number,\n judge: (link: ClickContext) => { engagement: Engagement; backdate: boolean }\n ): Promise<{ url: string; engagement: Engagement } | undefined>;\n /**\n * The email copy's recipient opted out: of the copy's topic, or with `scope: 'all'` of every\n * email. Recorded on the copy and as a suppression (the topic's row, or the channel-wide\n * null-topic row), in one transaction, which also yields who the recipient is and the topic —\n * so the caller acts on the unsubscribe without a second read. A transactional copy\n * (`topic: null`) has nothing to opt out of: nothing is recorded, and the answer says so.\n * Undefined for an unknown copy.\n */\n recordUnsubscribe(\n messageId: string,\n at: number,\n scope?: 'topic' | 'all'\n ): Promise<{ userId: string; topic: string | null } | undefined>;\n /** The copy's owner, channel and topic; undefined for an unknown copy. */\n lookupMessage(messageId: string): Promise<MessageSummary | undefined>;\n /** A push copy the device reported it received. False for an unknown copy. */\n recordDeviceDelivered(messageId: string, at: number): Promise<boolean>;\n /** Drop members of segments no longer deployed, then record every deployed segment's size. */\n snapshotSegments(at: number): Promise<{ segment: string; size: number }[]>;\n}\n\n/** A user the segment sweep has to evaluate, and for which segments. */\nexport interface SegmentCandidate {\n userId: string;\n segments: string[];\n}\n\nexport interface JourneyStore {\n /** Where instances stand, every copy and its engagement, segment sizes. */\n readonly reporting: JourneyReporting;\n\n /**\n * Record that the user's data must go (a deleted account): the tombstone every write about\n * the user is conditional on, from here on and for good. Committed before anything is\n * stopped or erased, so an entry that got in before it is found by the erasure and none gets\n * in after it. Idempotent: a second request, or one for a user already erased, changes nothing.\n */\n requestErasure(userId: string, at: number): Promise<void>;\n /** Users whose erasure was requested and not carried out yet (`'erasing'`), oldest first. */\n pendingErasures(): Promise<string[]>;\n /**\n * Where the user stands: `'none'` (no tombstone), `'erasing'` (requested, their instances\n * being stopped, data still there) or `'erased'` (`forgetUser` went through). Both\n * tombstone states refuse every write about the user; the entry points read this for a fast\n * answer, and the store's conditional writes are the gate itself.\n */\n erasureState(userId: string): Promise<ErasureState>;\n /** The user's journey instances still running, by the entry ledger. */\n runningInstances(userId: string): Promise<RunningInstance[]>;\n /**\n * The runner's admission, before its first observable effect: records that the instance runs\n * (`admitted_at`, kept on replay) — unless the user has an erasure tombstone, in which case\n * nothing is recorded and the runner must not run. Mutually exclusive with `requestErasure`\n * and `forgetUser` for that user (Postgres: the user's advisory lock), so an erasure either\n * finds the instance admitted and stops it, or commits its tombstone first and the admission\n * is refused — never neither.\n *\n * A first admission needs the entry running. An instance already admitted is admitted again\n * whatever the entry's status — the legitimate replay of an instance whose finalisation was\n * cut short after the entry reached its terminal status, which resumes on its own checkpoint\n * and finishes what is left. Never a new attempt's way in: false for a terminal entry never\n * admitted, for an entry no longer in the ledger, for another user's entry, and for an\n * instance whose entry was reclaimed since (`enterJourney` resets the admission with the\n * instance id, so a superseded attempt cannot pass under the id it lost).\n */\n admitInstance(instanceId: string, userId: string): Promise<boolean>;\n /**\n * Erase everything kept about the user — profile, devices, events, wake subscriptions, segment\n * memberships, entries and reporting — and mark the tombstone erased (`erased_at`), which\n * stays: a write that was in flight when the data went cannot bring the user back, and the\n * same id is never reactivated by ingest — which is also why the suppressions go with the\n * rest: nothing is ever sent to the id again, so no opt-out needs keeping. Run only once no\n * instance of theirs runs any more, or one could write some of it back. Idempotent. Mutually\n * exclusive with every write about the user (the user's advisory lock), so the data is gone\n * when this returns — nothing of theirs lands afterwards.\n */\n forgetUser(userId: string, at: number): Promise<void>;\n\n /**\n * The user opted out of the channel (recorded by an unsubscribe, or set through\n * `/subscriptions`): with a `topic`, an opt-out of that topic or of the whole channel; without\n * one, only the whole channel.\n */\n isSuppressed(userId: string, channel: string, topic?: string): Promise<boolean>;\n /** Every opt-out of the user on the channel: the topics, and `null` for the whole channel. */\n listSuppressions(userId: string, channel: string): Promise<(string | null)[]>;\n /**\n * Set or clear opt-outs, all in one transaction: a suppressed change inserts the row (keeping\n * the time of one already there), an unsuppressed one deletes it. Written whatever the user's\n * erasure state — the caller refuses an erased user first — since an opt-out is never refused.\n */\n updateSuppressions(\n userId: string,\n channel: string,\n changes: readonly SuppressionChange[],\n at: number\n ): Promise<void>;\n\n /**\n * What the live database lacks against the engine's tables (read-only); a deploy refuses to\n * run while it lists anything.\n */\n checkSchema(): Promise<string[]>;\n\n /* ---------------------------------- events --------------------------------- */\n /**\n * Append the occurrence; false when its id was already in the log (nothing written). Throws\n * `UserErasedError` for a user with a tombstone, like every write about a user below.\n */\n insertEvent(input: EventInput): Promise<boolean>;\n /**\n * Occurrences of `event` for the user, optionally only those within the window. With\n * `atLeast`, counting may stop there: the result is exact below it and some number ≥ it\n * otherwise — all a `performed(…, { count })` check needs, without counting a heavy user's\n * whole history.\n */\n countEvents(\n userId: string,\n event: string,\n window?: EventWindow,\n atLeast?: number\n ): Promise<number>;\n /**\n * One page of those occurrences' payloads, oldest first — the caller filters them with the\n * shared evaluator and pages on with `next` only while it still needs matches, so a `where`\n * never loads a user's whole history at once. `next` is absent on the last page.\n */\n pageEventPayloads(\n userId: string,\n event: string,\n window: EventWindow | undefined,\n page: { limit: number; after?: EventCursor | undefined }\n ): Promise<{ payloads: Record<string, unknown>[]; next?: EventCursor | undefined }>;\n\n /* -------------------------------- visitors --------------------------------- */\n /** The user a visitor became, if they did. */\n linkedUser(visitorId: string): Promise<string | undefined>;\n /** The visitor was heard from: who they are now, the user they became or their own subject. */\n touchVisitor(visitorId: string, at: number): Promise<string>;\n /**\n * Forget visitors who never signed in and were last heard from before `before`, with no journey\n * running: up to `limit` of them, leaving no tombstone. Answers how many.\n */\n pruneVisitors(before: number, limit: number): Promise<number>;\n /**\n * The visitor became the user: their events, profile, devices, opt-outs and journeys move to the\n * user, once. A visitor already linked keeps its first user. The user's own entry wins over the\n * visitor's in the same workflow, and the visitor's instance is dropped.\n */\n linkVisitor(visitorId: string, userId: string, at: number): Promise<VisitorMerge | null>;\n /** Who an instance's entry belongs to now. */\n entryOwner(instanceId: string): Promise<string | undefined>;\n\n /* --------------------------------- profiles -------------------------------- */\n getProfile(userId: string): Promise<ProfileProps | undefined>;\n /**\n * Merge-patch `props` into the stored profile (a null removes the property) and return the\n * result, atomically: two concurrent identifies cannot lose each other's keys. `at` is when the\n * values were read (now by default): a property written with a later `at` keeps its value, so\n * a redelivered patch cannot undo a newer one.\n */\n mergeProfile(userId: string, props: ProfileProps, at?: number): Promise<ProfileProps>;\n\n /**\n * Who the segment sweep has to evaluate: for each user the engine knows (a profile, an event or\n * a membership), the segments whose membership may have to change, or whose entry still owes a\n * start. It may name a user whose membership turns out right, since `evaluateCondition` decides;\n * it must not leave out one whose membership is wrong. Users come in ascending order; `after`\n * resumes past the last id of the previous page, and users with nothing to do are left out.\n */\n listSegmentCandidates(page: {\n segments: readonly { id: string; condition: ConditionIR }[];\n now: number;\n after?: string | undefined;\n limit: number;\n }): Promise<SegmentCandidate[]>;\n\n /* ---------------------------------- devices --------------------------------- */\n /** The push devices a user or visitor holds now, most recently registered first. */\n listDevices(subject: string): Promise<PushDevice[]>;\n /**\n * Register a device: one row per token and per device, holding only its current owner. A token\n * registered again moves to whoever registered it last (a phone signed into another account) and\n * to the front of their list; a device's new token replaces its old one. Answers the previous\n * owners, whose device waits must wake.\n */\n upsertDevice(device: DeviceInput): Promise<string[]>;\n /** Forget a token (reported dead by APNs / FCM); the user or visitor it belonged to, if any. */\n removeDevice(token: string): Promise<string | undefined>;\n /**\n * The user signed out on the device: the token is kept, bound to nobody, until the next\n * registration binds it again. Who it belonged to, if anyone.\n */\n unbindDevice(token: string): Promise<string | undefined>;\n\n /* ---------------------------- deployed definitions -------------------------- */\n getSegmentCondition(id: string): Promise<ConditionIR | undefined>;\n /** Every deployed segment with its definition: what the segment sweep evaluates. */\n listSegments(): Promise<{ id: string; condition: ConditionIR }[]>;\n findTriggers(event: string): Promise<TriggerRoute[]>;\n listSegmentTriggers(): Promise<SegmentTriggerRoute[]>;\n putWorkflowIR(workflow: WorkflowIR): Promise<void>;\n getWorkflowIR(contentHash: string): Promise<WorkflowIR | undefined>;\n /**\n * Swap the routing tables (segments, event triggers, segment triggers) for\n * the bundle's — atomically, so a concurrent ingest never sees them half\n * empty. Workflow bodies are stored separately (`putWorkflowIR`) because they\n * are content-addressed and outlive the routes that point at them.\n *\n * With `version` — a forced apply of the code's own bundle (`POST /bundle/deploy`) — the\n * version is recorded too, unconditionally and in the same transaction, so the live version\n * always names the code the routes came from. Without it (a hand-posted `POST /deploy`) the\n * recorded version stays as it was: that bundle holds until a newer code version applies its own.\n */\n replaceRoutes(bundle: BundleIR, version?: BundleVersion): Promise<void>;\n /**\n * The Worker version whose bundle the routes came from, when one put them there (through\n * `replaceRoutesIfNewer`, or a forced `replaceRoutes` with a version). Read from the database\n * itself, not a cache in front of it.\n */\n liveBundleVersion(): Promise<BundleVersion | undefined>;\n /**\n * `replaceRoutes`, only when `version` is newer than the live one, recorded in the same\n * transaction: two versions serving side by side during a rollout cannot leave the older one's\n * routes in force. False, with nothing written, when a version as new or newer is live.\n */\n replaceRoutesIfNewer(bundle: BundleIR, version: BundleVersion): Promise<boolean>;\n\n /* ----------------------------- segment membership --------------------------- */\n isSegmentMember(segment: string, userId: string): Promise<boolean>;\n /** Record the membership, with the workflows its entry has to start (see segment_start). */\n addSegmentMember(\n segment: string,\n userId: string,\n ts: number,\n workflows: readonly string[]\n ): Promise<void>;\n /** The workflows the member's entry has not started yet. */\n pendingSegmentStarts(segment: string, userId: string): Promise<string[]>;\n completeSegmentStart(segment: string, userId: string, workflow: string): Promise<void>;\n /** Drop the membership and any start it still owed. */\n removeSegmentMember(segment: string, userId: string): Promise<void>;\n\n /* ------------------------------- entry ledger ------------------------------- */\n /**\n * Record the entry (the once policy). Atomic under concurrent ingests: of\n * two racing attempts exactly one is 'entered' (or 'reclaimed', when the\n * pair's previous instance is recorded as 'failed') and the other null.\n */\n enterJourney(entry: EntryInput): Promise<EntryOutcome>;\n removeEntry(instanceId: string): Promise<void>;\n setEntryStatus(instanceId: string, status: string): Promise<void>;\n /** The (workflow, user) entry as it stands, read from the database itself; undefined if none. */\n getEntry(workflow: string, userId: string): Promise<EntryState | undefined>;\n /**\n * Running entries no runner has admitted, claimed before `claimedBefore` (epoch ms), oldest\n * first: candidates for a start that was lost between the claim and the platform's create.\n */\n unadmittedEntries(claimedBefore: number, limit: number): Promise<UnadmittedEntry[]>;\n /**\n * Mark a lost start `failed`, so the pair's next trigger reclaims it. Only while the entry is\n * still running and unadmitted under this instance id; false when it moved on meanwhile.\n */\n failUnadmittedEntry(instanceId: string): Promise<boolean>;\n /** Execution starts and wake callbacks, for a runtime that submits its starts (AWS). */\n readonly executions: ExecutionRegistry;\n\n /* ------------------------------ wake subscriptions --------------------------- */\n /** Distinct wake handles registered for (user, event). */\n findWakeHandles(userId: string, event: string): Promise<string[]>;\n /** Register the handle for each event; repeated registrations are idempotent. */\n subscribe(handle: string, userId: string, events: readonly string[], ts: number): Promise<void>;\n /** Drop every registration the handle holds. */\n unsubscribe(handle: string): Promise<void>;\n}\n\nexport { JourneyFactSource } from './facts';\nexport { PostgresJourneyStore } from './postgres';\nexport { PostgresReporting } from './postgres-reporting';\nexport {\n DEFAULT_SCHEMA,\n TABLES,\n renderDrizzleSchema,\n renderPostgresDdl,\n type TableSpec,\n type UniqueConstraintSpec,\n} from './tables';\nexport type { PostgresJourneyStoreOptions, SqlClientLike } from './postgres';\n"],"mappings":";;;;;;;;;;;AA0MA,IAAa,sBAAb,cAAyC,MAAM;CAGxB;CAFrB,OAAyB;CAEzB,YAAY,UAAsC;EAChD,MACE,4IAA4I,SAAS,KAAK,MAAM,GAClK;EAHmB,KAAA,WAAA;CAIrB;AACF"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/store/index.ts"],"sourcesContent":["import type { PushDevice } from '../engine/ports';\nimport type { BundleIR, ConditionIR, ScalarIR, WorkflowIR } from '../ir';\n\n/**\n * The journey engine's data plane. Postgres is the store (`PostgresJourneyStore`); this\n * interface is the port the host, the runner and the fact source use to persist or look\n * anything up, and what a test stub implements in its place. Execution (durable steps,\n * sleeps, wake-ups) is the workflow runtime's job and stays out of here.\n *\n * Timestamps are epoch milliseconds throughout; a store may persist them in\n * its native type as long as `sinceMs` comparisons behave.\n */\n\n/**\n * A profile patch or a stored profile. On the way in, `null` means \"remove\n * this property\" (RFC 7386 merge-patch, the only way the API has to unset\n * one); a stored profile never contains null.\n */\nexport type ProfileProps = Record<string, ScalarIR | null | undefined>;\n\nexport interface EventInput {\n /** The occurrence's identity, chosen by the sender; a second insert with the same id is a no-op. */\n id: string;\n userId: string;\n event: string;\n ts: number;\n payload: Record<string, unknown>;\n}\n\nexport interface TriggerRoute {\n workflow: string;\n hash: string;\n /** Payload gate (payload-only condition tree), or null when the trigger has none. */\n where: ConditionIR | null;\n /** Profile gate, or null. */\n filter: ConditionIR | null;\n /** Whether a visitor not signed in may enter. */\n visitors: boolean;\n /**\n * The code version that applied these routes; absent after a hand-posted `POST /deploy`. Read\n * in the same row as the hash, so the pair is always one deploy's.\n */\n version?: string | undefined;\n}\n\nexport interface SegmentTriggerRoute {\n workflow: string;\n hash: string;\n segment: string;\n /** As `TriggerRoute.version`. */\n version?: string | undefined;\n}\n\n/**\n * A Worker version, as Cloudflare's version metadata binding names it: the bundle compiled into\n * that version goes live only over an older one (`replaceRoutesIfNewer`).\n */\nexport interface BundleVersion {\n id: string;\n /** When the version was created (epoch ms): later versions are newer. */\n createdAt: number;\n}\n\nexport interface EntryInput {\n workflow: string;\n userId: string;\n instanceId: string;\n hash: string;\n ts: number;\n /**\n * The instance's start, recorded in the same transaction as the entry — for a runtime whose\n * start is an asynchronous submit (AWS) that may have to be retried.\n */\n execution?: ExecutionStart | undefined;\n}\n\n/** How an instance is started on a runtime that submits it: replayed as is on every retry. */\nexport interface ExecutionStart {\n /** The runtime's idempotency key (the durable execution name). */\n name: string;\n /** What is invoked: a qualified function ARN, its version included. */\n target: string;\n /** The exact payload: a retried submit must send the same bytes, or the runtime refuses it. */\n input: string;\n}\n\nexport interface ExecutionRecord extends ExecutionStart {\n instanceId: string;\n workflow: string;\n userId: string;\n createdAt: number;\n submittedAt?: number | undefined;\n arn?: string | undefined;\n unresolvedAt?: number | undefined;\n callbackId?: string | undefined;\n /** Bumped by every wait the instance arms: a wake or a cleanup of an older one leaves it alone. */\n generation: number;\n deadlineAt?: number | undefined;\n checkedAt?: number | undefined;\n stoppedAt?: number | undefined;\n}\n\n/**\n * What a runtime that submits its starts and parks waits on one-shot callbacks (AWS) keeps\n * beside the entry ledger. The runtime holds the execution history; this holds only the\n * mapping from the engine's instance id to it.\n */\nexport interface ExecutionRegistry {\n get(instanceId: string): Promise<ExecutionRecord | undefined>;\n /**\n * Starts not yet confirmed and not given up on, oldest first: of one (workflow, user) entry\n * still running, or every one created before `createdBefore`.\n */\n pendingStarts(filter: {\n workflow?: string;\n userId?: string;\n createdBefore?: number;\n limit: number;\n }): Promise<ExecutionRecord[]>;\n markSubmitted(instanceId: string, arn: string | undefined, at: number): Promise<void>;\n markUnresolved(instanceId: string, at: number): Promise<void>;\n setArn(instanceId: string, arn: string): Promise<void>;\n /** Record the callback a wait parks on; false when a newer generation is already armed. */\n armCallback(\n instanceId: string,\n callback: { id: string; generation: number; deadlineAt?: number | undefined }\n ): Promise<boolean>;\n /** Forget the callback — only if it is still that generation's. */\n clearCallback(instanceId: string, generation: number): Promise<void>;\n /** Submitted executions whose entry is still running, least recently checked first. */\n toReconcile(limit: number, checkedBefore: number): Promise<ExecutionRecord[]>;\n markChecked(instanceId: string, at: number): Promise<void>;\n /** Record the first stop; a later one keeps it. */\n markStopped(instanceId: string, at: number): Promise<void>;\n}\n\n/**\n * How an entry attempt ended: a fresh row, a 'failed' row of a dead instance\n * taken over (the once policy does not bar a user over one outage), or\n * refused because the pair already entered and is still alive.\n */\nexport type EntryOutcome = 'entered' | 'reclaimed' | null;\n\n/** A device registered for pushes: its provider token belongs to the user who registered it last. */\nexport interface DeviceInput {\n deviceId?: string | undefined;\n /** Null until the device's visitor signs in. */\n userId: string | null;\n visitorId?: string | undefined;\n token: string;\n /** `ios` (APNs) or `android` (FCM): which provider the token belongs to. */\n platform: string;\n at: number;\n}\n\n/** What linking a visitor to a user did; null when it was already linked, or to another user. */\nexport interface VisitorMerge {\n /** Instances dropped because the user is already in the same workflow: to stop. */\n dropped: string[];\n /** The event names and profile properties the user gained: their waits must wake. */\n events: string[];\n props: string[];\n}\n\n/** An instance of a user still in the ledger, and whether its runner was admitted (see `admitInstance`). */\nexport interface RunningInstance {\n instanceId: string;\n /** False until the runner admitted itself: nothing has run, and the tombstone keeps it that way. */\n admitted: boolean;\n}\n\n/** One (workflow, user) entry: the attempt it records and how far that attempt got. */\nexport interface EntryState {\n instanceId: string;\n /** 'running' until the run finalizes it ('completed', 'goal', 'failed', …). */\n status: string;\n /** The runner admitted itself (`admitInstance`): the instance did reach the runtime. */\n admitted: boolean;\n}\n\n/** An entry still waiting for its first admission (`unadmittedEntries`). */\nexport interface UnadmittedEntry {\n instanceId: string;\n workflow: string;\n userId: string;\n}\n\n/** A running entry whose runner admitted itself (`admittedEntries`). */\nexport interface AdmittedEntry {\n instanceId: string;\n workflow: string;\n userId: string;\n}\n\n/** A store plus how to release what opening it acquired (a connection, say). */\nexport interface StoreLease {\n store: JourneyStore;\n close?: () => Promise<void>;\n}\n\nimport type { ErasureState } from './erasure';\n\nexport { type ErasureState, UserErasedError } from './erasure';\nexport { instanceOwner, isVisitorSubject, visitorIdOf, visitorSubject } from './identity';\n\n/**\n * The live database lacks tables or columns the engine reads (an SDK upgrade added them, or the\n * host never ran the migration). Raised before a deploy writes anything.\n */\nexport class SchemaMismatchError extends Error {\n override readonly name = 'SchemaMismatchError';\n\n constructor(readonly problems: readonly string[]) {\n super(\n `the database does not match @shware/workflow's tables — regenerate the drizzle schema (workflow-ui generate) and run your migrations:\\n ${problems.join('\\n ')}`\n );\n }\n}\n\n/** A message copy as reporting records it, before it goes out. */\nexport interface MessageRecord {\n idempotencyKey: string;\n workflow: string;\n instanceId: string;\n nodeId: string;\n template: string;\n /** Where the copy lands: `email`, `push_ios`, `push_android`. */\n channel: string;\n userId: string;\n /** An email's subscription topic; null for a transactional email and for every other channel. */\n topic: string | null;\n sentAt: number;\n}\n\n/** What is known of a copy without its engagement: whose it is, where it went, its topic. */\nexport interface MessageSummary {\n userId: string;\n channel: string;\n topic: string | null;\n}\n\n/** One opt-out to set or clear: a topic of the channel, or the whole channel (`topic: null`). */\nexport interface SuppressionChange {\n topic: string | null;\n suppressed: boolean;\n}\n\n/** What a click is judged by. */\nexport interface ClickContext {\n url: string;\n deliveredAt: number;\n linkCount: number;\n /** The copy's clicks in the window before this one. */\n recent: { position: number; at: number }[];\n}\n\n/** An open or a click, with the verdict on who made it and where it came from. */\nexport interface Engagement {\n at: number;\n machine: boolean;\n /** The rule that called it a machine; null for a person. */\n reason: string | null;\n userAgent: string | null;\n /** The network the request came from (Cloudflare's `cf.asn`, `cf.asOrganization`, `cf.country`), never the address. */\n asn: number | null;\n organization: string | null;\n country: string | null;\n}\n\n/**\n * Journey reporting: where each instance stands, every copy and how it was engaged with, segment\n * sizes. The store exposes it as `reporting`; the host records it and serves email tracking.\n * Every write is idempotent — a retried step reports again.\n */\n/** Bounds on when an event happened, both inclusive (epoch ms); an absent side is open. */\nexport interface EventWindow {\n sinceMs?: number;\n untilMs?: number;\n}\n\n/**\n * Where a page of events ended: the last row's timestamp and id, the keyset `(ts, id)` the next\n * page starts after. Opaque to callers; `ts` is the store's own rendering of the timestamp, so a\n * round trip loses no precision.\n */\nexport interface EventCursor {\n ts: string;\n id: string;\n}\n\nexport interface JourneyReporting {\n /** The instance entered: its row exists from here, so one a guard ends at once still counts. */\n instanceStarted(input: {\n workflow: string;\n instanceId: string;\n userId: string;\n at: number;\n }): Promise<void>;\n nodeReached(input: {\n workflow: string;\n instanceId: string;\n userId: string;\n nodeId: string;\n at: number;\n }): Promise<void>;\n instanceFinished(input: {\n workflow: string;\n instanceId: string;\n userId: string;\n status: string;\n at: number;\n convertedAt?: number;\n }): Promise<void>;\n /** The copy's row id; the row a retry already wrote, when there is one. */\n recordMessage(message: MessageRecord): Promise<string>;\n /**\n * Whether the copy was settled (`settleMessage`: delivered, or settled undelivered) — read from\n * the database itself, so a replayed send step never trusts a cache that missed the settle.\n */\n messageSettled(idempotencyKey: string): Promise<boolean>;\n settleMessage(\n idempotencyKey: string,\n result: { delivered: true } | { delivered: false; reason: string },\n at: number\n ): Promise<void>;\n /** An email's tracked links, in document order. */\n recordLinks(messageId: string, urls: readonly string[]): Promise<void>;\n /**\n * Record an open of the copy, judged by `judge` against the copy (its recipient and when it was\n * delivered — when it was sent, before the transport answered); `undefined` from the judge\n * records nothing. Read and written in one transaction. False for an unknown copy.\n */\n recordOpen(\n messageId: string,\n judge: (message: { userId: string; deliveredAt: number }) => Engagement | undefined\n ): Promise<boolean>;\n /**\n * Record a click on the copy's link at `position`, judged by `judge` against the link, how\n * many links the copy has and its clicks of the last `windowMs`. The copy is locked for the\n * transaction, so clicks arriving together (a scanner following every link at once) are\n * judged one after another, each seeing the ones before. When the judge asks to `backdate`,\n * the window's earlier clicks are reclassified with this one's verdict. A click implies an\n * open. Undefined for an unknown link.\n */\n recordClick(\n messageId: string,\n position: number,\n at: number,\n windowMs: number,\n judge: (link: ClickContext) => { engagement: Engagement; backdate: boolean }\n ): Promise<{ url: string; engagement: Engagement } | undefined>;\n /**\n * The email copy's recipient opted out: of the copy's topic, or with `scope: 'all'` of every\n * email. Recorded on the copy and as a suppression (the topic's row, or the channel-wide\n * null-topic row), in one transaction, which also yields who the recipient is and the topic —\n * so the caller acts on the unsubscribe without a second read. A transactional copy\n * (`topic: null`) has nothing to opt out of: nothing is recorded, and the answer says so.\n * Undefined for an unknown copy.\n */\n recordUnsubscribe(\n messageId: string,\n at: number,\n scope?: 'topic' | 'all'\n ): Promise<{ userId: string; topic: string | null } | undefined>;\n /** The copy's owner, channel and topic; undefined for an unknown copy. */\n lookupMessage(messageId: string): Promise<MessageSummary | undefined>;\n /** A push copy the device reported it received. False for an unknown copy. */\n recordDeviceDelivered(messageId: string, at: number): Promise<boolean>;\n /** Drop members of segments no longer deployed, then record every deployed segment's size. */\n snapshotSegments(at: number): Promise<{ segment: string; size: number }[]>;\n}\n\n/** A user the segment sweep has to evaluate, and for which segments. */\nexport interface SegmentCandidate {\n userId: string;\n segments: string[];\n}\n\nexport interface JourneyStore {\n /** Where instances stand, every copy and its engagement, segment sizes. */\n readonly reporting: JourneyReporting;\n\n /**\n * Record that the user's data must go (a deleted account): the tombstone every write about\n * the user is conditional on, from here on and for good. Committed before anything is\n * stopped or erased, so an entry that got in before it is found by the erasure and none gets\n * in after it. Idempotent: a second request, or one for a user already erased, changes nothing.\n */\n requestErasure(userId: string, at: number): Promise<void>;\n /** Users whose erasure was requested and not carried out yet (`'erasing'`), oldest first. */\n pendingErasures(): Promise<string[]>;\n /**\n * Where the user stands: `'none'` (no tombstone), `'erasing'` (requested, their instances\n * being stopped, data still there) or `'erased'` (`forgetUser` went through). Both\n * tombstone states refuse every write about the user; the entry points read this for a fast\n * answer, and the store's conditional writes are the gate itself.\n */\n erasureState(userId: string): Promise<ErasureState>;\n /** The user's journey instances still running, by the entry ledger. */\n runningInstances(userId: string): Promise<RunningInstance[]>;\n /**\n * The runner's admission, before its first observable effect: records that the instance runs\n * (`admitted_at`, kept on replay) — unless the user has an erasure tombstone, in which case\n * nothing is recorded and the runner must not run. Mutually exclusive with `requestErasure`\n * and `forgetUser` for that user (Postgres: the user's advisory lock), so an erasure either\n * finds the instance admitted and stops it, or commits its tombstone first and the admission\n * is refused — never neither.\n *\n * A first admission needs the entry running. An instance already admitted is admitted again\n * whatever the entry's status — the legitimate replay of an instance whose finalisation was\n * cut short after the entry reached its terminal status, which resumes on its own checkpoint\n * and finishes what is left. Never a new attempt's way in: false for a terminal entry never\n * admitted, for an entry no longer in the ledger, for another user's entry, and for an\n * instance whose entry was reclaimed since (`enterJourney` resets the admission with the\n * instance id, so a superseded attempt cannot pass under the id it lost).\n */\n admitInstance(instanceId: string, userId: string): Promise<boolean>;\n /**\n * Erase everything kept about the user — profile, devices, events, wake subscriptions, segment\n * memberships, entries and reporting — and mark the tombstone erased (`erased_at`), which\n * stays: a write that was in flight when the data went cannot bring the user back, and the\n * same id is never reactivated by ingest — which is also why the suppressions go with the\n * rest: nothing is ever sent to the id again, so no opt-out needs keeping. Run only once no\n * instance of theirs runs any more, or one could write some of it back. Idempotent. Mutually\n * exclusive with every write about the user (the user's advisory lock), so the data is gone\n * when this returns — nothing of theirs lands afterwards.\n */\n forgetUser(userId: string, at: number): Promise<void>;\n\n /**\n * The user opted out of the channel (recorded by an unsubscribe, or set through\n * `/subscriptions`): with a `topic`, an opt-out of that topic or of the whole channel; without\n * one, only the whole channel.\n */\n isSuppressed(userId: string, channel: string, topic?: string): Promise<boolean>;\n /** Every opt-out of the user on the channel: the topics, and `null` for the whole channel. */\n listSuppressions(userId: string, channel: string): Promise<(string | null)[]>;\n /**\n * Set or clear opt-outs, all in one transaction: a suppressed change inserts the row (keeping\n * the time of one already there), an unsuppressed one deletes it. Written whatever the user's\n * erasure state — the caller refuses an erased user first — since an opt-out is never refused.\n */\n updateSuppressions(\n userId: string,\n channel: string,\n changes: readonly SuppressionChange[],\n at: number\n ): Promise<void>;\n\n /**\n * What the live database lacks against the engine's tables (read-only); a deploy refuses to\n * run while it lists anything.\n */\n checkSchema(): Promise<string[]>;\n\n /* ---------------------------------- events --------------------------------- */\n /**\n * Append the occurrence; false when its id was already in the log (nothing written). Throws\n * `UserErasedError` for a user with a tombstone, like every write about a user below.\n */\n insertEvent(input: EventInput): Promise<boolean>;\n /**\n * Occurrences of `event` for the user, optionally only those within the window. With\n * `atLeast`, counting may stop there: the result is exact below it and some number ≥ it\n * otherwise — all a `performed(…, { count })` check needs, without counting a heavy user's\n * whole history.\n */\n countEvents(\n userId: string,\n event: string,\n window?: EventWindow,\n atLeast?: number\n ): Promise<number>;\n /**\n * One page of those occurrences' payloads, oldest first — the caller filters them with the\n * shared evaluator and pages on with `next` only while it still needs matches, so a `where`\n * never loads a user's whole history at once. `next` is absent on the last page.\n */\n pageEventPayloads(\n userId: string,\n event: string,\n window: EventWindow | undefined,\n page: { limit: number; after?: EventCursor | undefined }\n ): Promise<{ payloads: Record<string, unknown>[]; next?: EventCursor | undefined }>;\n\n /* -------------------------------- visitors --------------------------------- */\n /** The user a visitor became, if they did. */\n linkedUser(visitorId: string): Promise<string | undefined>;\n /** The visitor was heard from: who they are now, the user they became or their own subject. */\n touchVisitor(visitorId: string, at: number): Promise<string>;\n /**\n * Forget visitors who never signed in and were last heard from before `before`, with no journey\n * running: up to `limit` of them, leaving no tombstone. Answers how many.\n */\n pruneVisitors(before: number, limit: number): Promise<number>;\n /**\n * The visitor became the user: their events, profile, devices, opt-outs and journeys move to the\n * user, once. A visitor already linked keeps its first user. The user's own entry wins over the\n * visitor's in the same workflow, and the visitor's instance is dropped.\n */\n linkVisitor(visitorId: string, userId: string, at: number): Promise<VisitorMerge | null>;\n /** Who an instance's entry belongs to now. */\n entryOwner(instanceId: string): Promise<string | undefined>;\n\n /* --------------------------------- profiles -------------------------------- */\n getProfile(userId: string): Promise<ProfileProps | undefined>;\n /**\n * Merge-patch `props` into the stored profile (a null removes the property) and return the\n * result, atomically: two concurrent identifies cannot lose each other's keys. `at` is when the\n * values were read (now by default): a property written with a later `at` keeps its value, so\n * a redelivered patch cannot undo a newer one.\n */\n mergeProfile(userId: string, props: ProfileProps, at?: number): Promise<ProfileProps>;\n\n /**\n * Who the segment sweep has to evaluate: for each user the engine knows (a profile, an event or\n * a membership), the segments whose membership may have to change, or whose entry still owes a\n * start. It may name a user whose membership turns out right, since `evaluateCondition` decides;\n * it must not leave out one whose membership is wrong. Users come in ascending order; `after`\n * resumes past the last id of the previous page, and users with nothing to do are left out.\n */\n listSegmentCandidates(page: {\n segments: readonly { id: string; condition: ConditionIR }[];\n now: number;\n after?: string | undefined;\n limit: number;\n }): Promise<SegmentCandidate[]>;\n\n /* ---------------------------------- devices --------------------------------- */\n /** The push devices a user or visitor holds now, most recently registered first. */\n listDevices(subject: string): Promise<PushDevice[]>;\n /**\n * Register a device: one row per token and per device, holding only its current owner. A token\n * registered again moves to whoever registered it last (a phone signed into another account) and\n * to the front of their list; a device's new token replaces its old one. Answers the previous\n * owners, whose device waits must wake.\n */\n upsertDevice(device: DeviceInput): Promise<string[]>;\n /** Forget a token (reported dead by APNs / FCM); the user or visitor it belonged to, if any. */\n removeDevice(token: string): Promise<string | undefined>;\n /**\n * The user signed out on the device: the token is kept, bound to nobody, until the next\n * registration binds it again. Who it belonged to, if anyone.\n */\n unbindDevice(token: string): Promise<string | undefined>;\n\n /* ---------------------------- deployed definitions -------------------------- */\n getSegmentCondition(id: string): Promise<ConditionIR | undefined>;\n /** Every deployed segment with its definition: what the segment sweep evaluates. */\n listSegments(): Promise<{ id: string; condition: ConditionIR }[]>;\n findTriggers(event: string): Promise<TriggerRoute[]>;\n listSegmentTriggers(): Promise<SegmentTriggerRoute[]>;\n putWorkflowIR(workflow: WorkflowIR): Promise<void>;\n getWorkflowIR(contentHash: string): Promise<WorkflowIR | undefined>;\n /**\n * Swap the routing tables (segments, event triggers, segment triggers) for\n * the bundle's — atomically, so a concurrent ingest never sees them half\n * empty. Workflow bodies are stored separately (`putWorkflowIR`) because they\n * are content-addressed and outlive the routes that point at them.\n *\n * With `version` — a forced apply of the code's own bundle (`POST /bundle/deploy`) — the\n * version is recorded too, unconditionally and in the same transaction, so the live version\n * always names the code the routes came from. Without it (a hand-posted `POST /deploy`) the\n * recorded version stays as it was: that bundle holds until a newer code version applies its own.\n */\n replaceRoutes(bundle: BundleIR, version?: BundleVersion): Promise<void>;\n /**\n * The Worker version whose bundle the routes came from, when one put them there (through\n * `replaceRoutesIfNewer`, or a forced `replaceRoutes` with a version). Read from the database\n * itself, not a cache in front of it.\n */\n liveBundleVersion(): Promise<BundleVersion | undefined>;\n /**\n * `replaceRoutes`, only when `version` is newer than the live one, recorded in the same\n * transaction: two versions serving side by side during a rollout cannot leave the older one's\n * routes in force. False, with nothing written, when a version as new or newer is live.\n */\n replaceRoutesIfNewer(bundle: BundleIR, version: BundleVersion): Promise<boolean>;\n\n /* ----------------------------- segment membership --------------------------- */\n isSegmentMember(segment: string, userId: string): Promise<boolean>;\n /** Record the membership, with the workflows its entry has to start (see segment_start). */\n addSegmentMember(\n segment: string,\n userId: string,\n ts: number,\n workflows: readonly string[]\n ): Promise<void>;\n /** The workflows the member's entry has not started yet. */\n pendingSegmentStarts(segment: string, userId: string): Promise<string[]>;\n completeSegmentStart(segment: string, userId: string, workflow: string): Promise<void>;\n /** Drop the membership and any start it still owed. */\n removeSegmentMember(segment: string, userId: string): Promise<void>;\n\n /* ------------------------------- entry ledger ------------------------------- */\n /**\n * Record the entry (the once policy). Atomic under concurrent ingests: of\n * two racing attempts exactly one is 'entered' (or 'reclaimed', when the\n * pair's previous instance is recorded as 'failed') and the other null.\n */\n enterJourney(entry: EntryInput): Promise<EntryOutcome>;\n removeEntry(instanceId: string): Promise<void>;\n setEntryStatus(instanceId: string, status: string): Promise<void>;\n /** The (workflow, user) entry as it stands, read from the database itself; undefined if none. */\n getEntry(workflow: string, userId: string): Promise<EntryState | undefined>;\n /**\n * Running entries no runner has admitted, claimed before `claimedBefore` (epoch ms), oldest\n * first: candidates for a start that was lost between the claim and the platform's create.\n */\n unadmittedEntries(claimedBefore: number, limit: number): Promise<UnadmittedEntry[]>;\n /**\n * Mark a lost start `failed`, so the pair's next trigger reclaims it. Only while the entry is\n * still running and unadmitted under this instance id; false when it moved on meanwhile.\n */\n failUnadmittedEntry(instanceId: string): Promise<boolean>;\n /**\n * Running entries whose runner admitted itself before `admittedBefore` (epoch ms): candidates\n * for an instance the platform ended without the run finalizing its entry. A sample of at most\n * `limit`, in no fixed order — ordered by age, the oldest entries still legitimately running\n * (a long wait) would fill every batch and a lost one behind them would never be reached.\n */\n admittedEntries(admittedBefore: number, limit: number): Promise<AdmittedEntry[]>;\n /**\n * Mark an admitted entry whose instance ended without finalizing `failed`, so the pair's next\n * trigger reclaims it. Only while the entry is still running and admitted under this instance\n * id: a run that finalized meanwhile keeps its status, and a reclaim its new attempt; false\n * then.\n */\n failAdmittedEntry(instanceId: string): Promise<boolean>;\n /** Execution starts and wake callbacks, for a runtime that submits its starts (AWS). */\n readonly executions: ExecutionRegistry;\n\n /* ------------------------------ wake subscriptions --------------------------- */\n /** Distinct wake handles registered for (user, event). */\n findWakeHandles(userId: string, event: string): Promise<string[]>;\n /** Register the handle for each event; repeated registrations are idempotent. */\n subscribe(handle: string, userId: string, events: readonly string[], ts: number): Promise<void>;\n /** Drop every registration the handle holds. */\n unsubscribe(handle: string): Promise<void>;\n}\n\nexport { JourneyFactSource } from './facts';\nexport { PostgresJourneyStore } from './postgres';\nexport { PostgresReporting } from './postgres-reporting';\nexport {\n DEFAULT_SCHEMA,\n TABLES,\n renderDrizzleSchema,\n renderPostgresDdl,\n type TableSpec,\n type UniqueConstraintSpec,\n} from './tables';\nexport type { PostgresJourneyStoreOptions, SqlClientLike } from './postgres';\n"],"mappings":";;;;;;;;;;;AAiNA,IAAa,sBAAb,cAAyC,MAAM;CAGxB;CAFrB,OAAyB;CAEzB,YAAY,UAAsC;EAChD,MACE,4IAA4I,SAAS,KAAK,MAAM,GAClK;EAHmB,KAAA,WAAA;CAIrB;AACF"}
|
package/dist/store/postgres.cjs
CHANGED
|
@@ -744,6 +744,20 @@ var PostgresJourneyStore = class {
|
|
|
744
744
|
async failUnadmittedEntry(instanceId) {
|
|
745
745
|
return (await this.sql.query(`UPDATE ${this.t.entry} SET status = 'failed'
|
|
746
746
|
WHERE instance_id = $1 AND status = 'running' AND admitted_at IS NULL
|
|
747
|
+
RETURNING instance_id`, [instanceId])).length > 0;
|
|
748
|
+
}
|
|
749
|
+
async admittedEntries(admittedBefore, limit) {
|
|
750
|
+
return (await this.sql.query(`SELECT instance_id, workflow, user_id FROM ${this.t.entry}
|
|
751
|
+
WHERE status = 'running' AND admitted_at < ${TS("$1")}
|
|
752
|
+
ORDER BY random() LIMIT $2`, [admittedBefore, limit])).map((row) => ({
|
|
753
|
+
instanceId: row.instance_id,
|
|
754
|
+
workflow: row.workflow,
|
|
755
|
+
userId: row.user_id
|
|
756
|
+
}));
|
|
757
|
+
}
|
|
758
|
+
async failAdmittedEntry(instanceId) {
|
|
759
|
+
return (await this.sql.query(`UPDATE ${this.t.entry} SET status = 'failed'
|
|
760
|
+
WHERE instance_id = $1 AND status = 'running' AND admitted_at IS NOT NULL
|
|
747
761
|
RETURNING instance_id`, [instanceId])).length > 0;
|
|
748
762
|
}
|
|
749
763
|
async setEntryStatus(instanceId, status) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres.cjs","names":["visitorSubject","TABLES","PostgresReporting","columnName","indexName","uniqueConstraintName","uuidv7","UserErasedError","lockUserTransaction","conditionToSql","visitorIdOf","beginReadCommitted","lockUser","ConditionIR","WorkflowIR"],"sources":["../../src/store/postgres.ts"],"sourcesContent":["import type { PushDevice } from '../engine/ports';\nimport { type BundleIR, ConditionIR, WorkflowIR } from '../ir';\nimport { conditionToSql } from './condition-sql';\nimport { type ErasureState, UserErasedError } from './erasure';\nimport { visitorIdOf, visitorSubject } from './identity';\nimport type {\n BundleVersion,\n DeviceInput,\n EntryInput,\n EntryOutcome,\n EntryState,\n EventCursor,\n EventInput,\n EventWindow,\n ExecutionRecord,\n ExecutionRegistry,\n ExecutionStart,\n JourneyReporting,\n JourneyStore,\n ProfileProps,\n RunningInstance,\n SegmentCandidate,\n SegmentTriggerRoute,\n SuppressionChange,\n TriggerRoute,\n UnadmittedEntry,\n VisitorMerge,\n} from './index';\nimport { PostgresReporting } from './postgres-reporting';\nimport { type SqlClientLike, uuidv7 } from './sql';\nimport {\n type ColumnType,\n DEFAULT_SCHEMA,\n TABLES,\n type TableKey,\n columnName,\n indexName,\n uniqueConstraintName,\n} from './tables';\nimport { beginReadCommitted, lockUser, lockUserTransaction } from './user-lock';\n\n/** Who a device row belongs to: its user, or before sign-in its visitor. */\nfunction deviceOwner(row: { user_id: string | null; visitor_id: string | null }) {\n if (row.user_id !== null) return row.user_id;\n return row.visitor_id === null ? undefined : visitorSubject(row.visitor_id);\n}\n\nexport { DEFAULT_SCHEMA } from './tables';\nexport { type SqlClientLike, uuidv7 } from './sql';\n\nexport interface PostgresJourneyStoreOptions {\n /**\n * The Postgres schema the engine's tables live in — `workflow` by default, the config's\n * `database.schema` under journeyWorker. A schema of its own keeps the tables apart from the\n * app's. The host creates them with its own migrations, from the drizzle file\n * `renderDrizzleSchema` generates (or the plain `renderPostgresDdl`).\n */\n schema?: string;\n}\n\n/** information_schema's udt_name per column type. */\nconst UDT: Record<ColumnType, string> = {\n uuid: 'uuid',\n text: 'text',\n jsonb: 'jsonb',\n timestamptz: 'timestamptz',\n integer: 'int4',\n boolean: 'bool',\n};\n\n/** Epoch milliseconds → timestamptz, so the tables keep the database's native type. */\nconst TS = (param: string) => `to_timestamp(${param}::double precision / 1000)`;\n\nfunction asJson<T>(value: unknown): T {\n return (typeof value === 'string' ? JSON.parse(value) : value) as T;\n}\n\n/**\n * The journey data plane on Postgres: native types where Postgres has them\n * (jsonb, timestamptz), and the concurrency guarantees come from the database: ON CONFLICT for the once policy and\n * idempotent subscriptions, a transaction for the route swap, a jsonb merge\n * for /identify. The tables are declared in tables.ts.\n *\n * Every write about a user is conditional on their erasure tombstone not existing\n * (`INSERT ... SELECT ... WHERE NOT EXISTS`, see `notErased`), and runs in a transaction that\n * holds the user's advisory lock (`lockUser`, the protocol in user-lock.ts): an event, a\n * profile merge, a device, a subscription, a segment membership with its starts, an entry, an\n * admission, and the reporting's instance and message rows (postgres-reporting.ts). The\n * erasure's two steps, `requestErasure` and `forgetUser`, take the same lock. Under READ\n * COMMITTED a conditional write sees only what was committed when its statement began, and an\n * erasure sees only rows committed when it looked — so without the lock a write's transaction\n * could commit its rows after the erase deleted everything (the user comes back), and a runner\n * could admit itself after the tombstone (the instance runs on). With it, every write and the\n * erasure of one user run one after another: a write either commits before the tombstone,\n * where the erasure finds and deletes it, or waits for the lock, reads the tombstone and is\n * refused (`UserErasedError`). Nothing lands after the erase, so there is no second pass. The\n * conditional statement stays the in-transaction check; a write that wrote nothing reads the\n * tombstone on that path only, to tell a refusal from a plain no-op (`refuseIfErased`).\n * Suppressions are the one exception: an opt-out is written whatever the user's state.\n */\nexport class PostgresJourneyStore implements JourneyStore {\n private readonly t: Record<TableKey, string>;\n private readonly schema: string;\n readonly reporting: JourneyReporting;\n readonly executions: ExecutionRegistry;\n\n constructor(\n private readonly sql: SqlClientLike,\n options: PostgresJourneyStoreOptions = {}\n ) {\n this.schema = options.schema ?? DEFAULT_SCHEMA;\n const qualify = (name: string) => `\"${this.schema}\".\"${name}\"`;\n this.t = Object.fromEntries(\n (Object.keys(TABLES) as TableKey[]).map((key) => [key, qualify(TABLES[key].name)])\n ) as Record<TableKey, string>;\n this.reporting = new PostgresReporting(sql, this.t, this.schema);\n this.executions = new PostgresExecutions(sql, this.t.execution, this.t.entry);\n }\n\n /**\n * What the live database lacks against tables.ts: missing tables, missing columns, a column\n * of another type or nullability, and missing unique indexes (the store's ON CONFLICT targets\n * — without one the first ingest fails, not the deploy). Read-only — the engine never migrates\n * the host's database; a deploy fails with these problems listed, and the fix is to regenerate\n * the drizzle file and run the host's migrations.\n */\n async checkSchema(): Promise<string[]> {\n // In a transaction, so Hyperdrive answers from the database, not from its read cache — a\n // deploy right after the migration must see the new columns\n const { columns, indexes } = await this.sql.transaction(async (tx) => ({\n columns: await tx.query<{\n table_name: string;\n column_name: string;\n udt_name: string;\n is_nullable: string;\n }>(\n `SELECT table_name, column_name, udt_name, is_nullable FROM information_schema.columns WHERE table_schema = $1`,\n [this.schema]\n ),\n indexes: await tx.query<{ tablename: string; indexname: string; indexdef: string }>(\n `SELECT tablename, indexname, indexdef FROM pg_indexes WHERE schemaname = $1`,\n [this.schema]\n ),\n }));\n const live = new Map(columns.map((row) => [`${row.table_name}.${row.column_name}`, row]));\n const tables = new Set(columns.map((row) => row.table_name));\n const liveIndexes = new Map(indexes.map((row) => [`${row.tablename}.${row.indexname}`, row]));\n const problems: string[] = [];\n for (const spec of Object.values(TABLES)) {\n const where = `\"${this.schema}\".\"${spec.name}\"`;\n if (!tables.has(spec.name)) {\n problems.push(`table ${where} is missing`);\n continue;\n }\n for (const [key, column] of Object.entries(spec.columns)) {\n const name = columnName(key);\n const row = live.get(`${spec.name}.${name}`);\n if (row === undefined) problems.push(`column ${where}.\"${name}\" is missing`);\n else if (row.udt_name !== UDT[column.type])\n problems.push(\n `column ${where}.\"${name}\" is ${row.udt_name}, expected ${UDT[column.type]}`\n );\n else if ((row.is_nullable === 'YES') !== (column.nullable === true))\n problems.push(\n `column ${where}.\"${name}\" should be ${column.nullable ? 'nullable' : 'NOT NULL'}`\n );\n }\n // The names drizzle-kit and renderPostgresDdl both give an unnamed unique index\n for (const keys of spec.uniqueIndexes ?? []) {\n const name = indexName(spec, keys);\n const row = liveIndexes.get(`${spec.name}.${name}`);\n if (row === undefined) problems.push(`missing unique index \"${this.schema}\".\"${name}\"`);\n else if (!/^CREATE UNIQUE INDEX/i.test(row.indexdef))\n problems.push(`index \"${this.schema}\".\"${name}\" is not unique`);\n }\n // A unique constraint is backed by an index of its name; its definition says how nulls compare\n for (const unique of spec.uniqueConstraints ?? []) {\n const name = uniqueConstraintName(spec, unique);\n const row = liveIndexes.get(`${spec.name}.${name}`);\n if (row === undefined) {\n problems.push(`missing unique constraint \"${this.schema}\".\"${name}\"`);\n } else if (!/^CREATE UNIQUE INDEX/i.test(row.indexdef)) {\n problems.push(`index \"${this.schema}\".\"${name}\" is not unique`);\n } else if (!/NULLS NOT DISTINCT/i.test(row.indexdef)) {\n problems.push(`unique constraint \"${this.schema}\".\"${name}\" must be NULLS NOT DISTINCT`);\n }\n }\n }\n return problems;\n }\n\n async insertEvent(input: EventInput): Promise<boolean> {\n // The row key stays a UUID v7 (the house rule for every table); the\n // sender's occurrence id lives in event_id, unique per user, which is\n // what makes a second delivery a no-op. ON CONFLICT DO NOTHING + RETURNING\n // reports which of the two happened without a second round trip.\n return this.sql.transaction(async (tx) => {\n await this.lockUser(tx, input.userId);\n const rows = await tx.query(\n `INSERT INTO ${this.t.event} (id, event_id, user_id, name, ts, payload)\n SELECT $1::uuid, $2, $3, $4, ${TS('$5')}, $6::text::jsonb ${this.notErased('$3')}\n ON CONFLICT (user_id, event_id) DO NOTHING RETURNING id`,\n [\n uuidv7(input.ts),\n input.id,\n input.userId,\n input.event,\n input.ts,\n JSON.stringify(input.payload),\n ]\n );\n if (rows.length > 0) return true;\n // Nothing written: a duplicate, unless the tombstone refused it\n await this.refuseIfErased(tx, input.userId);\n return false;\n });\n }\n\n /** The condition every write about a user carries: no erasure tombstone for `$user`. */\n private notErased(user: string): string {\n return `WHERE NOT EXISTS (SELECT 1 FROM ${this.t.erasure} WHERE user_id = ${user})`;\n }\n\n /**\n * A conditional write wrote nothing: on that path only, read why. Throws when the tombstone\n * refused it; returns when the statement had another reason (a duplicate, an entry taken).\n */\n private async refuseIfErased(sql: SqlClientLike, userId: string): Promise<void> {\n const rows = await sql.query(`SELECT 1 AS x FROM ${this.t.erasure} WHERE user_id = $1`, [\n userId,\n ]);\n if (rows.length > 0) throw new UserErasedError(userId);\n }\n\n /**\n * The first two statements of every transaction that writes about the user (user-lock.ts):\n * READ COMMITTED, then the user's advisory lock, held until the transaction ends. Any read\n * about the user comes after, on a snapshot taken once the lock is held.\n */\n private lockUser(tx: SqlClientLike, userId: string): Promise<void> {\n return lockUserTransaction(tx, this.schema, userId);\n }\n\n /** The event rows of one user and name, within the window's bounds (both inclusive). */\n private eventWindow(userId: string, event: string, window?: EventWindow) {\n const params: unknown[] = [userId, event];\n let where = 'user_id = $1 AND name = $2';\n if (window?.sinceMs !== undefined) {\n params.push(window.sinceMs);\n where += ` AND ts >= ${TS(`$${params.length}`)}`;\n }\n if (window?.untilMs !== undefined) {\n params.push(window.untilMs);\n where += ` AND ts <= ${TS(`$${params.length}`)}`;\n }\n return { where, params };\n }\n\n async countEvents(\n userId: string,\n event: string,\n window?: EventWindow,\n atLeast?: number\n ): Promise<number> {\n const { where, params } = this.eventWindow(userId, event, window);\n // A LIMITed inner scan stops on the (user_id, name, ts) index once enough rows are seen\n const capped = atLeast !== undefined && Number.isInteger(atLeast) && atLeast > 0;\n if (capped) params.push(atLeast);\n const rows = await this.sql.query<{ c: number | string }>(\n capped\n ? `SELECT count(*)::int AS c FROM (SELECT 1 FROM ${this.t.event} WHERE ${where} LIMIT $${params.length}) capped`\n : `SELECT count(*)::int AS c FROM ${this.t.event} WHERE ${where}`,\n params\n );\n return Number(rows.at(0)?.c ?? 0);\n }\n\n async pageEventPayloads(\n userId: string,\n event: string,\n window: EventWindow | undefined,\n page: { limit: number; after?: EventCursor | undefined }\n ): Promise<{ payloads: Record<string, unknown>[]; next?: EventCursor | undefined }> {\n const { where, params } = this.eventWindow(userId, event, window);\n let keyset = '';\n if (page.after !== undefined) {\n params.push(page.after.ts, page.after.id);\n keyset = ` AND (ts, id) > ($${params.length - 1}::timestamptz, $${params.length}::uuid)`;\n }\n params.push(page.limit);\n // `ts::text` is the cursor: Postgres renders and re-reads its own timestamp exactly, where\n // a float of epoch milliseconds could land a hair off and repeat or skip a row\n const rows = await this.sql.query<{ payload: unknown; ts: string; id: string }>(\n `SELECT payload, ts::text AS ts, id::text AS id FROM ${this.t.event}\n WHERE ${where}${keyset} ORDER BY ts, id LIMIT $${params.length}`,\n params\n );\n const last = rows.at(-1);\n return {\n payloads: rows.map((row) => asJson<Record<string, unknown>>(row.payload)),\n ...(last !== undefined && rows.length === page.limit\n ? { next: { ts: last.ts, id: last.id } }\n : {}),\n };\n }\n\n async getProfile(userId: string): Promise<ProfileProps | undefined> {\n const rows = await this.sql.query<{ props: unknown }>(\n `SELECT props FROM ${this.t.profile} WHERE user_id = $1`,\n [userId]\n );\n const row = rows.at(0);\n return row === undefined ? undefined : asJson<ProfileProps>(row.props);\n }\n\n async mergeProfile(\n userId: string,\n props: ProfileProps,\n at: number = Date.now()\n ): Promise<ProfileProps> {\n // RFC 7386 merge-patch: jsonb `||` overwrites key by key and jsonb_strip_nulls then drops the\n // keys the patch set to null (the API's only way to unset a property). Under the user's lock,\n // so two concurrent identifies cannot lose each other's keys; a key written later than `at`\n // keeps its value, cleared ones included (their time stays in props_at).\n return this.sql.transaction(async (tx) => {\n await this.lockUser(tx, userId);\n const existing = await tx.query<{ props_at: unknown }>(\n `SELECT props_at FROM ${this.t.profile} WHERE user_id = $1`,\n [userId]\n );\n const times = asJson<Record<string, number> | null>(existing[0]?.props_at ?? null) ?? {};\n const patch = Object.fromEntries(\n Object.entries(props).filter(([key]) => (times[key] ?? Number.NEGATIVE_INFINITY) <= at)\n );\n const stamped = { ...times, ...Object.fromEntries(Object.keys(patch).map((k) => [k, at])) };\n const rows = await tx.query<{ props: unknown }>(\n `INSERT INTO ${this.t.profile} (user_id, props, props_at)\n SELECT $1, jsonb_strip_nulls($2::text::jsonb), $4::text::jsonb ${this.notErased('$1')}\n ON CONFLICT (user_id) DO UPDATE SET\n props = jsonb_strip_nulls(${this.t.profile}.props || $3::text::jsonb),\n props_at = $4::text::jsonb\n RETURNING props`,\n // EXCLUDED.props would be the stripped insert value, so the update merges the raw patch again\n [userId, JSON.stringify(patch), JSON.stringify(patch), JSON.stringify(stamped)]\n );\n // The upsert always yields its row — unless the tombstone refused it\n if (rows.length === 0) throw new UserErasedError(userId);\n return asJson<ProfileProps>(rows[0].props);\n });\n }\n\n async listSegmentCandidates(page: {\n segments: readonly { id: string; condition: ConditionIR }[];\n now: number;\n after?: string | undefined;\n limit: number;\n }): Promise<SegmentCandidate[]> {\n if (page.segments.length === 0) return [];\n const params: unknown[] = [];\n const bind = (value: unknown) => {\n params.push(value);\n return `$${params.length}`;\n };\n const definitions = new Map(page.segments.map((segment) => [segment.id, segment.condition]));\n const branches = page.segments.map(({ id, condition }) => {\n const segment = bind(id);\n const matches = conditionToSql(condition, {\n tables: this.t,\n user: 'u.user_id',\n props: 'p.props',\n nowMs: page.now,\n segments: definitions,\n bind,\n });\n const key = `segment = ${segment}::text AND user_id = u.user_id`;\n // NULL is distinct from both, so whatever SQL cannot decide goes to the evaluator\n return `SELECT ${segment}::text AS segment\n WHERE (${matches}) IS DISTINCT FROM EXISTS (SELECT 1 FROM ${this.t.segmentMember} WHERE ${key})\n OR EXISTS (SELECT 1 FROM ${this.t.segmentStart} WHERE ${key})`;\n });\n let keyset = '';\n if (page.after !== undefined) keyset = ` AND user_id > ${bind(page.after)}::text`;\n const rows = await this.sql.query<{ user_id: string; segments: unknown }>(\n `WITH u AS MATERIALIZED (\n SELECT user_id FROM (\n SELECT user_id FROM ${this.t.profile}\n UNION SELECT user_id FROM ${this.t.event}\n UNION SELECT user_id FROM ${this.t.segmentMember}\n ) known\n -- Segments are about users: a visitor is not one until they sign in\n WHERE user_id NOT LIKE '${visitorSubject('')}%'${keyset}\n )\n SELECT u.user_id, json_agg(c.segment) AS segments\n FROM u\n LEFT JOIN ${this.t.profile} p ON p.user_id = u.user_id\n CROSS JOIN LATERAL (${branches.join('\\n UNION ALL ')}) c\n GROUP BY u.user_id\n ORDER BY u.user_id\n LIMIT ${bind(page.limit)}::int`,\n params\n );\n return rows.map((row) => ({ userId: row.user_id, segments: asJson<string[]>(row.segments) }));\n }\n\n listDevices(subject: string): Promise<PushDevice[]> {\n const visitorId = visitorIdOf(subject);\n return this.sql.query<PushDevice>(\n visitorId === undefined\n ? `SELECT token, platform FROM ${this.t.device} WHERE user_id = $1 ORDER BY updated_at DESC`\n : // A device a user has taken over is theirs, whichever visitor it still names\n `SELECT token, platform FROM ${this.t.device} WHERE visitor_id = $1 AND user_id IS NULL ORDER BY updated_at DESC`,\n [visitorId ?? subject]\n );\n }\n\n async upsertDevice(device: DeviceInput): Promise<string[]> {\n const { deviceId = null, userId, visitorId = null, token, platform, at } = device;\n const subject = userId ?? visitorSubject(visitorId ?? '');\n return this.sql.transaction(async (tx) => {\n await this.lockUser(tx, subject);\n // Who held this token, or this device under an older token: their device waits must wake\n const previous = await tx.query<{\n token: string;\n user_id: string | null;\n visitor_id: string | null;\n }>(\n `SELECT token, user_id, visitor_id FROM ${this.t.device} WHERE token = $1 OR device_id = $2 FOR UPDATE`,\n [token, deviceId]\n );\n // A device id is only the client's word. A new token replaces the old one for the same owner\n // or for nobody; someone else's token keeps working, it only stops naming this device.\n for (const row of previous) {\n if (row.token === token) continue;\n const owner = deviceOwner(row);\n await tx.query(\n owner === undefined || owner === subject\n ? `DELETE FROM ${this.t.device} WHERE token = $1`\n : `UPDATE ${this.t.device} SET device_id = NULL WHERE token = $1`,\n [row.token]\n );\n }\n const rows = await tx.query(\n `INSERT INTO ${this.t.device} (device_id, user_id, visitor_id, token, platform, created_at, updated_at)\n SELECT $1, $2, $3, $4, $5, ${TS('$6')}, ${TS('$6')} ${this.notErased('$7')}\n ON CONFLICT (token) DO UPDATE SET\n device_id = COALESCE(EXCLUDED.device_id, ${this.t.device}.device_id),\n user_id = EXCLUDED.user_id,\n visitor_id = COALESCE(EXCLUDED.visitor_id, ${this.t.device}.visitor_id),\n platform = EXCLUDED.platform,\n updated_at = EXCLUDED.updated_at\n RETURNING token`,\n [deviceId, userId, visitorId, token, platform, at, subject]\n );\n if (rows.length === 0) throw new UserErasedError(subject);\n const owners = previous.flatMap((row) => {\n const owner = deviceOwner(row);\n return owner === undefined || owner === subject ? [] : [owner];\n });\n return [...new Set(owners)];\n });\n }\n\n async linkedUser(visitorId: string): Promise<string | undefined> {\n const rows = await this.sql.query<{ user_id: string | null }>(\n `SELECT user_id FROM ${this.t.visitor} WHERE visitor_id = $1`,\n [visitorId]\n );\n return rows[0]?.user_id ?? undefined;\n }\n\n async touchVisitor(visitorId: string, at: number): Promise<string> {\n const rows = await this.sql.query<{ user_id: string | null }>(\n `INSERT INTO ${this.t.visitor} (visitor_id, user_id, created_at, updated_at)\n VALUES ($1, NULL, ${TS('$2')}, ${TS('$2')})\n ON CONFLICT (visitor_id) DO UPDATE SET updated_at = EXCLUDED.updated_at\n RETURNING user_id`,\n [visitorId, at]\n );\n return rows[0]?.user_id ?? visitorSubject(visitorId);\n }\n\n async pruneVisitors(before: number, limit: number): Promise<number> {\n const idle = `user_id IS NULL AND updated_at < ${TS('$1')} AND NOT EXISTS (\n SELECT 1 FROM ${this.t.entry} e\n WHERE e.user_id = '${visitorSubject('')}' || visitor_id AND e.status = 'running')`;\n const candidates = await this.sql.query<{ visitor_id: string }>(\n `SELECT visitor_id FROM ${this.t.visitor} WHERE ${idle} ORDER BY updated_at LIMIT $2`,\n [before, limit]\n );\n let pruned = 0;\n for (const { visitor_id: visitorId } of candidates) {\n const subject = visitorSubject(visitorId);\n await this.sql.transaction(async (tx) => {\n await this.lockUser(tx, subject);\n // Asked again under the lock: they may have come back, or signed in, since the list\n const gone = await tx.query(\n `DELETE FROM ${this.t.visitor} WHERE visitor_id = $2 AND ${idle} RETURNING visitor_id`,\n [before, visitorId]\n );\n if (gone.length === 0) return;\n await this.deleteSubjectRows(tx, subject);\n pruned += 1;\n });\n }\n return pruned;\n }\n\n async entryOwner(instanceId: string): Promise<string | undefined> {\n const rows = await this.sql.query<{ user_id: string }>(\n `SELECT user_id FROM ${this.t.entry} WHERE instance_id = $1`,\n [instanceId]\n );\n return rows[0]?.user_id;\n }\n\n async linkVisitor(visitorId: string, userId: string, at: number): Promise<VisitorMerge | null> {\n const subject = visitorSubject(visitorId);\n // The common case, every signed-in batch: no lock taken\n if ((await this.linkedUser(visitorId)) !== undefined) return null;\n return this.sql.transaction(async (tx) => {\n await beginReadCommitted(tx);\n // In one order, so two merges cannot deadlock\n for (const key of [subject, userId].sort()) {\n await lockUser(tx, this.schema, key);\n }\n const linked = await tx.query(\n `INSERT INTO ${this.t.visitor} (visitor_id, user_id, created_at, updated_at)\n SELECT $1, $2, ${TS('$3')}, ${TS('$3')} ${this.notErased('$2')}\n ON CONFLICT (visitor_id) DO UPDATE SET user_id = EXCLUDED.user_id, updated_at = EXCLUDED.updated_at\n WHERE ${this.t.visitor}.user_id IS NULL\n RETURNING visitor_id`,\n [visitorId, userId, at]\n );\n if (linked.length === 0) {\n const erased = await tx.query(`SELECT 1 FROM ${this.t.erasure} WHERE user_id = $1`, [\n userId,\n ]);\n if (erased.length > 0) throw new UserErasedError(userId);\n return null;\n }\n // The user's own entry wins: the visitor's copy of that journey stops\n const dropped = await tx.query<{ instance_id: string }>(\n `DELETE FROM ${this.t.entry} v USING ${this.t.entry} u\n WHERE v.user_id = $1 AND u.user_id = $2 AND u.workflow = v.workflow\n RETURNING v.instance_id`,\n [subject, userId]\n );\n const droppedIds = dropped.map((row) => row.instance_id);\n // Gone with its entry: nothing should wake it, and no start of it is owed\n await tx.query(`DELETE FROM ${this.t.subscription} WHERE wake_handle = ANY($1::text[])`, [\n droppedIds,\n ]);\n await tx.query(`DELETE FROM ${this.t.execution} WHERE instance_id = ANY($1::text[])`, [\n droppedIds,\n ]);\n // One occurrence per id: a repeat the user already has is not moved\n await tx.query(\n `DELETE FROM ${this.t.event} v USING ${this.t.event} u\n WHERE v.user_id = $1 AND u.user_id = $2 AND u.event_id = v.event_id`,\n [subject, userId]\n );\n const events = await tx.query<{ name: string }>(\n `SELECT DISTINCT name FROM ${this.t.event} WHERE user_id = $1`,\n [subject]\n );\n // An opt-out made before sign-in still holds\n await tx.query(\n `INSERT INTO ${this.t.suppression} (user_id, channel, topic, ts)\n SELECT $2, channel, topic, ts FROM ${this.t.suppression} WHERE user_id = $1\n ON CONFLICT (user_id, channel, topic) DO NOTHING`,\n [subject, userId]\n );\n await tx.query(`DELETE FROM ${this.t.suppression} WHERE user_id = $1`, [subject]);\n const props = await this.mergeProfileInto(tx, subject, userId);\n for (const table of [\n this.t.event,\n this.t.entry,\n this.t.execution,\n this.t.subscription,\n this.t.instance,\n this.t.message,\n ]) {\n await tx.query(`UPDATE ${table} SET user_id = $2 WHERE user_id = $1`, [subject, userId]);\n }\n await tx.query(\n `UPDATE ${this.t.device} SET user_id = $2, updated_at = ${TS('$3')}\n WHERE visitor_id = $1 AND user_id IS NULL`,\n [visitorId, userId, at]\n );\n return { dropped: droppedIds, events: events.map((row) => row.name), props };\n });\n }\n\n /**\n * The visitor's properties under the user's, a key the user has a time for going to the later\n * write, one a row older than the times going to the user if they hold it. The visitor's row\n * goes. Answers the keys that moved.\n */\n private async mergeProfileInto(\n tx: SqlClientLike,\n subject: string,\n userId: string\n ): Promise<string[]> {\n const rows = await tx.query<{ user_id: string; props: unknown; props_at: unknown }>(\n `SELECT user_id, props, props_at FROM ${this.t.profile} WHERE user_id = ANY($1::text[])`,\n [[subject, userId]]\n );\n const from = rows.find((row) => row.user_id === subject);\n if (from === undefined) return [];\n const to = rows.find((row) => row.user_id === userId);\n const fromTimes = asJson<Record<string, number> | null>(from.props_at ?? null) ?? {};\n const toTimes = asJson<Record<string, number> | null>(to?.props_at ?? null) ?? {};\n const toProps = to === undefined ? {} : asJson<ProfileProps>(to.props);\n const moved = Object.entries(asJson<ProfileProps>(from.props)).filter(([key]) => {\n const written = toTimes[key];\n return written === undefined ? !Object.hasOwn(toProps, key) : (fromTimes[key] ?? 0) > written;\n });\n const props = { ...toProps, ...Object.fromEntries(moved) };\n const times = {\n ...toTimes,\n ...Object.fromEntries(moved.map(([key]) => [key, fromTimes[key] ?? 0])),\n };\n await tx.query(\n `INSERT INTO ${this.t.profile} (user_id, props, props_at) VALUES ($1, $2::text::jsonb, $3::text::jsonb)\n ON CONFLICT (user_id) DO UPDATE SET props = EXCLUDED.props, props_at = EXCLUDED.props_at`,\n [userId, JSON.stringify(props), JSON.stringify(times)]\n );\n await tx.query(`DELETE FROM ${this.t.profile} WHERE user_id = $1`, [subject]);\n return moved.map(([key]) => key);\n }\n\n async requestErasure(userId: string, at: number): Promise<void> {\n // Under the user's lock: a write in flight commits before the tombstone (and is found by\n // the erase) or reads it after (and is refused)\n await this.sql.transaction(async (tx) => {\n await this.lockUser(tx, userId);\n await tx.query(\n `INSERT INTO ${this.t.erasure} (user_id, requested_at) VALUES ($1, ${TS('$2')})\n ON CONFLICT DO NOTHING`,\n [userId, at]\n );\n });\n }\n\n async pendingErasures(): Promise<string[]> {\n const rows = await this.sql.query<{ user_id: string }>(\n `SELECT user_id FROM ${this.t.erasure} WHERE erased_at IS NULL ORDER BY requested_at`\n );\n return rows.map((row) => row.user_id);\n }\n\n async erasureState(userId: string): Promise<ErasureState> {\n const rows = await this.sql.query<{ erased: boolean }>(\n `SELECT erased_at IS NOT NULL AS erased FROM ${this.t.erasure} WHERE user_id = $1`,\n [userId]\n );\n const row = rows.at(0);\n if (row === undefined) return 'none';\n return row.erased ? 'erased' : 'erasing';\n }\n\n async runningInstances(userId: string): Promise<RunningInstance[]> {\n const rows = await this.sql.query<{ instance_id: string; admitted: boolean }>(\n `SELECT instance_id, admitted_at IS NOT NULL AS admitted\n FROM ${this.t.entry} WHERE user_id = $1 AND status = 'running'`,\n [userId]\n );\n return rows.map((row) => ({ instanceId: row.instance_id, admitted: row.admitted }));\n }\n\n async admitInstance(instanceId: string, userId: string): Promise<boolean> {\n return this.sql.transaction(async (tx) => {\n await this.lockUser(tx, userId);\n // A first admission needs a running entry; an instance once admitted may replay its\n // finalisation after the entry reached a terminal status, under its own instance id (a\n // reclaim resets `admitted_at`, so a superseded attempt cannot). The first admission's\n // time is kept, the status never touched, and the tombstone refuses both.\n const rows = await tx.query(\n `UPDATE ${this.t.entry} SET admitted_at = COALESCE(admitted_at, ${TS('$3')})\n WHERE instance_id = $1 AND user_id = $2\n AND (status = 'running' OR admitted_at IS NOT NULL)\n AND NOT EXISTS (SELECT 1 FROM ${this.t.erasure} WHERE user_id = $2)\n RETURNING instance_id`,\n [instanceId, userId, Date.now()]\n );\n return rows.length > 0;\n });\n }\n\n async forgetUser(userId: string, at: number): Promise<void> {\n await this.sql.transaction(async (tx) => {\n // Under the user's lock, every row committed before the tombstone is visible from here,\n // and no write about the user can commit until this transaction ends\n await this.lockUser(tx, userId);\n await this.deleteSubjectRows(tx, userId);\n // The tombstone stays, now marked erased; the first erase's time is kept by a repeat\n await tx.query(\n `UPDATE ${this.t.erasure} SET erased_at = COALESCE(erased_at, ${TS('$2')}) WHERE user_id = $1`,\n [userId, at]\n );\n });\n }\n\n /** Everything kept about a user or visitor, on a transaction holding their lock. */\n private async deleteSubjectRows(tx: SqlClientLike, subject: string): Promise<void> {\n // Children before the copies they belong to\n for (const child of [this.t.messageOpen, this.t.messageClick, this.t.messageLink]) {\n await tx.query(\n `DELETE FROM ${child} WHERE message_id IN (SELECT id FROM ${this.t.message} WHERE user_id = $1)`,\n [subject]\n );\n }\n for (const table of [\n this.t.message,\n this.t.instance,\n this.t.entry,\n this.t.execution,\n this.t.subscription,\n this.t.segmentStart,\n this.t.segmentMember,\n // The opt-outs too: the tombstone keeps the id from ever being sent to again\n this.t.suppression,\n this.t.device,\n this.t.event,\n this.t.profile,\n ]) {\n await tx.query(`DELETE FROM ${table} WHERE user_id = $1`, [subject]);\n }\n const visitorId = visitorIdOf(subject);\n if (visitorId !== undefined) {\n await tx.query(`DELETE FROM ${this.t.device} WHERE visitor_id = $1 AND user_id IS NULL`, [\n visitorId,\n ]);\n }\n }\n\n async isSuppressed(userId: string, channel: string, topic?: string): Promise<boolean> {\n // The channel-wide row (topic null) covers every topic; a topic's row covers that topic\n const rows = await this.sql.query(\n `SELECT 1 AS x FROM ${this.t.suppression}\n WHERE user_id = $1 AND channel = $2 AND (topic IS NULL OR topic = $3::text)`,\n [userId, channel, topic ?? null]\n );\n return rows.length > 0;\n }\n\n async listSuppressions(userId: string, channel: string): Promise<(string | null)[]> {\n const rows = await this.sql.query<{ topic: string | null }>(\n `SELECT topic FROM ${this.t.suppression} WHERE user_id = $1 AND channel = $2\n ORDER BY topic NULLS FIRST`,\n [userId, channel]\n );\n return rows.map((row) => row.topic);\n }\n\n async updateSuppressions(\n userId: string,\n channel: string,\n changes: readonly SuppressionChange[],\n at: number\n ): Promise<void> {\n if (changes.length === 0) return;\n await this.sql.transaction(async (tx) => {\n await this.lockUser(tx, userId);\n for (const { topic, suppressed } of changes) {\n if (suppressed) {\n // The first opt-out sets the time; a repeat keeps it\n await tx.query(\n `INSERT INTO ${this.t.suppression} (user_id, channel, topic, ts)\n VALUES ($1, $2, $3::text, ${TS('$4')}) ON CONFLICT DO NOTHING`,\n [userId, channel, topic, at]\n );\n } else {\n await tx.query(\n `DELETE FROM ${this.t.suppression}\n WHERE user_id = $1 AND channel = $2 AND topic IS NOT DISTINCT FROM $3::text`,\n [userId, channel, topic]\n );\n }\n }\n });\n }\n\n async removeDevice(token: string): Promise<string | undefined> {\n const rows = await this.sql.query<{ user_id: string | null; visitor_id: string | null }>(\n `DELETE FROM ${this.t.device} WHERE token = $1 RETURNING user_id, visitor_id`,\n [token]\n );\n return rows[0] === undefined ? undefined : deviceOwner(rows[0]);\n }\n\n async unbindDevice(token: string): Promise<string | undefined> {\n // The owner as it was before the update: RETURNING alone would only show the nulls\n const rows = await this.sql.query<{ user_id: string | null; visitor_id: string | null }>(\n `WITH old AS (SELECT token, user_id, visitor_id FROM ${this.t.device} WHERE token = $1 FOR UPDATE)\n UPDATE ${this.t.device} d SET user_id = NULL, visitor_id = NULL, updated_at = ${TS('$2')} FROM old WHERE d.token = old.token\n RETURNING old.user_id, old.visitor_id`,\n [token, Date.now()]\n );\n return rows[0] === undefined ? undefined : deviceOwner(rows[0]);\n }\n\n async listSegments(): Promise<{ id: string; condition: ConditionIR }[]> {\n const rows = await this.sql.query<{ id: string; condition: unknown }>(\n `SELECT id, condition FROM ${this.t.segment} ORDER BY id`\n );\n return rows.map((row) => ({\n id: row.id,\n condition: ConditionIR.parse(asJson(row.condition)),\n }));\n }\n\n async getSegmentCondition(id: string): Promise<ConditionIR | undefined> {\n const rows = await this.sql.query<{ condition: unknown }>(\n `SELECT condition FROM ${this.t.segment} WHERE id = $1`,\n [id]\n );\n const row = rows.at(0);\n return row === undefined ? undefined : ConditionIR.parse(asJson(row.condition));\n }\n\n async findTriggers(event: string): Promise<TriggerRoute[]> {\n const rows = await this.sql.query<{\n workflow: string;\n hash: string;\n where_clause: unknown;\n filter: unknown;\n visitors: boolean | null;\n version: string | null;\n }>(\n `SELECT workflow, hash, \"where\" AS where_clause, filter, visitors, version FROM ${this.t.trigger} WHERE event = $1`,\n [event]\n );\n return rows.map((row) => ({\n workflow: row.workflow,\n hash: row.hash,\n where: row.where_clause === null ? null : ConditionIR.parse(asJson(row.where_clause)),\n filter: row.filter === null ? null : ConditionIR.parse(asJson(row.filter)),\n visitors: row.visitors === true,\n version: row.version ?? undefined,\n }));\n }\n\n async listSegmentTriggers(): Promise<SegmentTriggerRoute[]> {\n const rows = await this.sql.query<{\n workflow: string;\n hash: string;\n segment: string;\n version: string | null;\n }>(`SELECT workflow, hash, segment, version FROM ${this.t.segmentTrigger}`);\n return rows.map((row) => ({\n workflow: row.workflow,\n hash: row.hash,\n segment: row.segment,\n version: row.version ?? undefined,\n }));\n }\n\n async putWorkflowIR(workflow: WorkflowIR): Promise<void> {\n await this.sql.query(\n `INSERT INTO ${this.t.ir} (content_hash, ir) VALUES ($1, $2::text::jsonb)\n ON CONFLICT (content_hash) DO UPDATE SET ir = EXCLUDED.ir`,\n [workflow.contentHash, JSON.stringify(workflow)]\n );\n }\n\n async getWorkflowIR(contentHash: string): Promise<WorkflowIR | undefined> {\n const rows = await this.sql.query<{ ir: unknown }>(\n `SELECT ir FROM ${this.t.ir} WHERE content_hash = $1`,\n [contentHash]\n );\n const row = rows.at(0);\n return row === undefined ? undefined : WorkflowIR.parse(asJson(row.ir));\n }\n\n replaceRoutes(bundle: BundleIR, version?: BundleVersion): Promise<void> {\n return this.sql.transaction(async (tx) => {\n if (version !== undefined) {\n // Unconditional, unlike replaceRoutesIfNewer: a forced apply wins, and the row must then\n // name the code its routes came from. Leaving it naming a newer version would make that\n // version skip re-applying its own bundle, with these older routes left live.\n await tx.query(\n `INSERT INTO ${this.t.deployment} (scope, version_id, version_created_at, applied_at)\n VALUES ('routes', $1, ${TS('$2')}, ${TS('$3')})\n ON CONFLICT (scope) DO UPDATE SET version_id = EXCLUDED.version_id,\n version_created_at = EXCLUDED.version_created_at, applied_at = EXCLUDED.applied_at`,\n [version.id, version.createdAt, Date.now()]\n );\n }\n await this.swapRoutes(tx, bundle, version?.id);\n });\n }\n\n async liveBundleVersion(): Promise<BundleVersion | undefined> {\n // In a transaction, so Hyperdrive answers from the database, not from its read cache\n const rows = await this.sql.transaction((tx) =>\n tx.query<{ version_id: string; created_at: number | string }>(\n `SELECT version_id, (extract(epoch FROM version_created_at) * 1000)::double precision AS created_at\n FROM ${this.t.deployment} WHERE scope = 'routes'`\n )\n );\n const row = rows.at(0);\n return row === undefined\n ? undefined\n : { id: row.version_id, createdAt: Number(row.created_at) };\n }\n\n replaceRoutesIfNewer(bundle: BundleIR, version: BundleVersion): Promise<boolean> {\n return this.sql.transaction(async (tx) => {\n // The row lock decides between versions applying at once: the loser's WHERE re-reads the\n // winner's committed row and updates nothing\n const claimed = await tx.query(\n `INSERT INTO ${this.t.deployment} AS live (scope, version_id, version_created_at, applied_at)\n VALUES ('routes', $1, ${TS('$2')}, ${TS('$3')})\n ON CONFLICT (scope) DO UPDATE SET version_id = EXCLUDED.version_id,\n version_created_at = EXCLUDED.version_created_at, applied_at = EXCLUDED.applied_at\n WHERE live.version_created_at < EXCLUDED.version_created_at\n RETURNING scope`,\n [version.id, version.createdAt, Date.now()]\n );\n if (claimed.length === 0) return false;\n await this.swapRoutes(tx, bundle, version.id);\n return true;\n });\n }\n\n /** `version`: the code version applying the bundle, kept on every route it writes. */\n private async swapRoutes(tx: SqlClientLike, bundle: BundleIR, version?: string): Promise<void> {\n await tx.query(`DELETE FROM ${this.t.trigger}`);\n await tx.query(`DELETE FROM ${this.t.segmentTrigger}`);\n await tx.query(`DELETE FROM ${this.t.segment}`);\n for (const segment of bundle.segments) {\n await tx.query(\n `INSERT INTO ${this.t.segment} (id, condition, hash) VALUES ($1, $2::text::jsonb, $3)`,\n [segment.id, JSON.stringify(segment.condition), segment.contentHash]\n );\n }\n for (const workflow of bundle.workflows) {\n if (workflow.trigger.type === 'event') {\n await tx.query(\n `INSERT INTO ${this.t.trigger} (workflow, hash, event, \"where\", filter, visitors, version) VALUES ($1, $2, $3, $4::text::jsonb, $5::text::jsonb, $6, $7)`,\n [\n workflow.id,\n workflow.contentHash,\n workflow.trigger.event,\n workflow.trigger.where === undefined ? null : JSON.stringify(workflow.trigger.where),\n workflow.trigger.filter === undefined ? null : JSON.stringify(workflow.trigger.filter),\n workflow.trigger.visitors === true,\n version ?? null,\n ]\n );\n } else if (workflow.trigger.type === 'segment') {\n await tx.query(\n `INSERT INTO ${this.t.segmentTrigger} (workflow, hash, segment, version) VALUES ($1, $2, $3, $4)`,\n [workflow.id, workflow.contentHash, workflow.trigger.segment, version ?? null]\n );\n }\n }\n }\n\n async isSegmentMember(segment: string, userId: string): Promise<boolean> {\n const rows = await this.sql.query(\n `SELECT 1 AS x FROM ${this.t.segmentMember} WHERE segment = $1 AND user_id = $2`,\n [segment, userId]\n );\n return rows.length > 0;\n }\n\n async addSegmentMember(\n segment: string,\n userId: string,\n ts: number,\n workflows: readonly string[]\n ): Promise<void> {\n // One locked transaction: the membership and the starts it owes commit together\n await this.sql.transaction(async (tx) => {\n await this.lockUser(tx, userId);\n const member = await tx.query(\n `INSERT INTO ${this.t.segmentMember} (segment, user_id, ts)\n SELECT $1, $2, ${TS('$3')} ${this.notErased('$2')}\n ON CONFLICT DO NOTHING RETURNING segment`,\n [segment, userId, ts]\n );\n // Already a member, or refused: either way the starts below write nothing new\n if (member.length === 0) await this.refuseIfErased(tx, userId);\n for (const workflow of workflows) {\n await tx.query(\n `INSERT INTO ${this.t.segmentStart} (segment, workflow, user_id, ts)\n SELECT $1, $2, $3, ${TS('$4')} ${this.notErased('$3')}\n ON CONFLICT DO NOTHING`,\n [segment, workflow, userId, ts]\n );\n }\n });\n }\n\n async pendingSegmentStarts(segment: string, userId: string): Promise<string[]> {\n const rows = await this.sql.query<{ workflow: string }>(\n `SELECT workflow FROM ${this.t.segmentStart} WHERE segment = $1 AND user_id = $2`,\n [segment, userId]\n );\n return rows.map((row) => row.workflow);\n }\n\n async completeSegmentStart(segment: string, userId: string, workflow: string): Promise<void> {\n await this.sql.query(\n `DELETE FROM ${this.t.segmentStart} WHERE segment = $1 AND user_id = $2 AND workflow = $3`,\n [segment, userId, workflow]\n );\n }\n\n async removeSegmentMember(segment: string, userId: string): Promise<void> {\n await this.sql.transaction(async (tx) => {\n await tx.query(`DELETE FROM ${this.t.segmentStart} WHERE segment = $1 AND user_id = $2`, [\n segment,\n userId,\n ]);\n await tx.query(`DELETE FROM ${this.t.segmentMember} WHERE segment = $1 AND user_id = $2`, [\n segment,\n userId,\n ]);\n });\n }\n\n async enterJourney(entry: EntryInput): Promise<EntryOutcome> {\n const { execution } = entry;\n // One transaction under the user's lock: the claim (or reclaim) and the start it owes commit\n // together — a crash between them would leave a running entry no retry could ever start —\n // and before or after an erasure of the user, never astride it\n return this.sql.transaction(async (tx) => {\n await this.lockUser(tx, entry.userId);\n const outcome = await this.claimEntry(tx, entry);\n if (outcome !== null && execution !== undefined) {\n await this.recordStart(tx, entry, execution);\n }\n return outcome;\n });\n }\n\n private async claimEntry(sql: SqlClientLike, entry: EntryInput): Promise<EntryOutcome> {\n // The (workflow, user_id) primary key makes the once policy atomic:\n // concurrent ingests race on the insert and exactly one wins.\n const claimedAt = Date.now();\n const inserted = await sql.query(\n `INSERT INTO ${this.t.entry} (workflow, user_id, instance_id, hash, status, ts, claimed_at)\n SELECT $1, $2, $3, $4, 'running', ${TS('$5')}, ${TS('$6')} ${this.notErased('$2')}\n ON CONFLICT (workflow, user_id) DO NOTHING RETURNING instance_id`,\n [entry.workflow, entry.userId, entry.instanceId, entry.hash, entry.ts, claimedAt]\n );\n if (inserted.length > 0) return 'entered';\n\n // An existing row blocks the entry unless it records an instance that died:\n // 'failed' is reclaimed rather than barring the user forever over one\n // outage. The status test is inside the UPDATE, so of two concurrent\n // retries exactly one gets a row back. The reclaim is a new attempt: it\n // starts unadmitted, even under the same instance id, so the previous\n // attempt's admission is not inherited (`runningInstances` would report a\n // runner that has not been created yet).\n const claimed = await sql.query(\n `UPDATE ${this.t.entry} SET instance_id = $1, hash = $2, status = 'running', ts = ${TS('$3')},\n admitted_at = NULL, claimed_at = ${TS('$6')}\n WHERE workflow = $4 AND user_id = $5 AND status = 'failed'\n AND NOT EXISTS (SELECT 1 FROM ${this.t.erasure} WHERE user_id = $5)\n RETURNING instance_id`,\n [entry.instanceId, entry.hash, entry.ts, entry.workflow, entry.userId, claimedAt]\n );\n if (claimed.length > 0) return 'reclaimed';\n // Taken by a live instance, or refused by the tombstone: tell them apart\n await this.refuseIfErased(sql, entry.userId);\n return null;\n }\n\n /**\n * Only an entry this call claimed records a start, so a row already there is a previous\n * attempt's — a failed entry reclaimed under the same instance id (an event replayed with its\n * ts). It is replaced whole: the new attempt's name and payload, nothing submitted yet.\n */\n private async recordStart(\n tx: SqlClientLike,\n entry: EntryInput,\n execution: ExecutionStart\n ): Promise<void> {\n const rows = await tx.query(\n `INSERT INTO ${this.t.execution} (instance_id, workflow, user_id, name, target, input, created_at, generation)\n SELECT $1, $2, $3, $4, $5, $6, ${TS('$7')}, 0 ${this.notErased('$3')}\n ON CONFLICT (instance_id) DO UPDATE SET\n name = EXCLUDED.name, target = EXCLUDED.target, input = EXCLUDED.input,\n created_at = EXCLUDED.created_at, generation = 0, submitted_at = NULL, arn = NULL,\n unresolved_at = NULL, callback_id = NULL, deadline_at = NULL, checked_at = NULL,\n stopped_at = NULL\n RETURNING instance_id`,\n [\n entry.instanceId,\n entry.workflow,\n entry.userId,\n execution.name,\n execution.target,\n execution.input,\n // The start's own age (the retry budget), not the event's ts, which the producer chose\n Date.now(),\n ]\n );\n // Refused between the claim and this: the throw rolls the claimed entry back with it\n if (rows.length === 0) throw new UserErasedError(entry.userId);\n }\n\n async removeEntry(instanceId: string): Promise<void> {\n await this.sql.query(`DELETE FROM ${this.t.entry} WHERE instance_id = $1`, [instanceId]);\n }\n\n async getEntry(workflow: string, userId: string): Promise<EntryState | undefined> {\n // In a transaction, so Hyperdrive answers from the database: the caller decides whether a\n // start is lost on this read, and a cached row could still show an admission as missing\n const rows = await this.sql.transaction((tx) =>\n tx.query<{ instance_id: string; status: string; admitted: boolean }>(\n `SELECT instance_id, status, admitted_at IS NOT NULL AS admitted\n FROM ${this.t.entry} WHERE workflow = $1 AND user_id = $2`,\n [workflow, userId]\n )\n );\n const row = rows.at(0);\n return row === undefined\n ? undefined\n : { instanceId: row.instance_id, status: row.status, admitted: row.admitted };\n }\n\n async unadmittedEntries(claimedBefore: number, limit: number): Promise<UnadmittedEntry[]> {\n // Rows from before claimed_at existed fall back to ts: older than any real claim, they are\n // exactly the entries a crash may have stranded\n const rows = await this.sql.query<{ instance_id: string; workflow: string; user_id: string }>(\n `SELECT instance_id, workflow, user_id FROM ${this.t.entry}\n WHERE admitted_at IS NULL AND status = 'running'\n AND COALESCE(claimed_at, ts) < ${TS('$1')}\n ORDER BY COALESCE(claimed_at, ts) LIMIT $2`,\n [claimedBefore, limit]\n );\n return rows.map((row) => ({\n instanceId: row.instance_id,\n workflow: row.workflow,\n userId: row.user_id,\n }));\n }\n\n async failUnadmittedEntry(instanceId: string): Promise<boolean> {\n // Conditional on the same state the caller saw: a runner that admitted itself meanwhile, or a\n // reclaim under a new instance id, leaves the row alone\n const rows = await this.sql.query(\n `UPDATE ${this.t.entry} SET status = 'failed'\n WHERE instance_id = $1 AND status = 'running' AND admitted_at IS NULL\n RETURNING instance_id`,\n [instanceId]\n );\n return rows.length > 0;\n }\n\n async setEntryStatus(instanceId: string, status: string): Promise<void> {\n await this.sql.query(`UPDATE ${this.t.entry} SET status = $1 WHERE instance_id = $2`, [\n status,\n instanceId,\n ]);\n }\n\n async findWakeHandles(userId: string, event: string): Promise<string[]> {\n const rows = await this.sql.query<{ handle: string }>(\n `SELECT DISTINCT wake_handle AS handle FROM ${this.t.subscription} WHERE user_id = $1 AND event = $2`,\n [userId, event]\n );\n return rows.map((row) => row.handle);\n }\n\n async subscribe(\n handle: string,\n userId: string,\n events: readonly string[],\n ts: number\n ): Promise<void> {\n // One locked transaction for the call's registrations: all of them, or none\n await this.sql.transaction(async (tx) => {\n await this.lockUser(tx, userId);\n let skipped = false;\n for (const event of events) {\n const rows = await tx.query(\n `INSERT INTO ${this.t.subscription} (user_id, event, wake_handle, ts)\n SELECT $1, $2, $3, ${TS('$4')} ${this.notErased('$1')}\n ON CONFLICT DO NOTHING RETURNING event`,\n [userId, event, handle, ts]\n );\n if (rows.length === 0) skipped = true;\n }\n // A registration already there, or refused: one read tells which\n if (skipped) await this.refuseIfErased(tx, userId);\n });\n }\n\n async unsubscribe(handle: string): Promise<void> {\n await this.sql.query(`DELETE FROM ${this.t.subscription} WHERE wake_handle = $1`, [handle]);\n }\n}\n\ninterface ExecutionRow {\n instance_id: string;\n workflow: string;\n user_id: string;\n name: string;\n target: string;\n input: string;\n // float8 comes back as a number from most drivers, as a string from some\n created_at: number | string;\n submitted_at: number | string | null;\n arn: string | null;\n unresolved_at: number | string | null;\n callback_id: string | null;\n generation: number | string;\n deadline_at: number | string | null;\n checked_at: number | string | null;\n stopped_at: number | string | null;\n}\n\nconst PLAIN_COLUMNS = ['instance_id', 'workflow', 'user_id', 'name', 'target', 'input', 'arn'];\nconst MORE_PLAIN = ['callback_id', 'generation'];\nconst TIME_COLUMNS = [\n 'created_at',\n 'submitted_at',\n 'unresolved_at',\n 'deadline_at',\n 'checked_at',\n 'stopped_at',\n];\n\n/** The execution columns, timestamps as epoch ms; `alias` qualifies them in a join. */\nfunction executionColumns(alias = ''): string {\n const q = alias === '' ? '' : `${alias}.`;\n return [\n ...[...PLAIN_COLUMNS, ...MORE_PLAIN].map((column) => `${q}${column}`),\n ...TIME_COLUMNS.map(\n (column) => `(extract(epoch from ${q}${column}) * 1000)::double precision AS ${column}`\n ),\n ].join(', ');\n}\n\nconst optional = <T>(value: T | null): T | undefined => value ?? undefined;\nconst ms = (value: number | string | null): number | undefined =>\n value === null ? undefined : Number(value);\n\nfunction toRecord(row: ExecutionRow): ExecutionRecord {\n return {\n instanceId: row.instance_id,\n workflow: row.workflow,\n userId: row.user_id,\n name: row.name,\n target: row.target,\n input: row.input,\n createdAt: Number(row.created_at),\n submittedAt: ms(row.submitted_at),\n arn: optional(row.arn),\n unresolvedAt: ms(row.unresolved_at),\n callbackId: optional(row.callback_id),\n generation: Number(row.generation),\n deadlineAt: ms(row.deadline_at),\n checkedAt: ms(row.checked_at),\n stoppedAt: ms(row.stopped_at),\n };\n}\n\n/** The execution table (tables.ts): starts to confirm, and the callback each wait parks on. */\nclass PostgresExecutions implements ExecutionRegistry {\n constructor(\n private readonly sql: SqlClientLike,\n private readonly table: string,\n private readonly entry: string\n ) {}\n\n async get(instanceId: string): Promise<ExecutionRecord | undefined> {\n const rows = await this.sql.query<ExecutionRow>(\n `SELECT ${executionColumns()} FROM ${this.table} WHERE instance_id = $1`,\n [instanceId]\n );\n return rows.length === 0 ? undefined : toRecord(rows[0]);\n }\n\n async pendingStarts(filter: {\n workflow?: string;\n userId?: string;\n createdBefore?: number;\n limit: number;\n }): Promise<ExecutionRecord[]> {\n const where = ['x.submitted_at IS NULL', 'x.unresolved_at IS NULL', \"e.status = 'running'\"];\n const params: unknown[] = [];\n if (filter.workflow !== undefined) {\n params.push(filter.workflow);\n where.push(`x.workflow = $${params.length}`);\n }\n if (filter.userId !== undefined) {\n params.push(filter.userId);\n where.push(`x.user_id = $${params.length}`);\n }\n if (filter.createdBefore !== undefined) {\n params.push(filter.createdBefore);\n where.push(`x.created_at < ${TS(`$${params.length}`)}`);\n }\n params.push(filter.limit);\n // Only the start the entry still names: a reclaimed entry's old start is history\n const rows = await this.sql.query<ExecutionRow>(\n `SELECT ${executionColumns('x')}\n FROM ${this.table} x JOIN ${this.entry} e ON e.instance_id = x.instance_id\n WHERE ${where.join(' AND ')} ORDER BY x.created_at LIMIT $${params.length}`,\n params\n );\n return rows.map(toRecord);\n }\n\n async markSubmitted(instanceId: string, arn: string | undefined, at: number): Promise<void> {\n await this.sql.query(\n `UPDATE ${this.table} SET submitted_at = COALESCE(submitted_at, ${TS('$2')}), arn = COALESCE($3, arn) WHERE instance_id = $1`,\n [instanceId, at, arn ?? null]\n );\n }\n\n async markUnresolved(instanceId: string, at: number): Promise<void> {\n await this.sql.query(\n `UPDATE ${this.table} SET unresolved_at = ${TS('$2')} WHERE instance_id = $1 AND submitted_at IS NULL`,\n [instanceId, at]\n );\n }\n\n async setArn(instanceId: string, arn: string): Promise<void> {\n await this.sql.query(`UPDATE ${this.table} SET arn = $2 WHERE instance_id = $1`, [\n instanceId,\n arn,\n ]);\n }\n\n async armCallback(\n instanceId: string,\n callback: { id: string; generation: number; deadlineAt?: number | undefined }\n ): Promise<boolean> {\n // The generation test is inside the UPDATE: a retried arm of an older wait cannot overwrite\n // the callback a newer one parks on\n const rows = await this.sql.query(\n `UPDATE ${this.table} SET callback_id = $2, generation = $3,\n deadline_at = CASE WHEN $4::double precision IS NULL THEN NULL ELSE ${TS('$4')} END\n WHERE instance_id = $1 AND generation <= $3 RETURNING instance_id`,\n [instanceId, callback.id, callback.generation, callback.deadlineAt ?? null]\n );\n return rows.length > 0;\n }\n\n async clearCallback(instanceId: string, generation: number): Promise<void> {\n await this.sql.query(\n `UPDATE ${this.table} SET callback_id = NULL, deadline_at = NULL WHERE instance_id = $1 AND generation = $2`,\n [instanceId, generation]\n );\n }\n\n async toReconcile(limit: number, checkedBefore: number): Promise<ExecutionRecord[]> {\n const rows = await this.sql.query<ExecutionRow>(\n `SELECT ${executionColumns('x')}\n FROM ${this.table} x JOIN ${this.entry} e ON e.instance_id = x.instance_id\n WHERE e.status = 'running' AND x.submitted_at IS NOT NULL\n AND (x.checked_at IS NULL OR x.checked_at < ${TS('$1')})\n ORDER BY x.checked_at NULLS FIRST LIMIT $2`,\n [checkedBefore, limit]\n );\n return rows.map(toRecord);\n }\n\n async markStopped(instanceId: string, at: number): Promise<void> {\n await this.sql.query(\n `UPDATE ${this.table} SET stopped_at = COALESCE(stopped_at, ${TS('$2')}) WHERE instance_id = $1`,\n [instanceId, at]\n );\n }\n\n async markChecked(instanceId: string, at: number): Promise<void> {\n await this.sql.query(\n `UPDATE ${this.table} SET checked_at = ${TS('$2')} WHERE instance_id = $1`,\n [instanceId, at]\n );\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"postgres.cjs","names":["visitorSubject","TABLES","PostgresReporting","columnName","indexName","uniqueConstraintName","uuidv7","UserErasedError","lockUserTransaction","conditionToSql","visitorIdOf","beginReadCommitted","lockUser","ConditionIR","WorkflowIR"],"sources":["../../src/store/postgres.ts"],"sourcesContent":["import type { PushDevice } from '../engine/ports';\nimport { type BundleIR, ConditionIR, WorkflowIR } from '../ir';\nimport { conditionToSql } from './condition-sql';\nimport { type ErasureState, UserErasedError } from './erasure';\nimport { visitorIdOf, visitorSubject } from './identity';\nimport type {\n AdmittedEntry,\n BundleVersion,\n DeviceInput,\n EntryInput,\n EntryOutcome,\n EntryState,\n EventCursor,\n EventInput,\n EventWindow,\n ExecutionRecord,\n ExecutionRegistry,\n ExecutionStart,\n JourneyReporting,\n JourneyStore,\n ProfileProps,\n RunningInstance,\n SegmentCandidate,\n SegmentTriggerRoute,\n SuppressionChange,\n TriggerRoute,\n UnadmittedEntry,\n VisitorMerge,\n} from './index';\nimport { PostgresReporting } from './postgres-reporting';\nimport { type SqlClientLike, uuidv7 } from './sql';\nimport {\n type ColumnType,\n DEFAULT_SCHEMA,\n TABLES,\n type TableKey,\n columnName,\n indexName,\n uniqueConstraintName,\n} from './tables';\nimport { beginReadCommitted, lockUser, lockUserTransaction } from './user-lock';\n\n/** Who a device row belongs to: its user, or before sign-in its visitor. */\nfunction deviceOwner(row: { user_id: string | null; visitor_id: string | null }) {\n if (row.user_id !== null) return row.user_id;\n return row.visitor_id === null ? undefined : visitorSubject(row.visitor_id);\n}\n\nexport { DEFAULT_SCHEMA } from './tables';\nexport { type SqlClientLike, uuidv7 } from './sql';\n\nexport interface PostgresJourneyStoreOptions {\n /**\n * The Postgres schema the engine's tables live in — `workflow` by default, the config's\n * `database.schema` under journeyWorker. A schema of its own keeps the tables apart from the\n * app's. The host creates them with its own migrations, from the drizzle file\n * `renderDrizzleSchema` generates (or the plain `renderPostgresDdl`).\n */\n schema?: string;\n}\n\n/** information_schema's udt_name per column type. */\nconst UDT: Record<ColumnType, string> = {\n uuid: 'uuid',\n text: 'text',\n jsonb: 'jsonb',\n timestamptz: 'timestamptz',\n integer: 'int4',\n boolean: 'bool',\n};\n\n/** Epoch milliseconds → timestamptz, so the tables keep the database's native type. */\nconst TS = (param: string) => `to_timestamp(${param}::double precision / 1000)`;\n\nfunction asJson<T>(value: unknown): T {\n return (typeof value === 'string' ? JSON.parse(value) : value) as T;\n}\n\n/**\n * The journey data plane on Postgres: native types where Postgres has them\n * (jsonb, timestamptz), and the concurrency guarantees come from the database: ON CONFLICT for the once policy and\n * idempotent subscriptions, a transaction for the route swap, a jsonb merge\n * for /identify. The tables are declared in tables.ts.\n *\n * Every write about a user is conditional on their erasure tombstone not existing\n * (`INSERT ... SELECT ... WHERE NOT EXISTS`, see `notErased`), and runs in a transaction that\n * holds the user's advisory lock (`lockUser`, the protocol in user-lock.ts): an event, a\n * profile merge, a device, a subscription, a segment membership with its starts, an entry, an\n * admission, and the reporting's instance and message rows (postgres-reporting.ts). The\n * erasure's two steps, `requestErasure` and `forgetUser`, take the same lock. Under READ\n * COMMITTED a conditional write sees only what was committed when its statement began, and an\n * erasure sees only rows committed when it looked — so without the lock a write's transaction\n * could commit its rows after the erase deleted everything (the user comes back), and a runner\n * could admit itself after the tombstone (the instance runs on). With it, every write and the\n * erasure of one user run one after another: a write either commits before the tombstone,\n * where the erasure finds and deletes it, or waits for the lock, reads the tombstone and is\n * refused (`UserErasedError`). Nothing lands after the erase, so there is no second pass. The\n * conditional statement stays the in-transaction check; a write that wrote nothing reads the\n * tombstone on that path only, to tell a refusal from a plain no-op (`refuseIfErased`).\n * Suppressions are the one exception: an opt-out is written whatever the user's state.\n */\nexport class PostgresJourneyStore implements JourneyStore {\n private readonly t: Record<TableKey, string>;\n private readonly schema: string;\n readonly reporting: JourneyReporting;\n readonly executions: ExecutionRegistry;\n\n constructor(\n private readonly sql: SqlClientLike,\n options: PostgresJourneyStoreOptions = {}\n ) {\n this.schema = options.schema ?? DEFAULT_SCHEMA;\n const qualify = (name: string) => `\"${this.schema}\".\"${name}\"`;\n this.t = Object.fromEntries(\n (Object.keys(TABLES) as TableKey[]).map((key) => [key, qualify(TABLES[key].name)])\n ) as Record<TableKey, string>;\n this.reporting = new PostgresReporting(sql, this.t, this.schema);\n this.executions = new PostgresExecutions(sql, this.t.execution, this.t.entry);\n }\n\n /**\n * What the live database lacks against tables.ts: missing tables, missing columns, a column\n * of another type or nullability, and missing unique indexes (the store's ON CONFLICT targets\n * — without one the first ingest fails, not the deploy). Read-only — the engine never migrates\n * the host's database; a deploy fails with these problems listed, and the fix is to regenerate\n * the drizzle file and run the host's migrations.\n */\n async checkSchema(): Promise<string[]> {\n // In a transaction, so Hyperdrive answers from the database, not from its read cache — a\n // deploy right after the migration must see the new columns\n const { columns, indexes } = await this.sql.transaction(async (tx) => ({\n columns: await tx.query<{\n table_name: string;\n column_name: string;\n udt_name: string;\n is_nullable: string;\n }>(\n `SELECT table_name, column_name, udt_name, is_nullable FROM information_schema.columns WHERE table_schema = $1`,\n [this.schema]\n ),\n indexes: await tx.query<{ tablename: string; indexname: string; indexdef: string }>(\n `SELECT tablename, indexname, indexdef FROM pg_indexes WHERE schemaname = $1`,\n [this.schema]\n ),\n }));\n const live = new Map(columns.map((row) => [`${row.table_name}.${row.column_name}`, row]));\n const tables = new Set(columns.map((row) => row.table_name));\n const liveIndexes = new Map(indexes.map((row) => [`${row.tablename}.${row.indexname}`, row]));\n const problems: string[] = [];\n for (const spec of Object.values(TABLES)) {\n const where = `\"${this.schema}\".\"${spec.name}\"`;\n if (!tables.has(spec.name)) {\n problems.push(`table ${where} is missing`);\n continue;\n }\n for (const [key, column] of Object.entries(spec.columns)) {\n const name = columnName(key);\n const row = live.get(`${spec.name}.${name}`);\n if (row === undefined) problems.push(`column ${where}.\"${name}\" is missing`);\n else if (row.udt_name !== UDT[column.type])\n problems.push(\n `column ${where}.\"${name}\" is ${row.udt_name}, expected ${UDT[column.type]}`\n );\n else if ((row.is_nullable === 'YES') !== (column.nullable === true))\n problems.push(\n `column ${where}.\"${name}\" should be ${column.nullable ? 'nullable' : 'NOT NULL'}`\n );\n }\n // The names drizzle-kit and renderPostgresDdl both give an unnamed unique index\n for (const keys of spec.uniqueIndexes ?? []) {\n const name = indexName(spec, keys);\n const row = liveIndexes.get(`${spec.name}.${name}`);\n if (row === undefined) problems.push(`missing unique index \"${this.schema}\".\"${name}\"`);\n else if (!/^CREATE UNIQUE INDEX/i.test(row.indexdef))\n problems.push(`index \"${this.schema}\".\"${name}\" is not unique`);\n }\n // A unique constraint is backed by an index of its name; its definition says how nulls compare\n for (const unique of spec.uniqueConstraints ?? []) {\n const name = uniqueConstraintName(spec, unique);\n const row = liveIndexes.get(`${spec.name}.${name}`);\n if (row === undefined) {\n problems.push(`missing unique constraint \"${this.schema}\".\"${name}\"`);\n } else if (!/^CREATE UNIQUE INDEX/i.test(row.indexdef)) {\n problems.push(`index \"${this.schema}\".\"${name}\" is not unique`);\n } else if (!/NULLS NOT DISTINCT/i.test(row.indexdef)) {\n problems.push(`unique constraint \"${this.schema}\".\"${name}\" must be NULLS NOT DISTINCT`);\n }\n }\n }\n return problems;\n }\n\n async insertEvent(input: EventInput): Promise<boolean> {\n // The row key stays a UUID v7 (the house rule for every table); the\n // sender's occurrence id lives in event_id, unique per user, which is\n // what makes a second delivery a no-op. ON CONFLICT DO NOTHING + RETURNING\n // reports which of the two happened without a second round trip.\n return this.sql.transaction(async (tx) => {\n await this.lockUser(tx, input.userId);\n const rows = await tx.query(\n `INSERT INTO ${this.t.event} (id, event_id, user_id, name, ts, payload)\n SELECT $1::uuid, $2, $3, $4, ${TS('$5')}, $6::text::jsonb ${this.notErased('$3')}\n ON CONFLICT (user_id, event_id) DO NOTHING RETURNING id`,\n [\n uuidv7(input.ts),\n input.id,\n input.userId,\n input.event,\n input.ts,\n JSON.stringify(input.payload),\n ]\n );\n if (rows.length > 0) return true;\n // Nothing written: a duplicate, unless the tombstone refused it\n await this.refuseIfErased(tx, input.userId);\n return false;\n });\n }\n\n /** The condition every write about a user carries: no erasure tombstone for `$user`. */\n private notErased(user: string): string {\n return `WHERE NOT EXISTS (SELECT 1 FROM ${this.t.erasure} WHERE user_id = ${user})`;\n }\n\n /**\n * A conditional write wrote nothing: on that path only, read why. Throws when the tombstone\n * refused it; returns when the statement had another reason (a duplicate, an entry taken).\n */\n private async refuseIfErased(sql: SqlClientLike, userId: string): Promise<void> {\n const rows = await sql.query(`SELECT 1 AS x FROM ${this.t.erasure} WHERE user_id = $1`, [\n userId,\n ]);\n if (rows.length > 0) throw new UserErasedError(userId);\n }\n\n /**\n * The first two statements of every transaction that writes about the user (user-lock.ts):\n * READ COMMITTED, then the user's advisory lock, held until the transaction ends. Any read\n * about the user comes after, on a snapshot taken once the lock is held.\n */\n private lockUser(tx: SqlClientLike, userId: string): Promise<void> {\n return lockUserTransaction(tx, this.schema, userId);\n }\n\n /** The event rows of one user and name, within the window's bounds (both inclusive). */\n private eventWindow(userId: string, event: string, window?: EventWindow) {\n const params: unknown[] = [userId, event];\n let where = 'user_id = $1 AND name = $2';\n if (window?.sinceMs !== undefined) {\n params.push(window.sinceMs);\n where += ` AND ts >= ${TS(`$${params.length}`)}`;\n }\n if (window?.untilMs !== undefined) {\n params.push(window.untilMs);\n where += ` AND ts <= ${TS(`$${params.length}`)}`;\n }\n return { where, params };\n }\n\n async countEvents(\n userId: string,\n event: string,\n window?: EventWindow,\n atLeast?: number\n ): Promise<number> {\n const { where, params } = this.eventWindow(userId, event, window);\n // A LIMITed inner scan stops on the (user_id, name, ts) index once enough rows are seen\n const capped = atLeast !== undefined && Number.isInteger(atLeast) && atLeast > 0;\n if (capped) params.push(atLeast);\n const rows = await this.sql.query<{ c: number | string }>(\n capped\n ? `SELECT count(*)::int AS c FROM (SELECT 1 FROM ${this.t.event} WHERE ${where} LIMIT $${params.length}) capped`\n : `SELECT count(*)::int AS c FROM ${this.t.event} WHERE ${where}`,\n params\n );\n return Number(rows.at(0)?.c ?? 0);\n }\n\n async pageEventPayloads(\n userId: string,\n event: string,\n window: EventWindow | undefined,\n page: { limit: number; after?: EventCursor | undefined }\n ): Promise<{ payloads: Record<string, unknown>[]; next?: EventCursor | undefined }> {\n const { where, params } = this.eventWindow(userId, event, window);\n let keyset = '';\n if (page.after !== undefined) {\n params.push(page.after.ts, page.after.id);\n keyset = ` AND (ts, id) > ($${params.length - 1}::timestamptz, $${params.length}::uuid)`;\n }\n params.push(page.limit);\n // `ts::text` is the cursor: Postgres renders and re-reads its own timestamp exactly, where\n // a float of epoch milliseconds could land a hair off and repeat or skip a row\n const rows = await this.sql.query<{ payload: unknown; ts: string; id: string }>(\n `SELECT payload, ts::text AS ts, id::text AS id FROM ${this.t.event}\n WHERE ${where}${keyset} ORDER BY ts, id LIMIT $${params.length}`,\n params\n );\n const last = rows.at(-1);\n return {\n payloads: rows.map((row) => asJson<Record<string, unknown>>(row.payload)),\n ...(last !== undefined && rows.length === page.limit\n ? { next: { ts: last.ts, id: last.id } }\n : {}),\n };\n }\n\n async getProfile(userId: string): Promise<ProfileProps | undefined> {\n const rows = await this.sql.query<{ props: unknown }>(\n `SELECT props FROM ${this.t.profile} WHERE user_id = $1`,\n [userId]\n );\n const row = rows.at(0);\n return row === undefined ? undefined : asJson<ProfileProps>(row.props);\n }\n\n async mergeProfile(\n userId: string,\n props: ProfileProps,\n at: number = Date.now()\n ): Promise<ProfileProps> {\n // RFC 7386 merge-patch: jsonb `||` overwrites key by key and jsonb_strip_nulls then drops the\n // keys the patch set to null (the API's only way to unset a property). Under the user's lock,\n // so two concurrent identifies cannot lose each other's keys; a key written later than `at`\n // keeps its value, cleared ones included (their time stays in props_at).\n return this.sql.transaction(async (tx) => {\n await this.lockUser(tx, userId);\n const existing = await tx.query<{ props_at: unknown }>(\n `SELECT props_at FROM ${this.t.profile} WHERE user_id = $1`,\n [userId]\n );\n const times = asJson<Record<string, number> | null>(existing[0]?.props_at ?? null) ?? {};\n const patch = Object.fromEntries(\n Object.entries(props).filter(([key]) => (times[key] ?? Number.NEGATIVE_INFINITY) <= at)\n );\n const stamped = { ...times, ...Object.fromEntries(Object.keys(patch).map((k) => [k, at])) };\n const rows = await tx.query<{ props: unknown }>(\n `INSERT INTO ${this.t.profile} (user_id, props, props_at)\n SELECT $1, jsonb_strip_nulls($2::text::jsonb), $4::text::jsonb ${this.notErased('$1')}\n ON CONFLICT (user_id) DO UPDATE SET\n props = jsonb_strip_nulls(${this.t.profile}.props || $3::text::jsonb),\n props_at = $4::text::jsonb\n RETURNING props`,\n // EXCLUDED.props would be the stripped insert value, so the update merges the raw patch again\n [userId, JSON.stringify(patch), JSON.stringify(patch), JSON.stringify(stamped)]\n );\n // The upsert always yields its row — unless the tombstone refused it\n if (rows.length === 0) throw new UserErasedError(userId);\n return asJson<ProfileProps>(rows[0].props);\n });\n }\n\n async listSegmentCandidates(page: {\n segments: readonly { id: string; condition: ConditionIR }[];\n now: number;\n after?: string | undefined;\n limit: number;\n }): Promise<SegmentCandidate[]> {\n if (page.segments.length === 0) return [];\n const params: unknown[] = [];\n const bind = (value: unknown) => {\n params.push(value);\n return `$${params.length}`;\n };\n const definitions = new Map(page.segments.map((segment) => [segment.id, segment.condition]));\n const branches = page.segments.map(({ id, condition }) => {\n const segment = bind(id);\n const matches = conditionToSql(condition, {\n tables: this.t,\n user: 'u.user_id',\n props: 'p.props',\n nowMs: page.now,\n segments: definitions,\n bind,\n });\n const key = `segment = ${segment}::text AND user_id = u.user_id`;\n // NULL is distinct from both, so whatever SQL cannot decide goes to the evaluator\n return `SELECT ${segment}::text AS segment\n WHERE (${matches}) IS DISTINCT FROM EXISTS (SELECT 1 FROM ${this.t.segmentMember} WHERE ${key})\n OR EXISTS (SELECT 1 FROM ${this.t.segmentStart} WHERE ${key})`;\n });\n let keyset = '';\n if (page.after !== undefined) keyset = ` AND user_id > ${bind(page.after)}::text`;\n const rows = await this.sql.query<{ user_id: string; segments: unknown }>(\n `WITH u AS MATERIALIZED (\n SELECT user_id FROM (\n SELECT user_id FROM ${this.t.profile}\n UNION SELECT user_id FROM ${this.t.event}\n UNION SELECT user_id FROM ${this.t.segmentMember}\n ) known\n -- Segments are about users: a visitor is not one until they sign in\n WHERE user_id NOT LIKE '${visitorSubject('')}%'${keyset}\n )\n SELECT u.user_id, json_agg(c.segment) AS segments\n FROM u\n LEFT JOIN ${this.t.profile} p ON p.user_id = u.user_id\n CROSS JOIN LATERAL (${branches.join('\\n UNION ALL ')}) c\n GROUP BY u.user_id\n ORDER BY u.user_id\n LIMIT ${bind(page.limit)}::int`,\n params\n );\n return rows.map((row) => ({ userId: row.user_id, segments: asJson<string[]>(row.segments) }));\n }\n\n listDevices(subject: string): Promise<PushDevice[]> {\n const visitorId = visitorIdOf(subject);\n return this.sql.query<PushDevice>(\n visitorId === undefined\n ? `SELECT token, platform FROM ${this.t.device} WHERE user_id = $1 ORDER BY updated_at DESC`\n : // A device a user has taken over is theirs, whichever visitor it still names\n `SELECT token, platform FROM ${this.t.device} WHERE visitor_id = $1 AND user_id IS NULL ORDER BY updated_at DESC`,\n [visitorId ?? subject]\n );\n }\n\n async upsertDevice(device: DeviceInput): Promise<string[]> {\n const { deviceId = null, userId, visitorId = null, token, platform, at } = device;\n const subject = userId ?? visitorSubject(visitorId ?? '');\n return this.sql.transaction(async (tx) => {\n await this.lockUser(tx, subject);\n // Who held this token, or this device under an older token: their device waits must wake\n const previous = await tx.query<{\n token: string;\n user_id: string | null;\n visitor_id: string | null;\n }>(\n `SELECT token, user_id, visitor_id FROM ${this.t.device} WHERE token = $1 OR device_id = $2 FOR UPDATE`,\n [token, deviceId]\n );\n // A device id is only the client's word. A new token replaces the old one for the same owner\n // or for nobody; someone else's token keeps working, it only stops naming this device.\n for (const row of previous) {\n if (row.token === token) continue;\n const owner = deviceOwner(row);\n await tx.query(\n owner === undefined || owner === subject\n ? `DELETE FROM ${this.t.device} WHERE token = $1`\n : `UPDATE ${this.t.device} SET device_id = NULL WHERE token = $1`,\n [row.token]\n );\n }\n const rows = await tx.query(\n `INSERT INTO ${this.t.device} (device_id, user_id, visitor_id, token, platform, created_at, updated_at)\n SELECT $1, $2, $3, $4, $5, ${TS('$6')}, ${TS('$6')} ${this.notErased('$7')}\n ON CONFLICT (token) DO UPDATE SET\n device_id = COALESCE(EXCLUDED.device_id, ${this.t.device}.device_id),\n user_id = EXCLUDED.user_id,\n visitor_id = COALESCE(EXCLUDED.visitor_id, ${this.t.device}.visitor_id),\n platform = EXCLUDED.platform,\n updated_at = EXCLUDED.updated_at\n RETURNING token`,\n [deviceId, userId, visitorId, token, platform, at, subject]\n );\n if (rows.length === 0) throw new UserErasedError(subject);\n const owners = previous.flatMap((row) => {\n const owner = deviceOwner(row);\n return owner === undefined || owner === subject ? [] : [owner];\n });\n return [...new Set(owners)];\n });\n }\n\n async linkedUser(visitorId: string): Promise<string | undefined> {\n const rows = await this.sql.query<{ user_id: string | null }>(\n `SELECT user_id FROM ${this.t.visitor} WHERE visitor_id = $1`,\n [visitorId]\n );\n return rows[0]?.user_id ?? undefined;\n }\n\n async touchVisitor(visitorId: string, at: number): Promise<string> {\n const rows = await this.sql.query<{ user_id: string | null }>(\n `INSERT INTO ${this.t.visitor} (visitor_id, user_id, created_at, updated_at)\n VALUES ($1, NULL, ${TS('$2')}, ${TS('$2')})\n ON CONFLICT (visitor_id) DO UPDATE SET updated_at = EXCLUDED.updated_at\n RETURNING user_id`,\n [visitorId, at]\n );\n return rows[0]?.user_id ?? visitorSubject(visitorId);\n }\n\n async pruneVisitors(before: number, limit: number): Promise<number> {\n const idle = `user_id IS NULL AND updated_at < ${TS('$1')} AND NOT EXISTS (\n SELECT 1 FROM ${this.t.entry} e\n WHERE e.user_id = '${visitorSubject('')}' || visitor_id AND e.status = 'running')`;\n const candidates = await this.sql.query<{ visitor_id: string }>(\n `SELECT visitor_id FROM ${this.t.visitor} WHERE ${idle} ORDER BY updated_at LIMIT $2`,\n [before, limit]\n );\n let pruned = 0;\n for (const { visitor_id: visitorId } of candidates) {\n const subject = visitorSubject(visitorId);\n await this.sql.transaction(async (tx) => {\n await this.lockUser(tx, subject);\n // Asked again under the lock: they may have come back, or signed in, since the list\n const gone = await tx.query(\n `DELETE FROM ${this.t.visitor} WHERE visitor_id = $2 AND ${idle} RETURNING visitor_id`,\n [before, visitorId]\n );\n if (gone.length === 0) return;\n await this.deleteSubjectRows(tx, subject);\n pruned += 1;\n });\n }\n return pruned;\n }\n\n async entryOwner(instanceId: string): Promise<string | undefined> {\n const rows = await this.sql.query<{ user_id: string }>(\n `SELECT user_id FROM ${this.t.entry} WHERE instance_id = $1`,\n [instanceId]\n );\n return rows[0]?.user_id;\n }\n\n async linkVisitor(visitorId: string, userId: string, at: number): Promise<VisitorMerge | null> {\n const subject = visitorSubject(visitorId);\n // The common case, every signed-in batch: no lock taken\n if ((await this.linkedUser(visitorId)) !== undefined) return null;\n return this.sql.transaction(async (tx) => {\n await beginReadCommitted(tx);\n // In one order, so two merges cannot deadlock\n for (const key of [subject, userId].sort()) {\n await lockUser(tx, this.schema, key);\n }\n const linked = await tx.query(\n `INSERT INTO ${this.t.visitor} (visitor_id, user_id, created_at, updated_at)\n SELECT $1, $2, ${TS('$3')}, ${TS('$3')} ${this.notErased('$2')}\n ON CONFLICT (visitor_id) DO UPDATE SET user_id = EXCLUDED.user_id, updated_at = EXCLUDED.updated_at\n WHERE ${this.t.visitor}.user_id IS NULL\n RETURNING visitor_id`,\n [visitorId, userId, at]\n );\n if (linked.length === 0) {\n const erased = await tx.query(`SELECT 1 FROM ${this.t.erasure} WHERE user_id = $1`, [\n userId,\n ]);\n if (erased.length > 0) throw new UserErasedError(userId);\n return null;\n }\n // The user's own entry wins: the visitor's copy of that journey stops\n const dropped = await tx.query<{ instance_id: string }>(\n `DELETE FROM ${this.t.entry} v USING ${this.t.entry} u\n WHERE v.user_id = $1 AND u.user_id = $2 AND u.workflow = v.workflow\n RETURNING v.instance_id`,\n [subject, userId]\n );\n const droppedIds = dropped.map((row) => row.instance_id);\n // Gone with its entry: nothing should wake it, and no start of it is owed\n await tx.query(`DELETE FROM ${this.t.subscription} WHERE wake_handle = ANY($1::text[])`, [\n droppedIds,\n ]);\n await tx.query(`DELETE FROM ${this.t.execution} WHERE instance_id = ANY($1::text[])`, [\n droppedIds,\n ]);\n // One occurrence per id: a repeat the user already has is not moved\n await tx.query(\n `DELETE FROM ${this.t.event} v USING ${this.t.event} u\n WHERE v.user_id = $1 AND u.user_id = $2 AND u.event_id = v.event_id`,\n [subject, userId]\n );\n const events = await tx.query<{ name: string }>(\n `SELECT DISTINCT name FROM ${this.t.event} WHERE user_id = $1`,\n [subject]\n );\n // An opt-out made before sign-in still holds\n await tx.query(\n `INSERT INTO ${this.t.suppression} (user_id, channel, topic, ts)\n SELECT $2, channel, topic, ts FROM ${this.t.suppression} WHERE user_id = $1\n ON CONFLICT (user_id, channel, topic) DO NOTHING`,\n [subject, userId]\n );\n await tx.query(`DELETE FROM ${this.t.suppression} WHERE user_id = $1`, [subject]);\n const props = await this.mergeProfileInto(tx, subject, userId);\n for (const table of [\n this.t.event,\n this.t.entry,\n this.t.execution,\n this.t.subscription,\n this.t.instance,\n this.t.message,\n ]) {\n await tx.query(`UPDATE ${table} SET user_id = $2 WHERE user_id = $1`, [subject, userId]);\n }\n await tx.query(\n `UPDATE ${this.t.device} SET user_id = $2, updated_at = ${TS('$3')}\n WHERE visitor_id = $1 AND user_id IS NULL`,\n [visitorId, userId, at]\n );\n return { dropped: droppedIds, events: events.map((row) => row.name), props };\n });\n }\n\n /**\n * The visitor's properties under the user's, a key the user has a time for going to the later\n * write, one a row older than the times going to the user if they hold it. The visitor's row\n * goes. Answers the keys that moved.\n */\n private async mergeProfileInto(\n tx: SqlClientLike,\n subject: string,\n userId: string\n ): Promise<string[]> {\n const rows = await tx.query<{ user_id: string; props: unknown; props_at: unknown }>(\n `SELECT user_id, props, props_at FROM ${this.t.profile} WHERE user_id = ANY($1::text[])`,\n [[subject, userId]]\n );\n const from = rows.find((row) => row.user_id === subject);\n if (from === undefined) return [];\n const to = rows.find((row) => row.user_id === userId);\n const fromTimes = asJson<Record<string, number> | null>(from.props_at ?? null) ?? {};\n const toTimes = asJson<Record<string, number> | null>(to?.props_at ?? null) ?? {};\n const toProps = to === undefined ? {} : asJson<ProfileProps>(to.props);\n const moved = Object.entries(asJson<ProfileProps>(from.props)).filter(([key]) => {\n const written = toTimes[key];\n return written === undefined ? !Object.hasOwn(toProps, key) : (fromTimes[key] ?? 0) > written;\n });\n const props = { ...toProps, ...Object.fromEntries(moved) };\n const times = {\n ...toTimes,\n ...Object.fromEntries(moved.map(([key]) => [key, fromTimes[key] ?? 0])),\n };\n await tx.query(\n `INSERT INTO ${this.t.profile} (user_id, props, props_at) VALUES ($1, $2::text::jsonb, $3::text::jsonb)\n ON CONFLICT (user_id) DO UPDATE SET props = EXCLUDED.props, props_at = EXCLUDED.props_at`,\n [userId, JSON.stringify(props), JSON.stringify(times)]\n );\n await tx.query(`DELETE FROM ${this.t.profile} WHERE user_id = $1`, [subject]);\n return moved.map(([key]) => key);\n }\n\n async requestErasure(userId: string, at: number): Promise<void> {\n // Under the user's lock: a write in flight commits before the tombstone (and is found by\n // the erase) or reads it after (and is refused)\n await this.sql.transaction(async (tx) => {\n await this.lockUser(tx, userId);\n await tx.query(\n `INSERT INTO ${this.t.erasure} (user_id, requested_at) VALUES ($1, ${TS('$2')})\n ON CONFLICT DO NOTHING`,\n [userId, at]\n );\n });\n }\n\n async pendingErasures(): Promise<string[]> {\n const rows = await this.sql.query<{ user_id: string }>(\n `SELECT user_id FROM ${this.t.erasure} WHERE erased_at IS NULL ORDER BY requested_at`\n );\n return rows.map((row) => row.user_id);\n }\n\n async erasureState(userId: string): Promise<ErasureState> {\n const rows = await this.sql.query<{ erased: boolean }>(\n `SELECT erased_at IS NOT NULL AS erased FROM ${this.t.erasure} WHERE user_id = $1`,\n [userId]\n );\n const row = rows.at(0);\n if (row === undefined) return 'none';\n return row.erased ? 'erased' : 'erasing';\n }\n\n async runningInstances(userId: string): Promise<RunningInstance[]> {\n const rows = await this.sql.query<{ instance_id: string; admitted: boolean }>(\n `SELECT instance_id, admitted_at IS NOT NULL AS admitted\n FROM ${this.t.entry} WHERE user_id = $1 AND status = 'running'`,\n [userId]\n );\n return rows.map((row) => ({ instanceId: row.instance_id, admitted: row.admitted }));\n }\n\n async admitInstance(instanceId: string, userId: string): Promise<boolean> {\n return this.sql.transaction(async (tx) => {\n await this.lockUser(tx, userId);\n // A first admission needs a running entry; an instance once admitted may replay its\n // finalisation after the entry reached a terminal status, under its own instance id (a\n // reclaim resets `admitted_at`, so a superseded attempt cannot). The first admission's\n // time is kept, the status never touched, and the tombstone refuses both.\n const rows = await tx.query(\n `UPDATE ${this.t.entry} SET admitted_at = COALESCE(admitted_at, ${TS('$3')})\n WHERE instance_id = $1 AND user_id = $2\n AND (status = 'running' OR admitted_at IS NOT NULL)\n AND NOT EXISTS (SELECT 1 FROM ${this.t.erasure} WHERE user_id = $2)\n RETURNING instance_id`,\n [instanceId, userId, Date.now()]\n );\n return rows.length > 0;\n });\n }\n\n async forgetUser(userId: string, at: number): Promise<void> {\n await this.sql.transaction(async (tx) => {\n // Under the user's lock, every row committed before the tombstone is visible from here,\n // and no write about the user can commit until this transaction ends\n await this.lockUser(tx, userId);\n await this.deleteSubjectRows(tx, userId);\n // The tombstone stays, now marked erased; the first erase's time is kept by a repeat\n await tx.query(\n `UPDATE ${this.t.erasure} SET erased_at = COALESCE(erased_at, ${TS('$2')}) WHERE user_id = $1`,\n [userId, at]\n );\n });\n }\n\n /** Everything kept about a user or visitor, on a transaction holding their lock. */\n private async deleteSubjectRows(tx: SqlClientLike, subject: string): Promise<void> {\n // Children before the copies they belong to\n for (const child of [this.t.messageOpen, this.t.messageClick, this.t.messageLink]) {\n await tx.query(\n `DELETE FROM ${child} WHERE message_id IN (SELECT id FROM ${this.t.message} WHERE user_id = $1)`,\n [subject]\n );\n }\n for (const table of [\n this.t.message,\n this.t.instance,\n this.t.entry,\n this.t.execution,\n this.t.subscription,\n this.t.segmentStart,\n this.t.segmentMember,\n // The opt-outs too: the tombstone keeps the id from ever being sent to again\n this.t.suppression,\n this.t.device,\n this.t.event,\n this.t.profile,\n ]) {\n await tx.query(`DELETE FROM ${table} WHERE user_id = $1`, [subject]);\n }\n const visitorId = visitorIdOf(subject);\n if (visitorId !== undefined) {\n await tx.query(`DELETE FROM ${this.t.device} WHERE visitor_id = $1 AND user_id IS NULL`, [\n visitorId,\n ]);\n }\n }\n\n async isSuppressed(userId: string, channel: string, topic?: string): Promise<boolean> {\n // The channel-wide row (topic null) covers every topic; a topic's row covers that topic\n const rows = await this.sql.query(\n `SELECT 1 AS x FROM ${this.t.suppression}\n WHERE user_id = $1 AND channel = $2 AND (topic IS NULL OR topic = $3::text)`,\n [userId, channel, topic ?? null]\n );\n return rows.length > 0;\n }\n\n async listSuppressions(userId: string, channel: string): Promise<(string | null)[]> {\n const rows = await this.sql.query<{ topic: string | null }>(\n `SELECT topic FROM ${this.t.suppression} WHERE user_id = $1 AND channel = $2\n ORDER BY topic NULLS FIRST`,\n [userId, channel]\n );\n return rows.map((row) => row.topic);\n }\n\n async updateSuppressions(\n userId: string,\n channel: string,\n changes: readonly SuppressionChange[],\n at: number\n ): Promise<void> {\n if (changes.length === 0) return;\n await this.sql.transaction(async (tx) => {\n await this.lockUser(tx, userId);\n for (const { topic, suppressed } of changes) {\n if (suppressed) {\n // The first opt-out sets the time; a repeat keeps it\n await tx.query(\n `INSERT INTO ${this.t.suppression} (user_id, channel, topic, ts)\n VALUES ($1, $2, $3::text, ${TS('$4')}) ON CONFLICT DO NOTHING`,\n [userId, channel, topic, at]\n );\n } else {\n await tx.query(\n `DELETE FROM ${this.t.suppression}\n WHERE user_id = $1 AND channel = $2 AND topic IS NOT DISTINCT FROM $3::text`,\n [userId, channel, topic]\n );\n }\n }\n });\n }\n\n async removeDevice(token: string): Promise<string | undefined> {\n const rows = await this.sql.query<{ user_id: string | null; visitor_id: string | null }>(\n `DELETE FROM ${this.t.device} WHERE token = $1 RETURNING user_id, visitor_id`,\n [token]\n );\n return rows[0] === undefined ? undefined : deviceOwner(rows[0]);\n }\n\n async unbindDevice(token: string): Promise<string | undefined> {\n // The owner as it was before the update: RETURNING alone would only show the nulls\n const rows = await this.sql.query<{ user_id: string | null; visitor_id: string | null }>(\n `WITH old AS (SELECT token, user_id, visitor_id FROM ${this.t.device} WHERE token = $1 FOR UPDATE)\n UPDATE ${this.t.device} d SET user_id = NULL, visitor_id = NULL, updated_at = ${TS('$2')} FROM old WHERE d.token = old.token\n RETURNING old.user_id, old.visitor_id`,\n [token, Date.now()]\n );\n return rows[0] === undefined ? undefined : deviceOwner(rows[0]);\n }\n\n async listSegments(): Promise<{ id: string; condition: ConditionIR }[]> {\n const rows = await this.sql.query<{ id: string; condition: unknown }>(\n `SELECT id, condition FROM ${this.t.segment} ORDER BY id`\n );\n return rows.map((row) => ({\n id: row.id,\n condition: ConditionIR.parse(asJson(row.condition)),\n }));\n }\n\n async getSegmentCondition(id: string): Promise<ConditionIR | undefined> {\n const rows = await this.sql.query<{ condition: unknown }>(\n `SELECT condition FROM ${this.t.segment} WHERE id = $1`,\n [id]\n );\n const row = rows.at(0);\n return row === undefined ? undefined : ConditionIR.parse(asJson(row.condition));\n }\n\n async findTriggers(event: string): Promise<TriggerRoute[]> {\n const rows = await this.sql.query<{\n workflow: string;\n hash: string;\n where_clause: unknown;\n filter: unknown;\n visitors: boolean | null;\n version: string | null;\n }>(\n `SELECT workflow, hash, \"where\" AS where_clause, filter, visitors, version FROM ${this.t.trigger} WHERE event = $1`,\n [event]\n );\n return rows.map((row) => ({\n workflow: row.workflow,\n hash: row.hash,\n where: row.where_clause === null ? null : ConditionIR.parse(asJson(row.where_clause)),\n filter: row.filter === null ? null : ConditionIR.parse(asJson(row.filter)),\n visitors: row.visitors === true,\n version: row.version ?? undefined,\n }));\n }\n\n async listSegmentTriggers(): Promise<SegmentTriggerRoute[]> {\n const rows = await this.sql.query<{\n workflow: string;\n hash: string;\n segment: string;\n version: string | null;\n }>(`SELECT workflow, hash, segment, version FROM ${this.t.segmentTrigger}`);\n return rows.map((row) => ({\n workflow: row.workflow,\n hash: row.hash,\n segment: row.segment,\n version: row.version ?? undefined,\n }));\n }\n\n async putWorkflowIR(workflow: WorkflowIR): Promise<void> {\n await this.sql.query(\n `INSERT INTO ${this.t.ir} (content_hash, ir) VALUES ($1, $2::text::jsonb)\n ON CONFLICT (content_hash) DO UPDATE SET ir = EXCLUDED.ir`,\n [workflow.contentHash, JSON.stringify(workflow)]\n );\n }\n\n async getWorkflowIR(contentHash: string): Promise<WorkflowIR | undefined> {\n const rows = await this.sql.query<{ ir: unknown }>(\n `SELECT ir FROM ${this.t.ir} WHERE content_hash = $1`,\n [contentHash]\n );\n const row = rows.at(0);\n return row === undefined ? undefined : WorkflowIR.parse(asJson(row.ir));\n }\n\n replaceRoutes(bundle: BundleIR, version?: BundleVersion): Promise<void> {\n return this.sql.transaction(async (tx) => {\n if (version !== undefined) {\n // Unconditional, unlike replaceRoutesIfNewer: a forced apply wins, and the row must then\n // name the code its routes came from. Leaving it naming a newer version would make that\n // version skip re-applying its own bundle, with these older routes left live.\n await tx.query(\n `INSERT INTO ${this.t.deployment} (scope, version_id, version_created_at, applied_at)\n VALUES ('routes', $1, ${TS('$2')}, ${TS('$3')})\n ON CONFLICT (scope) DO UPDATE SET version_id = EXCLUDED.version_id,\n version_created_at = EXCLUDED.version_created_at, applied_at = EXCLUDED.applied_at`,\n [version.id, version.createdAt, Date.now()]\n );\n }\n await this.swapRoutes(tx, bundle, version?.id);\n });\n }\n\n async liveBundleVersion(): Promise<BundleVersion | undefined> {\n // In a transaction, so Hyperdrive answers from the database, not from its read cache\n const rows = await this.sql.transaction((tx) =>\n tx.query<{ version_id: string; created_at: number | string }>(\n `SELECT version_id, (extract(epoch FROM version_created_at) * 1000)::double precision AS created_at\n FROM ${this.t.deployment} WHERE scope = 'routes'`\n )\n );\n const row = rows.at(0);\n return row === undefined\n ? undefined\n : { id: row.version_id, createdAt: Number(row.created_at) };\n }\n\n replaceRoutesIfNewer(bundle: BundleIR, version: BundleVersion): Promise<boolean> {\n return this.sql.transaction(async (tx) => {\n // The row lock decides between versions applying at once: the loser's WHERE re-reads the\n // winner's committed row and updates nothing\n const claimed = await tx.query(\n `INSERT INTO ${this.t.deployment} AS live (scope, version_id, version_created_at, applied_at)\n VALUES ('routes', $1, ${TS('$2')}, ${TS('$3')})\n ON CONFLICT (scope) DO UPDATE SET version_id = EXCLUDED.version_id,\n version_created_at = EXCLUDED.version_created_at, applied_at = EXCLUDED.applied_at\n WHERE live.version_created_at < EXCLUDED.version_created_at\n RETURNING scope`,\n [version.id, version.createdAt, Date.now()]\n );\n if (claimed.length === 0) return false;\n await this.swapRoutes(tx, bundle, version.id);\n return true;\n });\n }\n\n /** `version`: the code version applying the bundle, kept on every route it writes. */\n private async swapRoutes(tx: SqlClientLike, bundle: BundleIR, version?: string): Promise<void> {\n await tx.query(`DELETE FROM ${this.t.trigger}`);\n await tx.query(`DELETE FROM ${this.t.segmentTrigger}`);\n await tx.query(`DELETE FROM ${this.t.segment}`);\n for (const segment of bundle.segments) {\n await tx.query(\n `INSERT INTO ${this.t.segment} (id, condition, hash) VALUES ($1, $2::text::jsonb, $3)`,\n [segment.id, JSON.stringify(segment.condition), segment.contentHash]\n );\n }\n for (const workflow of bundle.workflows) {\n if (workflow.trigger.type === 'event') {\n await tx.query(\n `INSERT INTO ${this.t.trigger} (workflow, hash, event, \"where\", filter, visitors, version) VALUES ($1, $2, $3, $4::text::jsonb, $5::text::jsonb, $6, $7)`,\n [\n workflow.id,\n workflow.contentHash,\n workflow.trigger.event,\n workflow.trigger.where === undefined ? null : JSON.stringify(workflow.trigger.where),\n workflow.trigger.filter === undefined ? null : JSON.stringify(workflow.trigger.filter),\n workflow.trigger.visitors === true,\n version ?? null,\n ]\n );\n } else if (workflow.trigger.type === 'segment') {\n await tx.query(\n `INSERT INTO ${this.t.segmentTrigger} (workflow, hash, segment, version) VALUES ($1, $2, $3, $4)`,\n [workflow.id, workflow.contentHash, workflow.trigger.segment, version ?? null]\n );\n }\n }\n }\n\n async isSegmentMember(segment: string, userId: string): Promise<boolean> {\n const rows = await this.sql.query(\n `SELECT 1 AS x FROM ${this.t.segmentMember} WHERE segment = $1 AND user_id = $2`,\n [segment, userId]\n );\n return rows.length > 0;\n }\n\n async addSegmentMember(\n segment: string,\n userId: string,\n ts: number,\n workflows: readonly string[]\n ): Promise<void> {\n // One locked transaction: the membership and the starts it owes commit together\n await this.sql.transaction(async (tx) => {\n await this.lockUser(tx, userId);\n const member = await tx.query(\n `INSERT INTO ${this.t.segmentMember} (segment, user_id, ts)\n SELECT $1, $2, ${TS('$3')} ${this.notErased('$2')}\n ON CONFLICT DO NOTHING RETURNING segment`,\n [segment, userId, ts]\n );\n // Already a member, or refused: either way the starts below write nothing new\n if (member.length === 0) await this.refuseIfErased(tx, userId);\n for (const workflow of workflows) {\n await tx.query(\n `INSERT INTO ${this.t.segmentStart} (segment, workflow, user_id, ts)\n SELECT $1, $2, $3, ${TS('$4')} ${this.notErased('$3')}\n ON CONFLICT DO NOTHING`,\n [segment, workflow, userId, ts]\n );\n }\n });\n }\n\n async pendingSegmentStarts(segment: string, userId: string): Promise<string[]> {\n const rows = await this.sql.query<{ workflow: string }>(\n `SELECT workflow FROM ${this.t.segmentStart} WHERE segment = $1 AND user_id = $2`,\n [segment, userId]\n );\n return rows.map((row) => row.workflow);\n }\n\n async completeSegmentStart(segment: string, userId: string, workflow: string): Promise<void> {\n await this.sql.query(\n `DELETE FROM ${this.t.segmentStart} WHERE segment = $1 AND user_id = $2 AND workflow = $3`,\n [segment, userId, workflow]\n );\n }\n\n async removeSegmentMember(segment: string, userId: string): Promise<void> {\n await this.sql.transaction(async (tx) => {\n await tx.query(`DELETE FROM ${this.t.segmentStart} WHERE segment = $1 AND user_id = $2`, [\n segment,\n userId,\n ]);\n await tx.query(`DELETE FROM ${this.t.segmentMember} WHERE segment = $1 AND user_id = $2`, [\n segment,\n userId,\n ]);\n });\n }\n\n async enterJourney(entry: EntryInput): Promise<EntryOutcome> {\n const { execution } = entry;\n // One transaction under the user's lock: the claim (or reclaim) and the start it owes commit\n // together — a crash between them would leave a running entry no retry could ever start —\n // and before or after an erasure of the user, never astride it\n return this.sql.transaction(async (tx) => {\n await this.lockUser(tx, entry.userId);\n const outcome = await this.claimEntry(tx, entry);\n if (outcome !== null && execution !== undefined) {\n await this.recordStart(tx, entry, execution);\n }\n return outcome;\n });\n }\n\n private async claimEntry(sql: SqlClientLike, entry: EntryInput): Promise<EntryOutcome> {\n // The (workflow, user_id) primary key makes the once policy atomic:\n // concurrent ingests race on the insert and exactly one wins.\n const claimedAt = Date.now();\n const inserted = await sql.query(\n `INSERT INTO ${this.t.entry} (workflow, user_id, instance_id, hash, status, ts, claimed_at)\n SELECT $1, $2, $3, $4, 'running', ${TS('$5')}, ${TS('$6')} ${this.notErased('$2')}\n ON CONFLICT (workflow, user_id) DO NOTHING RETURNING instance_id`,\n [entry.workflow, entry.userId, entry.instanceId, entry.hash, entry.ts, claimedAt]\n );\n if (inserted.length > 0) return 'entered';\n\n // An existing row blocks the entry unless it records an instance that died:\n // 'failed' is reclaimed rather than barring the user forever over one\n // outage. The status test is inside the UPDATE, so of two concurrent\n // retries exactly one gets a row back. The reclaim is a new attempt: it\n // starts unadmitted, even under the same instance id, so the previous\n // attempt's admission is not inherited (`runningInstances` would report a\n // runner that has not been created yet).\n const claimed = await sql.query(\n `UPDATE ${this.t.entry} SET instance_id = $1, hash = $2, status = 'running', ts = ${TS('$3')},\n admitted_at = NULL, claimed_at = ${TS('$6')}\n WHERE workflow = $4 AND user_id = $5 AND status = 'failed'\n AND NOT EXISTS (SELECT 1 FROM ${this.t.erasure} WHERE user_id = $5)\n RETURNING instance_id`,\n [entry.instanceId, entry.hash, entry.ts, entry.workflow, entry.userId, claimedAt]\n );\n if (claimed.length > 0) return 'reclaimed';\n // Taken by a live instance, or refused by the tombstone: tell them apart\n await this.refuseIfErased(sql, entry.userId);\n return null;\n }\n\n /**\n * Only an entry this call claimed records a start, so a row already there is a previous\n * attempt's — a failed entry reclaimed under the same instance id (an event replayed with its\n * ts). It is replaced whole: the new attempt's name and payload, nothing submitted yet.\n */\n private async recordStart(\n tx: SqlClientLike,\n entry: EntryInput,\n execution: ExecutionStart\n ): Promise<void> {\n const rows = await tx.query(\n `INSERT INTO ${this.t.execution} (instance_id, workflow, user_id, name, target, input, created_at, generation)\n SELECT $1, $2, $3, $4, $5, $6, ${TS('$7')}, 0 ${this.notErased('$3')}\n ON CONFLICT (instance_id) DO UPDATE SET\n name = EXCLUDED.name, target = EXCLUDED.target, input = EXCLUDED.input,\n created_at = EXCLUDED.created_at, generation = 0, submitted_at = NULL, arn = NULL,\n unresolved_at = NULL, callback_id = NULL, deadline_at = NULL, checked_at = NULL,\n stopped_at = NULL\n RETURNING instance_id`,\n [\n entry.instanceId,\n entry.workflow,\n entry.userId,\n execution.name,\n execution.target,\n execution.input,\n // The start's own age (the retry budget), not the event's ts, which the producer chose\n Date.now(),\n ]\n );\n // Refused between the claim and this: the throw rolls the claimed entry back with it\n if (rows.length === 0) throw new UserErasedError(entry.userId);\n }\n\n async removeEntry(instanceId: string): Promise<void> {\n await this.sql.query(`DELETE FROM ${this.t.entry} WHERE instance_id = $1`, [instanceId]);\n }\n\n async getEntry(workflow: string, userId: string): Promise<EntryState | undefined> {\n // In a transaction, so Hyperdrive answers from the database: the caller decides whether a\n // start is lost on this read, and a cached row could still show an admission as missing\n const rows = await this.sql.transaction((tx) =>\n tx.query<{ instance_id: string; status: string; admitted: boolean }>(\n `SELECT instance_id, status, admitted_at IS NOT NULL AS admitted\n FROM ${this.t.entry} WHERE workflow = $1 AND user_id = $2`,\n [workflow, userId]\n )\n );\n const row = rows.at(0);\n return row === undefined\n ? undefined\n : { instanceId: row.instance_id, status: row.status, admitted: row.admitted };\n }\n\n async unadmittedEntries(claimedBefore: number, limit: number): Promise<UnadmittedEntry[]> {\n // Rows from before claimed_at existed fall back to ts: older than any real claim, they are\n // exactly the entries a crash may have stranded\n const rows = await this.sql.query<{ instance_id: string; workflow: string; user_id: string }>(\n `SELECT instance_id, workflow, user_id FROM ${this.t.entry}\n WHERE admitted_at IS NULL AND status = 'running'\n AND COALESCE(claimed_at, ts) < ${TS('$1')}\n ORDER BY COALESCE(claimed_at, ts) LIMIT $2`,\n [claimedBefore, limit]\n );\n return rows.map((row) => ({\n instanceId: row.instance_id,\n workflow: row.workflow,\n userId: row.user_id,\n }));\n }\n\n async failUnadmittedEntry(instanceId: string): Promise<boolean> {\n // Conditional on the same state the caller saw: a runner that admitted itself meanwhile, or a\n // reclaim under a new instance id, leaves the row alone\n const rows = await this.sql.query(\n `UPDATE ${this.t.entry} SET status = 'failed'\n WHERE instance_id = $1 AND status = 'running' AND admitted_at IS NULL\n RETURNING instance_id`,\n [instanceId]\n );\n return rows.length > 0;\n }\n\n async admittedEntries(admittedBefore: number, limit: number): Promise<AdmittedEntry[]> {\n // random(): a sample, not the oldest — see the interface\n const rows = await this.sql.query<{ instance_id: string; workflow: string; user_id: string }>(\n `SELECT instance_id, workflow, user_id FROM ${this.t.entry}\n WHERE status = 'running' AND admitted_at < ${TS('$1')}\n ORDER BY random() LIMIT $2`,\n [admittedBefore, limit]\n );\n return rows.map((row) => ({\n instanceId: row.instance_id,\n workflow: row.workflow,\n userId: row.user_id,\n }));\n }\n\n async failAdmittedEntry(instanceId: string): Promise<boolean> {\n // Conditional on the same state the caller saw: a run that finalized between the caller's\n // read and its platform lookup, or a reclaim under a new instance id, leaves the row alone\n const rows = await this.sql.query(\n `UPDATE ${this.t.entry} SET status = 'failed'\n WHERE instance_id = $1 AND status = 'running' AND admitted_at IS NOT NULL\n RETURNING instance_id`,\n [instanceId]\n );\n return rows.length > 0;\n }\n\n async setEntryStatus(instanceId: string, status: string): Promise<void> {\n await this.sql.query(`UPDATE ${this.t.entry} SET status = $1 WHERE instance_id = $2`, [\n status,\n instanceId,\n ]);\n }\n\n async findWakeHandles(userId: string, event: string): Promise<string[]> {\n const rows = await this.sql.query<{ handle: string }>(\n `SELECT DISTINCT wake_handle AS handle FROM ${this.t.subscription} WHERE user_id = $1 AND event = $2`,\n [userId, event]\n );\n return rows.map((row) => row.handle);\n }\n\n async subscribe(\n handle: string,\n userId: string,\n events: readonly string[],\n ts: number\n ): Promise<void> {\n // One locked transaction for the call's registrations: all of them, or none\n await this.sql.transaction(async (tx) => {\n await this.lockUser(tx, userId);\n let skipped = false;\n for (const event of events) {\n const rows = await tx.query(\n `INSERT INTO ${this.t.subscription} (user_id, event, wake_handle, ts)\n SELECT $1, $2, $3, ${TS('$4')} ${this.notErased('$1')}\n ON CONFLICT DO NOTHING RETURNING event`,\n [userId, event, handle, ts]\n );\n if (rows.length === 0) skipped = true;\n }\n // A registration already there, or refused: one read tells which\n if (skipped) await this.refuseIfErased(tx, userId);\n });\n }\n\n async unsubscribe(handle: string): Promise<void> {\n await this.sql.query(`DELETE FROM ${this.t.subscription} WHERE wake_handle = $1`, [handle]);\n }\n}\n\ninterface ExecutionRow {\n instance_id: string;\n workflow: string;\n user_id: string;\n name: string;\n target: string;\n input: string;\n // float8 comes back as a number from most drivers, as a string from some\n created_at: number | string;\n submitted_at: number | string | null;\n arn: string | null;\n unresolved_at: number | string | null;\n callback_id: string | null;\n generation: number | string;\n deadline_at: number | string | null;\n checked_at: number | string | null;\n stopped_at: number | string | null;\n}\n\nconst PLAIN_COLUMNS = ['instance_id', 'workflow', 'user_id', 'name', 'target', 'input', 'arn'];\nconst MORE_PLAIN = ['callback_id', 'generation'];\nconst TIME_COLUMNS = [\n 'created_at',\n 'submitted_at',\n 'unresolved_at',\n 'deadline_at',\n 'checked_at',\n 'stopped_at',\n];\n\n/** The execution columns, timestamps as epoch ms; `alias` qualifies them in a join. */\nfunction executionColumns(alias = ''): string {\n const q = alias === '' ? '' : `${alias}.`;\n return [\n ...[...PLAIN_COLUMNS, ...MORE_PLAIN].map((column) => `${q}${column}`),\n ...TIME_COLUMNS.map(\n (column) => `(extract(epoch from ${q}${column}) * 1000)::double precision AS ${column}`\n ),\n ].join(', ');\n}\n\nconst optional = <T>(value: T | null): T | undefined => value ?? undefined;\nconst ms = (value: number | string | null): number | undefined =>\n value === null ? undefined : Number(value);\n\nfunction toRecord(row: ExecutionRow): ExecutionRecord {\n return {\n instanceId: row.instance_id,\n workflow: row.workflow,\n userId: row.user_id,\n name: row.name,\n target: row.target,\n input: row.input,\n createdAt: Number(row.created_at),\n submittedAt: ms(row.submitted_at),\n arn: optional(row.arn),\n unresolvedAt: ms(row.unresolved_at),\n callbackId: optional(row.callback_id),\n generation: Number(row.generation),\n deadlineAt: ms(row.deadline_at),\n checkedAt: ms(row.checked_at),\n stoppedAt: ms(row.stopped_at),\n };\n}\n\n/** The execution table (tables.ts): starts to confirm, and the callback each wait parks on. */\nclass PostgresExecutions implements ExecutionRegistry {\n constructor(\n private readonly sql: SqlClientLike,\n private readonly table: string,\n private readonly entry: string\n ) {}\n\n async get(instanceId: string): Promise<ExecutionRecord | undefined> {\n const rows = await this.sql.query<ExecutionRow>(\n `SELECT ${executionColumns()} FROM ${this.table} WHERE instance_id = $1`,\n [instanceId]\n );\n return rows.length === 0 ? undefined : toRecord(rows[0]);\n }\n\n async pendingStarts(filter: {\n workflow?: string;\n userId?: string;\n createdBefore?: number;\n limit: number;\n }): Promise<ExecutionRecord[]> {\n const where = ['x.submitted_at IS NULL', 'x.unresolved_at IS NULL', \"e.status = 'running'\"];\n const params: unknown[] = [];\n if (filter.workflow !== undefined) {\n params.push(filter.workflow);\n where.push(`x.workflow = $${params.length}`);\n }\n if (filter.userId !== undefined) {\n params.push(filter.userId);\n where.push(`x.user_id = $${params.length}`);\n }\n if (filter.createdBefore !== undefined) {\n params.push(filter.createdBefore);\n where.push(`x.created_at < ${TS(`$${params.length}`)}`);\n }\n params.push(filter.limit);\n // Only the start the entry still names: a reclaimed entry's old start is history\n const rows = await this.sql.query<ExecutionRow>(\n `SELECT ${executionColumns('x')}\n FROM ${this.table} x JOIN ${this.entry} e ON e.instance_id = x.instance_id\n WHERE ${where.join(' AND ')} ORDER BY x.created_at LIMIT $${params.length}`,\n params\n );\n return rows.map(toRecord);\n }\n\n async markSubmitted(instanceId: string, arn: string | undefined, at: number): Promise<void> {\n await this.sql.query(\n `UPDATE ${this.table} SET submitted_at = COALESCE(submitted_at, ${TS('$2')}), arn = COALESCE($3, arn) WHERE instance_id = $1`,\n [instanceId, at, arn ?? null]\n );\n }\n\n async markUnresolved(instanceId: string, at: number): Promise<void> {\n await this.sql.query(\n `UPDATE ${this.table} SET unresolved_at = ${TS('$2')} WHERE instance_id = $1 AND submitted_at IS NULL`,\n [instanceId, at]\n );\n }\n\n async setArn(instanceId: string, arn: string): Promise<void> {\n await this.sql.query(`UPDATE ${this.table} SET arn = $2 WHERE instance_id = $1`, [\n instanceId,\n arn,\n ]);\n }\n\n async armCallback(\n instanceId: string,\n callback: { id: string; generation: number; deadlineAt?: number | undefined }\n ): Promise<boolean> {\n // The generation test is inside the UPDATE: a retried arm of an older wait cannot overwrite\n // the callback a newer one parks on\n const rows = await this.sql.query(\n `UPDATE ${this.table} SET callback_id = $2, generation = $3,\n deadline_at = CASE WHEN $4::double precision IS NULL THEN NULL ELSE ${TS('$4')} END\n WHERE instance_id = $1 AND generation <= $3 RETURNING instance_id`,\n [instanceId, callback.id, callback.generation, callback.deadlineAt ?? null]\n );\n return rows.length > 0;\n }\n\n async clearCallback(instanceId: string, generation: number): Promise<void> {\n await this.sql.query(\n `UPDATE ${this.table} SET callback_id = NULL, deadline_at = NULL WHERE instance_id = $1 AND generation = $2`,\n [instanceId, generation]\n );\n }\n\n async toReconcile(limit: number, checkedBefore: number): Promise<ExecutionRecord[]> {\n const rows = await this.sql.query<ExecutionRow>(\n `SELECT ${executionColumns('x')}\n FROM ${this.table} x JOIN ${this.entry} e ON e.instance_id = x.instance_id\n WHERE e.status = 'running' AND x.submitted_at IS NOT NULL\n AND (x.checked_at IS NULL OR x.checked_at < ${TS('$1')})\n ORDER BY x.checked_at NULLS FIRST LIMIT $2`,\n [checkedBefore, limit]\n );\n return rows.map(toRecord);\n }\n\n async markStopped(instanceId: string, at: number): Promise<void> {\n await this.sql.query(\n `UPDATE ${this.table} SET stopped_at = COALESCE(stopped_at, ${TS('$2')}) WHERE instance_id = $1`,\n [instanceId, at]\n );\n }\n\n async markChecked(instanceId: string, at: number): Promise<void> {\n await this.sql.query(\n `UPDATE ${this.table} SET checked_at = ${TS('$2')} WHERE instance_id = $1`,\n [instanceId, at]\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -3,7 +3,7 @@ import { PushDevice } from "../engine/ports.cjs";
|
|
|
3
3
|
import { ErasureState } from "./erasure.cjs";
|
|
4
4
|
import { SqlClientLike, uuidv7 } from "./sql.cjs";
|
|
5
5
|
import { DEFAULT_SCHEMA } from "./tables.cjs";
|
|
6
|
-
import { BundleVersion, DeviceInput, EntryInput, EntryOutcome, EntryState, EventCursor, EventInput, EventWindow, ExecutionRegistry, JourneyReporting, JourneyStore, ProfileProps, RunningInstance, SegmentCandidate, SegmentTriggerRoute, SuppressionChange, TriggerRoute, UnadmittedEntry, VisitorMerge } from "./index.cjs";
|
|
6
|
+
import { AdmittedEntry, BundleVersion, DeviceInput, EntryInput, EntryOutcome, EntryState, EventCursor, EventInput, EventWindow, ExecutionRegistry, JourneyReporting, JourneyStore, ProfileProps, RunningInstance, SegmentCandidate, SegmentTriggerRoute, SuppressionChange, TriggerRoute, UnadmittedEntry, VisitorMerge } from "./index.cjs";
|
|
7
7
|
export interface PostgresJourneyStoreOptions {
|
|
8
8
|
/**
|
|
9
9
|
* The Postgres schema the engine's tables live in — `workflow` by default, the config's
|
|
@@ -143,6 +143,8 @@ export declare class PostgresJourneyStore implements JourneyStore {
|
|
|
143
143
|
getEntry(workflow: string, userId: string): Promise<EntryState | undefined>;
|
|
144
144
|
unadmittedEntries(claimedBefore: number, limit: number): Promise<UnadmittedEntry[]>;
|
|
145
145
|
failUnadmittedEntry(instanceId: string): Promise<boolean>;
|
|
146
|
+
admittedEntries(admittedBefore: number, limit: number): Promise<AdmittedEntry[]>;
|
|
147
|
+
failAdmittedEntry(instanceId: string): Promise<boolean>;
|
|
146
148
|
setEntryStatus(instanceId: string, status: string): Promise<void>;
|
|
147
149
|
findWakeHandles(userId: string, event: string): Promise<string[]>;
|
|
148
150
|
subscribe(handle: string, userId: string, events: readonly string[], ts: number): Promise<void>;
|