@cello-protocol/daemon 0.0.131 → 0.0.133
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/content-park.d.ts.map +1 -1
- package/dist/content-park.js +29 -11
- package/dist/content-park.js.map +1 -1
- package/dist/daemon.d.ts +11 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +398 -18
- package/dist/daemon.js.map +1 -1
- package/dist/document-ack-inbound.d.ts +57 -0
- package/dist/document-ack-inbound.d.ts.map +1 -0
- package/dist/document-ack-inbound.js +174 -0
- package/dist/document-ack-inbound.js.map +1 -0
- package/dist/document-control-notifier.d.ts +61 -0
- package/dist/document-control-notifier.d.ts.map +1 -0
- package/dist/document-control-notifier.js +70 -0
- package/dist/document-control-notifier.js.map +1 -0
- package/dist/document-delivery-transport.d.ts +94 -0
- package/dist/document-delivery-transport.d.ts.map +1 -0
- package/dist/document-delivery-transport.js +179 -0
- package/dist/document-delivery-transport.js.map +1 -0
- package/dist/document-delivery.d.ts +181 -0
- package/dist/document-delivery.d.ts.map +1 -0
- package/dist/document-delivery.js +289 -0
- package/dist/document-delivery.js.map +1 -0
- package/dist/document-frame-router.d.ts +210 -0
- package/dist/document-frame-router.d.ts.map +1 -0
- package/dist/document-frame-router.js +396 -0
- package/dist/document-frame-router.js.map +1 -0
- package/dist/document-handlers.d.ts +47 -0
- package/dist/document-handlers.d.ts.map +1 -0
- package/dist/document-handlers.js +657 -0
- package/dist/document-handlers.js.map +1 -0
- package/dist/document-handshake.d.ts +156 -0
- package/dist/document-handshake.d.ts.map +1 -0
- package/dist/document-handshake.js +398 -0
- package/dist/document-handshake.js.map +1 -0
- package/dist/document-inbound.d.ts +91 -0
- package/dist/document-inbound.d.ts.map +1 -0
- package/dist/document-inbound.js +290 -0
- package/dist/document-inbound.js.map +1 -0
- package/dist/document-layer.d.ts +137 -0
- package/dist/document-layer.d.ts.map +1 -0
- package/dist/document-layer.js +255 -0
- package/dist/document-layer.js.map +1 -0
- package/dist/document-lifecycle.d.ts +125 -0
- package/dist/document-lifecycle.d.ts.map +1 -0
- package/dist/document-lifecycle.js +433 -0
- package/dist/document-lifecycle.js.map +1 -0
- package/dist/document-live-docs.d.ts +58 -0
- package/dist/document-live-docs.d.ts.map +1 -0
- package/dist/document-live-docs.js +126 -0
- package/dist/document-live-docs.js.map +1 -0
- package/dist/document-notify.d.ts +173 -0
- package/dist/document-notify.d.ts.map +1 -0
- package/dist/document-notify.js +438 -0
- package/dist/document-notify.js.map +1 -0
- package/dist/document-publish.d.ts +67 -0
- package/dist/document-publish.d.ts.map +1 -0
- package/dist/document-publish.js +149 -0
- package/dist/document-publish.js.map +1 -0
- package/dist/document-reachability.d.ts +42 -0
- package/dist/document-reachability.d.ts.map +1 -0
- package/dist/document-reachability.js +80 -0
- package/dist/document-reachability.js.map +1 -0
- package/dist/document-rejection.d.ts +240 -0
- package/dist/document-rejection.d.ts.map +1 -0
- package/dist/document-rejection.js +407 -0
- package/dist/document-rejection.js.map +1 -0
- package/dist/document-store.d.ts +154 -8
- package/dist/document-store.d.ts.map +1 -1
- package/dist/document-store.js +462 -4
- package/dist/document-store.js.map +1 -1
- package/dist/document-write-path.d.ts.map +1 -1
- package/dist/document-write-path.js +10 -43
- package/dist/document-write-path.js.map +1 -1
- package/dist/inbound-sessions.d.ts +25 -0
- package/dist/inbound-sessions.d.ts.map +1 -1
- package/dist/inbound-sessions.js +60 -0
- package/dist/inbound-sessions.js.map +1 -1
- package/dist/initiate-session-handler.d.ts +24 -1
- package/dist/initiate-session-handler.d.ts.map +1 -1
- package/dist/initiate-session-handler.js +35 -9
- package/dist/initiate-session-handler.js.map +1 -1
- package/dist/ipc-server.d.ts +11 -1
- package/dist/ipc-server.d.ts.map +1 -1
- package/dist/ipc-server.js +7 -1
- package/dist/ipc-server.js.map +1 -1
- package/dist/line-lcs.d.ts +51 -0
- package/dist/line-lcs.d.ts.map +1 -0
- package/dist/line-lcs.js +71 -0
- package/dist/line-lcs.js.map +1 -0
- package/dist/notification-handlers.d.ts +2 -1
- package/dist/notification-handlers.d.ts.map +1 -1
- package/dist/notification-handlers.js +11 -2
- package/dist/notification-handlers.js.map +1 -1
- package/dist/outbound-sessions.d.ts +2 -0
- package/dist/outbound-sessions.d.ts.map +1 -1
- package/dist/outbound-sessions.js +23 -2
- package/dist/outbound-sessions.js.map +1 -1
- package/dist/session-content-handlers.d.ts.map +1 -1
- package/dist/session-content-handlers.js +3 -2
- package/dist/session-content-handlers.js.map +1 -1
- package/dist/session-node-manager.d.ts +15 -0
- package/dist/session-node-manager.d.ts.map +1 -1
- package/dist/session-node-manager.js +188 -9
- package/dist/session-node-manager.js.map +1 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +16 -0
- package/dist/vocabulary.js.map +1 -1
- package/dist/wire-content-hash.d.ts +27 -0
- package/dist/wire-content-hash.d.ts.map +1 -0
- package/dist/wire-content-hash.js +37 -0
- package/dist/wire-content-hash.js.map +1 -0
- package/package.json +5 -5
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOD-DOC-DELIVERY-1 (outbound half) — turning a local edit into a signed envelope in the log.
|
|
3
|
+
*
|
|
4
|
+
* Publish is fire-and-forget by design (§16.4): it writes the envelope and returns. Delivery is the
|
|
5
|
+
* worker's job, and pending is derived from the log, so the only thing publish has to get right is
|
|
6
|
+
* that what it writes is complete, signed, and correctly chained.
|
|
7
|
+
*
|
|
8
|
+
* ── WHAT IS PUBLISHED IS A DIFF AGAINST THE PEER'S STATE VECTOR ───────────────────────────────
|
|
9
|
+
*
|
|
10
|
+
* Not the whole document. §3.2 step 3 and §7: an update computed against what the peer has already
|
|
11
|
+
* seen carries exactly the operations they lack — including, after a rejection, the refused
|
|
12
|
+
* operations PLUS their inverses, which is what lets supersession converge. Publishing the full
|
|
13
|
+
* state instead would work but would re-send the entire document on every edit, and would lose the
|
|
14
|
+
* property that makes the rejection protocol terminate.
|
|
15
|
+
*
|
|
16
|
+
* The peer's state vector is the one they last told us about. Absent — before they have ever
|
|
17
|
+
* published — the diff is against an empty document, which is the whole state and correct.
|
|
18
|
+
*
|
|
19
|
+
* ── THE CHAIN LINK IS READ, NEVER GUESSED ─────────────────────────────────────────────────────
|
|
20
|
+
*
|
|
21
|
+
* `doc_prev_hash` is our own last envelope for this document, read from the log at publish time. A
|
|
22
|
+
* cached value would go stale the moment anything else appended — a rejection, a withdrawal — and a
|
|
23
|
+
* wrong link is not a soft failure: the peer refuses it and, if it is ever replayed locally, the
|
|
24
|
+
* document stops rebuilding.
|
|
25
|
+
*/
|
|
26
|
+
import type * as Y from "yjs";
|
|
27
|
+
import type { DocumentStore } from "./document-store.js";
|
|
28
|
+
import type { DocumentEngine } from "./document-engine.js";
|
|
29
|
+
import type { Logger } from "./types.js";
|
|
30
|
+
export type PublishResult = {
|
|
31
|
+
ok: true;
|
|
32
|
+
envelopeHash: string;
|
|
33
|
+
bytes: number;
|
|
34
|
+
} | {
|
|
35
|
+
ok: false;
|
|
36
|
+
reason: string;
|
|
37
|
+
detail: string;
|
|
38
|
+
};
|
|
39
|
+
export interface DocumentPublishDeps {
|
|
40
|
+
store: DocumentStore;
|
|
41
|
+
engine: DocumentEngine;
|
|
42
|
+
logger: Logger;
|
|
43
|
+
/** Sign as the owning agent, over the update envelope's TBS. */
|
|
44
|
+
sign(ownerAgentId: string, tbs: Uint8Array): Promise<Uint8Array>;
|
|
45
|
+
/** The owning agent's own id — which is its pubkey hex (M14-D5) — for the envelope's sender. */
|
|
46
|
+
senderIdFor(ownerAgentId: string): string | null;
|
|
47
|
+
/** May this agent publish into this document right now? LIFECYCLE-1 owns the answer. */
|
|
48
|
+
canPublish(ownerAgentId: string, documentId: string): {
|
|
49
|
+
ok: true;
|
|
50
|
+
} | {
|
|
51
|
+
ok: false;
|
|
52
|
+
reason: string;
|
|
53
|
+
detail: string;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export declare class DocumentPublish {
|
|
57
|
+
#private;
|
|
58
|
+
constructor(deps: DocumentPublishDeps);
|
|
59
|
+
/**
|
|
60
|
+
* Publish the live document's state as an update the peer does not yet have.
|
|
61
|
+
*
|
|
62
|
+
* Returns the envelope hash so a caller can follow it — a publish that says only "ok" gives an
|
|
63
|
+
* operator nothing to correlate with what later lands or stalls.
|
|
64
|
+
*/
|
|
65
|
+
publish(ownerAgentId: string, documentId: string, doc: Y.Doc, nowMs: number): Promise<PublishResult>;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=document-publish.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-publish.d.ts","sourceRoot":"","sources":["../src/document-publish.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,KAAK,CAAC,MAAM,KAAK,CAAC;AAQ9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC,MAAM,MAAM,aAAa,GACrB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAElD,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,aAAa,CAAC;IACrB,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACjE,gGAAgG;IAChG,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACjD,wFAAwF;IACxF,UAAU,CAAC,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG;QAAE,EAAE,EAAE,IAAI,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACpH;AAQD,qBAAa,eAAe;;gBAGd,IAAI,EAAE,mBAAmB;IAIrC;;;;;OAKG;IACG,OAAO,CACX,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,CAAC,CAAC,GAAG,EACV,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,aAAa,CAAC;CA6G1B"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOD-DOC-DELIVERY-1 (outbound half) — turning a local edit into a signed envelope in the log.
|
|
3
|
+
*
|
|
4
|
+
* Publish is fire-and-forget by design (§16.4): it writes the envelope and returns. Delivery is the
|
|
5
|
+
* worker's job, and pending is derived from the log, so the only thing publish has to get right is
|
|
6
|
+
* that what it writes is complete, signed, and correctly chained.
|
|
7
|
+
*
|
|
8
|
+
* ── WHAT IS PUBLISHED IS A DIFF AGAINST THE PEER'S STATE VECTOR ───────────────────────────────
|
|
9
|
+
*
|
|
10
|
+
* Not the whole document. §3.2 step 3 and §7: an update computed against what the peer has already
|
|
11
|
+
* seen carries exactly the operations they lack — including, after a rejection, the refused
|
|
12
|
+
* operations PLUS their inverses, which is what lets supersession converge. Publishing the full
|
|
13
|
+
* state instead would work but would re-send the entire document on every edit, and would lose the
|
|
14
|
+
* property that makes the rejection protocol terminate.
|
|
15
|
+
*
|
|
16
|
+
* The peer's state vector is the one they last told us about. Absent — before they have ever
|
|
17
|
+
* published — the diff is against an empty document, which is the whole state and correct.
|
|
18
|
+
*
|
|
19
|
+
* ── THE CHAIN LINK IS READ, NEVER GUESSED ─────────────────────────────────────────────────────
|
|
20
|
+
*
|
|
21
|
+
* `doc_prev_hash` is our own last envelope for this document, read from the log at publish time. A
|
|
22
|
+
* cached value would go stale the moment anything else appended — a rejection, a withdrawal — and a
|
|
23
|
+
* wrong link is not a soft failure: the peer refuses it and, if it is ever replayed locally, the
|
|
24
|
+
* document stops rebuilding.
|
|
25
|
+
*/
|
|
26
|
+
import { buildDocumentUpdateTbs, documentEnvelopeHash, DOCUMENT_EPOCH_V1, DOCUMENT_UPDATE_ENCODING_V1, } from "@cello-protocol/protocol-types";
|
|
27
|
+
function equalBytes(a, b) {
|
|
28
|
+
if (a.length !== b.length)
|
|
29
|
+
return false;
|
|
30
|
+
for (let i = 0; i < a.length; i++)
|
|
31
|
+
if (a[i] !== b[i])
|
|
32
|
+
return false;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
export class DocumentPublish {
|
|
36
|
+
#d;
|
|
37
|
+
constructor(deps) {
|
|
38
|
+
this.#d = deps;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Publish the live document's state as an update the peer does not yet have.
|
|
42
|
+
*
|
|
43
|
+
* Returns the envelope hash so a caller can follow it — a publish that says only "ok" gives an
|
|
44
|
+
* operator nothing to correlate with what later lands or stalls.
|
|
45
|
+
*/
|
|
46
|
+
async publish(ownerAgentId, documentId, doc, nowMs) {
|
|
47
|
+
const document = this.#d.store.getDocument(ownerAgentId, documentId);
|
|
48
|
+
if (!document) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
reason: "document_unknown",
|
|
52
|
+
detail: `no document ${documentId.slice(0, 16)}… for this agent`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// LIFECYCLE-1 decides. A closed, killed, stalled or platform-paused agent must not publish, and
|
|
56
|
+
// the reason it gives is the one the operator needs — not a generic refusal from here.
|
|
57
|
+
const allowed = this.#d.canPublish(ownerAgentId, documentId);
|
|
58
|
+
if (!allowed.ok)
|
|
59
|
+
return { ok: false, reason: allowed.reason, detail: allowed.detail };
|
|
60
|
+
const senderId = this.#d.senderIdFor(ownerAgentId);
|
|
61
|
+
if (senderId === null) {
|
|
62
|
+
return {
|
|
63
|
+
ok: false,
|
|
64
|
+
reason: "document_publish_no_identity",
|
|
65
|
+
detail: `no signing identity for ${ownerAgentId}, so an envelope cannot be authored`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// THE PEER's state vector, from their most recent envelope — not our own snapshot. Our
|
|
69
|
+
// snapshot answers "what have we not yet materialized", which is a different question whose
|
|
70
|
+
// answer looks plausible: it produced an update every time, so a republish with no new edits
|
|
71
|
+
// appended a second envelope instead of reporting that there was nothing to say.
|
|
72
|
+
const peerStateVector = this.#d.store.peerStateVector(ownerAgentId, documentId, document.peerAgentId) ?? undefined;
|
|
73
|
+
const update = this.#d.engine.encodeState(doc, peerStateVector);
|
|
74
|
+
// NOTHING NEW is a different question from what to SEND, and conflating them was a bug. The
|
|
75
|
+
// update is a diff against the PEER's state vector, so before they have ever published it is
|
|
76
|
+
// the whole document — non-empty however long we have gone without editing. Whether there is
|
|
77
|
+
// anything new is a fact about US: compare our current state vector against the one our own
|
|
78
|
+
// last envelope carried.
|
|
79
|
+
const ourLast = this.#d.store.lastPublishedStateVector(ownerAgentId, documentId, senderId);
|
|
80
|
+
const ourNow = this.#d.engine.encodeStateVector(doc);
|
|
81
|
+
if (ourLast !== null && equalBytes(ourLast, ourNow)) {
|
|
82
|
+
// Publishing anyway would append a leaf, cost a delivery, and converge nothing.
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
reason: "document_nothing_to_publish",
|
|
86
|
+
detail: "nothing has changed in this document since your last publish",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (update.length === 0) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
reason: "document_nothing_to_publish",
|
|
93
|
+
detail: "the peer already has everything in this document",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const envelope = {
|
|
97
|
+
type: "document_update",
|
|
98
|
+
document_id: documentId,
|
|
99
|
+
epoch_id: DOCUMENT_EPOCH_V1,
|
|
100
|
+
// READ, never cached. Anything else appended since — a rejection, a withdrawal — moves this,
|
|
101
|
+
// and a wrong link is refused by the peer and stops the document rebuilding locally.
|
|
102
|
+
doc_prev_hash: this.#d.store.lastEnvelopeHashBySender(ownerAgentId, documentId, senderId),
|
|
103
|
+
sender_agent_id: senderId,
|
|
104
|
+
sender_client_id: doc.clientID,
|
|
105
|
+
update_encoding: DOCUMENT_UPDATE_ENCODING_V1,
|
|
106
|
+
state_vector: ourNow,
|
|
107
|
+
update,
|
|
108
|
+
signature: new Uint8Array(0),
|
|
109
|
+
};
|
|
110
|
+
envelope.signature = await this.#d.sign(ownerAgentId, buildDocumentUpdateTbs(envelope));
|
|
111
|
+
const envelopeHash = documentEnvelopeHash(envelope);
|
|
112
|
+
const appended = this.#d.store.appendEnvelope(ownerAgentId, {
|
|
113
|
+
envelopeHash,
|
|
114
|
+
documentId,
|
|
115
|
+
senderAgentId: senderId,
|
|
116
|
+
docPrevHash: envelope.doc_prev_hash,
|
|
117
|
+
epochId: envelope.epoch_id,
|
|
118
|
+
signature: envelope.signature,
|
|
119
|
+
stateVector: envelope.state_vector,
|
|
120
|
+
payload: update,
|
|
121
|
+
kind: "update",
|
|
122
|
+
referencesEnvelopeHash: null,
|
|
123
|
+
// Recorded so the peer's authorship binding is derived from what we SIGNED, matching what the
|
|
124
|
+
// inbound path stores for their envelopes.
|
|
125
|
+
senderClientId: doc.clientID,
|
|
126
|
+
createdAtMs: nowMs,
|
|
127
|
+
});
|
|
128
|
+
if (!appended) {
|
|
129
|
+
// The same content published twice hashes the same, so this is a genuine no-op rather than a
|
|
130
|
+
// fault — but it is reported, because a caller that believes it published something new and
|
|
131
|
+
// waits for it to deliver would wait forever.
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
reason: "document_already_published",
|
|
135
|
+
detail: `this exact update is already in the log as ${envelopeHash.slice(0, 16)}…`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
this.#d.logger.info("document.published", {
|
|
139
|
+
documentId,
|
|
140
|
+
envelopeHash,
|
|
141
|
+
bytes: update.length,
|
|
142
|
+
senderClientId: doc.clientID,
|
|
143
|
+
});
|
|
144
|
+
// Fire and forget: the delivery worker derives pending FROM THE LOG, so writing the envelope is
|
|
145
|
+
// the whole of publishing. Nothing here waits for a peer.
|
|
146
|
+
return { ok: true, envelopeHash, bytes: update.length };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=document-publish.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-publish.js","sourceRoot":"","sources":["../src/document-publish.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOD-DOC-DELIVERY-2 — binding the delivery worker's reachability check to `discovery_lookup`.
|
|
3
|
+
*
|
|
4
|
+
* The worker asks one question — "is this peer reachable right now?" — and its whole retry
|
|
5
|
+
* behaviour turns on the answer being about the PEER. `runDiscoveryLookup` deliberately returns
|
|
6
|
+
* five outcomes, only one of which is an answer about the peer at all, and this maps them:
|
|
7
|
+
*
|
|
8
|
+
* result / offline | unknown_agent -> false the peer is not reachable. A fact about them.
|
|
9
|
+
* result / online -> true
|
|
10
|
+
* error | malformed | timeout | send_failed
|
|
11
|
+
* -> THROW the lookup did not happen. A fact about US.
|
|
12
|
+
*
|
|
13
|
+
* The throw is the point. `isPeerReachable` returning false for a directory fault would record a
|
|
14
|
+
* directory outage — or our own signaling stream being down — as the operator's collaborator being
|
|
15
|
+
* absent, and send them to ask the wrong person why nothing is syncing. The worker already treats
|
|
16
|
+
* a throw as `lookup_failed` and keeps that distinction in the log; this is what feeds it.
|
|
17
|
+
*
|
|
18
|
+
* `unknown_agent` maps to false rather than throwing on purpose. It IS an answer from the
|
|
19
|
+
* directory about the peer — the address does not resolve — and the honest consequence is the same
|
|
20
|
+
* as offline: do not dial, retry later. It is a bad address rather than a transient absence, so it
|
|
21
|
+
* is surfaced distinctly by the caller rather than being collapsed into the same log line.
|
|
22
|
+
*/
|
|
23
|
+
import type { DiscoveryOutcome } from "./cross-node-negotiation.js";
|
|
24
|
+
/** Why a lookup could not be performed. Carries the upstream reason verbatim. */
|
|
25
|
+
export declare class DiscoveryUnavailableError extends Error {
|
|
26
|
+
readonly kind: string;
|
|
27
|
+
constructor(kind: string, reason: string);
|
|
28
|
+
}
|
|
29
|
+
export interface Reachability {
|
|
30
|
+
reachable: boolean;
|
|
31
|
+
/** Present when the directory answered but the address does not resolve to a known agent. */
|
|
32
|
+
unknownAgent: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Map a discovery outcome onto the worker's reachability question.
|
|
36
|
+
*
|
|
37
|
+
* Pure, so the mapping is testable without a directory — which matters, because the expensive way
|
|
38
|
+
* to discover this mapping is wrong is in production, where the symptom is an operator chasing a
|
|
39
|
+
* collaborator who was never offline.
|
|
40
|
+
*/
|
|
41
|
+
export declare function reachabilityFromDiscovery(outcome: DiscoveryOutcome): Reachability;
|
|
42
|
+
//# sourceMappingURL=document-reachability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-reachability.d.ts","sourceRoot":"","sources":["../src/document-reachability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAEpE,iFAAiF;AACjF,qBAAa,yBAA0B,SAAQ,KAAK;IAClD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBACV,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAKzC;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,OAAO,CAAC;IACnB,6FAA6F;IAC7F,YAAY,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,gBAAgB,GAAG,YAAY,CA+CjF"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOD-DOC-DELIVERY-2 — binding the delivery worker's reachability check to `discovery_lookup`.
|
|
3
|
+
*
|
|
4
|
+
* The worker asks one question — "is this peer reachable right now?" — and its whole retry
|
|
5
|
+
* behaviour turns on the answer being about the PEER. `runDiscoveryLookup` deliberately returns
|
|
6
|
+
* five outcomes, only one of which is an answer about the peer at all, and this maps them:
|
|
7
|
+
*
|
|
8
|
+
* result / offline | unknown_agent -> false the peer is not reachable. A fact about them.
|
|
9
|
+
* result / online -> true
|
|
10
|
+
* error | malformed | timeout | send_failed
|
|
11
|
+
* -> THROW the lookup did not happen. A fact about US.
|
|
12
|
+
*
|
|
13
|
+
* The throw is the point. `isPeerReachable` returning false for a directory fault would record a
|
|
14
|
+
* directory outage — or our own signaling stream being down — as the operator's collaborator being
|
|
15
|
+
* absent, and send them to ask the wrong person why nothing is syncing. The worker already treats
|
|
16
|
+
* a throw as `lookup_failed` and keeps that distinction in the log; this is what feeds it.
|
|
17
|
+
*
|
|
18
|
+
* `unknown_agent` maps to false rather than throwing on purpose. It IS an answer from the
|
|
19
|
+
* directory about the peer — the address does not resolve — and the honest consequence is the same
|
|
20
|
+
* as offline: do not dial, retry later. It is a bad address rather than a transient absence, so it
|
|
21
|
+
* is surfaced distinctly by the caller rather than being collapsed into the same log line.
|
|
22
|
+
*/
|
|
23
|
+
/** Why a lookup could not be performed. Carries the upstream reason verbatim. */
|
|
24
|
+
export class DiscoveryUnavailableError extends Error {
|
|
25
|
+
kind;
|
|
26
|
+
constructor(kind, reason) {
|
|
27
|
+
super(`document_discovery_unavailable: ${kind}${reason ? ` (${reason})` : ""}`);
|
|
28
|
+
this.name = "DiscoveryUnavailableError";
|
|
29
|
+
this.kind = kind;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Map a discovery outcome onto the worker's reachability question.
|
|
34
|
+
*
|
|
35
|
+
* Pure, so the mapping is testable without a directory — which matters, because the expensive way
|
|
36
|
+
* to discover this mapping is wrong is in production, where the symptom is an operator chasing a
|
|
37
|
+
* collaborator who was never offline.
|
|
38
|
+
*/
|
|
39
|
+
export function reachabilityFromDiscovery(outcome) {
|
|
40
|
+
switch (outcome.kind) {
|
|
41
|
+
case "result":
|
|
42
|
+
if (outcome.state === "online") {
|
|
43
|
+
if (outcome.owningNodeIds.length === 0) {
|
|
44
|
+
// `classifyOnlineResult` calls exactly this shape malformed — "online but no owner named
|
|
45
|
+
// … never dial a fabricated node" — and treats it as retry, not as availability. This
|
|
46
|
+
// module's whole thesis is that only a RESULT is an answer about the peer; a result that
|
|
47
|
+
// names no home answers nothing.
|
|
48
|
+
throw new DiscoveryUnavailableError("online_without_owner", "no owning node named");
|
|
49
|
+
}
|
|
50
|
+
return { reachable: true, unknownAgent: false };
|
|
51
|
+
}
|
|
52
|
+
if (outcome.state === "unknown_agent")
|
|
53
|
+
return { reachable: false, unknownAgent: true };
|
|
54
|
+
return { reachable: false, unknownAgent: false };
|
|
55
|
+
case "error":
|
|
56
|
+
// A directory DB fault. Retryable, and emphatically not the counterparty being offline —
|
|
57
|
+
// the outcome type's own comment says so, and collapsing it here would undo that care.
|
|
58
|
+
throw new DiscoveryUnavailableError("directory_error", outcome.reason);
|
|
59
|
+
case "malformed":
|
|
60
|
+
// A reply that did not parse: a protocol or version anomaly on a directory that DID respond.
|
|
61
|
+
// Distinct from a clean directory error, and never availability.
|
|
62
|
+
throw new DiscoveryUnavailableError("malformed_reply", "");
|
|
63
|
+
case "timeout":
|
|
64
|
+
// No reply in the window — an old directory, or a slow or dropped reply on a new one. Either
|
|
65
|
+
// way we learned nothing about the peer.
|
|
66
|
+
throw new DiscoveryUnavailableError("timeout", "");
|
|
67
|
+
case "send_failed":
|
|
68
|
+
// Our OWN signaling stream is down. The most important one to keep separate: reporting our
|
|
69
|
+
// transport fault as the peer's absence points the operator at the wrong machine entirely.
|
|
70
|
+
throw new DiscoveryUnavailableError("signaling_unavailable", outcome.reason);
|
|
71
|
+
default: {
|
|
72
|
+
// Exhaustiveness, enforced by the compiler. A new outcome kind must be classified
|
|
73
|
+
// deliberately rather than defaulting into "reachable" or "offline", which are the two
|
|
74
|
+
// answers a silent default would have to invent.
|
|
75
|
+
const never = outcome;
|
|
76
|
+
throw new DiscoveryUnavailableError("unrecognized_outcome", `unhandled discovery outcome ${JSON.stringify(never)}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=document-reachability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-reachability.js","sourceRoot":"","sources":["../src/document-reachability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,iFAAiF;AACjF,MAAM,OAAO,yBAA0B,SAAQ,KAAK;IACzC,IAAI,CAAS;IACtB,YAAY,IAAY,EAAE,MAAc;QACtC,KAAK,CAAC,mCAAmC,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAChF,IAAI,CAAC,IAAI,GAAG,2BAA2B,CAAC;QACxC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAQD;;;;;;GAMG;AACH,MAAM,UAAU,yBAAyB,CAAC,OAAyB;IACjE,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;QACrB,KAAK,QAAQ;YACX,IAAI,OAAO,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;gBAC/B,IAAI,OAAO,CAAC,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBACvC,yFAAyF;oBACzF,sFAAsF;oBACtF,yFAAyF;oBACzF,iCAAiC;oBACjC,MAAM,IAAI,yBAAyB,CAAC,sBAAsB,EAAE,sBAAsB,CAAC,CAAC;gBACtF,CAAC;gBACD,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC;YAClD,CAAC;YACD,IAAI,OAAO,CAAC,KAAK,KAAK,eAAe;gBAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;YACvF,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC;QAEnD,KAAK,OAAO;YACV,yFAAyF;YACzF,uFAAuF;YACvF,MAAM,IAAI,yBAAyB,CAAC,iBAAiB,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;QAEzE,KAAK,WAAW;YACd,6FAA6F;YAC7F,iEAAiE;YACjE,MAAM,IAAI,yBAAyB,CAAC,iBAAiB,EAAE,EAAE,CAAC,CAAC;QAE7D,KAAK,SAAS;YACZ,6FAA6F;YAC7F,yCAAyC;YACzC,MAAM,IAAI,yBAAyB,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QAErD,KAAK,aAAa;YAChB,2FAA2F;YAC3F,2FAA2F;YAC3F,MAAM,IAAI,yBAAyB,CAAC,uBAAuB,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;QAE/E,OAAO,CAAC,CAAC,CAAC;YACR,kFAAkF;YAClF,uFAAuF;YACvF,iDAAiD;YACjD,MAAM,KAAK,GAAU,OAAO,CAAC;YAC7B,MAAM,IAAI,yBAAyB,CACjC,sBAAsB,EACtB,+BAA+B,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CACvD,CAAC;QACJ,CAAC;IACH,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOD-DOC-REJECT-1 — rejection and supersession (§3.2, §16.7-2).
|
|
3
|
+
*
|
|
4
|
+
* ── WHY SUPERSESSION, AND NOT "BOTH SIDES ROLL BACK" ──────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* The naive protocol is: the receiver discards, the sender undoes locally, and both return to the
|
|
7
|
+
* pre-update state. It does not work, and the reason is a property of CRDTs rather than a bug.
|
|
8
|
+
*
|
|
9
|
+
* **Yjs undo adds INVERSES; it does not erase.** The rejected operations stay in the sender's
|
|
10
|
+
* document. So every later update the sender computes against the receiver's state vector
|
|
11
|
+
* re-transmits them, and the receiver — which refuses to hold them — can never integrate the
|
|
12
|
+
* legitimate work stacked causally on top, because Yjs will not apply operations whose
|
|
13
|
+
* predecessors are missing. A permanent causal gap, from a protocol that looked symmetric.
|
|
14
|
+
*
|
|
15
|
+
* The protocol that works (§3.2):
|
|
16
|
+
* 1. The receiver rejects with a REASON — a protocol message and its own `0x05` leaf, never a
|
|
17
|
+
* silent drop. The update goes to quarantine, held rather than discarded.
|
|
18
|
+
* 2. The sender rolls back locally, which emits inverses into its own log and leaves an
|
|
19
|
+
* auditable "wrote X, was rejected, undid X" trail.
|
|
20
|
+
* 3. The sender publishes a SUPERSEDING update against the receiver's state vector, which
|
|
21
|
+
* necessarily carries the rejected operations PLUS their inverses plus any new work.
|
|
22
|
+
* 4. The receiver validates the now-clean projected diff — the rejected content nets to zero —
|
|
23
|
+
* admits it, and clears the quarantine. Causality intact, both parties converge, and the
|
|
24
|
+
* rejected content survives only as inert tombstones.
|
|
25
|
+
*
|
|
26
|
+
* ── SCOPE ─────────────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
*
|
|
28
|
+
* This proves the protocol against STORE-1's local envelope log. The CBOR wire encoding is
|
|
29
|
+
* DOD-DOC-ENVELOPE-1's job and the cross-daemon proof is DOD-DOC-E2E-REJECT-1's.
|
|
30
|
+
*/
|
|
31
|
+
import * as Y from "yjs";
|
|
32
|
+
import type { DocumentStore } from "./document-store.js";
|
|
33
|
+
import type { Logger } from "./types.js";
|
|
34
|
+
/**
|
|
35
|
+
* The document stalls on the THIRD rejected round: the original refusal, one superseding attempt,
|
|
36
|
+
* and no more (§16.7-2 "one retry, then freeze").
|
|
37
|
+
*
|
|
38
|
+
* Counted in rounds rather than retries because the previous spelling — a `REJECTION_RETRY_LIMIT`
|
|
39
|
+
* of 1 compared as `round > LIMIT + 1` — permitted two retries under a name that said one, which
|
|
40
|
+
* is the kind of thing the next edit gets wrong.
|
|
41
|
+
*
|
|
42
|
+
* Bounded at all because an unbounded retry loop between two daemons that disagree is not
|
|
43
|
+
* convergence, it is a hot loop neither operator can see. Stalling is the visible failure.
|
|
44
|
+
*/
|
|
45
|
+
export declare const MAX_REJECTED_ROUNDS = 3;
|
|
46
|
+
export interface RejectionInput {
|
|
47
|
+
/** The envelope being rejected — the `0x05` leaf references this hash (§9). */
|
|
48
|
+
rejectedEnvelopeHash: string;
|
|
49
|
+
/** The bytes, held rather than discarded (§3.2). Supplied by the gate's quarantine verdict. */
|
|
50
|
+
quarantined: Uint8Array;
|
|
51
|
+
/**
|
|
52
|
+
* The refused envelope's OWN chain link — `null` only when the refused envelope was genuinely
|
|
53
|
+
* that sender's first. REQUIRED rather than optional: the refused envelope is deliberately never
|
|
54
|
+
* written to the log, so this is the only thing keeping the peer's next link resolvable, and an
|
|
55
|
+
* omitted value would default every refused envelope to a genesis stub — manufacturing exactly
|
|
56
|
+
* the fork the bridge exists to prevent. The caller decoded the envelope; it knows.
|
|
57
|
+
*/
|
|
58
|
+
rejectedDocPrevHash: string | null;
|
|
59
|
+
reason: string;
|
|
60
|
+
detail?: string;
|
|
61
|
+
senderAgentId: string;
|
|
62
|
+
/** Which pluggable rule refused, when one did (from the gate's verdict). */
|
|
63
|
+
rule?: string;
|
|
64
|
+
/** The limit breached, when one was (from the gate's verdict). */
|
|
65
|
+
limit?: {
|
|
66
|
+
name: string;
|
|
67
|
+
limit: number;
|
|
68
|
+
actual: number;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Sign the rejection's canonical preimage (DOD-DOC-REJECT-2).
|
|
72
|
+
*
|
|
73
|
+
* The caller supplies the SIGNER, not the signature: a signature with no defined preimage is a
|
|
74
|
+
* field that can only be filled dishonestly, which is exactly what this used to be. An all-zero
|
|
75
|
+
* placeholder written into an immutable log is indistinguishable from a real signature that
|
|
76
|
+
* fails to verify, so a later verifier would send an operator to the crypto layer for a value
|
|
77
|
+
* nobody ever signed.
|
|
78
|
+
*/
|
|
79
|
+
sign(tbs: Uint8Array): Promise<Uint8Array>;
|
|
80
|
+
/**
|
|
81
|
+
* The clock, passed in rather than read here. The rejection's timestamp is SIGNED, so it must be
|
|
82
|
+
* the same value in the preimage and in the row — reading `Date.now()` twice would sign one
|
|
83
|
+
* moment and store another.
|
|
84
|
+
*/
|
|
85
|
+
nowMs: number;
|
|
86
|
+
}
|
|
87
|
+
export interface RejectionOutcome {
|
|
88
|
+
/** The document has exhausted its retries and stopped accepting updates. */
|
|
89
|
+
stalled: boolean;
|
|
90
|
+
/** How many rejections this document has seen, including this one. */
|
|
91
|
+
round: number;
|
|
92
|
+
/**
|
|
93
|
+
* The signed rejection, encoded, for the caller to put on the wire — or absent when this was a
|
|
94
|
+
* duplicate and nothing new was authored.
|
|
95
|
+
*
|
|
96
|
+
* RETURNED, because it was not, and the consequence was that the entire retry protocol was
|
|
97
|
+
* unreachable. The envelope was built here, signed here, leafed here, and then discarded: nothing
|
|
98
|
+
* in production ever called `encodeDocumentRejection`. So the refusing side kept a perfect local
|
|
99
|
+
* record of a decision it never communicated, and the sender — whose round counter is advanced by
|
|
100
|
+
* RECEIVING this frame — never advanced past round zero. A peer whose every update is refused
|
|
101
|
+
* republished forever, and its own surface said `active` the whole time. Measured live.
|
|
102
|
+
*/
|
|
103
|
+
wire?: Uint8Array;
|
|
104
|
+
}
|
|
105
|
+
export interface QuarantineEntry {
|
|
106
|
+
rejectedEnvelopeHash: string;
|
|
107
|
+
quarantined: Uint8Array;
|
|
108
|
+
reason: string;
|
|
109
|
+
detail?: string;
|
|
110
|
+
/** Which rule refused, and the number it refused on. Surfaced, not just stored. */
|
|
111
|
+
rule?: string;
|
|
112
|
+
limitName?: string;
|
|
113
|
+
limitValue?: number;
|
|
114
|
+
limitActual?: number;
|
|
115
|
+
}
|
|
116
|
+
/** A handle over the sender's local edits, so a rejection can be rolled back as inverses. */
|
|
117
|
+
export interface TrackedEdits {
|
|
118
|
+
readonly doc: Y.Doc;
|
|
119
|
+
readonly undoManager: Y.UndoManager;
|
|
120
|
+
/**
|
|
121
|
+
* Release the tracker. Required, not optional hygiene: the depth guard in `rollback` admits
|
|
122
|
+
* exactly one stacked edit, so the only workable pattern is a FRESH tracker per publish — which
|
|
123
|
+
* means one live UndoManager per publish on a long-lived Y.Doc, each holding `afterTransaction`
|
|
124
|
+
* observers and accumulating undo items that retain deleted structs. Call it once the rejection
|
|
125
|
+
* is resolved, either way.
|
|
126
|
+
*/
|
|
127
|
+
dispose(): void;
|
|
128
|
+
/** Stack depth when tracking began, so `rollback` can prove it is undoing the right item. */
|
|
129
|
+
readonly depthAtTracking: number;
|
|
130
|
+
}
|
|
131
|
+
export declare class DocumentRejections {
|
|
132
|
+
#private;
|
|
133
|
+
constructor(store: DocumentStore, logger: Logger);
|
|
134
|
+
/**
|
|
135
|
+
* Record a rejection: a `0x05` row referencing the rejected envelope, the quarantined bytes
|
|
136
|
+
* held, and a policy record carrying the reason.
|
|
137
|
+
*
|
|
138
|
+
* The envelope log is append-only, so this is a NEW ROW — nothing is edited or removed.
|
|
139
|
+
*
|
|
140
|
+
* ── HOW A REFUSAL IS ACTUALLY REALIZED, AND WHY NOT THE WAY §9 SAYS ─────────────────────────
|
|
141
|
+
*
|
|
142
|
+
* §9 phrases effectiveness as a replay-time set property: "an update leaf is effective iff no
|
|
143
|
+
* rejection leaf references it". Implemented literally that is unsound, and it was measured
|
|
144
|
+
* rather than argued. Sender publishes a base, then a refused update, then rolls back and
|
|
145
|
+
* supersedes. Replaying the log while SKIPPING the refused leaf gives:
|
|
146
|
+
*
|
|
147
|
+
* text "agreed base. " pendingStructs PRESENT pendingDs PRESENT
|
|
148
|
+
*
|
|
149
|
+
* The supersession is causally stacked on the refused operations — the rollback is a DELETION of
|
|
150
|
+
* those structs and the new work is positioned after them — so dropping them leaves everything
|
|
151
|
+
* later permanently pending. The document reads as complete and is silently missing the
|
|
152
|
+
* legitimate work. §16.7-5 already retired §9's "document-log order" phrasing; this retires its
|
|
153
|
+
* effectiveness phrasing on the same grounds.
|
|
154
|
+
*
|
|
155
|
+
* What is sound: the receiver NEVER WRITES the refused payload to its log. There is nothing to
|
|
156
|
+
* subtract at replay because it was never added, and the peer's supersession — computed against
|
|
157
|
+
* the RECEIVER's state vector per §3.2 step 3 — is self-contained. Measured on the same fixture:
|
|
158
|
+
*
|
|
159
|
+
* text "agreed base. clean text. " pendingStructs null pendingDs null converged true
|
|
160
|
+
*
|
|
161
|
+
* The refused bytes do travel again inside that supersession, carrying their own inverses, which
|
|
162
|
+
* is precisely "inverses, not erasure" (§3.2) — the content nets to zero and survives only as
|
|
163
|
+
* tombstones. The bytes we refused live in `document_quarantine`, and the chain bridges across
|
|
164
|
+
* the refused envelope so the peer's next link still resolves (see `verifyChainLinkage`).
|
|
165
|
+
*/
|
|
166
|
+
reject(agentId: string, documentId: string, input: RejectionInput): Promise<RejectionOutcome>;
|
|
167
|
+
/**
|
|
168
|
+
* Record a rejection ARRIVING from the peer — the receiving half of §3.2's "both sides".
|
|
169
|
+
*
|
|
170
|
+
* ── THE ROUTING DECISION (the DoD requires it resolved in-unit) ─────────────────────────────
|
|
171
|
+
*
|
|
172
|
+
* A document rejection is written **daemon-side**, into this document's own log and quarantine,
|
|
173
|
+
* NOT through the gateway record store's `source` discriminator.
|
|
174
|
+
*
|
|
175
|
+
* The gateway's record store exists for SCREENING verdicts, and most V1 rejection reasons are
|
|
176
|
+
* not screening at all — `append_only`, the receiver-local limits, malformed updates, unresolved
|
|
177
|
+
* dependencies. Routing every document rejection through a screening store would file structural
|
|
178
|
+
* protocol events as policy verdicts, and it would couple this unit to a schema owned by a
|
|
179
|
+
* component that is not involved. DOD-DOC-SCREEN-1 is parked, so that coupling would also have
|
|
180
|
+
* to be built speculatively and unwound if screening lands differently.
|
|
181
|
+
*
|
|
182
|
+
* When SCREEN-1 does land, a rejection whose reason came from the screening rule can ADDITIONALLY
|
|
183
|
+
* write a gateway record — the discriminator exists for exactly that, and adding it later costs
|
|
184
|
+
* nothing, whereas removing a premature coupling costs a migration.
|
|
185
|
+
*/
|
|
186
|
+
recordIncomingRejection(agentId: string, documentId: string, input: {
|
|
187
|
+
/** The peer's 0x05 leaf hash — the row's identity, so a redelivery does not advance a round. */
|
|
188
|
+
rejectionEnvelopeHash: string;
|
|
189
|
+
rejectedEnvelopeHash: string;
|
|
190
|
+
reason: string;
|
|
191
|
+
detail?: string;
|
|
192
|
+
fromAgentId: string;
|
|
193
|
+
}): {
|
|
194
|
+
stalled: boolean;
|
|
195
|
+
round: number;
|
|
196
|
+
};
|
|
197
|
+
/** Entries held for this document — never admitted, never discarded (§3.2). From the store. */
|
|
198
|
+
quarantined(agentId: string, documentId: string): QuarantineEntry[];
|
|
199
|
+
/** Clear one entry once its superseding update has been admitted (§3.2 step 4). */
|
|
200
|
+
clearQuarantine(agentId: string, documentId: string, rejectedEnvelopeHash: string): void;
|
|
201
|
+
/**
|
|
202
|
+
* Whether this document still accepts updates.
|
|
203
|
+
*
|
|
204
|
+
* A stalled document REFUSES, naming the reason it stalled — both operators need to see why,
|
|
205
|
+
* because a document that silently stops converging is the failure this exists to prevent.
|
|
206
|
+
*/
|
|
207
|
+
acceptsUpdates(agentId: string, documentId: string): {
|
|
208
|
+
ok: true;
|
|
209
|
+
} | {
|
|
210
|
+
ok: false;
|
|
211
|
+
reason: string;
|
|
212
|
+
detail: string;
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* Start tracking local edits so a rejection can be rolled back.
|
|
216
|
+
*
|
|
217
|
+
* Yjs's own UndoManager, deliberately: rolling back by hand would mean computing inverses, and
|
|
218
|
+
* the inverse of a CRDT operation is not something to hand-roll — the whole reason supersession
|
|
219
|
+
* works is that Yjs's undo produces operations that compose correctly with everything stacked
|
|
220
|
+
* on top of them.
|
|
221
|
+
*/
|
|
222
|
+
trackLocalEdits(doc: Y.Doc): TrackedEdits;
|
|
223
|
+
/**
|
|
224
|
+
* Roll back the last tracked edit, as INVERSES.
|
|
225
|
+
*
|
|
226
|
+
* The sender's history grows rather than shrinking. That is not a limitation to work around: it
|
|
227
|
+
* is what leaves an auditable "wrote X, was rejected, undid X" trail, and what lets the
|
|
228
|
+
* superseding update carry the rejected operations plus their inverses so causality survives.
|
|
229
|
+
*
|
|
230
|
+
* **PRECONDITION: roll back before making further local edits.** Yjs's UndoManager undoes the
|
|
231
|
+
* most recent stack item, and it cannot undo one out of order — so a sender that keeps editing
|
|
232
|
+
* after a rejection arrives and then rolls back would undo the WRONG transaction. §3.2's
|
|
233
|
+
* ordering is steps 2 then 3 for exactly this reason: roll back, THEN do new work, THEN
|
|
234
|
+
* publish the supersession that carries all three. A caller that needs to interleave has a
|
|
235
|
+
* genuine design question, not a call-order detail, and it belongs in the unit that wires
|
|
236
|
+
* publish to the receive path.
|
|
237
|
+
*/
|
|
238
|
+
rollback(tracked: TrackedEdits): void;
|
|
239
|
+
}
|
|
240
|
+
//# sourceMappingURL=document-rejection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-rejection.d.ts","sourceRoot":"","sources":["../src/document-rejection.ts"],"names":[],"mappings":"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"}
|