effect-agent 0.1.0-beta.155 → 0.1.0-beta.156
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.
|
@@ -3,7 +3,7 @@ import * as SqlClientService from "effect/unstable/sql/SqlClient";
|
|
|
3
3
|
import type { SqlError } from "effect/unstable/sql/SqlError";
|
|
4
4
|
|
|
5
5
|
import { utf8ByteLength } from "../core/internal/utf8.ts";
|
|
6
|
-
import { MemoryNamespaceAddress } from "../core/MemoryNamespace.ts";
|
|
6
|
+
import { type Any as MemoryNamespace, MemoryNamespaceAddress } from "../core/MemoryNamespace.ts";
|
|
7
7
|
import {
|
|
8
8
|
applyMemoryWrite,
|
|
9
9
|
MemoryDocument,
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
MemoryReader,
|
|
15
15
|
MemoryStorageError,
|
|
16
16
|
MemoryWrite,
|
|
17
|
+
type MemoryWriteError,
|
|
17
18
|
MemoryWriter,
|
|
18
19
|
} from "../core/MemoryStore.ts";
|
|
19
20
|
|
|
@@ -38,6 +39,42 @@ const equivalentContent = Schema.toEquivalence(
|
|
|
38
39
|
|
|
39
40
|
const equivalentScopes = Schema.toEquivalence(MemoryWrite.Wire.members[0].fields.scopes);
|
|
40
41
|
|
|
42
|
+
/** Ordered commands committed atomically by the SQL memory adapter. */
|
|
43
|
+
export const SqlMemoryWriteBatch = Schema.Array(MemoryWrite.Wire).check(Schema.isMaxLength(128));
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* SQL-only bulk writing. Results preserve input order, including exact operation replays.
|
|
47
|
+
* Commands see earlier staged revisions; any refusal rolls back the entire batch. Storage
|
|
48
|
+
* limits apply to every intermediate transition. Empty batches succeed without mutation.
|
|
49
|
+
* Supplied by the memory store Layers alongside MemoryWriter's single-command API.
|
|
50
|
+
*/
|
|
51
|
+
export class SqlMemoryBatchWriter extends Context.Service<
|
|
52
|
+
SqlMemoryBatchWriter,
|
|
53
|
+
{
|
|
54
|
+
readonly changeMany: <Namespace extends MemoryNamespace>(
|
|
55
|
+
writes: ReadonlyArray<MemoryWrite<Namespace>>,
|
|
56
|
+
) => Effect.Effect<ReadonlyArray<MemoryDocument<Namespace>>, MemoryWriteError>;
|
|
57
|
+
}
|
|
58
|
+
>()("@effect-agent/thread/SqlMemoryBatchWriter") {}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* An exclusive database owner's transaction gate. All cached services over that database
|
|
62
|
+
* must share this identity, and every failed transaction must run its invalidators before
|
|
63
|
+
* releasing readers. Hosts with independent database writers must leave this unset.
|
|
64
|
+
*/
|
|
65
|
+
export interface SqlStorageOwner {
|
|
66
|
+
readonly read: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
|
|
67
|
+
readonly transaction: <A, E, R>(
|
|
68
|
+
effect: Effect.Effect<A, E, R>,
|
|
69
|
+
) => Effect.Effect<A, E | SqlError, R | SqlClientService.SqlClient>;
|
|
70
|
+
readonly invalidators: Set<() => void>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const SqlStorageOwner = Context.Reference<SqlStorageOwner | undefined>(
|
|
74
|
+
"@effect-agent/thread/SqlStorageOwner",
|
|
75
|
+
{ defaultValue: () => undefined },
|
|
76
|
+
);
|
|
77
|
+
|
|
41
78
|
/**
|
|
42
79
|
* Independent adapter limits. Counts and encoded bytes include durable operation receipts.
|
|
43
80
|
* Bytes count UTF-8 row payloads plus 128 bytes per row, excluding SQLite pages/indexes.
|
|
@@ -84,6 +121,80 @@ const UsageRow = Schema.Struct({
|
|
|
84
121
|
bytes: Schema.Natural,
|
|
85
122
|
});
|
|
86
123
|
|
|
124
|
+
type DocumentView = { readonly document: MemoryDocument; readonly bytes: number } | null;
|
|
125
|
+
type ReceiptView = { readonly commandJson: string; readonly result: MemoryDocument } | null;
|
|
126
|
+
type MemoryView =
|
|
127
|
+
| { readonly _tag: "Document"; readonly value: DocumentView }
|
|
128
|
+
| { readonly _tag: "Receipt"; readonly value: ReceiptView }
|
|
129
|
+
| { readonly _tag: "Usage"; readonly value: typeof UsageRow.Type };
|
|
130
|
+
|
|
131
|
+
// Demand-loaded views are disposable. The shared owner's gate keeps writes invisible
|
|
132
|
+
// until commit, and its rollback invalidator discards every speculative view together.
|
|
133
|
+
class MemoryViews {
|
|
134
|
+
private readonly entries = new Map<
|
|
135
|
+
string,
|
|
136
|
+
{ readonly view: MemoryView; readonly bytes: number }
|
|
137
|
+
>();
|
|
138
|
+
private bytes = 0;
|
|
139
|
+
|
|
140
|
+
readonly clear = () => {
|
|
141
|
+
this.entries.clear();
|
|
142
|
+
this.bytes = 0;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
get(key: string): MemoryView | undefined {
|
|
146
|
+
const entry = this.entries.get(key);
|
|
147
|
+
|
|
148
|
+
if (entry === undefined) return undefined;
|
|
149
|
+
this.entries.delete(key);
|
|
150
|
+
this.entries.set(key, entry);
|
|
151
|
+
|
|
152
|
+
return entry.view;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
set(key: string, view: MemoryView): void {
|
|
156
|
+
const previous = this.entries.get(key);
|
|
157
|
+
|
|
158
|
+
if (previous !== undefined) this.bytes -= previous.bytes;
|
|
159
|
+
this.entries.delete(key);
|
|
160
|
+
const bytes = utf8ByteLength(key) + utf8ByteLength(JSON.stringify(view)) + 128;
|
|
161
|
+
|
|
162
|
+
if (bytes > 4 * 1024 * 1024) return;
|
|
163
|
+
this.entries.set(key, { view, bytes });
|
|
164
|
+
this.bytes += bytes;
|
|
165
|
+
while (this.entries.size > 128 || this.bytes > 4 * 1024 * 1024) {
|
|
166
|
+
const oldest = this.entries.entries().next().value;
|
|
167
|
+
|
|
168
|
+
if (oldest === undefined) break;
|
|
169
|
+
this.bytes -= oldest[1].bytes;
|
|
170
|
+
this.entries.delete(oldest[0]);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const ownedViews = new WeakMap<SqlStorageOwner, MemoryViews>();
|
|
176
|
+
|
|
177
|
+
const memoryViews = Effect.fn("SqliteMemoryStore.memoryViews")(function* () {
|
|
178
|
+
const owner = yield* SqlStorageOwner;
|
|
179
|
+
|
|
180
|
+
if (owner === undefined) return undefined;
|
|
181
|
+
let views = ownedViews.get(owner);
|
|
182
|
+
|
|
183
|
+
if (views === undefined) {
|
|
184
|
+
views = new MemoryViews();
|
|
185
|
+
ownedViews.set(owner, views);
|
|
186
|
+
owner.invalidators.add(views.clear);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return views;
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const documentViewKey = (key: MemoryKey) =>
|
|
193
|
+
JSON.stringify(["document", key.namespace.address, key.id]);
|
|
194
|
+
|
|
195
|
+
const receiptViewKey = (write: MemoryWrite) =>
|
|
196
|
+
JSON.stringify(["receipt", write.key.namespace.address, write.operationId]);
|
|
197
|
+
|
|
87
198
|
// These expressions deliberately match the legacy aggregate's logical byte accounting.
|
|
88
199
|
const documentBytesSql = (row: string) =>
|
|
89
200
|
`length(CAST(${row}.namespace AS BLOB)) + length(CAST(${row}.source_id AS BLOB)) + length(CAST(${row}.document_json AS BLOB)) + 128`;
|
|
@@ -296,6 +407,7 @@ const validateReceiptResult = Effect.fn("SqliteMemoryStore.validateReceiptResult
|
|
|
296
407
|
|
|
297
408
|
const readUsage = Effect.fn("SqliteMemoryStore.readUsage")(function* () {
|
|
298
409
|
const sql = yield* SqlClientService.SqlClient;
|
|
410
|
+
const views = yield* memoryViews();
|
|
299
411
|
const operation = "read memory usage";
|
|
300
412
|
|
|
301
413
|
const rows = yield* query(
|
|
@@ -307,6 +419,8 @@ const readUsage = Effect.fn("SqliteMemoryStore.readUsage")(function* () {
|
|
|
307
419
|
|
|
308
420
|
if (rows.length !== 1) return yield* storageError(operation, "corrupt");
|
|
309
421
|
|
|
422
|
+
views?.set("usage", { _tag: "Usage", value: rows[0] });
|
|
423
|
+
|
|
310
424
|
return rows[0];
|
|
311
425
|
});
|
|
312
426
|
|
|
@@ -366,55 +480,56 @@ const initializeMemoryUsage = Effect.fn("SqliteMemoryStore.initializeUsage")(fun
|
|
|
366
480
|
|
|
367
481
|
const initializeMemorySchema = Effect.fn("SqliteMemoryStore.initialize")(function* () {
|
|
368
482
|
const sql = yield* SqlClientService.SqlClient;
|
|
483
|
+
const owner = yield* SqlStorageOwner;
|
|
484
|
+
const transaction = owner === undefined ? sql.withTransaction : owner.transaction;
|
|
369
485
|
const failpoint = yield* MemoryMutationFailpoint;
|
|
370
486
|
|
|
371
487
|
yield* failpoint.hit("memory:initialize:before");
|
|
372
|
-
yield*
|
|
373
|
-
.
|
|
374
|
-
|
|
375
|
-
yield* sql`
|
|
488
|
+
yield* transaction(
|
|
489
|
+
Effect.gen(function* () {
|
|
490
|
+
yield* sql`
|
|
376
491
|
CREATE TABLE IF NOT EXISTS effect_agent_memory_metadata (
|
|
377
492
|
component TEXT PRIMARY KEY NOT NULL,
|
|
378
493
|
version INTEGER NOT NULL
|
|
379
494
|
)
|
|
380
495
|
`;
|
|
381
496
|
|
|
382
|
-
|
|
497
|
+
const metadataRows = yield* sql<Record<string, unknown>>`
|
|
383
498
|
SELECT version
|
|
384
499
|
FROM effect_agent_memory_metadata
|
|
385
500
|
WHERE component = ${METADATA_COMPONENT}
|
|
386
501
|
`;
|
|
387
502
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
503
|
+
const metadata = yield* decodeRows(
|
|
504
|
+
MemoryMetadataRow,
|
|
505
|
+
metadataRows,
|
|
506
|
+
"decode memory schema version",
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
if (metadata.length > 1)
|
|
510
|
+
return yield* storageError("decode memory schema version", "corrupt");
|
|
511
|
+
const currentVersion = metadata[0]?.version;
|
|
512
|
+
|
|
513
|
+
if (currentVersion !== undefined && currentVersion !== STORAGE_VERSION) {
|
|
514
|
+
return yield* storageError("initialize memory schema", "incompatible");
|
|
515
|
+
}
|
|
516
|
+
if (currentVersion === undefined) {
|
|
517
|
+
const tableRows = yield* sql<Record<string, unknown>>`
|
|
403
518
|
SELECT name
|
|
404
519
|
FROM sqlite_master
|
|
405
520
|
WHERE type = 'table' AND name IN (${DOCUMENT_TABLE}, ${RECEIPT_TABLE})
|
|
406
521
|
`;
|
|
407
522
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
523
|
+
const existingTables = yield* decodeRows(
|
|
524
|
+
MemoryTableRow,
|
|
525
|
+
tableRows,
|
|
526
|
+
"inspect memory schema",
|
|
527
|
+
);
|
|
413
528
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
529
|
+
if (existingTables.length > 0) {
|
|
530
|
+
return yield* storageError("initialize memory schema", "incompatible");
|
|
531
|
+
}
|
|
532
|
+
yield* sql`
|
|
418
533
|
CREATE TABLE effect_agent_memory_documents_v1 (
|
|
419
534
|
namespace TEXT NOT NULL,
|
|
420
535
|
source_id TEXT NOT NULL,
|
|
@@ -425,7 +540,7 @@ const initializeMemorySchema = Effect.fn("SqliteMemoryStore.initialize")(functio
|
|
|
425
540
|
PRIMARY KEY (namespace, source_id)
|
|
426
541
|
)
|
|
427
542
|
`;
|
|
428
|
-
|
|
543
|
+
yield* sql`
|
|
429
544
|
CREATE TABLE effect_agent_memory_receipts_v1 (
|
|
430
545
|
namespace TEXT NOT NULL,
|
|
431
546
|
operation_id TEXT NOT NULL,
|
|
@@ -436,176 +551,296 @@ const initializeMemorySchema = Effect.fn("SqliteMemoryStore.initialize")(functio
|
|
|
436
551
|
PRIMARY KEY (namespace, operation_id)
|
|
437
552
|
)
|
|
438
553
|
`;
|
|
439
|
-
|
|
554
|
+
yield* sql`
|
|
440
555
|
INSERT INTO effect_agent_memory_metadata (component, version)
|
|
441
556
|
VALUES (${METADATA_COMPONENT}, ${STORAGE_VERSION})
|
|
442
557
|
`;
|
|
443
|
-
|
|
444
|
-
|
|
558
|
+
}
|
|
559
|
+
yield* sql`
|
|
445
560
|
SELECT namespace, source_id, format_version, generation, revision, document_json
|
|
446
561
|
FROM effect_agent_memory_documents_v1
|
|
447
562
|
LIMIT 0
|
|
448
563
|
`;
|
|
449
|
-
|
|
564
|
+
yield* sql`
|
|
450
565
|
SELECT namespace, operation_id, source_id, format_version, command_json, result_json
|
|
451
566
|
FROM effect_agent_memory_receipts_v1
|
|
452
567
|
LIMIT 0
|
|
453
568
|
`;
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
.pipe(Effect.catchTag("SqlError", () => Effect.fail(storageError("initialize memory schema"))));
|
|
569
|
+
yield* initializeMemoryUsage();
|
|
570
|
+
}),
|
|
571
|
+
).pipe(Effect.catchTag("SqlError", () => Effect.fail(storageError("initialize memory schema"))));
|
|
458
572
|
yield* failpoint.hit("memory:initialize:after");
|
|
459
573
|
});
|
|
460
574
|
|
|
461
575
|
const makeMemoryReader = Effect.fn("SqliteMemoryStore.makeReader")(function* () {
|
|
462
576
|
const sql = yield* SqlClientService.SqlClient;
|
|
577
|
+
const owner = yield* SqlStorageOwner;
|
|
578
|
+
const views = yield* memoryViews();
|
|
579
|
+
|
|
580
|
+
const readDocuments = Effect.fn("SqliteMemoryStore.readDocuments")(
|
|
581
|
+
function* (
|
|
582
|
+
keys: ReadonlyArray<MemoryKey>,
|
|
583
|
+
operation: string,
|
|
584
|
+
): Effect.fn.Return<ReadonlyMap<string, DocumentView>, MemoryStorageError> {
|
|
585
|
+
const found = new Map<string, DocumentView>();
|
|
586
|
+
const missing = new Map<string, MemoryKey>();
|
|
587
|
+
|
|
588
|
+
for (const key of keys) {
|
|
589
|
+
const viewKey = documentViewKey(key);
|
|
590
|
+
const cached = views?.get(viewKey);
|
|
591
|
+
|
|
592
|
+
if (cached?._tag === "Document") found.set(viewKey, cached.value);
|
|
593
|
+
else missing.set(viewKey, key);
|
|
594
|
+
}
|
|
595
|
+
const pending = Array.from(missing);
|
|
596
|
+
|
|
597
|
+
// Each exact composite key uses two bindings. No history or unrelated rows are loaded.
|
|
598
|
+
for (let index = 0; index < pending.length; index += 50) {
|
|
599
|
+
const requested = new Map(pending.slice(index, index + 50));
|
|
600
|
+
|
|
601
|
+
const rawRows = yield* query(
|
|
602
|
+
sql<Record<string, unknown>>`
|
|
603
|
+
SELECT namespace, source_id, format_version, generation, revision, document_json,
|
|
604
|
+
length(CAST(namespace AS BLOB)) + length(CAST(source_id AS BLOB)) +
|
|
605
|
+
length(CAST(document_json AS BLOB)) + 128 AS stored_bytes
|
|
606
|
+
FROM effect_agent_memory_documents_v1
|
|
607
|
+
WHERE ${sql.or(
|
|
608
|
+
Array.from(
|
|
609
|
+
requested.values(),
|
|
610
|
+
(key) => sql`(namespace = ${key.namespace.address} AND source_id = ${key.id})`,
|
|
611
|
+
),
|
|
612
|
+
)}
|
|
613
|
+
`,
|
|
614
|
+
operation,
|
|
615
|
+
);
|
|
463
616
|
|
|
464
|
-
|
|
465
|
-
key: MemoryKey,
|
|
466
|
-
operation: string,
|
|
467
|
-
): Effect.fn.Return<
|
|
468
|
-
{ readonly document: MemoryDocument; readonly bytes: number } | null,
|
|
469
|
-
MemoryStorageError
|
|
470
|
-
> {
|
|
471
|
-
const rawRows = yield* query(
|
|
472
|
-
sql<Record<string, unknown>>`
|
|
473
|
-
SELECT namespace, source_id, format_version, generation, revision, document_json,
|
|
474
|
-
length(CAST(namespace AS BLOB)) + length(CAST(source_id AS BLOB)) +
|
|
475
|
-
length(CAST(document_json AS BLOB)) + 128 AS stored_bytes
|
|
476
|
-
FROM effect_agent_memory_documents_v1
|
|
477
|
-
WHERE namespace = ${key.namespace.address} AND source_id = ${key.id}
|
|
478
|
-
`,
|
|
479
|
-
operation,
|
|
480
|
-
);
|
|
617
|
+
const rows = yield* decodeRows(MemoryDocumentRow, rawRows, operation);
|
|
481
618
|
|
|
482
|
-
|
|
619
|
+
for (const row of rows) {
|
|
620
|
+
const viewKey = JSON.stringify(["document", row.namespace, row.source_id]);
|
|
621
|
+
const key = requested.get(viewKey);
|
|
483
622
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
623
|
+
if (key === undefined || found.has(viewKey))
|
|
624
|
+
return yield* storageError(operation, "corrupt");
|
|
625
|
+
if (row.format_version !== STORAGE_VERSION)
|
|
626
|
+
return yield* storageError(operation, "incompatible");
|
|
487
627
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
628
|
+
const stored = yield* decodeVersionedJson(
|
|
629
|
+
StoredMemoryResult,
|
|
630
|
+
row.document_json,
|
|
631
|
+
operation,
|
|
632
|
+
);
|
|
493
633
|
|
|
494
|
-
|
|
495
|
-
if (
|
|
496
|
-
row.namespace !== key.namespace.address ||
|
|
497
|
-
row.source_id !== key.id ||
|
|
498
|
-
row.generation !== document.generation ||
|
|
499
|
-
row.revision !== document.source.revision
|
|
500
|
-
) {
|
|
501
|
-
return yield* storageError(operation, "corrupt");
|
|
502
|
-
}
|
|
634
|
+
const document = stored.value;
|
|
503
635
|
|
|
504
|
-
|
|
505
|
-
|
|
636
|
+
yield* validateDocument(document, key, operation);
|
|
637
|
+
if (row.generation !== document.generation || row.revision !== document.source.revision)
|
|
638
|
+
return yield* storageError(operation, "corrupt");
|
|
639
|
+
found.set(viewKey, { document, bytes: row.stored_bytes });
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
for (const viewKey of requested.keys()) {
|
|
643
|
+
const value = found.get(viewKey) ?? null;
|
|
644
|
+
|
|
645
|
+
found.set(viewKey, value);
|
|
646
|
+
views?.set(viewKey, { _tag: "Document", value });
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return found;
|
|
651
|
+
},
|
|
652
|
+
(effect) => (owner === undefined ? effect : owner.read(effect)),
|
|
653
|
+
);
|
|
506
654
|
|
|
507
655
|
const get = Effect.fn("SqliteMemoryStore.get")(function* (key: MemoryKey) {
|
|
508
656
|
const decodedKey = yield* decodeInput(MemoryKey.Wire, key, "get memory document");
|
|
509
657
|
|
|
510
|
-
|
|
658
|
+
const documents = yield* readDocuments([decodedKey], "get memory document");
|
|
659
|
+
|
|
660
|
+
return documents.get(documentViewKey(decodedKey))?.document ?? null;
|
|
511
661
|
});
|
|
512
662
|
|
|
513
|
-
return { get,
|
|
663
|
+
return { get, readDocuments };
|
|
514
664
|
});
|
|
515
665
|
|
|
516
666
|
const makeMemoryServices = Effect.fn("SqliteMemoryStore.make")(function* () {
|
|
517
667
|
const sql = yield* SqlClientService.SqlClient;
|
|
518
|
-
const
|
|
668
|
+
const owner = yield* SqlStorageOwner;
|
|
669
|
+
const views = yield* memoryViews();
|
|
670
|
+
|
|
671
|
+
const usage = readUsage().pipe(
|
|
672
|
+
Effect.provideService(SqlClientService.SqlClient, sql),
|
|
673
|
+
Effect.provideService(SqlStorageOwner, owner),
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
const transaction = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
677
|
+
owner === undefined
|
|
678
|
+
? sql.withTransaction(effect)
|
|
679
|
+
: owner.transaction(effect).pipe(Effect.provideService(SqlClientService.SqlClient, sql));
|
|
680
|
+
|
|
519
681
|
const failpoint = yield* MemoryMutationFailpoint;
|
|
520
682
|
const limits = yield* decodeInput(Limits, yield* SqlMemoryLimits, "memory storage limits");
|
|
521
683
|
|
|
522
684
|
yield* initializeMemorySchema();
|
|
523
|
-
const { get,
|
|
685
|
+
const { get, readDocuments } = yield* makeMemoryReader();
|
|
524
686
|
|
|
525
|
-
const
|
|
526
|
-
|
|
687
|
+
const readReceipts = Effect.fn("SqliteMemoryStore.readReceipts")(function* (
|
|
688
|
+
writes: ReadonlyArray<MemoryWrite>,
|
|
527
689
|
operation: string,
|
|
528
|
-
) {
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
SELECT namespace, operation_id, source_id, format_version, command_json, result_json
|
|
532
|
-
FROM effect_agent_memory_receipts_v1
|
|
533
|
-
WHERE namespace = ${write.key.namespace.address} AND operation_id = ${write.operationId}
|
|
534
|
-
`,
|
|
535
|
-
operation,
|
|
536
|
-
);
|
|
537
|
-
|
|
538
|
-
const rows = yield* decodeRows(MemoryReceiptRow, rawRows, operation);
|
|
690
|
+
): Effect.fn.Return<ReadonlyMap<string, ReceiptView>, MemoryStorageError> {
|
|
691
|
+
const found = new Map<string, ReceiptView>();
|
|
692
|
+
const missing = new Map<string, MemoryWrite>();
|
|
539
693
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
694
|
+
for (const write of writes) {
|
|
695
|
+
const viewKey = receiptViewKey(write);
|
|
696
|
+
const cached = views?.get(viewKey);
|
|
543
697
|
|
|
544
|
-
|
|
545
|
-
|
|
698
|
+
if (cached?._tag === "Receipt") found.set(viewKey, cached.value);
|
|
699
|
+
else missing.set(viewKey, write);
|
|
546
700
|
}
|
|
701
|
+
const pending = Array.from(missing);
|
|
547
702
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
row.command_json,
|
|
551
|
-
`${operation} command`,
|
|
552
|
-
);
|
|
703
|
+
for (let index = 0; index < pending.length; index += 50) {
|
|
704
|
+
const requested = new Map(pending.slice(index, index + 50));
|
|
553
705
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
706
|
+
const rawRows = yield* query(
|
|
707
|
+
sql<Record<string, unknown>>`
|
|
708
|
+
SELECT namespace, operation_id, source_id, format_version, command_json, result_json
|
|
709
|
+
FROM effect_agent_memory_receipts_v1
|
|
710
|
+
WHERE ${sql.or(
|
|
711
|
+
Array.from(
|
|
712
|
+
requested.values(),
|
|
713
|
+
(write) =>
|
|
714
|
+
sql`(namespace = ${write.key.namespace.address} AND operation_id = ${write.operationId})`,
|
|
715
|
+
),
|
|
716
|
+
)}
|
|
717
|
+
`,
|
|
718
|
+
operation,
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
const rows = yield* decodeRows(MemoryReceiptRow, rawRows, operation);
|
|
722
|
+
|
|
723
|
+
for (const row of rows) {
|
|
724
|
+
const viewKey = JSON.stringify(["receipt", row.namespace, row.operation_id]);
|
|
725
|
+
|
|
726
|
+
if (!requested.has(viewKey) || found.has(viewKey))
|
|
727
|
+
return yield* storageError(operation, "corrupt");
|
|
728
|
+
if (row.format_version !== STORAGE_VERSION)
|
|
729
|
+
return yield* storageError(operation, "incompatible");
|
|
730
|
+
|
|
731
|
+
const command = yield* decodeVersionedJson(
|
|
732
|
+
StoredMemoryCommand,
|
|
733
|
+
row.command_json,
|
|
734
|
+
`${operation} command`,
|
|
735
|
+
);
|
|
559
736
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
737
|
+
const result = yield* decodeVersionedJson(
|
|
738
|
+
StoredMemoryResult,
|
|
739
|
+
row.result_json,
|
|
740
|
+
`${operation} result`,
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
if (
|
|
744
|
+
row.namespace !== command.value.key.namespace.address ||
|
|
745
|
+
row.operation_id !== command.value.operationId ||
|
|
746
|
+
row.source_id !== command.value.key.id ||
|
|
747
|
+
result.value.key.namespace.address !== command.value.key.namespace.address ||
|
|
748
|
+
result.value.key.id !== command.value.key.id
|
|
749
|
+
) {
|
|
750
|
+
return yield* storageError(operation, "corrupt");
|
|
751
|
+
}
|
|
752
|
+
yield* validateDocument(result.value, command.value.key, operation);
|
|
753
|
+
yield* validateReceiptResult(command.value, result.value, operation);
|
|
754
|
+
found.set(viewKey, { commandJson: row.command_json, result: result.value });
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
for (const viewKey of requested.keys()) {
|
|
758
|
+
const value = found.get(viewKey) ?? null;
|
|
759
|
+
|
|
760
|
+
found.set(viewKey, value);
|
|
761
|
+
views?.set(viewKey, { _tag: "Receipt", value });
|
|
762
|
+
}
|
|
568
763
|
}
|
|
569
|
-
yield* validateDocument(result.value, command.value.key, operation);
|
|
570
|
-
yield* validateReceiptResult(command.value, result.value, operation);
|
|
571
764
|
|
|
572
|
-
return
|
|
765
|
+
return found;
|
|
573
766
|
});
|
|
574
767
|
|
|
575
|
-
const
|
|
768
|
+
const changeMany = Effect.fn("SqliteMemoryStore.changeMany")(function* (
|
|
769
|
+
writes: ReadonlyArray<MemoryWrite>,
|
|
770
|
+
) {
|
|
576
771
|
const operation = "change memory document";
|
|
577
|
-
const
|
|
772
|
+
const decodedWrites = yield* decodeInput(SqlMemoryWriteBatch, writes, operation);
|
|
578
773
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
774
|
+
if (decodedWrites.length === 0) return [];
|
|
775
|
+
|
|
776
|
+
const commands = yield* Effect.forEach(decodedWrites, (write) =>
|
|
777
|
+
encodeJson(
|
|
778
|
+
StoredMemoryCommand,
|
|
779
|
+
StoredMemoryCommand.make({ version: STORAGE_VERSION, value: write }),
|
|
780
|
+
"encode memory command",
|
|
781
|
+
).pipe(Effect.map((commandJson) => ({ write, commandJson }))),
|
|
583
782
|
);
|
|
584
783
|
|
|
585
784
|
yield* failpoint.hit("memory:change:before");
|
|
586
785
|
|
|
587
786
|
// The adapter must serialize before reading receipts and state. Node uses BEGIN
|
|
588
787
|
// IMMEDIATE; Durable Objects use the driver's storage-backed transaction and permit.
|
|
589
|
-
const transactionResult = yield*
|
|
590
|
-
.
|
|
591
|
-
|
|
592
|
-
|
|
788
|
+
const transactionResult = yield* transaction(
|
|
789
|
+
Effect.gen(function* () {
|
|
790
|
+
const retainedReceipts = yield* readReceipts(decodedWrites, operation);
|
|
791
|
+
|
|
792
|
+
const retainedDocuments = yield* readDocuments(
|
|
793
|
+
decodedWrites
|
|
794
|
+
.filter((write) => retainedReceipts.get(receiptViewKey(write)) === null)
|
|
795
|
+
.map((write) => write.key),
|
|
796
|
+
operation,
|
|
797
|
+
);
|
|
798
|
+
|
|
799
|
+
const documents = new Map<
|
|
800
|
+
string,
|
|
801
|
+
{
|
|
802
|
+
readonly document: MemoryDocument;
|
|
803
|
+
readonly bytes: number;
|
|
804
|
+
readonly documentJson: string;
|
|
805
|
+
}
|
|
806
|
+
>();
|
|
807
|
+
|
|
808
|
+
const receipts = new Map<
|
|
809
|
+
string,
|
|
810
|
+
{
|
|
811
|
+
readonly write: MemoryWrite;
|
|
812
|
+
readonly commandJson: string;
|
|
813
|
+
readonly resultJson: string;
|
|
814
|
+
readonly result: MemoryDocument;
|
|
815
|
+
}
|
|
816
|
+
>();
|
|
817
|
+
|
|
818
|
+
const results: Array<MemoryDocument> = [];
|
|
819
|
+
let projectedUsage: typeof UsageRow.Type | undefined;
|
|
820
|
+
|
|
821
|
+
for (const { write, commandJson } of commands) {
|
|
822
|
+
const receiptKey = receiptViewKey(write);
|
|
823
|
+
const receipt = receipts.get(receiptKey) ?? retainedReceipts.get(receiptKey) ?? null;
|
|
593
824
|
|
|
594
825
|
if (receipt !== null) {
|
|
595
826
|
if (receipt.commandJson !== commandJson) {
|
|
596
827
|
return yield* MemoryOperationConflict.make({
|
|
597
|
-
key:
|
|
598
|
-
operationId:
|
|
828
|
+
key: write.key,
|
|
829
|
+
operationId: write.operationId,
|
|
599
830
|
});
|
|
600
831
|
}
|
|
601
|
-
|
|
602
|
-
|
|
832
|
+
results.push(receipt.result);
|
|
833
|
+
continue;
|
|
603
834
|
}
|
|
604
835
|
|
|
605
|
-
const
|
|
836
|
+
const documentKey = documentViewKey(write.key);
|
|
837
|
+
|
|
838
|
+
const currentRow =
|
|
839
|
+
documents.get(documentKey) ?? retainedDocuments.get(documentKey) ?? null;
|
|
840
|
+
|
|
606
841
|
const current = currentRow?.document ?? null;
|
|
607
842
|
const modifiedAt = yield* Clock.currentTimeMillis;
|
|
608
|
-
const next = yield* applyMemoryWrite(current,
|
|
843
|
+
const next = yield* applyMemoryWrite(current, write, modifiedAt);
|
|
609
844
|
|
|
610
845
|
const resultJson = yield* encodeJson(
|
|
611
846
|
StoredMemoryResult,
|
|
@@ -623,17 +858,16 @@ const makeMemoryServices = Effect.fn("SqliteMemoryStore.make")(function* () {
|
|
|
623
858
|
const documentBytes = identityBytes + resultBytes + 128;
|
|
624
859
|
|
|
625
860
|
const receiptBytes =
|
|
626
|
-
identityBytes +
|
|
627
|
-
bytes(decodedWrite.operationId) +
|
|
628
|
-
bytes(commandJson) +
|
|
629
|
-
resultBytes +
|
|
630
|
-
128;
|
|
861
|
+
identityBytes + bytes(write.operationId) + bytes(commandJson) + resultBytes + 128;
|
|
631
862
|
|
|
632
863
|
if (Math.max(documentBytes, receiptBytes) > limits.maxRowBytes) {
|
|
633
864
|
return yield* storageError("memory row byte limit", "invalid-input");
|
|
634
865
|
}
|
|
635
866
|
|
|
636
|
-
const
|
|
867
|
+
const cachedUsage = views?.get("usage");
|
|
868
|
+
|
|
869
|
+
const used =
|
|
870
|
+
projectedUsage ?? (cachedUsage?._tag === "Usage" ? cachedUsage.value : yield* usage);
|
|
637
871
|
|
|
638
872
|
if (used.bytes < (currentRow?.bytes ?? 0) || (current !== null && used.documents === 0)) {
|
|
639
873
|
return yield* storageError("read memory usage", "corrupt");
|
|
@@ -641,14 +875,14 @@ const makeMemoryServices = Effect.fn("SqliteMemoryStore.make")(function* () {
|
|
|
641
875
|
|
|
642
876
|
const maxReceipts =
|
|
643
877
|
limits.maxReceipts -
|
|
644
|
-
(
|
|
878
|
+
(write._tag === "Put" ? (limits.reservedWithdrawalReceipts ?? 0) : 0);
|
|
645
879
|
|
|
646
880
|
const maxStorageBytes =
|
|
647
881
|
limits.maxStorageBytes -
|
|
648
|
-
(
|
|
882
|
+
(write._tag === "Put" ? (limits.reservedWithdrawalBytes ?? 0) : 0);
|
|
649
883
|
|
|
650
|
-
//
|
|
651
|
-
//
|
|
884
|
+
// Check every staged transition, including intermediate document sizes and
|
|
885
|
+
// receipts. A later shrinking command cannot evade the earlier write's limits.
|
|
652
886
|
if (
|
|
653
887
|
used.documents > limits.maxDocuments - (current === null ? 1 : 0) ||
|
|
654
888
|
used.receipts >= maxReceipts ||
|
|
@@ -657,74 +891,114 @@ const makeMemoryServices = Effect.fn("SqliteMemoryStore.make")(function* () {
|
|
|
657
891
|
return yield* storageError("memory storage limit", "invalid-input");
|
|
658
892
|
}
|
|
659
893
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
894
|
+
projectedUsage = {
|
|
895
|
+
documents: used.documents + (current === null ? 1 : 0),
|
|
896
|
+
receipts: used.receipts + 1,
|
|
897
|
+
bytes: used.bytes - (currentRow?.bytes ?? 0) + documentBytes + receiptBytes,
|
|
898
|
+
};
|
|
899
|
+
documents.set(documentKey, { document: next, bytes: documentBytes, documentJson });
|
|
900
|
+
receipts.set(receiptKey, { write, commandJson, resultJson, result: next });
|
|
901
|
+
results.push(next);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
if (projectedUsage === undefined) return { documents: results, changed: false };
|
|
905
|
+
|
|
906
|
+
// Both persisted rows have six bound columns: sixteen rows stay below the DO
|
|
907
|
+
// driver's 100-parameter limit. The surrounding transaction owns all revision
|
|
908
|
+
// checks, and only the final row for each key needs physical writeback.
|
|
909
|
+
const documentRows = Array.from(documents.values(), ({ document, documentJson }) => ({
|
|
910
|
+
namespace: document.key.namespace.address,
|
|
911
|
+
source_id: document.key.id,
|
|
912
|
+
format_version: STORAGE_VERSION,
|
|
913
|
+
generation: document.generation,
|
|
914
|
+
revision: document.source.revision,
|
|
915
|
+
document_json: documentJson,
|
|
916
|
+
}));
|
|
917
|
+
|
|
918
|
+
for (let index = 0; index < documentRows.length; index += 16) {
|
|
919
|
+
const chunk = documentRows.slice(index, index + 16);
|
|
682
920
|
|
|
683
921
|
const changedRows = yield* sql<Record<string, unknown>>`
|
|
684
|
-
|
|
685
|
-
|
|
922
|
+
INSERT INTO effect_agent_memory_documents_v1 ${sql.insert(chunk)}
|
|
923
|
+
ON CONFLICT (namespace, source_id) DO UPDATE SET
|
|
924
|
+
format_version = excluded.format_version,
|
|
925
|
+
generation = excluded.generation,
|
|
926
|
+
revision = excluded.revision,
|
|
927
|
+
document_json = excluded.document_json
|
|
928
|
+
RETURNING 1 AS changed
|
|
929
|
+
`;
|
|
686
930
|
|
|
687
931
|
const changed = yield* decodeRows(MemoryChangeCountRow, changedRows, operation);
|
|
688
932
|
|
|
689
|
-
if (changed.length !==
|
|
933
|
+
if (changed.length !== chunk.length || changed.some((row) => row.changed !== 1)) {
|
|
690
934
|
return yield* storageError(operation, "corrupt");
|
|
691
935
|
}
|
|
692
|
-
|
|
936
|
+
}
|
|
937
|
+
yield* failpoint.hit("memory:change:after-state");
|
|
938
|
+
|
|
939
|
+
const receiptRows = Array.from(receipts.values(), ({ write, commandJson, resultJson }) => ({
|
|
940
|
+
namespace: write.key.namespace.address,
|
|
941
|
+
operation_id: write.operationId,
|
|
942
|
+
source_id: write.key.id,
|
|
943
|
+
format_version: STORAGE_VERSION,
|
|
944
|
+
command_json: commandJson,
|
|
945
|
+
result_json: resultJson,
|
|
946
|
+
}));
|
|
947
|
+
|
|
948
|
+
for (let index = 0; index < receiptRows.length; index += 16) {
|
|
693
949
|
yield* sql`
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
950
|
+
INSERT INTO effect_agent_memory_receipts_v1 ${sql.insert(receiptRows.slice(index, index + 16))}
|
|
951
|
+
`;
|
|
952
|
+
}
|
|
953
|
+
yield* failpoint.hit("memory:change:after-receipt");
|
|
954
|
+
|
|
955
|
+
for (const [key, { document, bytes }] of documents)
|
|
956
|
+
views?.set(key, { _tag: "Document", value: { document, bytes } });
|
|
957
|
+
for (const [key, { commandJson, result }] of receipts)
|
|
958
|
+
views?.set(key, { _tag: "Receipt", value: { commandJson, result } });
|
|
959
|
+
views?.set("usage", { _tag: "Usage", value: projectedUsage });
|
|
960
|
+
|
|
961
|
+
return { documents: results, changed: true };
|
|
962
|
+
}),
|
|
963
|
+
).pipe(Effect.catchTag("SqlError", () => Effect.fail(storageError(operation))));
|
|
707
964
|
|
|
708
965
|
if (transactionResult.changed) yield* failpoint.hit("memory:change:after");
|
|
709
966
|
|
|
710
|
-
return transactionResult.
|
|
967
|
+
return transactionResult.documents;
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
const change = Effect.fn("SqliteMemoryStore.change")(function* (write: MemoryWrite) {
|
|
971
|
+
return (yield* changeMany([write]))[0];
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
const batchWriter = SqlMemoryBatchWriter.of({
|
|
975
|
+
changeMany: Effect.fn("SqlMemoryBatchWriter.changeMany")(function* <
|
|
976
|
+
Namespace extends MemoryNamespace,
|
|
977
|
+
>(writes: ReadonlyArray<MemoryWrite<Namespace>>) {
|
|
978
|
+
const documents = yield* changeMany(writes);
|
|
979
|
+
|
|
980
|
+
return yield* Effect.forEach(documents, (document, index) =>
|
|
981
|
+
MemoryDocument.restore(writes[index].key.namespace, document),
|
|
982
|
+
);
|
|
983
|
+
}),
|
|
711
984
|
});
|
|
712
985
|
|
|
713
986
|
return Context.make(MemoryReader, MemoryReader.fromAdapter({ get })).pipe(
|
|
714
987
|
Context.add(MemoryWriter, MemoryWriter.fromAdapter({ change })),
|
|
988
|
+
Context.add(SqlMemoryBatchWriter, batchWriter),
|
|
715
989
|
);
|
|
716
990
|
});
|
|
717
991
|
|
|
718
992
|
/** SQLite memory ports with the mutation failpoint kept injectable for recovery tests. */
|
|
719
993
|
export const memoryStoreLayerWithFailpoints: Layer.Layer<
|
|
720
|
-
MemoryReader | MemoryWriter,
|
|
994
|
+
MemoryReader | MemoryWriter | SqlMemoryBatchWriter,
|
|
721
995
|
SqliteMemoryInitializationError,
|
|
722
996
|
SqlClientService.SqlClient | MemoryMutationFailpoint
|
|
723
997
|
> = Layer.effectContext(makeMemoryServices());
|
|
724
998
|
|
|
725
|
-
/** SQLite memory reader and writer with the production no-op
|
|
999
|
+
/** SQLite memory reader, single writer and batch writer with the production no-op failpoint. */
|
|
726
1000
|
export const memoryStoreLayer: Layer.Layer<
|
|
727
|
-
MemoryReader | MemoryWriter,
|
|
1001
|
+
MemoryReader | MemoryWriter | SqlMemoryBatchWriter,
|
|
728
1002
|
SqliteMemoryInitializationError,
|
|
729
1003
|
SqlClientService.SqlClient
|
|
730
1004
|
> = memoryStoreLayerWithFailpoints.pipe(Layer.provide(MemoryMutationFailpoint.layer));
|