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.
- package/dist/browser/index.mjs +2 -0
- package/dist/core/BackgroundTaskTracker.d.ts +18 -0
- package/dist/core/BackgroundTaskTracker.d.ts.map +1 -0
- package/dist/core/BackgroundTaskTracker.js +37 -0
- package/dist/core/BackgroundTaskTracker.js.map +1 -0
- package/dist/core/BaseMindooDB.d.ts +43 -7
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +230 -65
- package/dist/core/BaseMindooDB.js.map +1 -1
- package/dist/core/BaseMindooTenant.d.ts +30 -12
- package/dist/core/BaseMindooTenant.d.ts.map +1 -1
- package/dist/core/BaseMindooTenant.js +90 -65
- package/dist/core/BaseMindooTenant.js.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.d.ts +57 -14
- package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.js +319 -164
- package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
- package/dist/core/cache/CacheManager.d.ts +4 -1
- package/dist/core/cache/CacheManager.d.ts.map +1 -1
- package/dist/core/cache/CacheManager.js +47 -19
- package/dist/core/cache/CacheManager.js.map +1 -1
- package/dist/core/indexing/fulltext/DocumentFullTextIndex.d.ts +16 -1
- package/dist/core/indexing/fulltext/DocumentFullTextIndex.d.ts.map +1 -1
- package/dist/core/indexing/fulltext/DocumentFullTextIndex.js +86 -14
- package/dist/core/indexing/fulltext/DocumentFullTextIndex.js.map +1 -1
- package/dist/core/indexing/summary/DocumentSummaryStore.d.ts +2 -0
- package/dist/core/indexing/summary/DocumentSummaryStore.d.ts.map +1 -1
- package/dist/core/indexing/summary/DocumentSummaryStore.js +19 -5
- package/dist/core/indexing/summary/DocumentSummaryStore.js.map +1 -1
- package/dist/core/types.d.ts +29 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +22 -0
- package/dist/core/types.js.map +1 -1
- package/dist/core/utils/singleFlight.d.ts +21 -0
- package/dist/core/utils/singleFlight.d.ts.map +1 -0
- package/dist/core/utils/singleFlight.js +47 -0
- package/dist/core/utils/singleFlight.js.map +1 -0
- package/dist/core/utils/timeSource.d.ts +3 -1
- package/dist/core/utils/timeSource.d.ts.map +1 -1
- package/dist/core/utils/timeSource.js +4 -0
- package/dist/core/utils/timeSource.js.map +1 -1
- 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("./
|
|
11
|
+
const types_2 = require("./types");
|
|
12
|
+
const types_3 = require("./appendonlystores/types");
|
|
12
13
|
const syncStores_1 = require("./appendonlystores/syncStores");
|
|
13
|
-
const
|
|
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
|
|
37
|
+
const types_5 = require("./indexing/summary/types");
|
|
37
38
|
const DocumentFullTextIndex_1 = require("./indexing/fulltext/DocumentFullTextIndex");
|
|
38
|
-
const
|
|
39
|
-
const
|
|
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) !==
|
|
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
|
-
*
|
|
253
|
-
*
|
|
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.
|
|
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() !==
|
|
571
|
-
throw new Error(`[BaseMindooDB] Expected primary store kind ${
|
|
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() !==
|
|
574
|
-
throw new Error(`[BaseMindooDB] Expected attachment store kind ${
|
|
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?.
|
|
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(
|
|
943
|
-
return (0,
|
|
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:
|
|
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[
|
|
975
|
+
delete data[types_5.SUMMARY_SETUP_FIELD];
|
|
970
976
|
}
|
|
971
977
|
else {
|
|
972
|
-
data[
|
|
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 ===
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
1107
|
-
return ((0,
|
|
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,
|
|
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:
|
|
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[
|
|
1146
|
+
delete data[types_6.FULLTEXT_SETUP_FIELD];
|
|
1141
1147
|
}
|
|
1142
1148
|
else {
|
|
1143
|
-
data[
|
|
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(
|
|
1158
|
-
return ((0,
|
|
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,
|
|
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:
|
|
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[
|
|
1193
|
+
delete data[types_7.EXTRACTION_SETUP_FIELD];
|
|
1188
1194
|
}
|
|
1189
1195
|
else {
|
|
1190
|
-
data[
|
|
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 ===
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) !==
|
|
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.
|
|
10554
|
-
*
|
|
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.
|
|
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.
|
|
10580
|
-
this.logger.debug(`[ACL] active-check failed, reusing last known verdict (${this.
|
|
10581
|
-
return this.
|
|
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 =
|
|
12535
|
-
return storeKind ===
|
|
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 =
|
|
12704
|
+
resolveStore(remote, storeKind = types_3.StoreKind.docs) {
|
|
12540
12705
|
if ("getStore" in remote &&
|
|
12541
12706
|
typeof remote.getStore === "function") {
|
|
12542
|
-
return storeKind ===
|
|
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 ??
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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 ??
|
|
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() ||
|