@shware/workflow 2.0.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -2
- package/dist/apns-relay.cjs +4 -17
- package/dist/apns-relay.cjs.map +1 -1
- package/dist/apns-relay.mjs +4 -17
- package/dist/apns-relay.mjs.map +1 -1
- package/dist/aws/engine-step.cjs +104 -0
- package/dist/aws/engine-step.cjs.map +1 -0
- package/dist/aws/engine-step.d.cts +28 -0
- package/dist/aws/engine-step.d.cts.map +1 -0
- package/dist/aws/engine-step.d.mts +28 -0
- package/dist/aws/engine-step.d.mts.map +1 -0
- package/dist/aws/engine-step.mjs +102 -0
- package/dist/aws/engine-step.mjs.map +1 -0
- package/dist/aws/function-url.cjs +62 -0
- package/dist/aws/function-url.cjs.map +1 -0
- package/dist/aws/function-url.d.cts +38 -0
- package/dist/aws/function-url.d.cts.map +1 -0
- package/dist/aws/function-url.d.mts +38 -0
- package/dist/aws/function-url.d.mts.map +1 -0
- package/dist/aws/function-url.mjs +59 -0
- package/dist/aws/function-url.mjs.map +1 -0
- package/dist/aws/index.cjs +160 -0
- package/dist/aws/index.cjs.map +1 -0
- package/dist/aws/index.d.cts +68 -0
- package/dist/aws/index.d.cts.map +1 -0
- package/dist/aws/index.d.mts +68 -0
- package/dist/aws/index.d.mts.map +1 -0
- package/dist/aws/index.mjs +148 -0
- package/dist/aws/index.mjs.map +1 -0
- package/dist/aws/instances.cjs +196 -0
- package/dist/aws/instances.cjs.map +1 -0
- package/dist/aws/instances.d.cts +94 -0
- package/dist/aws/instances.d.cts.map +1 -0
- package/dist/aws/instances.d.mts +94 -0
- package/dist/aws/instances.d.mts.map +1 -0
- package/dist/aws/instances.mjs +192 -0
- package/dist/aws/instances.mjs.map +1 -0
- package/dist/aws/lambda-api.cjs +89 -0
- package/dist/aws/lambda-api.cjs.map +1 -0
- package/dist/aws/lambda-api.d.cts +43 -0
- package/dist/aws/lambda-api.d.cts.map +1 -0
- package/dist/aws/lambda-api.d.mts +43 -0
- package/dist/aws/lambda-api.d.mts.map +1 -0
- package/dist/aws/lambda-api.mjs +88 -0
- package/dist/aws/lambda-api.mjs.map +1 -0
- package/dist/client.d.cts +1 -1
- package/dist/client.d.mts +1 -1
- package/dist/cloudflare/bindings.cjs +19 -0
- package/dist/cloudflare/bindings.cjs.map +1 -1
- package/dist/cloudflare/bindings.d.cts +39 -0
- package/dist/cloudflare/bindings.d.cts.map +1 -1
- package/dist/cloudflare/bindings.d.mts +39 -0
- package/dist/cloudflare/bindings.d.mts.map +1 -1
- package/dist/cloudflare/bindings.mjs +18 -1
- package/dist/cloudflare/bindings.mjs.map +1 -1
- package/dist/cloudflare/index.cjs +32 -28
- package/dist/cloudflare/index.d.cts +8 -6
- package/dist/cloudflare/index.d.mts +8 -6
- package/dist/cloudflare/index.mjs +12 -10
- package/dist/cloudflare/runner.cjs +27 -76
- package/dist/cloudflare/runner.cjs.map +1 -1
- package/dist/cloudflare/runner.d.cts +7 -2
- package/dist/cloudflare/runner.d.cts.map +1 -1
- package/dist/cloudflare/runner.d.mts +7 -2
- package/dist/cloudflare/runner.d.mts.map +1 -1
- package/dist/cloudflare/runner.mjs +26 -75
- package/dist/cloudflare/runner.mjs.map +1 -1
- package/dist/cloudflare/worker.cjs +22 -203
- package/dist/cloudflare/worker.cjs.map +1 -1
- package/dist/cloudflare/worker.d.cts +10 -56
- package/dist/cloudflare/worker.d.cts.map +1 -1
- package/dist/cloudflare/worker.d.mts +10 -56
- package/dist/cloudflare/worker.d.mts.map +1 -1
- package/dist/cloudflare/worker.mjs +20 -199
- package/dist/cloudflare/worker.mjs.map +1 -1
- package/dist/email-sender.cjs +62 -0
- package/dist/email-sender.cjs.map +1 -0
- package/dist/email-sender.d.cts +70 -0
- package/dist/email-sender.d.cts.map +1 -0
- package/dist/email-sender.d.mts +70 -0
- package/dist/email-sender.d.mts.map +1 -0
- package/dist/email-sender.mjs +60 -0
- package/dist/email-sender.mjs.map +1 -0
- package/dist/email.cjs +21 -0
- package/dist/email.cjs.map +1 -1
- package/dist/email.d.cts +6 -0
- package/dist/email.d.cts.map +1 -1
- package/dist/email.d.mts +6 -0
- package/dist/email.d.mts.map +1 -1
- package/dist/email.mjs +21 -1
- package/dist/email.mjs.map +1 -1
- package/dist/engine/index.cjs +1 -1
- package/dist/engine/index.mjs +1 -1
- package/dist/engine/interpreter.cjs +1 -1
- package/dist/engine/interpreter.cjs.map +1 -1
- package/dist/engine/interpreter.mjs +1 -1
- package/dist/engine/interpreter.mjs.map +1 -1
- package/dist/engine/ports.d.cts +6 -1
- package/dist/engine/ports.d.cts.map +1 -1
- package/dist/engine/ports.d.mts +6 -1
- package/dist/engine/ports.d.mts.map +1 -1
- package/dist/{cloudflare → host}/channels.cjs +13 -11
- package/dist/host/channels.cjs.map +1 -0
- package/dist/{cloudflare → host}/channels.d.cts +8 -3
- package/dist/host/channels.d.cts.map +1 -0
- package/dist/{cloudflare → host}/channels.d.mts +8 -3
- package/dist/host/channels.d.mts.map +1 -0
- package/dist/{cloudflare → host}/channels.mjs +9 -7
- package/dist/host/channels.mjs.map +1 -0
- package/dist/host/execute.cjs +81 -0
- package/dist/host/execute.cjs.map +1 -0
- package/dist/host/execute.d.cts +37 -0
- package/dist/host/execute.d.cts.map +1 -0
- package/dist/host/execute.d.mts +37 -0
- package/dist/host/execute.d.mts.map +1 -0
- package/dist/host/execute.mjs +80 -0
- package/dist/host/execute.mjs.map +1 -0
- package/dist/host/journey-host.cjs +214 -0
- package/dist/host/journey-host.cjs.map +1 -0
- package/dist/host/journey-host.d.cts +105 -0
- package/dist/host/journey-host.d.cts.map +1 -0
- package/dist/host/journey-host.d.mts +105 -0
- package/dist/host/journey-host.d.mts.map +1 -0
- package/dist/host/journey-host.mjs +210 -0
- package/dist/host/journey-host.mjs.map +1 -0
- package/dist/{cloudflare → host}/router.cjs +34 -13
- package/dist/host/router.cjs.map +1 -0
- package/dist/{cloudflare → host}/router.d.cts +2 -2
- package/dist/host/router.d.cts.map +1 -0
- package/dist/{cloudflare → host}/router.d.mts +2 -2
- package/dist/host/router.d.mts.map +1 -0
- package/dist/{cloudflare → host}/router.mjs +34 -13
- package/dist/host/router.mjs.map +1 -0
- package/dist/host/senders.cjs +63 -0
- package/dist/host/senders.cjs.map +1 -0
- package/dist/host/senders.d.cts +31 -0
- package/dist/host/senders.d.cts.map +1 -0
- package/dist/host/senders.d.mts +31 -0
- package/dist/host/senders.d.mts.map +1 -0
- package/dist/host/senders.mjs +59 -0
- package/dist/host/senders.mjs.map +1 -0
- package/dist/http2-client.cjs +90 -0
- package/dist/http2-client.cjs.map +1 -0
- package/dist/http2-client.d.cts +47 -0
- package/dist/http2-client.d.cts.map +1 -0
- package/dist/http2-client.d.mts +47 -0
- package/dist/http2-client.d.mts.map +1 -0
- package/dist/http2-client.mjs +88 -0
- package/dist/http2-client.mjs.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/react-email.cjs.map +1 -1
- package/dist/react-email.d.cts +1 -2
- package/dist/react-email.d.cts.map +1 -1
- package/dist/react-email.d.mts +1 -2
- package/dist/react-email.d.mts.map +1 -1
- package/dist/react-email.mjs.map +1 -1
- package/dist/store/d1.cjs +1 -0
- package/dist/store/d1.cjs.map +1 -1
- package/dist/store/d1.d.cts.map +1 -1
- package/dist/store/d1.d.mts.map +1 -1
- package/dist/store/d1.mjs +1 -0
- package/dist/store/d1.mjs.map +1 -1
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +74 -0
- package/dist/store/index.d.cts.map +1 -1
- package/dist/store/index.d.mts +74 -0
- package/dist/store/index.d.mts.map +1 -1
- package/dist/store/index.mjs.map +1 -1
- package/dist/store/postgres.cjs +177 -12
- package/dist/store/postgres.cjs.map +1 -1
- package/dist/store/postgres.d.cts +10 -1
- package/dist/store/postgres.d.cts.map +1 -1
- package/dist/store/postgres.d.mts +10 -1
- package/dist/store/postgres.d.mts.map +1 -1
- package/dist/store/postgres.mjs +177 -12
- package/dist/store/postgres.mjs.map +1 -1
- package/dist/store/tables.cjs +31 -2
- package/dist/store/tables.cjs.map +1 -1
- package/dist/store/tables.d.cts +1 -0
- package/dist/store/tables.d.cts.map +1 -1
- package/dist/store/tables.d.mts +1 -0
- package/dist/store/tables.d.mts.map +1 -1
- package/dist/store/tables.mjs +31 -2
- package/dist/store/tables.mjs.map +1 -1
- package/dist/transports/cloudflare-email.cjs +59 -0
- package/dist/transports/cloudflare-email.cjs.map +1 -0
- package/dist/transports/cloudflare-email.d.cts +38 -0
- package/dist/transports/cloudflare-email.d.cts.map +1 -0
- package/dist/transports/cloudflare-email.d.mts +38 -0
- package/dist/transports/cloudflare-email.d.mts.map +1 -0
- package/dist/transports/cloudflare-email.mjs +57 -0
- package/dist/transports/cloudflare-email.mjs.map +1 -0
- package/dist/transports/ses.cjs +91 -0
- package/dist/transports/ses.cjs.map +1 -0
- package/dist/transports/ses.d.cts +39 -0
- package/dist/transports/ses.d.cts.map +1 -0
- package/dist/transports/ses.d.mts +39 -0
- package/dist/transports/ses.d.mts.map +1 -0
- package/dist/transports/ses.mjs +89 -0
- package/dist/transports/ses.mjs.map +1 -0
- package/dist/transports/sigv4.cjs +83 -0
- package/dist/transports/sigv4.cjs.map +1 -0
- package/dist/transports/sigv4.d.cts +27 -0
- package/dist/transports/sigv4.d.cts.map +1 -0
- package/dist/transports/sigv4.d.mts +27 -0
- package/dist/transports/sigv4.d.mts.map +1 -0
- package/dist/transports/sigv4.mjs +82 -0
- package/dist/transports/sigv4.mjs.map +1 -0
- package/package.json +53 -1
- package/dist/cloudflare/channels.cjs.map +0 -1
- package/dist/cloudflare/channels.d.cts.map +0 -1
- package/dist/cloudflare/channels.d.mts.map +0 -1
- package/dist/cloudflare/channels.mjs.map +0 -1
- package/dist/cloudflare/router.cjs.map +0 -1
- package/dist/cloudflare/router.d.cts.map +0 -1
- package/dist/cloudflare/router.d.mts.map +0 -1
- package/dist/cloudflare/router.mjs.map +0 -1
- package/dist/cloudflare/senders.cjs +0 -121
- package/dist/cloudflare/senders.cjs.map +0 -1
- package/dist/cloudflare/senders.d.cts +0 -72
- package/dist/cloudflare/senders.d.cts.map +0 -1
- package/dist/cloudflare/senders.d.mts +0 -72
- package/dist/cloudflare/senders.d.mts.map +0 -1
- package/dist/cloudflare/senders.mjs +0 -115
- package/dist/cloudflare/senders.mjs.map +0 -1
package/dist/store/d1.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"d1.d.mts","names":[],"sources":["../../src/store/d1.ts"],"mappings":";;;;;;;;;;;qBAqBa,0BAA0B;mBAElB;mBACA;EAFnB,YACmB,IAAI,gBACJ,IAAI;EAGjB,YAAY,OAAO,aAAa;;UAW9B;EAcF,YAAY,gBAAgB,eAAe,SAAS,cAAc;EASlE,kBACJ,gBACA,eACA,SAAS,cACR,QAAQ;EASL,WAAW,iBAAiB,QAAQ;EAQpC,aAAa,gBAAgB,OAAO,eAAe,QAAQ;EAgB3D,eAAe;EAQf,YAAY,iBAAiB,QAAQ;EAQrC,aAAa,QAAQ,cAAc;EASnC,eAAe,gBAAgB,aAAa;EAO5C,mBAAmB;EAQnB,iBAAiB,iBAAiB;EAQlC,WAAW,iBAAiB;EAgB5B,aAAa,gBAAgB;EAQ7B,aAAa,gBAAgB;EAS7B,gBAAgB;IAAU;IAAY,WAAW;;EAWjD,oBAAoB,aAAa,QAAQ;EAQzC,aAAa,gBAAgB,QAAQ;EAerC,uBAAuB,QAAQ;EAO/B,cAAc,UAAU,aAAa;EAIrC,cAAc,sBAAsB,QAAQ;EAK5C,cAAc,QAAQ,WAAW;EA4CjC,gBAAgB,iBAAiB,iBAAiB;EAQlD,iBACJ,iBACA,gBACA,YACA,+BACC;EAeG,qBAAqB,iBAAiB,iBAAiB;EAQvD,qBAAqB,iBAAiB,gBAAgB,mBAAmB;EAOzE,oBAAoB,iBAAiB,iBAAiB;EAWtD,aAAa,OAAO,aAAa,QAAQ;
|
|
1
|
+
{"version":3,"file":"d1.d.mts","names":[],"sources":["../../src/store/d1.ts"],"mappings":";;;;;;;;;;;qBAqBa,0BAA0B;mBAElB;mBACA;EAFnB,YACmB,IAAI,gBACJ,IAAI;EAGjB,YAAY,OAAO,aAAa;;UAW9B;EAcF,YAAY,gBAAgB,eAAe,SAAS,cAAc;EASlE,kBACJ,gBACA,eACA,SAAS,cACR,QAAQ;EASL,WAAW,iBAAiB,QAAQ;EAQpC,aAAa,gBAAgB,OAAO,eAAe,QAAQ;EAgB3D,eAAe;EAQf,YAAY,iBAAiB,QAAQ;EAQrC,aAAa,QAAQ,cAAc;EASnC,eAAe,gBAAgB,aAAa;EAO5C,mBAAmB;EAQnB,iBAAiB,iBAAiB;EAQlC,WAAW,iBAAiB;EAgB5B,aAAa,gBAAgB;EAQ7B,aAAa,gBAAgB;EAS7B,gBAAgB;IAAU;IAAY,WAAW;;EAWjD,oBAAoB,aAAa,QAAQ;EAQzC,aAAa,gBAAgB,QAAQ;EAerC,uBAAuB,QAAQ;EAO/B,cAAc,UAAU,aAAa;EAIrC,cAAc,sBAAsB,QAAQ;EAK5C,cAAc,QAAQ,WAAW;EA4CjC,gBAAgB,iBAAiB,iBAAiB;EAQlD,iBACJ,iBACA,gBACA,YACA,+BACC;EAeG,qBAAqB,iBAAiB,iBAAiB;EAQvD,qBAAqB,iBAAiB,gBAAgB,mBAAmB;EAOzE,oBAAoB,iBAAiB,iBAAiB;EAWtD,aAAa,OAAO,aAAa,QAAQ;EAyBzC,YAAY,qBAAqB;EAIjC,eAAe,oBAAoB,iBAAiB;EAOpD,gBAAgB,gBAAgB,gBAAgB;EAUhD,UACJ,gBACA,gBACA,2BACA,aACC;EAaG,YAAY,iBAAiB"}
|
package/dist/store/d1.mjs
CHANGED
|
@@ -154,6 +154,7 @@ var D1JourneyStore = class {
|
|
|
154
154
|
await this.db.batch([this.db.prepare("DELETE FROM segment_starts WHERE segment = ? AND user_id = ?").bind(segment, userId), this.db.prepare("DELETE FROM segment_members WHERE segment = ? AND user_id = ?").bind(segment, userId)]);
|
|
155
155
|
}
|
|
156
156
|
async enterJourney(entry) {
|
|
157
|
+
if (entry.execution !== void 0) throw new Error("D1JourneyStore keeps no execution starts: this runtime needs Postgres");
|
|
157
158
|
if (((await this.db.prepare("INSERT OR IGNORE INTO entries (workflow, user_id, instance_id, hash, status, ts) VALUES (?, ?, ?, ?, ?, ?)").bind(entry.workflow, entry.userId, entry.instanceId, entry.hash, "running", entry.ts).run()).meta?.changes ?? 1) !== 0) return "entered";
|
|
158
159
|
return ((await this.db.prepare("UPDATE entries SET instance_id = ?, hash = ?, status = 'running', ts = ? WHERE workflow = ? AND user_id = ? AND status = 'failed'").bind(entry.instanceId, entry.hash, entry.ts, entry.workflow, entry.userId).run()).meta?.changes ?? 0) > 0 ? "reclaimed" : null;
|
|
159
160
|
}
|
package/dist/store/d1.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"d1.mjs","names":[],"sources":["../../src/store/d1.ts"],"sourcesContent":["import type { D1DatabaseLike, KVNamespaceLike } from '../cloudflare/bindings';\nimport type { PushDevice } from '../engine/ports';\nimport { type BundleIR, ConditionIR, WorkflowIR } from '../ir';\nimport type {\n DeviceInput,\n EntryInput,\n EntryOutcome,\n EventInput,\n EventWindow,\n JourneyStore,\n ProfileProps,\n SegmentTriggerRoute,\n TriggerRoute,\n} from './index';\n\n/**\n * The original data plane: D1 for the tables (see ../cloudflare/schema.sql)\n * and KV for the content-addressed workflow bodies (`wf:${contentHash}`).\n * The SQL below is exactly what the router and fact source used to inline —\n * the in-memory FakeD1 in the tests dispatches on these strings verbatim.\n */\nexport class D1JourneyStore implements JourneyStore {\n constructor(\n private readonly db: D1DatabaseLike,\n private readonly kv: KVNamespaceLike\n ) {}\n\n async insertEvent(input: EventInput): Promise<boolean> {\n const written = await this.db\n .prepare(\n 'INSERT OR IGNORE INTO events (id, user_id, name, ts, payload) VALUES (?, ?, ?, ?, ?)'\n )\n .bind(input.id, input.userId, input.event, input.ts, JSON.stringify(input.payload))\n .run();\n return (written.meta?.changes ?? 1) > 0;\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 = ? AND name = ?';\n if (window?.sinceMs !== undefined) {\n where += ' AND ts >= ?';\n params.push(window.sinceMs);\n }\n if (window?.untilMs !== undefined) {\n where += ' AND ts <= ?';\n params.push(window.untilMs);\n }\n return { where, params };\n }\n\n async countEvents(userId: string, event: string, window?: EventWindow): Promise<number> {\n const { where, params } = this.eventWindow(userId, event, window);\n const row = await this.db\n .prepare(`SELECT COUNT(*) AS c FROM events WHERE ${where}`)\n .bind(...params)\n .first<{ c: number }>();\n return row?.c ?? 0;\n }\n\n async listEventPayloads(\n userId: string,\n event: string,\n window?: EventWindow\n ): Promise<Record<string, unknown>[]> {\n const { where, params } = this.eventWindow(userId, event, window);\n const { results } = await this.db\n .prepare(`SELECT payload FROM events WHERE ${where}`)\n .bind(...params)\n .all<{ payload: string }>();\n return results.map((row) => JSON.parse(row.payload) as Record<string, unknown>);\n }\n\n async getProfile(userId: string): Promise<ProfileProps | undefined> {\n const row = await this.db\n .prepare('SELECT props FROM profiles WHERE user_id = ?')\n .bind(userId)\n .first<{ props: string }>();\n return row ? (JSON.parse(row.props) as ProfileProps) : undefined;\n }\n\n async mergeProfile(userId: string, props: ProfileProps): Promise<ProfileProps> {\n // SQLite's json_patch is RFC 7386 merge-patch: a null removes its key. The\n // merge happens in the statement, so two concurrent identifies cannot lose\n // each other's fields the way a read-modify-write did.\n const patch = JSON.stringify(props);\n const row = await this.db\n .prepare(\n \"INSERT INTO profiles (user_id, props) VALUES (?, json_patch('{}', ?)) ON CONFLICT(user_id) DO UPDATE SET props = json_patch(profiles.props, ?) RETURNING props\"\n )\n .bind(userId, patch, patch)\n .first<{ props: string }>();\n // RETURNING always yields the row it just wrote; the fallback only covers a\n // driver that declines to return one, and reports what was asked for.\n return row === null ? stripNulls(props) : (JSON.parse(row.props) as ProfileProps);\n }\n\n async listUserIds(): Promise<string[]> {\n const { results } = await this.db\n .prepare('SELECT user_id FROM profiles UNION SELECT DISTINCT user_id FROM events')\n .bind()\n .all<{ user_id: string }>();\n return results.map((row) => row.user_id);\n }\n\n async listDevices(userId: string): Promise<PushDevice[]> {\n const { results } = await this.db\n .prepare('SELECT token, platform FROM devices WHERE user_id = ? ORDER BY ts DESC')\n .bind(userId)\n .all<PushDevice>();\n return results;\n }\n\n async upsertDevice(device: DeviceInput): Promise<void> {\n await this.db\n .prepare(\n 'INSERT INTO devices (token, user_id, platform, ts) VALUES (?, ?, ?, ?) ON CONFLICT (token) DO UPDATE SET user_id = excluded.user_id, platform = excluded.platform, ts = excluded.ts'\n )\n .bind(device.token, device.userId, device.platform, device.ts)\n .run();\n }\n\n async requestErasure(userId: string, at: number): Promise<void> {\n await this.db\n .prepare('INSERT OR IGNORE INTO erasures (user_id, ts) VALUES (?, ?)')\n .bind(userId, at)\n .run();\n }\n\n async pendingErasures(): Promise<string[]> {\n const { results } = await this.db\n .prepare('SELECT user_id FROM erasures ORDER BY ts')\n .bind()\n .all<{ user_id: string }>();\n return results.map((row) => row.user_id);\n }\n\n async runningInstances(userId: string): Promise<string[]> {\n const { results } = await this.db\n .prepare(\"SELECT instance_id FROM entries WHERE user_id = ? AND status = 'running'\")\n .bind(userId)\n .all<{ instance_id: string }>();\n return results.map((row) => row.instance_id);\n }\n\n async forgetUser(userId: string): Promise<void> {\n // One batch: D1 runs it as a transaction; the request goes last, with the data it erases\n await this.db.batch(\n [\n 'entries',\n 'subscriptions',\n 'segment_starts',\n 'segment_members',\n 'devices',\n 'events',\n 'profiles',\n 'erasures',\n ].map((table) => this.db.prepare(`DELETE FROM ${table} WHERE user_id = ?`).bind(userId))\n );\n }\n\n async removeDevice(token: string): Promise<string | undefined> {\n const row = await this.db\n .prepare('DELETE FROM devices WHERE token = ? RETURNING user_id')\n .bind(token)\n .first<{ user_id: string | null }>();\n return row?.user_id ?? undefined;\n }\n\n async unbindDevice(token: string): Promise<string | undefined> {\n const row = await this.db\n .prepare('SELECT user_id FROM devices WHERE token = ?')\n .bind(token)\n .first<{ user_id: string | null }>();\n await this.db.prepare('UPDATE devices SET user_id = NULL WHERE token = ?').bind(token).run();\n return row?.user_id ?? undefined;\n }\n\n async listSegments(): Promise<{ id: string; condition: ConditionIR }[]> {\n const { results } = await this.db\n .prepare('SELECT id, condition FROM segments ORDER BY id')\n .bind()\n .all<{ id: string; condition: string }>();\n return results.map((row) => ({\n id: row.id,\n condition: ConditionIR.parse(JSON.parse(row.condition)),\n }));\n }\n\n async getSegmentCondition(id: string): Promise<ConditionIR | undefined> {\n const row = await this.db\n .prepare('SELECT condition FROM segments WHERE id = ?')\n .bind(id)\n .first<{ condition: string }>();\n return row ? ConditionIR.parse(JSON.parse(row.condition)) : undefined;\n }\n\n async findTriggers(event: string): Promise<TriggerRoute[]> {\n const { results } = await this.db\n .prepare(\n 'SELECT workflow, hash, \"where\" AS whereClause, filter FROM triggers WHERE event = ?'\n )\n .bind(event)\n .all<{ workflow: string; hash: string; whereClause: string | null; filter: string | null }>();\n return results.map((row) => ({\n workflow: row.workflow,\n hash: row.hash,\n where: row.whereClause === null ? null : ConditionIR.parse(JSON.parse(row.whereClause)),\n filter: row.filter === null ? null : ConditionIR.parse(JSON.parse(row.filter)),\n }));\n }\n\n async listSegmentTriggers(): Promise<SegmentTriggerRoute[]> {\n const { results } = await this.db\n .prepare('SELECT workflow, hash, segment FROM segment_triggers')\n .all<SegmentTriggerRoute>();\n return results;\n }\n\n async putWorkflowIR(workflow: WorkflowIR): Promise<void> {\n await this.kv.put(`wf:${workflow.contentHash}`, JSON.stringify(workflow));\n }\n\n async getWorkflowIR(contentHash: string): Promise<WorkflowIR | undefined> {\n const raw = await this.kv.get(`wf:${contentHash}`);\n return raw === null ? undefined : WorkflowIR.parse(JSON.parse(raw));\n }\n\n async replaceRoutes(bundle: BundleIR): Promise<void> {\n // One atomic batch: all-or-nothing, and a concurrent ingest never observes\n // the half-empty routing table a delete-then-insert loop would expose.\n await this.db.batch([\n this.db.prepare('DELETE FROM triggers'),\n this.db.prepare('DELETE FROM segment_triggers'),\n this.db.prepare('DELETE FROM segments'),\n ...bundle.segments.map((segment) =>\n this.db\n .prepare('INSERT INTO segments (id, condition, hash) VALUES (?, ?, ?)')\n .bind(segment.id, JSON.stringify(segment.condition), segment.contentHash)\n ),\n ...bundle.workflows.flatMap((workflow) => {\n if (workflow.trigger.type === 'event') {\n return [\n this.db\n .prepare(\n 'INSERT INTO triggers (workflow, hash, event, \"where\", filter) VALUES (?, ?, ?, ?, ?)'\n )\n .bind(\n workflow.id,\n workflow.contentHash,\n workflow.trigger.event,\n workflow.trigger.where !== undefined\n ? JSON.stringify(workflow.trigger.where)\n : null,\n workflow.trigger.filter !== undefined\n ? JSON.stringify(workflow.trigger.filter)\n : null\n ),\n ];\n }\n if (workflow.trigger.type === 'segment') {\n return [\n this.db\n .prepare('INSERT INTO segment_triggers (workflow, hash, segment) VALUES (?, ?, ?)')\n .bind(workflow.id, workflow.contentHash, workflow.trigger.segment),\n ];\n }\n return [];\n }),\n ]);\n }\n\n async isSegmentMember(segment: string, userId: string): Promise<boolean> {\n const row = await this.db\n .prepare('SELECT 1 AS x FROM segment_members WHERE segment = ? AND user_id = ?')\n .bind(segment, userId)\n .first<{ x: number }>();\n return row !== null;\n }\n\n async addSegmentMember(\n segment: string,\n userId: string,\n ts: number,\n workflows: readonly string[]\n ): Promise<void> {\n await this.db.batch([\n this.db\n .prepare('INSERT OR IGNORE INTO segment_members (segment, user_id, ts) VALUES (?, ?, ?)')\n .bind(segment, userId, ts),\n ...workflows.map((workflow) =>\n this.db\n .prepare(\n 'INSERT OR IGNORE INTO segment_starts (segment, workflow, user_id, ts) VALUES (?, ?, ?, ?)'\n )\n .bind(segment, workflow, userId, ts)\n ),\n ]);\n }\n\n async pendingSegmentStarts(segment: string, userId: string): Promise<string[]> {\n const { results } = await this.db\n .prepare('SELECT workflow FROM segment_starts WHERE segment = ? AND user_id = ?')\n .bind(segment, userId)\n .all<{ workflow: string }>();\n return results.map((row) => row.workflow);\n }\n\n async completeSegmentStart(segment: string, userId: string, workflow: string): Promise<void> {\n await this.db\n .prepare('DELETE FROM segment_starts WHERE segment = ? AND user_id = ? AND workflow = ?')\n .bind(segment, userId, workflow)\n .run();\n }\n\n async removeSegmentMember(segment: string, userId: string): Promise<void> {\n await this.db.batch([\n this.db\n .prepare('DELETE FROM segment_starts WHERE segment = ? AND user_id = ?')\n .bind(segment, userId),\n this.db\n .prepare('DELETE FROM segment_members WHERE segment = ? AND user_id = ?')\n .bind(segment, userId),\n ]);\n }\n\n async enterJourney(entry: EntryInput): Promise<EntryOutcome> {\n const inserted = await this.db\n .prepare(\n 'INSERT OR IGNORE INTO entries (workflow, user_id, instance_id, hash, status, ts) VALUES (?, ?, ?, ?, ?, ?)'\n )\n .bind(entry.workflow, entry.userId, entry.instanceId, entry.hash, 'running', entry.ts)\n .run();\n if ((inserted.meta?.changes ?? 1) !== 0) return 'entered';\n\n // A row already there blocks the entry, unless it records an instance that\n // died: 'failed' is reclaimed rather than barring the user forever over one\n // outage. The status test lives inside the UPDATE, so of two concurrent\n // retries exactly one sees a change.\n const claimed = await this.db\n .prepare(\n \"UPDATE entries SET instance_id = ?, hash = ?, status = 'running', ts = ? WHERE workflow = ? AND user_id = ? AND status = 'failed'\"\n )\n .bind(entry.instanceId, entry.hash, entry.ts, entry.workflow, entry.userId)\n .run();\n return (claimed.meta?.changes ?? 0) > 0 ? 'reclaimed' : null;\n }\n\n async removeEntry(instanceId: string): Promise<void> {\n await this.db.prepare('DELETE FROM entries WHERE instance_id = ?').bind(instanceId).run();\n }\n\n async setEntryStatus(instanceId: string, status: string): Promise<void> {\n await this.db\n .prepare('UPDATE entries SET status = ? WHERE instance_id = ?')\n .bind(status, instanceId)\n .run();\n }\n\n async findWakeHandles(userId: string, event: string): Promise<string[]> {\n const { results } = await this.db\n .prepare(\n 'SELECT DISTINCT wake_handle AS handle FROM subscriptions WHERE user_id = ? AND event = ?'\n )\n .bind(userId, event)\n .all<{ handle: string }>();\n return results.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 for (const event of events) {\n // OR IGNORE on the (wake_handle, event) PK: the interpreter re-subscribes on\n // every wait attempt (one-shot-callback contract) and rows persist here.\n await this.db\n .prepare(\n 'INSERT OR IGNORE INTO subscriptions (user_id, event, wake_handle, ts) VALUES (?, ?, ?, ?)'\n )\n .bind(userId, event, handle, ts)\n .run();\n }\n }\n\n async unsubscribe(handle: string): Promise<void> {\n await this.db.prepare('DELETE FROM subscriptions WHERE wake_handle = ?').bind(handle).run();\n }\n}\n\n/** What a merge-patch of `props` into an empty profile yields: the patch minus its nulls. */\nfunction stripNulls(props: ProfileProps): ProfileProps {\n return Object.fromEntries(Object.entries(props).filter(([, value]) => value !== null));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"d1.mjs","names":[],"sources":["../../src/store/d1.ts"],"sourcesContent":["import type { D1DatabaseLike, KVNamespaceLike } from '../cloudflare/bindings';\nimport type { PushDevice } from '../engine/ports';\nimport { type BundleIR, ConditionIR, WorkflowIR } from '../ir';\nimport type {\n DeviceInput,\n EntryInput,\n EntryOutcome,\n EventInput,\n EventWindow,\n JourneyStore,\n ProfileProps,\n SegmentTriggerRoute,\n TriggerRoute,\n} from './index';\n\n/**\n * The original data plane: D1 for the tables (see ../cloudflare/schema.sql)\n * and KV for the content-addressed workflow bodies (`wf:${contentHash}`).\n * The SQL below is exactly what the router and fact source used to inline —\n * the in-memory FakeD1 in the tests dispatches on these strings verbatim.\n */\nexport class D1JourneyStore implements JourneyStore {\n constructor(\n private readonly db: D1DatabaseLike,\n private readonly kv: KVNamespaceLike\n ) {}\n\n async insertEvent(input: EventInput): Promise<boolean> {\n const written = await this.db\n .prepare(\n 'INSERT OR IGNORE INTO events (id, user_id, name, ts, payload) VALUES (?, ?, ?, ?, ?)'\n )\n .bind(input.id, input.userId, input.event, input.ts, JSON.stringify(input.payload))\n .run();\n return (written.meta?.changes ?? 1) > 0;\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 = ? AND name = ?';\n if (window?.sinceMs !== undefined) {\n where += ' AND ts >= ?';\n params.push(window.sinceMs);\n }\n if (window?.untilMs !== undefined) {\n where += ' AND ts <= ?';\n params.push(window.untilMs);\n }\n return { where, params };\n }\n\n async countEvents(userId: string, event: string, window?: EventWindow): Promise<number> {\n const { where, params } = this.eventWindow(userId, event, window);\n const row = await this.db\n .prepare(`SELECT COUNT(*) AS c FROM events WHERE ${where}`)\n .bind(...params)\n .first<{ c: number }>();\n return row?.c ?? 0;\n }\n\n async listEventPayloads(\n userId: string,\n event: string,\n window?: EventWindow\n ): Promise<Record<string, unknown>[]> {\n const { where, params } = this.eventWindow(userId, event, window);\n const { results } = await this.db\n .prepare(`SELECT payload FROM events WHERE ${where}`)\n .bind(...params)\n .all<{ payload: string }>();\n return results.map((row) => JSON.parse(row.payload) as Record<string, unknown>);\n }\n\n async getProfile(userId: string): Promise<ProfileProps | undefined> {\n const row = await this.db\n .prepare('SELECT props FROM profiles WHERE user_id = ?')\n .bind(userId)\n .first<{ props: string }>();\n return row ? (JSON.parse(row.props) as ProfileProps) : undefined;\n }\n\n async mergeProfile(userId: string, props: ProfileProps): Promise<ProfileProps> {\n // SQLite's json_patch is RFC 7386 merge-patch: a null removes its key. The\n // merge happens in the statement, so two concurrent identifies cannot lose\n // each other's fields the way a read-modify-write did.\n const patch = JSON.stringify(props);\n const row = await this.db\n .prepare(\n \"INSERT INTO profiles (user_id, props) VALUES (?, json_patch('{}', ?)) ON CONFLICT(user_id) DO UPDATE SET props = json_patch(profiles.props, ?) RETURNING props\"\n )\n .bind(userId, patch, patch)\n .first<{ props: string }>();\n // RETURNING always yields the row it just wrote; the fallback only covers a\n // driver that declines to return one, and reports what was asked for.\n return row === null ? stripNulls(props) : (JSON.parse(row.props) as ProfileProps);\n }\n\n async listUserIds(): Promise<string[]> {\n const { results } = await this.db\n .prepare('SELECT user_id FROM profiles UNION SELECT DISTINCT user_id FROM events')\n .bind()\n .all<{ user_id: string }>();\n return results.map((row) => row.user_id);\n }\n\n async listDevices(userId: string): Promise<PushDevice[]> {\n const { results } = await this.db\n .prepare('SELECT token, platform FROM devices WHERE user_id = ? ORDER BY ts DESC')\n .bind(userId)\n .all<PushDevice>();\n return results;\n }\n\n async upsertDevice(device: DeviceInput): Promise<void> {\n await this.db\n .prepare(\n 'INSERT INTO devices (token, user_id, platform, ts) VALUES (?, ?, ?, ?) ON CONFLICT (token) DO UPDATE SET user_id = excluded.user_id, platform = excluded.platform, ts = excluded.ts'\n )\n .bind(device.token, device.userId, device.platform, device.ts)\n .run();\n }\n\n async requestErasure(userId: string, at: number): Promise<void> {\n await this.db\n .prepare('INSERT OR IGNORE INTO erasures (user_id, ts) VALUES (?, ?)')\n .bind(userId, at)\n .run();\n }\n\n async pendingErasures(): Promise<string[]> {\n const { results } = await this.db\n .prepare('SELECT user_id FROM erasures ORDER BY ts')\n .bind()\n .all<{ user_id: string }>();\n return results.map((row) => row.user_id);\n }\n\n async runningInstances(userId: string): Promise<string[]> {\n const { results } = await this.db\n .prepare(\"SELECT instance_id FROM entries WHERE user_id = ? AND status = 'running'\")\n .bind(userId)\n .all<{ instance_id: string }>();\n return results.map((row) => row.instance_id);\n }\n\n async forgetUser(userId: string): Promise<void> {\n // One batch: D1 runs it as a transaction; the request goes last, with the data it erases\n await this.db.batch(\n [\n 'entries',\n 'subscriptions',\n 'segment_starts',\n 'segment_members',\n 'devices',\n 'events',\n 'profiles',\n 'erasures',\n ].map((table) => this.db.prepare(`DELETE FROM ${table} WHERE user_id = ?`).bind(userId))\n );\n }\n\n async removeDevice(token: string): Promise<string | undefined> {\n const row = await this.db\n .prepare('DELETE FROM devices WHERE token = ? RETURNING user_id')\n .bind(token)\n .first<{ user_id: string | null }>();\n return row?.user_id ?? undefined;\n }\n\n async unbindDevice(token: string): Promise<string | undefined> {\n const row = await this.db\n .prepare('SELECT user_id FROM devices WHERE token = ?')\n .bind(token)\n .first<{ user_id: string | null }>();\n await this.db.prepare('UPDATE devices SET user_id = NULL WHERE token = ?').bind(token).run();\n return row?.user_id ?? undefined;\n }\n\n async listSegments(): Promise<{ id: string; condition: ConditionIR }[]> {\n const { results } = await this.db\n .prepare('SELECT id, condition FROM segments ORDER BY id')\n .bind()\n .all<{ id: string; condition: string }>();\n return results.map((row) => ({\n id: row.id,\n condition: ConditionIR.parse(JSON.parse(row.condition)),\n }));\n }\n\n async getSegmentCondition(id: string): Promise<ConditionIR | undefined> {\n const row = await this.db\n .prepare('SELECT condition FROM segments WHERE id = ?')\n .bind(id)\n .first<{ condition: string }>();\n return row ? ConditionIR.parse(JSON.parse(row.condition)) : undefined;\n }\n\n async findTriggers(event: string): Promise<TriggerRoute[]> {\n const { results } = await this.db\n .prepare(\n 'SELECT workflow, hash, \"where\" AS whereClause, filter FROM triggers WHERE event = ?'\n )\n .bind(event)\n .all<{ workflow: string; hash: string; whereClause: string | null; filter: string | null }>();\n return results.map((row) => ({\n workflow: row.workflow,\n hash: row.hash,\n where: row.whereClause === null ? null : ConditionIR.parse(JSON.parse(row.whereClause)),\n filter: row.filter === null ? null : ConditionIR.parse(JSON.parse(row.filter)),\n }));\n }\n\n async listSegmentTriggers(): Promise<SegmentTriggerRoute[]> {\n const { results } = await this.db\n .prepare('SELECT workflow, hash, segment FROM segment_triggers')\n .all<SegmentTriggerRoute>();\n return results;\n }\n\n async putWorkflowIR(workflow: WorkflowIR): Promise<void> {\n await this.kv.put(`wf:${workflow.contentHash}`, JSON.stringify(workflow));\n }\n\n async getWorkflowIR(contentHash: string): Promise<WorkflowIR | undefined> {\n const raw = await this.kv.get(`wf:${contentHash}`);\n return raw === null ? undefined : WorkflowIR.parse(JSON.parse(raw));\n }\n\n async replaceRoutes(bundle: BundleIR): Promise<void> {\n // One atomic batch: all-or-nothing, and a concurrent ingest never observes\n // the half-empty routing table a delete-then-insert loop would expose.\n await this.db.batch([\n this.db.prepare('DELETE FROM triggers'),\n this.db.prepare('DELETE FROM segment_triggers'),\n this.db.prepare('DELETE FROM segments'),\n ...bundle.segments.map((segment) =>\n this.db\n .prepare('INSERT INTO segments (id, condition, hash) VALUES (?, ?, ?)')\n .bind(segment.id, JSON.stringify(segment.condition), segment.contentHash)\n ),\n ...bundle.workflows.flatMap((workflow) => {\n if (workflow.trigger.type === 'event') {\n return [\n this.db\n .prepare(\n 'INSERT INTO triggers (workflow, hash, event, \"where\", filter) VALUES (?, ?, ?, ?, ?)'\n )\n .bind(\n workflow.id,\n workflow.contentHash,\n workflow.trigger.event,\n workflow.trigger.where !== undefined\n ? JSON.stringify(workflow.trigger.where)\n : null,\n workflow.trigger.filter !== undefined\n ? JSON.stringify(workflow.trigger.filter)\n : null\n ),\n ];\n }\n if (workflow.trigger.type === 'segment') {\n return [\n this.db\n .prepare('INSERT INTO segment_triggers (workflow, hash, segment) VALUES (?, ?, ?)')\n .bind(workflow.id, workflow.contentHash, workflow.trigger.segment),\n ];\n }\n return [];\n }),\n ]);\n }\n\n async isSegmentMember(segment: string, userId: string): Promise<boolean> {\n const row = await this.db\n .prepare('SELECT 1 AS x FROM segment_members WHERE segment = ? AND user_id = ?')\n .bind(segment, userId)\n .first<{ x: number }>();\n return row !== null;\n }\n\n async addSegmentMember(\n segment: string,\n userId: string,\n ts: number,\n workflows: readonly string[]\n ): Promise<void> {\n await this.db.batch([\n this.db\n .prepare('INSERT OR IGNORE INTO segment_members (segment, user_id, ts) VALUES (?, ?, ?)')\n .bind(segment, userId, ts),\n ...workflows.map((workflow) =>\n this.db\n .prepare(\n 'INSERT OR IGNORE INTO segment_starts (segment, workflow, user_id, ts) VALUES (?, ?, ?, ?)'\n )\n .bind(segment, workflow, userId, ts)\n ),\n ]);\n }\n\n async pendingSegmentStarts(segment: string, userId: string): Promise<string[]> {\n const { results } = await this.db\n .prepare('SELECT workflow FROM segment_starts WHERE segment = ? AND user_id = ?')\n .bind(segment, userId)\n .all<{ workflow: string }>();\n return results.map((row) => row.workflow);\n }\n\n async completeSegmentStart(segment: string, userId: string, workflow: string): Promise<void> {\n await this.db\n .prepare('DELETE FROM segment_starts WHERE segment = ? AND user_id = ? AND workflow = ?')\n .bind(segment, userId, workflow)\n .run();\n }\n\n async removeSegmentMember(segment: string, userId: string): Promise<void> {\n await this.db.batch([\n this.db\n .prepare('DELETE FROM segment_starts WHERE segment = ? AND user_id = ?')\n .bind(segment, userId),\n this.db\n .prepare('DELETE FROM segment_members WHERE segment = ? AND user_id = ?')\n .bind(segment, userId),\n ]);\n }\n\n async enterJourney(entry: EntryInput): Promise<EntryOutcome> {\n if (entry.execution !== undefined) {\n throw new Error('D1JourneyStore keeps no execution starts: this runtime needs Postgres');\n }\n const inserted = await this.db\n .prepare(\n 'INSERT OR IGNORE INTO entries (workflow, user_id, instance_id, hash, status, ts) VALUES (?, ?, ?, ?, ?, ?)'\n )\n .bind(entry.workflow, entry.userId, entry.instanceId, entry.hash, 'running', entry.ts)\n .run();\n if ((inserted.meta?.changes ?? 1) !== 0) return 'entered';\n\n // A row already there blocks the entry, unless it records an instance that\n // died: 'failed' is reclaimed rather than barring the user forever over one\n // outage. The status test lives inside the UPDATE, so of two concurrent\n // retries exactly one sees a change.\n const claimed = await this.db\n .prepare(\n \"UPDATE entries SET instance_id = ?, hash = ?, status = 'running', ts = ? WHERE workflow = ? AND user_id = ? AND status = 'failed'\"\n )\n .bind(entry.instanceId, entry.hash, entry.ts, entry.workflow, entry.userId)\n .run();\n return (claimed.meta?.changes ?? 0) > 0 ? 'reclaimed' : null;\n }\n\n async removeEntry(instanceId: string): Promise<void> {\n await this.db.prepare('DELETE FROM entries WHERE instance_id = ?').bind(instanceId).run();\n }\n\n async setEntryStatus(instanceId: string, status: string): Promise<void> {\n await this.db\n .prepare('UPDATE entries SET status = ? WHERE instance_id = ?')\n .bind(status, instanceId)\n .run();\n }\n\n async findWakeHandles(userId: string, event: string): Promise<string[]> {\n const { results } = await this.db\n .prepare(\n 'SELECT DISTINCT wake_handle AS handle FROM subscriptions WHERE user_id = ? AND event = ?'\n )\n .bind(userId, event)\n .all<{ handle: string }>();\n return results.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 for (const event of events) {\n // OR IGNORE on the (wake_handle, event) PK: the interpreter re-subscribes on\n // every wait attempt (one-shot-callback contract) and rows persist here.\n await this.db\n .prepare(\n 'INSERT OR IGNORE INTO subscriptions (user_id, event, wake_handle, ts) VALUES (?, ?, ?, ?)'\n )\n .bind(userId, event, handle, ts)\n .run();\n }\n }\n\n async unsubscribe(handle: string): Promise<void> {\n await this.db.prepare('DELETE FROM subscriptions WHERE wake_handle = ?').bind(handle).run();\n }\n}\n\n/** What a merge-patch of `props` into an empty profile yields: the patch minus its nulls. */\nfunction stripNulls(props: ProfileProps): ProfileProps {\n return Object.fromEntries(Object.entries(props).filter(([, value]) => value !== null));\n}\n"],"mappings":"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"}
|
package/dist/store/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","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 as a port. Everything the ingest router, the\n * runner and the fact source persist or look up goes through this interface,\n * so the storage engine is a deployment choice: D1 + KV (`D1JourneyStore`,\n * the original), Postgres (`PostgresJourneyStore`), or anything else that can\n * satisfy the contract below. Execution (durable steps, sleeps, wake-ups) is\n * 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}\n\nexport interface SegmentTriggerRoute {\n workflow: string;\n hash: string;\n segment: string;\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\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 userId: string;\n token: string;\n /** `ios` (APNs) or `android` (FCM): which provider the token belongs to. */\n platform: string;\n ts: number;\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\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 sentAt: number;\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. A store that keeps it (Postgres) exposes it as `reporting`; the Worker then records it\n * and serves email tracking. 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\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 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 /** The email copy's recipient opted out of email: recorded on the copy and as a suppression. False for an unknown copy. */\n recordUnsubscribe(messageId: string, at: number): Promise<boolean>;\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\nexport interface JourneyStore {\n /** Reporting, when the store keeps it (Postgres); D1 stores none. */\n readonly reporting?: JourneyReporting;\n\n /**\n * Record that the user's data must go (a deleted account), before anything is erased: the\n * request outlives a failure to stop their instances, and the sweep retries it.\n */\n requestErasure(userId: string, at: number): Promise<void>;\n /** Users whose erasure was requested and not finished yet. */\n pendingErasures(): Promise<string[]>;\n /** The user's journey instances still running, by the entry ledger. */\n runningInstances(userId: string): Promise<string[]>;\n /**\n * Erase everything kept about the user — profile, devices, events, wake subscriptions, segment\n * memberships, suppressions, entries and reporting — and the erasure request last. Run only\n * once no instance of theirs runs any more, or one could write some of it back.\n */\n forgetUser(userId: string): Promise<void>;\n\n /** The user opted out of the channel (recorded by an unsubscribe); stores without reporting omit it. */\n isSuppressed?(userId: string, channel: string): Promise<boolean>;\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. Stores whose schema the engine owns (D1) omit it.\n */\n checkSchema?(): Promise<string[]>;\n\n /* ---------------------------------- events --------------------------------- */\n /** Append the occurrence; false when its id was already in the log (nothing written). */\n insertEvent(input: EventInput): Promise<boolean>;\n /** Occurrences of `event` for the user, optionally only those within the window. */\n countEvents(userId: string, event: string, window?: EventWindow): Promise<number>;\n /** The payloads of those occurrences — the caller filters them with the shared evaluator. */\n listEventPayloads(\n userId: string,\n event: string,\n window?: EventWindow\n ): Promise<Record<string, unknown>[]>;\n\n /* --------------------------------- profiles -------------------------------- */\n getProfile(userId: string): Promise<ProfileProps | undefined>;\n /**\n * Merge-patch `props` into the stored profile in one statement (a null\n * removes the property) and return the result. One statement, because a\n * read-modify-write loses one of two concurrent identifies.\n */\n mergeProfile(userId: string, props: ProfileProps): Promise<ProfileProps>;\n\n /** Every user the engine knows (a profile or an event): who the segment sweep evaluates. */\n listUserIds(): Promise<string[]>;\n\n /* ---------------------------------- devices --------------------------------- */\n /** The user's push devices, most recently registered first. */\n listDevices(userId: string): Promise<PushDevice[]>;\n /**\n * Register a device: one row per token, so a token registered again moves to the user who\n * registered it last (a phone signed into another account) and to the front of their list.\n */\n upsertDevice(device: DeviceInput): Promise<void>;\n /** Forget a token (reported dead by APNs / FCM); the user 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 sign-in\n * binds it again. The user it belonged to, if any.\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 replaceRoutes(bundle: BundleIR): Promise<void>;\n /**\n * The Worker version whose bundle the routes came from, when one put them there through\n * `replaceRoutesIfNewer` (a forced deploy leaves it as it was). Read from the database itself,\n * not a cache in front of it. Stores without it (D1) leave every deploy to `POST /bundle/deploy`.\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\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 { D1JourneyStore } from './d1';\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} from './tables';\nexport type { PostgresJourneyStoreOptions, SqlClientLike } from './postgres';\n"],"mappings":";;;;;;;;;;;AA0FA,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.cjs","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 as a port. Everything the ingest router, the\n * runner and the fact source persist or look up goes through this interface,\n * so the storage engine is a deployment choice: D1 + KV (`D1JourneyStore`,\n * the original), Postgres (`PostgresJourneyStore`), or anything else that can\n * satisfy the contract below. Execution (durable steps, sleeps, wake-ups) is\n * 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 /**\n * The code version that applied these routes (`replaceRoutesIfNewer`); absent after a forced\n * deploy, or on a store that keeps none. Read in the same row as the hash, so the pair is\n * 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. Only a store with\n * `executions` accepts it.\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 userId: string;\n token: string;\n /** `ios` (APNs) or `android` (FCM): which provider the token belongs to. */\n platform: string;\n ts: number;\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\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 sentAt: number;\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. A store that keeps it (Postgres) exposes it as `reporting`; the Worker then records it\n * and serves email tracking. 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\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 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 /** The email copy's recipient opted out of email: recorded on the copy and as a suppression. False for an unknown copy. */\n recordUnsubscribe(messageId: string, at: number): Promise<boolean>;\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\nexport interface JourneyStore {\n /** Reporting, when the store keeps it (Postgres); D1 stores none. */\n readonly reporting?: JourneyReporting;\n\n /**\n * Record that the user's data must go (a deleted account), before anything is erased: the\n * request outlives a failure to stop their instances, and the sweep retries it.\n */\n requestErasure(userId: string, at: number): Promise<void>;\n /** Users whose erasure was requested and not finished yet. */\n pendingErasures(): Promise<string[]>;\n /** The user's journey instances still running, by the entry ledger. */\n runningInstances(userId: string): Promise<string[]>;\n /**\n * Erase everything kept about the user — profile, devices, events, wake subscriptions, segment\n * memberships, suppressions, entries and reporting — and the erasure request last. Run only\n * once no instance of theirs runs any more, or one could write some of it back.\n */\n forgetUser(userId: string): Promise<void>;\n\n /** The user opted out of the channel (recorded by an unsubscribe); stores without reporting omit it. */\n isSuppressed?(userId: string, channel: string): Promise<boolean>;\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. Stores whose schema the engine owns (D1) omit it.\n */\n checkSchema?(): Promise<string[]>;\n\n /* ---------------------------------- events --------------------------------- */\n /** Append the occurrence; false when its id was already in the log (nothing written). */\n insertEvent(input: EventInput): Promise<boolean>;\n /** Occurrences of `event` for the user, optionally only those within the window. */\n countEvents(userId: string, event: string, window?: EventWindow): Promise<number>;\n /** The payloads of those occurrences — the caller filters them with the shared evaluator. */\n listEventPayloads(\n userId: string,\n event: string,\n window?: EventWindow\n ): Promise<Record<string, unknown>[]>;\n\n /* --------------------------------- profiles -------------------------------- */\n getProfile(userId: string): Promise<ProfileProps | undefined>;\n /**\n * Merge-patch `props` into the stored profile in one statement (a null\n * removes the property) and return the result. One statement, because a\n * read-modify-write loses one of two concurrent identifies.\n */\n mergeProfile(userId: string, props: ProfileProps): Promise<ProfileProps>;\n\n /** Every user the engine knows (a profile or an event): who the segment sweep evaluates. */\n listUserIds(): Promise<string[]>;\n\n /* ---------------------------------- devices --------------------------------- */\n /** The user's push devices, most recently registered first. */\n listDevices(userId: string): Promise<PushDevice[]>;\n /**\n * Register a device: one row per token, so a token registered again moves to the user who\n * registered it last (a phone signed into another account) and to the front of their list.\n */\n upsertDevice(device: DeviceInput): Promise<void>;\n /** Forget a token (reported dead by APNs / FCM); the user 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 sign-in\n * binds it again. The user it belonged to, if any.\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 replaceRoutes(bundle: BundleIR): Promise<void>;\n /**\n * The Worker version whose bundle the routes came from, when one put them there through\n * `replaceRoutesIfNewer` (a forced deploy leaves it as it was). Read from the database itself,\n * not a cache in front of it. Stores without it (D1) leave every deploy to `POST /bundle/deploy`.\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 /** Execution starts and wake callbacks, for a runtime that needs them (AWS); Postgres only. */\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 { D1JourneyStore } from './d1';\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} from './tables';\nexport type { PostgresJourneyStoreOptions, SqlClientLike } from './postgres';\n"],"mappings":";;;;;;;;;;;AAoKA,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/index.d.cts
CHANGED
|
@@ -39,11 +39,19 @@ export interface TriggerRoute {
|
|
|
39
39
|
where: ConditionIR | null;
|
|
40
40
|
/** Profile gate, or null. */
|
|
41
41
|
filter: ConditionIR | null;
|
|
42
|
+
/**
|
|
43
|
+
* The code version that applied these routes (`replaceRoutesIfNewer`); absent after a forced
|
|
44
|
+
* deploy, or on a store that keeps none. Read in the same row as the hash, so the pair is
|
|
45
|
+
* always one deploy's.
|
|
46
|
+
*/
|
|
47
|
+
version?: string | undefined;
|
|
42
48
|
}
|
|
43
49
|
export interface SegmentTriggerRoute {
|
|
44
50
|
workflow: string;
|
|
45
51
|
hash: string;
|
|
46
52
|
segment: string;
|
|
53
|
+
/** As `TriggerRoute.version`. */
|
|
54
|
+
version?: string | undefined;
|
|
47
55
|
}
|
|
48
56
|
/**
|
|
49
57
|
* A Worker version, as Cloudflare's version metadata binding names it: the bundle compiled into
|
|
@@ -60,6 +68,70 @@ export interface EntryInput {
|
|
|
60
68
|
instanceId: string;
|
|
61
69
|
hash: string;
|
|
62
70
|
ts: number;
|
|
71
|
+
/**
|
|
72
|
+
* The instance's start, recorded in the same transaction as the entry — for a runtime whose
|
|
73
|
+
* start is an asynchronous submit (AWS) that may have to be retried. Only a store with
|
|
74
|
+
* `executions` accepts it.
|
|
75
|
+
*/
|
|
76
|
+
execution?: ExecutionStart | undefined;
|
|
77
|
+
}
|
|
78
|
+
/** How an instance is started on a runtime that submits it: replayed as is on every retry. */
|
|
79
|
+
export interface ExecutionStart {
|
|
80
|
+
/** The runtime's idempotency key (the durable execution name). */
|
|
81
|
+
name: string;
|
|
82
|
+
/** What is invoked: a qualified function ARN, its version included. */
|
|
83
|
+
target: string;
|
|
84
|
+
/** The exact payload: a retried submit must send the same bytes, or the runtime refuses it. */
|
|
85
|
+
input: string;
|
|
86
|
+
}
|
|
87
|
+
export interface ExecutionRecord extends ExecutionStart {
|
|
88
|
+
instanceId: string;
|
|
89
|
+
workflow: string;
|
|
90
|
+
userId: string;
|
|
91
|
+
createdAt: number;
|
|
92
|
+
submittedAt?: number | undefined;
|
|
93
|
+
arn?: string | undefined;
|
|
94
|
+
unresolvedAt?: number | undefined;
|
|
95
|
+
callbackId?: string | undefined;
|
|
96
|
+
/** Bumped by every wait the instance arms: a wake or a cleanup of an older one leaves it alone. */
|
|
97
|
+
generation: number;
|
|
98
|
+
deadlineAt?: number | undefined;
|
|
99
|
+
checkedAt?: number | undefined;
|
|
100
|
+
stoppedAt?: number | undefined;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* What a runtime that submits its starts and parks waits on one-shot callbacks (AWS) keeps
|
|
104
|
+
* beside the entry ledger. The runtime holds the execution history; this holds only the
|
|
105
|
+
* mapping from the engine's instance id to it.
|
|
106
|
+
*/
|
|
107
|
+
export interface ExecutionRegistry {
|
|
108
|
+
get(instanceId: string): Promise<ExecutionRecord | undefined>;
|
|
109
|
+
/**
|
|
110
|
+
* Starts not yet confirmed and not given up on, oldest first: of one (workflow, user) entry
|
|
111
|
+
* still running, or every one created before `createdBefore`.
|
|
112
|
+
*/
|
|
113
|
+
pendingStarts(filter: {
|
|
114
|
+
workflow?: string;
|
|
115
|
+
userId?: string;
|
|
116
|
+
createdBefore?: number;
|
|
117
|
+
limit: number;
|
|
118
|
+
}): Promise<ExecutionRecord[]>;
|
|
119
|
+
markSubmitted(instanceId: string, arn: string | undefined, at: number): Promise<void>;
|
|
120
|
+
markUnresolved(instanceId: string, at: number): Promise<void>;
|
|
121
|
+
setArn(instanceId: string, arn: string): Promise<void>;
|
|
122
|
+
/** Record the callback a wait parks on; false when a newer generation is already armed. */
|
|
123
|
+
armCallback(instanceId: string, callback: {
|
|
124
|
+
id: string;
|
|
125
|
+
generation: number;
|
|
126
|
+
deadlineAt?: number | undefined;
|
|
127
|
+
}): Promise<boolean>;
|
|
128
|
+
/** Forget the callback — only if it is still that generation's. */
|
|
129
|
+
clearCallback(instanceId: string, generation: number): Promise<void>;
|
|
130
|
+
/** Submitted executions whose entry is still running, least recently checked first. */
|
|
131
|
+
toReconcile(limit: number, checkedBefore: number): Promise<ExecutionRecord[]>;
|
|
132
|
+
markChecked(instanceId: string, at: number): Promise<void>;
|
|
133
|
+
/** Record the first stop; a later one keeps it. */
|
|
134
|
+
markStopped(instanceId: string, at: number): Promise<void>;
|
|
63
135
|
}
|
|
64
136
|
/**
|
|
65
137
|
* How an entry attempt ended: a fresh row, a 'failed' row of a dead instance
|
|
@@ -300,6 +372,8 @@ export interface JourneyStore {
|
|
|
300
372
|
enterJourney(entry: EntryInput): Promise<EntryOutcome>;
|
|
301
373
|
removeEntry(instanceId: string): Promise<void>;
|
|
302
374
|
setEntryStatus(instanceId: string, status: string): Promise<void>;
|
|
375
|
+
/** Execution starts and wake callbacks, for a runtime that needs them (AWS); Postgres only. */
|
|
376
|
+
readonly executions?: ExecutionRegistry;
|
|
303
377
|
/** Distinct wake handles registered for (user, event). */
|
|
304
378
|
findWakeHandles(userId: string, event: string): Promise<string[]>;
|
|
305
379
|
/** Register the handle for each event; repeated registrations are idempotent. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/store/index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;YAoBY,eAAe,eAAe;iBAEzB;;EAEf;EACA;EACA;EACA;EACA,SAAS;;iBAGM;EACf;EACA;;EAEA,OAAO;;EAEP,QAAQ;;
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/store/index.ts"],"mappings":"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"}
|
package/dist/store/index.d.mts
CHANGED
|
@@ -39,11 +39,19 @@ export interface TriggerRoute {
|
|
|
39
39
|
where: ConditionIR | null;
|
|
40
40
|
/** Profile gate, or null. */
|
|
41
41
|
filter: ConditionIR | null;
|
|
42
|
+
/**
|
|
43
|
+
* The code version that applied these routes (`replaceRoutesIfNewer`); absent after a forced
|
|
44
|
+
* deploy, or on a store that keeps none. Read in the same row as the hash, so the pair is
|
|
45
|
+
* always one deploy's.
|
|
46
|
+
*/
|
|
47
|
+
version?: string | undefined;
|
|
42
48
|
}
|
|
43
49
|
export interface SegmentTriggerRoute {
|
|
44
50
|
workflow: string;
|
|
45
51
|
hash: string;
|
|
46
52
|
segment: string;
|
|
53
|
+
/** As `TriggerRoute.version`. */
|
|
54
|
+
version?: string | undefined;
|
|
47
55
|
}
|
|
48
56
|
/**
|
|
49
57
|
* A Worker version, as Cloudflare's version metadata binding names it: the bundle compiled into
|
|
@@ -60,6 +68,70 @@ export interface EntryInput {
|
|
|
60
68
|
instanceId: string;
|
|
61
69
|
hash: string;
|
|
62
70
|
ts: number;
|
|
71
|
+
/**
|
|
72
|
+
* The instance's start, recorded in the same transaction as the entry — for a runtime whose
|
|
73
|
+
* start is an asynchronous submit (AWS) that may have to be retried. Only a store with
|
|
74
|
+
* `executions` accepts it.
|
|
75
|
+
*/
|
|
76
|
+
execution?: ExecutionStart | undefined;
|
|
77
|
+
}
|
|
78
|
+
/** How an instance is started on a runtime that submits it: replayed as is on every retry. */
|
|
79
|
+
export interface ExecutionStart {
|
|
80
|
+
/** The runtime's idempotency key (the durable execution name). */
|
|
81
|
+
name: string;
|
|
82
|
+
/** What is invoked: a qualified function ARN, its version included. */
|
|
83
|
+
target: string;
|
|
84
|
+
/** The exact payload: a retried submit must send the same bytes, or the runtime refuses it. */
|
|
85
|
+
input: string;
|
|
86
|
+
}
|
|
87
|
+
export interface ExecutionRecord extends ExecutionStart {
|
|
88
|
+
instanceId: string;
|
|
89
|
+
workflow: string;
|
|
90
|
+
userId: string;
|
|
91
|
+
createdAt: number;
|
|
92
|
+
submittedAt?: number | undefined;
|
|
93
|
+
arn?: string | undefined;
|
|
94
|
+
unresolvedAt?: number | undefined;
|
|
95
|
+
callbackId?: string | undefined;
|
|
96
|
+
/** Bumped by every wait the instance arms: a wake or a cleanup of an older one leaves it alone. */
|
|
97
|
+
generation: number;
|
|
98
|
+
deadlineAt?: number | undefined;
|
|
99
|
+
checkedAt?: number | undefined;
|
|
100
|
+
stoppedAt?: number | undefined;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* What a runtime that submits its starts and parks waits on one-shot callbacks (AWS) keeps
|
|
104
|
+
* beside the entry ledger. The runtime holds the execution history; this holds only the
|
|
105
|
+
* mapping from the engine's instance id to it.
|
|
106
|
+
*/
|
|
107
|
+
export interface ExecutionRegistry {
|
|
108
|
+
get(instanceId: string): Promise<ExecutionRecord | undefined>;
|
|
109
|
+
/**
|
|
110
|
+
* Starts not yet confirmed and not given up on, oldest first: of one (workflow, user) entry
|
|
111
|
+
* still running, or every one created before `createdBefore`.
|
|
112
|
+
*/
|
|
113
|
+
pendingStarts(filter: {
|
|
114
|
+
workflow?: string;
|
|
115
|
+
userId?: string;
|
|
116
|
+
createdBefore?: number;
|
|
117
|
+
limit: number;
|
|
118
|
+
}): Promise<ExecutionRecord[]>;
|
|
119
|
+
markSubmitted(instanceId: string, arn: string | undefined, at: number): Promise<void>;
|
|
120
|
+
markUnresolved(instanceId: string, at: number): Promise<void>;
|
|
121
|
+
setArn(instanceId: string, arn: string): Promise<void>;
|
|
122
|
+
/** Record the callback a wait parks on; false when a newer generation is already armed. */
|
|
123
|
+
armCallback(instanceId: string, callback: {
|
|
124
|
+
id: string;
|
|
125
|
+
generation: number;
|
|
126
|
+
deadlineAt?: number | undefined;
|
|
127
|
+
}): Promise<boolean>;
|
|
128
|
+
/** Forget the callback — only if it is still that generation's. */
|
|
129
|
+
clearCallback(instanceId: string, generation: number): Promise<void>;
|
|
130
|
+
/** Submitted executions whose entry is still running, least recently checked first. */
|
|
131
|
+
toReconcile(limit: number, checkedBefore: number): Promise<ExecutionRecord[]>;
|
|
132
|
+
markChecked(instanceId: string, at: number): Promise<void>;
|
|
133
|
+
/** Record the first stop; a later one keeps it. */
|
|
134
|
+
markStopped(instanceId: string, at: number): Promise<void>;
|
|
63
135
|
}
|
|
64
136
|
/**
|
|
65
137
|
* How an entry attempt ended: a fresh row, a 'failed' row of a dead instance
|
|
@@ -300,6 +372,8 @@ export interface JourneyStore {
|
|
|
300
372
|
enterJourney(entry: EntryInput): Promise<EntryOutcome>;
|
|
301
373
|
removeEntry(instanceId: string): Promise<void>;
|
|
302
374
|
setEntryStatus(instanceId: string, status: string): Promise<void>;
|
|
375
|
+
/** Execution starts and wake callbacks, for a runtime that needs them (AWS); Postgres only. */
|
|
376
|
+
readonly executions?: ExecutionRegistry;
|
|
303
377
|
/** Distinct wake handles registered for (user, event). */
|
|
304
378
|
findWakeHandles(userId: string, event: string): Promise<string[]>;
|
|
305
379
|
/** Register the handle for each event; repeated registrations are idempotent. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/store/index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;YAoBY,eAAe,eAAe;iBAEzB;;EAEf;EACA;EACA;EACA;EACA,SAAS;;iBAGM;EACf;EACA;;EAEA,OAAO;;EAEP,QAAQ;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/store/index.ts"],"mappings":"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"}
|
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 as a port. Everything the ingest router, the\n * runner and the fact source persist or look up goes through this interface,\n * so the storage engine is a deployment choice: D1 + KV (`D1JourneyStore`,\n * the original), Postgres (`PostgresJourneyStore`), or anything else that can\n * satisfy the contract below. Execution (durable steps, sleeps, wake-ups) is\n * 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}\n\nexport interface SegmentTriggerRoute {\n workflow: string;\n hash: string;\n segment: string;\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\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 userId: string;\n token: string;\n /** `ios` (APNs) or `android` (FCM): which provider the token belongs to. */\n platform: string;\n ts: number;\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\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 sentAt: number;\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. A store that keeps it (Postgres) exposes it as `reporting`; the Worker then records it\n * and serves email tracking. 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\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 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 /** The email copy's recipient opted out of email: recorded on the copy and as a suppression. False for an unknown copy. */\n recordUnsubscribe(messageId: string, at: number): Promise<boolean>;\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\nexport interface JourneyStore {\n /** Reporting, when the store keeps it (Postgres); D1 stores none. */\n readonly reporting?: JourneyReporting;\n\n /**\n * Record that the user's data must go (a deleted account), before anything is erased: the\n * request outlives a failure to stop their instances, and the sweep retries it.\n */\n requestErasure(userId: string, at: number): Promise<void>;\n /** Users whose erasure was requested and not finished yet. */\n pendingErasures(): Promise<string[]>;\n /** The user's journey instances still running, by the entry ledger. */\n runningInstances(userId: string): Promise<string[]>;\n /**\n * Erase everything kept about the user — profile, devices, events, wake subscriptions, segment\n * memberships, suppressions, entries and reporting — and the erasure request last. Run only\n * once no instance of theirs runs any more, or one could write some of it back.\n */\n forgetUser(userId: string): Promise<void>;\n\n /** The user opted out of the channel (recorded by an unsubscribe); stores without reporting omit it. */\n isSuppressed?(userId: string, channel: string): Promise<boolean>;\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. Stores whose schema the engine owns (D1) omit it.\n */\n checkSchema?(): Promise<string[]>;\n\n /* ---------------------------------- events --------------------------------- */\n /** Append the occurrence; false when its id was already in the log (nothing written). */\n insertEvent(input: EventInput): Promise<boolean>;\n /** Occurrences of `event` for the user, optionally only those within the window. */\n countEvents(userId: string, event: string, window?: EventWindow): Promise<number>;\n /** The payloads of those occurrences — the caller filters them with the shared evaluator. */\n listEventPayloads(\n userId: string,\n event: string,\n window?: EventWindow\n ): Promise<Record<string, unknown>[]>;\n\n /* --------------------------------- profiles -------------------------------- */\n getProfile(userId: string): Promise<ProfileProps | undefined>;\n /**\n * Merge-patch `props` into the stored profile in one statement (a null\n * removes the property) and return the result. One statement, because a\n * read-modify-write loses one of two concurrent identifies.\n */\n mergeProfile(userId: string, props: ProfileProps): Promise<ProfileProps>;\n\n /** Every user the engine knows (a profile or an event): who the segment sweep evaluates. */\n listUserIds(): Promise<string[]>;\n\n /* ---------------------------------- devices --------------------------------- */\n /** The user's push devices, most recently registered first. */\n listDevices(userId: string): Promise<PushDevice[]>;\n /**\n * Register a device: one row per token, so a token registered again moves to the user who\n * registered it last (a phone signed into another account) and to the front of their list.\n */\n upsertDevice(device: DeviceInput): Promise<void>;\n /** Forget a token (reported dead by APNs / FCM); the user 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 sign-in\n * binds it again. The user it belonged to, if any.\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 replaceRoutes(bundle: BundleIR): Promise<void>;\n /**\n * The Worker version whose bundle the routes came from, when one put them there through\n * `replaceRoutesIfNewer` (a forced deploy leaves it as it was). Read from the database itself,\n * not a cache in front of it. Stores without it (D1) leave every deploy to `POST /bundle/deploy`.\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\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 { D1JourneyStore } from './d1';\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} from './tables';\nexport type { PostgresJourneyStoreOptions, SqlClientLike } from './postgres';\n"],"mappings":";;;;;;;;;;AA0FA,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 as a port. Everything the ingest router, the\n * runner and the fact source persist or look up goes through this interface,\n * so the storage engine is a deployment choice: D1 + KV (`D1JourneyStore`,\n * the original), Postgres (`PostgresJourneyStore`), or anything else that can\n * satisfy the contract below. Execution (durable steps, sleeps, wake-ups) is\n * 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 /**\n * The code version that applied these routes (`replaceRoutesIfNewer`); absent after a forced\n * deploy, or on a store that keeps none. Read in the same row as the hash, so the pair is\n * 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. Only a store with\n * `executions` accepts it.\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 userId: string;\n token: string;\n /** `ios` (APNs) or `android` (FCM): which provider the token belongs to. */\n platform: string;\n ts: number;\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\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 sentAt: number;\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. A store that keeps it (Postgres) exposes it as `reporting`; the Worker then records it\n * and serves email tracking. 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\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 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 /** The email copy's recipient opted out of email: recorded on the copy and as a suppression. False for an unknown copy. */\n recordUnsubscribe(messageId: string, at: number): Promise<boolean>;\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\nexport interface JourneyStore {\n /** Reporting, when the store keeps it (Postgres); D1 stores none. */\n readonly reporting?: JourneyReporting;\n\n /**\n * Record that the user's data must go (a deleted account), before anything is erased: the\n * request outlives a failure to stop their instances, and the sweep retries it.\n */\n requestErasure(userId: string, at: number): Promise<void>;\n /** Users whose erasure was requested and not finished yet. */\n pendingErasures(): Promise<string[]>;\n /** The user's journey instances still running, by the entry ledger. */\n runningInstances(userId: string): Promise<string[]>;\n /**\n * Erase everything kept about the user — profile, devices, events, wake subscriptions, segment\n * memberships, suppressions, entries and reporting — and the erasure request last. Run only\n * once no instance of theirs runs any more, or one could write some of it back.\n */\n forgetUser(userId: string): Promise<void>;\n\n /** The user opted out of the channel (recorded by an unsubscribe); stores without reporting omit it. */\n isSuppressed?(userId: string, channel: string): Promise<boolean>;\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. Stores whose schema the engine owns (D1) omit it.\n */\n checkSchema?(): Promise<string[]>;\n\n /* ---------------------------------- events --------------------------------- */\n /** Append the occurrence; false when its id was already in the log (nothing written). */\n insertEvent(input: EventInput): Promise<boolean>;\n /** Occurrences of `event` for the user, optionally only those within the window. */\n countEvents(userId: string, event: string, window?: EventWindow): Promise<number>;\n /** The payloads of those occurrences — the caller filters them with the shared evaluator. */\n listEventPayloads(\n userId: string,\n event: string,\n window?: EventWindow\n ): Promise<Record<string, unknown>[]>;\n\n /* --------------------------------- profiles -------------------------------- */\n getProfile(userId: string): Promise<ProfileProps | undefined>;\n /**\n * Merge-patch `props` into the stored profile in one statement (a null\n * removes the property) and return the result. One statement, because a\n * read-modify-write loses one of two concurrent identifies.\n */\n mergeProfile(userId: string, props: ProfileProps): Promise<ProfileProps>;\n\n /** Every user the engine knows (a profile or an event): who the segment sweep evaluates. */\n listUserIds(): Promise<string[]>;\n\n /* ---------------------------------- devices --------------------------------- */\n /** The user's push devices, most recently registered first. */\n listDevices(userId: string): Promise<PushDevice[]>;\n /**\n * Register a device: one row per token, so a token registered again moves to the user who\n * registered it last (a phone signed into another account) and to the front of their list.\n */\n upsertDevice(device: DeviceInput): Promise<void>;\n /** Forget a token (reported dead by APNs / FCM); the user 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 sign-in\n * binds it again. The user it belonged to, if any.\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 replaceRoutes(bundle: BundleIR): Promise<void>;\n /**\n * The Worker version whose bundle the routes came from, when one put them there through\n * `replaceRoutesIfNewer` (a forced deploy leaves it as it was). Read from the database itself,\n * not a cache in front of it. Stores without it (D1) leave every deploy to `POST /bundle/deploy`.\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 /** Execution starts and wake callbacks, for a runtime that needs them (AWS); Postgres only. */\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 { D1JourneyStore } from './d1';\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} from './tables';\nexport type { PostgresJourneyStoreOptions, SqlClientLike } from './postgres';\n"],"mappings":";;;;;;;;;;AAoKA,IAAa,sBAAb,cAAyC,MAAM;CAGxB;CAFrB,OAAyB;CAEzB,YAAY,UAAsC;EAChD,MACE,4IAA4I,SAAS,KAAK,MAAM,GAClK;EAHmB,KAAA,WAAA;CAIrB;AACF"}
|