@cello-protocol/daemon 0.0.121 → 0.0.122

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.
Files changed (48) hide show
  1. package/dist/document-delivery-transport.d.ts +78 -0
  2. package/dist/document-delivery-transport.d.ts.map +1 -0
  3. package/dist/document-delivery-transport.js +109 -0
  4. package/dist/document-delivery-transport.js.map +1 -0
  5. package/dist/document-delivery.d.ts +130 -0
  6. package/dist/document-delivery.d.ts.map +1 -0
  7. package/dist/document-delivery.js +246 -0
  8. package/dist/document-delivery.js.map +1 -0
  9. package/dist/document-engine.d.ts.map +1 -1
  10. package/dist/document-engine.js +10 -4
  11. package/dist/document-engine.js.map +1 -1
  12. package/dist/document-gate.d.ts +139 -0
  13. package/dist/document-gate.d.ts.map +1 -0
  14. package/dist/document-gate.js +465 -0
  15. package/dist/document-gate.js.map +1 -0
  16. package/dist/document-handshake.d.ts +88 -0
  17. package/dist/document-handshake.d.ts.map +1 -0
  18. package/dist/document-handshake.js +239 -0
  19. package/dist/document-handshake.js.map +1 -0
  20. package/dist/document-lifecycle.d.ts +104 -0
  21. package/dist/document-lifecycle.d.ts.map +1 -0
  22. package/dist/document-lifecycle.js +363 -0
  23. package/dist/document-lifecycle.js.map +1 -0
  24. package/dist/document-notify.d.ts +130 -0
  25. package/dist/document-notify.d.ts.map +1 -0
  26. package/dist/document-notify.js +313 -0
  27. package/dist/document-notify.js.map +1 -0
  28. package/dist/document-reachability.d.ts +42 -0
  29. package/dist/document-reachability.d.ts.map +1 -0
  30. package/dist/document-reachability.js +72 -0
  31. package/dist/document-reachability.js.map +1 -0
  32. package/dist/document-rejection.d.ts +224 -0
  33. package/dist/document-rejection.d.ts.map +1 -0
  34. package/dist/document-rejection.js +374 -0
  35. package/dist/document-rejection.js.map +1 -0
  36. package/dist/document-store.d.ts +88 -0
  37. package/dist/document-store.d.ts.map +1 -1
  38. package/dist/document-store.js +344 -1
  39. package/dist/document-store.js.map +1 -1
  40. package/dist/document-write-path.d.ts +84 -0
  41. package/dist/document-write-path.d.ts.map +1 -0
  42. package/dist/document-write-path.js +412 -0
  43. package/dist/document-write-path.js.map +1 -0
  44. package/dist/line-lcs.d.ts +51 -0
  45. package/dist/line-lcs.d.ts.map +1 -0
  46. package/dist/line-lcs.js +71 -0
  47. package/dist/line-lcs.js.map +1 -0
  48. package/package.json +4 -4
@@ -0,0 +1,78 @@
1
+ /**
2
+ * DOD-DOC-DELIVERY-2 — the transport behind `DocumentDeliveryTransport` (§16.4).
3
+ *
4
+ * DELIVERY-1 built the scheduler and the bookkeeping against an injected seam. This is the seam's
5
+ * implementation: the first NON-HANDLER consumer of the session machinery, which is what makes it
6
+ * interesting — every other caller of `SessionNegotiator` runs because an agent asked for
7
+ * something, and this one runs because a document has a pending envelope and nobody is watching.
8
+ *
9
+ * ── REUSE BEFORE OPEN, AND WHY THAT ORDER ─────────────────────────────────────────────────────
10
+ *
11
+ * §16.4: "the daemon uses the most recent active session with that peer or opens one". Reuse first,
12
+ * because opening is the expensive half — a directory negotiation, a dial, and a seal — and a
13
+ * backlog of pending envelopes for one peer would otherwise pay it per envelope. It is also the
14
+ * behaviour an operator expects when they are mid-conversation about the document: the change lands
15
+ * in the same sealed record as the discussion, without anyone passing a session hint.
16
+ *
17
+ * An explicit `sessionHint` overrides the choice but NOT the validation — a hint naming a session
18
+ * that is not active with this peer is refused rather than silently replaced by the daemon's own
19
+ * pick, because the one reason to pass a hint is to control which sealed record the change lands
20
+ * in, and quietly choosing a different one defeats exactly that.
21
+ *
22
+ * ── WHAT AN ACK IS HERE, AND WHAT IT IS NOT ───────────────────────────────────────────────────
23
+ *
24
+ * `sendContent` reports that the content left, or was PARKED at the relay for an offline peer. That
25
+ * is a transport fact. The DoD's ack is a different one — "the peer's daemon confirms admission (or
26
+ * rejection)" — and it can only come from the peer's inbound document handler, which is its own
27
+ * line. So this adapter returns `admitted: null`: SENT, not acked.
28
+ *
29
+ * That third state is not hedging; both two-valued answers are dishonest here. `true` would mark
30
+ * the envelope acknowledged in the log while the peer may never have applied it, and the log being
31
+ * right about what the peer holds is the entire reason pending is derived from it. `false` would
32
+ * count a send that WORKED as a failure and re-send content already in flight — the
33
+ * permanent-redelivery shape this milestone has already fixed once. The worker records the envelope
34
+ * as delivered, leaves it unacked, and asks again on the capped backoff.
35
+ */
36
+ import type { DocumentDeliveryTransport } from "./document-delivery.js";
37
+ import { DiscoveryUnavailableError } from "./document-reachability.js";
38
+ import type { DiscoveryOutcome } from "./cross-node-negotiation.js";
39
+ import type { Logger } from "./types.js";
40
+ export interface DocumentTransportDeps {
41
+ /** The agent this worker delivers for. One worker per attended agent. */
42
+ agentName: string;
43
+ /** `runDiscoveryLookup`, supplied by the composition root — it lives in a closure there. */
44
+ lookupPeer(peerAgentId: string, correlationId: string): Promise<DiscoveryOutcome>;
45
+ /** Active sessions with this peer, most recent LAST. */
46
+ activeSessionsWith(agentName: string, peerAgentId: string): string[];
47
+ /** The existing initiate path: negotiate, dial, create the session node. */
48
+ openSession(agentName: string, peerAgentId: string, correlationId: string): Promise<{
49
+ ok: true;
50
+ sessionId: string;
51
+ } | {
52
+ ok: false;
53
+ reason: string;
54
+ guidance?: string;
55
+ }>;
56
+ /** `SessionNodeManager.sendContent`. */
57
+ sendContent(agentName: string, sessionId: string, content: Uint8Array, contentHash: Uint8Array, correlationId: string): Promise<{
58
+ ok: true;
59
+ delivered: true;
60
+ } | {
61
+ ok: true;
62
+ delivered: false;
63
+ parked: true;
64
+ } | {
65
+ ok: false;
66
+ reason: string;
67
+ error: string;
68
+ }>;
69
+ /** Encode a document envelope row onto the wire, and its content hash. */
70
+ encodeEnvelope(envelope: unknown): {
71
+ bytes: Uint8Array;
72
+ hash: Uint8Array;
73
+ };
74
+ logger: Logger;
75
+ }
76
+ export declare function createDocumentDeliveryTransport(deps: DocumentTransportDeps): DocumentDeliveryTransport;
77
+ export { DiscoveryUnavailableError };
78
+ //# sourceMappingURL=document-delivery-transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-delivery-transport.d.ts","sourceRoot":"","sources":["../src/document-delivery-transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AACxE,OAAO,EAA6B,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AAClG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC,MAAM,WAAW,qBAAqB;IACpC,yEAAyE;IACzE,SAAS,EAAE,MAAM,CAAC;IAClB,4FAA4F;IAC5F,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAClF,wDAAwD;IACxD,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACrE,4EAA4E;IAC5E,WAAW,CACT,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/F,wCAAwC;IACxC,WAAW,CACT,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,UAAU,EACnB,WAAW,EAAE,UAAU,EACvB,aAAa,EAAE,MAAM,GACpB,OAAO,CACN;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,SAAS,EAAE,IAAI,CAAA;KAAE,GAC7B;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,SAAS,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,IAAI,CAAA;KAAE,GAC5C;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAC/C,CAAC;IACF,0EAA0E;IAC1E,cAAc,CAAC,QAAQ,EAAE,OAAO,GAAG;QAAE,KAAK,EAAE,UAAU,CAAC;QAAC,IAAI,EAAE,UAAU,CAAA;KAAE,CAAC;IAC3E,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,qBAAqB,GAC1B,yBAAyB,CA2E3B;AAED,OAAO,EAAE,yBAAyB,EAAE,CAAC"}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * DOD-DOC-DELIVERY-2 — the transport behind `DocumentDeliveryTransport` (§16.4).
3
+ *
4
+ * DELIVERY-1 built the scheduler and the bookkeeping against an injected seam. This is the seam's
5
+ * implementation: the first NON-HANDLER consumer of the session machinery, which is what makes it
6
+ * interesting — every other caller of `SessionNegotiator` runs because an agent asked for
7
+ * something, and this one runs because a document has a pending envelope and nobody is watching.
8
+ *
9
+ * ── REUSE BEFORE OPEN, AND WHY THAT ORDER ─────────────────────────────────────────────────────
10
+ *
11
+ * §16.4: "the daemon uses the most recent active session with that peer or opens one". Reuse first,
12
+ * because opening is the expensive half — a directory negotiation, a dial, and a seal — and a
13
+ * backlog of pending envelopes for one peer would otherwise pay it per envelope. It is also the
14
+ * behaviour an operator expects when they are mid-conversation about the document: the change lands
15
+ * in the same sealed record as the discussion, without anyone passing a session hint.
16
+ *
17
+ * An explicit `sessionHint` overrides the choice but NOT the validation — a hint naming a session
18
+ * that is not active with this peer is refused rather than silently replaced by the daemon's own
19
+ * pick, because the one reason to pass a hint is to control which sealed record the change lands
20
+ * in, and quietly choosing a different one defeats exactly that.
21
+ *
22
+ * ── WHAT AN ACK IS HERE, AND WHAT IT IS NOT ───────────────────────────────────────────────────
23
+ *
24
+ * `sendContent` reports that the content left, or was PARKED at the relay for an offline peer. That
25
+ * is a transport fact. The DoD's ack is a different one — "the peer's daemon confirms admission (or
26
+ * rejection)" — and it can only come from the peer's inbound document handler, which is its own
27
+ * line. So this adapter returns `admitted: null`: SENT, not acked.
28
+ *
29
+ * That third state is not hedging; both two-valued answers are dishonest here. `true` would mark
30
+ * the envelope acknowledged in the log while the peer may never have applied it, and the log being
31
+ * right about what the peer holds is the entire reason pending is derived from it. `false` would
32
+ * count a send that WORKED as a failure and re-send content already in flight — the
33
+ * permanent-redelivery shape this milestone has already fixed once. The worker records the envelope
34
+ * as delivered, leaves it unacked, and asks again on the capped backoff.
35
+ */
36
+ import { reachabilityFromDiscovery, DiscoveryUnavailableError } from "./document-reachability.js";
37
+ export function createDocumentDeliveryTransport(deps) {
38
+ return {
39
+ async isPeerReachable(peerAgentId) {
40
+ const outcome = await deps.lookupPeer(peerAgentId, `reach-${peerAgentId.slice(0, 8)}`);
41
+ // Throws on everything that is not an answer about the peer — see document-reachability.ts.
42
+ // The worker treats the throw as `lookup_failed` and keeps it out of the offline-peer count.
43
+ const { reachable, unknownAgent } = reachabilityFromDiscovery(outcome);
44
+ if (unknownAgent) {
45
+ // Distinct from "offline": the address does not resolve at all. Same consequence (do not
46
+ // dial), different thing to tell an operator, so it is said rather than folded in.
47
+ deps.logger.warn("document.delivery.peer_unknown", { peerAgentId });
48
+ }
49
+ return reachable;
50
+ },
51
+ async deliver(input) {
52
+ const { peerAgentId, documentId, envelope, sessionHint, correlationId } = input;
53
+ let sessionId;
54
+ let sessionOpened = false;
55
+ const active = deps.activeSessionsWith(deps.agentName, peerAgentId);
56
+ if (sessionHint !== undefined) {
57
+ if (!active.includes(sessionHint)) {
58
+ // Refused, not replaced. The only reason to pass a hint is to control which sealed
59
+ // record the change lands in; quietly substituting the daemon's own pick would defeat
60
+ // exactly that, and it would do so silently.
61
+ return {
62
+ ok: false,
63
+ reason: "document_session_hint_invalid",
64
+ detail: `session ${sessionHint.slice(0, 16)}… is not an active session with ${peerAgentId}, ` +
65
+ `so the change cannot be placed in that record`,
66
+ };
67
+ }
68
+ sessionId = sessionHint;
69
+ }
70
+ else if (active.length > 0) {
71
+ sessionId = active[active.length - 1];
72
+ }
73
+ else {
74
+ const opened = await deps.openSession(deps.agentName, peerAgentId, correlationId);
75
+ if (!opened.ok) {
76
+ // The upstream reason verbatim. `document_delivery_threw` is reserved for a genuine
77
+ // programming fault; a dial that was refused should say it was refused.
78
+ return { ok: false, reason: opened.reason, detail: opened.guidance };
79
+ }
80
+ sessionId = opened.sessionId;
81
+ sessionOpened = true;
82
+ }
83
+ const { bytes, hash } = deps.encodeEnvelope(envelope);
84
+ const sent = await deps.sendContent(deps.agentName, sessionId, bytes, hash, correlationId);
85
+ if (!sent.ok) {
86
+ return { ok: false, reason: sent.reason, detail: sent.error };
87
+ }
88
+ deps.logger.info("document.delivery.sent", {
89
+ documentId,
90
+ sessionId,
91
+ sessionOpened,
92
+ parked: sent.delivered === false,
93
+ correlationId,
94
+ });
95
+ // SENT, NOT ACKED — `admitted: null`. The content left (or was parked for an offline peer),
96
+ // which is a transport fact; the DoD's ack is the peer's daemon confirming admission, and
97
+ // that answer comes from the inbound document handler, which is its own line.
98
+ //
99
+ // Neither of the two-valued answers is available honestly. `true` would mark the envelope
100
+ // acknowledged in the log while the peer may never have applied it — and the log being right
101
+ // about what the peer holds is the entire reason pending is derived from it. `false` would
102
+ // count a send that WORKED as a failure and re-send content already in flight, which is the
103
+ // permanent-redelivery shape this milestone already fixed once.
104
+ return { ok: true, sessionId, sessionOpened, admitted: null };
105
+ },
106
+ };
107
+ }
108
+ export { DiscoveryUnavailableError };
109
+ //# sourceMappingURL=document-delivery-transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-delivery-transport.js","sourceRoot":"","sources":["../src/document-delivery-transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAGH,OAAO,EAAE,yBAAyB,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AAkClG,MAAM,UAAU,+BAA+B,CAC7C,IAA2B;IAE3B,OAAO;QACL,KAAK,CAAC,eAAe,CAAC,WAAmB;YACvC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,WAAW,EAAE,SAAS,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;YACvF,4FAA4F;YAC5F,6FAA6F;YAC7F,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,GAAG,yBAAyB,CAAC,OAAO,CAAC,CAAC;YACvE,IAAI,YAAY,EAAE,CAAC;gBACjB,yFAAyF;gBACzF,mFAAmF;gBACnF,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,gCAAgC,EAAE,EAAE,WAAW,EAAE,CAAC,CAAC;YACtE,CAAC;YACD,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,KAAK,CAAC,OAAO,CAAC,KAAK;YACjB,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa,EAAE,GAAG,KAAK,CAAC;YAEhF,IAAI,SAAiB,CAAC;YACtB,IAAI,aAAa,GAAG,KAAK,CAAC;YAE1B,MAAM,MAAM,GAAG,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;YACpE,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;gBAC9B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;oBAClC,mFAAmF;oBACnF,sFAAsF;oBACtF,6CAA6C;oBAC7C,OAAO;wBACL,EAAE,EAAE,KAAK;wBACT,MAAM,EAAE,+BAA+B;wBACvC,MAAM,EACJ,WAAW,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,mCAAmC,WAAW,IAAI;4BACrF,+CAA+C;qBAClD,CAAC;gBACJ,CAAC;gBACD,SAAS,GAAG,WAAW,CAAC;YAC1B,CAAC;iBAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC7B,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC;YACzC,CAAC;iBAAM,CAAC;gBACN,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,EAAE,aAAa,CAAC,CAAC;gBAClF,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;oBACf,oFAAoF;oBACpF,wEAAwE;oBACxE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC;gBACvE,CAAC;gBACD,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC;gBAC7B,aAAa,GAAG,IAAI,CAAC;YACvB,CAAC;YAED,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;YACtD,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC;YAC3F,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;gBACb,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC;YAChE,CAAC;YAED,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,wBAAwB,EAAE;gBACzC,UAAU;gBACV,SAAS;gBACT,aAAa;gBACb,MAAM,EAAE,IAAI,CAAC,SAAS,KAAK,KAAK;gBAChC,aAAa;aACd,CAAC,CAAC;YAEH,4FAA4F;YAC5F,0FAA0F;YAC1F,8EAA8E;YAC9E,EAAE;YACF,0FAA0F;YAC1F,6FAA6F;YAC7F,2FAA2F;YAC3F,4FAA4F;YAC5F,gEAAgE;YAChE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,aAAa,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QAChE,CAAC;KACF,CAAC;AACJ,CAAC;AAED,OAAO,EAAE,yBAAyB,EAAE,CAAC"}
@@ -0,0 +1,130 @@
1
+ /**
2
+ * DOD-DOC-DELIVERY-1 — daemon-autonomous delivery (§16.4).
3
+ *
4
+ * Publish writes the envelope to the log and returns. Delivery is somebody else's job, and that
5
+ * somebody is this worker: it derives what is pending FROM THE LOG, checks the peer is reachable
6
+ * before dialing, opens or reuses a session itself, delivers, and records the ack — with zero agent
7
+ * attention on either end. Two agents in opposite time zones sync overnight without either agent
8
+ * doing anything.
9
+ *
10
+ * ── PENDING IS DERIVED, NEVER HELD ────────────────────────────────────────────────────────────
11
+ *
12
+ * "Unacknowledged envelopes I authored" is a WHERE clause over the log, and that is the whole
13
+ * definition. A queue in memory does not survive a restart — the daemon is a long-running local
14
+ * process that gets restarted routinely — and a queue in its own table is a second source of truth
15
+ * that can disagree with the log about what was sent. The attempt counter and the next-attempt
16
+ * time live on the envelope row for the same reason: a backoff that resets on restart is not a
17
+ * backoff, and a daemon restarting in a reconnect loop would hammer an unreachable peer at full
18
+ * rate forever.
19
+ *
20
+ * ── WHY LOOKUP BEFORE DIAL ────────────────────────────────────────────────────────────────────
21
+ *
22
+ * §16.4's design is presence-driven push, but there is no presence subscription today (parked,
23
+ * M14-P4). Without one, the honest substitute is: ask the directory whether the peer is reachable,
24
+ * and if it is not, do not burn a dial — schedule a retry on a capped backoff. Dialing an offline
25
+ * peer to find out it is offline is the same information at much higher cost, and it is the cost
26
+ * that would be paid on every pending envelope, on every tick, for as long as the peer is away.
27
+ */
28
+ import type { DocumentStore, DocumentEnvelopeRow } from "./document-store.js";
29
+ import type { Logger } from "./types.js";
30
+ /**
31
+ * The transport seam. Narrow on purpose (M4 rule: add to an interface only when a failing test or
32
+ * a production behaviour requires it) — the worker's job is scheduling and bookkeeping, and every
33
+ * dial-level concern belongs to the adapter behind this.
34
+ */
35
+ export interface DocumentDeliveryTransport {
36
+ /**
37
+ * Is the peer reachable right now? `discovery_lookup` today. Returns false rather than throwing
38
+ * for an ordinary "not online"; a throw means the LOOKUP failed, which is a different fact and
39
+ * must not be recorded as the peer being away.
40
+ */
41
+ isPeerReachable(peerAgentId: string): Promise<boolean>;
42
+ /**
43
+ * Deliver one envelope over a session the transport opens or reuses (§16.4: daemon-chooses by
44
+ * default; `sessionHint` is the one case with audit value — the agent is mid-conversation about
45
+ * the document and wants the discussion and the change in one sealed record).
46
+ */
47
+ /**
48
+ * `ok` means the peer's daemon ANSWERED about this envelope — it is not "the peer liked it".
49
+ * `admitted: false` is a rejection (§3.2's `0x05`), and a rejection IS an ack for delivery
50
+ * purposes: the peer has decided, so there is nothing left to retry. Without the distinction the
51
+ * adapter has to map a rejection onto one of two lies — `ok: true` makes the delivery record say
52
+ * the peer admitted content it refused, and `ok: false` redelivers an envelope the peer has
53
+ * already ruled on, forever, re-triggering their gate and their retry counter until the document
54
+ * stalls for reasons the operator cannot see.
55
+ *
56
+ * `sessionOpened` distinguishes a session this delivery opened from one it reused — the audit
57
+ * distinction §16.4 cares about, and unrecoverable after the fact.
58
+ */
59
+ deliver(input: {
60
+ peerAgentId: string;
61
+ documentId: string;
62
+ envelope: DocumentEnvelopeRow;
63
+ sessionHint?: string;
64
+ correlationId: string;
65
+ }): Promise<{
66
+ ok: true;
67
+ sessionId: string;
68
+ sessionOpened: boolean;
69
+ /**
70
+ * `true` admitted, `false` rejected — both are ACKS, the peer has decided. `null` means the
71
+ * envelope LEFT (or was parked for an offline peer) and no answer has come back yet.
72
+ *
73
+ * The third state is not hedging; both two-valued answers are dishonest for a send whose
74
+ * outcome is unknown. `true` marks the envelope acknowledged in the log while the peer may
75
+ * never have applied it — and the log being right about what the peer holds is the entire
76
+ * reason pending is derived from it. `false` counts a send that WORKED as a failure and
77
+ * re-sends content already in flight.
78
+ */
79
+ admitted: boolean | null;
80
+ rejectionReason?: string;
81
+ } | {
82
+ ok: false;
83
+ reason: string;
84
+ detail?: string;
85
+ }>;
86
+ }
87
+ /**
88
+ * Backoff schedule in ms: ~1s, 5s, 30s, 2m, 10m, then the last entry repeats.
89
+ *
90
+ * The final entry IS the cap — there is no separate ceiling constant. There was one, set to 900s,
91
+ * and it was unreachable: the index clamp meant the schedule never produced a value above 600s, so
92
+ * the exported "cap" was a number the code could not emit and the tests were using it as a synonym
93
+ * for "much later". A documented limit the implementation cannot reach is worse than none.
94
+ *
95
+ * Capped at all because the peer coming back online is the event we are waiting for and it can
96
+ * happen at any moment — an uncapped curve leaves a peer that returned after a long absence waiting
97
+ * hours for a delivery that has been ready the whole time.
98
+ */
99
+ export declare const DELIVERY_BACKOFF_MS: readonly [1000, 5000, 30000, 120000, 600000];
100
+ export declare const DELIVERY_BACKOFF_CAP_MS: 5000 | 1000 | 30000 | 120000 | 600000;
101
+ /**
102
+ * A document whose peer cannot be resolved is not transient, so it gets the cap immediately rather
103
+ * than climbing to it — but it DOES get scheduled. See the `no_peer` branch.
104
+ */
105
+ export declare const DELIVERY_UNRESOLVABLE_RETRY_MS: 5000 | 1000 | 30000 | 120000 | 600000;
106
+ export declare function backoffFor(attempts: number): number;
107
+ export interface DeliveryTickResult {
108
+ attempted: number;
109
+ delivered: number;
110
+ /** Sent (or parked) with no answer yet — in flight, neither done nor failed. */
111
+ sent: number;
112
+ /** Answered but refused. Acked all the same — the peer has decided. */
113
+ rejected: number;
114
+ deferred: number;
115
+ failed: number;
116
+ }
117
+ export declare class DocumentDelivery {
118
+ #private;
119
+ constructor(store: DocumentStore, transport: DocumentDeliveryTransport, logger: Logger);
120
+ /**
121
+ * One pass over what is due. Returns counts rather than throwing on a per-envelope failure: one
122
+ * unreachable peer must not stop delivery to every other peer, which is precisely the "works
123
+ * only when all nodes are healthy" shape the project forbids.
124
+ */
125
+ tick(ownerAgentId: string, peerFor: (documentId: string) => string | null, nowMs: number, opts?: {
126
+ sessionHint?: string;
127
+ correlationId?: string;
128
+ }): Promise<DeliveryTickResult>;
129
+ }
130
+ //# sourceMappingURL=document-delivery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-delivery.d.ts","sourceRoot":"","sources":["../src/document-delivery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC9E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC;;;;GAIG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;;OAIG;IACH,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACvD;;;;OAIG;IACH;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,KAAK,EAAE;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,mBAAmB,CAAC;QAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,aAAa,EAAE,MAAM,CAAC;KACvB,GAAG,OAAO,CACP;QACE,EAAE,EAAE,IAAI,CAAC;QACT,SAAS,EAAE,MAAM,CAAC;QAClB,aAAa,EAAE,OAAO,CAAC;QACvB;;;;;;;;;WASG;QACH,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC;QACzB,eAAe,CAAC,EAAE,MAAM,CAAC;KAC1B,GACD;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CACjD,CAAC;CACH;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB,8CAAoD,CAAC;AACrF,eAAO,MAAM,uBAAuB,uCAAsD,CAAC;AAE3F;;;GAGG;AACH,eAAO,MAAM,8BAA8B,uCAA0B,CAAC;AAEtE,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGnD;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,gBAAgB;;gBAMf,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,yBAAyB,EAAE,MAAM,EAAE,MAAM;IAMtF;;;;OAIG;IACG,IAAI,CACR,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,EAC9C,KAAK,EAAE,MAAM,EACb,IAAI,GAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAO,GAC1D,OAAO,CAAC,kBAAkB,CAAC;CA4M/B"}
@@ -0,0 +1,246 @@
1
+ /**
2
+ * DOD-DOC-DELIVERY-1 — daemon-autonomous delivery (§16.4).
3
+ *
4
+ * Publish writes the envelope to the log and returns. Delivery is somebody else's job, and that
5
+ * somebody is this worker: it derives what is pending FROM THE LOG, checks the peer is reachable
6
+ * before dialing, opens or reuses a session itself, delivers, and records the ack — with zero agent
7
+ * attention on either end. Two agents in opposite time zones sync overnight without either agent
8
+ * doing anything.
9
+ *
10
+ * ── PENDING IS DERIVED, NEVER HELD ────────────────────────────────────────────────────────────
11
+ *
12
+ * "Unacknowledged envelopes I authored" is a WHERE clause over the log, and that is the whole
13
+ * definition. A queue in memory does not survive a restart — the daemon is a long-running local
14
+ * process that gets restarted routinely — and a queue in its own table is a second source of truth
15
+ * that can disagree with the log about what was sent. The attempt counter and the next-attempt
16
+ * time live on the envelope row for the same reason: a backoff that resets on restart is not a
17
+ * backoff, and a daemon restarting in a reconnect loop would hammer an unreachable peer at full
18
+ * rate forever.
19
+ *
20
+ * ── WHY LOOKUP BEFORE DIAL ────────────────────────────────────────────────────────────────────
21
+ *
22
+ * §16.4's design is presence-driven push, but there is no presence subscription today (parked,
23
+ * M14-P4). Without one, the honest substitute is: ask the directory whether the peer is reachable,
24
+ * and if it is not, do not burn a dial — schedule a retry on a capped backoff. Dialing an offline
25
+ * peer to find out it is offline is the same information at much higher cost, and it is the cost
26
+ * that would be paid on every pending envelope, on every tick, for as long as the peer is away.
27
+ */
28
+ /**
29
+ * Backoff schedule in ms: ~1s, 5s, 30s, 2m, 10m, then the last entry repeats.
30
+ *
31
+ * The final entry IS the cap — there is no separate ceiling constant. There was one, set to 900s,
32
+ * and it was unreachable: the index clamp meant the schedule never produced a value above 600s, so
33
+ * the exported "cap" was a number the code could not emit and the tests were using it as a synonym
34
+ * for "much later". A documented limit the implementation cannot reach is worse than none.
35
+ *
36
+ * Capped at all because the peer coming back online is the event we are waiting for and it can
37
+ * happen at any moment — an uncapped curve leaves a peer that returned after a long absence waiting
38
+ * hours for a delivery that has been ready the whole time.
39
+ */
40
+ export const DELIVERY_BACKOFF_MS = [1_000, 5_000, 30_000, 120_000, 600_000];
41
+ export const DELIVERY_BACKOFF_CAP_MS = DELIVERY_BACKOFF_MS[DELIVERY_BACKOFF_MS.length - 1];
42
+ /**
43
+ * A document whose peer cannot be resolved is not transient, so it gets the cap immediately rather
44
+ * than climbing to it — but it DOES get scheduled. See the `no_peer` branch.
45
+ */
46
+ export const DELIVERY_UNRESOLVABLE_RETRY_MS = DELIVERY_BACKOFF_CAP_MS;
47
+ export function backoffFor(attempts) {
48
+ const idx = Math.min(Math.max(attempts, 0), DELIVERY_BACKOFF_MS.length - 1);
49
+ return DELIVERY_BACKOFF_MS[idx];
50
+ }
51
+ export class DocumentDelivery {
52
+ #store;
53
+ #transport;
54
+ #logger;
55
+ #inFlight = null;
56
+ constructor(store, transport, logger) {
57
+ this.#store = store;
58
+ this.#transport = transport;
59
+ this.#logger = logger;
60
+ }
61
+ /**
62
+ * One pass over what is due. Returns counts rather than throwing on a per-envelope failure: one
63
+ * unreachable peer must not stop delivery to every other peer, which is precisely the "works
64
+ * only when all nodes are healthy" shape the project forbids.
65
+ */
66
+ async tick(ownerAgentId, peerFor, nowMs, opts = {}) {
67
+ // NO RE-ENTRY. Nothing marks a row in-flight until its outcome lands, so a `deliver` slower
68
+ // than the tick interval — a dial to a distant peer is exactly that — would have the next tick
69
+ // return the same rows and dial again: two autonomous sessions and two seals for one envelope,
70
+ // and the loser's markAcked returning false, which logs as if the PEER had redelivered an ack.
71
+ if (this.#inFlight)
72
+ return this.#inFlight;
73
+ const run = this.#run(ownerAgentId, peerFor, nowMs, opts);
74
+ this.#inFlight = run;
75
+ try {
76
+ return await run;
77
+ }
78
+ finally {
79
+ this.#inFlight = null;
80
+ }
81
+ }
82
+ async #run(ownerAgentId, peerFor, nowMs, opts) {
83
+ // Minted once per pass and threaded through every event, so an operator can tie a failure back
84
+ // to the lookup that preceded it and the session that carried it. Delivery is precisely the
85
+ // async multi-step flow the convention exists for: lookup, dial, deliver, ack — across ticks
86
+ // and across restarts.
87
+ const correlationId = opts.correlationId ?? `dlv-${ownerAgentId.slice(0, 8)}-${nowMs}`;
88
+ const pending = this.#store.pendingDeliveries(ownerAgentId, nowMs);
89
+ const result = {
90
+ attempted: 0, delivered: 0, sent: 0, rejected: 0, deferred: 0, failed: 0,
91
+ };
92
+ // Group by document so one reachability lookup serves every pending envelope for that peer.
93
+ // Per-envelope lookups would multiply directory traffic by the size of the backlog, which is
94
+ // largest exactly when the peer has been away longest.
95
+ const byDocument = new Map();
96
+ for (const e of pending) {
97
+ const list = byDocument.get(e.documentId);
98
+ if (list)
99
+ list.push(e);
100
+ else
101
+ byDocument.set(e.documentId, [e]);
102
+ }
103
+ for (const [documentId, envelopes] of byDocument) {
104
+ const peerAgentId = peerFor(documentId);
105
+ if (peerAgentId === null) {
106
+ // No peer means the document row is gone or malformed, which is NOT transient — so it goes
107
+ // straight to the cap rather than climbing to it, and crucially it is SCHEDULED. Skipping
108
+ // the schedule made this the one exit that never self-corrects: the rows stayed due on
109
+ // every tick forever, emitting an error line each time, and — because the pending window
110
+ // is ordered and bounded — they filled it and starved every other document. The operator's
111
+ // work on an unrelated document would silently never leave the machine, with the only
112
+ // signal being an error naming a different one.
113
+ this.#logger.error("document.delivery.no_peer", {
114
+ documentId,
115
+ pending: envelopes.length,
116
+ correlationId,
117
+ });
118
+ for (const e of envelopes) {
119
+ this.#store.recordDeliveryAttempt(ownerAgentId, documentId, e.envelopeHash, nowMs + DELIVERY_UNRESOLVABLE_RETRY_MS);
120
+ }
121
+ result.failed += envelopes.length;
122
+ continue;
123
+ }
124
+ let reachable;
125
+ try {
126
+ reachable = await this.#transport.isPeerReachable(peerAgentId);
127
+ }
128
+ catch (err) {
129
+ // A FAILED LOOKUP IS NOT AN OFFLINE PEER. Recording it as "away" would report a directory
130
+ // outage to the operator as their collaborator being absent — an error substituted for a
131
+ // different error, and one that sends them to ask the wrong person.
132
+ this.#logger.warn("document.delivery.lookup_failed", {
133
+ documentId,
134
+ peerAgentId,
135
+ correlationId,
136
+ reason: err instanceof Error ? err.message : String(err),
137
+ });
138
+ this.#defer(ownerAgentId, documentId, envelopes, nowMs);
139
+ result.deferred += envelopes.length;
140
+ continue;
141
+ }
142
+ if (!reachable) {
143
+ this.#logger.info("document.delivery.peer_unreachable", {
144
+ documentId,
145
+ peerAgentId,
146
+ pending: envelopes.length,
147
+ correlationId,
148
+ });
149
+ this.#defer(ownerAgentId, documentId, envelopes, nowMs);
150
+ result.deferred += envelopes.length;
151
+ continue;
152
+ }
153
+ for (const envelope of envelopes) {
154
+ result.attempted += 1;
155
+ // CLAIM THE ROW BEFORE DIALING. Scheduling only on the outcome leaves the row due for the
156
+ // whole duration of the dial, and leaves it due FOREVER if the daemon dies mid-dial. The
157
+ // claim is corrected below when the outcome lands.
158
+ const attempts = this.#store.recordDeliveryAttempt(ownerAgentId, documentId, envelope.envelopeHash, nowMs + backoffFor(envelope.attempts ?? 0));
159
+ let outcome;
160
+ try {
161
+ outcome = await this.#transport.deliver({
162
+ peerAgentId,
163
+ documentId,
164
+ envelope,
165
+ sessionHint: opts.sessionHint,
166
+ correlationId,
167
+ });
168
+ }
169
+ catch (err) {
170
+ // A THROW IS A FAILURE, never a success. Wrapped here rather than left to propagate so
171
+ // one envelope's transport fault does not abandon the rest of the backlog mid-pass.
172
+ outcome = {
173
+ ok: false,
174
+ reason: "document_delivery_threw",
175
+ detail: err instanceof Error ? err.message : String(err),
176
+ };
177
+ }
178
+ if (outcome.ok && outcome.admitted === null) {
179
+ // IN FLIGHT. The content left, and the peer has not answered. Recorded as DELIVERED so
180
+ // the operator can see it went, and left unacked so the worker keeps asking — on the
181
+ // capped backoff the claim above already scheduled, not at full rate.
182
+ this.#store.markDelivered(ownerAgentId, documentId, envelope.envelopeHash, nowMs);
183
+ this.#logger.info("document.delivery.sent", {
184
+ documentId,
185
+ envelopeHash: envelope.envelopeHash,
186
+ sessionId: outcome.sessionId,
187
+ sessionOpened: outcome.sessionOpened,
188
+ correlationId,
189
+ });
190
+ result.sent += 1;
191
+ }
192
+ else if (outcome.ok) {
193
+ // ACK = the peer's daemon ANSWERED, admitted or not. A rejection is an ack for delivery
194
+ // purposes (§3.2's 0x05): the peer has decided, so there is nothing left to retry, and
195
+ // retrying would re-trigger their gate and their retry counter until the document stalls
196
+ // for reasons the operator cannot see. Supersession is REJECT-1's job, not the worker's.
197
+ const first = this.#store.markAcked(ownerAgentId, documentId, envelope.envelopeHash, nowMs);
198
+ this.#logger.info("document.delivery.session", {
199
+ documentId,
200
+ sessionId: outcome.sessionId,
201
+ opened: outcome.sessionOpened,
202
+ correlationId,
203
+ });
204
+ if (outcome.admitted) {
205
+ this.#logger.info("document.delivery.acked", {
206
+ documentId,
207
+ envelopeHash: envelope.envelopeHash,
208
+ sessionId: outcome.sessionId,
209
+ firstAck: first,
210
+ correlationId,
211
+ });
212
+ result.delivered += 1;
213
+ }
214
+ else {
215
+ this.#logger.warn("document.delivery.rejected", {
216
+ documentId,
217
+ envelopeHash: envelope.envelopeHash,
218
+ sessionId: outcome.sessionId,
219
+ reason: outcome.rejectionReason,
220
+ correlationId,
221
+ });
222
+ result.rejected += 1;
223
+ }
224
+ }
225
+ else {
226
+ this.#logger.warn("document.delivery.failed", {
227
+ documentId,
228
+ envelopeHash: envelope.envelopeHash,
229
+ reason: outcome.reason,
230
+ detail: outcome.detail,
231
+ attempts,
232
+ correlationId,
233
+ });
234
+ result.failed += 1;
235
+ }
236
+ }
237
+ }
238
+ return result;
239
+ }
240
+ #defer(ownerAgentId, documentId, envelopes, nowMs) {
241
+ for (const e of envelopes) {
242
+ this.#store.recordDeliveryAttempt(ownerAgentId, documentId, e.envelopeHash, nowMs + backoffFor(e.attempts ?? 0));
243
+ }
244
+ }
245
+ }
246
+ //# sourceMappingURL=document-delivery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-delivery.js","sourceRoot":"","sources":["../src/document-delivery.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"document-engine.d.ts","sourceRoot":"","sources":["../src/document-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AACzB,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC,MAAM,MAAM,qBAAqB,GAC7B,2BAA2B,GAC3B,2BAA2B,GAC3B,2BAA2B,GAC3B,yCAAyC,GACzC,6BAA6B,GAC7B,8BAA8B,GAC9B,0BAA0B,CAAC;AAE/B,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE;AACd,kEAAkE;GAChE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,qBAAqB,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAElE;oDACoD;AACpD,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,MAAM,EAAE,qBAAqB,CAAC;IACvC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;gBACb,MAAM,EAAE,qBAAqB,EAAE,MAAM,CAAC,EAAE,MAAM;CAM3D;AAcD,qBAAa,cAAc;;gBAGb,MAAM,EAAE,MAAM;IAI1B,2EAA2E;IAC3E,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED;;;;;;;;OAQG;IACH,cAAc,CAAC,eAAe,SAAK,GAAG,CAAC,CAAC,GAAG;IAM3C;;;;;;;OAOG;IACH,YAAY,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,GAAG,MAAM;IAIhC,kBAAkB,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAIjE,sFAAsF;IACtF,WAAW,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,gBAAgB,CAAC,EAAE,UAAU,GAAG,UAAU;IAIlE,iBAAiB,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,GAAG,UAAU;IAIzC,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,GAAG;QAAE,MAAM,EAAE,UAAU,CAAC;QAAC,WAAW,EAAE,UAAU,CAAA;KAAE;IAIrE;;;;;OAKG;IACH,OAAO,CAAC,MAAM,EAAE,UAAU,GAAG,CAAC,CAAC,GAAG;IA4BlC;;;;;;OAMG;IACH;;;;;;;;;OASG;IACH,WAAW,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,GAAG,oBAAoB;IA6EjE,wEAAwE;IACxE,kBAAkB,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI;IAKxD;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,MAAM,CAAC,SAAS,EAAE,SAAS,mBAAmB,EAAE,GAAG;QAAE,MAAM,EAAE,UAAU,CAAC;QAAC,WAAW,EAAE,UAAU,CAAA;KAAE;CA6CnG"}
1
+ {"version":3,"file":"document-engine.d.ts","sourceRoot":"","sources":["../src/document-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AACzB,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC,MAAM,MAAM,qBAAqB,GAC7B,2BAA2B,GAC3B,2BAA2B,GAC3B,2BAA2B,GAC3B,yCAAyC,GACzC,6BAA6B,GAC7B,8BAA8B,GAC9B,0BAA0B,CAAC;AAE/B,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE;AACd,kEAAkE;GAChE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,qBAAqB,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAElE;oDACoD;AACpD,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,MAAM,EAAE,qBAAqB,CAAC;IACvC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;gBACb,MAAM,EAAE,qBAAqB,EAAE,MAAM,CAAC,EAAE,MAAM;CAM3D;AAcD,qBAAa,cAAc;;gBAGb,MAAM,EAAE,MAAM;IAI1B,2EAA2E;IAC3E,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED;;;;;;;;OAQG;IACH,cAAc,CAAC,eAAe,SAAK,GAAG,CAAC,CAAC,GAAG;IAM3C;;;;;;;OAOG;IACH,YAAY,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,GAAG,MAAM;IAIhC,kBAAkB,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAIjE,sFAAsF;IACtF,WAAW,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,gBAAgB,CAAC,EAAE,UAAU,GAAG,UAAU;IAIlE,iBAAiB,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,GAAG,UAAU;IAIzC,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,GAAG;QAAE,MAAM,EAAE,UAAU,CAAC;QAAC,WAAW,EAAE,UAAU,CAAA;KAAE;IAIrE;;;;;OAKG;IACH,OAAO,CAAC,MAAM,EAAE,UAAU,GAAG,CAAC,CAAC,GAAG;IA4BlC;;;;;;OAMG;IACH;;;;;;;;;OASG;IACH,WAAW,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,GAAG,oBAAoB;IAmFjE,wEAAwE;IACxE,kBAAkB,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI;IAKxD;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,MAAM,CAAC,SAAS,EAAE,SAAS,mBAAmB,EAAE,GAAG;QAAE,MAAM,EAAE,UAAU,CAAC;QAAC,WAAW,EAAE,UAAU,CAAA;KAAE;CA6CnG"}
@@ -171,8 +171,12 @@ export class DocumentEngine {
171
171
  // otherwise BUFFER an out-of-order update and resolve it on the next state-vector exchange;
172
172
  // this refuses instead. That is right for replay, where a gap means a corrupt log — a live
173
173
  // receive path wanting buffer-and-re-request semantics needs its own entry point, not this one.
174
- if (shadow.store.pendingStructs !== null) {
175
- return this.#refuse("document_update_unresolved_dependencies", `depends on ${shadow.store.pendingStructs.missing.size} client(s) whose earlier operations are absent`, update);
174
+ // BOTH pending sets: an update carrying a delete set for structs never seen leaves
175
+ // pendingStructs null and pendingDs populated, and retains just the same.
176
+ if (shadow.store.pendingStructs !== null || shadow.store.pendingDs !== null) {
177
+ return this.#refuse("document_update_unresolved_dependencies", shadow.store.pendingStructs
178
+ ? `depends on ${shadow.store.pendingStructs.missing.size} client(s) whose earlier operations are absent`
179
+ : "carries a delete set referring to operations this document has never seen", update);
176
180
  }
177
181
  Y.applyUpdate(doc, update);
178
182
  return { ok: true };
@@ -198,8 +202,10 @@ export class DocumentEngine {
198
202
  catch (err) {
199
203
  return this.#refuse("document_update_malformed", err instanceof Error ? err.message : String(err), update);
200
204
  }
201
- if (doc.store.pendingStructs !== null) {
202
- return this.#refuse("document_update_unresolved_dependencies", `depends on ${doc.store.pendingStructs.missing.size} client(s) whose earlier operations are absent`, update);
205
+ if (doc.store.pendingStructs !== null || doc.store.pendingDs !== null) {
206
+ return this.#refuse("document_update_unresolved_dependencies", doc.store.pendingStructs
207
+ ? `depends on ${doc.store.pendingStructs.missing.size} client(s) whose earlier operations are absent`
208
+ : "carries a delete set referring to operations this document has never seen", update);
203
209
  }
204
210
  return { ok: true };
205
211
  }