mindoodb 0.0.73 → 0.0.75

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 (74) hide show
  1. package/dist/browser/index.mjs +9 -0
  2. package/dist/core/BackgroundTaskTracker.d.ts +18 -0
  3. package/dist/core/BackgroundTaskTracker.d.ts.map +1 -0
  4. package/dist/core/BackgroundTaskTracker.js +37 -0
  5. package/dist/core/BackgroundTaskTracker.js.map +1 -0
  6. package/dist/core/BaseMindooDB.d.ts +43 -7
  7. package/dist/core/BaseMindooDB.d.ts.map +1 -1
  8. package/dist/core/BaseMindooDB.js +271 -79
  9. package/dist/core/BaseMindooDB.js.map +1 -1
  10. package/dist/core/BaseMindooTenant.d.ts +30 -12
  11. package/dist/core/BaseMindooTenant.d.ts.map +1 -1
  12. package/dist/core/BaseMindooTenant.js +90 -65
  13. package/dist/core/BaseMindooTenant.js.map +1 -1
  14. package/dist/core/BaseMindooTenantDirectory.d.ts +57 -14
  15. package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
  16. package/dist/core/BaseMindooTenantDirectory.js +319 -164
  17. package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
  18. package/dist/core/appendonlystores/migration/StoreMigration.d.ts +34 -0
  19. package/dist/core/appendonlystores/migration/StoreMigration.d.ts.map +1 -0
  20. package/dist/core/appendonlystores/migration/StoreMigration.js +70 -0
  21. package/dist/core/appendonlystores/migration/StoreMigration.js.map +1 -0
  22. package/dist/core/appendonlystores/rpc/RpcContentAddressedStore.d.ts +97 -0
  23. package/dist/core/appendonlystores/rpc/RpcContentAddressedStore.d.ts.map +1 -0
  24. package/dist/core/appendonlystores/rpc/RpcContentAddressedStore.js +128 -0
  25. package/dist/core/appendonlystores/rpc/RpcContentAddressedStore.js.map +1 -0
  26. package/dist/core/appendonlystores/rpc/StoreRpcServer.d.ts +24 -0
  27. package/dist/core/appendonlystores/rpc/StoreRpcServer.d.ts.map +1 -0
  28. package/dist/core/appendonlystores/rpc/StoreRpcServer.js +102 -0
  29. package/dist/core/appendonlystores/rpc/StoreRpcServer.js.map +1 -0
  30. package/dist/core/appendonlystores/rpc/storeRpcCodec.d.ts +15 -0
  31. package/dist/core/appendonlystores/rpc/storeRpcCodec.d.ts.map +1 -0
  32. package/dist/core/appendonlystores/rpc/storeRpcCodec.js +100 -0
  33. package/dist/core/appendonlystores/rpc/storeRpcCodec.js.map +1 -0
  34. package/dist/core/appendonlystores/sqlite/SqliteContentAddressedStore.d.ts +3 -0
  35. package/dist/core/appendonlystores/sqlite/SqliteContentAddressedStore.d.ts.map +1 -1
  36. package/dist/core/appendonlystores/sqlite/SqliteContentAddressedStore.js +18 -11
  37. package/dist/core/appendonlystores/sqlite/SqliteContentAddressedStore.js.map +1 -1
  38. package/dist/core/appendonlystores/sqlite/SqliteDriver.d.ts +13 -0
  39. package/dist/core/appendonlystores/sqlite/SqliteDriver.d.ts.map +1 -1
  40. package/dist/core/appendonlystores/sqlite/SqliteDriver.js +74 -0
  41. package/dist/core/appendonlystores/sqlite/SqliteDriver.js.map +1 -1
  42. package/dist/core/cache/CacheManager.d.ts +4 -1
  43. package/dist/core/cache/CacheManager.d.ts.map +1 -1
  44. package/dist/core/cache/CacheManager.js +47 -19
  45. package/dist/core/cache/CacheManager.js.map +1 -1
  46. package/dist/core/index.d.ts +3 -0
  47. package/dist/core/index.d.ts.map +1 -1
  48. package/dist/core/index.js +13 -2
  49. package/dist/core/index.js.map +1 -1
  50. package/dist/core/indexing/fulltext/DocumentFullTextIndex.d.ts +16 -1
  51. package/dist/core/indexing/fulltext/DocumentFullTextIndex.d.ts.map +1 -1
  52. package/dist/core/indexing/fulltext/DocumentFullTextIndex.js +86 -14
  53. package/dist/core/indexing/fulltext/DocumentFullTextIndex.js.map +1 -1
  54. package/dist/core/indexing/summary/DocumentSummaryStore.d.ts +2 -0
  55. package/dist/core/indexing/summary/DocumentSummaryStore.d.ts.map +1 -1
  56. package/dist/core/indexing/summary/DocumentSummaryStore.js +19 -5
  57. package/dist/core/indexing/summary/DocumentSummaryStore.js.map +1 -1
  58. package/dist/core/types.d.ts +29 -0
  59. package/dist/core/types.d.ts.map +1 -1
  60. package/dist/core/types.js +22 -0
  61. package/dist/core/types.js.map +1 -1
  62. package/dist/core/utils/singleFlight.d.ts +21 -0
  63. package/dist/core/utils/singleFlight.d.ts.map +1 -0
  64. package/dist/core/utils/singleFlight.js +47 -0
  65. package/dist/core/utils/singleFlight.js.map +1 -0
  66. package/dist/core/utils/timeSource.d.ts +3 -1
  67. package/dist/core/utils/timeSource.d.ts.map +1 -1
  68. package/dist/core/utils/timeSource.js +4 -0
  69. package/dist/core/utils/timeSource.js.map +1 -1
  70. package/dist/sqlite/index.d.ts +2 -0
  71. package/dist/sqlite/index.d.ts.map +1 -1
  72. package/dist/sqlite/index.js +8 -1
  73. package/dist/sqlite/index.js.map +1 -1
  74. package/package.json +1 -1
@@ -8,9 +8,10 @@ const automerge_adapter_1 = require("./automerge-adapter");
8
8
  const values_1 = require("./values");
9
9
  const storeEntryTime_1 = require("./storeEntryTime");
10
10
  const types_1 = require("./types");
11
- const types_2 = require("./appendonlystores/types");
11
+ const types_2 = require("./types");
12
+ const types_3 = require("./appendonlystores/types");
12
13
  const syncStores_1 = require("./appendonlystores/syncStores");
13
- const types_3 = require("./appendonlystores/network/types");
14
+ const types_4 = require("./appendonlystores/network/types");
14
15
  const MaterializationPlanner_1 = require("./appendonlystores/MaterializationPlanner");
15
16
  const DocumentDagAnalysis_1 = require("./DocumentDagAnalysis");
16
17
  const DocumentConflictAnalysis_1 = require("./DocumentConflictAnalysis");
@@ -33,13 +34,14 @@ const builtinDbInvariants_1 = require("./builtinDbInvariants");
33
34
  const recipients_1 = require("./userkeys/recipients");
34
35
  const sealedTypes_1 = require("./userkeys/sealedTypes");
35
36
  const DocumentSummaryStore_1 = require("./indexing/summary/DocumentSummaryStore");
36
- const types_4 = require("./indexing/summary/types");
37
+ const types_5 = require("./indexing/summary/types");
37
38
  const DocumentFullTextIndex_1 = require("./indexing/fulltext/DocumentFullTextIndex");
38
- const types_5 = require("./indexing/fulltext/types");
39
- const types_6 = require("./extraction/types");
39
+ const types_6 = require("./indexing/fulltext/types");
40
+ const types_7 = require("./extraction/types");
40
41
  const executeQuery_1 = require("./query/executeQuery");
41
42
  const queryView_1 = require("./query/queryView");
42
43
  const queryLive_1 = require("./query/queryLive");
44
+ const BackgroundTaskTracker_1 = require("./BackgroundTaskTracker");
43
45
  /**
44
46
  * Default chunk size for attachments: 256KB
45
47
  */
@@ -116,7 +118,7 @@ function networkErrorType(error) {
116
118
  * server's `Retry-After` when it asks for a longer wait than our backoff.
117
119
  */
118
120
  function rateLimitRetryDelayMs(error, attempt) {
119
- if (networkErrorType(error) !== types_3.NetworkErrorType.RATE_LIMITED ||
121
+ if (networkErrorType(error) !== types_4.NetworkErrorType.RATE_LIMITED ||
120
122
  attempt >= PREFETCH_RATE_LIMIT_DELAYS_MS.length) {
121
123
  return null;
122
124
  }
@@ -249,10 +251,12 @@ class BaseMindooDB {
249
251
  */
250
252
  this.quarantineLog = new Map();
251
253
  /**
252
- * Short-TTL cache of "is access control active for this tenant?" so the
253
- * materialization fast-path does not re-query the directory on every load.
254
+ * Last known "is access control active for this tenant?" verdict. Only used
255
+ * as the fail-closed fallback when the directory cannot be consulted; the
256
+ * live check itself is cheap because the directory skips its rebuild while
257
+ * its changefeed `changeSeq` is unchanged.
254
258
  */
255
- this.aclActiveCache = null;
259
+ this.aclActiveLastKnown = null;
256
260
  /**
257
261
  * Per-document cache of the original creator's signing public key and the
258
262
  * trusted time of the `doc_create` entry. Used by the client write prechecks
@@ -452,6 +456,8 @@ class BaseMindooDB {
452
456
  this.changeNotifyTimer = null;
453
457
  /** While > 0, timer-based emission is suppressed (e.g. during a sync batch). */
454
458
  this.changeNotificationHolds = 0;
459
+ /** Fire-and-forget work of this database; see {@link whenBackgroundIdle}. */
460
+ this.backgroundTasks = new BackgroundTaskTracker_1.BackgroundTaskTracker();
455
461
  // ---------------------------------------------------------------------------
456
462
  // Summary auto-follow (see setSummaryAutoUpdateEnabled): keeps the summary
457
463
  // buffer current as changes arrive (local writes and sync batches), so the
@@ -567,11 +573,11 @@ class BaseMindooDB {
567
573
  if (store.getId() !== attachmentStore.getId()) {
568
574
  throw new Error(`[BaseMindooDB] Expected document and attachment stores to share dbId but received ${store.getId()} and ${attachmentStore.getId()}`);
569
575
  }
570
- if (store.getStoreKind() !== types_2.StoreKind.docs) {
571
- throw new Error(`[BaseMindooDB] Expected primary store kind ${types_2.StoreKind.docs} but received ${store.getStoreKind()}`);
576
+ if (store.getStoreKind() !== types_3.StoreKind.docs) {
577
+ throw new Error(`[BaseMindooDB] Expected primary store kind ${types_3.StoreKind.docs} but received ${store.getStoreKind()}`);
572
578
  }
573
- if (attachmentStore.getStoreKind() !== types_2.StoreKind.attachments) {
574
- throw new Error(`[BaseMindooDB] Expected attachment store kind ${types_2.StoreKind.attachments} but received ${attachmentStore.getStoreKind()}`);
579
+ if (attachmentStore.getStoreKind() !== types_3.StoreKind.attachments) {
580
+ throw new Error(`[BaseMindooDB] Expected attachment store kind ${types_3.StoreKind.attachments} but received ${attachmentStore.getStoreKind()}`);
575
581
  }
576
582
  this.tenant = tenant;
577
583
  this.store = store;
@@ -898,7 +904,7 @@ class BaseMindooDB {
898
904
  this.dirtyDocIds.clear();
899
905
  this.cacheMetaDirty = false;
900
906
  this.summaryStore?.discardDirtyState();
901
- this.fulltextIndex?.clearDirty();
907
+ this.fulltextIndex?.discardDirtyState();
902
908
  void cacheManager.deregister(this);
903
909
  if (this.summaryStore) {
904
910
  void cacheManager.deregister(this.summaryStore);
@@ -939,8 +945,8 @@ class BaseMindooDB {
939
945
  */
940
946
  async getSummarySetup() {
941
947
  try {
942
- const doc = await this.getDocument(types_4.DB_SETUP_DOC_ID);
943
- return (0, types_4.sanitizeSummaryConfig)(doc.getData()[types_4.SUMMARY_SETUP_FIELD]) ?? null;
948
+ const doc = await this.getDocument(types_5.DB_SETUP_DOC_ID);
949
+ return (0, types_5.sanitizeSummaryConfig)(doc.getData()[types_5.SUMMARY_SETUP_FIELD]) ?? null;
944
950
  }
945
951
  catch (error) {
946
952
  if (error instanceof errors_1.DocumentNotFoundError ||
@@ -962,14 +968,14 @@ class BaseMindooDB {
962
968
  * backfill — locally and, after sync, on every other replica.
963
969
  */
964
970
  async setSummarySetup(config) {
965
- const doc = await this.createDocument({ id: types_4.DB_SETUP_DOC_ID });
971
+ const doc = await this.createDocument({ id: types_5.DB_SETUP_DOC_ID });
966
972
  await this.changeDoc(doc, (d) => {
967
973
  const data = d.getData();
968
974
  if (config === null) {
969
- delete data[types_4.SUMMARY_SETUP_FIELD];
975
+ delete data[types_5.SUMMARY_SETUP_FIELD];
970
976
  }
971
977
  else {
972
- data[types_4.SUMMARY_SETUP_FIELD] = (0, types_4.sanitizeSummaryConfig)(config) ?? {};
978
+ data[types_5.SUMMARY_SETUP_FIELD] = (0, types_5.sanitizeSummaryConfig)(config) ?? {};
973
979
  }
974
980
  });
975
981
  }
@@ -1013,12 +1019,12 @@ class BaseMindooDB {
1013
1019
  this.store.getId() === types_1.USER_DIRECTORY_DB_ID) {
1014
1020
  return;
1015
1021
  }
1016
- const hasSetupChange = changes.some((change) => change.docId === types_4.DB_SETUP_DOC_ID);
1022
+ const hasSetupChange = changes.some((change) => change.docId === types_5.DB_SETUP_DOC_ID);
1017
1023
  if (!hasSetupChange && this.summarySetupProbed) {
1018
1024
  return;
1019
1025
  }
1020
1026
  this.summarySetupProbed = true;
1021
- void this.activateSummaryFromSetupDoc();
1027
+ this.backgroundTasks.run(this.activateSummaryFromSetupDoc());
1022
1028
  }
1023
1029
  /** Create the summary store when the setup document configures one. */
1024
1030
  async activateSummaryFromSetupDoc() {
@@ -1049,7 +1055,7 @@ class BaseMindooDB {
1049
1055
  return;
1050
1056
  }
1051
1057
  this.summaryAutoUpdateRunning = true;
1052
- void (async () => {
1058
+ this.backgroundTasks.run((async () => {
1053
1059
  try {
1054
1060
  do {
1055
1061
  this.summaryAutoUpdatePending = false;
@@ -1063,7 +1069,7 @@ class BaseMindooDB {
1063
1069
  finally {
1064
1070
  this.summaryAutoUpdateRunning = false;
1065
1071
  }
1066
- })();
1072
+ })());
1067
1073
  }
1068
1074
  // ---------------------------------------------------------------------------
1069
1075
  // Full-text index (see docs/fulltext-search.md)
@@ -1103,8 +1109,8 @@ class BaseMindooDB {
1103
1109
  */
1104
1110
  async getFulltextSetup() {
1105
1111
  try {
1106
- const doc = await this.getDocument(types_4.DB_SETUP_DOC_ID);
1107
- return ((0, types_5.sanitizeFulltextConfig)(doc.getData()[types_5.FULLTEXT_SETUP_FIELD]) ?? null);
1112
+ const doc = await this.getDocument(types_5.DB_SETUP_DOC_ID);
1113
+ return ((0, types_6.sanitizeFulltextConfig)(doc.getData()[types_6.FULLTEXT_SETUP_FIELD]) ?? null);
1108
1114
  }
1109
1115
  catch (error) {
1110
1116
  if (error instanceof errors_1.DocumentNotFoundError ||
@@ -1128,19 +1134,19 @@ class BaseMindooDB {
1128
1134
  * resumable backfill — locally and, after sync, on every other replica.
1129
1135
  */
1130
1136
  async setFulltextSetup(config) {
1131
- const sanitized = config === null ? null : ((0, types_5.sanitizeFulltextConfig)(config) ?? {});
1137
+ const sanitized = config === null ? null : ((0, types_6.sanitizeFulltextConfig)(config) ?? {});
1132
1138
  const current = await this.getFulltextSetup();
1133
1139
  if (JSON.stringify(current) === JSON.stringify(sanitized)) {
1134
1140
  return;
1135
1141
  }
1136
- const doc = await this.createDocument({ id: types_4.DB_SETUP_DOC_ID });
1142
+ const doc = await this.createDocument({ id: types_5.DB_SETUP_DOC_ID });
1137
1143
  await this.changeDoc(doc, (d) => {
1138
1144
  const data = d.getData();
1139
1145
  if (sanitized === null) {
1140
- delete data[types_5.FULLTEXT_SETUP_FIELD];
1146
+ delete data[types_6.FULLTEXT_SETUP_FIELD];
1141
1147
  }
1142
1148
  else {
1143
- data[types_5.FULLTEXT_SETUP_FIELD] = sanitized;
1149
+ data[types_6.FULLTEXT_SETUP_FIELD] = sanitized;
1144
1150
  }
1145
1151
  });
1146
1152
  }
@@ -1154,8 +1160,8 @@ class BaseMindooDB {
1154
1160
  */
1155
1161
  async getExtractionSetup() {
1156
1162
  try {
1157
- const doc = await this.getDocument(types_4.DB_SETUP_DOC_ID);
1158
- return ((0, types_6.sanitizeExtractionConfig)(doc.getData()[types_6.EXTRACTION_SETUP_FIELD]) ?? null);
1163
+ const doc = await this.getDocument(types_5.DB_SETUP_DOC_ID);
1164
+ return ((0, types_7.sanitizeExtractionConfig)(doc.getData()[types_7.EXTRACTION_SETUP_FIELD]) ?? null);
1159
1165
  }
1160
1166
  catch (error) {
1161
1167
  if (error instanceof errors_1.DocumentNotFoundError ||
@@ -1175,19 +1181,19 @@ class BaseMindooDB {
1175
1181
  * without producing changefeed/sync churn.
1176
1182
  */
1177
1183
  async setExtractionSetup(config) {
1178
- const sanitized = config === null ? null : ((0, types_6.sanitizeExtractionConfig)(config) ?? {});
1184
+ const sanitized = config === null ? null : ((0, types_7.sanitizeExtractionConfig)(config) ?? {});
1179
1185
  const current = await this.getExtractionSetup();
1180
1186
  if (JSON.stringify(current) === JSON.stringify(sanitized)) {
1181
1187
  return;
1182
1188
  }
1183
- const doc = await this.createDocument({ id: types_4.DB_SETUP_DOC_ID });
1189
+ const doc = await this.createDocument({ id: types_5.DB_SETUP_DOC_ID });
1184
1190
  await this.changeDoc(doc, (d) => {
1185
1191
  const data = d.getData();
1186
1192
  if (sanitized === null) {
1187
- delete data[types_6.EXTRACTION_SETUP_FIELD];
1193
+ delete data[types_7.EXTRACTION_SETUP_FIELD];
1188
1194
  }
1189
1195
  else {
1190
- data[types_6.EXTRACTION_SETUP_FIELD] = sanitized;
1196
+ data[types_7.EXTRACTION_SETUP_FIELD] = sanitized;
1191
1197
  }
1192
1198
  });
1193
1199
  }
@@ -1255,12 +1261,12 @@ class BaseMindooDB {
1255
1261
  this.store.getId() === types_1.USER_DIRECTORY_DB_ID) {
1256
1262
  return;
1257
1263
  }
1258
- const hasSetupChange = changes.some((change) => change.docId === types_4.DB_SETUP_DOC_ID);
1264
+ const hasSetupChange = changes.some((change) => change.docId === types_5.DB_SETUP_DOC_ID);
1259
1265
  if (!hasSetupChange && this.fulltextSetupProbed) {
1260
1266
  return;
1261
1267
  }
1262
1268
  this.fulltextSetupProbed = true;
1263
- void this.activateFulltextFromSetupDoc();
1269
+ this.backgroundTasks.run(this.activateFulltextFromSetupDoc());
1264
1270
  }
1265
1271
  /** Create the full-text index when the setup document enables one. */
1266
1272
  async activateFulltextFromSetupDoc() {
@@ -1290,7 +1296,7 @@ class BaseMindooDB {
1290
1296
  return;
1291
1297
  }
1292
1298
  this.fulltextAutoUpdateRunning = true;
1293
- void (async () => {
1299
+ this.backgroundTasks.run((async () => {
1294
1300
  try {
1295
1301
  do {
1296
1302
  this.fulltextAutoUpdatePending = false;
@@ -1304,7 +1310,7 @@ class BaseMindooDB {
1304
1310
  finally {
1305
1311
  this.fulltextAutoUpdateRunning = false;
1306
1312
  }
1307
- })();
1313
+ })());
1308
1314
  }
1309
1315
  // ---------------------------------------------------------------------------
1310
1316
  // Change notifications (reactive support)
@@ -1325,6 +1331,29 @@ class BaseMindooDB {
1325
1331
  this.changeListeners.delete(listener);
1326
1332
  };
1327
1333
  }
1334
+ /**
1335
+ * Resolve once this database has no background work left: no coalesced
1336
+ * change event waiting for its timer and no auto-follow catch-up or setup
1337
+ * probe in flight (including work those start). Returns whether there was
1338
+ * anything to wait for.
1339
+ *
1340
+ * @internal Shutdown and test hook; not part of the {@link MindooDB}
1341
+ * interface.
1342
+ */
1343
+ async whenBackgroundIdle() {
1344
+ let waited = false;
1345
+ for (;;) {
1346
+ if (this.changeNotifyTimer !== null) {
1347
+ waited = true;
1348
+ await new Promise((resolve) => setTimeout(resolve, 0));
1349
+ continue;
1350
+ }
1351
+ if (!(await this.backgroundTasks.settle())) {
1352
+ return waited;
1353
+ }
1354
+ waited = true;
1355
+ }
1356
+ }
1328
1357
  /** Record a document transition for the coalesced change event. */
1329
1358
  noteChangeForListeners(docId, lastModified, isDeleted) {
1330
1359
  if (this.changeListeners.size === 0) {
@@ -2539,7 +2568,7 @@ class BaseMindooDB {
2539
2568
  return;
2540
2569
  }
2541
2570
  this.summarySetupProbed = true;
2542
- void (async () => {
2571
+ this.backgroundTasks.run((async () => {
2543
2572
  try {
2544
2573
  const config = await this.getSummarySetup();
2545
2574
  if (config === null || this.summaryStore) {
@@ -2554,7 +2583,7 @@ class BaseMindooDB {
2554
2583
  catch (error) {
2555
2584
  this.logger.warn(`Time-travel summary activation failed: ${error}`);
2556
2585
  }
2557
- })();
2586
+ })());
2558
2587
  }
2559
2588
  /**
2560
2589
  * Open-time probe for full-text auto-activation. Without it, a
@@ -2580,7 +2609,7 @@ class BaseMindooDB {
2580
2609
  return;
2581
2610
  }
2582
2611
  this.fulltextSetupProbed = true;
2583
- void this.activateFulltextFromSetupDoc();
2612
+ this.backgroundTasks.run(this.activateFulltextFromSetupDoc());
2584
2613
  }
2585
2614
  /**
2586
2615
  * Compute the current tenant KeyBag fingerprint, or `null` when the
@@ -7478,8 +7507,7 @@ class BaseMindooDB {
7478
7507
  try {
7479
7508
  newDoc = this.runChangeWithOutdatedDocRecovery(internalDoc, (doc) => {
7480
7509
  if (patch.baseHeads && patch.baseHeads.length > 0) {
7481
- const result = automerge_adapter_1.Automerge.changeAt(doc, patch.baseHeads, applySpans);
7482
- return result.newDoc;
7510
+ return this.changeOnForkAt(doc, patch.baseHeads, applySpans);
7483
7511
  }
7484
7512
  return automerge_adapter_1.Automerge.change(doc, applySpans);
7485
7513
  });
@@ -7505,6 +7533,120 @@ class BaseMindooDB {
7505
7533
  data: wrapped.getData(),
7506
7534
  };
7507
7535
  }
7536
+ async applyDocumentUpdate(doc, update) {
7537
+ this.assertWritable("applyDocumentUpdate");
7538
+ const docId = doc.getId();
7539
+ let internalDoc = this.getCachedDocument(docId);
7540
+ if (!internalDoc) {
7541
+ const loadedDoc = await this.loadDocumentInternal(docId);
7542
+ if (!loadedDoc) {
7543
+ throw new errors_1.DocumentNotFoundError(docId);
7544
+ }
7545
+ internalDoc = loadedDoc;
7546
+ }
7547
+ if (internalDoc.isDeleted) {
7548
+ throw new errors_1.DocumentDeletedError(docId);
7549
+ }
7550
+ const baseHeads = (0, types_2.commonUpdateBaseHeads)(update);
7551
+ if (baseHeads === null) {
7552
+ throw new Error("applyDocumentUpdate: the parts of one update must share their baseHeads");
7553
+ }
7554
+ const hasFields = Object.keys(update.set ?? {}).length > 0 || (update.unset?.length ?? 0) > 0;
7555
+ if (update.json)
7556
+ this.validateJsonPatch(update.json);
7557
+ for (const patch of update.text ?? [])
7558
+ this.validateTextPatch(patch);
7559
+ for (const patch of update.richText ?? [])
7560
+ this.validateRichTextPatch(patch);
7561
+ for (const patch of update.richTextSteps ?? [])
7562
+ this.validateRichTextStepPatch(patch);
7563
+ // top-level fields go through changeDoc elsewhere, which checks sealed access
7564
+ if (hasFields)
7565
+ await this.assertCurrentSealedAccess(internalDoc);
7566
+ const now = (0, timeSource_1.semanticNow)();
7567
+ const headsBeforeChange = automerge_adapter_1.Automerge.getHeads(internalDoc.doc);
7568
+ const applyAll = (automergeDoc) => {
7569
+ for (const [key, value] of Object.entries(update.set ?? {})) {
7570
+ if (value !== undefined) {
7571
+ automergeDoc[key] = this.hydrateTypedValues(value);
7572
+ }
7573
+ }
7574
+ for (const key of update.unset ?? []) {
7575
+ delete automergeDoc[key];
7576
+ }
7577
+ if (update.json)
7578
+ this.applyJsonPatchOperations(automergeDoc, update.json);
7579
+ for (const patch of update.text ?? [])
7580
+ this.applyTextEditsInChange(automergeDoc, patch);
7581
+ for (const patch of update.richText ?? [])
7582
+ this.applyRichTextSpansInChange(automergeDoc, patch, docId);
7583
+ for (const patch of update.richTextSteps ?? [])
7584
+ this.applyRichTextStepsInChange(automergeDoc, patch);
7585
+ automergeDoc._lastModified = now;
7586
+ };
7587
+ let newDoc;
7588
+ try {
7589
+ newDoc = this.runChangeWithOutdatedDocRecovery(internalDoc, (current) => baseHeads
7590
+ ? this.changeOnForkAt(current, baseHeads, applyAll)
7591
+ : automerge_adapter_1.Automerge.change(current, applyAll));
7592
+ }
7593
+ catch (error) {
7594
+ this.logger.error(`Error applying document update for document ${docId}:`, error);
7595
+ throw error;
7596
+ }
7597
+ await this.persistDocumentChange({
7598
+ internalDoc,
7599
+ newDoc,
7600
+ now,
7601
+ headsBeforeChange,
7602
+ useCustomKey: false,
7603
+ successMessage: "document updated",
7604
+ });
7605
+ const wrapped = this.wrapDocument(internalDoc);
7606
+ return {
7607
+ doc: wrapped,
7608
+ heads: wrapped.getHeads(),
7609
+ data: wrapped.getData(),
7610
+ };
7611
+ }
7612
+ /** The edits of one text patch, inside a change. */
7613
+ applyTextEditsInChange(automergeDoc, patch) {
7614
+ this.ensureTextPath(automergeDoc, patch.path);
7615
+ for (const edit of patch.edits) {
7616
+ automerge_adapter_1.Automerge.splice(automergeDoc, patch.path, edit.index, edit.deleteCount, edit.insert ?? "");
7617
+ }
7618
+ }
7619
+ /** One rich-text snapshot (or sequence), inside a change, with the reset-and-retry recovery. */
7620
+ applyRichTextSpansInChange(automergeDoc, patch, docId) {
7621
+ this.ensureRichTextPath(automergeDoc, patch.path);
7622
+ const spansSequence = patch.spansSequence ?? (patch.spans ? [patch.spans] : []);
7623
+ for (const spans of spansSequence) {
7624
+ const revivedSpans = this.reviveRichTextSpans(spans);
7625
+ try {
7626
+ automerge_adapter_1.Automerge.updateSpans(automergeDoc, patch.path, revivedSpans, patch.updateSpansConfig);
7627
+ }
7628
+ catch (error) {
7629
+ if (!this.isRichTextUpdateSpansBoundsError(error)) {
7630
+ throw error;
7631
+ }
7632
+ this.logger.warn(`updateSpans failed for document ${docId} at '${patch.path.map(String).join(".")}', resetting rich-text field and applying snapshot from scratch`, error);
7633
+ this.setJsonValueAtPath(automergeDoc, patch.path, "");
7634
+ automerge_adapter_1.Automerge.updateSpans(automergeDoc, patch.path, revivedSpans, patch.updateSpansConfig);
7635
+ }
7636
+ }
7637
+ }
7638
+ /** Positional rich-text steps (splices and marks), inside a change. */
7639
+ applyRichTextStepsInChange(automergeDoc, patch) {
7640
+ this.ensureRichTextPath(automergeDoc, patch.path);
7641
+ for (const step of patch.steps) {
7642
+ automerge_adapter_1.Automerge.splice(automergeDoc, patch.path, step.index, step.deleteCount, step.insert ?? "");
7643
+ for (const markRange of step.marks ?? []) {
7644
+ for (const [name, value] of Object.entries(markRange.marks)) {
7645
+ automerge_adapter_1.Automerge.mark(automergeDoc, patch.path, { start: markRange.index, end: markRange.index + markRange.length, expand: "none" }, name, this.reviveRichTextValue(value));
7646
+ }
7647
+ }
7648
+ }
7649
+ }
7508
7650
  async applyRichTextStepsPatch(doc, patch) {
7509
7651
  this.assertWritable("applyRichTextStepsPatch");
7510
7652
  const docId = doc.getId();
@@ -7904,6 +8046,16 @@ class BaseMindooDB {
7904
8046
  // incrementCounter() calls, applied in call order after the field
7905
8047
  // assignments so they can target counters assigned in the same callback.
7906
8048
  const pendingCounterIncrements = [];
8049
+ // Assignments and deletions are applied before the increments, so an
8050
+ // assignment or deletion of a field supersedes the increments queued on it
8051
+ // (or below it) earlier in the callback; later increments still apply.
8052
+ const dropPendingCounterIncrements = (field) => {
8053
+ for (let i = pendingCounterIncrements.length - 1; i >= 0; i--) {
8054
+ if (pendingCounterIncrements[i].path[0] === field) {
8055
+ pendingCounterIncrements.splice(i, 1);
8056
+ }
8057
+ }
8058
+ };
7907
8059
  // Reference to db for closures
7908
8060
  const db = this;
7909
8061
  // Guard flag to prevent operations after callback completes
@@ -7938,6 +8090,7 @@ class BaseMindooDB {
7938
8090
  set: (target, prop, value) => {
7939
8091
  throwIfCallbackInactive("set property");
7940
8092
  if (typeof prop === "string") {
8093
+ dropPendingCounterIncrements(prop);
7941
8094
  // If this property was marked for deletion, remove it from deletions
7942
8095
  pendingDeletions.delete(prop);
7943
8096
  // Track the change
@@ -7950,6 +8103,7 @@ class BaseMindooDB {
7950
8103
  deleteProperty: (target, prop) => {
7951
8104
  throwIfCallbackInactive("delete property");
7952
8105
  if (typeof prop === "string") {
8106
+ dropPendingCounterIncrements(prop);
7953
8107
  // Mark for deletion
7954
8108
  pendingDeletions.add(prop);
7955
8109
  // Remove from pending changes if it was there
@@ -8414,7 +8568,10 @@ class BaseMindooDB {
8414
8568
  return value;
8415
8569
  }
8416
8570
  if (Array.isArray(value)) {
8417
- return value.map((entry) => this.hydrateTypedValues(entry, depth + 1));
8571
+ // Array.from, not map: map keeps the realm of an array that came from
8572
+ // another realm (structuredClone in a VM context, another frame), and
8573
+ // Automerge only assigns arrays that pass `instanceof Array`.
8574
+ return Array.from(value, (entry) => this.hydrateTypedValues(entry, depth + 1));
8418
8575
  }
8419
8576
  const record = value;
8420
8577
  if (Object.prototype.hasOwnProperty.call(record, values_1.MINDOO_VALUE_TAG)) {
@@ -8851,6 +9008,34 @@ class BaseMindooDB {
8851
9008
  * {@link applyNewEntriesToCachedDocument} (line ~6606) for the sync
8852
9009
  * pipeline; this helper centralizes it for the patch / changeDoc paths.
8853
9010
  */
9011
+ /**
9012
+ * `Automerge.changeAt(doc, baseHeads, apply)` for changes that call
9013
+ * `updateSpans`. Automerge 3.5's `updateSpans` inside `changeAt` diffs
9014
+ * against the text as of `baseHeads` but applies the resulting indexes to the
9015
+ * current text, so once a concurrent edit of that text is in the document it
9016
+ * throws "out of bounds" (and the reset-and-retry recovery would then drop
9017
+ * the concurrent edit) or edits the wrong characters. `splice`, `updateText`
9018
+ * and `mark` are not affected.
9019
+ *
9020
+ * The change is made on a fork that stands exactly at `baseHeads` and then
9021
+ * merged back: the same causal result `changeAt` would produce. The fork
9022
+ * writes as this replica's actor, unless that actor changed the document
9023
+ * after `baseHeads` (its next sequence number is taken then); in that case a
9024
+ * fresh actor writes the one change.
9025
+ */
9026
+ changeOnForkAt(doc, baseHeads, apply) {
9027
+ const current = automerge_adapter_1.Automerge.getHeads(doc);
9028
+ if (current.length === baseHeads.length &&
9029
+ current.every((head) => baseHeads.includes(head))) {
9030
+ return automerge_adapter_1.Automerge.change(doc, apply);
9031
+ }
9032
+ const base = automerge_adapter_1.Automerge.view(doc, baseHeads);
9033
+ const actor = automerge_adapter_1.Automerge.getActorId(doc);
9034
+ const ownChangeSince = automerge_adapter_1.Automerge.getChanges(base, doc).some((change) => automerge_adapter_1.Automerge.decodeChange(change).actor === actor);
9035
+ const fork = automerge_adapter_1.Automerge.clone(base, ownChangeSince ? undefined : { actor });
9036
+ const changed = automerge_adapter_1.Automerge.change(fork, apply);
9037
+ return automerge_adapter_1.Automerge.merge(doc, changed);
9038
+ }
8854
9039
  runChangeWithOutdatedDocRecovery(internalDoc, apply) {
8855
9040
  try {
8856
9041
  return apply(internalDoc.doc);
@@ -8942,19 +9127,31 @@ class BaseMindooDB {
8942
9127
  if (!changeBytes) {
8943
9128
  throw new Error("Failed to get change bytes from Automerge document");
8944
9129
  }
8945
- await this.persistAutomergeDocumentChanges({
8946
- internalDoc,
8947
- newDoc,
8948
- now,
8949
- changeBytesList: [changeBytes],
8950
- useCustomKey,
8951
- signingKeyPair,
8952
- signingKeyPassword,
8953
- successMessage,
8954
- attachmentIds,
8955
- bypassPrecheck,
8956
- recipients: options.recipients,
8957
- });
9130
+ try {
9131
+ await this.persistAutomergeDocumentChanges({
9132
+ internalDoc,
9133
+ newDoc,
9134
+ now,
9135
+ changeBytesList: [changeBytes],
9136
+ useCustomKey,
9137
+ signingKeyPair,
9138
+ signingKeyPassword,
9139
+ successMessage,
9140
+ attachmentIds,
9141
+ bypassPrecheck,
9142
+ recipients: options.recipients,
9143
+ });
9144
+ }
9145
+ catch (error) {
9146
+ // Automerge.change() outdated the cached document and moved its WASM
9147
+ // handle to `newDoc`, so reads of the cache slot would return the change
9148
+ // that was just refused (denied precheck, wrong signing password, failed
9149
+ // store write). Fork the cache back to the heads the change started from.
9150
+ if (internalDoc.doc !== newDoc && headsBeforeChange) {
9151
+ internalDoc.doc = automerge_adapter_1.Automerge.clone(automerge_adapter_1.Automerge.view(newDoc, headsBeforeChange));
9152
+ }
9153
+ throw error;
9154
+ }
8958
9155
  }
8959
9156
  async persistAutomergeDocumentChanges(options) {
8960
9157
  const { internalDoc, newDoc, now, changeBytesList, headsBeforeChange, useCustomKey, signingKeyPair, signingKeyPassword, successMessage, attachmentIds, bypassPrecheck, recipients, } = options;
@@ -9221,7 +9418,7 @@ class BaseMindooDB {
9221
9418
  return await this.withRateLimitBackoff("entry prefetch", () => this.store.getEntries(ids));
9222
9419
  }
9223
9420
  catch (error) {
9224
- if (networkErrorType(error) !== types_3.NetworkErrorType.PAYLOAD_TOO_LARGE ||
9421
+ if (networkErrorType(error) !== types_4.NetworkErrorType.PAYLOAD_TOO_LARGE ||
9225
9422
  ids.length <= 1) {
9226
9423
  throw error;
9227
9424
  }
@@ -10550,24 +10747,19 @@ class BaseMindooDB {
10550
10747
  * Whether access control is active for this tenant and should gate
10551
10748
  * materialization. Admin-only databases (incl. the directory itself) are
10552
10749
  * always exempt — admin is the root of trust, and exempting the directory DB
10553
- * also avoids re-entrancy. Cached briefly to keep the load fast-path cheap.
10554
- * Fails closed-to-disabled on any directory error so document loads never
10555
- * break due to a transient directory issue (Tier 2 only gates honest clients).
10750
+ * also avoids re-entrancy. Not cached by wall-clock time: a policy change must
10751
+ * take effect on the very next write or load, independent of machine speed.
10556
10752
  */
10557
10753
  async isAclEnforced() {
10558
10754
  if (this._isAdminOnlyDb)
10559
10755
  return false;
10560
- const now = Date.now();
10561
- if (this.aclActiveCache && now - this.aclActiveCache.at < 3000) {
10562
- return this.aclActiveCache.value;
10563
- }
10564
10756
  try {
10565
10757
  const directory = await this.tenant.openDirectory();
10566
10758
  let value = false;
10567
10759
  if (typeof directory.isAccessControlActive === "function") {
10568
10760
  value = await directory.isAccessControlActive();
10569
10761
  }
10570
- this.aclActiveCache = { value, at: now };
10762
+ this.aclActiveLastKnown = value;
10571
10763
  return value;
10572
10764
  }
10573
10765
  catch (error) {
@@ -10576,9 +10768,9 @@ class BaseMindooDB {
10576
10768
  // materialization gate entirely). Reuse the last known verdict if we have
10577
10769
  // one; otherwise assume enforced so the materialization path governs the
10578
10770
  // load and retries once the directory is reachable again.
10579
- if (this.aclActiveCache) {
10580
- this.logger.debug(`[ACL] active-check failed, reusing last known verdict (${this.aclActiveCache.value}): ${error}`);
10581
- return this.aclActiveCache.value;
10771
+ if (this.aclActiveLastKnown !== null) {
10772
+ this.logger.debug(`[ACL] active-check failed, reusing last known verdict (${this.aclActiveLastKnown}): ${error}`);
10773
+ return this.aclActiveLastKnown;
10582
10774
  }
10583
10775
  this.logger.warn(`[ACL] active-check failed with no prior verdict, assuming enforced (fail closed): ${error}`);
10584
10776
  return true;
@@ -12531,15 +12723,15 @@ class BaseMindooDB {
12531
12723
  * Resolve a sync target to a ContentAddressedStore.
12532
12724
  * Accepts either a raw store or a MindooDB instance (calls getStore()).
12533
12725
  */
12534
- getStoreForKind(storeKind = types_2.StoreKind.docs) {
12535
- return storeKind === types_2.StoreKind.attachments
12726
+ getStoreForKind(storeKind = types_3.StoreKind.docs) {
12727
+ return storeKind === types_3.StoreKind.attachments
12536
12728
  ? this.attachmentStore
12537
12729
  : this.store;
12538
12730
  }
12539
- resolveStore(remote, storeKind = types_2.StoreKind.docs) {
12731
+ resolveStore(remote, storeKind = types_3.StoreKind.docs) {
12540
12732
  if ("getStore" in remote &&
12541
12733
  typeof remote.getStore === "function") {
12542
- return storeKind === types_2.StoreKind.attachments
12734
+ return storeKind === types_3.StoreKind.attachments
12543
12735
  ? remote.getAttachmentStore()
12544
12736
  : remote.getStore();
12545
12737
  }
@@ -12787,7 +12979,7 @@ class BaseMindooDB {
12787
12979
  */
12788
12980
  async pullChangesFrom(remote, options) {
12789
12981
  this.assertWritable("pullChangesFrom");
12790
- const storeKind = options?.storeKind ?? types_2.StoreKind.docs;
12982
+ const storeKind = options?.storeKind ?? types_3.StoreKind.docs;
12791
12983
  const remoteStore = this.resolveStore(remote, storeKind);
12792
12984
  // Entries of documents purged by an admin-signed request are refused on the
12793
12985
  // way in, so a replica that has not executed the purge yet cannot hand the
@@ -12809,7 +13001,7 @@ class BaseMindooDB {
12809
13001
  // summary buffer / full-text index materialize and extract documents
12810
13002
  // while the next page is still on the wire, instead of all at once
12811
13003
  // after the transfer (see docs/adhoc-queries.md, "Cold start").
12812
- const syncResult = await this.syncEntriesFromStore(remoteStore, localStore, options, storeKind === types_2.StoreKind.docs
13004
+ const syncResult = await this.syncEntriesFromStore(remoteStore, localStore, options, storeKind === types_3.StoreKind.docs
12813
13005
  ? () => this.syncStoreChanges()
12814
13006
  : undefined);
12815
13007
  const transferredBytes = syncResult.transferredBytes ?? 0;
@@ -12825,7 +13017,7 @@ class BaseMindooDB {
12825
13017
  }
12826
13018
  if (syncResult.transferred === 0) {
12827
13019
  this.logger.debug(`No new entries to pull`);
12828
- if (storeKind === types_2.StoreKind.docs) {
13020
+ if (storeKind === types_3.StoreKind.docs) {
12829
13021
  await this.reconcileAfterDocsPull();
12830
13022
  }
12831
13023
  return {
@@ -12853,7 +13045,7 @@ class BaseMindooDB {
12853
13045
  cancelled: true,
12854
13046
  };
12855
13047
  }
12856
- if (storeKind === types_2.StoreKind.docs) {
13048
+ if (storeKind === types_3.StoreKind.docs) {
12857
13049
  await this.syncStoreChanges();
12858
13050
  await this.reconcileAfterDocsPull();
12859
13051
  }
@@ -12922,7 +13114,7 @@ class BaseMindooDB {
12922
13114
  */
12923
13115
  async pushChangesTo(remote, options) {
12924
13116
  this.assertWritable("pushChangesTo");
12925
- const storeKind = options?.storeKind ?? types_2.StoreKind.docs;
13117
+ const storeKind = options?.storeKind ?? types_3.StoreKind.docs;
12926
13118
  const localStore = this.getStoreForKind(storeKind);
12927
13119
  const remoteStore = this.resolveStore(remote, storeKind);
12928
13120
  if (localStore.getId() !== remoteStore.getId() ||