@shware/workflow 4.1.1 → 5.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 +16 -38
- package/dist/aws/index.cjs +2 -3
- package/dist/aws/index.cjs.map +1 -1
- package/dist/aws/index.d.cts +1 -1
- package/dist/aws/index.d.mts +1 -1
- package/dist/aws/index.mjs +2 -3
- package/dist/aws/index.mjs.map +1 -1
- package/dist/aws/instances.cjs +1 -1
- package/dist/aws/instances.cjs.map +1 -1
- package/dist/aws/instances.d.cts +1 -1
- package/dist/aws/instances.d.mts +1 -1
- package/dist/aws/instances.mjs +1 -1
- package/dist/aws/instances.mjs.map +1 -1
- package/dist/cloudflare/bindings.cjs.map +1 -1
- package/dist/cloudflare/bindings.d.cts +0 -19
- package/dist/cloudflare/bindings.d.cts.map +1 -1
- package/dist/cloudflare/bindings.d.mts +0 -19
- package/dist/cloudflare/bindings.d.mts.map +1 -1
- package/dist/cloudflare/bindings.mjs.map +1 -1
- package/dist/cloudflare/index.cjs +0 -5
- package/dist/cloudflare/index.d.cts +4 -6
- package/dist/cloudflare/index.d.mts +4 -6
- package/dist/cloudflare/index.mjs +2 -4
- package/dist/cloudflare/runner.cjs +3 -4
- package/dist/cloudflare/runner.cjs.map +1 -1
- package/dist/cloudflare/runner.d.cts +3 -3
- package/dist/cloudflare/runner.d.cts.map +1 -1
- package/dist/cloudflare/runner.d.mts +3 -3
- package/dist/cloudflare/runner.d.mts.map +1 -1
- package/dist/cloudflare/runner.mjs +3 -4
- package/dist/cloudflare/runner.mjs.map +1 -1
- package/dist/config.cjs.map +1 -1
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +1 -1
- package/dist/config.mjs.map +1 -1
- package/dist/engine/condition.cjs +2 -2
- package/dist/engine/condition.cjs.map +1 -1
- package/dist/engine/condition.d.cts +1 -1
- package/dist/engine/condition.d.mts +1 -1
- package/dist/engine/condition.mjs +2 -2
- package/dist/engine/condition.mjs.map +1 -1
- package/dist/engine/ports.d.cts +1 -1
- package/dist/engine/ports.d.mts +1 -1
- package/dist/host/channels.d.cts +1 -1
- package/dist/host/channels.d.mts +1 -1
- package/dist/host/execute.d.cts +1 -1
- package/dist/host/execute.d.mts +1 -1
- package/dist/host/http.cjs +3 -4
- package/dist/host/http.cjs.map +1 -1
- package/dist/host/http.d.cts +2 -2
- package/dist/host/http.d.cts.map +1 -1
- package/dist/host/http.d.mts +2 -2
- package/dist/host/http.d.mts.map +1 -1
- package/dist/host/http.mjs +3 -4
- package/dist/host/http.mjs.map +1 -1
- package/dist/host/ingest.cjs +11 -22
- package/dist/host/ingest.cjs.map +1 -1
- package/dist/host/ingest.d.cts +8 -13
- package/dist/host/ingest.d.cts.map +1 -1
- package/dist/host/ingest.d.mts +8 -13
- package/dist/host/ingest.d.mts.map +1 -1
- package/dist/host/ingest.mjs +12 -22
- package/dist/host/ingest.mjs.map +1 -1
- package/dist/host/journey-host.cjs +2 -4
- package/dist/host/journey-host.cjs.map +1 -1
- package/dist/host/journey-host.d.cts +6 -6
- package/dist/host/journey-host.d.cts.map +1 -1
- package/dist/host/journey-host.d.mts +6 -6
- package/dist/host/journey-host.d.mts.map +1 -1
- package/dist/host/journey-host.mjs +2 -4
- package/dist/host/journey-host.mjs.map +1 -1
- package/dist/host/platform.cjs.map +1 -1
- package/dist/host/platform.d.cts +1 -7
- package/dist/host/platform.d.cts.map +1 -1
- package/dist/host/platform.d.mts +1 -7
- package/dist/host/platform.d.mts.map +1 -1
- package/dist/host/platform.mjs.map +1 -1
- package/dist/store/facts.cjs +1 -1
- package/dist/store/facts.cjs.map +1 -1
- package/dist/store/facts.mjs +1 -1
- package/dist/store/facts.mjs.map +1 -1
- package/dist/store/index.cjs +0 -2
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +21 -26
- package/dist/store/index.d.cts.map +1 -1
- package/dist/store/index.d.mts +21 -26
- package/dist/store/index.d.mts.map +1 -1
- package/dist/store/index.mjs +1 -2
- package/dist/store/index.mjs.map +1 -1
- package/dist/store/postgres.cjs +2 -3
- package/dist/store/postgres.cjs.map +1 -1
- package/dist/store/postgres.d.cts +2 -3
- package/dist/store/postgres.d.cts.map +1 -1
- package/dist/store/postgres.d.mts +2 -3
- package/dist/store/postgres.d.mts.map +1 -1
- package/dist/store/postgres.mjs +2 -3
- package/dist/store/postgres.mjs.map +1 -1
- package/dist/transports/cloudflare-email.d.cts +1 -1
- package/dist/transports/cloudflare-email.d.mts +1 -1
- package/package.json +1 -3
- package/dist/cloudflare/d1-env.cjs +0 -17
- package/dist/cloudflare/d1-env.cjs.map +0 -1
- package/dist/cloudflare/d1-env.d.cts +0 -12
- package/dist/cloudflare/d1-env.d.cts.map +0 -1
- package/dist/cloudflare/d1-env.d.mts +0 -12
- package/dist/cloudflare/d1-env.d.mts.map +0 -1
- package/dist/cloudflare/d1-env.mjs +0 -16
- package/dist/cloudflare/d1-env.mjs.map +0 -1
- package/dist/cloudflare/schema.sql +0 -148
- package/dist/store/d1.cjs +0 -245
- package/dist/store/d1.cjs.map +0 -1
- package/dist/store/d1.d.cts +0 -73
- package/dist/store/d1.d.cts.map +0 -1
- package/dist/store/d1.d.mts +0 -73
- package/dist/store/d1.d.mts.map +0 -1
- package/dist/store/d1.mjs +0 -244
- package/dist/store/d1.mjs.map +0 -1
- package/src/cloudflare/schema.sql +0 -148
package/dist/store/d1.cjs
DELETED
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_ir = require("../ir.cjs");
|
|
3
|
-
const require_store_erasure = require("./erasure.cjs");
|
|
4
|
-
//#region src/store/d1.ts
|
|
5
|
-
/**
|
|
6
|
-
* The condition every write about a user carries, as the last bound parameter: no erasure
|
|
7
|
-
* tombstone for the user. Inside the statement, so the check and the write share one snapshot.
|
|
8
|
-
*/
|
|
9
|
-
const NOT_ERASED = "WHERE NOT EXISTS (SELECT 1 FROM erasures WHERE user_id = ?)";
|
|
10
|
-
/**
|
|
11
|
-
* The D1 data plane: one database for everything (see ../cloudflare/schema.sql),
|
|
12
|
-
* the content-addressed workflow bodies included — beside the routes that name
|
|
13
|
-
* them, so a deploy is read-your-writes (KV's eventual consistency would let a
|
|
14
|
-
* route name a body that is not readable yet). The in-memory FakeD1 in the
|
|
15
|
-
* tests dispatches on these statements.
|
|
16
|
-
*
|
|
17
|
-
* Every write about a user is one statement conditional on their erasure tombstone not
|
|
18
|
-
* existing (`INSERT ... SELECT ... WHERE NOT EXISTS`): a write that changed nothing is then
|
|
19
|
-
* read back once to tell a refusal (`UserErasedError`) from a plain no-op. D1 has one writer,
|
|
20
|
-
* so a statement is atomic against every other: the runner's admission is one conditional
|
|
21
|
-
* UPDATE, and a tombstone either precedes it (refused) or follows it (the erasure finds the
|
|
22
|
-
* instance admitted) — no lock needed.
|
|
23
|
-
*/
|
|
24
|
-
var D1JourneyStore = class {
|
|
25
|
-
db;
|
|
26
|
-
constructor(db) {
|
|
27
|
-
this.db = db;
|
|
28
|
-
}
|
|
29
|
-
async insertEvent(input) {
|
|
30
|
-
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;
|
|
31
|
-
await this.refuseIfErased(input.userId);
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* A conditional write changed nothing: on that path only, read why. Throws when the tombstone
|
|
36
|
-
* refused it; returns when the statement had another reason (a duplicate, an entry taken).
|
|
37
|
-
*/
|
|
38
|
-
async refuseIfErased(userId) {
|
|
39
|
-
if (await this.erasureState(userId) !== "none") throw new require_store_erasure.UserErasedError(userId);
|
|
40
|
-
}
|
|
41
|
-
/** The event rows of one user and name, within the window's bounds (both inclusive). */
|
|
42
|
-
eventWindow(userId, event, window) {
|
|
43
|
-
const params = [userId, event];
|
|
44
|
-
let where = "user_id = ? AND name = ?";
|
|
45
|
-
if (window?.sinceMs !== void 0) {
|
|
46
|
-
where += " AND ts >= ?";
|
|
47
|
-
params.push(window.sinceMs);
|
|
48
|
-
}
|
|
49
|
-
if (window?.untilMs !== void 0) {
|
|
50
|
-
where += " AND ts <= ?";
|
|
51
|
-
params.push(window.untilMs);
|
|
52
|
-
}
|
|
53
|
-
return {
|
|
54
|
-
where,
|
|
55
|
-
params
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
async countEvents(userId, event, window) {
|
|
59
|
-
const { where, params } = this.eventWindow(userId, event, window);
|
|
60
|
-
return (await this.db.prepare(`SELECT COUNT(*) AS c FROM events WHERE ${where}`).bind(...params).first())?.c ?? 0;
|
|
61
|
-
}
|
|
62
|
-
async listEventPayloads(userId, event, window) {
|
|
63
|
-
const { where, params } = this.eventWindow(userId, event, window);
|
|
64
|
-
const { results } = await this.db.prepare(`SELECT payload FROM events WHERE ${where}`).bind(...params).all();
|
|
65
|
-
return results.map((row) => JSON.parse(row.payload));
|
|
66
|
-
}
|
|
67
|
-
async getProfile(userId) {
|
|
68
|
-
const row = await this.db.prepare("SELECT props FROM profiles WHERE user_id = ?").bind(userId).first();
|
|
69
|
-
return row ? JSON.parse(row.props) : void 0;
|
|
70
|
-
}
|
|
71
|
-
async mergeProfile(userId, props) {
|
|
72
|
-
const patch = JSON.stringify(props);
|
|
73
|
-
const replaced = Object.entries(props).filter(([, value]) => typeof value === "object" && value !== null).map(([key]) => key);
|
|
74
|
-
const stored = replaced.length === 0 ? "profiles.props" : `json_remove(profiles.props, ${replaced.map(() => "?").join(", ")})`;
|
|
75
|
-
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();
|
|
76
|
-
if (row !== null) return JSON.parse(row.props);
|
|
77
|
-
await this.refuseIfErased(userId);
|
|
78
|
-
return stripNulls(props);
|
|
79
|
-
}
|
|
80
|
-
async listUserIds(page) {
|
|
81
|
-
const params = [];
|
|
82
|
-
let where = "";
|
|
83
|
-
if (page?.after !== void 0) {
|
|
84
|
-
where = " WHERE user_id > ?";
|
|
85
|
-
params.push(page.after);
|
|
86
|
-
}
|
|
87
|
-
let limit = "";
|
|
88
|
-
if (page?.limit !== void 0) {
|
|
89
|
-
limit = " LIMIT ?";
|
|
90
|
-
params.push(page.limit);
|
|
91
|
-
}
|
|
92
|
-
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();
|
|
93
|
-
return results.map((row) => row.user_id);
|
|
94
|
-
}
|
|
95
|
-
async listDevices(userId) {
|
|
96
|
-
const { results } = await this.db.prepare("SELECT token, platform FROM devices WHERE user_id = ? ORDER BY ts DESC").bind(userId).all();
|
|
97
|
-
return results;
|
|
98
|
-
}
|
|
99
|
-
async upsertDevice(device) {
|
|
100
|
-
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 require_store_erasure.UserErasedError(device.userId);
|
|
101
|
-
}
|
|
102
|
-
async requestErasure(userId, at) {
|
|
103
|
-
await this.db.prepare("INSERT OR IGNORE INTO erasures (user_id, ts) VALUES (?, ?)").bind(userId, at).run();
|
|
104
|
-
}
|
|
105
|
-
async pendingErasures() {
|
|
106
|
-
const { results } = await this.db.prepare("SELECT user_id FROM erasures WHERE erased_at IS NULL ORDER BY ts").bind().all();
|
|
107
|
-
return results.map((row) => row.user_id);
|
|
108
|
-
}
|
|
109
|
-
async erasureState(userId) {
|
|
110
|
-
const row = await this.db.prepare("SELECT erased_at FROM erasures WHERE user_id = ?").bind(userId).first();
|
|
111
|
-
if (row === null) return "none";
|
|
112
|
-
return row.erased_at === null ? "erasing" : "erased";
|
|
113
|
-
}
|
|
114
|
-
async runningInstances(userId) {
|
|
115
|
-
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();
|
|
116
|
-
return results.map((row) => ({
|
|
117
|
-
instanceId: row.instance_id,
|
|
118
|
-
admitted: Boolean(row.admitted)
|
|
119
|
-
}));
|
|
120
|
-
}
|
|
121
|
-
async admitInstance(instanceId, userId) {
|
|
122
|
-
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;
|
|
123
|
-
}
|
|
124
|
-
async forgetUser(userId, at) {
|
|
125
|
-
await this.db.batch([...[
|
|
126
|
-
"entries",
|
|
127
|
-
"subscriptions",
|
|
128
|
-
"segment_starts",
|
|
129
|
-
"segment_members",
|
|
130
|
-
"devices",
|
|
131
|
-
"events",
|
|
132
|
-
"profiles"
|
|
133
|
-
].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)]);
|
|
134
|
-
}
|
|
135
|
-
async removeDevice(token) {
|
|
136
|
-
return (await this.db.prepare("DELETE FROM devices WHERE token = ? RETURNING user_id").bind(token).first())?.user_id ?? void 0;
|
|
137
|
-
}
|
|
138
|
-
async unbindDevice(token) {
|
|
139
|
-
const row = await this.db.prepare("SELECT user_id FROM devices WHERE token = ?").bind(token).first();
|
|
140
|
-
await this.db.prepare("UPDATE devices SET user_id = NULL WHERE token = ?").bind(token).run();
|
|
141
|
-
return row?.user_id ?? void 0;
|
|
142
|
-
}
|
|
143
|
-
async listSegments() {
|
|
144
|
-
const { results } = await this.db.prepare("SELECT id, condition FROM segments ORDER BY id").bind().all();
|
|
145
|
-
return results.map((row) => ({
|
|
146
|
-
id: row.id,
|
|
147
|
-
condition: require_ir.ConditionIR.parse(JSON.parse(row.condition))
|
|
148
|
-
}));
|
|
149
|
-
}
|
|
150
|
-
async getSegmentCondition(id) {
|
|
151
|
-
const row = await this.db.prepare("SELECT condition FROM segments WHERE id = ?").bind(id).first();
|
|
152
|
-
return row ? require_ir.ConditionIR.parse(JSON.parse(row.condition)) : void 0;
|
|
153
|
-
}
|
|
154
|
-
async findTriggers(event) {
|
|
155
|
-
const { results } = await this.db.prepare("SELECT workflow, hash, \"where\" AS whereClause, filter FROM triggers WHERE event = ?").bind(event).all();
|
|
156
|
-
return results.map((row) => ({
|
|
157
|
-
workflow: row.workflow,
|
|
158
|
-
hash: row.hash,
|
|
159
|
-
where: row.whereClause === null ? null : require_ir.ConditionIR.parse(JSON.parse(row.whereClause)),
|
|
160
|
-
filter: row.filter === null ? null : require_ir.ConditionIR.parse(JSON.parse(row.filter))
|
|
161
|
-
}));
|
|
162
|
-
}
|
|
163
|
-
async listSegmentTriggers() {
|
|
164
|
-
const { results } = await this.db.prepare("SELECT workflow, hash, segment FROM segment_triggers").all();
|
|
165
|
-
return results;
|
|
166
|
-
}
|
|
167
|
-
async putWorkflowIR(workflow) {
|
|
168
|
-
await this.db.prepare("INSERT OR REPLACE INTO workflow_ir (content_hash, ir) VALUES (?, ?)").bind(workflow.contentHash, JSON.stringify(workflow)).run();
|
|
169
|
-
}
|
|
170
|
-
async getWorkflowIR(contentHash) {
|
|
171
|
-
const row = await this.db.prepare("SELECT ir FROM workflow_ir WHERE content_hash = ?").bind(contentHash).first();
|
|
172
|
-
return row === null ? void 0 : require_ir.WorkflowIR.parse(JSON.parse(row.ir));
|
|
173
|
-
}
|
|
174
|
-
async replaceRoutes(bundle) {
|
|
175
|
-
await this.db.batch([
|
|
176
|
-
this.db.prepare("DELETE FROM triggers"),
|
|
177
|
-
this.db.prepare("DELETE FROM segment_triggers"),
|
|
178
|
-
this.db.prepare("DELETE FROM segments"),
|
|
179
|
-
...bundle.segments.map((segment) => this.db.prepare("INSERT INTO segments (id, condition, hash) VALUES (?, ?, ?)").bind(segment.id, JSON.stringify(segment.condition), segment.contentHash)),
|
|
180
|
-
...bundle.workflows.flatMap((workflow) => {
|
|
181
|
-
if (workflow.trigger.type === "event") return [this.db.prepare("INSERT INTO triggers (workflow, hash, event, \"where\", filter) VALUES (?, ?, ?, ?, ?)").bind(workflow.id, workflow.contentHash, workflow.trigger.event, workflow.trigger.where !== void 0 ? JSON.stringify(workflow.trigger.where) : null, workflow.trigger.filter !== void 0 ? JSON.stringify(workflow.trigger.filter) : null)];
|
|
182
|
-
if (workflow.trigger.type === "segment") return [this.db.prepare("INSERT INTO segment_triggers (workflow, hash, segment) VALUES (?, ?, ?)").bind(workflow.id, workflow.contentHash, workflow.trigger.segment)];
|
|
183
|
-
return [];
|
|
184
|
-
})
|
|
185
|
-
]);
|
|
186
|
-
}
|
|
187
|
-
async isSegmentMember(segment, userId) {
|
|
188
|
-
return await this.db.prepare("SELECT 1 AS x FROM segment_members WHERE segment = ? AND user_id = ?").bind(segment, userId).first() !== null;
|
|
189
|
-
}
|
|
190
|
-
async addSegmentMember(segment, userId, ts, workflows) {
|
|
191
|
-
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))]);
|
|
192
|
-
if ((member.meta?.changes ?? 1) === 0) await this.refuseIfErased(userId);
|
|
193
|
-
}
|
|
194
|
-
async pendingSegmentStarts(segment, userId) {
|
|
195
|
-
const { results } = await this.db.prepare("SELECT workflow FROM segment_starts WHERE segment = ? AND user_id = ?").bind(segment, userId).all();
|
|
196
|
-
return results.map((row) => row.workflow);
|
|
197
|
-
}
|
|
198
|
-
async completeSegmentStart(segment, userId, workflow) {
|
|
199
|
-
await this.db.prepare("DELETE FROM segment_starts WHERE segment = ? AND user_id = ? AND workflow = ?").bind(segment, userId, workflow).run();
|
|
200
|
-
}
|
|
201
|
-
async removeSegmentMember(segment, userId) {
|
|
202
|
-
await this.db.batch([this.db.prepare("DELETE FROM segment_starts WHERE segment = ? AND user_id = ?").bind(segment, userId), this.db.prepare("DELETE FROM segment_members WHERE segment = ? AND user_id = ?").bind(segment, userId)]);
|
|
203
|
-
}
|
|
204
|
-
async enterJourney(entry) {
|
|
205
|
-
if (entry.execution !== void 0) throw new Error("D1JourneyStore keeps no execution starts: this runtime needs Postgres");
|
|
206
|
-
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";
|
|
207
|
-
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";
|
|
208
|
-
await this.refuseIfErased(entry.userId);
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
211
|
-
async removeEntry(instanceId) {
|
|
212
|
-
await this.db.prepare("DELETE FROM entries WHERE instance_id = ?").bind(instanceId).run();
|
|
213
|
-
}
|
|
214
|
-
async setEntryStatus(instanceId, status) {
|
|
215
|
-
await this.db.prepare("UPDATE entries SET status = ? WHERE instance_id = ?").bind(status, instanceId).run();
|
|
216
|
-
}
|
|
217
|
-
async findWakeHandles(userId, event) {
|
|
218
|
-
const { results } = await this.db.prepare("SELECT DISTINCT wake_handle AS handle FROM subscriptions WHERE user_id = ? AND event = ?").bind(userId, event).all();
|
|
219
|
-
return results.map((row) => row.handle);
|
|
220
|
-
}
|
|
221
|
-
async subscribe(handle, userId, events, ts) {
|
|
222
|
-
let skipped = false;
|
|
223
|
-
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;
|
|
224
|
-
if (skipped) await this.refuseIfErased(userId);
|
|
225
|
-
}
|
|
226
|
-
async unsubscribe(handle) {
|
|
227
|
-
await this.db.prepare("DELETE FROM subscriptions WHERE wake_handle = ?").bind(handle).run();
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
/** What a merge-patch of `props` into an empty profile yields: the patch minus its nulls. */
|
|
231
|
-
function stripNulls(props) {
|
|
232
|
-
return Object.fromEntries(Object.entries(props).filter(([, value]) => value !== null));
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* The SQLite JSON path of a top-level key. Quoted, so dots and brackets in the key are literal;
|
|
236
|
-
* a double quote has no escape in SQLite's path syntax, so such a key cannot be addressed.
|
|
237
|
-
*/
|
|
238
|
-
function jsonPath(key) {
|
|
239
|
-
if (key.includes("\"")) throw new Error(`profile property "${key}" cannot hold an object: its name contains a quote`);
|
|
240
|
-
return `$."${key}"`;
|
|
241
|
-
}
|
|
242
|
-
//#endregion
|
|
243
|
-
exports.D1JourneyStore = D1JourneyStore;
|
|
244
|
-
|
|
245
|
-
//# sourceMappingURL=d1.cjs.map
|
package/dist/store/d1.cjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { BundleIR, ConditionIR, WorkflowIR } from "../ir.cjs";
|
|
2
|
-
import { PushDevice } from "../engine/ports.cjs";
|
|
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";
|
|
6
|
-
//#region src/store/d1.d.ts
|
|
7
|
-
/**
|
|
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.
|
|
20
|
-
*/
|
|
21
|
-
export declare class D1JourneyStore implements JourneyStore {
|
|
22
|
-
private readonly db;
|
|
23
|
-
constructor(db: D1DatabaseLike);
|
|
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;
|
|
30
|
-
/** The event rows of one user and name, within the window's bounds (both inclusive). */
|
|
31
|
-
private eventWindow;
|
|
32
|
-
countEvents(userId: string, event: string, window?: EventWindow): Promise<number>;
|
|
33
|
-
listEventPayloads(userId: string, event: string, window?: EventWindow): Promise<Record<string, unknown>[]>;
|
|
34
|
-
getProfile(userId: string): Promise<ProfileProps | undefined>;
|
|
35
|
-
mergeProfile(userId: string, props: ProfileProps): Promise<ProfileProps>;
|
|
36
|
-
listUserIds(page?: {
|
|
37
|
-
after?: string | undefined;
|
|
38
|
-
limit?: number | undefined;
|
|
39
|
-
}): Promise<string[]>;
|
|
40
|
-
listDevices(userId: string): Promise<PushDevice[]>;
|
|
41
|
-
upsertDevice(device: DeviceInput): Promise<void>;
|
|
42
|
-
requestErasure(userId: string, at: number): Promise<void>;
|
|
43
|
-
pendingErasures(): Promise<string[]>;
|
|
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>;
|
|
48
|
-
removeDevice(token: string): Promise<string | undefined>;
|
|
49
|
-
unbindDevice(token: string): Promise<string | undefined>;
|
|
50
|
-
listSegments(): Promise<{
|
|
51
|
-
id: string;
|
|
52
|
-
condition: ConditionIR;
|
|
53
|
-
}[]>;
|
|
54
|
-
getSegmentCondition(id: string): Promise<ConditionIR | undefined>;
|
|
55
|
-
findTriggers(event: string): Promise<TriggerRoute[]>;
|
|
56
|
-
listSegmentTriggers(): Promise<SegmentTriggerRoute[]>;
|
|
57
|
-
putWorkflowIR(workflow: WorkflowIR): Promise<void>;
|
|
58
|
-
getWorkflowIR(contentHash: string): Promise<WorkflowIR | undefined>;
|
|
59
|
-
replaceRoutes(bundle: BundleIR): Promise<void>;
|
|
60
|
-
isSegmentMember(segment: string, userId: string): Promise<boolean>;
|
|
61
|
-
addSegmentMember(segment: string, userId: string, ts: number, workflows: readonly string[]): Promise<void>;
|
|
62
|
-
pendingSegmentStarts(segment: string, userId: string): Promise<string[]>;
|
|
63
|
-
completeSegmentStart(segment: string, userId: string, workflow: string): Promise<void>;
|
|
64
|
-
removeSegmentMember(segment: string, userId: string): Promise<void>;
|
|
65
|
-
enterJourney(entry: EntryInput): Promise<EntryOutcome>;
|
|
66
|
-
removeEntry(instanceId: string): Promise<void>;
|
|
67
|
-
setEntryStatus(instanceId: string, status: string): Promise<void>;
|
|
68
|
-
findWakeHandles(userId: string, event: string): Promise<string[]>;
|
|
69
|
-
subscribe(handle: string, userId: string, events: readonly string[], ts: number): Promise<void>;
|
|
70
|
-
unsubscribe(handle: string): Promise<void>;
|
|
71
|
-
}
|
|
72
|
-
//#endregion
|
|
73
|
-
//# sourceMappingURL=d1.d.cts.map
|
package/dist/store/d1.d.cts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { BundleIR, ConditionIR, WorkflowIR } from "../ir.mjs";
|
|
2
|
-
import { PushDevice } from "../engine/ports.mjs";
|
|
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";
|
|
6
|
-
//#region src/store/d1.d.ts
|
|
7
|
-
/**
|
|
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.
|
|
20
|
-
*/
|
|
21
|
-
export declare class D1JourneyStore implements JourneyStore {
|
|
22
|
-
private readonly db;
|
|
23
|
-
constructor(db: D1DatabaseLike);
|
|
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;
|
|
30
|
-
/** The event rows of one user and name, within the window's bounds (both inclusive). */
|
|
31
|
-
private eventWindow;
|
|
32
|
-
countEvents(userId: string, event: string, window?: EventWindow): Promise<number>;
|
|
33
|
-
listEventPayloads(userId: string, event: string, window?: EventWindow): Promise<Record<string, unknown>[]>;
|
|
34
|
-
getProfile(userId: string): Promise<ProfileProps | undefined>;
|
|
35
|
-
mergeProfile(userId: string, props: ProfileProps): Promise<ProfileProps>;
|
|
36
|
-
listUserIds(page?: {
|
|
37
|
-
after?: string | undefined;
|
|
38
|
-
limit?: number | undefined;
|
|
39
|
-
}): Promise<string[]>;
|
|
40
|
-
listDevices(userId: string): Promise<PushDevice[]>;
|
|
41
|
-
upsertDevice(device: DeviceInput): Promise<void>;
|
|
42
|
-
requestErasure(userId: string, at: number): Promise<void>;
|
|
43
|
-
pendingErasures(): Promise<string[]>;
|
|
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>;
|
|
48
|
-
removeDevice(token: string): Promise<string | undefined>;
|
|
49
|
-
unbindDevice(token: string): Promise<string | undefined>;
|
|
50
|
-
listSegments(): Promise<{
|
|
51
|
-
id: string;
|
|
52
|
-
condition: ConditionIR;
|
|
53
|
-
}[]>;
|
|
54
|
-
getSegmentCondition(id: string): Promise<ConditionIR | undefined>;
|
|
55
|
-
findTriggers(event: string): Promise<TriggerRoute[]>;
|
|
56
|
-
listSegmentTriggers(): Promise<SegmentTriggerRoute[]>;
|
|
57
|
-
putWorkflowIR(workflow: WorkflowIR): Promise<void>;
|
|
58
|
-
getWorkflowIR(contentHash: string): Promise<WorkflowIR | undefined>;
|
|
59
|
-
replaceRoutes(bundle: BundleIR): Promise<void>;
|
|
60
|
-
isSegmentMember(segment: string, userId: string): Promise<boolean>;
|
|
61
|
-
addSegmentMember(segment: string, userId: string, ts: number, workflows: readonly string[]): Promise<void>;
|
|
62
|
-
pendingSegmentStarts(segment: string, userId: string): Promise<string[]>;
|
|
63
|
-
completeSegmentStart(segment: string, userId: string, workflow: string): Promise<void>;
|
|
64
|
-
removeSegmentMember(segment: string, userId: string): Promise<void>;
|
|
65
|
-
enterJourney(entry: EntryInput): Promise<EntryOutcome>;
|
|
66
|
-
removeEntry(instanceId: string): Promise<void>;
|
|
67
|
-
setEntryStatus(instanceId: string, status: string): Promise<void>;
|
|
68
|
-
findWakeHandles(userId: string, event: string): Promise<string[]>;
|
|
69
|
-
subscribe(handle: string, userId: string, events: readonly string[], ts: number): Promise<void>;
|
|
70
|
-
unsubscribe(handle: string): Promise<void>;
|
|
71
|
-
}
|
|
72
|
-
//#endregion
|
|
73
|
-
//# sourceMappingURL=d1.d.mts.map
|
package/dist/store/d1.d.mts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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"}
|