@yieldstar/sqlite-runtime 0.6.0 → 0.7.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.js +1 -1
- package/dist/dao/executions-dao.d.ts +3 -2
- package/dist/dao/executions-dao.d.ts.map +1 -1
- package/dist/index-ekggxe8v.js +987 -0
- package/dist/index-t60e4kvz.js +988 -0
- package/dist/index.js +1 -1
- package/dist/node.js +1 -1
- package/dist/sqlite-execution.d.ts +2 -1
- package/dist/sqlite-execution.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/dao/executions-dao.ts +14 -3
- package/src/sqlite-execution.ts +6 -2
|
@@ -0,0 +1,987 @@
|
|
|
1
|
+
// src/sqlite-runtime.ts
|
|
2
|
+
import { Runtime } from "@yieldstar/core";
|
|
3
|
+
|
|
4
|
+
// src/sqlite-effect.ts
|
|
5
|
+
import {
|
|
6
|
+
createEffectState,
|
|
7
|
+
effectNotFound,
|
|
8
|
+
matchEffectBegin,
|
|
9
|
+
prepareEffectBegin,
|
|
10
|
+
prepareEffectSettle,
|
|
11
|
+
prepareEffectWatch,
|
|
12
|
+
settleEffect,
|
|
13
|
+
toEffectRecord
|
|
14
|
+
} from "@yieldstar/core";
|
|
15
|
+
|
|
16
|
+
// src/dao/effect-waiters-dao.ts
|
|
17
|
+
class EffectWaitersDao {
|
|
18
|
+
db;
|
|
19
|
+
constructor(db) {
|
|
20
|
+
this.db = db;
|
|
21
|
+
this.setupDb();
|
|
22
|
+
}
|
|
23
|
+
setupDb() {
|
|
24
|
+
this.db.run(`
|
|
25
|
+
CREATE TABLE IF NOT EXISTS effect_waiters (
|
|
26
|
+
effect_id TEXT NOT NULL,
|
|
27
|
+
execution_id TEXT NOT NULL,
|
|
28
|
+
step_key TEXT NOT NULL,
|
|
29
|
+
PRIMARY KEY (effect_id, execution_id, step_key)
|
|
30
|
+
);
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_effect_waiters_step
|
|
32
|
+
ON effect_waiters(execution_id, step_key);
|
|
33
|
+
`);
|
|
34
|
+
}
|
|
35
|
+
upsertWaiter(waiter) {
|
|
36
|
+
this.db.query(`INSERT INTO effect_waiters (effect_id, execution_id, step_key)
|
|
37
|
+
VALUES ($effectId, $executionId, $stepKey)
|
|
38
|
+
ON CONFLICT(effect_id, execution_id, step_key) DO NOTHING`).run({
|
|
39
|
+
$effectId: waiter.effectId,
|
|
40
|
+
$executionId: waiter.executionId,
|
|
41
|
+
$stepKey: waiter.stepKey
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
getWaitingExecutionIds(effectId) {
|
|
45
|
+
return this.db.query(`SELECT DISTINCT execution_id FROM effect_waiters WHERE effect_id = $effectId`).all({ $effectId: effectId }).map((row) => row.execution_id);
|
|
46
|
+
}
|
|
47
|
+
deleteStepWaiters(executionId, stepKey) {
|
|
48
|
+
this.db.query(`DELETE FROM effect_waiters WHERE execution_id = $executionId AND step_key = $stepKey`).run({ $executionId: executionId, $stepKey: stepKey });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/dao/effects-dao.ts
|
|
53
|
+
class EffectsDao {
|
|
54
|
+
db;
|
|
55
|
+
constructor(db) {
|
|
56
|
+
this.db = db;
|
|
57
|
+
this.setupDb();
|
|
58
|
+
}
|
|
59
|
+
setupDb() {
|
|
60
|
+
this.db.run(`
|
|
61
|
+
CREATE TABLE IF NOT EXISTS effects (
|
|
62
|
+
effect_id TEXT PRIMARY KEY,
|
|
63
|
+
operation TEXT NOT NULL,
|
|
64
|
+
input TEXT NOT NULL,
|
|
65
|
+
status TEXT NOT NULL,
|
|
66
|
+
result TEXT
|
|
67
|
+
);
|
|
68
|
+
CREATE TABLE IF NOT EXISTS effect_conflicts (
|
|
69
|
+
effect_id TEXT NOT NULL,
|
|
70
|
+
result TEXT NOT NULL,
|
|
71
|
+
conflicting_result TEXT NOT NULL,
|
|
72
|
+
reported_at INTEGER NOT NULL,
|
|
73
|
+
PRIMARY KEY (effect_id, conflicting_result)
|
|
74
|
+
);
|
|
75
|
+
`);
|
|
76
|
+
}
|
|
77
|
+
getState(effectId) {
|
|
78
|
+
const row = this.db.query(`SELECT * FROM effects WHERE effect_id = $effectId`).get({ $effectId: effectId });
|
|
79
|
+
if (!row)
|
|
80
|
+
return;
|
|
81
|
+
const conflicts = this.db.query(`SELECT result, conflicting_result, reported_at FROM effect_conflicts
|
|
82
|
+
WHERE effect_id = $effectId
|
|
83
|
+
ORDER BY reported_at, rowid`).all({ $effectId: effectId });
|
|
84
|
+
const state = {
|
|
85
|
+
effectId: row.effect_id,
|
|
86
|
+
operation: row.operation,
|
|
87
|
+
input: row.input,
|
|
88
|
+
status: row.status,
|
|
89
|
+
conflicts: conflicts.map((conflict) => ({
|
|
90
|
+
result: conflict.result,
|
|
91
|
+
conflictingResult: conflict.conflicting_result,
|
|
92
|
+
reportedAt: conflict.reported_at
|
|
93
|
+
}))
|
|
94
|
+
};
|
|
95
|
+
if (row.result !== null)
|
|
96
|
+
state.result = row.result;
|
|
97
|
+
return state;
|
|
98
|
+
}
|
|
99
|
+
insertState(state) {
|
|
100
|
+
this.db.query(`INSERT INTO effects (effect_id, operation, input, status, result)
|
|
101
|
+
VALUES ($effectId, $operation, $input, $status, $result)`).run({
|
|
102
|
+
$effectId: state.effectId,
|
|
103
|
+
$operation: state.operation,
|
|
104
|
+
$input: state.input,
|
|
105
|
+
$status: state.status,
|
|
106
|
+
$result: state.result ?? null
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
updateState(before, after) {
|
|
110
|
+
if (before.status !== after.status || before.result !== after.result) {
|
|
111
|
+
this.db.query(`UPDATE effects SET status = $status, result = $result
|
|
112
|
+
WHERE effect_id = $effectId`).run({
|
|
113
|
+
$effectId: after.effectId,
|
|
114
|
+
$status: after.status,
|
|
115
|
+
$result: after.result ?? null
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
for (const conflict of after.conflicts.slice(before.conflicts.length)) {
|
|
119
|
+
this.insertConflict(after.effectId, conflict);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
insertConflict(effectId, conflict) {
|
|
123
|
+
this.db.query(`INSERT INTO effect_conflicts (effect_id, result, conflicting_result, reported_at)
|
|
124
|
+
VALUES ($effectId, $result, $conflictingResult, $reportedAt)`).run({
|
|
125
|
+
$effectId: effectId,
|
|
126
|
+
$result: conflict.result,
|
|
127
|
+
$conflictingResult: conflict.conflictingResult,
|
|
128
|
+
$reportedAt: conflict.reportedAt
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/sqlite-execution.ts
|
|
134
|
+
import {
|
|
135
|
+
acknowledgeExecution,
|
|
136
|
+
claimExecution,
|
|
137
|
+
createExecutionState,
|
|
138
|
+
createStartedExecution,
|
|
139
|
+
executionIdConflict,
|
|
140
|
+
executionNotFound,
|
|
141
|
+
isExecutionClaimable,
|
|
142
|
+
matchStartRequest,
|
|
143
|
+
prepareExecutionAdmission,
|
|
144
|
+
prepareExecutionStart,
|
|
145
|
+
releaseExecutionLease,
|
|
146
|
+
renewExecutionLease,
|
|
147
|
+
requestExecutionWake,
|
|
148
|
+
toExecutionRecord
|
|
149
|
+
} from "@yieldstar/core";
|
|
150
|
+
|
|
151
|
+
// src/dao/executions-dao.ts
|
|
152
|
+
import { nextClaimAt } from "@yieldstar/core";
|
|
153
|
+
class ExecutionsDao {
|
|
154
|
+
db;
|
|
155
|
+
constructor(db) {
|
|
156
|
+
this.db = db;
|
|
157
|
+
this.setupDb();
|
|
158
|
+
}
|
|
159
|
+
setupDb() {
|
|
160
|
+
this.db.run(`
|
|
161
|
+
CREATE TABLE IF NOT EXISTS executions (
|
|
162
|
+
execution_id TEXT PRIMARY KEY,
|
|
163
|
+
workflow_id TEXT NOT NULL,
|
|
164
|
+
params TEXT NOT NULL,
|
|
165
|
+
context TEXT NOT NULL,
|
|
166
|
+
status TEXT NOT NULL,
|
|
167
|
+
outcome TEXT,
|
|
168
|
+
admitted_at INTEGER NOT NULL,
|
|
169
|
+
updated_at INTEGER NOT NULL,
|
|
170
|
+
due_at INTEGER,
|
|
171
|
+
claimed_due_at INTEGER,
|
|
172
|
+
lease_token INTEGER NOT NULL DEFAULT 0,
|
|
173
|
+
lease_expires_at INTEGER,
|
|
174
|
+
unacknowledged_delivery_count INTEGER NOT NULL DEFAULT 0,
|
|
175
|
+
redeliver_at INTEGER,
|
|
176
|
+
next_claim_at INTEGER,
|
|
177
|
+
parent_execution_id TEXT,
|
|
178
|
+
parent_step_key TEXT
|
|
179
|
+
);
|
|
180
|
+
CREATE INDEX IF NOT EXISTS idx_executions_next_claim_at
|
|
181
|
+
ON executions(next_claim_at) WHERE next_claim_at IS NOT NULL;
|
|
182
|
+
`);
|
|
183
|
+
}
|
|
184
|
+
getState(executionId) {
|
|
185
|
+
const row = this.db.query(`SELECT * FROM executions WHERE execution_id = $executionId`).get({ $executionId: executionId });
|
|
186
|
+
return row ? toState(row) : undefined;
|
|
187
|
+
}
|
|
188
|
+
getClaimableIds(now, limit, executionIds) {
|
|
189
|
+
return this.db.query(`SELECT execution_id FROM executions
|
|
190
|
+
WHERE next_claim_at IS NOT NULL AND next_claim_at <= $now
|
|
191
|
+
AND ($executionIds IS NULL
|
|
192
|
+
OR execution_id IN (SELECT value FROM json_each($executionIds)))
|
|
193
|
+
ORDER BY next_claim_at, execution_id
|
|
194
|
+
LIMIT $limit`).all({
|
|
195
|
+
$now: now,
|
|
196
|
+
$limit: limit,
|
|
197
|
+
$executionIds: executionIds ? JSON.stringify(executionIds) : null
|
|
198
|
+
}).map((row) => row.execution_id);
|
|
199
|
+
}
|
|
200
|
+
insertState(state) {
|
|
201
|
+
const result = this.db.query(`INSERT INTO executions (
|
|
202
|
+
execution_id, workflow_id, params, context, status, outcome,
|
|
203
|
+
admitted_at, updated_at, due_at, claimed_due_at, lease_token,
|
|
204
|
+
lease_expires_at, unacknowledged_delivery_count, redeliver_at,
|
|
205
|
+
next_claim_at, parent_execution_id, parent_step_key
|
|
206
|
+
) VALUES (
|
|
207
|
+
$executionId, $workflowId, $params, $context, $status, $outcome,
|
|
208
|
+
$admittedAt, $updatedAt, $dueAt, $claimedDueAt, $leaseToken,
|
|
209
|
+
$leaseExpiresAt, $unacknowledgedDeliveryCount, $redeliverAt,
|
|
210
|
+
$nextClaimAt, $parentExecutionId, $parentStepKey
|
|
211
|
+
)
|
|
212
|
+
ON CONFLICT(execution_id) DO NOTHING`).run({
|
|
213
|
+
...toBindings(state),
|
|
214
|
+
$parentExecutionId: state.parent?.executionId ?? null,
|
|
215
|
+
$parentStepKey: state.parent?.stepKey ?? null
|
|
216
|
+
});
|
|
217
|
+
return Number(result.changes) === 1;
|
|
218
|
+
}
|
|
219
|
+
updateState(state) {
|
|
220
|
+
this.db.query(`UPDATE executions SET
|
|
221
|
+
workflow_id = $workflowId,
|
|
222
|
+
params = $params,
|
|
223
|
+
context = $context,
|
|
224
|
+
status = $status,
|
|
225
|
+
outcome = $outcome,
|
|
226
|
+
admitted_at = $admittedAt,
|
|
227
|
+
updated_at = $updatedAt,
|
|
228
|
+
due_at = $dueAt,
|
|
229
|
+
claimed_due_at = $claimedDueAt,
|
|
230
|
+
lease_token = $leaseToken,
|
|
231
|
+
lease_expires_at = $leaseExpiresAt,
|
|
232
|
+
unacknowledged_delivery_count = $unacknowledgedDeliveryCount,
|
|
233
|
+
redeliver_at = $redeliverAt,
|
|
234
|
+
next_claim_at = $nextClaimAt
|
|
235
|
+
WHERE execution_id = $executionId`).run(toBindings(state));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function toState(row) {
|
|
239
|
+
return {
|
|
240
|
+
executionId: row.execution_id,
|
|
241
|
+
workflowId: row.workflow_id,
|
|
242
|
+
params: row.params,
|
|
243
|
+
context: row.context,
|
|
244
|
+
status: row.status,
|
|
245
|
+
outcome: row.outcome,
|
|
246
|
+
admittedAt: Number(row.admitted_at),
|
|
247
|
+
updatedAt: Number(row.updated_at),
|
|
248
|
+
dueAt: toNumberOrNull(row.due_at),
|
|
249
|
+
claimedDueAt: toNumberOrNull(row.claimed_due_at),
|
|
250
|
+
leaseToken: Number(row.lease_token),
|
|
251
|
+
leaseExpiresAt: toNumberOrNull(row.lease_expires_at),
|
|
252
|
+
unacknowledgedDeliveryCount: Number(row.unacknowledged_delivery_count),
|
|
253
|
+
redeliverAt: toNumberOrNull(row.redeliver_at),
|
|
254
|
+
parent: row.parent_execution_id !== null && row.parent_step_key !== null ? { executionId: row.parent_execution_id, stepKey: row.parent_step_key } : null
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function toBindings(state) {
|
|
258
|
+
return {
|
|
259
|
+
$executionId: state.executionId,
|
|
260
|
+
$workflowId: state.workflowId,
|
|
261
|
+
$params: state.params,
|
|
262
|
+
$context: state.context,
|
|
263
|
+
$status: state.status,
|
|
264
|
+
$outcome: state.outcome,
|
|
265
|
+
$admittedAt: state.admittedAt,
|
|
266
|
+
$updatedAt: state.updatedAt,
|
|
267
|
+
$dueAt: state.dueAt,
|
|
268
|
+
$claimedDueAt: state.claimedDueAt,
|
|
269
|
+
$leaseToken: state.leaseToken,
|
|
270
|
+
$leaseExpiresAt: state.leaseExpiresAt,
|
|
271
|
+
$unacknowledgedDeliveryCount: state.unacknowledgedDeliveryCount,
|
|
272
|
+
$redeliverAt: state.redeliverAt,
|
|
273
|
+
$nextClaimAt: nextClaimAt(state)
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function toNumberOrNull(value) {
|
|
277
|
+
return value === null ? null : Number(value);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/dao/start-requests-dao.ts
|
|
281
|
+
class StartRequestsDao {
|
|
282
|
+
db;
|
|
283
|
+
constructor(db) {
|
|
284
|
+
this.db = db;
|
|
285
|
+
this.setupDb();
|
|
286
|
+
}
|
|
287
|
+
setupDb() {
|
|
288
|
+
this.db.run(`
|
|
289
|
+
CREATE TABLE IF NOT EXISTS start_requests (
|
|
290
|
+
caller_execution_id TEXT NOT NULL,
|
|
291
|
+
step_key TEXT NOT NULL,
|
|
292
|
+
workflow_id TEXT NOT NULL,
|
|
293
|
+
params TEXT,
|
|
294
|
+
execution_id TEXT NOT NULL,
|
|
295
|
+
PRIMARY KEY (caller_execution_id, step_key)
|
|
296
|
+
);
|
|
297
|
+
`);
|
|
298
|
+
}
|
|
299
|
+
get(callerExecutionId, stepKey) {
|
|
300
|
+
const row = this.db.query(`SELECT * FROM start_requests
|
|
301
|
+
WHERE caller_execution_id = $callerExecutionId AND step_key = $stepKey`).get({ $callerExecutionId: callerExecutionId, $stepKey: stepKey });
|
|
302
|
+
return row ? {
|
|
303
|
+
callerExecutionId: row.caller_execution_id,
|
|
304
|
+
stepKey: row.step_key,
|
|
305
|
+
workflowId: row.workflow_id,
|
|
306
|
+
params: row.params,
|
|
307
|
+
executionId: row.execution_id
|
|
308
|
+
} : undefined;
|
|
309
|
+
}
|
|
310
|
+
insert(request) {
|
|
311
|
+
this.db.query(`INSERT INTO start_requests
|
|
312
|
+
(caller_execution_id, step_key, workflow_id, params, execution_id)
|
|
313
|
+
VALUES
|
|
314
|
+
($callerExecutionId, $stepKey, $workflowId, $params, $executionId)`).run({
|
|
315
|
+
$callerExecutionId: request.callerExecutionId,
|
|
316
|
+
$stepKey: request.stepKey,
|
|
317
|
+
$workflowId: request.workflowId,
|
|
318
|
+
$params: request.params,
|
|
319
|
+
$executionId: request.executionId
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/sqlite-transaction.ts
|
|
325
|
+
function runTransaction(db, fn) {
|
|
326
|
+
db.run("BEGIN IMMEDIATE");
|
|
327
|
+
try {
|
|
328
|
+
const result = fn();
|
|
329
|
+
db.run("COMMIT");
|
|
330
|
+
return result;
|
|
331
|
+
} catch (err) {
|
|
332
|
+
db.run("ROLLBACK");
|
|
333
|
+
throw err;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/sqlite-execution.ts
|
|
338
|
+
class SqliteExecutionClient2 {
|
|
339
|
+
db;
|
|
340
|
+
executionsDao;
|
|
341
|
+
startRequestsDao;
|
|
342
|
+
clock;
|
|
343
|
+
constructor(db, options = {}) {
|
|
344
|
+
this.db = db;
|
|
345
|
+
this.executionsDao = new ExecutionsDao(db);
|
|
346
|
+
this.startRequestsDao = new StartRequestsDao(db);
|
|
347
|
+
this.clock = options.clock ?? (() => Date.now());
|
|
348
|
+
}
|
|
349
|
+
async admit(request) {
|
|
350
|
+
const prepared = prepareExecutionAdmission(request);
|
|
351
|
+
if (!prepared.ok) {
|
|
352
|
+
return {
|
|
353
|
+
status: "rejected",
|
|
354
|
+
code: prepared.code,
|
|
355
|
+
message: prepared.message
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
return runTransaction(this.db, () => {
|
|
359
|
+
const state = createExecutionState({
|
|
360
|
+
executionId: prepared.executionId,
|
|
361
|
+
workflowId: request.workflowId,
|
|
362
|
+
encoded: prepared.encoded,
|
|
363
|
+
now: this.clock()
|
|
364
|
+
});
|
|
365
|
+
if (!this.executionsDao.insertState(state)) {
|
|
366
|
+
return executionIdConflict(prepared.executionId);
|
|
367
|
+
}
|
|
368
|
+
return { status: "accepted", executionId: prepared.executionId };
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
async start(request) {
|
|
372
|
+
const prepared = prepareExecutionStart(request);
|
|
373
|
+
if (!prepared.ok)
|
|
374
|
+
return prepared.result;
|
|
375
|
+
const { start } = prepared;
|
|
376
|
+
return runTransaction(this.db, () => {
|
|
377
|
+
const recorded = this.startRequestsDao.get(start.callerExecutionId, start.stepKey);
|
|
378
|
+
if (recorded)
|
|
379
|
+
return matchStartRequest(recorded, start);
|
|
380
|
+
const caller = this.executionsDao.getState(start.callerExecutionId);
|
|
381
|
+
if (!caller) {
|
|
382
|
+
throw new Error(`Cannot start a workflow from execution "${start.callerExecutionId}": it does not exist`);
|
|
383
|
+
}
|
|
384
|
+
let created = createStartedExecution({ start, caller, now: this.clock() });
|
|
385
|
+
while (!this.executionsDao.insertState(created.target)) {
|
|
386
|
+
created = createStartedExecution({ start, caller, now: this.clock() });
|
|
387
|
+
}
|
|
388
|
+
this.startRequestsDao.insert(created.startRequest);
|
|
389
|
+
return {
|
|
390
|
+
status: "accepted",
|
|
391
|
+
executionId: created.target.executionId
|
|
392
|
+
};
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
async get(executionId) {
|
|
396
|
+
const state = this.executionsDao.getState(executionId);
|
|
397
|
+
return state ? toExecutionRecord(state, this.clock()) : undefined;
|
|
398
|
+
}
|
|
399
|
+
async requestWake(request) {
|
|
400
|
+
return runTransaction(this.db, () => {
|
|
401
|
+
const state = this.executionsDao.getState(request.executionId);
|
|
402
|
+
if (!state)
|
|
403
|
+
return executionNotFound(request.executionId);
|
|
404
|
+
const next = requestExecutionWake(state, request.wakeAt, this.clock());
|
|
405
|
+
if (!next)
|
|
406
|
+
return { status: "ignored" };
|
|
407
|
+
this.executionsDao.updateState(next);
|
|
408
|
+
return { status: "requested" };
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
async claimDue(request) {
|
|
412
|
+
if (request.limit <= 0 || request.executionIds?.length === 0)
|
|
413
|
+
return [];
|
|
414
|
+
return runTransaction(this.db, () => {
|
|
415
|
+
const now = this.clock();
|
|
416
|
+
const leases = [];
|
|
417
|
+
for (const executionId of this.executionsDao.getClaimableIds(now, request.limit, request.executionIds)) {
|
|
418
|
+
const state = this.executionsDao.getState(executionId);
|
|
419
|
+
if (!state || !isExecutionClaimable(state, now))
|
|
420
|
+
continue;
|
|
421
|
+
const claimed = claimExecution(state, {
|
|
422
|
+
now,
|
|
423
|
+
duration: request.duration
|
|
424
|
+
});
|
|
425
|
+
this.executionsDao.updateState(claimed.state);
|
|
426
|
+
leases.push(claimed.lease);
|
|
427
|
+
}
|
|
428
|
+
return leases;
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
async renew(request) {
|
|
432
|
+
return this.transition(request.lease, (state) => renewExecutionLease(state, {
|
|
433
|
+
lease: request.lease,
|
|
434
|
+
duration: request.duration,
|
|
435
|
+
now: this.clock()
|
|
436
|
+
}));
|
|
437
|
+
}
|
|
438
|
+
async release(request) {
|
|
439
|
+
return this.transition(request.lease, (state) => releaseExecutionLease(state, { lease: request.lease, now: this.clock() }));
|
|
440
|
+
}
|
|
441
|
+
async acknowledge(request) {
|
|
442
|
+
return this.transition(request.lease, (state) => acknowledgeExecution(state, {
|
|
443
|
+
lease: request.lease,
|
|
444
|
+
outcome: request.outcome,
|
|
445
|
+
now: this.clock()
|
|
446
|
+
}));
|
|
447
|
+
}
|
|
448
|
+
transition(lease, fn) {
|
|
449
|
+
return Promise.resolve(runTransaction(this.db, () => {
|
|
450
|
+
const state = this.executionsDao.getState(lease.executionId);
|
|
451
|
+
if (!state)
|
|
452
|
+
return { status: "lost" };
|
|
453
|
+
const next = fn(state);
|
|
454
|
+
if (next.state !== state)
|
|
455
|
+
this.executionsDao.updateState(next.state);
|
|
456
|
+
return next.result;
|
|
457
|
+
}));
|
|
458
|
+
}
|
|
459
|
+
wakeInTransaction(executionId, wakeAt = this.clock()) {
|
|
460
|
+
const state = this.executionsDao.getState(executionId);
|
|
461
|
+
if (!state)
|
|
462
|
+
return;
|
|
463
|
+
const next = requestExecutionWake(state, wakeAt, this.clock());
|
|
464
|
+
if (next)
|
|
465
|
+
this.executionsDao.updateState(next);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// src/sqlite-effect.ts
|
|
470
|
+
class SqliteEffectClient2 {
|
|
471
|
+
db;
|
|
472
|
+
executions;
|
|
473
|
+
effectsDao;
|
|
474
|
+
effectWaitersDao;
|
|
475
|
+
clock;
|
|
476
|
+
constructor(params) {
|
|
477
|
+
this.db = params.db;
|
|
478
|
+
this.clock = params.clock ?? (() => Date.now());
|
|
479
|
+
this.executions = params.executions ?? new SqliteExecutionClient2(params.db, { clock: this.clock });
|
|
480
|
+
this.effectsDao = new EffectsDao(params.db);
|
|
481
|
+
this.effectWaitersDao = new EffectWaitersDao(params.db);
|
|
482
|
+
}
|
|
483
|
+
async begin(request) {
|
|
484
|
+
const begin = prepareEffectBegin(request);
|
|
485
|
+
return runTransaction(this.db, () => {
|
|
486
|
+
const state = this.effectsDao.getState(begin.effectId);
|
|
487
|
+
if (state)
|
|
488
|
+
return matchEffectBegin(state, begin);
|
|
489
|
+
this.effectsDao.insertState(createEffectState(begin));
|
|
490
|
+
return { status: "new" };
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
async settle(request) {
|
|
494
|
+
const settle = prepareEffectSettle(request);
|
|
495
|
+
return runTransaction(this.db, () => {
|
|
496
|
+
const state = this.effectsDao.getState(settle.effectId);
|
|
497
|
+
if (!state)
|
|
498
|
+
throw effectNotFound(settle.effectId);
|
|
499
|
+
const settled = settleEffect(state, settle, this.clock());
|
|
500
|
+
if (settled.state !== state)
|
|
501
|
+
this.effectsDao.updateState(state, settled.state);
|
|
502
|
+
if (settled.wake) {
|
|
503
|
+
for (const executionId of this.effectWaitersDao.getWaitingExecutionIds(settle.effectId)) {
|
|
504
|
+
this.executions.wakeInTransaction(executionId);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return settled.result;
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
async watch(request) {
|
|
511
|
+
const watch = prepareEffectWatch(request);
|
|
512
|
+
runTransaction(this.db, () => {
|
|
513
|
+
const state = this.effectsDao.getState(watch.effectId);
|
|
514
|
+
if (!state)
|
|
515
|
+
throw effectNotFound(watch.effectId);
|
|
516
|
+
this.effectWaitersDao.upsertWaiter(watch);
|
|
517
|
+
if (state.status === "settled") {
|
|
518
|
+
this.executions.wakeInTransaction(watch.executionId);
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
async getEffect(effectId) {
|
|
523
|
+
const state = this.effectsDao.getState(effectId);
|
|
524
|
+
return state ? toEffectRecord(state) : undefined;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// src/sqlite-heap.ts
|
|
529
|
+
import {
|
|
530
|
+
isLeaseCurrent
|
|
531
|
+
} from "@yieldstar/core";
|
|
532
|
+
|
|
533
|
+
// src/dao/step-response-dao.ts
|
|
534
|
+
class StepResponsesDao {
|
|
535
|
+
db;
|
|
536
|
+
constructor(db) {
|
|
537
|
+
this.db = db;
|
|
538
|
+
this.setupDb();
|
|
539
|
+
}
|
|
540
|
+
setupDb() {
|
|
541
|
+
this.db.run(`
|
|
542
|
+
CREATE TABLE IF NOT EXISTS step_responses (
|
|
543
|
+
execution_id TEXT NOT NULL,
|
|
544
|
+
step_key TEXT NOT NULL,
|
|
545
|
+
step_attempt INTEGER NOT NULL,
|
|
546
|
+
step_done BOOL NOT NULL,
|
|
547
|
+
step_response TEXT NOT NULL,
|
|
548
|
+
PRIMARY KEY (execution_id, step_key, step_attempt)
|
|
549
|
+
);
|
|
550
|
+
CREATE INDEX IF NOT EXISTS idx_execution_step_attempt
|
|
551
|
+
ON step_responses(execution_id, step_key, step_attempt DESC);
|
|
552
|
+
`);
|
|
553
|
+
}
|
|
554
|
+
getAllSteps() {
|
|
555
|
+
const query = this.db.query(`SELECT * FROM step_responses`);
|
|
556
|
+
return query.all();
|
|
557
|
+
}
|
|
558
|
+
deleteAll() {
|
|
559
|
+
const query = this.db.query(`DELETE FROM step_responses`);
|
|
560
|
+
query.run();
|
|
561
|
+
}
|
|
562
|
+
getLatestStepResponse(executionId, stepKey) {
|
|
563
|
+
const query = this.db.query(`SELECT * FROM step_responses
|
|
564
|
+
WHERE execution_id = $executionId
|
|
565
|
+
AND step_key = $stepKey
|
|
566
|
+
ORDER BY step_attempt DESC LIMIT 1`);
|
|
567
|
+
const result = query.get({
|
|
568
|
+
$executionId: executionId,
|
|
569
|
+
$stepKey: stepKey
|
|
570
|
+
});
|
|
571
|
+
return result;
|
|
572
|
+
}
|
|
573
|
+
getStepResponse(executionId, stepKey, stepAttempt) {
|
|
574
|
+
return this.db.query(`SELECT * FROM step_responses
|
|
575
|
+
WHERE execution_id = $executionId
|
|
576
|
+
AND step_key = $stepKey
|
|
577
|
+
AND step_attempt = $stepAttempt`).get({
|
|
578
|
+
$executionId: executionId,
|
|
579
|
+
$stepKey: stepKey,
|
|
580
|
+
$stepAttempt: stepAttempt
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
insertStepResponse(executionId, stepKey, stepAttempt, stepDone, stepResponseJson) {
|
|
584
|
+
const query = this.db.query(`INSERT INTO step_responses (execution_id, step_key, step_attempt, step_done, step_response)
|
|
585
|
+
VALUES ($executionId, $stepKey, $stepAttempt, $stepDone, $stepResponse)`);
|
|
586
|
+
query.run({
|
|
587
|
+
$executionId: executionId,
|
|
588
|
+
$stepKey: stepKey,
|
|
589
|
+
$stepAttempt: stepAttempt,
|
|
590
|
+
$stepDone: stepDone,
|
|
591
|
+
$stepResponse: stepResponseJson
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// src/dao/store-waiters-dao.ts
|
|
597
|
+
class StoreWaitersDao {
|
|
598
|
+
db;
|
|
599
|
+
constructor(db) {
|
|
600
|
+
this.db = db;
|
|
601
|
+
this.setupDb();
|
|
602
|
+
}
|
|
603
|
+
setupDb() {
|
|
604
|
+
this.db.run(`
|
|
605
|
+
CREATE TABLE IF NOT EXISTS store_waiters (
|
|
606
|
+
workflow_id TEXT NOT NULL,
|
|
607
|
+
execution_id TEXT NOT NULL,
|
|
608
|
+
step_key TEXT NOT NULL,
|
|
609
|
+
store_name TEXT NOT NULL,
|
|
610
|
+
store_id TEXT NOT NULL,
|
|
611
|
+
since_version INTEGER NOT NULL,
|
|
612
|
+
read_paths TEXT NOT NULL,
|
|
613
|
+
PRIMARY KEY (store_name, store_id, execution_id, step_key)
|
|
614
|
+
);
|
|
615
|
+
CREATE INDEX IF NOT EXISTS idx_store_waiters_step
|
|
616
|
+
ON store_waiters(execution_id, step_key);
|
|
617
|
+
`);
|
|
618
|
+
}
|
|
619
|
+
upsertWaiter(waiter) {
|
|
620
|
+
this.db.query(`INSERT INTO store_waiters
|
|
621
|
+
(workflow_id, execution_id, step_key, store_name, store_id, since_version, read_paths)
|
|
622
|
+
VALUES
|
|
623
|
+
($workflowId, $executionId, $stepKey, $storeName, $storeId, $sinceVersion, $readPaths)
|
|
624
|
+
ON CONFLICT(store_name, store_id, execution_id, step_key)
|
|
625
|
+
DO UPDATE SET
|
|
626
|
+
workflow_id = excluded.workflow_id,
|
|
627
|
+
since_version = excluded.since_version,
|
|
628
|
+
read_paths = excluded.read_paths`).run({
|
|
629
|
+
$workflowId: waiter.workflowId,
|
|
630
|
+
$executionId: waiter.executionId,
|
|
631
|
+
$stepKey: waiter.stepKey,
|
|
632
|
+
$storeName: waiter.storeName,
|
|
633
|
+
$storeId: waiter.storeId,
|
|
634
|
+
$sinceVersion: waiter.sinceVersion,
|
|
635
|
+
$readPaths: JSON.stringify(waiter.readPaths)
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
getStoreWaiters(storeName, storeId) {
|
|
639
|
+
return this.db.query(`SELECT * FROM store_waiters
|
|
640
|
+
WHERE store_name = $storeName AND store_id = $storeId`).all({ $storeName: storeName, $storeId: storeId });
|
|
641
|
+
}
|
|
642
|
+
deleteStoreWaiters(storeName, storeId) {
|
|
643
|
+
this.db.query(`DELETE FROM store_waiters WHERE store_name = $storeName AND store_id = $storeId`).run({ $storeName: storeName, $storeId: storeId });
|
|
644
|
+
}
|
|
645
|
+
deleteStepWaiters(executionId, stepKey) {
|
|
646
|
+
this.db.query(`DELETE FROM store_waiters WHERE execution_id = $executionId AND step_key = $stepKey`).run({ $executionId: executionId, $stepKey: stepKey });
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// src/sqlite-heap.ts
|
|
651
|
+
class SqliteHeapClient2 {
|
|
652
|
+
db;
|
|
653
|
+
executionsDao;
|
|
654
|
+
stepResponsesDao;
|
|
655
|
+
storeWaitersDao;
|
|
656
|
+
effectWaitersDao;
|
|
657
|
+
constructor(db) {
|
|
658
|
+
this.db = db;
|
|
659
|
+
this.executionsDao = new ExecutionsDao(db);
|
|
660
|
+
this.stepResponsesDao = new StepResponsesDao(db);
|
|
661
|
+
this.storeWaitersDao = new StoreWaitersDao(db);
|
|
662
|
+
this.effectWaitersDao = new EffectWaitersDao(db);
|
|
663
|
+
}
|
|
664
|
+
async getAllSteps() {
|
|
665
|
+
return this.stepResponsesDao.getAllSteps();
|
|
666
|
+
}
|
|
667
|
+
async deleteAll() {
|
|
668
|
+
this.stepResponsesDao.deleteAll();
|
|
669
|
+
}
|
|
670
|
+
async readStep(params) {
|
|
671
|
+
const result = this.stepResponsesDao.getLatestStepResponse(params.executionId, params.stepKey);
|
|
672
|
+
if (!result) {
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
return {
|
|
676
|
+
stepResponseJson: result.step_response,
|
|
677
|
+
meta: {
|
|
678
|
+
attempt: result.step_attempt,
|
|
679
|
+
done: Boolean(result.step_done)
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
async writeStep(params) {
|
|
684
|
+
const { lease, stepKey, stepAttempt, done, stepResponseJson } = params;
|
|
685
|
+
const { executionId } = lease;
|
|
686
|
+
return runTransaction(this.db, () => {
|
|
687
|
+
const state = this.executionsDao.getState(executionId);
|
|
688
|
+
if (!state || !isLeaseCurrent(state, lease)) {
|
|
689
|
+
return { status: "lost" };
|
|
690
|
+
}
|
|
691
|
+
const existing = this.stepResponsesDao.getStepResponse(executionId, stepKey, stepAttempt);
|
|
692
|
+
if (existing) {
|
|
693
|
+
return {
|
|
694
|
+
stepResponseJson: existing.step_response,
|
|
695
|
+
meta: { attempt: existing.step_attempt, done: Boolean(existing.step_done) }
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
this.stepResponsesDao.insertStepResponse(executionId, stepKey, stepAttempt, done, stepResponseJson);
|
|
699
|
+
if (done) {
|
|
700
|
+
this.storeWaitersDao.deleteStepWaiters(executionId, stepKey);
|
|
701
|
+
this.effectWaitersDao.deleteStepWaiters(executionId, stepKey);
|
|
702
|
+
}
|
|
703
|
+
return { stepResponseJson, meta: { attempt: stepAttempt, done } };
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// src/sqlite-store.ts
|
|
709
|
+
import { v7 as uuidv7 } from "uuid";
|
|
710
|
+
import {
|
|
711
|
+
cloneStoreState,
|
|
712
|
+
CasStoreClient,
|
|
713
|
+
decodeStoreValue,
|
|
714
|
+
encodeStoreValue,
|
|
715
|
+
storePathsIntersect
|
|
716
|
+
} from "@yieldstar/core";
|
|
717
|
+
class SqliteStoreClient2 extends CasStoreClient {
|
|
718
|
+
db;
|
|
719
|
+
executions;
|
|
720
|
+
storeWaitersDao;
|
|
721
|
+
constructor(params) {
|
|
722
|
+
super();
|
|
723
|
+
this.db = params.db;
|
|
724
|
+
this.executions = params.executions ?? new SqliteExecutionClient2(params.db);
|
|
725
|
+
this.setupDb();
|
|
726
|
+
this.storeWaitersDao = new StoreWaitersDao(params.db);
|
|
727
|
+
}
|
|
728
|
+
createInstanceId() {
|
|
729
|
+
return uuidv7();
|
|
730
|
+
}
|
|
731
|
+
async readStore(params) {
|
|
732
|
+
const row = this.getStoreRow(params.definition.name, params.id);
|
|
733
|
+
return row ? toSnapshot(row) : undefined;
|
|
734
|
+
}
|
|
735
|
+
async insertStore(params) {
|
|
736
|
+
return runTransaction(this.db, () => {
|
|
737
|
+
const row = this.getStoreRow(params.definition.name, params.id);
|
|
738
|
+
if (row)
|
|
739
|
+
return toSnapshot(row);
|
|
740
|
+
this.insertStoreRow({
|
|
741
|
+
storeName: params.definition.name,
|
|
742
|
+
storeId: params.id,
|
|
743
|
+
instanceId: params.snapshot.instanceId,
|
|
744
|
+
version: params.snapshot.version,
|
|
745
|
+
state: params.snapshot.state
|
|
746
|
+
});
|
|
747
|
+
return cloneStoreState(params.snapshot);
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
async getAppliedStoreStep(params) {
|
|
751
|
+
const row = this.getAppliedStepRow({
|
|
752
|
+
storeName: params.definition.name,
|
|
753
|
+
storeId: params.id,
|
|
754
|
+
stepId: params.stepId
|
|
755
|
+
});
|
|
756
|
+
return row ? { result: decodeStoreValue(row.result) } : undefined;
|
|
757
|
+
}
|
|
758
|
+
async commitStoreMutation(mutation) {
|
|
759
|
+
return runTransaction(this.db, () => {
|
|
760
|
+
if (mutation.stepId) {
|
|
761
|
+
const applied = this.getAppliedStepRow({
|
|
762
|
+
storeName: mutation.definition.name,
|
|
763
|
+
storeId: mutation.id,
|
|
764
|
+
stepId: mutation.stepId
|
|
765
|
+
});
|
|
766
|
+
if (applied) {
|
|
767
|
+
return {
|
|
768
|
+
status: "already-applied",
|
|
769
|
+
result: decodeStoreValue(applied.result)
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
const row = this.getStoreRow(mutation.definition.name, mutation.id);
|
|
774
|
+
if (!row && !mutation.expected.created)
|
|
775
|
+
return { status: "missing" };
|
|
776
|
+
if (row && (mutation.expected.created || row.instance_id !== mutation.expected.instanceId || row.version !== mutation.expected.version)) {
|
|
777
|
+
return { status: "conflict", snapshot: toSnapshot(row) };
|
|
778
|
+
}
|
|
779
|
+
const version = mutation.expected.version + 1;
|
|
780
|
+
if (mutation.expected.created) {
|
|
781
|
+
this.insertStoreRow({
|
|
782
|
+
storeName: mutation.definition.name,
|
|
783
|
+
storeId: mutation.id,
|
|
784
|
+
instanceId: mutation.expected.instanceId,
|
|
785
|
+
version,
|
|
786
|
+
state: mutation.nextState
|
|
787
|
+
});
|
|
788
|
+
} else {
|
|
789
|
+
const changed = this.db.query(`UPDATE stores
|
|
790
|
+
SET version = $version, state = $state
|
|
791
|
+
WHERE instance_id = $instanceId AND version = $expectedVersion`).run({
|
|
792
|
+
$instanceId: mutation.expected.instanceId,
|
|
793
|
+
$expectedVersion: mutation.expected.version,
|
|
794
|
+
$version: version,
|
|
795
|
+
$state: encodeStoreValue(mutation.nextState)
|
|
796
|
+
});
|
|
797
|
+
if (Number(changed.changes) !== 1) {
|
|
798
|
+
throw new Error("Store changed during its mutation transaction");
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (mutation.stepId) {
|
|
802
|
+
this.insertAppliedStepRow({
|
|
803
|
+
storeName: mutation.definition.name,
|
|
804
|
+
storeId: mutation.id,
|
|
805
|
+
stepId: mutation.stepId,
|
|
806
|
+
result: encodeStoreValue(mutation.result)
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
const executionIds = this.selectMatchingExecutionIds({
|
|
810
|
+
storeName: mutation.definition.name,
|
|
811
|
+
storeId: mutation.id,
|
|
812
|
+
version,
|
|
813
|
+
changedPaths: mutation.changedPaths
|
|
814
|
+
});
|
|
815
|
+
for (const executionId of executionIds) {
|
|
816
|
+
this.executions.wakeInTransaction(executionId);
|
|
817
|
+
}
|
|
818
|
+
return { status: "committed" };
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
async listStores(definition) {
|
|
822
|
+
const rows = this.db.query(`SELECT store_id FROM stores
|
|
823
|
+
WHERE store_name = $storeName
|
|
824
|
+
ORDER BY store_id ASC`).all({ $storeName: definition.name });
|
|
825
|
+
return rows.map((row) => row.store_id);
|
|
826
|
+
}
|
|
827
|
+
async deleteStore(params) {
|
|
828
|
+
runTransaction(this.db, () => {
|
|
829
|
+
this.db.query(`DELETE FROM stores WHERE store_name = $storeName AND store_id = $storeId`).run({
|
|
830
|
+
$storeName: params.definition.name,
|
|
831
|
+
$storeId: params.id
|
|
832
|
+
});
|
|
833
|
+
this.storeWaitersDao.deleteStoreWaiters(params.definition.name, params.id);
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
async commitStoreDeletion(deletion) {
|
|
837
|
+
return runTransaction(this.db, () => {
|
|
838
|
+
if (deletion.stepId) {
|
|
839
|
+
const applied = this.getAppliedStepRow({
|
|
840
|
+
storeName: deletion.definition.name,
|
|
841
|
+
storeId: deletion.id,
|
|
842
|
+
stepId: deletion.stepId
|
|
843
|
+
});
|
|
844
|
+
if (applied) {
|
|
845
|
+
return {
|
|
846
|
+
status: "already-applied",
|
|
847
|
+
result: decodeStoreValue(applied.result)
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
const row = this.getStoreRow(deletion.definition.name, deletion.id);
|
|
852
|
+
if (!row)
|
|
853
|
+
return { status: "missing" };
|
|
854
|
+
if (row.instance_id !== deletion.expected.instanceId || row.version !== deletion.expected.version) {
|
|
855
|
+
return { status: "conflict", snapshot: toSnapshot(row) };
|
|
856
|
+
}
|
|
857
|
+
this.storeWaitersDao.deleteStoreWaiters(deletion.definition.name, deletion.id);
|
|
858
|
+
this.db.query(`DELETE FROM stores WHERE instance_id = $instanceId`).run({ $instanceId: row.instance_id });
|
|
859
|
+
if (deletion.stepId) {
|
|
860
|
+
this.insertAppliedStepRow({
|
|
861
|
+
storeName: deletion.definition.name,
|
|
862
|
+
storeId: deletion.id,
|
|
863
|
+
stepId: deletion.stepId,
|
|
864
|
+
result: encodeStoreValue(deletion.result)
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
return { status: "committed" };
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
async registerWaiter(waiter) {
|
|
871
|
+
runTransaction(this.db, () => {
|
|
872
|
+
this.storeWaitersDao.upsertWaiter(waiter);
|
|
873
|
+
const row = this.getStoreRow(waiter.storeName, waiter.storeId);
|
|
874
|
+
if (!row || row.instance_id !== waiter.instanceId || row.version > waiter.sinceVersion) {
|
|
875
|
+
this.executions.wakeInTransaction(waiter.executionId);
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
setupDb() {
|
|
880
|
+
this.createStoresTable();
|
|
881
|
+
this.db.run(`
|
|
882
|
+
CREATE TABLE IF NOT EXISTS store_applied_steps (
|
|
883
|
+
store_name TEXT NOT NULL,
|
|
884
|
+
store_id TEXT NOT NULL,
|
|
885
|
+
execution_id TEXT NOT NULL,
|
|
886
|
+
step_key TEXT NOT NULL,
|
|
887
|
+
result TEXT NOT NULL,
|
|
888
|
+
PRIMARY KEY (store_name, store_id, execution_id, step_key)
|
|
889
|
+
);
|
|
890
|
+
`);
|
|
891
|
+
}
|
|
892
|
+
createStoresTable() {
|
|
893
|
+
this.db.run(`
|
|
894
|
+
CREATE TABLE IF NOT EXISTS stores (
|
|
895
|
+
instance_id TEXT PRIMARY KEY,
|
|
896
|
+
store_name TEXT NOT NULL,
|
|
897
|
+
store_id TEXT NOT NULL,
|
|
898
|
+
version INTEGER NOT NULL,
|
|
899
|
+
state TEXT NOT NULL,
|
|
900
|
+
UNIQUE (store_name, store_id)
|
|
901
|
+
)
|
|
902
|
+
`);
|
|
903
|
+
}
|
|
904
|
+
getAppliedStepRow(params) {
|
|
905
|
+
return this.db.query(`SELECT result FROM store_applied_steps
|
|
906
|
+
WHERE store_name = $storeName
|
|
907
|
+
AND store_id = $storeId
|
|
908
|
+
AND execution_id = $executionId
|
|
909
|
+
AND step_key = $stepKey`).get({
|
|
910
|
+
$storeName: params.storeName,
|
|
911
|
+
$storeId: params.storeId,
|
|
912
|
+
$executionId: params.stepId.executionId,
|
|
913
|
+
$stepKey: params.stepId.stepKey
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
insertAppliedStepRow(params) {
|
|
917
|
+
this.db.query(`INSERT INTO store_applied_steps
|
|
918
|
+
(store_name, store_id, execution_id, step_key, result)
|
|
919
|
+
VALUES
|
|
920
|
+
($storeName, $storeId, $executionId, $stepKey, $result)`).run({
|
|
921
|
+
$storeName: params.storeName,
|
|
922
|
+
$storeId: params.storeId,
|
|
923
|
+
$executionId: params.stepId.executionId,
|
|
924
|
+
$stepKey: params.stepId.stepKey,
|
|
925
|
+
$result: params.result
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
getStoreRow(storeName, storeId) {
|
|
929
|
+
return this.db.query(`SELECT instance_id, state, version FROM stores
|
|
930
|
+
WHERE store_name = $storeName AND store_id = $storeId`).get({
|
|
931
|
+
$storeName: storeName,
|
|
932
|
+
$storeId: storeId
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
selectMatchingExecutionIds(params) {
|
|
936
|
+
const executionIds = new Set;
|
|
937
|
+
const rows = this.storeWaitersDao.getStoreWaiters(params.storeName, params.storeId);
|
|
938
|
+
for (const row of rows) {
|
|
939
|
+
if (row.since_version >= params.version)
|
|
940
|
+
continue;
|
|
941
|
+
const readPaths = JSON.parse(row.read_paths);
|
|
942
|
+
if (!storePathsIntersect(readPaths, params.changedPaths))
|
|
943
|
+
continue;
|
|
944
|
+
executionIds.add(row.execution_id);
|
|
945
|
+
}
|
|
946
|
+
return executionIds;
|
|
947
|
+
}
|
|
948
|
+
insertStoreRow(params) {
|
|
949
|
+
this.db.query(`INSERT INTO stores (instance_id, store_name, store_id, version, state)
|
|
950
|
+
VALUES ($instanceId, $storeName, $storeId, $version, $state)`).run({
|
|
951
|
+
$instanceId: params.instanceId,
|
|
952
|
+
$storeName: params.storeName,
|
|
953
|
+
$storeId: params.storeId,
|
|
954
|
+
$version: params.version,
|
|
955
|
+
$state: encodeStoreValue(params.state)
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
function toSnapshot(row) {
|
|
960
|
+
return {
|
|
961
|
+
state: decodeStoreValue(row.state),
|
|
962
|
+
instanceId: row.instance_id,
|
|
963
|
+
version: row.version
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// src/sqlite-runtime.ts
|
|
968
|
+
class SqliteRuntime2 extends Runtime {
|
|
969
|
+
clock;
|
|
970
|
+
executions;
|
|
971
|
+
effects;
|
|
972
|
+
heap;
|
|
973
|
+
stores;
|
|
974
|
+
constructor(db, options = {}) {
|
|
975
|
+
super();
|
|
976
|
+
this.clock = options.clock ?? (() => Date.now());
|
|
977
|
+
this.executions = new SqliteExecutionClient2(db, { clock: this.clock });
|
|
978
|
+
this.heap = new SqliteHeapClient2(db);
|
|
979
|
+
this.stores = new SqliteStoreClient2({ db, executions: this.executions });
|
|
980
|
+
this.effects = new SqliteEffectClient2({
|
|
981
|
+
db,
|
|
982
|
+
executions: this.executions,
|
|
983
|
+
clock: this.clock
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
export { SqliteExecutionClient2, SqliteEffectClient2, SqliteHeapClient2, SqliteStoreClient2, SqliteRuntime2 };
|