mindoodb 0.0.66 → 0.0.69
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 +77 -16
- package/README.md +41 -58
- package/dist/appendonlystores/network/HttpTransport.d.ts +2 -0
- package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -1
- package/dist/appendonlystores/network/HttpTransport.js +31 -6
- package/dist/appendonlystores/network/HttpTransport.js.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +18 -8
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +30 -16
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -1
- package/dist/browser/index.mjs +29 -0
- package/dist/core/BaseMindooDB.d.ts +168 -8
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +763 -74
- package/dist/core/BaseMindooDB.js.map +1 -1
- package/dist/core/appendonlystores/network/IrohNetworkTransport.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/IrohNetworkTransport.js +5 -1
- package/dist/core/appendonlystores/network/IrohNetworkTransport.js.map +1 -1
- package/dist/core/appendonlystores/network/IrohStreamIO.d.ts +10 -0
- package/dist/core/appendonlystores/network/IrohStreamIO.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/IrohStreamIO.js.map +1 -1
- package/dist/core/appendonlystores/network/types.d.ts +20 -0
- package/dist/core/appendonlystores/network/types.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/types.js +33 -0
- package/dist/core/appendonlystores/network/types.js.map +1 -1
- package/dist/core/appendonlystores/syncStores.d.ts +80 -2
- package/dist/core/appendonlystores/syncStores.d.ts.map +1 -1
- package/dist/core/appendonlystores/syncStores.js +82 -9
- package/dist/core/appendonlystores/syncStores.js.map +1 -1
- package/dist/core/appendonlystores/types.d.ts +88 -6
- package/dist/core/appendonlystores/types.d.ts.map +1 -1
- package/dist/core/appendonlystores/types.js +61 -1
- package/dist/core/appendonlystores/types.js.map +1 -1
- package/dist/core/builtinDbInvariants.d.ts +8 -5
- package/dist/core/builtinDbInvariants.d.ts.map +1 -1
- package/dist/core/builtinDbInvariants.js +8 -5
- package/dist/core/builtinDbInvariants.js.map +1 -1
- package/dist/core/expressions/evaluateExpression.d.ts.map +1 -1
- package/dist/core/expressions/evaluateExpression.js +7 -1
- package/dist/core/expressions/evaluateExpression.js.map +1 -1
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +45 -11
- package/dist/core/index.js.map +1 -1
- package/dist/core/indexing/summary/DocumentSummaryStore.d.ts +26 -3
- package/dist/core/indexing/summary/DocumentSummaryStore.d.ts.map +1 -1
- package/dist/core/indexing/summary/DocumentSummaryStore.js +100 -15
- package/dist/core/indexing/summary/DocumentSummaryStore.js.map +1 -1
- package/dist/core/indexing/summary/extractSummaryFields.d.ts +10 -1
- package/dist/core/indexing/summary/extractSummaryFields.d.ts.map +1 -1
- package/dist/core/indexing/summary/extractSummaryFields.js +34 -7
- package/dist/core/indexing/summary/extractSummaryFields.js.map +1 -1
- package/dist/core/indexing/summary/types.d.ts +6 -0
- package/dist/core/indexing/summary/types.d.ts.map +1 -1
- package/dist/core/indexing/summary/types.js +8 -1
- package/dist/core/indexing/summary/types.js.map +1 -1
- package/dist/core/quarantine/QuarantineDocument.d.ts +211 -0
- package/dist/core/quarantine/QuarantineDocument.d.ts.map +1 -0
- package/dist/core/quarantine/QuarantineDocument.js +314 -0
- package/dist/core/quarantine/QuarantineDocument.js.map +1 -0
- package/dist/core/quarantine/QuarantineView.d.ts +90 -0
- package/dist/core/quarantine/QuarantineView.d.ts.map +1 -0
- package/dist/core/quarantine/QuarantineView.js +106 -0
- package/dist/core/quarantine/QuarantineView.js.map +1 -0
- package/dist/core/quarantine/recordPushRejections.d.ts +73 -0
- package/dist/core/quarantine/recordPushRejections.d.ts.map +1 -0
- package/dist/core/quarantine/recordPushRejections.js +112 -0
- package/dist/core/quarantine/recordPushRejections.js.map +1 -0
- package/dist/core/quarantine/resubmitQuarantined.d.ts +81 -0
- package/dist/core/quarantine/resubmitQuarantined.d.ts.map +1 -0
- package/dist/core/quarantine/resubmitQuarantined.js +120 -0
- package/dist/core/quarantine/resubmitQuarantined.js.map +1 -0
- package/dist/core/types.d.ts +132 -4
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/values.d.ts +72 -0
- package/dist/core/values.d.ts.map +1 -0
- package/dist/core/values.js +107 -0
- package/dist/core/values.js.map +1 -0
- package/dist/node/server/MindooDBServer.d.ts.map +1 -1
- package/dist/node/server/MindooDBServer.js +6 -1
- package/dist/node/server/MindooDBServer.js.map +1 -1
- package/dist/node/server/peer/PeerReplicator.d.ts.map +1 -1
- package/dist/node/server/peer/PeerReplicator.js +35 -15
- package/dist/node/server/peer/PeerReplicator.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One list for both kinds of quarantine.
|
|
3
|
+
*
|
|
4
|
+
* A change can be held back in two directions, and from the user's side they are
|
|
5
|
+
* the same complaint — "something I have is not where it should be, and nobody
|
|
6
|
+
* is retrying it":
|
|
7
|
+
*
|
|
8
|
+
* - **inbound** — an entry arrived here (over peer-to-peer sync, usually) and
|
|
9
|
+
* was refused at materialization, so it sits in the local store without
|
|
10
|
+
* affecting any document. Recorded in the metadata checkpoint, local to this
|
|
11
|
+
* device, keyed per document. See `QuarantineRecord`.
|
|
12
|
+
* - **outbound** — an entry was written here and the server refused to ingest
|
|
13
|
+
* it, so the push moved on without it. Recorded as a `qtn_` document in
|
|
14
|
+
* `userdirectory`, which syncs to the whole tenant. See
|
|
15
|
+
* {@link QuarantineRecordDoc}.
|
|
16
|
+
*
|
|
17
|
+
* The two live in different places for good reasons — an inbound refusal is a
|
|
18
|
+
* local verdict that every replica reaches on its own and would be pointless to
|
|
19
|
+
* broadcast, whereas an outbound one is exactly what other members need to see —
|
|
20
|
+
* but a user looking for a missing change does not know which of the two
|
|
21
|
+
* happened. So the storage stays split and the *reading* is unified here.
|
|
22
|
+
*
|
|
23
|
+
* ## Why the directions keep their own vocabulary
|
|
24
|
+
*
|
|
25
|
+
* Their reasons are genuinely different: an inbound refusal names the check that
|
|
26
|
+
* failed on this device (`tier2_denied`, `cascade_dependent`, …), an outbound one
|
|
27
|
+
* names what the server said (`policy`, `purged`, …). Flattening both into one
|
|
28
|
+
* enum would invent equivalences that do not hold, so {@link QuarantineViewRecord}
|
|
29
|
+
* is a discriminated union: `direction` tells a caller which vocabulary `reason`
|
|
30
|
+
* is drawn from, and which fields are even present.
|
|
31
|
+
*/
|
|
32
|
+
import type { QuarantineRecord, QuarantineReason } from "../accesscontrol/materializationGuard";
|
|
33
|
+
import type { PutRejectionClass } from "../appendonlystores/types";
|
|
34
|
+
import { type QuarantineRecordDoc } from "./QuarantineDocument";
|
|
35
|
+
/** Which way the refused change was going. */
|
|
36
|
+
export type QuarantineDirection = "inbound" | "outbound";
|
|
37
|
+
/** What both directions can answer. */
|
|
38
|
+
interface QuarantineViewCommon {
|
|
39
|
+
/** Database the refused entries belong to. */
|
|
40
|
+
dbid: string;
|
|
41
|
+
/** Document the refused entries belong to. */
|
|
42
|
+
documentId: string;
|
|
43
|
+
/** The refusing side's own wording, for the audit view. */
|
|
44
|
+
detail: string;
|
|
45
|
+
/** Refused entry ids, capped at {@link MAX_QUARANTINED_ENTRY_IDS}. */
|
|
46
|
+
entryIds: string[];
|
|
47
|
+
/** How many refused entries did not fit into {@link entryIds}. */
|
|
48
|
+
omittedEntryCount: number;
|
|
49
|
+
/** When this refusal was first seen. */
|
|
50
|
+
firstSeenAt: number;
|
|
51
|
+
/** When it was last confirmed. */
|
|
52
|
+
updatedAt: number;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* One row of the unified quarantine view: all the entries of one document held
|
|
56
|
+
* back for one reason, in one direction.
|
|
57
|
+
*/
|
|
58
|
+
export type QuarantineViewRecord = (QuarantineViewCommon & {
|
|
59
|
+
direction: "inbound";
|
|
60
|
+
/** Which materialization check refused the entries on this device. */
|
|
61
|
+
reason: QuarantineReason;
|
|
62
|
+
}) | (QuarantineViewCommon & {
|
|
63
|
+
direction: "outbound";
|
|
64
|
+
/** How the target classified its refusal. */
|
|
65
|
+
reason: PutRejectionClass;
|
|
66
|
+
/** Fingerprint of the signing key whose push was refused. */
|
|
67
|
+
signingKeyFingerprint: string;
|
|
68
|
+
/** The `qtn_` document holding this record, so a caller can withdraw it. */
|
|
69
|
+
recordDocId: string;
|
|
70
|
+
});
|
|
71
|
+
/**
|
|
72
|
+
* Fold the local materialization log into view rows.
|
|
73
|
+
*
|
|
74
|
+
* The log holds one record per entry, because an inbound refusal is decided per
|
|
75
|
+
* entry; the view groups them per (database, document, reason) so one poisoned
|
|
76
|
+
* entry dragging fifty dependents along reads as one problem rather than
|
|
77
|
+
* fifty-one — matching how the outbound records are already shaped.
|
|
78
|
+
*/
|
|
79
|
+
export declare function inboundQuarantineView(log: readonly QuarantineRecord[]): QuarantineViewRecord[];
|
|
80
|
+
/** Turn verified `qtn_` records into view rows. */
|
|
81
|
+
export declare function outboundQuarantineView(records: readonly QuarantineRecordDoc[]): QuarantineViewRecord[];
|
|
82
|
+
/**
|
|
83
|
+
* The whole quarantine view, newest first.
|
|
84
|
+
*
|
|
85
|
+
* Newest first because the list is read when a change has gone missing, and the
|
|
86
|
+
* change someone is looking for is almost always the last one they made.
|
|
87
|
+
*/
|
|
88
|
+
export declare function mergeQuarantineViews(...groups: readonly (readonly QuarantineViewRecord[])[]): QuarantineViewRecord[];
|
|
89
|
+
export {};
|
|
90
|
+
//# sourceMappingURL=QuarantineView.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"QuarantineView.d.ts","sourceRoot":"","sources":["../../../src/core/quarantine/QuarantineView.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAChG,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,EAA6B,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAE3F,8CAA8C;AAC9C,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,UAAU,CAAC;AAEzD,uCAAuC;AACvC,UAAU,oBAAoB;IAC5B,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,UAAU,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,kEAAkE;IAClE,iBAAiB,EAAE,MAAM,CAAC;IAC1B,wCAAwC;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,kCAAkC;IAClC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,MAAM,MAAM,oBAAoB,GAC5B,CAAC,oBAAoB,GAAG;IACtB,SAAS,EAAE,SAAS,CAAC;IACrB,sEAAsE;IACtE,MAAM,EAAE,gBAAgB,CAAC;CAC1B,CAAC,GACF,CAAC,oBAAoB,GAAG;IACtB,SAAS,EAAE,UAAU,CAAC;IACtB,6CAA6C;IAC7C,MAAM,EAAE,iBAAiB,CAAC;IAC1B,6DAA6D;IAC7D,qBAAqB,EAAE,MAAM,CAAC;IAC9B,4EAA4E;IAC5E,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC,CAAC;AAEP;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,SAAS,gBAAgB,EAAE,GAC/B,oBAAoB,EAAE,CA4BxB;AAED,mDAAmD;AACnD,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,SAAS,mBAAmB,EAAE,GACtC,oBAAoB,EAAE,CAcxB;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,MAAM,EAAE,SAAS,CAAC,SAAS,oBAAoB,EAAE,CAAC,EAAE,GACtD,oBAAoB,EAAE,CAUxB"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* One list for both kinds of quarantine.
|
|
4
|
+
*
|
|
5
|
+
* A change can be held back in two directions, and from the user's side they are
|
|
6
|
+
* the same complaint — "something I have is not where it should be, and nobody
|
|
7
|
+
* is retrying it":
|
|
8
|
+
*
|
|
9
|
+
* - **inbound** — an entry arrived here (over peer-to-peer sync, usually) and
|
|
10
|
+
* was refused at materialization, so it sits in the local store without
|
|
11
|
+
* affecting any document. Recorded in the metadata checkpoint, local to this
|
|
12
|
+
* device, keyed per document. See `QuarantineRecord`.
|
|
13
|
+
* - **outbound** — an entry was written here and the server refused to ingest
|
|
14
|
+
* it, so the push moved on without it. Recorded as a `qtn_` document in
|
|
15
|
+
* `userdirectory`, which syncs to the whole tenant. See
|
|
16
|
+
* {@link QuarantineRecordDoc}.
|
|
17
|
+
*
|
|
18
|
+
* The two live in different places for good reasons — an inbound refusal is a
|
|
19
|
+
* local verdict that every replica reaches on its own and would be pointless to
|
|
20
|
+
* broadcast, whereas an outbound one is exactly what other members need to see —
|
|
21
|
+
* but a user looking for a missing change does not know which of the two
|
|
22
|
+
* happened. So the storage stays split and the *reading* is unified here.
|
|
23
|
+
*
|
|
24
|
+
* ## Why the directions keep their own vocabulary
|
|
25
|
+
*
|
|
26
|
+
* Their reasons are genuinely different: an inbound refusal names the check that
|
|
27
|
+
* failed on this device (`tier2_denied`, `cascade_dependent`, …), an outbound one
|
|
28
|
+
* names what the server said (`policy`, `purged`, …). Flattening both into one
|
|
29
|
+
* enum would invent equivalences that do not hold, so {@link QuarantineViewRecord}
|
|
30
|
+
* is a discriminated union: `direction` tells a caller which vocabulary `reason`
|
|
31
|
+
* is drawn from, and which fields are even present.
|
|
32
|
+
*/
|
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
+
exports.inboundQuarantineView = inboundQuarantineView;
|
|
35
|
+
exports.outboundQuarantineView = outboundQuarantineView;
|
|
36
|
+
exports.mergeQuarantineViews = mergeQuarantineViews;
|
|
37
|
+
const QuarantineDocument_1 = require("./QuarantineDocument");
|
|
38
|
+
/**
|
|
39
|
+
* Fold the local materialization log into view rows.
|
|
40
|
+
*
|
|
41
|
+
* The log holds one record per entry, because an inbound refusal is decided per
|
|
42
|
+
* entry; the view groups them per (database, document, reason) so one poisoned
|
|
43
|
+
* entry dragging fifty dependents along reads as one problem rather than
|
|
44
|
+
* fifty-one — matching how the outbound records are already shaped.
|
|
45
|
+
*/
|
|
46
|
+
function inboundQuarantineView(log) {
|
|
47
|
+
const rows = new Map();
|
|
48
|
+
for (const rec of log) {
|
|
49
|
+
const key = `${rec.dbid}\u0000${rec.docId}\u0000${rec.reason}`;
|
|
50
|
+
const existing = rows.get(key);
|
|
51
|
+
if (!existing) {
|
|
52
|
+
rows.set(key, {
|
|
53
|
+
direction: "inbound",
|
|
54
|
+
dbid: rec.dbid,
|
|
55
|
+
documentId: rec.docId,
|
|
56
|
+
reason: rec.reason,
|
|
57
|
+
detail: rec.detail,
|
|
58
|
+
entryIds: [rec.entryId],
|
|
59
|
+
omittedEntryCount: 0,
|
|
60
|
+
firstSeenAt: rec.recordedAt,
|
|
61
|
+
updatedAt: rec.recordedAt,
|
|
62
|
+
});
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (existing.entryIds.length < QuarantineDocument_1.MAX_QUARANTINED_ENTRY_IDS) {
|
|
66
|
+
existing.entryIds.push(rec.entryId);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
existing.omittedEntryCount += 1;
|
|
70
|
+
}
|
|
71
|
+
existing.firstSeenAt = Math.min(existing.firstSeenAt, rec.recordedAt);
|
|
72
|
+
existing.updatedAt = Math.max(existing.updatedAt, rec.recordedAt);
|
|
73
|
+
}
|
|
74
|
+
return [...rows.values()];
|
|
75
|
+
}
|
|
76
|
+
/** Turn verified `qtn_` records into view rows. */
|
|
77
|
+
function outboundQuarantineView(records) {
|
|
78
|
+
return records.map((rec) => ({
|
|
79
|
+
direction: "outbound",
|
|
80
|
+
dbid: rec.dbid,
|
|
81
|
+
documentId: rec.quarantinedDocId,
|
|
82
|
+
reason: rec.rejectionClass,
|
|
83
|
+
detail: rec.reason,
|
|
84
|
+
entryIds: [...rec.entryIds],
|
|
85
|
+
omittedEntryCount: rec.omittedEntryCount,
|
|
86
|
+
firstSeenAt: rec.firstSeenAt,
|
|
87
|
+
updatedAt: rec.updatedAt,
|
|
88
|
+
signingKeyFingerprint: rec.signingKeyFingerprint,
|
|
89
|
+
recordDocId: rec.docId,
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The whole quarantine view, newest first.
|
|
94
|
+
*
|
|
95
|
+
* Newest first because the list is read when a change has gone missing, and the
|
|
96
|
+
* change someone is looking for is almost always the last one they made.
|
|
97
|
+
*/
|
|
98
|
+
function mergeQuarantineViews(...groups) {
|
|
99
|
+
const merged = groups.flat();
|
|
100
|
+
merged.sort((a, b) => b.updatedAt - a.updatedAt ||
|
|
101
|
+
a.dbid.localeCompare(b.dbid) ||
|
|
102
|
+
a.documentId.localeCompare(b.documentId) ||
|
|
103
|
+
a.reason.localeCompare(b.reason));
|
|
104
|
+
return merged;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=QuarantineView.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"QuarantineView.js","sourceRoot":"","sources":["../../../src/core/quarantine/QuarantineView.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;;AAuDH,sDA8BC;AAGD,wDAgBC;AAQD,oDAYC;AAxHD,6DAA2F;AA2C3F;;;;;;;GAOG;AACH,SAAgB,qBAAqB,CACnC,GAAgC;IAEhC,MAAM,IAAI,GAAG,IAAI,GAAG,EAA2D,CAAC;IAChF,KAAK,MAAM,GAAG,IAAI,GAAG,EAAE,CAAC;QACtB,MAAM,GAAG,GAAG,GAAG,GAAG,CAAC,IAAI,SAAS,GAAG,CAAC,KAAK,SAAS,GAAG,CAAC,MAAM,EAAE,CAAC;QAC/D,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE;gBACZ,SAAS,EAAE,SAAS;gBACpB,IAAI,EAAE,GAAG,CAAC,IAAI;gBACd,UAAU,EAAE,GAAG,CAAC,KAAK;gBACrB,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,QAAQ,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC;gBACvB,iBAAiB,EAAE,CAAC;gBACpB,WAAW,EAAE,GAAG,CAAC,UAAU;gBAC3B,SAAS,EAAE,GAAG,CAAC,UAAU;aAC1B,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QACD,IAAI,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,8CAAyB,EAAE,CAAC;YACzD,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACtC,CAAC;aAAM,CAAC;YACN,QAAQ,CAAC,iBAAiB,IAAI,CAAC,CAAC;QAClC,CAAC;QACD,QAAQ,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,WAAW,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC;QACtE,QAAQ,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,SAAS,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC;IACpE,CAAC;IACD,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;AAC5B,CAAC;AAED,mDAAmD;AACnD,SAAgB,sBAAsB,CACpC,OAAuC;IAEvC,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QAC3B,SAAS,EAAE,UAAmB;QAC9B,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,UAAU,EAAE,GAAG,CAAC,gBAAgB;QAChC,MAAM,EAAE,GAAG,CAAC,cAAc;QAC1B,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,QAAQ,EAAE,CAAC,GAAG,GAAG,CAAC,QAAQ,CAAC;QAC3B,iBAAiB,EAAE,GAAG,CAAC,iBAAiB;QACxC,WAAW,EAAE,GAAG,CAAC,WAAW;QAC5B,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,qBAAqB,EAAE,GAAG,CAAC,qBAAqB;QAChD,WAAW,EAAE,GAAG,CAAC,KAAK;KACvB,CAAC,CAAC,CAAC;AACN,CAAC;AAED;;;;;GAKG;AACH,SAAgB,oBAAoB,CAClC,GAAG,MAAoD;IAEvD,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;IAC7B,MAAM,CAAC,IAAI,CACT,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CACP,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS;QACzB,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC;QAC5B,CAAC,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,CAAC,UAAU,CAAC;QACxC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,CACnC,CAAC;IACF,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn what a push left behind into quarantine records.
|
|
3
|
+
*
|
|
4
|
+
* A client push uses the `"advance"` rejection policy: an entry the server
|
|
5
|
+
* refuses is stepped over so the rest of the database keeps syncing. That is the
|
|
6
|
+
* right trade — one refused entry must not stop a device from ever pushing
|
|
7
|
+
* again — but it means the cursor will never offer that entry a second time. If
|
|
8
|
+
* nothing writes it down at this moment, the change is gone with the process.
|
|
9
|
+
*
|
|
10
|
+
* This is the one place that happens, and it is deliberately *not* inside
|
|
11
|
+
* `pushChangesTo`: the records live in `userdirectory`, so recording them is a
|
|
12
|
+
* write to a different database than the one being pushed. A core sync call that
|
|
13
|
+
* quietly wrote somewhere else would be a surprise to every caller, and an
|
|
14
|
+
* expensive one — it would open the directory on every push. Callers that want
|
|
15
|
+
* the records ask for them.
|
|
16
|
+
*/
|
|
17
|
+
import type { MindooDB, SigningKeyPair } from "../types";
|
|
18
|
+
import type { PutRejectionClass, RejectedPutEntry } from "../appendonlystores/types";
|
|
19
|
+
/**
|
|
20
|
+
* Stands in for the document of an entry whose id follows neither the document
|
|
21
|
+
* nor the attachment scheme. Such an entry still has to be recorded — losing it
|
|
22
|
+
* silently is the whole problem — and `$` cannot begin a real document id, so
|
|
23
|
+
* this cannot collide with one.
|
|
24
|
+
*/
|
|
25
|
+
export declare const UNKNOWN_QUARANTINED_DOC_ID = "$unknown";
|
|
26
|
+
/** What {@link recordPushRejections} managed to write. */
|
|
27
|
+
export interface PushQuarantineResult {
|
|
28
|
+
/** One per record written or extended. */
|
|
29
|
+
recorded: Array<{
|
|
30
|
+
documentId: string;
|
|
31
|
+
rejectionClass: PutRejectionClass;
|
|
32
|
+
entryCount: number;
|
|
33
|
+
}>;
|
|
34
|
+
/**
|
|
35
|
+
* Refused entries that belong to a quarantine record themselves. Counted
|
|
36
|
+
* rather than recorded: a record about a refused record would be refused in
|
|
37
|
+
* turn, and each refusal would name a new document, so one server that turns
|
|
38
|
+
* away `qtn_` writes could otherwise make a device generate them without end.
|
|
39
|
+
*/
|
|
40
|
+
skippedOwnRecords: number;
|
|
41
|
+
/** Records that could not be written, so a caller can log or retry them. */
|
|
42
|
+
failures: Array<{
|
|
43
|
+
documentId: string;
|
|
44
|
+
rejectionClass: PutRejectionClass;
|
|
45
|
+
error: string;
|
|
46
|
+
}>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Write a quarantine record for every document a push was refused on.
|
|
50
|
+
*
|
|
51
|
+
* Grouped by (document, class) to match how the records are keyed, so a refusal
|
|
52
|
+
* that took a document's whole causal tail with it becomes one record holding
|
|
53
|
+
* many entry ids instead of one record per entry.
|
|
54
|
+
*
|
|
55
|
+
* Never throws: a device that cannot write the record has still lost the entry,
|
|
56
|
+
* and a failure here must not turn a partially-successful push into a failed
|
|
57
|
+
* one. Failures come back in {@link PushQuarantineResult.failures}.
|
|
58
|
+
*/
|
|
59
|
+
export declare function recordPushRejections(input: {
|
|
60
|
+
/** Id of the database whose push was refused. Not `userdirectory` as a rule. */
|
|
61
|
+
dbid: string;
|
|
62
|
+
/** The tenant's `userdirectory`, where the records live. */
|
|
63
|
+
userdirectoryDb: MindooDB;
|
|
64
|
+
/** What the push reported as refused. */
|
|
65
|
+
rejected: readonly RejectedPutEntry[];
|
|
66
|
+
/** Signing public key (PEM) of this device. */
|
|
67
|
+
signingPublicKey: string;
|
|
68
|
+
subtle: SubtleCrypto;
|
|
69
|
+
signingKeyPair?: SigningKeyPair;
|
|
70
|
+
signingKeyPassword?: string;
|
|
71
|
+
now?: number;
|
|
72
|
+
}): Promise<PushQuarantineResult>;
|
|
73
|
+
//# sourceMappingURL=recordPushRejections.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recordPushRejections.d.ts","sourceRoot":"","sources":["../../../src/core/quarantine/recordPushRejections.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AACzD,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAKrF;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,aAAa,CAAC;AAErD,0DAA0D;AAC1D,MAAM,WAAW,oBAAoB;IACnC,0CAA0C;IAC1C,QAAQ,EAAE,KAAK,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,EAAE,iBAAiB,CAAC;QAClC,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC,CAAC;IACH;;;;;OAKG;IACH,iBAAiB,EAAE,MAAM,CAAC;IAC1B,4EAA4E;IAC5E,QAAQ,EAAE,KAAK,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,EAAE,iBAAiB,CAAC;QAClC,KAAK,EAAE,MAAM,CAAC;KACf,CAAC,CAAC;CACJ;AAWD;;;;;;;;;;GAUG;AACH,wBAAsB,oBAAoB,CAAC,KAAK,EAAE;IAChD,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,eAAe,EAAE,QAAQ,CAAC;IAC1B,yCAAyC;IACzC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACtC,+CAA+C;IAC/C,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,YAAY,CAAC;IACrB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAyEhC"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Turn what a push left behind into quarantine records.
|
|
4
|
+
*
|
|
5
|
+
* A client push uses the `"advance"` rejection policy: an entry the server
|
|
6
|
+
* refuses is stepped over so the rest of the database keeps syncing. That is the
|
|
7
|
+
* right trade — one refused entry must not stop a device from ever pushing
|
|
8
|
+
* again — but it means the cursor will never offer that entry a second time. If
|
|
9
|
+
* nothing writes it down at this moment, the change is gone with the process.
|
|
10
|
+
*
|
|
11
|
+
* This is the one place that happens, and it is deliberately *not* inside
|
|
12
|
+
* `pushChangesTo`: the records live in `userdirectory`, so recording them is a
|
|
13
|
+
* write to a different database than the one being pushed. A core sync call that
|
|
14
|
+
* quietly wrote somewhere else would be a surprise to every caller, and an
|
|
15
|
+
* expensive one — it would open the directory on every push. Callers that want
|
|
16
|
+
* the records ask for them.
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.UNKNOWN_QUARANTINED_DOC_ID = void 0;
|
|
20
|
+
exports.recordPushRejections = recordPushRejections;
|
|
21
|
+
const types_1 = require("../appendonlystores/types");
|
|
22
|
+
const idGeneration_1 = require("../utils/idGeneration");
|
|
23
|
+
const QuarantineDocument_1 = require("./QuarantineDocument");
|
|
24
|
+
/**
|
|
25
|
+
* Stands in for the document of an entry whose id follows neither the document
|
|
26
|
+
* nor the attachment scheme. Such an entry still has to be recorded — losing it
|
|
27
|
+
* silently is the whole problem — and `$` cannot begin a real document id, so
|
|
28
|
+
* this cannot collide with one.
|
|
29
|
+
*/
|
|
30
|
+
exports.UNKNOWN_QUARANTINED_DOC_ID = "$unknown";
|
|
31
|
+
/** Which document a refused entry belongs to. */
|
|
32
|
+
function documentIdOf(entryId) {
|
|
33
|
+
return ((0, idGeneration_1.parseDocEntryId)(entryId)?.docId ??
|
|
34
|
+
(0, idGeneration_1.parseAttachmentChunkId)(entryId)?.docId ??
|
|
35
|
+
exports.UNKNOWN_QUARANTINED_DOC_ID);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Write a quarantine record for every document a push was refused on.
|
|
39
|
+
*
|
|
40
|
+
* Grouped by (document, class) to match how the records are keyed, so a refusal
|
|
41
|
+
* that took a document's whole causal tail with it becomes one record holding
|
|
42
|
+
* many entry ids instead of one record per entry.
|
|
43
|
+
*
|
|
44
|
+
* Never throws: a device that cannot write the record has still lost the entry,
|
|
45
|
+
* and a failure here must not turn a partially-successful push into a failed
|
|
46
|
+
* one. Failures come back in {@link PushQuarantineResult.failures}.
|
|
47
|
+
*/
|
|
48
|
+
async function recordPushRejections(input) {
|
|
49
|
+
const result = {
|
|
50
|
+
recorded: [],
|
|
51
|
+
skippedOwnRecords: 0,
|
|
52
|
+
failures: [],
|
|
53
|
+
};
|
|
54
|
+
if (input.rejected.length === 0) {
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
const groups = new Map();
|
|
58
|
+
for (const entry of input.rejected) {
|
|
59
|
+
const documentId = documentIdOf(entry.id);
|
|
60
|
+
if ((0, QuarantineDocument_1.isQuarantineDocId)(documentId)) {
|
|
61
|
+
result.skippedOwnRecords += 1;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const rejectionClass = (0, types_1.rejectionClassOf)(entry);
|
|
65
|
+
const key = `${documentId}\u0000${rejectionClass}`;
|
|
66
|
+
const group = groups.get(key);
|
|
67
|
+
if (group) {
|
|
68
|
+
group.entryIds.push(entry.id);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
groups.set(key, {
|
|
72
|
+
documentId,
|
|
73
|
+
rejectionClass,
|
|
74
|
+
// The first refusal's wording stands for the group: the target repeats
|
|
75
|
+
// itself across a causal cascade, and the class is what differs.
|
|
76
|
+
reason: entry.reason,
|
|
77
|
+
entryIds: [entry.id],
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const group of groups.values()) {
|
|
82
|
+
try {
|
|
83
|
+
await (0, QuarantineDocument_1.recordPushQuarantine)({
|
|
84
|
+
db: input.userdirectoryDb,
|
|
85
|
+
signingPublicKey: input.signingPublicKey,
|
|
86
|
+
dbid: input.dbid,
|
|
87
|
+
quarantinedDocId: group.documentId,
|
|
88
|
+
rejectionClass: group.rejectionClass,
|
|
89
|
+
reason: group.reason,
|
|
90
|
+
entryIds: group.entryIds,
|
|
91
|
+
subtle: input.subtle,
|
|
92
|
+
signingKeyPair: input.signingKeyPair,
|
|
93
|
+
signingKeyPassword: input.signingKeyPassword,
|
|
94
|
+
now: input.now,
|
|
95
|
+
});
|
|
96
|
+
result.recorded.push({
|
|
97
|
+
documentId: group.documentId,
|
|
98
|
+
rejectionClass: group.rejectionClass,
|
|
99
|
+
entryCount: group.entryIds.length,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
result.failures.push({
|
|
104
|
+
documentId: group.documentId,
|
|
105
|
+
rejectionClass: group.rejectionClass,
|
|
106
|
+
error: error instanceof Error ? error.message : String(error),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=recordPushRejections.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recordPushRejections.js","sourceRoot":"","sources":["../../../src/core/quarantine/recordPushRejections.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offer quarantined entries to the server again.
|
|
3
|
+
*
|
|
4
|
+
* The push cursor has long since moved past these entries, so nothing will ever
|
|
5
|
+
* offer them on its own — that is the whole reason the records exist. Re-submit
|
|
6
|
+
* therefore goes straight to the target store with the specific entries, rather
|
|
7
|
+
* than rewinding the cursor: rewinding would re-scan the entire database to find
|
|
8
|
+
* a handful of entries, and would drag every *other* entry behind that position
|
|
9
|
+
* back through the refusal that made the cursor advance in the first place.
|
|
10
|
+
*
|
|
11
|
+
* ## Not every record may be tried again
|
|
12
|
+
*
|
|
13
|
+
* Filtered through {@link isResubmittableRejection}, and one of the exclusions is
|
|
14
|
+
* a matter of correctness rather than thrift: a `"purged"` refusal means the
|
|
15
|
+
* document's history was deliberately erased, so re-offering the entry is an
|
|
16
|
+
* attempt to undo that erasure. This function must never do it, whether it is
|
|
17
|
+
* called by a scheduler or by someone pressing a button.
|
|
18
|
+
*/
|
|
19
|
+
import type { ContentAddressedStore, PutRejectionClass, RejectedPutEntry } from "../appendonlystores/types";
|
|
20
|
+
import type { MindooDB } from "../types";
|
|
21
|
+
import { type QuarantineRecordDoc } from "./QuarantineDocument";
|
|
22
|
+
/**
|
|
23
|
+
* How many records one pass tries.
|
|
24
|
+
*
|
|
25
|
+
* A pass runs whenever the tenant directory changes, and a record that still
|
|
26
|
+
* fails costs a round trip to learn nothing new. The budget keeps a backlog from
|
|
27
|
+
* turning every directory write into a burst of pushes; the remainder is tried
|
|
28
|
+
* by the next pass.
|
|
29
|
+
*/
|
|
30
|
+
export declare const DEFAULT_MAX_RESUBMIT_RECORDS = 25;
|
|
31
|
+
/** What happened to one record. */
|
|
32
|
+
export interface ResubmitOutcome {
|
|
33
|
+
/** The `qtn_` document this came from. */
|
|
34
|
+
recordDocId: string;
|
|
35
|
+
documentId: string;
|
|
36
|
+
rejectionClass: PutRejectionClass;
|
|
37
|
+
/** Entries the target took this time. */
|
|
38
|
+
accepted: string[];
|
|
39
|
+
/** Entries refused again. */
|
|
40
|
+
refused: RejectedPutEntry[];
|
|
41
|
+
/** Entries no longer in the local store, so nothing can be offered for them. */
|
|
42
|
+
missing: string[];
|
|
43
|
+
/** Whether the record was withdrawn. */
|
|
44
|
+
cleared: boolean;
|
|
45
|
+
/** Set when the attempt itself failed, rather than the entries being refused. */
|
|
46
|
+
error?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface ResubmitResult {
|
|
49
|
+
outcomes: ResubmitOutcome[];
|
|
50
|
+
/**
|
|
51
|
+
* Records whose class rules out another attempt — permanently refused, or
|
|
52
|
+
* about a document that was purged on purpose.
|
|
53
|
+
*/
|
|
54
|
+
skippedFinal: number;
|
|
55
|
+
/** Records left for the next pass by {@link DEFAULT_MAX_RESUBMIT_RECORDS}. */
|
|
56
|
+
notAttempted: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Re-offer the entries named by `records`, all of which must describe the
|
|
60
|
+
* database that `localStore` and `remoteStore` are two ends of.
|
|
61
|
+
*
|
|
62
|
+
* A record is withdrawn only when nothing about it is refused any more. A record
|
|
63
|
+
* that is *partly* accepted is deliberately left standing rather than rewritten
|
|
64
|
+
* to name fewer entries: rewriting it is a write, a write is something to push,
|
|
65
|
+
* and a push is what brings this function round again. It over-reports until the
|
|
66
|
+
* rest passes too, which is the cheaper of the two inaccuracies.
|
|
67
|
+
*
|
|
68
|
+
* Never throws — a re-submit is a background courtesy, and one unreachable
|
|
69
|
+
* server must not take the caller down with it.
|
|
70
|
+
*/
|
|
71
|
+
export declare function resubmitQuarantinedEntries(input: {
|
|
72
|
+
/** Records about one database. */
|
|
73
|
+
records: readonly QuarantineRecordDoc[];
|
|
74
|
+
localStore: ContentAddressedStore;
|
|
75
|
+
remoteStore: ContentAddressedStore;
|
|
76
|
+
/** Where the records live, so accepted ones can be withdrawn. */
|
|
77
|
+
userdirectoryDb: MindooDB;
|
|
78
|
+
subtle: SubtleCrypto;
|
|
79
|
+
maxRecords?: number;
|
|
80
|
+
}): Promise<ResubmitResult>;
|
|
81
|
+
//# sourceMappingURL=resubmitQuarantined.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resubmitQuarantined.d.ts","sourceRoot":"","sources":["../../../src/core/quarantine/resubmitQuarantined.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EACV,qBAAqB,EACrB,iBAAiB,EACjB,gBAAgB,EACjB,MAAM,2BAA2B,CAAC;AAGnC,OAAO,KAAK,EAAE,QAAQ,EAAc,MAAM,UAAU,CAAC;AACrD,OAAO,EAAyB,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEvF;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAE/C,mCAAmC;AACnC,MAAM,WAAW,eAAe;IAC9B,0CAA0C;IAC1C,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,iBAAiB,CAAC;IAClC,yCAAyC;IACzC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,6BAA6B;IAC7B,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,gFAAgF;IAChF,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,wCAAwC;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,iFAAiF;IACjF,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,0BAA0B,CAAC,KAAK,EAAE;IACtD,kCAAkC;IAClC,OAAO,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACxC,UAAU,EAAE,qBAAqB,CAAC;IAClC,WAAW,EAAE,qBAAqB,CAAC;IACnC,iEAAiE;IACjE,eAAe,EAAE,QAAQ,CAAC;IAC1B,MAAM,EAAE,YAAY,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,cAAc,CAAC,CAkE1B"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Offer quarantined entries to the server again.
|
|
4
|
+
*
|
|
5
|
+
* The push cursor has long since moved past these entries, so nothing will ever
|
|
6
|
+
* offer them on its own — that is the whole reason the records exist. Re-submit
|
|
7
|
+
* therefore goes straight to the target store with the specific entries, rather
|
|
8
|
+
* than rewinding the cursor: rewinding would re-scan the entire database to find
|
|
9
|
+
* a handful of entries, and would drag every *other* entry behind that position
|
|
10
|
+
* back through the refusal that made the cursor advance in the first place.
|
|
11
|
+
*
|
|
12
|
+
* ## Not every record may be tried again
|
|
13
|
+
*
|
|
14
|
+
* Filtered through {@link isResubmittableRejection}, and one of the exclusions is
|
|
15
|
+
* a matter of correctness rather than thrift: a `"purged"` refusal means the
|
|
16
|
+
* document's history was deliberately erased, so re-offering the entry is an
|
|
17
|
+
* attempt to undo that erasure. This function must never do it, whether it is
|
|
18
|
+
* called by a scheduler or by someone pressing a button.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.DEFAULT_MAX_RESUBMIT_RECORDS = void 0;
|
|
22
|
+
exports.resubmitQuarantinedEntries = resubmitQuarantinedEntries;
|
|
23
|
+
const types_1 = require("../appendonlystores/types");
|
|
24
|
+
const syncStores_1 = require("../appendonlystores/syncStores");
|
|
25
|
+
const QuarantineDocument_1 = require("./QuarantineDocument");
|
|
26
|
+
/**
|
|
27
|
+
* How many records one pass tries.
|
|
28
|
+
*
|
|
29
|
+
* A pass runs whenever the tenant directory changes, and a record that still
|
|
30
|
+
* fails costs a round trip to learn nothing new. The budget keeps a backlog from
|
|
31
|
+
* turning every directory write into a burst of pushes; the remainder is tried
|
|
32
|
+
* by the next pass.
|
|
33
|
+
*/
|
|
34
|
+
exports.DEFAULT_MAX_RESUBMIT_RECORDS = 25;
|
|
35
|
+
/**
|
|
36
|
+
* Re-offer the entries named by `records`, all of which must describe the
|
|
37
|
+
* database that `localStore` and `remoteStore` are two ends of.
|
|
38
|
+
*
|
|
39
|
+
* A record is withdrawn only when nothing about it is refused any more. A record
|
|
40
|
+
* that is *partly* accepted is deliberately left standing rather than rewritten
|
|
41
|
+
* to name fewer entries: rewriting it is a write, a write is something to push,
|
|
42
|
+
* and a push is what brings this function round again. It over-reports until the
|
|
43
|
+
* rest passes too, which is the cheaper of the two inaccuracies.
|
|
44
|
+
*
|
|
45
|
+
* Never throws — a re-submit is a background courtesy, and one unreachable
|
|
46
|
+
* server must not take the caller down with it.
|
|
47
|
+
*/
|
|
48
|
+
async function resubmitQuarantinedEntries(input) {
|
|
49
|
+
const budget = input.maxRecords ?? exports.DEFAULT_MAX_RESUBMIT_RECORDS;
|
|
50
|
+
const result = { outcomes: [], skippedFinal: 0, notAttempted: 0 };
|
|
51
|
+
const candidates = [];
|
|
52
|
+
for (const record of input.records) {
|
|
53
|
+
if ((0, types_1.isResubmittableRejection)(record.rejectionClass)) {
|
|
54
|
+
candidates.push(record);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
result.skippedFinal += 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (candidates.length > budget) {
|
|
61
|
+
result.notAttempted = candidates.length - budget;
|
|
62
|
+
candidates.length = budget;
|
|
63
|
+
}
|
|
64
|
+
for (const record of candidates) {
|
|
65
|
+
const outcome = {
|
|
66
|
+
recordDocId: record.docId,
|
|
67
|
+
documentId: record.quarantinedDocId,
|
|
68
|
+
rejectionClass: record.rejectionClass,
|
|
69
|
+
accepted: [],
|
|
70
|
+
refused: [],
|
|
71
|
+
missing: [],
|
|
72
|
+
cleared: false,
|
|
73
|
+
};
|
|
74
|
+
result.outcomes.push(outcome);
|
|
75
|
+
try {
|
|
76
|
+
const entries = await input.localStore.getEntries([...record.entryIds]);
|
|
77
|
+
const found = new Map(entries.map((entry) => [entry.id, entry]));
|
|
78
|
+
outcome.missing = record.entryIds.filter((id) => !found.has(id));
|
|
79
|
+
if (entries.length === 0) {
|
|
80
|
+
// Every entry is gone locally. Nothing can ever be offered for this
|
|
81
|
+
// record, so leaving it would make it permanent.
|
|
82
|
+
await clearRecord(input, record);
|
|
83
|
+
outcome.cleared = true;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const { receipts, batchRejected } = (0, syncStores_1.normalizePutResult)(await input.remoteStore.putEntries(entries));
|
|
87
|
+
// Without this the entries stay provisional locally even though the target
|
|
88
|
+
// has now witnessed them, and their trusted time never settles.
|
|
89
|
+
if (receipts.length > 0 && input.localStore.applyWitnessReceipts) {
|
|
90
|
+
await input.localStore.applyWitnessReceipts(receipts);
|
|
91
|
+
}
|
|
92
|
+
const refusedIds = new Set(batchRejected.map((entry) => entry.id));
|
|
93
|
+
outcome.refused = batchRejected;
|
|
94
|
+
outcome.accepted = entries
|
|
95
|
+
.map((entry) => entry.id)
|
|
96
|
+
.filter((id) => !refusedIds.has(id));
|
|
97
|
+
if (outcome.refused.length === 0) {
|
|
98
|
+
await clearRecord(input, record);
|
|
99
|
+
outcome.cleared = true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
outcome.error = error instanceof Error ? error.message : String(error);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
async function clearRecord(input, record) {
|
|
109
|
+
await (0, QuarantineDocument_1.clearQuarantineRecord)({
|
|
110
|
+
db: input.userdirectoryDb,
|
|
111
|
+
key: {
|
|
112
|
+
signingKeyFingerprint: record.signingKeyFingerprint,
|
|
113
|
+
dbid: record.dbid,
|
|
114
|
+
quarantinedDocId: record.quarantinedDocId,
|
|
115
|
+
rejectionClass: record.rejectionClass,
|
|
116
|
+
},
|
|
117
|
+
subtle: input.subtle,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=resubmitQuarantined.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resubmitQuarantined.js","sourceRoot":"","sources":["../../../src/core/quarantine/resubmitQuarantined.ts"],"names":[],"mappings":"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"}
|