@opendatalabs/personal-server-ts-server 1.34.2 → 1.35.1

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 (183) 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/data.d.ts.map +1 -1
  43. package/dist/routes/data.js +5 -0
  44. package/dist/routes/data.js.map +1 -1
  45. package/dist/routes/mcp.d.ts +18 -2
  46. package/dist/routes/mcp.d.ts.map +1 -1
  47. package/dist/routes/mcp.js +111 -11
  48. package/dist/routes/mcp.js.map +1 -1
  49. package/dist/routes/oura.d.ts +6 -0
  50. package/dist/routes/oura.d.ts.map +1 -0
  51. package/dist/routes/oura.js +272 -0
  52. package/dist/routes/oura.js.map +1 -0
  53. package/dist/routes/pdpp-auth-query-keys.d.ts +7 -0
  54. package/dist/routes/pdpp-auth-query-keys.d.ts.map +1 -0
  55. package/dist/routes/pdpp-auth-query-keys.js +25 -0
  56. package/dist/routes/pdpp-auth-query-keys.js.map +1 -0
  57. package/dist/routes/pdpp-auth.d.ts +3 -1
  58. package/dist/routes/pdpp-auth.d.ts.map +1 -1
  59. package/dist/routes/pdpp-auth.js +126 -45
  60. package/dist/routes/pdpp-auth.js.map +1 -1
  61. package/dist/routes/pdpp-blobs.d.ts +13 -1
  62. package/dist/routes/pdpp-blobs.d.ts.map +1 -1
  63. package/dist/routes/pdpp-blobs.js +100 -65
  64. package/dist/routes/pdpp-blobs.js.map +1 -1
  65. package/dist/routes/pdpp-connections.d.ts.map +1 -1
  66. package/dist/routes/pdpp-connections.js +24 -1
  67. package/dist/routes/pdpp-connections.js.map +1 -1
  68. package/dist/routes/pdpp-instance-bindings.d.ts +3 -0
  69. package/dist/routes/pdpp-instance-bindings.d.ts.map +1 -1
  70. package/dist/routes/pdpp-instance-bindings.js +63 -0
  71. package/dist/routes/pdpp-instance-bindings.js.map +1 -1
  72. package/dist/routes/pdpp-record-query.d.ts +19 -0
  73. package/dist/routes/pdpp-record-query.d.ts.map +1 -0
  74. package/dist/routes/pdpp-record-query.js +51 -0
  75. package/dist/routes/pdpp-record-query.js.map +1 -0
  76. package/dist/routes/pdpp-records.d.ts +12 -1
  77. package/dist/routes/pdpp-records.d.ts.map +1 -1
  78. package/dist/routes/pdpp-records.js +120 -233
  79. package/dist/routes/pdpp-records.js.map +1 -1
  80. package/dist/routes/pdpp-replica-quarantine-fixture.d.ts +65 -0
  81. package/dist/routes/pdpp-replica-quarantine-fixture.d.ts.map +1 -0
  82. package/dist/routes/pdpp-replica-quarantine-fixture.js +389 -0
  83. package/dist/routes/pdpp-replica-quarantine-fixture.js.map +1 -0
  84. package/dist/storage/claude-method-transition.d.ts +12 -0
  85. package/dist/storage/claude-method-transition.d.ts.map +1 -0
  86. package/dist/storage/claude-method-transition.js +139 -0
  87. package/dist/storage/claude-method-transition.js.map +1 -0
  88. package/dist/storage/import-operations.d.ts +16 -0
  89. package/dist/storage/import-operations.d.ts.map +1 -1
  90. package/dist/storage/import-operations.js +54 -1
  91. package/dist/storage/import-operations.js.map +1 -1
  92. package/dist/storage/index-schema.d.ts +1 -1
  93. package/dist/storage/index-schema.d.ts.map +1 -1
  94. package/dist/storage/index-schema.js +22 -5
  95. package/dist/storage/index-schema.js.map +1 -1
  96. package/dist/storage/legacy-scope-authority.d.ts.map +1 -1
  97. package/dist/storage/legacy-scope-authority.js +83 -11
  98. package/dist/storage/legacy-scope-authority.js.map +1 -1
  99. package/dist/storage/node-data-storage.d.ts.map +1 -1
  100. package/dist/storage/node-data-storage.js +58 -2
  101. package/dist/storage/node-data-storage.js.map +1 -1
  102. package/dist/storage/pdpp-binary-chunks.d.ts +120 -0
  103. package/dist/storage/pdpp-binary-chunks.d.ts.map +1 -0
  104. package/dist/storage/pdpp-binary-chunks.js +480 -0
  105. package/dist/storage/pdpp-binary-chunks.js.map +1 -0
  106. package/dist/storage/pdpp-records-sqlite-store.d.ts +96 -15
  107. package/dist/storage/pdpp-records-sqlite-store.d.ts.map +1 -1
  108. package/dist/storage/pdpp-records-sqlite-store.js +616 -211
  109. package/dist/storage/pdpp-records-sqlite-store.js.map +1 -1
  110. package/dist/storage/pdpp-replica-canonical-port.d.ts +202 -0
  111. package/dist/storage/pdpp-replica-canonical-port.d.ts.map +1 -0
  112. package/dist/storage/pdpp-replica-canonical-port.js +451 -0
  113. package/dist/storage/pdpp-replica-canonical-port.js.map +1 -0
  114. package/dist/storage/pdpp-replica-coordinator-state.d.ts +16 -0
  115. package/dist/storage/pdpp-replica-coordinator-state.d.ts.map +1 -0
  116. package/dist/storage/pdpp-replica-coordinator-state.js +25 -0
  117. package/dist/storage/pdpp-replica-coordinator-state.js.map +1 -0
  118. package/dist/storage/pdpp-replica-copies.d.ts +81 -0
  119. package/dist/storage/pdpp-replica-copies.d.ts.map +1 -0
  120. package/dist/storage/pdpp-replica-copies.js +59 -0
  121. package/dist/storage/pdpp-replica-copies.js.map +1 -0
  122. package/dist/storage/pdpp-replica-reconciler.d.ts +96 -0
  123. package/dist/storage/pdpp-replica-reconciler.d.ts.map +1 -0
  124. package/dist/storage/pdpp-replica-reconciler.js +710 -0
  125. package/dist/storage/pdpp-replica-reconciler.js.map +1 -0
  126. package/dist/storage/pdpp-replica-sql.d.ts +13 -0
  127. package/dist/storage/pdpp-replica-sql.d.ts.map +1 -0
  128. package/dist/storage/pdpp-replica-sql.js +43 -0
  129. package/dist/storage/pdpp-replica-sql.js.map +1 -0
  130. package/dist/storage/pdpp-replica-staging.d.ts +36 -0
  131. package/dist/storage/pdpp-replica-staging.d.ts.map +1 -0
  132. package/dist/storage/pdpp-replica-staging.js +202 -0
  133. package/dist/storage/pdpp-replica-staging.js.map +1 -0
  134. package/dist/storage/pdpp-replica-visibility-view.d.ts +64 -0
  135. package/dist/storage/pdpp-replica-visibility-view.d.ts.map +1 -0
  136. package/dist/storage/pdpp-replica-visibility-view.js +1397 -0
  137. package/dist/storage/pdpp-replica-visibility-view.js.map +1 -0
  138. package/dist/storage/pdpp-replica-writer-identity.d.ts +64 -0
  139. package/dist/storage/pdpp-replica-writer-identity.d.ts.map +1 -0
  140. package/dist/storage/pdpp-replica-writer-identity.js +81 -0
  141. package/dist/storage/pdpp-replica-writer-identity.js.map +1 -0
  142. package/dist/storage/pdpp-replication-door.d.ts +60 -0
  143. package/dist/storage/pdpp-replication-door.d.ts.map +1 -0
  144. package/dist/storage/pdpp-replication-door.js +203 -0
  145. package/dist/storage/pdpp-replication-door.js.map +1 -0
  146. package/dist/storage/pdpp-replication-host.d.ts +67 -0
  147. package/dist/storage/pdpp-replication-host.d.ts.map +1 -0
  148. package/dist/storage/pdpp-replication-host.js +159 -0
  149. package/dist/storage/pdpp-replication-host.js.map +1 -0
  150. package/dist/storage/pdpp-replication-memory.fixture.d.ts +2 -0
  151. package/dist/storage/pdpp-replication-memory.fixture.d.ts.map +1 -0
  152. package/dist/storage/pdpp-replication-memory.fixture.js +267 -0
  153. package/dist/storage/pdpp-replication-memory.fixture.js.map +1 -0
  154. package/dist/storage/pdpp-replication-preparation.d.ts +3 -0
  155. package/dist/storage/pdpp-replication-preparation.d.ts.map +1 -0
  156. package/dist/storage/pdpp-replication-preparation.js +3 -0
  157. package/dist/storage/pdpp-replication-preparation.js.map +1 -0
  158. package/dist/storage/pdpp-replication-promotion-memory.fixture.d.ts +2 -0
  159. package/dist/storage/pdpp-replication-promotion-memory.fixture.d.ts.map +1 -0
  160. package/dist/storage/pdpp-replication-promotion-memory.fixture.js +65 -0
  161. package/dist/storage/pdpp-replication-promotion-memory.fixture.js.map +1 -0
  162. package/dist/storage/pdpp-replication-receiver.d.ts +230 -0
  163. package/dist/storage/pdpp-replication-receiver.d.ts.map +1 -0
  164. package/dist/storage/pdpp-replication-receiver.js +950 -0
  165. package/dist/storage/pdpp-replication-receiver.js.map +1 -0
  166. package/dist/storage/pdpp-replication-sqlite.d.ts +95 -0
  167. package/dist/storage/pdpp-replication-sqlite.d.ts.map +1 -0
  168. package/dist/storage/pdpp-replication-sqlite.js +615 -0
  169. package/dist/storage/pdpp-replication-sqlite.js.map +1 -0
  170. package/dist/storage/pdpp-replication-transport.d.ts +39 -0
  171. package/dist/storage/pdpp-replication-transport.d.ts.map +1 -0
  172. package/dist/storage/pdpp-replication-transport.js +196 -0
  173. package/dist/storage/pdpp-replication-transport.js.map +1 -0
  174. package/dist/storage/schema-versions.d.ts +5 -0
  175. package/dist/storage/schema-versions.d.ts.map +1 -0
  176. package/dist/storage/schema-versions.js +26 -0
  177. package/dist/storage/schema-versions.js.map +1 -0
  178. package/dist/storage/strava-method-transition.d.ts +18 -0
  179. package/dist/storage/strava-method-transition.d.ts.map +1 -0
  180. package/dist/storage/strava-method-transition.js +209 -0
  181. package/dist/storage/strava-method-transition.js.map +1 -0
  182. package/dist/ui/ps-lite-debug.js +154947 -124896
  183. package/package.json +3 -3
@@ -0,0 +1,1397 @@
1
+ import { createHash } from "node:crypto";
2
+ import { canonicalJson, } from "@opendatalabs/personal-server-ts-core/replication/envelope";
3
+ import { planReplicaMerge, } from "@opendatalabs/personal-server-ts-core/replication/merge-plan";
4
+ import { replicaPendingMetadata, replicaConflictMetadata, replicaConnectionVisibility, replicaStateVisible, } from "@opendatalabs/personal-server-ts-core/replication/visibility-policy";
5
+ import { CursorExpiredError, InvalidCursorError, decodeCursor, encodeCursor, } from "@opendatalabs/personal-server-ts-core/storage/pdpp-records";
6
+ import { timeSortKey } from "@opendatalabs/personal-server-ts-core/pdpp";
7
+ import { replicaOperationDigest } from "./pdpp-replication-sqlite.js";
8
+ import { captureWrites } from "./pdpp-replication-door.js";
9
+ /** Bump when the derived view layout changes; construction then rebuilds it. */
10
+ const VIEW_SCHEMA = 11;
11
+ /** How often a transaction may find another connection committed first. */
12
+ const SYNC_ATTEMPTS = 16;
13
+ /**
14
+ * Thrown by a view call inside a caller's transaction that cannot be shown to
15
+ * match canonical rows: another connection committed before the transaction
16
+ * took its snapshot, and a transaction cannot write the re-derivation.
17
+ */
18
+ class DerivedStateStale extends Error {
19
+ constructor() {
20
+ super("replica view is stale inside a transaction not opened through the replica boundary; open it with readSnapshot or writeTransaction");
21
+ }
22
+ }
23
+ /** Thrown inside a transaction that started after another connection committed. */
24
+ class DerivedStateMoved extends Error {
25
+ constructor() {
26
+ super("replica view could not catch up with concurrent writers");
27
+ }
28
+ }
29
+ /**
30
+ * Revisions are comparable only within one view layout, so the layout version
31
+ * tags every cursor. 70d52db0 cursors carry an untagged revision and expire.
32
+ */
33
+ const CURSOR_FORMAT = `v${VIEW_SCHEMA}`;
34
+ const LIFECYCLE = "('connection-register','connection-reset','connection-delete')";
35
+ // Canonical triggers ignore rewrites that change no stored value.
36
+ const recordChanged = "(OLD.version IS NOT NEW.version OR OLD.data IS NOT NEW.data OR OLD.emitted_at IS NOT NEW.emitted_at OR OLD.deleted IS NOT NEW.deleted OR OLD.deleted_at IS NOT NEW.deleted_at OR OLD.blob_id IS NOT NEW.blob_id)";
37
+ // Retained operations are indexed in place by these expressions; no copy of their JSON.
38
+ // The receiver's reconciler queries the same indexes, so it reads them from here.
39
+ export const operationExpressions = {
40
+ instance: "json_extract(operation_json,'$.connectionId')",
41
+ stream: "json_extract(operation_json,'$.declaration.stream')",
42
+ record_key: "json_extract(operation_json,'$.recordKey')",
43
+ kind: "json_extract(operation_json,'$.kind')",
44
+ writer: "json_extract(operation_json,'$.revision.writerId')",
45
+ operation_id: "json_extract(operation_json,'$.revision.operationId')",
46
+ blob_id: "COALESCE(json_extract(operation_json,'$.blob.blobId'),json_extract(operation_json,'$.blobs[0].blobId'))",
47
+ };
48
+ const expression = (column) => operationExpressions[column];
49
+ const operationColumns = Object.entries(operationExpressions)
50
+ .map(([column, sql]) => `${sql} AS ${column}`)
51
+ .join(",");
52
+ /**
53
+ * Triggers the view's maintenance used to create as persistent objects. They
54
+ * are now TEMP triggers of the enabling connection, so a persistent one is a
55
+ * leftover of an earlier build; on a canonical table it would fire for a
56
+ * plain open's writes. Drops every one, on every enabled open.
57
+ */
58
+ function dropPersistentViewTriggers(db) {
59
+ for (const { name } of db
60
+ .prepare("SELECT name FROM sqlite_master WHERE type='trigger' AND (name GLOB 'pdpp_replica_view*' OR name GLOB 'pdpp_replica_local*' OR name GLOB 'pdpp_records_summary_*')")
61
+ .all())
62
+ db.exec(`DROP TRIGGER IF EXISTS main."${name}"`);
63
+ }
64
+ /** Drops only derived view objects. Canonical, retained and pending tables stay. */
65
+ function dropViewObjects(db) {
66
+ const objects = db
67
+ .prepare("SELECT type,name FROM sqlite_master WHERE (type IN ('trigger','view','index') AND (name GLOB 'pdpp_replica_view*' OR name GLOB 'pdpp_replica_local*' OR name GLOB 'pdpp_records_summary_*')) OR (type='table' AND (name GLOB 'pdpp_replica_view*' OR name='pdpp_replica_local_view'))")
68
+ .all();
69
+ const order = ["trigger", "view", "index", "table"];
70
+ for (const { type, name } of objects.sort((a, b) => order.indexOf(a.type) - order.indexOf(b.type)))
71
+ db.exec(`DROP ${type.toUpperCase()} IF EXISTS "${name}"`);
72
+ }
73
+ /**
74
+ * Sparse owner preview, reconciled at mutation time. It persists only keys whose
75
+ * owner-visible state differs from canonical: pending values, marked pending
76
+ * removals and hidden equivocated keys. Without pending evidence or operation-ID
77
+ * variants for a key's merge inputs, owner preview equals canonical by definition
78
+ * and stores nothing. Reads never run the reducer.
79
+ */
80
+ export function createReplicaVisibilityView(deps) {
81
+ const { db } = deps;
82
+ const hasBindingConflictFacts = !!db
83
+ .prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='pdpp_replica_binding_conflicts'")
84
+ .get();
85
+ const localIdentitySql = (alias) => `EXISTS (SELECT 1 FROM pdpp_instance_binding b WHERE b.instance=${alias}.instance AND b.deleted_at IS NULL AND b.source_id IS NOT NULL AND b.method IS NOT NULL AND b.source_id=${alias}.source_id AND b.method=${alias}.method AND b.generation=${alias}.generation)`;
86
+ const localProjection = `SELECT j.connection_id,j.stream,j.record_key,json_extract(j.value_json,'$.emittedAt'),j.value_json,json_extract(j.value_json,'$.data.blob_ref.blob_id'),0,json_extract(j.value_json,'$.deleted'),NULL,j.sequence,j.generation,j.source_id,j.method FROM pdpp_replica_own_writes j JOIN pdpp_instance_binding b ON b.instance=j.connection_id AND b.generation=j.generation AND b.source_id=j.source_id AND b.method=j.method WHERE b.source_id IS NOT NULL AND b.method IS NOT NULL AND b.deleted_at IS NULL AND j.writer_id=(SELECT writer_id FROM pdpp_installation_writer WHERE id=1) AND NOT EXISTS (SELECT 1 FROM pdpp_replica_own_writes newer WHERE newer.connection_id=j.connection_id AND newer.generation=j.generation AND newer.stream=j.stream AND newer.record_key=j.record_key AND newer.source_id=j.source_id AND newer.method=j.method AND newer.writer_id=j.writer_id AND newer.sequence>j.sequence)`;
87
+ // Pending custody can supply a retained record on another connection.
88
+ const pendingBlobReference = (instance, stream, key) => `EXISTS (SELECT 1 FROM pdpp_replica_view_operations bo JOIN pdpp_replica_view_pending bp ON bp.blob_id=bo.blob_id AND bp.kind='blob-claim' WHERE bo.instance=${instance} AND bo.stream=${stream} AND bo.record_key=${key} AND bo.kind='upsert')`;
89
+ // Whether a key can have an overlay row at all. Canonical writes and retained
90
+ // record operations outside this predicate cannot change the owner preview.
91
+ const touches = (instance, stream, key) => `(EXISTS (SELECT 1 FROM pdpp_replica_view v WHERE v.instance=${instance} AND v.stream=${stream} AND v.record_key=${key}) OR EXISTS (SELECT 1 FROM pdpp_replica_view_pending p WHERE p.instance=${instance}) OR ${pendingBlobReference(instance, stream, key)} OR EXISTS (SELECT 1 FROM pdpp_replica_view_conflicts c WHERE c.instance=${instance}) OR EXISTS (SELECT 1 FROM pdpp_instance_binding b WHERE b.instance=${instance} AND b.deleted_at IS NOT NULL))`;
92
+ // Trigger bodies use NOT EXISTS, never a conflict clause (OR IGNORE, OR
93
+ // REPLACE, ...): an outer statement's conflict clause, including an upsert's,
94
+ // overrides one inside a trigger. A receiver test checks every trigger body.
95
+ const notDirtyKey = (alias) => `NOT EXISTS (SELECT 1 FROM pdpp_replica_view_dirty_keys d WHERE d.instance=${alias}.instance AND d.stream=${alias}.stream AND d.record_key=${alias}.record_key)`;
96
+ // Upserts that reference any of `blobIds` (a SQL value list or subquery),
97
+ // restricted to keys that can have an overlay row.
98
+ const markBlobDependents = (blobIds) => `INSERT INTO pdpp_replica_view_dirty_keys SELECT DISTINCT o.instance,o.stream,o.record_key FROM pdpp_replica_view_operations o WHERE o.blob_id IN (${blobIds}) AND o.kind='upsert' AND o.record_key IS NOT NULL AND ${touches("o.instance", "o.stream", "o.record_key")} AND ${notDirtyKey("o")};`;
99
+ // The blob id of a chunk manifest; pending custody keys are JSON [batch, blob].
100
+ const chunkedBlobId = (alias) => `SELECT CASE WHEN ${alias}.namespace<>'pending' THEN ${alias}.blob_id WHEN json_valid(${alias}.blob_id) THEN json_extract(${alias}.blob_id,'$[1]') END`;
101
+ const markLatest = (alias) =>
102
+ // NOT EXISTS, not OR IGNORE: an outer statement's conflict clause
103
+ // overrides one inside a trigger.
104
+ `INSERT INTO pdpp_replica_view_dirty_latest SELECT ${alias}.instance,${alias}.stream WHERE NOT EXISTS (SELECT 1 FROM pdpp_replica_view_dirty_latest d WHERE d.instance=${alias}.instance AND d.stream=${alias}.stream);`;
105
+ const hasOverlaySummary = (alias) => `EXISTS (SELECT 1 FROM pdpp_replica_view_streams s WHERE s.preview=1 AND s.instance=${alias}.instance AND s.stream=${alias}.stream)`;
106
+ const markKey = (alias) => `INSERT INTO pdpp_replica_view_dirty_keys SELECT ${alias}.instance,${alias}.stream,${alias}.record_key WHERE ${touches(`${alias}.instance`, `${alias}.stream`, `${alias}.record_key`)} AND ${notDirtyKey(alias)};`;
107
+ /**
108
+ * `PRAGMA data_version` as of the last time the derived state matched
109
+ * canonical rows. It moves only when another connection commits.
110
+ */
111
+ let seen = -1;
112
+ let syncProbe;
113
+ let dataVersionStatement;
114
+ let transitions;
115
+ db.transaction(() => {
116
+ const schema = db
117
+ .prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='pdpp_replica_view_schema'")
118
+ .get()
119
+ ? db.prepare("SELECT version FROM pdpp_replica_view_schema").get()
120
+ : undefined;
121
+ const initialized = schema?.version === VIEW_SCHEMA;
122
+ dropPersistentViewTriggers(db);
123
+ // Derived state only: an earlier view layout is dropped and rebuilt here, at
124
+ // construction inside this write transaction, never during a read.
125
+ if (!initialized)
126
+ dropViewObjects(db);
127
+ db.exec(`
128
+ CREATE TABLE IF NOT EXISTS pdpp_replica_view_schema (version INTEGER NOT NULL);
129
+ CREATE TABLE IF NOT EXISTS pdpp_replica_visibility_epoch (id INTEGER PRIMARY KEY CHECK(id=1), epoch INTEGER NOT NULL, signature TEXT NOT NULL DEFAULT '');
130
+ INSERT OR IGNORE INTO pdpp_replica_visibility_epoch(id,epoch) VALUES (1,0);
131
+ -- Sparse overlay. present: shown in owner pages; live: a visible value;
132
+ -- base_live: canonical row was live when this row was derived.
133
+ CREATE TABLE IF NOT EXISTS pdpp_replica_view (
134
+ instance TEXT NOT NULL, stream TEXT NOT NULL, record_key TEXT NOT NULL,
135
+ emitted_at TEXT NOT NULL, row_json TEXT, blob_id TEXT, blob_json TEXT,
136
+ present INTEGER NOT NULL, live INTEGER NOT NULL, pending INTEGER NOT NULL, base_live INTEGER NOT NULL,
137
+ -- Generation of the value this row shows: a pending reset's winner can
138
+ -- be newer than the binding. Quarantine is applied by it.
139
+ generation INTEGER NOT NULL,
140
+ -- List order is the emitted instant, as on the canonical store.
141
+ emitted_key TEXT GENERATED ALWAYS AS (pdpp_time_key(emitted_at)) VIRTUAL,
142
+ PRIMARY KEY(instance,stream,record_key)
143
+ );
144
+ CREATE INDEX IF NOT EXISTS pdpp_replica_view_page ON pdpp_replica_view(instance,stream,emitted_key,record_key) WHERE present=1;
145
+ CREATE INDEX IF NOT EXISTS pdpp_replica_view_blob ON pdpp_replica_view(blob_id) WHERE blob_id IS NOT NULL;
146
+ DROP INDEX IF EXISTS pdpp_replica_canonical_page;
147
+ CREATE INDEX IF NOT EXISTS pdpp_replica_view_canonical_page ON pdpp_records(instance,stream,emitted_key,record_key) WHERE deleted=0;
148
+ CREATE INDEX IF NOT EXISTS pdpp_replica_pending_blob ON pdpp_replica_pending_bytes(blob_id,batch_id);
149
+ -- One summary and one monotone cursor revision per (mode table, connection, stream).
150
+ -- Mode 1 holds the overlay's deltas against mode 0, not a second count.
151
+ CREATE TABLE IF NOT EXISTS pdpp_replica_view_streams (preview INTEGER NOT NULL, instance TEXT NOT NULL, stream TEXT NOT NULL, live_count INTEGER NOT NULL, row_count INTEGER NOT NULL, pending_count INTEGER NOT NULL, last_updated TEXT, revision INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(preview,instance,stream));
152
+ -- Active pending operations, one row per (batch, operation).
153
+ CREATE TABLE IF NOT EXISTS pdpp_replica_view_pending (
154
+ batch_id TEXT NOT NULL, digest TEXT NOT NULL, operation_json TEXT NOT NULL,
155
+ instance TEXT NOT NULL, stream TEXT, record_key TEXT, kind TEXT NOT NULL,
156
+ writer TEXT NOT NULL, operation_id TEXT NOT NULL, blob_id TEXT,
157
+ PRIMARY KEY(batch_id,digest)
158
+ );
159
+ CREATE INDEX IF NOT EXISTS pdpp_replica_view_pending_key ON pdpp_replica_view_pending(instance,stream,record_key);
160
+ CREATE INDEX IF NOT EXISTS pdpp_replica_view_pending_kind ON pdpp_replica_view_pending(instance,kind);
161
+ CREATE INDEX IF NOT EXISTS pdpp_replica_view_pending_id ON pdpp_replica_view_pending(writer,operation_id);
162
+ CREATE INDEX IF NOT EXISTS pdpp_replica_view_pending_blob ON pdpp_replica_view_pending(blob_id,instance) WHERE blob_id IS NOT NULL;
163
+ -- Operation-ID variants (global equivocation), by digest.
164
+ CREATE TABLE IF NOT EXISTS pdpp_replica_view_conflicts (writer TEXT NOT NULL, operation_id TEXT NOT NULL, instance TEXT NOT NULL, digest TEXT NOT NULL, PRIMARY KEY(writer,operation_id,digest));
165
+ CREATE INDEX IF NOT EXISTS pdpp_replica_view_conflict_instance ON pdpp_replica_view_conflicts(instance);
166
+ CREATE TABLE IF NOT EXISTS pdpp_replica_view_dirty_operations (digest TEXT PRIMARY KEY, operation_json TEXT NOT NULL);
167
+ CREATE TABLE IF NOT EXISTS pdpp_replica_view_dirty_keys (instance TEXT NOT NULL, stream TEXT NOT NULL, record_key TEXT NOT NULL, PRIMARY KEY(instance,stream,record_key));
168
+ CREATE TABLE IF NOT EXISTS pdpp_replica_view_dirty_connections (instance TEXT PRIMARY KEY);
169
+ -- Streams whose owner-preview latest row may have moved; flush recomputes them.
170
+ CREATE TABLE IF NOT EXISTS pdpp_replica_view_dirty_latest (instance TEXT NOT NULL, stream TEXT NOT NULL, PRIMARY KEY(instance,stream));
171
+ -- What the derived state above was last made to match, as of this connection's
172
+ -- last write. It rolls back with the transaction that moved it.
173
+ CREATE TABLE IF NOT EXISTS pdpp_replica_view_sync (id INTEGER PRIMARY KEY CHECK(id=1), change_mark INTEGER NOT NULL, structure TEXT NOT NULL);
174
+ `);
175
+ const epochColumns = db
176
+ .prepare("PRAGMA table_info(pdpp_replica_visibility_epoch)")
177
+ .all();
178
+ if (!epochColumns.some((column) => column.name === "signature")) {
179
+ db.exec("ALTER TABLE pdpp_replica_visibility_epoch ADD COLUMN signature TEXT NOT NULL DEFAULT ''");
180
+ db.prepare("UPDATE pdpp_replica_visibility_epoch SET epoch=epoch+1 WHERE id=1").run();
181
+ }
182
+ for (const table of ["pdpp_replica_operations", "pdpp_replica_received"])
183
+ db.exec(`
184
+ CREATE INDEX IF NOT EXISTS pdpp_replica_view_ops_key_${table} ON ${table}(${expression("instance")},${expression("stream")},${expression("record_key")}) WHERE ${expression("record_key")} IS NOT NULL;
185
+ CREATE INDEX IF NOT EXISTS pdpp_replica_view_ops_kind_${table} ON ${table}(${expression("instance")},${expression("kind")});
186
+ CREATE INDEX IF NOT EXISTS pdpp_replica_view_ops_id_${table} ON ${table}(${expression("writer")},${expression("operation_id")});
187
+ CREATE INDEX IF NOT EXISTS pdpp_replica_view_ops_blob_${table} ON ${table}(${expression("blob_id")},${expression("instance")}) WHERE ${expression("blob_id")} IS NOT NULL;
188
+ `);
189
+ db.exec(`
190
+ CREATE VIEW IF NOT EXISTS pdpp_replica_view_operations AS
191
+ SELECT digest,operation_json,${operationColumns},1 AS confirmed,0 AS pending FROM pdpp_replica_operations
192
+ UNION ALL SELECT digest,operation_json,${operationColumns},1,0 FROM pdpp_replica_received
193
+ UNION ALL SELECT digest,operation_json,instance,stream,record_key,kind,writer,operation_id,blob_id,0,1 FROM pdpp_replica_view_pending;
194
+ `);
195
+ for (const table of ["pdpp_replica_operations", "pdpp_replica_received"])
196
+ db.exec(`
197
+ CREATE TEMP TRIGGER IF NOT EXISTS pdpp_replica_view_retained_${table} AFTER INSERT ON ${table}
198
+ WHEN json_extract(NEW.operation_json,'$.kind') NOT IN ('upsert','delete')
199
+ OR EXISTS (SELECT 1 FROM pdpp_replica_view_operations o WHERE o.writer=json_extract(NEW.operation_json,'$.revision.writerId') AND o.operation_id=json_extract(NEW.operation_json,'$.revision.operationId') AND o.digest<>NEW.digest)
200
+ OR ${touches("json_extract(NEW.operation_json,'$.connectionId')", "json_extract(NEW.operation_json,'$.declaration.stream')", "json_extract(NEW.operation_json,'$.recordKey')")}
201
+ BEGIN
202
+ INSERT INTO pdpp_replica_view_dirty_operations SELECT NEW.digest,NEW.operation_json WHERE NOT EXISTS (SELECT 1 FROM pdpp_replica_view_dirty_operations d WHERE d.digest=NEW.digest);
203
+ END;
204
+ `);
205
+ // Every maintenance trigger is a TEMP trigger: it lives on this connection
206
+ // only. A plain open of the same file (replication off, or a main build)
207
+ // then runs none of this SQL, and a persistent one would fire on its writes.
208
+ db.exec(`
209
+ CREATE TEMP TRIGGER IF NOT EXISTS pdpp_replica_view_record_insert AFTER INSERT ON pdpp_records BEGIN ${markKey("NEW")} END;
210
+ CREATE TEMP TRIGGER IF NOT EXISTS pdpp_replica_view_record_update AFTER UPDATE ON pdpp_records WHEN ${recordChanged} BEGIN ${markKey("NEW")} END;
211
+ CREATE TEMP TRIGGER IF NOT EXISTS pdpp_replica_view_record_delete AFTER DELETE ON pdpp_records BEGIN ${markKey("OLD")} END;
212
+ CREATE TEMP TRIGGER IF NOT EXISTS pdpp_replica_view_binding AFTER UPDATE ON pdpp_instance_binding
213
+ WHEN NEW.generation != OLD.generation OR NEW.deleted_at IS NOT OLD.deleted_at OR NEW.source_id IS NOT OLD.source_id OR NEW.method IS NOT OLD.method OR NEW.reset_clock IS NOT OLD.reset_clock BEGIN
214
+ -- Binding and lifecycle changes expire every stream cursor of the connection.
215
+ UPDATE pdpp_replica_view_streams SET revision=revision+1 WHERE instance=NEW.instance;
216
+ INSERT INTO pdpp_replica_view_dirty_connections SELECT NEW.instance WHERE NOT EXISTS (SELECT 1 FROM pdpp_replica_view_dirty_connections d WHERE d.instance=NEW.instance);
217
+ END;
218
+ -- Blob bytes decide whether an upsert that references them is visible. Bytes
219
+ -- can arrive or leave without any new record operation (a replayed claim,
220
+ -- another connection's import, another pending batch), so their arrival
221
+ -- and removal mark every dependent key the view can reach.
222
+ CREATE TEMP TRIGGER IF NOT EXISTS pdpp_replica_view_bytes_received AFTER INSERT ON pdpp_replica_received_bytes BEGIN ${markBlobDependents("NEW.blob_id")} END;
223
+ CREATE TEMP TRIGGER IF NOT EXISTS pdpp_replica_view_bytes_received_delete AFTER DELETE ON pdpp_replica_received_bytes BEGIN ${markBlobDependents("OLD.blob_id")} END;
224
+ CREATE TEMP TRIGGER IF NOT EXISTS pdpp_replica_view_bytes_canonical AFTER INSERT ON pdpp_blob_bytes BEGIN ${markBlobDependents("NEW.blob_id")} END;
225
+ CREATE TEMP TRIGGER IF NOT EXISTS pdpp_replica_view_bytes_canonical_delete AFTER DELETE ON pdpp_blob_bytes BEGIN ${markBlobDependents("OLD.blob_id")} END;
226
+ CREATE TEMP TRIGGER IF NOT EXISTS pdpp_replica_view_bytes_pending AFTER INSERT ON pdpp_replica_pending_bytes BEGIN ${markBlobDependents("NEW.blob_id")} END;
227
+ CREATE TEMP TRIGGER IF NOT EXISTS pdpp_replica_view_bytes_pending_delete AFTER DELETE ON pdpp_replica_pending_bytes BEGIN ${markBlobDependents("OLD.blob_id")} END;
228
+ -- Chunked bytes (#439) exist once their manifest is published after EOF,
229
+ -- and are gone once it is deleted. Pending chunks are keyed [batch, blob].
230
+ CREATE TEMP TRIGGER IF NOT EXISTS pdpp_replica_view_bytes_chunked AFTER INSERT ON pdpp_binary_manifests WHEN NEW.namespace IN ('canonical','received','pending') BEGIN ${markBlobDependents(chunkedBlobId("NEW"))} END;
231
+ CREATE TEMP TRIGGER IF NOT EXISTS pdpp_replica_view_bytes_chunked_delete AFTER DELETE ON pdpp_binary_manifests WHEN OLD.namespace IN ('canonical','received','pending') BEGIN ${markBlobDependents(chunkedBlobId("OLD"))} END;
232
+ -- Pending bytes, legacy rows or chunks, count only while their batch is
233
+ -- pending. blobs_json lists both.
234
+ CREATE TEMP TRIGGER IF NOT EXISTS pdpp_replica_view_bytes_batch AFTER UPDATE OF state ON pdpp_replica_pending_batches WHEN NEW.state IS NOT OLD.state BEGIN
235
+ ${markBlobDependents("SELECT blob_id FROM pdpp_replica_pending_bytes WHERE batch_id=NEW.id UNION SELECT json_extract(value,'$.blobId') FROM json_each(CASE WHEN json_valid(NEW.blobs_json) THEN NEW.blobs_json ELSE '[]' END)")}
236
+ END;
237
+ -- Mode 1's last_updated is the owner preview's latest row, derived at
238
+ -- write time: overlay rows can mask any number of newer canonical rows.
239
+ CREATE TEMP TRIGGER IF NOT EXISTS pdpp_replica_view_latest_overlay_insert AFTER INSERT ON pdpp_replica_view BEGIN ${markLatest("NEW")} END;
240
+ CREATE TEMP TRIGGER IF NOT EXISTS pdpp_replica_view_latest_overlay_update AFTER UPDATE ON pdpp_replica_view BEGIN ${markLatest("NEW")} END;
241
+ CREATE TEMP TRIGGER IF NOT EXISTS pdpp_replica_view_latest_overlay_delete AFTER DELETE ON pdpp_replica_view BEGIN ${markLatest("OLD")} END;
242
+ CREATE TEMP TRIGGER IF NOT EXISTS pdpp_replica_view_latest_record_insert AFTER INSERT ON pdpp_records WHEN ${hasOverlaySummary("NEW")} BEGIN ${markLatest("NEW")} END;
243
+ CREATE TEMP TRIGGER IF NOT EXISTS pdpp_replica_view_latest_record_update AFTER UPDATE ON pdpp_records WHEN ${recordChanged} AND ${hasOverlaySummary("NEW")} BEGIN ${markLatest("NEW")} END;
244
+ CREATE TEMP TRIGGER IF NOT EXISTS pdpp_replica_view_latest_record_delete AFTER DELETE ON pdpp_records WHEN ${hasOverlaySummary("OLD")} BEGIN ${markLatest("OLD")} END;
245
+ CREATE TEMP TRIGGER IF NOT EXISTS pdpp_replica_view_overlay_insert AFTER INSERT ON pdpp_replica_view BEGIN
246
+ INSERT INTO pdpp_replica_view_streams VALUES (1,NEW.instance,NEW.stream,NEW.live-NEW.base_live,NEW.present-NEW.base_live,NEW.pending,NULL,1)
247
+ ON CONFLICT(preview,instance,stream) DO UPDATE SET revision=revision+1,live_count=live_count+NEW.live-NEW.base_live,row_count=row_count+NEW.present-NEW.base_live,pending_count=pending_count+NEW.pending;
248
+ END;
249
+ CREATE TEMP TRIGGER IF NOT EXISTS pdpp_replica_view_overlay_update AFTER UPDATE ON pdpp_replica_view BEGIN
250
+ UPDATE pdpp_replica_view_streams SET revision=revision+1,live_count=live_count+(NEW.live-NEW.base_live)-(OLD.live-OLD.base_live),row_count=row_count+(NEW.present-NEW.base_live)-(OLD.present-OLD.base_live),pending_count=pending_count+NEW.pending-OLD.pending
251
+ WHERE preview=1 AND instance=OLD.instance AND stream=OLD.stream;
252
+ END;
253
+ CREATE TEMP TRIGGER IF NOT EXISTS pdpp_replica_view_overlay_delete AFTER DELETE ON pdpp_replica_view BEGIN
254
+ UPDATE pdpp_replica_view_streams SET revision=revision+1,live_count=live_count-(OLD.live-OLD.base_live),row_count=row_count-(OLD.present-OLD.base_live),pending_count=pending_count-OLD.pending
255
+ WHERE preview=1 AND instance=OLD.instance AND stream=OLD.stream;
256
+ END;
257
+ `);
258
+ // The local journal projection exists only for connections with a durable
259
+ // binding-conflict fact; the fact table is monotone.
260
+ if (hasBindingConflictFacts) {
261
+ const conflicted = (instance) => `EXISTS (SELECT 1 FROM pdpp_replica_binding_conflicts f WHERE f.instance=${instance})`;
262
+ db.exec(`
263
+ CREATE TABLE IF NOT EXISTS pdpp_replica_local_view (instance TEXT NOT NULL,stream TEXT NOT NULL,record_key TEXT NOT NULL,emitted_at TEXT NOT NULL,row_json TEXT NOT NULL,blob_id TEXT,pending INTEGER NOT NULL,deleted INTEGER NOT NULL,blob_json TEXT,sequence INTEGER NOT NULL,generation INTEGER NOT NULL,source_id TEXT NOT NULL,method TEXT NOT NULL,emitted_key TEXT GENERATED ALWAYS AS (pdpp_time_key(emitted_at)) VIRTUAL,PRIMARY KEY(instance,stream,record_key));
264
+ CREATE INDEX IF NOT EXISTS pdpp_replica_local_page ON pdpp_replica_local_view(instance,stream,emitted_key,record_key) WHERE deleted=0;
265
+ CREATE INDEX IF NOT EXISTS pdpp_replica_local_blob ON pdpp_replica_local_view(blob_id) WHERE deleted=0 AND blob_id IS NOT NULL;
266
+ CREATE TEMP TRIGGER IF NOT EXISTS pdpp_replica_local_journal AFTER INSERT ON pdpp_replica_own_writes
267
+ WHEN ${conflicted("NEW.connection_id")} AND NEW.writer_id=(SELECT writer_id FROM pdpp_installation_writer WHERE id=1) AND EXISTS (SELECT 1 FROM pdpp_instance_binding b WHERE b.instance=NEW.connection_id AND b.generation=NEW.generation AND b.source_id=NEW.source_id AND b.method=NEW.method AND b.source_id IS NOT NULL AND b.method IS NOT NULL AND b.deleted_at IS NULL) BEGIN
268
+ INSERT INTO pdpp_replica_local_view VALUES (NEW.connection_id,NEW.stream,NEW.record_key,json_extract(NEW.value_json,'$.emittedAt'),NEW.value_json,json_extract(NEW.value_json,'$.data.blob_ref.blob_id'),0,json_extract(NEW.value_json,'$.deleted'),NULL,NEW.sequence,NEW.generation,NEW.source_id,NEW.method)
269
+ ON CONFLICT(instance,stream,record_key) DO UPDATE SET emitted_at=excluded.emitted_at,row_json=excluded.row_json,blob_id=excluded.blob_id,deleted=excluded.deleted,sequence=excluded.sequence,generation=excluded.generation,source_id=excluded.source_id,method=excluded.method WHERE excluded.sequence>pdpp_replica_local_view.sequence;
270
+ END;
271
+ CREATE TEMP TRIGGER IF NOT EXISTS pdpp_replica_local_binding AFTER UPDATE ON pdpp_instance_binding
272
+ WHEN ${conflicted("NEW.instance")} AND (NEW.generation IS NOT OLD.generation OR NEW.source_id IS NOT OLD.source_id OR NEW.method IS NOT OLD.method OR NEW.deleted_at IS NOT OLD.deleted_at) BEGIN
273
+ DELETE FROM pdpp_replica_local_view WHERE instance=NEW.instance;
274
+ INSERT INTO pdpp_replica_local_view ${localProjection} AND j.connection_id=NEW.instance;
275
+ END;
276
+ CREATE TEMP TRIGGER IF NOT EXISTS pdpp_replica_local_conflict AFTER INSERT ON pdpp_replica_binding_conflicts BEGIN
277
+ DELETE FROM pdpp_replica_local_view WHERE instance=NEW.instance;
278
+ INSERT INTO pdpp_replica_local_view ${localProjection} AND j.connection_id=NEW.instance;
279
+ END;
280
+ `);
281
+ }
282
+ for (const [table, preview] of [
283
+ ["pdpp_records", 0],
284
+ ...(hasBindingConflictFacts
285
+ ? [["pdpp_replica_local_view", 2]]
286
+ : []),
287
+ ]) {
288
+ const latest = `SELECT emitted_at FROM ${table} WHERE instance=OLD.instance AND stream=OLD.stream AND deleted=0 ORDER BY emitted_at DESC,record_key DESC LIMIT 1`;
289
+ db.exec(`
290
+ CREATE TEMP TRIGGER IF NOT EXISTS ${table}_summary_insert AFTER INSERT ON ${table} BEGIN
291
+ INSERT INTO pdpp_replica_view_streams VALUES (${preview},NEW.instance,NEW.stream,1-NEW.deleted,1-NEW.deleted,0,CASE WHEN NEW.deleted=0 THEN NEW.emitted_at ELSE NULL END,1)
292
+ ON CONFLICT(preview,instance,stream) DO UPDATE SET revision=revision+1,
293
+ live_count=live_count+1-NEW.deleted,row_count=row_count+1-NEW.deleted,
294
+ last_updated=CASE WHEN NEW.deleted=0 AND (last_updated IS NULL OR NEW.emitted_at>last_updated) THEN NEW.emitted_at ELSE last_updated END;
295
+ END;
296
+ CREATE TEMP TRIGGER IF NOT EXISTS ${table}_summary_update AFTER UPDATE ON ${table} ${preview === 0 ? `WHEN ${recordChanged}` : ""} BEGIN
297
+ UPDATE pdpp_replica_view_streams SET revision=revision+1,live_count=live_count+OLD.deleted-NEW.deleted,
298
+ row_count=row_count+OLD.deleted-NEW.deleted,last_updated=(${latest}) WHERE preview=${preview} AND instance=OLD.instance AND stream=OLD.stream;
299
+ END;
300
+ CREATE TEMP TRIGGER IF NOT EXISTS ${table}_summary_delete AFTER DELETE ON ${table} BEGIN
301
+ UPDATE pdpp_replica_view_streams SET revision=revision+1,live_count=live_count-(1-OLD.deleted),row_count=row_count-(1-OLD.deleted),last_updated=(${latest})
302
+ WHERE preview=${preview} AND instance=OLD.instance AND stream=OLD.stream;
303
+ END;
304
+ `);
305
+ }
306
+ // The triggers above exist only on this connection. Writes made while
307
+ // replication was off (a plain open or a main build) fed none of them, so
308
+ // derive again from canonical rows what they maintain.
309
+ if (initialized)
310
+ rederive();
311
+ else
312
+ rederiveLocalView();
313
+ if (!initialized) {
314
+ // One-time rebuild. Thereafter persisted dirty queues also cover downtime/restarts.
315
+ // Only pending work, confirmed operation-ID variants and deleted bindings can
316
+ // differ from canonical, so only they are reconciled; no canonical row is copied.
317
+ db.exec(`
318
+ INSERT INTO pdpp_replica_view_streams SELECT 0,instance,stream,SUM(1-deleted),SUM(1-deleted),0,MAX(CASE WHEN deleted=0 THEN emitted_at END),1 FROM pdpp_records GROUP BY instance,stream;
319
+ INSERT OR IGNORE INTO pdpp_replica_view_dirty_operations
320
+ SELECT o.digest,o.operation_json FROM pdpp_replica_view_operations o
321
+ WHERE (o.writer,o.operation_id) IN (SELECT writer,operation_id FROM pdpp_replica_view_operations GROUP BY writer,operation_id HAVING COUNT(DISTINCT digest)>1);
322
+ INSERT OR IGNORE INTO pdpp_replica_view_dirty_connections SELECT instance FROM pdpp_instance_binding WHERE deleted_at IS NOT NULL;
323
+ `);
324
+ for (const row of db
325
+ .prepare("SELECT id,envelope_json FROM pdpp_replica_pending_batches WHERE state='pending'")
326
+ .all()) {
327
+ addPending(row.id, JSON.parse(row.envelope_json).operations);
328
+ }
329
+ db.exec("DELETE FROM pdpp_replica_view_schema");
330
+ db.prepare("INSERT INTO pdpp_replica_view_schema VALUES (?)").run(VIEW_SCHEMA);
331
+ }
332
+ markSynced();
333
+ seen = dataVersion();
334
+ }).immediate();
335
+ // ---- Following commits by other connections --------------------------------
336
+ // The maintenance triggers are TEMP triggers: they see this connection's writes
337
+ // only. A commit by another connection (a flag-off build, or main, running on
338
+ // the same file) fed none of them. `PRAGMA data_version` says exactly when such
339
+ // a commit happened, so every read and write that trusts the derived state
340
+ // runs through `snapshot` or `exclusive`, which first make that state match
341
+ // canonical rows again.
342
+ function dataVersion() {
343
+ dataVersionStatement ??= db.prepare("PRAGMA data_version").pluck();
344
+ return dataVersionStatement.get();
345
+ }
346
+ function maxChangeId() {
347
+ return db
348
+ .prepare("SELECT COALESCE(MAX(id),0) AS id FROM pdpp_record_changes")
349
+ .get().id;
350
+ }
351
+ function transitionTables() {
352
+ return (transitions ??= [
353
+ "pdpp_claude_transition",
354
+ "pdpp_strava_transition",
355
+ ].filter((name) => !!db
356
+ .prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?")
357
+ .get(name)));
358
+ }
359
+ /**
360
+ * Everything besides record history that can move canonical rows or decide
361
+ * their visibility without a history row: connection lifecycle and the
362
+ * archive moves of method transitions. A change here re-derives everything.
363
+ */
364
+ function structureSignature() {
365
+ return JSON.stringify([
366
+ db
367
+ .prepare("SELECT instance,generation,deleted_at,source_id,method,reset_clock FROM pdpp_instance_binding ORDER BY instance")
368
+ .all(),
369
+ ...transitionTables().map((table) => db.prepare(`SELECT * FROM ${table} ORDER BY instance`).all()),
370
+ ]);
371
+ }
372
+ /** Records that the derived state matches canonical rows as of now. */
373
+ function markSynced() {
374
+ const mark = maxChangeId();
375
+ const structure = structureSignature();
376
+ // Written only when it moved: an idle transaction writes no view row.
377
+ const moved = db
378
+ .prepare("UPDATE pdpp_replica_view_sync SET change_mark=?,structure=? WHERE id=1 AND (change_mark<>? OR structure<>?)")
379
+ .run(mark, structure, mark, structure).changes;
380
+ if (!moved)
381
+ db.prepare("INSERT OR IGNORE INTO pdpp_replica_view_sync VALUES (1,?,?)").run(mark, structure);
382
+ }
383
+ /** Re-derives the local journal projection unless it already matches. */
384
+ function rederiveLocalView() {
385
+ if (!hasBindingConflictFacts)
386
+ return;
387
+ const projected = `SELECT * FROM (${localProjection} AND j.connection_id IN (SELECT instance FROM pdpp_replica_binding_conflicts))`;
388
+ const stored = "SELECT instance,stream,record_key,emitted_at,row_json,blob_id,pending,deleted,blob_json,sequence,generation,source_id,method FROM pdpp_replica_local_view";
389
+ const drifted = db
390
+ .prepare(`SELECT 1 FROM (${stored} EXCEPT ${projected}) UNION ALL SELECT 1 FROM (${projected} EXCEPT ${stored}) LIMIT 1`)
391
+ .get();
392
+ if (drifted)
393
+ db.exec(`DELETE FROM pdpp_replica_local_view; INSERT INTO pdpp_replica_local_view ${localProjection} AND j.connection_id IN (SELECT instance FROM pdpp_replica_binding_conflicts);`);
394
+ }
395
+ /** Re-derives every projection from canonical rows without invalidating unchanged cursors. */
396
+ function rederive() {
397
+ rederiveLocalView();
398
+ db.exec(`
399
+ UPDATE pdpp_replica_view_streams SET revision=revision+1;
400
+ INSERT INTO pdpp_replica_view_streams SELECT 0,instance,stream,SUM(1-deleted),SUM(1-deleted),0,MAX(CASE WHEN deleted=0 THEN emitted_at END),1 FROM pdpp_records WHERE true GROUP BY instance,stream
401
+ ON CONFLICT(preview,instance,stream) DO UPDATE SET live_count=excluded.live_count,row_count=excluded.row_count,last_updated=excluded.last_updated
402
+ WHERE live_count IS NOT excluded.live_count OR row_count IS NOT excluded.row_count OR last_updated IS NOT excluded.last_updated;
403
+ UPDATE pdpp_replica_view_streams SET live_count=0,row_count=0,last_updated=NULL
404
+ WHERE preview=0 AND (live_count<>0 OR row_count<>0 OR last_updated IS NOT NULL)
405
+ AND NOT EXISTS (SELECT 1 FROM pdpp_records r WHERE r.instance=pdpp_replica_view_streams.instance AND r.stream=pdpp_replica_view_streams.stream);
406
+ INSERT OR IGNORE INTO pdpp_replica_view_dirty_connections SELECT DISTINCT instance FROM pdpp_replica_view;
407
+ INSERT OR IGNORE INTO pdpp_replica_view_dirty_connections SELECT DISTINCT instance FROM pdpp_replica_view_pending;
408
+ INSERT OR IGNORE INTO pdpp_replica_view_dirty_connections SELECT DISTINCT instance FROM pdpp_replica_view_conflicts;
409
+ INSERT OR IGNORE INTO pdpp_replica_view_dirty_latest SELECT instance,stream FROM pdpp_replica_view_streams WHERE preview=1;
410
+ `);
411
+ }
412
+ /**
413
+ * Another connection committed record writes (history rows `from` < id <=
414
+ * `to`): re-derive the streams and keys they name. Every record write
415
+ * appends a history row in the same transaction, so the window names every
416
+ * changed key.
417
+ */
418
+ function applyChangeWindow(from, to) {
419
+ db.prepare(`INSERT INTO pdpp_replica_view_dirty_keys SELECT DISTINCT c.instance,c.stream,c.record_key FROM pdpp_record_changes c WHERE c.id>? AND c.id<=? AND ${touches("c.instance", "c.stream", "c.record_key")} AND ${notDirtyKey("c")}`).run(from, to);
420
+ const changed = db.prepare("SELECT DISTINCT instance,stream FROM pdpp_record_changes WHERE id>? AND id<=?");
421
+ const bump = db.prepare("UPDATE pdpp_replica_view_streams SET revision=revision+1 WHERE preview<>0 AND instance=@instance AND stream=@stream");
422
+ const summarize = db.prepare(`INSERT INTO pdpp_replica_view_streams SELECT 0,@instance,@stream,live,live,0,(SELECT emitted_at FROM pdpp_records WHERE instance=@instance AND stream=@stream AND deleted=0 ORDER BY emitted_at DESC,record_key DESC LIMIT 1),1
423
+ FROM (SELECT COUNT(*) AS live FROM pdpp_records WHERE instance=@instance AND stream=@stream AND deleted=0) WHERE true
424
+ ON CONFLICT(preview,instance,stream) DO UPDATE SET revision=revision+1,live_count=excluded.live_count,row_count=excluded.row_count,last_updated=excluded.last_updated`);
425
+ const latest = db.prepare("INSERT OR IGNORE INTO pdpp_replica_view_dirty_latest SELECT instance,stream FROM pdpp_replica_view_streams WHERE preview=1 AND instance=@instance AND stream=@stream");
426
+ for (const row of changed.all(from, to)) {
427
+ summarize.run(row);
428
+ bump.run(row);
429
+ latest.run(row);
430
+ }
431
+ }
432
+ /** Blob bytes arrive and leave without a history row; revisit what depends on them. */
433
+ function revisitBlobDependents() {
434
+ // Each branch repeats its table's partial index predicate so the index
435
+ // serves it; the operations view would scan every retained operation.
436
+ const retained = (table) => `SELECT ${expression("instance")} AS instance,${expression("stream")} AS stream,${expression("record_key")} AS record_key FROM ${table} WHERE ${expression("blob_id")} IS NOT NULL AND ${expression("kind")}='upsert' AND ${expression("record_key")} IS NOT NULL`;
437
+ db.exec(`INSERT INTO pdpp_replica_view_dirty_keys SELECT DISTINCT o.instance,o.stream,o.record_key FROM (
438
+ ${retained("pdpp_replica_operations")}
439
+ UNION ALL ${retained("pdpp_replica_received")}
440
+ UNION ALL SELECT instance,stream,record_key FROM pdpp_replica_view_pending WHERE blob_id IS NOT NULL AND kind='upsert' AND record_key IS NOT NULL
441
+ ) o WHERE ${touches("o.instance", "o.stream", "o.record_key")} AND ${notDirtyKey("o")}`);
442
+ }
443
+ /** Must run in a write transaction, after nobody else can commit. */
444
+ function followOtherConnections() {
445
+ const marked = db
446
+ .prepare("SELECT change_mark,structure FROM pdpp_replica_view_sync")
447
+ .get();
448
+ const upTo = maxChangeId();
449
+ if (!marked ||
450
+ marked.structure !== structureSignature() ||
451
+ upTo < marked.change_mark)
452
+ rederive();
453
+ else if (upTo > marked.change_mark)
454
+ applyChangeWindow(marked.change_mark, upTo);
455
+ revisitBlobDependents();
456
+ markSynced();
457
+ flush();
458
+ }
459
+ /** Brings the derived state up to date unless it already is. Never inside a transaction. */
460
+ function sync() {
461
+ if (db.inTransaction || dataVersion() === seen)
462
+ return;
463
+ seen = db
464
+ .transaction(() => {
465
+ const version = dataVersion();
466
+ followOtherConnections();
467
+ return version;
468
+ })
469
+ .immediate();
470
+ }
471
+ /** A public read: it answers from derived state that matches canonical rows. */
472
+ const reading = (read) => (...args) => snapshot(() => read(...args));
473
+ /**
474
+ * Whether the derived state matches the snapshot this transaction reads. The
475
+ * first read fixes the snapshot of a deferred transaction, and the version
476
+ * is stable from then on.
477
+ */
478
+ function matchesSnapshot() {
479
+ (syncProbe ??= db.prepare("SELECT 1 FROM pdpp_replica_view_sync")).get();
480
+ return dataVersion() === seen;
481
+ }
482
+ /**
483
+ * Inside a transaction the view cannot re-derive. A transaction opened
484
+ * through `snapshot` or `exclusive` matches by construction; any other must
485
+ * show it does, or the call fails rather than answer from stale summaries.
486
+ */
487
+ function assertNestedMatches() {
488
+ if (!matchesSnapshot())
489
+ throw new DerivedStateStale();
490
+ }
491
+ /**
492
+ * Runs `fn` in a transaction that starts from a derived state matching
493
+ * canonical rows. Another connection can commit between `sync` and the
494
+ * transaction starting; the transaction then notices, rolls back untouched
495
+ * and tries again.
496
+ */
497
+ function guarded(begin, fn, afterwards) {
498
+ for (let attempt = 1;; attempt++) {
499
+ sync();
500
+ try {
501
+ return db
502
+ .transaction(() => {
503
+ if (!matchesSnapshot())
504
+ throw new DerivedStateMoved();
505
+ const result = fn();
506
+ afterwards?.();
507
+ return result;
508
+ })[begin]();
509
+ }
510
+ catch (error) {
511
+ if (!(error instanceof DerivedStateMoved) || attempt >= SYNC_ATTEMPTS)
512
+ throw error;
513
+ }
514
+ }
515
+ }
516
+ /** A read transaction over derived state that matches canonical rows. */
517
+ function snapshot(read) {
518
+ if (!db.inTransaction)
519
+ return guarded("deferred", read);
520
+ assertNestedMatches();
521
+ return read();
522
+ }
523
+ /**
524
+ * A write transaction (`BEGIN IMMEDIATE`) over derived state that matches
525
+ * canonical rows. Everything this connection wrote is derived by the
526
+ * triggers, so it ends by recording the new horizon. Nested, it is a
527
+ * savepoint of the caller's transaction.
528
+ */
529
+ function exclusive(write) {
530
+ // Receiver imports and local captures both enter here: neither trips
531
+ // the replication door's fence.
532
+ const captured = () => captureWrites(db, write);
533
+ if (!db.inTransaction)
534
+ return guarded("immediate", captured, markSynced);
535
+ assertNestedMatches();
536
+ return db.transaction(captured).immediate();
537
+ }
538
+ function markOperation(digest, json) {
539
+ db.prepare("INSERT OR IGNORE INTO pdpp_replica_view_dirty_operations VALUES (?,?)").run(digest, json);
540
+ }
541
+ function addPending(batchId, operations) {
542
+ for (const op of operations) {
543
+ const json = canonicalJson(op);
544
+ const digest = replicaOperationDigest(op);
545
+ const inserted = db
546
+ .prepare("INSERT OR IGNORE INTO pdpp_replica_view_pending VALUES (?,?,?,?,?,?,?,?,?,?)")
547
+ .run(batchId, digest, json, op.connectionId, "declaration" in op ? op.declaration.stream : null, "recordKey" in op ? op.recordKey : null, op.kind, op.revision.writerId, op.revision.operationId, op.kind === "blob-claim"
548
+ ? op.blob.blobId
549
+ : op.kind === "upsert"
550
+ ? (op.blobs[0]?.blobId ?? null)
551
+ : null).changes;
552
+ if (inserted)
553
+ markOperation(digest, json);
554
+ }
555
+ }
556
+ function removePending(batchId) {
557
+ for (const row of db
558
+ .prepare("SELECT digest,operation_json FROM pdpp_replica_view_pending WHERE batch_id=?")
559
+ .all(batchId))
560
+ markOperation(row.digest, row.operation_json);
561
+ db.prepare("DELETE FROM pdpp_replica_view_pending WHERE batch_id=?").run(batchId);
562
+ }
563
+ /** Retained and pending evidence matching `where`, merged by operation value. */
564
+ function evidence(where, ...args) {
565
+ const merged = new Map();
566
+ for (const row of db
567
+ .prepare(`SELECT operation_json,confirmed,pending FROM pdpp_replica_view_operations WHERE ${where}`)
568
+ .all(...args)) {
569
+ const entry = merged.get(row.operation_json) ?? {
570
+ op: JSON.parse(row.operation_json),
571
+ confirmed: false,
572
+ pending: 0,
573
+ };
574
+ entry.confirmed ||= row.confirmed === 1;
575
+ entry.pending += row.pending;
576
+ merged.set(row.operation_json, entry);
577
+ }
578
+ return merged;
579
+ }
580
+ const ops = (entries) => [...entries.values()].map((entry) => entry.op);
581
+ function dirtyKey(instance, stream, key) {
582
+ db.prepare("INSERT OR IGNORE INTO pdpp_replica_view_dirty_keys VALUES (?,?,?)").run(instance, stream, key);
583
+ }
584
+ /**
585
+ * Pending lifecycle, pending blob claims and operation-ID variants can change
586
+ * every key of a connection. A compatible registration already in force cannot.
587
+ */
588
+ function connectionInvolvement(instance) {
589
+ const conflicts = !!db
590
+ .prepare("SELECT 1 FROM pdpp_replica_view_conflicts WHERE instance=?")
591
+ .get(instance);
592
+ const pendingLifecycle = ops(evidence(`instance=? AND kind IN ${LIFECYCLE} AND pending=1`, instance));
593
+ const registrations = pendingLifecycle.some((op) => op.kind === "connection-register")
594
+ ? ops(evidence("instance=? AND kind='connection-register' AND confirmed=1", instance))
595
+ : [];
596
+ const lifecycle = pendingLifecycle.some((op) => op.kind !== "connection-register" ||
597
+ !registrations.some((other) => other.owner === op.owner &&
598
+ other.sourceId === op.sourceId &&
599
+ other.methodId === op.methodId));
600
+ return { all: conflicts || lifecycle };
601
+ }
602
+ function flush() {
603
+ if (!db.inTransaction)
604
+ return db.transaction(flush).immediate();
605
+ const dirtyOps = db
606
+ .prepare("SELECT operation_json FROM pdpp_replica_view_dirty_operations")
607
+ .all().map((row) => JSON.parse(row.operation_json));
608
+ const connections = new Set(db
609
+ .prepare("SELECT instance FROM pdpp_replica_view_dirty_connections")
610
+ .all().map((row) => row.instance));
611
+ for (const op of dirtyOps) {
612
+ const { writerId, operationId } = op.revision;
613
+ const previous = db
614
+ .prepare("SELECT instance FROM pdpp_replica_view_conflicts WHERE writer=? AND operation_id=?")
615
+ .all(writerId, operationId);
616
+ const variants = db
617
+ .prepare("SELECT DISTINCT digest,instance FROM pdpp_replica_view_operations WHERE writer=? AND operation_id=?")
618
+ .all(writerId, operationId);
619
+ db.prepare("DELETE FROM pdpp_replica_view_conflicts WHERE writer=? AND operation_id=?").run(writerId, operationId);
620
+ for (const row of previous)
621
+ connections.add(row.instance);
622
+ if (variants.length > 1)
623
+ for (const variant of variants) {
624
+ connections.add(variant.instance);
625
+ db.prepare("INSERT OR IGNORE INTO pdpp_replica_view_conflicts VALUES (?,?,?,?)").run(writerId, operationId, variant.instance, variant.digest);
626
+ }
627
+ if (op.kind === "upsert" || op.kind === "delete")
628
+ dirtyKey(op.connectionId, op.declaration.stream, op.recordKey);
629
+ else if (op.kind === "blob-claim") {
630
+ for (const dependent of ops(evidence("blob_id=? AND kind='upsert'", op.blob.blobId)))
631
+ if (dependent.kind === "upsert")
632
+ dirtyKey(dependent.connectionId, dependent.declaration.stream, dependent.recordKey);
633
+ }
634
+ else if (op.kind === "connection-register") {
635
+ // A compatible registration already in force does not change any key.
636
+ // Operation-ID equivocation was handled globally above, before this shortcut.
637
+ const json = canonicalJson(op);
638
+ const established = [
639
+ ...evidence("instance=? AND kind='connection-register'", op.connectionId),
640
+ ].some(([other, entry]) => (other !== json ||
641
+ entry.pending > 1 ||
642
+ (entry.confirmed && entry.pending > 0)) &&
643
+ entry.op.owner === op.owner &&
644
+ entry.op.sourceId === op.sourceId &&
645
+ entry.op.methodId === op.methodId);
646
+ if (!established)
647
+ connections.add(op.connectionId);
648
+ }
649
+ else if (op.kind.startsWith("connection-"))
650
+ connections.add(op.connectionId);
651
+ }
652
+ for (const instance of connections) {
653
+ // Lifecycle/equivocation changes can affect every key, once at WRITE time.
654
+ // Otherwise only existing overlay and pending keys can change.
655
+ db.prepare("INSERT OR IGNORE INTO pdpp_replica_view_dirty_keys SELECT instance,stream,record_key FROM pdpp_replica_view WHERE instance=?").run(instance);
656
+ db.prepare("INSERT OR IGNORE INTO pdpp_replica_view_dirty_keys SELECT instance,stream,record_key FROM pdpp_replica_view_pending WHERE instance=? AND record_key IS NOT NULL").run(instance);
657
+ const deleted = !!db
658
+ .prepare("SELECT 1 FROM pdpp_instance_binding WHERE instance=? AND deleted_at IS NOT NULL")
659
+ .get(instance);
660
+ if (deleted || connectionInvolvement(instance).all) {
661
+ db.prepare("INSERT OR IGNORE INTO pdpp_replica_view_dirty_keys SELECT instance,stream,record_key FROM pdpp_records WHERE instance=?").run(instance);
662
+ db.prepare("INSERT OR IGNORE INTO pdpp_replica_view_dirty_keys SELECT DISTINCT instance,stream,record_key FROM pdpp_replica_view_operations WHERE instance=? AND record_key IS NOT NULL").run(instance);
663
+ }
664
+ }
665
+ const involvement = new Map();
666
+ // Cursor revisions advance in the summary triggers, only for rows that change.
667
+ // A deleted or conflicted connection queues every canonical key. Take the
668
+ // queue a page at a time, in key order, so memory does not grow with it.
669
+ const page = db.prepare("SELECT * FROM pdpp_replica_view_dirty_keys WHERE (instance,stream,record_key)>(@instance,@stream,@record_key) ORDER BY instance,stream,record_key LIMIT 1000");
670
+ let after = { instance: "", stream: "", record_key: "" };
671
+ for (;;) {
672
+ const rows = page.all(after);
673
+ for (const row of rows) {
674
+ if (!involvement.has(row.instance))
675
+ involvement.set(row.instance, connectionInvolvement(row.instance));
676
+ reconcileKey(row.instance, row.stream, row.record_key, involvement.get(row.instance));
677
+ }
678
+ if (rows.length < 1000)
679
+ break;
680
+ after = rows[rows.length - 1];
681
+ }
682
+ // The scan below can step over every masked canonical row of a stream,
683
+ // so it runs here, once per changed stream, and never in a read.
684
+ for (const row of db
685
+ .prepare("SELECT instance,stream FROM pdpp_replica_view_dirty_latest")
686
+ .all())
687
+ db.prepare(`UPDATE pdpp_replica_view_streams SET last_updated=(${previewLatestSql}) WHERE preview=1 AND instance=@instance AND stream=@stream`).run(row);
688
+ db.exec("DELETE FROM pdpp_replica_view_dirty_operations; DELETE FROM pdpp_replica_view_dirty_keys; DELETE FROM pdpp_replica_view_dirty_connections; DELETE FROM pdpp_replica_view_dirty_latest;");
689
+ const signature = visibilitySignature();
690
+ db.prepare(`UPDATE pdpp_replica_visibility_epoch
691
+ SET epoch=epoch+CASE WHEN signature<>'' AND signature<>? THEN 1 ELSE 0 END,
692
+ signature=? WHERE id=1`).run(signature, signature);
693
+ }
694
+ /**
695
+ * Latest owner-preview emitted_at of one stream: present overlay rows and
696
+ * canonical live rows without an overlay row. Quarantine is not applied;
697
+ * a read with quarantined keys in the stream recomputes it instead.
698
+ */
699
+ const previewLatestSql = `SELECT MAX(t) FROM (
700
+ SELECT (SELECT r.emitted_at FROM pdpp_records r WHERE r.instance=@instance AND r.stream=@stream AND r.deleted=0 AND NOT EXISTS (SELECT 1 FROM pdpp_replica_view v WHERE v.instance=r.instance AND v.stream=r.stream AND v.record_key=r.record_key) ORDER BY r.emitted_at DESC,r.record_key DESC LIMIT 1) AS t
701
+ UNION ALL SELECT (SELECT v.emitted_at FROM pdpp_replica_view v WHERE v.instance=@instance AND v.stream=@stream AND v.present=1 ORDER BY v.emitted_at DESC,v.record_key DESC LIMIT 1))`;
702
+ function canonical(row) {
703
+ return row.deleted
704
+ ? undefined
705
+ : {
706
+ instance: row.instance,
707
+ stream: row.stream,
708
+ recordKey: row.record_key,
709
+ data: JSON.parse(row.data),
710
+ version: row.version,
711
+ emittedAt: row.emitted_at,
712
+ deleted: false,
713
+ };
714
+ }
715
+ /** Whether pending evidence or a variant reaches this key's merge inputs. */
716
+ function involved(instance, stream, key, connection) {
717
+ if (connection.all)
718
+ return true;
719
+ if (db
720
+ .prepare("SELECT 1 FROM pdpp_replica_view_pending WHERE instance=? AND stream=? AND record_key=?")
721
+ .get(instance, stream, key))
722
+ return true;
723
+ return !!db
724
+ .prepare(`SELECT 1 WHERE ${pendingBlobReference("?", "?", "?")}`)
725
+ .get(instance, stream, key);
726
+ }
727
+ function reconcileKey(instance, stream, key, connection) {
728
+ const current = db
729
+ .prepare("SELECT * FROM pdpp_records WHERE instance=? AND stream=? AND record_key=?")
730
+ .get(instance, stream, key);
731
+ const local = db
732
+ .prepare("SELECT source_id,generation,deleted_at FROM pdpp_instance_binding WHERE instance=?")
733
+ .get(instance);
734
+ const base = current && canonical(current);
735
+ const baseGeneration = local?.generation ?? 0;
736
+ let row = base;
737
+ let generation = baseGeneration;
738
+ let blobId = row ? current.blob_id : null;
739
+ let blobMeta;
740
+ if (local?.source_id &&
741
+ !local.deleted_at &&
742
+ involved(instance, stream, key, connection)) {
743
+ const evidenceRows = evidence("instance=? AND stream=? AND record_key=?", instance, stream, key);
744
+ // The conflict index carries global operation-ID variants, including other connections.
745
+ const conflictOps = [];
746
+ for (const pair of db
747
+ .prepare("SELECT DISTINCT c2.writer,c2.operation_id FROM pdpp_replica_view_conflicts c JOIN pdpp_replica_view_conflicts c2 ON c.writer=c2.writer AND c.operation_id=c2.operation_id WHERE c.instance=?")
748
+ .all(instance))
749
+ conflictOps.push(...ops(evidence("writer=? AND operation_id=?", pair.writer, pair.operation_id)));
750
+ const lifecycleRows = evidence(`instance=? AND kind IN ${LIFECYCLE}`, instance);
751
+ const lifecycle = ops(lifecycleRows);
752
+ const blobIds = [
753
+ ...new Set(ops(evidenceRows).flatMap((op) => op.kind === "upsert" ? op.blobs.map((blob) => blob.blobId) : [])),
754
+ ];
755
+ const claims = blobIds.length
756
+ ? ops(evidence(`instance=? AND kind='blob-claim' AND blob_id IN (${blobIds.map(() => "?").join(",")})`, instance, ...blobIds))
757
+ : [];
758
+ const all = [
759
+ ...ops(evidenceRows),
760
+ ...lifecycle,
761
+ ...claims,
762
+ ...conflictOps,
763
+ ];
764
+ const plan = planReplicaMerge({ owner: deps.owner, declarations: deps.declarations() }, all);
765
+ const blocked = plan.conflicts.some((issue) => ![
766
+ "append_only_content_conflict",
767
+ "incompatible_record_declaration",
768
+ ].includes(issue.reason) &&
769
+ issue.operations.some((op) => op.connectionId === instance));
770
+ const deletion = lifecycle.find((op) => op.kind === "connection-delete");
771
+ const effective = Math.max(local.generation, 1, ...lifecycle
772
+ .filter((op) => op.kind === "connection-reset")
773
+ .map((op) => op.generation));
774
+ const missingReset = plan.pending.some((issue) => issue.reason === "missing_generation_reset" &&
775
+ issue.operations.some((op) => op.kind === "connection-reset" && op.connectionId === instance));
776
+ const provisionalLifecycle = [...lifecycleRows.values()].some(({ op, confirmed, pending }) => !confirmed &&
777
+ pending > 0 &&
778
+ (op.kind === "connection-delete" ||
779
+ (op.kind === "connection-reset" &&
780
+ op.generation > local.generation)));
781
+ const removal = (emittedAt, deletedAt) => ({
782
+ instance,
783
+ stream,
784
+ recordKey: key,
785
+ version: current?.version ?? 0,
786
+ emittedAt,
787
+ deletedAt,
788
+ deleted: true,
789
+ data: {},
790
+ ...replicaPendingMetadata(),
791
+ });
792
+ if (blocked) {
793
+ row = undefined;
794
+ blobId = null;
795
+ }
796
+ else if (deletion || (effective > local.generation && !missingReset)) {
797
+ // Recompute the value withdrawn by this lifecycle transition. In particular,
798
+ // pending-only records have no canonical row to use as a removal marker.
799
+ const before = planReplicaMerge({ owner: deps.owner, declarations: deps.declarations() }, all.filter((op) => op.kind !== "connection-delete" &&
800
+ !(op.kind === "connection-reset" &&
801
+ op.generation === effective &&
802
+ !deletion)));
803
+ const prior = before.records.find((record) => record.identity.connectionId === instance &&
804
+ record.identity.stream === stream &&
805
+ record.identity.recordKey === key)?.winner;
806
+ const withdrawn = prior?.kind === "upsert" && deps.blobsValid(prior, all)
807
+ ? prior
808
+ : undefined;
809
+ row =
810
+ provisionalLifecycle && (withdrawn || row)
811
+ ? removal(withdrawn?.emittedAt ?? row.emittedAt, withdrawn?.emittedAt ?? row.emittedAt)
812
+ : undefined;
813
+ // The marker withdraws the value of the generation being left.
814
+ generation = withdrawn?.generation ?? local.generation;
815
+ blobId = null;
816
+ }
817
+ if (!blocked && !deletion) {
818
+ const record = plan.records.find((r) => r.identity.connectionId === instance &&
819
+ r.identity.stream === stream &&
820
+ r.identity.recordKey === key &&
821
+ r.identity.generation === effective);
822
+ if (record) {
823
+ generation = effective;
824
+ const winner = record.winner;
825
+ const state = winner && evidenceRows.get(canonicalJson(winner));
826
+ const pending = !!state && !state.confirmed && state.pending > 0;
827
+ if (!winner) {
828
+ row = undefined;
829
+ blobId = null;
830
+ }
831
+ else if (winner.kind === "delete") {
832
+ row = pending
833
+ ? removal(winner.emittedAt, winner.deletedAt)
834
+ : undefined;
835
+ blobId = null;
836
+ }
837
+ else if (!deps.blobsValid(winner, all)) {
838
+ row = undefined;
839
+ blobId = null;
840
+ }
841
+ else {
842
+ // A confirmed winner is still pending when its content is not the
843
+ // canonical row, for example when its bytes arrive only as pending
844
+ // evidence (oracle D3).
845
+ const differs = canonicalJson(winner.data) !== canonicalJson(base?.data ?? null);
846
+ row = {
847
+ instance,
848
+ stream,
849
+ recordKey: key,
850
+ data: winner.data,
851
+ version: current?.version ?? 0,
852
+ emittedAt: winner.emittedAt,
853
+ deleted: false,
854
+ ...(pending || differs ? replicaPendingMetadata() : {}),
855
+ };
856
+ const descriptor = winner.blobs[0];
857
+ blobId = descriptor?.blobId ?? null;
858
+ blobMeta = descriptor
859
+ ? {
860
+ blobId: descriptor.blobId,
861
+ mimeType: descriptor.mimeType,
862
+ sizeBytes: descriptor.sizeBytes,
863
+ sha256: descriptor.sha256.slice(7),
864
+ }
865
+ : undefined;
866
+ }
867
+ }
868
+ }
869
+ }
870
+ else if (local?.deleted_at) {
871
+ row = undefined;
872
+ blobId = null;
873
+ }
874
+ writeOverlay(instance, stream, key, current, base, row, blobId, blobMeta,
875
+ // A hidden key hides the canonical value, of the binding's generation.
876
+ row ? generation : baseGeneration, baseGeneration);
877
+ }
878
+ /** Persist the difference from canonical, or nothing when there is none. */
879
+ function writeOverlay(instance, stream, key, current, base, row, blobId, blobMeta, generation, baseGeneration) {
880
+ const same = row === undefined
881
+ ? base === undefined
882
+ : base !== undefined &&
883
+ canonicalJson(row) === canonicalJson(base) &&
884
+ blobId === current.blob_id &&
885
+ generation === baseGeneration;
886
+ if (same) {
887
+ db.prepare("DELETE FROM pdpp_replica_view WHERE instance=? AND stream=? AND record_key=?").run(instance, stream, key);
888
+ return;
889
+ }
890
+ const overlay = row
891
+ ? {
892
+ emitted_at: row.emittedAt,
893
+ row_json: JSON.stringify(row),
894
+ blob_id: blobId,
895
+ blob_json: blobId && blobMeta ? JSON.stringify(blobMeta) : null,
896
+ present: 1,
897
+ live: row.deleted ? 0 : 1,
898
+ pending: row.visibility === "pending" ? 1 : 0,
899
+ base_live: base ? 1 : 0,
900
+ generation,
901
+ }
902
+ : {
903
+ // A hidden key: canonical shows it, the owner preview must not.
904
+ emitted_at: base.emittedAt,
905
+ row_json: null,
906
+ blob_id: null,
907
+ blob_json: null,
908
+ present: 0,
909
+ live: 0,
910
+ pending: 0,
911
+ base_live: 1,
912
+ generation,
913
+ };
914
+ // An unchanged row is not rewritten, so it advances no cursor revision.
915
+ db.prepare(`INSERT INTO pdpp_replica_view VALUES (@instance,@stream,@key,@emitted_at,@row_json,@blob_id,@blob_json,@present,@live,@pending,@base_live,@generation)
916
+ ON CONFLICT(instance,stream,record_key) DO UPDATE SET emitted_at=excluded.emitted_at,row_json=excluded.row_json,blob_id=excluded.blob_id,blob_json=excluded.blob_json,present=excluded.present,live=excluded.live,pending=excluded.pending,base_live=excluded.base_live,generation=excluded.generation
917
+ WHERE emitted_at IS NOT excluded.emitted_at OR row_json IS NOT excluded.row_json OR blob_id IS NOT excluded.blob_id OR blob_json IS NOT excluded.blob_json OR present IS NOT excluded.present OR live IS NOT excluded.live OR pending IS NOT excluded.pending OR base_live IS NOT excluded.base_live OR generation IS NOT excluded.generation`).run({ instance, stream, key, ...overlay });
918
+ }
919
+ function connectionVisibility(instance) {
920
+ return replicaConnectionVisibility(hasBindingConflictFacts &&
921
+ !!db
922
+ .prepare("SELECT 1 FROM pdpp_replica_binding_conflicts WHERE instance=?")
923
+ .get(instance));
924
+ }
925
+ function readBranch(instance, reader, preview) {
926
+ const conflict = connectionVisibility(instance);
927
+ if (conflict)
928
+ return replicaStateVisible(conflict.state, reader, undefined, "local")
929
+ ? "local"
930
+ : "hidden";
931
+ return preview &&
932
+ replicaStateVisible("pending", reader, deps.visibilityPolicy)
933
+ ? "preview"
934
+ : "canonical";
935
+ }
936
+ const unquarantined = (alias = "r") => deps.unquarantinedRecordSql(alias);
937
+ // Overlay and local-projection rows carry the generation of their value.
938
+ const ownUnquarantined = (alias = "r") => deps.unquarantinedRecordSql(alias, `${alias}.generation`);
939
+ const overlaid = (alias = "r") => `EXISTS (SELECT 1 FROM pdpp_replica_view v WHERE v.instance=${alias}.instance AND v.stream=${alias}.stream AND v.record_key=${alias}.record_key)`;
940
+ const notOverlaid = (alias = "r") => `NOT ${overlaid(alias)}`;
941
+ const canonicalJsonColumn = (alias = "r") => `json_object('instance',${alias}.instance,'stream',${alias}.stream,'recordKey',${alias}.record_key,'data',json(${alias}.data),'version',${alias}.version,'emittedAt',${alias}.emitted_at,'deleted',json('false'))`;
942
+ /**
943
+ * Row sources of one mode, each an indexed keyset over (emitted_at, record_key).
944
+ * Preview merges canonical rows with present overlay rows. A canonical row
945
+ * that has an overlay row is `masked`: the overlay row replaces it. Masked
946
+ * rows stay in the keyset so a page can step over them within its window.
947
+ */
948
+ function sources(mode, alias = "r") {
949
+ const source = (table, json, origin, filter, masked = "0") => ({
950
+ table,
951
+ columns: `${alias}.instance,${alias}.stream,${alias}.record_key,${alias}.emitted_at,${alias}.emitted_key,${json} AS row_json,'${origin}' AS origin,(${masked}) AS masked`,
952
+ filter,
953
+ masked,
954
+ });
955
+ const canonicalSource = (masked) => source("pdpp_records", canonicalJsonColumn(alias), "canonical", `${unquarantined(alias)} AND ${alias}.deleted=0`, masked);
956
+ if (mode === "canonical")
957
+ return [canonicalSource()];
958
+ if (mode === "local")
959
+ return [
960
+ source("pdpp_replica_local_view", `${alias}.row_json`, "local", `${ownUnquarantined(alias)} AND ${alias}.deleted=0 AND ${localIdentitySql(alias)}`),
961
+ ];
962
+ return [
963
+ canonicalSource(overlaid(alias)),
964
+ source("pdpp_replica_view", `${alias}.row_json`, "preview", `${ownUnquarantined(alias)} AND ${alias}.present=1`),
965
+ ];
966
+ }
967
+ function visibleRow(row) {
968
+ const value = JSON.parse(row.row_json);
969
+ return row.origin === "local" && !value.deleted
970
+ ? { ...value, ...replicaConflictMetadata() }
971
+ : value;
972
+ }
973
+ // Summary tables whose revisions bind a cursor in each read mode.
974
+ const revisionTables = {
975
+ canonical: [0],
976
+ preview: [0, 1],
977
+ local: [2],
978
+ hidden: [],
979
+ };
980
+ /**
981
+ * A cursor binds, per connection: read mode, reset fence, the host integrity
982
+ * epoch when the store provides one, and the revisions of this stream's rows
983
+ * in that mode. Other streams and hidden pending churn never move it.
984
+ */
985
+ function cursorRevision(stream, ids, modes) {
986
+ return JSON.stringify([
987
+ CURSOR_FORMAT,
988
+ ...ids.map((instance) => {
989
+ const mode = modes.get(instance);
990
+ const fence = db
991
+ .prepare("SELECT reset_clock FROM pdpp_instance_binding WHERE instance=?")
992
+ .get(instance)?.reset_clock ?? 0;
993
+ const revisions = revisionTables[mode].map((table) => db
994
+ .prepare("SELECT revision FROM pdpp_replica_view_streams WHERE preview=? AND instance=? AND stream=?")
995
+ .get(table, instance, stream)?.revision ?? 0);
996
+ return [
997
+ instance,
998
+ mode,
999
+ fence,
1000
+ deps.integrityEpoch?.(instance) ?? null,
1001
+ ...revisions,
1002
+ ];
1003
+ }),
1004
+ ]);
1005
+ }
1006
+ function cursorVisibilityEpoch(stream, ids, reader, preview) {
1007
+ const orderedIds = [...new Set(ids)].sort();
1008
+ const modes = new Map(orderedIds.map((instance) => [
1009
+ instance,
1010
+ readBranch(instance, reader, preview),
1011
+ ]));
1012
+ return cursorRevision(stream, orderedIds, modes);
1013
+ }
1014
+ function getRecord(instance, stream, key, reader = "owner", preview = true) {
1015
+ const mode = readBranch(instance, reader, preview);
1016
+ if (mode === "hidden")
1017
+ return undefined;
1018
+ if (mode === "preview") {
1019
+ const overlay = db
1020
+ .prepare(`SELECT r.row_json,r.present,(${ownUnquarantined()}) AS visible FROM pdpp_replica_view r WHERE r.instance=? AND r.stream=? AND r.record_key=?`)
1021
+ .get(instance, stream, key);
1022
+ if (overlay)
1023
+ return overlay.present && overlay.visible
1024
+ ? JSON.parse(overlay.row_json)
1025
+ : undefined;
1026
+ }
1027
+ const source = sources(mode === "preview" ? "canonical" : mode)[0];
1028
+ const row = db
1029
+ .prepare(`SELECT ${source.columns} FROM ${source.table} r WHERE r.instance=? AND r.stream=? AND r.record_key=? AND ${source.filter}`)
1030
+ .get(instance, stream, key);
1031
+ return row && visibleRow(row);
1032
+ }
1033
+ function listRecords(stream, options, reader, preview) {
1034
+ return db
1035
+ .transaction(() => {
1036
+ const { epoch } = db
1037
+ .prepare("SELECT epoch FROM pdpp_store_metadata WHERE id=1")
1038
+ .get();
1039
+ const { value } = db
1040
+ .prepare("SELECT value FROM pdpp_write_clock WHERE id=1")
1041
+ .get();
1042
+ const ids = [...new Set(options.instanceIds)].sort();
1043
+ const modes = new Map(ids.map((instance) => [
1044
+ instance,
1045
+ readBranch(instance, reader, preview),
1046
+ ]));
1047
+ const viewRevision = cursorRevision(stream, ids, modes);
1048
+ const cursor = options.cursor
1049
+ ? decodeCursor(options.cursor)
1050
+ : undefined;
1051
+ if (cursor && cursor.kind !== "list")
1052
+ throw new InvalidCursorError();
1053
+ if (cursor &&
1054
+ (cursor.viewRevision !== viewRevision ||
1055
+ cursor.visibilityEpoch !== viewRevision ||
1056
+ cursor.epoch !== epoch ||
1057
+ cursor.stream !== stream ||
1058
+ cursor.view !== reader ||
1059
+ JSON.stringify(cursor.instanceIds) !== JSON.stringify(ids) ||
1060
+ typeof cursor.instance !== "string"))
1061
+ throw new CursorExpiredError();
1062
+ if (cursor && cursor.order !== options.order)
1063
+ throw new InvalidCursorError();
1064
+ const dir = options.order === "asc" ? "ASC" : "DESC", cmp = options.order === "asc" ? ">" : "<";
1065
+ // One keyset window of at most limit+1 rows after `after`, masked rows
1066
+ // included, so each window's work is bounded by the page size, not by
1067
+ // how many canonical rows an overlay masks. A masked row and its
1068
+ // overlay can share a position; the overlay sorts first, so a window
1069
+ // boundary between them loses nothing.
1070
+ const windowAfter = (after) => {
1071
+ const params = [];
1072
+ const branches = ids.flatMap((instance) => {
1073
+ const mode = modes.get(instance);
1074
+ if (mode === "hidden")
1075
+ return [];
1076
+ let boundary = "";
1077
+ const bounds = [];
1078
+ if (after) {
1079
+ const instanceOrder = Buffer.compare(Buffer.from(instance), Buffer.from(after.instance));
1080
+ const equal = options.order === "asc" ? instanceOrder > 0 : instanceOrder < 0;
1081
+ boundary = ` AND (r.emitted_key,r.record_key) ${equal ? cmp + "=" : cmp} (?,?)`;
1082
+ bounds.push(timeSortKey(after.sortValue ?? ""), after.recordKey);
1083
+ }
1084
+ return sources(mode).map((source) => {
1085
+ params.push(instance, stream, ...bounds, options.limit + 1);
1086
+ return `SELECT * FROM (SELECT ${source.columns} FROM ${source.table} r WHERE r.instance=? AND r.stream=? AND ${source.filter}${boundary} ORDER BY r.emitted_key ${dir},r.record_key ${dir} LIMIT ?)`;
1087
+ });
1088
+ });
1089
+ return branches.length
1090
+ ? db
1091
+ .prepare(`SELECT * FROM (${branches.join(" UNION ALL ")}) ORDER BY emitted_key ${dir},record_key ${dir},instance ${dir},masked ASC LIMIT ?`)
1092
+ .all(...params, options.limit + 1)
1093
+ : [];
1094
+ };
1095
+ // Grant eligibility applies before pagination, as on the canonical
1096
+ // store. A masked row still takes a page slot, so page work stays
1097
+ // bounded by the page size; the page may then hold fewer than `limit`
1098
+ // rows and the cursor continues after the last consumed position.
1099
+ // Scan windows until one slot beyond the page proves continuation.
1100
+ const taken = [];
1101
+ let after = cursor && {
1102
+ instance: cursor.instance,
1103
+ sortValue: cursor.sortValue,
1104
+ recordKey: cursor.recordKey,
1105
+ };
1106
+ for (;;) {
1107
+ const rows = windowAfter(after);
1108
+ for (const raw of rows) {
1109
+ if (raw.masked)
1110
+ taken.push({ raw });
1111
+ else {
1112
+ const row = visibleRow(raw);
1113
+ if (options.isVisible && !options.isVisible(row))
1114
+ continue;
1115
+ taken.push({ raw, row });
1116
+ }
1117
+ if (taken.length > options.limit)
1118
+ break;
1119
+ }
1120
+ if (taken.length > options.limit || rows.length <= options.limit)
1121
+ break;
1122
+ const tail = rows[rows.length - 1];
1123
+ after = {
1124
+ instance: tail.instance,
1125
+ sortValue: tail.emitted_at,
1126
+ recordKey: tail.record_key,
1127
+ };
1128
+ }
1129
+ const hasMore = taken.length > options.limit;
1130
+ const consumed = taken.slice(0, options.limit);
1131
+ const last = consumed.at(-1)?.raw;
1132
+ const page = consumed.flatMap(({ row }) => (row ? [{ row }] : []));
1133
+ const data = page.map(({ row }) => options.fields && !row.deleted
1134
+ ? {
1135
+ ...row,
1136
+ data: Object.fromEntries(options.fields
1137
+ .filter((field) => field in row.data)
1138
+ .map((field) => [field, row.data[field]])),
1139
+ }
1140
+ : row);
1141
+ return {
1142
+ data,
1143
+ hasMore,
1144
+ horizon: String(value),
1145
+ nextCursor: hasMore && last
1146
+ ? encodeCursor({
1147
+ kind: "list",
1148
+ stream,
1149
+ epoch,
1150
+ viewRevision,
1151
+ visibilityEpoch: viewRevision,
1152
+ view: reader,
1153
+ instanceIds: ids,
1154
+ instance: last.instance,
1155
+ order: options.order,
1156
+ sortValue: last.emitted_at,
1157
+ recordKey: last.record_key,
1158
+ horizon: String(value),
1159
+ })
1160
+ : undefined,
1161
+ };
1162
+ })
1163
+ .deferred();
1164
+ }
1165
+ function summary(table, instance, stream) {
1166
+ return db
1167
+ .prepare("SELECT live_count,row_count,pending_count,last_updated FROM pdpp_replica_view_streams WHERE preview=? AND instance=? AND stream=?")
1168
+ .get(table, instance, stream);
1169
+ }
1170
+ /** Rows of this stream the shared quarantine predicate removes from the mode. */
1171
+ function quarantinedCounts(mode, instance, stream) {
1172
+ // Driven by the stream's quarantine facts (CROSS JOIN keeps them the
1173
+ // outer loop), so the work grows with the facts, not the rows. Overlay rows are judged by their own generation;
1174
+ // a canonical row without an overlay row by the binding's.
1175
+ const canonicalHidden = `SELECT c.record_key,1 AS present,1 AS live,0 AS pending FROM pdpp_replica_quarantined_keys q CROSS JOIN pdpp_records c ON c.instance=q.instance AND c.stream=q.stream AND c.record_key=q.record_key WHERE q.instance=@instance AND q.stream=@stream AND c.deleted=0 AND NOT (${unquarantined("c")})`;
1176
+ const effective = mode === "preview"
1177
+ ? `SELECT v.record_key,v.present,v.live,v.pending FROM pdpp_replica_quarantined_keys q CROSS JOIN pdpp_replica_view v ON v.instance=q.instance AND v.stream=q.stream AND v.record_key=q.record_key WHERE q.instance=@instance AND q.stream=@stream AND NOT (${ownUnquarantined("v")})
1178
+ UNION ${canonicalHidden} AND ${notOverlaid("c")}`
1179
+ : mode === "local"
1180
+ ? `SELECT r.record_key,1 AS present,1 AS live,0 AS pending FROM pdpp_replica_quarantined_keys q CROSS JOIN pdpp_replica_local_view r ON r.instance=q.instance AND r.stream=q.stream AND r.record_key=q.record_key WHERE q.instance=@instance AND q.stream=@stream AND r.deleted=0 AND NOT (${ownUnquarantined("r")})`
1181
+ : canonicalHidden;
1182
+ return db
1183
+ .prepare(`SELECT COALESCE(SUM(present),0) AS rows,COALESCE(SUM(live),0) AS live,COALESCE(SUM(pending),0) AS pending FROM (SELECT DISTINCT * FROM (${effective}))`)
1184
+ .get({ instance, stream });
1185
+ }
1186
+ function latestVisible(mode, instance, stream) {
1187
+ return sources(mode)
1188
+ .map((source) => db
1189
+ .prepare(`SELECT r.emitted_at FROM ${source.table} r WHERE r.instance=? AND r.stream=? AND ${source.filter} AND NOT (${source.masked}) ORDER BY r.emitted_at DESC,r.record_key DESC LIMIT 1`)
1190
+ .get(instance, stream)?.emitted_at ?? null)
1191
+ .reduce((latest, value) => value !== null && (latest === null || value > latest)
1192
+ ? value
1193
+ : latest, null);
1194
+ }
1195
+ function listStreams(ids, preview = true, reader = "owner") {
1196
+ const combined = new Map();
1197
+ for (const instance of new Set(ids)) {
1198
+ const mode = readBranch(instance, reader, preview);
1199
+ if (mode === "hidden")
1200
+ continue;
1201
+ // The shared predicate on a null key checks the connection fence; individual
1202
+ // quarantined keys are subtracted through indexed quarantine-key lookups below.
1203
+ if (!db
1204
+ .prepare(`SELECT 1 FROM (SELECT ? AS instance,NULL AS stream,NULL AS record_key) r WHERE ${unquarantined()}`)
1205
+ .get(instance))
1206
+ continue;
1207
+ const tables = mode === "local" ? [2] : mode === "preview" ? [0, 1] : [0];
1208
+ const streams = db
1209
+ .prepare(`SELECT DISTINCT stream FROM pdpp_replica_view_streams WHERE instance=? AND preview IN (${tables.join(",")})`)
1210
+ .all(instance).map((row) => row.stream);
1211
+ for (const stream of streams) {
1212
+ const counts = tables.map((table) => summary(table, instance, stream));
1213
+ const total = (field) => counts.reduce((sum, row) => sum + (row?.[field] ?? 0), 0);
1214
+ const hidden = quarantinedCounts(mode, instance, stream);
1215
+ if (total("row_count") - hidden.rows <= 0)
1216
+ continue;
1217
+ // A quarantined key can hide the latest row; that read recomputes it
1218
+ // (its work grows with the rows it skips). Otherwise the summary holds
1219
+ // it: mode 1's latest is derived at write time, mode 0's and 2's by
1220
+ // their triggers.
1221
+ const last = hidden.rows
1222
+ ? latestVisible(mode, instance, stream)
1223
+ : ((counts[1] ?? counts[0])?.last_updated ?? null);
1224
+ const previous = combined.get(stream) ?? {
1225
+ count: 0,
1226
+ last: null,
1227
+ pending: false,
1228
+ conflicted: false,
1229
+ };
1230
+ combined.set(stream, {
1231
+ count: previous.count + total("live_count") - hidden.live,
1232
+ last: last && (previous.last === null || last > previous.last)
1233
+ ? last
1234
+ : previous.last,
1235
+ pending: previous.pending || total("pending_count") - hidden.pending > 0,
1236
+ conflicted: previous.conflicted || mode === "local",
1237
+ });
1238
+ }
1239
+ }
1240
+ return [...combined].map(([stream, row]) => ({
1241
+ stream,
1242
+ recordCount: row.count,
1243
+ lastUpdated: row.last,
1244
+ ...(row.conflicted
1245
+ ? replicaConflictMetadata()
1246
+ : row.pending
1247
+ ? replicaPendingMetadata()
1248
+ : {}),
1249
+ }));
1250
+ }
1251
+ function blobReferenceQuery(id, reader, preview, onlyExists) {
1252
+ const selection = onlyExists
1253
+ ? "1"
1254
+ : "r.instance,r.stream,r.record_key AS recordKey";
1255
+ const normalFact = hasBindingConflictFacts
1256
+ ? "AND NOT EXISTS (SELECT 1 FROM pdpp_replica_binding_conflicts f WHERE f.instance=r.instance)"
1257
+ : "";
1258
+ const branches = [
1259
+ `SELECT ${selection} FROM pdpp_records r WHERE r.blob_id=? AND r.deleted=0 AND ${unquarantined()} ${preview ? `AND ${notOverlaid()}` : ""} ${normalFact}`,
1260
+ ];
1261
+ const args = [id];
1262
+ if (preview) {
1263
+ branches.push(`SELECT ${selection} FROM pdpp_replica_view r WHERE r.blob_id=? AND r.live=1 AND ${ownUnquarantined()} ${normalFact}`);
1264
+ args.push(id);
1265
+ }
1266
+ if (hasBindingConflictFacts &&
1267
+ replicaStateVisible("conflicted", reader, undefined, "local")) {
1268
+ branches.push(`SELECT ${selection} FROM pdpp_replica_local_view r WHERE r.blob_id=? AND ${sources("local")[0].filter} AND EXISTS (SELECT 1 FROM pdpp_replica_binding_conflicts f WHERE f.instance=r.instance)`);
1269
+ args.push(id);
1270
+ }
1271
+ return {
1272
+ sql: `${branches.join(" UNION ALL ")}${onlyExists ? " LIMIT 1" : ""}`,
1273
+ args,
1274
+ };
1275
+ }
1276
+ function findBlobReferences(id, reader = "owner", preview = true) {
1277
+ const query = blobReferenceQuery(id, reader, preview, false);
1278
+ return db.prepare(query.sql).all(...query.args);
1279
+ }
1280
+ function hasBlobReference(id, reader = "owner", preview = true) {
1281
+ const query = blobReferenceQuery(id, reader, preview, true);
1282
+ return !!db.prepare(query.sql).get(...query.args);
1283
+ }
1284
+ function visibilityEpoch() {
1285
+ const row = db
1286
+ .prepare("SELECT epoch FROM pdpp_replica_visibility_epoch WHERE id=1")
1287
+ .get();
1288
+ if (!row)
1289
+ throw new Error("replica visibility epoch is unavailable");
1290
+ const integrity = db
1291
+ .prepare("SELECT instance, fences FROM pdpp_replica_integrity_fences ORDER BY instance")
1292
+ .all();
1293
+ const bindings = db
1294
+ .prepare("SELECT instance, generation, method, reset_clock FROM pdpp_instance_binding ORDER BY instance")
1295
+ .all();
1296
+ const quarantinedConnections = db
1297
+ .prepare("SELECT instance FROM pdpp_replica_quarantined_connections ORDER BY instance")
1298
+ .all();
1299
+ const quarantinedKeys = db
1300
+ .prepare("SELECT instance, stream, record_key, generation FROM pdpp_replica_quarantined_keys ORDER BY instance, stream, record_key, generation")
1301
+ .all();
1302
+ const bindingConflicts = hasBindingConflictFacts
1303
+ ? db
1304
+ .prepare("SELECT instance, reason FROM pdpp_replica_binding_conflicts ORDER BY instance")
1305
+ .all()
1306
+ : [];
1307
+ return createHash("sha256")
1308
+ .update(JSON.stringify([
1309
+ row.epoch,
1310
+ integrity,
1311
+ bindings,
1312
+ quarantinedConnections,
1313
+ quarantinedKeys,
1314
+ bindingConflicts,
1315
+ ]))
1316
+ .digest("hex");
1317
+ }
1318
+ /** Stable digest of reader-visible state; excludes derivation queues and cursor revisions. */
1319
+ function visibilitySignature() {
1320
+ const projectionRows = db
1321
+ .prepare("SELECT instance,stream,record_key,emitted_at,row_json,blob_id,blob_json,present,live,pending,base_live,generation FROM pdpp_replica_view ORDER BY instance,stream,record_key")
1322
+ .all();
1323
+ const streamViews = db
1324
+ .prepare("SELECT preview,instance,stream,live_count,row_count,last_updated FROM pdpp_replica_view_streams ORDER BY preview,instance,stream")
1325
+ .all();
1326
+ const localRows = hasBindingConflictFacts
1327
+ ? db
1328
+ .prepare("SELECT instance,stream,record_key,emitted_at,row_json,blob_id,pending,deleted,blob_json,sequence,generation,source_id,method FROM pdpp_replica_local_view ORDER BY instance,stream,record_key")
1329
+ .all()
1330
+ : [];
1331
+ const completions = db
1332
+ .prepare("SELECT instance,stream,method,generation,source_collected_at,write_seq,live_count,snapshot_sha256,collection_time_basis FROM pdpp_stream_completion ORDER BY instance,stream")
1333
+ .all();
1334
+ return createHash("sha256")
1335
+ .update(JSON.stringify([
1336
+ maxChangeId(),
1337
+ structureSignature(),
1338
+ projectionRows,
1339
+ streamViews,
1340
+ db
1341
+ .prepare("SELECT writer,operation_id,instance,digest FROM pdpp_replica_view_conflicts ORDER BY writer,operation_id,instance")
1342
+ .all(),
1343
+ localRows,
1344
+ completions,
1345
+ db
1346
+ .prepare("SELECT instance,fences FROM pdpp_replica_integrity_fences ORDER BY instance")
1347
+ .all(),
1348
+ db
1349
+ .prepare("SELECT instance FROM pdpp_replica_quarantined_connections ORDER BY instance")
1350
+ .all(),
1351
+ db
1352
+ .prepare("SELECT instance,generation,stream,record_key FROM pdpp_replica_quarantined_keys ORDER BY instance,generation,stream,record_key")
1353
+ .all(),
1354
+ hasBindingConflictFacts
1355
+ ? db
1356
+ .prepare("SELECT instance,reason,evidence_json FROM pdpp_replica_binding_conflicts ORDER BY instance")
1357
+ .all()
1358
+ : [],
1359
+ ]))
1360
+ .digest("hex");
1361
+ }
1362
+ /** Descriptor of a pending blob; canonical blobs keep their metadata in pdpp_blobs. */
1363
+ function getBlobMeta(id) {
1364
+ const row = db
1365
+ .prepare(`SELECT r.blob_json FROM pdpp_replica_view r WHERE r.blob_id=? AND r.blob_json IS NOT NULL AND r.live=1 AND ${ownUnquarantined()} ${hasBindingConflictFacts ? "AND NOT EXISTS (SELECT 1 FROM pdpp_replica_binding_conflicts f WHERE f.instance=r.instance)" : ""} LIMIT 1`)
1366
+ .get(id);
1367
+ return row ? JSON.parse(row.blob_json) : undefined;
1368
+ }
1369
+ /** Explicit host call after trusted declarations change: re-derive every overlay. */
1370
+ function reconcileAll() {
1371
+ db.exec(`INSERT OR IGNORE INTO pdpp_replica_view_dirty_connections SELECT DISTINCT instance FROM pdpp_replica_view;
1372
+ INSERT OR IGNORE INTO pdpp_replica_view_dirty_connections SELECT DISTINCT instance FROM pdpp_replica_view_pending;
1373
+ INSERT OR IGNORE INTO pdpp_replica_view_dirty_connections SELECT DISTINCT instance FROM pdpp_replica_view_conflicts;`);
1374
+ flush();
1375
+ }
1376
+ flush();
1377
+ return {
1378
+ flush,
1379
+ sync,
1380
+ snapshot,
1381
+ exclusive,
1382
+ visibilityEpoch,
1383
+ cursorVisibilityEpoch: reading(cursorVisibilityEpoch),
1384
+ reconcileAll,
1385
+ addPending,
1386
+ removePending,
1387
+ connectionVisibility,
1388
+ hasBindingConflictFacts,
1389
+ getRecord: reading(getRecord),
1390
+ listRecords: reading(listRecords),
1391
+ listStreams: reading(listStreams),
1392
+ findBlobReferences: reading(findBlobReferences),
1393
+ hasBlobReference: reading(hasBlobReference),
1394
+ getBlobMeta: reading(getBlobMeta),
1395
+ };
1396
+ }
1397
+ //# sourceMappingURL=pdpp-replica-visibility-view.js.map