mindoodb 0.0.72 → 0.0.74

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 (73) hide show
  1. package/dist/browser/index.mjs +2 -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 +60 -7
  7. package/dist/core/BaseMindooDB.d.ts.map +1 -1
  8. package/dist/core/BaseMindooDB.js +280 -66
  9. package/dist/core/BaseMindooDB.js.map +1 -1
  10. package/dist/core/BaseMindooTenant.d.ts +46 -12
  11. package/dist/core/BaseMindooTenant.d.ts.map +1 -1
  12. package/dist/core/BaseMindooTenant.js +133 -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/InMemoryContentAddressedStore.d.ts +11 -0
  19. package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts.map +1 -1
  20. package/dist/core/appendonlystores/InMemoryContentAddressedStore.js +44 -18
  21. package/dist/core/appendonlystores/InMemoryContentAddressedStore.js.map +1 -1
  22. package/dist/core/appendonlystores/purgeDocumentHistory.d.ts +52 -0
  23. package/dist/core/appendonlystores/purgeDocumentHistory.d.ts.map +1 -0
  24. package/dist/core/appendonlystores/purgeDocumentHistory.js +107 -0
  25. package/dist/core/appendonlystores/purgeDocumentHistory.js.map +1 -0
  26. package/dist/core/appendonlystores/purgedDocumentGuard.d.ts +33 -0
  27. package/dist/core/appendonlystores/purgedDocumentGuard.d.ts.map +1 -0
  28. package/dist/core/appendonlystores/purgedDocumentGuard.js +78 -0
  29. package/dist/core/appendonlystores/purgedDocumentGuard.js.map +1 -0
  30. package/dist/core/cache/CacheManager.d.ts +4 -1
  31. package/dist/core/cache/CacheManager.d.ts.map +1 -1
  32. package/dist/core/cache/CacheManager.js +47 -19
  33. package/dist/core/cache/CacheManager.js.map +1 -1
  34. package/dist/core/indexing/fulltext/DocumentFullTextIndex.d.ts +16 -1
  35. package/dist/core/indexing/fulltext/DocumentFullTextIndex.d.ts.map +1 -1
  36. package/dist/core/indexing/fulltext/DocumentFullTextIndex.js +86 -14
  37. package/dist/core/indexing/fulltext/DocumentFullTextIndex.js.map +1 -1
  38. package/dist/core/indexing/summary/DocumentSummaryStore.d.ts +2 -0
  39. package/dist/core/indexing/summary/DocumentSummaryStore.d.ts.map +1 -1
  40. package/dist/core/indexing/summary/DocumentSummaryStore.js +19 -5
  41. package/dist/core/indexing/summary/DocumentSummaryStore.js.map +1 -1
  42. package/dist/core/types.d.ts +57 -0
  43. package/dist/core/types.d.ts.map +1 -1
  44. package/dist/core/types.js +22 -0
  45. package/dist/core/types.js.map +1 -1
  46. package/dist/core/utils/singleFlight.d.ts +21 -0
  47. package/dist/core/utils/singleFlight.d.ts.map +1 -0
  48. package/dist/core/utils/singleFlight.js +47 -0
  49. package/dist/core/utils/singleFlight.js.map +1 -0
  50. package/dist/core/utils/timeSource.d.ts +3 -1
  51. package/dist/core/utils/timeSource.d.ts.map +1 -1
  52. package/dist/core/utils/timeSource.js +4 -0
  53. package/dist/core/utils/timeSource.js.map +1 -1
  54. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts +13 -0
  55. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts.map +1 -1
  56. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js +26 -2
  57. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js.map +1 -1
  58. package/dist/node/server/MindooDBServer.d.ts.map +1 -1
  59. package/dist/node/server/MindooDBServer.js +2 -0
  60. package/dist/node/server/MindooDBServer.js.map +1 -1
  61. package/dist/node/server/TenantManager.d.ts +11 -0
  62. package/dist/node/server/TenantManager.d.ts.map +1 -1
  63. package/dist/node/server/TenantManager.js +40 -9
  64. package/dist/node/server/TenantManager.js.map +1 -1
  65. package/dist/node/server/peer/ClusterManager.d.ts +4 -0
  66. package/dist/node/server/peer/ClusterManager.d.ts.map +1 -1
  67. package/dist/node/server/peer/ClusterManager.js +2 -0
  68. package/dist/node/server/peer/ClusterManager.js.map +1 -1
  69. package/dist/node/server/peer/PeerReplicator.d.ts +12 -0
  70. package/dist/node/server/peer/PeerReplicator.d.ts.map +1 -1
  71. package/dist/node/server/peer/PeerReplicator.js +19 -1
  72. package/dist/node/server/peer/PeerReplicator.js.map +1 -1
  73. package/package.json +1 -1
@@ -8,13 +8,16 @@ 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");
17
18
  const AttachmentReadPlanner_1 = require("./appendonlystores/AttachmentReadPlanner");
19
+ const purgedDocumentGuard_1 = require("./appendonlystores/purgedDocumentGuard");
20
+ const purgeDocumentHistory_1 = require("./appendonlystores/purgeDocumentHistory");
18
21
  const idGeneration_1 = require("./utils/idGeneration");
19
22
  const timeSource_1 = require("./utils/timeSource");
20
23
  const errors_1 = require("./errors");
@@ -31,13 +34,14 @@ const builtinDbInvariants_1 = require("./builtinDbInvariants");
31
34
  const recipients_1 = require("./userkeys/recipients");
32
35
  const sealedTypes_1 = require("./userkeys/sealedTypes");
33
36
  const DocumentSummaryStore_1 = require("./indexing/summary/DocumentSummaryStore");
34
- const types_4 = require("./indexing/summary/types");
37
+ const types_5 = require("./indexing/summary/types");
35
38
  const DocumentFullTextIndex_1 = require("./indexing/fulltext/DocumentFullTextIndex");
36
- const types_5 = require("./indexing/fulltext/types");
37
- const types_6 = require("./extraction/types");
39
+ const types_6 = require("./indexing/fulltext/types");
40
+ const types_7 = require("./extraction/types");
38
41
  const executeQuery_1 = require("./query/executeQuery");
39
42
  const queryView_1 = require("./query/queryView");
40
43
  const queryLive_1 = require("./query/queryLive");
44
+ const BackgroundTaskTracker_1 = require("./BackgroundTaskTracker");
41
45
  /**
42
46
  * Default chunk size for attachments: 256KB
43
47
  */
@@ -114,7 +118,7 @@ function networkErrorType(error) {
114
118
  * server's `Retry-After` when it asks for a longer wait than our backoff.
115
119
  */
116
120
  function rateLimitRetryDelayMs(error, attempt) {
117
- if (networkErrorType(error) !== types_3.NetworkErrorType.RATE_LIMITED ||
121
+ if (networkErrorType(error) !== types_4.NetworkErrorType.RATE_LIMITED ||
118
122
  attempt >= PREFETCH_RATE_LIMIT_DELAYS_MS.length) {
119
123
  return null;
120
124
  }
@@ -247,10 +251,12 @@ class BaseMindooDB {
247
251
  */
248
252
  this.quarantineLog = new Map();
249
253
  /**
250
- * Short-TTL cache of "is access control active for this tenant?" so the
251
- * 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.
252
258
  */
253
- this.aclActiveCache = null;
259
+ this.aclActiveLastKnown = null;
254
260
  /**
255
261
  * Per-document cache of the original creator's signing public key and the
256
262
  * trusted time of the `doc_create` entry. Used by the client write prechecks
@@ -450,6 +456,8 @@ class BaseMindooDB {
450
456
  this.changeNotifyTimer = null;
451
457
  /** While > 0, timer-based emission is suppressed (e.g. during a sync batch). */
452
458
  this.changeNotificationHolds = 0;
459
+ /** Fire-and-forget work of this database; see {@link whenBackgroundIdle}. */
460
+ this.backgroundTasks = new BackgroundTaskTracker_1.BackgroundTaskTracker();
453
461
  // ---------------------------------------------------------------------------
454
462
  // Summary auto-follow (see setSummaryAutoUpdateEnabled): keeps the summary
455
463
  // buffer current as changes arrive (local writes and sync batches), so the
@@ -565,11 +573,11 @@ class BaseMindooDB {
565
573
  if (store.getId() !== attachmentStore.getId()) {
566
574
  throw new Error(`[BaseMindooDB] Expected document and attachment stores to share dbId but received ${store.getId()} and ${attachmentStore.getId()}`);
567
575
  }
568
- if (store.getStoreKind() !== types_2.StoreKind.docs) {
569
- 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()}`);
570
578
  }
571
- if (attachmentStore.getStoreKind() !== types_2.StoreKind.attachments) {
572
- 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()}`);
573
581
  }
574
582
  this.tenant = tenant;
575
583
  this.store = store;
@@ -896,7 +904,7 @@ class BaseMindooDB {
896
904
  this.dirtyDocIds.clear();
897
905
  this.cacheMetaDirty = false;
898
906
  this.summaryStore?.discardDirtyState();
899
- this.fulltextIndex?.clearDirty();
907
+ this.fulltextIndex?.discardDirtyState();
900
908
  void cacheManager.deregister(this);
901
909
  if (this.summaryStore) {
902
910
  void cacheManager.deregister(this.summaryStore);
@@ -937,8 +945,8 @@ class BaseMindooDB {
937
945
  */
938
946
  async getSummarySetup() {
939
947
  try {
940
- const doc = await this.getDocument(types_4.DB_SETUP_DOC_ID);
941
- 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;
942
950
  }
943
951
  catch (error) {
944
952
  if (error instanceof errors_1.DocumentNotFoundError ||
@@ -960,14 +968,14 @@ class BaseMindooDB {
960
968
  * backfill — locally and, after sync, on every other replica.
961
969
  */
962
970
  async setSummarySetup(config) {
963
- 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 });
964
972
  await this.changeDoc(doc, (d) => {
965
973
  const data = d.getData();
966
974
  if (config === null) {
967
- delete data[types_4.SUMMARY_SETUP_FIELD];
975
+ delete data[types_5.SUMMARY_SETUP_FIELD];
968
976
  }
969
977
  else {
970
- data[types_4.SUMMARY_SETUP_FIELD] = (0, types_4.sanitizeSummaryConfig)(config) ?? {};
978
+ data[types_5.SUMMARY_SETUP_FIELD] = (0, types_5.sanitizeSummaryConfig)(config) ?? {};
971
979
  }
972
980
  });
973
981
  }
@@ -1011,12 +1019,12 @@ class BaseMindooDB {
1011
1019
  this.store.getId() === types_1.USER_DIRECTORY_DB_ID) {
1012
1020
  return;
1013
1021
  }
1014
- 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);
1015
1023
  if (!hasSetupChange && this.summarySetupProbed) {
1016
1024
  return;
1017
1025
  }
1018
1026
  this.summarySetupProbed = true;
1019
- void this.activateSummaryFromSetupDoc();
1027
+ this.backgroundTasks.run(this.activateSummaryFromSetupDoc());
1020
1028
  }
1021
1029
  /** Create the summary store when the setup document configures one. */
1022
1030
  async activateSummaryFromSetupDoc() {
@@ -1047,7 +1055,7 @@ class BaseMindooDB {
1047
1055
  return;
1048
1056
  }
1049
1057
  this.summaryAutoUpdateRunning = true;
1050
- void (async () => {
1058
+ this.backgroundTasks.run((async () => {
1051
1059
  try {
1052
1060
  do {
1053
1061
  this.summaryAutoUpdatePending = false;
@@ -1061,7 +1069,7 @@ class BaseMindooDB {
1061
1069
  finally {
1062
1070
  this.summaryAutoUpdateRunning = false;
1063
1071
  }
1064
- })();
1072
+ })());
1065
1073
  }
1066
1074
  // ---------------------------------------------------------------------------
1067
1075
  // Full-text index (see docs/fulltext-search.md)
@@ -1101,8 +1109,8 @@ class BaseMindooDB {
1101
1109
  */
1102
1110
  async getFulltextSetup() {
1103
1111
  try {
1104
- const doc = await this.getDocument(types_4.DB_SETUP_DOC_ID);
1105
- 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);
1106
1114
  }
1107
1115
  catch (error) {
1108
1116
  if (error instanceof errors_1.DocumentNotFoundError ||
@@ -1126,19 +1134,19 @@ class BaseMindooDB {
1126
1134
  * resumable backfill — locally and, after sync, on every other replica.
1127
1135
  */
1128
1136
  async setFulltextSetup(config) {
1129
- const sanitized = config === null ? null : ((0, types_5.sanitizeFulltextConfig)(config) ?? {});
1137
+ const sanitized = config === null ? null : ((0, types_6.sanitizeFulltextConfig)(config) ?? {});
1130
1138
  const current = await this.getFulltextSetup();
1131
1139
  if (JSON.stringify(current) === JSON.stringify(sanitized)) {
1132
1140
  return;
1133
1141
  }
1134
- 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 });
1135
1143
  await this.changeDoc(doc, (d) => {
1136
1144
  const data = d.getData();
1137
1145
  if (sanitized === null) {
1138
- delete data[types_5.FULLTEXT_SETUP_FIELD];
1146
+ delete data[types_6.FULLTEXT_SETUP_FIELD];
1139
1147
  }
1140
1148
  else {
1141
- data[types_5.FULLTEXT_SETUP_FIELD] = sanitized;
1149
+ data[types_6.FULLTEXT_SETUP_FIELD] = sanitized;
1142
1150
  }
1143
1151
  });
1144
1152
  }
@@ -1152,8 +1160,8 @@ class BaseMindooDB {
1152
1160
  */
1153
1161
  async getExtractionSetup() {
1154
1162
  try {
1155
- const doc = await this.getDocument(types_4.DB_SETUP_DOC_ID);
1156
- 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);
1157
1165
  }
1158
1166
  catch (error) {
1159
1167
  if (error instanceof errors_1.DocumentNotFoundError ||
@@ -1173,19 +1181,19 @@ class BaseMindooDB {
1173
1181
  * without producing changefeed/sync churn.
1174
1182
  */
1175
1183
  async setExtractionSetup(config) {
1176
- const sanitized = config === null ? null : ((0, types_6.sanitizeExtractionConfig)(config) ?? {});
1184
+ const sanitized = config === null ? null : ((0, types_7.sanitizeExtractionConfig)(config) ?? {});
1177
1185
  const current = await this.getExtractionSetup();
1178
1186
  if (JSON.stringify(current) === JSON.stringify(sanitized)) {
1179
1187
  return;
1180
1188
  }
1181
- 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 });
1182
1190
  await this.changeDoc(doc, (d) => {
1183
1191
  const data = d.getData();
1184
1192
  if (sanitized === null) {
1185
- delete data[types_6.EXTRACTION_SETUP_FIELD];
1193
+ delete data[types_7.EXTRACTION_SETUP_FIELD];
1186
1194
  }
1187
1195
  else {
1188
- data[types_6.EXTRACTION_SETUP_FIELD] = sanitized;
1196
+ data[types_7.EXTRACTION_SETUP_FIELD] = sanitized;
1189
1197
  }
1190
1198
  });
1191
1199
  }
@@ -1253,12 +1261,12 @@ class BaseMindooDB {
1253
1261
  this.store.getId() === types_1.USER_DIRECTORY_DB_ID) {
1254
1262
  return;
1255
1263
  }
1256
- 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);
1257
1265
  if (!hasSetupChange && this.fulltextSetupProbed) {
1258
1266
  return;
1259
1267
  }
1260
1268
  this.fulltextSetupProbed = true;
1261
- void this.activateFulltextFromSetupDoc();
1269
+ this.backgroundTasks.run(this.activateFulltextFromSetupDoc());
1262
1270
  }
1263
1271
  /** Create the full-text index when the setup document enables one. */
1264
1272
  async activateFulltextFromSetupDoc() {
@@ -1288,7 +1296,7 @@ class BaseMindooDB {
1288
1296
  return;
1289
1297
  }
1290
1298
  this.fulltextAutoUpdateRunning = true;
1291
- void (async () => {
1299
+ this.backgroundTasks.run((async () => {
1292
1300
  try {
1293
1301
  do {
1294
1302
  this.fulltextAutoUpdatePending = false;
@@ -1302,7 +1310,7 @@ class BaseMindooDB {
1302
1310
  finally {
1303
1311
  this.fulltextAutoUpdateRunning = false;
1304
1312
  }
1305
- })();
1313
+ })());
1306
1314
  }
1307
1315
  // ---------------------------------------------------------------------------
1308
1316
  // Change notifications (reactive support)
@@ -1323,6 +1331,29 @@ class BaseMindooDB {
1323
1331
  this.changeListeners.delete(listener);
1324
1332
  };
1325
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
+ }
1326
1357
  /** Record a document transition for the coalesced change event. */
1327
1358
  noteChangeForListeners(docId, lastModified, isDeleted) {
1328
1359
  if (this.changeListeners.size === 0) {
@@ -2537,7 +2568,7 @@ class BaseMindooDB {
2537
2568
  return;
2538
2569
  }
2539
2570
  this.summarySetupProbed = true;
2540
- void (async () => {
2571
+ this.backgroundTasks.run((async () => {
2541
2572
  try {
2542
2573
  const config = await this.getSummarySetup();
2543
2574
  if (config === null || this.summaryStore) {
@@ -2552,7 +2583,7 @@ class BaseMindooDB {
2552
2583
  catch (error) {
2553
2584
  this.logger.warn(`Time-travel summary activation failed: ${error}`);
2554
2585
  }
2555
- })();
2586
+ })());
2556
2587
  }
2557
2588
  /**
2558
2589
  * Open-time probe for full-text auto-activation. Without it, a
@@ -2578,7 +2609,7 @@ class BaseMindooDB {
2578
2609
  return;
2579
2610
  }
2580
2611
  this.fulltextSetupProbed = true;
2581
- void this.activateFulltextFromSetupDoc();
2612
+ this.backgroundTasks.run(this.activateFulltextFromSetupDoc());
2582
2613
  }
2583
2614
  /**
2584
2615
  * Compute the current tenant KeyBag fingerprint, or `null` when the
@@ -2749,6 +2780,46 @@ class BaseMindooDB {
2749
2780
  }
2750
2781
  }
2751
2782
  }
2783
+ /**
2784
+ * Physically purge a document's history from this database (see
2785
+ * {@link MindooDB.purgeDocumentHistory}).
2786
+ */
2787
+ async purgeDocumentHistory(docId) {
2788
+ this.assertWritable("purgeDocumentHistory");
2789
+ const outcome = await (0, purgeDocumentHistory_1.purgeDocumentHistoryFromStores)({ docsStore: this.store, attachmentStore: this.attachmentStore }, docId);
2790
+ await this.forgetPurgedDocument(docId);
2791
+ // Open time-travel snapshots and persisted snapshot caches hold their own
2792
+ // plaintext copies of the document. Skipped on a repeated purge (callers
2793
+ // re-apply all purge requests on every directory sync), which would
2794
+ // otherwise wipe every snapshot cache each time.
2795
+ if (outcome.docEntriesPurged > 0 || outcome.attachmentChunksPurged > 0) {
2796
+ await this.tenant.forgetPurgedDocumentInSnapshots(this.store.getId(), docId);
2797
+ }
2798
+ return outcome;
2799
+ }
2800
+ /**
2801
+ * Drop everything this instance derived from a document whose store entries
2802
+ * were purged: the L1/L2 caches, summary values and full-text tokens (via
2803
+ * {@link purgeMaterializedDocument}), and the live index entry, which turns
2804
+ * into a deleted tombstone so changefeed consumers and views remove the
2805
+ * document. Safe to call on time-travel snapshots and when nothing is known
2806
+ * about the document.
2807
+ *
2808
+ * @internal Called by {@link purgeDocumentHistory} and the tenant.
2809
+ */
2810
+ async forgetPurgedDocument(docId) {
2811
+ await this.purgeMaterializedDocument(docId);
2812
+ const position = this.getDocIndexPosition(docId);
2813
+ // Already a tombstone (soft-deleted earlier, or purged before): views have
2814
+ // removed it, and bumping it again on every repeated purge would only
2815
+ // churn the changefeed.
2816
+ if (position !== undefined && !this.index[position].isDeleted) {
2817
+ const existing = this.index[position];
2818
+ this.updateIndex(docId, Math.max(existing.lastModified, (0, timeSource_1.semanticNow)()), true, existing.decryptionKeyId, existing.accessState, false, false, true);
2819
+ }
2820
+ this.cacheMetaDirty = true;
2821
+ this.cacheManager?.markDirty();
2822
+ }
2752
2823
  /**
2753
2824
  * Reconcile the in-memory index with the current tenant KeyBag.
2754
2825
  *
@@ -7436,8 +7507,7 @@ class BaseMindooDB {
7436
7507
  try {
7437
7508
  newDoc = this.runChangeWithOutdatedDocRecovery(internalDoc, (doc) => {
7438
7509
  if (patch.baseHeads && patch.baseHeads.length > 0) {
7439
- const result = automerge_adapter_1.Automerge.changeAt(doc, patch.baseHeads, applySpans);
7440
- return result.newDoc;
7510
+ return this.changeOnForkAt(doc, patch.baseHeads, applySpans);
7441
7511
  }
7442
7512
  return automerge_adapter_1.Automerge.change(doc, applySpans);
7443
7513
  });
@@ -7463,6 +7533,120 @@ class BaseMindooDB {
7463
7533
  data: wrapped.getData(),
7464
7534
  };
7465
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
+ }
7466
7650
  async applyRichTextStepsPatch(doc, patch) {
7467
7651
  this.assertWritable("applyRichTextStepsPatch");
7468
7652
  const docId = doc.getId();
@@ -8809,6 +8993,34 @@ class BaseMindooDB {
8809
8993
  * {@link applyNewEntriesToCachedDocument} (line ~6606) for the sync
8810
8994
  * pipeline; this helper centralizes it for the patch / changeDoc paths.
8811
8995
  */
8996
+ /**
8997
+ * `Automerge.changeAt(doc, baseHeads, apply)` for changes that call
8998
+ * `updateSpans`. Automerge 3.5's `updateSpans` inside `changeAt` diffs
8999
+ * against the text as of `baseHeads` but applies the resulting indexes to the
9000
+ * current text, so once a concurrent edit of that text is in the document it
9001
+ * throws "out of bounds" (and the reset-and-retry recovery would then drop
9002
+ * the concurrent edit) or edits the wrong characters. `splice`, `updateText`
9003
+ * and `mark` are not affected.
9004
+ *
9005
+ * The change is made on a fork that stands exactly at `baseHeads` and then
9006
+ * merged back: the same causal result `changeAt` would produce. The fork
9007
+ * writes as this replica's actor, unless that actor changed the document
9008
+ * after `baseHeads` (its next sequence number is taken then); in that case a
9009
+ * fresh actor writes the one change.
9010
+ */
9011
+ changeOnForkAt(doc, baseHeads, apply) {
9012
+ const current = automerge_adapter_1.Automerge.getHeads(doc);
9013
+ if (current.length === baseHeads.length &&
9014
+ current.every((head) => baseHeads.includes(head))) {
9015
+ return automerge_adapter_1.Automerge.change(doc, apply);
9016
+ }
9017
+ const base = automerge_adapter_1.Automerge.view(doc, baseHeads);
9018
+ const actor = automerge_adapter_1.Automerge.getActorId(doc);
9019
+ const ownChangeSince = automerge_adapter_1.Automerge.getChanges(base, doc).some((change) => automerge_adapter_1.Automerge.decodeChange(change).actor === actor);
9020
+ const fork = automerge_adapter_1.Automerge.clone(base, ownChangeSince ? undefined : { actor });
9021
+ const changed = automerge_adapter_1.Automerge.change(fork, apply);
9022
+ return automerge_adapter_1.Automerge.merge(doc, changed);
9023
+ }
8812
9024
  runChangeWithOutdatedDocRecovery(internalDoc, apply) {
8813
9025
  try {
8814
9026
  return apply(internalDoc.doc);
@@ -9179,7 +9391,7 @@ class BaseMindooDB {
9179
9391
  return await this.withRateLimitBackoff("entry prefetch", () => this.store.getEntries(ids));
9180
9392
  }
9181
9393
  catch (error) {
9182
- if (networkErrorType(error) !== types_3.NetworkErrorType.PAYLOAD_TOO_LARGE ||
9394
+ if (networkErrorType(error) !== types_4.NetworkErrorType.PAYLOAD_TOO_LARGE ||
9183
9395
  ids.length <= 1) {
9184
9396
  throw error;
9185
9397
  }
@@ -10508,24 +10720,19 @@ class BaseMindooDB {
10508
10720
  * Whether access control is active for this tenant and should gate
10509
10721
  * materialization. Admin-only databases (incl. the directory itself) are
10510
10722
  * always exempt — admin is the root of trust, and exempting the directory DB
10511
- * also avoids re-entrancy. Cached briefly to keep the load fast-path cheap.
10512
- * Fails closed-to-disabled on any directory error so document loads never
10513
- * break due to a transient directory issue (Tier 2 only gates honest clients).
10723
+ * also avoids re-entrancy. Not cached by wall-clock time: a policy change must
10724
+ * take effect on the very next write or load, independent of machine speed.
10514
10725
  */
10515
10726
  async isAclEnforced() {
10516
10727
  if (this._isAdminOnlyDb)
10517
10728
  return false;
10518
- const now = Date.now();
10519
- if (this.aclActiveCache && now - this.aclActiveCache.at < 3000) {
10520
- return this.aclActiveCache.value;
10521
- }
10522
10729
  try {
10523
10730
  const directory = await this.tenant.openDirectory();
10524
10731
  let value = false;
10525
10732
  if (typeof directory.isAccessControlActive === "function") {
10526
10733
  value = await directory.isAccessControlActive();
10527
10734
  }
10528
- this.aclActiveCache = { value, at: now };
10735
+ this.aclActiveLastKnown = value;
10529
10736
  return value;
10530
10737
  }
10531
10738
  catch (error) {
@@ -10534,9 +10741,9 @@ class BaseMindooDB {
10534
10741
  // materialization gate entirely). Reuse the last known verdict if we have
10535
10742
  // one; otherwise assume enforced so the materialization path governs the
10536
10743
  // load and retries once the directory is reachable again.
10537
- if (this.aclActiveCache) {
10538
- this.logger.debug(`[ACL] active-check failed, reusing last known verdict (${this.aclActiveCache.value}): ${error}`);
10539
- return this.aclActiveCache.value;
10744
+ if (this.aclActiveLastKnown !== null) {
10745
+ this.logger.debug(`[ACL] active-check failed, reusing last known verdict (${this.aclActiveLastKnown}): ${error}`);
10746
+ return this.aclActiveLastKnown;
10540
10747
  }
10541
10748
  this.logger.warn(`[ACL] active-check failed with no prior verdict, assuming enforced (fail closed): ${error}`);
10542
10749
  return true;
@@ -12489,15 +12696,15 @@ class BaseMindooDB {
12489
12696
  * Resolve a sync target to a ContentAddressedStore.
12490
12697
  * Accepts either a raw store or a MindooDB instance (calls getStore()).
12491
12698
  */
12492
- getStoreForKind(storeKind = types_2.StoreKind.docs) {
12493
- return storeKind === types_2.StoreKind.attachments
12699
+ getStoreForKind(storeKind = types_3.StoreKind.docs) {
12700
+ return storeKind === types_3.StoreKind.attachments
12494
12701
  ? this.attachmentStore
12495
12702
  : this.store;
12496
12703
  }
12497
- resolveStore(remote, storeKind = types_2.StoreKind.docs) {
12704
+ resolveStore(remote, storeKind = types_3.StoreKind.docs) {
12498
12705
  if ("getStore" in remote &&
12499
12706
  typeof remote.getStore === "function") {
12500
- return storeKind === types_2.StoreKind.attachments
12707
+ return storeKind === types_3.StoreKind.attachments
12501
12708
  ? remote.getAttachmentStore()
12502
12709
  : remote.getStore();
12503
12710
  }
@@ -12745,9 +12952,16 @@ class BaseMindooDB {
12745
12952
  */
12746
12953
  async pullChangesFrom(remote, options) {
12747
12954
  this.assertWritable("pullChangesFrom");
12748
- const storeKind = options?.storeKind ?? types_2.StoreKind.docs;
12749
- const localStore = this.getStoreForKind(storeKind);
12955
+ const storeKind = options?.storeKind ?? types_3.StoreKind.docs;
12750
12956
  const remoteStore = this.resolveStore(remote, storeKind);
12957
+ // Entries of documents purged by an admin-signed request are refused on the
12958
+ // way in, so a replica that has not executed the purge yet cannot hand the
12959
+ // erased history back (docs/accesscontrol.md §13). Never applied to the
12960
+ // directory, which carries the purge requests themselves.
12961
+ const dbId = this.store.getId();
12962
+ const localStore = dbId === "directory"
12963
+ ? this.getStoreForKind(storeKind)
12964
+ : (0, purgedDocumentGuard_1.guardStoreAgainstPurgedDocuments)(this.getStoreForKind(storeKind), () => this.tenant.getPurgedDocumentIds(dbId), this.logger);
12751
12965
  if (localStore.getId() !== remoteStore.getId() ||
12752
12966
  localStore.getStoreKind() !== remoteStore.getStoreKind()) {
12753
12967
  throw new Error(`[BaseMindooDB] Cannot pull entries from the incompatible store ${localStore.getId()}/${localStore.getStoreKind()}`);
@@ -12760,7 +12974,7 @@ class BaseMindooDB {
12760
12974
  // summary buffer / full-text index materialize and extract documents
12761
12975
  // while the next page is still on the wire, instead of all at once
12762
12976
  // after the transfer (see docs/adhoc-queries.md, "Cold start").
12763
- const syncResult = await this.syncEntriesFromStore(remoteStore, localStore, options, storeKind === types_2.StoreKind.docs
12977
+ const syncResult = await this.syncEntriesFromStore(remoteStore, localStore, options, storeKind === types_3.StoreKind.docs
12764
12978
  ? () => this.syncStoreChanges()
12765
12979
  : undefined);
12766
12980
  const transferredBytes = syncResult.transferredBytes ?? 0;
@@ -12776,7 +12990,7 @@ class BaseMindooDB {
12776
12990
  }
12777
12991
  if (syncResult.transferred === 0) {
12778
12992
  this.logger.debug(`No new entries to pull`);
12779
- if (storeKind === types_2.StoreKind.docs) {
12993
+ if (storeKind === types_3.StoreKind.docs) {
12780
12994
  await this.reconcileAfterDocsPull();
12781
12995
  }
12782
12996
  return {
@@ -12804,7 +13018,7 @@ class BaseMindooDB {
12804
13018
  cancelled: true,
12805
13019
  };
12806
13020
  }
12807
- if (storeKind === types_2.StoreKind.docs) {
13021
+ if (storeKind === types_3.StoreKind.docs) {
12808
13022
  await this.syncStoreChanges();
12809
13023
  await this.reconcileAfterDocsPull();
12810
13024
  }
@@ -12873,7 +13087,7 @@ class BaseMindooDB {
12873
13087
  */
12874
13088
  async pushChangesTo(remote, options) {
12875
13089
  this.assertWritable("pushChangesTo");
12876
- const storeKind = options?.storeKind ?? types_2.StoreKind.docs;
13090
+ const storeKind = options?.storeKind ?? types_3.StoreKind.docs;
12877
13091
  const localStore = this.getStoreForKind(storeKind);
12878
13092
  const remoteStore = this.resolveStore(remote, storeKind);
12879
13093
  if (localStore.getId() !== remoteStore.getId() ||