@effect-agent/storage-sqlite 0.1.0-beta.42 → 0.1.0-beta.45
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/index.mjs.map +1 -1
- package/dist/testing.mjs.map +1 -1
- package/package.json +1 -49
- package/src/errors.ts +1 -0
- package/src/sqlite-activity-store.ts +53 -0
- package/src/sqlite-journal.ts +52 -0
- package/src/sqlite-ledger.ts +307 -0
- package/src/sqlite-schedule-store.ts +39 -0
- package/src/sqlite-storage-failpoint-testing.ts +1 -0
- package/src/sqlite-subscription-store.ts +129 -0
- package/src/sqlite-thread-store.ts +74 -0
|
@@ -81,6 +81,7 @@ const decodeRecord = Effect.fn("SqliteScheduleStore.decodeRecord")(function* (
|
|
|
81
81
|
const record = yield* Schema.decodeEffect(Schema.fromJsonString(ScheduleRecord))(
|
|
82
82
|
row.record_json,
|
|
83
83
|
).pipe(Effect.mapError(() => corrupt("decode schedule")));
|
|
84
|
+
|
|
84
85
|
if (
|
|
85
86
|
record.owner.tenantId !== row.tenant_id ||
|
|
86
87
|
record.owner.ownerId !== row.owner_id ||
|
|
@@ -89,6 +90,7 @@ const decodeRecord = Effect.fn("SqliteScheduleStore.decodeRecord")(function* (
|
|
|
89
90
|
) {
|
|
90
91
|
return yield* corrupt("decode schedule identity");
|
|
91
92
|
}
|
|
93
|
+
|
|
92
94
|
return record;
|
|
93
95
|
});
|
|
94
96
|
|
|
@@ -127,6 +129,7 @@ const makeScheduleStore = Effect.gen(function* () {
|
|
|
127
129
|
AND owner_id = ${key.owner.ownerId}
|
|
128
130
|
AND schedule_id = ${key.scheduleId}
|
|
129
131
|
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
132
|
+
|
|
130
133
|
return yield* decodeRows(Schema.Array(ScheduleRow), rows, operation);
|
|
131
134
|
});
|
|
132
135
|
|
|
@@ -135,8 +138,10 @@ const makeScheduleStore = Effect.gen(function* () {
|
|
|
135
138
|
operation: string,
|
|
136
139
|
): Effect.fn.Return<ScheduleRecord | null, ScheduleStorageError> {
|
|
137
140
|
const rows = yield* readRows(key, operation);
|
|
141
|
+
|
|
138
142
|
if (rows.length === 0) return null;
|
|
139
143
|
if (rows.length !== 1) return yield* corrupt(operation);
|
|
144
|
+
|
|
140
145
|
return yield* decodeRecord(rows[0]);
|
|
141
146
|
});
|
|
142
147
|
|
|
@@ -145,19 +150,23 @@ const makeScheduleStore = Effect.gen(function* () {
|
|
|
145
150
|
const operation = "insert schedule";
|
|
146
151
|
const canonical = yield* decodeInput(operation, ScheduleRecord, record);
|
|
147
152
|
const recordJson = yield* encodeRecord(canonical);
|
|
153
|
+
|
|
148
154
|
const result = yield* sql
|
|
149
155
|
.withTransaction(
|
|
150
156
|
Effect.gen(function* () {
|
|
151
157
|
const existing = yield* readOne(canonical, operation);
|
|
158
|
+
|
|
152
159
|
if (existing !== null) {
|
|
153
160
|
if (existing.creationFingerprint === canonical.creationFingerprint) {
|
|
154
161
|
return { record: existing, inserted: false } as const;
|
|
155
162
|
}
|
|
163
|
+
|
|
156
164
|
return yield* ScheduleConflict.make({
|
|
157
165
|
reason: "creation",
|
|
158
166
|
key: { owner: canonical.owner, scheduleId: canonical.scheduleId },
|
|
159
167
|
});
|
|
160
168
|
}
|
|
169
|
+
|
|
161
170
|
const rawCounts = yield* sql<Record<string, unknown>>`
|
|
162
171
|
SELECT COUNT(*) AS schedule_count
|
|
163
172
|
FROM effect_agent_schedules
|
|
@@ -166,7 +175,9 @@ const makeScheduleStore = Effect.gen(function* () {
|
|
|
166
175
|
AND (json_extract(record_json, '$.pending') IS NOT NULL OR
|
|
167
176
|
(json_extract(record_json, '$.state') != 'cancelled' AND json_extract(record_json, '$.nextAtMillis') IS NOT NULL))
|
|
168
177
|
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
178
|
+
|
|
169
179
|
const counts = yield* decodeRows(Schema.Array(ScheduleCountRow), rawCounts, operation);
|
|
180
|
+
|
|
170
181
|
if (counts.length !== 1) return yield* corrupt(operation);
|
|
171
182
|
if (counts[0].schedule_count >= ownerLimit) {
|
|
172
183
|
return yield* ScheduleCapacityError.make({ limit: ownerLimit });
|
|
@@ -183,11 +194,14 @@ const makeScheduleStore = Effect.gen(function* () {
|
|
|
183
194
|
${recordJson}
|
|
184
195
|
)
|
|
185
196
|
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
197
|
+
|
|
186
198
|
return { record: canonical, inserted: true } as const;
|
|
187
199
|
}),
|
|
188
200
|
)
|
|
189
201
|
.pipe(Effect.catchTag("SqlError", () => Effect.fail(unavailable(operation))));
|
|
202
|
+
|
|
190
203
|
if (result.inserted) yield* scheduleFailpoint.hit("schedule:insert:after");
|
|
204
|
+
|
|
191
205
|
return result.record;
|
|
192
206
|
},
|
|
193
207
|
);
|
|
@@ -195,6 +209,7 @@ const makeScheduleStore = Effect.gen(function* () {
|
|
|
195
209
|
const get: ScheduleStore["Service"]["get"] = Effect.fn("SqliteScheduleStore.get")(
|
|
196
210
|
function* (key) {
|
|
197
211
|
const decodedKey = yield* decodeInput("get schedule", ScheduleKey, key);
|
|
212
|
+
|
|
198
213
|
return yield* readOne(decodedKey, "get schedule");
|
|
199
214
|
},
|
|
200
215
|
);
|
|
@@ -204,6 +219,7 @@ const makeScheduleStore = Effect.gen(function* () {
|
|
|
204
219
|
): Effect.fn.Return<SchedulePage, ScheduleStorageError> {
|
|
205
220
|
const operation = "list schedules";
|
|
206
221
|
const decodedRequest = yield* decodeInput(operation, SchedulePageRequest, request);
|
|
222
|
+
|
|
207
223
|
const rows =
|
|
208
224
|
decodedRequest.after === undefined
|
|
209
225
|
? yield* sql<Record<string, unknown>>`
|
|
@@ -223,10 +239,12 @@ const makeScheduleStore = Effect.gen(function* () {
|
|
|
223
239
|
ORDER BY schedule_id
|
|
224
240
|
LIMIT ${decodedRequest.limit + 1}
|
|
225
241
|
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
242
|
+
|
|
226
243
|
const decoded = yield* decodeRows(Schema.Array(ScheduleRow), rows, operation);
|
|
227
244
|
const records = yield* Effect.forEach(decoded, decodeRecord);
|
|
228
245
|
const hasNext = records.length > decodedRequest.limit;
|
|
229
246
|
const items = hasNext ? records.slice(0, decodedRequest.limit) : records;
|
|
247
|
+
|
|
230
248
|
return { items, next: hasNext ? (items.at(-1)?.scheduleId ?? null) : null };
|
|
231
249
|
});
|
|
232
250
|
|
|
@@ -235,14 +253,18 @@ const makeScheduleStore = Effect.gen(function* () {
|
|
|
235
253
|
const operation = "change schedule";
|
|
236
254
|
const decodedKey = yield* decodeInput(operation, ScheduleKey, key);
|
|
237
255
|
const decodedChange = yield* decodeInput(operation, ScheduleChange, change);
|
|
256
|
+
|
|
238
257
|
const result = yield* sql
|
|
239
258
|
.withTransaction(
|
|
240
259
|
Effect.gen(function* () {
|
|
241
260
|
const current = yield* readOne(decodedKey, operation);
|
|
261
|
+
|
|
242
262
|
if (current === null) return yield* ScheduleNotFound.make({ key: decodedKey });
|
|
243
263
|
const transition = applyScheduleChange(current, decodedChange);
|
|
264
|
+
|
|
244
265
|
if (Result.isFailure(transition)) return yield* transition.failure;
|
|
245
266
|
const next = transition.success;
|
|
267
|
+
|
|
246
268
|
if (!scheduleUsesCapacity(current) && scheduleUsesCapacity(next)) {
|
|
247
269
|
const rawCounts = yield* sql<Record<string, unknown>>`
|
|
248
270
|
SELECT COUNT(*) AS schedule_count FROM effect_agent_schedules
|
|
@@ -250,17 +272,20 @@ const makeScheduleStore = Effect.gen(function* () {
|
|
|
250
272
|
AND (json_extract(record_json, '$.pending') IS NOT NULL OR
|
|
251
273
|
(json_extract(record_json, '$.state') != 'cancelled' AND json_extract(record_json, '$.nextAtMillis') IS NOT NULL))
|
|
252
274
|
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
275
|
+
|
|
253
276
|
const counts = yield* decodeRows(
|
|
254
277
|
Schema.Array(ScheduleCountRow),
|
|
255
278
|
rawCounts,
|
|
256
279
|
operation,
|
|
257
280
|
);
|
|
281
|
+
|
|
258
282
|
if (counts.length !== 1) return yield* corrupt(operation);
|
|
259
283
|
if (counts[0].schedule_count >= ownerLimit)
|
|
260
284
|
return yield* ScheduleCapacityError.make({ limit: ownerLimit });
|
|
261
285
|
}
|
|
262
286
|
if (next === current) return { record: current, changed: false } as const;
|
|
263
287
|
const recordJson = yield* encodeRecord(next);
|
|
288
|
+
|
|
264
289
|
yield* scheduleFailpoint.hit(`schedule:${decodedChange._tag.toLowerCase()}:before`);
|
|
265
290
|
yield* sql`
|
|
266
291
|
UPDATE effect_agent_schedules
|
|
@@ -269,13 +294,16 @@ const makeScheduleStore = Effect.gen(function* () {
|
|
|
269
294
|
AND owner_id = ${decodedKey.owner.ownerId}
|
|
270
295
|
AND schedule_id = ${decodedKey.scheduleId}
|
|
271
296
|
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
297
|
+
|
|
272
298
|
return { record: next, changed: true } as const;
|
|
273
299
|
}),
|
|
274
300
|
)
|
|
275
301
|
.pipe(Effect.catchTag("SqlError", () => Effect.fail(unavailable(operation))));
|
|
302
|
+
|
|
276
303
|
if (result.changed) {
|
|
277
304
|
yield* scheduleFailpoint.hit(`schedule:${decodedChange._tag.toLowerCase()}:after`);
|
|
278
305
|
}
|
|
306
|
+
|
|
279
307
|
return result.record;
|
|
280
308
|
},
|
|
281
309
|
);
|
|
@@ -287,20 +315,24 @@ const makeScheduleStore = Effect.gen(function* () {
|
|
|
287
315
|
after?: ScheduleDueCursor,
|
|
288
316
|
) {
|
|
289
317
|
const operation = "query due schedules";
|
|
318
|
+
|
|
290
319
|
const decodedOwner =
|
|
291
320
|
owner === undefined ? undefined : yield* decodeInput(operation, ScheduleOwner, owner);
|
|
321
|
+
|
|
292
322
|
const cursor =
|
|
293
323
|
after === undefined
|
|
294
324
|
? undefined
|
|
295
325
|
: yield* Schema.decodeUnknownEffect(ScheduleDueCursor)(after).pipe(
|
|
296
326
|
Effect.mapError(() => corrupt(operation)),
|
|
297
327
|
);
|
|
328
|
+
|
|
298
329
|
const continuation =
|
|
299
330
|
cursor === undefined
|
|
300
331
|
? sql`1 = 1`
|
|
301
332
|
: sql`
|
|
302
333
|
(deadline_at_millis, tenant_id, owner_id, schedule_id) >
|
|
303
334
|
(${cursor.deadlineAtMillis}, ${cursor.owner.tenantId}, ${cursor.owner.ownerId}, ${cursor.scheduleId})`;
|
|
335
|
+
|
|
304
336
|
const rows =
|
|
305
337
|
decodedOwner === undefined
|
|
306
338
|
? yield* sql<Record<string, unknown>>`
|
|
@@ -319,7 +351,9 @@ const makeScheduleStore = Effect.gen(function* () {
|
|
|
319
351
|
ORDER BY deadline_at_millis, schedule_id
|
|
320
352
|
LIMIT ${limit}
|
|
321
353
|
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
354
|
+
|
|
322
355
|
const decoded = yield* decodeRows(Schema.Array(ScheduleDueRow), rows, operation);
|
|
356
|
+
|
|
323
357
|
return decoded.map((row) => ({
|
|
324
358
|
owner: { tenantId: row.tenant_id, ownerId: row.owner_id },
|
|
325
359
|
scheduleId: row.schedule_id,
|
|
@@ -331,8 +365,10 @@ const makeScheduleStore = Effect.gen(function* () {
|
|
|
331
365
|
"SqliteScheduleStore.nextDeadline",
|
|
332
366
|
)(function* (owner?: ScheduleOwner) {
|
|
333
367
|
const operation = "query next schedule deadline";
|
|
368
|
+
|
|
334
369
|
const decodedOwner =
|
|
335
370
|
owner === undefined ? undefined : yield* decodeInput(operation, ScheduleOwner, owner);
|
|
371
|
+
|
|
336
372
|
const rows =
|
|
337
373
|
decodedOwner === undefined
|
|
338
374
|
? yield* sql<Record<string, unknown>>`
|
|
@@ -347,8 +383,11 @@ const makeScheduleStore = Effect.gen(function* () {
|
|
|
347
383
|
AND owner_id = ${decodedOwner.ownerId}
|
|
348
384
|
AND deadline_at_millis IS NOT NULL
|
|
349
385
|
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
386
|
+
|
|
350
387
|
const decoded = yield* decodeRows(Schema.Array(ScheduleDeadlineRow), rows, operation);
|
|
388
|
+
|
|
351
389
|
if (decoded.length !== 1) return yield* corrupt(operation);
|
|
390
|
+
|
|
352
391
|
return decoded[0].deadline_at_millis;
|
|
353
392
|
});
|
|
354
393
|
|
|
@@ -19,6 +19,7 @@ export class SqliteStorageFailpointTestControl extends Context.Service<
|
|
|
19
19
|
static readonly layer = Layer.effectContext(
|
|
20
20
|
Effect.gen(function* () {
|
|
21
21
|
const handler = yield* Ref.make<SqliteStorageFailpointHandler>(noFailpoint);
|
|
22
|
+
|
|
22
23
|
return Context.make(
|
|
23
24
|
SqliteStorageFailpoint,
|
|
24
25
|
SqliteStorageFailpoint.of({
|