wire-mesh-core 4.1.5 → 4.2.1
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.md +2 -1
- package/dist/adapters/tls-transport.cjs +2 -0
- package/dist/adapters/tls-transport.mjs +2 -0
- package/dist/domain/async-queue.cjs +48 -12
- package/dist/domain/async-queue.d.cts +1 -1
- package/dist/domain/async-queue.d.mts +1 -1
- package/dist/domain/async-queue.mjs +48 -12
- package/dist/domain/bulk.cjs +50 -10
- package/dist/domain/bulk.d.cts +5 -1
- package/dist/domain/bulk.d.mts +5 -1
- package/dist/domain/bulk.mjs +50 -11
- package/dist/domain/capability-grant.d.cts +1 -1
- package/dist/domain/capability-grant.d.mts +1 -1
- package/dist/domain/capability-request.cjs +2 -5
- package/dist/domain/capability-request.d.cts +1 -1
- package/dist/domain/capability-request.d.mts +1 -1
- package/dist/domain/capability-request.mjs +1 -4
- package/dist/domain/consume-incoming.cjs +96 -0
- package/dist/domain/consume-incoming.d.cts +27 -0
- package/dist/domain/consume-incoming.d.mts +27 -0
- package/dist/domain/consume-incoming.mjs +93 -0
- package/dist/domain/direct-manage-request.cjs +25 -12
- package/dist/domain/direct-manage-request.d.cts +1 -1
- package/dist/domain/direct-manage-request.d.mts +1 -1
- package/dist/domain/direct-manage-request.mjs +25 -12
- package/dist/domain/fault-report.cjs +136 -0
- package/dist/domain/fault-report.d.cts +25 -0
- package/dist/domain/fault-report.d.mts +25 -0
- package/dist/domain/fault-report.mjs +129 -0
- package/dist/domain/gossip-expansion.cjs +1 -1
- package/dist/domain/gossip-expansion.d.cts +1 -1
- package/dist/domain/gossip-expansion.d.mts +1 -1
- package/dist/domain/gossip-expansion.mjs +1 -1
- package/dist/domain/grant-candidates.d.cts +1 -1
- package/dist/domain/grant-candidates.d.mts +1 -1
- package/dist/domain/mesh-session.cjs +373 -126
- package/dist/domain/mesh-session.d.cts +2 -162
- package/dist/domain/mesh-session.d.mts +2 -162
- package/dist/domain/mesh-session.mjs +373 -126
- package/dist/domain/path-trace.d.cts +1 -1
- package/dist/domain/path-trace.d.mts +1 -1
- package/dist/domain/ping-round-trips.cjs +21 -8
- package/dist/domain/ping-round-trips.mjs +21 -8
- package/dist/domain/room-rekey.d.cts +1 -1
- package/dist/domain/room-rekey.d.mts +1 -1
- package/dist/domain/topology.d.cts +1 -1
- package/dist/domain/topology.d.mts +1 -1
- package/dist/domain/webrtc-signaling.d.cts +1 -1
- package/dist/domain/webrtc-signaling.d.mts +1 -1
- package/dist/mesh-session-By06Mnu-.d.cts +174 -0
- package/dist/mesh-session-DzcZYdhe.d.mts +174 -0
- package/dist/timers-DTJNuBa8.cjs +12 -0
- package/dist/timers-Q1_kDTyY.mjs +7 -0
- package/package.json +11 -2
package/README.md
CHANGED
|
@@ -14,8 +14,9 @@ The TypeScript implementation of wire-mesh's protocol, built ports/adapters: dom
|
|
|
14
14
|
- **Handshake negotiation** (`src/domain/handshake.ts`) -- protocol-version and capability-domain negotiation between two peers, the mechanism agent-comms issue #31 is fixed by.
|
|
15
15
|
- **Capability-token verification** (`src/domain/tokens.ts`) -- the full chain tokens.cddl documents: COSE_Sign1 signature verification, the self-certifying issuer-key check (`sha256(issuer-key.public-key) == issuer`), expiry/not-before, issuer-matched revocation (only a token's own issuer's signed revocation-entry counts, checked across every ancestor in the delegation chain, not just the leaf), and recursive delegation-chain narrowing across all three axes of authority: a delegated token's issuer must be its parent's bearer (the chain is unbroken), its expiry must not exceed its parent's, and its scope must narrow its parent's (identical kind, equal-or-descendant path when the parent carries one) with an identical capability verb (the verb grammar has no sub-verb relation, so a different verb is different authority, not narrower). Also exports `verifyRevocationEntry` for ingesting gossiped revocation-announce frames: each entry is itself a signed, self-certifying COSE_Sign1 over revocation-claims, verified before it may enter the revocation view.
|
|
16
16
|
- **Peer-advert verification** (`src/domain/peer-advert.ts`): the two checks transport.cddl states for a gossiped advert, applied by the relay hub before it registers or forwards one and by every session before one reaches its directory: the embedded identity-key self-certifies (`sha256(identity-key.public-key) == device`), and the signature verifies under that same key over the domain-separated canonical encoding of the advert with its signature removed, which covers the open extension tail as well. Deliberately not tokens.cddl's COSE Sig_structure: an advert's open tail lets an attacker craft one whose encoding also satisfies token-claims, so the prefixed construction keeps the two byte strings disjoint. Reaches a verdict for every input rather than throwing, since an advert's algorithm and key bytes are attacker-chosen.
|
|
17
|
-
- **Secure channel** (`src/domain/secure-channel.ts`, `src/domain/relay-channels.ts`): the end-to-end channel spec/secure-channel.cddl defines between two devices that reach each other through a relay. A session opens one with each peer it addresses through a relay pairing, seals every manage-request, manage-response and core/data frame in it, drops any that arrives as plain relay-data, and attributes a request to the identity the peer's handshake proved rather than to the `from-device` a hub stamps. There is no plaintext fallback, so a peer that does not speak it cannot be reached through a relay.
|
|
17
|
+
- **Secure channel** (`src/domain/secure-channel.ts`, `src/domain/relay-channels.ts`): the end-to-end channel spec/secure-channel.cddl defines between two devices that reach each other through a relay. A session opens one with each peer it addresses through a relay pairing, seals every manage-request, manage-response and core/data frame in it, drops any that arrives as plain relay-data, and attributes a request to the identity the peer's handshake proved rather than to the `from-device` a hub stamps. A hello never replaces a channel by itself: it builds a candidate beside it, which takes over only once a frame from the peer opens under it, and a side does not answer a hello while its own handshake with that peer is outstanding. There is no plaintext fallback, so a peer that does not speak it cannot be reached through a relay.
|
|
18
18
|
- **Relay hub** (`src/domain/relay-hub.ts`, `src/domain/relay-pairing.ts`, `src/domain/hub-mailbox.ts`): the hub's relay role against the Transport port alone. It verifies every gossiped advert before registering, forwarding or replaying it, pairs devices for relayed traffic, and, as a mailbox, replicates the `core/data` logs of devices that are offline.
|
|
19
|
+
- **Consuming incoming streams** (`src/domain/consume-incoming.ts`, `src/domain/fault-report.ts`): the one way to read a session's incoming streams (`incomingManageRequests`, `incomingDataFrames`, `revocationAnnouncements`, `coordinatorFrames`, `events`). `consumeEach` handles each item in order and contains a failure while handling one, so it never ends the loop or rejects to whoever started it; `serveManageRequests` also answers a request whose handling failed with a fixed `internal-error` refusal that carries no text from the failure. A failure is told to an optional `onFault` callback through a reporter that allows one report per source per interval, with a count of those left out, and that contains a callback that throws or rejects. A loop can be stopped with an `AbortSignal`, which closes the stream's iterator so the stream's next reader takes what the stopped one did not.
|
|
19
20
|
- **Bulk transfer** (`src/domain/bulk.ts`): resumable, flow-controlled `core/bulk` transfer, with a byte-credit window that the receiver's acknowledgements move. `src/domain/sharded-delivery.ts` composes it with erasure coding (`src/domain/erasure-coding.ts`) and shard manifests (`src/domain/shard-manifest.ts`) to spread a payload across holder devices.
|
|
20
21
|
- **Data sync** (`src/domain/data-sync.ts`): the have, request and entries primitive over a storage-backed oplog, which never accepts a gap. What to broadcast, whose logs to track and which holder to ask are left to the application.
|
|
21
22
|
- **Coordinator election** (`src/domain/coordinator-election.ts`): the gossiped, term-based election over `coordinator-frame`, where a higher term supersedes a lower one and equal terms are broken by lowest device-id.
|
|
@@ -149,6 +149,8 @@ function createTlsTransport(identity) {
|
|
|
149
149
|
}, (socket) => {
|
|
150
150
|
authenticatedPeerDeviceId(socket).then((peerDeviceId) => {
|
|
151
151
|
onConnection(wrapSocket(socket, peerDeviceId));
|
|
152
|
+
}).catch((error) => {
|
|
153
|
+
socket.destroy(error instanceof Error ? error : void 0);
|
|
152
154
|
});
|
|
153
155
|
});
|
|
154
156
|
server.once("error", reject);
|
|
@@ -148,6 +148,8 @@ function createTlsTransport(identity) {
|
|
|
148
148
|
}, (socket) => {
|
|
149
149
|
authenticatedPeerDeviceId(socket).then((peerDeviceId) => {
|
|
150
150
|
onConnection(wrapSocket(socket, peerDeviceId));
|
|
151
|
+
}).catch((error) => {
|
|
152
|
+
socket.destroy(error instanceof Error ? error : void 0);
|
|
151
153
|
});
|
|
152
154
|
});
|
|
153
155
|
server.once("error", reject);
|
|
@@ -10,19 +10,55 @@ function createAsyncQueue() {
|
|
|
10
10
|
else backlog.push(value);
|
|
11
11
|
},
|
|
12
12
|
stream: { [Symbol.asyncIterator]() {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
let finished = false;
|
|
14
|
+
const releases = /* @__PURE__ */ new Set();
|
|
15
|
+
return {
|
|
16
|
+
next: async () => new Promise((resolve) => {
|
|
17
|
+
if (finished) {
|
|
18
|
+
resolve({
|
|
19
|
+
value: void 0,
|
|
20
|
+
done: true
|
|
21
|
+
});
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const backlogValue = backlog.shift();
|
|
25
|
+
if (backlogValue !== void 0) {
|
|
26
|
+
resolve({
|
|
27
|
+
value: backlogValue,
|
|
28
|
+
done: false
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const registration = {};
|
|
33
|
+
const waiter = (value) => {
|
|
34
|
+
if (registration.release !== void 0) releases.delete(registration.release);
|
|
35
|
+
resolve({
|
|
36
|
+
value,
|
|
37
|
+
done: false
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
registration.release = () => {
|
|
41
|
+
waiters.splice(waiters.indexOf(waiter), 1);
|
|
42
|
+
resolve({
|
|
43
|
+
value: void 0,
|
|
44
|
+
done: true
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
waiters.push(waiter);
|
|
48
|
+
releases.add(registration.release);
|
|
49
|
+
}),
|
|
50
|
+
return: async () => {
|
|
51
|
+
finished = true;
|
|
52
|
+
for (const release of [...releases]) {
|
|
53
|
+
releases.delete(release);
|
|
54
|
+
release();
|
|
55
|
+
}
|
|
56
|
+
return Promise.resolve({
|
|
57
|
+
value: void 0,
|
|
58
|
+
done: true
|
|
23
59
|
});
|
|
24
|
-
}
|
|
25
|
-
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
26
62
|
} }
|
|
27
63
|
};
|
|
28
64
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region src/domain/async-queue.d.ts
|
|
2
|
-
/** A backlog-plus-waiters async queue: push() delivers to whichever consumer is already waiting on next(), or buffers the value for a consumer that hasn't asked yet. mesh-session.ts used this exact pattern three times over (events, incomingManageRequests, revocationAnnouncements) with nothing session-shaped about it -- extracted here once new features kept pushing that file over this repo's own max-lines lint budget. Never ends on its own (matches every one of mesh-session.ts's own async iterables, none of which the session itself ever closes): a consumer stops by
|
|
2
|
+
/** A backlog-plus-waiters async queue: push() delivers to whichever consumer is already waiting on next(), or buffers the value for a consumer that hasn't asked yet. mesh-session.ts used this exact pattern three times over (events, incomingManageRequests, revocationAnnouncements) with nothing session-shaped about it -- extracted here once new features kept pushing that file over this repo's own max-lines lint budget. Never ends on its own (matches every one of mesh-session.ts's own async iterables, none of which the session itself ever closes): a consumer stops by calling return() on its iterator, which also withdraws a next() it has waiting. */
|
|
3
3
|
export interface AsyncQueue<T> {
|
|
4
4
|
/** Delivers value to whichever consumer's own next() call is already waiting, or buffers it for the next one that asks. */
|
|
5
5
|
push: (value: T) => void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region src/domain/async-queue.d.ts
|
|
2
|
-
/** A backlog-plus-waiters async queue: push() delivers to whichever consumer is already waiting on next(), or buffers the value for a consumer that hasn't asked yet. mesh-session.ts used this exact pattern three times over (events, incomingManageRequests, revocationAnnouncements) with nothing session-shaped about it -- extracted here once new features kept pushing that file over this repo's own max-lines lint budget. Never ends on its own (matches every one of mesh-session.ts's own async iterables, none of which the session itself ever closes): a consumer stops by
|
|
2
|
+
/** A backlog-plus-waiters async queue: push() delivers to whichever consumer is already waiting on next(), or buffers the value for a consumer that hasn't asked yet. mesh-session.ts used this exact pattern three times over (events, incomingManageRequests, revocationAnnouncements) with nothing session-shaped about it -- extracted here once new features kept pushing that file over this repo's own max-lines lint budget. Never ends on its own (matches every one of mesh-session.ts's own async iterables, none of which the session itself ever closes): a consumer stops by calling return() on its iterator, which also withdraws a next() it has waiting. */
|
|
3
3
|
export interface AsyncQueue<T> {
|
|
4
4
|
/** Delivers value to whichever consumer's own next() call is already waiting, or buffers it for the next one that asks. */
|
|
5
5
|
push: (value: T) => void;
|
|
@@ -9,19 +9,55 @@ function createAsyncQueue() {
|
|
|
9
9
|
else backlog.push(value);
|
|
10
10
|
},
|
|
11
11
|
stream: { [Symbol.asyncIterator]() {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
let finished = false;
|
|
13
|
+
const releases = /* @__PURE__ */ new Set();
|
|
14
|
+
return {
|
|
15
|
+
next: async () => new Promise((resolve) => {
|
|
16
|
+
if (finished) {
|
|
17
|
+
resolve({
|
|
18
|
+
value: void 0,
|
|
19
|
+
done: true
|
|
20
|
+
});
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const backlogValue = backlog.shift();
|
|
24
|
+
if (backlogValue !== void 0) {
|
|
25
|
+
resolve({
|
|
26
|
+
value: backlogValue,
|
|
27
|
+
done: false
|
|
28
|
+
});
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const registration = {};
|
|
32
|
+
const waiter = (value) => {
|
|
33
|
+
if (registration.release !== void 0) releases.delete(registration.release);
|
|
34
|
+
resolve({
|
|
35
|
+
value,
|
|
36
|
+
done: false
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
registration.release = () => {
|
|
40
|
+
waiters.splice(waiters.indexOf(waiter), 1);
|
|
41
|
+
resolve({
|
|
42
|
+
value: void 0,
|
|
43
|
+
done: true
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
waiters.push(waiter);
|
|
47
|
+
releases.add(registration.release);
|
|
48
|
+
}),
|
|
49
|
+
return: async () => {
|
|
50
|
+
finished = true;
|
|
51
|
+
for (const release of [...releases]) {
|
|
52
|
+
releases.delete(release);
|
|
53
|
+
release();
|
|
54
|
+
}
|
|
55
|
+
return Promise.resolve({
|
|
56
|
+
value: void 0,
|
|
57
|
+
done: true
|
|
22
58
|
});
|
|
23
|
-
}
|
|
24
|
-
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
25
61
|
} }
|
|
26
62
|
};
|
|
27
63
|
}
|
package/dist/domain/bulk.cjs
CHANGED
|
@@ -70,33 +70,63 @@ function buildBulkCancelCommand(transferId, capability, reason) {
|
|
|
70
70
|
}
|
|
71
71
|
};
|
|
72
72
|
}
|
|
73
|
+
/** How long a sender waits for the receiver's next ack before giving the stream up as incomplete, so that a receiver that stays connected and goes silent cannot hold the stream, and its registration, for ever. A transfer resumes from the receiver's own ack-seq, so giving up loses nothing. */
|
|
74
|
+
const DEFAULT_ACK_TIMEOUT_MS = 6e4;
|
|
73
75
|
/**
|
|
74
76
|
* Builds a sender coordinator over one `source`. A caller sending several independent transfers from different sources constructs one sender per source, mirroring createCapabilityRequestHandler's own one-instance-per-capability convention.
|
|
75
77
|
*/
|
|
76
78
|
function createBulkSender(options) {
|
|
77
79
|
const pendingAcks = /* @__PURE__ */ new Map();
|
|
78
80
|
const cancelled = /* @__PURE__ */ new Set();
|
|
81
|
+
/** Connections whose receive ended. A transfer started over one, or an ack awaited after it ended, is incomplete at once: nothing will ever deliver an ack over it. */
|
|
82
|
+
const ended = /* @__PURE__ */ new WeakSet();
|
|
79
83
|
function resolveNextAck(id, frame) {
|
|
80
84
|
const queue = pendingAcks.get(id);
|
|
81
|
-
|
|
82
|
-
queue.
|
|
85
|
+
const waiter = queue?.shift();
|
|
86
|
+
if (queue?.length === 0) pendingAcks.delete(id);
|
|
87
|
+
waiter?.resolve(frame);
|
|
83
88
|
}
|
|
84
89
|
function rejectAllAcks(id, error) {
|
|
85
90
|
const queue = pendingAcks.get(id);
|
|
86
91
|
if (queue === void 0) return;
|
|
87
|
-
pendingAcks.
|
|
92
|
+
pendingAcks.delete(id);
|
|
88
93
|
for (const waiter of queue) waiter.reject(error);
|
|
89
94
|
}
|
|
95
|
+
/** Resolves with the next ack for `id`. Rejects when the connection ends or when `ackTimeoutMs` passes first, removing its registration either way, and the transfer's entry goes with its last registration. */
|
|
90
96
|
async function waitForAck(id) {
|
|
91
97
|
return new Promise((resolve, reject) => {
|
|
92
98
|
const queue = pendingAcks.get(id) ?? [];
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
99
|
+
const timer = {};
|
|
100
|
+
const entry = {
|
|
101
|
+
resolve: (frame) => {
|
|
102
|
+
clearTimeout(timer.handle);
|
|
103
|
+
resolve(frame);
|
|
104
|
+
},
|
|
105
|
+
reject: (error) => {
|
|
106
|
+
clearTimeout(timer.handle);
|
|
107
|
+
reject(error);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
timer.handle = setTimeout(() => {
|
|
111
|
+
remove(id, entry);
|
|
112
|
+
reject(/* @__PURE__ */ new Error("timed out waiting for the receiver's ack"));
|
|
113
|
+
}, options.ackTimeoutMs ?? 6e4);
|
|
114
|
+
queue.push(entry);
|
|
97
115
|
pendingAcks.set(id, queue);
|
|
98
116
|
});
|
|
99
117
|
}
|
|
118
|
+
/** waitForAck over a connection: refused at once when that connection has already ended. */
|
|
119
|
+
async function waitForAckOver(connection, id) {
|
|
120
|
+
if (ended.has(connection)) throw new Error("connection ended");
|
|
121
|
+
return waitForAck(id);
|
|
122
|
+
}
|
|
123
|
+
function remove(id, entry) {
|
|
124
|
+
const queue = pendingAcks.get(id);
|
|
125
|
+
if (queue === void 0) return;
|
|
126
|
+
const index = queue.indexOf(entry);
|
|
127
|
+
if (index !== -1) queue.splice(index, 1);
|
|
128
|
+
if (queue.length === 0) pendingAcks.delete(id);
|
|
129
|
+
}
|
|
100
130
|
async function stream(connection, transferId, fromSeq, initialWindow) {
|
|
101
131
|
const id = transferIdHex(transferId);
|
|
102
132
|
let seq = fromSeq;
|
|
@@ -116,7 +146,7 @@ function createBulkSender(options) {
|
|
|
116
146
|
const exceedsWindow = (length) => bytesInFlight + length > window;
|
|
117
147
|
/** Takes acks until `length` more bytes fit in the window. */
|
|
118
148
|
const awaitCredit = async (length) => {
|
|
119
|
-
applyAck(await
|
|
149
|
+
applyAck(await waitForAckOver(connection, id));
|
|
120
150
|
if (exceedsWindow(length)) await awaitCredit(length);
|
|
121
151
|
};
|
|
122
152
|
try {
|
|
@@ -126,7 +156,7 @@ function createBulkSender(options) {
|
|
|
126
156
|
ackedCount
|
|
127
157
|
};
|
|
128
158
|
if (exceedsWindow(chunk.length)) {
|
|
129
|
-
applyAck(await
|
|
159
|
+
applyAck(await waitForAckOver(connection, id));
|
|
130
160
|
if (exceedsWindow(chunk.length)) await awaitCredit(chunk.length);
|
|
131
161
|
}
|
|
132
162
|
const dataFrame = {
|
|
@@ -164,7 +194,15 @@ function createBulkSender(options) {
|
|
|
164
194
|
}
|
|
165
195
|
/** Waits for the receiver's own post-accept bulk-ack -- ack-seq is a count of chunks durably persisted so far, so it doubles as the seq to resume streaming from with no adjustment -- and starts streaming from there. Shared by open() (after bulk.open is accepted, where the very first ack's own ack-seq is naturally 0 for a fresh transfer) and continueFrom() (after a bulk.resume is accepted), since a receiver sends this identical proactive ack in both cases per spec/bulk.cddl's own bulk-ack-frame comment. */
|
|
166
196
|
async function resumeStream(connection, transferId) {
|
|
167
|
-
|
|
197
|
+
let ack;
|
|
198
|
+
try {
|
|
199
|
+
ack = await waitForAckOver(connection, transferIdHex(transferId));
|
|
200
|
+
} catch {
|
|
201
|
+
return {
|
|
202
|
+
status: "incomplete",
|
|
203
|
+
ackedCount: 0
|
|
204
|
+
};
|
|
205
|
+
}
|
|
168
206
|
return stream(connection, transferId, ack["ack-seq"], ack.window);
|
|
169
207
|
}
|
|
170
208
|
let activeTransferId;
|
|
@@ -175,6 +213,7 @@ function createBulkSender(options) {
|
|
|
175
213
|
resolveNextAck(transferIdHex(frame["transfer-id"]), frame);
|
|
176
214
|
},
|
|
177
215
|
onConnectionEnd(connection) {
|
|
216
|
+
ended.add(connection);
|
|
178
217
|
if (activeTransferId === void 0 || activeConnection !== connection) return;
|
|
179
218
|
rejectAllAcks(activeTransferId, /* @__PURE__ */ new Error("connection ended"));
|
|
180
219
|
},
|
|
@@ -427,6 +466,7 @@ function createBulkReceiver(options) {
|
|
|
427
466
|
//#endregion
|
|
428
467
|
exports.BULK_READ_CAPABILITY = BULK_READ_CAPABILITY;
|
|
429
468
|
exports.BULK_WRITE_CAPABILITY = BULK_WRITE_CAPABILITY;
|
|
469
|
+
exports.DEFAULT_ACK_TIMEOUT_MS = DEFAULT_ACK_TIMEOUT_MS;
|
|
430
470
|
exports.buildBulkCancelCommand = buildBulkCancelCommand;
|
|
431
471
|
exports.buildBulkOpenCommand = buildBulkOpenCommand;
|
|
432
472
|
exports.buildBulkResumeCommand = buildBulkResumeCommand;
|
package/dist/domain/bulk.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { D as DeviceId, N as Frame, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-Cb3BnuTL.cjs";
|
|
2
2
|
import { t as Connection } from "../transport-DaeS9N-o.cjs";
|
|
3
3
|
import { t as KeyValueStorage } from "../storage-jbGFAYIV.cjs";
|
|
4
|
-
import { IncomingManageRequest, MeshSession } from "
|
|
4
|
+
import { d as IncomingManageRequest, p as MeshSession } from "../mesh-session-By06Mnu-.cjs";
|
|
5
5
|
//#region src/domain/bulk.d.ts
|
|
6
6
|
/** The two capabilities matching the direction each manage-command actually flows (spec/bulk.cddl): the sender proves write authority to open a transfer, the receiver proves read authority to ask for one to continue. */
|
|
7
7
|
export declare const BULK_WRITE_CAPABILITY = "bulk:write";
|
|
@@ -24,7 +24,11 @@ export interface CreateBulkSenderOptions {
|
|
|
24
24
|
* Yields every chunk of the transfer starting at `fromSeq` (0 for a fresh transfer) -- MUST be callable more than once, from any previously-reached seq, and yield the identical bytes each time (spec/bulk.cddl's own "a file is inherently durable/re-readable" design point): this is what makes both mid-transfer resume and the post-transfer digest re-derivation below correct without this module buffering the whole transfer in memory during the send itself.
|
|
25
25
|
*/
|
|
26
26
|
source: (fromSeq: number) => AsyncIterable<Uint8Array<ArrayBuffer>>;
|
|
27
|
+
/** How long to wait for each ack before the stream returns "incomplete"; DEFAULT_ACK_TIMEOUT_MS when absent. */
|
|
28
|
+
ackTimeoutMs?: number;
|
|
27
29
|
}
|
|
30
|
+
/** How long a sender waits for the receiver's next ack before giving the stream up as incomplete, so that a receiver that stays connected and goes silent cannot hold the stream, and its registration, for ever. A transfer resumes from the receiver's own ack-seq, so giving up loses nothing. */
|
|
31
|
+
export declare const DEFAULT_ACK_TIMEOUT_MS = 60000;
|
|
28
32
|
export interface BulkSenderOpenOptions {
|
|
29
33
|
transferId?: Uint8Array<ArrayBuffer>;
|
|
30
34
|
totalSize?: number;
|
package/dist/domain/bulk.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { D as DeviceId, N as Frame, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-Cb3BnuTL.mjs";
|
|
2
2
|
import { t as Connection } from "../transport-CAD419ge.mjs";
|
|
3
3
|
import { t as KeyValueStorage } from "../storage-jbGFAYIV.mjs";
|
|
4
|
-
import { IncomingManageRequest, MeshSession } from "
|
|
4
|
+
import { d as IncomingManageRequest, p as MeshSession } from "../mesh-session-DzcZYdhe.mjs";
|
|
5
5
|
//#region src/domain/bulk.d.ts
|
|
6
6
|
/** The two capabilities matching the direction each manage-command actually flows (spec/bulk.cddl): the sender proves write authority to open a transfer, the receiver proves read authority to ask for one to continue. */
|
|
7
7
|
export declare const BULK_WRITE_CAPABILITY = "bulk:write";
|
|
@@ -24,7 +24,11 @@ export interface CreateBulkSenderOptions {
|
|
|
24
24
|
* Yields every chunk of the transfer starting at `fromSeq` (0 for a fresh transfer) -- MUST be callable more than once, from any previously-reached seq, and yield the identical bytes each time (spec/bulk.cddl's own "a file is inherently durable/re-readable" design point): this is what makes both mid-transfer resume and the post-transfer digest re-derivation below correct without this module buffering the whole transfer in memory during the send itself.
|
|
25
25
|
*/
|
|
26
26
|
source: (fromSeq: number) => AsyncIterable<Uint8Array<ArrayBuffer>>;
|
|
27
|
+
/** How long to wait for each ack before the stream returns "incomplete"; DEFAULT_ACK_TIMEOUT_MS when absent. */
|
|
28
|
+
ackTimeoutMs?: number;
|
|
27
29
|
}
|
|
30
|
+
/** How long a sender waits for the receiver's next ack before giving the stream up as incomplete, so that a receiver that stays connected and goes silent cannot hold the stream, and its registration, for ever. A transfer resumes from the receiver's own ack-seq, so giving up loses nothing. */
|
|
31
|
+
export declare const DEFAULT_ACK_TIMEOUT_MS = 60000;
|
|
28
32
|
export interface BulkSenderOpenOptions {
|
|
29
33
|
transferId?: Uint8Array<ArrayBuffer>;
|
|
30
34
|
totalSize?: number;
|
package/dist/domain/bulk.mjs
CHANGED
|
@@ -69,33 +69,63 @@ function buildBulkCancelCommand(transferId, capability, reason) {
|
|
|
69
69
|
}
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
|
+
/** How long a sender waits for the receiver's next ack before giving the stream up as incomplete, so that a receiver that stays connected and goes silent cannot hold the stream, and its registration, for ever. A transfer resumes from the receiver's own ack-seq, so giving up loses nothing. */
|
|
73
|
+
const DEFAULT_ACK_TIMEOUT_MS = 6e4;
|
|
72
74
|
/**
|
|
73
75
|
* Builds a sender coordinator over one `source`. A caller sending several independent transfers from different sources constructs one sender per source, mirroring createCapabilityRequestHandler's own one-instance-per-capability convention.
|
|
74
76
|
*/
|
|
75
77
|
function createBulkSender(options) {
|
|
76
78
|
const pendingAcks = /* @__PURE__ */ new Map();
|
|
77
79
|
const cancelled = /* @__PURE__ */ new Set();
|
|
80
|
+
/** Connections whose receive ended. A transfer started over one, or an ack awaited after it ended, is incomplete at once: nothing will ever deliver an ack over it. */
|
|
81
|
+
const ended = /* @__PURE__ */ new WeakSet();
|
|
78
82
|
function resolveNextAck(id, frame) {
|
|
79
83
|
const queue = pendingAcks.get(id);
|
|
80
|
-
|
|
81
|
-
queue.
|
|
84
|
+
const waiter = queue?.shift();
|
|
85
|
+
if (queue?.length === 0) pendingAcks.delete(id);
|
|
86
|
+
waiter?.resolve(frame);
|
|
82
87
|
}
|
|
83
88
|
function rejectAllAcks(id, error) {
|
|
84
89
|
const queue = pendingAcks.get(id);
|
|
85
90
|
if (queue === void 0) return;
|
|
86
|
-
pendingAcks.
|
|
91
|
+
pendingAcks.delete(id);
|
|
87
92
|
for (const waiter of queue) waiter.reject(error);
|
|
88
93
|
}
|
|
94
|
+
/** Resolves with the next ack for `id`. Rejects when the connection ends or when `ackTimeoutMs` passes first, removing its registration either way, and the transfer's entry goes with its last registration. */
|
|
89
95
|
async function waitForAck(id) {
|
|
90
96
|
return new Promise((resolve, reject) => {
|
|
91
97
|
const queue = pendingAcks.get(id) ?? [];
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
98
|
+
const timer = {};
|
|
99
|
+
const entry = {
|
|
100
|
+
resolve: (frame) => {
|
|
101
|
+
clearTimeout(timer.handle);
|
|
102
|
+
resolve(frame);
|
|
103
|
+
},
|
|
104
|
+
reject: (error) => {
|
|
105
|
+
clearTimeout(timer.handle);
|
|
106
|
+
reject(error);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
timer.handle = setTimeout(() => {
|
|
110
|
+
remove(id, entry);
|
|
111
|
+
reject(/* @__PURE__ */ new Error("timed out waiting for the receiver's ack"));
|
|
112
|
+
}, options.ackTimeoutMs ?? 6e4);
|
|
113
|
+
queue.push(entry);
|
|
96
114
|
pendingAcks.set(id, queue);
|
|
97
115
|
});
|
|
98
116
|
}
|
|
117
|
+
/** waitForAck over a connection: refused at once when that connection has already ended. */
|
|
118
|
+
async function waitForAckOver(connection, id) {
|
|
119
|
+
if (ended.has(connection)) throw new Error("connection ended");
|
|
120
|
+
return waitForAck(id);
|
|
121
|
+
}
|
|
122
|
+
function remove(id, entry) {
|
|
123
|
+
const queue = pendingAcks.get(id);
|
|
124
|
+
if (queue === void 0) return;
|
|
125
|
+
const index = queue.indexOf(entry);
|
|
126
|
+
if (index !== -1) queue.splice(index, 1);
|
|
127
|
+
if (queue.length === 0) pendingAcks.delete(id);
|
|
128
|
+
}
|
|
99
129
|
async function stream(connection, transferId, fromSeq, initialWindow) {
|
|
100
130
|
const id = transferIdHex(transferId);
|
|
101
131
|
let seq = fromSeq;
|
|
@@ -115,7 +145,7 @@ function createBulkSender(options) {
|
|
|
115
145
|
const exceedsWindow = (length) => bytesInFlight + length > window;
|
|
116
146
|
/** Takes acks until `length` more bytes fit in the window. */
|
|
117
147
|
const awaitCredit = async (length) => {
|
|
118
|
-
applyAck(await
|
|
148
|
+
applyAck(await waitForAckOver(connection, id));
|
|
119
149
|
if (exceedsWindow(length)) await awaitCredit(length);
|
|
120
150
|
};
|
|
121
151
|
try {
|
|
@@ -125,7 +155,7 @@ function createBulkSender(options) {
|
|
|
125
155
|
ackedCount
|
|
126
156
|
};
|
|
127
157
|
if (exceedsWindow(chunk.length)) {
|
|
128
|
-
applyAck(await
|
|
158
|
+
applyAck(await waitForAckOver(connection, id));
|
|
129
159
|
if (exceedsWindow(chunk.length)) await awaitCredit(chunk.length);
|
|
130
160
|
}
|
|
131
161
|
const dataFrame = {
|
|
@@ -163,7 +193,15 @@ function createBulkSender(options) {
|
|
|
163
193
|
}
|
|
164
194
|
/** Waits for the receiver's own post-accept bulk-ack -- ack-seq is a count of chunks durably persisted so far, so it doubles as the seq to resume streaming from with no adjustment -- and starts streaming from there. Shared by open() (after bulk.open is accepted, where the very first ack's own ack-seq is naturally 0 for a fresh transfer) and continueFrom() (after a bulk.resume is accepted), since a receiver sends this identical proactive ack in both cases per spec/bulk.cddl's own bulk-ack-frame comment. */
|
|
165
195
|
async function resumeStream(connection, transferId) {
|
|
166
|
-
|
|
196
|
+
let ack;
|
|
197
|
+
try {
|
|
198
|
+
ack = await waitForAckOver(connection, transferIdHex(transferId));
|
|
199
|
+
} catch {
|
|
200
|
+
return {
|
|
201
|
+
status: "incomplete",
|
|
202
|
+
ackedCount: 0
|
|
203
|
+
};
|
|
204
|
+
}
|
|
167
205
|
return stream(connection, transferId, ack["ack-seq"], ack.window);
|
|
168
206
|
}
|
|
169
207
|
let activeTransferId;
|
|
@@ -174,6 +212,7 @@ function createBulkSender(options) {
|
|
|
174
212
|
resolveNextAck(transferIdHex(frame["transfer-id"]), frame);
|
|
175
213
|
},
|
|
176
214
|
onConnectionEnd(connection) {
|
|
215
|
+
ended.add(connection);
|
|
177
216
|
if (activeTransferId === void 0 || activeConnection !== connection) return;
|
|
178
217
|
rejectAllAcks(activeTransferId, /* @__PURE__ */ new Error("connection ended"));
|
|
179
218
|
},
|
|
@@ -424,4 +463,4 @@ function createBulkReceiver(options) {
|
|
|
424
463
|
};
|
|
425
464
|
}
|
|
426
465
|
//#endregion
|
|
427
|
-
export { BULK_READ_CAPABILITY, BULK_WRITE_CAPABILITY, buildBulkCancelCommand, buildBulkOpenCommand, buildBulkResumeCommand, createBulkReceiver, createBulkSender, storedTransferSource };
|
|
466
|
+
export { BULK_READ_CAPABILITY, BULK_WRITE_CAPABILITY, DEFAULT_ACK_TIMEOUT_MS, buildBulkCancelCommand, buildBulkOpenCommand, buildBulkResumeCommand, createBulkReceiver, createBulkSender, storedTransferSource };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-Cb3BnuTL.cjs";
|
|
2
2
|
import { r as IdentityPort } from "../identity-CQof0iEM.cjs";
|
|
3
3
|
import { t as Clock } from "../clock-DhUPRfLh.cjs";
|
|
4
|
-
import { IncomingManageRequest, ManageOutcome, MeshSession } from "
|
|
4
|
+
import { d as IncomingManageRequest, f as ManageOutcome, p as MeshSession } from "../mesh-session-By06Mnu-.cjs";
|
|
5
5
|
import { RevocationCheck } from "./tokens.cjs";
|
|
6
6
|
//#region src/domain/capability-grant.d.ts
|
|
7
7
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-Cb3BnuTL.mjs";
|
|
2
2
|
import { r as IdentityPort } from "../identity-CUr-jaAB.mjs";
|
|
3
3
|
import { t as Clock } from "../clock-DhUPRfLh.mjs";
|
|
4
|
-
import { IncomingManageRequest, ManageOutcome, MeshSession } from "
|
|
4
|
+
import { d as IncomingManageRequest, f as ManageOutcome, p as MeshSession } from "../mesh-session-DzcZYdhe.mjs";
|
|
5
5
|
import { RevocationCheck } from "./tokens.mjs";
|
|
6
6
|
//#region src/domain/capability-grant.d.ts
|
|
7
7
|
/**
|
|
@@ -2,6 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
const require_generated_protocol = require("../generated/protocol.cjs");
|
|
3
3
|
const require_domain_token_scope = require("./token-scope.cjs");
|
|
4
4
|
const require_domain_tokens = require("./tokens.cjs");
|
|
5
|
+
const require_timers = require("../timers-DTJNuBa8.cjs");
|
|
5
6
|
//#region src/domain/capability-request.ts
|
|
6
7
|
/**
|
|
7
8
|
* The generic, capability-agnostic capability-request / capability-grant-ok primitive (wire-mesh#78, spec/management.cddl). Lifts core/room's own room.join shape -- an ungated ask, held open, a human (or any other domain-supplied decision) accepts or refuses, acceptance mints and returns a token on the same response -- out of that one domain so any capability can reuse it, not just room:member. core/room's own room-client.ts is the reference consumer: it rewires requestToJoin/createRoomRouter's room.join handling onto requestCapability/createCapabilityRequestHandler below, supplying room-specific extension fields (its member list) through this module's own extensions mechanism rather than this module knowing anything about rooms.
|
|
@@ -14,10 +15,6 @@ function randomTokenId() {
|
|
|
14
15
|
crypto.getRandomValues(bytes);
|
|
15
16
|
return bytes;
|
|
16
17
|
}
|
|
17
|
-
/** Stops a pending timer from keeping the process alive, where the runtime has that notion. Node's setTimeout returns an object with unref(); a browser's or a Worker's returns a number, which has nothing to unref and must not be called as if it did. */
|
|
18
|
-
function unrefIfSupported(handle) {
|
|
19
|
-
if (typeof handle === "object" && handle !== null && "unref" in handle && typeof handle.unref === "function") Reflect.apply(handle.unref, handle, []);
|
|
20
|
-
}
|
|
21
18
|
/**
|
|
22
19
|
* Builds a capability-request command per management.cddl. The outer `manage-command.verb` is the capability string itself -- the same value as this map's own `params.capability` field -- never a generic literal: capability-verb's own CDDL grammar is a closed alternation of colon-delimited `domain:noun` regexes (spec/tokens.cddl), so a bare literal like "capability" could never satisfy it, and routing by capability (the same way room.send/room.join already share the ROOM_MEMBER_CAPABILITY outer verb) is how a receiver's own per-capability handler knows which grant this ask is even for. `params.verb` is the fixed "capability.request" marker that distinguishes this ask from any other verb sharing the same outer capability (mirroring room.send/room.join's own inner `params.verb` split under one shared outer verb). `valid-until`, when given, bounds how long this ask is worth granting (wire-mesh#82) -- a receiver refuses outright once its own clock has passed it, rather than presenting a stale ask to a human for approval.
|
|
23
20
|
*/
|
|
@@ -131,7 +128,7 @@ function createCapabilityRequestHandler(options) {
|
|
|
131
128
|
message: "no decision within the capability-request timeout"
|
|
132
129
|
}).catch(() => void 0);
|
|
133
130
|
}, options.timeoutMs);
|
|
134
|
-
unrefIfSupported(timeoutHandle);
|
|
131
|
+
require_timers.unrefIfSupported(timeoutHandle);
|
|
135
132
|
options.onRequest({
|
|
136
133
|
scope: incoming.scope,
|
|
137
134
|
requesterDevice: options.bearerDevice,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken, u as CapabilityGrantOk } from "../protocol-Cb3BnuTL.cjs";
|
|
2
2
|
import { r as IdentityPort } from "../identity-CQof0iEM.cjs";
|
|
3
3
|
import { t as Clock } from "../clock-DhUPRfLh.cjs";
|
|
4
|
-
import { IncomingManageRequest, MeshSession } from "
|
|
4
|
+
import { d as IncomingManageRequest, p as MeshSession } from "../mesh-session-By06Mnu-.cjs";
|
|
5
5
|
import { m as TokenDelegateHandler, r as ConditionsContext } from "../token-predicates-ooQCcAZm.cjs";
|
|
6
6
|
import { RevocationCheck } from "./tokens.cjs";
|
|
7
7
|
//#region src/domain/capability-request.d.ts
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { D as DeviceId, V as ManageCommand, f as CapabilityScope, p as CapabilityToken, u as CapabilityGrantOk } from "../protocol-Cb3BnuTL.mjs";
|
|
2
2
|
import { r as IdentityPort } from "../identity-CUr-jaAB.mjs";
|
|
3
3
|
import { t as Clock } from "../clock-DhUPRfLh.mjs";
|
|
4
|
-
import { IncomingManageRequest, MeshSession } from "
|
|
4
|
+
import { d as IncomingManageRequest, p as MeshSession } from "../mesh-session-DzcZYdhe.mjs";
|
|
5
5
|
import { m as TokenDelegateHandler, r as ConditionsContext } from "../token-predicates-CS4DprDE.mjs";
|
|
6
6
|
import { RevocationCheck } from "./tokens.mjs";
|
|
7
7
|
//#region src/domain/capability-request.d.ts
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { capabilityGrantOkSchema, capabilityRequestSchema } from "../generated/protocol.mjs";
|
|
2
2
|
import { scopeNarrows } from "./token-scope.mjs";
|
|
3
3
|
import { mintCapabilityToken, verifyCapabilityToken } from "./tokens.mjs";
|
|
4
|
+
import { t as unrefIfSupported } from "../timers-Q1_kDTyY.mjs";
|
|
4
5
|
//#region src/domain/capability-request.ts
|
|
5
6
|
/**
|
|
6
7
|
* The generic, capability-agnostic capability-request / capability-grant-ok primitive (wire-mesh#78, spec/management.cddl). Lifts core/room's own room.join shape -- an ungated ask, held open, a human (or any other domain-supplied decision) accepts or refuses, acceptance mints and returns a token on the same response -- out of that one domain so any capability can reuse it, not just room:member. core/room's own room-client.ts is the reference consumer: it rewires requestToJoin/createRoomRouter's room.join handling onto requestCapability/createCapabilityRequestHandler below, supplying room-specific extension fields (its member list) through this module's own extensions mechanism rather than this module knowing anything about rooms.
|
|
@@ -13,10 +14,6 @@ function randomTokenId() {
|
|
|
13
14
|
crypto.getRandomValues(bytes);
|
|
14
15
|
return bytes;
|
|
15
16
|
}
|
|
16
|
-
/** Stops a pending timer from keeping the process alive, where the runtime has that notion. Node's setTimeout returns an object with unref(); a browser's or a Worker's returns a number, which has nothing to unref and must not be called as if it did. */
|
|
17
|
-
function unrefIfSupported(handle) {
|
|
18
|
-
if (typeof handle === "object" && handle !== null && "unref" in handle && typeof handle.unref === "function") Reflect.apply(handle.unref, handle, []);
|
|
19
|
-
}
|
|
20
17
|
/**
|
|
21
18
|
* Builds a capability-request command per management.cddl. The outer `manage-command.verb` is the capability string itself -- the same value as this map's own `params.capability` field -- never a generic literal: capability-verb's own CDDL grammar is a closed alternation of colon-delimited `domain:noun` regexes (spec/tokens.cddl), so a bare literal like "capability" could never satisfy it, and routing by capability (the same way room.send/room.join already share the ROOM_MEMBER_CAPABILITY outer verb) is how a receiver's own per-capability handler knows which grant this ask is even for. `params.verb` is the fixed "capability.request" marker that distinguishes this ask from any other verb sharing the same outer capability (mirroring room.send/room.join's own inner `params.verb` split under one shared outer verb). `valid-until`, when given, bounds how long this ask is worth granting (wire-mesh#82) -- a receiver refuses outright once its own clock has passed it, rather than presenting a stale ask to a human for approval.
|
|
22
19
|
*/
|