@aigne/afs-index 1.12.0-beta.5

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 (186) hide show
  1. package/LICENSE.md +26 -0
  2. package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs +11 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs +10 -0
  4. package/dist/backend/d1-index-backend.cjs +656 -0
  5. package/dist/backend/d1-index-backend.d.cts +149 -0
  6. package/dist/backend/d1-index-backend.d.cts.map +1 -0
  7. package/dist/backend/d1-index-backend.d.mts +149 -0
  8. package/dist/backend/d1-index-backend.d.mts.map +1 -0
  9. package/dist/backend/d1-index-backend.mjs +657 -0
  10. package/dist/backend/d1-index-backend.mjs.map +1 -0
  11. package/dist/backend/d1-vector-backend.cjs +83 -0
  12. package/dist/backend/d1-vector-backend.d.cts +21 -0
  13. package/dist/backend/d1-vector-backend.d.cts.map +1 -0
  14. package/dist/backend/d1-vector-backend.d.mts +21 -0
  15. package/dist/backend/d1-vector-backend.d.mts.map +1 -0
  16. package/dist/backend/d1-vector-backend.mjs +84 -0
  17. package/dist/backend/d1-vector-backend.mjs.map +1 -0
  18. package/dist/backend/memory-backend.cjs +304 -0
  19. package/dist/backend/memory-backend.d.cts +66 -0
  20. package/dist/backend/memory-backend.d.cts.map +1 -0
  21. package/dist/backend/memory-backend.d.mts +66 -0
  22. package/dist/backend/memory-backend.d.mts.map +1 -0
  23. package/dist/backend/memory-backend.mjs +305 -0
  24. package/dist/backend/memory-backend.mjs.map +1 -0
  25. package/dist/backend/prefix-range.cjs +25 -0
  26. package/dist/backend/prefix-range.mjs +25 -0
  27. package/dist/backend/prefix-range.mjs.map +1 -0
  28. package/dist/backend/schema.cjs +187 -0
  29. package/dist/backend/schema.mjs +188 -0
  30. package/dist/backend/schema.mjs.map +1 -0
  31. package/dist/backend/sqlite-backend.cjs +889 -0
  32. package/dist/backend/sqlite-backend.d.cts +96 -0
  33. package/dist/backend/sqlite-backend.d.cts.map +1 -0
  34. package/dist/backend/sqlite-backend.d.mts +96 -0
  35. package/dist/backend/sqlite-backend.d.mts.map +1 -0
  36. package/dist/backend/sqlite-backend.mjs +890 -0
  37. package/dist/backend/sqlite-backend.mjs.map +1 -0
  38. package/dist/backend/types.d.cts +209 -0
  39. package/dist/backend/types.d.cts.map +1 -0
  40. package/dist/backend/types.d.mts +209 -0
  41. package/dist/backend/types.d.mts.map +1 -0
  42. package/dist/backend/upstash-vector-backend.cjs +98 -0
  43. package/dist/backend/upstash-vector-backend.d.cts +21 -0
  44. package/dist/backend/upstash-vector-backend.d.cts.map +1 -0
  45. package/dist/backend/upstash-vector-backend.d.mts +21 -0
  46. package/dist/backend/upstash-vector-backend.d.mts.map +1 -0
  47. package/dist/backend/upstash-vector-backend.mjs +98 -0
  48. package/dist/backend/upstash-vector-backend.mjs.map +1 -0
  49. package/dist/backend/vector-backend.d.cts +29 -0
  50. package/dist/backend/vector-backend.d.cts.map +1 -0
  51. package/dist/backend/vector-backend.d.mts +29 -0
  52. package/dist/backend/vector-backend.d.mts.map +1 -0
  53. package/dist/dirty-queue.d.cts +41 -0
  54. package/dist/dirty-queue.d.cts.map +1 -0
  55. package/dist/dirty-queue.d.mts +41 -0
  56. package/dist/dirty-queue.d.mts.map +1 -0
  57. package/dist/embed.cjs +45 -0
  58. package/dist/embed.d.cts +20 -0
  59. package/dist/embed.d.cts.map +1 -0
  60. package/dist/embed.d.mts +20 -0
  61. package/dist/embed.d.mts.map +1 -0
  62. package/dist/embed.mjs +46 -0
  63. package/dist/embed.mjs.map +1 -0
  64. package/dist/extraction/defaults.cjs +242 -0
  65. package/dist/extraction/defaults.d.cts +14 -0
  66. package/dist/extraction/defaults.d.cts.map +1 -0
  67. package/dist/extraction/defaults.d.mts +14 -0
  68. package/dist/extraction/defaults.d.mts.map +1 -0
  69. package/dist/extraction/defaults.mjs +242 -0
  70. package/dist/extraction/defaults.mjs.map +1 -0
  71. package/dist/extraction/field-map.cjs +162 -0
  72. package/dist/extraction/field-map.d.cts +70 -0
  73. package/dist/extraction/field-map.d.cts.map +1 -0
  74. package/dist/extraction/field-map.d.mts +70 -0
  75. package/dist/extraction/field-map.d.mts.map +1 -0
  76. package/dist/extraction/field-map.mjs +157 -0
  77. package/dist/extraction/field-map.mjs.map +1 -0
  78. package/dist/extraction/manager.cjs +74 -0
  79. package/dist/extraction/manager.d.cts +47 -0
  80. package/dist/extraction/manager.d.cts.map +1 -0
  81. package/dist/extraction/manager.d.mts +47 -0
  82. package/dist/extraction/manager.d.mts.map +1 -0
  83. package/dist/extraction/manager.mjs +74 -0
  84. package/dist/extraction/manager.mjs.map +1 -0
  85. package/dist/extraction/path-anchors.cjs +86 -0
  86. package/dist/extraction/path-anchors.d.cts +72 -0
  87. package/dist/extraction/path-anchors.d.cts.map +1 -0
  88. package/dist/extraction/path-anchors.d.mts +72 -0
  89. package/dist/extraction/path-anchors.d.mts.map +1 -0
  90. package/dist/extraction/path-anchors.mjs +86 -0
  91. package/dist/extraction/path-anchors.mjs.map +1 -0
  92. package/dist/follower-scheduler.cjs +92 -0
  93. package/dist/follower-scheduler.d.cts +58 -0
  94. package/dist/follower-scheduler.d.cts.map +1 -0
  95. package/dist/follower-scheduler.d.mts +58 -0
  96. package/dist/follower-scheduler.d.mts.map +1 -0
  97. package/dist/follower-scheduler.mjs +92 -0
  98. package/dist/follower-scheduler.mjs.map +1 -0
  99. package/dist/follower.cjs +577 -0
  100. package/dist/follower.d.cts +241 -0
  101. package/dist/follower.d.cts.map +1 -0
  102. package/dist/follower.d.mts +241 -0
  103. package/dist/follower.d.mts.map +1 -0
  104. package/dist/follower.mjs +571 -0
  105. package/dist/follower.mjs.map +1 -0
  106. package/dist/index-provider.cjs +1166 -0
  107. package/dist/index-provider.d.cts +175 -0
  108. package/dist/index-provider.d.cts.map +1 -0
  109. package/dist/index-provider.d.mts +175 -0
  110. package/dist/index-provider.d.mts.map +1 -0
  111. package/dist/index-provider.mjs +1167 -0
  112. package/dist/index-provider.mjs.map +1 -0
  113. package/dist/index.cjs +53 -0
  114. package/dist/index.d.cts +22 -0
  115. package/dist/index.d.mts +22 -0
  116. package/dist/index.mjs +20 -0
  117. package/dist/keys/exact.cjs +20 -0
  118. package/dist/keys/exact.mjs +20 -0
  119. package/dist/keys/exact.mjs.map +1 -0
  120. package/dist/keys/location.cjs +13 -0
  121. package/dist/keys/location.mjs +13 -0
  122. package/dist/keys/location.mjs.map +1 -0
  123. package/dist/keys/normalized.cjs +62 -0
  124. package/dist/keys/normalized.mjs +63 -0
  125. package/dist/keys/normalized.mjs.map +1 -0
  126. package/dist/keys/person.cjs +15 -0
  127. package/dist/keys/person.mjs +15 -0
  128. package/dist/keys/person.mjs.map +1 -0
  129. package/dist/keys/registry.cjs +45 -0
  130. package/dist/keys/registry.d.cts +15 -0
  131. package/dist/keys/registry.d.cts.map +1 -0
  132. package/dist/keys/registry.d.mts +15 -0
  133. package/dist/keys/registry.d.mts.map +1 -0
  134. package/dist/keys/registry.mjs +46 -0
  135. package/dist/keys/registry.mjs.map +1 -0
  136. package/dist/keys/time.cjs +128 -0
  137. package/dist/keys/time.mjs +128 -0
  138. package/dist/keys/time.mjs.map +1 -0
  139. package/dist/keys/topic.cjs +23 -0
  140. package/dist/keys/topic.mjs +23 -0
  141. package/dist/keys/topic.mjs.map +1 -0
  142. package/dist/manifest-index.cjs +29 -0
  143. package/dist/manifest-index.d.cts +24 -0
  144. package/dist/manifest-index.d.cts.map +1 -0
  145. package/dist/manifest-index.d.mts +24 -0
  146. package/dist/manifest-index.d.mts.map +1 -0
  147. package/dist/manifest-index.mjs +28 -0
  148. package/dist/manifest-index.mjs.map +1 -0
  149. package/dist/offline-rebuild.cjs +48 -0
  150. package/dist/offline-rebuild.d.cts +57 -0
  151. package/dist/offline-rebuild.d.cts.map +1 -0
  152. package/dist/offline-rebuild.d.mts +57 -0
  153. package/dist/offline-rebuild.d.mts.map +1 -0
  154. package/dist/offline-rebuild.mjs +49 -0
  155. package/dist/offline-rebuild.mjs.map +1 -0
  156. package/dist/query/engine.cjs +433 -0
  157. package/dist/query/engine.d.cts +34 -0
  158. package/dist/query/engine.d.cts.map +1 -0
  159. package/dist/query/engine.d.mts +34 -0
  160. package/dist/query/engine.d.mts.map +1 -0
  161. package/dist/query/engine.mjs +433 -0
  162. package/dist/query/engine.mjs.map +1 -0
  163. package/dist/query/limits.cjs +12 -0
  164. package/dist/query/limits.mjs +12 -0
  165. package/dist/query/limits.mjs.map +1 -0
  166. package/dist/text/cjk.cjs +30 -0
  167. package/dist/text/cjk.d.cts +24 -0
  168. package/dist/text/cjk.d.cts.map +1 -0
  169. package/dist/text/cjk.d.mts +24 -0
  170. package/dist/text/cjk.d.mts.map +1 -0
  171. package/dist/text/cjk.mjs +30 -0
  172. package/dist/text/cjk.mjs.map +1 -0
  173. package/dist/text/fts.cjs +244 -0
  174. package/dist/text/fts.mjs +245 -0
  175. package/dist/text/fts.mjs.map +1 -0
  176. package/dist/text/host.cjs +39 -0
  177. package/dist/text/host.mjs +38 -0
  178. package/dist/text/host.mjs.map +1 -0
  179. package/dist/types.cjs +44 -0
  180. package/dist/types.d.cts +393 -0
  181. package/dist/types.d.cts.map +1 -0
  182. package/dist/types.d.mts +393 -0
  183. package/dist/types.d.mts.map +1 -0
  184. package/dist/types.mjs +42 -0
  185. package/dist/types.mjs.map +1 -0
  186. package/package.json +59 -0
@@ -0,0 +1,656 @@
1
+ const require_registry = require('../keys/registry.cjs');
2
+ const require_cjk = require('../text/cjk.cjs');
3
+ const require_d1_vector_backend = require('./d1-vector-backend.cjs');
4
+ const require_prefix_range = require('./prefix-range.cjs');
5
+ let _aigne_afs_sqlite = require("@aigne/afs-sqlite");
6
+ let drizzle_orm = require("drizzle-orm");
7
+
8
+ //#region src/backend/d1-index-backend.ts
9
+ /**
10
+ * D1IndexBackend — IndexBackend implementation for Cloudflare D1.
11
+ *
12
+ * Runs in the DID Space D1 (the same edge SQLite as the tree index), so it is
13
+ * reachable from every path that builds a session view — afs-rpc, MCP, and the
14
+ * WebSocket/DO — because all three funnel through `buildCfSessionView`.
15
+ *
16
+ * Isolation is by SCOPE (a `scope` column on every table, mirroring how the DID
17
+ * Space tree index partitions its D1 rows). One backend instance is bound to one
18
+ * scope: per-user index → `deriveScope(callerDid)`, per-instance index →
19
+ * `deriveScope(installerDid)`. Different scopes never see each other's rows.
20
+ *
21
+ * Notes vs the (removed) DO-SQLite backend:
22
+ * - db is built via `initDatabase({ d1 })` (drizzle-orm/d1), not ctx.storage.sql.
23
+ * - D1 returns BLOB columns as `number[]`; `decodeStoredEmbedding` handles that
24
+ * alongside ArrayBuffer/Uint8Array.
25
+ * - No explicit `BEGIN TRANSACTION`: D1 auto-commits each statement and the
26
+ * index is a derived, rebuildable read model (a partial batch self-heals on
27
+ * the next reconcile).
28
+ */
29
+ const DEFAULT_FTS_TOKENIZER = "unicode61 remove_diacritics 2";
30
+ /** D1 handles whose scope-partitioned schema has been ensured this isolate. */
31
+ const schemaEnsured = /* @__PURE__ */ new WeakSet();
32
+ const cursorServedAtSupported = /* @__PURE__ */ new WeakMap();
33
+ var D1IndexBackend = class {
34
+ db;
35
+ d1;
36
+ scope;
37
+ keyRegistry;
38
+ ftsTokenizer;
39
+ _injectedVectorBackend;
40
+ _vectorBackend;
41
+ constructor(d1, scope, keyRegistry, opts = {}) {
42
+ this.d1 = d1;
43
+ this.scope = scope;
44
+ this.keyRegistry = keyRegistry ?? new require_registry.KeyGeneratorRegistry();
45
+ this.ftsTokenizer = opts.ftsTokenizer ?? DEFAULT_FTS_TOKENIZER;
46
+ this._injectedVectorBackend = opts.vectorBackend;
47
+ }
48
+ /** See `IndexBackend.setKeyRegistry` — wires an injected backend to the provider's shared registry. */
49
+ setKeyRegistry(registry) {
50
+ this.keyRegistry = registry;
51
+ }
52
+ async initialize() {
53
+ this.db = await (0, _aigne_afs_sqlite.initDatabase)({ d1: this.d1 });
54
+ if (!this.d1 || typeof this.d1 !== "object" || !schemaEnsured.has(this.d1)) {
55
+ await this.ensureSchema();
56
+ if (this.d1 && typeof this.d1 === "object") schemaEnsured.add(this.d1);
57
+ }
58
+ this._vectorBackend = this._injectedVectorBackend ?? new require_d1_vector_backend.D1VectorBackend(this.db, this.scope);
59
+ }
60
+ async close() {
61
+ this.db = void 0;
62
+ }
63
+ d1BatchBinding() {
64
+ if (!this.d1 || typeof this.d1 !== "object") return null;
65
+ const candidate = this.d1;
66
+ return typeof candidate.prepare === "function" && typeof candidate.batch === "function" ? candidate : null;
67
+ }
68
+ /**
69
+ * Rename the anchor tables `arc_anchor_*` → `idx_anchor_*` in place, preserving
70
+ * rows. Probe-guarded (renames only when the legacy table exists and the target
71
+ * does not) so it is idempotent and safe on fresh / already-migrated / racing
72
+ * isolates. Concurrent isolates may both attempt the ALTER on the same D1 —
73
+ * the loser's `no such table` / `already exists` is swallowed.
74
+ *
75
+ * ensureSchema runs on the cold-start (first-request-per-isolate) path, so the
76
+ * existence check is a SINGLE `sqlite_master` read for all four names rather
77
+ * than one per pair — one O(1) probe, no per-request cost (steady state is
78
+ * gated out by the `schemaEnsured` WeakSet).
79
+ */
80
+ async renameAnchorTables() {
81
+ const pairs = [["arc_anchor_data", "idx_anchor_data"], ["arc_anchor_key", "idx_anchor_key"]];
82
+ const names = pairs.flat().map((n) => `'${n}'`);
83
+ const rows = await this.db.all(drizzle_orm.sql.raw(`SELECT name FROM sqlite_master WHERE type='table' AND name IN (${names.join(", ")})`));
84
+ const present = new Set(rows.map((r) => r.name));
85
+ for (const [legacy, next] of pairs) {
86
+ if (!present.has(legacy)) continue;
87
+ try {
88
+ if (present.has(next)) await this.db.run(drizzle_orm.sql.raw(`DROP TABLE IF EXISTS ${legacy}`));
89
+ else await this.db.run(drizzle_orm.sql.raw(`ALTER TABLE ${legacy} RENAME TO ${next}`));
90
+ } catch {}
91
+ }
92
+ }
93
+ async ensureSchema() {
94
+ const tokenizer = this.ftsTokenizer.replace(/'/g, "''");
95
+ await this.renameAnchorTables();
96
+ await this.db.run(drizzle_orm.sql.raw(`CREATE TABLE IF NOT EXISTS ds_indexed_entry (
97
+ scope TEXT NOT NULL,
98
+ entry_path TEXT NOT NULL,
99
+ domain TEXT,
100
+ state TEXT,
101
+ indexed_at TEXT,
102
+ updated_at TEXT,
103
+ anchor_count INTEGER DEFAULT 0,
104
+ source_path TEXT,
105
+ source_hash TEXT,
106
+ extractor_id TEXT,
107
+ meta TEXT,
108
+ PRIMARY KEY (scope, entry_path)
109
+ )`));
110
+ await this.db.run(drizzle_orm.sql.raw(`CREATE TABLE IF NOT EXISTS idx_anchor_data (
111
+ scope TEXT NOT NULL,
112
+ entry_path TEXT NOT NULL,
113
+ anchor_type TEXT NOT NULL,
114
+ confidence REAL,
115
+ raw_anchor TEXT
116
+ )`));
117
+ await this.db.run(drizzle_orm.sql.raw(`CREATE TABLE IF NOT EXISTS idx_anchor_key (
118
+ scope TEXT NOT NULL,
119
+ index_key TEXT NOT NULL,
120
+ entry_path TEXT NOT NULL,
121
+ anchor_type TEXT NOT NULL,
122
+ confidence REAL,
123
+ PRIMARY KEY (scope, index_key, entry_path, anchor_type)
124
+ )`));
125
+ await this.db.run(drizzle_orm.sql.raw(`CREATE INDEX IF NOT EXISTS idx_ad_scope_path ON idx_anchor_data(scope, entry_path)`));
126
+ await this.db.run(drizzle_orm.sql.raw(`CREATE INDEX IF NOT EXISTS idx_ad_scope_anchor_type ON idx_anchor_data(scope, anchor_type)`));
127
+ await this.db.run(drizzle_orm.sql.raw(`CREATE INDEX IF NOT EXISTS idx_ak_scope_path ON idx_anchor_key(scope, entry_path)`));
128
+ await this.db.run(drizzle_orm.sql.raw(`CREATE TABLE IF NOT EXISTS idx_embedding_vector (
129
+ scope TEXT NOT NULL,
130
+ entry_path TEXT NOT NULL,
131
+ embedding TEXT,
132
+ embedding_blob BLOB,
133
+ dimension INTEGER,
134
+ PRIMARY KEY (scope, entry_path)
135
+ )`));
136
+ await this.db.run(drizzle_orm.sql.raw(`CREATE VIRTUAL TABLE IF NOT EXISTS idx_fts USING fts5(
137
+ scope UNINDEXED,
138
+ entry_path UNINDEXED,
139
+ summary,
140
+ tokenize='${tokenizer}'
141
+ )`));
142
+ await this.ensureFtsRowidMapping();
143
+ await this.db.run(drizzle_orm.sql.raw(`CREATE TABLE IF NOT EXISTS idx_index_cursor (
144
+ scope TEXT NOT NULL,
145
+ consumer_id TEXT NOT NULL,
146
+ last_seq INTEGER NOT NULL DEFAULT 0,
147
+ watermarks TEXT,
148
+ updated_at INTEGER NOT NULL,
149
+ served_at INTEGER,
150
+ instance_did TEXT,
151
+ domains TEXT,
152
+ PRIMARY KEY (scope, consumer_id)
153
+ )`));
154
+ }
155
+ /**
156
+ * Create `idx_fts_rowid` the first time this D1 sees it, backfilling from
157
+ * any pre-existing `idx_fts` rows (issue #1103). The backfill maps every
158
+ * existing FTS row to its own rowid so a later rewrite REPLACEs in place
159
+ * instead of leaving a duplicate for the same (scope, entry_path).
160
+ *
161
+ * Race-safe across concurrent cold isolates — D1 auto-commits each statement
162
+ * (no transaction), so two isolates can run this at once on a fresh D1:
163
+ * - `CREATE TABLE IF NOT EXISTS` never throws on a concurrent create.
164
+ * - The one-time backfill runs only while the mapping is still empty (a
165
+ * cheap LIMIT-1 probe, NOT a scan), so steady state costs nothing extra.
166
+ * - `INSERT OR IGNORE` makes the backfill itself idempotent, so two
167
+ * isolates racing it (or a re-run) can't collide on the PK/UNIQUE.
168
+ */
169
+ async ensureFtsRowidMapping() {
170
+ await this.db.run(drizzle_orm.sql.raw(`CREATE TABLE IF NOT EXISTS idx_fts_rowid (
171
+ fts_rowid INTEGER PRIMARY KEY AUTOINCREMENT,
172
+ scope TEXT NOT NULL,
173
+ entry_path TEXT NOT NULL,
174
+ UNIQUE (scope, entry_path)
175
+ )`));
176
+ if ((await this.db.all(drizzle_orm.sql.raw("SELECT 1 AS one FROM idx_fts_rowid LIMIT 1"))).length > 0) return;
177
+ await this.db.run(drizzle_orm.sql.raw(`INSERT OR IGNORE INTO idx_fts_rowid (fts_rowid, scope, entry_path) SELECT rowid, scope, entry_path FROM idx_fts`));
178
+ }
179
+ async supportsCursorServedAt() {
180
+ if (this.d1 && typeof this.d1 === "object") {
181
+ const cached = cursorServedAtSupported.get(this.d1);
182
+ if (cached !== void 0) return cached;
183
+ }
184
+ const supported = (await this.db.all(drizzle_orm.sql.raw("PRAGMA table_info(idx_index_cursor)"))).some((c) => c.name === "served_at");
185
+ if (supported && this.d1 && typeof this.d1 === "object") cursorServedAtSupported.set(this.d1, supported);
186
+ return supported;
187
+ }
188
+ async writeEntry(entry) {
189
+ const e = entryToRow(entry);
190
+ await this.db.run(drizzle_orm.sql`INSERT OR REPLACE INTO ds_indexed_entry (scope, entry_path, domain, state, indexed_at, updated_at, anchor_count, source_path, source_hash, extractor_id, meta)
191
+ VALUES (${this.scope}, ${e.entry_path}, ${e.domain}, ${e.state}, ${e.indexed_at}, ${e.updated_at}, ${e.anchor_count}, ${e.source_path ?? null}, ${e.source_hash ?? null}, ${e.extractor_id ?? null}, ${e.meta ?? null})`);
192
+ }
193
+ async writeIndexedDocument(document) {
194
+ const d1 = this.d1BatchBinding();
195
+ if (!d1) {
196
+ await this.writeEntry(document.entry);
197
+ await this.writeAnchors(document.entry.entryPath, document.anchors ?? []);
198
+ if (document.summary) await this.writeSummary(document.entry.entryPath, document.summary);
199
+ return;
200
+ }
201
+ const entry = entryToRow(document.entry);
202
+ const entryPath = document.entry.entryPath;
203
+ const statements = [
204
+ d1.prepare(`INSERT OR REPLACE INTO ds_indexed_entry (scope, entry_path, domain, state, indexed_at, updated_at, anchor_count, source_path, source_hash, extractor_id, meta)
205
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).bind(this.scope, entry.entry_path, entry.domain, entry.state, entry.indexed_at, entry.updated_at, entry.anchor_count, entry.source_path, entry.source_hash, entry.extractor_id, entry.meta),
206
+ d1.prepare("DELETE FROM idx_anchor_data WHERE scope = ? AND entry_path = ?").bind(this.scope, entryPath),
207
+ d1.prepare("DELETE FROM idx_anchor_key WHERE scope = ? AND entry_path = ?").bind(this.scope, entryPath)
208
+ ];
209
+ const anchors = document.anchors ?? [];
210
+ for (const chunk of chunkArray(anchors, 18)) statements.push(d1.prepare(`INSERT INTO idx_anchor_data (scope, entry_path, anchor_type, confidence, raw_anchor) VALUES ${chunk.map(() => "(?, ?, ?, ?, ?)").join(", ")}`).bind(...chunk.flatMap((a) => [
211
+ this.scope,
212
+ entryPath,
213
+ a.type,
214
+ a.confidence,
215
+ JSON.stringify(a)
216
+ ])));
217
+ const keyRows = anchors.flatMap((a) => this.keyRegistry.generateKeys(a).map((key) => ({
218
+ anchor: a,
219
+ key
220
+ })));
221
+ for (const chunk of chunkArray(keyRows, 18)) statements.push(d1.prepare(`INSERT OR REPLACE INTO idx_anchor_key (scope, index_key, entry_path, anchor_type, confidence) VALUES ${chunk.map(() => "(?, ?, ?, ?, ?)").join(", ")}`).bind(...chunk.flatMap(({ anchor, key }) => [
222
+ this.scope,
223
+ key,
224
+ entryPath,
225
+ anchor.type,
226
+ anchor.confidence
227
+ ])));
228
+ if (document.summary) statements.push(d1.prepare("INSERT INTO idx_fts_rowid (scope, entry_path) VALUES (?, ?) ON CONFLICT(scope, entry_path) DO NOTHING").bind(this.scope, entryPath), d1.prepare(`INSERT OR REPLACE INTO idx_fts (rowid, scope, entry_path, summary)
229
+ VALUES ((SELECT fts_rowid FROM idx_fts_rowid WHERE scope = ? AND entry_path = ?), ?, ?, ?)`).bind(this.scope, entryPath, this.scope, entryPath, require_cjk.segmentCJK(document.summary)));
230
+ await d1.batch(statements);
231
+ }
232
+ async getEntry(path) {
233
+ const rows = await this.db.all(drizzle_orm.sql`SELECT * FROM ds_indexed_entry WHERE scope = ${this.scope} AND entry_path = ${path} LIMIT 1`);
234
+ if (rows.length === 0) return null;
235
+ return rowToEntry(rows[0]);
236
+ }
237
+ async removeEntry(path) {
238
+ await this.db.run(drizzle_orm.sql`DELETE FROM ds_indexed_entry WHERE scope = ${this.scope} AND entry_path = ${path}`);
239
+ }
240
+ async removeIndexedDocument(path, opts) {
241
+ const skipVector = opts?.skipVector === true;
242
+ const d1 = this.d1BatchBinding();
243
+ if (!d1) {
244
+ await this.removeAnchors(path);
245
+ await this.removeSummary(path);
246
+ if (!skipVector) await this.removeEmbedding(path);
247
+ await this.removeEntry(path);
248
+ return;
249
+ }
250
+ if (this._injectedVectorBackend && !skipVector) await this.removeEmbedding(path);
251
+ const statements = [
252
+ d1.prepare("DELETE FROM idx_anchor_data WHERE scope = ? AND entry_path = ?").bind(this.scope, path),
253
+ d1.prepare("DELETE FROM idx_anchor_key WHERE scope = ? AND entry_path = ?").bind(this.scope, path),
254
+ d1.prepare("DELETE FROM idx_fts WHERE rowid = (SELECT fts_rowid FROM idx_fts_rowid WHERE scope = ? AND entry_path = ?)").bind(this.scope, path),
255
+ d1.prepare("DELETE FROM idx_fts_rowid WHERE scope = ? AND entry_path = ?").bind(this.scope, path),
256
+ d1.prepare("DELETE FROM ds_indexed_entry WHERE scope = ? AND entry_path = ?").bind(this.scope, path)
257
+ ];
258
+ if (!this._injectedVectorBackend && !skipVector) statements.splice(4, 0, d1.prepare("DELETE FROM idx_embedding_vector WHERE scope = ? AND entry_path = ?").bind(this.scope, path));
259
+ await d1.batch(statements);
260
+ }
261
+ async listEntries(filter) {
262
+ const conditions = this.entryConditions(filter);
263
+ return (await this.db.all(drizzle_orm.sql`SELECT * FROM ds_indexed_entry WHERE ${drizzle_orm.sql.join(conditions, drizzle_orm.sql` AND `)}`)).map(rowToEntry);
264
+ }
265
+ entryConditions(filter) {
266
+ const conditions = [drizzle_orm.sql`scope = ${this.scope}`];
267
+ if (filter.domain) conditions.push(drizzle_orm.sql`domain = ${filter.domain}`);
268
+ if (filter.state) conditions.push(drizzle_orm.sql`state = ${filter.state}`);
269
+ if (filter.extractorId) conditions.push(drizzle_orm.sql`extractor_id = ${filter.extractorId}`);
270
+ if (filter.scope) conditions.push(drizzle_orm.sql`entry_path LIKE ${`${filter.scope}%`}`);
271
+ if (filter.boundDomains && filter.boundDomains.length > 0) {
272
+ const ps = filter.boundDomains.map((d) => drizzle_orm.sql`${d}`);
273
+ conditions.push(drizzle_orm.sql`domain IN (${drizzle_orm.sql.join(ps, drizzle_orm.sql`, `)})`);
274
+ }
275
+ return conditions;
276
+ }
277
+ async markStale(filter) {
278
+ const conditions = [drizzle_orm.sql`scope = ${this.scope}`, drizzle_orm.sql`state = 'active'`];
279
+ if (filter.domain) conditions.push(drizzle_orm.sql`domain = ${filter.domain}`);
280
+ if (filter.extractorId) conditions.push(drizzle_orm.sql`extractor_id = ${filter.extractorId}`);
281
+ if (filter.scope) conditions.push(drizzle_orm.sql`entry_path LIKE ${`${filter.scope}%`}`);
282
+ if (filter.boundDomains && filter.boundDomains.length > 0) {
283
+ const ps = filter.boundDomains.map((d) => drizzle_orm.sql`${d}`);
284
+ conditions.push(drizzle_orm.sql`domain IN (${drizzle_orm.sql.join(ps, drizzle_orm.sql`, `)})`);
285
+ }
286
+ if (filter.anchorType) {
287
+ const anchorRows = await this.db.all(drizzle_orm.sql`SELECT DISTINCT entry_path FROM idx_anchor_data INDEXED BY idx_ad_scope_anchor_type WHERE scope = ${this.scope} AND anchor_type = ${filter.anchorType}`);
288
+ if (anchorRows.length === 0) return 0;
289
+ const placeholders = anchorRows.map((r) => drizzle_orm.sql`${r.entry_path}`);
290
+ conditions.push(drizzle_orm.sql`entry_path IN (${drizzle_orm.sql.join(placeholders, drizzle_orm.sql`, `)})`);
291
+ }
292
+ const where = drizzle_orm.sql.join(conditions, drizzle_orm.sql` AND `);
293
+ const count = (await this.db.all(drizzle_orm.sql`SELECT COUNT(*) as count FROM ds_indexed_entry WHERE ${where}`))[0]?.count ?? 0;
294
+ if (count === 0) return 0;
295
+ await this.db.run(drizzle_orm.sql`UPDATE ds_indexed_entry SET state = 'stale' WHERE ${where}`);
296
+ return count;
297
+ }
298
+ async countEntries(filter) {
299
+ const conditions = this.entryConditions(filter);
300
+ return (await this.db.all(drizzle_orm.sql`SELECT COUNT(*) as count FROM ds_indexed_entry WHERE ${drizzle_orm.sql.join(conditions, drizzle_orm.sql` AND `)}`))[0]?.count ?? 0;
301
+ }
302
+ async writeAnchors(path, anchors) {
303
+ await this.db.run(drizzle_orm.sql`DELETE FROM idx_anchor_data WHERE scope = ${this.scope} AND entry_path = ${path}`);
304
+ await this.db.run(drizzle_orm.sql`DELETE FROM idx_anchor_key WHERE scope = ${this.scope} AND entry_path = ${path}`);
305
+ if (anchors.length === 0) return;
306
+ const dataRows = anchors.map((a) => drizzle_orm.sql`(${this.scope}, ${path}, ${a.type}, ${a.confidence}, ${JSON.stringify(a)})`);
307
+ await this.multiRowInsert(drizzle_orm.sql`INSERT INTO idx_anchor_data (scope, entry_path, anchor_type, confidence, raw_anchor) VALUES `, dataRows);
308
+ const keyRows = anchors.flatMap((a) => this.keyRegistry.generateKeys(a).map((key) => drizzle_orm.sql`(${this.scope}, ${key}, ${path}, ${a.type}, ${a.confidence})`));
309
+ await this.multiRowInsert(drizzle_orm.sql`INSERT OR REPLACE INTO idx_anchor_key (scope, index_key, entry_path, anchor_type, confidence) VALUES `, keyRows);
310
+ }
311
+ /**
312
+ * Run `<prefix>(row),(row),…` as multi-row INSERT statement(s), chunked so
313
+ * each stays under D1's 100-bound-parameter-per-statement limit (5 columns per
314
+ * row → 18 rows ≈ 90 params). No-op on an empty row list. Portable across all
315
+ * db backends (plain `db.run`, unlike the driver-specific db.batch).
316
+ */
317
+ async multiRowInsert(prefix, rows) {
318
+ const CHUNK = 18;
319
+ for (let i = 0; i < rows.length; i += CHUNK) await this.db.run(drizzle_orm.sql`${prefix}${drizzle_orm.sql.join(rows.slice(i, i + CHUNK), drizzle_orm.sql`, `)}`);
320
+ }
321
+ async queryAnchors(query) {
322
+ if (query.keys.length === 0) return [];
323
+ const keyGroups = query.keys.map((kg) => {
324
+ const values = /* @__PURE__ */ new Set();
325
+ for (const raw of kg.values) {
326
+ const synthetic = {
327
+ type: kg.type ?? "exact",
328
+ values: [raw],
329
+ confidence: 1
330
+ };
331
+ for (const k of this.keyRegistry.generateKeys(synthetic)) values.add(k);
332
+ values.add(raw);
333
+ }
334
+ return { values: [...values] };
335
+ });
336
+ const cteNames = [];
337
+ const cteParts = [];
338
+ for (let i = 0; i < keyGroups.length; i++) {
339
+ const cteName = `g${i}`;
340
+ cteNames.push(cteName);
341
+ const extraConds = [];
342
+ for (const prefix of [query.scope, query.pathPrefix]) if (prefix) {
343
+ const [lo, hi] = require_prefix_range.entryPathPrefixRange(prefix);
344
+ extraConds.push(drizzle_orm.sql`entry_path >= ${lo} AND entry_path < ${hi}`);
345
+ }
346
+ if (query.minConfidence != null) extraConds.push(drizzle_orm.sql`confidence >= ${query.minConfidence}`);
347
+ const arms = keyGroups[i].values.map((v) => drizzle_orm.sql`SELECT entry_path, confidence FROM idx_anchor_key WHERE ${drizzle_orm.sql.join([
348
+ drizzle_orm.sql`scope = ${this.scope}`,
349
+ drizzle_orm.sql`index_key = ${v}`,
350
+ ...extraConds
351
+ ], drizzle_orm.sql` AND `)}`);
352
+ cteParts.push(drizzle_orm.sql`${drizzle_orm.sql.raw(cteName)} AS (SELECT entry_path, MIN(confidence) as min_conf FROM (${drizzle_orm.sql.join(arms, drizzle_orm.sql` UNION ALL `)}) GROUP BY entry_path)`);
353
+ }
354
+ const cteSql = drizzle_orm.sql`WITH ${drizzle_orm.sql.join(cteParts, drizzle_orm.sql`, `)}`;
355
+ const cteJoinParts = [];
356
+ let scoreExpr;
357
+ if (cteNames.length === 1) scoreExpr = `${cteNames[0]}.min_conf`;
358
+ else {
359
+ for (let i = 1; i < cteNames.length; i++) cteJoinParts.push(`INNER JOIN ${cteNames[i]} ON ${cteNames[0]}.entry_path = ${cteNames[i]}.entry_path`);
360
+ scoreExpr = `MIN(${cteNames.map((n) => `${n}.min_conf`).join(", ")})`;
361
+ }
362
+ const metaFilter = query.metaFilter;
363
+ const domains = query.domains;
364
+ const entryJoinPart = [];
365
+ let entryWhereSql = null;
366
+ if (domains && domains.length > 0 || metaFilter && Object.keys(metaFilter).length > 0) {
367
+ entryJoinPart.push(`INNER JOIN ds_indexed_entry ie ON ${cteNames[0]}.entry_path = ie.entry_path AND ie.scope = '${sqlEscape(this.scope)}'`);
368
+ const entryConditions = [];
369
+ if (domains && domains.length > 0) {
370
+ const ps = domains.map((d) => drizzle_orm.sql`${d}`);
371
+ entryConditions.push(drizzle_orm.sql`ie.domain IN (${drizzle_orm.sql.join(ps, drizzle_orm.sql`, `)})`);
372
+ }
373
+ for (const [key, expected] of Object.entries(metaFilter ?? {})) if (Array.isArray(expected)) {
374
+ if (expected.length === 0) return [];
375
+ const ps = expected.map((v) => drizzle_orm.sql`${v}`);
376
+ entryConditions.push(drizzle_orm.sql`json_extract(ie.meta, ${`$.${key}`}) IN (${drizzle_orm.sql.join(ps, drizzle_orm.sql`, `)})`);
377
+ } else entryConditions.push(drizzle_orm.sql`json_extract(ie.meta, ${`$.${key}`}) = ${expected}`);
378
+ entryWhereSql = drizzle_orm.sql.join(entryConditions, drizzle_orm.sql` AND `);
379
+ }
380
+ const allJoins = [...cteJoinParts, ...entryJoinPart].join(" ");
381
+ let selectSql;
382
+ if (entryWhereSql) selectSql = drizzle_orm.sql`${cteSql} SELECT ${drizzle_orm.sql.raw(cteNames[0])}.entry_path, ${drizzle_orm.sql.raw(scoreExpr)} as score FROM ${drizzle_orm.sql.raw(cteNames[0])} ${drizzle_orm.sql.raw(allJoins)} WHERE ${entryWhereSql}`;
383
+ else selectSql = drizzle_orm.sql`${cteSql} SELECT ${drizzle_orm.sql.raw(cteNames[0])}.entry_path, ${drizzle_orm.sql.raw(scoreExpr)} as score FROM ${drizzle_orm.sql.raw(cteNames[0])} ${drizzle_orm.sql.raw(allJoins)}`;
384
+ const orderSql = query.limit != null ? drizzle_orm.sql`${selectSql} ORDER BY score DESC, ${drizzle_orm.sql.raw(cteNames[0])}.entry_path LIMIT ${query.limit}` : drizzle_orm.sql`${selectSql} ORDER BY score DESC, ${drizzle_orm.sql.raw(cteNames[0])}.entry_path`;
385
+ return (await this.db.all(orderSql)).map((r) => ({
386
+ entryPath: r.entry_path,
387
+ score: r.score
388
+ }));
389
+ }
390
+ async removeAnchors(path) {
391
+ await this.db.run(drizzle_orm.sql`DELETE FROM idx_anchor_data WHERE scope = ${this.scope} AND entry_path = ${path}`);
392
+ await this.db.run(drizzle_orm.sql`DELETE FROM idx_anchor_key WHERE scope = ${this.scope} AND entry_path = ${path}`);
393
+ }
394
+ async getAnchorData(path) {
395
+ return (await this.db.all(drizzle_orm.sql`SELECT * FROM idx_anchor_data WHERE scope = ${this.scope} AND entry_path = ${path}`)).map((r) => ({
396
+ entryPath: r.entry_path,
397
+ anchorType: r.anchor_type,
398
+ confidence: r.confidence,
399
+ rawAnchor: r.raw_anchor
400
+ }));
401
+ }
402
+ async writeSummary(path, summary) {
403
+ await writeFtsRow(this.db, this.scope, path, require_cjk.segmentCJK(summary));
404
+ }
405
+ async getSummary(path) {
406
+ return (await this.db.all(drizzle_orm.sql`SELECT summary FROM idx_fts WHERE scope = ${this.scope} AND entry_path = ${path} LIMIT 1`))[0]?.summary ?? null;
407
+ }
408
+ async queryFTS(text, limit, metaFilter, opts) {
409
+ try {
410
+ const sanitized = sanitizeFTS5(text);
411
+ if (!sanitized) return [];
412
+ const needsJoin = metaFilter && Object.keys(metaFilter).length > 0 || typeof opts?.domain === "string" && opts.domain.length > 0 || Array.isArray(opts?.domains) && opts.domains.length > 0 || typeof opts?.pathPrefix === "string" && opts.pathPrefix.length > 0;
413
+ let query;
414
+ if (needsJoin) {
415
+ const conditions = [
416
+ drizzle_orm.sql`idx_fts MATCH ${sanitized}`,
417
+ drizzle_orm.sql`idx_fts.scope = ${this.scope}`,
418
+ drizzle_orm.sql`ie.scope = ${this.scope}`
419
+ ];
420
+ if (opts?.domain) conditions.push(drizzle_orm.sql`ie.domain = ${opts.domain}`);
421
+ if (opts?.domains && opts.domains.length > 0) {
422
+ const ps = opts.domains.map((d) => drizzle_orm.sql`${d}`);
423
+ conditions.push(drizzle_orm.sql`ie.domain IN (${drizzle_orm.sql.join(ps, drizzle_orm.sql`, `)})`);
424
+ }
425
+ if (opts?.pathPrefix) conditions.push(drizzle_orm.sql`ie.entry_path LIKE ${`${opts.pathPrefix}%`}`);
426
+ for (const [key, expected] of Object.entries(metaFilter ?? {})) if (Array.isArray(expected)) {
427
+ if (expected.length === 0) return [];
428
+ const ps = expected.map((v) => drizzle_orm.sql`${v}`);
429
+ conditions.push(drizzle_orm.sql`json_extract(ie.meta, ${`$.${key}`}) IN (${drizzle_orm.sql.join(ps, drizzle_orm.sql`, `)})`);
430
+ } else conditions.push(drizzle_orm.sql`json_extract(ie.meta, ${`$.${key}`}) = ${expected}`);
431
+ const where = drizzle_orm.sql.join(conditions, drizzle_orm.sql` AND `);
432
+ query = limit != null ? drizzle_orm.sql`SELECT idx_fts.entry_path, idx_fts.rank FROM idx_fts INNER JOIN ds_indexed_entry ie ON idx_fts.entry_path = ie.entry_path WHERE ${where} ORDER BY idx_fts.rank LIMIT ${limit}` : drizzle_orm.sql`SELECT idx_fts.entry_path, idx_fts.rank FROM idx_fts INNER JOIN ds_indexed_entry ie ON idx_fts.entry_path = ie.entry_path WHERE ${where} ORDER BY idx_fts.rank`;
433
+ } else query = limit != null ? drizzle_orm.sql`SELECT entry_path, rank FROM idx_fts WHERE idx_fts MATCH ${sanitized} AND scope = ${this.scope} ORDER BY rank LIMIT ${limit}` : drizzle_orm.sql`SELECT entry_path, rank FROM idx_fts WHERE idx_fts MATCH ${sanitized} AND scope = ${this.scope} ORDER BY rank`;
434
+ return (await this.db.all(query)).map((r) => ({
435
+ entryPath: r.entry_path,
436
+ rank: -r.rank
437
+ }));
438
+ } catch {
439
+ return [];
440
+ }
441
+ }
442
+ async removeSummary(path) {
443
+ await deleteFtsRow(this.db, this.scope, path);
444
+ }
445
+ async writeEmbedding(path, embedding, domain) {
446
+ await this._vectorBackend.upsert(path, embedding, domain);
447
+ }
448
+ async queryVector(embedding, limit, opts) {
449
+ return this._vectorBackend.query(embedding, {
450
+ domain: opts?.domain,
451
+ domains: opts?.domains,
452
+ pathPrefix: opts?.pathPrefix
453
+ }, limit);
454
+ }
455
+ async removeEmbedding(path) {
456
+ await this._vectorBackend.delete(path);
457
+ }
458
+ embeddingCapability() {
459
+ return this._vectorBackend.capability();
460
+ }
461
+ async readCursor(consumerId) {
462
+ const rows = await this.db.all(drizzle_orm.sql`SELECT consumer_id, last_seq, watermarks, updated_at FROM idx_index_cursor WHERE scope = ${this.scope} AND consumer_id = ${consumerId} LIMIT 1`);
463
+ if (rows.length === 0) return null;
464
+ const row = rows[0];
465
+ return {
466
+ consumerId: row.consumer_id,
467
+ lastSeq: row.last_seq,
468
+ watermarks: row.watermarks ? JSON.parse(row.watermarks) : void 0,
469
+ updatedAt: row.updated_at
470
+ };
471
+ }
472
+ /**
473
+ * Set a cursor row's `last_seq` (+ watermarks/updated_at/served_at).
474
+ *
475
+ * NOTE (index-follower-dirty-path §4.2 cutover): the index FOLLOWER no longer
476
+ * calls this — after the changelog replay was deleted, `last_seq` is a DEAD
477
+ * COLUMN (nothing advances it during drain; scheduling rides `served_at` via
478
+ * `markCursorServed`). `writeCursor` now has NO production caller (the ops
479
+ * scripts write cursor rows via raw SQL, not this method); it is retained for
480
+ * the monotonic-guard test suite + as a stable backend primitive, so the
481
+ * `MAX(last_seq, excluded.last_seq)` guard (issue #935 BLOCK fix #2) stays
482
+ * correct there; watermarks/updated_at/served_at update unconditionally.
483
+ */
484
+ async writeCursor(consumerId, lastSeq, watermarks) {
485
+ const now = Date.now();
486
+ const watermarksJson = watermarks ? JSON.stringify(watermarks) : null;
487
+ if (await this.supportsCursorServedAt()) {
488
+ await this.db.run(drizzle_orm.sql`INSERT INTO idx_index_cursor (scope, consumer_id, last_seq, watermarks, updated_at, served_at)
489
+ VALUES (${this.scope}, ${consumerId}, ${lastSeq}, ${watermarksJson}, ${now}, ${now})
490
+ ON CONFLICT(scope, consumer_id) DO UPDATE SET
491
+ last_seq = MAX(last_seq, excluded.last_seq),
492
+ watermarks = excluded.watermarks,
493
+ updated_at = excluded.updated_at,
494
+ served_at = excluded.served_at`);
495
+ return;
496
+ }
497
+ await this.db.run(drizzle_orm.sql`INSERT INTO idx_index_cursor (scope, consumer_id, last_seq, watermarks, updated_at)
498
+ VALUES (${this.scope}, ${consumerId}, ${lastSeq}, ${watermarksJson}, ${now})
499
+ ON CONFLICT(scope, consumer_id) DO UPDATE SET
500
+ last_seq = MAX(last_seq, excluded.last_seq),
501
+ watermarks = excluded.watermarks,
502
+ updated_at = excluded.updated_at`);
503
+ }
504
+ async markCursorServed(consumerId) {
505
+ if (!await this.supportsCursorServedAt()) return;
506
+ await this.db.run(drizzle_orm.sql`UPDATE idx_index_cursor SET served_at = ${Date.now()} WHERE scope = ${this.scope} AND consumer_id = ${consumerId}`);
507
+ }
508
+ async initCursor(consumerId, registration) {
509
+ const now = Date.now();
510
+ if (await this.supportsCursorServedAt()) {
511
+ await this.db.run(drizzle_orm.sql`INSERT OR IGNORE INTO idx_index_cursor (scope, consumer_id, last_seq, watermarks, updated_at, served_at, instance_did, domains)
512
+ VALUES (${this.scope}, ${consumerId}, 0, NULL, ${now}, ${now}, ${registration?.instanceDid ?? null}, ${registration?.domainsJson ?? null})`);
513
+ return;
514
+ }
515
+ await this.db.run(drizzle_orm.sql`INSERT OR IGNORE INTO idx_index_cursor (scope, consumer_id, last_seq, watermarks, updated_at, instance_did, domains)
516
+ VALUES (${this.scope}, ${consumerId}, 0, NULL, ${now}, ${registration?.instanceDid ?? null}, ${registration?.domainsJson ?? null})`);
517
+ }
518
+ /**
519
+ * See {@link IndexBackend.listCursorScopes}. Deliberately NOT filtered by
520
+ * `this.scope` — this is the one cursor operation that must see EVERY
521
+ * scope in the D1, so the CF cron can discover work registered by ANY
522
+ * caller in ANY prior isolate, not just the scope this backend instance
523
+ * happens to be constructed for.
524
+ *
525
+ * `consumerId` is a FAMILY prefix, not an exact match (design.md §4.4
526
+ * addendum, owner decision 2026-07-05): every real cursor row's
527
+ * `consumer_id` is scope-qualified via `deriveFollowerConsumerId(scope)`
528
+ * ("index:{scope}") so a single shared backend/database can host multiple
529
+ * scopes without their cursor rows colliding — see follower.ts. Discovery
530
+ * doesn't know the scope in advance (that's its whole job), so it matches
531
+ * every row whose `consumer_id` starts with `"{consumerId}:"` instead of
532
+ * one exact value. Callers keep passing the bare `FOLLOWER_CONSUMER_ID`
533
+ * ("index") — this method appends the `:` separator internally.
534
+ */
535
+ async listCursorScopes(consumerId) {
536
+ return (await this.db.all(await this.supportsCursorServedAt() ? drizzle_orm.sql`SELECT scope, consumer_id, instance_did, domains, updated_at, served_at FROM idx_index_cursor WHERE consumer_id LIKE ${`${consumerId}:%`}` : drizzle_orm.sql`SELECT scope, consumer_id, instance_did, domains, updated_at FROM idx_index_cursor WHERE consumer_id LIKE ${`${consumerId}:%`}`)).map((r) => ({
537
+ scope: r.scope,
538
+ consumerId: r.consumer_id,
539
+ instanceDid: r.instance_did,
540
+ domainsJson: r.domains,
541
+ updatedAt: r.updated_at,
542
+ servedAt: r.served_at ?? r.updated_at
543
+ }));
544
+ }
545
+ async writeEntriesBatch(entries) {
546
+ for (const entry of entries) await this.writeEntry(entry);
547
+ }
548
+ async writeAnchorsBatch(items) {
549
+ for (const item of items) await this.writeAnchors(item.path, item.anchors);
550
+ }
551
+ async writeSummariesBatch(items) {
552
+ for (const item of items) await this.writeSummary(item.path, item.summary);
553
+ }
554
+ async purgeEntries(paths, opts) {
555
+ const skipVector = opts?.skipVector === true;
556
+ const d1 = this.d1BatchBinding();
557
+ if (d1 && skipVector && paths.length > 0) {
558
+ const stmts = [];
559
+ for (const path of paths) stmts.push(d1.prepare("DELETE FROM idx_anchor_data WHERE scope = ? AND entry_path = ?").bind(this.scope, path), d1.prepare("DELETE FROM idx_anchor_key WHERE scope = ? AND entry_path = ?").bind(this.scope, path), d1.prepare("DELETE FROM idx_fts WHERE rowid = (SELECT fts_rowid FROM idx_fts_rowid WHERE scope = ? AND entry_path = ?)").bind(this.scope, path), d1.prepare("DELETE FROM idx_fts_rowid WHERE scope = ? AND entry_path = ?").bind(this.scope, path), d1.prepare("DELETE FROM ds_indexed_entry WHERE scope = ? AND entry_path = ?").bind(this.scope, path));
560
+ for (const chunk of chunkArray(stmts, 50)) await d1.batch(chunk);
561
+ return paths.length;
562
+ }
563
+ for (const path of paths) {
564
+ await this.removeAnchors(path);
565
+ await this.removeSummary(path);
566
+ if (!skipVector) await this.removeEmbedding(path);
567
+ await this.removeEntry(path);
568
+ }
569
+ return paths.length;
570
+ }
571
+ async queryCombined(keys, text, opts) {
572
+ const anchorResults = keys.length > 0 ? await this.queryAnchors({
573
+ keys,
574
+ scope: opts?.scope,
575
+ minConfidence: opts?.minConfidence,
576
+ limit: opts?.limit
577
+ }) : [];
578
+ const anchorPaths = new Set(anchorResults.map((r) => r.entryPath));
579
+ const ftsResults = text ? await this.queryFTS(text, opts?.limit) : [];
580
+ const results = anchorResults.map((r) => {
581
+ const fts = ftsResults.find((f) => f.entryPath === r.entryPath);
582
+ return {
583
+ entryPath: r.entryPath,
584
+ anchorScore: r.score,
585
+ ftsRank: fts?.rank,
586
+ score: r.score + (fts?.rank ?? 0)
587
+ };
588
+ });
589
+ for (const f of ftsResults) if (!anchorPaths.has(f.entryPath)) results.push({
590
+ entryPath: f.entryPath,
591
+ ftsRank: f.rank,
592
+ score: f.rank
593
+ });
594
+ results.sort((a, b) => b.score - a.score);
595
+ if (opts?.limit != null) return results.slice(0, opts.limit);
596
+ return results;
597
+ }
598
+ };
599
+ /** Get-or-create the stable idx_fts rowid for (scope, path) via the mapping table. */
600
+ async function ensureFtsRowid(db, scope, path) {
601
+ await db.run(drizzle_orm.sql`INSERT INTO idx_fts_rowid (scope, entry_path) VALUES (${scope}, ${path})
602
+ ON CONFLICT(scope, entry_path) DO NOTHING`);
603
+ return (await db.all(drizzle_orm.sql`SELECT fts_rowid FROM idx_fts_rowid WHERE scope = ${scope} AND entry_path = ${path} LIMIT 1`))[0].fts_rowid;
604
+ }
605
+ /** Atomically insert-or-replace an entry's FTS row at its stable mapped rowid. */
606
+ async function writeFtsRow(db, scope, path, summary) {
607
+ const rowid = await ensureFtsRowid(db, scope, path);
608
+ await db.run(drizzle_orm.sql`INSERT OR REPLACE INTO idx_fts (rowid, scope, entry_path, summary) VALUES (${rowid}, ${scope}, ${path}, ${summary})`);
609
+ }
610
+ /** Delete an entry's idx_fts row (by mapped rowid, O(1)) and its mapping row. */
611
+ async function deleteFtsRow(db, scope, path) {
612
+ await db.run(drizzle_orm.sql`DELETE FROM idx_fts WHERE rowid = (SELECT fts_rowid FROM idx_fts_rowid WHERE scope = ${scope} AND entry_path = ${path})`);
613
+ await db.run(drizzle_orm.sql`DELETE FROM idx_fts_rowid WHERE scope = ${scope} AND entry_path = ${path}`);
614
+ }
615
+ function entryToRow(entry) {
616
+ return {
617
+ entry_path: entry.entryPath,
618
+ domain: entry.domain,
619
+ state: entry.state,
620
+ indexed_at: entry.indexedAt,
621
+ updated_at: entry.updatedAt,
622
+ anchor_count: entry.anchorCount,
623
+ source_path: entry.sourcePath ?? null,
624
+ source_hash: entry.sourceHash ?? null,
625
+ extractor_id: entry.extractorId ?? null,
626
+ meta: entry.meta != null ? JSON.stringify(entry.meta) : null
627
+ };
628
+ }
629
+ function rowToEntry(row) {
630
+ return {
631
+ entryPath: row.entry_path,
632
+ sourcePath: row.source_path ?? void 0,
633
+ sourceHash: row.source_hash ?? void 0,
634
+ domain: row.domain,
635
+ state: row.state,
636
+ extractorId: row.extractor_id ?? void 0,
637
+ indexedAt: row.indexed_at,
638
+ updatedAt: row.updated_at,
639
+ anchorCount: row.anchor_count,
640
+ meta: row.meta ? JSON.parse(row.meta) : void 0
641
+ };
642
+ }
643
+ function sqlEscape(value) {
644
+ return value.replace(/'/g, "''");
645
+ }
646
+ function chunkArray(items, size) {
647
+ const chunks = [];
648
+ for (let i = 0; i < items.length; i += size) chunks.push(items.slice(i, i + size));
649
+ return chunks;
650
+ }
651
+ function sanitizeFTS5(text) {
652
+ return text.replace(/["*^()\-+{}:]/g, " ").replace(/\s+/g, " ").trim();
653
+ }
654
+
655
+ //#endregion
656
+ exports.D1IndexBackend = D1IndexBackend;