mindoodb 0.0.73 → 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 (42) 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 +43 -7
  7. package/dist/core/BaseMindooDB.d.ts.map +1 -1
  8. package/dist/core/BaseMindooDB.js +230 -65
  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/cache/CacheManager.d.ts +4 -1
  19. package/dist/core/cache/CacheManager.d.ts.map +1 -1
  20. package/dist/core/cache/CacheManager.js +47 -19
  21. package/dist/core/cache/CacheManager.js.map +1 -1
  22. package/dist/core/indexing/fulltext/DocumentFullTextIndex.d.ts +16 -1
  23. package/dist/core/indexing/fulltext/DocumentFullTextIndex.d.ts.map +1 -1
  24. package/dist/core/indexing/fulltext/DocumentFullTextIndex.js +86 -14
  25. package/dist/core/indexing/fulltext/DocumentFullTextIndex.js.map +1 -1
  26. package/dist/core/indexing/summary/DocumentSummaryStore.d.ts +2 -0
  27. package/dist/core/indexing/summary/DocumentSummaryStore.d.ts.map +1 -1
  28. package/dist/core/indexing/summary/DocumentSummaryStore.js +19 -5
  29. package/dist/core/indexing/summary/DocumentSummaryStore.js.map +1 -1
  30. package/dist/core/types.d.ts +29 -0
  31. package/dist/core/types.d.ts.map +1 -1
  32. package/dist/core/types.js +22 -0
  33. package/dist/core/types.js.map +1 -1
  34. package/dist/core/utils/singleFlight.d.ts +21 -0
  35. package/dist/core/utils/singleFlight.d.ts.map +1 -0
  36. package/dist/core/utils/singleFlight.js +47 -0
  37. package/dist/core/utils/singleFlight.js.map +1 -0
  38. package/dist/core/utils/timeSource.d.ts +3 -1
  39. package/dist/core/utils/timeSource.d.ts.map +1 -1
  40. package/dist/core/utils/timeSource.js +4 -0
  41. package/dist/core/utils/timeSource.js.map +1 -1
  42. 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();
@@ -8851,6 +8993,34 @@ class BaseMindooDB {
8851
8993
  * {@link applyNewEntriesToCachedDocument} (line ~6606) for the sync
8852
8994
  * pipeline; this helper centralizes it for the patch / changeDoc paths.
8853
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
+ }
8854
9024
  runChangeWithOutdatedDocRecovery(internalDoc, apply) {
8855
9025
  try {
8856
9026
  return apply(internalDoc.doc);
@@ -9221,7 +9391,7 @@ class BaseMindooDB {
9221
9391
  return await this.withRateLimitBackoff("entry prefetch", () => this.store.getEntries(ids));
9222
9392
  }
9223
9393
  catch (error) {
9224
- if (networkErrorType(error) !== types_3.NetworkErrorType.PAYLOAD_TOO_LARGE ||
9394
+ if (networkErrorType(error) !== types_4.NetworkErrorType.PAYLOAD_TOO_LARGE ||
9225
9395
  ids.length <= 1) {
9226
9396
  throw error;
9227
9397
  }
@@ -10550,24 +10720,19 @@ class BaseMindooDB {
10550
10720
  * Whether access control is active for this tenant and should gate
10551
10721
  * materialization. Admin-only databases (incl. the directory itself) are
10552
10722
  * 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).
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.
10556
10725
  */
10557
10726
  async isAclEnforced() {
10558
10727
  if (this._isAdminOnlyDb)
10559
10728
  return false;
10560
- const now = Date.now();
10561
- if (this.aclActiveCache && now - this.aclActiveCache.at < 3000) {
10562
- return this.aclActiveCache.value;
10563
- }
10564
10729
  try {
10565
10730
  const directory = await this.tenant.openDirectory();
10566
10731
  let value = false;
10567
10732
  if (typeof directory.isAccessControlActive === "function") {
10568
10733
  value = await directory.isAccessControlActive();
10569
10734
  }
10570
- this.aclActiveCache = { value, at: now };
10735
+ this.aclActiveLastKnown = value;
10571
10736
  return value;
10572
10737
  }
10573
10738
  catch (error) {
@@ -10576,9 +10741,9 @@ class BaseMindooDB {
10576
10741
  // materialization gate entirely). Reuse the last known verdict if we have
10577
10742
  // one; otherwise assume enforced so the materialization path governs the
10578
10743
  // 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;
10744
+ if (this.aclActiveLastKnown !== null) {
10745
+ this.logger.debug(`[ACL] active-check failed, reusing last known verdict (${this.aclActiveLastKnown}): ${error}`);
10746
+ return this.aclActiveLastKnown;
10582
10747
  }
10583
10748
  this.logger.warn(`[ACL] active-check failed with no prior verdict, assuming enforced (fail closed): ${error}`);
10584
10749
  return true;
@@ -12531,15 +12696,15 @@ class BaseMindooDB {
12531
12696
  * Resolve a sync target to a ContentAddressedStore.
12532
12697
  * Accepts either a raw store or a MindooDB instance (calls getStore()).
12533
12698
  */
12534
- getStoreForKind(storeKind = types_2.StoreKind.docs) {
12535
- return storeKind === types_2.StoreKind.attachments
12699
+ getStoreForKind(storeKind = types_3.StoreKind.docs) {
12700
+ return storeKind === types_3.StoreKind.attachments
12536
12701
  ? this.attachmentStore
12537
12702
  : this.store;
12538
12703
  }
12539
- resolveStore(remote, storeKind = types_2.StoreKind.docs) {
12704
+ resolveStore(remote, storeKind = types_3.StoreKind.docs) {
12540
12705
  if ("getStore" in remote &&
12541
12706
  typeof remote.getStore === "function") {
12542
- return storeKind === types_2.StoreKind.attachments
12707
+ return storeKind === types_3.StoreKind.attachments
12543
12708
  ? remote.getAttachmentStore()
12544
12709
  : remote.getStore();
12545
12710
  }
@@ -12787,7 +12952,7 @@ class BaseMindooDB {
12787
12952
  */
12788
12953
  async pullChangesFrom(remote, options) {
12789
12954
  this.assertWritable("pullChangesFrom");
12790
- const storeKind = options?.storeKind ?? types_2.StoreKind.docs;
12955
+ const storeKind = options?.storeKind ?? types_3.StoreKind.docs;
12791
12956
  const remoteStore = this.resolveStore(remote, storeKind);
12792
12957
  // Entries of documents purged by an admin-signed request are refused on the
12793
12958
  // way in, so a replica that has not executed the purge yet cannot hand the
@@ -12809,7 +12974,7 @@ class BaseMindooDB {
12809
12974
  // summary buffer / full-text index materialize and extract documents
12810
12975
  // while the next page is still on the wire, instead of all at once
12811
12976
  // after the transfer (see docs/adhoc-queries.md, "Cold start").
12812
- 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
12813
12978
  ? () => this.syncStoreChanges()
12814
12979
  : undefined);
12815
12980
  const transferredBytes = syncResult.transferredBytes ?? 0;
@@ -12825,7 +12990,7 @@ class BaseMindooDB {
12825
12990
  }
12826
12991
  if (syncResult.transferred === 0) {
12827
12992
  this.logger.debug(`No new entries to pull`);
12828
- if (storeKind === types_2.StoreKind.docs) {
12993
+ if (storeKind === types_3.StoreKind.docs) {
12829
12994
  await this.reconcileAfterDocsPull();
12830
12995
  }
12831
12996
  return {
@@ -12853,7 +13018,7 @@ class BaseMindooDB {
12853
13018
  cancelled: true,
12854
13019
  };
12855
13020
  }
12856
- if (storeKind === types_2.StoreKind.docs) {
13021
+ if (storeKind === types_3.StoreKind.docs) {
12857
13022
  await this.syncStoreChanges();
12858
13023
  await this.reconcileAfterDocsPull();
12859
13024
  }
@@ -12922,7 +13087,7 @@ class BaseMindooDB {
12922
13087
  */
12923
13088
  async pushChangesTo(remote, options) {
12924
13089
  this.assertWritable("pushChangesTo");
12925
- const storeKind = options?.storeKind ?? types_2.StoreKind.docs;
13090
+ const storeKind = options?.storeKind ?? types_3.StoreKind.docs;
12926
13091
  const localStore = this.getStoreForKind(storeKind);
12927
13092
  const remoteStore = this.resolveStore(remote, storeKind);
12928
13093
  if (localStore.getId() !== remoteStore.getId() ||