wire-mesh-core 1.24.0 → 1.25.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/dist/adapters/frame-codec.d.cts +1 -1
- package/dist/adapters/frame-codec.d.mts +1 -1
- package/dist/adapters/node-identity.d.cts +2 -2
- package/dist/adapters/node-identity.d.mts +2 -2
- package/dist/adapters/tcp-transport.d.cts +1 -1
- package/dist/adapters/tcp-transport.d.mts +1 -1
- package/dist/adapters/tls-transport.d.cts +1 -1
- package/dist/adapters/tls-transport.d.mts +1 -1
- package/dist/domain/bulk.cjs +400 -0
- package/dist/domain/bulk.d.cts +97 -0
- package/dist/domain/bulk.d.mts +97 -0
- package/dist/domain/bulk.mjs +393 -0
- package/dist/domain/capability-grant.d.cts +3 -3
- package/dist/domain/capability-grant.d.mts +3 -3
- package/dist/domain/capability-request.d.cts +2 -2
- package/dist/domain/capability-request.d.mts +2 -2
- package/dist/domain/device-id.d.cts +1 -1
- package/dist/domain/device-id.d.mts +1 -1
- package/dist/domain/grant-candidates.d.cts +1 -1
- package/dist/domain/grant-candidates.d.mts +1 -1
- package/dist/domain/handshake.d.cts +1 -1
- package/dist/domain/handshake.d.mts +1 -1
- package/dist/domain/mesh-session.d.cts +3 -3
- package/dist/domain/mesh-session.d.mts +3 -3
- package/dist/domain/relay-hub.d.cts +2 -2
- package/dist/domain/relay-hub.d.mts +2 -2
- package/dist/domain/relay-use-gate.d.cts +2 -2
- package/dist/domain/relay-use-gate.d.mts +2 -2
- package/dist/domain/revocation-view.cjs +1 -1
- package/dist/domain/revocation-view.d.cts +2 -2
- package/dist/domain/revocation-view.d.mts +2 -2
- package/dist/domain/revocation-view.mjs +1 -1
- package/dist/domain/room-token-verification.cjs +1 -1
- package/dist/domain/room-token-verification.d.cts +2 -2
- package/dist/domain/room-token-verification.d.mts +2 -2
- package/dist/domain/room-token-verification.mjs +1 -1
- package/dist/domain/room.cjs +108 -0
- package/dist/domain/room.d.cts +36 -0
- package/dist/domain/room.d.mts +36 -0
- package/dist/domain/room.mjs +106 -0
- package/dist/domain/tokens.d.cts +1 -1
- package/dist/domain/tokens.d.mts +1 -1
- package/dist/generated/protocol.cjs +78 -28
- package/dist/generated/protocol.d.cts +2 -2
- package/dist/generated/protocol.d.mts +2 -2
- package/dist/generated/protocol.mjs +72 -29
- package/dist/{identity-5LJ_B8go.d.mts → identity-C5N86BPz.d.mts} +1 -1
- package/dist/{identity-bg6IVGsH.d.cts → identity-DJ06jn4k.d.cts} +1 -1
- package/dist/ports/identity.d.cts +1 -1
- package/dist/ports/identity.d.mts +1 -1
- package/dist/ports/transport.d.cts +1 -1
- package/dist/ports/transport.d.mts +1 -1
- package/dist/{protocol-l9RMR3s5.d.cts → protocol-sW8d6Og-.d.cts} +177 -43
- package/dist/{protocol-l9RMR3s5.d.mts → protocol-sW8d6Og-.d.mts} +177 -43
- package/dist/{tokens-CBI54oF-.d.mts → tokens-CpLDI5Fo.d.mts} +2 -2
- package/dist/{tokens-H_mV_JCC.d.cts → tokens-DfVfE00W.d.cts} +2 -2
- package/dist/{transport-Baipikk7.d.cts → transport-B5E896PN.d.mts} +1 -1
- package/dist/{transport-DWbvUc3L.d.mts → transport-DxZ4JWZc.d.cts} +1 -1
- package/package.json +9 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { N as Frame } from "../protocol-sW8d6Og-.cjs";
|
|
2
2
|
//#region src/adapters/frame-codec.d.ts
|
|
3
3
|
export declare function messageFromFrame(frame: Frame): Uint8Array<ArrayBuffer>;
|
|
4
4
|
/** A frame that fails schema validation, caught separately from a decode failure so it can be dropped without disconnecting. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { N as Frame } from "../protocol-sW8d6Og-.mjs";
|
|
2
2
|
//#region src/adapters/frame-codec.d.ts
|
|
3
3
|
export declare function messageFromFrame(frame: Frame): Uint8Array<ArrayBuffer>;
|
|
4
4
|
/** A frame that fails schema validation, caught separately from a decode failure so it can be dropped without disconnecting. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as IdentityPort } from "../identity-
|
|
1
|
+
import { B as IdentityKey, D as DeviceId } from "../protocol-sW8d6Og-.cjs";
|
|
2
|
+
import { t as IdentityPort } from "../identity-DJ06jn4k.cjs";
|
|
3
3
|
import { webcrypto } from "node:crypto";
|
|
4
4
|
//#region src/adapters/node-identity.d.ts
|
|
5
5
|
export declare function deriveDeviceId(publicKey: Uint8Array): Promise<DeviceId>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as IdentityPort } from "../identity-
|
|
1
|
+
import { B as IdentityKey, D as DeviceId } from "../protocol-sW8d6Og-.mjs";
|
|
2
|
+
import { t as IdentityPort } from "../identity-C5N86BPz.mjs";
|
|
3
3
|
import { webcrypto } from "node:crypto";
|
|
4
4
|
//#region src/adapters/node-identity.d.ts
|
|
5
5
|
export declare function deriveDeviceId(publicKey: Uint8Array): Promise<DeviceId>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as Transport } from "../transport-
|
|
1
|
+
import { r as Transport } from "../transport-DxZ4JWZc.cjs";
|
|
2
2
|
//#region src/adapters/tcp-transport.d.ts
|
|
3
3
|
/** A Node net.Socket-based Transport: length-prefixed, CBOR-encoded frames over plain TCP -- matching Cascade's own transport shape, since interop with Cascade nodes is wire-mesh's stated goal. Framing (not TLS) is this adapter's own concern; a TLS-terminated variant is a separate adapter behind the same Transport contract. */
|
|
4
4
|
export declare function createTcpTransport(): Transport;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as Transport } from "../transport-
|
|
1
|
+
import { r as Transport } from "../transport-B5E896PN.mjs";
|
|
2
2
|
//#region src/adapters/tcp-transport.d.ts
|
|
3
3
|
/** A Node net.Socket-based Transport: length-prefixed, CBOR-encoded frames over plain TCP -- matching Cascade's own transport shape, since interop with Cascade nodes is wire-mesh's stated goal. Framing (not TLS) is this adapter's own concern; a TLS-terminated variant is a separate adapter behind the same Transport contract. */
|
|
4
4
|
export declare function createTcpTransport(): Transport;
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_generated_protocol = require("../generated/protocol.cjs");
|
|
3
|
+
const require_domain_device_id = require("./device-id.cjs");
|
|
4
|
+
//#region src/domain/bulk.ts
|
|
5
|
+
/**
|
|
6
|
+
* core/bulk (wire-mesh#34/#123): resumable, flow-controlled bulk transfer. `bulk.open`/`bulk.resume`/`bulk.cancel` ride manage-request-frame as ordinary manage-command params; `bulk-data`/`bulk-ack`/`bulk-end` are their own frame kind, riding the connection directly rather than a manage-response -- see spec/bulk.cddl's own header comment for the full design, including the three previously-open questions this module resolves in code (chunk-size negotiation is not a wire concept at all; transfer-id/issuer pairing is a verifier obligation, not a wire field; token expiry is checked once per manage-command, never per data frame).
|
|
7
|
+
*
|
|
8
|
+
* Two long-lived, connection-independent coordinators, not a single monolithic object: createBulkReceiver's own state (which transfer-id maps to which connection, and every durably-persisted chunk) must outlive any one connection for resumption to mean anything, so it is constructed once and its onFrame/createOpenHandler/createResumeHandler are wired into every connection a caller accepts, not recreated per connection. createBulkSender is scoped to what it can honestly promise: MeshSession itself has no mechanism that hands a caller a fresh Connection across a reconnect (acceptMeshSession's own doc comment says so explicitly), so open()/continueFrom() each operate over one connection and settle once that connection ends: the caller -- who already owns whatever reconnection policy it uses -- decides when to call continueFrom() again on a fresh connection, typically from its own createResumeHandler once the receiver's bulk.resume names a transfer this sender still remembers.
|
|
9
|
+
*/
|
|
10
|
+
const TRANSFER_ID_BYTE_LENGTH = 16;
|
|
11
|
+
/** 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. */
|
|
12
|
+
const BULK_WRITE_CAPABILITY = "bulk:write";
|
|
13
|
+
const BULK_READ_CAPABILITY = "bulk:read";
|
|
14
|
+
function randomTransferId() {
|
|
15
|
+
const bytes = new Uint8Array(TRANSFER_ID_BYTE_LENGTH);
|
|
16
|
+
crypto.getRandomValues(bytes);
|
|
17
|
+
return bytes;
|
|
18
|
+
}
|
|
19
|
+
function transferIdHex(transferId) {
|
|
20
|
+
return require_domain_device_id.bytesToHex(transferId);
|
|
21
|
+
}
|
|
22
|
+
async function digestOf(bytes) {
|
|
23
|
+
const buffer = await crypto.subtle.digest("SHA-256", new Uint8Array(bytes).buffer);
|
|
24
|
+
return new Uint8Array(buffer);
|
|
25
|
+
}
|
|
26
|
+
function bytesEqual(a, b) {
|
|
27
|
+
if (a.length !== b.length) return false;
|
|
28
|
+
for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false;
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
function concatChunks(chunks) {
|
|
32
|
+
const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
33
|
+
const out = new Uint8Array(total);
|
|
34
|
+
let offset = 0;
|
|
35
|
+
for (const chunk of chunks) {
|
|
36
|
+
out.set(chunk, offset);
|
|
37
|
+
offset += chunk.length;
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
function buildBulkOpenCommand(transferId, options = {}) {
|
|
42
|
+
return {
|
|
43
|
+
verb: BULK_WRITE_CAPABILITY,
|
|
44
|
+
params: {
|
|
45
|
+
verb: "bulk.open",
|
|
46
|
+
"transfer-id": transferId,
|
|
47
|
+
...options.totalSize !== void 0 ? { "total-size": options.totalSize } : {},
|
|
48
|
+
...options.contentType !== void 0 ? { "content-type": options.contentType } : {}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function buildBulkResumeCommand(transferId, ackSeq) {
|
|
53
|
+
return {
|
|
54
|
+
verb: BULK_READ_CAPABILITY,
|
|
55
|
+
params: {
|
|
56
|
+
verb: "bulk.resume",
|
|
57
|
+
"transfer-id": transferId,
|
|
58
|
+
"ack-seq": ackSeq
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function buildBulkCancelCommand(transferId, capability, reason) {
|
|
63
|
+
return {
|
|
64
|
+
verb: capability,
|
|
65
|
+
params: {
|
|
66
|
+
verb: "bulk.cancel",
|
|
67
|
+
"transfer-id": transferId,
|
|
68
|
+
...reason !== void 0 ? { reason } : {}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 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
|
+
*/
|
|
75
|
+
function createBulkSender(options) {
|
|
76
|
+
const pendingAcks = /* @__PURE__ */ new Map();
|
|
77
|
+
const cancelled = /* @__PURE__ */ new Set();
|
|
78
|
+
function resolveNextAck(id, frame) {
|
|
79
|
+
const queue = pendingAcks.get(id);
|
|
80
|
+
if (queue === void 0 || queue.length === 0) return;
|
|
81
|
+
queue.shift()?.resolve(frame);
|
|
82
|
+
}
|
|
83
|
+
function rejectAllAcks(id, error) {
|
|
84
|
+
const queue = pendingAcks.get(id);
|
|
85
|
+
if (queue === void 0) return;
|
|
86
|
+
pendingAcks.set(id, []);
|
|
87
|
+
for (const waiter of queue) waiter.reject(error);
|
|
88
|
+
}
|
|
89
|
+
async function waitForAck(id) {
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
const queue = pendingAcks.get(id) ?? [];
|
|
92
|
+
queue.push({
|
|
93
|
+
resolve,
|
|
94
|
+
reject
|
|
95
|
+
});
|
|
96
|
+
pendingAcks.set(id, queue);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
async function stream(connection, transferId, fromSeq, initialWindow) {
|
|
100
|
+
const id = transferIdHex(transferId);
|
|
101
|
+
let seq = fromSeq;
|
|
102
|
+
let ackedCount = fromSeq;
|
|
103
|
+
let window = initialWindow;
|
|
104
|
+
let bytesInFlight = 0;
|
|
105
|
+
const inFlight = [];
|
|
106
|
+
try {
|
|
107
|
+
for await (const chunk of options.source(fromSeq)) {
|
|
108
|
+
if (cancelled.has(id)) return {
|
|
109
|
+
status: "incomplete",
|
|
110
|
+
ackedCount
|
|
111
|
+
};
|
|
112
|
+
while (bytesInFlight + chunk.length > window) {
|
|
113
|
+
const ack = await waitForAck(id);
|
|
114
|
+
ackedCount = ack["ack-seq"];
|
|
115
|
+
window = ack.window;
|
|
116
|
+
for (;;) {
|
|
117
|
+
const head = inFlight[0];
|
|
118
|
+
if (head === void 0 || head.seq >= ackedCount) break;
|
|
119
|
+
inFlight.shift();
|
|
120
|
+
bytesInFlight -= head.length;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const dataFrame = {
|
|
124
|
+
type: "bulk-data",
|
|
125
|
+
"transfer-id": transferId,
|
|
126
|
+
seq,
|
|
127
|
+
bytes: chunk
|
|
128
|
+
};
|
|
129
|
+
await connection.send(dataFrame);
|
|
130
|
+
inFlight.push({
|
|
131
|
+
seq,
|
|
132
|
+
length: chunk.length
|
|
133
|
+
});
|
|
134
|
+
bytesInFlight += chunk.length;
|
|
135
|
+
seq += 1;
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
return {
|
|
139
|
+
status: "incomplete",
|
|
140
|
+
ackedCount
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const chunks = [];
|
|
144
|
+
for await (const chunk of options.source(0)) chunks.push(chunk);
|
|
145
|
+
const endFrame = {
|
|
146
|
+
type: "bulk-end",
|
|
147
|
+
"transfer-id": transferId,
|
|
148
|
+
digest: await digestOf(concatChunks(chunks))
|
|
149
|
+
};
|
|
150
|
+
await connection.send(endFrame);
|
|
151
|
+
return {
|
|
152
|
+
status: "complete",
|
|
153
|
+
ackedCount: seq
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** 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. */
|
|
157
|
+
async function resumeStream(connection, transferId) {
|
|
158
|
+
const ack = await waitForAck(transferIdHex(transferId));
|
|
159
|
+
return stream(connection, transferId, ack["ack-seq"], ack.window);
|
|
160
|
+
}
|
|
161
|
+
let activeTransferId;
|
|
162
|
+
let activeConnection;
|
|
163
|
+
return {
|
|
164
|
+
onFrame(_connection, frame) {
|
|
165
|
+
if (frame.type !== "bulk-ack") return;
|
|
166
|
+
resolveNextAck(transferIdHex(frame["transfer-id"]), frame);
|
|
167
|
+
},
|
|
168
|
+
onConnectionEnd(connection) {
|
|
169
|
+
if (activeTransferId === void 0 || activeConnection !== connection) return;
|
|
170
|
+
rejectAllAcks(activeTransferId, /* @__PURE__ */ new Error("connection ended"));
|
|
171
|
+
},
|
|
172
|
+
createCancelHandler() {
|
|
173
|
+
return async function handleBulkCancel(incoming) {
|
|
174
|
+
const parsed = require_generated_protocol.bulkCancelSchema.safeParse(incoming.command.params);
|
|
175
|
+
if (!parsed.success) {
|
|
176
|
+
await incoming.respond({
|
|
177
|
+
result: "error",
|
|
178
|
+
code: "malformed"
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
cancelled.add(transferIdHex(parsed.data["transfer-id"]));
|
|
183
|
+
await incoming.respond({ result: "ok" });
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
async open(connection, session, scope, openOptions = {}) {
|
|
187
|
+
const transferId = openOptions.transferId ?? randomTransferId();
|
|
188
|
+
activeTransferId = transferIdHex(transferId);
|
|
189
|
+
activeConnection = connection;
|
|
190
|
+
const outcome = await session.sendManageRequest(buildBulkOpenCommand(transferId, {
|
|
191
|
+
...openOptions.totalSize !== void 0 ? { totalSize: openOptions.totalSize } : {},
|
|
192
|
+
...openOptions.contentType !== void 0 ? { contentType: openOptions.contentType } : {}
|
|
193
|
+
}), scope, openOptions.targetDevice, openOptions.token, openOptions.timeoutMs);
|
|
194
|
+
if (outcome.result !== "ok") return {
|
|
195
|
+
status: "rejected",
|
|
196
|
+
ackedCount: 0,
|
|
197
|
+
code: outcome.code
|
|
198
|
+
};
|
|
199
|
+
return resumeStream(connection, transferId);
|
|
200
|
+
},
|
|
201
|
+
async continueFrom(connection, transferId) {
|
|
202
|
+
activeTransferId = transferIdHex(transferId);
|
|
203
|
+
activeConnection = connection;
|
|
204
|
+
return resumeStream(connection, transferId);
|
|
205
|
+
},
|
|
206
|
+
createResumeHandler(connection, onResumed) {
|
|
207
|
+
return async function handleBulkResume(incoming) {
|
|
208
|
+
const parsed = require_generated_protocol.bulkResumeSchema.safeParse(incoming.command.params);
|
|
209
|
+
if (!parsed.success) {
|
|
210
|
+
await incoming.respond({
|
|
211
|
+
result: "error",
|
|
212
|
+
code: "malformed"
|
|
213
|
+
});
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const { "transfer-id": transferId } = parsed.data;
|
|
217
|
+
if (transferIdHex(transferId) !== activeTransferId) {
|
|
218
|
+
await incoming.respond({
|
|
219
|
+
result: "error",
|
|
220
|
+
code: "unknown_transfer"
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
await incoming.respond({ result: "ok" });
|
|
225
|
+
activeConnection = connection;
|
|
226
|
+
resumeStream(connection, transferId).then((outcome) => onResumed?.(outcome)).catch(() => void 0);
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function openerKey(idHex) {
|
|
232
|
+
return `bulk/${idHex}/opener`;
|
|
233
|
+
}
|
|
234
|
+
function ackSeqKey(idHex) {
|
|
235
|
+
return `bulk/${idHex}/ack-seq`;
|
|
236
|
+
}
|
|
237
|
+
function windowKey(idHex) {
|
|
238
|
+
return `bulk/${idHex}/window`;
|
|
239
|
+
}
|
|
240
|
+
function chunkKey(idHex, seq) {
|
|
241
|
+
return `bulk/${idHex}/chunk/${String(seq)}`;
|
|
242
|
+
}
|
|
243
|
+
function encodeUint(n) {
|
|
244
|
+
return new TextEncoder().encode(String(n));
|
|
245
|
+
}
|
|
246
|
+
function decodeUint(bytes) {
|
|
247
|
+
return Number(new TextDecoder().decode(bytes));
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Builds a receiver coordinator over one KeyValueStorage. Constructed once and shared across every connection this side accepts -- durable per-transfer state (which chunks are persisted, who opened it) and the in-memory registry of which connection currently owns each transfer must both outlive any one connection for resumption to mean anything at all.
|
|
251
|
+
*/
|
|
252
|
+
function createBulkReceiver(options) {
|
|
253
|
+
const { storage } = options;
|
|
254
|
+
const registrations = /* @__PURE__ */ new Map();
|
|
255
|
+
async function readAckSeq(idHex) {
|
|
256
|
+
const bytes = await storage.get(ackSeqKey(idHex));
|
|
257
|
+
return bytes === void 0 ? 0 : decodeUint(bytes);
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
async onFrame(connection, frame) {
|
|
261
|
+
if (frame.type !== "bulk-data" && frame.type !== "bulk-end") return;
|
|
262
|
+
const idHex = transferIdHex(frame["transfer-id"]);
|
|
263
|
+
const reg = registrations.get(idHex);
|
|
264
|
+
if (reg?.connection !== connection) return;
|
|
265
|
+
if (frame.type === "bulk-data") {
|
|
266
|
+
const ackedCount = await readAckSeq(idHex);
|
|
267
|
+
if (frame.seq !== ackedCount) return;
|
|
268
|
+
await storage.set(chunkKey(idHex, frame.seq), frame.bytes);
|
|
269
|
+
const newAckedCount = ackedCount + 1;
|
|
270
|
+
await storage.set(ackSeqKey(idHex), encodeUint(newAckedCount));
|
|
271
|
+
const windowBytes = await storage.get(windowKey(idHex));
|
|
272
|
+
const window = windowBytes === void 0 ? 0 : decodeUint(windowBytes);
|
|
273
|
+
const ackFrame = {
|
|
274
|
+
type: "bulk-ack",
|
|
275
|
+
"transfer-id": frame["transfer-id"],
|
|
276
|
+
"ack-seq": newAckedCount,
|
|
277
|
+
window
|
|
278
|
+
};
|
|
279
|
+
await connection.send(ackFrame);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const ackedCount = await readAckSeq(idHex);
|
|
283
|
+
const chunks = [];
|
|
284
|
+
for (let seq = 0; seq < ackedCount; seq += 1) {
|
|
285
|
+
const chunk = await storage.get(chunkKey(idHex, seq));
|
|
286
|
+
if (chunk !== void 0) chunks.push(chunk);
|
|
287
|
+
}
|
|
288
|
+
const ok = bytesEqual(await digestOf(concatChunks(chunks)), frame.digest);
|
|
289
|
+
reg.onComplete?.({
|
|
290
|
+
ok,
|
|
291
|
+
chunkCount: ackedCount
|
|
292
|
+
});
|
|
293
|
+
},
|
|
294
|
+
createOpenHandler(openerDevice, connection, onOffer) {
|
|
295
|
+
return async function handleBulkOpen(incoming) {
|
|
296
|
+
const parsed = require_generated_protocol.bulkOpenSchema.safeParse(incoming.command.params);
|
|
297
|
+
if (!parsed.success) {
|
|
298
|
+
await incoming.respond({
|
|
299
|
+
result: "error",
|
|
300
|
+
code: "malformed"
|
|
301
|
+
});
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const { "transfer-id": transferId } = parsed.data;
|
|
305
|
+
const idHex = transferIdHex(transferId);
|
|
306
|
+
if (await storage.get(openerKey(idHex)) !== void 0) {
|
|
307
|
+
await incoming.respond({
|
|
308
|
+
result: "error",
|
|
309
|
+
code: "already_exists"
|
|
310
|
+
});
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
onOffer({
|
|
314
|
+
transferId,
|
|
315
|
+
...parsed.data["total-size"] !== void 0 ? { totalSize: parsed.data["total-size"] } : {},
|
|
316
|
+
...parsed.data["content-type"] !== void 0 ? { contentType: parsed.data["content-type"] } : {},
|
|
317
|
+
openerDevice,
|
|
318
|
+
async decide(decision) {
|
|
319
|
+
if (decision.kind === "reject") {
|
|
320
|
+
await incoming.respond({
|
|
321
|
+
result: "error",
|
|
322
|
+
code: "denied",
|
|
323
|
+
...decision.reason !== void 0 ? { message: decision.reason } : {}
|
|
324
|
+
});
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
await storage.set(openerKey(idHex), openerDevice);
|
|
328
|
+
await storage.set(ackSeqKey(idHex), encodeUint(0));
|
|
329
|
+
await storage.set(windowKey(idHex), encodeUint(decision.window));
|
|
330
|
+
registrations.set(idHex, {
|
|
331
|
+
openerDevice,
|
|
332
|
+
connection,
|
|
333
|
+
...decision.onComplete !== void 0 ? { onComplete: decision.onComplete } : {}
|
|
334
|
+
});
|
|
335
|
+
await incoming.respond({ result: "ok" });
|
|
336
|
+
const ackFrame = {
|
|
337
|
+
type: "bulk-ack",
|
|
338
|
+
"transfer-id": transferId,
|
|
339
|
+
"ack-seq": 0,
|
|
340
|
+
window: decision.window
|
|
341
|
+
};
|
|
342
|
+
await connection.send(ackFrame);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
};
|
|
346
|
+
},
|
|
347
|
+
async requestResume(connection, session, transferId, scope, targetDevice, token, timeoutMs) {
|
|
348
|
+
const idHex = transferIdHex(transferId);
|
|
349
|
+
const existing = registrations.get(idHex);
|
|
350
|
+
const storedOpener = await storage.get(openerKey(idHex));
|
|
351
|
+
const openerDevice = existing?.openerDevice ?? (storedOpener === void 0 ? void 0 : new Uint8Array(storedOpener));
|
|
352
|
+
if (openerDevice === void 0) return {
|
|
353
|
+
status: "rejected",
|
|
354
|
+
code: "unknown_transfer"
|
|
355
|
+
};
|
|
356
|
+
const ackedCount = await readAckSeq(idHex);
|
|
357
|
+
const outcome = await session.sendManageRequest(buildBulkResumeCommand(transferId, ackedCount), scope, targetDevice, token, timeoutMs);
|
|
358
|
+
if (outcome.result !== "ok") return {
|
|
359
|
+
status: "rejected",
|
|
360
|
+
code: outcome.code
|
|
361
|
+
};
|
|
362
|
+
registrations.set(idHex, {
|
|
363
|
+
openerDevice,
|
|
364
|
+
connection,
|
|
365
|
+
...existing?.onComplete !== void 0 ? { onComplete: existing.onComplete } : {}
|
|
366
|
+
});
|
|
367
|
+
const windowBytes = await storage.get(windowKey(idHex));
|
|
368
|
+
const ackFrame = {
|
|
369
|
+
type: "bulk-ack",
|
|
370
|
+
"transfer-id": transferId,
|
|
371
|
+
"ack-seq": ackedCount,
|
|
372
|
+
window: windowBytes === void 0 ? 0 : decodeUint(windowBytes)
|
|
373
|
+
};
|
|
374
|
+
await connection.send(ackFrame);
|
|
375
|
+
return { status: "ok" };
|
|
376
|
+
},
|
|
377
|
+
createCancelHandler() {
|
|
378
|
+
return async function handleBulkCancel(incoming) {
|
|
379
|
+
const parsed = require_generated_protocol.bulkCancelSchema.safeParse(incoming.command.params);
|
|
380
|
+
if (!parsed.success) {
|
|
381
|
+
await incoming.respond({
|
|
382
|
+
result: "error",
|
|
383
|
+
code: "malformed"
|
|
384
|
+
});
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
registrations.delete(transferIdHex(parsed.data["transfer-id"]));
|
|
388
|
+
await incoming.respond({ result: "ok" });
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
//#endregion
|
|
394
|
+
exports.BULK_READ_CAPABILITY = BULK_READ_CAPABILITY;
|
|
395
|
+
exports.BULK_WRITE_CAPABILITY = BULK_WRITE_CAPABILITY;
|
|
396
|
+
exports.buildBulkCancelCommand = buildBulkCancelCommand;
|
|
397
|
+
exports.buildBulkOpenCommand = buildBulkOpenCommand;
|
|
398
|
+
exports.buildBulkResumeCommand = buildBulkResumeCommand;
|
|
399
|
+
exports.createBulkReceiver = createBulkReceiver;
|
|
400
|
+
exports.createBulkSender = createBulkSender;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { D as DeviceId, N as Frame, V as ManageCommand, f as CapabilityScope, p as CapabilityToken } from "../protocol-sW8d6Og-.cjs";
|
|
2
|
+
import { KeyValueStorage } from "../ports/storage.cjs";
|
|
3
|
+
import { t as Connection } from "../transport-DxZ4JWZc.cjs";
|
|
4
|
+
import { IncomingManageRequest, MeshSession } from "./mesh-session.cjs";
|
|
5
|
+
//#region src/domain/bulk.d.ts
|
|
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
|
+
export declare const BULK_WRITE_CAPABILITY = "bulk:write";
|
|
8
|
+
export declare const BULK_READ_CAPABILITY = "bulk:read";
|
|
9
|
+
export declare function buildBulkOpenCommand(transferId: Uint8Array<ArrayBuffer>, options?: Readonly<{
|
|
10
|
+
totalSize?: number;
|
|
11
|
+
contentType?: string;
|
|
12
|
+
}>): ManageCommand;
|
|
13
|
+
export declare function buildBulkResumeCommand(transferId: Uint8Array<ArrayBuffer>, ackSeq: number): ManageCommand;
|
|
14
|
+
export declare function buildBulkCancelCommand(transferId: Uint8Array<ArrayBuffer>, capability: typeof BULK_WRITE_CAPABILITY | typeof BULK_READ_CAPABILITY, reason?: string): ManageCommand;
|
|
15
|
+
export interface BulkSendOutcome {
|
|
16
|
+
status: "complete" | "incomplete" | "rejected";
|
|
17
|
+
/** Count of chunks the receiver had acked as durably persisted when this call settled. */
|
|
18
|
+
ackedCount: number;
|
|
19
|
+
/** Present only when status is "rejected": the manage-error code the receiver responded with. */
|
|
20
|
+
code?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface CreateBulkSenderOptions {
|
|
23
|
+
/**
|
|
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
|
+
*/
|
|
26
|
+
source: (fromSeq: number) => AsyncIterable<Uint8Array<ArrayBuffer>>;
|
|
27
|
+
}
|
|
28
|
+
export interface BulkSenderOpenOptions {
|
|
29
|
+
transferId?: Uint8Array<ArrayBuffer>;
|
|
30
|
+
totalSize?: number;
|
|
31
|
+
contentType?: string;
|
|
32
|
+
targetDevice?: DeviceId;
|
|
33
|
+
token?: CapabilityToken;
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
}
|
|
36
|
+
export interface BulkSender {
|
|
37
|
+
/** Observes bulk-ack frames for transfers this sender currently owns. Wire as (part of) the onFrame passed to acceptMeshSession/createSessionCore, alongside anything else already observing the same connection. */
|
|
38
|
+
onFrame: (connection: Readonly<Connection>, frame: Frame) => void;
|
|
39
|
+
/** Opens a brand-new transfer and streams it from seq 0 until the source is exhausted (returns "complete") or this connection ends (returns "incomplete", i.e. a caller with a fresh connection may retry via continueFrom once the receiver's own bulk.resume arrives). */
|
|
40
|
+
open: (connection: Readonly<Connection>, session: Readonly<MeshSession>, scope: Readonly<CapabilityScope>, options?: Readonly<BulkSenderOpenOptions>) => Promise<BulkSendOutcome>;
|
|
41
|
+
/** Resumes an already-open transfer on a fresh connection, streaming from wherever the receiver's own post-accept bulk-ack says to (its ack-seq, a count of chunks already durably persisted) with no bulk.open sent again -- the receiver already confirmed via a prior bulk.resume that it wants this. */
|
|
42
|
+
continueFrom: (connection: Readonly<Connection>, transferId: Uint8Array<ArrayBuffer>) => Promise<BulkSendOutcome>;
|
|
43
|
+
/** Builds a manage-request handler for incoming bulk.resume asks, bound to one connection (wire as this session's own bulk:read-scoped handler). Refuses a request naming any transfer other than the one this sender currently has open. onResumed, if given, is called with the resulting BulkSendOutcome once the resumed stream settles -- the handler itself does not await it, so a caller not needing to observe it may omit onResumed entirely without leaking an unhandled rejection (errors inside stream() are already caught and folded into an "incomplete" outcome, never thrown). */
|
|
44
|
+
createResumeHandler: (connection: Readonly<Connection>, onResumed?: (outcome: Readonly<BulkSendOutcome>) => void) => (incoming: Readonly<IncomingManageRequest>) => Promise<void>;
|
|
45
|
+
/** Builds a manage-request handler for incoming bulk.cancel asks (wire as this session's own bulk:write-scoped handler). Marks the named transfer cancelled so the next iteration of an in-progress stream() loop stops rather than sending further chunks -- a currently-blocked wait for an ack is not itself interrupted, since nothing here has a way to abort an in-flight Promise, only to stop starting new work once it resolves. */
|
|
46
|
+
createCancelHandler: () => (incoming: Readonly<IncomingManageRequest>) => Promise<void>;
|
|
47
|
+
/** Tells this sender that a connection it was streaming over has ended (its own receive() stream reached done:true) -- without this, an in-progress stream() call blocked waiting for the next ack would hang forever rather than settling as "incomplete", since nothing else here observes connection lifecycle (open()/continueFrom() only ever learn about the connection through frames arriving on it). A caller's own pump/onFrame loop should call this once its receive() iteration ends, mirroring the onSessionEnd hook acceptMeshSession already exposes for the identical reason. */
|
|
48
|
+
onConnectionEnd: (connection: Readonly<Connection>) => void;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 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.
|
|
52
|
+
*/
|
|
53
|
+
export declare function createBulkSender(options: Readonly<CreateBulkSenderOptions>): BulkSender;
|
|
54
|
+
export interface BulkReceiveResult {
|
|
55
|
+
ok: boolean;
|
|
56
|
+
/** Count of chunks durably persisted for this transfer once bulk-end arrived -- meaningful even when ok is false (digest mismatch), since the chunks themselves are still exactly what was received. */
|
|
57
|
+
chunkCount: number;
|
|
58
|
+
}
|
|
59
|
+
export type BulkOfferDecision = {
|
|
60
|
+
kind: "accept";
|
|
61
|
+
/** Initial credit window, in bytes, this receiver is willing to accept past its own ack-seq -- a resource-limit choice for the caller to make (per spec/bulk.cddl's own "receiver resource limits are deployment policy, not protocol" design point), not a value this module invents a default for. */
|
|
62
|
+
window: number;
|
|
63
|
+
/** Called once, when bulk-end arrives and the digest has been checked against every durably-persisted chunk -- the only signal a caller gets that this transfer finished (successfully or corrupt), since createOpenHandler's own handler returns long before that. */
|
|
64
|
+
onComplete?: (result: Readonly<BulkReceiveResult>) => void;
|
|
65
|
+
} | {
|
|
66
|
+
kind: "reject";
|
|
67
|
+
reason?: string;
|
|
68
|
+
};
|
|
69
|
+
export interface BulkOfferEvent {
|
|
70
|
+
transferId: Uint8Array<ArrayBuffer>;
|
|
71
|
+
totalSize?: number;
|
|
72
|
+
contentType?: string;
|
|
73
|
+
/** The peer device-id authenticated on the connection this bulk.open arrived over -- supplied by the caller (createOpenHandler's own parameter), the same "MeshSession exposes no way to learn this independently" reason capability-request.ts's own bearerDevice parameter exists. */
|
|
74
|
+
openerDevice: DeviceId;
|
|
75
|
+
decide: (decision: Readonly<BulkOfferDecision>) => Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
export interface BulkReceiver {
|
|
78
|
+
/** Handles bulk-data/bulk-end frames for transfers this receiver has accepted, durably persisting each contiguous chunk and acking before advertising it (spec/bulk.cddl's own obligation 1). Ignores a frame naming a transfer this receiver hasn't accepted, or arriving on a connection other than the one currently registered for it (an old, since-superseded connection racing a fresh bulk.resume). Wire as (part of) the onFrame passed to acceptMeshSession, alongside anything else observing the same connection. */
|
|
79
|
+
onFrame: (connection: Readonly<Connection>, frame: Frame) => Promise<void>;
|
|
80
|
+
/** Builds a manage-request handler for incoming bulk.open asks, bound to one connection and its already-authenticated peer device-id (mirroring createCapabilityRequestHandler's own per-connection factory shape). Refuses a transfer-id already known to this receiver as "already_exists" -- resuming an existing transfer is requestResume's job, not a second bulk.open. */
|
|
81
|
+
createOpenHandler: (openerDevice: DeviceId, connection: Readonly<Connection>, onOffer: (event: Readonly<BulkOfferEvent>) => void) => (incoming: Readonly<IncomingManageRequest>) => Promise<void>;
|
|
82
|
+
/** Actively sends bulk.resume on a fresh connection for an already-accepted transfer, reading this receiver's own durably-persisted ack-seq as the resume point (spec/bulk.cddl's own "resumption is receiver-driven" design point) -- there is no separate bulk.open to send again. On success, re-associates the transfer with this connection so onFrame accepts its subsequent bulk-data/bulk-end frames. */
|
|
83
|
+
requestResume: (connection: Readonly<Connection>, session: Readonly<MeshSession>, transferId: Uint8Array<ArrayBuffer>, scope: Readonly<CapabilityScope>, targetDevice?: DeviceId, token?: CapabilityToken, timeoutMs?: number) => Promise<{
|
|
84
|
+
status: "ok" | "rejected";
|
|
85
|
+
code?: string;
|
|
86
|
+
}>;
|
|
87
|
+
/** Builds a manage-request handler for incoming bulk.cancel asks (wire as this session's own bulk:read-scoped handler, mirroring the sender's own bulk:write-scoped one) -- deregisters the transfer's connection association so a stray already-in-flight bulk-data/bulk-end for it is ignored by onFrame from then on. Durably-persisted chunks are left in storage rather than purged: a cancelled transfer is not proof the caller has lost interest in what was already received, only that no more is coming, and purging is the caller's own decision to make with the KeyValueStorage it supplied. */
|
|
88
|
+
createCancelHandler: () => (incoming: Readonly<IncomingManageRequest>) => Promise<void>;
|
|
89
|
+
}
|
|
90
|
+
export interface CreateBulkReceiverOptions {
|
|
91
|
+
storage: KeyValueStorage;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Builds a receiver coordinator over one KeyValueStorage. Constructed once and shared across every connection this side accepts -- durable per-transfer state (which chunks are persisted, who opened it) and the in-memory registry of which connection currently owns each transfer must both outlive any one connection for resumption to mean anything at all.
|
|
95
|
+
*/
|
|
96
|
+
export declare function createBulkReceiver(options: Readonly<CreateBulkReceiverOptions>): BulkReceiver;
|
|
97
|
+
//#endregion
|