@harperfast/harper 5.2.0 → 5.2.1

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 (241) hide show
  1. package/bin/copyDb.ts +21 -4
  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/mcp/tools/operations.ts +6 -0
  12. package/components/operations.js +4 -6
  13. package/config/configUtils.ts +12 -9
  14. package/config-root.schema.json +10 -0
  15. package/dataLayer/harperBridge/ResourceBridge.ts +26 -5
  16. package/dataLayer/hdbInfoController.ts +8 -0
  17. package/dataLayer/schemaDescribe.ts +2 -1
  18. package/dist/bin/copyDb.js +13 -2
  19. package/dist/bin/copyDb.js.map +1 -1
  20. package/dist/bin/harper.d.ts +6 -0
  21. package/dist/bin/harper.js +18 -50
  22. package/dist/bin/harper.js.map +1 -1
  23. package/dist/bin/help.d.ts +8 -0
  24. package/dist/bin/help.js +192 -0
  25. package/dist/bin/help.js.map +1 -0
  26. package/dist/components/Application.d.ts +16 -1
  27. package/dist/components/Application.js +210 -38
  28. package/dist/components/Application.js.map +1 -1
  29. package/dist/components/ApplicationScope.d.ts +7 -0
  30. package/dist/components/ApplicationScope.js +22 -0
  31. package/dist/components/ApplicationScope.js.map +1 -1
  32. package/dist/components/EntryHandler.d.ts +4 -4
  33. package/dist/components/EntryHandler.js +386 -95
  34. package/dist/components/EntryHandler.js.map +1 -1
  35. package/dist/components/RuntimeModuleTracker.d.ts +11 -0
  36. package/dist/components/RuntimeModuleTracker.js +189 -0
  37. package/dist/components/RuntimeModuleTracker.js.map +1 -0
  38. package/dist/components/Scope.d.ts +1 -0
  39. package/dist/components/Scope.js +69 -22
  40. package/dist/components/Scope.js.map +1 -1
  41. package/dist/components/componentLoader.js +69 -10
  42. package/dist/components/componentLoader.js.map +1 -1
  43. package/dist/components/deployLifecycle.d.ts +6 -2
  44. package/dist/components/deployLifecycle.js +109 -31
  45. package/dist/components/deployLifecycle.js.map +1 -1
  46. package/dist/components/mcp/tools/operations.js +6 -0
  47. package/dist/components/mcp/tools/operations.js.map +1 -1
  48. package/dist/components/operations.js +4 -6
  49. package/dist/components/operations.js.map +1 -1
  50. package/dist/config/configUtils.js +13 -9
  51. package/dist/config/configUtils.js.map +1 -1
  52. package/dist/dataLayer/harperBridge/ResourceBridge.js +12 -5
  53. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  54. package/dist/dataLayer/hdbInfoController.js +4 -0
  55. package/dist/dataLayer/hdbInfoController.js.map +1 -1
  56. package/dist/dataLayer/schemaDescribe.js +2 -1
  57. package/dist/dataLayer/schemaDescribe.js.map +1 -1
  58. package/dist/resources/DatabaseTransaction.d.ts +55 -0
  59. package/dist/resources/DatabaseTransaction.js +282 -103
  60. package/dist/resources/DatabaseTransaction.js.map +1 -1
  61. package/dist/resources/ResourceInterface.d.ts +8 -2
  62. package/dist/resources/ResourceInterface.js.map +1 -1
  63. package/dist/resources/Resources.js +22 -4
  64. package/dist/resources/Resources.js.map +1 -1
  65. package/dist/resources/Table.d.ts +6 -5
  66. package/dist/resources/Table.js +92 -15
  67. package/dist/resources/Table.js.map +1 -1
  68. package/dist/resources/analytics/write.js +6 -6
  69. package/dist/resources/analytics/write.js.map +1 -1
  70. package/dist/resources/blob.d.ts +0 -1
  71. package/dist/resources/blob.js +15 -7
  72. package/dist/resources/blob.js.map +1 -1
  73. package/dist/resources/databases.d.ts +22 -5
  74. package/dist/resources/databases.js +118 -9
  75. package/dist/resources/databases.js.map +1 -1
  76. package/dist/resources/jsResource.d.ts +4 -26
  77. package/dist/resources/jsResource.js +5 -59
  78. package/dist/resources/jsResource.js.map +1 -1
  79. package/dist/resources/models/Models.d.ts +11 -1
  80. package/dist/resources/models/Models.js +10 -1
  81. package/dist/resources/models/Models.js.map +1 -1
  82. package/dist/resources/models/backendRegistry.d.ts +9 -0
  83. package/dist/resources/models/backendRegistry.js +10 -0
  84. package/dist/resources/models/backendRegistry.js.map +1 -1
  85. package/dist/resources/models/openaiStream.d.ts +16 -1
  86. package/dist/resources/models/openaiStream.js +113 -21
  87. package/dist/resources/models/openaiStream.js.map +1 -1
  88. package/dist/resources/models/v1/chatCompletions.d.ts +54 -0
  89. package/dist/resources/models/v1/chatCompletions.js +115 -0
  90. package/dist/resources/models/v1/chatCompletions.js.map +1 -0
  91. package/dist/resources/models/v1/embeddings.d.ts +11 -0
  92. package/dist/resources/models/v1/embeddings.js +71 -0
  93. package/dist/resources/models/v1/embeddings.js.map +1 -0
  94. package/dist/resources/models/v1/errors.d.ts +54 -0
  95. package/dist/resources/models/v1/errors.js +130 -0
  96. package/dist/resources/models/v1/errors.js.map +1 -0
  97. package/dist/resources/models/v1/index.d.ts +36 -0
  98. package/dist/resources/models/v1/index.js +75 -0
  99. package/dist/resources/models/v1/index.js.map +1 -0
  100. package/dist/resources/models/v1/models.d.ts +26 -0
  101. package/dist/resources/models/v1/models.js +44 -0
  102. package/dist/resources/models/v1/models.js.map +1 -0
  103. package/dist/resources/models/v1/translation.d.ts +133 -0
  104. package/dist/resources/models/v1/translation.js +298 -0
  105. package/dist/resources/models/v1/translation.js.map +1 -0
  106. package/dist/resources/roles.d.ts +1 -1
  107. package/dist/resources/roles.js +54 -7
  108. package/dist/resources/roles.js.map +1 -1
  109. package/dist/security/jsLoader.js +84 -33
  110. package/dist/security/jsLoader.js.map +1 -1
  111. package/dist/security/role.js +4 -0
  112. package/dist/security/role.js.map +1 -1
  113. package/dist/security/superUserGuard.d.ts +7 -0
  114. package/dist/security/superUserGuard.js +23 -0
  115. package/dist/security/superUserGuard.js.map +1 -0
  116. package/dist/security/tokenAuthentication.d.ts +0 -1
  117. package/dist/security/tokenAuthentication.js +6 -2
  118. package/dist/security/tokenAuthentication.js.map +1 -1
  119. package/dist/security/user.d.ts +6 -1
  120. package/dist/security/user.js +23 -1
  121. package/dist/security/user.js.map +1 -1
  122. package/dist/server/http.d.ts +23 -2
  123. package/dist/server/http.js +99 -12
  124. package/dist/server/http.js.map +1 -1
  125. package/dist/server/operationsServer.d.ts +0 -2
  126. package/dist/server/operationsServer.js.map +1 -1
  127. package/dist/server/serverHelpers/multipartParser.js +9 -0
  128. package/dist/server/serverHelpers/multipartParser.js.map +1 -1
  129. package/dist/server/serverHelpers/operationAuthorizationState.d.ts +2 -0
  130. package/dist/server/serverHelpers/operationAuthorizationState.js +13 -0
  131. package/dist/server/serverHelpers/operationAuthorizationState.js.map +1 -0
  132. package/dist/server/serverHelpers/registeredOperations.d.ts +3 -2
  133. package/dist/server/serverHelpers/registeredOperations.js +14 -13
  134. package/dist/server/serverHelpers/registeredOperations.js.map +1 -1
  135. package/dist/server/serverHelpers/serverHandlers.js +19 -3
  136. package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
  137. package/dist/server/serverHelpers/serverUtilities.d.ts +1 -1
  138. package/dist/server/serverHelpers/serverUtilities.js +10 -7
  139. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  140. package/dist/server/static.js +57 -18
  141. package/dist/server/static.js.map +1 -1
  142. package/dist/server/storageReclamation.d.ts +17 -0
  143. package/dist/server/storageReclamation.js +90 -6
  144. package/dist/server/storageReclamation.js.map +1 -1
  145. package/dist/server/threads/socketRouter.js +20 -0
  146. package/dist/server/threads/socketRouter.js.map +1 -1
  147. package/dist/server/threads/threadServer.js +12 -0
  148. package/dist/server/threads/threadServer.js.map +1 -1
  149. package/dist/sqlEngine/diff/differential.js +7 -3
  150. package/dist/sqlEngine/diff/differential.js.map +1 -1
  151. package/dist/sqlTranslator/index.js +6 -1
  152. package/dist/sqlTranslator/index.js.map +1 -1
  153. package/dist/upgrade/upgradePrompt.d.ts +2 -2
  154. package/dist/upgrade/upgradePrompt.js +22 -3
  155. package/dist/upgrade/upgradePrompt.js.map +1 -1
  156. package/dist/utility/errors/commonErrors.d.ts +1 -0
  157. package/dist/utility/errors/commonErrors.js +1 -0
  158. package/dist/utility/errors/commonErrors.js.map +1 -1
  159. package/dist/utility/hdbTerms.d.ts +2 -0
  160. package/dist/utility/hdbTerms.js +2 -0
  161. package/dist/utility/hdbTerms.js.map +1 -1
  162. package/dist/utility/install/installer.js +32 -0
  163. package/dist/utility/install/installer.js.map +1 -1
  164. package/dist/utility/logging/harper_logger.d.ts +7 -0
  165. package/dist/utility/logging/harper_logger.js +57 -24
  166. package/dist/utility/logging/harper_logger.js.map +1 -1
  167. package/npm-shrinkwrap.json +197 -193
  168. package/package.json +9 -7
  169. package/resources/DatabaseTransaction.ts +310 -101
  170. package/resources/ResourceInterface.ts +8 -2
  171. package/resources/Resources.ts +22 -4
  172. package/resources/Table.ts +321 -243
  173. package/resources/analytics/write.ts +22 -20
  174. package/resources/blob.ts +15 -8
  175. package/resources/databases.ts +123 -12
  176. package/resources/jsResource.ts +5 -62
  177. package/resources/models/Models.ts +14 -1
  178. package/resources/models/backendRegistry.ts +10 -0
  179. package/resources/models/openaiStream.ts +131 -19
  180. package/resources/models/v1/chatCompletions.ts +128 -0
  181. package/resources/models/v1/embeddings.ts +70 -0
  182. package/resources/models/v1/errors.ts +141 -0
  183. package/resources/models/v1/index.ts +72 -0
  184. package/resources/models/v1/models.ts +53 -0
  185. package/resources/models/v1/translation.ts +362 -0
  186. package/resources/roles.ts +67 -7
  187. package/security/jsLoader.ts +84 -30
  188. package/security/role.ts +7 -0
  189. package/security/superUserGuard.ts +20 -0
  190. package/security/tokenAuthentication.ts +6 -3
  191. package/security/user.ts +26 -1
  192. package/server/DESIGN.md +45 -34
  193. package/server/http.ts +100 -13
  194. package/server/operationsServer.ts +0 -2
  195. package/server/serverHelpers/multipartParser.ts +9 -0
  196. package/server/serverHelpers/operationAuthorizationState.ts +11 -0
  197. package/server/serverHelpers/registeredOperations.ts +19 -15
  198. package/server/serverHelpers/serverHandlers.js +20 -3
  199. package/server/serverHelpers/serverUtilities.ts +10 -7
  200. package/server/static.ts +75 -20
  201. package/server/storageReclamation.ts +104 -8
  202. package/server/threads/socketRouter.ts +20 -0
  203. package/server/threads/threadServer.js +11 -0
  204. package/sqlTranslator/index.ts +6 -1
  205. package/static/defaultConfig.yaml +2 -0
  206. package/studio/web/assets/{Chat-DoVWScmq.js → Chat-aApwhRmz.js} +2 -2
  207. package/studio/web/assets/{Chat-DoVWScmq.js.map → Chat-aApwhRmz.js.map} +1 -1
  208. package/studio/web/assets/{FloatingChat-UZ2NsUOZ.js → FloatingChat-sC0H91n9.js} +4 -4
  209. package/studio/web/assets/{FloatingChat-UZ2NsUOZ.js.map → FloatingChat-sC0H91n9.js.map} +1 -1
  210. package/studio/web/assets/{apiToken-BUI_04o7.js → apiToken-DZ8JhHOI.js} +2 -2
  211. package/studio/web/assets/{apiToken-BUI_04o7.js.map → apiToken-DZ8JhHOI.js.map} +1 -1
  212. package/studio/web/assets/{applications-D03NA7wW.js → applications-9SEVLIO5.js} +2 -2
  213. package/studio/web/assets/{applications-D03NA7wW.js.map → applications-9SEVLIO5.js.map} +1 -1
  214. package/studio/web/assets/{index-Bh_CNAHr.js → index-Dy3uDGXb.js} +6 -6
  215. package/studio/web/assets/{index-Bh_CNAHr.js.map → index-Dy3uDGXb.js.map} +1 -1
  216. package/studio/web/assets/{index.lazy-Dx3MpyDC.js → index.lazy-CN1zq4I4.js} +4 -4
  217. package/studio/web/assets/{index.lazy-Dx3MpyDC.js.map → index.lazy-CN1zq4I4.js.map} +1 -1
  218. package/studio/web/assets/{notifications-0edoFTsb.js → notifications-BFAF07xr.js} +2 -2
  219. package/studio/web/assets/{notifications-0edoFTsb.js.map → notifications-BFAF07xr.js.map} +1 -1
  220. package/studio/web/assets/{notifications-CwKhipK7.js → notifications-CKlYVvVN.js} +2 -2
  221. package/studio/web/assets/{notifications-CwKhipK7.js.map → notifications-CKlYVvVN.js.map} +1 -1
  222. package/studio/web/assets/{profile-DUfEPQtx.js → profile-A1zhEdFG.js} +2 -2
  223. package/studio/web/assets/{profile-DUfEPQtx.js.map → profile-A1zhEdFG.js.map} +1 -1
  224. package/studio/web/assets/{setComponentFile-DMPo4UjC.js → setComponentFile-SEtBt_GV.js} +2 -2
  225. package/studio/web/assets/{setComponentFile-DMPo4UjC.js.map → setComponentFile-SEtBt_GV.js.map} +1 -1
  226. package/studio/web/assets/{setup-B56Oz1_u.js → setup-BisINqdH.js} +2 -2
  227. package/studio/web/assets/{setup-B56Oz1_u.js.map → setup-BisINqdH.js.map} +1 -1
  228. package/studio/web/assets/{status-BAod7p3o.js → status-BuQoCc7l.js} +2 -2
  229. package/studio/web/assets/{status-BAod7p3o.js.map → status-BuQoCc7l.js.map} +1 -1
  230. package/studio/web/assets/{swagger-ui-react-lQrBxfwM.js → swagger-ui-react-CAi_s1PC.js} +2 -2
  231. package/studio/web/assets/{swagger-ui-react-lQrBxfwM.js.map → swagger-ui-react-CAi_s1PC.js.map} +1 -1
  232. package/studio/web/assets/{tsMode-CrHCRjTK.js → tsMode-DT74tlkM.js} +2 -2
  233. package/studio/web/assets/{tsMode-CrHCRjTK.js.map → tsMode-DT74tlkM.js.map} +1 -1
  234. package/studio/web/assets/{useEntityRestURL-DoaBMEvU.js → useEntityRestURL-CGRGc1n7.js} +2 -2
  235. package/studio/web/assets/{useEntityRestURL-DoaBMEvU.js.map → useEntityRestURL-CGRGc1n7.js.map} +1 -1
  236. package/studio/web/index.html +1 -1
  237. package/upgrade/upgradePrompt.ts +22 -3
  238. package/utility/errors/commonErrors.ts +2 -0
  239. package/utility/hdbTerms.ts +2 -0
  240. package/utility/install/installer.ts +37 -0
  241. package/utility/logging/harper_logger.ts +57 -26
@@ -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';
@@ -393,6 +393,15 @@ export function makeTable(options) {
393
393
  let hasSourceGet: any;
394
394
  let primaryKeyAttribute: Attribute | undefined;
395
395
  let lastEvictionCompletion: Promise<void> = Promise.resolve();
396
+ // getFromSource() intentionally resolves its caller before the resolved record's cache
397
+ // write has committed (see there) so GET latency doesn't pay for the write. dropTable()
398
+ // must not drop this table's column families while one of those writes is still landing —
399
+ // racing a drop against an in-flight write is what corrupts the column family handle and
400
+ // produces "Invalid column family specified in write batch" (harper#1381). Track the
401
+ // in-flight commit promises here so dropTable() can drain them first, and stop admitting
402
+ // new ones (droppingTable) once a drop has actually started.
403
+ const pendingSourceCommits = new Set<Promise<any>>();
404
+ let droppingTable = false;
396
405
  let createdTimeProperty: Attribute | undefined,
397
406
  updatedTimeProperty: Attribute | undefined,
398
407
  expiresAtProperty: Attribute | undefined;
@@ -1375,12 +1384,54 @@ export function makeTable(options) {
1375
1384
  if (tombstoneWrite?.then) await tombstoneWrite;
1376
1385
  }
1377
1386
  }
1387
+ // A get() against a sourcedFrom table resolves to its caller before the resolved
1388
+ // record's cache write has committed (see getFromSource) - the write lands "in the
1389
+ // background" for latency reasons. Flip this BEFORE removing the table from the
1390
+ // schema below: getFromSource() checks it and skips caching (treats the load as
1391
+ // noCacheStore) for any call it admits from here on, including one that slipped in
1392
+ // through a stale reference to this Table between the two steps.
1393
+ droppingTable = true;
1378
1394
  // Remove the table from the in-memory schema immediately so concurrent
1379
1395
  // requests get "table does not exist" instead of racing the column
1380
1396
  // family drops below. If a drop fails past this point the table stays
1381
1397
  // invisible, and the tombstone guarantees the drop completes on the
1382
1398
  // next startup (or on a same-name create).
1383
1399
  delete databases[databaseName][tableName];
1400
+ // The above stops new source-fill writes from starting, but a write from a get()
1401
+ // that already returned to its caller may still be in flight. Dropping the column
1402
+ // families out from under that write is a genuine invariant violation, not just a
1403
+ // benign race: RocksDB rejects the still-open write batch with "Invalid column
1404
+ // family specified in write batch" (or "Could not access column family N"), which
1405
+ // can also abort this drop before it removes the tombstoned catalog rows - leaving
1406
+ // the table stuck "dropping" for completeInterruptedDrop to retry (and fail
1407
+ // identically) on every subsequent load (harper#1381). Drain any in-flight commits
1408
+ // before the blob sweep below (so it observes every row a drain-caught write just
1409
+ // committed) and before touching a single column family.
1410
+ //
1411
+ // Bounded, and fails CLOSED: the tracked promise covers the whole source round-trip
1412
+ // plus the local commit (see getFromSource), so a hung/slow source or a slow commit
1413
+ // (e.g. a large blob write) could otherwise wedge this drop forever. Rather than
1414
+ // give up and drop anyway - which would reopen exactly the race this drain exists to
1415
+ // close, just less often - a timeout FAILS the drop. The tombstone written above is
1416
+ // already durable, so completeInterruptedDrop picks the drop back up on the next
1417
+ // load, once the stuck write has had time to finish.
1418
+ if (pendingSourceCommits.size) {
1419
+ const pending = [...pendingSourceCommits];
1420
+ let timer: NodeJS.Timeout;
1421
+ const timedOut = Symbol('timedOut');
1422
+ const result = await Promise.race([
1423
+ Promise.allSettled(pending),
1424
+ new Promise<typeof timedOut>((resolve) => {
1425
+ timer = setTimeout(() => resolve(timedOut), LOCK_TIMEOUT);
1426
+ }),
1427
+ ]);
1428
+ clearTimeout(timer);
1429
+ if (result === timedOut) {
1430
+ throw new Error(
1431
+ `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.`
1432
+ );
1433
+ }
1434
+ }
1384
1435
  for (const entry of primaryStore.getRange({ versions: true, snapshot: false, lazy: true })) {
1385
1436
  if (entry.metadataFlags & HAS_BLOBS && entry.value) {
1386
1437
  deleteBlobsInObject(entry.value);
@@ -4707,13 +4758,13 @@ export function makeTable(options) {
4707
4758
  (stats.treeBranchPageCount + stats.treeLeafPageCount + stats.overflowPages) * stats.pageSize)
4708
4759
  );
4709
4760
  }
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
- };
4761
+ /**
4762
+ * Get available/free/size storage stats for the table's underlying volume. Async because
4763
+ * this may need to read quota-status.json (#1976); getSize/getAuditSize stay sync because
4764
+ * they only read in-memory store stats.
4765
+ */
4766
+ static async getStorageStats() {
4767
+ return getStorageSpaceStats(primaryStore.path);
4717
4768
  }
4718
4769
  static async getRecordCount(options?: any) {
4719
4770
  // iterate through the metadata entries to exclude their count and exclude the deletion counts
@@ -5511,9 +5562,10 @@ export function makeTable(options) {
5511
5562
  transaction.next.isReplay = transaction.isReplay;
5512
5563
  // Inherit which resource/method started this logical operation, so a chained (second
5513
5564
  // 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.)
5565
+ // identify the request that caused it instead of leaving that field blank. The
5566
+ // checkOverloaded() stuck-commit log reads this too every chained link's own native
5567
+ // commit is tracked with its own identity (DatabaseTransaction.ts's trackOutstandingCommit),
5568
+ // so a wedged second-store commit is named just as precisely as a wedged first one.
5517
5569
  transaction.next.startedFrom = transaction.startedFrom;
5518
5570
  if (transaction.open === TRANSACTION_STATE.CLOSED) {
5519
5571
  // if the current transaction is already closed, we need to retain that state on new databases we work with
@@ -5700,7 +5752,10 @@ export function makeTable(options) {
5700
5752
  replacingRecord: existingRecord,
5701
5753
  replacingEntry: existingEntry,
5702
5754
  replacingVersion: existingVersion,
5703
- noCacheStore: false,
5755
+ // Once dropTable() has started, no new source-fill write may begin (dropTable()
5756
+ // only drains writes already in flight - see there); still resolve the caller's
5757
+ // read with fresh source data, just don't cache it into a table that's going away.
5758
+ noCacheStore: droppingTable,
5704
5759
  source: null,
5705
5760
  transaction: undefined,
5706
5761
  expiresAt: undefined,
@@ -5711,264 +5766,287 @@ export function makeTable(options) {
5711
5766
  // we don't want to wait for the transaction because we want to return as fast as possible
5712
5767
  // and let the transaction commit in the background
5713
5768
  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
- }
5769
+ // Tracked in pendingSourceCommits (below) for the full lifetime of this transaction -
5770
+ // including the source round-trip, not just from the point it's actually staged -
5771
+ // so dropTable() can wait for it before dropping the table's column families (see the
5772
+ // comment there). Registering only once a write reaches staging would track less (no
5773
+ // Set churn for a plain cache miss, and a slow/hung source couldn't delay a drop) and
5774
+ // is safe on its own given the live droppingTable re-checks in this function - but it
5775
+ // would make dropTable()'s correctness depend on every future early-return path in
5776
+ // this function remembering to check droppingTable, rather than on dropTable() simply
5777
+ // waiting for whatever this function is doing. Tracking the whole lifetime is the
5778
+ // belt to that suspenders, at the cost of a bounded wait on a merely slow source
5779
+ // before the drain's fail-closed timeout below.
5780
+ const commitPromise = transaction(sourceContext, async (_txn) => {
5781
+ const start = performance.now();
5782
+ let updatedRecord;
5783
+ let hasChanges, invalidated;
5784
+ try {
5785
+ updatedRecord = await throttledCallToSource(source, id, sourceContext, existingEntry);
5786
+ invalidated = metadataFlags & INVALIDATED;
5787
+ let version = sourceContext.lastModified || (invalidated && existingVersion);
5788
+ hasChanges = invalidated || version > existingVersion || !existingRecord;
5789
+ const resolveDuration = performance.now() - start;
5790
+ recordAction(resolveDuration, 'cache-resolution', tableName, null, 'success');
5791
+ if (responseHeaders)
5792
+ appendHeader(responseHeaders, 'Server-Timing', `cache-resolve;dur=${resolveDuration.toFixed(2)}`, true);
5793
+ if (expirationMs && sourceContext.expiresAt == undefined) sourceContext.expiresAt = Date.now() + expirationMs;
5794
+ if (updatedRecord) {
5795
+ if (typeof updatedRecord !== 'object') throw new Error('Only objects can be cached and stored in tables');
5796
+ if (updatedRecord.status > 0 && updatedRecord.headers) {
5797
+ // if the source has a status code and headers, treat it as a response
5798
+ const status = updatedRecord.status;
5799
+ if (status === 304) {
5800
+ // revalidation of our current cached record
5801
+ updatedRecord = existingRecord;
5802
+ version = existingVersion;
5803
+ } else if (!CACHEABLE_STATUS_CODES.has(status)) {
5804
+ // non-cacheable status - propagate to client without caching
5805
+ throw new ServerError(updatedRecord.body || 'Error from source', status);
5806
+ } else {
5807
+ let headers: any;
5808
+ const sourceHeaders = updatedRecord.headers;
5809
+ if (sourceHeaders[Symbol.iterator]) {
5810
+ headers = {};
5811
+ for (let [name, value] of sourceHeaders) {
5812
+ headers[name.toLowerCase()] = value;
5765
5813
  }
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) };
5814
+ } else {
5815
+ headers = sourceHeaders; // just a plain object
5816
+ }
5817
+ const contentType = sourceHeaders.get?.('Content-Type');
5818
+ let data: any;
5819
+ if (contentType === 'application/json' && updatedRecord.json) {
5820
+ // use native .json() if possible
5821
+ data = await updatedRecord.json();
5822
+ } else {
5823
+ const contentTypeHandler = contentType && contentTypes.get(contentType);
5824
+ if (contentTypeHandler?.deserialize) {
5825
+ data = contentTypeHandler.deserialize(
5826
+ await (contentType.startsWith('text/') ? updatedRecord.text() : updatedRecord.bytes())
5827
+ );
5772
5828
  }
5773
- if (status !== 200) updatedRecord.status = status;
5774
5829
  }
5830
+ if (data !== undefined) {
5831
+ // we have structured data that we have parsed
5832
+ delete headers['content-type']; // don't store the content type if we have already parsed it
5833
+ updatedRecord = { headers, data };
5834
+ } else {
5835
+ updatedRecord = { headers, body: createBlob(updatedRecord.body) };
5836
+ }
5837
+ if (status !== 200) updatedRecord.status = status;
5775
5838
  }
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
- }
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
5839
  }
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;
5840
+ if (typeof updatedRecord.toJSON === 'function') updatedRecord = updatedRecord.toJSON();
5841
+ // updatedRecord may still be a frozen record (e.g. a reused existingRecord); copy-on-mutate
5842
+ // before stamping the primary key and created/updated times below (records are immutable —
5843
+ // 5.2 record caching relies on it — so we must not write through the frozen object).
5844
+ if (isFrozenRecordObject(updatedRecord)) updatedRecord = { ...updatedRecord };
5845
+ if (primaryKey && updatedRecord[primaryKey] !== id) updatedRecord[primaryKey] = id;
5830
5846
  }
5831
- if (context?.noCacheStore || sourceContext.noCacheStore) {
5832
- // abort before we write any change
5833
- sourceContext.transaction.abort();
5834
- return;
5835
- }
5836
- const dbTxn = txnForContext(sourceContext);
5837
- const sourceWrite: any = {
5847
+ resolved = true;
5848
+ const resolvedEntry: Entry = {
5838
5849
  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;
5850
+ version,
5851
+ value: updatedRecord,
5852
+ expiresAt: sourceContext.expiresAt,
5853
+ metadataFlags: 0,
5854
+ size: 0,
5855
+ localTime: 0,
5856
+ nodeId: 0,
5857
+ residencyId: 0,
5858
+ } as any;
5859
+ // Give the plain object the RecordObject prototype so getExpiresAt/getUpdatedTime
5860
+ // are available on the immediately-resolved entry. We mutate the prototype
5861
+ // in-place rather than copying so that the commit callback (which adds
5862
+ // createdAt/updatedAt to updatedRecord) is still reflected in the entry value.
5863
+ if (updatedRecord && updatedRecord.constructor === Object) {
5864
+ Object.setPrototypeOf(updatedRecord, primaryStore.encoder.structPrototype);
5865
+ entryMap.set(updatedRecord, resolvedEntry);
5866
+ }
5867
+ resolve(resolvedEntry);
5868
+ } catch (error) {
5869
+ error.message += ` while resolving record ${id} for ${tableName}`;
5870
+ if (
5871
+ existingRecord &&
5872
+ (((error.code === 'ECONNRESET' || error.code === 'ECONNREFUSED' || error.code === 'EAI_AGAIN') &&
5873
+ !context?.mustRevalidate) ||
5874
+ (context?.staleIfError &&
5875
+ (error.statusCode === 500 ||
5876
+ error.statusCode === 502 ||
5877
+ error.statusCode === 503 ||
5878
+ error.statusCode === 504)))
5879
+ ) {
5880
+ // these are conditions under which we can use stale data after an error
5881
+ resolve({
5882
+ key: id,
5883
+ version: existingVersion,
5884
+ value: existingRecord,
5885
+ } as any);
5886
+ logger.trace?.(error.message, '(returned stale record)');
5887
+ } else reject(error);
5888
+ const resolveDuration = performance.now() - start;
5889
+ recordAction(resolveDuration, 'cache-resolution', tableName, null, 'fail');
5890
+ if (responseHeaders)
5891
+ appendHeader(responseHeaders, 'Server-Timing', `cache-resolve;dur=${resolveDuration.toFixed(2)}`, true);
5892
+ sourceContext.transaction.abort();
5893
+ return;
5894
+ }
5895
+ if (context?.noCacheStore || sourceContext.noCacheStore || droppingTable) {
5896
+ // abort before we write any change. droppingTable is re-checked live (not just
5897
+ // the noCacheStore snapshot taken at call start) because a call admitted before
5898
+ // dropTable() began can still be sitting here after it started - the await above
5899
+ // waited on the source, which may take arbitrarily long.
5900
+ sourceContext.transaction.abort();
5901
+ return;
5902
+ }
5903
+ const dbTxn = txnForContext(sourceContext);
5904
+ const sourceWrite: any = {
5905
+ key: id,
5906
+ store: primaryStore,
5907
+ entry: existingEntry,
5908
+ nodeName: 'source',
5909
+ commit: (txnTime, existingEntry, _retry, transaction: any) => {
5910
+ sourceWrite.skipped = false; // reset on each retry; cleanup happens after commit if still true
5911
+ if (existingEntry?.version !== existingVersion) {
5912
+ // don't do anything if the version has changed
5913
+ sourceWrite.skipped = true;
5914
+ return;
5915
+ }
5916
+ updateIndices(id, existingRecord, updatedRecord, transaction && { transaction });
5917
+ if (updatedRecord) {
5918
+ if (existingEntry) {
5919
+ context.previousResidency = TableResource.getResidencyRecord(existingEntry.residencyId);
5848
5920
  }
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'
5921
+ let auditRecord: any;
5922
+ let omitLocalRecord = false;
5923
+ let residencyId: number;
5924
+ if (updatedTimeProperty) {
5925
+ updatedRecord[updatedTimeProperty.name] =
5926
+ updatedTimeProperty.type === 'Date'
5927
+ ? new Date(txnTime)
5928
+ : updatedTimeProperty.type === 'String'
5929
+ ? new Date(txnTime).toISOString()
5930
+ : txnTime;
5931
+ }
5932
+ if (createdTimeProperty && updatedRecord[createdTimeProperty.name] == null) {
5933
+ const existingCreatedTime = existingEntry?.value?.[createdTimeProperty.name];
5934
+ if (existingCreatedTime != null) {
5935
+ updatedRecord[createdTimeProperty.name] = existingCreatedTime;
5936
+ } else {
5937
+ updatedRecord[createdTimeProperty.name] =
5938
+ createdTimeProperty.type === 'Date'
5860
5939
  ? new Date(txnTime)
5861
- : updatedTimeProperty.type === 'String'
5940
+ : createdTimeProperty.type === 'String'
5862
5941
  ? new Date(txnTime).toISOString()
5863
5942
  : txnTime;
5864
5943
  }
5865
- if (createdTimeProperty && updatedRecord[createdTimeProperty.name] == null) {
5866
- const existingCreatedTime = existingEntry?.value?.[createdTimeProperty.name];
5867
- if (existingCreatedTime != null) {
5868
- updatedRecord[createdTimeProperty.name] = existingCreatedTime;
5944
+ }
5945
+ const residency = residencyFromFunction(TableResource.getResidency(updatedRecord, context));
5946
+ if (residency) {
5947
+ if (!residency.includes(server.hostname)) {
5948
+ // if we aren't in the residency list, specify that our local record should be omitted or be partial
5949
+ auditRecord = updatedRecord;
5950
+ omitLocalRecord = true;
5951
+ if (TableResource.getResidencyById) {
5952
+ // complete omission of the record that doesn't belong here
5953
+ updatedRecord = undefined;
5869
5954
  } 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];
5955
+ // store the partial record
5956
+ updatedRecord = null;
5957
+ for (const name in indices) {
5958
+ if (!updatedRecord) {
5959
+ updatedRecord = {};
5901
5960
  }
5961
+ // if there are any indices, we need to preserve a partial invalidated record to ensure we can still do searches
5962
+ updatedRecord[name] = auditRecord[name];
5963
+ }
5964
+ if (createdTimeProperty && auditRecord[createdTimeProperty.name] != null) {
5965
+ // preserve the created timestamp in the partial record so it isn't lost when we don't have residency
5966
+ if (!updatedRecord) updatedRecord = {};
5967
+ updatedRecord[createdTimeProperty.name] = auditRecord[createdTimeProperty.name];
5902
5968
  }
5903
5969
  }
5904
- residencyId = getResidencyId(residency);
5905
5970
  }
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
5971
+ residencyId = getResidencyId(residency);
5972
+ }
5973
+ logger.trace?.(
5974
+ `Writing resolved record from source with id: ${id}, timestamp: ${new Date(txnTime).toISOString()}`
5975
+ );
5976
+ // TODO: We are doing a double check for ifVersion that should probably be cleaned out
5977
+ updateRecord(
5978
+ id,
5979
+ updatedRecord,
5980
+ existingEntry,
5981
+ txnTime,
5982
+ omitLocalRecord ? INVALIDATED : 0,
5983
+ (audit && (hasChanges || omitLocalRecord)) || null,
5984
+ {
5985
+ user: (sourceContext as any)?.user,
5986
+ expiresAt: sourceContext.expiresAt,
5987
+ residencyId,
5988
+ transaction,
5989
+ tableToTrack: tableName,
5990
+ },
5991
+ 'put',
5992
+ Boolean(invalidated),
5993
+ auditRecord
5994
+ );
5995
+ // arm the eviction scanner, mirroring the .put() path
5996
+ if (sourceContext.expiresAt) scheduleCleanup();
5997
+ } else if (existingEntry) {
5998
+ logger.trace?.(
5999
+ `Deleting resolved record from source with id: ${id}, timestamp: ${new Date(txnTime).toISOString()}`
6000
+ );
6001
+ if (audit || trackDeletes) {
5910
6002
  updateRecord(
5911
6003
  id,
5912
- updatedRecord,
6004
+ null,
5913
6005
  existingEntry,
5914
6006
  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()}`
6007
+ 0,
6008
+ (audit && hasChanges) || null,
6009
+ { user: (sourceContext as any)?.user, transaction, tableToTrack: tableName },
6010
+ 'delete',
6011
+ Boolean(invalidated)
5933
6012
  );
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
- }
6013
+ } else {
6014
+ removeEntry(primaryStore, existingEntry, existingVersion);
5949
6015
  }
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
- }),
6016
+ }
6017
+ },
6018
+ };
6019
+ // The cache-from-source write bypasses `_writeUpdate`, so wire the embed hook here
6020
+ // too (always the originating node). It runs after the client GET has resolved with
6021
+ // fresh source data, so it's a background commit: an embedder failure aborts the cache
6022
+ // write via the outer error handler (row re-embeds next read) and never reaches the
6023
+ // caller. Source-resolution errors are handled earlier, with the stale-data fallback.
6024
+ const embedBefore = buildEmbedBefore(
6025
+ updatedRecord,
6026
+ sourceContext,
6027
+ undefined,
6028
+ TableResource.embedAttributes,
6029
+ TableResource.userEmbedders
6030
+ );
6031
+ if (embedBefore) await embedBefore();
6032
+ if (droppingTable) {
6033
+ // Re-check right before staging the write: dropTable() may have started
6034
+ // while we were awaiting the embed step above (harper#1381).
6035
+ sourceContext.transaction.abort();
6036
+ return;
6037
+ }
6038
+ sourceWrite.before = preCommitBlobsForRecordBefore(sourceWrite, updatedRecord);
6039
+ dbTxn.addWrite(sourceWrite);
6040
+ });
6041
+ pendingSourceCommits.add(commitPromise);
6042
+ when(
6043
+ commitPromise,
5968
6044
  () => {
6045
+ pendingSourceCommits.delete(commitPromise);
5969
6046
  primaryStore.unlock(id);
5970
6047
  },
5971
6048
  (error) => {
6049
+ pendingSourceCommits.delete(commitPromise);
5972
6050
  primaryStore.unlock(id);
5973
6051
  if (resolved) logger.error?.('Error committing cache update', error);
5974
6052
  // else the error was already propagated as part of the promise that we returned