@yieldstar/sqlite-runtime 0.5.0 → 0.6.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 +15 -17
- package/dist/dao/effect-waiters-dao.d.ts +19 -0
- package/dist/dao/effect-waiters-dao.d.ts.map +1 -0
- package/dist/dao/effects-dao.d.ts +17 -0
- package/dist/dao/effects-dao.d.ts.map +1 -0
- package/dist/dao/executions-dao.d.ts +23 -0
- package/dist/dao/executions-dao.d.ts.map +1 -0
- package/dist/dao/start-requests-dao.d.ts +15 -0
- package/dist/dao/start-requests-dao.d.ts.map +1 -0
- package/dist/dao/step-response-dao.d.ts +1 -0
- package/dist/dao/step-response-dao.d.ts.map +1 -1
- package/dist/dao/store-waiters-dao.d.ts +26 -0
- package/dist/dao/store-waiters-dao.d.ts.map +1 -0
- package/dist/index-083bmj5k.js +800 -0
- package/dist/index-4kybaxga.js +798 -0
- package/dist/index-a8thbaee.js +718 -0
- package/dist/index-ag93sv33.js +664 -0
- package/dist/index-eabcgmaa.js +996 -0
- package/dist/index-gjfs8rn6.js +997 -0
- package/dist/index-h3h7r4b6.js +981 -0
- package/dist/index-ma984fze.js +718 -0
- package/dist/index-ne28djfn.js +718 -0
- package/dist/index-v6rn0pr6.js +664 -0
- package/dist/index-wx6vk994.js +669 -0
- package/dist/index.d.ts +3 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -13
- package/dist/node.js +15 -17
- package/dist/sqlite-effect.d.ts +30 -0
- package/dist/sqlite-effect.d.ts.map +1 -0
- package/dist/sqlite-execution.d.ts +54 -0
- package/dist/sqlite-execution.d.ts.map +1 -0
- package/dist/sqlite-heap.d.ts +12 -4
- package/dist/sqlite-heap.d.ts.map +1 -1
- package/dist/sqlite-runtime.d.ts +22 -0
- package/dist/sqlite-runtime.d.ts.map +1 -0
- package/dist/sqlite-store.d.ts +31 -25
- package/dist/sqlite-store.d.ts.map +1 -1
- package/dist/sqlite-transaction.d.ts +9 -0
- package/dist/sqlite-transaction.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/dao/effect-waiters-dao.ts +63 -0
- package/src/dao/effects-dao.ts +123 -0
- package/src/dao/executions-dao.ts +188 -0
- package/src/dao/start-requests-dao.ts +72 -0
- package/src/dao/step-response-dao.ts +16 -1
- package/src/dao/store-waiters-dao.ts +92 -0
- package/src/index.ts +6 -4
- package/src/sqlite-effect.ts +99 -0
- package/src/sqlite-execution.ts +238 -0
- package/src/sqlite-heap.ts +64 -12
- package/src/sqlite-runtime.ts +36 -0
- package/src/sqlite-store.ts +184 -520
- package/src/sqlite-transaction.ts +19 -0
- package/dist/dao/task-queue-dao.d.ts +0 -21
- package/dist/dao/task-queue-dao.d.ts.map +0 -1
- package/dist/dao/timers-dao.d.ts +0 -22
- package/dist/dao/timers-dao.d.ts.map +0 -1
- package/dist/index-4vq70w0r.js +0 -796
- package/dist/index-s8wqgfwt.js +0 -19
- package/dist/index-x891hxwg.js +0 -796
- package/dist/node-driver.d.ts +0 -10
- package/dist/node-driver.d.ts.map +0 -1
- package/dist/sqlite-event-loop.d.ts +0 -18
- package/dist/sqlite-event-loop.d.ts.map +0 -1
- package/dist/sqlite-scheduler.d.ts +0 -13
- package/dist/sqlite-scheduler.d.ts.map +0 -1
- package/dist/sqlite-store.test.d.ts +0 -2
- package/dist/sqlite-store.test.d.ts.map +0 -1
- package/dist/sqlite-task-queue.d.ts +0 -26
- package/dist/sqlite-task-queue.d.ts.map +0 -1
- package/dist/sqlite-timers.d.ts +0 -18
- package/dist/sqlite-timers.d.ts.map +0 -1
- package/src/dao/task-queue-dao.ts +0 -79
- package/src/dao/timers-dao.ts +0 -81
- package/src/sqlite-event-loop.ts +0 -41
- package/src/sqlite-scheduler.ts +0 -24
- package/src/sqlite-task-queue.ts +0 -64
- package/src/sqlite-timers.ts +0 -40
- package/tsconfig.tsbuildinfo +0 -1
- /package/dist/{index-h7bm9xg4.js → index-dapqh99v.js} +0 -0
package/src/sqlite-store.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
import type { SchedulerClient, WorkflowEvent } from "@yieldstar/core";
|
|
2
1
|
import { v7 as uuidv7 } from "uuid";
|
|
2
|
+
import { StoreWaitersDao } from "./dao/store-waiters-dao";
|
|
3
3
|
import type { SqliteDriver } from "./sqlite-driver";
|
|
4
|
+
import { SqliteExecutionClient } from "./sqlite-execution";
|
|
5
|
+
import { runTransaction } from "./sqlite-transaction";
|
|
4
6
|
import {
|
|
5
7
|
cloneStoreState,
|
|
6
8
|
CasStoreClient,
|
|
9
|
+
decodeStoreValue,
|
|
10
|
+
encodeStoreValue,
|
|
7
11
|
storePathsIntersect,
|
|
8
|
-
validateStoreState,
|
|
9
12
|
type StandardSchemaV1,
|
|
10
13
|
type StoreDefinition,
|
|
11
|
-
type
|
|
14
|
+
type StoreDeletion,
|
|
12
15
|
type StorePath,
|
|
13
16
|
type StoreSnapshot,
|
|
14
|
-
type StoreState,
|
|
15
17
|
type StoreStepId,
|
|
16
18
|
type StoreWaiter,
|
|
17
19
|
type StoreMutation,
|
|
@@ -32,140 +34,62 @@ class StoreIdRow {
|
|
|
32
34
|
store_id!: string;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
class WakeOutboxRow {
|
|
47
|
-
store_name!: string;
|
|
48
|
-
store_id!: string;
|
|
49
|
-
execution_id!: string;
|
|
50
|
-
step_key!: string;
|
|
51
|
-
event!: string | null;
|
|
52
|
-
since_version!: number | null;
|
|
53
|
-
}
|
|
54
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Stores, waiters and the applied-steps ledger, in the same database as the
|
|
39
|
+
* execution records. A commit that satisfies waiters requests their wakes in
|
|
40
|
+
* the same transaction, so a durable commit always has durable wakes.
|
|
41
|
+
*
|
|
42
|
+
* Every write is one synchronous IMMEDIATE transaction: nothing awaits
|
|
43
|
+
* between BEGIN and COMMIT, so operations on a shared connection cannot
|
|
44
|
+
* interleave, and processes sharing the file serialize on SQLite's lock.
|
|
45
|
+
*/
|
|
55
46
|
export class SqliteStoreClient extends CasStoreClient {
|
|
56
47
|
private db: SqliteDriver;
|
|
57
|
-
private
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
48
|
+
private executions: SqliteExecutionClient;
|
|
49
|
+
private storeWaitersDao: StoreWaitersDao;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param params.executions The execution client whose records waiters wake.
|
|
53
|
+
* Defaults to one over `db` with the default clock; SqliteRuntime passes
|
|
54
|
+
* its own so both share the runtime clock.
|
|
55
|
+
*/
|
|
56
|
+
constructor(params: { db: SqliteDriver; executions?: SqliteExecutionClient }) {
|
|
62
57
|
super();
|
|
63
58
|
this.db = params.db;
|
|
64
|
-
this.
|
|
59
|
+
this.executions = params.executions ?? new SqliteExecutionClient(params.db);
|
|
65
60
|
this.setupDb();
|
|
66
|
-
|
|
67
|
-
// intents before it could enqueue them. Delivery is idempotent, so every
|
|
68
|
-
// client startup can safely resume the durable outbox.
|
|
69
|
-
void this.enqueueWrite(() => this.drainWakeOutboxBestEffort());
|
|
61
|
+
this.storeWaitersDao = new StoreWaitersDao(params.db);
|
|
70
62
|
}
|
|
71
63
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
this.writeLock = result.catch(() => {});
|
|
75
|
-
return result;
|
|
64
|
+
protected createInstanceId(): string {
|
|
65
|
+
return uuidv7();
|
|
76
66
|
}
|
|
77
67
|
|
|
78
|
-
async
|
|
79
|
-
definition: StoreDefinition
|
|
68
|
+
protected async readStore(params: {
|
|
69
|
+
definition: StoreDefinition;
|
|
80
70
|
id: string;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}): Promise<StoreSnapshot<StoreState<Schema>>> {
|
|
85
|
-
const existing = this.getStoreRow(params.definition.name, params.id);
|
|
86
|
-
|
|
87
|
-
if (existing) {
|
|
88
|
-
return {
|
|
89
|
-
state: JSON.parse(existing.state),
|
|
90
|
-
instanceId: existing.instance_id,
|
|
91
|
-
version: existing.version,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (!params.initial) {
|
|
96
|
-
throw new Error(
|
|
97
|
-
`Store "${params.definition.name}:${params.id}" does not exist`
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const initialValue = params.initial;
|
|
102
|
-
const initial =
|
|
103
|
-
typeof initialValue === "function"
|
|
104
|
-
? await (
|
|
105
|
-
initialValue as () =>
|
|
106
|
-
| StoreState<Schema>
|
|
107
|
-
| Promise<StoreState<Schema>>
|
|
108
|
-
)()
|
|
109
|
-
: initialValue;
|
|
110
|
-
const state = await validateStoreState(params.definition, initial);
|
|
111
|
-
|
|
112
|
-
return this.enqueueWrite(async () => {
|
|
113
|
-
this.db.run("BEGIN IMMEDIATE");
|
|
114
|
-
try {
|
|
115
|
-
const existingTx = this.getStoreRow(params.definition.name, params.id);
|
|
116
|
-
if (existingTx) {
|
|
117
|
-
this.db.run("COMMIT");
|
|
118
|
-
return {
|
|
119
|
-
state: JSON.parse(existingTx.state),
|
|
120
|
-
instanceId: existingTx.instance_id,
|
|
121
|
-
version: existingTx.version,
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const instanceId = uuidv7();
|
|
126
|
-
this.db
|
|
127
|
-
.query(
|
|
128
|
-
`INSERT INTO stores (instance_id, store_name, store_id, version, state)
|
|
129
|
-
VALUES ($instanceId, $storeName, $storeId, 0, $state)`
|
|
130
|
-
)
|
|
131
|
-
.run({
|
|
132
|
-
$instanceId: instanceId,
|
|
133
|
-
$storeName: params.definition.name,
|
|
134
|
-
$storeId: params.id,
|
|
135
|
-
$state: JSON.stringify(state),
|
|
136
|
-
});
|
|
137
|
-
this.db.run("COMMIT");
|
|
138
|
-
return {
|
|
139
|
-
state: cloneStoreState(state),
|
|
140
|
-
instanceId,
|
|
141
|
-
version: 0,
|
|
142
|
-
};
|
|
143
|
-
} catch (err) {
|
|
144
|
-
this.db.run("ROLLBACK");
|
|
145
|
-
throw err;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
throw new Error("Store creation transaction returned unexpectedly");
|
|
149
|
-
});
|
|
71
|
+
}): Promise<StoreSnapshot<unknown> | undefined> {
|
|
72
|
+
const row = this.getStoreRow(params.definition.name, params.id);
|
|
73
|
+
return row ? toSnapshot(row) : undefined;
|
|
150
74
|
}
|
|
151
75
|
|
|
152
|
-
async
|
|
153
|
-
definition: StoreDefinition
|
|
76
|
+
protected async insertStore(params: {
|
|
77
|
+
definition: StoreDefinition;
|
|
154
78
|
id: string;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
};
|
|
79
|
+
snapshot: StoreSnapshot<unknown>;
|
|
80
|
+
}): Promise<StoreSnapshot<unknown>> {
|
|
81
|
+
return runTransaction(this.db, () => {
|
|
82
|
+
const row = this.getStoreRow(params.definition.name, params.id);
|
|
83
|
+
if (row) return toSnapshot(row);
|
|
84
|
+
this.insertStoreRow({
|
|
85
|
+
storeName: params.definition.name,
|
|
86
|
+
storeId: params.id,
|
|
87
|
+
instanceId: params.snapshot.instanceId,
|
|
88
|
+
version: params.snapshot.version,
|
|
89
|
+
state: params.snapshot.state,
|
|
90
|
+
});
|
|
91
|
+
return cloneStoreState(params.snapshot);
|
|
92
|
+
});
|
|
169
93
|
}
|
|
170
94
|
|
|
171
95
|
protected async getAppliedStoreStep(params: {
|
|
@@ -178,52 +102,48 @@ export class SqliteStoreClient extends CasStoreClient {
|
|
|
178
102
|
storeId: params.id,
|
|
179
103
|
stepId: params.stepId,
|
|
180
104
|
});
|
|
181
|
-
return row ? { result:
|
|
105
|
+
return row ? { result: decodeStoreValue(row.result) } : undefined;
|
|
182
106
|
}
|
|
183
107
|
|
|
184
|
-
protected commitStoreMutation(
|
|
108
|
+
protected async commitStoreMutation(
|
|
185
109
|
mutation: StoreMutation
|
|
186
110
|
): Promise<StoreMutationCommitResult> {
|
|
187
|
-
return this.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
});
|
|
196
|
-
if (applied) {
|
|
197
|
-
this.db.run("COMMIT");
|
|
198
|
-
return {
|
|
199
|
-
status: "already-applied",
|
|
200
|
-
result: JSON.parse(applied.result),
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const row = this.getStoreRow(mutation.definition.name, mutation.id);
|
|
206
|
-
if (!row) {
|
|
207
|
-
throw new Error(
|
|
208
|
-
`Store "${mutation.definition.name}:${mutation.id}" does not exist`
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
if (
|
|
212
|
-
row.instance_id !== mutation.expected.instanceId ||
|
|
213
|
-
row.version !== mutation.expected.version
|
|
214
|
-
) {
|
|
215
|
-
this.db.run("COMMIT");
|
|
111
|
+
return runTransaction(this.db, (): StoreMutationCommitResult => {
|
|
112
|
+
if (mutation.stepId) {
|
|
113
|
+
const applied = this.getAppliedStepRow({
|
|
114
|
+
storeName: mutation.definition.name,
|
|
115
|
+
storeId: mutation.id,
|
|
116
|
+
stepId: mutation.stepId,
|
|
117
|
+
});
|
|
118
|
+
if (applied) {
|
|
216
119
|
return {
|
|
217
|
-
status: "
|
|
218
|
-
|
|
219
|
-
state: JSON.parse(row.state),
|
|
220
|
-
instanceId: row.instance_id,
|
|
221
|
-
version: row.version,
|
|
222
|
-
},
|
|
120
|
+
status: "already-applied",
|
|
121
|
+
result: decodeStoreValue(applied.result),
|
|
223
122
|
};
|
|
224
123
|
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const row = this.getStoreRow(mutation.definition.name, mutation.id);
|
|
127
|
+
if (!row && !mutation.expected.created) return { status: "missing" };
|
|
128
|
+
if (
|
|
129
|
+
row &&
|
|
130
|
+
(mutation.expected.created ||
|
|
131
|
+
row.instance_id !== mutation.expected.instanceId ||
|
|
132
|
+
row.version !== mutation.expected.version)
|
|
133
|
+
) {
|
|
134
|
+
return { status: "conflict", snapshot: toSnapshot(row) };
|
|
135
|
+
}
|
|
225
136
|
|
|
226
|
-
|
|
137
|
+
const version = mutation.expected.version + 1;
|
|
138
|
+
if (mutation.expected.created) {
|
|
139
|
+
this.insertStoreRow({
|
|
140
|
+
storeName: mutation.definition.name,
|
|
141
|
+
storeId: mutation.id,
|
|
142
|
+
instanceId: mutation.expected.instanceId,
|
|
143
|
+
version,
|
|
144
|
+
state: mutation.nextState,
|
|
145
|
+
});
|
|
146
|
+
} else {
|
|
227
147
|
const changed = this.db
|
|
228
148
|
.query(
|
|
229
149
|
`UPDATE stores
|
|
@@ -234,46 +154,35 @@ export class SqliteStoreClient extends CasStoreClient {
|
|
|
234
154
|
$instanceId: mutation.expected.instanceId,
|
|
235
155
|
$expectedVersion: mutation.expected.version,
|
|
236
156
|
$version: version,
|
|
237
|
-
$state:
|
|
157
|
+
$state: encodeStoreValue(mutation.nextState),
|
|
238
158
|
});
|
|
239
159
|
if (Number(changed.changes) !== 1) {
|
|
240
160
|
throw new Error("Store changed during its mutation transaction");
|
|
241
161
|
}
|
|
162
|
+
}
|
|
242
163
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
storeName: mutation.definition.name,
|
|
246
|
-
storeId: mutation.id,
|
|
247
|
-
stepId: mutation.stepId,
|
|
248
|
-
result: JSON.stringify(mutation.result),
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const waiters = this.selectMatchingWaiters({
|
|
164
|
+
if (mutation.stepId) {
|
|
165
|
+
this.insertAppliedStepRow({
|
|
253
166
|
storeName: mutation.definition.name,
|
|
254
167
|
storeId: mutation.id,
|
|
255
|
-
|
|
256
|
-
|
|
168
|
+
stepId: mutation.stepId,
|
|
169
|
+
result: encodeStoreValue(mutation.result),
|
|
257
170
|
});
|
|
258
|
-
|
|
259
|
-
this.insertWakeOutboxRow({
|
|
260
|
-
storeName: mutation.definition.name,
|
|
261
|
-
storeId: mutation.id,
|
|
262
|
-
executionId: waiter.executionId,
|
|
263
|
-
stepKey: waiter.stepKey,
|
|
264
|
-
});
|
|
265
|
-
}
|
|
171
|
+
}
|
|
266
172
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
173
|
+
// Wakes commit with the mutation. Waiters stay registered until the
|
|
174
|
+
// heap records their step's result; a replay whose selector still does
|
|
175
|
+
// not match re-registers with a newer sinceVersion.
|
|
176
|
+
const executionIds = this.selectMatchingExecutionIds({
|
|
177
|
+
storeName: mutation.definition.name,
|
|
178
|
+
storeId: mutation.id,
|
|
179
|
+
version,
|
|
180
|
+
changedPaths: mutation.changedPaths,
|
|
181
|
+
});
|
|
182
|
+
for (const executionId of executionIds) {
|
|
183
|
+
this.executions.wakeInTransaction(executionId);
|
|
271
184
|
}
|
|
272
185
|
|
|
273
|
-
// Wake delivery is deliberately outside the mutation result. Once the
|
|
274
|
-
// outbox intent commits, scheduler failure must not poison later store
|
|
275
|
-
// operations; another operation or client startup will retry it.
|
|
276
|
-
await this.drainWakeOutboxBestEffort();
|
|
277
186
|
return { status: "committed" };
|
|
278
187
|
});
|
|
279
188
|
}
|
|
@@ -295,176 +204,78 @@ export class SqliteStoreClient extends CasStoreClient {
|
|
|
295
204
|
definition: StoreDefinition<Schema>;
|
|
296
205
|
id: string;
|
|
297
206
|
}): Promise<void> {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
)
|
|
309
|
-
.run(bindings);
|
|
310
|
-
this.db
|
|
311
|
-
.query(
|
|
312
|
-
`DELETE FROM store_waiters WHERE store_name = $storeName AND store_id = $storeId`
|
|
313
|
-
)
|
|
314
|
-
.run(bindings);
|
|
315
|
-
this.db
|
|
316
|
-
.query(
|
|
317
|
-
`DELETE FROM store_wake_outbox WHERE store_name = $storeName AND store_id = $storeId`
|
|
318
|
-
)
|
|
319
|
-
.run(bindings);
|
|
320
|
-
this.db.run("COMMIT");
|
|
321
|
-
} catch (err) {
|
|
322
|
-
this.db.run("ROLLBACK");
|
|
323
|
-
throw err;
|
|
324
|
-
}
|
|
207
|
+
runTransaction(this.db, () => {
|
|
208
|
+
this.db
|
|
209
|
+
.query(
|
|
210
|
+
`DELETE FROM stores WHERE store_name = $storeName AND store_id = $storeId`
|
|
211
|
+
)
|
|
212
|
+
.run({
|
|
213
|
+
$storeName: params.definition.name,
|
|
214
|
+
$storeId: params.id,
|
|
215
|
+
});
|
|
216
|
+
this.storeWaitersDao.deleteStoreWaiters(params.definition.name, params.id);
|
|
325
217
|
});
|
|
326
218
|
}
|
|
327
219
|
|
|
328
|
-
async
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
this.db.run("BEGIN IMMEDIATE");
|
|
340
|
-
try {
|
|
341
|
-
if (params.stepId) {
|
|
342
|
-
const applied = this.getAppliedStepRow({
|
|
343
|
-
storeName: params.definition.name,
|
|
344
|
-
storeId: params.id,
|
|
345
|
-
stepId: params.stepId,
|
|
346
|
-
});
|
|
347
|
-
if (applied) {
|
|
348
|
-
this.db.run("COMMIT");
|
|
349
|
-
return JSON.parse(applied.result) as StoreDeleteFromResult;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
this.assertSnapshotHasInstanceId(params.snapshot);
|
|
354
|
-
|
|
355
|
-
const row = this.getStoreRow(params.definition.name, params.id);
|
|
356
|
-
if (!row) {
|
|
357
|
-
this.db.run("COMMIT");
|
|
358
|
-
return {
|
|
359
|
-
deleted: false,
|
|
360
|
-
reason: "not-found",
|
|
361
|
-
expectedInstanceId: params.snapshot.instanceId,
|
|
362
|
-
expectedVersion: params.snapshot.version,
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
if (
|
|
366
|
-
row.instance_id !== params.snapshot.instanceId ||
|
|
367
|
-
row.version !== params.snapshot.version
|
|
368
|
-
) {
|
|
369
|
-
this.db.run("COMMIT");
|
|
220
|
+
protected async commitStoreDeletion(
|
|
221
|
+
deletion: StoreDeletion
|
|
222
|
+
): Promise<StoreMutationCommitResult> {
|
|
223
|
+
return runTransaction(this.db, (): StoreMutationCommitResult => {
|
|
224
|
+
if (deletion.stepId) {
|
|
225
|
+
const applied = this.getAppliedStepRow({
|
|
226
|
+
storeName: deletion.definition.name,
|
|
227
|
+
storeId: deletion.id,
|
|
228
|
+
stepId: deletion.stepId,
|
|
229
|
+
});
|
|
230
|
+
if (applied) {
|
|
370
231
|
return {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
expectedInstanceId: params.snapshot.instanceId,
|
|
374
|
-
actualInstanceId: row.instance_id,
|
|
375
|
-
expectedVersion: params.snapshot.version,
|
|
376
|
-
actualVersion: row.version,
|
|
232
|
+
status: "already-applied",
|
|
233
|
+
result: decodeStoreValue(applied.result),
|
|
377
234
|
};
|
|
378
235
|
}
|
|
236
|
+
}
|
|
379
237
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
`DELETE FROM store_wake_outbox WHERE store_name = $storeName AND store_id = $storeId`
|
|
388
|
-
)
|
|
389
|
-
.run(bindings);
|
|
390
|
-
this.db
|
|
391
|
-
.query(`DELETE FROM stores WHERE instance_id = $instanceId`)
|
|
392
|
-
.run({ $instanceId: row.instance_id });
|
|
393
|
-
const result = { deleted: true as const };
|
|
394
|
-
if (params.stepId) {
|
|
395
|
-
this.insertAppliedStepRow({
|
|
396
|
-
storeName: params.definition.name,
|
|
397
|
-
storeId: params.id,
|
|
398
|
-
stepId: params.stepId,
|
|
399
|
-
result: JSON.stringify(result),
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
this.db.run("COMMIT");
|
|
403
|
-
return result;
|
|
404
|
-
} catch (err) {
|
|
405
|
-
this.db.run("ROLLBACK");
|
|
406
|
-
throw err;
|
|
238
|
+
const row = this.getStoreRow(deletion.definition.name, deletion.id);
|
|
239
|
+
if (!row) return { status: "missing" };
|
|
240
|
+
if (
|
|
241
|
+
row.instance_id !== deletion.expected.instanceId ||
|
|
242
|
+
row.version !== deletion.expected.version
|
|
243
|
+
) {
|
|
244
|
+
return { status: "conflict", snapshot: toSnapshot(row) };
|
|
407
245
|
}
|
|
246
|
+
|
|
247
|
+
this.storeWaitersDao.deleteStoreWaiters(deletion.definition.name, deletion.id);
|
|
248
|
+
this.db
|
|
249
|
+
.query(`DELETE FROM stores WHERE instance_id = $instanceId`)
|
|
250
|
+
.run({ $instanceId: row.instance_id });
|
|
251
|
+
if (deletion.stepId) {
|
|
252
|
+
this.insertAppliedStepRow({
|
|
253
|
+
storeName: deletion.definition.name,
|
|
254
|
+
storeId: deletion.id,
|
|
255
|
+
stepId: deletion.stepId,
|
|
256
|
+
result: encodeStoreValue(deletion.result),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
return { status: "committed" };
|
|
408
260
|
});
|
|
409
261
|
}
|
|
410
262
|
|
|
411
263
|
async registerWaiter(waiter: StoreWaiter): Promise<void> {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
read_paths = excluded.read_paths`
|
|
428
|
-
)
|
|
429
|
-
.run({
|
|
430
|
-
$workflowId: waiter.workflowId,
|
|
431
|
-
$executionId: waiter.executionId,
|
|
432
|
-
$stepKey: waiter.stepKey,
|
|
433
|
-
$event: JSON.stringify(serializeEvent(waiter.event)),
|
|
434
|
-
$storeName: waiter.storeName,
|
|
435
|
-
$storeId: waiter.storeId,
|
|
436
|
-
$sinceVersion: waiter.sinceVersion,
|
|
437
|
-
$readPaths: JSON.stringify(waiter.readPaths),
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
// Lost-wakeup guard: if an update committed between the caller's
|
|
441
|
-
// getStore and this registration, the version has already advanced
|
|
442
|
-
// past sinceVersion and no future update is guaranteed. Trigger an
|
|
443
|
-
// immediate wake – the replay re-evaluates the selector, so a
|
|
444
|
-
// spurious wake is safe.
|
|
445
|
-
const row = this.getStoreRow(waiter.storeName, waiter.storeId);
|
|
446
|
-
if (
|
|
447
|
-
!row ||
|
|
448
|
-
row.instance_id !== waiter.instanceId ||
|
|
449
|
-
row.version > waiter.sinceVersion
|
|
450
|
-
) {
|
|
451
|
-
versionAdvanced = true;
|
|
452
|
-
this.insertWakeOutboxRow({
|
|
453
|
-
storeName: waiter.storeName,
|
|
454
|
-
storeId: waiter.storeId,
|
|
455
|
-
executionId: waiter.executionId,
|
|
456
|
-
stepKey: waiter.stepKey,
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
this.db.run("COMMIT");
|
|
461
|
-
} catch (err) {
|
|
462
|
-
this.db.run("ROLLBACK");
|
|
463
|
-
throw err;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
if (versionAdvanced) {
|
|
467
|
-
await this.drainWakeOutboxBestEffort();
|
|
264
|
+
runTransaction(this.db, () => {
|
|
265
|
+
this.storeWaitersDao.upsertWaiter(waiter);
|
|
266
|
+
|
|
267
|
+
// Lost-wakeup guard: if an update committed between the caller's
|
|
268
|
+
// getStore and this registration, the version has already advanced
|
|
269
|
+
// past sinceVersion and no future update is guaranteed. Request an
|
|
270
|
+
// immediate wake – the replay re-evaluates the selector, so a
|
|
271
|
+
// spurious wake is safe.
|
|
272
|
+
const row = this.getStoreRow(waiter.storeName, waiter.storeId);
|
|
273
|
+
if (
|
|
274
|
+
!row ||
|
|
275
|
+
row.instance_id !== waiter.instanceId ||
|
|
276
|
+
row.version > waiter.sinceVersion
|
|
277
|
+
) {
|
|
278
|
+
this.executions.wakeInTransaction(waiter.executionId);
|
|
468
279
|
}
|
|
469
280
|
});
|
|
470
281
|
}
|
|
@@ -473,18 +284,6 @@ export class SqliteStoreClient extends CasStoreClient {
|
|
|
473
284
|
this.createStoresTable();
|
|
474
285
|
|
|
475
286
|
this.db.run(`
|
|
476
|
-
CREATE TABLE IF NOT EXISTS store_waiters (
|
|
477
|
-
workflow_id TEXT NOT NULL,
|
|
478
|
-
execution_id TEXT NOT NULL,
|
|
479
|
-
step_key TEXT NOT NULL,
|
|
480
|
-
event TEXT NOT NULL,
|
|
481
|
-
store_name TEXT NOT NULL,
|
|
482
|
-
store_id TEXT NOT NULL,
|
|
483
|
-
since_version INTEGER NOT NULL,
|
|
484
|
-
read_paths TEXT NOT NULL,
|
|
485
|
-
PRIMARY KEY (store_name, store_id, execution_id, step_key)
|
|
486
|
-
);
|
|
487
|
-
|
|
488
287
|
CREATE TABLE IF NOT EXISTS store_applied_steps (
|
|
489
288
|
store_name TEXT NOT NULL,
|
|
490
289
|
store_id TEXT NOT NULL,
|
|
@@ -493,14 +292,6 @@ export class SqliteStoreClient extends CasStoreClient {
|
|
|
493
292
|
result TEXT NOT NULL,
|
|
494
293
|
PRIMARY KEY (store_name, store_id, execution_id, step_key)
|
|
495
294
|
);
|
|
496
|
-
|
|
497
|
-
CREATE TABLE IF NOT EXISTS store_wake_outbox (
|
|
498
|
-
store_name TEXT NOT NULL,
|
|
499
|
-
store_id TEXT NOT NULL,
|
|
500
|
-
execution_id TEXT NOT NULL,
|
|
501
|
-
step_key TEXT NOT NULL,
|
|
502
|
-
PRIMARY KEY (store_name, store_id, execution_id, step_key)
|
|
503
|
-
);
|
|
504
295
|
`);
|
|
505
296
|
}
|
|
506
297
|
|
|
@@ -572,22 +363,18 @@ export class SqliteStoreClient extends CasStoreClient {
|
|
|
572
363
|
});
|
|
573
364
|
}
|
|
574
365
|
|
|
575
|
-
|
|
366
|
+
/** Executions with a waiter that the commit at `version` satisfies. */
|
|
367
|
+
private selectMatchingExecutionIds(params: {
|
|
576
368
|
storeName: string;
|
|
577
369
|
storeId: string;
|
|
578
370
|
version: number;
|
|
579
371
|
changedPaths: StorePath[];
|
|
580
|
-
}):
|
|
581
|
-
const
|
|
582
|
-
const rows = this.
|
|
583
|
-
.
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
)
|
|
587
|
-
.all({
|
|
588
|
-
$storeName: params.storeName,
|
|
589
|
-
$storeId: params.storeId,
|
|
590
|
-
});
|
|
372
|
+
}): Set<string> {
|
|
373
|
+
const executionIds = new Set<string>();
|
|
374
|
+
const rows = this.storeWaitersDao.getStoreWaiters(
|
|
375
|
+
params.storeName,
|
|
376
|
+
params.storeId
|
|
377
|
+
);
|
|
591
378
|
|
|
592
379
|
for (const row of rows) {
|
|
593
380
|
if (row.since_version >= params.version) continue;
|
|
@@ -595,161 +382,38 @@ export class SqliteStoreClient extends CasStoreClient {
|
|
|
595
382
|
const readPaths = JSON.parse(row.read_paths) as StorePath[];
|
|
596
383
|
if (!storePathsIntersect(readPaths, params.changedPaths)) continue;
|
|
597
384
|
|
|
598
|
-
|
|
599
|
-
executionId: row.execution_id,
|
|
600
|
-
stepKey: row.step_key,
|
|
601
|
-
event: deserializeEvent(JSON.parse(row.event)),
|
|
602
|
-
});
|
|
385
|
+
executionIds.add(row.execution_id);
|
|
603
386
|
}
|
|
604
387
|
|
|
605
|
-
return
|
|
388
|
+
return executionIds;
|
|
606
389
|
}
|
|
607
390
|
|
|
608
|
-
private
|
|
391
|
+
private insertStoreRow(params: {
|
|
609
392
|
storeName: string;
|
|
610
393
|
storeId: string;
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
}) {
|
|
615
|
-
this.db
|
|
616
|
-
.query(
|
|
617
|
-
`DELETE FROM store_waiters
|
|
618
|
-
WHERE store_name = $storeName
|
|
619
|
-
AND store_id = $storeId
|
|
620
|
-
AND execution_id = $executionId
|
|
621
|
-
AND step_key = $stepKey
|
|
622
|
-
AND ($sinceVersion IS NULL OR since_version = $sinceVersion)`
|
|
623
|
-
)
|
|
624
|
-
.run({
|
|
625
|
-
$storeName: params.storeName,
|
|
626
|
-
$storeId: params.storeId,
|
|
627
|
-
$executionId: params.executionId,
|
|
628
|
-
$stepKey: params.stepKey,
|
|
629
|
-
$sinceVersion: params.sinceVersion ?? null,
|
|
630
|
-
});
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
private insertWakeOutboxRow(params: {
|
|
634
|
-
storeName: string;
|
|
635
|
-
storeId: string;
|
|
636
|
-
executionId: string;
|
|
637
|
-
stepKey: string;
|
|
394
|
+
instanceId: string;
|
|
395
|
+
version: number;
|
|
396
|
+
state: unknown;
|
|
638
397
|
}) {
|
|
639
398
|
this.db
|
|
640
399
|
.query(
|
|
641
|
-
`INSERT
|
|
642
|
-
|
|
643
|
-
VALUES ($storeName, $storeId, $executionId, $stepKey)`
|
|
400
|
+
`INSERT INTO stores (instance_id, store_name, store_id, version, state)
|
|
401
|
+
VALUES ($instanceId, $storeName, $storeId, $version, $state)`
|
|
644
402
|
)
|
|
645
403
|
.run({
|
|
404
|
+
$instanceId: params.instanceId,
|
|
646
405
|
$storeName: params.storeName,
|
|
647
406
|
$storeId: params.storeId,
|
|
648
|
-
$
|
|
649
|
-
$
|
|
407
|
+
$version: params.version,
|
|
408
|
+
$state: encodeStoreValue(params.state),
|
|
650
409
|
});
|
|
651
410
|
}
|
|
652
|
-
|
|
653
|
-
private async drainWakeOutbox(): Promise<void> {
|
|
654
|
-
const rows = this.db
|
|
655
|
-
.query<WakeOutboxRow>(
|
|
656
|
-
`SELECT
|
|
657
|
-
o.store_name,
|
|
658
|
-
o.store_id,
|
|
659
|
-
o.execution_id,
|
|
660
|
-
o.step_key,
|
|
661
|
-
w.event,
|
|
662
|
-
w.since_version
|
|
663
|
-
FROM store_wake_outbox o
|
|
664
|
-
LEFT JOIN store_waiters w
|
|
665
|
-
ON w.store_name = o.store_name
|
|
666
|
-
AND w.store_id = o.store_id
|
|
667
|
-
AND w.execution_id = o.execution_id
|
|
668
|
-
AND w.step_key = o.step_key
|
|
669
|
-
ORDER BY o.store_name, o.store_id, o.execution_id, o.step_key`
|
|
670
|
-
)
|
|
671
|
-
.all();
|
|
672
|
-
|
|
673
|
-
for (const row of rows) {
|
|
674
|
-
try {
|
|
675
|
-
if (row.event !== null) {
|
|
676
|
-
await this.schedulerClient.requestWakeUp(
|
|
677
|
-
deserializeEvent(JSON.parse(row.event))
|
|
678
|
-
);
|
|
679
|
-
this.deleteWaiterRow({
|
|
680
|
-
storeName: row.store_name,
|
|
681
|
-
storeId: row.store_id,
|
|
682
|
-
executionId: row.execution_id,
|
|
683
|
-
stepKey: row.step_key,
|
|
684
|
-
sinceVersion: row.since_version ?? undefined,
|
|
685
|
-
});
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
this.db
|
|
689
|
-
.query(
|
|
690
|
-
`DELETE FROM store_wake_outbox
|
|
691
|
-
WHERE store_name = $storeName
|
|
692
|
-
AND store_id = $storeId
|
|
693
|
-
AND execution_id = $executionId
|
|
694
|
-
AND step_key = $stepKey`
|
|
695
|
-
)
|
|
696
|
-
.run({
|
|
697
|
-
$storeName: row.store_name,
|
|
698
|
-
$storeId: row.store_id,
|
|
699
|
-
$executionId: row.execution_id,
|
|
700
|
-
$stepKey: row.step_key,
|
|
701
|
-
});
|
|
702
|
-
} catch (error) {
|
|
703
|
-
// Leave only this row pending. A malformed event or event-specific
|
|
704
|
-
// scheduler failure must not prevent later wake intents from draining.
|
|
705
|
-
console.error("Failed to deliver pending store wake", error);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
private async drainWakeOutboxBestEffort(): Promise<void> {
|
|
711
|
-
try {
|
|
712
|
-
await this.drainWakeOutbox();
|
|
713
|
-
} catch (error) {
|
|
714
|
-
// Query/database failures also stay outside store-operation success.
|
|
715
|
-
console.error("Failed to drain pending store wakes", error);
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
private assertSnapshotHasInstanceId(snapshot: { instanceId?: string }) {
|
|
720
|
-
if (!snapshot.instanceId) {
|
|
721
|
-
throw new Error(
|
|
722
|
-
"Store snapshot is missing instanceId; read a fresh snapshot"
|
|
723
|
-
);
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
type MatchedWaiter = {
|
|
729
|
-
executionId: string;
|
|
730
|
-
stepKey: string;
|
|
731
|
-
event: WorkflowEvent;
|
|
732
|
-
};
|
|
733
|
-
|
|
734
|
-
function serializeEvent(event: WorkflowEvent) {
|
|
735
|
-
return {
|
|
736
|
-
workflowId: event.workflowId,
|
|
737
|
-
executionId: event.executionId,
|
|
738
|
-
params: event.params,
|
|
739
|
-
context:
|
|
740
|
-
event.context instanceof Map
|
|
741
|
-
? [...event.context.entries()]
|
|
742
|
-
: event.context
|
|
743
|
-
? Object.entries(event.context as any)
|
|
744
|
-
: [],
|
|
745
|
-
};
|
|
746
411
|
}
|
|
747
412
|
|
|
748
|
-
function
|
|
413
|
+
function toSnapshot(row: StoreRow) {
|
|
749
414
|
return {
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
context: new Map(event.context ?? []),
|
|
415
|
+
state: decodeStoreValue(row.state),
|
|
416
|
+
instanceId: row.instance_id,
|
|
417
|
+
version: row.version,
|
|
754
418
|
};
|
|
755
419
|
}
|