@harperfast/harper 5.2.9 → 5.2.10

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 +9 -0
  13. package/dist/resources/databases.js +315 -76
  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 +296 -70
  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-Br06zdMA.js +2267 -0
  35. package/studio/web/assets/FloatingChat-BWImX5fA.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-DN0nmDsq.js} +1 -1
  39. package/studio/web/assets/applications-kSxVoyeU.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-C1JeufH5.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-qoo9CrGO.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-Dt4IIy04.js} +1 -1
  88. package/studio/web/assets/{htmlMode-DyO31v-P.js → htmlMode-DXgKKr4C.js} +1 -1
  89. package/studio/web/assets/index-6onkYFOG.js +824 -0
  90. package/studio/web/assets/{index-Cxj2_wsl.css → index-7RMEgVG1.css} +1 -1
  91. package/studio/web/assets/index.lazy-BrCFnpNJ.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-DNCQGUBc.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-CR6HWruP.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-BM4fI4rS.js} +1 -1
  100. package/studio/web/assets/{lspLanguageFeatures-B4pCF1zO.js → lspLanguageFeatures-DSa1ttcD.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-D3tIQ4sg.js → notifications-BHXLnh6x.js} +1 -1
  105. package/studio/web/assets/notifications-CUmtIA6z.js +1 -0
  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-8BeFSF3j.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-C8qR0HhD.js} +1 -1
  119. package/studio/web/assets/{register-BUyhWjBO.js → register-B4n5i0SD.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-g0_B0lgX.js} +1 -1
  124. package/studio/web/assets/{setup-D2kn7cAA.js → setup-B0CTj_Q5.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-C6Yib7-K.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-Dgta7bVi.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-CH_jHvU-.js} +1 -1
  136. package/studio/web/assets/{typescript-DApRQir3.js → typescript-Co9LCXd5.js} +1 -1
  137. package/studio/web/assets/{useEntityRestURL-DB6JStU1.js → useEntityRestURL-wKC8NsC_.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-CWeLxCXA.js} +1 -1
  148. package/studio/web/assets/x-DIzaLEdK.js +1 -0
  149. package/studio/web/assets/{xml-HWd01lU-.js → xml-BadC-0Rk.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-BiUfxPbC.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';
@@ -348,6 +350,20 @@ _assignPackageExport('databases', databases);
348
350
  _assignPackageExport('tables', tables);
349
351
 
350
352
  const NEXT_TABLE_ID = Symbol.for('next-table-id');
353
+ /**
354
+ * True when a descriptor claims an index build no live operation in this process can own. The PID and
355
+ * worker generation cannot answer that alone: a container reuses PID 1 and starts the in-memory
356
+ * generation back at 1 while the persisted one is higher. A descriptor with no incarnation was written
357
+ * before the field existed, so it belongs to an earlier process; a thread started without one of its
358
+ * own cannot judge, and falls back rather than declaring a live build dead.
359
+ */
360
+ function isAbandonedIndexBuild(descriptor: any, currentRestartGeneration: number): boolean {
361
+ if (!descriptor) return false;
362
+ if (descriptor.indexingPID && descriptor.indexingPID !== process.pid) return true;
363
+ if (descriptor.restartNumber < currentRestartGeneration) return true;
364
+ const incarnation = manageThreads.processIncarnation;
365
+ return !!descriptor.indexingPID && incarnation != null && descriptor.indexingIncarnation !== incarnation;
366
+ }
351
367
  // How many times the schema load will try to finish a tombstoned drop before
352
368
  // giving up for the rest of this process's lifetime. A drop that fails once
353
369
  // almost always fails identically forever - the usual cause is a RocksDB
@@ -637,6 +653,7 @@ export function readMetaDb(
637
653
  lmdbDatabaseEnvs.set(path, rootStore);
638
654
  }
639
655
 
656
+ rootStore.dbisDb?.resetReadTxn();
640
657
  return initStores(path, rootStore, databaseName, defaultTable, auditPath, isLegacy);
641
658
  } catch (error) {
642
659
  error.message += ` opening database ${path}`;
@@ -896,6 +913,8 @@ function initStores(
896
913
  indices[attribute.name] = dbi;
897
914
  indices[attribute.name].indexNulls = attribute.indexNulls;
898
915
  }
916
+ // the only way a thread that never declares the schema reaches Table.indices
917
+ indices[attribute.name].isIndexing = !!attribute.indexingPID;
899
918
  const existingAttribute = existingAttributes.find(
900
919
  (existingAttribute) => existingAttribute.name === attribute.name
901
920
  );
@@ -1253,6 +1272,10 @@ export async function dropDatabase(databaseName) {
1253
1272
  databaseEventsEmitter.emit('dropDatabase', databaseName);
1254
1273
 
1255
1274
  if (rootStore) {
1275
+ // awaited: retirement stops the loop admitting work, the barrier is what says the pass that was
1276
+ // already running has released the stores this is about to close and unlink
1277
+ await rootStore.auditStore?.stopAuditCleanup?.();
1278
+ removeStorageReclamation(rootStore.path);
1256
1279
  if (rootStore.status === 'open') {
1257
1280
  if (rootStore instanceof RocksDatabase) {
1258
1281
  rootStore.close();
@@ -1267,6 +1290,8 @@ export async function dropDatabase(databaseName) {
1267
1290
  // a tableless database resolves its root store here rather than in the loop above, so take
1268
1291
  // the drop lock now (still before any destructive step)
1269
1292
  if (rootStore instanceof RocksDatabase) lockDatabaseForDrop(rootStore.path, databaseName, restoreLocks);
1293
+ await rootStore.auditStore?.stopAuditCleanup?.();
1294
+ removeStorageReclamation(rootStore.path);
1270
1295
  if (rootStore instanceof RocksDatabase) {
1271
1296
  rootStore.close();
1272
1297
  rootStore.destroy();
@@ -1304,17 +1329,27 @@ export function closeDatabase(databaseName: string): boolean {
1304
1329
  const table: any = dbTables[tableName];
1305
1330
  if (!table?.primaryStore) continue;
1306
1331
  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
1332
  }
1312
1333
  // a database with no tables (an empty schema, or one whose tables were all dropped) still holds
1313
1334
  // an open root store, tracked only on the defined-database entry rather than any table — include
1314
1335
  // it so its handles are released too (the Set dedupes it against the per-table root stores above)
1315
1336
  const definedRoot = (definedDatabases?.get(databaseName) as any)?.rootStore;
1316
1337
  if (definedRoot) rootStores.add(definedRoot);
1338
+ // before any table store closes, so no further pass is admitted. This is synchronous, so it cannot
1339
+ // await the drain barrier stopAuditCleanup() returns; what covers it is the in-pass status checks,
1340
+ // plus the fact that its production callers reach it only for RocksDB databases, whose pass is one
1341
+ // synchronous purgeLogs() call with nothing suspended mid-removal.
1342
+ for (const rootStore of rootStores) rootStore.auditStore?.stopAuditCleanup?.();
1343
+ for (const tableName in dbTables) {
1344
+ const table: any = dbTables[tableName];
1345
+ if (!table?.primaryStore) continue;
1346
+ for (const indexName in table.indices || {}) {
1347
+ closeStore(table.indices[indexName], `index ${tableName}.${indexName}`);
1348
+ }
1349
+ closeStore(table.primaryStore, `table ${tableName}`);
1350
+ }
1317
1351
  for (const rootStore of rootStores) {
1352
+ removeStorageReclamation(rootStore.path);
1318
1353
  closeStore(rootStore.dbisDb, 'attributes store');
1319
1354
  closeStore(rootStore, 'root store');
1320
1355
  lmdbDatabaseEnvs.delete(rootStore.path);
@@ -1882,8 +1917,7 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
1882
1917
  changed ||
1883
1918
  indexFormatNeedsPersist ||
1884
1919
  attributeDescriptor?.indexingFailed ||
1885
- (attributeDescriptor?.indexingPID && attributeDescriptor?.indexingPID !== process.pid) ||
1886
- attributeDescriptor?.restartNumber < currentRestartGeneration
1920
+ isAbandonedIndexBuild(attributeDescriptor, currentRestartGeneration)
1887
1921
  ) {
1888
1922
  hasChanges = true;
1889
1923
  exclusiveLock();
@@ -1891,8 +1925,7 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
1891
1925
  if (
1892
1926
  structurallyChanged ||
1893
1927
  attributeDescriptor?.indexingFailed ||
1894
- (attributeDescriptor?.indexingPID && attributeDescriptor?.indexingPID !== process.pid) ||
1895
- attributeDescriptor?.restartNumber < currentRestartGeneration
1928
+ isAbandonedIndexBuild(attributeDescriptor, currentRestartGeneration)
1896
1929
  ) {
1897
1930
  hasChanges = true;
1898
1931
  if (attribute.indexNulls === undefined) attribute.indexNulls = true;
@@ -1912,9 +1945,17 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
1912
1945
  // resumes rather than restarts. Canonicalized to match structurallyChanged above.
1913
1946
  const indexOptionsChanged =
1914
1947
  canonicalIndexKey(attributeDescriptor?.indexed) !== canonicalIndexKey(attribute.indexed);
1915
- attribute.lastIndexedKey = indexOptionsChanged
1916
- ? undefined
1917
- : (attributeDescriptor?.lastIndexedKey ?? undefined);
1948
+ // Only a checkpoint runIndexing stamped with its own key resumes: earlier releases advanced
1949
+ // lastIndexedKey past failed and unflushed index writes, so any other is a full rebuild.
1950
+ const uncertifiedCheckpoint =
1951
+ attributeDescriptor?.lastIndexedKey !== undefined &&
1952
+ (attributeDescriptor.checkpointCertified === undefined ||
1953
+ compareKeys(attributeDescriptor.checkpointCertified, attributeDescriptor.lastIndexedKey) !== 0);
1954
+ attribute.lastIndexedKey =
1955
+ indexOptionsChanged || uncertifiedCheckpoint
1956
+ ? undefined
1957
+ : (attributeDescriptor?.lastIndexedKey ?? undefined);
1958
+ if (attribute.lastIndexedKey !== undefined) attribute.checkpointCertified = attribute.lastIndexedKey;
1918
1959
  // Explicit reindex is the upgrade path from a legacy (un-versioned) custom-index
1919
1960
  // object store to the versioned, VT-cacheable format. A full rebuild from scratch
1920
1961
  // (lastIndexedKey === undefined) clears the store and rewrites every node, so the
@@ -1938,6 +1979,9 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
1938
1979
  // the new process reuses the old PID. Cleared on clean completion; left in place
1939
1980
  // on failure/crash so the next, higher-numbered restart re-triggers the backfill.
1940
1981
  attribute.restartNumber = currentRestartGeneration;
1982
+ if (manageThreads.processIncarnation != null)
1983
+ attribute.indexingIncarnation = manageThreads.processIncarnation;
1984
+ attribute.indexingBuildId = randomBytes(8).toString('hex');
1941
1985
  delete attribute.indexingFailed; // clear failure flag for the new run
1942
1986
  dbi.isIndexing = true;
1943
1987
  Object.defineProperty(attribute, 'dbi', { value: dbi, configurable: true, enumerable: false });
@@ -1951,6 +1995,13 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
1951
1995
  if (attributeDescriptor?.indexingPID && attributeDescriptor.indexingPID !== process.pid)
1952
1996
  reindexReasons.push(`crash-recovery(pid=${attributeDescriptor.indexingPID})`);
1953
1997
  if (attributeDescriptor?.restartNumber < currentRestartGeneration) reindexReasons.push('restart-number');
1998
+ if (uncertifiedCheckpoint) reindexReasons.push('uncertified-checkpoint');
1999
+ if (
2000
+ attributeDescriptor?.indexingPID === process.pid &&
2001
+ manageThreads.processIncarnation != null &&
2002
+ attributeDescriptor.indexingIncarnation !== manageThreads.processIncarnation
2003
+ )
2004
+ reindexReasons.push('abandoned-build(previous process incarnation)');
1954
2005
  logger.info(
1955
2006
  `reindex ${databaseName}.${tableName}.${attribute.name}: reason=${reindexReasons.join(',') || 'unknown'}`
1956
2007
  );
@@ -1964,9 +2015,13 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
1964
2015
  // workers / a reload would treat the still-partial index as ready and return incomplete results.
1965
2016
  attribute.indexingPID = attributeDescriptor.indexingPID;
1966
2017
  attribute.lastIndexedKey = attributeDescriptor.lastIndexedKey;
2018
+ if (attributeDescriptor.checkpointCertified !== undefined)
2019
+ attribute.checkpointCertified = attributeDescriptor.checkpointCertified;
1967
2020
  // Carry the in-progress restart generation too, so persisting this metadata-only
1968
2021
  // change doesn't drop it and break the crash-recovery trigger for the running backfill.
1969
2022
  attribute.restartNumber = attributeDescriptor.restartNumber;
2023
+ attribute.indexingIncarnation = attributeDescriptor.indexingIncarnation;
2024
+ attribute.indexingBuildId = attributeDescriptor.indexingBuildId;
1970
2025
  if (attributeDescriptor.indexingFailed) attribute.indexingFailed = attributeDescriptor.indexingFailed;
1971
2026
  }
1972
2027
  attributesDbi.put(dbiKey, attribute);
@@ -1994,7 +2049,10 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
1994
2049
  }
1995
2050
  logger.trace(`${tableName} table loading, running index`);
1996
2051
  if (attributesToIndex.length > 0 || indicesToRemove.length > 0) {
1997
- Table.indexingOperation = runIndexing(Table, attributesToIndex, indicesToRemove);
2052
+ // captured before the backfill can rewrite the attributes
2053
+ const buildIds = new Map(attributesToIndex.map((attribute) => [attribute, attribute.indexingBuildId]));
2054
+ const markSettled = () => markAbandonedIndexBuild(Table, rootStore, buildIds);
2055
+ Table.indexingOperation = runIndexing(Table, attributesToIndex, indicesToRemove).then(markSettled, markSettled);
1998
2056
  } else if (hasChanges)
1999
2057
  signalling.signalSchemaChange(
2000
2058
  new SchemaEventMsg(process.pid, 'schema-change', Table.databaseName, Table.tableName)
@@ -2066,7 +2124,139 @@ export function canonicalizeIndexOptions(value: any): any {
2066
2124
  }
2067
2125
  const MAX_OUTSTANDING_INDEXING = 1000;
2068
2126
  const MIN_OUTSTANDING_INDEXING = 10;
2127
+ const INDEXING_YIELD_INTERVAL = 100;
2128
+ // A resumable checkpoint is written only after a flush (see flushIndexStores), at most once per period
2129
+ // and never before this many more records: the flush seals every column family in the database, so a
2130
+ // slow backfill must not impose the period's flush rate on unrelated tables.
2131
+ let indexingCheckpointPeriodMs = 5000;
2132
+ let indexingCheckpointMinRecords = 10000;
2133
+ export function setIndexingCheckpointPeriod(ms: number, minRecords = indexingCheckpointMinRecords) {
2134
+ const previous = { ms: indexingCheckpointPeriodMs, minRecords: indexingCheckpointMinRecords };
2135
+ indexingCheckpointPeriodMs = ms;
2136
+ indexingCheckpointMinRecords = minRecords;
2137
+ return previous;
2138
+ }
2139
+ const yieldEventTurn = () => new Promise((resolve) => setImmediate(resolve));
2140
+ // RocksDB index stores have no WAL (openRocksDatabase defaults disableWAL), so a flush is what makes the
2141
+ // entries a checkpoint certifies durable. A flush only covers writes issued before it started, so a caller
2142
+ // never joins one in flight: it joins the next one, which every backfill on that database asking meanwhile
2143
+ // shares — at most one in flight and one queued.
2144
+ const indexingFlushes = new WeakMap<object, { inFlight?: Promise<void>; queued?: Promise<void> }>();
2145
+ function flushIndexStores(rootStore: any): Promise<void> | undefined {
2146
+ if (!(rootStore instanceof RocksDatabase)) return;
2147
+ let flushes = indexingFlushes.get(rootStore);
2148
+ if (!flushes) indexingFlushes.set(rootStore, (flushes = {}));
2149
+ if (flushes.queued) return flushes.queued;
2150
+ const start = () => {
2151
+ flushes.queued = undefined;
2152
+ const flush = rootStore.flush().finally(() => {
2153
+ if (flushes.inFlight === flush) flushes.inFlight = undefined;
2154
+ });
2155
+ flushes.inFlight = flush;
2156
+ return flush;
2157
+ };
2158
+ if (!flushes.inFlight) return start();
2159
+ return (flushes.queued = flushes.inFlight.then(start, start));
2160
+ }
2161
+ export function resumeStartKey(attributes: { lastIndexedKey?: any }[]): any {
2162
+ let start: any;
2163
+ for (const attribute of attributes) {
2164
+ if (attribute.lastIndexedKey == undefined) return undefined;
2165
+ if (start === undefined || compareKeys(attribute.lastIndexedKey, start) < 0) start = attribute.lastIndexedKey;
2166
+ }
2167
+ return start;
2168
+ }
2169
+
2170
+ // Bounded, unlike the exclusiveLock() spin above: that one runs on the declaring path, where the
2171
+ // caller is waiting on the result and there is nothing useful to do without the lock. This one runs
2172
+ // after a backfill has already settled, so a holder that never releases would wedge the worker's
2173
+ // event loop for nothing. Giving up costs only the marker, and the next load of the table
2174
+ // re-triggers the build regardless.
2175
+ export const ABANDONED_MARK_LOCK_TIMEOUT = 10000;
2176
+ const abandonedMarkLockWait = new Int32Array(new SharedArrayBuffer(4));
2177
+ // `timeout` is the test seam; production callers take the default.
2178
+ export function tryAcquireUpdateAttributesLock(
2179
+ rootStore: RocksDatabase,
2180
+ timeout = ABANDONED_MARK_LOCK_TIMEOUT
2181
+ ): boolean {
2182
+ if (rootStore.tryLock('update-attributes')) return true;
2183
+ const startTime = performance.now();
2184
+ let waitTime = 1;
2185
+ while (!rootStore.tryLock('update-attributes')) {
2186
+ const elapsed = performance.now() - startTime;
2187
+ if (elapsed >= timeout) return false;
2188
+ // Atomics.wait rather than a busy spin: the section this guards is synchronous, so the wait
2189
+ // blocks this thread either way, but sleeping does not burn a core while it does.
2190
+ if (elapsed >= 2) {
2191
+ Atomics.wait(abandonedMarkLockWait, 0, 0, Math.min(waitTime, timeout - elapsed));
2192
+ if (waitTime < 16) waitTime *= 2;
2193
+ }
2194
+ }
2195
+ return true;
2196
+ }
2197
+
2198
+ /**
2199
+ * Persists the failure marker for a build that ended without running one of runIndexing's own exit
2200
+ * paths, so something re-triggers it. Fenced on `indexingBuildId` inside the storage engine's catalog
2201
+ * serialization boundary, because a replacement generation (or another thread declaring different index
2202
+ * options) can claim the attribute before an outgoing build's promise settles, and marking that would fail
2203
+ * a live build. The fence read and write stay synchronous, and nothing here may throw because
2204
+ * `Table.indexingOperation` reaches operations-API callers.
2205
+ */
2206
+ async function markAbandonedIndexBuild(Table, rootStore, buildIds: Map<any, string>) {
2207
+ for (const [attribute, buildId] of buildIds) {
2208
+ try {
2209
+ let marked;
2210
+ if (buildId == null || Table.dbisDB.getSync(attribute.key)?.indexingBuildId !== buildId) continue;
2211
+ const markIfOwned = () => {
2212
+ const descriptor = Table.dbisDB.getSync(attribute.key);
2213
+ if (descriptor?.indexingBuildId === buildId && !descriptor.indexingFailed) {
2214
+ Table.dbisDB.putSync(attribute.key, { ...descriptor, indexingFailed: true });
2215
+ marked = true;
2216
+ }
2217
+ };
2218
+ if (rootStore instanceof RocksDatabase) {
2219
+ if (!tryAcquireUpdateAttributesLock(rootStore)) {
2220
+ logger.warn(
2221
+ `Could not mark the abandoned index build of ${Table.databaseName}.${Table.tableName}.${attribute.name}: ` +
2222
+ `timed out after ${ABANDONED_MARK_LOCK_TIMEOUT}ms waiting for the exclusive 'update-attributes' lock. ` +
2223
+ `The index stays incomplete and the next load of the table re-triggers the backfill.`
2224
+ );
2225
+ continue;
2226
+ }
2227
+ try {
2228
+ markIfOwned();
2229
+ } finally {
2230
+ rootStore.unlock('update-attributes');
2231
+ }
2232
+ } else {
2233
+ rootStore.transactionSync(markIfOwned);
2234
+ }
2235
+ if (marked)
2236
+ logger.warn(
2237
+ `Indexing of ${Table.databaseName}.${Table.tableName}.${attribute.name} ended without completing. ` +
2238
+ `The index stays incomplete and every query on the attribute reports it as not indexed yet; ` +
2239
+ `the next load of the table retries the backfill from the last checkpoint (indexingFailed=true).`
2240
+ );
2241
+ } catch (error) {
2242
+ // A store closed by shutdown is the common case, and it cannot be written to at all.
2243
+ try {
2244
+ logger.debug(`Could not mark the abandoned index build of ${Table.tableName}.${attribute.name}`, error);
2245
+ } catch {}
2246
+ }
2247
+ }
2248
+ }
2069
2249
  async function runIndexing(Table, attributes, indicesToRemove) {
2250
+ let checkpointing;
2251
+ let hadIndexingErrors = false;
2252
+ const attributeErrorReported = {};
2253
+ const onIndexPutRejected = (property, error) => {
2254
+ hadIndexingErrors = true;
2255
+ if (attributeErrorReported[property]) return;
2256
+ attributeErrorReported[property] = true;
2257
+ logger.error(`Error indexing attribute ${property}`, error);
2258
+ };
2259
+ const putRejectionHandlers = attributes.map((attribute) => (error) => onIndexPutRejected(attribute.name, error));
2070
2260
  try {
2071
2261
  logger.info(`Indexing ${Table.tableName} attributes`, attributes);
2072
2262
  await signalling.signalSchemaChange(
@@ -2075,20 +2265,16 @@ async function runIndexing(Table, attributes, indicesToRemove) {
2075
2265
  let lastResolution;
2076
2266
  for (const index of indicesToRemove) {
2077
2267
  lastResolution = index.drop();
2268
+ if (lastResolution?.then) lastResolution.then(undefined, (error) => onIndexPutRejected(index.name, error));
2078
2269
  }
2079
2270
  let interrupted;
2080
- let hadIndexingErrors = false;
2081
- const attributeErrorReported = {};
2082
2271
  let indexed = 0;
2083
2272
  const attributesLength = attributes.length;
2084
2273
  await new Promise((resolve) => setImmediate(resolve)); // yield event turn, indexing should consistently take at least one event turn
2085
2274
  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
2275
+ const start = resumeStartKey(attributes);
2276
+ if (start === undefined) {
2277
+ for (const attribute of attributes) {
2092
2278
  if (attribute.dbi.clearAsync) {
2093
2279
  // LMDB, note that we don't need to wait for this to complete, just gets enqueued in front of the other writes
2094
2280
  attribute.dbi.clearAsync();
@@ -2098,6 +2284,26 @@ async function runIndexing(Table, attributes, indicesToRemove) {
2098
2284
  }
2099
2285
  }
2100
2286
  let outstanding = 0;
2287
+ // A resumed scan starts at the checkpoint, so it must only name a key whose every predecessor is
2288
+ // durably indexed: persisted once the writes it covers have settled and flushed, frozen after any
2289
+ // record fails so the retry re-covers it, and stamped with its own key (see the trigger in table()).
2290
+ const persistCheckpoint = async (key) => {
2291
+ if (hadIndexingErrors) return;
2292
+ try {
2293
+ await flushIndexStores(Table.primaryStore.rootStore);
2294
+ const puts = [];
2295
+ for (const attribute of attributes) {
2296
+ attribute.lastIndexedKey = key;
2297
+ attribute.checkpointCertified = key;
2298
+ puts.push(Table.dbisDB.put(attribute.key, attribute));
2299
+ }
2300
+ await Promise.all(puts);
2301
+ } catch (error) {
2302
+ logger.warn(`Could not persist the indexing checkpoint for ${Table.tableName}`, error);
2303
+ }
2304
+ };
2305
+ let nextCheckpointAt = performance.now() + indexingCheckpointPeriodMs;
2306
+ let nextCheckpointRecord = indexingCheckpointMinRecords;
2101
2307
  // this means that a new attribute has been introduced that needs to be indexed
2102
2308
  for (const { key, value: record } of Table.primaryStore.getRange({
2103
2309
  start,
@@ -2105,7 +2311,7 @@ async function runIndexing(Table, attributes, indicesToRemove) {
2105
2311
  versions: true,
2106
2312
  snapshot: false, // don't hold a read transaction this whole time
2107
2313
  })) {
2108
- if (!record) continue; // deletion entry
2314
+ const atInterval = ++indexed % INDEXING_YIELD_INTERVAL === 0;
2109
2315
  // TODO: Do we ever need to interrupt due to a schema change that was not a restart?
2110
2316
  //if (Table.schemaVersion !== schemaVersion) return; // break out if there are any schema changes and let someone else pick it up
2111
2317
  outstanding++;
@@ -2117,71 +2323,77 @@ async function runIndexing(Table, attributes, indicesToRemove) {
2117
2323
  // we index, that's fine because indexing is idempotent, we can just put the same values again. If it changes
2118
2324
  // during the indexing, the indexing here will fail. This is also fine because it means the other thread will have
2119
2325
  // 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);
2326
+ if (record) {
2327
+ for (let i = 0; i < attributesLength; i++) {
2328
+ const attribute = attributes[i];
2329
+ const property = attribute.name;
2330
+ const index = attribute.dbi;
2331
+ const onPutRejected = putRejectionHandlers[i];
2332
+ try {
2333
+ const resolver = attribute.resolve;
2334
+ const value = record && (resolver ? resolver(record) : record[property]);
2335
+ if (index.customIndex) {
2336
+ index.customIndex.index(key, value);
2337
+ didSynchronousIndexing = true;
2338
+ continue;
2339
+ }
2340
+ const values = getIndexedValues(value, index.indexNulls);
2341
+ if (values) {
2342
+ for (let i = 0, l = values.length; i < l; i++) {
2343
+ lastResolution = index.put(values[i], key);
2344
+ if (lastResolution?.then) lastResolution.then(undefined, onPutRejected);
2345
+ }
2346
+ }
2347
+ } catch (error) {
2348
+ hadIndexingErrors = true;
2349
+ if (!attributeErrorReported[property]) {
2350
+ // just report an indexing error once per attribute so we don't spam the logs.
2351
+ // A store closed by worker shutdown surfaces here as "Database not open"; that is
2352
+ // a benign interruption (the next generation re-runs the backfill), so don't log
2353
+ // it as an error — the outer catch returns quietly once the iterator also throws.
2354
+ attributeErrorReported[property] = true;
2355
+ if (Table.primaryStore?.rootStore?.status === 'closed')
2356
+ logger.debug(`Indexing attribute ${property} interrupted by store shutdown`, error);
2357
+ else logger.error(`Error indexing attribute ${property}`, error);
2136
2358
  }
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
2359
  }
2150
2360
  }
2151
2361
  }
2152
2362
  when(
2153
2363
  lastResolution,
2154
2364
  () => outstanding--,
2155
- (error) => {
2156
- outstanding--;
2157
- hadIndexingErrors = true;
2158
- logger.error(error);
2159
- }
2365
+ () => outstanding--
2160
2366
  );
2161
2367
  if (workerData && workerData.restartNumber !== manageThreads.restartNumber) {
2162
2368
  interrupted = true;
2163
2369
  }
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);
2370
+ if (interrupted) {
2371
+ try {
2372
+ await lastResolution;
2373
+ } catch {
2374
+ // already counted and logged by the rejection handler above
2169
2375
  }
2170
- if (interrupted) return;
2376
+ await checkpointing;
2377
+ await persistCheckpoint(key);
2378
+ return;
2379
+ }
2380
+ if (atInterval && indexed >= nextCheckpointRecord && performance.now() >= nextCheckpointAt) {
2381
+ nextCheckpointAt = performance.now() + indexingCheckpointPeriodMs;
2382
+ nextCheckpointRecord = indexed + indexingCheckpointMinRecords;
2383
+ await checkpointing;
2384
+ checkpointing = when(
2385
+ lastResolution,
2386
+ () => persistCheckpoint(key),
2387
+ () => {}
2388
+ );
2171
2389
  }
2172
2390
  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
2391
+ if (atInterval || didSynchronousIndexing || outstanding > MIN_OUTSTANDING_INDEXING) await yieldEventTurn();
2176
2392
  }
2177
2393
  }
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.
2394
+ await checkpointing;
2395
+ // Await the last pending put. If it rejects, that is also an indexing error (already counted by
2396
+ // onIndexPutRejected); catching it here keeps it from escaping to the outer catch.
2185
2397
  try {
2186
2398
  await lastResolution;
2187
2399
  } catch (error) {
@@ -2192,6 +2404,16 @@ async function runIndexing(Table, attributes, indicesToRemove) {
2192
2404
  // microtasks when their tracked promise settles) have a chance to set hadIndexingErrors
2193
2405
  // before we decide whether to mark indexing as complete.
2194
2406
  await new Promise((resolve) => setImmediate(resolve));
2407
+ // the tail since the last checkpoint is not durable until flushed; announcing the index complete
2408
+ // before that would outlive a crash that loses it
2409
+ if (!hadIndexingErrors) {
2410
+ try {
2411
+ await flushIndexStores(Table.primaryStore.rootStore);
2412
+ } catch (error) {
2413
+ hadIndexingErrors = true;
2414
+ logger.error(`Could not flush the indexes of ${Table.tableName} before marking them complete`, error);
2415
+ }
2416
+ }
2195
2417
  if (hadIndexingErrors) {
2196
2418
  // Some records failed to index. Persist the failure marker in the descriptor so
2197
2419
  // the next call to table() (including after a restart with a fresh PID) re-triggers
@@ -2220,9 +2442,12 @@ async function runIndexing(Table, attributes, indicesToRemove) {
2220
2442
  // update the attributes to indicate that we are finished
2221
2443
  for (const attribute of attributes) {
2222
2444
  delete attribute.lastIndexedKey;
2445
+ delete attribute.checkpointCertified;
2223
2446
  delete attribute.indexingPID;
2224
2447
  delete attribute.indexingFailed;
2225
2448
  delete attribute.restartNumber;
2449
+ delete attribute.indexingIncarnation;
2450
+ delete attribute.indexingBuildId;
2226
2451
  attribute.dbi.isIndexing = false;
2227
2452
  // Also clear isIndexing on the currently-active dbi in Table.indices, which may
2228
2453
  // differ from attribute.dbi if a resetDatabases() call during this migration
@@ -2239,6 +2464,7 @@ async function runIndexing(Table, attributes, indicesToRemove) {
2239
2464
  logger.info(`Finished indexing ${Table.tableName} attributes`, attributes);
2240
2465
  }
2241
2466
  } catch (error) {
2467
+ await checkpointing;
2242
2468
  // A worker shutting down closes its stores mid-backfill, so the range iterator or a
2243
2469
  // put throws (e.g. "Database not open" / "Iterator not initialized"). This is an
2244
2470
  // interruption, not a data error: the next worker generation re-runs the backfill via
@@ -1055,7 +1055,7 @@ export function filterByType(searchCondition, Table, context, filtered, isPrimar
1055
1055
  canUseIndex =
1056
1056
  canUseIndex && // is it a comparator that makes sense to use index
1057
1057
  !isPrimaryKey && // no need to use index for primary keys, since we will be iterating over the primary keys
1058
- Table?.indices[attribute] && // is there an index for this attribute
1058
+ !!(Table && usableIndex(Table, attribute)) && // is there a usable (complete) index for this attribute
1059
1059
  estimatedIncomingCount > 3; // do we have a valid estimate of multiple incoming records (that is worth using an index for)
1060
1060
  if (canUseIndex) {
1061
1061
  if (searchCondition.estimated_count == undefined) estimateCondition(Table)(searchCondition);
@@ -1112,6 +1112,28 @@ export function filterByType(searchCondition, Table, context, filtered, isPrimar
1112
1112
  }
1113
1113
  }
1114
1114
 
1115
+ /** The index a condition can be driven by; searchByIndex refuses a rebuilding one, so it reads as absent. */
1116
+ function usableIndex(table, attributeName): any {
1117
+ const index = attributeName == null ? undefined : table.indices[attributeName];
1118
+ return index?.isIndexing ? undefined : index;
1119
+ }
1120
+
1121
+ /**
1122
+ * True when an index this attribute would have to be driven by is still being built, following a
1123
+ * relationship path to the local join index and on to the related table's leaf index.
1124
+ */
1125
+ function drivesRebuildingIndex(table, attributeName, relationshipOffset = 0): boolean {
1126
+ if (!Array.isArray(attributeName))
1127
+ return attributeName != null && attributeName !== table.primaryKey && !!table.indices[attributeName]?.isIndexing;
1128
+ if (relationshipOffset >= attributeName.length - 1)
1129
+ return drivesRebuildingIndex(table, attributeName[relationshipOffset]);
1130
+ const attribute = findAttribute(table.attributes, attributeName[relationshipOffset]);
1131
+ if (!attribute) return false;
1132
+ if (table.indices[attribute.relationship?.from]?.isIndexing) return true;
1133
+ const relatedTable = attribute.definition?.tableClass || attribute.elements?.definition?.tableClass;
1134
+ return !!relatedTable && drivesRebuildingIndex(relatedTable, attributeName, relationshipOffset + 1);
1135
+ }
1136
+
1115
1137
  export function estimateCondition(table) {
1116
1138
  function estimateConditionForTable(condition) {
1117
1139
  if (condition.estimated_count === undefined) {
@@ -1141,7 +1163,10 @@ export function estimateCondition(table) {
1141
1163
  // skip if it is cached
1142
1164
  let searchType = condition.comparator || condition.search_type;
1143
1165
  searchType = ALTERNATE_COMPARATOR_NAMES[searchType] || searchType;
1144
- if (searchType === SEARCH_TYPES.EQUALS || !searchType) {
1166
+ // before comparator dispatch: several branches fall back to a finite table-fraction heuristic
1167
+ if (drivesRebuildingIndex(table, condition[0] ?? condition.attribute)) {
1168
+ condition.estimated_count = Infinity;
1169
+ } else if (searchType === SEARCH_TYPES.EQUALS || !searchType) {
1145
1170
  const attribute_name = condition[0] ?? condition.attribute;
1146
1171
  if (attribute_name == null || attribute_name === table.primaryKey) condition.estimated_count = 1;
1147
1172
  else if (Array.isArray(attribute_name) && attribute_name.length > 1) {
@@ -1167,12 +1192,12 @@ export function estimateCondition(table) {
1167
1192
  }
1168
1193
  } else {
1169
1194
  // we only attempt to estimate count on equals operator because that's really all that LMDB supports (some other key-value stores like libmdbx could be considered if we need to do estimated counts of ranges at some point)
1170
- const index = table.indices[attribute_name];
1195
+ const index = usableIndex(table, attribute_name);
1171
1196
  condition.estimated_count = index ? index.getValuesCount(condition[1] ?? condition.value) : Infinity;
1172
1197
  }
1173
1198
  } else if (searchType === 'contains' || searchType === 'ends_with' || searchType === 'ne') {
1174
1199
  const attribute_name = condition[0] ?? condition.attribute;
1175
- const index = table.indices[attribute_name];
1200
+ const index = usableIndex(table, attribute_name);
1176
1201
  if (condition.value === null && searchType === 'ne') {
1177
1202
  condition.estimated_count = Math.max(
1178
1203
  estimatedEntryCount(table.primaryStore) - (index ? index.getValuesCount(null) : 0),
@@ -1181,7 +1206,7 @@ export function estimateCondition(table) {
1181
1206
  } else condition.estimated_count = Infinity;
1182
1207
  } else if (searchType === 'in') {
1183
1208
  const attribute_name = condition[0] ?? condition.attribute;
1184
- const index = table.indices[attribute_name];
1209
+ const index = usableIndex(table, attribute_name);
1185
1210
  if (Array.isArray(condition.value) && index) {
1186
1211
  // Sum of per-value matches (over-counts duplicates but is a fine ceiling)
1187
1212
  let estimate = 0;
@@ -1199,7 +1224,7 @@ export function estimateCondition(table) {
1199
1224
  condition.estimated_count = BETWEEN_ESTIMATE * estimatedEntryCount(table.primaryStore) + 1;
1200
1225
  else if (searchType === 'sort') {
1201
1226
  const attribute_name = condition[0] ?? condition.attribute;
1202
- const index = table.indices[attribute_name];
1227
+ const index = usableIndex(table, attribute_name);
1203
1228
  if (index?.customIndex?.estimateCountAsSort)
1204
1229
  // allow custom index to define its own estimation of counts
1205
1230
  condition.estimated_count = index.customIndex.estimateCountAsSort(condition);
@@ -1207,7 +1232,7 @@ export function estimateCondition(table) {
1207
1232
  } else {
1208
1233
  // for the search types that use the broadest range, try do them last
1209
1234
  const attribute_name = condition[0] ?? condition.attribute;
1210
- const index = table.indices[attribute_name];
1235
+ const index = usableIndex(table, attribute_name);
1211
1236
  if (index?.customIndex?.estimateCount)
1212
1237
  // allow custom index to define its own estimation of counts
1213
1238
  condition.estimated_count = index.customIndex.estimateCount(condition.value);