@harperfast/harper 5.2.0 → 5.2.2

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 (308) hide show
  1. package/bin/copyDb.ts +40 -11
  2. package/bin/harper.ts +20 -51
  3. package/bin/help.ts +216 -0
  4. package/components/Application.ts +236 -46
  5. package/components/ApplicationScope.ts +26 -0
  6. package/components/EntryHandler.ts +410 -105
  7. package/components/RuntimeModuleTracker.ts +189 -0
  8. package/components/Scope.ts +68 -24
  9. package/components/componentLoader.ts +70 -16
  10. package/components/deployLifecycle.ts +119 -33
  11. package/components/deploymentRecorder.ts +102 -11
  12. package/components/mcp/tools/operations.ts +6 -0
  13. package/components/operations.js +8 -6
  14. package/config/configUtils.ts +78 -20
  15. package/config/harperConfigEnvVars.ts +114 -17
  16. package/config-root.schema.json +10 -0
  17. package/dataLayer/harperBridge/ResourceBridge.ts +26 -5
  18. package/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -4
  19. package/dataLayer/hdbInfoController.ts +8 -0
  20. package/dataLayer/schemaDescribe.ts +2 -1
  21. package/dist/bin/copyDb.js +33 -9
  22. package/dist/bin/copyDb.js.map +1 -1
  23. package/dist/bin/harper.d.ts +6 -0
  24. package/dist/bin/harper.js +18 -50
  25. package/dist/bin/harper.js.map +1 -1
  26. package/dist/bin/help.d.ts +8 -0
  27. package/dist/bin/help.js +192 -0
  28. package/dist/bin/help.js.map +1 -0
  29. package/dist/components/Application.d.ts +16 -1
  30. package/dist/components/Application.js +210 -38
  31. package/dist/components/Application.js.map +1 -1
  32. package/dist/components/ApplicationScope.d.ts +7 -0
  33. package/dist/components/ApplicationScope.js +22 -0
  34. package/dist/components/ApplicationScope.js.map +1 -1
  35. package/dist/components/EntryHandler.d.ts +4 -4
  36. package/dist/components/EntryHandler.js +386 -95
  37. package/dist/components/EntryHandler.js.map +1 -1
  38. package/dist/components/RuntimeModuleTracker.d.ts +11 -0
  39. package/dist/components/RuntimeModuleTracker.js +189 -0
  40. package/dist/components/RuntimeModuleTracker.js.map +1 -0
  41. package/dist/components/Scope.d.ts +1 -0
  42. package/dist/components/Scope.js +69 -22
  43. package/dist/components/Scope.js.map +1 -1
  44. package/dist/components/componentLoader.js +69 -10
  45. package/dist/components/componentLoader.js.map +1 -1
  46. package/dist/components/deployLifecycle.d.ts +6 -2
  47. package/dist/components/deployLifecycle.js +109 -31
  48. package/dist/components/deployLifecycle.js.map +1 -1
  49. package/dist/components/deploymentRecorder.d.ts +6 -0
  50. package/dist/components/deploymentRecorder.js +94 -12
  51. package/dist/components/deploymentRecorder.js.map +1 -1
  52. package/dist/components/mcp/tools/operations.js +6 -0
  53. package/dist/components/mcp/tools/operations.js.map +1 -1
  54. package/dist/components/operations.js +8 -6
  55. package/dist/components/operations.js.map +1 -1
  56. package/dist/config/configUtils.d.ts +6 -0
  57. package/dist/config/configUtils.js +78 -18
  58. package/dist/config/configUtils.js.map +1 -1
  59. package/dist/config/harperConfigEnvVars.js +108 -16
  60. package/dist/config/harperConfigEnvVars.js.map +1 -1
  61. package/dist/dataLayer/harperBridge/ResourceBridge.js +12 -5
  62. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  63. package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -4
  64. package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js.map +1 -1
  65. package/dist/dataLayer/hdbInfoController.js +4 -0
  66. package/dist/dataLayer/hdbInfoController.js.map +1 -1
  67. package/dist/dataLayer/schemaDescribe.js +2 -1
  68. package/dist/dataLayer/schemaDescribe.js.map +1 -1
  69. package/dist/resources/DatabaseTransaction.d.ts +57 -0
  70. package/dist/resources/DatabaseTransaction.js +284 -104
  71. package/dist/resources/DatabaseTransaction.js.map +1 -1
  72. package/dist/resources/ResourceInterface.d.ts +8 -2
  73. package/dist/resources/ResourceInterface.js.map +1 -1
  74. package/dist/resources/Resources.js +22 -4
  75. package/dist/resources/Resources.js.map +1 -1
  76. package/dist/resources/Table.d.ts +8 -6
  77. package/dist/resources/Table.js +158 -31
  78. package/dist/resources/Table.js.map +1 -1
  79. package/dist/resources/analytics/write.js +6 -6
  80. package/dist/resources/analytics/write.js.map +1 -1
  81. package/dist/resources/auditStore.js +27 -1
  82. package/dist/resources/auditStore.js.map +1 -1
  83. package/dist/resources/blob.d.ts +0 -1
  84. package/dist/resources/blob.js +15 -7
  85. package/dist/resources/blob.js.map +1 -1
  86. package/dist/resources/databases.d.ts +22 -5
  87. package/dist/resources/databases.js +118 -9
  88. package/dist/resources/databases.js.map +1 -1
  89. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +15 -2
  90. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +128 -39
  91. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
  92. package/dist/resources/jsResource.d.ts +4 -26
  93. package/dist/resources/jsResource.js +5 -59
  94. package/dist/resources/jsResource.js.map +1 -1
  95. package/dist/resources/models/Models.d.ts +11 -1
  96. package/dist/resources/models/Models.js +10 -1
  97. package/dist/resources/models/Models.js.map +1 -1
  98. package/dist/resources/models/backendRegistry.d.ts +9 -0
  99. package/dist/resources/models/backendRegistry.js +10 -0
  100. package/dist/resources/models/backendRegistry.js.map +1 -1
  101. package/dist/resources/models/openaiStream.d.ts +16 -1
  102. package/dist/resources/models/openaiStream.js +113 -21
  103. package/dist/resources/models/openaiStream.js.map +1 -1
  104. package/dist/resources/models/v1/chatCompletions.d.ts +54 -0
  105. package/dist/resources/models/v1/chatCompletions.js +115 -0
  106. package/dist/resources/models/v1/chatCompletions.js.map +1 -0
  107. package/dist/resources/models/v1/embeddings.d.ts +11 -0
  108. package/dist/resources/models/v1/embeddings.js +71 -0
  109. package/dist/resources/models/v1/embeddings.js.map +1 -0
  110. package/dist/resources/models/v1/errors.d.ts +54 -0
  111. package/dist/resources/models/v1/errors.js +130 -0
  112. package/dist/resources/models/v1/errors.js.map +1 -0
  113. package/dist/resources/models/v1/index.d.ts +36 -0
  114. package/dist/resources/models/v1/index.js +75 -0
  115. package/dist/resources/models/v1/index.js.map +1 -0
  116. package/dist/resources/models/v1/models.d.ts +26 -0
  117. package/dist/resources/models/v1/models.js +44 -0
  118. package/dist/resources/models/v1/models.js.map +1 -0
  119. package/dist/resources/models/v1/translation.d.ts +133 -0
  120. package/dist/resources/models/v1/translation.js +298 -0
  121. package/dist/resources/models/v1/translation.js.map +1 -0
  122. package/dist/resources/roles.d.ts +1 -1
  123. package/dist/resources/roles.js +54 -7
  124. package/dist/resources/roles.js.map +1 -1
  125. package/dist/resources/search.d.ts +1 -1
  126. package/dist/resources/search.js +7 -3
  127. package/dist/resources/search.js.map +1 -1
  128. package/dist/security/jsLoader.js +84 -33
  129. package/dist/security/jsLoader.js.map +1 -1
  130. package/dist/security/role.js +4 -0
  131. package/dist/security/role.js.map +1 -1
  132. package/dist/security/superUserGuard.d.ts +7 -0
  133. package/dist/security/superUserGuard.js +23 -0
  134. package/dist/security/superUserGuard.js.map +1 -0
  135. package/dist/security/tokenAuthentication.d.ts +0 -1
  136. package/dist/security/tokenAuthentication.js +6 -2
  137. package/dist/security/tokenAuthentication.js.map +1 -1
  138. package/dist/security/user.d.ts +6 -1
  139. package/dist/security/user.js +23 -1
  140. package/dist/security/user.js.map +1 -1
  141. package/dist/server/http.d.ts +23 -2
  142. package/dist/server/http.js +99 -12
  143. package/dist/server/http.js.map +1 -1
  144. package/dist/server/operationsServer.d.ts +0 -2
  145. package/dist/server/operationsServer.js.map +1 -1
  146. package/dist/server/serverHelpers/multipartParser.js +9 -0
  147. package/dist/server/serverHelpers/multipartParser.js.map +1 -1
  148. package/dist/server/serverHelpers/operationAuthorizationState.d.ts +2 -0
  149. package/dist/server/serverHelpers/operationAuthorizationState.js +13 -0
  150. package/dist/server/serverHelpers/operationAuthorizationState.js.map +1 -0
  151. package/dist/server/serverHelpers/registeredOperations.d.ts +3 -2
  152. package/dist/server/serverHelpers/registeredOperations.js +14 -13
  153. package/dist/server/serverHelpers/registeredOperations.js.map +1 -1
  154. package/dist/server/serverHelpers/serverHandlers.js +19 -3
  155. package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
  156. package/dist/server/serverHelpers/serverUtilities.d.ts +1 -1
  157. package/dist/server/serverHelpers/serverUtilities.js +10 -7
  158. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  159. package/dist/server/static.js +57 -18
  160. package/dist/server/static.js.map +1 -1
  161. package/dist/server/storageReclamation.d.ts +17 -0
  162. package/dist/server/storageReclamation.js +90 -6
  163. package/dist/server/storageReclamation.js.map +1 -1
  164. package/dist/server/threads/manageThreads.js +8 -0
  165. package/dist/server/threads/manageThreads.js.map +1 -1
  166. package/dist/server/threads/socketRouter.js +20 -0
  167. package/dist/server/threads/socketRouter.js.map +1 -1
  168. package/dist/server/threads/threadServer.js +12 -0
  169. package/dist/server/threads/threadServer.js.map +1 -1
  170. package/dist/sqlEngine/diff/differential.js +7 -3
  171. package/dist/sqlEngine/diff/differential.js.map +1 -1
  172. package/dist/sqlEngine/parser/normalizer.js +37 -9
  173. package/dist/sqlEngine/parser/normalizer.js.map +1 -1
  174. package/dist/sqlTranslator/index.js +6 -1
  175. package/dist/sqlTranslator/index.js.map +1 -1
  176. package/dist/upgrade/upgradePrompt.d.ts +2 -2
  177. package/dist/upgrade/upgradePrompt.js +22 -3
  178. package/dist/upgrade/upgradePrompt.js.map +1 -1
  179. package/dist/utility/environment/environmentManager.d.ts +11 -0
  180. package/dist/utility/environment/environmentManager.js +154 -0
  181. package/dist/utility/environment/environmentManager.js.map +1 -1
  182. package/dist/utility/errors/commonErrors.d.ts +1 -0
  183. package/dist/utility/errors/commonErrors.js +1 -0
  184. package/dist/utility/errors/commonErrors.js.map +1 -1
  185. package/dist/utility/hdbTerms.d.ts +2 -0
  186. package/dist/utility/hdbTerms.js +2 -0
  187. package/dist/utility/hdbTerms.js.map +1 -1
  188. package/dist/utility/install/installer.js +32 -0
  189. package/dist/utility/install/installer.js.map +1 -1
  190. package/dist/utility/lmdb/environmentUtility.js +2 -1
  191. package/dist/utility/lmdb/environmentUtility.js.map +1 -1
  192. package/dist/utility/logging/harper_logger.d.ts +7 -0
  193. package/dist/utility/logging/harper_logger.js +57 -24
  194. package/dist/utility/logging/harper_logger.js.map +1 -1
  195. package/npm-shrinkwrap.json +208 -198
  196. package/package.json +10 -8
  197. package/resources/DESIGN.md +1 -0
  198. package/resources/DatabaseTransaction.ts +313 -102
  199. package/resources/ResourceInterface.ts +8 -2
  200. package/resources/Resources.ts +22 -4
  201. package/resources/Table.ts +398 -259
  202. package/resources/analytics/write.ts +22 -20
  203. package/resources/auditStore.ts +32 -1
  204. package/resources/blob.ts +15 -8
  205. package/resources/databases.ts +123 -12
  206. package/resources/indexes/HierarchicalNavigableSmallWorld.ts +119 -45
  207. package/resources/jsResource.ts +5 -62
  208. package/resources/models/Models.ts +14 -1
  209. package/resources/models/backendRegistry.ts +10 -0
  210. package/resources/models/openaiStream.ts +131 -19
  211. package/resources/models/v1/chatCompletions.ts +128 -0
  212. package/resources/models/v1/embeddings.ts +70 -0
  213. package/resources/models/v1/errors.ts +141 -0
  214. package/resources/models/v1/index.ts +72 -0
  215. package/resources/models/v1/models.ts +53 -0
  216. package/resources/models/v1/translation.ts +362 -0
  217. package/resources/roles.ts +67 -7
  218. package/resources/search.ts +8 -3
  219. package/security/jsLoader.ts +84 -30
  220. package/security/role.ts +7 -0
  221. package/security/superUserGuard.ts +20 -0
  222. package/security/tokenAuthentication.ts +6 -3
  223. package/security/user.ts +26 -1
  224. package/server/DESIGN.md +45 -34
  225. package/server/http.ts +100 -13
  226. package/server/operationsServer.ts +0 -2
  227. package/server/serverHelpers/multipartParser.ts +9 -0
  228. package/server/serverHelpers/operationAuthorizationState.ts +11 -0
  229. package/server/serverHelpers/registeredOperations.ts +19 -15
  230. package/server/serverHelpers/serverHandlers.js +20 -3
  231. package/server/serverHelpers/serverUtilities.ts +10 -7
  232. package/server/static.ts +75 -20
  233. package/server/storageReclamation.ts +104 -8
  234. package/server/threads/manageThreads.js +8 -0
  235. package/server/threads/socketRouter.ts +20 -0
  236. package/server/threads/threadServer.js +11 -0
  237. package/sqlTranslator/index.ts +6 -1
  238. package/static/defaultConfig.yaml +2 -0
  239. package/studio/web/assets/{Chat-DoVWScmq.js → Chat-BMUjPklt.js} +5 -5
  240. package/studio/web/assets/{Chat-DoVWScmq.js.map → Chat-BMUjPklt.js.map} +1 -1
  241. package/studio/web/assets/{FloatingChat-UZ2NsUOZ.js → FloatingChat-Cbghuxe5.js} +5 -5
  242. package/studio/web/assets/{FloatingChat-UZ2NsUOZ.js.map → FloatingChat-Cbghuxe5.js.map} +1 -1
  243. package/studio/web/assets/{apiToken-BUI_04o7.js → apiToken-CkgTQAqc.js} +2 -2
  244. package/studio/web/assets/{apiToken-BUI_04o7.js.map → apiToken-CkgTQAqc.js.map} +1 -1
  245. package/studio/web/assets/{applications-D03NA7wW.js → applications-WvF4NOTN.js} +13 -13
  246. package/studio/web/assets/applications-WvF4NOTN.js.map +1 -0
  247. package/studio/web/assets/chevron-right-anDJCGlp.js +2 -0
  248. package/studio/web/assets/chevron-right-anDJCGlp.js.map +1 -0
  249. package/studio/web/assets/index-CDeqVx71.css +1 -0
  250. package/studio/web/assets/index-Sevt3MVx.js +819 -0
  251. package/studio/web/assets/index-Sevt3MVx.js.map +1 -0
  252. package/studio/web/assets/index.lazy-Dg5YYPGg.js +14 -0
  253. package/studio/web/assets/{index.lazy-Dx3MpyDC.js.map → index.lazy-Dg5YYPGg.js.map} +1 -1
  254. package/studio/web/assets/{jsonMode-Cjj7dtlr.js → jsonMode-CBSEleE7.js} +2 -2
  255. package/studio/web/assets/{jsonMode-Cjj7dtlr.js.map → jsonMode-CBSEleE7.js.map} +1 -1
  256. package/studio/web/assets/languageServices-gxedeeTo.js +2 -0
  257. package/studio/web/assets/languageServices-gxedeeTo.js.map +1 -0
  258. package/studio/web/assets/{notifications-0edoFTsb.js → notifications-Ap84YyQp.js} +2 -2
  259. package/studio/web/assets/{notifications-0edoFTsb.js.map → notifications-Ap84YyQp.js.map} +1 -1
  260. package/studio/web/assets/notifications-vehqyOLM.js +2 -0
  261. package/studio/web/assets/{notifications-CwKhipK7.js.map → notifications-vehqyOLM.js.map} +1 -1
  262. package/studio/web/assets/{pollUnlessForbidden-HHdQZW1N.js → pollUnlessForbidden-BhzefVRn.js} +2 -2
  263. package/studio/web/assets/pollUnlessForbidden-BhzefVRn.js.map +1 -0
  264. package/studio/web/assets/{profile-DUfEPQtx.js → profile-DzDpRLHY.js} +2 -2
  265. package/studio/web/assets/{profile-DUfEPQtx.js.map → profile-DzDpRLHY.js.map} +1 -1
  266. package/studio/web/assets/queryClient-CgtkoiCY.js +2 -0
  267. package/studio/web/assets/queryClient-CgtkoiCY.js.map +1 -0
  268. package/studio/web/assets/regions-DpJ8s7iN.js +2 -0
  269. package/studio/web/assets/regions-DpJ8s7iN.js.map +1 -0
  270. package/studio/web/assets/register-BDy01-q3.js +3 -0
  271. package/studio/web/assets/register-BDy01-q3.js.map +1 -0
  272. package/studio/web/assets/register-CQHRwNP4.js +3 -0
  273. package/studio/web/assets/register-CQHRwNP4.js.map +1 -0
  274. package/studio/web/assets/{setComponentFile-DMPo4UjC.js → setComponentFile-D0aPcI3W.js} +2 -2
  275. package/studio/web/assets/{setComponentFile-DMPo4UjC.js.map → setComponentFile-D0aPcI3W.js.map} +1 -1
  276. package/studio/web/assets/setup-I15FAF9H.js +3 -0
  277. package/studio/web/assets/{setup-B56Oz1_u.js.map → setup-I15FAF9H.js.map} +1 -1
  278. package/studio/web/assets/status-BF1eJshJ.js +62 -0
  279. package/studio/web/assets/status-BF1eJshJ.js.map +1 -0
  280. package/studio/web/assets/{swagger-ui-react-lQrBxfwM.js → swagger-ui-react-DwSPXXhl.js} +2 -2
  281. package/studio/web/assets/{swagger-ui-react-lQrBxfwM.js.map → swagger-ui-react-DwSPXXhl.js.map} +1 -1
  282. package/studio/web/assets/{textarea-DhLMZ3PA.js → table-DpOjNQHt.js} +2 -10
  283. package/studio/web/assets/table-DpOjNQHt.js.map +1 -0
  284. package/studio/web/assets/{tsMode-CrHCRjTK.js → tsMode-Bu51A-VG.js} +2 -2
  285. package/studio/web/assets/{tsMode-CrHCRjTK.js.map → tsMode-Bu51A-VG.js.map} +1 -1
  286. package/studio/web/assets/{useEntityRestURL-DoaBMEvU.js → useEntityRestURL-DrV1zMrN.js} +2 -2
  287. package/studio/web/assets/{useEntityRestURL-DoaBMEvU.js.map → useEntityRestURL-DrV1zMrN.js.map} +1 -1
  288. package/studio/web/index.html +6 -7
  289. package/upgrade/upgradePrompt.ts +22 -3
  290. package/utility/environment/environmentManager.ts +152 -0
  291. package/utility/errors/commonErrors.ts +2 -0
  292. package/utility/hdbTerms.ts +2 -0
  293. package/utility/install/installer.ts +37 -0
  294. package/utility/lmdb/environmentUtility.ts +2 -1
  295. package/utility/logging/harper_logger.ts +57 -26
  296. package/studio/web/assets/applications-D03NA7wW.js.map +0 -1
  297. package/studio/web/assets/index-Bh_CNAHr.js +0 -809
  298. package/studio/web/assets/index-Bh_CNAHr.js.map +0 -1
  299. package/studio/web/assets/index-C8Mv9kHV.css +0 -1
  300. package/studio/web/assets/index.lazy-Dx3MpyDC.js +0 -14
  301. package/studio/web/assets/notifications-CwKhipK7.js +0 -2
  302. package/studio/web/assets/pollUnlessForbidden-HHdQZW1N.js.map +0 -1
  303. package/studio/web/assets/queryClient-CbA8wM7J.js +0 -2
  304. package/studio/web/assets/queryClient-CbA8wM7J.js.map +0 -1
  305. package/studio/web/assets/setup-B56Oz1_u.js +0 -3
  306. package/studio/web/assets/status-BAod7p3o.js +0 -62
  307. package/studio/web/assets/status-BAod7p3o.js.map +0 -1
  308. package/studio/web/assets/textarea-DhLMZ3PA.js.map +0 -1
@@ -75,7 +75,7 @@ import { rebuildUpdateBefore } from './crdt.ts';
75
75
  import { appendHeader } from '../server/serverHelpers/Headers.ts';
76
76
  import fs from 'node:fs';
77
77
  import { Blob, deleteBlobsInObject, findBlobsInObject, startPreCommitBlobsForRecord } from './blob.ts';
78
- import { onStorageReclamation } from '../server/storageReclamation.ts';
78
+ import { onStorageReclamation, getStorageSpaceStats } from '../server/storageReclamation.ts';
79
79
  import { RequestTarget } from './RequestTarget.ts';
80
80
  import harperLogger from '../utility/logging/harper_logger.ts';
81
81
  import { throttle } from '../server/throttle.ts';
@@ -389,10 +389,20 @@ export function makeTable(options) {
389
389
  if (!attributes) attributes = [];
390
390
  if (!properties) properties = projectAttributesToProperties(attributes);
391
391
  const updateRecord = recordUpdater(primaryStore, tableId, auditStore);
392
+ let warnedNullSourcePut = false; // latched: one warn per table per worker (see _writeUpdate)
392
393
  let sourceLoad: any; // if a source has a load function (replicator), record it here
393
394
  let hasSourceGet: any;
394
395
  let primaryKeyAttribute: Attribute | undefined;
395
396
  let lastEvictionCompletion: Promise<void> = Promise.resolve();
397
+ // getFromSource() intentionally resolves its caller before the resolved record's cache
398
+ // write has committed (see there) so GET latency doesn't pay for the write. dropTable()
399
+ // must not drop this table's column families while one of those writes is still landing —
400
+ // racing a drop against an in-flight write is what corrupts the column family handle and
401
+ // produces "Invalid column family specified in write batch" (harper#1381). Track the
402
+ // in-flight commit promises here so dropTable() can drain them first, and stop admitting
403
+ // new ones (droppingTable) once a drop has actually started.
404
+ const pendingSourceCommits = new Set<Promise<any>>();
405
+ let droppingTable = false;
396
406
  let createdTimeProperty: Attribute | undefined,
397
407
  updatedTimeProperty: Attribute | undefined,
398
408
  expiresAtProperty: Attribute | undefined;
@@ -1375,12 +1385,54 @@ export function makeTable(options) {
1375
1385
  if (tombstoneWrite?.then) await tombstoneWrite;
1376
1386
  }
1377
1387
  }
1388
+ // A get() against a sourcedFrom table resolves to its caller before the resolved
1389
+ // record's cache write has committed (see getFromSource) - the write lands "in the
1390
+ // background" for latency reasons. Flip this BEFORE removing the table from the
1391
+ // schema below: getFromSource() checks it and skips caching (treats the load as
1392
+ // noCacheStore) for any call it admits from here on, including one that slipped in
1393
+ // through a stale reference to this Table between the two steps.
1394
+ droppingTable = true;
1378
1395
  // Remove the table from the in-memory schema immediately so concurrent
1379
1396
  // requests get "table does not exist" instead of racing the column
1380
1397
  // family drops below. If a drop fails past this point the table stays
1381
1398
  // invisible, and the tombstone guarantees the drop completes on the
1382
1399
  // next startup (or on a same-name create).
1383
1400
  delete databases[databaseName][tableName];
1401
+ // The above stops new source-fill writes from starting, but a write from a get()
1402
+ // that already returned to its caller may still be in flight. Dropping the column
1403
+ // families out from under that write is a genuine invariant violation, not just a
1404
+ // benign race: RocksDB rejects the still-open write batch with "Invalid column
1405
+ // family specified in write batch" (or "Could not access column family N"), which
1406
+ // can also abort this drop before it removes the tombstoned catalog rows - leaving
1407
+ // the table stuck "dropping" for completeInterruptedDrop to retry (and fail
1408
+ // identically) on every subsequent load (harper#1381). Drain any in-flight commits
1409
+ // before the blob sweep below (so it observes every row a drain-caught write just
1410
+ // committed) and before touching a single column family.
1411
+ //
1412
+ // Bounded, and fails CLOSED: the tracked promise covers the whole source round-trip
1413
+ // plus the local commit (see getFromSource), so a hung/slow source or a slow commit
1414
+ // (e.g. a large blob write) could otherwise wedge this drop forever. Rather than
1415
+ // give up and drop anyway - which would reopen exactly the race this drain exists to
1416
+ // close, just less often - a timeout FAILS the drop. The tombstone written above is
1417
+ // already durable, so completeInterruptedDrop picks the drop back up on the next
1418
+ // load, once the stuck write has had time to finish.
1419
+ if (pendingSourceCommits.size) {
1420
+ const pending = [...pendingSourceCommits];
1421
+ let timer: NodeJS.Timeout;
1422
+ const timedOut = Symbol('timedOut');
1423
+ const result = await Promise.race([
1424
+ Promise.allSettled(pending),
1425
+ new Promise<typeof timedOut>((resolve) => {
1426
+ timer = setTimeout(() => resolve(timedOut), LOCK_TIMEOUT);
1427
+ }),
1428
+ ]);
1429
+ clearTimeout(timer);
1430
+ if (result === timedOut) {
1431
+ throw new Error(
1432
+ `dropTable() timed out after ${LOCK_TIMEOUT}ms waiting for ${pending.length} in-flight source-populated cache write(s) on ${tableName} to settle; refusing to drop the column families out from under a write that may still be staged. The drop tombstone is durable, so this will be retried on the next load.`
1433
+ );
1434
+ }
1435
+ }
1384
1436
  for (const entry of primaryStore.getRange({ versions: true, snapshot: false, lazy: true })) {
1385
1437
  if (entry.metadataFlags & HAS_BLOBS && entry.value) {
1386
1438
  deleteBlobsInObject(entry.value);
@@ -2185,6 +2237,22 @@ export function makeTable(options) {
2185
2237
  const context = this.getContext();
2186
2238
  const transaction = txnForContext(context);
2187
2239
  checkValidId(id);
2240
+ if (fullUpdate && recordUpdate == null && options?.isNotification) {
2241
+ // A source/replication-applied put must carry the record; these applies skip record
2242
+ // validation, so this is the one path a nullish full update reaches (isNotification scopes
2243
+ // this to the apply dispatcher, not instance flows that fill from #changes). Applying it
2244
+ // stores nothing and mints an audit-only entry misrepresenting the write (#2153) — skip it;
2245
+ // a redelivery re-skips and a later real write supersedes.
2246
+ logger.trace?.('Skipped valueless source put', tableName, id, options?.nodeId);
2247
+ if (!warnedNullSourcePut) {
2248
+ warnedNullSourcePut = true;
2249
+ logger.warn?.(
2250
+ `Skipping a source-applied put with no record content for ${tableName} id ${id} from node ${options?.nodeId}`,
2251
+ new Error('valueless source put')
2252
+ );
2253
+ }
2254
+ return;
2255
+ }
2188
2256
  const entry = this.#entry ?? primaryStore.getEntry(id, { transaction: transaction.getReadTxn() });
2189
2257
  const writeToSource = () => {
2190
2258
  if (!(this.constructor as any).source || (context as any)?.source) return;
@@ -3295,6 +3363,11 @@ export function makeTable(options) {
3295
3363
  const operator = target.operator;
3296
3364
  if (conditions.length > 0 || operator) conditions = prepareConditions(conditions, operator);
3297
3365
  const sort = typeof target.sort === 'object' && target.sort;
3366
+ for (let order = sort; order; order = order.next) {
3367
+ if (typeof order.attribute !== 'string') continue;
3368
+ const customIndex = indices[order.attribute]?.customIndex;
3369
+ if (customIndex?.exactDistance) customIndex.exactDistance(order, null);
3370
+ }
3298
3371
  let postOrdering;
3299
3372
  if (sort) {
3300
3373
  // TODO: Support index-assisted sorts of unions, which will require potentially recursively adding/modifying an order aligned condition and be able to recursively undo it if necessary
@@ -3342,9 +3415,12 @@ export function makeTable(options) {
3342
3415
  if (sort.next) {
3343
3416
  postOrdering = {
3344
3417
  dbOrderedAttribute: sort.attribute,
3418
+ dbOrderedSort: sort,
3345
3419
  attribute: sort.next.attribute,
3346
3420
  descending: sort.next.descending,
3347
3421
  next: sort.next.next,
3422
+ target: (sort.next as any).target,
3423
+ distance: (sort.next as any).distance,
3348
3424
  };
3349
3425
  }
3350
3426
  } else {
@@ -3426,7 +3502,8 @@ export function makeTable(options) {
3426
3502
  ensure_loaded,
3427
3503
  true,
3428
3504
  boundRowFilter,
3429
- includeExpired
3505
+ includeExpired,
3506
+ postOrdering
3430
3507
  );
3431
3508
  let results = TableResource.transformToOrderedSelect(
3432
3509
  entries,
@@ -3487,10 +3564,9 @@ export function makeTable(options) {
3487
3564
  function createComparator(order: Sort) {
3488
3565
  const nextComparator = order.next && createComparator(order.next);
3489
3566
  const descending = order.descending;
3490
- (context as any).sort = order; // make sure this is set to the current sort order
3491
3567
  return (entryA, entryB) => {
3492
- const a = getAttributeValue(entryA, order.attribute, context);
3493
- const b = getAttributeValue(entryB, order.attribute, context);
3568
+ const a = getAttributeValue(entryA, order.attribute, context, order);
3569
+ const b = getAttributeValue(entryB, order.attribute, context, order);
3494
3570
  const diff = descending
3495
3571
  ? compareKeys(convertToComparableKeys(b), convertToComparableKeys(a))
3496
3572
  : compareKeys(convertToComparableKeys(a), convertToComparableKeys(b));
@@ -3531,7 +3607,12 @@ export function makeTable(options) {
3531
3607
  // if the index has already provided the first order of sorting, we only need to sort
3532
3608
  // within each grouping
3533
3609
  if (dbOrderedAttribute) {
3534
- const groupingValue = getAttributeValue(entry, dbOrderedAttribute, context);
3610
+ const groupingValue = getAttributeValue(
3611
+ entry,
3612
+ dbOrderedAttribute,
3613
+ context,
3614
+ (sort as any).dbOrderedSort
3615
+ );
3535
3616
  if (firstEntry) {
3536
3617
  firstEntry = false;
3537
3618
  lastGroupingValue = groupingValue;
@@ -3630,6 +3711,7 @@ export function makeTable(options) {
3630
3711
  * authorization verdict can't be made on bytes that differ from what the caller receives.
3631
3712
  * @param includeExpired when true, a row past its TTL but not yet swept is treated as a live
3632
3713
  * match rather than gone (used by the SQL engine's UPDATE/DELETE row-finder).
3714
+ * @param sort post-ordering owned by this selection
3633
3715
  * @returns
3634
3716
  */
3635
3717
  static transformEntryForSelect(
@@ -3640,7 +3722,8 @@ export function makeTable(options) {
3640
3722
  ensure_loaded?,
3641
3723
  canSkip?,
3642
3724
  rowFilter?,
3643
- includeExpired?
3725
+ includeExpired?,
3726
+ sort?
3644
3727
  ) {
3645
3728
  let checkLoaded;
3646
3729
  if (
@@ -3716,7 +3799,7 @@ export function makeTable(options) {
3716
3799
  swrResource.setRecord(record);
3717
3800
  const loadingFromSource = ensureLoadedFromSource(source, entry.key ?? entry, entry, context, swrResource);
3718
3801
  if (loadingFromSource?.then) {
3719
- return loadingFromSource.then(transform);
3802
+ return loadingFromSource.then(transform.bind(this));
3720
3803
  }
3721
3804
  }
3722
3805
  }
@@ -3746,7 +3829,7 @@ export function makeTable(options) {
3746
3829
  value = filterMap.fromRecord?.(record);
3747
3830
  }
3748
3831
  } else {
3749
- value = resolver(record, context, entry, true);
3832
+ value = resolver(record, context, entry, true, sort);
3750
3833
  }
3751
3834
  const handleResolvedValue = (value: any) => {
3752
3835
  if (resolver.directReturn) return callback(value, attribute_name);
@@ -3768,7 +3851,11 @@ export function makeTable(options) {
3768
3851
  context,
3769
3852
  targetReadTxn,
3770
3853
  filterMap,
3771
- ensure_loaded
3854
+ ensure_loaded,
3855
+ undefined,
3856
+ undefined,
3857
+ undefined,
3858
+ typeof attribute.sort === 'object' && attribute.sort
3772
3859
  ));
3773
3860
  if (Array.isArray(value)) {
3774
3861
  const results = [];
@@ -4707,13 +4794,13 @@ export function makeTable(options) {
4707
4794
  (stats.treeBranchPageCount + stats.treeLeafPageCount + stats.overflowPages) * stats.pageSize)
4708
4795
  );
4709
4796
  }
4710
- static getStorageStats() {
4711
- const stats = fs.statfsSync(primaryStore.path);
4712
- return {
4713
- available: stats.bavail * stats.bsize,
4714
- free: stats.bfree * stats.bsize,
4715
- size: stats.blocks * stats.bsize,
4716
- };
4797
+ /**
4798
+ * Get available/free/size storage stats for the table's underlying volume. Async because
4799
+ * this may need to read quota-status.json (#1976); getSize/getAuditSize stay sync because
4800
+ * they only read in-memory store stats.
4801
+ */
4802
+ static async getStorageStats() {
4803
+ return getStorageSpaceStats(primaryStore.path);
4717
4804
  }
4718
4805
  static async getRecordCount(options?: any) {
4719
4806
  // iterate through the metadata entries to exclude their count and exclude the deletion counts
@@ -4820,8 +4907,24 @@ export function makeTable(options) {
4820
4907
  $updatedTime: (object, context, entry) => entry.version,
4821
4908
  $expiresAt: (object, context, entry) => entry.expiresAt,
4822
4909
  $record: (object, context, entry) => (entry ? { value: object } : object),
4823
- $distance: (object, context, entry) => {
4824
- return entry && (entry.distance ?? context?.vectorDistances?.get(entry));
4910
+ $distance: (object, context, entry, returnEntry, sort) => {
4911
+ if (!entry) return;
4912
+ if (entry.distance !== undefined) return entry.distance;
4913
+ let distanceSort = sort;
4914
+ while (
4915
+ distanceSort &&
4916
+ (typeof distanceSort.attribute !== 'string' ||
4917
+ !Array.isArray(distanceSort.target) ||
4918
+ !indices[distanceSort.attribute]?.customIndex?.propertyResolver)
4919
+ )
4920
+ distanceSort = distanceSort.next;
4921
+ if (!distanceSort) return;
4922
+ const customIndex = indices[distanceSort.attribute].customIndex;
4923
+ const vector = object[distanceSort.attribute];
4924
+ const distanceCache = context?.vectorDistanceCaches?.get(distanceSort);
4925
+ const cachedDistance = distanceCache?.get(entry) ?? distanceCache?.get(vector);
4926
+ if (cachedDistance !== undefined) return cachedDistance;
4927
+ return customIndex.propertyResolver(vector, context, entry, distanceSort);
4825
4928
  },
4826
4929
  };
4827
4930
  for (const attribute of this.attributes) {
@@ -4969,9 +5072,14 @@ export function makeTable(options) {
4969
5072
  attribute.resolve.directReturn = true;
4970
5073
  } else if (indices[attribute.name]?.customIndex?.propertyResolver) {
4971
5074
  const customIndex = indices[attribute.name].customIndex;
4972
- propertyResolvers[attribute.name] = (object, context, entry) => {
5075
+ propertyResolvers[attribute.name] = (object, context, entry, returnEntry, sort, comparing) => {
4973
5076
  const value = object[attribute.name];
4974
- return customIndex.propertyResolver(value, context, entry);
5077
+ const sortAttribute = sort?.attribute;
5078
+ const resolvesSort =
5079
+ comparing === true &&
5080
+ (sortAttribute === attribute.name ||
5081
+ (Array.isArray(sortAttribute) && sortAttribute[sortAttribute.length - 1] === attribute.name));
5082
+ return customIndex.propertyResolver(value, context, entry, resolvesSort ? sort : undefined);
4975
5083
  };
4976
5084
  propertyResolvers[attribute.name].directReturn = true;
4977
5085
  }
@@ -5506,14 +5614,16 @@ export function makeTable(options) {
5506
5614
  // Inherit never-drop-on-conflict so a source-applied multi-store transaction doesn't
5507
5615
  // drop the canonical write when a secondary store hits a transient conflict.
5508
5616
  transaction.next.sourceApply = transaction.sourceApply;
5617
+ transaction.next.timeoutBudget = transaction.timeoutBudget;
5509
5618
  // Inherit the replay marker so a multi-table replay transaction skips validation on
5510
5619
  // every store, not just the first (harper#1316).
5511
5620
  transaction.next.isReplay = transaction.isReplay;
5512
5621
  // Inherit which resource/method started this logical operation, so a chained (second
5513
5622
  // table) transaction's long-transaction-abort log (DatabaseTransaction.ts) can still
5514
- // identify the request that caused it instead of leaving that field blank. (The
5515
- // checkOverloaded() stuck-commit log reads this too, but a chained link's own commit
5516
- // rarely arms outstandingCommit see the note at DatabaseTransaction.ts's arming site.)
5623
+ // identify the request that caused it instead of leaving that field blank. The
5624
+ // checkOverloaded() stuck-commit log reads this too every chained link's own native
5625
+ // commit is tracked with its own identity (DatabaseTransaction.ts's trackOutstandingCommit),
5626
+ // so a wedged second-store commit is named just as precisely as a wedged first one.
5517
5627
  transaction.next.startedFrom = transaction.startedFrom;
5518
5628
  if (transaction.open === TRANSACTION_STATE.CLOSED) {
5519
5629
  // if the current transaction is already closed, we need to retain that state on new databases we work with
@@ -5536,7 +5646,7 @@ export function makeTable(options) {
5536
5646
  return transaction;
5537
5647
  }
5538
5648
  }
5539
- function getAttributeValue(entry, attribute_name, context) {
5649
+ function getAttributeValue(entry, attribute_name, context, sort?) {
5540
5650
  if (!entry) {
5541
5651
  return;
5542
5652
  }
@@ -5548,14 +5658,17 @@ export function makeTable(options) {
5548
5658
  for (let i = 0, l = attribute_name.length; i < l; i++) {
5549
5659
  const attribute = attribute_name[i];
5550
5660
  const resolver = resolvers?.[attribute];
5551
- value = resolver && value ? resolver(value, context, entry) : value?.[attribute];
5661
+ value =
5662
+ resolver && value
5663
+ ? resolver(value, context, entry, false, i === l - 1 ? sort : undefined, true)
5664
+ : value?.[attribute];
5552
5665
  entry = null; // can't use this in the nested object
5553
5666
  resolvers = resolver?.definition?.tableClass?.propertyResolvers;
5554
5667
  }
5555
5668
  return value;
5556
5669
  }
5557
5670
  const resolver = propertyResolvers[attribute_name];
5558
- return resolver ? resolver(record, context, entry) : record[attribute_name];
5671
+ return resolver ? resolver(record, context, entry, false, sort, true) : record[attribute_name];
5559
5672
  }
5560
5673
  function transformToEntries(ids, select, context, readTxn, filters?) {
5561
5674
  // TODO: Test and ensure that we break out of these loops when a connection is lost
@@ -5700,7 +5813,10 @@ export function makeTable(options) {
5700
5813
  replacingRecord: existingRecord,
5701
5814
  replacingEntry: existingEntry,
5702
5815
  replacingVersion: existingVersion,
5703
- noCacheStore: false,
5816
+ // Once dropTable() has started, no new source-fill write may begin (dropTable()
5817
+ // only drains writes already in flight - see there); still resolve the caller's
5818
+ // read with fresh source data, just don't cache it into a table that's going away.
5819
+ noCacheStore: droppingTable,
5704
5820
  source: null,
5705
5821
  transaction: undefined,
5706
5822
  expiresAt: undefined,
@@ -5711,264 +5827,287 @@ export function makeTable(options) {
5711
5827
  // we don't want to wait for the transaction because we want to return as fast as possible
5712
5828
  // and let the transaction commit in the background
5713
5829
  let resolved;
5714
- when(
5715
- transaction(sourceContext, async (_txn) => {
5716
- const start = performance.now();
5717
- let updatedRecord;
5718
- let hasChanges, invalidated;
5719
- try {
5720
- updatedRecord = await throttledCallToSource(source, id, sourceContext, existingEntry);
5721
- invalidated = metadataFlags & INVALIDATED;
5722
- let version = sourceContext.lastModified || (invalidated && existingVersion);
5723
- hasChanges = invalidated || version > existingVersion || !existingRecord;
5724
- const resolveDuration = performance.now() - start;
5725
- recordAction(resolveDuration, 'cache-resolution', tableName, null, 'success');
5726
- if (responseHeaders)
5727
- appendHeader(responseHeaders, 'Server-Timing', `cache-resolve;dur=${resolveDuration.toFixed(2)}`, true);
5728
- if (expirationMs && sourceContext.expiresAt == undefined)
5729
- sourceContext.expiresAt = Date.now() + expirationMs;
5730
- if (updatedRecord) {
5731
- if (typeof updatedRecord !== 'object') throw new Error('Only objects can be cached and stored in tables');
5732
- if (updatedRecord.status > 0 && updatedRecord.headers) {
5733
- // if the source has a status code and headers, treat it as a response
5734
- const status = updatedRecord.status;
5735
- if (status === 304) {
5736
- // revalidation of our current cached record
5737
- updatedRecord = existingRecord;
5738
- version = existingVersion;
5739
- } else if (!CACHEABLE_STATUS_CODES.has(status)) {
5740
- // non-cacheable status - propagate to client without caching
5741
- throw new ServerError(updatedRecord.body || 'Error from source', status);
5742
- } else {
5743
- let headers: any;
5744
- const sourceHeaders = updatedRecord.headers;
5745
- if (sourceHeaders[Symbol.iterator]) {
5746
- headers = {};
5747
- for (let [name, value] of sourceHeaders) {
5748
- headers[name.toLowerCase()] = value;
5749
- }
5750
- } else {
5751
- headers = sourceHeaders; // just a plain object
5752
- }
5753
- const contentType = sourceHeaders.get?.('Content-Type');
5754
- let data: any;
5755
- if (contentType === 'application/json' && updatedRecord.json) {
5756
- // use native .json() if possible
5757
- data = await updatedRecord.json();
5758
- } else {
5759
- const contentTypeHandler = contentType && contentTypes.get(contentType);
5760
- if (contentTypeHandler?.deserialize) {
5761
- data = contentTypeHandler.deserialize(
5762
- await (contentType.startsWith('text/') ? updatedRecord.text() : updatedRecord.bytes())
5763
- );
5764
- }
5830
+ // Tracked in pendingSourceCommits (below) for the full lifetime of this transaction -
5831
+ // including the source round-trip, not just from the point it's actually staged -
5832
+ // so dropTable() can wait for it before dropping the table's column families (see the
5833
+ // comment there). Registering only once a write reaches staging would track less (no
5834
+ // Set churn for a plain cache miss, and a slow/hung source couldn't delay a drop) and
5835
+ // is safe on its own given the live droppingTable re-checks in this function - but it
5836
+ // would make dropTable()'s correctness depend on every future early-return path in
5837
+ // this function remembering to check droppingTable, rather than on dropTable() simply
5838
+ // waiting for whatever this function is doing. Tracking the whole lifetime is the
5839
+ // belt to that suspenders, at the cost of a bounded wait on a merely slow source
5840
+ // before the drain's fail-closed timeout below.
5841
+ const commitPromise = transaction(sourceContext, async (_txn) => {
5842
+ const start = performance.now();
5843
+ let updatedRecord;
5844
+ let hasChanges, invalidated;
5845
+ try {
5846
+ updatedRecord = await throttledCallToSource(source, id, sourceContext, existingEntry);
5847
+ invalidated = metadataFlags & INVALIDATED;
5848
+ let version = sourceContext.lastModified || (invalidated && existingVersion);
5849
+ hasChanges = invalidated || version > existingVersion || !existingRecord;
5850
+ const resolveDuration = performance.now() - start;
5851
+ recordAction(resolveDuration, 'cache-resolution', tableName, null, 'success');
5852
+ if (responseHeaders)
5853
+ appendHeader(responseHeaders, 'Server-Timing', `cache-resolve;dur=${resolveDuration.toFixed(2)}`, true);
5854
+ if (expirationMs && sourceContext.expiresAt == undefined) sourceContext.expiresAt = Date.now() + expirationMs;
5855
+ if (updatedRecord) {
5856
+ if (typeof updatedRecord !== 'object') throw new Error('Only objects can be cached and stored in tables');
5857
+ if (updatedRecord.status > 0 && updatedRecord.headers) {
5858
+ // if the source has a status code and headers, treat it as a response
5859
+ const status = updatedRecord.status;
5860
+ if (status === 304) {
5861
+ // revalidation of our current cached record
5862
+ updatedRecord = existingRecord;
5863
+ version = existingVersion;
5864
+ } else if (!CACHEABLE_STATUS_CODES.has(status)) {
5865
+ // non-cacheable status - propagate to client without caching
5866
+ throw new ServerError(updatedRecord.body || 'Error from source', status);
5867
+ } else {
5868
+ let headers: any;
5869
+ const sourceHeaders = updatedRecord.headers;
5870
+ if (sourceHeaders[Symbol.iterator]) {
5871
+ headers = {};
5872
+ for (let [name, value] of sourceHeaders) {
5873
+ headers[name.toLowerCase()] = value;
5765
5874
  }
5766
- if (data !== undefined) {
5767
- // we have structured data that we have parsed
5768
- delete headers['content-type']; // don't store the content type if we have already parsed it
5769
- updatedRecord = { headers, data };
5770
- } else {
5771
- updatedRecord = { headers, body: createBlob(updatedRecord.body) };
5875
+ } else {
5876
+ headers = sourceHeaders; // just a plain object
5877
+ }
5878
+ const contentType = sourceHeaders.get?.('Content-Type');
5879
+ let data: any;
5880
+ if (contentType === 'application/json' && updatedRecord.json) {
5881
+ // use native .json() if possible
5882
+ data = await updatedRecord.json();
5883
+ } else {
5884
+ const contentTypeHandler = contentType && contentTypes.get(contentType);
5885
+ if (contentTypeHandler?.deserialize) {
5886
+ data = contentTypeHandler.deserialize(
5887
+ await (contentType.startsWith('text/') ? updatedRecord.text() : updatedRecord.bytes())
5888
+ );
5772
5889
  }
5773
- if (status !== 200) updatedRecord.status = status;
5774
5890
  }
5891
+ if (data !== undefined) {
5892
+ // we have structured data that we have parsed
5893
+ delete headers['content-type']; // don't store the content type if we have already parsed it
5894
+ updatedRecord = { headers, data };
5895
+ } else {
5896
+ updatedRecord = { headers, body: createBlob(updatedRecord.body) };
5897
+ }
5898
+ if (status !== 200) updatedRecord.status = status;
5775
5899
  }
5776
- if (typeof updatedRecord.toJSON === 'function') updatedRecord = updatedRecord.toJSON();
5777
- // updatedRecord may still be a frozen record (e.g. a reused existingRecord); copy-on-mutate
5778
- // before stamping the primary key and created/updated times below (records are immutable —
5779
- // 5.2 record caching relies on it — so we must not write through the frozen object).
5780
- if (isFrozenRecordObject(updatedRecord)) updatedRecord = { ...updatedRecord };
5781
- if (primaryKey && updatedRecord[primaryKey] !== id) updatedRecord[primaryKey] = id;
5782
5900
  }
5783
- resolved = true;
5784
- const resolvedEntry: Entry = {
5785
- key: id,
5786
- version,
5787
- value: updatedRecord,
5788
- expiresAt: sourceContext.expiresAt,
5789
- metadataFlags: 0,
5790
- size: 0,
5791
- localTime: 0,
5792
- nodeId: 0,
5793
- residencyId: 0,
5794
- } as any;
5795
- // Give the plain object the RecordObject prototype so getExpiresAt/getUpdatedTime
5796
- // are available on the immediately-resolved entry. We mutate the prototype
5797
- // in-place rather than copying so that the commit callback (which adds
5798
- // createdAt/updatedAt to updatedRecord) is still reflected in the entry value.
5799
- if (updatedRecord && updatedRecord.constructor === Object) {
5800
- Object.setPrototypeOf(updatedRecord, primaryStore.encoder.structPrototype);
5801
- entryMap.set(updatedRecord, resolvedEntry);
5802
- }
5803
- resolve(resolvedEntry);
5804
- } catch (error) {
5805
- error.message += ` while resolving record ${id} for ${tableName}`;
5806
- if (
5807
- existingRecord &&
5808
- (((error.code === 'ECONNRESET' || error.code === 'ECONNREFUSED' || error.code === 'EAI_AGAIN') &&
5809
- !context?.mustRevalidate) ||
5810
- (context?.staleIfError &&
5811
- (error.statusCode === 500 ||
5812
- error.statusCode === 502 ||
5813
- error.statusCode === 503 ||
5814
- error.statusCode === 504)))
5815
- ) {
5816
- // these are conditions under which we can use stale data after an error
5817
- resolve({
5818
- key: id,
5819
- version: existingVersion,
5820
- value: existingRecord,
5821
- } as any);
5822
- logger.trace?.(error.message, '(returned stale record)');
5823
- } else reject(error);
5824
- const resolveDuration = performance.now() - start;
5825
- recordAction(resolveDuration, 'cache-resolution', tableName, null, 'fail');
5826
- if (responseHeaders)
5827
- appendHeader(responseHeaders, 'Server-Timing', `cache-resolve;dur=${resolveDuration.toFixed(2)}`, true);
5828
- sourceContext.transaction.abort();
5829
- return;
5830
- }
5831
- if (context?.noCacheStore || sourceContext.noCacheStore) {
5832
- // abort before we write any change
5833
- sourceContext.transaction.abort();
5834
- return;
5901
+ if (typeof updatedRecord.toJSON === 'function') updatedRecord = updatedRecord.toJSON();
5902
+ // updatedRecord may still be a frozen record (e.g. a reused existingRecord); copy-on-mutate
5903
+ // before stamping the primary key and created/updated times below (records are immutable —
5904
+ // 5.2 record caching relies on it — so we must not write through the frozen object).
5905
+ if (isFrozenRecordObject(updatedRecord)) updatedRecord = { ...updatedRecord };
5906
+ if (primaryKey && updatedRecord[primaryKey] !== id) updatedRecord[primaryKey] = id;
5835
5907
  }
5836
- const dbTxn = txnForContext(sourceContext);
5837
- const sourceWrite: any = {
5908
+ resolved = true;
5909
+ const resolvedEntry: Entry = {
5838
5910
  key: id,
5839
- store: primaryStore,
5840
- entry: existingEntry,
5841
- nodeName: 'source',
5842
- commit: (txnTime, existingEntry, _retry, transaction: any) => {
5843
- sourceWrite.skipped = false; // reset on each retry; cleanup happens after commit if still true
5844
- if (existingEntry?.version !== existingVersion) {
5845
- // don't do anything if the version has changed
5846
- sourceWrite.skipped = true;
5847
- return;
5911
+ version,
5912
+ value: updatedRecord,
5913
+ expiresAt: sourceContext.expiresAt,
5914
+ metadataFlags: 0,
5915
+ size: 0,
5916
+ localTime: 0,
5917
+ nodeId: 0,
5918
+ residencyId: 0,
5919
+ } as any;
5920
+ // Give the plain object the RecordObject prototype so getExpiresAt/getUpdatedTime
5921
+ // are available on the immediately-resolved entry. We mutate the prototype
5922
+ // in-place rather than copying so that the commit callback (which adds
5923
+ // createdAt/updatedAt to updatedRecord) is still reflected in the entry value.
5924
+ if (updatedRecord && updatedRecord.constructor === Object) {
5925
+ Object.setPrototypeOf(updatedRecord, primaryStore.encoder.structPrototype);
5926
+ entryMap.set(updatedRecord, resolvedEntry);
5927
+ }
5928
+ resolve(resolvedEntry);
5929
+ } catch (error) {
5930
+ error.message += ` while resolving record ${id} for ${tableName}`;
5931
+ if (
5932
+ existingRecord &&
5933
+ (((error.code === 'ECONNRESET' || error.code === 'ECONNREFUSED' || error.code === 'EAI_AGAIN') &&
5934
+ !context?.mustRevalidate) ||
5935
+ (context?.staleIfError &&
5936
+ (error.statusCode === 500 ||
5937
+ error.statusCode === 502 ||
5938
+ error.statusCode === 503 ||
5939
+ error.statusCode === 504)))
5940
+ ) {
5941
+ // these are conditions under which we can use stale data after an error
5942
+ resolve({
5943
+ key: id,
5944
+ version: existingVersion,
5945
+ value: existingRecord,
5946
+ } as any);
5947
+ logger.trace?.(error.message, '(returned stale record)');
5948
+ } else reject(error);
5949
+ const resolveDuration = performance.now() - start;
5950
+ recordAction(resolveDuration, 'cache-resolution', tableName, null, 'fail');
5951
+ if (responseHeaders)
5952
+ appendHeader(responseHeaders, 'Server-Timing', `cache-resolve;dur=${resolveDuration.toFixed(2)}`, true);
5953
+ sourceContext.transaction.abort();
5954
+ return;
5955
+ }
5956
+ if (context?.noCacheStore || sourceContext.noCacheStore || droppingTable) {
5957
+ // abort before we write any change. droppingTable is re-checked live (not just
5958
+ // the noCacheStore snapshot taken at call start) because a call admitted before
5959
+ // dropTable() began can still be sitting here after it started - the await above
5960
+ // waited on the source, which may take arbitrarily long.
5961
+ sourceContext.transaction.abort();
5962
+ return;
5963
+ }
5964
+ const dbTxn = txnForContext(sourceContext);
5965
+ const sourceWrite: any = {
5966
+ key: id,
5967
+ store: primaryStore,
5968
+ entry: existingEntry,
5969
+ nodeName: 'source',
5970
+ commit: (txnTime, existingEntry, _retry, transaction: any) => {
5971
+ sourceWrite.skipped = false; // reset on each retry; cleanup happens after commit if still true
5972
+ if (existingEntry?.version !== existingVersion) {
5973
+ // don't do anything if the version has changed
5974
+ sourceWrite.skipped = true;
5975
+ return;
5976
+ }
5977
+ updateIndices(id, existingRecord, updatedRecord, transaction && { transaction });
5978
+ if (updatedRecord) {
5979
+ if (existingEntry) {
5980
+ context.previousResidency = TableResource.getResidencyRecord(existingEntry.residencyId);
5848
5981
  }
5849
- updateIndices(id, existingRecord, updatedRecord, transaction && { transaction });
5850
- if (updatedRecord) {
5851
- if (existingEntry) {
5852
- context.previousResidency = TableResource.getResidencyRecord(existingEntry.residencyId);
5853
- }
5854
- let auditRecord: any;
5855
- let omitLocalRecord = false;
5856
- let residencyId: number;
5857
- if (updatedTimeProperty) {
5858
- updatedRecord[updatedTimeProperty.name] =
5859
- updatedTimeProperty.type === 'Date'
5982
+ let auditRecord: any;
5983
+ let omitLocalRecord = false;
5984
+ let residencyId: number;
5985
+ if (updatedTimeProperty) {
5986
+ updatedRecord[updatedTimeProperty.name] =
5987
+ updatedTimeProperty.type === 'Date'
5988
+ ? new Date(txnTime)
5989
+ : updatedTimeProperty.type === 'String'
5990
+ ? new Date(txnTime).toISOString()
5991
+ : txnTime;
5992
+ }
5993
+ if (createdTimeProperty && updatedRecord[createdTimeProperty.name] == null) {
5994
+ const existingCreatedTime = existingEntry?.value?.[createdTimeProperty.name];
5995
+ if (existingCreatedTime != null) {
5996
+ updatedRecord[createdTimeProperty.name] = existingCreatedTime;
5997
+ } else {
5998
+ updatedRecord[createdTimeProperty.name] =
5999
+ createdTimeProperty.type === 'Date'
5860
6000
  ? new Date(txnTime)
5861
- : updatedTimeProperty.type === 'String'
6001
+ : createdTimeProperty.type === 'String'
5862
6002
  ? new Date(txnTime).toISOString()
5863
6003
  : txnTime;
5864
6004
  }
5865
- if (createdTimeProperty && updatedRecord[createdTimeProperty.name] == null) {
5866
- const existingCreatedTime = existingEntry?.value?.[createdTimeProperty.name];
5867
- if (existingCreatedTime != null) {
5868
- updatedRecord[createdTimeProperty.name] = existingCreatedTime;
6005
+ }
6006
+ const residency = residencyFromFunction(TableResource.getResidency(updatedRecord, context));
6007
+ if (residency) {
6008
+ if (!residency.includes(server.hostname)) {
6009
+ // if we aren't in the residency list, specify that our local record should be omitted or be partial
6010
+ auditRecord = updatedRecord;
6011
+ omitLocalRecord = true;
6012
+ if (TableResource.getResidencyById) {
6013
+ // complete omission of the record that doesn't belong here
6014
+ updatedRecord = undefined;
5869
6015
  } else {
5870
- updatedRecord[createdTimeProperty.name] =
5871
- createdTimeProperty.type === 'Date'
5872
- ? new Date(txnTime)
5873
- : createdTimeProperty.type === 'String'
5874
- ? new Date(txnTime).toISOString()
5875
- : txnTime;
5876
- }
5877
- }
5878
- const residency = residencyFromFunction(TableResource.getResidency(updatedRecord, context));
5879
- if (residency) {
5880
- if (!residency.includes(server.hostname)) {
5881
- // if we aren't in the residency list, specify that our local record should be omitted or be partial
5882
- auditRecord = updatedRecord;
5883
- omitLocalRecord = true;
5884
- if (TableResource.getResidencyById) {
5885
- // complete omission of the record that doesn't belong here
5886
- updatedRecord = undefined;
5887
- } else {
5888
- // store the partial record
5889
- updatedRecord = null;
5890
- for (const name in indices) {
5891
- if (!updatedRecord) {
5892
- updatedRecord = {};
5893
- }
5894
- // if there are any indices, we need to preserve a partial invalidated record to ensure we can still do searches
5895
- updatedRecord[name] = auditRecord[name];
5896
- }
5897
- if (createdTimeProperty && auditRecord[createdTimeProperty.name] != null) {
5898
- // preserve the created timestamp in the partial record so it isn't lost when we don't have residency
5899
- if (!updatedRecord) updatedRecord = {};
5900
- updatedRecord[createdTimeProperty.name] = auditRecord[createdTimeProperty.name];
6016
+ // store the partial record
6017
+ updatedRecord = null;
6018
+ for (const name in indices) {
6019
+ if (!updatedRecord) {
6020
+ updatedRecord = {};
5901
6021
  }
6022
+ // if there are any indices, we need to preserve a partial invalidated record to ensure we can still do searches
6023
+ updatedRecord[name] = auditRecord[name];
6024
+ }
6025
+ if (createdTimeProperty && auditRecord[createdTimeProperty.name] != null) {
6026
+ // preserve the created timestamp in the partial record so it isn't lost when we don't have residency
6027
+ if (!updatedRecord) updatedRecord = {};
6028
+ updatedRecord[createdTimeProperty.name] = auditRecord[createdTimeProperty.name];
5902
6029
  }
5903
6030
  }
5904
- residencyId = getResidencyId(residency);
5905
6031
  }
5906
- logger.trace?.(
5907
- `Writing resolved record from source with id: ${id}, timestamp: ${new Date(txnTime).toISOString()}`
5908
- );
5909
- // TODO: We are doing a double check for ifVersion that should probably be cleaned out
6032
+ residencyId = getResidencyId(residency);
6033
+ }
6034
+ logger.trace?.(
6035
+ `Writing resolved record from source with id: ${id}, timestamp: ${new Date(txnTime).toISOString()}`
6036
+ );
6037
+ // TODO: We are doing a double check for ifVersion that should probably be cleaned out
6038
+ updateRecord(
6039
+ id,
6040
+ updatedRecord,
6041
+ existingEntry,
6042
+ txnTime,
6043
+ omitLocalRecord ? INVALIDATED : 0,
6044
+ (audit && (hasChanges || omitLocalRecord)) || null,
6045
+ {
6046
+ user: (sourceContext as any)?.user,
6047
+ expiresAt: sourceContext.expiresAt,
6048
+ residencyId,
6049
+ transaction,
6050
+ tableToTrack: tableName,
6051
+ },
6052
+ 'put',
6053
+ Boolean(invalidated),
6054
+ auditRecord
6055
+ );
6056
+ // arm the eviction scanner, mirroring the .put() path
6057
+ if (sourceContext.expiresAt) scheduleCleanup();
6058
+ } else if (existingEntry) {
6059
+ logger.trace?.(
6060
+ `Deleting resolved record from source with id: ${id}, timestamp: ${new Date(txnTime).toISOString()}`
6061
+ );
6062
+ if (audit || trackDeletes) {
5910
6063
  updateRecord(
5911
6064
  id,
5912
- updatedRecord,
6065
+ null,
5913
6066
  existingEntry,
5914
6067
  txnTime,
5915
- omitLocalRecord ? INVALIDATED : 0,
5916
- (audit && (hasChanges || omitLocalRecord)) || null,
5917
- {
5918
- user: (sourceContext as any)?.user,
5919
- expiresAt: sourceContext.expiresAt,
5920
- residencyId,
5921
- transaction,
5922
- tableToTrack: tableName,
5923
- },
5924
- 'put',
5925
- Boolean(invalidated),
5926
- auditRecord
5927
- );
5928
- // arm the eviction scanner, mirroring the .put() path
5929
- if (sourceContext.expiresAt) scheduleCleanup();
5930
- } else if (existingEntry) {
5931
- logger.trace?.(
5932
- `Deleting resolved record from source with id: ${id}, timestamp: ${new Date(txnTime).toISOString()}`
6068
+ 0,
6069
+ (audit && hasChanges) || null,
6070
+ { user: (sourceContext as any)?.user, transaction, tableToTrack: tableName },
6071
+ 'delete',
6072
+ Boolean(invalidated)
5933
6073
  );
5934
- if (audit || trackDeletes) {
5935
- updateRecord(
5936
- id,
5937
- null,
5938
- existingEntry,
5939
- txnTime,
5940
- 0,
5941
- (audit && hasChanges) || null,
5942
- { user: (sourceContext as any)?.user, transaction, tableToTrack: tableName },
5943
- 'delete',
5944
- Boolean(invalidated)
5945
- );
5946
- } else {
5947
- removeEntry(primaryStore, existingEntry, existingVersion);
5948
- }
6074
+ } else {
6075
+ removeEntry(primaryStore, existingEntry, existingVersion);
5949
6076
  }
5950
- },
5951
- };
5952
- // The cache-from-source write bypasses `_writeUpdate`, so wire the embed hook here
5953
- // too (always the originating node). It runs after the client GET has resolved with
5954
- // fresh source data, so it's a background commit: an embedder failure aborts the cache
5955
- // write via the outer error handler (row re-embeds next read) and never reaches the
5956
- // caller. Source-resolution errors are handled earlier, with the stale-data fallback.
5957
- const embedBefore = buildEmbedBefore(
5958
- updatedRecord,
5959
- sourceContext,
5960
- undefined,
5961
- TableResource.embedAttributes,
5962
- TableResource.userEmbedders
5963
- );
5964
- if (embedBefore) await embedBefore();
5965
- sourceWrite.before = preCommitBlobsForRecordBefore(sourceWrite, updatedRecord);
5966
- dbTxn.addWrite(sourceWrite);
5967
- }),
6077
+ }
6078
+ },
6079
+ };
6080
+ // The cache-from-source write bypasses `_writeUpdate`, so wire the embed hook here
6081
+ // too (always the originating node). It runs after the client GET has resolved with
6082
+ // fresh source data, so it's a background commit: an embedder failure aborts the cache
6083
+ // write via the outer error handler (row re-embeds next read) and never reaches the
6084
+ // caller. Source-resolution errors are handled earlier, with the stale-data fallback.
6085
+ const embedBefore = buildEmbedBefore(
6086
+ updatedRecord,
6087
+ sourceContext,
6088
+ undefined,
6089
+ TableResource.embedAttributes,
6090
+ TableResource.userEmbedders
6091
+ );
6092
+ if (embedBefore) await embedBefore();
6093
+ if (droppingTable) {
6094
+ // Re-check right before staging the write: dropTable() may have started
6095
+ // while we were awaiting the embed step above (harper#1381).
6096
+ sourceContext.transaction.abort();
6097
+ return;
6098
+ }
6099
+ sourceWrite.before = preCommitBlobsForRecordBefore(sourceWrite, updatedRecord);
6100
+ dbTxn.addWrite(sourceWrite);
6101
+ });
6102
+ pendingSourceCommits.add(commitPromise);
6103
+ when(
6104
+ commitPromise,
5968
6105
  () => {
6106
+ pendingSourceCommits.delete(commitPromise);
5969
6107
  primaryStore.unlock(id);
5970
6108
  },
5971
6109
  (error) => {
6110
+ pendingSourceCommits.delete(commitPromise);
5972
6111
  primaryStore.unlock(id);
5973
6112
  if (resolved) logger.error?.('Error committing cache update', error);
5974
6113
  // else the error was already propagated as part of the promise that we returned