@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,92 @@
1
+ //#region src/follower-scheduler.ts
2
+ function clampSpent(value, remaining) {
3
+ if (!Number.isFinite(value ?? 0) || (value ?? 0) <= 0) return 0;
4
+ return Math.min(value ?? 0, remaining);
5
+ }
6
+ /**
7
+ * Fairly spend one index-follower tick across a selected scope set.
8
+ *
9
+ * Each scope receives a small bounded slice first; scopes that still report
10
+ * `hasMore` are appended to the tail and can receive another slice only after
11
+ * the other selected scopes had their chance. The helper is intentionally
12
+ * storage/runtime-agnostic so Cloudflare and Node share the same starvation
13
+ * semantics while keeping different budget sizes.
14
+ */
15
+ async function runIndexFollowerRoundRobin(opts) {
16
+ let remainingProcessed = Math.max(0, opts.maxProcessed);
17
+ let remainingNoop = Math.max(0, opts.maxNoop);
18
+ let remainingScan = Math.max(0, opts.maxScan);
19
+ const perScopeMaxProcessed = Math.max(0, opts.perScopeMaxProcessed ?? 1);
20
+ const perScopeMaxNoop = Math.max(0, opts.perScopeMaxNoop ?? 2);
21
+ const perScopeMaxScan = Math.max(1, opts.perScopeMaxScan ?? 50);
22
+ const maxRuns = opts.maxRuns ?? opts.scopes.length + Math.ceil((remainingProcessed || 0) / Math.max(1, perScopeMaxProcessed || 1)) + Math.ceil((remainingNoop || 0) / Math.max(1, perScopeMaxNoop || 1)) + Math.ceil((remainingScan || 0) / Math.max(1, perScopeMaxScan));
23
+ const queue = [...opts.scopes];
24
+ const runs = [];
25
+ let indexedThisTick = 0;
26
+ let deletedThisTick = 0;
27
+ let noopedThisTick = 0;
28
+ let ackRaceThisTick = 0;
29
+ let stoppedBy = "drained";
30
+ let sawBudgetBlockedScope = false;
31
+ while (queue.length > 0) {
32
+ if (opts.shouldStop?.()) {
33
+ stoppedBy = "time";
34
+ break;
35
+ }
36
+ if (runs.length >= maxRuns) {
37
+ stoppedBy = "max_runs";
38
+ break;
39
+ }
40
+ if (remainingScan <= 0 || remainingProcessed <= 0 && remainingNoop <= 0) {
41
+ stoppedBy = "budget";
42
+ break;
43
+ }
44
+ const scope = queue.shift();
45
+ const limits = {
46
+ maxProcessed: Math.min(perScopeMaxProcessed, remainingProcessed),
47
+ maxNoop: Math.min(perScopeMaxNoop, remainingNoop),
48
+ maxScan: Math.min(perScopeMaxScan, remainingScan)
49
+ };
50
+ if (limits.maxScan <= 0 || limits.maxProcessed <= 0 && limits.maxNoop <= 0) {
51
+ stoppedBy = "budget";
52
+ break;
53
+ }
54
+ const result = await opts.runScope(scope, limits);
55
+ runs.push({
56
+ scope,
57
+ limits,
58
+ result
59
+ });
60
+ remainingProcessed -= clampSpent(result.processed, remainingProcessed);
61
+ remainingNoop -= clampSpent(result.noopProcessed, remainingNoop);
62
+ remainingScan -= clampSpent(result.scanned, remainingScan);
63
+ indexedThisTick += result.indexed;
64
+ deletedThisTick += result.deleted;
65
+ noopedThisTick += result.nooped;
66
+ if (result.ackRace) ackRaceThisTick++;
67
+ if (!result.advanced && result.hasMore && [
68
+ "processed_budget",
69
+ "noop_budget",
70
+ "scan_budget"
71
+ ].includes(result.stopReason ?? "")) sawBudgetBlockedScope = true;
72
+ if (result.advanced && result.hasMore && !result.aborted && remainingScan > 0 && (remainingProcessed > 0 || remainingNoop > 0)) queue.push(scope);
73
+ }
74
+ return {
75
+ runs,
76
+ processedThisTick: opts.maxProcessed - remainingProcessed,
77
+ noopThisTick: opts.maxNoop - remainingNoop,
78
+ indexedThisTick,
79
+ deletedThisTick,
80
+ noopedThisTick,
81
+ scannedThisTick: opts.maxScan - remainingScan,
82
+ ackRaceThisTick,
83
+ stoppedBy: stoppedBy === "drained" && sawBudgetBlockedScope ? "budget" : stoppedBy,
84
+ remainingProcessed,
85
+ remainingNoop,
86
+ remainingScan
87
+ };
88
+ }
89
+
90
+ //#endregion
91
+ export { runIndexFollowerRoundRobin };
92
+ //# sourceMappingURL=follower-scheduler.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"follower-scheduler.mjs","names":[],"sources":["../src/follower-scheduler.ts"],"sourcesContent":["import type { IndexFollowerBatchResult } from \"./follower.js\";\n\n// `maxScan` / `remainingScan` / `scannedThisTick` here count DIRTY ROWS fetched,\n// not changelog rows (the §13 cutover deleted the changelog scan). The `maxScan`\n// field key is kept as a stable public name across the budget chain — see\n// DEFAULT_MAX_SCAN_PER_TICK in follower.ts (§B: doc over public-API rename).\nexport interface IndexFollowerRunLimits {\n maxProcessed: number;\n maxNoop: number;\n maxScan: number;\n}\n\nexport interface IndexFollowerRoundRobinOptions<T> {\n scopes: T[];\n maxProcessed: number;\n maxNoop: number;\n maxScan: number;\n perScopeMaxProcessed?: number;\n perScopeMaxNoop?: number;\n perScopeMaxScan?: number;\n maxRuns?: number;\n shouldStop?: () => boolean;\n runScope: (scope: T, limits: IndexFollowerRunLimits) => Promise<IndexFollowerBatchResult>;\n}\n\nexport interface IndexFollowerRoundRobinRun<T> {\n scope: T;\n limits: IndexFollowerRunLimits;\n result: IndexFollowerBatchResult;\n}\n\nexport interface IndexFollowerRoundRobinResult<T> {\n runs: IndexFollowerRoundRobinRun<T>[];\n processedThisTick: number;\n noopThisTick: number;\n indexedThisTick: number;\n deletedThisTick: number;\n noopedThisTick: number;\n scannedThisTick: number;\n /**\n * Drain RUNS that lost the ack race-guard this tick (healthy concurrent-write\n * churn, not failures). Counted per `runScope` call, so a single hot scope\n * requeued within the tick can contribute more than once — this is a run\n * occurrence count, not a distinct-scope count.\n */\n ackRaceThisTick: number;\n stoppedBy: \"drained\" | \"budget\" | \"max_runs\" | \"time\";\n remainingProcessed: number;\n remainingNoop: number;\n remainingScan: number;\n}\n\nfunction clampSpent(value: number | undefined, remaining: number): number {\n if (!Number.isFinite(value ?? 0) || (value ?? 0) <= 0) return 0;\n return Math.min(value ?? 0, remaining);\n}\n\n/**\n * Fairly spend one index-follower tick across a selected scope set.\n *\n * Each scope receives a small bounded slice first; scopes that still report\n * `hasMore` are appended to the tail and can receive another slice only after\n * the other selected scopes had their chance. The helper is intentionally\n * storage/runtime-agnostic so Cloudflare and Node share the same starvation\n * semantics while keeping different budget sizes.\n */\nexport async function runIndexFollowerRoundRobin<T>(\n opts: IndexFollowerRoundRobinOptions<T>,\n): Promise<IndexFollowerRoundRobinResult<T>> {\n let remainingProcessed = Math.max(0, opts.maxProcessed);\n let remainingNoop = Math.max(0, opts.maxNoop);\n let remainingScan = Math.max(0, opts.maxScan);\n const perScopeMaxProcessed = Math.max(0, opts.perScopeMaxProcessed ?? 1);\n const perScopeMaxNoop = Math.max(0, opts.perScopeMaxNoop ?? 2);\n const perScopeMaxScan = Math.max(1, opts.perScopeMaxScan ?? 50);\n const maxRuns =\n opts.maxRuns ??\n opts.scopes.length +\n Math.ceil((remainingProcessed || 0) / Math.max(1, perScopeMaxProcessed || 1)) +\n Math.ceil((remainingNoop || 0) / Math.max(1, perScopeMaxNoop || 1)) +\n Math.ceil((remainingScan || 0) / Math.max(1, perScopeMaxScan));\n const queue = [...opts.scopes];\n const runs: IndexFollowerRoundRobinRun<T>[] = [];\n let indexedThisTick = 0;\n let deletedThisTick = 0;\n let noopedThisTick = 0;\n let ackRaceThisTick = 0;\n let stoppedBy: IndexFollowerRoundRobinResult<T>[\"stoppedBy\"] = \"drained\";\n let sawBudgetBlockedScope = false;\n\n while (queue.length > 0) {\n if (opts.shouldStop?.()) {\n stoppedBy = \"time\";\n break;\n }\n if (runs.length >= maxRuns) {\n stoppedBy = \"max_runs\";\n break;\n }\n if (remainingScan <= 0 || (remainingProcessed <= 0 && remainingNoop <= 0)) {\n stoppedBy = \"budget\";\n break;\n }\n\n const scope = queue.shift()!;\n const limits: IndexFollowerRunLimits = {\n maxProcessed: Math.min(perScopeMaxProcessed, remainingProcessed),\n maxNoop: Math.min(perScopeMaxNoop, remainingNoop),\n maxScan: Math.min(perScopeMaxScan, remainingScan),\n };\n if (limits.maxScan <= 0 || (limits.maxProcessed <= 0 && limits.maxNoop <= 0)) {\n stoppedBy = \"budget\";\n break;\n }\n\n const result = await opts.runScope(scope, limits);\n runs.push({ scope, limits, result });\n\n remainingProcessed -= clampSpent(result.processed, remainingProcessed);\n remainingNoop -= clampSpent(result.noopProcessed, remainingNoop);\n remainingScan -= clampSpent(result.scanned, remainingScan);\n indexedThisTick += result.indexed;\n deletedThisTick += result.deleted;\n noopedThisTick += result.nooped;\n if (result.ackRace) ackRaceThisTick++;\n if (\n !result.advanced &&\n result.hasMore &&\n [\"processed_budget\", \"noop_budget\", \"scan_budget\"].includes(result.stopReason ?? \"\")\n ) {\n sawBudgetBlockedScope = true;\n }\n\n if (\n result.advanced &&\n result.hasMore &&\n !result.aborted &&\n remainingScan > 0 &&\n (remainingProcessed > 0 || remainingNoop > 0)\n ) {\n queue.push(scope);\n }\n }\n\n return {\n runs,\n processedThisTick: opts.maxProcessed - remainingProcessed,\n noopThisTick: opts.maxNoop - remainingNoop,\n indexedThisTick,\n deletedThisTick,\n noopedThisTick,\n scannedThisTick: opts.maxScan - remainingScan,\n ackRaceThisTick,\n stoppedBy: stoppedBy === \"drained\" && sawBudgetBlockedScope ? \"budget\" : stoppedBy,\n remainingProcessed,\n remainingNoop,\n remainingScan,\n };\n}\n"],"mappings":";AAoDA,SAAS,WAAW,OAA2B,WAA2B;AACxE,KAAI,CAAC,OAAO,SAAS,SAAS,EAAE,KAAK,SAAS,MAAM,EAAG,QAAO;AAC9D,QAAO,KAAK,IAAI,SAAS,GAAG,UAAU;;;;;;;;;;;AAYxC,eAAsB,2BACpB,MAC2C;CAC3C,IAAI,qBAAqB,KAAK,IAAI,GAAG,KAAK,aAAa;CACvD,IAAI,gBAAgB,KAAK,IAAI,GAAG,KAAK,QAAQ;CAC7C,IAAI,gBAAgB,KAAK,IAAI,GAAG,KAAK,QAAQ;CAC7C,MAAM,uBAAuB,KAAK,IAAI,GAAG,KAAK,wBAAwB,EAAE;CACxE,MAAM,kBAAkB,KAAK,IAAI,GAAG,KAAK,mBAAmB,EAAE;CAC9D,MAAM,kBAAkB,KAAK,IAAI,GAAG,KAAK,mBAAmB,GAAG;CAC/D,MAAM,UACJ,KAAK,WACL,KAAK,OAAO,SACV,KAAK,MAAM,sBAAsB,KAAK,KAAK,IAAI,GAAG,wBAAwB,EAAE,CAAC,GAC7E,KAAK,MAAM,iBAAiB,KAAK,KAAK,IAAI,GAAG,mBAAmB,EAAE,CAAC,GACnE,KAAK,MAAM,iBAAiB,KAAK,KAAK,IAAI,GAAG,gBAAgB,CAAC;CAClE,MAAM,QAAQ,CAAC,GAAG,KAAK,OAAO;CAC9B,MAAM,OAAwC,EAAE;CAChD,IAAI,kBAAkB;CACtB,IAAI,kBAAkB;CACtB,IAAI,iBAAiB;CACrB,IAAI,kBAAkB;CACtB,IAAI,YAA2D;CAC/D,IAAI,wBAAwB;AAE5B,QAAO,MAAM,SAAS,GAAG;AACvB,MAAI,KAAK,cAAc,EAAE;AACvB,eAAY;AACZ;;AAEF,MAAI,KAAK,UAAU,SAAS;AAC1B,eAAY;AACZ;;AAEF,MAAI,iBAAiB,KAAM,sBAAsB,KAAK,iBAAiB,GAAI;AACzE,eAAY;AACZ;;EAGF,MAAM,QAAQ,MAAM,OAAO;EAC3B,MAAM,SAAiC;GACrC,cAAc,KAAK,IAAI,sBAAsB,mBAAmB;GAChE,SAAS,KAAK,IAAI,iBAAiB,cAAc;GACjD,SAAS,KAAK,IAAI,iBAAiB,cAAc;GAClD;AACD,MAAI,OAAO,WAAW,KAAM,OAAO,gBAAgB,KAAK,OAAO,WAAW,GAAI;AAC5E,eAAY;AACZ;;EAGF,MAAM,SAAS,MAAM,KAAK,SAAS,OAAO,OAAO;AACjD,OAAK,KAAK;GAAE;GAAO;GAAQ;GAAQ,CAAC;AAEpC,wBAAsB,WAAW,OAAO,WAAW,mBAAmB;AACtE,mBAAiB,WAAW,OAAO,eAAe,cAAc;AAChE,mBAAiB,WAAW,OAAO,SAAS,cAAc;AAC1D,qBAAmB,OAAO;AAC1B,qBAAmB,OAAO;AAC1B,oBAAkB,OAAO;AACzB,MAAI,OAAO,QAAS;AACpB,MACE,CAAC,OAAO,YACR,OAAO,WACP;GAAC;GAAoB;GAAe;GAAc,CAAC,SAAS,OAAO,cAAc,GAAG,CAEpF,yBAAwB;AAG1B,MACE,OAAO,YACP,OAAO,WACP,CAAC,OAAO,WACR,gBAAgB,MACf,qBAAqB,KAAK,gBAAgB,GAE3C,OAAM,KAAK,MAAM;;AAIrB,QAAO;EACL;EACA,mBAAmB,KAAK,eAAe;EACvC,cAAc,KAAK,UAAU;EAC7B;EACA;EACA;EACA,iBAAiB,KAAK,UAAU;EAChC;EACA,WAAW,cAAc,aAAa,wBAAwB,WAAW;EACzE;EACA;EACA;EACD"}
@@ -0,0 +1,577 @@
1
+ const require_field_map = require('./extraction/field-map.cjs');
2
+ let _aigne_afs = require("@aigne/afs");
3
+ let ufo = require("ufo");
4
+
5
+ //#region src/follower.ts
6
+ /**
7
+ * IndexFollower — DRAINS the source-side dirty queue (`ds_dirty_path`, the merged
8
+ * current-state projection of the changelog) and incrementally indexes file
9
+ * writes/deletes into the index backend. Dirty drain is the SOLE mechanism —
10
+ * the changelog `/.changelog` replay was deleted at the §13 cutover; a follower
11
+ * with no `dirtyQueue` is inert.
12
+ *
13
+ * Two entry points:
14
+ * runCatchUp() — inline drain; called from execQuery so a query always sees a
15
+ * fresh index (processes whatever dirty rows are pending).
16
+ * runBatch() — bounded per-tick drain (maxProcessed/maxNoop/maxScan caps);
17
+ * called from the cron task.
18
+ *
19
+ * Cursor: a `consumer_id` row in `idx_index_cursor` registers the follower and
20
+ * carries its instanceDid + domains snapshot for durable cron discovery. Its
21
+ * `last_seq` is a DEAD column post-cutover (nothing advances it during drain —
22
+ * §4.2); scheduling fairness rides `served_at` (markCursorServed). All domains a
23
+ * SINGLE blocklet declares share one cursor; DIFFERENT blocklets under the same
24
+ * caller each get their own (their `/user` data lives in separate instance
25
+ * spaces / dirty-queue prefixes).
26
+ *
27
+ * Consumer-id identity (design.md §4.4 decision-lock addendum, owner
28
+ * 2026-07-05; per-instance fix): a single shared backend hosts MULTIPLE
29
+ * `(scope, instanceDid)` pairs at once — Node's one-shared-SQLite-file rework
30
+ * of #936 P4, and CF's one-shared-D1 model. So the bare `FOLLOWER_CONSUMER_ID`
31
+ * isn't enough to identify ONE follower's cursor row;
32
+ * `deriveFollowerConsumerId(scope, instanceDid)` folds BOTH in
33
+ * (`index:{scope}:{instanceDid}`), the same compositing technique already used
34
+ * for domain-name isolation (`{declared name}:{scope}`). Keying by scope ALONE
35
+ * (the pre-fix behavior) let a caller's SECOND index-blocklet collide on the
36
+ * first's cursor row (`INSERT OR IGNORE` → dropped) so its changelog was never
37
+ * followed. D1 additionally isolates via its own `scope` column (PRIMARY KEY
38
+ * (scope, consumer_id)); both runtimes derive the consumer_id the same way so a
39
+ * persisted row means the same thing everywhere (`IndexBackend.listCursorScopes`
40
+ * matches on the `{FOLLOWER_CONSUMER_ID}:` PREFIX for exactly this reason — see
41
+ * d1-index-backend.ts).
42
+ */
43
+ const log = (0, _aigne_afs.makeNsLog)("provider:index");
44
+ const FOLLOWER_CONSUMER_ID = "index";
45
+ /**
46
+ * Derive a follower consumer_id that isolates per `(scope, instanceDid)` —
47
+ * `"index:{scope}:{instanceDid}"`. A single shared backend (Node's `/user/index`
48
+ * file, CF's D1) hosts many callers AND many blocklets per caller; the cursor
49
+ * row must be unique per (caller scope × blocklet instance) because each
50
+ * blocklet follows its OWN instance-space changelog. `instanceDid` is optional
51
+ * for backward compatibility (bare `index:{scope}` for pre-fix rows / callers
52
+ * that don't supply it); passing the bare `FOLLOWER_CONSUMER_ID` is only safe
53
+ * for single-scope callers (tests, standalone use).
54
+ */
55
+ function deriveFollowerConsumerId(scope, instanceDid) {
56
+ return instanceDid ? `${FOLLOWER_CONSUMER_ID}:${scope}:${instanceDid}` : `${FOLLOWER_CONSUMER_ID}:${scope}`;
57
+ }
58
+ /**
59
+ * Memory-map key for a caller's per-blocklet index resources (the AFSIndex
60
+ * handle, its in-flight build promise, its IndexFollower). Isolation is
61
+ * per-`(scope, instanceDid)`: the SAME caller (scope) gets a SEPARATE index per
62
+ * blocklet (instanceDid). Keying these isolate-lived maps by scope ALONE let a
63
+ * caller's second index-blocklet reuse the FIRST's handle/follower (wrong bound
64
+ * domains + wrong instance-space changelog) — the in-memory twin of the cursor
65
+ * collision `deriveFollowerConsumerId` guards against.
66
+ */
67
+ function userIndexKey(scope, instanceDid) {
68
+ return instanceDid ? `${scope}::${instanceDid}` : scope;
69
+ }
70
+ const DEFAULT_MAX_PROCESSED_PER_TICK = 4;
71
+ const DEFAULT_MAX_NOOP_PER_TICK = 12;
72
+ const DEFAULT_MAX_SCAN_PER_TICK = 400;
73
+ /** Preferred alias for {@link DEFAULT_MAX_SCAN_PER_TICK} in dirty-drain call sites. */
74
+ const DEFAULT_MAX_DIRTY_ROWS_PER_TICK = DEFAULT_MAX_SCAN_PER_TICK;
75
+ const DIRTY_RETRY_BACKOFF_MS = 6e4;
76
+ function toAbsolute(path) {
77
+ return (0, ufo.joinURL)("/", path);
78
+ }
79
+ /**
80
+ * Internal signal thrown by `indexEntry` / the dirty-drain remove path when a
81
+ * read/stat/index failed on content that is STILL current (issue #935 BLOCK
82
+ * fix #3 / sync-engine parity): `processDirty` catches this and BACKS OFF the
83
+ * dirty row (marks it, doesn't ack) so the next tick retries it instead of
84
+ * silently losing the write. (Historically named for the changelog replay that
85
+ * the dirty-path cutover deleted; kept as the dirty-drain retry signal.)
86
+ */
87
+ var ChangelogAbortError = class extends Error {
88
+ constructor(originalPath, reason = "transient read failure") {
89
+ super(`${reason} for "${originalPath}" — retry next run`);
90
+ this.name = "ChangelogAbortError";
91
+ }
92
+ };
93
+ var IndexFollower = class {
94
+ spaceAfs;
95
+ backend;
96
+ domains;
97
+ consumerId;
98
+ embed;
99
+ catchUpLimits;
100
+ dirtyQueue;
101
+ constructor(opts) {
102
+ this.spaceAfs = opts.spaceAfs;
103
+ this.backend = opts.backend;
104
+ this.domains = opts.domains;
105
+ this.consumerId = opts.consumerId ?? FOLLOWER_CONSUMER_ID;
106
+ this.embed = opts.embed;
107
+ this.dirtyQueue = opts.dirtyQueue;
108
+ }
109
+ /**
110
+ * Override the inline `runCatchUp()` budget — the per-invocation caps on
111
+ * matched PUT attempts, matched delete/remove rows, and dirty rows FETCHED
112
+ * (`maxScan`). Injected by the runtime from the SAME env group the cron uses
113
+ * (`AFS_INDEX_MAX_*_PER_TICK`) so both indexing paths share one budget group.
114
+ * Unset → provider defaults (standalone/tests).
115
+ */
116
+ setCatchUpLimits(limits) {
117
+ this.catchUpLimits = limits;
118
+ }
119
+ /**
120
+ * Wire (or replace) the passage embedder used at index time — semantic recall
121
+ * (Epic #932). Called by `AFSIndex.setFollower` (query-path follower) and by
122
+ * `rebuildFollowerFromCursor` (CF cron-path follower) so BOTH indexing paths
123
+ * write embeddings; without this a changelog-indexed entry has only FTS +
124
+ * anchors and cross-lingual / semantic queries can't match it. A no-op
125
+ * embedder (or a backend without `writeEmbedding`) degrades cleanly to
126
+ * FTS+anchor.
127
+ */
128
+ setEmbedder(embed) {
129
+ this.embed = embed;
130
+ }
131
+ /**
132
+ * Lightweight catch-up: drain any ready dirty rows so a query sees a fresh
133
+ * index without waiting for the next cron tick. Called from execQuery.
134
+ *
135
+ * Dirty drain is the SOLE catch-up mechanism (index-follower-dirty-path §13
136
+ * cutover — the changelog `/.changelog` replay was deleted). Freshness = "are
137
+ * there ready dirty rows under my prefix", so there is no `last_seq` probe: a
138
+ * query sees a fresh index by processing whatever dirty paths are pending
139
+ * (design §4.2, §8). A follower with no dirty queue (standalone/legacy) is a
140
+ * no-op here — it has no source to drain.
141
+ */
142
+ async runCatchUp() {
143
+ if (!this.dirtyQueue) return;
144
+ await this.processDirty({
145
+ maxProcessed: this.catchUpLimits?.maxProcessed ?? DEFAULT_MAX_PROCESSED_PER_TICK,
146
+ maxNoop: this.catchUpLimits?.maxNoop ?? DEFAULT_MAX_NOOP_PER_TICK,
147
+ maxDirtyRows: this.catchUpLimits?.maxScan ?? DEFAULT_MAX_DIRTY_ROWS_PER_TICK
148
+ });
149
+ }
150
+ /**
151
+ * Bounded per-tick drain of the dirty queue — attempt up to `maxProcessed`
152
+ * matched PUT rows, up to `maxNoop` matched delete/remove/noop rows, fetching
153
+ * at most `maxScan` (a.k.a. `maxDirtyRows`) dirty rows. Called from the cron.
154
+ *
155
+ * Dirty drain is the SOLE mechanism (index-follower-dirty-path §13 cutover —
156
+ * changelog replay was deleted). A follower with no dirty queue has no source
157
+ * to drain, so it is idle. Separate budgets are intentional: matched PUT rows
158
+ * are the expensive read+embed+write axis, delete/noop rows are cheaper.
159
+ */
160
+ async runBatch(limits) {
161
+ if (!this.dirtyQueue) return this.idleResult();
162
+ return this.processDirty({
163
+ maxProcessed: limits?.maxProcessed ?? DEFAULT_MAX_PROCESSED_PER_TICK,
164
+ maxNoop: limits?.maxNoop ?? DEFAULT_MAX_NOOP_PER_TICK,
165
+ maxDirtyRows: limits?.maxDirtyRows ?? limits?.maxScan ?? DEFAULT_MAX_DIRTY_ROWS_PER_TICK
166
+ });
167
+ }
168
+ /** Empty batch result (no dirty queue wired / nothing to drain). */
169
+ idleResult() {
170
+ return {
171
+ scanned: 0,
172
+ processed: 0,
173
+ noopProcessed: 0,
174
+ indexed: 0,
175
+ deleted: 0,
176
+ nooped: 0,
177
+ advanced: false,
178
+ hasMore: false,
179
+ ackRace: false,
180
+ stopReason: "exhausted",
181
+ fromSeq: 0,
182
+ toSeq: 0,
183
+ matched: 0,
184
+ unmatched: 0,
185
+ skipped: 0,
186
+ failed: 0,
187
+ aborted: false
188
+ };
189
+ }
190
+ /**
191
+ * Cold rebuild of an EXPLICIT list of space-relative paths — the enumeration
192
+ * is done by the caller (an external driver listing the live tree via D1),
193
+ * NOT in this process. Reads CURRENT live state per path and writes the
194
+ * derived index layers, independent of any queue — so it is the seam the
195
+ * offline reset-and-rebuild (`offlineRebuildScope`) uses to rebuild from live
196
+ * `ds_entries`. Keeping the per-call cost to just `paths.length`
197
+ * read+embed+writes with ZERO in-Worker tree enumeration is what keeps a small
198
+ * batch inside the CF free-tier per-request CPU/subrequest budget (an in-Worker
199
+ * `maxDepth:-1` enumeration pushed cold isolates over that budget into 503s).
200
+ * Each path is matched against the declared domains and indexed through the
201
+ * same `indexEntry` as the drain path (byte-identical output). `paths` are
202
+ * space-relative, e.g. `"items/foo.json"` (leading slash tolerated).
203
+ */
204
+ async reindexPaths(paths, onProgress) {
205
+ let indexed = 0;
206
+ let failed = 0;
207
+ let skipped = 0;
208
+ let done = 0;
209
+ for (const raw of paths) {
210
+ const rel = raw.replace(/^\/+/, "");
211
+ let matched = false;
212
+ for (const domain of this.domains) {
213
+ if (!this.pathMatchesDomain(rel, domain)) continue;
214
+ matched = true;
215
+ try {
216
+ if (await this.indexEntry(domain, rel, this.makeEntryPath(domain, rel)) === "indexed") indexed++;
217
+ } catch (err) {
218
+ failed++;
219
+ log.warn(`reindex failed: ${rel}`, err instanceof Error ? err.message : err);
220
+ }
221
+ }
222
+ if (!matched) skipped++;
223
+ done++;
224
+ if (onProgress && (done % 50 === 0 || done === paths.length)) onProgress(done, paths.length);
225
+ }
226
+ return {
227
+ indexed,
228
+ failed,
229
+ skipped
230
+ };
231
+ }
232
+ /**
233
+ * Dirty drain (index-follower-dirty-path §8) — the O(unique dirty paths)
234
+ * replacement for changelog replay. Fetches ready dirty rows under this
235
+ * follower's prefix, and for each: domain-matches the stripped `originalPath`,
236
+ * reads CURRENT live state, and indexes / removes accordingly, then acks with
237
+ * the `latestSeq` race guard. A transient failure marks the row with a backoff
238
+ * (so a poison row can't refill the tick) instead of acking.
239
+ *
240
+ * Dirty mode NEVER writes `idx_index_cursor.last_seq` (design §4.2 — it is a
241
+ * dead column post-cutover); scheduling reads `advanced/hasMore/scanned` +
242
+ * the dirty count. The `dirtyQueue.listDirtyPaths({ prefix: "" })` call assumes
243
+ * a fragment-scoped queue (PrefixedTreeIndex), so `row.path` is already the
244
+ * stripped `originalPath` the domain globs + `spaceAfs.read` expect.
245
+ */
246
+ async processDirty(limits) {
247
+ const result0 = () => ({
248
+ scanned: 0,
249
+ processed: 0,
250
+ noopProcessed: 0,
251
+ indexed: 0,
252
+ deleted: 0,
253
+ nooped: 0,
254
+ advanced: false,
255
+ hasMore: false,
256
+ ackRace: false,
257
+ stopReason: "exhausted",
258
+ fromSeq: 0,
259
+ toSeq: 0,
260
+ matched: 0,
261
+ unmatched: 0,
262
+ skipped: 0,
263
+ failed: 0,
264
+ aborted: false
265
+ });
266
+ const dirtyQueue = this.dirtyQueue;
267
+ if (!dirtyQueue) return result0();
268
+ const maxDirtyRows = limits.maxDirtyRows > 0 ? limits.maxDirtyRows : 0;
269
+ if (maxDirtyRows <= 0 || limits.maxProcessed <= 0 && limits.maxNoop <= 0) return result0();
270
+ let processedRemaining = limits.maxProcessed;
271
+ let noopRemaining = limits.maxNoop;
272
+ let scanned = 0;
273
+ let processed = 0;
274
+ let noopProcessed = 0;
275
+ let indexed = 0;
276
+ let deleted = 0;
277
+ let nooped = 0;
278
+ let matched = 0;
279
+ let unmatched = 0;
280
+ let failed = 0;
281
+ let advanced = false;
282
+ let hasMore = false;
283
+ let ackRace = false;
284
+ let stopReason;
285
+ const now = Date.now();
286
+ const rows = await dirtyQueue.listDirtyPaths({
287
+ prefix: "",
288
+ limit: maxDirtyRows,
289
+ now
290
+ });
291
+ for (const row of rows) {
292
+ scanned++;
293
+ const matchedDomains = this.domains.filter((d) => this.pathMatchesDomain(row.path, d));
294
+ if (matchedDomains.length === 0) {
295
+ if (noopRemaining <= 0) {
296
+ hasMore = true;
297
+ stopReason = "noop_budget";
298
+ break;
299
+ }
300
+ unmatched++;
301
+ noopRemaining--;
302
+ noopProcessed++;
303
+ if (await dirtyQueue.ackDirtyPath(row.path, row.latestSeq)) advanced = true;
304
+ else {
305
+ hasMore = true;
306
+ ackRace = true;
307
+ }
308
+ continue;
309
+ }
310
+ const isDelete = row.op === "delete";
311
+ if (isDelete) {
312
+ if (noopRemaining <= 0) {
313
+ hasMore = true;
314
+ stopReason = "noop_budget";
315
+ break;
316
+ }
317
+ } else if (processedRemaining <= 0) {
318
+ hasMore = true;
319
+ stopReason = "processed_budget";
320
+ break;
321
+ }
322
+ try {
323
+ let didIndex = 0;
324
+ let didRemove = 0;
325
+ if (isDelete) {
326
+ await this.removeDirty(matchedDomains, row.path);
327
+ didRemove = 1;
328
+ } else {
329
+ const r = await this.indexPutDirty(matchedDomains, row.path);
330
+ didIndex = r.indexed;
331
+ didRemove = r.removed;
332
+ }
333
+ matched++;
334
+ if (isDelete) {
335
+ noopRemaining--;
336
+ noopProcessed++;
337
+ deleted++;
338
+ } else if (didIndex > 0) {
339
+ processedRemaining--;
340
+ processed++;
341
+ indexed++;
342
+ } else if (didRemove > 0) {
343
+ noopProcessed++;
344
+ deleted++;
345
+ } else nooped++;
346
+ if (await dirtyQueue.ackDirtyPath(row.path, row.latestSeq)) advanced = true;
347
+ else {
348
+ advanced = true;
349
+ hasMore = true;
350
+ ackRace = true;
351
+ }
352
+ } catch (err) {
353
+ failed++;
354
+ const msg = err instanceof Error ? err.message : String(err);
355
+ await dirtyQueue.markDirtyPathError(row.path, row.latestSeq, msg, now + DIRTY_RETRY_BACKOFF_MS);
356
+ log.warn(`dirty drain failed (backed off): ${row.path}`, msg);
357
+ }
358
+ if (processedRemaining <= 0 && noopRemaining <= 0) {
359
+ hasMore = true;
360
+ stopReason = "processed_budget";
361
+ break;
362
+ }
363
+ }
364
+ if (!stopReason) {
365
+ if (rows.length >= maxDirtyRows && maxDirtyRows > 0) {
366
+ if ((await dirtyQueue.listDirtyPaths({
367
+ prefix: "",
368
+ limit: 1,
369
+ now
370
+ })).length > 0) hasMore = true;
371
+ }
372
+ stopReason = hasMore ? "scan_budget" : "exhausted";
373
+ }
374
+ log.debug("dirty drain processed", {
375
+ consumerId: this.consumerId,
376
+ scanned,
377
+ processed,
378
+ noopProcessed,
379
+ indexed,
380
+ deleted,
381
+ nooped,
382
+ matched,
383
+ unmatched,
384
+ failed,
385
+ advanced,
386
+ hasMore,
387
+ ackRace,
388
+ stopReason
389
+ });
390
+ return {
391
+ scanned,
392
+ processed,
393
+ noopProcessed,
394
+ indexed,
395
+ deleted,
396
+ nooped,
397
+ advanced,
398
+ hasMore,
399
+ ackRace,
400
+ stopReason,
401
+ fromSeq: 0,
402
+ toSeq: 0,
403
+ matched,
404
+ unmatched,
405
+ skipped: 0,
406
+ failed,
407
+ aborted: false
408
+ };
409
+ }
410
+ /** Remove the indexed doc(s) for a delete/removed dirty row across matched domains. */
411
+ async removeDirty(matchedDomains, originalPath) {
412
+ for (const domain of matchedDomains) await this.removeIndexed(this.makeEntryPath(domain, originalPath), originalPath);
413
+ }
414
+ /** Shared index-removal (delete branch) — throws ChangelogAbortError on failure so the row backs off. */
415
+ async removeIndexed(entryPath, originalPath) {
416
+ try {
417
+ if (this.backend.removeIndexedDocument) await this.backend.removeIndexedDocument(entryPath);
418
+ else {
419
+ await this.backend.removeAnchors(entryPath);
420
+ await this.backend.removeSummary(entryPath);
421
+ await this.backend.removeEntry(entryPath);
422
+ }
423
+ } catch (err) {
424
+ throw new ChangelogAbortError(originalPath, `index delete failed: ${err instanceof Error ? err.message : String(err)}`);
425
+ }
426
+ }
427
+ /**
428
+ * Handle a `put` dirty row against CURRENT live state (design §4.3 / §8 step 6):
429
+ * - live exists → indexEntry (writes FTS/anchor/summary/embedding).
430
+ * - live missing (authoritative 404) → remove the indexed doc.
431
+ * - stat throws a NON-404 error → transient → ChangelogAbortError (retry).
432
+ * This is what makes a stale put whose file was deleted converge to "removed",
433
+ * while a replica-lag / D1 blip is NEVER mistaken for a deletion.
434
+ */
435
+ async indexPutDirty(matchedDomains, originalPath) {
436
+ let live;
437
+ try {
438
+ live = (await this.spaceAfs.stat?.(toAbsolute(originalPath)))?.data;
439
+ } catch (err) {
440
+ if (err instanceof _aigne_afs.AFSNotFoundError) live = void 0;
441
+ else throw new ChangelogAbortError(originalPath, `stat failed: ${err instanceof Error ? err.message : String(err)}`);
442
+ }
443
+ if (!live) {
444
+ await this.removeDirty(matchedDomains, originalPath);
445
+ return {
446
+ indexed: 0,
447
+ removed: 1
448
+ };
449
+ }
450
+ let indexed = 0;
451
+ for (const domain of matchedDomains) if (await this.indexEntry(domain, originalPath, this.makeEntryPath(domain, originalPath)) === "indexed") indexed++;
452
+ return {
453
+ indexed,
454
+ removed: 0
455
+ };
456
+ }
457
+ /**
458
+ * Does `originalPath` STILL EXIST (via `stat`)? Used to classify a `read()`
459
+ * failure (issue #935 BLOCK fix #3 / sync-engine parity):
460
+ * - stat 404 (or throws) → the file is gone (later delete, or never existed
461
+ * at this position) → the row is stale, safe to skip.
462
+ * - stat succeeds → the file is live; the read failure was transient (R2/D1
463
+ * blip) and must be retried, not lost.
464
+ * Post-cutover the drain only ever indexes live state (cid is never compared —
465
+ * supersession is caught by the ack race-guard), so existence is the only
466
+ * question left here.
467
+ */
468
+ async entryStillCurrent(originalPath) {
469
+ try {
470
+ return !!(await this.spaceAfs.stat?.(toAbsolute(originalPath)))?.data;
471
+ } catch {
472
+ return false;
473
+ }
474
+ }
475
+ pathMatchesDomain(originalPath, domain) {
476
+ if (!domain.indexable || domain.indexable.length === 0) return true;
477
+ return domain.indexable.some((pat) => matchGlob(originalPath, pat));
478
+ }
479
+ async indexEntry(domain, originalPath, entryPath) {
480
+ let content;
481
+ try {
482
+ const raw = (await this.spaceAfs.read?.(toAbsolute(originalPath)))?.data?.content;
483
+ if (typeof raw !== "string" || raw.length === 0) return "noop";
484
+ try {
485
+ content = JSON.parse(raw);
486
+ } catch {
487
+ content = raw;
488
+ }
489
+ } catch {
490
+ if (await this.entryStillCurrent(originalPath)) throw new ChangelogAbortError(originalPath);
491
+ return "noop";
492
+ }
493
+ const now = (/* @__PURE__ */ new Date()).toISOString();
494
+ const anchors = [];
495
+ if (domain.fields && typeof content === "object" && content !== null) {
496
+ const fieldValues = require_field_map.applyFieldMap(content, domain.fields);
497
+ for (const [fieldName, values] of Object.entries(fieldValues)) for (const value of values) if (fieldName === "time") anchors.push({
498
+ type: "time",
499
+ confidence: 1,
500
+ timestamp: value
501
+ });
502
+ else anchors.push({
503
+ type: fieldName,
504
+ confidence: 1,
505
+ value
506
+ });
507
+ }
508
+ const entry = {
509
+ entryPath,
510
+ sourcePath: originalPath,
511
+ domain: domain.name,
512
+ state: "active",
513
+ indexedAt: now,
514
+ updatedAt: now,
515
+ anchorCount: anchors.length
516
+ };
517
+ let summary;
518
+ if (domain.summary) {
519
+ summary = require_field_map.extractSummaryText(content, domain.summary);
520
+ if (domain.enrich && typeof content === "object" && content !== null) summary = await require_field_map.applyEnrichContentFile({
521
+ afs: this.spaceAfs,
522
+ itemJson: content,
523
+ baseSummary: summary,
524
+ enrich: domain.enrich,
525
+ scope: domain.scope
526
+ });
527
+ }
528
+ try {
529
+ if (this.backend.writeIndexedDocument) await this.backend.writeIndexedDocument({
530
+ entry,
531
+ anchors,
532
+ summary
533
+ });
534
+ else {
535
+ await this.backend.writeEntry(entry);
536
+ await this.backend.writeAnchors(entryPath, anchors);
537
+ if (summary) await this.backend.writeSummary(entryPath, summary);
538
+ }
539
+ } catch (err) {
540
+ throw new ChangelogAbortError(originalPath, `index write failed: ${err instanceof Error ? err.message : String(err)}`);
541
+ }
542
+ if (summary) {
543
+ if (this.embed && this.backend.writeEmbedding) try {
544
+ const vector = await this.embed(summary);
545
+ if (vector) await this.backend.writeEmbedding(entryPath, vector, domain.name);
546
+ else log.debug("embed skipped (embedder returned null)", { entryPath });
547
+ } catch (err) {
548
+ log.debug("embed failed (entry kept, FTS/anchor only)", {
549
+ entryPath,
550
+ error: err instanceof Error ? err.message : String(err)
551
+ });
552
+ }
553
+ }
554
+ return "indexed";
555
+ }
556
+ /**
557
+ * Build the stable index entry path for a file. Uses the domain scope
558
+ * (default "/user") as a prefix so query-scope filtering works.
559
+ */
560
+ makeEntryPath(domain, originalPath) {
561
+ return (0, ufo.joinURL)(domain.scope ?? "/user", originalPath);
562
+ }
563
+ };
564
+ function matchGlob(path, pattern) {
565
+ const regexStr = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "⧫").replace(/\*/g, "[^/]*").replace(/⧫/g, ".*").replace(/\?/g, "[^/]");
566
+ return (/* @__PURE__ */ new RegExp(`(^|/)${regexStr}$`)).test(path);
567
+ }
568
+
569
+ //#endregion
570
+ exports.DEFAULT_MAX_DIRTY_ROWS_PER_TICK = DEFAULT_MAX_DIRTY_ROWS_PER_TICK;
571
+ exports.DEFAULT_MAX_NOOP_PER_TICK = DEFAULT_MAX_NOOP_PER_TICK;
572
+ exports.DEFAULT_MAX_PROCESSED_PER_TICK = DEFAULT_MAX_PROCESSED_PER_TICK;
573
+ exports.DEFAULT_MAX_SCAN_PER_TICK = DEFAULT_MAX_SCAN_PER_TICK;
574
+ exports.FOLLOWER_CONSUMER_ID = FOLLOWER_CONSUMER_ID;
575
+ exports.IndexFollower = IndexFollower;
576
+ exports.deriveFollowerConsumerId = deriveFollowerConsumerId;
577
+ exports.userIndexKey = userIndexKey;