@yieldstar/sqlite-runtime 0.5.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/dist/bun.d.ts +13 -0
- package/dist/bun.d.ts.map +1 -0
- package/dist/bun.js +43 -0
- package/dist/dao/step-response-dao.d.ts +19 -0
- package/dist/dao/step-response-dao.d.ts.map +1 -0
- package/dist/dao/task-queue-dao.d.ts +21 -0
- package/dist/dao/task-queue-dao.d.ts.map +1 -0
- package/dist/dao/timers-dao.d.ts +22 -0
- package/dist/dao/timers-dao.d.ts.map +1 -0
- package/dist/index-4vq70w0r.js +796 -0
- package/dist/index-h7bm9xg4.js +26 -0
- package/dist/index-s8wqgfwt.js +19 -0
- package/dist/index-x891hxwg.js +796 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/node-driver.d.ts +10 -0
- package/dist/node-driver.d.ts.map +1 -0
- package/dist/node.d.ts +13 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +49 -0
- package/dist/sqlite-db.d.ts +29 -0
- package/dist/sqlite-db.d.ts.map +1 -0
- package/dist/sqlite-driver.d.ts +21 -0
- package/dist/sqlite-driver.d.ts.map +1 -0
- package/dist/sqlite-event-loop.d.ts +18 -0
- package/dist/sqlite-event-loop.d.ts.map +1 -0
- package/dist/sqlite-heap.d.ts +26 -0
- package/dist/sqlite-heap.d.ts.map +1 -0
- package/dist/sqlite-scheduler.d.ts +13 -0
- package/dist/sqlite-scheduler.d.ts.map +1 -0
- package/dist/sqlite-store.d.ts +54 -0
- package/dist/sqlite-store.d.ts.map +1 -0
- package/dist/sqlite-store.test.d.ts +2 -0
- package/dist/sqlite-store.test.d.ts.map +1 -0
- package/dist/sqlite-task-queue.d.ts +26 -0
- package/dist/sqlite-task-queue.d.ts.map +1 -0
- package/dist/sqlite-timers.d.ts +18 -0
- package/dist/sqlite-timers.d.ts.map +1 -0
- package/package.json +43 -0
- package/src/bun.ts +32 -0
- package/src/dao/step-response-dao.ts +80 -0
- package/src/dao/task-queue-dao.ts +79 -0
- package/src/dao/timers-dao.ts +81 -0
- package/src/index.ts +14 -0
- package/src/node.ts +47 -0
- package/src/sqlite-db.ts +70 -0
- package/src/sqlite-driver.ts +30 -0
- package/src/sqlite-event-loop.ts +41 -0
- package/src/sqlite-heap.ts +54 -0
- package/src/sqlite-scheduler.ts +24 -0
- package/src/sqlite-store.ts +755 -0
- package/src/sqlite-task-queue.ts +64 -0
- package/src/sqlite-timers.ts +40 -0
- package/tsconfig.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
// src/dao/step-response-dao.ts
|
|
2
|
+
class StepResponsesDao {
|
|
3
|
+
db;
|
|
4
|
+
constructor(db) {
|
|
5
|
+
this.db = db;
|
|
6
|
+
this.setupDb();
|
|
7
|
+
}
|
|
8
|
+
setupDb() {
|
|
9
|
+
this.db.run(`
|
|
10
|
+
CREATE TABLE IF NOT EXISTS step_responses (
|
|
11
|
+
execution_id TEXT NOT NULL,
|
|
12
|
+
step_key TEXT NOT NULL,
|
|
13
|
+
step_attempt INTEGER NOT NULL,
|
|
14
|
+
step_done BOOL NOT NULL,
|
|
15
|
+
step_response JSONB,
|
|
16
|
+
PRIMARY KEY (execution_id, step_key, step_attempt)
|
|
17
|
+
);
|
|
18
|
+
CREATE INDEX IF NOT EXISTS idx_execution_step_attempt
|
|
19
|
+
ON step_responses(execution_id, step_key, step_attempt DESC);
|
|
20
|
+
`);
|
|
21
|
+
}
|
|
22
|
+
getAllSteps() {
|
|
23
|
+
const query = this.db.query(`SELECT * FROM step_responses`);
|
|
24
|
+
return query.all();
|
|
25
|
+
}
|
|
26
|
+
deleteAll() {
|
|
27
|
+
const query = this.db.query(`DELETE FROM step_responses`);
|
|
28
|
+
query.run();
|
|
29
|
+
}
|
|
30
|
+
getLatestStepResponse(executionId, stepKey) {
|
|
31
|
+
const query = this.db.query(`SELECT * FROM step_responses
|
|
32
|
+
WHERE execution_id = $executionId
|
|
33
|
+
AND step_key = $stepKey
|
|
34
|
+
ORDER BY step_attempt DESC LIMIT 1`);
|
|
35
|
+
const result = query.get({
|
|
36
|
+
$executionId: executionId,
|
|
37
|
+
$stepKey: stepKey
|
|
38
|
+
});
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
insertStepResponse(executionId, stepKey, stepAttempt, stepDone, stepResponseJson) {
|
|
42
|
+
const query = this.db.query(`INSERT INTO step_responses (execution_id, step_key, step_attempt, step_done, step_response)
|
|
43
|
+
VALUES ($executionId, $stepKey, $stepAttempt, $stepDone, $stepResponse)`);
|
|
44
|
+
query.run({
|
|
45
|
+
$executionId: executionId,
|
|
46
|
+
$stepKey: stepKey,
|
|
47
|
+
$stepAttempt: stepAttempt,
|
|
48
|
+
$stepDone: stepDone,
|
|
49
|
+
$stepResponse: stepResponseJson
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/sqlite-heap.ts
|
|
55
|
+
class SqliteHeapClient {
|
|
56
|
+
stepResponsesDao;
|
|
57
|
+
constructor(db) {
|
|
58
|
+
this.stepResponsesDao = new StepResponsesDao(db);
|
|
59
|
+
}
|
|
60
|
+
async getAllSteps() {
|
|
61
|
+
return this.stepResponsesDao.getAllSteps();
|
|
62
|
+
}
|
|
63
|
+
async deleteAll() {
|
|
64
|
+
this.stepResponsesDao.deleteAll();
|
|
65
|
+
}
|
|
66
|
+
async readStep(params) {
|
|
67
|
+
const result = this.stepResponsesDao.getLatestStepResponse(params.executionId, params.stepKey);
|
|
68
|
+
if (!result?.step_response) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
stepResponseJson: result.step_response,
|
|
73
|
+
meta: {
|
|
74
|
+
attempt: result.step_attempt,
|
|
75
|
+
done: Boolean(result.step_done)
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
async writeStep(params) {
|
|
80
|
+
this.stepResponsesDao.insertStepResponse(params.executionId, params.stepKey, params.stepAttempt, params.stepDone, params.stepResponseJson);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// src/sqlite-store.ts
|
|
84
|
+
import { v7 as uuidv7 } from "uuid";
|
|
85
|
+
import {
|
|
86
|
+
cloneStoreState,
|
|
87
|
+
CasStoreClient,
|
|
88
|
+
storePathsIntersect,
|
|
89
|
+
validateStoreState
|
|
90
|
+
} from "@yieldstar/core";
|
|
91
|
+
class SqliteStoreClient extends CasStoreClient {
|
|
92
|
+
db;
|
|
93
|
+
schedulerClient;
|
|
94
|
+
writeLock = Promise.resolve();
|
|
95
|
+
constructor(params) {
|
|
96
|
+
super();
|
|
97
|
+
this.db = params.db;
|
|
98
|
+
this.schedulerClient = params.schedulerClient;
|
|
99
|
+
this.setupDb();
|
|
100
|
+
this.enqueueWrite(() => this.drainWakeOutboxBestEffort());
|
|
101
|
+
}
|
|
102
|
+
enqueueWrite(fn) {
|
|
103
|
+
const result = this.writeLock.then(fn, fn);
|
|
104
|
+
this.writeLock = result.catch(() => {});
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
async getOrCreateStore(params) {
|
|
108
|
+
const existing = this.getStoreRow(params.definition.name, params.id);
|
|
109
|
+
if (existing) {
|
|
110
|
+
return {
|
|
111
|
+
state: JSON.parse(existing.state),
|
|
112
|
+
instanceId: existing.instance_id,
|
|
113
|
+
version: existing.version
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (!params.initial) {
|
|
117
|
+
throw new Error(`Store "${params.definition.name}:${params.id}" does not exist`);
|
|
118
|
+
}
|
|
119
|
+
const initialValue = params.initial;
|
|
120
|
+
const initial = typeof initialValue === "function" ? await initialValue() : initialValue;
|
|
121
|
+
const state = await validateStoreState(params.definition, initial);
|
|
122
|
+
return this.enqueueWrite(async () => {
|
|
123
|
+
this.db.run("BEGIN IMMEDIATE");
|
|
124
|
+
try {
|
|
125
|
+
const existingTx = this.getStoreRow(params.definition.name, params.id);
|
|
126
|
+
if (existingTx) {
|
|
127
|
+
this.db.run("COMMIT");
|
|
128
|
+
return {
|
|
129
|
+
state: JSON.parse(existingTx.state),
|
|
130
|
+
instanceId: existingTx.instance_id,
|
|
131
|
+
version: existingTx.version
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const instanceId = uuidv7();
|
|
135
|
+
this.db.query(`INSERT INTO stores (instance_id, store_name, store_id, version, state)
|
|
136
|
+
VALUES ($instanceId, $storeName, $storeId, 0, $state)`).run({
|
|
137
|
+
$instanceId: instanceId,
|
|
138
|
+
$storeName: params.definition.name,
|
|
139
|
+
$storeId: params.id,
|
|
140
|
+
$state: JSON.stringify(state)
|
|
141
|
+
});
|
|
142
|
+
this.db.run("COMMIT");
|
|
143
|
+
return {
|
|
144
|
+
state: cloneStoreState(state),
|
|
145
|
+
instanceId,
|
|
146
|
+
version: 0
|
|
147
|
+
};
|
|
148
|
+
} catch (err) {
|
|
149
|
+
this.db.run("ROLLBACK");
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
throw new Error("Store creation transaction returned unexpectedly");
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
async getStore(params) {
|
|
156
|
+
const row = this.getStoreRow(params.definition.name, params.id);
|
|
157
|
+
if (!row) {
|
|
158
|
+
throw new Error(`Store "${params.definition.name}:${params.id}" does not exist`);
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
state: JSON.parse(row.state),
|
|
162
|
+
instanceId: row.instance_id,
|
|
163
|
+
version: row.version
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
async getAppliedStoreStep(params) {
|
|
167
|
+
const row = this.getAppliedStepRow({
|
|
168
|
+
storeName: params.definition.name,
|
|
169
|
+
storeId: params.id,
|
|
170
|
+
stepId: params.stepId
|
|
171
|
+
});
|
|
172
|
+
return row ? { result: JSON.parse(row.result) } : undefined;
|
|
173
|
+
}
|
|
174
|
+
commitStoreMutation(mutation) {
|
|
175
|
+
return this.enqueueWrite(async () => {
|
|
176
|
+
this.db.run("BEGIN IMMEDIATE");
|
|
177
|
+
try {
|
|
178
|
+
if (mutation.stepId) {
|
|
179
|
+
const applied = this.getAppliedStepRow({
|
|
180
|
+
storeName: mutation.definition.name,
|
|
181
|
+
storeId: mutation.id,
|
|
182
|
+
stepId: mutation.stepId
|
|
183
|
+
});
|
|
184
|
+
if (applied) {
|
|
185
|
+
this.db.run("COMMIT");
|
|
186
|
+
return {
|
|
187
|
+
status: "already-applied",
|
|
188
|
+
result: JSON.parse(applied.result)
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const row = this.getStoreRow(mutation.definition.name, mutation.id);
|
|
193
|
+
if (!row) {
|
|
194
|
+
throw new Error(`Store "${mutation.definition.name}:${mutation.id}" does not exist`);
|
|
195
|
+
}
|
|
196
|
+
if (row.instance_id !== mutation.expected.instanceId || row.version !== mutation.expected.version) {
|
|
197
|
+
this.db.run("COMMIT");
|
|
198
|
+
return {
|
|
199
|
+
status: "conflict",
|
|
200
|
+
snapshot: {
|
|
201
|
+
state: JSON.parse(row.state),
|
|
202
|
+
instanceId: row.instance_id,
|
|
203
|
+
version: row.version
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const version = mutation.expected.version + 1;
|
|
208
|
+
const changed = this.db.query(`UPDATE stores
|
|
209
|
+
SET version = $version, state = $state
|
|
210
|
+
WHERE instance_id = $instanceId AND version = $expectedVersion`).run({
|
|
211
|
+
$instanceId: mutation.expected.instanceId,
|
|
212
|
+
$expectedVersion: mutation.expected.version,
|
|
213
|
+
$version: version,
|
|
214
|
+
$state: JSON.stringify(mutation.nextState)
|
|
215
|
+
});
|
|
216
|
+
if (Number(changed.changes) !== 1) {
|
|
217
|
+
throw new Error("Store changed during its mutation transaction");
|
|
218
|
+
}
|
|
219
|
+
if (mutation.stepId) {
|
|
220
|
+
this.insertAppliedStepRow({
|
|
221
|
+
storeName: mutation.definition.name,
|
|
222
|
+
storeId: mutation.id,
|
|
223
|
+
stepId: mutation.stepId,
|
|
224
|
+
result: JSON.stringify(mutation.result)
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
const waiters = this.selectMatchingWaiters({
|
|
228
|
+
storeName: mutation.definition.name,
|
|
229
|
+
storeId: mutation.id,
|
|
230
|
+
version,
|
|
231
|
+
changedPaths: mutation.changedPaths
|
|
232
|
+
});
|
|
233
|
+
for (const waiter of waiters) {
|
|
234
|
+
this.insertWakeOutboxRow({
|
|
235
|
+
storeName: mutation.definition.name,
|
|
236
|
+
storeId: mutation.id,
|
|
237
|
+
executionId: waiter.executionId,
|
|
238
|
+
stepKey: waiter.stepKey
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
this.db.run("COMMIT");
|
|
242
|
+
} catch (error) {
|
|
243
|
+
this.db.run("ROLLBACK");
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
await this.drainWakeOutboxBestEffort();
|
|
247
|
+
return { status: "committed" };
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
async listStores(definition) {
|
|
251
|
+
const rows = this.db.query(`SELECT store_id FROM stores
|
|
252
|
+
WHERE store_name = $storeName
|
|
253
|
+
ORDER BY store_id ASC`).all({ $storeName: definition.name });
|
|
254
|
+
return rows.map((row) => row.store_id);
|
|
255
|
+
}
|
|
256
|
+
async deleteStore(params) {
|
|
257
|
+
return this.enqueueWrite(async () => {
|
|
258
|
+
const bindings = {
|
|
259
|
+
$storeName: params.definition.name,
|
|
260
|
+
$storeId: params.id
|
|
261
|
+
};
|
|
262
|
+
this.db.run("BEGIN IMMEDIATE");
|
|
263
|
+
try {
|
|
264
|
+
this.db.query(`DELETE FROM stores WHERE store_name = $storeName AND store_id = $storeId`).run(bindings);
|
|
265
|
+
this.db.query(`DELETE FROM store_waiters WHERE store_name = $storeName AND store_id = $storeId`).run(bindings);
|
|
266
|
+
this.db.query(`DELETE FROM store_wake_outbox WHERE store_name = $storeName AND store_id = $storeId`).run(bindings);
|
|
267
|
+
this.db.run("COMMIT");
|
|
268
|
+
} catch (err) {
|
|
269
|
+
this.db.run("ROLLBACK");
|
|
270
|
+
throw err;
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
async deleteStoreFrom(params) {
|
|
275
|
+
return this.enqueueWrite(async () => {
|
|
276
|
+
const bindings = {
|
|
277
|
+
$storeName: params.definition.name,
|
|
278
|
+
$storeId: params.id
|
|
279
|
+
};
|
|
280
|
+
this.db.run("BEGIN IMMEDIATE");
|
|
281
|
+
try {
|
|
282
|
+
if (params.stepId) {
|
|
283
|
+
const applied = this.getAppliedStepRow({
|
|
284
|
+
storeName: params.definition.name,
|
|
285
|
+
storeId: params.id,
|
|
286
|
+
stepId: params.stepId
|
|
287
|
+
});
|
|
288
|
+
if (applied) {
|
|
289
|
+
this.db.run("COMMIT");
|
|
290
|
+
return JSON.parse(applied.result);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
this.assertSnapshotHasInstanceId(params.snapshot);
|
|
294
|
+
const row = this.getStoreRow(params.definition.name, params.id);
|
|
295
|
+
if (!row) {
|
|
296
|
+
this.db.run("COMMIT");
|
|
297
|
+
return {
|
|
298
|
+
deleted: false,
|
|
299
|
+
reason: "not-found",
|
|
300
|
+
expectedInstanceId: params.snapshot.instanceId,
|
|
301
|
+
expectedVersion: params.snapshot.version
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
if (row.instance_id !== params.snapshot.instanceId || row.version !== params.snapshot.version) {
|
|
305
|
+
this.db.run("COMMIT");
|
|
306
|
+
return {
|
|
307
|
+
deleted: false,
|
|
308
|
+
reason: "conflict",
|
|
309
|
+
expectedInstanceId: params.snapshot.instanceId,
|
|
310
|
+
actualInstanceId: row.instance_id,
|
|
311
|
+
expectedVersion: params.snapshot.version,
|
|
312
|
+
actualVersion: row.version
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
this.db.query(`DELETE FROM store_waiters WHERE store_name = $storeName AND store_id = $storeId`).run(bindings);
|
|
316
|
+
this.db.query(`DELETE FROM store_wake_outbox WHERE store_name = $storeName AND store_id = $storeId`).run(bindings);
|
|
317
|
+
this.db.query(`DELETE FROM stores WHERE instance_id = $instanceId`).run({ $instanceId: row.instance_id });
|
|
318
|
+
const result = { deleted: true };
|
|
319
|
+
if (params.stepId) {
|
|
320
|
+
this.insertAppliedStepRow({
|
|
321
|
+
storeName: params.definition.name,
|
|
322
|
+
storeId: params.id,
|
|
323
|
+
stepId: params.stepId,
|
|
324
|
+
result: JSON.stringify(result)
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
this.db.run("COMMIT");
|
|
328
|
+
return result;
|
|
329
|
+
} catch (err) {
|
|
330
|
+
this.db.run("ROLLBACK");
|
|
331
|
+
throw err;
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
async registerWaiter(waiter) {
|
|
336
|
+
return this.enqueueWrite(async () => {
|
|
337
|
+
let versionAdvanced = false;
|
|
338
|
+
this.db.run("BEGIN IMMEDIATE");
|
|
339
|
+
try {
|
|
340
|
+
this.db.query(`INSERT INTO store_waiters
|
|
341
|
+
(workflow_id, execution_id, step_key, event, store_name, store_id, since_version, read_paths)
|
|
342
|
+
VALUES
|
|
343
|
+
($workflowId, $executionId, $stepKey, $event, $storeName, $storeId, $sinceVersion, $readPaths)
|
|
344
|
+
ON CONFLICT(store_name, store_id, execution_id, step_key)
|
|
345
|
+
DO UPDATE SET
|
|
346
|
+
event = excluded.event,
|
|
347
|
+
since_version = excluded.since_version,
|
|
348
|
+
read_paths = excluded.read_paths`).run({
|
|
349
|
+
$workflowId: waiter.workflowId,
|
|
350
|
+
$executionId: waiter.executionId,
|
|
351
|
+
$stepKey: waiter.stepKey,
|
|
352
|
+
$event: JSON.stringify(serializeEvent(waiter.event)),
|
|
353
|
+
$storeName: waiter.storeName,
|
|
354
|
+
$storeId: waiter.storeId,
|
|
355
|
+
$sinceVersion: waiter.sinceVersion,
|
|
356
|
+
$readPaths: JSON.stringify(waiter.readPaths)
|
|
357
|
+
});
|
|
358
|
+
const row = this.getStoreRow(waiter.storeName, waiter.storeId);
|
|
359
|
+
if (!row || row.instance_id !== waiter.instanceId || row.version > waiter.sinceVersion) {
|
|
360
|
+
versionAdvanced = true;
|
|
361
|
+
this.insertWakeOutboxRow({
|
|
362
|
+
storeName: waiter.storeName,
|
|
363
|
+
storeId: waiter.storeId,
|
|
364
|
+
executionId: waiter.executionId,
|
|
365
|
+
stepKey: waiter.stepKey
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
this.db.run("COMMIT");
|
|
369
|
+
} catch (err) {
|
|
370
|
+
this.db.run("ROLLBACK");
|
|
371
|
+
throw err;
|
|
372
|
+
}
|
|
373
|
+
if (versionAdvanced) {
|
|
374
|
+
await this.drainWakeOutboxBestEffort();
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
setupDb() {
|
|
379
|
+
this.createStoresTable();
|
|
380
|
+
this.db.run(`
|
|
381
|
+
CREATE TABLE IF NOT EXISTS store_waiters (
|
|
382
|
+
workflow_id TEXT NOT NULL,
|
|
383
|
+
execution_id TEXT NOT NULL,
|
|
384
|
+
step_key TEXT NOT NULL,
|
|
385
|
+
event TEXT NOT NULL,
|
|
386
|
+
store_name TEXT NOT NULL,
|
|
387
|
+
store_id TEXT NOT NULL,
|
|
388
|
+
since_version INTEGER NOT NULL,
|
|
389
|
+
read_paths TEXT NOT NULL,
|
|
390
|
+
PRIMARY KEY (store_name, store_id, execution_id, step_key)
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
CREATE TABLE IF NOT EXISTS store_applied_steps (
|
|
394
|
+
store_name TEXT NOT NULL,
|
|
395
|
+
store_id TEXT NOT NULL,
|
|
396
|
+
execution_id TEXT NOT NULL,
|
|
397
|
+
step_key TEXT NOT NULL,
|
|
398
|
+
result TEXT NOT NULL,
|
|
399
|
+
PRIMARY KEY (store_name, store_id, execution_id, step_key)
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
CREATE TABLE IF NOT EXISTS store_wake_outbox (
|
|
403
|
+
store_name TEXT NOT NULL,
|
|
404
|
+
store_id TEXT NOT NULL,
|
|
405
|
+
execution_id TEXT NOT NULL,
|
|
406
|
+
step_key TEXT NOT NULL,
|
|
407
|
+
PRIMARY KEY (store_name, store_id, execution_id, step_key)
|
|
408
|
+
);
|
|
409
|
+
`);
|
|
410
|
+
}
|
|
411
|
+
createStoresTable() {
|
|
412
|
+
this.db.run(`
|
|
413
|
+
CREATE TABLE IF NOT EXISTS stores (
|
|
414
|
+
instance_id TEXT PRIMARY KEY,
|
|
415
|
+
store_name TEXT NOT NULL,
|
|
416
|
+
store_id TEXT NOT NULL,
|
|
417
|
+
version INTEGER NOT NULL,
|
|
418
|
+
state TEXT NOT NULL,
|
|
419
|
+
UNIQUE (store_name, store_id)
|
|
420
|
+
)
|
|
421
|
+
`);
|
|
422
|
+
}
|
|
423
|
+
getAppliedStepRow(params) {
|
|
424
|
+
return this.db.query(`SELECT result FROM store_applied_steps
|
|
425
|
+
WHERE store_name = $storeName
|
|
426
|
+
AND store_id = $storeId
|
|
427
|
+
AND execution_id = $executionId
|
|
428
|
+
AND step_key = $stepKey`).get({
|
|
429
|
+
$storeName: params.storeName,
|
|
430
|
+
$storeId: params.storeId,
|
|
431
|
+
$executionId: params.stepId.executionId,
|
|
432
|
+
$stepKey: params.stepId.stepKey
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
insertAppliedStepRow(params) {
|
|
436
|
+
this.db.query(`INSERT INTO store_applied_steps
|
|
437
|
+
(store_name, store_id, execution_id, step_key, result)
|
|
438
|
+
VALUES
|
|
439
|
+
($storeName, $storeId, $executionId, $stepKey, $result)`).run({
|
|
440
|
+
$storeName: params.storeName,
|
|
441
|
+
$storeId: params.storeId,
|
|
442
|
+
$executionId: params.stepId.executionId,
|
|
443
|
+
$stepKey: params.stepId.stepKey,
|
|
444
|
+
$result: params.result
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
getStoreRow(storeName, storeId) {
|
|
448
|
+
return this.db.query(`SELECT instance_id, state, version FROM stores
|
|
449
|
+
WHERE store_name = $storeName AND store_id = $storeId`).get({
|
|
450
|
+
$storeName: storeName,
|
|
451
|
+
$storeId: storeId
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
selectMatchingWaiters(params) {
|
|
455
|
+
const waiters = [];
|
|
456
|
+
const rows = this.db.query(`SELECT * FROM store_waiters
|
|
457
|
+
WHERE store_name = $storeName AND store_id = $storeId`).all({
|
|
458
|
+
$storeName: params.storeName,
|
|
459
|
+
$storeId: params.storeId
|
|
460
|
+
});
|
|
461
|
+
for (const row of rows) {
|
|
462
|
+
if (row.since_version >= params.version)
|
|
463
|
+
continue;
|
|
464
|
+
const readPaths = JSON.parse(row.read_paths);
|
|
465
|
+
if (!storePathsIntersect(readPaths, params.changedPaths))
|
|
466
|
+
continue;
|
|
467
|
+
waiters.push({
|
|
468
|
+
executionId: row.execution_id,
|
|
469
|
+
stepKey: row.step_key,
|
|
470
|
+
event: deserializeEvent(JSON.parse(row.event))
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
return waiters;
|
|
474
|
+
}
|
|
475
|
+
deleteWaiterRow(params) {
|
|
476
|
+
this.db.query(`DELETE FROM store_waiters
|
|
477
|
+
WHERE store_name = $storeName
|
|
478
|
+
AND store_id = $storeId
|
|
479
|
+
AND execution_id = $executionId
|
|
480
|
+
AND step_key = $stepKey
|
|
481
|
+
AND ($sinceVersion IS NULL OR since_version = $sinceVersion)`).run({
|
|
482
|
+
$storeName: params.storeName,
|
|
483
|
+
$storeId: params.storeId,
|
|
484
|
+
$executionId: params.executionId,
|
|
485
|
+
$stepKey: params.stepKey,
|
|
486
|
+
$sinceVersion: params.sinceVersion ?? null
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
insertWakeOutboxRow(params) {
|
|
490
|
+
this.db.query(`INSERT OR IGNORE INTO store_wake_outbox
|
|
491
|
+
(store_name, store_id, execution_id, step_key)
|
|
492
|
+
VALUES ($storeName, $storeId, $executionId, $stepKey)`).run({
|
|
493
|
+
$storeName: params.storeName,
|
|
494
|
+
$storeId: params.storeId,
|
|
495
|
+
$executionId: params.executionId,
|
|
496
|
+
$stepKey: params.stepKey
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
async drainWakeOutbox() {
|
|
500
|
+
const rows = this.db.query(`SELECT
|
|
501
|
+
o.store_name,
|
|
502
|
+
o.store_id,
|
|
503
|
+
o.execution_id,
|
|
504
|
+
o.step_key,
|
|
505
|
+
w.event,
|
|
506
|
+
w.since_version
|
|
507
|
+
FROM store_wake_outbox o
|
|
508
|
+
LEFT JOIN store_waiters w
|
|
509
|
+
ON w.store_name = o.store_name
|
|
510
|
+
AND w.store_id = o.store_id
|
|
511
|
+
AND w.execution_id = o.execution_id
|
|
512
|
+
AND w.step_key = o.step_key
|
|
513
|
+
ORDER BY o.store_name, o.store_id, o.execution_id, o.step_key`).all();
|
|
514
|
+
for (const row of rows) {
|
|
515
|
+
try {
|
|
516
|
+
if (row.event !== null) {
|
|
517
|
+
await this.schedulerClient.requestWakeUp(deserializeEvent(JSON.parse(row.event)));
|
|
518
|
+
this.deleteWaiterRow({
|
|
519
|
+
storeName: row.store_name,
|
|
520
|
+
storeId: row.store_id,
|
|
521
|
+
executionId: row.execution_id,
|
|
522
|
+
stepKey: row.step_key,
|
|
523
|
+
sinceVersion: row.since_version ?? undefined
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
this.db.query(`DELETE FROM store_wake_outbox
|
|
527
|
+
WHERE store_name = $storeName
|
|
528
|
+
AND store_id = $storeId
|
|
529
|
+
AND execution_id = $executionId
|
|
530
|
+
AND step_key = $stepKey`).run({
|
|
531
|
+
$storeName: row.store_name,
|
|
532
|
+
$storeId: row.store_id,
|
|
533
|
+
$executionId: row.execution_id,
|
|
534
|
+
$stepKey: row.step_key
|
|
535
|
+
});
|
|
536
|
+
} catch (error) {
|
|
537
|
+
console.error("Failed to deliver pending store wake", error);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
async drainWakeOutboxBestEffort() {
|
|
542
|
+
try {
|
|
543
|
+
await this.drainWakeOutbox();
|
|
544
|
+
} catch (error) {
|
|
545
|
+
console.error("Failed to drain pending store wakes", error);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
assertSnapshotHasInstanceId(snapshot) {
|
|
549
|
+
if (!snapshot.instanceId) {
|
|
550
|
+
throw new Error("Store snapshot is missing instanceId; read a fresh snapshot");
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function serializeEvent(event) {
|
|
555
|
+
return {
|
|
556
|
+
workflowId: event.workflowId,
|
|
557
|
+
executionId: event.executionId,
|
|
558
|
+
params: event.params,
|
|
559
|
+
context: event.context instanceof Map ? [...event.context.entries()] : event.context ? Object.entries(event.context) : []
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
function deserializeEvent(event) {
|
|
563
|
+
return {
|
|
564
|
+
workflowId: event.workflowId,
|
|
565
|
+
executionId: event.executionId,
|
|
566
|
+
params: event.params,
|
|
567
|
+
context: new Map(event.context ?? [])
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
// src/sqlite-scheduler.ts
|
|
571
|
+
class SqliteSchedulerClient {
|
|
572
|
+
taskQueue;
|
|
573
|
+
timersClient;
|
|
574
|
+
constructor(params) {
|
|
575
|
+
this.taskQueue = params.taskQueueClient;
|
|
576
|
+
this.timersClient = params.timersClient;
|
|
577
|
+
}
|
|
578
|
+
async requestWakeUp(event, resumeIn) {
|
|
579
|
+
if (resumeIn) {
|
|
580
|
+
this.timersClient.createTimer(event, resumeIn);
|
|
581
|
+
} else {
|
|
582
|
+
this.taskQueue.add(event);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
// src/dao/task-queue-dao.ts
|
|
587
|
+
class TaskQueueDao {
|
|
588
|
+
db;
|
|
589
|
+
constructor(db) {
|
|
590
|
+
this.db = db;
|
|
591
|
+
}
|
|
592
|
+
setupDb() {
|
|
593
|
+
this.db.run(`
|
|
594
|
+
CREATE TABLE IF NOT EXISTS task_queue (
|
|
595
|
+
task_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
596
|
+
workflow_id TEXT NOT NULL,
|
|
597
|
+
execution_id TEXT NOT NULL,
|
|
598
|
+
params TEXT,
|
|
599
|
+
context TEXT,
|
|
600
|
+
visible_from INTEGER DEFAULT (strftime('%s', 'now'))
|
|
601
|
+
)
|
|
602
|
+
`);
|
|
603
|
+
}
|
|
604
|
+
insertTask(event) {
|
|
605
|
+
const query = this.db.query(`INSERT INTO task_queue (workflow_id, execution_id, params, context)
|
|
606
|
+
VALUES ($workflowId, $executionId, $params, $context)`);
|
|
607
|
+
query.run({
|
|
608
|
+
$workflowId: event.workflowId,
|
|
609
|
+
$executionId: event.executionId,
|
|
610
|
+
$params: event.params ? JSON.stringify(event.params) : null,
|
|
611
|
+
$context: event.context ? JSON.stringify(Array.from(event.context.entries())) : null
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
getNextTask() {
|
|
615
|
+
const query = this.db.query(`SELECT * FROM task_queue WHERE visible_from < strftime('%s', 'now') ORDER BY task_id LIMIT 1`);
|
|
616
|
+
return query.get();
|
|
617
|
+
}
|
|
618
|
+
updateTaskVisibility(taskId, visibleFrom) {
|
|
619
|
+
const query = this.db.query("UPDATE task_queue SET visible_from = $visibleFrom WHERE task_id = $taskId");
|
|
620
|
+
query.run({ $taskId: taskId, $visibleFrom: visibleFrom });
|
|
621
|
+
}
|
|
622
|
+
deleteTaskById(id) {
|
|
623
|
+
const query = this.db.query(`DELETE FROM task_queue WHERE task_id = $taskId`);
|
|
624
|
+
query.run({ $taskId: id });
|
|
625
|
+
}
|
|
626
|
+
getTaskCount() {
|
|
627
|
+
const query = this.db.query(`SELECT COUNT(*) as count FROM task_queue WHERE visible_from < strftime('%s', 'now')`);
|
|
628
|
+
return query.get().count;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// src/sqlite-task-queue.ts
|
|
633
|
+
var VISIBILITY_WINDOW = 300000;
|
|
634
|
+
|
|
635
|
+
class SqliteTaskQueue {
|
|
636
|
+
taskQueueDao;
|
|
637
|
+
constructor(db) {
|
|
638
|
+
this.taskQueueDao = new TaskQueueDao(db);
|
|
639
|
+
this.taskQueueDao.setupDb();
|
|
640
|
+
}
|
|
641
|
+
add(event) {
|
|
642
|
+
this.taskQueueDao.insertTask(event);
|
|
643
|
+
}
|
|
644
|
+
process() {
|
|
645
|
+
const row = this.taskQueueDao.getNextTask();
|
|
646
|
+
if (!row)
|
|
647
|
+
return;
|
|
648
|
+
const now = Date.now();
|
|
649
|
+
const visibilityTimeout = now + VISIBILITY_WINDOW;
|
|
650
|
+
this.taskQueueDao.updateTaskVisibility(row.task_id, visibilityTimeout);
|
|
651
|
+
return {
|
|
652
|
+
taskId: row.task_id,
|
|
653
|
+
visibilityTimeout,
|
|
654
|
+
event: {
|
|
655
|
+
workflowId: row.workflow_id,
|
|
656
|
+
executionId: row.execution_id,
|
|
657
|
+
params: row.params ? JSON.parse(row.params) : undefined,
|
|
658
|
+
context: row.context ? new Map(JSON.parse(row.context)) : new Map
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
remove(taskId) {
|
|
663
|
+
this.taskQueueDao.deleteTaskById(taskId);
|
|
664
|
+
}
|
|
665
|
+
makeVisible(taskId) {
|
|
666
|
+
this.taskQueueDao.updateTaskVisibility(taskId, 0);
|
|
667
|
+
}
|
|
668
|
+
get isEmpty() {
|
|
669
|
+
return this.taskQueueDao.getTaskCount() === 0;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
class SqliteTaskQueueClient {
|
|
674
|
+
taskQueueDao;
|
|
675
|
+
constructor(db) {
|
|
676
|
+
this.taskQueueDao = new TaskQueueDao(db);
|
|
677
|
+
}
|
|
678
|
+
add(event) {
|
|
679
|
+
this.taskQueueDao.insertTask(event);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// src/dao/timers-dao.ts
|
|
684
|
+
class TimersDao {
|
|
685
|
+
db;
|
|
686
|
+
constructor(db) {
|
|
687
|
+
this.db = db;
|
|
688
|
+
this.setupDb();
|
|
689
|
+
}
|
|
690
|
+
setupDb() {
|
|
691
|
+
this.db.run(`
|
|
692
|
+
CREATE TABLE IF NOT EXISTS scheduled_tasks (
|
|
693
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
694
|
+
delay INTEGER NOT NULL,
|
|
695
|
+
workflow_id TEXT NOT NULL,
|
|
696
|
+
execution_id TEXT NOT NULL,
|
|
697
|
+
created_at INTEGER NOT NULL,
|
|
698
|
+
params TEXT,
|
|
699
|
+
context TEXT
|
|
700
|
+
);
|
|
701
|
+
`);
|
|
702
|
+
}
|
|
703
|
+
getExpiredTimers() {
|
|
704
|
+
const query = this.db.query(`SELECT id, delay, workflow_id, execution_id, created_at, params, context
|
|
705
|
+
FROM scheduled_tasks
|
|
706
|
+
WHERE (created_at + delay) < $currentTime`);
|
|
707
|
+
return query.all({ $currentTime: Date.now() });
|
|
708
|
+
}
|
|
709
|
+
getTaskCount() {
|
|
710
|
+
const query = this.db.query(`SELECT COUNT(*) as count
|
|
711
|
+
FROM scheduled_tasks
|
|
712
|
+
WHERE (created_at + delay) < $currentTime`);
|
|
713
|
+
const count = query.get({ $currentTime: Date.now() });
|
|
714
|
+
return count.count;
|
|
715
|
+
}
|
|
716
|
+
insertTimer(event, delay) {
|
|
717
|
+
const query = this.db.query(`INSERT INTO scheduled_tasks (delay, workflow_id, execution_id, created_at, params, context)
|
|
718
|
+
VALUES ($delay, $workflowId, $executionId, $createdAt, $params, $context)`);
|
|
719
|
+
query.run({
|
|
720
|
+
$delay: delay,
|
|
721
|
+
$workflowId: event.workflowId,
|
|
722
|
+
$executionId: event.executionId,
|
|
723
|
+
$createdAt: Date.now(),
|
|
724
|
+
$params: event.params ? JSON.stringify(event.params) : null,
|
|
725
|
+
$context: JSON.stringify(Array.from(event.context.entries()))
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
deleteTimerById(id) {
|
|
729
|
+
const query = this.db.query(`DELETE FROM scheduled_tasks WHERE id = $id`);
|
|
730
|
+
query.run({ $id: id });
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// src/sqlite-timers.ts
|
|
735
|
+
class SqliteTimers {
|
|
736
|
+
timersDao;
|
|
737
|
+
taskQueue;
|
|
738
|
+
constructor(params) {
|
|
739
|
+
this.timersDao = new TimersDao(params.db);
|
|
740
|
+
this.taskQueue = params.taskQueue;
|
|
741
|
+
}
|
|
742
|
+
processTimers() {
|
|
743
|
+
const timers = this.timersDao.getExpiredTimers();
|
|
744
|
+
for (const timer of timers) {
|
|
745
|
+
this.timersDao.deleteTimerById(timer.id);
|
|
746
|
+
this.taskQueue.add({
|
|
747
|
+
workflowId: timer.workflow_id,
|
|
748
|
+
executionId: timer.execution_id,
|
|
749
|
+
params: timer.params ? JSON.parse(timer.params) : undefined,
|
|
750
|
+
context: timer.context ? new Map(JSON.parse(timer.context)) : new Map
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
class SqliteTimersClient {
|
|
757
|
+
timersDao;
|
|
758
|
+
constructor(db) {
|
|
759
|
+
this.timersDao = new TimersDao(db);
|
|
760
|
+
}
|
|
761
|
+
createTimer(event, delay) {
|
|
762
|
+
this.timersDao.insertTimer(event, delay);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// src/sqlite-event-loop.ts
|
|
767
|
+
class SqliteEventLoop {
|
|
768
|
+
taskQueue;
|
|
769
|
+
timers;
|
|
770
|
+
isRunning = false;
|
|
771
|
+
constructor(db) {
|
|
772
|
+
this.taskQueue = new SqliteTaskQueue(db);
|
|
773
|
+
this.timers = new SqliteTimers({ db, taskQueue: this.taskQueue });
|
|
774
|
+
}
|
|
775
|
+
start(params) {
|
|
776
|
+
this.isRunning = true;
|
|
777
|
+
this.loop(params.onNewEvent, params.logger);
|
|
778
|
+
}
|
|
779
|
+
stop() {
|
|
780
|
+
this.isRunning = false;
|
|
781
|
+
}
|
|
782
|
+
async loop(processEvent, logger) {
|
|
783
|
+
if (!this.isRunning)
|
|
784
|
+
return;
|
|
785
|
+
while (!this.taskQueue.isEmpty) {
|
|
786
|
+
const task = this.taskQueue.process();
|
|
787
|
+
if (task) {
|
|
788
|
+
await processEvent(task.event, logger);
|
|
789
|
+
this.taskQueue.remove(task.taskId);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
this.timers.processTimers();
|
|
793
|
+
setTimeout(() => this.loop(processEvent, logger), 10);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
export { SqliteHeapClient, SqliteStoreClient, SqliteSchedulerClient, SqliteTaskQueueClient, SqliteTimersClient, SqliteEventLoop };
|