@harperfast/harper 5.2.9 → 5.2.12

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 (177) hide show
  1. package/dist/resources/DatabaseTransaction.d.ts +3 -0
  2. package/dist/resources/DatabaseTransaction.js +90 -2
  3. package/dist/resources/DatabaseTransaction.js.map +1 -1
  4. package/dist/resources/PrimaryRocksDatabase.js +2 -1
  5. package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
  6. package/dist/resources/RocksIndexStore.js +2 -1
  7. package/dist/resources/RocksIndexStore.js.map +1 -1
  8. package/dist/resources/Table.js +13 -5
  9. package/dist/resources/Table.js.map +1 -1
  10. package/dist/resources/auditStore.js +142 -53
  11. package/dist/resources/auditStore.js.map +1 -1
  12. package/dist/resources/databases.d.ts +10 -0
  13. package/dist/resources/databases.js +369 -97
  14. package/dist/resources/databases.js.map +1 -1
  15. package/dist/resources/search.js +33 -7
  16. package/dist/resources/search.js.map +1 -1
  17. package/dist/server/storageReclamation.d.ts +7 -0
  18. package/dist/server/storageReclamation.js +10 -0
  19. package/dist/server/storageReclamation.js.map +1 -1
  20. package/dist/server/threads/manageThreads.d.ts +1 -0
  21. package/dist/server/threads/manageThreads.js +7 -0
  22. package/dist/server/threads/manageThreads.js.map +1 -1
  23. package/npm-shrinkwrap.json +41 -41
  24. package/package.json +2 -2
  25. package/resources/DatabaseTransaction.ts +88 -2
  26. package/resources/PrimaryRocksDatabase.ts +2 -1
  27. package/resources/RocksIndexStore.ts +2 -1
  28. package/resources/Table.ts +13 -5
  29. package/resources/auditStore.ts +144 -52
  30. package/resources/databases.ts +351 -93
  31. package/resources/search.ts +32 -7
  32. package/server/storageReclamation.ts +10 -0
  33. package/server/threads/manageThreads.js +7 -0
  34. package/studio/web/assets/Chat-CokJOjyX.js +2267 -0
  35. package/studio/web/assets/FloatingChat-h9BJo6YG.js +23 -0
  36. package/studio/web/assets/{abnfDiagram-VCTEODGH-C0_BAZyO.js → abnfDiagram-VCTEODGH-B0BebmD2.js} +1 -1
  37. package/studio/web/assets/{alertDialog-DIHt7Z0r.js → alertDialog-CQyAJJhl.js} +1 -1
  38. package/studio/web/assets/{apiToken-c3Rd-w6g.js → apiToken-B_g12QrM.js} +1 -1
  39. package/studio/web/assets/applications-Dlq5kJ2O.js +296 -0
  40. package/studio/web/assets/architecture-7GRP2DOG-LB-MLAAb.js +1 -0
  41. package/studio/web/assets/{architectureDiagram-5GKGNRK7-BWzrASgm.js → architectureDiagram-5GKGNRK7-7SW3GD-K.js} +1 -1
  42. package/studio/web/assets/authStore-C3Nfubqr.js +3 -0
  43. package/studio/web/assets/{blockDiagram-NRAW4CY4-BdJX9Khj.js → blockDiagram-I7D4REHJ-BqguiadH.js} +2 -2
  44. package/studio/web/assets/{button-DhiX-njv.js → button-BIsUKRZq.js} +2 -2
  45. package/studio/web/assets/{c4Diagram-UCG6FXSJ-CI6MzGmQ.js → c4Diagram-7LVT6UL2-LBNf8t_X.js} +1 -1
  46. package/studio/web/assets/channel-yictG-U-.js +1 -0
  47. package/studio/web/assets/{chevron-up-Df2c8uoU.js → chevron-up-DtKGqDn3.js} +1 -1
  48. package/studio/web/assets/{chunk-TEH6E4GO-P87k5mNi.js → chunk-4HAMMTFA-DWtTut21.js} +1 -1
  49. package/studio/web/assets/{chunk-75Z2AOVW-BT8tVmks.js → chunk-75Z2AOVW-QGQD6th2.js} +1 -1
  50. package/studio/web/assets/{chunk-DU6HZSFF-9kAOOmI6.js → chunk-DU6HZSFF-Chq20Ba5.js} +1 -1
  51. package/studio/web/assets/{chunk-F27PBJKO-BW7ao8AY.js → chunk-F27PBJKO-BVA5EPhV.js} +1 -1
  52. package/studio/web/assets/{chunk-GMAD6QVW-BNyXpoQO.js → chunk-GMAD6QVW-BeS7S07A.js} +1 -1
  53. package/studio/web/assets/{chunk-OBVCFTLP-D4wWOqDQ.js → chunk-GVQU2GXP-sbwVIQ8i.js} +1 -1
  54. package/studio/web/assets/{chunk-G27WJ6UU-COyLMcgK.js → chunk-IMKFNOWR-Bnh3tAVd.js} +1 -1
  55. package/studio/web/assets/{chunk-JQ64N6SF-Cyz1IeLf.js → chunk-L3NEJ4N5-COfUyKII.js} +1 -1
  56. package/studio/web/assets/chunk-OSK3NFVY-ByciRftO.js +10 -0
  57. package/studio/web/assets/{chunk-P2QGCYS3-DmIFY4d7.js → chunk-P2QGCYS3-CP1VhG_c.js} +1 -1
  58. package/studio/web/assets/{chunk-POPQ4Y6H-BPrvMyKz.js → chunk-POPQ4Y6H-ClWhhkwW.js} +1 -1
  59. package/studio/web/assets/{chunk-PWAF6VOD-2zB6IW9i.js → chunk-PWAF6VOD-1z1THyS5.js} +1 -1
  60. package/studio/web/assets/{chunk-RHFEMEQ7-2FgyI8YU.js → chunk-SHT3W25Y-LpQkMsah.js} +2 -2
  61. package/studio/web/assets/{chunk-SVP7TREG-FwtbH2QC.js → chunk-SVP7TREG-jtdAHw0S.js} +1 -1
  62. package/studio/web/assets/{chunk-LCL6LL3I-HOzK_ppE.js → chunk-TICWLB2K-VOwzetX-.js} +1 -1
  63. package/studio/web/assets/classDiagram-ZZMXUADV-VaEwSy_g.js +1 -0
  64. package/studio/web/assets/classDiagram-v2-VYDZK3BY-VaEwSy_g.js +1 -0
  65. package/studio/web/assets/{createLucideIcon-BKGPfjm2.js → createLucideIcon-CzW9508A.js} +1 -1
  66. package/studio/web/assets/{cssMode-CEN2mzSA.js → cssMode-DAjzPoBf.js} +1 -1
  67. package/studio/web/assets/{cynefin-OW5HDTMX-BRkpLFQV.js → cynefin-OW5HDTMX-BbdbCvub.js} +1 -1
  68. package/studio/web/assets/{cynefinDiagram-5FMLGOSQ-CHT1DaX6.js → cynefinDiagram-5FMLGOSQ-TP-aIqbt.js} +1 -1
  69. package/studio/web/assets/{dagre-3AP2YEHR-DpUXBh63.js → dagre-GXQ25YYZ-DShnGpGo.js} +1 -1
  70. package/studio/web/assets/{diagram-S7CK7UJ4-BuymVFZT.js → diagram-S7CK7UJ4-aoCVTtcy.js} +1 -1
  71. package/studio/web/assets/{diagram-UQ7AKVKN-CyP148RM.js → diagram-UQ7AKVKN-DglXtQ6x.js} +1 -1
  72. package/studio/web/assets/{diagram-VSXAHHWV-CoCAg3M9.js → diagram-VSXAHHWV-fhEdmkwM.js} +1 -1
  73. package/studio/web/assets/{diagram-VX7I27RA-BpOqCFca.js → diagram-VX7I27RA-DccVJet6.js} +1 -1
  74. package/studio/web/assets/{diagram-Z3DM3KII-Bfpw7Vbj.js → diagram-Z3DM3KII-D-RyJJb7.js} +1 -1
  75. package/studio/web/assets/{dialog-CBf0Mr1d.js → dialog-Cn2uWgD4.js} +1 -1
  76. package/studio/web/assets/{dist-lkA3O3eM.js → dist-DP8UjMB_.js} +1 -1
  77. package/studio/web/assets/{download-BtTOBem-.js → download-B5T5r7ss.js} +1 -1
  78. package/studio/web/assets/{ebnfDiagram-PWID7BFC-DS_6aWqL.js → ebnfDiagram-PWID7BFC-DJGpIpz_.js} +1 -1
  79. package/studio/web/assets/{editor-D8oDeCTL.js → editor-VUNQ-5Ye.js} +1 -1
  80. package/studio/web/assets/{erDiagram-SSCWMZ5O-DJNk6Fgw.js → erDiagram-RLTQ6QDP-CIfNlgkC.js} +1 -1
  81. package/studio/web/assets/eventmodeling-NTZA5JFV-CLxnp2CR.js +1 -0
  82. package/studio/web/assets/flowDiagram-HODETNUW-BIbhmz9f.js +1 -0
  83. package/studio/web/assets/{ganttDiagram-EL5Y4UJY-2pOExxMY.js → ganttDiagram-EL5Y4UJY-BxToTzzD.js} +1 -1
  84. package/studio/web/assets/{getAnalytics-D4LKGeVy.js → getAnalytics-GHK8ORfM.js} +1 -1
  85. package/studio/web/assets/{gitGraph-4MIJSDKK-CH5ZxwzF.js → gitGraph-4MIJSDKK-D2s2w8lE.js} +1 -1
  86. package/studio/web/assets/{gitGraphDiagram-WWUBYQGX-DVIsIhbO.js → gitGraphDiagram-WWUBYQGX-Dwntd4-x.js} +1 -1
  87. package/studio/web/assets/{html-u3vOg7LJ.js → html-CFqhtdfa.js} +1 -1
  88. package/studio/web/assets/{htmlMode-DyO31v-P.js → htmlMode-CpgOTfjz.js} +1 -1
  89. package/studio/web/assets/{index-Cxj2_wsl.css → index-7RMEgVG1.css} +1 -1
  90. package/studio/web/assets/index-ZQ1upnh7.js +824 -0
  91. package/studio/web/assets/index.lazy-HDafSRxx.js +2 -0
  92. package/studio/web/assets/{info-A6RAGUB7-CPQfTnaG.js → info-A6RAGUB7-DYjkvb0C.js} +1 -1
  93. package/studio/web/assets/{infoDiagram-RXCK75RN-DlwLYlwm.js → infoDiagram-27XIBGKW-Bnp1FJE5.js} +1 -1
  94. package/studio/web/assets/{ishikawaDiagram-5VMMS53U-BRXRp29U.js → ishikawaDiagram-5VMMS53U-D9Xh2r6X.js} +1 -1
  95. package/studio/web/assets/{javascript-CUvxOyTC.js → javascript-9l5JXJwc.js} +1 -1
  96. package/studio/web/assets/{journeyDiagram-EYS64GPL-B0ou8k0n.js → journeyDiagram-3NMN7TZE-CokIi6ll.js} +2 -2
  97. package/studio/web/assets/{jsonMode-f_IwbF3D.js → jsonMode-BJBH-iR0.js} +1 -1
  98. package/studio/web/assets/{kanban-definition-3QL26DDD-uYg7iYzp.js → kanban-definition-UXKFOSKX-CukSFJfX.js} +1 -1
  99. package/studio/web/assets/{languageServices-DXtZ6rEF.js → languageServices-0ZvgO63b.js} +1 -1
  100. package/studio/web/assets/{lspLanguageFeatures-B4pCF1zO.js → lspLanguageFeatures-sNVDopx3.js} +1 -1
  101. package/studio/web/assets/{mermaid-parser.core-Ck-fC8b7.js → mermaid-parser.core-BlEsOWNO.js} +3 -3
  102. package/studio/web/assets/{mermaid.core-CP8aNNYm.js → mermaid.core-BlkGaMIH.js} +5 -5
  103. package/studio/web/assets/{mindmap-definition-FBJOCRG2-CgTZ-rit.js → mindmap-definition-YA3MSWOX-IprMc_0j.js} +1 -1
  104. package/studio/web/assets/notifications-BukjVuVe.js +1 -0
  105. package/studio/web/assets/{notifications-D3tIQ4sg.js → notifications-DvBJIIO0.js} +1 -1
  106. package/studio/web/assets/{packet-AYTQ26CC-DEyoPtPb.js → packet-AYTQ26CC-Bi3V04Zi.js} +1 -1
  107. package/studio/web/assets/{pegDiagram-XKGWAZYB-DrD-7sD9.js → pegDiagram-XKGWAZYB-BNuPDLZY.js} +1 -1
  108. package/studio/web/assets/{pie-WAS4IAKB-wjj-EI1d.js → pie-WAS4IAKB-_6DoDbng.js} +1 -1
  109. package/studio/web/assets/{pieDiagram-E7YTZNPT-GntqDCzv.js → pieDiagram-E7YTZNPT-DqNb6Ht2.js} +1 -1
  110. package/studio/web/assets/{profile-DZWU7MgT.js → profile-B-jyYdzV.js} +1 -1
  111. package/studio/web/assets/{quadrantDiagram-AXDQQJYC-0UeqXQGd.js → quadrantDiagram-AXDQQJYC-BGH9E2YR.js} +1 -1
  112. package/studio/web/assets/{radar-RG4KPBEZ-DFSA5h7k.js → radar-RG4KPBEZ-DDdVczcL.js} +1 -1
  113. package/studio/web/assets/{railroad-74A4TZTK-CaOUG9wR.js → railroad-74A4TZTK-BJUP4Jds.js} +1 -1
  114. package/studio/web/assets/railroad-abnf-HS5TGJTU-Bm3L1L0h.js +1 -0
  115. package/studio/web/assets/railroad-ebnf-LZEXJU2U-CKzLGlkw.js +1 -0
  116. package/studio/web/assets/railroad-peg-WCYAUIDC-S8xLjslx.js +1 -0
  117. package/studio/web/assets/{railroadDiagram-O6MQD6OU-yHUELZaV.js → railroadDiagram-O6MQD6OU-DGTPh2KZ.js} +1 -1
  118. package/studio/web/assets/{regions-CkyurXzE.js → regions-GN6Mu5aH.js} +1 -1
  119. package/studio/web/assets/{register-BUyhWjBO.js → register-CU-gCfLP.js} +3 -3
  120. package/studio/web/assets/{requirementDiagram-EFPCY7ZU-DNEGFjuW.js → requirementDiagram-BXWQKSXE-BJnO6uLz.js} +1 -1
  121. package/studio/web/assets/{sankeyDiagram-P5KCCOFB-DpyAmSVR.js → sankeyDiagram-P5KCCOFB-0vSOdymH.js} +1 -1
  122. package/studio/web/assets/{sequenceDiagram-WJ2MYXX4-TyaT7xNk.js → sequenceDiagram-WJ2MYXX4-hETizDWE.js} +1 -1
  123. package/studio/web/assets/{setComponentFile-CeyKSZAa.js → setComponentFile-D7Fww0lC.js} +1 -1
  124. package/studio/web/assets/{setup-D2kn7cAA.js → setup-DG3Ws0Gg.js} +2 -2
  125. package/studio/web/assets/{stateDiagram-HBIQ2CUA-CeEdTArZ.js → stateDiagram-D77RDMKH-CdYQ_KtC.js} +1 -1
  126. package/studio/web/assets/stateDiagram-v2-MP3YSRHH-CdKuzQMT.js +1 -0
  127. package/studio/web/assets/status-koJd66XF.js +61 -0
  128. package/studio/web/assets/{swimlanes-XN3QIQJK-B54FmF46.js → swimlanes-42K2YHIH-B8cHIpU4.js} +1 -1
  129. package/studio/web/assets/swimlanesDiagram-VR7AAH4N-DmOSJwaH.js +8 -0
  130. package/studio/web/assets/{tabs-B_G5zscN.js → tabs-BrHu7gJi.js} +1 -1
  131. package/studio/web/assets/{timeline-definition-24CTP7MA-D-a9ujbo.js → timeline-definition-24CTP7MA-BJWYSXqF.js} +1 -1
  132. package/studio/web/assets/{toggleHighContrast-C0UW6rI2.js → toggleHighContrast-CGksgPgv.js} +1 -1
  133. package/studio/web/assets/{treeView-Q6P3EWNA-CrW_6JnS.js → treeView-Q6P3EWNA-qxe_v6CQ.js} +1 -1
  134. package/studio/web/assets/{treemap-WGGIJYW6-BxyYLdP_.js → treemap-WGGIJYW6-dDo97XXF.js} +1 -1
  135. package/studio/web/assets/{tsMode-DBC0zmDx.js → tsMode-D3T_5QzW.js} +1 -1
  136. package/studio/web/assets/{typescript-DApRQir3.js → typescript-BegjUvtB.js} +1 -1
  137. package/studio/web/assets/{useEntityRestURL-DB6JStU1.js → useEntityRestURL-BicFpx_N.js} +1 -1
  138. package/studio/web/assets/{useLocalStorage-Dtj1QS8_.js → useLocalStorage-BqMR3D8_.js} +1 -1
  139. package/studio/web/assets/vendor-core-c2JRRJpV.js +58 -0
  140. package/studio/web/assets/vendor-datadog-CLUcJXOo.js +6 -0
  141. package/studio/web/assets/{vendor-react-Dyj4O3HE.js → vendor-react-CJV_K1u4.js} +1 -1
  142. package/studio/web/assets/vendor-tanstack-DxzraizX.js +1 -0
  143. package/studio/web/assets/{vendor-ui-vhu-UHhF.js → vendor-ui-BUjK0h8a.js} +2 -2
  144. package/studio/web/assets/{vennDiagram-4TSXK5OY-Cy7s7Mpy.js → vennDiagram-4TSXK5OY-A3i-lCdl.js} +1 -1
  145. package/studio/web/assets/{wardley-WFR3VGLG-BeBL35g2.js → wardley-WFR3VGLG-B0ik-_6g.js} +1 -1
  146. package/studio/web/assets/{wardleyDiagram-VM6X3IG4-BylmIGSg.js → wardleyDiagram-VM6X3IG4-CjrkKWUR.js} +1 -1
  147. package/studio/web/assets/{workers-C0bFIedw.js → workers-Bmx4l_9B.js} +1 -1
  148. package/studio/web/assets/x-DIzaLEdK.js +1 -0
  149. package/studio/web/assets/{xml-HWd01lU-.js → xml-Bg40Rcra.js} +1 -1
  150. package/studio/web/assets/{xychartDiagram-S5SC5T6Z-CoKALMXr.js → xychartDiagram-S5SC5T6Z-Biok4GYV.js} +1 -1
  151. package/studio/web/assets/{yaml-CIH0Nt-h.js → yaml-DolDvfXE.js} +1 -1
  152. package/studio/web/index.html +14 -14
  153. package/studio/web/assets/Chat-JpO8EtUu.js +0 -2067
  154. package/studio/web/assets/FloatingChat-Bcj3xSZu.js +0 -23
  155. package/studio/web/assets/applications-ByqLRKyZ.js +0 -296
  156. package/studio/web/assets/architecture-7GRP2DOG-DNdx5tEU.js +0 -1
  157. package/studio/web/assets/authStore-qKmCZcaf.js +0 -3
  158. package/studio/web/assets/channel-DtCV8PTL.js +0 -1
  159. package/studio/web/assets/chunk-R7TYR2AO-Irip67yr.js +0 -10
  160. package/studio/web/assets/classDiagram-DTDB5LWJ-DbO_dCNE.js +0 -1
  161. package/studio/web/assets/classDiagram-v2-JRS7N3AN-DbO_dCNE.js +0 -1
  162. package/studio/web/assets/eventmodeling-NTZA5JFV-5jbe4A5P.js +0 -1
  163. package/studio/web/assets/flowDiagram-A5DVABFB-Dp9Ezlow.js +0 -1
  164. package/studio/web/assets/index-aSt5tY-L.js +0 -824
  165. package/studio/web/assets/index.lazy-B9jiPwT8.js +0 -2
  166. package/studio/web/assets/notifications-CUoYgU98.js +0 -1
  167. package/studio/web/assets/railroad-abnf-HS5TGJTU-Bc0Qi0WH.js +0 -1
  168. package/studio/web/assets/railroad-ebnf-LZEXJU2U-G8rVVZ2C.js +0 -1
  169. package/studio/web/assets/railroad-peg-WCYAUIDC-CrehKBhC.js +0 -1
  170. package/studio/web/assets/stateDiagram-v2-4QOOHH4V-D4tuw9Su.js +0 -1
  171. package/studio/web/assets/status-D7Xn5ePA.js +0 -61
  172. package/studio/web/assets/swimlanesDiagram-VK2B7HYN-XOhmNEvq.js +0 -8
  173. package/studio/web/assets/vendor-core-RCcadM3e.js +0 -73
  174. package/studio/web/assets/vendor-datadog-BRv-mOv1.js +0 -6
  175. package/studio/web/assets/vendor-tanstack-BiFWSB3W.js +0 -1
  176. package/studio/web/assets/x-B9o9hsep.js +0 -1
  177. /package/studio/web/assets/{sizeCapture-X5ZJPWSS-B0uUizjq.js → sizeCapture-INFHLROL-B0uUizjq.js} +0 -0
@@ -1,4 +1,5 @@
1
1
  import { EventEmitter } from 'node:events';
2
+ import { randomBytes } from 'node:crypto';
2
3
  import { initSync, getHdbBasePath, get as envGet } from '../utility/environment/environmentManager.ts';
3
4
  import { INTERNAL_DBIS_NAME } from '../utility/lmdb/terms.ts';
4
5
  import { open, compareKeys, type Database, type RootDatabase } from 'lmdb';
@@ -29,6 +30,7 @@ import harperLogger from '../utility/logging/harper_logger.ts';
29
30
  const { forComponent } = harperLogger;
30
31
  import * as manageThreads from '../server/threads/manageThreads.js';
31
32
  import { openAuditStore, readAuditEntry, createAuditEntry, type AuditRecord } from './auditStore.ts';
33
+ import { removeStorageReclamation } from '../server/storageReclamation.ts';
32
34
  import { handleLocalTimeForGets } from './RecordEncoder.ts';
33
35
  import { deleteRootBlobPathsForDB } from './blob.ts';
34
36
  import { CUSTOM_INDEXES } from './indexes/customIndexes.ts';
@@ -38,7 +40,6 @@ import { PrimaryRocksDatabase } from './PrimaryRocksDatabase.ts';
38
40
  import { replayLogs } from './replayLogs.ts';
39
41
  import { totalmem } from 'node:os';
40
42
  import { RocksIndexStore } from './RocksIndexStore.ts';
41
- import { when } from '../utility/when.ts';
42
43
  import { resolveRocksMemoryConfig } from '../utility/rocksMemoryConfig.ts';
43
44
  import { isProcessRunning } from '../utility/processManagement/processManagement.js';
44
45
  import {
@@ -348,6 +349,20 @@ _assignPackageExport('databases', databases);
348
349
  _assignPackageExport('tables', tables);
349
350
 
350
351
  const NEXT_TABLE_ID = Symbol.for('next-table-id');
352
+ /**
353
+ * True when a descriptor claims an index build no live operation in this process can own. The PID and
354
+ * worker generation cannot answer that alone: a container reuses PID 1 and starts the in-memory
355
+ * generation back at 1 while the persisted one is higher. A descriptor with no incarnation was written
356
+ * before the field existed, so it belongs to an earlier process; a thread started without one of its
357
+ * own cannot judge, and falls back rather than declaring a live build dead.
358
+ */
359
+ function isAbandonedIndexBuild(descriptor: any, currentRestartGeneration: number): boolean {
360
+ if (!descriptor) return false;
361
+ if (descriptor.indexingPID && descriptor.indexingPID !== process.pid) return true;
362
+ if (descriptor.restartNumber < currentRestartGeneration) return true;
363
+ const incarnation = manageThreads.processIncarnation;
364
+ return !!descriptor.indexingPID && incarnation != null && descriptor.indexingIncarnation !== incarnation;
365
+ }
351
366
  // How many times the schema load will try to finish a tombstoned drop before
352
367
  // giving up for the rest of this process's lifetime. A drop that fails once
353
368
  // almost always fails identically forever - the usual cause is a RocksDB
@@ -637,6 +652,7 @@ export function readMetaDb(
637
652
  lmdbDatabaseEnvs.set(path, rootStore);
638
653
  }
639
654
 
655
+ rootStore.dbisDb?.resetReadTxn();
640
656
  return initStores(path, rootStore, databaseName, defaultTable, auditPath, isLegacy);
641
657
  } catch (error) {
642
658
  error.message += ` opening database ${path}`;
@@ -896,6 +912,8 @@ function initStores(
896
912
  indices[attribute.name] = dbi;
897
913
  indices[attribute.name].indexNulls = attribute.indexNulls;
898
914
  }
915
+ // the only way a thread that never declares the schema reaches Table.indices
916
+ indices[attribute.name].isIndexing = !!attribute.indexingPID;
899
917
  const existingAttribute = existingAttributes.find(
900
918
  (existingAttribute) => existingAttribute.name === attribute.name
901
919
  );
@@ -1253,6 +1271,10 @@ export async function dropDatabase(databaseName) {
1253
1271
  databaseEventsEmitter.emit('dropDatabase', databaseName);
1254
1272
 
1255
1273
  if (rootStore) {
1274
+ // awaited: retirement stops the loop admitting work, the barrier is what says the pass that was
1275
+ // already running has released the stores this is about to close and unlink
1276
+ await rootStore.auditStore?.stopAuditCleanup?.();
1277
+ removeStorageReclamation(rootStore.path);
1256
1278
  if (rootStore.status === 'open') {
1257
1279
  if (rootStore instanceof RocksDatabase) {
1258
1280
  rootStore.close();
@@ -1267,6 +1289,8 @@ export async function dropDatabase(databaseName) {
1267
1289
  // a tableless database resolves its root store here rather than in the loop above, so take
1268
1290
  // the drop lock now (still before any destructive step)
1269
1291
  if (rootStore instanceof RocksDatabase) lockDatabaseForDrop(rootStore.path, databaseName, restoreLocks);
1292
+ await rootStore.auditStore?.stopAuditCleanup?.();
1293
+ removeStorageReclamation(rootStore.path);
1270
1294
  if (rootStore instanceof RocksDatabase) {
1271
1295
  rootStore.close();
1272
1296
  rootStore.destroy();
@@ -1304,17 +1328,27 @@ export function closeDatabase(databaseName: string): boolean {
1304
1328
  const table: any = dbTables[tableName];
1305
1329
  if (!table?.primaryStore) continue;
1306
1330
  if (table.primaryStore.rootStore) rootStores.add(table.primaryStore.rootStore);
1307
- for (const indexName in table.indices || {}) {
1308
- closeStore(table.indices[indexName], `index ${tableName}.${indexName}`);
1309
- }
1310
- closeStore(table.primaryStore, `table ${tableName}`);
1311
1331
  }
1312
1332
  // a database with no tables (an empty schema, or one whose tables were all dropped) still holds
1313
1333
  // an open root store, tracked only on the defined-database entry rather than any table — include
1314
1334
  // it so its handles are released too (the Set dedupes it against the per-table root stores above)
1315
1335
  const definedRoot = (definedDatabases?.get(databaseName) as any)?.rootStore;
1316
1336
  if (definedRoot) rootStores.add(definedRoot);
1337
+ // before any table store closes, so no further pass is admitted. This is synchronous, so it cannot
1338
+ // await the drain barrier stopAuditCleanup() returns; what covers it is the in-pass status checks,
1339
+ // plus the fact that its production callers reach it only for RocksDB databases, whose pass is one
1340
+ // synchronous purgeLogs() call with nothing suspended mid-removal.
1341
+ for (const rootStore of rootStores) rootStore.auditStore?.stopAuditCleanup?.();
1342
+ for (const tableName in dbTables) {
1343
+ const table: any = dbTables[tableName];
1344
+ if (!table?.primaryStore) continue;
1345
+ for (const indexName in table.indices || {}) {
1346
+ closeStore(table.indices[indexName], `index ${tableName}.${indexName}`);
1347
+ }
1348
+ closeStore(table.primaryStore, `table ${tableName}`);
1349
+ }
1317
1350
  for (const rootStore of rootStores) {
1351
+ removeStorageReclamation(rootStore.path);
1318
1352
  closeStore(rootStore.dbisDb, 'attributes store');
1319
1353
  closeStore(rootStore, 'root store');
1320
1354
  lmdbDatabaseEnvs.delete(rootStore.path);
@@ -1882,8 +1916,7 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
1882
1916
  changed ||
1883
1917
  indexFormatNeedsPersist ||
1884
1918
  attributeDescriptor?.indexingFailed ||
1885
- (attributeDescriptor?.indexingPID && attributeDescriptor?.indexingPID !== process.pid) ||
1886
- attributeDescriptor?.restartNumber < currentRestartGeneration
1919
+ isAbandonedIndexBuild(attributeDescriptor, currentRestartGeneration)
1887
1920
  ) {
1888
1921
  hasChanges = true;
1889
1922
  exclusiveLock();
@@ -1891,8 +1924,7 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
1891
1924
  if (
1892
1925
  structurallyChanged ||
1893
1926
  attributeDescriptor?.indexingFailed ||
1894
- (attributeDescriptor?.indexingPID && attributeDescriptor?.indexingPID !== process.pid) ||
1895
- attributeDescriptor?.restartNumber < currentRestartGeneration
1927
+ isAbandonedIndexBuild(attributeDescriptor, currentRestartGeneration)
1896
1928
  ) {
1897
1929
  hasChanges = true;
1898
1930
  if (attribute.indexNulls === undefined) attribute.indexNulls = true;
@@ -1912,9 +1944,21 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
1912
1944
  // resumes rather than restarts. Canonicalized to match structurallyChanged above.
1913
1945
  const indexOptionsChanged =
1914
1946
  canonicalIndexKey(attributeDescriptor?.indexed) !== canonicalIndexKey(attribute.indexed);
1915
- attribute.lastIndexedKey = indexOptionsChanged
1916
- ? undefined
1917
- : (attributeDescriptor?.lastIndexedKey ?? undefined);
1947
+ // Only a checkpoint runIndexing stamped with its own key resumes: earlier releases advanced
1948
+ // lastIndexedKey past failed and unflushed index writes, so any other is a full rebuild.
1949
+ const uncertifiedCheckpoint =
1950
+ attributeDescriptor?.lastIndexedKey !== undefined &&
1951
+ (attributeDescriptor.checkpointCertified === undefined ||
1952
+ attributeDescriptor.checkpointAlgorithm !== CHECKPOINT_ALGORITHM ||
1953
+ compareKeys(attributeDescriptor.checkpointCertified, attributeDescriptor.lastIndexedKey) !== 0);
1954
+ attribute.lastIndexedKey =
1955
+ indexOptionsChanged || uncertifiedCheckpoint
1956
+ ? undefined
1957
+ : (attributeDescriptor?.lastIndexedKey ?? undefined);
1958
+ if (attribute.lastIndexedKey !== undefined) {
1959
+ attribute.checkpointCertified = attribute.lastIndexedKey;
1960
+ attribute.checkpointAlgorithm = CHECKPOINT_ALGORITHM;
1961
+ }
1918
1962
  // Explicit reindex is the upgrade path from a legacy (un-versioned) custom-index
1919
1963
  // object store to the versioned, VT-cacheable format. A full rebuild from scratch
1920
1964
  // (lastIndexedKey === undefined) clears the store and rewrites every node, so the
@@ -1938,6 +1982,9 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
1938
1982
  // the new process reuses the old PID. Cleared on clean completion; left in place
1939
1983
  // on failure/crash so the next, higher-numbered restart re-triggers the backfill.
1940
1984
  attribute.restartNumber = currentRestartGeneration;
1985
+ if (manageThreads.processIncarnation != null)
1986
+ attribute.indexingIncarnation = manageThreads.processIncarnation;
1987
+ attribute.indexingBuildId = randomBytes(8).toString('hex');
1941
1988
  delete attribute.indexingFailed; // clear failure flag for the new run
1942
1989
  dbi.isIndexing = true;
1943
1990
  Object.defineProperty(attribute, 'dbi', { value: dbi, configurable: true, enumerable: false });
@@ -1951,6 +1998,13 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
1951
1998
  if (attributeDescriptor?.indexingPID && attributeDescriptor.indexingPID !== process.pid)
1952
1999
  reindexReasons.push(`crash-recovery(pid=${attributeDescriptor.indexingPID})`);
1953
2000
  if (attributeDescriptor?.restartNumber < currentRestartGeneration) reindexReasons.push('restart-number');
2001
+ if (uncertifiedCheckpoint) reindexReasons.push('uncertified-checkpoint');
2002
+ if (
2003
+ attributeDescriptor?.indexingPID === process.pid &&
2004
+ manageThreads.processIncarnation != null &&
2005
+ attributeDescriptor.indexingIncarnation !== manageThreads.processIncarnation
2006
+ )
2007
+ reindexReasons.push('abandoned-build(previous process incarnation)');
1954
2008
  logger.info(
1955
2009
  `reindex ${databaseName}.${tableName}.${attribute.name}: reason=${reindexReasons.join(',') || 'unknown'}`
1956
2010
  );
@@ -1964,11 +2018,21 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
1964
2018
  // workers / a reload would treat the still-partial index as ready and return incomplete results.
1965
2019
  attribute.indexingPID = attributeDescriptor.indexingPID;
1966
2020
  attribute.lastIndexedKey = attributeDescriptor.lastIndexedKey;
2021
+ if (attributeDescriptor.checkpointCertified !== undefined) {
2022
+ attribute.checkpointCertified = attributeDescriptor.checkpointCertified;
2023
+ attribute.checkpointAlgorithm = attributeDescriptor.checkpointAlgorithm;
2024
+ }
1967
2025
  // Carry the in-progress restart generation too, so persisting this metadata-only
1968
2026
  // change doesn't drop it and break the crash-recovery trigger for the running backfill.
1969
2027
  attribute.restartNumber = attributeDescriptor.restartNumber;
2028
+ attribute.indexingIncarnation = attributeDescriptor.indexingIncarnation;
2029
+ attribute.indexingBuildId = attributeDescriptor.indexingBuildId;
1970
2030
  if (attributeDescriptor.indexingFailed) attribute.indexingFailed = attributeDescriptor.indexingFailed;
1971
2031
  }
2032
+ // The declared attribute never carries the stamp, so any rewrite of a descriptor that has
2033
+ // one would drop it and make a completed index look like a pre-stamp build.
2034
+ if (attribute.checkpointAlgorithm === undefined && attributeDescriptor?.checkpointAlgorithm !== undefined)
2035
+ attribute.checkpointAlgorithm = attributeDescriptor.checkpointAlgorithm;
1972
2036
  attributesDbi.put(dbiKey, attribute);
1973
2037
  }
1974
2038
  // If a migration is in progress (indexingPID set), any newly opened dbi must also
@@ -1994,7 +2058,10 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
1994
2058
  }
1995
2059
  logger.trace(`${tableName} table loading, running index`);
1996
2060
  if (attributesToIndex.length > 0 || indicesToRemove.length > 0) {
1997
- Table.indexingOperation = runIndexing(Table, attributesToIndex, indicesToRemove);
2061
+ // captured before the backfill can rewrite the attributes
2062
+ const buildIds = new Map(attributesToIndex.map((attribute) => [attribute, attribute.indexingBuildId]));
2063
+ const markSettled = () => markAbandonedIndexBuild(Table, rootStore, buildIds);
2064
+ Table.indexingOperation = runIndexing(Table, attributesToIndex, indicesToRemove).then(markSettled, markSettled);
1998
2065
  } else if (hasChanges)
1999
2066
  signalling.signalSchemaChange(
2000
2067
  new SchemaEventMsg(process.pid, 'schema-change', Table.databaseName, Table.tableName)
@@ -2064,40 +2131,231 @@ export function canonicalizeIndexOptions(value: any): any {
2064
2131
  }
2065
2132
  return value;
2066
2133
  }
2134
+ // Bumped when a change alters which keys a checkpoint may certify. A descriptor stamped by any other
2135
+ // version resumes as uncertified (full rebuild) rather than being trusted, and a completed index keeps
2136
+ // the stamp of the build that wrote it.
2137
+ export const CHECKPOINT_ALGORITHM = 2;
2067
2138
  const MAX_OUTSTANDING_INDEXING = 1000;
2068
2139
  const MIN_OUTSTANDING_INDEXING = 10;
2140
+ const INDEXING_YIELD_INTERVAL = 100;
2141
+ // A resumable checkpoint is written only after a flush (see flushIndexStores), at most once per period
2142
+ // and never before this many more records: the flush seals every column family in the database, so a
2143
+ // slow backfill must not impose the period's flush rate on unrelated tables.
2144
+ let indexingCheckpointPeriodMs = 5000;
2145
+ let indexingCheckpointMinRecords = 10000;
2146
+ export function setIndexingCheckpointPeriod(ms: number, minRecords = indexingCheckpointMinRecords) {
2147
+ const previous = { ms: indexingCheckpointPeriodMs, minRecords: indexingCheckpointMinRecords };
2148
+ indexingCheckpointPeriodMs = ms;
2149
+ indexingCheckpointMinRecords = minRecords;
2150
+ return previous;
2151
+ }
2152
+ const yieldEventTurn = () => new Promise((resolve) => setImmediate(resolve));
2153
+ // RocksDB index stores have no WAL (openRocksDatabase defaults disableWAL), so a flush is what makes the
2154
+ // entries a checkpoint certifies durable. A flush only covers writes issued before it started, so a caller
2155
+ // never joins one in flight: it joins the next one, which every backfill on that database asking meanwhile
2156
+ // shares — at most one in flight and one queued.
2157
+ const indexingFlushes = new WeakMap<object, { inFlight?: Promise<void>; queued?: Promise<void> }>();
2158
+ function flushIndexStores(rootStore: any): Promise<void> | undefined {
2159
+ if (!(rootStore instanceof RocksDatabase)) return;
2160
+ let flushes = indexingFlushes.get(rootStore);
2161
+ if (!flushes) indexingFlushes.set(rootStore, (flushes = {}));
2162
+ if (flushes.queued) return flushes.queued;
2163
+ const start = () => {
2164
+ flushes.queued = undefined;
2165
+ const flush = rootStore.flush().finally(() => {
2166
+ if (flushes.inFlight === flush) flushes.inFlight = undefined;
2167
+ });
2168
+ flushes.inFlight = flush;
2169
+ return flush;
2170
+ };
2171
+ if (!flushes.inFlight) return start();
2172
+ return (flushes.queued = flushes.inFlight.then(start, start));
2173
+ }
2174
+ export function resumeStartKey(attributes: { lastIndexedKey?: any }[]): any {
2175
+ let start: any;
2176
+ for (const attribute of attributes) {
2177
+ if (attribute.lastIndexedKey == undefined) return undefined;
2178
+ if (start === undefined || compareKeys(attribute.lastIndexedKey, start) < 0) start = attribute.lastIndexedKey;
2179
+ }
2180
+ return start;
2181
+ }
2182
+
2183
+ // Bounded, unlike the exclusiveLock() spin above: that one runs on the declaring path, where the
2184
+ // caller is waiting on the result and there is nothing useful to do without the lock. This one runs
2185
+ // after a backfill has already settled, so a holder that never releases would wedge the worker's
2186
+ // event loop for nothing. Giving up costs only the marker, and the next load of the table
2187
+ // re-triggers the build regardless.
2188
+ export const ABANDONED_MARK_LOCK_TIMEOUT = 10000;
2189
+ const abandonedMarkLockWait = new Int32Array(new SharedArrayBuffer(4));
2190
+ // `timeout` is the test seam; production callers take the default.
2191
+ export function tryAcquireUpdateAttributesLock(
2192
+ rootStore: RocksDatabase,
2193
+ timeout = ABANDONED_MARK_LOCK_TIMEOUT
2194
+ ): boolean {
2195
+ if (rootStore.tryLock('update-attributes')) return true;
2196
+ const startTime = performance.now();
2197
+ let waitTime = 1;
2198
+ while (!rootStore.tryLock('update-attributes')) {
2199
+ const elapsed = performance.now() - startTime;
2200
+ if (elapsed >= timeout) return false;
2201
+ // Atomics.wait rather than a busy spin: the section this guards is synchronous, so the wait
2202
+ // blocks this thread either way, but sleeping does not burn a core while it does.
2203
+ if (elapsed >= 2) {
2204
+ Atomics.wait(abandonedMarkLockWait, 0, 0, Math.min(waitTime, timeout - elapsed));
2205
+ if (waitTime < 16) waitTime *= 2;
2206
+ }
2207
+ }
2208
+ return true;
2209
+ }
2210
+
2211
+ /**
2212
+ * Persists the failure marker for a build that ended without running one of runIndexing's own exit
2213
+ * paths, so something re-triggers it. Fenced on `indexingBuildId` inside the storage engine's catalog
2214
+ * serialization boundary, because a replacement generation (or another thread declaring different index
2215
+ * options) can claim the attribute before an outgoing build's promise settles, and marking that would fail
2216
+ * a live build. The fence read and write stay synchronous, and nothing here may throw because
2217
+ * `Table.indexingOperation` reaches operations-API callers.
2218
+ */
2219
+ async function markAbandonedIndexBuild(Table, rootStore, buildIds: Map<any, string>) {
2220
+ for (const [attribute, buildId] of buildIds) {
2221
+ try {
2222
+ let marked;
2223
+ if (buildId == null || Table.dbisDB.getSync(attribute.key)?.indexingBuildId !== buildId) continue;
2224
+ const markIfOwned = () => {
2225
+ const descriptor = Table.dbisDB.getSync(attribute.key);
2226
+ if (descriptor?.indexingBuildId === buildId && !descriptor.indexingFailed) {
2227
+ Table.dbisDB.putSync(attribute.key, { ...descriptor, indexingFailed: true });
2228
+ marked = true;
2229
+ }
2230
+ };
2231
+ if (rootStore instanceof RocksDatabase) {
2232
+ if (!tryAcquireUpdateAttributesLock(rootStore)) {
2233
+ logger.warn(
2234
+ `Could not mark the abandoned index build of ${Table.databaseName}.${Table.tableName}.${attribute.name}: ` +
2235
+ `timed out after ${ABANDONED_MARK_LOCK_TIMEOUT}ms waiting for the exclusive 'update-attributes' lock. ` +
2236
+ `The index stays incomplete and the next load of the table re-triggers the backfill.`
2237
+ );
2238
+ continue;
2239
+ }
2240
+ try {
2241
+ markIfOwned();
2242
+ } finally {
2243
+ rootStore.unlock('update-attributes');
2244
+ }
2245
+ } else {
2246
+ rootStore.transactionSync(markIfOwned);
2247
+ }
2248
+ if (marked)
2249
+ logger.warn(
2250
+ `Indexing of ${Table.databaseName}.${Table.tableName}.${attribute.name} ended without completing. ` +
2251
+ `The index stays incomplete and every query on the attribute reports it as not indexed yet; ` +
2252
+ `the next load of the table retries the backfill from the last checkpoint (indexingFailed=true).`
2253
+ );
2254
+ } catch (error) {
2255
+ // A store closed by shutdown is the common case, and it cannot be written to at all.
2256
+ try {
2257
+ logger.debug(`Could not mark the abandoned index build of ${Table.tableName}.${attribute.name}`, error);
2258
+ } catch {}
2259
+ }
2260
+ }
2261
+ }
2069
2262
  async function runIndexing(Table, attributes, indicesToRemove) {
2263
+ let checkpointing;
2264
+ let hadIndexingErrors = false;
2265
+ const attributeErrorReported = {};
2266
+ const onIndexPutRejected = (property, error) => {
2267
+ hadIndexingErrors = true;
2268
+ if (attributeErrorReported[property]) return;
2269
+ attributeErrorReported[property] = true;
2270
+ logger.error(`Error indexing attribute ${property}`, error);
2271
+ };
2272
+ const putRejectionHandlers = attributes.map((attribute) => (error) => onIndexPutRejected(attribute.name, error));
2070
2273
  try {
2071
2274
  logger.info(`Indexing ${Table.tableName} attributes`, attributes);
2072
2275
  await signalling.signalSchemaChange(
2073
2276
  new SchemaEventMsg(process.pid, 'schema-change', Table.databaseName, Table.tableName)
2074
2277
  );
2075
2278
  let lastResolution;
2279
+ // The checkpoint and completion barriers have to cover every mutation still in flight: any of them
2280
+ // may reject after those barriers read hadIndexingErrors.
2281
+ const pendingMutations = new Set();
2282
+ let settleWaiter;
2283
+ const track = (result, onRejected) => {
2284
+ if (!result?.then) return result;
2285
+ const tracked = result.then(
2286
+ () => {
2287
+ pendingMutations.delete(tracked);
2288
+ settleWaiter?.();
2289
+ return false;
2290
+ },
2291
+ (error) => {
2292
+ pendingMutations.delete(tracked);
2293
+ settleWaiter?.();
2294
+ onRejected(error);
2295
+ return true;
2296
+ }
2297
+ );
2298
+ pendingMutations.add(tracked);
2299
+ return result;
2300
+ };
2301
+ // The tracked promises absorb their own rejections, so one failure never abandons its siblings.
2302
+ const drainMutations = async () => {
2303
+ if (!pendingMutations.size) return false;
2304
+ return (await Promise.all([...pendingMutations])).some(Boolean);
2305
+ };
2306
+ // Waiting on a chosen entry would stall behind a slow one the others have already overtaken.
2307
+ const nextSettlement = () =>
2308
+ new Promise((resolve) => {
2309
+ settleWaiter = () => {
2310
+ settleWaiter = undefined;
2311
+ resolve(undefined);
2312
+ };
2313
+ });
2076
2314
  for (const index of indicesToRemove) {
2077
- lastResolution = index.drop();
2315
+ track(index.drop(), (error) => onIndexPutRejected(index.name, error));
2078
2316
  }
2079
2317
  let interrupted;
2080
- let hadIndexingErrors = false;
2081
- const attributeErrorReported = {};
2082
2318
  let indexed = 0;
2083
2319
  const attributesLength = attributes.length;
2084
2320
  await new Promise((resolve) => setImmediate(resolve)); // yield event turn, indexing should consistently take at least one event turn
2085
2321
  if (attributesLength > 0) {
2086
- let start: any;
2087
- for (const attribute of attributes) {
2088
- // if we are resuming, we need to start from the last key we indexed by all attributes
2089
- if (compareKeys(attribute.lastIndexedKey, start) < 0) start = attribute.lastIndexedKey;
2090
- if (attribute.lastIndexedKey == undefined) {
2091
- // if we are starting from the beginning, clear out any previous index entries since we are rewriting
2322
+ const start = resumeStartKey(attributes);
2323
+ if (start === undefined) {
2324
+ for (const attribute of attributes) {
2092
2325
  if (attribute.dbi.clearAsync) {
2093
- // LMDB, note that we don't need to wait for this to complete, just gets enqueued in front of the other writes
2094
- attribute.dbi.clearAsync();
2326
+ // LMDB enqueues this ahead of the index writes, so the scan need not wait for it — but the
2327
+ // barriers must, or a rejected clear certifies a checkpoint over stale entries.
2328
+ track(attribute.dbi.clearAsync(), (error) => onIndexPutRejected(attribute.name, error));
2095
2329
  } else {
2096
2330
  await attribute.dbi.clear();
2097
2331
  }
2098
2332
  }
2099
2333
  }
2100
- let outstanding = 0;
2334
+ // A resumed scan starts at the checkpoint, so it must only name a key whose every predecessor is
2335
+ // durably indexed: persisted once the writes it covers have settled and flushed, frozen after any
2336
+ // record fails so the retry re-covers it, and stamped with its own key (see the trigger in table()).
2337
+ const persistCheckpoint = async (key) => {
2338
+ if (hadIndexingErrors) return;
2339
+ try {
2340
+ // Everything still in flight was issued for a key at or before this one: the scan has not
2341
+ // moved past it yet. So a failure among them is a failure this checkpoint would cover.
2342
+ const failed = await drainMutations();
2343
+ if (failed) return;
2344
+ await flushIndexStores(Table.primaryStore.rootStore);
2345
+ const puts = [];
2346
+ for (const attribute of attributes) {
2347
+ attribute.lastIndexedKey = key;
2348
+ attribute.checkpointCertified = key;
2349
+ attribute.checkpointAlgorithm = CHECKPOINT_ALGORITHM;
2350
+ puts.push(Table.dbisDB.put(attribute.key, attribute));
2351
+ }
2352
+ await Promise.all(puts);
2353
+ } catch (error) {
2354
+ logger.warn(`Could not persist the indexing checkpoint for ${Table.tableName}`, error);
2355
+ }
2356
+ };
2357
+ let nextCheckpointAt = performance.now() + indexingCheckpointPeriodMs;
2358
+ let nextCheckpointRecord = indexingCheckpointMinRecords;
2101
2359
  // this means that a new attribute has been introduced that needs to be indexed
2102
2360
  for (const { key, value: record } of Table.primaryStore.getRange({
2103
2361
  start,
@@ -2105,93 +2363,86 @@ async function runIndexing(Table, attributes, indicesToRemove) {
2105
2363
  versions: true,
2106
2364
  snapshot: false, // don't hold a read transaction this whole time
2107
2365
  })) {
2108
- if (!record) continue; // deletion entry
2366
+ const atInterval = ++indexed % INDEXING_YIELD_INTERVAL === 0;
2109
2367
  // TODO: Do we ever need to interrupt due to a schema change that was not a restart?
2110
2368
  //if (Table.schemaVersion !== schemaVersion) return; // break out if there are any schema changes and let someone else pick it up
2111
- outstanding++;
2112
- // Custom indexes (e.g. HNSW) index synchronously and never raise `outstanding`, so the
2113
- // outstanding-based yield below never fires for them. Track that this row did synchronous
2369
+ // Custom indexes (e.g. HNSW) index synchronously and leave pendingMutations empty, so the
2370
+ // backpressure yield below never fires for them. Track that this row did synchronous
2114
2371
  // indexing work so we can still yield the event loop after it.
2115
2372
  let didSynchronousIndexing = false;
2116
2373
  // every index operation needs to be guarded by the version still be the same. If it has already changed before
2117
2374
  // we index, that's fine because indexing is idempotent, we can just put the same values again. If it changes
2118
2375
  // during the indexing, the indexing here will fail. This is also fine because it means the other thread will have
2119
2376
  // performed indexing and we don't need to do anything further
2120
- for (let i = 0; i < attributesLength; i++) {
2121
- const attribute = attributes[i];
2122
- const property = attribute.name;
2123
- const index = attribute.dbi;
2124
- try {
2125
- const resolver = attribute.resolve;
2126
- const value = record && (resolver ? resolver(record) : record[property]);
2127
- if (index.customIndex) {
2128
- index.customIndex.index(key, value);
2129
- didSynchronousIndexing = true;
2130
- continue;
2131
- }
2132
- const values = getIndexedValues(value, index.indexNulls);
2133
- if (values) {
2134
- for (let i = 0, l = values.length; i < l; i++) {
2135
- lastResolution = index.put(values[i], key);
2377
+ if (record) {
2378
+ for (let i = 0; i < attributesLength; i++) {
2379
+ const attribute = attributes[i];
2380
+ const property = attribute.name;
2381
+ const index = attribute.dbi;
2382
+ const onPutRejected = putRejectionHandlers[i];
2383
+ try {
2384
+ const resolver = attribute.resolve;
2385
+ const value = record && (resolver ? resolver(record) : record[property]);
2386
+ if (index.customIndex) {
2387
+ index.customIndex.index(key, value);
2388
+ didSynchronousIndexing = true;
2389
+ continue;
2390
+ }
2391
+ const values = getIndexedValues(value, index.indexNulls);
2392
+ if (values) {
2393
+ for (let i = 0, l = values.length; i < l; i++) {
2394
+ track(index.put(values[i], key), onPutRejected);
2395
+ }
2396
+ }
2397
+ } catch (error) {
2398
+ hadIndexingErrors = true;
2399
+ if (!attributeErrorReported[property]) {
2400
+ // just report an indexing error once per attribute so we don't spam the logs.
2401
+ // A store closed by worker shutdown surfaces here as "Database not open"; that is
2402
+ // a benign interruption (the next generation re-runs the backfill), so don't log
2403
+ // it as an error — the outer catch returns quietly once the iterator also throws.
2404
+ attributeErrorReported[property] = true;
2405
+ if (Table.primaryStore?.rootStore?.status === 'closed')
2406
+ logger.debug(`Indexing attribute ${property} interrupted by store shutdown`, error);
2407
+ else logger.error(`Error indexing attribute ${property}`, error);
2136
2408
  }
2137
- }
2138
- } catch (error) {
2139
- hadIndexingErrors = true;
2140
- if (!attributeErrorReported[property]) {
2141
- // just report an indexing error once per attribute so we don't spam the logs.
2142
- // A store closed by worker shutdown surfaces here as "Database not open"; that is
2143
- // a benign interruption (the next generation re-runs the backfill), so don't log
2144
- // it as an error — the outer catch returns quietly once the iterator also throws.
2145
- attributeErrorReported[property] = true;
2146
- if (Table.primaryStore?.rootStore?.status === 'closed')
2147
- logger.debug(`Indexing attribute ${property} interrupted by store shutdown`, error);
2148
- else logger.error(`Error indexing attribute ${property}`, error);
2149
2409
  }
2150
2410
  }
2151
2411
  }
2152
- when(
2153
- lastResolution,
2154
- () => outstanding--,
2155
- (error) => {
2156
- outstanding--;
2157
- hadIndexingErrors = true;
2158
- logger.error(error);
2159
- }
2160
- );
2161
2412
  if (workerData && workerData.restartNumber !== manageThreads.restartNumber) {
2162
2413
  interrupted = true;
2163
2414
  }
2164
- if (++indexed % 100 === 0 || interrupted) {
2165
- // occasionally update our progress so if we crash, we can resume
2166
- for (const attribute of attributes) {
2167
- attribute.lastIndexedKey = key;
2168
- Table.dbisDB.put(attribute.key, attribute);
2169
- }
2170
- if (interrupted) return;
2415
+ if (interrupted) {
2416
+ await drainMutations();
2417
+ await checkpointing;
2418
+ await persistCheckpoint(key);
2419
+ return;
2171
2420
  }
2172
- if (outstanding > MAX_OUTSTANDING_INDEXING) await lastResolution;
2173
- else if (outstanding > MIN_OUTSTANDING_INDEXING)
2174
- await new Promise((resolve) => setImmediate(resolve)); // yield event turn, don't want to use all computation
2175
- else if (didSynchronousIndexing) await new Promise((resolve) => setImmediate(resolve)); // custom indexes (e.g. HNSW) index synchronously and never raise `outstanding`; without this yield a large backfill runs in a single event-loop turn, starving keepalive/replication and queries and never letting the isIndexing flag be observed
2421
+ if (atInterval && indexed >= nextCheckpointRecord && performance.now() >= nextCheckpointAt) {
2422
+ nextCheckpointAt = performance.now() + indexingCheckpointPeriodMs;
2423
+ nextCheckpointRecord = indexed + indexingCheckpointMinRecords;
2424
+ await checkpointing;
2425
+ checkpointing = persistCheckpoint(key);
2426
+ }
2427
+ // Checked once per record, so a record's own fan-out can overshoot before the bound applies.
2428
+ while (pendingMutations.size > MAX_OUTSTANDING_INDEXING) await nextSettlement();
2429
+ if (atInterval || didSynchronousIndexing || pendingMutations.size > MIN_OUTSTANDING_INDEXING)
2430
+ await yieldEventTurn();
2176
2431
  }
2177
2432
  }
2178
- // Await the last pending put. If it rejects, that is also an indexing error.
2179
- // Note: the when() calls above already attach rejection handlers to each record's
2180
- // last-put promise; this try-catch specifically handles the case where lastResolution
2181
- // itself rejects (i.e. the very last put in the loop failed) which would otherwise
2182
- // throw past the hadIndexingErrors check to the outer catch. The broader issue of
2183
- // unhandled rejections from non-last puts in multi-value attributes is pre-existing
2184
- // and out of scope for this fix.
2185
- try {
2186
- await lastResolution;
2187
- } catch (error) {
2188
- hadIndexingErrors = true;
2189
- logger.error(error);
2433
+ await checkpointing;
2434
+ // A mutation that rejects after completion is declared has no build left to park.
2435
+ await drainMutations();
2436
+ // the tail since the last checkpoint is not durable until flushed; announcing the index complete
2437
+ // before that would outlive a crash that loses it
2438
+ if (!hadIndexingErrors) {
2439
+ try {
2440
+ await flushIndexStores(Table.primaryStore.rootStore);
2441
+ } catch (error) {
2442
+ hadIndexingErrors = true;
2443
+ logger.error(`Could not flush the indexes of ${Table.tableName} before marking them complete`, error);
2444
+ }
2190
2445
  }
2191
- // Yield one more event turn so any queued when() error callbacks (which fire as
2192
- // microtasks when their tracked promise settles) have a chance to set hadIndexingErrors
2193
- // before we decide whether to mark indexing as complete.
2194
- await new Promise((resolve) => setImmediate(resolve));
2195
2446
  if (hadIndexingErrors) {
2196
2447
  // Some records failed to index. Persist the failure marker in the descriptor so
2197
2448
  // the next call to table() (including after a restart with a fresh PID) re-triggers
@@ -2220,9 +2471,15 @@ async function runIndexing(Table, attributes, indicesToRemove) {
2220
2471
  // update the attributes to indicate that we are finished
2221
2472
  for (const attribute of attributes) {
2222
2473
  delete attribute.lastIndexedKey;
2474
+ delete attribute.checkpointCertified;
2475
+ // Survives completion, unlike the checkpoint fields: without it an index built here is
2476
+ // indistinguishable from one a release that could skip a failed record declared complete.
2477
+ attribute.checkpointAlgorithm = CHECKPOINT_ALGORITHM;
2223
2478
  delete attribute.indexingPID;
2224
2479
  delete attribute.indexingFailed;
2225
2480
  delete attribute.restartNumber;
2481
+ delete attribute.indexingIncarnation;
2482
+ delete attribute.indexingBuildId;
2226
2483
  attribute.dbi.isIndexing = false;
2227
2484
  // Also clear isIndexing on the currently-active dbi in Table.indices, which may
2228
2485
  // differ from attribute.dbi if a resetDatabases() call during this migration
@@ -2239,6 +2496,7 @@ async function runIndexing(Table, attributes, indicesToRemove) {
2239
2496
  logger.info(`Finished indexing ${Table.tableName} attributes`, attributes);
2240
2497
  }
2241
2498
  } catch (error) {
2499
+ await checkpointing;
2242
2500
  // A worker shutting down closes its stores mid-backfill, so the range iterator or a
2243
2501
  // put throws (e.g. "Database not open" / "Iterator not initialized"). This is an
2244
2502
  // interruption, not a data error: the next worker generation re-runs the backfill via