@yielded/agent-storage-cloudflare 0.1.0-beta.166

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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/dist/DoMemoryStore.d.mts +30 -0
  3. package/dist/DoMemoryStore.mjs +66 -0
  4. package/dist/DoMemoryStore.mjs.map +1 -0
  5. package/dist/DoMessageDeliveryStore.d.mts +17 -0
  6. package/dist/DoMessageDeliveryStore.mjs +30 -0
  7. package/dist/DoMessageDeliveryStore.mjs.map +1 -0
  8. package/dist/DoScheduleStore.d.mts +36 -0
  9. package/dist/DoScheduleStore.mjs +388 -0
  10. package/dist/DoScheduleStore.mjs.map +1 -0
  11. package/dist/DoStorageConfig.d.mts +45 -0
  12. package/dist/DoStorageConfig.mjs +52 -0
  13. package/dist/DoStorageConfig.mjs.map +1 -0
  14. package/dist/DoStorageError-CGuAfSwF.d.mts +101 -0
  15. package/dist/DoStorageError.d.mts +2 -0
  16. package/dist/DoStorageError.mjs +167 -0
  17. package/dist/DoStorageError.mjs.map +1 -0
  18. package/dist/DoStorageFailpoint.d.mts +17 -0
  19. package/dist/DoStorageFailpoint.mjs +14 -0
  20. package/dist/DoStorageFailpoint.mjs.map +1 -0
  21. package/dist/DoStorageFailpointTesting.d.mts +24 -0
  22. package/dist/DoStorageFailpointTesting.mjs +22 -0
  23. package/dist/DoStorageFailpointTesting.mjs.map +1 -0
  24. package/dist/DoStorageVersion-iNSyi3cJ.d.mts +12 -0
  25. package/dist/DoStorageVersion.d.mts +2 -0
  26. package/dist/DoStorageVersion.mjs +8 -0
  27. package/dist/DoStorageVersion.mjs.map +1 -0
  28. package/dist/DoSubmissionLedger.d.mts +23 -0
  29. package/dist/DoSubmissionLedger.mjs +1979 -0
  30. package/dist/DoSubmissionLedger.mjs.map +1 -0
  31. package/dist/DoSubscriptionStore.d.mts +25 -0
  32. package/dist/DoSubscriptionStore.mjs +186 -0
  33. package/dist/DoSubscriptionStore.mjs.map +1 -0
  34. package/dist/DoThreadStore-D_83OjCH.d.mts +72 -0
  35. package/dist/DoThreadStore.d.mts +2 -0
  36. package/dist/DoThreadStore.mjs +529 -0
  37. package/dist/DoThreadStore.mjs.map +1 -0
  38. package/dist/MemoryProtocol.d.mts +38160 -0
  39. package/dist/MemoryProtocol.mjs +219 -0
  40. package/dist/MemoryProtocol.mjs.map +1 -0
  41. package/dist/PortProtocol.d.mts +7726 -0
  42. package/dist/PortProtocol.mjs +234 -0
  43. package/dist/PortProtocol.mjs.map +1 -0
  44. package/dist/PortRouting.d.mts +97 -0
  45. package/dist/PortRouting.mjs +544 -0
  46. package/dist/PortRouting.mjs.map +1 -0
  47. package/dist/do-journal-DUhjZkOf.mjs +1352 -0
  48. package/dist/do-journal-DUhjZkOf.mjs.map +1 -0
  49. package/dist/index.d.mts +14 -0
  50. package/dist/index.mjs +14 -0
  51. package/dist/migrations-DtKz_Gxr.mjs +306 -0
  52. package/dist/migrations-DtKz_Gxr.mjs.map +1 -0
  53. package/dist/owned-state-DHm2aRXx.mjs +119 -0
  54. package/dist/owned-state-DHm2aRXx.mjs.map +1 -0
  55. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  56. package/package.json +1 -0
  57. package/src/DoMemoryStore.ts +73 -0
  58. package/src/DoMessageDeliveryStore.ts +39 -0
  59. package/src/DoScheduleStore.ts +622 -0
  60. package/src/DoStorageConfig.ts +53 -0
  61. package/src/DoStorageError.ts +180 -0
  62. package/src/DoStorageFailpoint.ts +20 -0
  63. package/src/DoStorageFailpointTesting.ts +41 -0
  64. package/src/DoStorageVersion.ts +2 -0
  65. package/src/DoSubmissionLedger.ts +4027 -0
  66. package/src/DoSubscriptionStore.ts +327 -0
  67. package/src/DoThreadStore.ts +1121 -0
  68. package/src/MemoryProtocol.ts +355 -0
  69. package/src/PortProtocol.ts +436 -0
  70. package/src/PortRouting.ts +1464 -0
  71. package/src/index.ts +13 -0
  72. package/src/internal/do-journal.ts +2282 -0
  73. package/src/internal/message-delivery-schema.ts +24 -0
  74. package/src/internal/migrations.ts +304 -0
  75. package/src/internal/owned-state.ts +228 -0
  76. package/src/internal/recovery-checkpoint-schema.ts +17 -0
  77. package/src/internal/storage-span.ts +44 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrations-DtKz_Gxr.mjs","names":["SqlClient","SqlClient","SqlClient"],"sources":["../src/internal/message-delivery-schema.ts","../src/internal/recovery-checkpoint-schema.ts","../src/internal/migrations.ts"],"sourcesContent":["import { Effect } from \"effect\";\nimport * as SqlClient from \"effect/sql/SqlClient\";\n\n/** Additive storage owned by this adapter; creation participates in its version transaction. */\nexport const createMessageDeliveryTables = Effect.gen(function* () {\n const sql = yield* SqlClient.SqlClient;\n\n yield* sql`\n CREATE TABLE effect_agent_message_deliveries (\n owner_thread_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n version INTEGER NOT NULL,\n state TEXT NOT NULL,\n deadline_at_millis INTEGER,\n record_json TEXT NOT NULL,\n PRIMARY KEY (owner_thread_id, message_id)\n )\n `.withoutTransform;\n yield* sql`\n CREATE INDEX effect_agent_message_deliveries_due\n ON effect_agent_message_deliveries (deadline_at_millis, owner_thread_id, message_id)\n WHERE deadline_at_millis IS NOT NULL\n `.withoutTransform;\n});\n","import { Effect } from \"effect\";\nimport * as SqlClient from \"effect/sql/SqlClient\";\n\n/** One disposable recovery snapshot per Thread, independent of generic projections. */\nexport const createRecoveryCheckpointTable = Effect.gen(function* () {\n const sql = yield* SqlClient.SqlClient;\n\n yield* sql`\n CREATE TABLE effect_agent_recovery_checkpoints (\n thread_id TEXT PRIMARY KEY NOT NULL,\n through_sequence INTEGER NOT NULL,\n tail_digest TEXT NOT NULL,\n checkpoint_json TEXT NOT NULL,\n FOREIGN KEY (thread_id) REFERENCES effect_agent_threads(thread_id) ON DELETE RESTRICT\n )\n `.withoutTransform;\n});\n","import { SqliteMigrator } from \"@effect/sql-sqlite-do\";\nimport { createMessageDeliveryPendingIndex } from \"@yielded/agent-storage-sql/sql-message-delivery-store\";\nimport { createNativeReadIndexes } from \"@yielded/agent-storage-sql/sql-thread-native-reads\";\nimport { Effect } from \"effect\";\nimport * as SqlClient from \"effect/sql/SqlClient\";\n\nimport { createMessageDeliveryTables } from \"./message-delivery-schema.ts\";\nimport { createRecoveryCheckpointTable } from \"./recovery-checkpoint-schema.ts\";\n\n/** The current storage version recorded in `effect_agent_meta`. */\nexport const CurrentDoStorageVersion = 9;\n\n/** One permanent destination inbox fence, including workers stopped before admission. */\nexport const createWorkerStops = Effect.gen(function* () {\n const sql = yield* SqlClient.SqlClient;\n\n yield* sql`CREATE TABLE effect_agent_worker_stops (thread_id TEXT PRIMARY KEY NOT NULL, terminal TEXT)`;\n yield* sql`CREATE INDEX effect_agent_worker_starts ON effect_agent_message_deliveries(owner_thread_id,\n json_extract(record_json, '$.envelope.workerAdmission.origin.worker.delegationId'),\n json_extract(record_json, '$.envelope.workerAdmission.origin.worker.targetAgentId'), message_id)\n WHERE message_id = json_extract(record_json, '$.envelope.workerAdmission.origin.firstMessageId')`;\n yield* sql`CREATE INDEX effect_agent_worker_pending ON effect_agent_message_deliveries(owner_thread_id,\n json_extract(record_json, '$.envelope.workerAdmission.origin.worker.threadId'), message_id)\n WHERE state IN ('pending', 'parked') AND json_extract(record_json, '$.receipt') IS NULL`;\n yield* sql`CREATE INDEX effect_agent_worker_execution ON effect_agent_canonical_records(thread_id,\n json_extract(record_json, '$.payload._tag'), sequence) WHERE json_extract(record_json, '$.payload.runId') IS NOT NULL`;\n});\n\n/** Index only outstanding obligations, ordered by the recovery scan's stable cursor. */\nexport const createNonterminalIndex = Effect.gen(function* () {\n const sql = yield* SqlClient.SqlClient;\n\n yield* sql`CREATE INDEX effect_agent_submissions_nonterminal ON effect_agent_submissions (thread_id, queue_sequence) WHERE state <> 'settled'`\n .withoutTransform;\n});\n\n/**\n * The Thread Durable Object schema shares its thread and ledger tables with Node/SQLite.\n * Schedules and subscriptions use separate Durable Objects. Two DC-specific additions:\n *\n * 1. `effect_agent_meta` replaces `PRAGMA user_version` as the exact-or-fresh version gate —\n * a meta table is portable regardless of which PRAGMAs Durable Object SQL storage allows.\n * 2. `effect_agent_child_settlements` is the durable cross-store notification marker the\n * SubmissionLedger port contract mandates for cross-store adapters (`suspend`'s covering\n * check and `recordChildSettled`'s wake both consult it): parent and child Threads\n * live in different Durable Objects, so a child settlement reported before the parent's\n * suspend commits must be observable from the PARENT's own storage.\n */\nexport const doMigrations = SqliteMigrator.fromRecord({\n \"1_current_cloudflare_thread_object\": Effect.gen(function* () {\n const sql = yield* SqlClient.SqlClient;\n\n yield* sql`\n CREATE TABLE effect_agent_threads (\n thread_id TEXT PRIMARY KEY NOT NULL,\n created_at TEXT NOT NULL,\n tail_sequence INTEGER NOT NULL,\n tail_digest TEXT NOT NULL,\n producer_epoch INTEGER NOT NULL\n )\n `.withoutTransform;\n\n yield* sql`\n CREATE TABLE effect_agent_canonical_batches (\n thread_id TEXT NOT NULL,\n batch_id TEXT NOT NULL,\n first_sequence INTEGER NOT NULL,\n last_sequence INTEGER NOT NULL,\n batch_digest TEXT NOT NULL,\n tail_digest TEXT NOT NULL,\n batch_json TEXT NOT NULL,\n PRIMARY KEY (thread_id, batch_id),\n FOREIGN KEY (thread_id)\n REFERENCES effect_agent_threads(thread_id)\n ON DELETE RESTRICT\n )\n `.withoutTransform;\n\n yield* sql`\n CREATE TABLE effect_agent_canonical_records (\n thread_id TEXT NOT NULL,\n sequence INTEGER NOT NULL,\n record_id TEXT NOT NULL,\n batch_id TEXT NOT NULL,\n record_json TEXT NOT NULL,\n PRIMARY KEY (thread_id, sequence),\n UNIQUE (thread_id, record_id),\n FOREIGN KEY (thread_id, batch_id)\n REFERENCES effect_agent_canonical_batches(thread_id, batch_id)\n ON DELETE RESTRICT\n )\n `.withoutTransform;\n\n yield* sql`\n CREATE INDEX effect_agent_canonical_records_batch\n ON effect_agent_canonical_records (thread_id, batch_id, sequence)\n `.withoutTransform;\n\n yield* sql`\n CREATE TABLE effect_agent_checkpoints (\n thread_id TEXT NOT NULL,\n through_sequence INTEGER NOT NULL,\n tail_digest TEXT NOT NULL,\n checkpoint_json TEXT NOT NULL,\n PRIMARY KEY (thread_id, through_sequence),\n FOREIGN KEY (thread_id)\n REFERENCES effect_agent_threads(thread_id)\n ON DELETE RESTRICT\n )\n `.withoutTransform;\n\n // Admission rows exist before Thread materialization (durability §4), so\n // thread_id intentionally carries no foreign key into effect_agent_threads.\n yield* sql`\n CREATE TABLE effect_agent_submissions (\n submission_id TEXT PRIMARY KEY NOT NULL,\n thread_id TEXT NOT NULL,\n queue_sequence INTEGER NOT NULL,\n principal TEXT NOT NULL,\n idempotency_key TEXT NOT NULL,\n agent_id TEXT NOT NULL,\n agent_digests_json TEXT NOT NULL,\n deployment_id TEXT NOT NULL,\n input_json TEXT NOT NULL,\n input_digest TEXT NOT NULL,\n receipt_id TEXT NOT NULL,\n state TEXT NOT NULL,\n settled_outcome TEXT,\n created_at TEXT NOT NULL,\n ready_at TEXT,\n input_applied_record_id TEXT,\n input_applied_sequence INTEGER,\n joined_host_submission_id TEXT,\n suspended_reason_json TEXT,\n suspended_at TEXT,\n unknown_reason TEXT,\n unknown_tool_call_ids_json TEXT,\n parent_submission_id TEXT,\n parent_tool_call_id TEXT,\n admission_group TEXT,\n admission_fence_json TEXT,\n worker_admission_json TEXT,\n message_admission_json TEXT,\n UNIQUE (thread_id, principal, idempotency_key),\n UNIQUE (thread_id, queue_sequence)\n )\n `.withoutTransform;\n\n yield* sql`\n CREATE INDEX effect_agent_submissions_joined_host\n ON effect_agent_submissions (joined_host_submission_id)\n `.withoutTransform;\n\n yield* sql`\n CREATE INDEX effect_agent_submissions_parent\n ON effect_agent_submissions (parent_submission_id)\n `.withoutTransform;\n\n yield* sql`\n CREATE INDEX effect_agent_submissions_group\n ON effect_agent_submissions (thread_id, admission_group, state)\n `.withoutTransform;\n\n yield* sql`\n CREATE TABLE effect_agent_submission_ownership (\n submission_id TEXT PRIMARY KEY NOT NULL,\n attempt_id TEXT NOT NULL,\n ownership_token TEXT NOT NULL,\n producer_epoch INTEGER NOT NULL,\n owner_producer_id TEXT NOT NULL,\n lease_expires_at TEXT NOT NULL,\n FOREIGN KEY (submission_id)\n REFERENCES effect_agent_submissions(submission_id)\n ON DELETE RESTRICT\n )\n `.withoutTransform;\n\n yield* sql`\n CREATE TABLE effect_agent_attempts (\n attempt_id TEXT PRIMARY KEY NOT NULL,\n submission_id TEXT NOT NULL,\n thread_id TEXT NOT NULL,\n owner_producer_id TEXT NOT NULL,\n producer_epoch INTEGER NOT NULL,\n claimed_at TEXT NOT NULL,\n FOREIGN KEY (submission_id)\n REFERENCES effect_agent_submissions(submission_id)\n ON DELETE RESTRICT\n )\n `.withoutTransform;\n\n yield* sql`\n CREATE TABLE effect_agent_settlement_reservations (\n submission_id TEXT PRIMARY KEY NOT NULL,\n settlement_id TEXT NOT NULL,\n outcome TEXT NOT NULL,\n record_id TEXT NOT NULL,\n record_json TEXT NOT NULL,\n record_digest TEXT NOT NULL,\n reserved_at TEXT NOT NULL,\n finalized_at TEXT,\n FOREIGN KEY (submission_id)\n REFERENCES effect_agent_submissions(submission_id)\n ON DELETE RESTRICT\n )\n `.withoutTransform;\n\n yield* sql`\n CREATE TABLE effect_agent_abort_intents (\n submission_id TEXT PRIMARY KEY NOT NULL,\n author TEXT NOT NULL,\n reason TEXT NOT NULL,\n requested_at TEXT NOT NULL,\n canonical_record_id TEXT,\n FOREIGN KEY (submission_id)\n REFERENCES effect_agent_submissions(submission_id)\n ON DELETE RESTRICT\n )\n `.withoutTransform;\n\n yield* sql`\n CREATE TABLE effect_agent_approval_decisions (\n submission_id TEXT NOT NULL,\n tool_call_id TEXT NOT NULL,\n decision TEXT NOT NULL,\n resolver TEXT NOT NULL,\n reason TEXT NOT NULL,\n decided_at TEXT NOT NULL,\n PRIMARY KEY (submission_id, tool_call_id),\n FOREIGN KEY (submission_id)\n REFERENCES effect_agent_submissions(submission_id)\n ON DELETE RESTRICT\n )\n `.withoutTransform;\n\n yield* sql`\n CREATE TABLE effect_agent_unknown_resolutions (\n submission_id TEXT NOT NULL,\n tool_call_id TEXT NOT NULL,\n author TEXT NOT NULL,\n reason TEXT NOT NULL,\n resolution_json TEXT NOT NULL,\n resolved_at TEXT NOT NULL,\n PRIMARY KEY (submission_id, tool_call_id),\n FOREIGN KEY (submission_id)\n REFERENCES effect_agent_submissions(submission_id)\n ON DELETE RESTRICT\n )\n `.withoutTransform;\n\n yield* sql`\n CREATE TABLE effect_agent_child_reservations (\n reservation_id TEXT PRIMARY KEY NOT NULL,\n parent_submission_id TEXT NOT NULL,\n parent_tool_call_id TEXT NOT NULL,\n child_submission_id TEXT,\n status TEXT NOT NULL,\n allocation_json TEXT NOT NULL,\n allocation_digest TEXT NOT NULL,\n accounting_json TEXT,\n reserved_at TEXT NOT NULL,\n release_began_at TEXT,\n released_at TEXT,\n UNIQUE (parent_submission_id, parent_tool_call_id),\n FOREIGN KEY (parent_submission_id)\n REFERENCES effect_agent_submissions(submission_id)\n ON DELETE RESTRICT\n )\n `.withoutTransform;\n\n // Durable cross-store child-settlement notification marker (parent-side; the child's row\n // lives in ANOTHER Durable Object). child_outcome is nullable: the notification command\n // carries identities only, and the child's canonical Settlement stays the outcome\n // authority (DUR-015). No foreign keys: the parent row is checked by the operation, and\n // the child row is intentionally foreign.\n yield* sql`\n CREATE TABLE effect_agent_child_settlements (\n parent_submission_id TEXT NOT NULL,\n child_submission_id TEXT NOT NULL,\n child_outcome TEXT,\n recorded_at TEXT NOT NULL,\n PRIMARY KEY (parent_submission_id, child_submission_id)\n )\n `.withoutTransform;\n\n yield* createNativeReadIndexes();\n yield* createNonterminalIndex;\n yield* createMessageDeliveryTables;\n yield* createMessageDeliveryPendingIndex();\n yield* createRecoveryCheckpointTable;\n yield* createWorkerStops;\n yield* sql`\n CREATE TABLE effect_agent_meta (\n key TEXT PRIMARY KEY NOT NULL,\n value TEXT NOT NULL\n )\n `.withoutTransform;\n\n yield* sql`\n INSERT INTO effect_agent_meta (key, value)\n VALUES ('storage_version', ${String(CurrentDoStorageVersion)})\n `.withoutTransform;\n }),\n});\n"],"mappings":";;;;;;;AAIA,MAAa,8BAA8B,OAAO,IAAI,aAAa;CACjE,MAAM,MAAM,OAAOA,YAAU;CAE7B,OAAO,GAAG;;;;;;;;;;IAUR;CACF,OAAO,GAAG;;;;IAIR;AACJ,CAAC;;;;ACnBD,MAAa,gCAAgC,OAAO,IAAI,aAAa;CAGnE,OAAO,CAAA,OAFYC,YAAU,UAAA,AAEnB;;;;;;;;IAQR;AACJ,CAAC;;;;ACND,MAAa,0BAA0B;;AAGvC,MAAa,oBAAoB,OAAO,IAAI,aAAa;CACvD,MAAM,MAAM,OAAOC,YAAU;CAE7B,OAAO,GAAG;CACV,OAAO,GAAG;;;;CAIV,OAAO,GAAG;;;CAGV,OAAO,GAAG;;AAEZ,CAAC;;AAGD,MAAa,yBAAyB,OAAO,IAAI,aAAa;CAG5D,OAAO,CAAA,OAFYA,YAAU,UAAA,AAEnB,qIACP;AACL,CAAC;;;;;;;;;;;;;AAcD,MAAa,eAAe,eAAe,WAAW,EACpD,sCAAsC,OAAO,IAAI,aAAa;CAC5D,MAAM,MAAM,OAAOA,YAAU;CAE7B,OAAO,GAAG;;;;;;;;MAQR;CAEF,OAAO,GAAG;;;;;;;;;;;;;;MAcR;CAEF,OAAO,GAAG;;;;;;;;;;;;;MAaR;CAEF,OAAO,GAAG;;;MAGR;CAEF,OAAO,GAAG;;;;;;;;;;;MAWR;CAIF,OAAO,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAiCR;CAEF,OAAO,GAAG;;;MAGR;CAEF,OAAO,GAAG;;;MAGR;CAEF,OAAO,GAAG;;;MAGR;CAEF,OAAO,GAAG;;;;;;;;;;;;MAYR;CAEF,OAAO,GAAG;;;;;;;;;;;;MAYR;CAEF,OAAO,GAAG;;;;;;;;;;;;;;MAcR;CAEF,OAAO,GAAG;;;;;;;;;;;MAWR;CAEF,OAAO,GAAG;;;;;;;;;;;;;MAaR;CAEF,OAAO,GAAG;;;;;;;;;;;;;MAaR;CAEF,OAAO,GAAG;;;;;;;;;;;;;;;;;;MAkBR;CAOF,OAAO,GAAG;;;;;;;;MAQR;CAEF,OAAO,wBAAwB;CAC/B,OAAO;CACP,OAAO;CACP,OAAO,kCAAkC;CACzC,OAAO;CACP,OAAO;CACP,OAAO,GAAG;;;;;MAKR;CAEF,OAAO,GAAG;;mCAEqB,OAAA,CAA8B,EAAE;MAC7D;AACJ,CAAC,EACH,CAAC"}
@@ -0,0 +1,119 @@
1
+ import { DoStorageCorruptionError, DoStorageError } from "./DoStorageError.mjs";
2
+ import { Context, Effect, Exit, Predicate, Schema, Semaphore } from "effect";
3
+ import { SqlClient } from "effect/sql/SqlClient";
4
+ import { SqliteClient } from "@effect/sql-sqlite-do";
5
+ //#region src/internal/owned-state.ts
6
+ const ActiveState = Context.Reference("@effect-agent/storage-cloudflare/internal/ActiveState", { defaultValue: () => void 0 });
7
+ /** One disposable view per physical Object database, including separately acquired adapters. */
8
+ var OwnedState = class {
9
+ gate = Semaphore.makeUnsafe(1);
10
+ invalidators = /* @__PURE__ */ new Set();
11
+ invalidate = Effect.sync(() => {
12
+ for (const invalidate of this.invalidators) invalidate();
13
+ });
14
+ /** Cached readers must wait for the complete transaction, including rollback/commit. */
15
+ read = (effect) => Effect.flatMap(ActiveState, (active) => active === this ? effect : this.gate.withPermits(1)(Effect.provideService(effect, ActiveState, this)));
16
+ transaction = (effect) => this.read(Effect.flatMap(SqlClient, (sql) => sql.withTransaction(effect).pipe(Effect.onExit((exit) => Exit.isFailure(exit) ? this.invalidate : Effect.void))));
17
+ };
18
+ const states = /* @__PURE__ */ new WeakMap();
19
+ /** Explicit invalidation for maintenance that writes private tables outside the adapters. */
20
+ const invalidateOwnedState = (storage) => Effect.suspend(() => {
21
+ const state = states.get(storage);
22
+ return state === void 0 ? Effect.void : state.read(state.invalidate);
23
+ });
24
+ const DoClient = Schema.declare((value) => Predicate.hasProperty(value, SqliteClient.TypeId));
25
+ const ownedState = (sql) => Schema.decodeUnknownEffect(DoClient)(sql).pipe(Effect.mapError(() => DoStorageError.make({
26
+ operation: "open owned state",
27
+ message: "Owned state requires a Durable Object SQLite client."
28
+ })), Effect.map((client) => {
29
+ const key = client.config.storage ?? client.config.db ?? client;
30
+ let state = states.get(key);
31
+ if (state === void 0) {
32
+ state = new OwnedState();
33
+ states.set(key, state);
34
+ }
35
+ return state;
36
+ }));
37
+ /**
38
+ * Demand-loaded row views. Writes apply SQLite's RETURNING rows to every retained view in
39
+ * the same transaction; rollback discards them. Bounded views avoid loading a host's entire
40
+ * retained submission history just to fence one live submission.
41
+ */
42
+ const ownedRows = (schema, table, key, identityColumn, uniqueFields = []) => {
43
+ const caches = /* @__PURE__ */ new WeakMap();
44
+ return (state, sql) => {
45
+ let cache = caches.get(state);
46
+ if (cache === void 0) {
47
+ cache = /* @__PURE__ */ new Map();
48
+ caches.set(state, cache);
49
+ const current = cache;
50
+ state.invalidators.add(() => current.clear());
51
+ }
52
+ const current = cache;
53
+ const decode = (rows) => Schema.decodeUnknownEffect(Schema.Array(schema))(rows).pipe(Effect.mapError(() => DoStorageCorruptionError.make({
54
+ table,
55
+ rowKey: "owned-state",
56
+ message: "Stored rows do not satisfy the storage schema"
57
+ })));
58
+ const retain = (id, matches, rows, maxRows) => {
59
+ const bytes = new TextEncoder().encode(JSON.stringify(rows)).byteLength;
60
+ current.delete(id);
61
+ if (rows.length > maxRows || bytes > 4194304) return;
62
+ current.set(id, {
63
+ id,
64
+ matches,
65
+ maxRows,
66
+ rows,
67
+ bytes
68
+ });
69
+ let total = 0;
70
+ for (const view of current.values()) total += view.bytes;
71
+ while (current.size > 128 || total > 4194304) {
72
+ const oldest = current.entries().next().value;
73
+ if (oldest === void 0) break;
74
+ total -= oldest[1].bytes;
75
+ current.delete(oldest[0]);
76
+ }
77
+ };
78
+ const matching = (id, matches, query, maxRows = Infinity) => state.read(Effect.gen(function* () {
79
+ const view = current.get(id);
80
+ if (view !== void 0) return view.rows;
81
+ const rows = yield* decode(yield* query);
82
+ retain(id, matches, rows, maxRows);
83
+ return rows;
84
+ }));
85
+ const by = (field, value) => matching(JSON.stringify([field, value]), (row) => row[field] === value, sql`SELECT * FROM ${sql(table)} WHERE ${sql(field)} = ${value}`);
86
+ const seed = (field, value, rows) => retain(JSON.stringify([field, value]), (row) => row[field] === value, rows, Infinity);
87
+ const byFields = (fields) => matching(JSON.stringify(fields), (row) => fields.every(([field, value]) => row[field] === value), sql`SELECT * FROM ${sql(table)} WHERE ${sql.and(fields.map(([field, value]) => sql`${sql(field)} = ${value}`))}`);
88
+ const apply = (remove) => (effect) => Effect.gen(function* () {
89
+ const rows = yield* decode(yield* effect);
90
+ const changed = new Set(rows.map(key));
91
+ for (const { id, matches, maxRows, rows: prior } of Array.from(current.values())) {
92
+ const next = prior.filter((row) => !changed.has(key(row)));
93
+ if (!remove) next.push(...rows.filter(matches));
94
+ retain(id, matches, next, maxRows);
95
+ }
96
+ if (identityColumn !== void 0) for (const row of rows) {
97
+ const value = row[identityColumn];
98
+ if (typeof value === "string") retain(JSON.stringify([identityColumn, value]), (row) => row[identityColumn] === value, remove ? [] : [row], Infinity);
99
+ }
100
+ for (const row of rows) for (const fields of uniqueFields) {
101
+ const values = fields.map((field) => [field, row[field]]);
102
+ retain(JSON.stringify(values), (candidate) => values.every(([field, value]) => candidate[field] === value), remove ? [] : [row], 1);
103
+ }
104
+ return rows;
105
+ });
106
+ return {
107
+ by,
108
+ byFields,
109
+ matching,
110
+ seed,
111
+ write: apply(false),
112
+ remove: apply(true)
113
+ };
114
+ };
115
+ };
116
+ //#endregion
117
+ export { ownedRows as n, ownedState as r, invalidateOwnedState as t };
118
+
119
+ //# sourceMappingURL=owned-state-DHm2aRXx.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"owned-state-DHm2aRXx.mjs","names":[],"sources":["../src/internal/owned-state.ts"],"sourcesContent":["import { SqliteClient } from \"@effect/sql-sqlite-do\";\nimport { Context, Effect, Exit, Predicate, Schema, Semaphore } from \"effect\";\nimport { SqlClient } from \"effect/sql/SqlClient\";\nimport type { SqlError } from \"effect/sql/SqlError\";\n\nimport { DoStorageCorruptionError, DoStorageError } from \"../DoStorageError.ts\";\n\nconst ActiveState = Context.Reference<OwnedState | undefined>(\n \"@effect-agent/storage-cloudflare/internal/ActiveState\",\n { defaultValue: () => undefined },\n);\n\n/** One disposable view per physical Object database, including separately acquired adapters. */\nexport class OwnedState {\n private readonly gate = Semaphore.makeUnsafe(1);\n readonly invalidators = new Set<() => void>();\n\n readonly invalidate = Effect.sync(() => {\n for (const invalidate of this.invalidators) invalidate();\n });\n\n /** Cached readers must wait for the complete transaction, including rollback/commit. */\n readonly read = <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>\n Effect.flatMap(ActiveState, (active) =>\n active === this\n ? effect\n : this.gate.withPermits(1)(Effect.provideService(effect, ActiveState, this)),\n );\n\n readonly transaction = <A, E, R>(\n effect: Effect.Effect<A, E, R>,\n ): Effect.Effect<A, E | SqlError, R | SqlClient> =>\n this.read(\n Effect.flatMap(SqlClient, (sql) =>\n sql\n .withTransaction(effect)\n .pipe(Effect.onExit((exit) => (Exit.isFailure(exit) ? this.invalidate : Effect.void))),\n ),\n );\n}\n\nconst states = new WeakMap<object, OwnedState>();\n\n/** Explicit invalidation for maintenance that writes private tables outside the adapters. */\nexport const invalidateOwnedState = (storage: DurableObjectStorage): Effect.Effect<void> =>\n Effect.suspend(() => {\n const state = states.get(storage);\n\n return state === undefined ? Effect.void : state.read(state.invalidate);\n });\n\nconst DoClient = Schema.declare<SqliteClient.SqliteClient>(\n (value): value is SqliteClient.SqliteClient => Predicate.hasProperty(value, SqliteClient.TypeId),\n);\n\nexport const ownedState = (sql: SqlClient) =>\n Schema.decodeUnknownEffect(DoClient)(sql).pipe(\n Effect.mapError(() =>\n DoStorageError.make({\n operation: \"open owned state\",\n message: \"Owned state requires a Durable Object SQLite client.\",\n }),\n ),\n Effect.map((client) => {\n // Instrumentation may return a fresh SQL wrapper on every storage.sql access.\n const key = client.config.storage ?? client.config.db ?? client;\n let state = states.get(key);\n\n if (state === undefined) {\n state = new OwnedState();\n states.set(key, state);\n }\n\n return state;\n }),\n );\n\n/**\n * Demand-loaded row views. Writes apply SQLite's RETURNING rows to every retained view in\n * the same transaction; rollback discards them. Bounded views avoid loading a host's entire\n * retained submission history just to fence one live submission.\n */\nexport const ownedRows = <A, I>(\n schema: Schema.Codec<A, I>,\n table: string,\n key: (row: A) => string,\n identityColumn?: keyof A & string,\n uniqueFields: ReadonlyArray<ReadonlyArray<keyof A & string>> = [],\n) => {\n type View = {\n readonly id: string;\n readonly matches: (row: A) => boolean;\n readonly maxRows: number;\n readonly rows: ReadonlyArray<A>;\n readonly bytes: number;\n };\n const caches = new WeakMap<OwnedState, Map<string, View>>();\n\n return (state: OwnedState, sql: SqlClient) => {\n let cache = caches.get(state);\n\n if (cache === undefined) {\n cache = new Map();\n caches.set(state, cache);\n const current = cache;\n\n state.invalidators.add(() => current.clear());\n }\n const current = cache;\n\n const decode = (rows: unknown) =>\n Schema.decodeUnknownEffect(Schema.Array(schema))(rows).pipe(\n Effect.mapError(() =>\n DoStorageCorruptionError.make({\n table,\n rowKey: \"owned-state\",\n message: \"Stored rows do not satisfy the storage schema\",\n }),\n ),\n );\n\n const retain = (\n id: string,\n matches: View[\"matches\"],\n rows: ReadonlyArray<A>,\n maxRows: number,\n ) => {\n const bytes = new TextEncoder().encode(JSON.stringify(rows)).byteLength;\n\n current.delete(id);\n if (rows.length > maxRows || bytes > 4 * 1024 * 1024) return;\n current.set(id, { id, matches, maxRows, rows, bytes });\n let total = 0;\n\n for (const view of current.values()) total += view.bytes;\n while (current.size > 128 || total > 4 * 1024 * 1024) {\n const oldest = current.entries().next().value;\n\n if (oldest === undefined) break;\n total -= oldest[1].bytes;\n current.delete(oldest[0]);\n }\n };\n\n const matching = (\n id: string,\n matches: View[\"matches\"],\n query: Effect.Effect<ReadonlyArray<unknown>, SqlError>,\n maxRows = Infinity,\n ) =>\n state.read(\n Effect.gen(function* () {\n const view = current.get(id);\n\n if (view !== undefined) return view.rows;\n\n const rows = yield* decode(yield* query);\n\n retain(id, matches, rows, maxRows);\n\n return rows;\n }),\n );\n\n const by = (field: keyof A & string, value: string) =>\n matching(\n JSON.stringify([field, value]),\n (row) => row[field] === value,\n sql`SELECT * FROM ${sql(table)} WHERE ${sql(field)} = ${value}`,\n );\n\n // Only a source mutation proving the complete result may seed a view. This must run\n // under the owner transaction; rollback invalidates it with every other retained view.\n const seed = (field: keyof A & string, value: string, rows: ReadonlyArray<A>) =>\n retain(JSON.stringify([field, value]), (row) => row[field] === value, rows, Infinity);\n\n const byFields = (fields: ReadonlyArray<readonly [keyof A & string, string]>) =>\n matching(\n JSON.stringify(fields),\n (row) => fields.every(([field, value]) => row[field] === value),\n sql`SELECT * FROM ${sql(table)} WHERE ${sql.and(fields.map(([field, value]) => sql`${sql(field)} = ${value}`))}`,\n );\n\n const apply =\n (remove: boolean) =>\n <E, R>(effect: Effect.Effect<ReadonlyArray<unknown>, E, R>) =>\n Effect.gen(function* () {\n const rows = yield* decode(yield* effect);\n const changed = new Set(rows.map(key));\n\n for (const { id, matches, maxRows, rows: prior } of Array.from(current.values())) {\n const next = prior.filter((row) => !changed.has(key(row)));\n\n if (!remove) next.push(...rows.filter(matches));\n retain(id, matches, next, maxRows);\n }\n // A RETURNING row completely defines its unique-key view, even on a new Thread.\n if (identityColumn !== undefined)\n for (const row of rows) {\n const value = row[identityColumn];\n\n if (typeof value === \"string\")\n retain(\n JSON.stringify([identityColumn, value]),\n (row) => row[identityColumn] === value,\n remove ? [] : [row],\n Infinity,\n );\n }\n\n for (const row of rows)\n for (const fields of uniqueFields) {\n const values = fields.map((field) => [field, row[field]] as const);\n\n retain(\n JSON.stringify(values),\n (candidate) => values.every(([field, value]) => candidate[field] === value),\n remove ? [] : [row],\n 1,\n );\n }\n\n return rows;\n });\n\n return { by, byFields, matching, seed, write: apply(false), remove: apply(true) };\n };\n};\n"],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __defProp = Object.defineProperty;
3
+ var __exportAll = (all, no_symbols) => {
4
+ let target = {};
5
+ for (var name in all) __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true
8
+ });
9
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
+ return target;
11
+ };
12
+ //#endregion
13
+ export { __exportAll as t };
package/package.json ADDED
@@ -0,0 +1 @@
1
+ {"name":"@yielded/agent-storage-cloudflare","version":"0.1.0-beta.166","dependencies":{"@effect/platform-browser":"4.0.0","@effect/sql-sqlite-do":"4.0.0","@yielded/agent":"0.1.0-beta.166","@yielded/agent-storage-sql":"0.1.0-beta.166"},"devDependencies":{"@cloudflare/vitest-pool-workers":"0.21.3","@cloudflare/workers-types":"5.20260926.1","@effect/vitest":"4.0.0","@yielded/agent-testing":"0.1.0-beta.166","effect":"4.0.0","typescript":"7.0.2","vite-plus":"0.3.3","vitest":"4.1.11"},"peerDependencies":{"effect":"^4.0.0"},"exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./do-memory-store":{"types":"./dist/DoMemoryStore.d.mts","default":"./dist/DoMemoryStore.mjs"},"./do-schedule-store":{"types":"./dist/DoScheduleStore.d.mts","default":"./dist/DoScheduleStore.mjs"},"./do-storage-config":{"types":"./dist/DoStorageConfig.d.mts","default":"./dist/DoStorageConfig.mjs"},"./do-storage-error":{"types":"./dist/DoStorageError.d.mts","default":"./dist/DoStorageError.mjs"},"./do-storage-failpoint":{"types":"./dist/DoStorageFailpoint.d.mts","default":"./dist/DoStorageFailpoint.mjs"},"./do-storage-version":{"types":"./dist/DoStorageVersion.d.mts","default":"./dist/DoStorageVersion.mjs"},"./do-submission-ledger":{"types":"./dist/DoSubmissionLedger.d.mts","default":"./dist/DoSubmissionLedger.mjs"},"./do-subscription-store":{"types":"./dist/DoSubscriptionStore.d.mts","default":"./dist/DoSubscriptionStore.mjs"},"./do-thread-store":{"types":"./dist/DoThreadStore.d.mts","default":"./dist/DoThreadStore.mjs"},"./memory-protocol":{"types":"./dist/MemoryProtocol.d.mts","default":"./dist/MemoryProtocol.mjs"},"./port-protocol":{"types":"./dist/PortProtocol.d.mts","default":"./dist/PortProtocol.mjs"},"./port-routing":{"types":"./dist/PortRouting.d.mts","default":"./dist/PortRouting.mjs"},"./testing/do-storage-failpoint-testing":{"types":"./dist/DoStorageFailpointTesting.d.mts","default":"./dist/DoStorageFailpointTesting.mjs"},"./do-message-delivery-store":{"types":"./dist/DoMessageDeliveryStore.d.mts","default":"./dist/DoMessageDeliveryStore.mjs"}},"description":"Durable Object SQLite storage adapters and the routed port protocol for Effect Agent on Cloudflare.","license":"MIT","repository":{"type":"git","url":"git+https://github.com/yielded-dev/agent.git","directory":"packages/storage-cloudflare"},"files":["dist","src"],"type":"module","sideEffects":[],"publishConfig":{"access":"public"},"scripts":{"build":"vp pack","check":"tsc --noEmit -p tsconfig.json"}}
@@ -0,0 +1,73 @@
1
+ import { SqliteClient } from "@effect/sql-sqlite-do";
2
+ import { MemoryStorageError, MemoryMutationFailpoint } from "@yielded/agent/memory-store";
3
+ import {
4
+ memoryStoreLayerWithFailpoints,
5
+ SqlMemoryLimits,
6
+ SqlStorageOwner,
7
+ } from "@yielded/agent/sql-memory-store";
8
+ import { Effect, Layer, Schema } from "effect";
9
+ import * as SqlClientService from "effect/sql/SqlClient";
10
+
11
+ import { ownedState } from "./internal/owned-state.ts";
12
+
13
+ export class DoMemoryStorageLimits extends Schema.Class<DoMemoryStorageLimits>(
14
+ "@effect-agent/storage-cloudflare/DoMemoryStorageLimits",
15
+ )({
16
+ maxRowBytes: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 1_900_000 })),
17
+ maxStorageBytes: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 536_870_912 })),
18
+ maxDocuments: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 100_000 })),
19
+ maxReceipts: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 1_000_000 })),
20
+ reservedWithdrawalBytes: Schema.optional(Schema.Natural),
21
+ reservedWithdrawalReceipts: Schema.optional(Schema.Natural),
22
+ }) {}
23
+
24
+ export const defaultDoMemoryStorageLimits = DoMemoryStorageLimits.make({
25
+ maxRowBytes: 1_900_000,
26
+ maxStorageBytes: 536_870_912,
27
+ maxDocuments: 10_000,
28
+ maxReceipts: 100_000,
29
+ });
30
+
31
+ /**
32
+ * Local memory only, without Thread tables. Pass the full storage handle: sql-only
33
+ * handles cannot provide atomic receipts and revisions. Services over the same storage
34
+ * share a transaction gate and bounded, write-through document and receipt views.
35
+ * Byte limits conservatively count encoded rows, not SQLite page/index overhead.
36
+ * Optional withdrawal reserves default to zero and stay within hard byte/receipt limits.
37
+ * Ordinary Put cannot consume them. Deploy exclusively upgraded writers before relying
38
+ * on reserves; the SQL accounting migration preserves older writers and their receipts.
39
+ */
40
+ export const doMemoryStoreLayerWithFailpoints = (
41
+ storage: NonNullable<SqliteClient.SqliteClientConfig["storage"]>,
42
+ limits: DoMemoryStorageLimits = defaultDoMemoryStorageLimits,
43
+ ) =>
44
+ Layer.unwrap(
45
+ Effect.gen(function* () {
46
+ const validated = yield* Schema.decodeEffect(DoMemoryStorageLimits)(limits).pipe(
47
+ Effect.mapError(() =>
48
+ MemoryStorageError.make({ operation: "memory storage limits", reason: "invalid-input" }),
49
+ ),
50
+ );
51
+
52
+ const sql = yield* SqlClientService.SqlClient;
53
+
54
+ const owner = yield* ownedState(sql).pipe(
55
+ Effect.mapError(() =>
56
+ MemoryStorageError.make({ operation: "open memory owner", reason: "unavailable" }),
57
+ ),
58
+ );
59
+
60
+ return memoryStoreLayerWithFailpoints.pipe(
61
+ Layer.provide(Layer.succeed(SqlMemoryLimits, validated)),
62
+ Layer.provide(Layer.succeed(SqlStorageOwner, owner)),
63
+ );
64
+ }),
65
+ ).pipe(Layer.provide(SqliteClient.layer({ storage })));
66
+
67
+ export const doMemoryStoreLayer = (
68
+ storage: NonNullable<SqliteClient.SqliteClientConfig["storage"]>,
69
+ limits: DoMemoryStorageLimits = defaultDoMemoryStorageLimits,
70
+ ) =>
71
+ doMemoryStoreLayerWithFailpoints(storage, limits).pipe(
72
+ Layer.provide(MemoryMutationFailpoint.layer),
73
+ );
@@ -0,0 +1,39 @@
1
+ import { SqlLifecycleSource } from "@yielded/agent-storage-sql/sql-lifecycle-publication";
2
+ import { makeSqlMessageDeliveryStore } from "@yielded/agent-storage-sql/sql-message-delivery-store";
3
+ import {
4
+ MessageDeliveryStore,
5
+ type MessageDeliveryStoreLimits,
6
+ } from "@yielded/agent/message-delivery";
7
+ import { SqlStorageOwner } from "@yielded/agent/sql-memory-store";
8
+ import { Effect, Layer } from "effect";
9
+ import * as SqlClient from "effect/sql/SqlClient";
10
+
11
+ import { DoStorageConfig } from "./DoStorageConfig.ts";
12
+ import { DoStorageFailpoint } from "./DoStorageFailpoint.ts";
13
+ import { initializeDoJournal } from "./internal/do-journal.ts";
14
+
15
+ /**
16
+ * Message obligations in a Thread Object's own SQL storage. The platform must prearm
17
+ * maintenance before insertion and retain its alarm while nextDeadline is present.
18
+ */
19
+ export const doMessageDeliveryStoreLayer = (limits?: MessageDeliveryStoreLimits) =>
20
+ Layer.effect(
21
+ MessageDeliveryStore,
22
+ Effect.gen(function* () {
23
+ const sql = yield* SqlClient.SqlClient;
24
+ const config = yield* DoStorageConfig;
25
+ const failpoint = yield* DoStorageFailpoint;
26
+
27
+ const journal = yield* initializeDoJournal(sql, failpoint.hit, config.maxStoredValueBytes);
28
+
29
+ return yield* makeSqlMessageDeliveryStore(limits, {
30
+ maxStoredValueBytes: config.maxStoredValueBytes,
31
+ }).pipe(
32
+ Effect.provideService(SqlStorageOwner, journal.owner),
33
+ Effect.provideService(SqlLifecycleSource, {
34
+ beforeRetain: (threadId) => journal.flushCanonical(threadId),
35
+ beforePending: journal.flushPublications(),
36
+ }),
37
+ );
38
+ }),
39
+ );