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.
- 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 +60 -7
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +280 -66
- package/dist/core/BaseMindooDB.js.map +1 -1
- package/dist/core/BaseMindooTenant.d.ts +46 -12
- package/dist/core/BaseMindooTenant.d.ts.map +1 -1
- package/dist/core/BaseMindooTenant.js +133 -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/appendonlystores/InMemoryContentAddressedStore.d.ts +11 -0
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts.map +1 -1
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.js +44 -18
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.js.map +1 -1
- package/dist/core/appendonlystores/purgeDocumentHistory.d.ts +52 -0
- package/dist/core/appendonlystores/purgeDocumentHistory.d.ts.map +1 -0
- package/dist/core/appendonlystores/purgeDocumentHistory.js +107 -0
- package/dist/core/appendonlystores/purgeDocumentHistory.js.map +1 -0
- package/dist/core/appendonlystores/purgedDocumentGuard.d.ts +33 -0
- package/dist/core/appendonlystores/purgedDocumentGuard.d.ts.map +1 -0
- package/dist/core/appendonlystores/purgedDocumentGuard.js +78 -0
- package/dist/core/appendonlystores/purgedDocumentGuard.js.map +1 -0
- 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 +57 -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/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts +13 -0
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts.map +1 -1
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js +26 -2
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js.map +1 -1
- package/dist/node/server/MindooDBServer.d.ts.map +1 -1
- package/dist/node/server/MindooDBServer.js +2 -0
- package/dist/node/server/MindooDBServer.js.map +1 -1
- package/dist/node/server/TenantManager.d.ts +11 -0
- package/dist/node/server/TenantManager.d.ts.map +1 -1
- package/dist/node/server/TenantManager.js +40 -9
- package/dist/node/server/TenantManager.js.map +1 -1
- package/dist/node/server/peer/ClusterManager.d.ts +4 -0
- package/dist/node/server/peer/ClusterManager.d.ts.map +1 -1
- package/dist/node/server/peer/ClusterManager.js +2 -0
- package/dist/node/server/peer/ClusterManager.js.map +1 -1
- package/dist/node/server/peer/PeerReplicator.d.ts +12 -0
- package/dist/node/server/peer/PeerReplicator.d.ts.map +1 -1
- package/dist/node/server/peer/PeerReplicator.js +19 -1
- package/dist/node/server/peer/PeerReplicator.js.map +1 -1
- 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("./
|
|
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");
|
|
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
|
|
37
|
+
const types_5 = require("./indexing/summary/types");
|
|
35
38
|
const DocumentFullTextIndex_1 = require("./indexing/fulltext/DocumentFullTextIndex");
|
|
36
|
-
const
|
|
37
|
-
const
|
|
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) !==
|
|
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
|
-
*
|
|
251
|
-
*
|
|
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.
|
|
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() !==
|
|
569
|
-
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()}`);
|
|
570
578
|
}
|
|
571
|
-
if (attachmentStore.getStoreKind() !==
|
|
572
|
-
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()}`);
|
|
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?.
|
|
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(
|
|
941
|
-
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;
|
|
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:
|
|
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[
|
|
975
|
+
delete data[types_5.SUMMARY_SETUP_FIELD];
|
|
968
976
|
}
|
|
969
977
|
else {
|
|
970
|
-
data[
|
|
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 ===
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
1105
|
-
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);
|
|
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,
|
|
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:
|
|
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[
|
|
1146
|
+
delete data[types_6.FULLTEXT_SETUP_FIELD];
|
|
1139
1147
|
}
|
|
1140
1148
|
else {
|
|
1141
|
-
data[
|
|
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(
|
|
1156
|
-
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);
|
|
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,
|
|
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:
|
|
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[
|
|
1193
|
+
delete data[types_7.EXTRACTION_SETUP_FIELD];
|
|
1186
1194
|
}
|
|
1187
1195
|
else {
|
|
1188
|
-
data[
|
|
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 ===
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) !==
|
|
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.
|
|
10512
|
-
*
|
|
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.
|
|
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.
|
|
10538
|
-
this.logger.debug(`[ACL] active-check failed, reusing last known verdict (${this.
|
|
10539
|
-
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;
|
|
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 =
|
|
12493
|
-
return storeKind ===
|
|
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 =
|
|
12704
|
+
resolveStore(remote, storeKind = types_3.StoreKind.docs) {
|
|
12498
12705
|
if ("getStore" in remote &&
|
|
12499
12706
|
typeof remote.getStore === "function") {
|
|
12500
|
-
return storeKind ===
|
|
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 ??
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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 ??
|
|
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() ||
|