@opendatalabs/personal-server-ts-server 1.34.1 → 1.35.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.
Files changed (180) hide show
  1. package/dist/api-auth.d.ts +1 -0
  2. package/dist/api-auth.d.ts.map +1 -1
  3. package/dist/api-auth.js.map +1 -1
  4. package/dist/api.d.ts +3 -0
  5. package/dist/api.d.ts.map +1 -1
  6. package/dist/api.js +3 -0
  7. package/dist/api.js.map +1 -1
  8. package/dist/app.d.ts +14 -2
  9. package/dist/app.d.ts.map +1 -1
  10. package/dist/app.js +64 -8
  11. package/dist/app.js.map +1 -1
  12. package/dist/bootstrap.d.ts +19 -0
  13. package/dist/bootstrap.d.ts.map +1 -1
  14. package/dist/bootstrap.js +87 -4
  15. package/dist/bootstrap.js.map +1 -1
  16. package/dist/client.d.ts +1 -1
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js +7 -0
  19. package/dist/client.js.map +1 -1
  20. package/dist/enclave-main.d.ts.map +1 -1
  21. package/dist/enclave-main.js +0 -2
  22. package/dist/enclave-main.js.map +1 -1
  23. package/dist/mcp/durable-state.d.ts +2 -1
  24. package/dist/mcp/durable-state.d.ts.map +1 -1
  25. package/dist/mcp/durable-state.js +35 -1
  26. package/dist/mcp/durable-state.js.map +1 -1
  27. package/dist/pdpp/bootstrap.d.ts.map +1 -1
  28. package/dist/pdpp/bootstrap.js +1 -0
  29. package/dist/pdpp/bootstrap.js.map +1 -1
  30. package/dist/pdpp/records-bootstrap.d.ts +47 -1
  31. package/dist/pdpp/records-bootstrap.d.ts.map +1 -1
  32. package/dist/pdpp/records-bootstrap.js +78 -2
  33. package/dist/pdpp/records-bootstrap.js.map +1 -1
  34. package/dist/pdpp/replica-authorization.d.ts +35 -0
  35. package/dist/pdpp/replica-authorization.d.ts.map +1 -0
  36. package/dist/pdpp/replica-authorization.js +234 -0
  37. package/dist/pdpp/replica-authorization.js.map +1 -0
  38. package/dist/pending-blob-deletions.d.ts +1 -0
  39. package/dist/pending-blob-deletions.d.ts.map +1 -1
  40. package/dist/pending-blob-deletions.js +8 -4
  41. package/dist/pending-blob-deletions.js.map +1 -1
  42. package/dist/routes/mcp.d.ts +18 -2
  43. package/dist/routes/mcp.d.ts.map +1 -1
  44. package/dist/routes/mcp.js +111 -11
  45. package/dist/routes/mcp.js.map +1 -1
  46. package/dist/routes/oura.d.ts +6 -0
  47. package/dist/routes/oura.d.ts.map +1 -0
  48. package/dist/routes/oura.js +272 -0
  49. package/dist/routes/oura.js.map +1 -0
  50. package/dist/routes/pdpp-auth-query-keys.d.ts +7 -0
  51. package/dist/routes/pdpp-auth-query-keys.d.ts.map +1 -0
  52. package/dist/routes/pdpp-auth-query-keys.js +25 -0
  53. package/dist/routes/pdpp-auth-query-keys.js.map +1 -0
  54. package/dist/routes/pdpp-auth.d.ts +3 -1
  55. package/dist/routes/pdpp-auth.d.ts.map +1 -1
  56. package/dist/routes/pdpp-auth.js +126 -45
  57. package/dist/routes/pdpp-auth.js.map +1 -1
  58. package/dist/routes/pdpp-blobs.d.ts +13 -1
  59. package/dist/routes/pdpp-blobs.d.ts.map +1 -1
  60. package/dist/routes/pdpp-blobs.js +100 -65
  61. package/dist/routes/pdpp-blobs.js.map +1 -1
  62. package/dist/routes/pdpp-connections.d.ts.map +1 -1
  63. package/dist/routes/pdpp-connections.js +24 -1
  64. package/dist/routes/pdpp-connections.js.map +1 -1
  65. package/dist/routes/pdpp-instance-bindings.d.ts +3 -0
  66. package/dist/routes/pdpp-instance-bindings.d.ts.map +1 -1
  67. package/dist/routes/pdpp-instance-bindings.js +63 -0
  68. package/dist/routes/pdpp-instance-bindings.js.map +1 -1
  69. package/dist/routes/pdpp-record-query.d.ts +19 -0
  70. package/dist/routes/pdpp-record-query.d.ts.map +1 -0
  71. package/dist/routes/pdpp-record-query.js +51 -0
  72. package/dist/routes/pdpp-record-query.js.map +1 -0
  73. package/dist/routes/pdpp-records.d.ts +12 -1
  74. package/dist/routes/pdpp-records.d.ts.map +1 -1
  75. package/dist/routes/pdpp-records.js +120 -233
  76. package/dist/routes/pdpp-records.js.map +1 -1
  77. package/dist/routes/pdpp-replica-quarantine-fixture.d.ts +65 -0
  78. package/dist/routes/pdpp-replica-quarantine-fixture.d.ts.map +1 -0
  79. package/dist/routes/pdpp-replica-quarantine-fixture.js +389 -0
  80. package/dist/routes/pdpp-replica-quarantine-fixture.js.map +1 -0
  81. package/dist/storage/claude-method-transition.d.ts +12 -0
  82. package/dist/storage/claude-method-transition.d.ts.map +1 -0
  83. package/dist/storage/claude-method-transition.js +139 -0
  84. package/dist/storage/claude-method-transition.js.map +1 -0
  85. package/dist/storage/import-operations.d.ts +16 -0
  86. package/dist/storage/import-operations.d.ts.map +1 -1
  87. package/dist/storage/import-operations.js +54 -1
  88. package/dist/storage/import-operations.js.map +1 -1
  89. package/dist/storage/index-schema.d.ts +1 -1
  90. package/dist/storage/index-schema.d.ts.map +1 -1
  91. package/dist/storage/index-schema.js +22 -5
  92. package/dist/storage/index-schema.js.map +1 -1
  93. package/dist/storage/legacy-scope-authority.d.ts.map +1 -1
  94. package/dist/storage/legacy-scope-authority.js +83 -11
  95. package/dist/storage/legacy-scope-authority.js.map +1 -1
  96. package/dist/storage/node-data-storage.d.ts.map +1 -1
  97. package/dist/storage/node-data-storage.js +58 -2
  98. package/dist/storage/node-data-storage.js.map +1 -1
  99. package/dist/storage/pdpp-binary-chunks.d.ts +120 -0
  100. package/dist/storage/pdpp-binary-chunks.d.ts.map +1 -0
  101. package/dist/storage/pdpp-binary-chunks.js +480 -0
  102. package/dist/storage/pdpp-binary-chunks.js.map +1 -0
  103. package/dist/storage/pdpp-records-sqlite-store.d.ts +96 -15
  104. package/dist/storage/pdpp-records-sqlite-store.d.ts.map +1 -1
  105. package/dist/storage/pdpp-records-sqlite-store.js +616 -211
  106. package/dist/storage/pdpp-records-sqlite-store.js.map +1 -1
  107. package/dist/storage/pdpp-replica-canonical-port.d.ts +202 -0
  108. package/dist/storage/pdpp-replica-canonical-port.d.ts.map +1 -0
  109. package/dist/storage/pdpp-replica-canonical-port.js +451 -0
  110. package/dist/storage/pdpp-replica-canonical-port.js.map +1 -0
  111. package/dist/storage/pdpp-replica-coordinator-state.d.ts +16 -0
  112. package/dist/storage/pdpp-replica-coordinator-state.d.ts.map +1 -0
  113. package/dist/storage/pdpp-replica-coordinator-state.js +25 -0
  114. package/dist/storage/pdpp-replica-coordinator-state.js.map +1 -0
  115. package/dist/storage/pdpp-replica-copies.d.ts +81 -0
  116. package/dist/storage/pdpp-replica-copies.d.ts.map +1 -0
  117. package/dist/storage/pdpp-replica-copies.js +59 -0
  118. package/dist/storage/pdpp-replica-copies.js.map +1 -0
  119. package/dist/storage/pdpp-replica-reconciler.d.ts +96 -0
  120. package/dist/storage/pdpp-replica-reconciler.d.ts.map +1 -0
  121. package/dist/storage/pdpp-replica-reconciler.js +710 -0
  122. package/dist/storage/pdpp-replica-reconciler.js.map +1 -0
  123. package/dist/storage/pdpp-replica-sql.d.ts +13 -0
  124. package/dist/storage/pdpp-replica-sql.d.ts.map +1 -0
  125. package/dist/storage/pdpp-replica-sql.js +43 -0
  126. package/dist/storage/pdpp-replica-sql.js.map +1 -0
  127. package/dist/storage/pdpp-replica-staging.d.ts +36 -0
  128. package/dist/storage/pdpp-replica-staging.d.ts.map +1 -0
  129. package/dist/storage/pdpp-replica-staging.js +202 -0
  130. package/dist/storage/pdpp-replica-staging.js.map +1 -0
  131. package/dist/storage/pdpp-replica-visibility-view.d.ts +64 -0
  132. package/dist/storage/pdpp-replica-visibility-view.d.ts.map +1 -0
  133. package/dist/storage/pdpp-replica-visibility-view.js +1397 -0
  134. package/dist/storage/pdpp-replica-visibility-view.js.map +1 -0
  135. package/dist/storage/pdpp-replica-writer-identity.d.ts +64 -0
  136. package/dist/storage/pdpp-replica-writer-identity.d.ts.map +1 -0
  137. package/dist/storage/pdpp-replica-writer-identity.js +81 -0
  138. package/dist/storage/pdpp-replica-writer-identity.js.map +1 -0
  139. package/dist/storage/pdpp-replication-door.d.ts +60 -0
  140. package/dist/storage/pdpp-replication-door.d.ts.map +1 -0
  141. package/dist/storage/pdpp-replication-door.js +203 -0
  142. package/dist/storage/pdpp-replication-door.js.map +1 -0
  143. package/dist/storage/pdpp-replication-host.d.ts +67 -0
  144. package/dist/storage/pdpp-replication-host.d.ts.map +1 -0
  145. package/dist/storage/pdpp-replication-host.js +159 -0
  146. package/dist/storage/pdpp-replication-host.js.map +1 -0
  147. package/dist/storage/pdpp-replication-memory.fixture.d.ts +2 -0
  148. package/dist/storage/pdpp-replication-memory.fixture.d.ts.map +1 -0
  149. package/dist/storage/pdpp-replication-memory.fixture.js +267 -0
  150. package/dist/storage/pdpp-replication-memory.fixture.js.map +1 -0
  151. package/dist/storage/pdpp-replication-preparation.d.ts +3 -0
  152. package/dist/storage/pdpp-replication-preparation.d.ts.map +1 -0
  153. package/dist/storage/pdpp-replication-preparation.js +3 -0
  154. package/dist/storage/pdpp-replication-preparation.js.map +1 -0
  155. package/dist/storage/pdpp-replication-promotion-memory.fixture.d.ts +2 -0
  156. package/dist/storage/pdpp-replication-promotion-memory.fixture.d.ts.map +1 -0
  157. package/dist/storage/pdpp-replication-promotion-memory.fixture.js +65 -0
  158. package/dist/storage/pdpp-replication-promotion-memory.fixture.js.map +1 -0
  159. package/dist/storage/pdpp-replication-receiver.d.ts +230 -0
  160. package/dist/storage/pdpp-replication-receiver.d.ts.map +1 -0
  161. package/dist/storage/pdpp-replication-receiver.js +950 -0
  162. package/dist/storage/pdpp-replication-receiver.js.map +1 -0
  163. package/dist/storage/pdpp-replication-sqlite.d.ts +95 -0
  164. package/dist/storage/pdpp-replication-sqlite.d.ts.map +1 -0
  165. package/dist/storage/pdpp-replication-sqlite.js +615 -0
  166. package/dist/storage/pdpp-replication-sqlite.js.map +1 -0
  167. package/dist/storage/pdpp-replication-transport.d.ts +39 -0
  168. package/dist/storage/pdpp-replication-transport.d.ts.map +1 -0
  169. package/dist/storage/pdpp-replication-transport.js +196 -0
  170. package/dist/storage/pdpp-replication-transport.js.map +1 -0
  171. package/dist/storage/schema-versions.d.ts +5 -0
  172. package/dist/storage/schema-versions.d.ts.map +1 -0
  173. package/dist/storage/schema-versions.js +26 -0
  174. package/dist/storage/schema-versions.js.map +1 -0
  175. package/dist/storage/strava-method-transition.d.ts +18 -0
  176. package/dist/storage/strava-method-transition.d.ts.map +1 -0
  177. package/dist/storage/strava-method-transition.js +209 -0
  178. package/dist/storage/strava-method-transition.js.map +1 -0
  179. package/dist/ui/ps-lite-debug.js +137018 -106931
  180. package/package.json +3 -3
@@ -0,0 +1,710 @@
1
+ import { canonicalJson, declarationShape, } from "@opendatalabs/personal-server-ts-core/replication/envelope";
2
+ import { declarationWait, planReplicaMerge, resolveDeclaration, } from "@opendatalabs/personal-server-ts-core/replication/merge-plan";
3
+ import { selectStreamCompletion } from "@opendatalabs/personal-server-ts-core/replication/completion";
4
+ import { blobReady, completionAvailability, connectionFromFacts, contradictsMapping, recordRow, } from "@opendatalabs/personal-server-ts-core/replication/visibility-engine";
5
+ import { operationExpressions as X } from "./pdpp-replica-visibility-view.js";
6
+ /**
7
+ * Durable state of the incremental reconciler. Replica tables only, created
8
+ * by the receiver, so a database that never enabled replication has none.
9
+ * - state: how far each retained table has been ingested (rowid frontiers).
10
+ * - bindings, equivocations: per-connection facts that `connectionFromFacts`
11
+ * needs, kept as retained evidence arrives instead of re-read from it.
12
+ * - connections: the last committed decision per connection, so a change is
13
+ * expanded to the connection's keys once, not on every import.
14
+ * - blockers: keys with an unresolved issue; a completion of their stream
15
+ * cannot be available.
16
+ * - dependencies: the operation ids each stream's retained completions pin,
17
+ * so an operation that arrives, or gains a variant, re-queues them.
18
+ * - queue: work not yet reconciled. `background` marks entries of a paged
19
+ * pass that an import does not have to finish.
20
+ */
21
+ export const RECONCILE_MIGRATION = `
22
+ CREATE TABLE IF NOT EXISTS pdpp_replica_reconcile_state (id INTEGER PRIMARY KEY CHECK (id = 1), operations_frontier INTEGER NOT NULL, received_frontier INTEGER NOT NULL);
23
+ CREATE TABLE IF NOT EXISTS pdpp_replica_reconcile_bindings (instance TEXT NOT NULL, source_id TEXT NOT NULL, method_id TEXT NOT NULL, PRIMARY KEY (instance, source_id, method_id));
24
+ CREATE TABLE IF NOT EXISTS pdpp_replica_reconcile_equivocations (instance TEXT NOT NULL, writer TEXT NOT NULL, operation_id TEXT NOT NULL, PRIMARY KEY (instance, writer, operation_id));
25
+ CREATE TABLE IF NOT EXISTS pdpp_replica_reconcile_connections (instance TEXT PRIMARY KEY, decision_json TEXT NOT NULL);
26
+ CREATE TABLE IF NOT EXISTS pdpp_replica_reconcile_blockers (instance TEXT NOT NULL, stream TEXT NOT NULL, record_key TEXT NOT NULL, PRIMARY KEY (instance, stream, record_key));
27
+ CREATE TABLE IF NOT EXISTS pdpp_replica_reconcile_dependencies (writer TEXT NOT NULL, operation_id TEXT NOT NULL, instance TEXT NOT NULL, stream TEXT NOT NULL, PRIMARY KEY (writer, operation_id, instance, stream));
28
+ CREATE INDEX IF NOT EXISTS pdpp_replica_reconcile_dependencies_stream ON pdpp_replica_reconcile_dependencies (instance, stream);
29
+ CREATE TABLE IF NOT EXISTS pdpp_replica_reconcile_queue (kind INTEGER NOT NULL, instance TEXT NOT NULL, stream TEXT NOT NULL, record_key TEXT NOT NULL, expand INTEGER NOT NULL DEFAULT 0, background INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (kind, instance, stream, record_key));
30
+ CREATE INDEX IF NOT EXISTS pdpp_replica_reconcile_queue_order ON pdpp_replica_reconcile_queue (background, kind, instance, stream, record_key);
31
+ ${["pdpp_replica_operations", "pdpp_replica_received"]
32
+ .map((table) => `CREATE INDEX IF NOT EXISTS pdpp_replica_reconcile_completions_${table} ON ${table}(${X.instance},${X.stream}) WHERE ${X.kind} IN ('stream-complete','stream-invalidate');`)
33
+ .join("\n")}
34
+ `;
35
+ /** Queue entry kinds, in processing order: tombstones before winners. */
36
+ const CONNECTION = 0;
37
+ const WITHDRAW = 1;
38
+ const KEY = 2;
39
+ const COMPLETION = 3;
40
+ const RETAINED = ["pdpp_replica_operations", "pdpp_replica_received"];
41
+ const LIFECYCLE = "('connection-register','connection-reset','connection-delete')";
42
+ const COMPLETIONS = "('stream-complete','stream-invalidate')";
43
+ const compareText = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
44
+ const isBindingConflict = (projection) => projection.status === "withdrawn" &&
45
+ (projection.reason === "local_mapping_conflict" ||
46
+ projection.reason === "source_binding_conflict");
47
+ /**
48
+ * Incremental reconcile of retained history into the canonical tables.
49
+ *
50
+ * The canonical result equals a reconcile of the full retained set: a
51
+ * connection's eligibility comes from `connectionFromFacts` over facts kept
52
+ * as evidence arrives, and each key is planned by `planReplicaMerge` over the
53
+ * connection's unique lifecycle and that key's operations only. Evidence
54
+ * marks the keys, connections and completions it can change. A connection
55
+ * whose decision changed is expanded to all of its keys and completions.
56
+ *
57
+ * The receiver supplies the canonical effects; this module decides which
58
+ * work exists and does it in bounded pages.
59
+ */
60
+ export function createReplicaReconciler(deps) {
61
+ const { db, effects } = deps;
62
+ db.exec(RECONCILE_MIGRATION);
63
+ const statements = new Map();
64
+ const sql = (source) => {
65
+ let statement = statements.get(source);
66
+ if (!statement) {
67
+ statement = db.prepare(source);
68
+ statements.set(source, statement);
69
+ }
70
+ return statement;
71
+ };
72
+ const stats = { operations: 0, bytes: 0, entries: 0 };
73
+ /** Charged by every retained read; a page stops when it is spent. */
74
+ let meter = { operations: 0, bytes: 0 };
75
+ /** Retained operations matching `where` in both tables, deduplicated, in JSON order. */
76
+ function retained(where, params) {
77
+ const rows = sql(RETAINED.map((table) => `SELECT operation_json FROM ${table} WHERE ${where}`).join(" UNION ")).all(...params, ...params);
78
+ rows.sort((a, b) => compareText(a.operation_json, b.operation_json));
79
+ for (const row of rows) {
80
+ const bytes = Buffer.byteLength(row.operation_json, "utf8");
81
+ meter.operations++;
82
+ meter.bytes += bytes;
83
+ stats.operations++;
84
+ stats.bytes += bytes;
85
+ }
86
+ return rows.map((row) => JSON.parse(row.operation_json));
87
+ }
88
+ /** Distinct (digest, connection) of every retained variant of one revision. */
89
+ function variants(writer, operationId) {
90
+ return sql(RETAINED.map((table) => `SELECT digest, ${X.instance} AS instance FROM ${table} WHERE ${X.writer} = ? AND ${X.operation_id} = ?`).join(" UNION ")).all(writer, operationId, writer, operationId);
91
+ }
92
+ // ------------------------------------------------------------ the queue
93
+ function enqueue(kind, instance, stream = "", recordKey = "", options = {}) {
94
+ sql("INSERT INTO pdpp_replica_reconcile_queue VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(kind, instance, stream, record_key) DO UPDATE SET expand = max(expand, excluded.expand), background = min(background, excluded.background)").run(kind, instance, stream, recordKey, options.expand ? 1 : 0, options.background ? 1 : 0);
95
+ }
96
+ /** Enqueue every row `select` returns as (instance, stream, record_key). */
97
+ function enqueueAll(kind, select, params, background) {
98
+ sql(`INSERT INTO pdpp_replica_reconcile_queue (kind, instance, stream, record_key, expand, background) SELECT ${kind}, instance, stream, record_key, 0, ${background ? 1 : 0} FROM (${select}) WHERE true ON CONFLICT(kind, instance, stream, record_key) DO UPDATE SET background = min(background, excluded.background)`).run(...params);
99
+ }
100
+ function take(kind, instance, stream = "", recordKey = "") {
101
+ return sql("DELETE FROM pdpp_replica_reconcile_queue WHERE kind = ? AND instance = ? AND stream = ? AND record_key = ? RETURNING *").get(kind, instance, stream, recordKey);
102
+ }
103
+ /** Marks for what one operation can change. */
104
+ function mark(op) {
105
+ switch (op.kind) {
106
+ case "upsert":
107
+ case "delete":
108
+ enqueue(KEY, op.connectionId, op.declaration.stream, op.recordKey);
109
+ return;
110
+ case "blob-claim":
111
+ // A claim makes the connection's upserts of that blob showable.
112
+ enqueueAll(KEY, RETAINED.map((table) => `SELECT ${X.instance} AS instance, ${X.stream} AS stream, ${X.record_key} AS record_key FROM ${table} WHERE ${X.blob_id} = ? AND ${X.instance} = ? AND ${X.kind} = 'upsert'`).join(" UNION "), [op.blob.blobId, op.connectionId, op.blob.blobId, op.connectionId], false);
113
+ return;
114
+ case "stream-complete":
115
+ case "stream-invalidate":
116
+ enqueue(COMPLETION, op.connectionId, op.declaration.stream);
117
+ return;
118
+ default:
119
+ enqueue(CONNECTION, op.connectionId);
120
+ }
121
+ }
122
+ /** Marks the upserts of every connection that show these blobs' bytes. */
123
+ function touchBlobs(blobIds) {
124
+ for (const blobId of new Set(blobIds))
125
+ enqueueAll(KEY, RETAINED.map((table) => `SELECT ${X.instance} AS instance, ${X.stream} AS stream, ${X.record_key} AS record_key FROM ${table} WHERE ${X.blob_id} = ? AND ${X.kind} = 'upsert'`).join(" UNION "), [blobId, blobId], false);
126
+ }
127
+ // ------------------------------------------------------------ ingestion
128
+ function frontiers() {
129
+ return sql("SELECT operations_frontier AS operations, received_frontier AS received FROM pdpp_replica_reconcile_state WHERE id = 1").get();
130
+ }
131
+ /** Connection facts of one new retained operation; marks what it changes. */
132
+ function admit(op) {
133
+ if (sql("INSERT OR IGNORE INTO pdpp_replica_reconcile_bindings VALUES (?, ?, ?)").run(op.connectionId, op.sourceId, op.methodId).changes > 0)
134
+ enqueue(CONNECTION, op.connectionId);
135
+ const found = variants(op.revision.writerId, op.revision.operationId);
136
+ if (new Set(found.map((row) => row.digest)).size > 1)
137
+ for (const { instance } of found)
138
+ if (sql("INSERT OR IGNORE INTO pdpp_replica_reconcile_equivocations VALUES (?, ?, ?)").run(instance, op.revision.writerId, op.revision.operationId)
139
+ .changes > 0)
140
+ enqueue(CONNECTION, instance);
141
+ // Completions that pin this operation id: it arrived, or it has another
142
+ // variant now. Either can change their availability.
143
+ enqueueAll(COMPLETION, "SELECT instance, stream, '' AS record_key FROM pdpp_replica_reconcile_dependencies WHERE writer = ? AND operation_id = ?", [op.revision.writerId, op.revision.operationId], false);
144
+ mark(op);
145
+ }
146
+ /**
147
+ * Admit every retained operation past the frontiers. Rowids of both tables
148
+ * only grow: both are append-only, and the journal's sequence is its
149
+ * Lamport counter.
150
+ */
151
+ function ingest() {
152
+ const at = frontiers();
153
+ const next = { ...at };
154
+ for (const [table, field] of [
155
+ ["pdpp_replica_operations", "operations"],
156
+ ["pdpp_replica_received", "received"],
157
+ ])
158
+ for (;;) {
159
+ const rows = sql(`SELECT rowid AS id, operation_json FROM ${table} WHERE rowid > ? ORDER BY rowid LIMIT 512`).all(next[field]);
160
+ if (!rows.length)
161
+ break;
162
+ for (const row of rows)
163
+ admit(JSON.parse(row.operation_json));
164
+ next[field] = rows[rows.length - 1].id;
165
+ }
166
+ if (next.operations !== at.operations || next.received !== at.received)
167
+ sql("UPDATE pdpp_replica_reconcile_state SET operations_frontier = ?, received_frontier = ? WHERE id = 1").run(next.operations, next.received);
168
+ }
169
+ /**
170
+ * A database whose retained evidence predates this state: derive the facts
171
+ * in three set queries, and queue every connection for a full expansion.
172
+ * The first import or step does that work, as the full reconcile it
173
+ * replaces did on every call.
174
+ */
175
+ function bootstrap() {
176
+ if (frontiers())
177
+ return;
178
+ const union = (select) => RETAINED.map(select).join(" UNION ");
179
+ db.prepare(`INSERT OR IGNORE INTO pdpp_replica_reconcile_bindings ${union((table) => `SELECT ${X.instance}, json_extract(operation_json,'$.sourceId'), json_extract(operation_json,'$.methodId') FROM ${table}`)}`).run();
180
+ db.prepare(`INSERT OR IGNORE INTO pdpp_replica_reconcile_equivocations SELECT instance, writer, operation_id FROM (${union((table) => `SELECT ${X.instance} AS instance, ${X.writer} AS writer, ${X.operation_id} AS operation_id, digest FROM ${table}`)}) v WHERE (SELECT count(DISTINCT digest) FROM (${union((table) => `SELECT digest FROM ${table} WHERE ${X.writer} = v.writer AND ${X.operation_id} = v.operation_id`)})) > 1`).run();
181
+ for (const { instance } of db
182
+ .prepare("SELECT DISTINCT instance FROM pdpp_replica_reconcile_bindings")
183
+ .all())
184
+ enqueue(CONNECTION, instance, "", "", { expand: true });
185
+ const max = (table) => db
186
+ .prepare(`SELECT coalesce(max(rowid), 0) AS id FROM ${table}`)
187
+ .get().id;
188
+ db.prepare("INSERT INTO pdpp_replica_reconcile_state VALUES (1, ?, ?)").run(max("pdpp_replica_operations"), max("pdpp_replica_received"));
189
+ }
190
+ db.transaction(bootstrap).immediate();
191
+ // ---------------------------------------------------------- connections
192
+ function worldOf(instance) {
193
+ const local = effects.binding(instance);
194
+ return {
195
+ mapping: local?.source_id
196
+ ? { sourceId: local.source_id, methodId: local.method ?? "" }
197
+ : null,
198
+ generation: local?.generation ?? null,
199
+ deletedAt: local?.deleted_at ?? null,
200
+ terminal: !!sql("SELECT 1 FROM pdpp_replica_quarantined_connections WHERE instance = ?").get(instance),
201
+ conflicted: !!sql("SELECT 1 FROM pdpp_replica_binding_conflicts WHERE instance = ?").get(instance),
202
+ };
203
+ }
204
+ function decisionOf(instance) {
205
+ const row = sql("SELECT decision_json FROM pdpp_replica_reconcile_connections WHERE instance = ?").get(instance);
206
+ return row && JSON.parse(row.decision_json);
207
+ }
208
+ /** Lifecycle operations of a connection whose revision has one content. */
209
+ let lifecycleCache = new Map();
210
+ function lifecycle(instance) {
211
+ const cached = lifecycleCache.get(instance);
212
+ if (cached)
213
+ return cached;
214
+ const found = retained(`${X.instance} = ? AND ${X.kind} IN ${LIFECYCLE}`, [
215
+ instance,
216
+ ]).filter((op) => variants(op.revision.writerId, op.revision.operationId).length === 1);
217
+ lifecycleCache.set(instance, found);
218
+ return found;
219
+ }
220
+ function bindingsOf(instance) {
221
+ return sql("SELECT source_id AS sourceId, method_id AS methodId FROM pdpp_replica_reconcile_bindings WHERE instance = ? ORDER BY source_id, method_id").all(instance).map(({ sourceId, methodId }) => ({ sourceId, methodId }));
222
+ }
223
+ function factsOf(instance, mapping, terminal) {
224
+ return {
225
+ mapping,
226
+ terminal,
227
+ equivocated: !!sql("SELECT 1 FROM pdpp_replica_reconcile_equivocations WHERE instance = ? LIMIT 1").get(instance),
228
+ bindings: bindingsOf(instance),
229
+ lifecycle: lifecycle(instance),
230
+ appliedGeneration: effects.binding(instance)?.generation ?? 1,
231
+ };
232
+ }
233
+ /** One retained operation of the connection, to name it in a plan issue. */
234
+ function representative(instance) {
235
+ const [first] = sql(RETAINED.map((table) => `SELECT operation_json FROM ${table} WHERE ${X.instance} = ? AND ${X.kind} IS NOT NULL`).join(" UNION ") + " ORDER BY operation_json LIMIT 1").all(instance, instance);
236
+ return first && JSON.parse(first.operation_json);
237
+ }
238
+ /** Persist a binding conflict with the evidence that shows it (#428 F2). */
239
+ function recordBindingConflict(instance, mapping, bindings) {
240
+ if (sql("SELECT 1 FROM pdpp_replica_binding_conflicts WHERE instance = ?").get(instance))
241
+ return;
242
+ if (!mapping) {
243
+ effects.markBindingConflict({
244
+ connectionId: instance,
245
+ reason: "source_binding_conflict",
246
+ evidence: [
247
+ ...new Set(bindings.map((b) => `${b.sourceId} ${b.methodId}`)),
248
+ ]
249
+ .sort()
250
+ .slice(0, 8),
251
+ });
252
+ return;
253
+ }
254
+ const [disagreeing] = retained(`${X.instance} = ? AND (json_extract(operation_json,'$.sourceId') IS NOT ? OR json_extract(operation_json,'$.methodId') IS NOT ?)`, [instance, mapping.sourceId, mapping.methodId]);
255
+ effects.markBindingConflict({
256
+ connectionId: instance,
257
+ reason: "local_mapping_conflict",
258
+ evidence: {
259
+ sourceId: disagreeing.sourceId,
260
+ methodId: disagreeing.methodId,
261
+ revision: disagreeing.revision,
262
+ },
263
+ });
264
+ }
265
+ /** Connection-level plan issues of a provisioned connection that is not active. */
266
+ function connectionIssues(instance, projection, plan, context) {
267
+ const unique = lifecycle(instance);
268
+ const rep = representative(instance);
269
+ const entries = [
270
+ ...new Map([...unique, ...(rep ? [rep] : [])].map((op) => [canonicalJson(op), op])).values(),
271
+ ];
272
+ if (projection.status === "withdrawn" &&
273
+ projection.reason === "operation_id_equivocation") {
274
+ for (const { writer, operation_id } of sql("SELECT writer, operation_id FROM pdpp_replica_reconcile_equivocations WHERE instance = ?").all(instance))
275
+ plan.conflicts.push({
276
+ reason: "operation_id_equivocation",
277
+ operations: retained(`${X.writer} = ? AND ${X.operation_id} = ?`, [
278
+ writer,
279
+ operation_id,
280
+ ]),
281
+ });
282
+ if (unique.some((op) => op.kind === "connection-delete"))
283
+ plan.suppressed.push({
284
+ reason: "connection_deleted",
285
+ operations: entries,
286
+ });
287
+ else
288
+ plan.conflicts.push({
289
+ reason: "incompatible_connection_binding",
290
+ operations: entries,
291
+ });
292
+ return;
293
+ }
294
+ const lifecyclePlan = planReplicaMerge(context, entries);
295
+ plan.conflicts.push(...lifecyclePlan.conflicts);
296
+ plan.pending.push(...lifecyclePlan.pending);
297
+ plan.suppressed.push(...lifecyclePlan.suppressed);
298
+ plan.rejected.push(...lifecyclePlan.rejected);
299
+ }
300
+ /**
301
+ * Decide one connection and apply its lifecycle effects, as the full
302
+ * reconcile did for every connection. A changed decision (or `expand`)
303
+ * queues all of the connection's keys and completions.
304
+ */
305
+ function reconcileConnection(instance, options, run) {
306
+ const bindings = bindingsOf(instance);
307
+ if (!bindings.length)
308
+ return; // No retained evidence names it.
309
+ const { context, plan } = run;
310
+ const before = decisionOf(instance);
311
+ const world = worldOf(instance);
312
+ const facts = factsOf(instance, world.mapping ?? bindings[0], false);
313
+ // A retained disagreement is a durable, connection-scoped fact, never a
314
+ // throw that would block every import and retry (#428 F2). It holds even
315
+ // when terminal quarantine or equivocation outranks it (#426).
316
+ if (contradictsMapping(facts))
317
+ recordBindingConflict(instance, world.mapping, bindings);
318
+ let projection;
319
+ if (!world.mapping) {
320
+ projection = { status: "unprovisioned" };
321
+ const rep = representative(instance);
322
+ if (rep)
323
+ plan.pending.push({
324
+ reason: "missing_local_connection",
325
+ operations: [rep],
326
+ });
327
+ }
328
+ else {
329
+ projection = connectionFromFacts(context, facts);
330
+ if (!isBindingConflict(projection)) {
331
+ applyLifecycle(instance, projection, options.background);
332
+ if (projection.status !== "active") {
333
+ connectionIssues(instance, projection, plan, context);
334
+ run.reported.add(instance);
335
+ }
336
+ }
337
+ }
338
+ const after = { projection, world: worldOf(instance) };
339
+ const encoded = canonicalJson(after);
340
+ if (options.expand || !before || canonicalJson(before) !== encoded) {
341
+ if (projection.status === "active")
342
+ enqueueAll(KEY, RETAINED.map((table) => `SELECT ${X.instance} AS instance, ${X.stream} AS stream, ${X.record_key} AS record_key FROM ${table} WHERE ${X.instance} = ? AND ${X.stream} IS NOT NULL AND ${X.record_key} IS NOT NULL`).join(" UNION "), [instance, instance], options.background);
343
+ if (world.mapping)
344
+ enqueueAll(COMPLETION, RETAINED.map((table) => `SELECT ${X.instance} AS instance, ${X.stream} AS stream, '' AS record_key FROM ${table} WHERE ${X.kind} IN ${COMPLETIONS} AND ${X.instance} = ? AND ${X.stream} IS NOT NULL`).join(" UNION "), [instance, instance], options.background);
345
+ // Only mapped connections' variants count toward a dependency, so a
346
+ // change here can change completions of other connections too.
347
+ enqueueAll(COMPLETION, `SELECT DISTINCT d.instance, d.stream, '' AS record_key FROM pdpp_replica_reconcile_dependencies d WHERE d.instance <> ? AND EXISTS (${RETAINED.map((table) => `SELECT 1 FROM ${table} WHERE ${X.writer} = d.writer AND ${X.operation_id} = d.operation_id AND ${X.instance} = ?`).join(" UNION ALL ")})`, [instance, instance, instance], options.background);
348
+ }
349
+ sql("INSERT INTO pdpp_replica_reconcile_connections VALUES (?, ?) ON CONFLICT(instance) DO UPDATE SET decision_json = excluded.decision_json").run(instance, encoded);
350
+ }
351
+ /**
352
+ * Lifecycle evidence gates canonical state, but never creates a connection
353
+ * or changes its acquisition method. Only the local authority provisions
354
+ * that. Rows the connection loses are queued as tombstones.
355
+ */
356
+ function applyLifecycle(instance, connection, background) {
357
+ const local = effects.binding(instance);
358
+ const deleted = connection.status === "deleted";
359
+ const blocked = connection.status === "withdrawn";
360
+ const reset = connection.status === "active" &&
361
+ connection.generation > local.generation;
362
+ const newlyBlocked = blocked && effects.quarantineConnection(instance);
363
+ // A receiver-derived deletion can become equivocated later. Retract only
364
+ // that derived marker; an actual local deletion remains in the merge set.
365
+ const retractDeletion = !deleted && local.deleted_at === "replica-deleted";
366
+ if (deleted || blocked || reset) {
367
+ enqueueAll(WITHDRAW, "SELECT instance, stream, record_key FROM pdpp_records WHERE instance = ? AND deleted = 0", [instance], background);
368
+ sql("DELETE FROM pdpp_stream_completion WHERE instance = ?").run(instance);
369
+ sql("DELETE FROM pdpp_blob_claims WHERE instance = ?").run(instance);
370
+ }
371
+ if ((deleted && local.deleted_at === null) ||
372
+ reset ||
373
+ newlyBlocked ||
374
+ retractDeletion) {
375
+ // Even an empty reset must expire a token at the old write horizon.
376
+ const { value: fence } = sql("UPDATE pdpp_write_clock SET value = value + 1 WHERE id = 1 RETURNING value").get();
377
+ sql("UPDATE pdpp_instance_binding SET generation = ?, deleted_at = ?, reset_clock = ? WHERE instance = ?").run(reset ? connection.generation : local.generation, deleted
378
+ ? (local.deleted_at ?? "replica-deleted")
379
+ : retractDeletion
380
+ ? null
381
+ : local.deleted_at, fence, instance);
382
+ }
383
+ }
384
+ /**
385
+ * Bring the connection's decision up to date before one of its keys or
386
+ * completions is decided: a queued connection entry first, and a world
387
+ * the decision did not see (a provisioned mapping, a quarantine fact).
388
+ */
389
+ function settleConnection(instance, background, run) {
390
+ const queued = take(CONNECTION, instance);
391
+ if (queued)
392
+ return reconcileConnection(instance, { background: queued.background === 1, expand: queued.expand === 1 }, run);
393
+ const decision = decisionOf(instance);
394
+ if (!decision ||
395
+ canonicalJson(decision.world) !== canonicalJson(worldOf(instance)))
396
+ reconcileConnection(instance, { background, expand: false }, run);
397
+ }
398
+ // ----------------------------------------------------------------- keys
399
+ function withdrawKey(instance, stream, recordKey) {
400
+ if (sql("SELECT 1 FROM pdpp_records WHERE instance = ? AND stream = ? AND record_key = ? AND deleted = 0").get(instance, stream, recordKey))
401
+ effects.write(instance, stream, recordKey, null);
402
+ }
403
+ function reconcileKey(instance, stream, recordKey, background, run) {
404
+ settleConnection(instance, background, run);
405
+ if (take(WITHDRAW, instance, stream, recordKey))
406
+ withdrawKey(instance, stream, recordKey);
407
+ const decision = decisionOf(instance);
408
+ if (!decision)
409
+ return;
410
+ const { context, plan } = run;
411
+ const where = `${X.instance} = ? AND ${X.stream} = ? AND ${X.record_key} = ?`;
412
+ const params = [instance, stream, recordKey];
413
+ if (decision.projection.status === "unprovisioned") {
414
+ plan.pending.push({
415
+ reason: "missing_local_connection",
416
+ operations: retained(where, params),
417
+ });
418
+ return;
419
+ }
420
+ if (isBindingConflict(decision.projection))
421
+ return;
422
+ if (decision.projection.status !== "active") {
423
+ // The plan names why the connection holds its records.
424
+ if (!run.reported.has(instance)) {
425
+ run.reported.add(instance);
426
+ connectionIssues(instance, decision.projection, plan, context);
427
+ }
428
+ return;
429
+ }
430
+ const keyPlan = planReplicaMerge(context, [
431
+ ...lifecycle(instance),
432
+ ...retained(where, params),
433
+ ]);
434
+ let blocked = [
435
+ ...keyPlan.pending,
436
+ ...keyPlan.conflicts,
437
+ ...keyPlan.rejected,
438
+ ].some((issue) => !declarationWait(issue.reason));
439
+ const local = effects.binding(instance);
440
+ for (const record of keyPlan.records) {
441
+ const { generation } = record.identity;
442
+ if (local.deleted_at || generation !== local.generation)
443
+ continue;
444
+ if (record.quarantined)
445
+ effects.quarantineKey(instance, generation, stream, recordKey);
446
+ if (effects.isRecordQuarantined(instance, stream, recordKey)) {
447
+ effects.write(instance, stream, recordKey, null);
448
+ continue;
449
+ }
450
+ // `write` turns a withdrawal into a tombstone only for a stored row, so
451
+ // the engine's "no prior row" case is the same as a withdrawal here.
452
+ const row = recordRow(undefined, record, (op) => blobReady(op, op.blobs.flatMap((blob) => retained(`${X.blob_id} = ? AND ${X.instance} = ? AND ${X.kind} = 'blob-claim'`, [blob.blobId, instance])), effects.verifiedBlobs(op.blobs)));
453
+ const shown = row && row.state !== "withdrawn" ? row.winner : null;
454
+ if (record.winner?.kind === "upsert" && !shown) {
455
+ blocked = true;
456
+ keyPlan.pending.push({
457
+ reason: "missing_or_invalid_blob",
458
+ operations: [record.winner],
459
+ });
460
+ }
461
+ if (shown?.kind === "upsert")
462
+ effects.persistBlobs(shown);
463
+ effects.write(instance, stream, recordKey, shown);
464
+ if (shown)
465
+ run.explaining.push(shown);
466
+ }
467
+ const materialized = new Set(keyPlan.records.map((record) => canonicalJson(record.identity)));
468
+ for (const issue of [...keyPlan.pending, ...keyPlan.rejected]) {
469
+ if (!declarationWait(issue.reason) &&
470
+ issue.reason !== "invalid_record_data")
471
+ continue;
472
+ for (const op of issue.operations) {
473
+ if (op.kind !== "upsert" && op.kind !== "delete")
474
+ continue;
475
+ const identity = {
476
+ owner: op.owner,
477
+ sourceId: op.sourceId,
478
+ connectionId: op.connectionId,
479
+ generation: op.generation,
480
+ stream: op.declaration.stream,
481
+ recordKey: op.recordKey,
482
+ };
483
+ if (local.generation === op.generation &&
484
+ !materialized.has(canonicalJson(identity)))
485
+ effects.write(instance, stream, recordKey, null);
486
+ }
487
+ }
488
+ sql(blocked
489
+ ? "INSERT OR IGNORE INTO pdpp_replica_reconcile_blockers VALUES (?, ?, ?)"
490
+ : "DELETE FROM pdpp_replica_reconcile_blockers WHERE instance = ? AND stream = ? AND record_key = ?").run(instance, stream, recordKey);
491
+ for (const bucket of [
492
+ "records",
493
+ "conflicts",
494
+ "rejected",
495
+ "pending",
496
+ "suppressed",
497
+ ])
498
+ plan[bucket].push(...keyPlan[bucket]);
499
+ enqueue(COMPLETION, instance, stream, "", { background });
500
+ }
501
+ // ---------------------------------------------------------- completions
502
+ /**
503
+ * Persist the engine's completion availability for one provisioned stream
504
+ * with completion evidence at its connection's generation. The terminal
505
+ * quarantine fact applies here, unlike to rows.
506
+ */
507
+ function reconcileCompletion(instance, stream, background, run) {
508
+ settleConnection(instance, background, run);
509
+ // A queued key re-queues this completion once it is decided.
510
+ if (sql(`SELECT 1 FROM pdpp_replica_reconcile_queue WHERE kind IN (${WITHDRAW}, ${KEY}) AND instance = ? AND stream = ? LIMIT 1`).get(instance, stream))
511
+ return;
512
+ const local = effects.binding(instance);
513
+ if (!local?.source_id)
514
+ return;
515
+ const mapping = { sourceId: local.source_id, methodId: local.method ?? "" };
516
+ const { context, plan } = run;
517
+ const resolution = (op) => resolveDeclaration(context.declarations, op.sourceId, op.declaration)
518
+ .status;
519
+ const all = retained(`${X.kind} IN ${COMPLETIONS} AND ${X.instance} = ? AND ${X.stream} = ?`, [instance, stream]);
520
+ sql("DELETE FROM pdpp_replica_reconcile_dependencies WHERE instance = ? AND stream = ?").run(instance, stream);
521
+ for (const op of all)
522
+ if (op.kind === "stream-complete")
523
+ for (const dependency of op.dependencies)
524
+ sql("INSERT OR IGNORE INTO pdpp_replica_reconcile_dependencies VALUES (?, ?, ?, ?)").run(dependency.writerId, dependency.operationId, instance, stream);
525
+ for (const op of all) {
526
+ const status = resolution(op);
527
+ if (status === "missing" || status === "incompatible")
528
+ plan.pending.push({
529
+ reason: `${status}_declaration`,
530
+ operations: [op],
531
+ });
532
+ }
533
+ if (!all.length)
534
+ return;
535
+ const connection = connectionFromFacts(context, factsOf(instance, mapping, worldOf(instance).terminal));
536
+ const events = all.filter((op) => op.generation === connection.generation);
537
+ if (!events.length)
538
+ return;
539
+ const first = events[0];
540
+ const { generation } = first;
541
+ let liveRows;
542
+ const loadLiveRows = () => (liveRows ??= sql("SELECT record_key AS recordKey, data AS dataJson FROM pdpp_records WHERE instance = ? AND stream = ? AND deleted = 0").all(instance, stream));
543
+ const available = completionAvailability({
544
+ connection,
545
+ connectionId: instance,
546
+ events,
547
+ declarations: context.declarations,
548
+ quarantinedKey: !!sql("SELECT 1 FROM pdpp_replica_quarantined_keys WHERE instance = ? AND generation = ? AND stream = ? LIMIT 1").get(instance, generation, stream),
549
+ unresolved: !!sql("SELECT 1 FROM pdpp_replica_reconcile_blockers WHERE instance = ? AND stream = ? LIMIT 1").get(instance, stream),
550
+ variantDigests: (writerId, operationId) => [
551
+ ...new Set(variants(writerId, operationId)
552
+ .filter((row) => effects.binding(row.instance)?.source_id)
553
+ .map((row) => row.digest)),
554
+ ],
555
+ // Read once, and only once every cheaper condition holds.
556
+ get liveRows() {
557
+ return loadLiveRows();
558
+ },
559
+ });
560
+ if (available) {
561
+ const completion = available.operation;
562
+ const seq = sql("SELECT value FROM pdpp_write_clock").get().value;
563
+ const native = completion.proof.kind === "native";
564
+ sql("INSERT INTO pdpp_stream_completion (instance,stream,method,generation,source_collected_at,write_seq,live_count,snapshot_sha256,collection_time_basis) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(instance,stream) DO UPDATE SET method=excluded.method,generation=excluded.generation,source_collected_at=excluded.source_collected_at,write_seq=excluded.write_seq,live_count=excluded.live_count,snapshot_sha256=excluded.snapshot_sha256,collection_time_basis=excluded.collection_time_basis WHERE method IS NOT excluded.method OR generation IS NOT excluded.generation OR source_collected_at IS NOT excluded.source_collected_at OR live_count IS NOT excluded.live_count OR snapshot_sha256 IS NOT excluded.snapshot_sha256 OR collection_time_basis IS NOT excluded.collection_time_basis").run(instance, stream, first.methodId, generation, completion.sourceCollectedAt, seq, completion.liveCount, native ? completion.snapshotSha256.slice(7) : null, native ? "host_run_finished" : null);
565
+ run.explaining.push(completion);
566
+ return;
567
+ }
568
+ sql("DELETE FROM pdpp_stream_completion WHERE instance = ? AND stream = ?").run(instance, stream);
569
+ // Diagnostics only: the engine already decided.
570
+ if (new Set(events.map((op) => declarationShape(op.declaration))).size > 1)
571
+ plan.conflicts.push({
572
+ reason: "incompatible_completion_declaration",
573
+ operations: events,
574
+ });
575
+ else {
576
+ const completion = selectStreamCompletion(events);
577
+ if (completion)
578
+ plan.pending.push({
579
+ reason: "unverified_completion",
580
+ operations: [completion],
581
+ });
582
+ }
583
+ }
584
+ /**
585
+ * Reconcile queued work. `background: false` leaves a paged pass's entries
586
+ * queued (an import finishes only what its evidence marked). Without a
587
+ * page it drains everything in scope; with one it stops once the page is
588
+ * spent, after at least one entry.
589
+ */
590
+ function drain(input) {
591
+ ingest();
592
+ const run = {
593
+ context: { owner: deps.owner, declarations: deps.declarations() },
594
+ plan: {
595
+ records: [],
596
+ conflicts: [],
597
+ rejected: [],
598
+ pending: [],
599
+ suppressed: [],
600
+ },
601
+ reported: new Set(),
602
+ explaining: [],
603
+ };
604
+ meter = { operations: 0, bytes: 0 };
605
+ // Retained evidence does not change during a drain: ingest ran above.
606
+ lifecycleCache = new Map();
607
+ const next = sql(input.background
608
+ ? "SELECT * FROM pdpp_replica_reconcile_queue ORDER BY kind, instance, stream, record_key LIMIT 1"
609
+ : "SELECT * FROM pdpp_replica_reconcile_queue WHERE background = 0 ORDER BY kind, instance, stream, record_key LIMIT 1");
610
+ let processed = 0;
611
+ for (;;) {
612
+ if (input.page &&
613
+ processed > 0 &&
614
+ (meter.operations >= input.page.operations ||
615
+ meter.bytes >= input.page.bytes))
616
+ break;
617
+ const entry = next.get();
618
+ if (!entry)
619
+ break;
620
+ take(entry.kind, entry.instance, entry.stream, entry.record_key);
621
+ processed++;
622
+ stats.entries++;
623
+ const background = entry.background === 1;
624
+ if (entry.kind === CONNECTION)
625
+ reconcileConnection(entry.instance, { background, expand: entry.expand === 1 }, run);
626
+ else if (entry.kind === WITHDRAW)
627
+ withdrawKey(entry.instance, entry.stream, entry.record_key);
628
+ else if (entry.kind === KEY)
629
+ reconcileKey(entry.instance, entry.stream, entry.record_key, background, run);
630
+ else
631
+ reconcileCompletion(entry.instance, entry.stream, background, run);
632
+ }
633
+ return {
634
+ plan: normalize(run.plan),
635
+ complete: !queued(),
636
+ explaining: run.explaining,
637
+ };
638
+ }
639
+ function queued() {
640
+ return !!sql("SELECT 1 FROM pdpp_replica_reconcile_queue LIMIT 1").get();
641
+ }
642
+ return {
643
+ /** Mark what an imported envelope can change, retained duplicates included. */
644
+ touch(operations) {
645
+ ingest();
646
+ for (const op of operations)
647
+ mark(op);
648
+ },
649
+ touchBlobs,
650
+ /** Mark one key, e.g. a local write that completes a conflict. */
651
+ touchKey(instance, stream, recordKey) {
652
+ ingest();
653
+ enqueue(KEY, instance, stream, recordKey);
654
+ },
655
+ /** Mark a connection whose quarantine fact changed outside a reconcile. */
656
+ touchConnection(instance) {
657
+ enqueue(CONNECTION, instance);
658
+ },
659
+ /**
660
+ * Queue a full pass: every connection, expanded to all its keys and
661
+ * completions. Needed when declarations or validators change.
662
+ */
663
+ beginPass() {
664
+ ingest();
665
+ enqueueAll(CONNECTION, "SELECT DISTINCT instance, '' AS stream, '' AS record_key FROM pdpp_replica_reconcile_bindings", [], true);
666
+ sql("UPDATE pdpp_replica_reconcile_queue SET expand = 1 WHERE kind = ?").run(CONNECTION);
667
+ },
668
+ drain,
669
+ queued,
670
+ stats: () => ({ ...stats }),
671
+ };
672
+ }
673
+ /** The planner's normalization over a plan assembled from many partial plans. */
674
+ function normalize(plan) {
675
+ const bucket = (issues) => {
676
+ const merged = new Map();
677
+ // One host issue per reason lists every operation it holds, as before.
678
+ for (const issue of issues) {
679
+ const key = issue.reason === "missing_local_connection"
680
+ ? issue.reason
681
+ : canonicalJson(issue);
682
+ const prior = merged.get(key);
683
+ merged.set(key, prior
684
+ ? {
685
+ reason: issue.reason,
686
+ operations: [...prior.operations, ...issue.operations],
687
+ }
688
+ : { reason: issue.reason, operations: [...issue.operations] });
689
+ }
690
+ const unique = new Map();
691
+ for (const entry of merged.values()) {
692
+ entry.operations = [
693
+ ...new Map(entry.operations.map((op) => [canonicalJson(op), op])).values(),
694
+ ].sort((a, b) => compareText(canonicalJson(a), canonicalJson(b)));
695
+ unique.set(canonicalJson(entry), entry);
696
+ }
697
+ return [...unique.values()].sort((a, b) => compareText(canonicalJson(a), canonicalJson(b)));
698
+ };
699
+ const records = [
700
+ ...new Map(plan.records.map((record) => [canonicalJson(record.identity), record])).values(),
701
+ ].sort((a, b) => compareText(canonicalJson(a.identity), canonicalJson(b.identity)));
702
+ return {
703
+ records,
704
+ conflicts: bucket(plan.conflicts),
705
+ rejected: bucket(plan.rejected),
706
+ pending: bucket(plan.pending),
707
+ suppressed: bucket(plan.suppressed),
708
+ };
709
+ }
710
+ //# sourceMappingURL=pdpp-replica-reconciler.js.map