@shware/workflow 3.2.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -29
- package/dist/apns.cjs +113 -26
- package/dist/apns.cjs.map +1 -1
- package/dist/apns.d.cts +52 -6
- package/dist/apns.d.cts.map +1 -1
- package/dist/apns.d.mts +52 -6
- package/dist/apns.d.mts.map +1 -1
- package/dist/apns.mjs +111 -27
- package/dist/apns.mjs.map +1 -1
- package/dist/aws/engine-step.cjs +5 -5
- package/dist/aws/engine-step.cjs.map +1 -1
- package/dist/aws/engine-step.d.cts +8 -2
- package/dist/aws/engine-step.d.cts.map +1 -1
- package/dist/aws/engine-step.d.mts +8 -2
- package/dist/aws/engine-step.d.mts.map +1 -1
- package/dist/aws/engine-step.mjs +5 -5
- package/dist/aws/engine-step.mjs.map +1 -1
- package/dist/aws/index.cjs +47 -6
- package/dist/aws/index.cjs.map +1 -1
- package/dist/aws/index.d.cts +2 -2
- package/dist/aws/index.d.cts.map +1 -1
- package/dist/aws/index.d.mts +2 -2
- package/dist/aws/index.d.mts.map +1 -1
- package/dist/aws/index.mjs +47 -6
- package/dist/aws/index.mjs.map +1 -1
- package/dist/aws/instances.cjs +29 -6
- package/dist/aws/instances.cjs.map +1 -1
- package/dist/aws/instances.d.cts +11 -1
- package/dist/aws/instances.d.cts.map +1 -1
- package/dist/aws/instances.d.mts +11 -1
- package/dist/aws/instances.d.mts.map +1 -1
- package/dist/aws/instances.mjs +27 -4
- package/dist/aws/instances.mjs.map +1 -1
- package/dist/client.cjs +4 -1
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +2 -2
- package/dist/client.d.cts.map +1 -1
- package/dist/client.d.mts +2 -2
- package/dist/client.d.mts.map +1 -1
- package/dist/client.mjs +4 -1
- package/dist/client.mjs.map +1 -1
- package/dist/cloudflare/bindings.cjs +28 -25
- package/dist/cloudflare/bindings.cjs.map +1 -1
- package/dist/cloudflare/bindings.d.cts +17 -129
- package/dist/cloudflare/bindings.d.cts.map +1 -1
- package/dist/cloudflare/bindings.d.mts +17 -129
- package/dist/cloudflare/bindings.d.mts.map +1 -1
- package/dist/cloudflare/bindings.mjs +28 -23
- package/dist/cloudflare/bindings.mjs.map +1 -1
- package/dist/cloudflare/d1-env.cjs +6 -6
- package/dist/cloudflare/d1-env.cjs.map +1 -1
- package/dist/cloudflare/d1-env.d.cts +5 -5
- package/dist/cloudflare/d1-env.d.mts +5 -5
- package/dist/cloudflare/d1-env.mjs +6 -6
- package/dist/cloudflare/d1-env.mjs.map +1 -1
- package/dist/cloudflare/index.cjs +28 -18
- package/dist/cloudflare/index.d.cts +8 -4
- package/dist/cloudflare/index.d.mts +8 -4
- package/dist/cloudflare/index.mjs +9 -5
- package/dist/cloudflare/runner.cjs +29 -14
- package/dist/cloudflare/runner.cjs.map +1 -1
- package/dist/cloudflare/runner.d.cts +9 -2
- package/dist/cloudflare/runner.d.cts.map +1 -1
- package/dist/cloudflare/runner.d.mts +9 -2
- package/dist/cloudflare/runner.d.mts.map +1 -1
- package/dist/cloudflare/runner.mjs +28 -13
- package/dist/cloudflare/runner.mjs.map +1 -1
- package/dist/cloudflare/schema.sql +27 -6
- package/dist/cloudflare/worker.cjs +19 -9
- package/dist/cloudflare/worker.cjs.map +1 -1
- package/dist/cloudflare/worker.d.cts +2 -2
- package/dist/cloudflare/worker.d.cts.map +1 -1
- package/dist/cloudflare/worker.d.mts +2 -2
- package/dist/cloudflare/worker.d.mts.map +1 -1
- package/dist/cloudflare/worker.mjs +19 -9
- package/dist/cloudflare/worker.mjs.map +1 -1
- package/dist/config.cjs +21 -5
- package/dist/config.cjs.map +1 -1
- package/dist/config.mjs +21 -5
- package/dist/config.mjs.map +1 -1
- package/dist/dsl/action.cjs +3 -6
- package/dist/dsl/action.cjs.map +1 -1
- package/dist/dsl/action.d.cts +12 -11
- package/dist/dsl/action.d.cts.map +1 -1
- package/dist/dsl/action.d.mts +12 -11
- package/dist/dsl/action.d.mts.map +1 -1
- package/dist/dsl/action.mjs +3 -6
- package/dist/dsl/action.mjs.map +1 -1
- package/dist/dsl/bundle.cjs +35 -31
- package/dist/dsl/bundle.cjs.map +1 -1
- package/dist/dsl/bundle.d.cts.map +1 -1
- package/dist/dsl/bundle.d.mts.map +1 -1
- package/dist/dsl/bundle.mjs +35 -31
- package/dist/dsl/bundle.mjs.map +1 -1
- package/dist/dsl/condition.cjs +13 -1
- package/dist/dsl/condition.cjs.map +1 -1
- package/dist/dsl/condition.d.cts +9 -1
- package/dist/dsl/condition.d.cts.map +1 -1
- package/dist/dsl/condition.d.mts +9 -1
- package/dist/dsl/condition.d.mts.map +1 -1
- package/dist/dsl/condition.mjs +13 -2
- package/dist/dsl/condition.mjs.map +1 -1
- package/dist/dsl/flow.cjs +33 -11
- package/dist/dsl/flow.cjs.map +1 -1
- package/dist/dsl/flow.d.cts +9 -1
- package/dist/dsl/flow.d.cts.map +1 -1
- package/dist/dsl/flow.d.mts +9 -1
- package/dist/dsl/flow.d.mts.map +1 -1
- package/dist/dsl/flow.mjs +34 -13
- package/dist/dsl/flow.mjs.map +1 -1
- package/dist/dsl/workflow.cjs +2 -3
- package/dist/dsl/workflow.cjs.map +1 -1
- package/dist/dsl/workflow.d.cts.map +1 -1
- package/dist/dsl/workflow.d.mts.map +1 -1
- package/dist/dsl/workflow.mjs +3 -4
- package/dist/dsl/workflow.mjs.map +1 -1
- package/dist/email-sender.cjs +24 -0
- package/dist/email-sender.cjs.map +1 -1
- package/dist/email-sender.d.cts +18 -0
- package/dist/email-sender.d.cts.map +1 -1
- package/dist/email-sender.d.mts +18 -0
- package/dist/email-sender.d.mts.map +1 -1
- package/dist/email-sender.mjs +24 -1
- package/dist/email-sender.mjs.map +1 -1
- package/dist/engine/actions.cjs +1 -8
- package/dist/engine/actions.cjs.map +1 -1
- package/dist/engine/actions.d.cts +1 -12
- package/dist/engine/actions.d.cts.map +1 -1
- package/dist/engine/actions.d.mts +1 -12
- package/dist/engine/actions.d.mts.map +1 -1
- package/dist/engine/actions.mjs +1 -8
- package/dist/engine/actions.mjs.map +1 -1
- package/dist/engine/condition.cjs +25 -5
- package/dist/engine/condition.cjs.map +1 -1
- package/dist/engine/condition.d.cts +12 -3
- package/dist/engine/condition.d.cts.map +1 -1
- package/dist/engine/condition.d.mts +12 -3
- package/dist/engine/condition.d.mts.map +1 -1
- package/dist/engine/condition.mjs +25 -6
- package/dist/engine/condition.mjs.map +1 -1
- package/dist/engine/index.cjs +3 -0
- package/dist/engine/index.d.cts +5 -5
- package/dist/engine/index.d.mts +5 -5
- package/dist/engine/index.mjs +4 -4
- package/dist/engine/interpreter.cjs +27 -20
- package/dist/engine/interpreter.cjs.map +1 -1
- package/dist/engine/interpreter.d.cts.map +1 -1
- package/dist/engine/interpreter.d.mts.map +1 -1
- package/dist/engine/interpreter.mjs +27 -20
- package/dist/engine/interpreter.mjs.map +1 -1
- package/dist/engine/ports.d.cts +21 -6
- package/dist/engine/ports.d.cts.map +1 -1
- package/dist/engine/ports.d.mts +21 -6
- package/dist/engine/ports.d.mts.map +1 -1
- package/dist/engine/retry.cjs +13 -0
- package/dist/engine/retry.cjs.map +1 -1
- package/dist/engine/retry.d.cts +12 -0
- package/dist/engine/retry.d.cts.map +1 -1
- package/dist/engine/retry.d.mts +12 -0
- package/dist/engine/retry.d.mts.map +1 -1
- package/dist/engine/retry.mjs +13 -1
- package/dist/engine/retry.mjs.map +1 -1
- package/dist/engine/time-scale.cjs +90 -15
- package/dist/engine/time-scale.cjs.map +1 -1
- package/dist/engine/time-scale.d.cts +9 -6
- package/dist/engine/time-scale.d.cts.map +1 -1
- package/dist/engine/time-scale.d.mts +9 -6
- package/dist/engine/time-scale.d.mts.map +1 -1
- package/dist/engine/time-scale.mjs +90 -16
- package/dist/engine/time-scale.mjs.map +1 -1
- package/dist/fcm.cjs +111 -38
- package/dist/fcm.cjs.map +1 -1
- package/dist/fcm.d.cts +45 -5
- package/dist/fcm.d.cts.map +1 -1
- package/dist/fcm.d.mts +45 -5
- package/dist/fcm.d.mts.map +1 -1
- package/dist/fcm.mjs +110 -39
- package/dist/fcm.mjs.map +1 -1
- package/dist/hash.cjs +6 -6
- package/dist/hash.cjs.map +1 -1
- package/dist/hash.mjs +6 -6
- package/dist/hash.mjs.map +1 -1
- package/dist/host/auth.cjs +49 -0
- package/dist/host/auth.cjs.map +1 -0
- package/dist/host/auth.d.cts +21 -0
- package/dist/host/auth.d.cts.map +1 -0
- package/dist/host/auth.d.mts +21 -0
- package/dist/host/auth.d.mts.map +1 -0
- package/dist/host/auth.mjs +47 -0
- package/dist/host/auth.mjs.map +1 -0
- package/dist/host/channels.cjs +118 -40
- package/dist/host/channels.cjs.map +1 -1
- package/dist/host/channels.d.cts +27 -1
- package/dist/host/channels.d.cts.map +1 -1
- package/dist/host/channels.d.mts +27 -1
- package/dist/host/channels.d.mts.map +1 -1
- package/dist/host/channels.mjs +121 -44
- package/dist/host/channels.mjs.map +1 -1
- package/dist/host/execute.cjs +14 -2
- package/dist/host/execute.cjs.map +1 -1
- package/dist/host/execute.d.cts +4 -4
- package/dist/host/execute.d.cts.map +1 -1
- package/dist/host/execute.d.mts +4 -4
- package/dist/host/execute.d.mts.map +1 -1
- package/dist/host/execute.mjs +13 -1
- package/dist/host/execute.mjs.map +1 -1
- package/dist/host/fresh-read.cjs +30 -0
- package/dist/host/fresh-read.cjs.map +1 -0
- package/dist/host/fresh-read.d.cts +18 -0
- package/dist/host/fresh-read.d.cts.map +1 -0
- package/dist/host/fresh-read.d.mts +18 -0
- package/dist/host/fresh-read.d.mts.map +1 -0
- package/dist/host/fresh-read.mjs +29 -0
- package/dist/host/fresh-read.mjs.map +1 -0
- package/dist/host/http.cjs +321 -0
- package/dist/host/http.cjs.map +1 -0
- package/dist/host/http.d.cts +54 -0
- package/dist/host/http.d.cts.map +1 -0
- package/dist/host/http.d.mts +54 -0
- package/dist/host/http.d.mts.map +1 -0
- package/dist/host/http.mjs +317 -0
- package/dist/host/http.mjs.map +1 -0
- package/dist/host/{router.cjs → ingest.cjs} +147 -175
- package/dist/host/ingest.cjs.map +1 -0
- package/dist/host/{router.d.cts → ingest.d.cts} +48 -30
- package/dist/host/ingest.d.cts.map +1 -0
- package/dist/host/{router.d.mts → ingest.d.mts} +48 -30
- package/dist/host/ingest.d.mts.map +1 -0
- package/dist/host/{router.mjs → ingest.mjs} +144 -173
- package/dist/host/ingest.mjs.map +1 -0
- package/dist/host/journey-host.cjs +65 -93
- package/dist/host/journey-host.cjs.map +1 -1
- package/dist/host/journey-host.d.cts +19 -26
- package/dist/host/journey-host.d.cts.map +1 -1
- package/dist/host/journey-host.d.mts +19 -26
- package/dist/host/journey-host.d.mts.map +1 -1
- package/dist/host/journey-host.mjs +65 -93
- package/dist/host/journey-host.mjs.map +1 -1
- package/dist/host/platform.cjs +21 -0
- package/dist/host/platform.cjs.map +1 -0
- package/dist/host/platform.d.cts +126 -0
- package/dist/host/platform.d.cts.map +1 -0
- package/dist/host/platform.d.mts +126 -0
- package/dist/host/platform.d.mts.map +1 -0
- package/dist/host/platform.mjs +19 -0
- package/dist/host/platform.mjs.map +1 -0
- package/dist/host/senders.cjs +18 -9
- package/dist/host/senders.cjs.map +1 -1
- package/dist/host/senders.d.cts +9 -4
- package/dist/host/senders.d.cts.map +1 -1
- package/dist/host/senders.d.mts +9 -4
- package/dist/host/senders.d.mts.map +1 -1
- package/dist/host/senders.mjs +18 -9
- package/dist/host/senders.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/ir.cjs +5 -7
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +3 -12
- package/dist/ir.d.cts.map +1 -1
- package/dist/ir.d.mts +3 -12
- package/dist/ir.d.mts.map +1 -1
- package/dist/ir.mjs +5 -7
- package/dist/ir.mjs.map +1 -1
- package/dist/plan.cjs +27 -33
- package/dist/plan.cjs.map +1 -1
- package/dist/plan.d.cts +1 -1
- package/dist/plan.d.cts.map +1 -1
- package/dist/plan.d.mts +1 -1
- package/dist/plan.d.mts.map +1 -1
- package/dist/plan.mjs +27 -33
- package/dist/plan.mjs.map +1 -1
- package/dist/reporting/observer.cjs +1 -0
- package/dist/reporting/observer.cjs.map +1 -1
- package/dist/reporting/observer.mjs +1 -0
- package/dist/reporting/observer.mjs.map +1 -1
- package/dist/reporting/tracking.cjs +19 -4
- package/dist/reporting/tracking.cjs.map +1 -1
- package/dist/reporting/tracking.d.cts +13 -11
- package/dist/reporting/tracking.d.cts.map +1 -1
- package/dist/reporting/tracking.d.mts +13 -11
- package/dist/reporting/tracking.d.mts.map +1 -1
- package/dist/reporting/tracking.mjs +19 -4
- package/dist/reporting/tracking.mjs.map +1 -1
- package/dist/store/d1.cjs +88 -29
- package/dist/store/d1.cjs.map +1 -1
- package/dist/store/d1.d.cts +29 -11
- package/dist/store/d1.d.cts.map +1 -1
- package/dist/store/d1.d.mts +29 -11
- package/dist/store/d1.d.mts.map +1 -1
- package/dist/store/d1.mjs +88 -29
- package/dist/store/d1.mjs.map +1 -1
- package/dist/store/erasure.cjs +22 -0
- package/dist/store/erasure.cjs.map +1 -0
- package/dist/store/erasure.d.cts +18 -0
- package/dist/store/erasure.d.cts.map +1 -0
- package/dist/store/erasure.d.mts +18 -0
- package/dist/store/erasure.d.mts.map +1 -0
- package/dist/store/erasure.mjs +21 -0
- package/dist/store/erasure.mjs.map +1 -0
- package/dist/store/index.cjs +2 -0
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +67 -13
- package/dist/store/index.d.cts.map +1 -1
- package/dist/store/index.d.mts +67 -13
- package/dist/store/index.d.mts.map +1 -1
- package/dist/store/index.mjs +2 -1
- package/dist/store/index.mjs.map +1 -1
- package/dist/store/postgres-reporting.cjs +125 -62
- package/dist/store/postgres-reporting.cjs.map +1 -1
- package/dist/store/postgres-reporting.d.cts +30 -2
- package/dist/store/postgres-reporting.d.cts.map +1 -1
- package/dist/store/postgres-reporting.d.mts +30 -2
- package/dist/store/postgres-reporting.d.mts.map +1 -1
- package/dist/store/postgres-reporting.mjs +125 -62
- package/dist/store/postgres-reporting.mjs.map +1 -1
- package/dist/store/postgres.cjs +179 -60
- package/dist/store/postgres.cjs.map +1 -1
- package/dist/store/postgres.d.cts +44 -7
- package/dist/store/postgres.d.cts.map +1 -1
- package/dist/store/postgres.d.mts +44 -7
- package/dist/store/postgres.d.mts.map +1 -1
- package/dist/store/postgres.mjs +179 -60
- package/dist/store/postgres.mjs.map +1 -1
- package/dist/store/tables.cjs +7 -5
- package/dist/store/tables.cjs.map +1 -1
- package/dist/store/tables.d.cts.map +1 -1
- package/dist/store/tables.d.mts.map +1 -1
- package/dist/store/tables.mjs +7 -5
- package/dist/store/tables.mjs.map +1 -1
- package/dist/store/user-lock.cjs +43 -0
- package/dist/store/user-lock.cjs.map +1 -0
- package/dist/store/user-lock.d.cts +32 -0
- package/dist/store/user-lock.d.cts.map +1 -0
- package/dist/store/user-lock.d.mts +32 -0
- package/dist/store/user-lock.d.mts.map +1 -0
- package/dist/store/user-lock.mjs +40 -0
- package/dist/store/user-lock.mjs.map +1 -0
- package/dist/transports/cloudflare-email.cjs +3 -2
- package/dist/transports/cloudflare-email.cjs.map +1 -1
- package/dist/transports/cloudflare-email.mjs +3 -2
- package/dist/transports/cloudflare-email.mjs.map +1 -1
- package/dist/transports/ses.cjs +52 -7
- package/dist/transports/ses.cjs.map +1 -1
- package/dist/transports/ses.d.cts +9 -1
- package/dist/transports/ses.d.cts.map +1 -1
- package/dist/transports/ses.d.mts +9 -1
- package/dist/transports/ses.d.mts.map +1 -1
- package/dist/transports/ses.mjs +52 -8
- package/dist/transports/ses.mjs.map +1 -1
- package/dist/webpush.cjs +133 -47
- package/dist/webpush.cjs.map +1 -1
- package/dist/webpush.d.cts +49 -8
- package/dist/webpush.d.cts.map +1 -1
- package/dist/webpush.d.mts +49 -8
- package/dist/webpush.d.mts.map +1 -1
- package/dist/webpush.mjs +130 -47
- package/dist/webpush.mjs.map +1 -1
- package/package.json +1 -1
- package/src/cloudflare/schema.sql +27 -6
- package/dist/host/router.cjs.map +0 -1
- package/dist/host/router.d.cts.map +0 -1
- package/dist/host/router.d.mts.map +0 -1
- package/dist/host/router.mjs.map +0 -1
package/dist/store/d1.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"d1.cjs","names":["ConditionIR","WorkflowIR"],"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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"}
|
|
1
|
+
{"version":3,"file":"d1.cjs","names":["UserErasedError","ConditionIR","WorkflowIR"],"sources":["../../src/store/d1.ts"],"sourcesContent":["import type { D1DatabaseLike, D1RunResultLike } from '../cloudflare/bindings';\nimport type { PushDevice } from '../engine/ports';\nimport { type BundleIR, ConditionIR, WorkflowIR } from '../ir';\nimport { type ErasureState, UserErasedError } from './erasure';\nimport type {\n DeviceInput,\n EntryInput,\n EntryOutcome,\n EventInput,\n EventWindow,\n JourneyStore,\n ProfileProps,\n RunningInstance,\n SegmentTriggerRoute,\n TriggerRoute,\n} from './index';\n\n/**\n * The condition every write about a user carries, as the last bound parameter: no erasure\n * tombstone for the user. Inside the statement, so the check and the write share one snapshot.\n */\nconst NOT_ERASED = 'WHERE NOT EXISTS (SELECT 1 FROM erasures WHERE user_id = ?)';\n\n/**\n * The D1 data plane: one database for everything (see ../cloudflare/schema.sql),\n * the content-addressed workflow bodies included — beside the routes that name\n * them, so a deploy is read-your-writes (KV's eventual consistency would let a\n * route name a body that is not readable yet). The in-memory FakeD1 in the\n * tests dispatches on these statements.\n *\n * Every write about a user is one statement conditional on their erasure tombstone not\n * existing (`INSERT ... SELECT ... WHERE NOT EXISTS`): a write that changed nothing is then\n * read back once to tell a refusal (`UserErasedError`) from a plain no-op. D1 has one writer,\n * so a statement is atomic against every other: the runner's admission is one conditional\n * UPDATE, and a tombstone either precedes it (refused) or follows it (the erasure finds the\n * instance admitted) — no lock needed.\n */\nexport class D1JourneyStore implements JourneyStore {\n constructor(private readonly db: D1DatabaseLike) {}\n\n async insertEvent(input: EventInput): Promise<boolean> {\n // The key is (user_id, id): the sender's occurrence id, per user\n const written = await this.db\n .prepare(\n `INSERT OR IGNORE INTO events (id, user_id, name, ts, payload) SELECT ?, ?, ?, ?, ? ${NOT_ERASED}`\n )\n .bind(\n input.id,\n input.userId,\n input.event,\n input.ts,\n JSON.stringify(input.payload),\n input.userId\n )\n .run();\n if ((written.meta?.changes ?? 1) > 0) return true;\n // Nothing written: a duplicate, unless the tombstone refused it\n await this.refuseIfErased(input.userId);\n return false;\n }\n\n /**\n * A conditional write changed nothing: on that path only, read why. Throws when the tombstone\n * refused it; returns when the statement had another reason (a duplicate, an entry taken).\n */\n private async refuseIfErased(userId: string): Promise<void> {\n if ((await this.erasureState(userId)) !== 'none') throw new UserErasedError(userId);\n }\n\n /** The event rows of one user and name, within the window's bounds (both inclusive). */\n private eventWindow(userId: string, event: string, window?: EventWindow) {\n const params: unknown[] = [userId, event];\n let where = 'user_id = ? 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 //\n // The merge is top-level only, as Postgres's `||` is: a key whose value is\n // an object replaces the stored value whole instead of being deep-merged\n // into it (json_patch's own behaviour), so the two stores agree. Those keys\n // are removed first, in the same statement; a patch of scalars — all the\n // API admits — needs no removal and is the plain json_patch.\n const patch = JSON.stringify(props);\n // Typed as scalars, but a caller outside the API may still hand an object: replace it whole\n const replaced = Object.entries(props as Record<string, unknown>)\n .filter(([, value]) => typeof value === 'object' && value !== null)\n .map(([key]) => key);\n const stored =\n replaced.length === 0\n ? 'profiles.props'\n : `json_remove(profiles.props, ${replaced.map(() => '?').join(', ')})`;\n // The SELECT keeps a WHERE clause, which is also what disambiguates the upsert for SQLite\n const row = await this.db\n .prepare(\n `INSERT INTO profiles (user_id, props) SELECT ?, json_patch('{}', ?) ${NOT_ERASED} ON CONFLICT(user_id) DO UPDATE SET props = json_patch(${stored}, ?) RETURNING props`\n )\n .bind(userId, patch, userId, ...replaced.map(jsonPath), patch)\n .first<{ props: string }>();\n if (row !== null) return JSON.parse(row.props) as ProfileProps;\n // No row: the tombstone refused the write — or a driver declined to return one, in which\n // case the merge went through and the answer is what was asked for\n await this.refuseIfErased(userId);\n return stripNulls(props);\n }\n\n async listUserIds(page?: {\n after?: string | undefined;\n limit?: number | undefined;\n }): Promise<string[]> {\n // Ascending user_id with a keyset bound, so a sweep can resume past the last id it saw\n const params: unknown[] = [];\n let where = '';\n if (page?.after !== undefined) {\n where = ' WHERE user_id > ?';\n params.push(page.after);\n }\n let limit = '';\n if (page?.limit !== undefined) {\n limit = ' LIMIT ?';\n params.push(page.limit);\n }\n const { results } = await this.db\n .prepare(\n `SELECT user_id FROM (SELECT user_id FROM profiles UNION SELECT user_id FROM events)${where} ORDER BY user_id${limit}`\n )\n .bind(...params)\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 const written = await this.db\n .prepare(\n `INSERT INTO devices (token, user_id, platform, ts) SELECT ?, ?, ?, ? ${NOT_ERASED} 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, device.userId)\n .run();\n // The upsert always changes its row — unless the tombstone refused it\n if ((written.meta?.changes ?? 1) === 0) throw new UserErasedError(device.userId);\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 WHERE erased_at IS NULL ORDER BY ts')\n .bind()\n .all<{ user_id: string }>();\n return results.map((row) => row.user_id);\n }\n\n async erasureState(userId: string): Promise<ErasureState> {\n const row = await this.db\n .prepare('SELECT erased_at FROM erasures WHERE user_id = ?')\n .bind(userId)\n .first<{ erased_at: number | null }>();\n if (row === null) return 'none';\n return row.erased_at === null ? 'erasing' : 'erased';\n }\n\n async runningInstances(userId: string): Promise<RunningInstance[]> {\n const { results } = await this.db\n .prepare(\n \"SELECT instance_id, admitted_at IS NOT NULL AS admitted FROM entries WHERE user_id = ? AND status = 'running'\"\n )\n .bind(userId)\n .all<{ instance_id: string; admitted: number | boolean }>();\n return results.map((row) => ({ instanceId: row.instance_id, admitted: Boolean(row.admitted) }));\n }\n\n async admitInstance(instanceId: string, userId: string): Promise<boolean> {\n // One statement. A first admission needs a running entry; an instance once admitted may\n // replay its finalisation after the entry reached a terminal status, under its own instance\n // id (a reclaim resets admitted_at, so a superseded attempt cannot). The first admission's\n // time is kept, the status never touched, and the tombstone refuses both\n const row = await this.db\n .prepare(\n \"UPDATE entries SET admitted_at = COALESCE(admitted_at, ?) WHERE instance_id = ? AND user_id = ? AND (status = 'running' OR admitted_at IS NOT NULL) AND NOT EXISTS (SELECT 1 FROM erasures WHERE user_id = ?) RETURNING instance_id\"\n )\n .bind(Date.now(), instanceId, userId, userId)\n .first<{ instance_id: string }>();\n return row !== null;\n }\n\n async forgetUser(userId: string, at: number): Promise<void> {\n // One batch: D1 runs it as a transaction. The tombstone stays, marked erased last; a repeat\n // keeps the first erase's time\n await this.db.batch([\n ...[\n 'entries',\n 'subscriptions',\n 'segment_starts',\n 'segment_members',\n 'devices',\n 'events',\n 'profiles',\n ].map((table) => this.db.prepare(`DELETE FROM ${table} WHERE user_id = ?`).bind(userId)),\n this.db\n .prepare('UPDATE erasures SET erased_at = COALESCE(erased_at, ?) WHERE user_id = ?')\n .bind(at, 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 // Content-addressed: a hash already there names the same body, so replacing is harmless\n await this.db\n .prepare('INSERT OR REPLACE INTO workflow_ir (content_hash, ir) VALUES (?, ?)')\n .bind(workflow.contentHash, JSON.stringify(workflow))\n .run();\n }\n\n async getWorkflowIR(contentHash: string): Promise<WorkflowIR | undefined> {\n const row = await this.db\n .prepare('SELECT ir FROM workflow_ir WHERE content_hash = ?')\n .bind(contentHash)\n .first<{ ir: string }>();\n return row === null ? undefined : WorkflowIR.parse(JSON.parse(row.ir));\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 const [member] = (await this.db.batch([\n this.db\n .prepare(\n `INSERT OR IGNORE INTO segment_members (segment, user_id, ts) SELECT ?, ?, ? ${NOT_ERASED}`\n )\n .bind(segment, userId, ts, userId),\n ...workflows.map((workflow) =>\n this.db\n .prepare(\n `INSERT OR IGNORE INTO segment_starts (segment, workflow, user_id, ts) SELECT ?, ?, ?, ? ${NOT_ERASED}`\n )\n .bind(segment, workflow, userId, ts, userId)\n ),\n ])) as D1RunResultLike[];\n // Already a member, or refused: either way the starts wrote nothing new\n if ((member.meta?.changes ?? 1) === 0) await this.refuseIfErased(userId);\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) SELECT ?, ?, ?, ?, ?, ? ${NOT_ERASED}`\n )\n .bind(\n entry.workflow,\n entry.userId,\n entry.instanceId,\n entry.hash,\n 'running',\n entry.ts,\n entry.userId\n )\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. The reclaim is a new attempt: it starts\n // unadmitted, even under the same instance id, so the previous attempt's\n // admission is not inherited.\n const claimed = await this.db\n .prepare(\n \"UPDATE entries SET instance_id = ?, hash = ?, status = 'running', ts = ?, admitted_at = NULL WHERE workflow = ? AND user_id = ? AND status = 'failed' AND NOT EXISTS (SELECT 1 FROM erasures WHERE user_id = ?)\"\n )\n .bind(entry.instanceId, entry.hash, entry.ts, entry.workflow, entry.userId, entry.userId)\n .run();\n if ((claimed.meta?.changes ?? 0) > 0) return 'reclaimed';\n // Taken by a live instance, or refused by the tombstone: tell them apart\n await this.refuseIfErased(entry.userId);\n return 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 let skipped = false;\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 const written = await this.db\n .prepare(\n `INSERT OR IGNORE INTO subscriptions (user_id, event, wake_handle, ts) SELECT ?, ?, ?, ? ${NOT_ERASED}`\n )\n .bind(userId, event, handle, ts, userId)\n .run();\n if ((written.meta?.changes ?? 1) === 0) skipped = true;\n }\n // A registration already there, or refused: one read tells which\n if (skipped) await this.refuseIfErased(userId);\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\n/**\n * The SQLite JSON path of a top-level key. Quoted, so dots and brackets in the key are literal;\n * a double quote has no escape in SQLite's path syntax, so such a key cannot be addressed.\n */\nfunction jsonPath(key: string): string {\n if (key.includes('\"')) {\n throw new Error(`profile property \"${key}\" cannot hold an object: its name contains a quote`);\n }\n return `$.\"${key}\"`;\n}\n"],"mappings":"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"}
|
package/dist/store/d1.d.cts
CHANGED
|
@@ -1,32 +1,50 @@
|
|
|
1
1
|
import { BundleIR, ConditionIR, WorkflowIR } from "../ir.cjs";
|
|
2
2
|
import { PushDevice } from "../engine/ports.cjs";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { ErasureState } from "./erasure.cjs";
|
|
4
|
+
import { D1DatabaseLike } from "../cloudflare/bindings.cjs";
|
|
5
|
+
import { DeviceInput, EntryInput, EntryOutcome, EventInput, EventWindow, JourneyStore, ProfileProps, RunningInstance, SegmentTriggerRoute, TriggerRoute } from "./index.cjs";
|
|
5
6
|
//#region src/store/d1.d.ts
|
|
6
7
|
/**
|
|
7
|
-
* The
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* The D1 data plane: one database for everything (see ../cloudflare/schema.sql),
|
|
9
|
+
* the content-addressed workflow bodies included — beside the routes that name
|
|
10
|
+
* them, so a deploy is read-your-writes (KV's eventual consistency would let a
|
|
11
|
+
* route name a body that is not readable yet). The in-memory FakeD1 in the
|
|
12
|
+
* tests dispatches on these statements.
|
|
13
|
+
*
|
|
14
|
+
* Every write about a user is one statement conditional on their erasure tombstone not
|
|
15
|
+
* existing (`INSERT ... SELECT ... WHERE NOT EXISTS`): a write that changed nothing is then
|
|
16
|
+
* read back once to tell a refusal (`UserErasedError`) from a plain no-op. D1 has one writer,
|
|
17
|
+
* so a statement is atomic against every other: the runner's admission is one conditional
|
|
18
|
+
* UPDATE, and a tombstone either precedes it (refused) or follows it (the erasure finds the
|
|
19
|
+
* instance admitted) — no lock needed.
|
|
11
20
|
*/
|
|
12
21
|
export declare class D1JourneyStore implements JourneyStore {
|
|
13
22
|
private readonly db;
|
|
14
|
-
|
|
15
|
-
constructor(db: D1DatabaseLike, kv: KVNamespaceLike);
|
|
23
|
+
constructor(db: D1DatabaseLike);
|
|
16
24
|
insertEvent(input: EventInput): Promise<boolean>;
|
|
25
|
+
/**
|
|
26
|
+
* A conditional write changed nothing: on that path only, read why. Throws when the tombstone
|
|
27
|
+
* refused it; returns when the statement had another reason (a duplicate, an entry taken).
|
|
28
|
+
*/
|
|
29
|
+
private refuseIfErased;
|
|
17
30
|
/** The event rows of one user and name, within the window's bounds (both inclusive). */
|
|
18
31
|
private eventWindow;
|
|
19
32
|
countEvents(userId: string, event: string, window?: EventWindow): Promise<number>;
|
|
20
33
|
listEventPayloads(userId: string, event: string, window?: EventWindow): Promise<Record<string, unknown>[]>;
|
|
21
34
|
getProfile(userId: string): Promise<ProfileProps | undefined>;
|
|
22
35
|
mergeProfile(userId: string, props: ProfileProps): Promise<ProfileProps>;
|
|
23
|
-
listUserIds(
|
|
36
|
+
listUserIds(page?: {
|
|
37
|
+
after?: string | undefined;
|
|
38
|
+
limit?: number | undefined;
|
|
39
|
+
}): Promise<string[]>;
|
|
24
40
|
listDevices(userId: string): Promise<PushDevice[]>;
|
|
25
41
|
upsertDevice(device: DeviceInput): Promise<void>;
|
|
26
42
|
requestErasure(userId: string, at: number): Promise<void>;
|
|
27
43
|
pendingErasures(): Promise<string[]>;
|
|
28
|
-
|
|
29
|
-
|
|
44
|
+
erasureState(userId: string): Promise<ErasureState>;
|
|
45
|
+
runningInstances(userId: string): Promise<RunningInstance[]>;
|
|
46
|
+
admitInstance(instanceId: string, userId: string): Promise<boolean>;
|
|
47
|
+
forgetUser(userId: string, at: number): Promise<void>;
|
|
30
48
|
removeDevice(token: string): Promise<string | undefined>;
|
|
31
49
|
unbindDevice(token: string): Promise<string | undefined>;
|
|
32
50
|
listSegments(): Promise<{
|
package/dist/store/d1.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"d1.d.cts","names":[],"sources":["../../src/store/d1.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"d1.d.cts","names":[],"sources":["../../src/store/d1.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;qBAqCa,0BAA0B;mBACR;EAA7B,YAA6B,IAAI;EAE3B,YAAY,OAAO,aAAa;;;;;UAyBxB;;UAKN;EAcF,YAAY,gBAAgB,eAAe,SAAS,cAAc;EASlE,kBACJ,gBACA,eACA,SAAS,cACR,QAAQ;EASL,WAAW,iBAAiB,QAAQ;EAQpC,aAAa,gBAAgB,OAAO,eAAe,QAAQ;EAiC3D,YAAY;IAChB;IACA;MACE;EAsBE,YAAY,iBAAiB,QAAQ;EAQrC,aAAa,QAAQ,cAAc;EAWnC,eAAe,gBAAgB,aAAa;EAO5C,mBAAmB;EAQnB,aAAa,iBAAiB,QAAQ;EAStC,iBAAiB,iBAAiB,QAAQ;EAU1C,cAAc,oBAAoB,iBAAiB;EAcnD,WAAW,gBAAgB,aAAa;EAmBxC,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;EAQrC,cAAc,sBAAsB,QAAQ;EAQ5C,cAAc,QAAQ,WAAW;EA4CjC,gBAAgB,iBAAiB,iBAAiB;EAQlD,iBACJ,iBACA,gBACA,YACA,+BACC;EAmBG,qBAAqB,iBAAiB,iBAAiB;EAQvD,qBAAqB,iBAAiB,gBAAgB,mBAAmB;EAOzE,oBAAoB,iBAAiB,iBAAiB;EAWtD,aAAa,OAAO,aAAa,QAAQ;EAsCzC,YAAY,qBAAqB;EAIjC,eAAe,oBAAoB,iBAAiB;EAOpD,gBAAgB,gBAAgB,gBAAgB;EAUhD,UACJ,gBACA,gBACA,2BACA,aACC;EAiBG,YAAY,iBAAiB"}
|
package/dist/store/d1.d.mts
CHANGED
|
@@ -1,32 +1,50 @@
|
|
|
1
1
|
import { BundleIR, ConditionIR, WorkflowIR } from "../ir.mjs";
|
|
2
2
|
import { PushDevice } from "../engine/ports.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { ErasureState } from "./erasure.mjs";
|
|
4
|
+
import { D1DatabaseLike } from "../cloudflare/bindings.mjs";
|
|
5
|
+
import { DeviceInput, EntryInput, EntryOutcome, EventInput, EventWindow, JourneyStore, ProfileProps, RunningInstance, SegmentTriggerRoute, TriggerRoute } from "./index.mjs";
|
|
5
6
|
//#region src/store/d1.d.ts
|
|
6
7
|
/**
|
|
7
|
-
* The
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* The D1 data plane: one database for everything (see ../cloudflare/schema.sql),
|
|
9
|
+
* the content-addressed workflow bodies included — beside the routes that name
|
|
10
|
+
* them, so a deploy is read-your-writes (KV's eventual consistency would let a
|
|
11
|
+
* route name a body that is not readable yet). The in-memory FakeD1 in the
|
|
12
|
+
* tests dispatches on these statements.
|
|
13
|
+
*
|
|
14
|
+
* Every write about a user is one statement conditional on their erasure tombstone not
|
|
15
|
+
* existing (`INSERT ... SELECT ... WHERE NOT EXISTS`): a write that changed nothing is then
|
|
16
|
+
* read back once to tell a refusal (`UserErasedError`) from a plain no-op. D1 has one writer,
|
|
17
|
+
* so a statement is atomic against every other: the runner's admission is one conditional
|
|
18
|
+
* UPDATE, and a tombstone either precedes it (refused) or follows it (the erasure finds the
|
|
19
|
+
* instance admitted) — no lock needed.
|
|
11
20
|
*/
|
|
12
21
|
export declare class D1JourneyStore implements JourneyStore {
|
|
13
22
|
private readonly db;
|
|
14
|
-
|
|
15
|
-
constructor(db: D1DatabaseLike, kv: KVNamespaceLike);
|
|
23
|
+
constructor(db: D1DatabaseLike);
|
|
16
24
|
insertEvent(input: EventInput): Promise<boolean>;
|
|
25
|
+
/**
|
|
26
|
+
* A conditional write changed nothing: on that path only, read why. Throws when the tombstone
|
|
27
|
+
* refused it; returns when the statement had another reason (a duplicate, an entry taken).
|
|
28
|
+
*/
|
|
29
|
+
private refuseIfErased;
|
|
17
30
|
/** The event rows of one user and name, within the window's bounds (both inclusive). */
|
|
18
31
|
private eventWindow;
|
|
19
32
|
countEvents(userId: string, event: string, window?: EventWindow): Promise<number>;
|
|
20
33
|
listEventPayloads(userId: string, event: string, window?: EventWindow): Promise<Record<string, unknown>[]>;
|
|
21
34
|
getProfile(userId: string): Promise<ProfileProps | undefined>;
|
|
22
35
|
mergeProfile(userId: string, props: ProfileProps): Promise<ProfileProps>;
|
|
23
|
-
listUserIds(
|
|
36
|
+
listUserIds(page?: {
|
|
37
|
+
after?: string | undefined;
|
|
38
|
+
limit?: number | undefined;
|
|
39
|
+
}): Promise<string[]>;
|
|
24
40
|
listDevices(userId: string): Promise<PushDevice[]>;
|
|
25
41
|
upsertDevice(device: DeviceInput): Promise<void>;
|
|
26
42
|
requestErasure(userId: string, at: number): Promise<void>;
|
|
27
43
|
pendingErasures(): Promise<string[]>;
|
|
28
|
-
|
|
29
|
-
|
|
44
|
+
erasureState(userId: string): Promise<ErasureState>;
|
|
45
|
+
runningInstances(userId: string): Promise<RunningInstance[]>;
|
|
46
|
+
admitInstance(instanceId: string, userId: string): Promise<boolean>;
|
|
47
|
+
forgetUser(userId: string, at: number): Promise<void>;
|
|
30
48
|
removeDevice(token: string): Promise<string | undefined>;
|
|
31
49
|
unbindDevice(token: string): Promise<string | undefined>;
|
|
32
50
|
listSegments(): Promise<{
|
package/dist/store/d1.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"d1.d.mts","names":[],"sources":["../../src/store/d1.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"d1.d.mts","names":[],"sources":["../../src/store/d1.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;qBAqCa,0BAA0B;mBACR;EAA7B,YAA6B,IAAI;EAE3B,YAAY,OAAO,aAAa;;;;;UAyBxB;;UAKN;EAcF,YAAY,gBAAgB,eAAe,SAAS,cAAc;EASlE,kBACJ,gBACA,eACA,SAAS,cACR,QAAQ;EASL,WAAW,iBAAiB,QAAQ;EAQpC,aAAa,gBAAgB,OAAO,eAAe,QAAQ;EAiC3D,YAAY;IAChB;IACA;MACE;EAsBE,YAAY,iBAAiB,QAAQ;EAQrC,aAAa,QAAQ,cAAc;EAWnC,eAAe,gBAAgB,aAAa;EAO5C,mBAAmB;EAQnB,aAAa,iBAAiB,QAAQ;EAStC,iBAAiB,iBAAiB,QAAQ;EAU1C,cAAc,oBAAoB,iBAAiB;EAcnD,WAAW,gBAAgB,aAAa;EAmBxC,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;EAQrC,cAAc,sBAAsB,QAAQ;EAQ5C,cAAc,QAAQ,WAAW;EA4CjC,gBAAgB,iBAAiB,iBAAiB;EAQlD,iBACJ,iBACA,gBACA,YACA,+BACC;EAmBG,qBAAqB,iBAAiB,iBAAiB;EAQvD,qBAAqB,iBAAiB,gBAAgB,mBAAmB;EAOzE,oBAAoB,iBAAiB,iBAAiB;EAWtD,aAAa,OAAO,aAAa,QAAQ;EAsCzC,YAAY,qBAAqB;EAIjC,eAAe,oBAAoB,iBAAiB;EAOpD,gBAAgB,gBAAgB,gBAAgB;EAUhD,UACJ,gBACA,gBACA,2BACA,aACC;EAiBG,YAAY,iBAAiB"}
|
package/dist/store/d1.mjs
CHANGED
|
@@ -1,20 +1,41 @@
|
|
|
1
1
|
import { ConditionIR, WorkflowIR } from "../ir.mjs";
|
|
2
|
+
import { UserErasedError } from "./erasure.mjs";
|
|
2
3
|
//#region src/store/d1.ts
|
|
3
4
|
/**
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
* The condition every write about a user carries, as the last bound parameter: no erasure
|
|
6
|
+
* tombstone for the user. Inside the statement, so the check and the write share one snapshot.
|
|
7
|
+
*/
|
|
8
|
+
const NOT_ERASED = "WHERE NOT EXISTS (SELECT 1 FROM erasures WHERE user_id = ?)";
|
|
9
|
+
/**
|
|
10
|
+
* The D1 data plane: one database for everything (see ../cloudflare/schema.sql),
|
|
11
|
+
* the content-addressed workflow bodies included — beside the routes that name
|
|
12
|
+
* them, so a deploy is read-your-writes (KV's eventual consistency would let a
|
|
13
|
+
* route name a body that is not readable yet). The in-memory FakeD1 in the
|
|
14
|
+
* tests dispatches on these statements.
|
|
15
|
+
*
|
|
16
|
+
* Every write about a user is one statement conditional on their erasure tombstone not
|
|
17
|
+
* existing (`INSERT ... SELECT ... WHERE NOT EXISTS`): a write that changed nothing is then
|
|
18
|
+
* read back once to tell a refusal (`UserErasedError`) from a plain no-op. D1 has one writer,
|
|
19
|
+
* so a statement is atomic against every other: the runner's admission is one conditional
|
|
20
|
+
* UPDATE, and a tombstone either precedes it (refused) or follows it (the erasure finds the
|
|
21
|
+
* instance admitted) — no lock needed.
|
|
8
22
|
*/
|
|
9
23
|
var D1JourneyStore = class {
|
|
10
24
|
db;
|
|
11
|
-
|
|
12
|
-
constructor(db, kv) {
|
|
25
|
+
constructor(db) {
|
|
13
26
|
this.db = db;
|
|
14
|
-
this.kv = kv;
|
|
15
27
|
}
|
|
16
28
|
async insertEvent(input) {
|
|
17
|
-
|
|
29
|
+
if (((await this.db.prepare(`INSERT OR IGNORE INTO events (id, user_id, name, ts, payload) SELECT ?, ?, ?, ?, ? ${NOT_ERASED}`).bind(input.id, input.userId, input.event, input.ts, JSON.stringify(input.payload), input.userId).run()).meta?.changes ?? 1) > 0) return true;
|
|
30
|
+
await this.refuseIfErased(input.userId);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A conditional write changed nothing: on that path only, read why. Throws when the tombstone
|
|
35
|
+
* refused it; returns when the statement had another reason (a duplicate, an entry taken).
|
|
36
|
+
*/
|
|
37
|
+
async refuseIfErased(userId) {
|
|
38
|
+
if (await this.erasureState(userId) !== "none") throw new UserErasedError(userId);
|
|
18
39
|
}
|
|
19
40
|
/** The event rows of one user and name, within the window's bounds (both inclusive). */
|
|
20
41
|
eventWindow(userId, event, window) {
|
|
@@ -48,11 +69,26 @@ var D1JourneyStore = class {
|
|
|
48
69
|
}
|
|
49
70
|
async mergeProfile(userId, props) {
|
|
50
71
|
const patch = JSON.stringify(props);
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
72
|
+
const replaced = Object.entries(props).filter(([, value]) => typeof value === "object" && value !== null).map(([key]) => key);
|
|
73
|
+
const stored = replaced.length === 0 ? "profiles.props" : `json_remove(profiles.props, ${replaced.map(() => "?").join(", ")})`;
|
|
74
|
+
const row = await this.db.prepare(`INSERT INTO profiles (user_id, props) SELECT ?, json_patch('{}', ?) ${NOT_ERASED} ON CONFLICT(user_id) DO UPDATE SET props = json_patch(${stored}, ?) RETURNING props`).bind(userId, patch, userId, ...replaced.map(jsonPath), patch).first();
|
|
75
|
+
if (row !== null) return JSON.parse(row.props);
|
|
76
|
+
await this.refuseIfErased(userId);
|
|
77
|
+
return stripNulls(props);
|
|
78
|
+
}
|
|
79
|
+
async listUserIds(page) {
|
|
80
|
+
const params = [];
|
|
81
|
+
let where = "";
|
|
82
|
+
if (page?.after !== void 0) {
|
|
83
|
+
where = " WHERE user_id > ?";
|
|
84
|
+
params.push(page.after);
|
|
85
|
+
}
|
|
86
|
+
let limit = "";
|
|
87
|
+
if (page?.limit !== void 0) {
|
|
88
|
+
limit = " LIMIT ?";
|
|
89
|
+
params.push(page.limit);
|
|
90
|
+
}
|
|
91
|
+
const { results } = await this.db.prepare(`SELECT user_id FROM (SELECT user_id FROM profiles UNION SELECT user_id FROM events)${where} ORDER BY user_id${limit}`).bind(...params).all();
|
|
56
92
|
return results.map((row) => row.user_id);
|
|
57
93
|
}
|
|
58
94
|
async listDevices(userId) {
|
|
@@ -60,30 +96,40 @@ var D1JourneyStore = class {
|
|
|
60
96
|
return results;
|
|
61
97
|
}
|
|
62
98
|
async upsertDevice(device) {
|
|
63
|
-
await this.db.prepare(
|
|
99
|
+
if (((await this.db.prepare(`INSERT INTO devices (token, user_id, platform, ts) SELECT ?, ?, ?, ? ${NOT_ERASED} ON CONFLICT (token) DO UPDATE SET user_id = excluded.user_id, platform = excluded.platform, ts = excluded.ts`).bind(device.token, device.userId, device.platform, device.ts, device.userId).run()).meta?.changes ?? 1) === 0) throw new UserErasedError(device.userId);
|
|
64
100
|
}
|
|
65
101
|
async requestErasure(userId, at) {
|
|
66
102
|
await this.db.prepare("INSERT OR IGNORE INTO erasures (user_id, ts) VALUES (?, ?)").bind(userId, at).run();
|
|
67
103
|
}
|
|
68
104
|
async pendingErasures() {
|
|
69
|
-
const { results } = await this.db.prepare("SELECT user_id FROM erasures ORDER BY ts").bind().all();
|
|
105
|
+
const { results } = await this.db.prepare("SELECT user_id FROM erasures WHERE erased_at IS NULL ORDER BY ts").bind().all();
|
|
70
106
|
return results.map((row) => row.user_id);
|
|
71
107
|
}
|
|
108
|
+
async erasureState(userId) {
|
|
109
|
+
const row = await this.db.prepare("SELECT erased_at FROM erasures WHERE user_id = ?").bind(userId).first();
|
|
110
|
+
if (row === null) return "none";
|
|
111
|
+
return row.erased_at === null ? "erasing" : "erased";
|
|
112
|
+
}
|
|
72
113
|
async runningInstances(userId) {
|
|
73
|
-
const { results } = await this.db.prepare("SELECT instance_id FROM entries WHERE user_id = ? AND status = 'running'").bind(userId).all();
|
|
74
|
-
return results.map((row) =>
|
|
114
|
+
const { results } = await this.db.prepare("SELECT instance_id, admitted_at IS NOT NULL AS admitted FROM entries WHERE user_id = ? AND status = 'running'").bind(userId).all();
|
|
115
|
+
return results.map((row) => ({
|
|
116
|
+
instanceId: row.instance_id,
|
|
117
|
+
admitted: Boolean(row.admitted)
|
|
118
|
+
}));
|
|
75
119
|
}
|
|
76
|
-
async
|
|
77
|
-
await this.db.
|
|
120
|
+
async admitInstance(instanceId, userId) {
|
|
121
|
+
return await this.db.prepare("UPDATE entries SET admitted_at = COALESCE(admitted_at, ?) WHERE instance_id = ? AND user_id = ? AND (status = 'running' OR admitted_at IS NOT NULL) AND NOT EXISTS (SELECT 1 FROM erasures WHERE user_id = ?) RETURNING instance_id").bind(Date.now(), instanceId, userId, userId).first() !== null;
|
|
122
|
+
}
|
|
123
|
+
async forgetUser(userId, at) {
|
|
124
|
+
await this.db.batch([...[
|
|
78
125
|
"entries",
|
|
79
126
|
"subscriptions",
|
|
80
127
|
"segment_starts",
|
|
81
128
|
"segment_members",
|
|
82
129
|
"devices",
|
|
83
130
|
"events",
|
|
84
|
-
"profiles"
|
|
85
|
-
|
|
86
|
-
].map((table) => this.db.prepare(`DELETE FROM ${table} WHERE user_id = ?`).bind(userId)));
|
|
131
|
+
"profiles"
|
|
132
|
+
].map((table) => this.db.prepare(`DELETE FROM ${table} WHERE user_id = ?`).bind(userId)), this.db.prepare("UPDATE erasures SET erased_at = COALESCE(erased_at, ?) WHERE user_id = ?").bind(at, userId)]);
|
|
87
133
|
}
|
|
88
134
|
async removeDevice(token) {
|
|
89
135
|
return (await this.db.prepare("DELETE FROM devices WHERE token = ? RETURNING user_id").bind(token).first())?.user_id ?? void 0;
|
|
@@ -118,11 +164,11 @@ var D1JourneyStore = class {
|
|
|
118
164
|
return results;
|
|
119
165
|
}
|
|
120
166
|
async putWorkflowIR(workflow) {
|
|
121
|
-
await this.
|
|
167
|
+
await this.db.prepare("INSERT OR REPLACE INTO workflow_ir (content_hash, ir) VALUES (?, ?)").bind(workflow.contentHash, JSON.stringify(workflow)).run();
|
|
122
168
|
}
|
|
123
169
|
async getWorkflowIR(contentHash) {
|
|
124
|
-
const
|
|
125
|
-
return
|
|
170
|
+
const row = await this.db.prepare("SELECT ir FROM workflow_ir WHERE content_hash = ?").bind(contentHash).first();
|
|
171
|
+
return row === null ? void 0 : WorkflowIR.parse(JSON.parse(row.ir));
|
|
126
172
|
}
|
|
127
173
|
async replaceRoutes(bundle) {
|
|
128
174
|
await this.db.batch([
|
|
@@ -141,7 +187,8 @@ var D1JourneyStore = class {
|
|
|
141
187
|
return await this.db.prepare("SELECT 1 AS x FROM segment_members WHERE segment = ? AND user_id = ?").bind(segment, userId).first() !== null;
|
|
142
188
|
}
|
|
143
189
|
async addSegmentMember(segment, userId, ts, workflows) {
|
|
144
|
-
await this.db.batch([this.db.prepare(
|
|
190
|
+
const [member] = await this.db.batch([this.db.prepare(`INSERT OR IGNORE INTO segment_members (segment, user_id, ts) SELECT ?, ?, ? ${NOT_ERASED}`).bind(segment, userId, ts, userId), ...workflows.map((workflow) => this.db.prepare(`INSERT OR IGNORE INTO segment_starts (segment, workflow, user_id, ts) SELECT ?, ?, ?, ? ${NOT_ERASED}`).bind(segment, workflow, userId, ts, userId))]);
|
|
191
|
+
if ((member.meta?.changes ?? 1) === 0) await this.refuseIfErased(userId);
|
|
145
192
|
}
|
|
146
193
|
async pendingSegmentStarts(segment, userId) {
|
|
147
194
|
const { results } = await this.db.prepare("SELECT workflow FROM segment_starts WHERE segment = ? AND user_id = ?").bind(segment, userId).all();
|
|
@@ -155,8 +202,10 @@ var D1JourneyStore = class {
|
|
|
155
202
|
}
|
|
156
203
|
async enterJourney(entry) {
|
|
157
204
|
if (entry.execution !== void 0) throw new Error("D1JourneyStore keeps no execution starts: this runtime needs Postgres");
|
|
158
|
-
if (((await this.db.prepare(
|
|
159
|
-
|
|
205
|
+
if (((await this.db.prepare(`INSERT OR IGNORE INTO entries (workflow, user_id, instance_id, hash, status, ts) SELECT ?, ?, ?, ?, ?, ? ${NOT_ERASED}`).bind(entry.workflow, entry.userId, entry.instanceId, entry.hash, "running", entry.ts, entry.userId).run()).meta?.changes ?? 1) !== 0) return "entered";
|
|
206
|
+
if (((await this.db.prepare("UPDATE entries SET instance_id = ?, hash = ?, status = 'running', ts = ?, admitted_at = NULL WHERE workflow = ? AND user_id = ? AND status = 'failed' AND NOT EXISTS (SELECT 1 FROM erasures WHERE user_id = ?)").bind(entry.instanceId, entry.hash, entry.ts, entry.workflow, entry.userId, entry.userId).run()).meta?.changes ?? 0) > 0) return "reclaimed";
|
|
207
|
+
await this.refuseIfErased(entry.userId);
|
|
208
|
+
return null;
|
|
160
209
|
}
|
|
161
210
|
async removeEntry(instanceId) {
|
|
162
211
|
await this.db.prepare("DELETE FROM entries WHERE instance_id = ?").bind(instanceId).run();
|
|
@@ -169,7 +218,9 @@ var D1JourneyStore = class {
|
|
|
169
218
|
return results.map((row) => row.handle);
|
|
170
219
|
}
|
|
171
220
|
async subscribe(handle, userId, events, ts) {
|
|
172
|
-
|
|
221
|
+
let skipped = false;
|
|
222
|
+
for (const event of events) if (((await this.db.prepare(`INSERT OR IGNORE INTO subscriptions (user_id, event, wake_handle, ts) SELECT ?, ?, ?, ? ${NOT_ERASED}`).bind(userId, event, handle, ts, userId).run()).meta?.changes ?? 1) === 0) skipped = true;
|
|
223
|
+
if (skipped) await this.refuseIfErased(userId);
|
|
173
224
|
}
|
|
174
225
|
async unsubscribe(handle) {
|
|
175
226
|
await this.db.prepare("DELETE FROM subscriptions WHERE wake_handle = ?").bind(handle).run();
|
|
@@ -179,6 +230,14 @@ var D1JourneyStore = class {
|
|
|
179
230
|
function stripNulls(props) {
|
|
180
231
|
return Object.fromEntries(Object.entries(props).filter(([, value]) => value !== null));
|
|
181
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* The SQLite JSON path of a top-level key. Quoted, so dots and brackets in the key are literal;
|
|
235
|
+
* a double quote has no escape in SQLite's path syntax, so such a key cannot be addressed.
|
|
236
|
+
*/
|
|
237
|
+
function jsonPath(key) {
|
|
238
|
+
if (key.includes("\"")) throw new Error(`profile property "${key}" cannot hold an object: its name contains a quote`);
|
|
239
|
+
return `$."${key}"`;
|
|
240
|
+
}
|
|
182
241
|
//#endregion
|
|
183
242
|
export { D1JourneyStore };
|
|
184
243
|
|
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 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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"}
|
|
1
|
+
{"version":3,"file":"d1.mjs","names":[],"sources":["../../src/store/d1.ts"],"sourcesContent":["import type { D1DatabaseLike, D1RunResultLike } from '../cloudflare/bindings';\nimport type { PushDevice } from '../engine/ports';\nimport { type BundleIR, ConditionIR, WorkflowIR } from '../ir';\nimport { type ErasureState, UserErasedError } from './erasure';\nimport type {\n DeviceInput,\n EntryInput,\n EntryOutcome,\n EventInput,\n EventWindow,\n JourneyStore,\n ProfileProps,\n RunningInstance,\n SegmentTriggerRoute,\n TriggerRoute,\n} from './index';\n\n/**\n * The condition every write about a user carries, as the last bound parameter: no erasure\n * tombstone for the user. Inside the statement, so the check and the write share one snapshot.\n */\nconst NOT_ERASED = 'WHERE NOT EXISTS (SELECT 1 FROM erasures WHERE user_id = ?)';\n\n/**\n * The D1 data plane: one database for everything (see ../cloudflare/schema.sql),\n * the content-addressed workflow bodies included — beside the routes that name\n * them, so a deploy is read-your-writes (KV's eventual consistency would let a\n * route name a body that is not readable yet). The in-memory FakeD1 in the\n * tests dispatches on these statements.\n *\n * Every write about a user is one statement conditional on their erasure tombstone not\n * existing (`INSERT ... SELECT ... WHERE NOT EXISTS`): a write that changed nothing is then\n * read back once to tell a refusal (`UserErasedError`) from a plain no-op. D1 has one writer,\n * so a statement is atomic against every other: the runner's admission is one conditional\n * UPDATE, and a tombstone either precedes it (refused) or follows it (the erasure finds the\n * instance admitted) — no lock needed.\n */\nexport class D1JourneyStore implements JourneyStore {\n constructor(private readonly db: D1DatabaseLike) {}\n\n async insertEvent(input: EventInput): Promise<boolean> {\n // The key is (user_id, id): the sender's occurrence id, per user\n const written = await this.db\n .prepare(\n `INSERT OR IGNORE INTO events (id, user_id, name, ts, payload) SELECT ?, ?, ?, ?, ? ${NOT_ERASED}`\n )\n .bind(\n input.id,\n input.userId,\n input.event,\n input.ts,\n JSON.stringify(input.payload),\n input.userId\n )\n .run();\n if ((written.meta?.changes ?? 1) > 0) return true;\n // Nothing written: a duplicate, unless the tombstone refused it\n await this.refuseIfErased(input.userId);\n return false;\n }\n\n /**\n * A conditional write changed nothing: on that path only, read why. Throws when the tombstone\n * refused it; returns when the statement had another reason (a duplicate, an entry taken).\n */\n private async refuseIfErased(userId: string): Promise<void> {\n if ((await this.erasureState(userId)) !== 'none') throw new UserErasedError(userId);\n }\n\n /** The event rows of one user and name, within the window's bounds (both inclusive). */\n private eventWindow(userId: string, event: string, window?: EventWindow) {\n const params: unknown[] = [userId, event];\n let where = 'user_id = ? 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 //\n // The merge is top-level only, as Postgres's `||` is: a key whose value is\n // an object replaces the stored value whole instead of being deep-merged\n // into it (json_patch's own behaviour), so the two stores agree. Those keys\n // are removed first, in the same statement; a patch of scalars — all the\n // API admits — needs no removal and is the plain json_patch.\n const patch = JSON.stringify(props);\n // Typed as scalars, but a caller outside the API may still hand an object: replace it whole\n const replaced = Object.entries(props as Record<string, unknown>)\n .filter(([, value]) => typeof value === 'object' && value !== null)\n .map(([key]) => key);\n const stored =\n replaced.length === 0\n ? 'profiles.props'\n : `json_remove(profiles.props, ${replaced.map(() => '?').join(', ')})`;\n // The SELECT keeps a WHERE clause, which is also what disambiguates the upsert for SQLite\n const row = await this.db\n .prepare(\n `INSERT INTO profiles (user_id, props) SELECT ?, json_patch('{}', ?) ${NOT_ERASED} ON CONFLICT(user_id) DO UPDATE SET props = json_patch(${stored}, ?) RETURNING props`\n )\n .bind(userId, patch, userId, ...replaced.map(jsonPath), patch)\n .first<{ props: string }>();\n if (row !== null) return JSON.parse(row.props) as ProfileProps;\n // No row: the tombstone refused the write — or a driver declined to return one, in which\n // case the merge went through and the answer is what was asked for\n await this.refuseIfErased(userId);\n return stripNulls(props);\n }\n\n async listUserIds(page?: {\n after?: string | undefined;\n limit?: number | undefined;\n }): Promise<string[]> {\n // Ascending user_id with a keyset bound, so a sweep can resume past the last id it saw\n const params: unknown[] = [];\n let where = '';\n if (page?.after !== undefined) {\n where = ' WHERE user_id > ?';\n params.push(page.after);\n }\n let limit = '';\n if (page?.limit !== undefined) {\n limit = ' LIMIT ?';\n params.push(page.limit);\n }\n const { results } = await this.db\n .prepare(\n `SELECT user_id FROM (SELECT user_id FROM profiles UNION SELECT user_id FROM events)${where} ORDER BY user_id${limit}`\n )\n .bind(...params)\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 const written = await this.db\n .prepare(\n `INSERT INTO devices (token, user_id, platform, ts) SELECT ?, ?, ?, ? ${NOT_ERASED} 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, device.userId)\n .run();\n // The upsert always changes its row — unless the tombstone refused it\n if ((written.meta?.changes ?? 1) === 0) throw new UserErasedError(device.userId);\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 WHERE erased_at IS NULL ORDER BY ts')\n .bind()\n .all<{ user_id: string }>();\n return results.map((row) => row.user_id);\n }\n\n async erasureState(userId: string): Promise<ErasureState> {\n const row = await this.db\n .prepare('SELECT erased_at FROM erasures WHERE user_id = ?')\n .bind(userId)\n .first<{ erased_at: number | null }>();\n if (row === null) return 'none';\n return row.erased_at === null ? 'erasing' : 'erased';\n }\n\n async runningInstances(userId: string): Promise<RunningInstance[]> {\n const { results } = await this.db\n .prepare(\n \"SELECT instance_id, admitted_at IS NOT NULL AS admitted FROM entries WHERE user_id = ? AND status = 'running'\"\n )\n .bind(userId)\n .all<{ instance_id: string; admitted: number | boolean }>();\n return results.map((row) => ({ instanceId: row.instance_id, admitted: Boolean(row.admitted) }));\n }\n\n async admitInstance(instanceId: string, userId: string): Promise<boolean> {\n // One statement. A first admission needs a running entry; an instance once admitted may\n // replay its finalisation after the entry reached a terminal status, under its own instance\n // id (a reclaim resets admitted_at, so a superseded attempt cannot). The first admission's\n // time is kept, the status never touched, and the tombstone refuses both\n const row = await this.db\n .prepare(\n \"UPDATE entries SET admitted_at = COALESCE(admitted_at, ?) WHERE instance_id = ? AND user_id = ? AND (status = 'running' OR admitted_at IS NOT NULL) AND NOT EXISTS (SELECT 1 FROM erasures WHERE user_id = ?) RETURNING instance_id\"\n )\n .bind(Date.now(), instanceId, userId, userId)\n .first<{ instance_id: string }>();\n return row !== null;\n }\n\n async forgetUser(userId: string, at: number): Promise<void> {\n // One batch: D1 runs it as a transaction. The tombstone stays, marked erased last; a repeat\n // keeps the first erase's time\n await this.db.batch([\n ...[\n 'entries',\n 'subscriptions',\n 'segment_starts',\n 'segment_members',\n 'devices',\n 'events',\n 'profiles',\n ].map((table) => this.db.prepare(`DELETE FROM ${table} WHERE user_id = ?`).bind(userId)),\n this.db\n .prepare('UPDATE erasures SET erased_at = COALESCE(erased_at, ?) WHERE user_id = ?')\n .bind(at, 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 // Content-addressed: a hash already there names the same body, so replacing is harmless\n await this.db\n .prepare('INSERT OR REPLACE INTO workflow_ir (content_hash, ir) VALUES (?, ?)')\n .bind(workflow.contentHash, JSON.stringify(workflow))\n .run();\n }\n\n async getWorkflowIR(contentHash: string): Promise<WorkflowIR | undefined> {\n const row = await this.db\n .prepare('SELECT ir FROM workflow_ir WHERE content_hash = ?')\n .bind(contentHash)\n .first<{ ir: string }>();\n return row === null ? undefined : WorkflowIR.parse(JSON.parse(row.ir));\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 const [member] = (await this.db.batch([\n this.db\n .prepare(\n `INSERT OR IGNORE INTO segment_members (segment, user_id, ts) SELECT ?, ?, ? ${NOT_ERASED}`\n )\n .bind(segment, userId, ts, userId),\n ...workflows.map((workflow) =>\n this.db\n .prepare(\n `INSERT OR IGNORE INTO segment_starts (segment, workflow, user_id, ts) SELECT ?, ?, ?, ? ${NOT_ERASED}`\n )\n .bind(segment, workflow, userId, ts, userId)\n ),\n ])) as D1RunResultLike[];\n // Already a member, or refused: either way the starts wrote nothing new\n if ((member.meta?.changes ?? 1) === 0) await this.refuseIfErased(userId);\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) SELECT ?, ?, ?, ?, ?, ? ${NOT_ERASED}`\n )\n .bind(\n entry.workflow,\n entry.userId,\n entry.instanceId,\n entry.hash,\n 'running',\n entry.ts,\n entry.userId\n )\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. The reclaim is a new attempt: it starts\n // unadmitted, even under the same instance id, so the previous attempt's\n // admission is not inherited.\n const claimed = await this.db\n .prepare(\n \"UPDATE entries SET instance_id = ?, hash = ?, status = 'running', ts = ?, admitted_at = NULL WHERE workflow = ? AND user_id = ? AND status = 'failed' AND NOT EXISTS (SELECT 1 FROM erasures WHERE user_id = ?)\"\n )\n .bind(entry.instanceId, entry.hash, entry.ts, entry.workflow, entry.userId, entry.userId)\n .run();\n if ((claimed.meta?.changes ?? 0) > 0) return 'reclaimed';\n // Taken by a live instance, or refused by the tombstone: tell them apart\n await this.refuseIfErased(entry.userId);\n return 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 let skipped = false;\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 const written = await this.db\n .prepare(\n `INSERT OR IGNORE INTO subscriptions (user_id, event, wake_handle, ts) SELECT ?, ?, ?, ? ${NOT_ERASED}`\n )\n .bind(userId, event, handle, ts, userId)\n .run();\n if ((written.meta?.changes ?? 1) === 0) skipped = true;\n }\n // A registration already there, or refused: one read tells which\n if (skipped) await this.refuseIfErased(userId);\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\n/**\n * The SQLite JSON path of a top-level key. Quoted, so dots and brackets in the key are literal;\n * a double quote has no escape in SQLite's path syntax, so such a key cannot be addressed.\n */\nfunction jsonPath(key: string): string {\n if (key.includes('\"')) {\n throw new Error(`profile property \"${key}\" cannot hold an object: its name contains a quote`);\n }\n return `$.\"${key}\"`;\n}\n"],"mappings":"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"}
|