wire-mesh-core 4.1.5 → 4.2.0
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 +1 -0
- 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 +4 -0
- package/dist/domain/bulk.d.mts +4 -0
- package/dist/domain/bulk.mjs +50 -11
- package/dist/domain/capability-request.cjs +2 -5
- 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.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.mjs +1 -1
- package/dist/domain/mesh-session.cjs +9 -13
- package/dist/domain/mesh-session.mjs +9 -13
- package/dist/domain/ping-round-trips.cjs +21 -8
- package/dist/domain/ping-round-trips.mjs +21 -8
- package/dist/timers-DTJNuBa8.cjs +12 -0
- package/dist/timers-Q1_kDTyY.mjs +7 -0
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ The TypeScript implementation of wire-mesh's protocol, built ports/adapters: dom
|
|
|
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
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.
|
|
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
|
@@ -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
|
@@ -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 };
|
|
@@ -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,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
|
*/
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_domain_device_id = require("./device-id.cjs");
|
|
3
|
+
const require_domain_fault_report = require("./fault-report.cjs");
|
|
4
|
+
//#region src/domain/consume-incoming.ts
|
|
5
|
+
/**
|
|
6
|
+
* The one way to consume a session's incoming streams (incomingManageRequests, incomingDataFrames, revocationAnnouncements, coordinatorFrames, events, or any other async iterable of items): a failure while handling one item is contained to that item, reported once, and the loop goes on to the next, and no rejection escapes to whoever started the loop. A consumer written as a bare `for await` loop ends for good on the first item whose handling throws, and when it was started with `void` the rejection is unhandled, which ends a Node process by default.
|
|
7
|
+
*
|
|
8
|
+
* A failure of the stream itself (the iterator throws, as it does when the session behind it has gone) ends the loop and is reported once with no item. A stream that simply finishes ends the loop without a report.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Handles each item of `source` in order with `handle`. Resolves when the stream ends, fails or is stopped, and never rejects.
|
|
12
|
+
*
|
|
13
|
+
* Stopping with `signal` closes the stream's iterator at once and ends the loop at the next item boundary. An item the loop has already taken is handled to completion, never discarded, so a stream whose `return()` withdraws a pending wait (the session's own streams do) loses nothing to a later reader. A source whose `return()` cannot settle a pending `next()` (an async generator) ends the loop when that `next()` settles. A failure of the stream itself, or of closing it, is reported once and at once: it is not coalesced with item failures, because it ends the loop and no later report could carry it.
|
|
14
|
+
*/
|
|
15
|
+
async function consumeEach(source, handle, options = {}) {
|
|
16
|
+
const { onFault, signal } = options;
|
|
17
|
+
const reporter = require_domain_fault_report.createFaultReporter(options.now);
|
|
18
|
+
const reportItem = (error, item) => {
|
|
19
|
+
if (onFault === void 0) return;
|
|
20
|
+
try {
|
|
21
|
+
const origin = options.sourceOf?.(item) ?? "";
|
|
22
|
+
reporter(origin, async (suppressed) => onFault(error, item, suppressed));
|
|
23
|
+
} catch (failure) {
|
|
24
|
+
console.error(`could not report a failure: ${require_domain_fault_report.describeFault(failure)}`);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const reportStream = (error) => {
|
|
28
|
+
if (onFault !== void 0) require_domain_fault_report.deliverContained(async (suppressed) => onFault(error, void 0, suppressed), 0);
|
|
29
|
+
};
|
|
30
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
31
|
+
let closing;
|
|
32
|
+
const close = () => {
|
|
33
|
+
closing ??= iterator.return?.().catch(reportStream);
|
|
34
|
+
};
|
|
35
|
+
signal?.addEventListener("abort", close, { once: true });
|
|
36
|
+
/** Takes and handles one item. Resolves true when there may be another, false when the stream ended or was stopped; rejects when the stream itself failed. */
|
|
37
|
+
async function step() {
|
|
38
|
+
if (signal?.aborted === true) return false;
|
|
39
|
+
const result = await iterator.next();
|
|
40
|
+
if (result.done === true) return false;
|
|
41
|
+
try {
|
|
42
|
+
await handle(result.value);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
reportItem(error, result.value);
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
await new Promise((resolve) => {
|
|
49
|
+
const pump = () => {
|
|
50
|
+
step().then((more) => {
|
|
51
|
+
if (more) pump();
|
|
52
|
+
else resolve();
|
|
53
|
+
}, (error) => {
|
|
54
|
+
reportStream(error);
|
|
55
|
+
resolve();
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
pump();
|
|
59
|
+
});
|
|
60
|
+
signal?.removeEventListener("abort", close);
|
|
61
|
+
if (signal?.aborted === true) close();
|
|
62
|
+
await closing;
|
|
63
|
+
}
|
|
64
|
+
/** The refusal sent for a request whose handling failed before it was answered. It is a fixed code with no text from the failure, so a peer learns nothing of this side's internals; the code is this repository's convention for it, beside the codes a handler itself answers with. */
|
|
65
|
+
const HANDLER_FAILED_OUTCOME = {
|
|
66
|
+
result: "error",
|
|
67
|
+
code: "internal-error"
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* consumeEach for incoming manage-requests. A request whose handling fails before it was answered is answered with `internal-error`, so its sender is not left waiting for a response that will never come; if that answer cannot be sent either (the connection has gone), the request is dropped, and either way the failure is reported once. A request the handler already answered is not answered again.
|
|
71
|
+
*/
|
|
72
|
+
async function serveManageRequests(source, handle, options = {}) {
|
|
73
|
+
await consumeEach(source, async (incoming) => {
|
|
74
|
+
const state = { answered: false };
|
|
75
|
+
const tracked = {
|
|
76
|
+
...incoming,
|
|
77
|
+
respond: async (outcome) => {
|
|
78
|
+
state.answered = true;
|
|
79
|
+
await incoming.respond(outcome);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
try {
|
|
83
|
+
await handle(tracked);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (!state.answered) await incoming.respond(HANDLER_FAILED_OUTCOME).catch(() => void 0);
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}, {
|
|
89
|
+
...options,
|
|
90
|
+
sourceOf: options.sourceOf ?? ((incoming) => incoming.fromDevice === void 0 ? "" : require_domain_device_id.deviceIdToHex(incoming.fromDevice))
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
exports.HANDLER_FAILED_OUTCOME = HANDLER_FAILED_OUTCOME;
|
|
95
|
+
exports.consumeEach = consumeEach;
|
|
96
|
+
exports.serveManageRequests = serveManageRequests;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { IncomingManageRequest, ManageOutcome } from "./mesh-session.cjs";
|
|
2
|
+
//#region src/domain/consume-incoming.d.ts
|
|
3
|
+
/** What a consumer is told about a failure. `item` is the item whose handling failed, or undefined when the stream itself failed. `suppressed` is how many failures from the same source were not reported before this report, not counting the one it carries (see FAULT_REPORT_INTERVAL_MS); it also counts failures of sources the reporter has stopped tracking. */
|
|
4
|
+
export type ConsumeFault<T> = (error: unknown, item: T | undefined, suppressed: number) => void | Promise<void>;
|
|
5
|
+
export interface ConsumeOptions<T> {
|
|
6
|
+
/** Told of a failure, at most once per source per FAULT_REPORT_INTERVAL_MS, so a peer cannot drive one report per failing item. A throw from this callback, or a promise it returns that rejects, is reported once to the console and dropped: reporting a fault never ends the loop either. Omit to drop failures silently. */
|
|
7
|
+
onFault?: ConsumeFault<T> | undefined;
|
|
8
|
+
/** Names the source an item's failure is counted against, so that one peer's failures do not hide another's. Items share one source when omitted. */
|
|
9
|
+
sourceOf?: ((item: T) => string) | undefined;
|
|
10
|
+
/** Stops the loop: the stream's iterator is closed when the signal aborts, and the loop ends at the next item boundary. See consumeEach for what that does to an item already taken. */
|
|
11
|
+
signal?: AbortSignal | undefined;
|
|
12
|
+
/** The clock the report interval is read from. */
|
|
13
|
+
now?: (() => number) | undefined;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Handles each item of `source` in order with `handle`. Resolves when the stream ends, fails or is stopped, and never rejects.
|
|
17
|
+
*
|
|
18
|
+
* Stopping with `signal` closes the stream's iterator at once and ends the loop at the next item boundary. An item the loop has already taken is handled to completion, never discarded, so a stream whose `return()` withdraws a pending wait (the session's own streams do) loses nothing to a later reader. A source whose `return()` cannot settle a pending `next()` (an async generator) ends the loop when that `next()` settles. A failure of the stream itself, or of closing it, is reported once and at once: it is not coalesced with item failures, because it ends the loop and no later report could carry it.
|
|
19
|
+
*/
|
|
20
|
+
export declare function consumeEach<T>(source: Readonly<AsyncIterable<T>>, handle: (item: T) => void | Promise<void>, options?: Readonly<ConsumeOptions<T>>): Promise<void>;
|
|
21
|
+
/** The refusal sent for a request whose handling failed before it was answered. It is a fixed code with no text from the failure, so a peer learns nothing of this side's internals; the code is this repository's convention for it, beside the codes a handler itself answers with. */
|
|
22
|
+
export declare const HANDLER_FAILED_OUTCOME: ManageOutcome;
|
|
23
|
+
/**
|
|
24
|
+
* consumeEach for incoming manage-requests. A request whose handling fails before it was answered is answered with `internal-error`, so its sender is not left waiting for a response that will never come; if that answer cannot be sent either (the connection has gone), the request is dropped, and either way the failure is reported once. A request the handler already answered is not answered again.
|
|
25
|
+
*/
|
|
26
|
+
export declare function serveManageRequests(source: Readonly<AsyncIterable<IncomingManageRequest>>, handle: (incoming: IncomingManageRequest) => Promise<void>, options?: Readonly<ConsumeOptions<IncomingManageRequest>>): Promise<void>;
|
|
27
|
+
//#endregion
|