mindoodb 0.0.53 → 0.0.55
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/README-server.md +125 -69
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts +17 -0
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +22 -0
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +58 -0
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +156 -3
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -1
- package/dist/browser/index.mjs +2 -0
- package/dist/core/BaseMindooDB.d.ts +73 -74
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +263 -509
- package/dist/core/BaseMindooDB.js.map +1 -1
- package/dist/core/MindooDBServerAdmin.d.ts +44 -22
- package/dist/core/MindooDBServerAdmin.d.ts.map +1 -1
- package/dist/core/MindooDBServerAdmin.js +45 -21
- package/dist/core/MindooDBServerAdmin.js.map +1 -1
- package/dist/core/appendonlystores/syncStores.d.ts +147 -0
- package/dist/core/appendonlystores/syncStores.d.ts.map +1 -0
- package/dist/core/appendonlystores/syncStores.js +612 -0
- package/dist/core/appendonlystores/syncStores.js.map +1 -0
- package/dist/core/builtinDbInvariants.d.ts +30 -0
- package/dist/core/builtinDbInvariants.d.ts.map +1 -1
- package/dist/core/builtinDbInvariants.js +97 -0
- package/dist/core/builtinDbInvariants.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +7 -4
- package/dist/core/index.js.map +1 -1
- package/dist/core/types.d.ts +42 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/node/server/CapabilityMatcher.d.ts +2 -2
- package/dist/node/server/CapabilityMatcher.js +2 -2
- package/dist/node/server/MindooDBServer.d.ts +14 -1
- package/dist/node/server/MindooDBServer.d.ts.map +1 -1
- package/dist/node/server/MindooDBServer.js +166 -159
- package/dist/node/server/MindooDBServer.js.map +1 -1
- package/dist/node/server/SyncEventBus.d.ts +10 -0
- package/dist/node/server/SyncEventBus.d.ts.map +1 -1
- package/dist/node/server/SyncEventBus.js.map +1 -1
- package/dist/node/server/TenantManager.d.ts +58 -6
- package/dist/node/server/TenantManager.d.ts.map +1 -1
- package/dist/node/server/TenantManager.js +90 -65
- package/dist/node/server/TenantManager.js.map +1 -1
- package/dist/node/server/add-to-network.d.ts.map +1 -1
- package/dist/node/server/add-to-network.js +33 -15
- package/dist/node/server/add-to-network.js.map +1 -1
- package/dist/node/server/canonicalSetupName.d.ts +22 -0
- package/dist/node/server/canonicalSetupName.d.ts.map +1 -0
- package/dist/node/server/canonicalSetupName.js +38 -0
- package/dist/node/server/canonicalSetupName.js.map +1 -0
- package/dist/node/server/config.d.ts.map +1 -1
- package/dist/node/server/config.js +33 -1
- package/dist/node/server/config.js.map +1 -1
- package/dist/node/server/peer/ClusterAuditLog.d.ts +64 -0
- package/dist/node/server/peer/ClusterAuditLog.d.ts.map +1 -0
- package/dist/node/server/peer/ClusterAuditLog.js +99 -0
- package/dist/node/server/peer/ClusterAuditLog.js.map +1 -0
- package/dist/node/server/peer/ClusterJobRunner.d.ts +43 -0
- package/dist/node/server/peer/ClusterJobRunner.d.ts.map +1 -0
- package/dist/node/server/peer/ClusterJobRunner.js +119 -0
- package/dist/node/server/peer/ClusterJobRunner.js.map +1 -0
- package/dist/node/server/peer/ClusterManager.d.ts +116 -0
- package/dist/node/server/peer/ClusterManager.d.ts.map +1 -0
- package/dist/node/server/peer/ClusterManager.js +274 -0
- package/dist/node/server/peer/ClusterManager.js.map +1 -0
- package/dist/node/server/peer/PeerAuthService.d.ts +69 -0
- package/dist/node/server/peer/PeerAuthService.d.ts.map +1 -0
- package/dist/node/server/peer/PeerAuthService.js +208 -0
- package/dist/node/server/peer/PeerAuthService.js.map +1 -0
- package/dist/node/server/peer/PeerReplicator.d.ts +232 -0
- package/dist/node/server/peer/PeerReplicator.d.ts.map +1 -0
- package/dist/node/server/peer/PeerReplicator.js +879 -0
- package/dist/node/server/peer/PeerReplicator.js.map +1 -0
- package/dist/node/server/peer/routes.d.ts +66 -0
- package/dist/node/server/peer/routes.d.ts.map +1 -0
- package/dist/node/server/peer/routes.js +322 -0
- package/dist/node/server/peer/routes.js.map +1 -0
- package/dist/node/server/peer/types.d.ts +244 -0
- package/dist/node/server/peer/types.d.ts.map +1 -0
- package/dist/node/server/peer/types.js +189 -0
- package/dist/node/server/peer/types.js.map +1 -0
- package/dist/node/server/server.d.ts +1 -1
- package/dist/node/server/server.js +18 -37
- package/dist/node/server/server.js.map +1 -1
- package/dist/node/server/serverinit.d.ts +2 -1
- package/dist/node/server/serverinit.d.ts.map +1 -1
- package/dist/node/server/serverinit.js +34 -12
- package/dist/node/server/serverinit.js.map +1 -1
- package/dist/node/server/types.d.ts +42 -30
- package/dist/node/server/types.d.ts.map +1 -1
- package/dist/node/server/types.js +1 -7
- package/dist/node/server/types.js.map +1 -1
- package/dist/node/server/validation.d.ts +0 -6
- package/dist/node/server/validation.d.ts.map +1 -1
- package/dist/node/server/validation.js +1 -7
- package/dist/node/server/validation.js.map +1 -1
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@ const storeEntryTime_1 = require("./storeEntryTime");
|
|
|
9
9
|
const uuid_1 = require("uuid");
|
|
10
10
|
const types_1 = require("./types");
|
|
11
11
|
const types_2 = require("./appendonlystores/types");
|
|
12
|
-
const
|
|
12
|
+
const syncStores_1 = require("./appendonlystores/syncStores");
|
|
13
13
|
const types_3 = require("./appendonlystores/network/types");
|
|
14
14
|
const MaterializationPlanner_1 = require("./appendonlystores/MaterializationPlanner");
|
|
15
15
|
const DocumentDagAnalysis_1 = require("./DocumentDagAnalysis");
|
|
@@ -352,6 +352,35 @@ class BaseMindooDB {
|
|
|
352
352
|
* from the cached materialization and seeds the pending map from them.
|
|
353
353
|
*/
|
|
354
354
|
this.legacyCheckpointNeedsAuthorTrustScan = false;
|
|
355
|
+
/**
|
|
356
|
+
* Ids of documents that exist in the local store but cannot be read with
|
|
357
|
+
* the current KeyBag, so they are absent from the index and from every
|
|
358
|
+
* document listing.
|
|
359
|
+
*
|
|
360
|
+
* The index alone cannot answer "how much is hidden from me": a document
|
|
361
|
+
* that was never readable on this device gets no index entry at all (see
|
|
362
|
+
* {@link reconcileKeyVisibility}), and one that lost its key becomes an
|
|
363
|
+
* `"inaccessible"` tombstone that listings filter out. This set is the
|
|
364
|
+
* only place that remembers both cases, which is what
|
|
365
|
+
* {@link getInaccessibleDocumentCount} reports so a UI can tell an empty
|
|
366
|
+
* database apart from one whose keys have not arrived yet.
|
|
367
|
+
*
|
|
368
|
+
* Counts documents regardless of their deletion state - the question it
|
|
369
|
+
* answers is "is it here and unreadable", which is knowable without the
|
|
370
|
+
* key, unlike a reliable lifecycle state for a doc we cannot decrypt.
|
|
371
|
+
*
|
|
372
|
+
* Persisted in the metadata checkpoint: a warm start with an unchanged
|
|
373
|
+
* KeyBag skips the visibility scan, so an in-memory-only tally would
|
|
374
|
+
* silently reset to zero on every restart.
|
|
375
|
+
*/
|
|
376
|
+
this.inaccessibleDocIds = new Set();
|
|
377
|
+
/**
|
|
378
|
+
* Set while restoring a metadata checkpoint that predates
|
|
379
|
+
* {@link inaccessibleDocIds}. Forces one visibility scan at open so the
|
|
380
|
+
* tally starts out complete instead of counting only what happens to
|
|
381
|
+
* arrive in this session.
|
|
382
|
+
*/
|
|
383
|
+
this.legacyCheckpointNeedsVisibilityTally = false;
|
|
355
384
|
// Local cache support
|
|
356
385
|
this.cacheManager = null;
|
|
357
386
|
this.cachePrefix = null;
|
|
@@ -462,6 +491,8 @@ class BaseMindooDB {
|
|
|
462
491
|
this.userdirectoryHashByDocId = new Map();
|
|
463
492
|
/** Grant `username_hash` for a signing public key at "now". */
|
|
464
493
|
this.signerUsernameHashByKey = new Map();
|
|
494
|
+
/** Owning person per personal userdirectory document (its create signer). */
|
|
495
|
+
this.personalDocCreatorHashByDocId = new Map();
|
|
465
496
|
// ---------------------------------------------------------------------------
|
|
466
497
|
// Author-trust reconciliation: heal documents whose entries were skipped
|
|
467
498
|
// because their author was not yet known to the tenant directory.
|
|
@@ -578,15 +609,22 @@ class BaseMindooDB {
|
|
|
578
609
|
* Hard-wired write gate for `directory` / `userdirectory` (and other
|
|
579
610
|
* admin-only databases). Independent of the ACL master switch.
|
|
580
611
|
*/
|
|
581
|
-
async assertBuiltinWriteAllowed(op, signerPublicKey, documentUsernameHash) {
|
|
612
|
+
async assertBuiltinWriteAllowed(op, signerPublicKey, documentUsernameHash, docId) {
|
|
582
613
|
const dbId = this.effectiveBuiltinDbId();
|
|
583
614
|
if (!(0, builtinDbInvariants_1.hasBuiltinWriteInvariant)(dbId) && !this._isAdminOnlyDb) {
|
|
584
615
|
return;
|
|
585
616
|
}
|
|
586
617
|
let signerUsernameHash = null;
|
|
618
|
+
let creatorUsernameHash = null;
|
|
587
619
|
if (dbId === types_1.USER_DIRECTORY_DB_ID) {
|
|
588
620
|
signerUsernameHash = await this.resolveSignerUsernameHash(signerPublicKey);
|
|
589
621
|
this.signerUsernameHashByKey.set(signerPublicKey, signerUsernameHash);
|
|
622
|
+
if ((0, builtinDbInvariants_1.isPersonalUserdirectoryDocId)(docId)) {
|
|
623
|
+
creatorUsernameHash =
|
|
624
|
+
op === "doc_create"
|
|
625
|
+
? signerUsernameHash
|
|
626
|
+
: await this.ensurePersonalDocCreatorHash(docId);
|
|
627
|
+
}
|
|
590
628
|
}
|
|
591
629
|
const decision = (0, builtinDbInvariants_1.evaluateBuiltinWrite)({
|
|
592
630
|
dbId,
|
|
@@ -595,6 +633,8 @@ class BaseMindooDB {
|
|
|
595
633
|
adminPublicKey: this.getAdminPublicKey(),
|
|
596
634
|
documentUsernameHash,
|
|
597
635
|
signerUsernameHash,
|
|
636
|
+
docId,
|
|
637
|
+
creatorUsernameHash,
|
|
598
638
|
});
|
|
599
639
|
if (!decision.allowed) {
|
|
600
640
|
throw new Error(decision.reason);
|
|
@@ -611,7 +651,14 @@ class BaseMindooDB {
|
|
|
611
651
|
const entryType = entry.entryType ?? "doc_change";
|
|
612
652
|
let documentUsernameHash = null;
|
|
613
653
|
let signerUsernameHash = null;
|
|
614
|
-
|
|
654
|
+
let creatorUsernameHash = null;
|
|
655
|
+
if (dbId === types_1.USER_DIRECTORY_DB_ID && (0, builtinDbInvariants_1.isPersonalUserdirectoryDocId)(entry.docId)) {
|
|
656
|
+
if (entryType !== "doc_create") {
|
|
657
|
+
creatorUsernameHash = await this.ensurePersonalDocCreatorHash(entry.docId);
|
|
658
|
+
signerUsernameHash = await this.ensureSignerUsernameHash(entry.createdByPublicKey);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
else if (dbId === types_1.USER_DIRECTORY_DB_ID &&
|
|
615
662
|
(entryType === "doc_change" || entryType === "doc_snapshot") &&
|
|
616
663
|
entry.docId) {
|
|
617
664
|
documentUsernameHash = await this.ensureUserdirectoryDocHash(entry.docId);
|
|
@@ -624,6 +671,8 @@ class BaseMindooDB {
|
|
|
624
671
|
adminPublicKey: this.getAdminPublicKey(),
|
|
625
672
|
documentUsernameHash,
|
|
626
673
|
signerUsernameHash,
|
|
674
|
+
docId: entry.docId,
|
|
675
|
+
creatorUsernameHash,
|
|
627
676
|
});
|
|
628
677
|
}
|
|
629
678
|
async ensureSignerUsernameHash(signerKey) {
|
|
@@ -634,6 +683,34 @@ class BaseMindooDB {
|
|
|
634
683
|
this.signerUsernameHashByKey.set(signerKey, hash);
|
|
635
684
|
return hash;
|
|
636
685
|
}
|
|
686
|
+
/**
|
|
687
|
+
* Owner of a personal `userdirectory` document: the person who signed its
|
|
688
|
+
* `doc_create`, resolved through grants rather than the payload. A sealed
|
|
689
|
+
* document is opaque to everyone but its owner, so the payload is not an
|
|
690
|
+
* option — and grants map every device of a person to one hash, which is
|
|
691
|
+
* exactly the granularity ownership needs.
|
|
692
|
+
*/
|
|
693
|
+
async ensurePersonalDocCreatorHash(docId) {
|
|
694
|
+
const cached = this.personalDocCreatorHashByDocId.get(docId);
|
|
695
|
+
if (cached !== undefined)
|
|
696
|
+
return cached;
|
|
697
|
+
let hash = null;
|
|
698
|
+
try {
|
|
699
|
+
const metas = await this.scanAllMetadata(this.store, { docId });
|
|
700
|
+
const create = metas.find((meta) => meta.entryType === "doc_create");
|
|
701
|
+
if (create) {
|
|
702
|
+
hash = await this.ensureSignerUsernameHash(create.createdByPublicKey);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
catch {
|
|
706
|
+
hash = null;
|
|
707
|
+
}
|
|
708
|
+
// Only a resolved owner is worth caching: a miss usually means the create
|
|
709
|
+
// entry has not arrived yet, and the next write must ask again.
|
|
710
|
+
if (hash)
|
|
711
|
+
this.personalDocCreatorHashByDocId.set(docId, hash);
|
|
712
|
+
return hash;
|
|
713
|
+
}
|
|
637
714
|
async ensureUserdirectoryDocHash(docId) {
|
|
638
715
|
const cached = this.userdirectoryHashByDocId.get(docId);
|
|
639
716
|
if (cached)
|
|
@@ -1472,6 +1549,11 @@ class BaseMindooDB {
|
|
|
1472
1549
|
}
|
|
1473
1550
|
}
|
|
1474
1551
|
checkpoint.pendingUntrustedAuthors = pendingUntrusted;
|
|
1552
|
+
// Persist the unreadable-document tally (additive field). Written even
|
|
1553
|
+
// when empty, for the same reason as above: the field's PRESENCE marks
|
|
1554
|
+
// the checkpoint as tally-aware, so restore can tell "nothing hidden"
|
|
1555
|
+
// apart from "written before this existed, count unknown".
|
|
1556
|
+
checkpoint.inaccessibleDocIds = Array.from(this.inaccessibleDocIds);
|
|
1475
1557
|
return new TextEncoder().encode(JSON.stringify(checkpoint));
|
|
1476
1558
|
}
|
|
1477
1559
|
/**
|
|
@@ -1518,6 +1600,22 @@ class BaseMindooDB {
|
|
|
1518
1600
|
else {
|
|
1519
1601
|
this.legacyCheckpointNeedsAuthorTrustScan = true;
|
|
1520
1602
|
}
|
|
1603
|
+
// Unreadable-document tally. Missing field means the checkpoint predates
|
|
1604
|
+
// it: the count would otherwise stay at zero for as long as the KeyBag
|
|
1605
|
+
// is unchanged, because that is exactly when the visibility scan is
|
|
1606
|
+
// skipped. Force one scan instead.
|
|
1607
|
+
this.inaccessibleDocIds.clear();
|
|
1608
|
+
if (Array.isArray(checkpoint.inaccessibleDocIds)) {
|
|
1609
|
+
this.legacyCheckpointNeedsVisibilityTally = false;
|
|
1610
|
+
for (const docId of checkpoint.inaccessibleDocIds) {
|
|
1611
|
+
if (typeof docId === "string") {
|
|
1612
|
+
this.inaccessibleDocIds.add(docId);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
else {
|
|
1617
|
+
this.legacyCheckpointNeedsVisibilityTally = true;
|
|
1618
|
+
}
|
|
1521
1619
|
this.syncScanCursors.clear();
|
|
1522
1620
|
if (checkpoint.syncScanCursors && typeof checkpoint.syncScanCursors === "object") {
|
|
1523
1621
|
for (const [key, value] of Object.entries(checkpoint.syncScanCursors)) {
|
|
@@ -1871,6 +1969,17 @@ class BaseMindooDB {
|
|
|
1871
1969
|
// Every path that writes entries for a document lands here, so this is
|
|
1872
1970
|
// where retained metadata for it goes stale (see scannedEntryMetadata).
|
|
1873
1971
|
this.forgetScannedEntryMetadata(docId);
|
|
1972
|
+
// Same reasoning for the unreadable-document tally: every reveal and every
|
|
1973
|
+
// inaccessibility tombstone passes through here, so keeping it in step
|
|
1974
|
+
// needs no bookkeeping at the individual call sites. The one case this
|
|
1975
|
+
// misses is a document that was never readable, which produces no index
|
|
1976
|
+
// entry at all - those call `noteInaccessibleDocument` directly.
|
|
1977
|
+
if (accessState === "visible") {
|
|
1978
|
+
this.forgetInaccessibleDocument(docId);
|
|
1979
|
+
}
|
|
1980
|
+
else {
|
|
1981
|
+
this.noteInaccessibleDocument(docId);
|
|
1982
|
+
}
|
|
1874
1983
|
const assignedSeq = this.nextChangeSeq;
|
|
1875
1984
|
const newEntry = {
|
|
1876
1985
|
docId,
|
|
@@ -2138,7 +2247,8 @@ class BaseMindooDB {
|
|
|
2138
2247
|
// transition.
|
|
2139
2248
|
const currentFingerprint = await this.computeCurrentKeyBagFingerprint();
|
|
2140
2249
|
const fingerprintMatches = currentFingerprint !== null && currentFingerprint === this.lastReconciledKeyBagFingerprint;
|
|
2141
|
-
if (!fingerprintMatches) {
|
|
2250
|
+
if (!fingerprintMatches || this.legacyCheckpointNeedsVisibilityTally) {
|
|
2251
|
+
this.legacyCheckpointNeedsVisibilityTally = false;
|
|
2142
2252
|
await this.reconcileKeyVisibility();
|
|
2143
2253
|
}
|
|
2144
2254
|
this.markKeyBagFingerprintReconciled(currentFingerprint);
|
|
@@ -2257,6 +2367,73 @@ class BaseMindooDB {
|
|
|
2257
2367
|
this.cacheMetaDirty = true;
|
|
2258
2368
|
this.cacheManager?.markDirty();
|
|
2259
2369
|
}
|
|
2370
|
+
/**
|
|
2371
|
+
* Remember that `docId` is present in the store but unreadable. Idempotent:
|
|
2372
|
+
* the same document is re-observed on every sync batch that carries another
|
|
2373
|
+
* entry for it.
|
|
2374
|
+
*/
|
|
2375
|
+
noteInaccessibleDocument(docId) {
|
|
2376
|
+
if (this.inaccessibleDocIds.has(docId)) {
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
2379
|
+
this.inaccessibleDocIds.add(docId);
|
|
2380
|
+
this.cacheMetaDirty = true;
|
|
2381
|
+
this.cacheManager?.markDirty();
|
|
2382
|
+
}
|
|
2383
|
+
/** Counterpart of {@link noteInaccessibleDocument} for a doc that became readable. */
|
|
2384
|
+
forgetInaccessibleDocument(docId) {
|
|
2385
|
+
if (!this.inaccessibleDocIds.delete(docId)) {
|
|
2386
|
+
return;
|
|
2387
|
+
}
|
|
2388
|
+
this.cacheMetaDirty = true;
|
|
2389
|
+
this.cacheManager?.markDirty();
|
|
2390
|
+
}
|
|
2391
|
+
/**
|
|
2392
|
+
* How many documents this database holds locally that the current KeyBag
|
|
2393
|
+
* cannot open. They appear in no listing and in no view, so this count is
|
|
2394
|
+
* the only signal that a database looks emptier than it is - typically on a
|
|
2395
|
+
* device whose user key is still waiting for approval.
|
|
2396
|
+
*
|
|
2397
|
+
* Includes deleted documents, and counts what has arrived on this device -
|
|
2398
|
+
* not what the tenant holds.
|
|
2399
|
+
*/
|
|
2400
|
+
getInaccessibleDocumentCount() {
|
|
2401
|
+
return this.inaccessibleDocIds.size;
|
|
2402
|
+
}
|
|
2403
|
+
/**
|
|
2404
|
+
* Documents in the local store that this KeyBag cannot open, described
|
|
2405
|
+
* from unsigned origin metadata. See {@link MindooDB.listInaccessibleDocuments}.
|
|
2406
|
+
*/
|
|
2407
|
+
async listInaccessibleDocuments(options) {
|
|
2408
|
+
const idPrefix = this.normalizeIdPrefixFilter(options?.idPrefix);
|
|
2409
|
+
const docIds = [...this.inaccessibleDocIds].filter((docId) => !idPrefix || (0, idGeneration_1.matchesDocIdPrefix)(docId, idPrefix));
|
|
2410
|
+
if (docIds.length === 0) {
|
|
2411
|
+
return [];
|
|
2412
|
+
}
|
|
2413
|
+
const results = [];
|
|
2414
|
+
for (const docId of docIds) {
|
|
2415
|
+
const originEntries = await this.scanAllMetadata(this.store, {
|
|
2416
|
+
docId,
|
|
2417
|
+
entryTypes: ["doc_create", "doc_snapshot"],
|
|
2418
|
+
});
|
|
2419
|
+
let origin;
|
|
2420
|
+
for (const entry of originEntries) {
|
|
2421
|
+
if (!origin || entry.createdAt < origin.createdAt) {
|
|
2422
|
+
origin = entry;
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
const indexPos = this.getDocIndexPosition(docId);
|
|
2426
|
+
const indexEntry = indexPos === undefined ? undefined : this.index[indexPos];
|
|
2427
|
+
results.push({
|
|
2428
|
+
docId,
|
|
2429
|
+
createdAt: origin?.createdAt ?? indexEntry?.lastModified ?? 0,
|
|
2430
|
+
decryptionKeyId: origin?.decryptionKeyId ?? indexEntry?.decryptionKeyId ?? "",
|
|
2431
|
+
createdByPublicKey: origin?.createdByPublicKey ?? "",
|
|
2432
|
+
});
|
|
2433
|
+
}
|
|
2434
|
+
results.sort((left, right) => left.docId.localeCompare(right.docId));
|
|
2435
|
+
return results;
|
|
2436
|
+
}
|
|
2260
2437
|
/**
|
|
2261
2438
|
* Derive the per-doc visibility summary needed to reconcile the index
|
|
2262
2439
|
* against the current KeyBag state.
|
|
@@ -2392,6 +2569,9 @@ class BaseMindooDB {
|
|
|
2392
2569
|
await this.tenant.ingestSealedFromEntries?.(visibility.decryptionKeyId, metadataByDoc.get(docId));
|
|
2393
2570
|
const canRead = await this.tenant.hasDecryptionKey(visibility.decryptionKeyId);
|
|
2394
2571
|
if (canRead) {
|
|
2572
|
+
// This scan is the authority on visibility, so it also heals a tally
|
|
2573
|
+
// that a visible->visible short-circuit below would leave untouched.
|
|
2574
|
+
this.forgetInaccessibleDocument(docId);
|
|
2395
2575
|
// Reveal-on-add only triggers when the previous state was
|
|
2396
2576
|
// missing or inaccessible; visible->visible transitions stay
|
|
2397
2577
|
// idempotent via the existing `updateIndex` short-circuit.
|
|
@@ -2411,8 +2591,11 @@ class BaseMindooDB {
|
|
|
2411
2591
|
changed = true;
|
|
2412
2592
|
}
|
|
2413
2593
|
else {
|
|
2414
|
-
// Already inaccessible (or never seen
|
|
2415
|
-
//
|
|
2594
|
+
// Already inaccessible (or never seen, which is the common case on a
|
|
2595
|
+
// device whose keys have not arrived). No index entry is written for
|
|
2596
|
+
// it, so the tally is the only record that it is here at all.
|
|
2597
|
+
this.noteInaccessibleDocument(docId);
|
|
2598
|
+
// Scrub defensively in case a previous session left an L2 record behind.
|
|
2416
2599
|
await this.purgeMaterializedDocument(docId);
|
|
2417
2600
|
}
|
|
2418
2601
|
}
|
|
@@ -2786,6 +2969,10 @@ class BaseMindooDB {
|
|
|
2786
2969
|
const keyAvailable = await this.tenant.hasDecryptionKey(representativeEntry.decryptionKeyId);
|
|
2787
2970
|
if (!keyAvailable) {
|
|
2788
2971
|
this.logger.debug(`Skipping metadata-first index for doc ${docId} — decryption key "${representativeEntry.decryptionKeyId}" not available`);
|
|
2972
|
+
// This is where a device that is still waiting for its keys meets
|
|
2973
|
+
// most of the tenant: the doc is skipped before it ever reaches
|
|
2974
|
+
// the index, so record it for `getInaccessibleDocumentCount`.
|
|
2975
|
+
this.noteInaccessibleDocument(docId);
|
|
2789
2976
|
const existingIndex = this.getDocIndexPosition(docId);
|
|
2790
2977
|
const existing = existingIndex === undefined ? undefined : this.index[existingIndex];
|
|
2791
2978
|
if (existing?.accessState === "visible") {
|
|
@@ -2840,6 +3027,7 @@ class BaseMindooDB {
|
|
|
2840
3027
|
// doc.
|
|
2841
3028
|
if (error instanceof errors_1.SymmetricKeyNotFoundError) {
|
|
2842
3029
|
this.logger.debug(`Skipping document ${docId} - missing key: ${error.keyId}`);
|
|
3030
|
+
this.noteInaccessibleDocument(docId);
|
|
2843
3031
|
// Avoid a per-doc scanAllMetadata round-trip here: everything
|
|
2844
3032
|
// we need is already in this batch's entryMetadataList plus the
|
|
2845
3033
|
// existing index entry (if any). We do NOT need a second pass
|
|
@@ -3150,54 +3338,8 @@ class BaseMindooDB {
|
|
|
3150
3338
|
nextCursor: cursor,
|
|
3151
3339
|
};
|
|
3152
3340
|
}
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
* error. Used on the target for missing-id pre-screening (callers fall back
|
|
3156
|
-
* to exact checks) and on the source for the total-entry-count progress
|
|
3157
|
-
* denominator.
|
|
3158
|
-
*/
|
|
3159
|
-
async getStoreBloomSummary(store) {
|
|
3160
|
-
if (typeof store.getIdBloomSummary !== "function") {
|
|
3161
|
-
return null;
|
|
3162
|
-
}
|
|
3163
|
-
try {
|
|
3164
|
-
return await store.getIdBloomSummary();
|
|
3165
|
-
}
|
|
3166
|
-
catch (error) {
|
|
3167
|
-
this.logger.warn("Failed to get bloom summary from store, falling back to exact checks", error);
|
|
3168
|
-
return null;
|
|
3169
|
-
}
|
|
3170
|
-
}
|
|
3171
|
-
static setSyncAbortSignalOnStore(store, signal) {
|
|
3172
|
-
if ('setSyncAbortSignal' in store && typeof store.setSyncAbortSignal === 'function') {
|
|
3173
|
-
store.setSyncAbortSignal(signal);
|
|
3174
|
-
}
|
|
3175
|
-
}
|
|
3176
|
-
/**
|
|
3177
|
-
* Fetch a store's head descriptor (`{ epoch, maxReceiptOrder }`), returning
|
|
3178
|
-
* null when the store (or the remote server behind it) does not support it
|
|
3179
|
-
* or the request fails. A null head simply disables the persisted-cursor
|
|
3180
|
-
* fast path for this sync.
|
|
3181
|
-
*/
|
|
3182
|
-
async getStoreHeadSafe(store) {
|
|
3183
|
-
if (typeof store.getStoreHead !== "function") {
|
|
3184
|
-
return null;
|
|
3185
|
-
}
|
|
3186
|
-
try {
|
|
3187
|
-
return await store.getStoreHead();
|
|
3188
|
-
}
|
|
3189
|
-
catch (error) {
|
|
3190
|
-
this.logger.debug("Store head unavailable, falling back to full scan", error);
|
|
3191
|
-
return null;
|
|
3192
|
-
}
|
|
3193
|
-
}
|
|
3194
|
-
/** Stable identity for a store in the persisted sync-cursor map. */
|
|
3195
|
-
static syncStoreIdentity(store) {
|
|
3196
|
-
return store.getCacheIdentity?.() ?? `${store.getId()}/${store.getStoreKind()}`;
|
|
3197
|
-
}
|
|
3198
|
-
/** Key for {@link syncScanCursors}: one record per (source, target) pair. */
|
|
3199
|
-
syncScanCursorKey(sourceStore, targetStore) {
|
|
3200
|
-
return `${BaseMindooDB.syncStoreIdentity(sourceStore)}->${BaseMindooDB.syncStoreIdentity(targetStore)}`;
|
|
3341
|
+
getStoreBloomSummary(store) {
|
|
3342
|
+
return (0, syncStores_1.getStoreBloomSummary)(store, this.logger);
|
|
3201
3343
|
}
|
|
3202
3344
|
/** Persist a sync scan cursor and schedule a checkpoint flush. */
|
|
3203
3345
|
saveSyncScanCursor(key, record) {
|
|
@@ -3213,171 +3355,8 @@ class BaseMindooDB {
|
|
|
3213
3355
|
this.cacheMetaDirty = true;
|
|
3214
3356
|
this.cacheManager?.markDirty();
|
|
3215
3357
|
}
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
* missing. Uses bloom-filter pre-screening when available, then falls
|
|
3219
|
-
* back to exact `hasEntries` for the uncertain set.
|
|
3220
|
-
*/
|
|
3221
|
-
async filterMissingIds(targetStore, candidateIds, bloom) {
|
|
3222
|
-
let definitelyMissing = [];
|
|
3223
|
-
let maybeExisting = candidateIds;
|
|
3224
|
-
if (bloom) {
|
|
3225
|
-
definitelyMissing = [];
|
|
3226
|
-
maybeExisting = [];
|
|
3227
|
-
for (const id of candidateIds) {
|
|
3228
|
-
if ((0, bloom_1.bloomMightContainId)(bloom, id)) {
|
|
3229
|
-
maybeExisting.push(id);
|
|
3230
|
-
}
|
|
3231
|
-
else {
|
|
3232
|
-
definitelyMissing.push(id);
|
|
3233
|
-
}
|
|
3234
|
-
}
|
|
3235
|
-
}
|
|
3236
|
-
let missingIds = definitelyMissing;
|
|
3237
|
-
if (maybeExisting.length > 0) {
|
|
3238
|
-
const existing = await targetStore.hasEntries(maybeExisting);
|
|
3239
|
-
const existingSet = new Set(existing);
|
|
3240
|
-
missingIds = missingIds.concat(maybeExisting.filter((id) => !existingSet.has(id)));
|
|
3241
|
-
}
|
|
3242
|
-
return missingIds;
|
|
3243
|
-
}
|
|
3244
|
-
resolveTransferBatchSize(options) {
|
|
3245
|
-
if (options?.transferBatchSize && options.transferBatchSize > 0) {
|
|
3246
|
-
return options.transferBatchSize;
|
|
3247
|
-
}
|
|
3248
|
-
if (options?.storeKind === types_2.StoreKind.attachments) {
|
|
3249
|
-
return 100;
|
|
3250
|
-
}
|
|
3251
|
-
return Math.min(options?.pageSize ?? 1000, BaseMindooDB.DEFAULT_DOCS_TRANSFER_BATCH_SIZE);
|
|
3252
|
-
}
|
|
3253
|
-
/**
|
|
3254
|
-
* Transfer a set of entry IDs from source to target in fixed-size batches,
|
|
3255
|
-
* emitting progress and checking for cancellation between each batch.
|
|
3256
|
-
*
|
|
3257
|
-
* Callers (cursor-scan path and legacy path) collect the IDs that need
|
|
3258
|
-
* transferring, then delegate to this method instead of issuing one
|
|
3259
|
-
* monolithic `getEntries`. This gives three benefits:
|
|
3260
|
-
*
|
|
3261
|
-
* 1. The UI receives frequent progress updates with batch metadata so long
|
|
3262
|
-
* transfers no longer look frozen.
|
|
3263
|
-
* 2. Cancellation can interrupt work between batches rather than waiting
|
|
3264
|
-
* for one large HTTP response to complete.
|
|
3265
|
-
* 3. Each server-side `getEntries` + RSA encryption unit is smaller,
|
|
3266
|
-
* reducing the risk of socket timeouts on large payloads.
|
|
3267
|
-
*
|
|
3268
|
-
* Returns partial progress on cancellation so callers can report how much
|
|
3269
|
-
* was actually transferred before the abort.
|
|
3270
|
-
*/
|
|
3271
|
-
async transferEntriesInBatches(sourceStore, targetStore, entryIds, options, state) {
|
|
3272
|
-
if (entryIds.length === 0) {
|
|
3273
|
-
return { transferred: state.transferred, transferredBytes: state.transferredBytes, cancelled: false, rejected: [] };
|
|
3274
|
-
}
|
|
3275
|
-
const onProgress = options?.onProgress;
|
|
3276
|
-
const signal = options?.signal;
|
|
3277
|
-
const transferBatchSize = this.resolveTransferBatchSize(options);
|
|
3278
|
-
const totalTransferBatches = Math.max(1, Math.ceil(entryIds.length / transferBatchSize));
|
|
3279
|
-
// Parallel transfer window (sync-v5, phase 4): each batch is one
|
|
3280
|
-
// getEntries + putEntries round trip; a small window of concurrent
|
|
3281
|
-
// batches overlaps network latency and server-side crypto. Witness
|
|
3282
|
-
// receipts are applied per batch and are order-independent.
|
|
3283
|
-
const maxConcurrent = Math.min(Math.max(1, Math.floor(options?.maxConcurrentBatches ?? BaseMindooDB.DEFAULT_MAX_CONCURRENT_TRANSFER_BATCHES)), totalTransferBatches);
|
|
3284
|
-
let transferred = state.transferred;
|
|
3285
|
-
let transferredBytes = state.transferredBytes;
|
|
3286
|
-
let cancelled = false;
|
|
3287
|
-
let firstError = null;
|
|
3288
|
-
let nextBatchIndex = 0;
|
|
3289
|
-
// Per-entry rejections reported by a witnessing target (sync-v5): the
|
|
3290
|
-
// push continues, the rejected entries are surfaced to the caller.
|
|
3291
|
-
const rejected = [];
|
|
3292
|
-
const runWorker = async () => {
|
|
3293
|
-
while (true) {
|
|
3294
|
-
if (firstError || cancelled || signal?.aborted) {
|
|
3295
|
-
if (signal?.aborted)
|
|
3296
|
-
cancelled = true;
|
|
3297
|
-
return;
|
|
3298
|
-
}
|
|
3299
|
-
const batchIndex = nextBatchIndex++;
|
|
3300
|
-
const offset = batchIndex * transferBatchSize;
|
|
3301
|
-
if (offset >= entryIds.length) {
|
|
3302
|
-
return;
|
|
3303
|
-
}
|
|
3304
|
-
const currentTransferBatch = batchIndex + 1;
|
|
3305
|
-
const batchIds = entryIds.slice(offset, offset + transferBatchSize);
|
|
3306
|
-
const pageSummary = state.currentPage ? `page ${state.currentPage}, ` : "";
|
|
3307
|
-
onProgress?.({
|
|
3308
|
-
phase: "transferring",
|
|
3309
|
-
message: `Transferring batch ${currentTransferBatch}/${totalTransferBatches} (${batchIds.length} entries, ${pageSummary}scanned ${state.scanned})...`,
|
|
3310
|
-
transferredEntries: transferred,
|
|
3311
|
-
transferredBytes,
|
|
3312
|
-
scannedEntries: state.scanned,
|
|
3313
|
-
totalSourceEntries: state.totalSourceEntries,
|
|
3314
|
-
currentPage: state.currentPage,
|
|
3315
|
-
currentTransferBatch,
|
|
3316
|
-
totalTransferBatches,
|
|
3317
|
-
transferBatchSize,
|
|
3318
|
-
});
|
|
3319
|
-
try {
|
|
3320
|
-
const batchEntries = await sourceStore.getEntries(batchIds);
|
|
3321
|
-
if (signal?.aborted) {
|
|
3322
|
-
cancelled = true;
|
|
3323
|
-
return;
|
|
3324
|
-
}
|
|
3325
|
-
const putResult = await targetStore.putEntries(batchEntries);
|
|
3326
|
-
// On push, the remote (witnessing) target returns receipts for accepted
|
|
3327
|
-
// entries; persist them back onto the local source so the revision feed
|
|
3328
|
-
// re-anchors them from the provisional head to their committed
|
|
3329
|
-
// `receivedAt` (docs/accesscontrol.md §5.3). Pull targets return void.
|
|
3330
|
-
// sync-v5 targets return a structured ack that additionally carries
|
|
3331
|
-
// per-entry rejections (signature-class failures the remote skipped).
|
|
3332
|
-
const { receipts, batchRejected } = BaseMindooDB.normalizePutResult(putResult);
|
|
3333
|
-
if (receipts.length > 0 && sourceStore.applyWitnessReceipts) {
|
|
3334
|
-
await sourceStore.applyWitnessReceipts(receipts);
|
|
3335
|
-
}
|
|
3336
|
-
const rejectedIds = batchRejected.length > 0 ? new Set(batchRejected.map((entry) => entry.id)) : null;
|
|
3337
|
-
if (rejectedIds) {
|
|
3338
|
-
rejected.push(...batchRejected);
|
|
3339
|
-
this.logger.warn(`Target rejected ${batchRejected.length} of ${batchEntries.length} entries in transfer batch ${currentTransferBatch}/${totalTransferBatches}`);
|
|
3340
|
-
}
|
|
3341
|
-
for (const entry of batchEntries) {
|
|
3342
|
-
if (rejectedIds?.has(entry.id)) {
|
|
3343
|
-
continue;
|
|
3344
|
-
}
|
|
3345
|
-
transferredBytes += entry.encryptedSize ?? entry.encryptedData.length;
|
|
3346
|
-
}
|
|
3347
|
-
transferred += batchEntries.length - batchRejected.length;
|
|
3348
|
-
}
|
|
3349
|
-
catch (error) {
|
|
3350
|
-
if (signal?.aborted) {
|
|
3351
|
-
cancelled = true;
|
|
3352
|
-
return;
|
|
3353
|
-
}
|
|
3354
|
-
if (!firstError) {
|
|
3355
|
-
firstError = error;
|
|
3356
|
-
}
|
|
3357
|
-
return;
|
|
3358
|
-
}
|
|
3359
|
-
onProgress?.({
|
|
3360
|
-
phase: "transferring",
|
|
3361
|
-
message: `Transferred ${transferred} entries after batch ${currentTransferBatch}/${totalTransferBatches}`,
|
|
3362
|
-
transferredEntries: transferred,
|
|
3363
|
-
transferredBytes,
|
|
3364
|
-
scannedEntries: state.scanned,
|
|
3365
|
-
totalSourceEntries: state.totalSourceEntries,
|
|
3366
|
-
currentPage: state.currentPage,
|
|
3367
|
-
currentTransferBatch,
|
|
3368
|
-
totalTransferBatches,
|
|
3369
|
-
transferBatchSize,
|
|
3370
|
-
});
|
|
3371
|
-
}
|
|
3372
|
-
};
|
|
3373
|
-
await Promise.all(Array.from({ length: maxConcurrent }, () => runWorker()));
|
|
3374
|
-
if (signal?.aborted || cancelled) {
|
|
3375
|
-
return { transferred, transferredBytes, cancelled: true, rejected };
|
|
3376
|
-
}
|
|
3377
|
-
if (firstError) {
|
|
3378
|
-
throw firstError;
|
|
3379
|
-
}
|
|
3380
|
-
return { transferred, transferredBytes, cancelled: false, rejected };
|
|
3358
|
+
filterMissingIds(targetStore, candidateIds, bloom) {
|
|
3359
|
+
return (0, syncStores_1.filterMissingIds)(targetStore, candidateIds, bloom);
|
|
3381
3360
|
}
|
|
3382
3361
|
/**
|
|
3383
3362
|
* Normalize the polymorphic putEntries result (`void`, receipts array, or
|
|
@@ -3395,15 +3374,39 @@ class BaseMindooDB {
|
|
|
3395
3374
|
}
|
|
3396
3375
|
return { receipts: [], batchRejected: [] };
|
|
3397
3376
|
}
|
|
3377
|
+
/**
|
|
3378
|
+
* Expose this database's persisted {@link syncScanCursors} map to the shared
|
|
3379
|
+
* store-transfer helper. The map lives in the metadata checkpoint, so saving
|
|
3380
|
+
* a cursor also marks the cache dirty.
|
|
3381
|
+
*/
|
|
3382
|
+
syncCursorStore() {
|
|
3383
|
+
return {
|
|
3384
|
+
get: (key) => this.syncScanCursors.get(key) ?? null,
|
|
3385
|
+
save: (key, record) => this.saveSyncScanCursor(key, record),
|
|
3386
|
+
delete: (key) => {
|
|
3387
|
+
this.syncScanCursors.delete(key);
|
|
3388
|
+
},
|
|
3389
|
+
};
|
|
3390
|
+
}
|
|
3398
3391
|
async syncEntriesFromStore(sourceStore, targetStore, options) {
|
|
3399
3392
|
const signal = options?.signal;
|
|
3400
|
-
|
|
3401
|
-
|
|
3393
|
+
(0, syncStores_1.setSyncAbortSignalOnStore)(sourceStore, signal);
|
|
3394
|
+
(0, syncStores_1.setSyncAbortSignalOnStore)(targetStore, signal);
|
|
3402
3395
|
try {
|
|
3403
3396
|
if (options?.mode === "dense") {
|
|
3404
3397
|
return await this.syncEntriesFromStoreDense(sourceStore, targetStore, options);
|
|
3405
3398
|
}
|
|
3406
|
-
|
|
3399
|
+
// Client rejection semantics: the scan cursor advances past a rejected
|
|
3400
|
+
// entry so a single corrupt or forged entry cannot block this database's
|
|
3401
|
+
// push sync forever (docs/network-sync-protocol.md §5.7.6). The
|
|
3402
|
+
// server-to-server replicator passes `"hold"` instead, because there a
|
|
3403
|
+
// rejection usually means "the peer has not replicated the directory
|
|
3404
|
+
// yet" and skipping would drop the entry permanently.
|
|
3405
|
+
return await (0, syncStores_1.syncEntriesBetweenStores)(sourceStore, targetStore, options, {
|
|
3406
|
+
logger: this.logger,
|
|
3407
|
+
cursors: this.syncCursorStore(),
|
|
3408
|
+
rejectionPolicy: "advance",
|
|
3409
|
+
});
|
|
3407
3410
|
}
|
|
3408
3411
|
catch (error) {
|
|
3409
3412
|
if (signal?.aborted) {
|
|
@@ -3413,258 +3416,10 @@ class BaseMindooDB {
|
|
|
3413
3416
|
throw error;
|
|
3414
3417
|
}
|
|
3415
3418
|
finally {
|
|
3416
|
-
|
|
3417
|
-
|
|
3419
|
+
(0, syncStores_1.setSyncAbortSignalOnStore)(sourceStore, undefined);
|
|
3420
|
+
(0, syncStores_1.setSyncAbortSignalOnStore)(targetStore, undefined);
|
|
3418
3421
|
}
|
|
3419
3422
|
}
|
|
3420
|
-
async syncEntriesFromStoreImpl(sourceStore, targetStore, options) {
|
|
3421
|
-
let transferred = 0;
|
|
3422
|
-
let transferredBytes = 0;
|
|
3423
|
-
let scanned = 0;
|
|
3424
|
-
// Per-entry rejections reported by a witnessing target (sync-v5): the
|
|
3425
|
-
// sync completes; rejected entries are surfaced to the caller as warnings.
|
|
3426
|
-
const rejected = [];
|
|
3427
|
-
const onProgress = options?.onProgress;
|
|
3428
|
-
const pageSize = options?.pageSize ?? 1000;
|
|
3429
|
-
const signal = options?.signal;
|
|
3430
|
-
if (this.supportsCursorScan(sourceStore)) {
|
|
3431
|
-
// Persisted-cursor fast path (sync-v5, phase 1): resume the metadata
|
|
3432
|
-
// scan where the previous sync between this (source, target) pair left
|
|
3433
|
-
// off — or skip the sync entirely when the source head shows nothing
|
|
3434
|
-
// new. The heads are fetched up front (cheap; one request per side for
|
|
3435
|
-
// network stores) and also anchor the epochs persisted with the cursor.
|
|
3436
|
-
const cursorKey = this.syncScanCursorKey(sourceStore, targetStore);
|
|
3437
|
-
const persisted = options?.forceFullScan
|
|
3438
|
-
? null
|
|
3439
|
-
: this.syncScanCursors.get(cursorKey) ?? null;
|
|
3440
|
-
const [sourceHead, targetHead] = await Promise.all([
|
|
3441
|
-
this.getStoreHeadSafe(sourceStore),
|
|
3442
|
-
this.getStoreHeadSafe(targetStore),
|
|
3443
|
-
]);
|
|
3444
|
-
let cursor = null;
|
|
3445
|
-
if (persisted && sourceHead && targetHead) {
|
|
3446
|
-
if (persisted.sourceEpoch === sourceHead.epoch &&
|
|
3447
|
-
persisted.targetEpoch === targetHead.epoch) {
|
|
3448
|
-
if (sourceHead.maxReceiptOrder <= persisted.cursor.receiptOrder) {
|
|
3449
|
-
this.logger.debug(`Sync skip: source head ${sourceHead.maxReceiptOrder} already covered by persisted cursor (${cursorKey})`);
|
|
3450
|
-
onProgress?.({
|
|
3451
|
-
phase: 'preparing',
|
|
3452
|
-
message: 'Source unchanged since last sync, nothing to scan',
|
|
3453
|
-
transferredEntries: 0,
|
|
3454
|
-
scannedEntries: 0,
|
|
3455
|
-
});
|
|
3456
|
-
return { transferred: 0, scanned: 0, cancelled: false };
|
|
3457
|
-
}
|
|
3458
|
-
cursor = persisted.cursor;
|
|
3459
|
-
this.logger.debug(`Resuming sync scan from persisted cursor receiptOrder=${cursor.receiptOrder} (${cursorKey})`);
|
|
3460
|
-
}
|
|
3461
|
-
else {
|
|
3462
|
-
// Epoch change on either side: the cursor lineage is broken
|
|
3463
|
-
// (store reset / receipt-order migration) — full rescan.
|
|
3464
|
-
this.logger.info(`Sync cursor epoch changed for ${cursorKey}, discarding persisted cursor and re-scanning`);
|
|
3465
|
-
this.syncScanCursors.delete(cursorKey);
|
|
3466
|
-
}
|
|
3467
|
-
}
|
|
3468
|
-
const persistScanCursor = (finalCursor) => {
|
|
3469
|
-
if (finalCursor && sourceHead && targetHead) {
|
|
3470
|
-
this.saveSyncScanCursor(cursorKey, {
|
|
3471
|
-
sourceEpoch: sourceHead.epoch,
|
|
3472
|
-
targetEpoch: targetHead.epoch,
|
|
3473
|
-
cursor: finalCursor,
|
|
3474
|
-
});
|
|
3475
|
-
}
|
|
3476
|
-
};
|
|
3477
|
-
const targetBloom = await this.getStoreBloomSummary(targetStore);
|
|
3478
|
-
// Fixed progress denominator: the SOURCE's total entry count. The cursor
|
|
3479
|
-
// scan below examines every source entry, so `scannedEntries/totalSourceEntries`
|
|
3480
|
-
// is a real completion ratio. (Local stores serve this from a cached bloom
|
|
3481
|
-
// summary; network stores answer with one extra request.) The target bloom
|
|
3482
|
-
// stays dedicated to missing-id pre-screening.
|
|
3483
|
-
const sourceBloom = await this.getStoreBloomSummary(sourceStore);
|
|
3484
|
-
const totalSourceEstimate = sourceBloom?.totalIds;
|
|
3485
|
-
onProgress?.({
|
|
3486
|
-
phase: 'preparing',
|
|
3487
|
-
message: 'Preparing to sync entries...',
|
|
3488
|
-
transferredEntries: 0,
|
|
3489
|
-
scannedEntries: 0,
|
|
3490
|
-
totalSourceEntries: totalSourceEstimate,
|
|
3491
|
-
});
|
|
3492
|
-
// Upper scan bound: the source head captured BEFORE the scan started.
|
|
3493
|
-
// Pushing to a witnessing target re-anchors the just-transferred
|
|
3494
|
-
// entries to a fresh receiptOrder on the source (applyWitnessReceipts),
|
|
3495
|
-
// i.e. past the running cursor — without this bound the scan would
|
|
3496
|
-
// re-discover its own transfers and never terminate (first-sync loop).
|
|
3497
|
-
// Entries beyond the bound (re-anchored or written concurrently) are
|
|
3498
|
-
// picked up by the next sync.
|
|
3499
|
-
const scanUpperBound = sourceHead?.maxReceiptOrder;
|
|
3500
|
-
// IDs already transferred in this sync session. Guards against
|
|
3501
|
-
// re-pushing entries the scan re-discovers when the target bloom
|
|
3502
|
-
// snapshot is stale (fetched once up front; empty on a first sync, so
|
|
3503
|
-
// it would classify every re-discovered id as "definitely missing"
|
|
3504
|
-
// and skip the exact hasEntries check).
|
|
3505
|
-
const transferredThisSync = new Set();
|
|
3506
|
-
let currentPage = 0;
|
|
3507
|
-
// Scan-page pipelining (sync-v5, phase 4): while page N is being
|
|
3508
|
-
// filtered and transferred, page N+1 is already being fetched.
|
|
3509
|
-
let nextPagePromise = null;
|
|
3510
|
-
const discardPrefetch = () => {
|
|
3511
|
-
// Swallow errors of an in-flight prefetch we will never consume
|
|
3512
|
-
// (abort/cancel paths) to avoid unhandled rejections.
|
|
3513
|
-
nextPagePromise?.catch(() => { });
|
|
3514
|
-
nextPagePromise = null;
|
|
3515
|
-
};
|
|
3516
|
-
while (true) {
|
|
3517
|
-
if (signal?.aborted) {
|
|
3518
|
-
discardPrefetch();
|
|
3519
|
-
persistScanCursor(cursor);
|
|
3520
|
-
return { transferred, transferredBytes, scanned, cancelled: true, rejected };
|
|
3521
|
-
}
|
|
3522
|
-
const page = nextPagePromise
|
|
3523
|
-
? await nextPagePromise
|
|
3524
|
-
: await sourceStore.scanEntriesSince(cursor, pageSize);
|
|
3525
|
-
nextPagePromise = page.hasMore
|
|
3526
|
-
? sourceStore.scanEntriesSince(page.nextCursor, pageSize)
|
|
3527
|
-
: null;
|
|
3528
|
-
currentPage++;
|
|
3529
|
-
// Clamp the page to the scan bound. Pages are ordered by
|
|
3530
|
-
// (receiptOrder, id), so everything past the first out-of-bound
|
|
3531
|
-
// entry is out of bound as well.
|
|
3532
|
-
let pageEntries = page.entries;
|
|
3533
|
-
let reachedScanBound = false;
|
|
3534
|
-
if (scanUpperBound !== undefined && pageEntries.length > 0) {
|
|
3535
|
-
const inBound = pageEntries.filter((m) => (m.receiptOrder ?? 0) <= scanUpperBound);
|
|
3536
|
-
if (inBound.length < pageEntries.length) {
|
|
3537
|
-
reachedScanBound = true;
|
|
3538
|
-
pageEntries = inBound;
|
|
3539
|
-
}
|
|
3540
|
-
}
|
|
3541
|
-
scanned += pageEntries.length;
|
|
3542
|
-
if (signal?.aborted) {
|
|
3543
|
-
discardPrefetch();
|
|
3544
|
-
persistScanCursor(cursor);
|
|
3545
|
-
return { transferred, transferredBytes, scanned, cancelled: true, rejected };
|
|
3546
|
-
}
|
|
3547
|
-
if (pageEntries.length > 0) {
|
|
3548
|
-
onProgress?.({
|
|
3549
|
-
phase: 'transferring',
|
|
3550
|
-
message: `Scanned ${scanned} entries, checking for changes (page ${currentPage})...`,
|
|
3551
|
-
transferredEntries: transferred,
|
|
3552
|
-
scannedEntries: scanned,
|
|
3553
|
-
totalSourceEntries: totalSourceEstimate,
|
|
3554
|
-
currentPage,
|
|
3555
|
-
});
|
|
3556
|
-
const ids = pageEntries
|
|
3557
|
-
.map((m) => m.id)
|
|
3558
|
-
.filter((id) => !transferredThisSync.has(id));
|
|
3559
|
-
const missingIds = ids.length > 0
|
|
3560
|
-
? await this.filterMissingIds(targetStore, ids, targetBloom)
|
|
3561
|
-
: [];
|
|
3562
|
-
if (signal?.aborted) {
|
|
3563
|
-
discardPrefetch();
|
|
3564
|
-
persistScanCursor(cursor);
|
|
3565
|
-
return { transferred, transferredBytes, scanned, cancelled: true, rejected };
|
|
3566
|
-
}
|
|
3567
|
-
if (missingIds.length > 0) {
|
|
3568
|
-
const transferResult = await this.transferEntriesInBatches(sourceStore, targetStore, missingIds, options, {
|
|
3569
|
-
transferred,
|
|
3570
|
-
transferredBytes,
|
|
3571
|
-
scanned,
|
|
3572
|
-
totalSourceEntries: totalSourceEstimate,
|
|
3573
|
-
currentPage,
|
|
3574
|
-
});
|
|
3575
|
-
transferred = transferResult.transferred;
|
|
3576
|
-
transferredBytes = transferResult.transferredBytes;
|
|
3577
|
-
rejected.push(...transferResult.rejected);
|
|
3578
|
-
if (transferResult.cancelled) {
|
|
3579
|
-
// The current page's transfer did not complete: persist the
|
|
3580
|
-
// boundary of the last fully transferred page instead.
|
|
3581
|
-
discardPrefetch();
|
|
3582
|
-
persistScanCursor(cursor);
|
|
3583
|
-
return { transferred, transferredBytes, scanned, cancelled: true, rejected };
|
|
3584
|
-
}
|
|
3585
|
-
for (const id of missingIds) {
|
|
3586
|
-
transferredThisSync.add(id);
|
|
3587
|
-
}
|
|
3588
|
-
}
|
|
3589
|
-
}
|
|
3590
|
-
onProgress?.({
|
|
3591
|
-
phase: 'transferring',
|
|
3592
|
-
message: `Transferred ${transferred} entries (page ${currentPage}, scanned ${scanned})`,
|
|
3593
|
-
transferredEntries: transferred,
|
|
3594
|
-
transferredBytes,
|
|
3595
|
-
scannedEntries: scanned,
|
|
3596
|
-
totalSourceEntries: totalSourceEstimate,
|
|
3597
|
-
currentPage,
|
|
3598
|
-
});
|
|
3599
|
-
if (reachedScanBound) {
|
|
3600
|
-
// Persist the last in-bound position (NOT page.nextCursor, which
|
|
3601
|
-
// already points into the out-of-bound tail): a concurrent write
|
|
3602
|
-
// that landed between the bound and the re-anchored tail must be
|
|
3603
|
-
// scanned by the next sync.
|
|
3604
|
-
const lastInBound = pageEntries.length > 0 ? pageEntries[pageEntries.length - 1] : null;
|
|
3605
|
-
if (lastInBound) {
|
|
3606
|
-
cursor = { receiptOrder: lastInBound.receiptOrder ?? 0, id: lastInBound.id };
|
|
3607
|
-
}
|
|
3608
|
-
discardPrefetch();
|
|
3609
|
-
break;
|
|
3610
|
-
}
|
|
3611
|
-
cursor = page.nextCursor;
|
|
3612
|
-
if (!page.hasMore) {
|
|
3613
|
-
break;
|
|
3614
|
-
}
|
|
3615
|
-
}
|
|
3616
|
-
persistScanCursor(cursor);
|
|
3617
|
-
return { transferred, transferredBytes, scanned, cancelled: false, rejected };
|
|
3618
|
-
}
|
|
3619
|
-
onProgress?.({
|
|
3620
|
-
phase: 'preparing',
|
|
3621
|
-
message: 'Finding new entries...',
|
|
3622
|
-
transferredEntries: 0,
|
|
3623
|
-
scannedEntries: 0,
|
|
3624
|
-
});
|
|
3625
|
-
const targetIds = await targetStore.getAllIds();
|
|
3626
|
-
const sourceNewMetadata = await sourceStore.findNewEntries(targetIds);
|
|
3627
|
-
if (sourceNewMetadata.length === 0) {
|
|
3628
|
-
return { transferred: 0, scanned: 0, cancelled: false };
|
|
3629
|
-
}
|
|
3630
|
-
if (options?.signal?.aborted) {
|
|
3631
|
-
return { transferred: 0, scanned: 0, cancelled: true };
|
|
3632
|
-
}
|
|
3633
|
-
onProgress?.({
|
|
3634
|
-
phase: 'transferring',
|
|
3635
|
-
message: `Transferring ${sourceNewMetadata.length} entries...`,
|
|
3636
|
-
transferredEntries: transferred,
|
|
3637
|
-
scannedEntries: sourceNewMetadata.length,
|
|
3638
|
-
totalSourceEntries: sourceNewMetadata.length,
|
|
3639
|
-
});
|
|
3640
|
-
const transferResult = await this.transferEntriesInBatches(sourceStore, targetStore, sourceNewMetadata.map((m) => m.id), options, {
|
|
3641
|
-
transferred,
|
|
3642
|
-
transferredBytes,
|
|
3643
|
-
scanned: sourceNewMetadata.length,
|
|
3644
|
-
totalSourceEntries: sourceNewMetadata.length,
|
|
3645
|
-
});
|
|
3646
|
-
transferred = transferResult.transferred;
|
|
3647
|
-
transferredBytes = transferResult.transferredBytes;
|
|
3648
|
-
rejected.push(...transferResult.rejected);
|
|
3649
|
-
if (transferResult.cancelled) {
|
|
3650
|
-
return { transferred, transferredBytes, scanned: sourceNewMetadata.length, cancelled: true, rejected };
|
|
3651
|
-
}
|
|
3652
|
-
onProgress?.({
|
|
3653
|
-
phase: 'transferring',
|
|
3654
|
-
message: `Transferred ${transferred} entries`,
|
|
3655
|
-
transferredEntries: transferred,
|
|
3656
|
-
transferredBytes,
|
|
3657
|
-
scannedEntries: sourceNewMetadata.length,
|
|
3658
|
-
totalSourceEntries: sourceNewMetadata.length,
|
|
3659
|
-
});
|
|
3660
|
-
return {
|
|
3661
|
-
transferred,
|
|
3662
|
-
transferredBytes,
|
|
3663
|
-
scanned: sourceNewMetadata.length,
|
|
3664
|
-
cancelled: false,
|
|
3665
|
-
rejected,
|
|
3666
|
-
};
|
|
3667
|
-
}
|
|
3668
3423
|
/**
|
|
3669
3424
|
* Dense sync: transfer only the entries required to reconstruct the latest
|
|
3670
3425
|
* state of each document, using the batch materialization planner to skip
|
|
@@ -4082,8 +3837,10 @@ class BaseMindooDB {
|
|
|
4082
3837
|
}
|
|
4083
3838
|
const signerPublicKey = await this.resolveSignerPublicKey(useCustomSigningKey ? signingKeyPair : undefined);
|
|
4084
3839
|
const createUsernameHash = this.usernameHashFromRecord(Object.fromEntries(initialValueEntries));
|
|
4085
|
-
|
|
3840
|
+
// The id is settled before the write gate because it decides which rule
|
|
3841
|
+
// applies in `userdirectory`: a personal document is recognized by its id.
|
|
4086
3842
|
const docId = useCustomDocId ? options.id : this.generateUnusedDocId(options.idPrefix);
|
|
3843
|
+
await this.assertBuiltinWriteAllowed("doc_create", signerPublicKey, createUsernameHash, docId);
|
|
4087
3844
|
// Idempotent create: when a caller-provided id already exists locally,
|
|
4088
3845
|
// return the existing document instead of producing a duplicate doc_create.
|
|
4089
3846
|
if (useCustomDocId) {
|
|
@@ -4318,8 +4075,8 @@ class BaseMindooDB {
|
|
|
4318
4075
|
: [];
|
|
4319
4076
|
const signerPublicKey = await this.resolveSignerPublicKey(useCustomSigningKey ? signingKeyPair : undefined);
|
|
4320
4077
|
const createUsernameHash = this.usernameHashFromRecord(Object.fromEntries(initialValueEntries));
|
|
4321
|
-
await this.assertBuiltinWriteAllowed("doc_create", signerPublicKey, createUsernameHash);
|
|
4322
4078
|
const docId = useCustomDocId ? options.id : this.generateUnusedDocId(options.idPrefix);
|
|
4079
|
+
await this.assertBuiltinWriteAllowed("doc_create", signerPublicKey, createUsernameHash, docId);
|
|
4323
4080
|
const now = (0, timeSource_1.semanticNow)();
|
|
4324
4081
|
const keyId = await this.resolveCreateKeyId(options, docId);
|
|
4325
4082
|
const sealedCreate = await this.prepareSealedCreate(options, docId, keyId, now, signerPublicKey);
|
|
@@ -4631,9 +4388,10 @@ class BaseMindooDB {
|
|
|
4631
4388
|
}
|
|
4632
4389
|
/**
|
|
4633
4390
|
* Unwrap a `$sealed:` DEK from entry recipient wraps into the tenant session
|
|
4634
|
-
* cache. `loadDocumentInternal` already does this; history
|
|
4635
|
-
* must as well — otherwise Haven's revision list
|
|
4636
|
-
* on a cold tenant even when this device is
|
|
4391
|
+
* cache. `loadDocumentInternal` already does this; history, timestamp, and
|
|
4392
|
+
* DAG reads must as well — otherwise Haven's revision list and DAG explorer
|
|
4393
|
+
* throw SymmetricKeyNotFoundError on a cold tenant even when this device is
|
|
4394
|
+
* in the wrap list.
|
|
4637
4395
|
*/
|
|
4638
4396
|
async ingestSealedKeyFromEntries(entries) {
|
|
4639
4397
|
if (entries.length === 0)
|
|
@@ -5298,6 +5056,9 @@ class BaseMindooDB {
|
|
|
5298
5056
|
const startedAt = Date.now();
|
|
5299
5057
|
const resolvedTimestamp = this.resolveDagTimestamp(timestamp);
|
|
5300
5058
|
const allEntryMetadata = await this.scanAllMetadata(this.store, { docId });
|
|
5059
|
+
if (allEntryMetadata.length > 0) {
|
|
5060
|
+
await this.ingestSealedKeyFromEntries(allEntryMetadata);
|
|
5061
|
+
}
|
|
5301
5062
|
const relevantEntries = allEntryMetadata
|
|
5302
5063
|
.filter((entry) => entry.createdAt <= resolvedTimestamp && (0, DocumentDagAnalysis_1.isDagEntry)(entry));
|
|
5303
5064
|
const result = (0, DocumentDagAnalysis_1.computeDocumentDagAnalysis)(docId, relevantEntries, resolvedTimestamp);
|
|
@@ -5496,6 +5257,9 @@ class BaseMindooDB {
|
|
|
5496
5257
|
let decodedChange = null;
|
|
5497
5258
|
if (metadata.entryType !== "doc_snapshot") {
|
|
5498
5259
|
const allEntryMetadata = await this.scanAllMetadata(this.store, { docId });
|
|
5260
|
+
if (allEntryMetadata.length > 0) {
|
|
5261
|
+
await this.ingestSealedKeyFromEntries(allEntryMetadata);
|
|
5262
|
+
}
|
|
5499
5263
|
const entries = await this.store.getEntries([entryId]);
|
|
5500
5264
|
const entry = entries[0];
|
|
5501
5265
|
if (entry) {
|
|
@@ -6186,6 +5950,9 @@ class BaseMindooDB {
|
|
|
6186
5950
|
* decoded Automerge hash/dependency metadata for private replay.
|
|
6187
5951
|
*/
|
|
6188
5952
|
async loadVerifiedReplayChanges(replayEntries) {
|
|
5953
|
+
if (replayEntries.length > 0) {
|
|
5954
|
+
await this.ingestSealedKeyFromEntries(replayEntries);
|
|
5955
|
+
}
|
|
6189
5956
|
const loadedEntries = replayEntries.length > 0
|
|
6190
5957
|
? await this.store.getEntries(replayEntries.map((entry) => entry.id))
|
|
6191
5958
|
: [];
|
|
@@ -6747,7 +6514,17 @@ class BaseMindooDB {
|
|
|
6747
6514
|
if (docIds.length === 0) {
|
|
6748
6515
|
return;
|
|
6749
6516
|
}
|
|
6750
|
-
|
|
6517
|
+
// One gate call per document id: inside `userdirectory` the applicable rule
|
|
6518
|
+
// depends on the id (personal documents are owned by their creator), so a
|
|
6519
|
+
// single batch-wide check would deny an owner deleting their own data.
|
|
6520
|
+
if ((0, builtinDbInvariants_1.hasBuiltinWriteInvariant)(this.effectiveBuiltinDbId()) || this._isAdminOnlyDb) {
|
|
6521
|
+
for (const docId of docIds) {
|
|
6522
|
+
await this.assertLifecycleMutationAllowed("doc_delete", options.signingKeyPair, options.signingKeyPassword, undefined, docId);
|
|
6523
|
+
}
|
|
6524
|
+
}
|
|
6525
|
+
else {
|
|
6526
|
+
await this.assertLifecycleMutationAllowed("doc_delete", options.signingKeyPair, options.signingKeyPassword);
|
|
6527
|
+
}
|
|
6751
6528
|
this.logger.info(`Bulk-deleting ${docIds.length} documents`);
|
|
6752
6529
|
// Unlike creation, a delete must first materialize the CURRENT document
|
|
6753
6530
|
// (the doc_delete entry is an Automerge change on its heads), which costs
|
|
@@ -6794,14 +6571,14 @@ class BaseMindooDB {
|
|
|
6794
6571
|
async undeleteDocument(docId, options = {}) {
|
|
6795
6572
|
return this.undeleteDocInternal(docId, options.signingKeyPair, options.signingKeyPassword, options.bypassAccessControlPrecheck);
|
|
6796
6573
|
}
|
|
6797
|
-
async assertLifecycleMutationAllowed(op, signingKeyPair, signingKeyPassword, documentUsernameHash) {
|
|
6574
|
+
async assertLifecycleMutationAllowed(op, signingKeyPair, signingKeyPassword, documentUsernameHash, docId) {
|
|
6798
6575
|
this.assertWritable("document lifecycle mutation");
|
|
6799
6576
|
const useCustomSigningKey = signingKeyPair !== undefined && signingKeyPassword !== undefined;
|
|
6800
6577
|
if ((signingKeyPair !== undefined) !== (signingKeyPassword !== undefined)) {
|
|
6801
6578
|
throw new Error("Lifecycle mutation requires both signingKeyPair and signingKeyPassword");
|
|
6802
6579
|
}
|
|
6803
6580
|
const signerPublicKey = await this.resolveSignerPublicKey(useCustomSigningKey ? signingKeyPair : undefined);
|
|
6804
|
-
await this.assertBuiltinWriteAllowed(op, signerPublicKey, documentUsernameHash);
|
|
6581
|
+
await this.assertBuiltinWriteAllowed(op, signerPublicKey, documentUsernameHash, docId);
|
|
6805
6582
|
}
|
|
6806
6583
|
/**
|
|
6807
6584
|
* Write a delete or undelete lifecycle entry for an existing document.
|
|
@@ -6975,7 +6752,7 @@ class BaseMindooDB {
|
|
|
6975
6752
|
async deleteDocInternal(docId, signingKeyPair, signingKeyPassword, bypassPrecheck) {
|
|
6976
6753
|
const useCustomSigningKey = signingKeyPair !== undefined && signingKeyPassword !== undefined;
|
|
6977
6754
|
this.logger.debug(`Deleting document ${docId}${useCustomSigningKey ? ' using custom signing key' : ''}`);
|
|
6978
|
-
await this.assertLifecycleMutationAllowed("doc_delete", signingKeyPair, signingKeyPassword);
|
|
6755
|
+
await this.assertLifecycleMutationAllowed("doc_delete", signingKeyPair, signingKeyPassword, undefined, docId);
|
|
6979
6756
|
// Get current document
|
|
6980
6757
|
const internalDoc = await this.loadDocumentInternal(docId);
|
|
6981
6758
|
if (!internalDoc || internalDoc.isDeleted) {
|
|
@@ -6991,7 +6768,7 @@ class BaseMindooDB {
|
|
|
6991
6768
|
if (!internalDoc) {
|
|
6992
6769
|
throw new errors_1.DocumentNotFoundError(docId);
|
|
6993
6770
|
}
|
|
6994
|
-
await this.assertLifecycleMutationAllowed("doc_undelete", signingKeyPair, signingKeyPassword, this.usernameHashFromInternalDoc(internalDoc));
|
|
6771
|
+
await this.assertLifecycleMutationAllowed("doc_undelete", signingKeyPair, signingKeyPassword, this.usernameHashFromInternalDoc(internalDoc), docId);
|
|
6995
6772
|
if (!internalDoc.isDeleted) {
|
|
6996
6773
|
this.logger.debug(`Document ${docId} is already alive; undelete is a no-op`);
|
|
6997
6774
|
return;
|
|
@@ -8293,7 +8070,7 @@ class BaseMindooDB {
|
|
|
8293
8070
|
throw new Error("No Automerge changes to persist");
|
|
8294
8071
|
}
|
|
8295
8072
|
const signerPublicKey = await this.resolveSignerPublicKey(useCustomKey ? signingKeyPair : undefined);
|
|
8296
|
-
await this.assertBuiltinWriteAllowed("doc_change", signerPublicKey, this.usernameHashFromInternalDoc(internalDoc));
|
|
8073
|
+
await this.assertBuiltinWriteAllowed("doc_change", signerPublicKey, this.usernameHashFromInternalDoc(internalDoc), docId);
|
|
8297
8074
|
// Signed snapshot of the document's attachments after this change, computed
|
|
8298
8075
|
// once from the resulting document state so EVERY change path (changeDoc,
|
|
8299
8076
|
// text patches, merges, ...) carries it consistently on each produced entry.
|
|
@@ -8904,6 +8681,9 @@ class BaseMindooDB {
|
|
|
8904
8681
|
if (replayEntriesForState.length === 0 && !snapshotEntryId) {
|
|
8905
8682
|
return null;
|
|
8906
8683
|
}
|
|
8684
|
+
if (allEntryMetadata.length > 0) {
|
|
8685
|
+
await this.ingestSealedKeyFromEntries(allEntryMetadata);
|
|
8686
|
+
}
|
|
8907
8687
|
const metadataById = new Map(allEntryMetadata.map((entry) => [entry.id, entry]));
|
|
8908
8688
|
let startFromSnapshot = snapshotEntryId !== null;
|
|
8909
8689
|
const snapshotMeta = snapshotEntryId ? (metadataById.get(snapshotEntryId) || null) : null;
|
|
@@ -11936,32 +11716,6 @@ class BaseMindooDB {
|
|
|
11936
11716
|
}
|
|
11937
11717
|
}
|
|
11938
11718
|
exports.BaseMindooDB = BaseMindooDB;
|
|
11939
|
-
/**
|
|
11940
|
-
* Determine how many entry IDs to fetch per `getEntries` call during sync.
|
|
11941
|
-
*
|
|
11942
|
-
* This is intentionally separate from the metadata scan `pageSize` so that
|
|
11943
|
-
* scanning can page through large ID lists quickly while the heavier
|
|
11944
|
-
* payload downloads use a smaller batch to keep progress responsive and
|
|
11945
|
-
* cancellation timely.
|
|
11946
|
-
*
|
|
11947
|
-
* Priority: explicit option > attachment default (100) > docs default (250,
|
|
11948
|
-
* capped by pageSize).
|
|
11949
|
-
*
|
|
11950
|
-
* The docs default is intentionally smaller than the metadata scan pageSize
|
|
11951
|
-
* (1000): a full scan page transferred as one `putEntries` call serializes
|
|
11952
|
-
* to a multi-megabyte JSON body whose upload cannot report progress, risks
|
|
11953
|
-
* the HTTP timeout on slow uplinks (each retry restarts the whole body),
|
|
11954
|
-
* and can exceed the remote JSON body limit. Smaller batches keep progress
|
|
11955
|
-
* events frequent and make each POST cheap to retry.
|
|
11956
|
-
*/
|
|
11957
|
-
BaseMindooDB.DEFAULT_DOCS_TRANSFER_BATCH_SIZE = 250;
|
|
11958
|
-
/**
|
|
11959
|
-
* Default window of transfer batches running in parallel (sync-v5,
|
|
11960
|
-
* phase 4). Kept small: each in-flight batch holds one decrypted page of
|
|
11961
|
-
* entries in memory, and HTTP/2 multiplexes the requests over a single
|
|
11962
|
-
* connection anyway. Overridable via `SyncOptions.maxConcurrentBatches`.
|
|
11963
|
-
*/
|
|
11964
|
-
BaseMindooDB.DEFAULT_MAX_CONCURRENT_TRANSFER_BATCHES = 3;
|
|
11965
11719
|
/** The "access control not enforced" allow decision returned by the
|
|
11966
11720
|
* non-throwing `canCreate`/`canChange`/`canDelete` prediction helpers. */
|
|
11967
11721
|
BaseMindooDB.ACL_NOT_ENFORCED_DECISION = {
|