wire-mesh-core 0.0.0 → 1.0.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 +32 -0
- package/dist/adapters/frame-codec.cjs +37 -0
- package/dist/adapters/frame-codec.d.cts +12 -0
- package/dist/adapters/frame-codec.d.mts +12 -0
- package/dist/adapters/frame-codec.mjs +33 -0
- package/dist/adapters/memory-storage.cjs +14 -0
- package/dist/adapters/memory-storage.d.cts +5 -0
- package/dist/adapters/memory-storage.d.mts +5 -0
- package/dist/adapters/memory-storage.mjs +13 -0
- package/dist/adapters/node-identity.cjs +55 -0
- package/dist/adapters/node-identity.d.cts +11 -0
- package/dist/adapters/node-identity.d.mts +11 -0
- package/dist/adapters/node-identity.mjs +52 -0
- package/dist/adapters/system-clock.cjs +8 -0
- package/dist/adapters/system-clock.d.cts +5 -0
- package/dist/adapters/system-clock.d.mts +5 -0
- package/dist/adapters/system-clock.mjs +7 -0
- package/dist/adapters/tcp-transport.cjs +154 -0
- package/dist/adapters/tcp-transport.d.cts +5 -0
- package/dist/adapters/tcp-transport.d.mts +5 -0
- package/dist/adapters/tcp-transport.mjs +153 -0
- package/dist/adapters/tls-transport.cjs +178 -0
- package/dist/adapters/tls-transport.d.cts +9 -0
- package/dist/adapters/tls-transport.d.mts +9 -0
- package/dist/adapters/tls-transport.mjs +177 -0
- package/dist/domain/device-id.cjs +27 -0
- package/dist/domain/device-id.d.cts +9 -0
- package/dist/domain/device-id.d.mts +9 -0
- package/dist/domain/device-id.mjs +24 -0
- package/dist/domain/handshake.cjs +23 -0
- package/dist/domain/handshake.d.cts +16 -0
- package/dist/domain/handshake.d.mts +16 -0
- package/dist/domain/handshake.mjs +21 -0
- package/dist/domain/mesh-session.cjs +439 -0
- package/dist/domain/mesh-session.d.cts +101 -0
- package/dist/domain/mesh-session.d.mts +101 -0
- package/dist/domain/mesh-session.mjs +436 -0
- package/dist/domain/relay-hub.cjs +99 -0
- package/dist/domain/relay-hub.d.cts +10 -0
- package/dist/domain/relay-hub.d.mts +10 -0
- package/dist/domain/relay-hub.mjs +98 -0
- package/dist/domain/tokens.cjs +300 -0
- package/dist/domain/tokens.d.cts +86 -0
- package/dist/domain/tokens.d.mts +86 -0
- package/dist/domain/tokens.mjs +296 -0
- package/dist/generated/protocol.cjs +466 -0
- package/dist/generated/protocol.d.cts +2 -0
- package/dist/generated/protocol.d.mts +2 -0
- package/dist/generated/protocol.mjs +382 -0
- package/dist/generated/runtime.cjs +8 -0
- package/dist/generated/runtime.d.cts +2 -0
- package/dist/generated/runtime.d.mts +2 -0
- package/dist/generated/runtime.mjs +2 -0
- package/dist/ports/clock.cjs +0 -0
- package/dist/ports/clock.d.cts +6 -0
- package/dist/ports/clock.d.mts +6 -0
- package/dist/ports/clock.mjs +1 -0
- package/dist/ports/identity.cjs +0 -0
- package/dist/ports/identity.d.cts +16 -0
- package/dist/ports/identity.d.mts +16 -0
- package/dist/ports/identity.mjs +1 -0
- package/dist/ports/storage.cjs +0 -0
- package/dist/ports/storage.d.cts +9 -0
- package/dist/ports/storage.d.mts +9 -0
- package/dist/ports/storage.mjs +1 -0
- package/dist/ports/transport.cjs +0 -0
- package/dist/ports/transport.d.cts +29 -0
- package/dist/ports/transport.d.mts +29 -0
- package/dist/ports/transport.mjs +1 -0
- package/dist/protocol-B26-5VX7.d.cts +1112 -0
- package/dist/protocol-B26-5VX7.d.mts +1112 -0
- package/package.json +126 -2
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { deviceIdToHex } from "./device-id.mjs";
|
|
2
|
+
import { negotiate } from "./handshake.mjs";
|
|
3
|
+
import { messageFromFrame, tryDecodeFrame } from "../adapters/frame-codec.mjs";
|
|
4
|
+
//#region src/domain/mesh-session.ts
|
|
5
|
+
const MS_PER_SECOND = 1e3;
|
|
6
|
+
/** How long to wait for the node's handshake before calling it unanswered. A relay-only node never sends one; that is a state to display, not an error. */
|
|
7
|
+
const HANDSHAKE_TIMEOUT_MS = 3e3;
|
|
8
|
+
function localHandshake(domains) {
|
|
9
|
+
return {
|
|
10
|
+
type: "handshake",
|
|
11
|
+
version: 1,
|
|
12
|
+
domains: [...domains]
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Builds the connection-agnostic session state machine and its public MeshSession surface. dial is null for a session that can never (re)connect on its own -- acceptMeshSession's case, where the one connection it will ever have already exists by construction and reconnect therefore cannot apply (only the remote redialing, and being accepted again, produces a fresh connection). createMeshSession supplies dial as transport.connect so its own connect()/reconnect behaviour is unchanged from before this was factored out.
|
|
17
|
+
*/
|
|
18
|
+
function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert) {
|
|
19
|
+
let connection = null;
|
|
20
|
+
let state = { status: "idle" };
|
|
21
|
+
let handshake = { status: "pending" };
|
|
22
|
+
const directory = /* @__PURE__ */ new Map();
|
|
23
|
+
const frameLog = [];
|
|
24
|
+
let feedCancelled = false;
|
|
25
|
+
const eventWaiters = [];
|
|
26
|
+
const eventBacklog = [];
|
|
27
|
+
let handshakeTimer = null;
|
|
28
|
+
let reconnectTimer = null;
|
|
29
|
+
let attempt = 0;
|
|
30
|
+
let currentToken = null;
|
|
31
|
+
let nextRequestId = 0;
|
|
32
|
+
let relayPeerDevice = null;
|
|
33
|
+
const pendingManageRequests = /* @__PURE__ */ new Map();
|
|
34
|
+
const incomingWaiters = [];
|
|
35
|
+
const incomingBacklog = [];
|
|
36
|
+
const revocationWaiters = [];
|
|
37
|
+
const revocationBacklog = [];
|
|
38
|
+
function snapshot() {
|
|
39
|
+
return {
|
|
40
|
+
state,
|
|
41
|
+
directory: [...directory.values()],
|
|
42
|
+
frameLog: [...frameLog]
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function emit() {
|
|
46
|
+
const event = snapshot();
|
|
47
|
+
const waiter = eventWaiters.shift();
|
|
48
|
+
if (waiter) waiter(event);
|
|
49
|
+
else eventBacklog.push(event);
|
|
50
|
+
}
|
|
51
|
+
function emitIncomingManageRequest(request) {
|
|
52
|
+
const waiter = incomingWaiters.shift();
|
|
53
|
+
if (waiter) waiter(request);
|
|
54
|
+
else incomingBacklog.push(request);
|
|
55
|
+
}
|
|
56
|
+
function emitRevocationEntry(entry) {
|
|
57
|
+
const waiter = revocationWaiters.shift();
|
|
58
|
+
if (waiter) waiter(entry);
|
|
59
|
+
else revocationBacklog.push(entry);
|
|
60
|
+
}
|
|
61
|
+
function rejectPendingManageRequests(reason) {
|
|
62
|
+
for (const pending of pendingManageRequests.values()) pending.reject(new Error(reason));
|
|
63
|
+
pendingManageRequests.clear();
|
|
64
|
+
}
|
|
65
|
+
function buildManageRequest(command, scope, tokenOverride) {
|
|
66
|
+
const requestId = nextRequestId;
|
|
67
|
+
nextRequestId += 1;
|
|
68
|
+
const token = tokenOverride ?? currentToken;
|
|
69
|
+
return {
|
|
70
|
+
type: "manage-request",
|
|
71
|
+
"request-id": requestId,
|
|
72
|
+
command,
|
|
73
|
+
scope,
|
|
74
|
+
...token !== null ? { token } : {}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** Sends a frame, wrapping it as relay-data first when viaRelay is set -- the single choke point every outbound manage-request/manage-response passes through, so a consumer of sendManageRequest/respond never needs its own relay-wrapping logic. */
|
|
78
|
+
async function transmit(frame, viaRelay) {
|
|
79
|
+
if (connection === null) throw new Error("not connected");
|
|
80
|
+
if (viaRelay) {
|
|
81
|
+
const relayFrame = {
|
|
82
|
+
type: "relay-data",
|
|
83
|
+
payload: messageFromFrame(frame)
|
|
84
|
+
};
|
|
85
|
+
await connection.send(relayFrame);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
await connection.send(frame);
|
|
89
|
+
}
|
|
90
|
+
function applyManageResponse(frame) {
|
|
91
|
+
const requestId = frame["request-id"];
|
|
92
|
+
const pending = pendingManageRequests.get(requestId);
|
|
93
|
+
if (pending !== void 0) {
|
|
94
|
+
pendingManageRequests.delete(requestId);
|
|
95
|
+
pending.resolve(frame.outcome);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function applyManageRequest(frame, viaRelay) {
|
|
99
|
+
const requestId = frame["request-id"];
|
|
100
|
+
const fromDevice = viaRelay && relayPeerDevice !== null ? relayPeerDevice : void 0;
|
|
101
|
+
emitIncomingManageRequest({
|
|
102
|
+
requestId,
|
|
103
|
+
command: frame.command,
|
|
104
|
+
scope: frame.scope,
|
|
105
|
+
...frame.token !== void 0 ? { token: frame.token } : {},
|
|
106
|
+
...fromDevice !== void 0 ? { fromDevice } : {},
|
|
107
|
+
respond: async (outcome) => {
|
|
108
|
+
const response = {
|
|
109
|
+
type: "manage-response",
|
|
110
|
+
"request-id": requestId,
|
|
111
|
+
outcome
|
|
112
|
+
};
|
|
113
|
+
frameLog.push({
|
|
114
|
+
direction: "sent",
|
|
115
|
+
frame: response
|
|
116
|
+
});
|
|
117
|
+
await transmit(response, viaRelay);
|
|
118
|
+
emit();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function applyFrame(frame) {
|
|
123
|
+
if (frame.type === "relay-data") {
|
|
124
|
+
const inner = tryDecodeFrame(frame.payload);
|
|
125
|
+
if (inner !== null && (inner.type === "manage-request" || inner.type === "manage-response")) {
|
|
126
|
+
frameLog.push({
|
|
127
|
+
direction: "received",
|
|
128
|
+
frame: inner
|
|
129
|
+
});
|
|
130
|
+
if (inner.type === "manage-response") applyManageResponse(inner);
|
|
131
|
+
else applyManageRequest(inner, true);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
frameLog.push({
|
|
135
|
+
direction: "received",
|
|
136
|
+
frame
|
|
137
|
+
});
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
frameLog.push({
|
|
141
|
+
direction: "received",
|
|
142
|
+
frame
|
|
143
|
+
});
|
|
144
|
+
if (frame.type === "handshake") applyRemoteHandshake(frame);
|
|
145
|
+
else if (frame.type === "gossip") for (const advert of frame.peers) {
|
|
146
|
+
directory.set(deviceIdToHex(advert.device), {
|
|
147
|
+
device: advert.device,
|
|
148
|
+
advert
|
|
149
|
+
});
|
|
150
|
+
onPeerAdvert?.(advert);
|
|
151
|
+
}
|
|
152
|
+
else if (frame.type === "relay-inbound") relayPeerDevice = frame["source-device"];
|
|
153
|
+
else if (frame.type === "manage-response") applyManageResponse(frame);
|
|
154
|
+
else if (frame.type === "manage-request") applyManageRequest(frame, false);
|
|
155
|
+
else if (frame.type === "revocation-announce") for (const entry of frame.entries) emitRevocationEntry(entry);
|
|
156
|
+
}
|
|
157
|
+
/** Establishes a relay-connect pairing to targetDevice if this session isn't already paired with it -- a no-op when it already is, whether that pairing was established by this session's own prior relay-connect (initiator role) or learned from an incoming relay-inbound (target role, replying back to whoever dialed it). relay-connect has no ack frame: the initiator proceeds to relay-data right after sending it. */
|
|
158
|
+
async function ensureRelayPairing(targetDevice) {
|
|
159
|
+
if (connection === null) throw new Error("not connected");
|
|
160
|
+
if (relayPeerDevice !== null && deviceIdToHex(relayPeerDevice) === deviceIdToHex(targetDevice)) return;
|
|
161
|
+
const relayConnect = {
|
|
162
|
+
type: "relay-connect",
|
|
163
|
+
"target-device": targetDevice
|
|
164
|
+
};
|
|
165
|
+
frameLog.push({
|
|
166
|
+
direction: "sent",
|
|
167
|
+
frame: relayConnect
|
|
168
|
+
});
|
|
169
|
+
await connection.send(relayConnect);
|
|
170
|
+
relayPeerDevice = targetDevice;
|
|
171
|
+
emit();
|
|
172
|
+
}
|
|
173
|
+
let localHandshakeSent = localHandshake([]);
|
|
174
|
+
function applyRemoteHandshake(remote) {
|
|
175
|
+
if (handshake.status !== "pending") return;
|
|
176
|
+
if (handshakeTimer !== null) {
|
|
177
|
+
clearTimeout(handshakeTimer);
|
|
178
|
+
handshakeTimer = null;
|
|
179
|
+
}
|
|
180
|
+
const result = negotiate(localHandshakeSent, remote);
|
|
181
|
+
handshake = result.ok ? {
|
|
182
|
+
status: "negotiated",
|
|
183
|
+
version: result.version,
|
|
184
|
+
sharedDomains: result.sharedDomains
|
|
185
|
+
} : {
|
|
186
|
+
status: "rejected",
|
|
187
|
+
reason: "no shared domains or version"
|
|
188
|
+
};
|
|
189
|
+
if (state.status === "connected") state = {
|
|
190
|
+
...state,
|
|
191
|
+
handshake
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function handleDisconnect(reason, address, localDomains) {
|
|
195
|
+
if (feedCancelled) return;
|
|
196
|
+
rejectPendingManageRequests("disconnected before a response arrived");
|
|
197
|
+
if (reconnect !== null && attempt < reconnect.maxAttempts) {
|
|
198
|
+
attempt += 1;
|
|
199
|
+
const currentAttempt = attempt;
|
|
200
|
+
state = {
|
|
201
|
+
status: "reconnecting",
|
|
202
|
+
address,
|
|
203
|
+
attempt: currentAttempt,
|
|
204
|
+
reason
|
|
205
|
+
};
|
|
206
|
+
emit();
|
|
207
|
+
reconnectTimer = setTimeout(() => {
|
|
208
|
+
reconnectTimer = null;
|
|
209
|
+
doConnect(address, localDomains).catch((error) => {
|
|
210
|
+
handleDisconnect(error instanceof Error ? error.message : String(error), address, localDomains);
|
|
211
|
+
});
|
|
212
|
+
}, reconnect.delayMs(currentAttempt));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
state = {
|
|
216
|
+
status: "closed",
|
|
217
|
+
address,
|
|
218
|
+
reason
|
|
219
|
+
};
|
|
220
|
+
emit();
|
|
221
|
+
}
|
|
222
|
+
async function consume(link, address, localDomains) {
|
|
223
|
+
for await (const frame of link.receive()) {
|
|
224
|
+
if (feedCancelled) return;
|
|
225
|
+
applyFrame(frame);
|
|
226
|
+
emit();
|
|
227
|
+
}
|
|
228
|
+
if (state.status === "connected") handleDisconnect("node closed the connection", address, localDomains);
|
|
229
|
+
}
|
|
230
|
+
/** Everything a connection needs once it exists, regardless of whether it was dialled (createMeshSession's own doConnect, below) or handed over already established (acceptMeshSession): send this side's handshake and self-advert, arm the handshake timeout, and start consuming frames. The two entry points differ only in how link itself came to exist and what address means for it -- a real dial target for one, a caller-chosen label for the other, since the Connection/Transport ports expose no remote-address concept of their own for an accepted connection. */
|
|
231
|
+
async function wireUpConnection(link, address, localDomains) {
|
|
232
|
+
connection = link;
|
|
233
|
+
localHandshakeSent = localHandshake(localDomains);
|
|
234
|
+
handshake = { status: "pending" };
|
|
235
|
+
state = {
|
|
236
|
+
status: "connected",
|
|
237
|
+
address,
|
|
238
|
+
handshake
|
|
239
|
+
};
|
|
240
|
+
frameLog.push({
|
|
241
|
+
direction: "sent",
|
|
242
|
+
frame: localHandshakeSent
|
|
243
|
+
});
|
|
244
|
+
await connection.send(localHandshakeSent);
|
|
245
|
+
emit();
|
|
246
|
+
const selfAdvert = {
|
|
247
|
+
type: "gossip",
|
|
248
|
+
peers: [{
|
|
249
|
+
device: identity.deviceId,
|
|
250
|
+
addresses: [],
|
|
251
|
+
"snapshot-seconds": Math.floor(clock.now() / MS_PER_SECOND)
|
|
252
|
+
}]
|
|
253
|
+
};
|
|
254
|
+
frameLog.push({
|
|
255
|
+
direction: "sent",
|
|
256
|
+
frame: selfAdvert
|
|
257
|
+
});
|
|
258
|
+
await connection.send(selfAdvert);
|
|
259
|
+
emit();
|
|
260
|
+
handshakeTimer = setTimeout(() => {
|
|
261
|
+
handshakeTimer = null;
|
|
262
|
+
if (handshake.status === "pending") {
|
|
263
|
+
handshake = { status: "unanswered" };
|
|
264
|
+
if (state.status === "connected") state = {
|
|
265
|
+
...state,
|
|
266
|
+
handshake
|
|
267
|
+
};
|
|
268
|
+
emit();
|
|
269
|
+
}
|
|
270
|
+
}, HANDSHAKE_TIMEOUT_MS);
|
|
271
|
+
consume(connection, address, localDomains).catch((error) => {
|
|
272
|
+
if (state.status === "connected") handleDisconnect(error instanceof Error ? error.message : String(error), address, localDomains);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
async function doConnect(address, localDomains) {
|
|
276
|
+
if (dial === null) throw new Error("this session is already connected; there is nothing to dial");
|
|
277
|
+
if (handshakeTimer !== null) {
|
|
278
|
+
clearTimeout(handshakeTimer);
|
|
279
|
+
handshakeTimer = null;
|
|
280
|
+
}
|
|
281
|
+
state = {
|
|
282
|
+
status: "connecting",
|
|
283
|
+
address
|
|
284
|
+
};
|
|
285
|
+
emit();
|
|
286
|
+
const link = await dial(address);
|
|
287
|
+
if (feedCancelled) {
|
|
288
|
+
await link.close();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
await wireUpConnection(link, address, localDomains);
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
wireUpConnection,
|
|
295
|
+
session: {
|
|
296
|
+
events: { [Symbol.asyncIterator]() {
|
|
297
|
+
return { next: async () => new Promise((resolve) => {
|
|
298
|
+
const backlogEvent = eventBacklog.shift();
|
|
299
|
+
if (backlogEvent) resolve({
|
|
300
|
+
value: backlogEvent,
|
|
301
|
+
done: false
|
|
302
|
+
});
|
|
303
|
+
else eventWaiters.push((event) => {
|
|
304
|
+
resolve({
|
|
305
|
+
value: event,
|
|
306
|
+
done: false
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
}) };
|
|
310
|
+
} },
|
|
311
|
+
incomingManageRequests: { [Symbol.asyncIterator]() {
|
|
312
|
+
return { next: async () => new Promise((resolve) => {
|
|
313
|
+
const backlogRequest = incomingBacklog.shift();
|
|
314
|
+
if (backlogRequest) resolve({
|
|
315
|
+
value: backlogRequest,
|
|
316
|
+
done: false
|
|
317
|
+
});
|
|
318
|
+
else incomingWaiters.push((request) => {
|
|
319
|
+
resolve({
|
|
320
|
+
value: request,
|
|
321
|
+
done: false
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
}) };
|
|
325
|
+
} },
|
|
326
|
+
revocationAnnouncements: { [Symbol.asyncIterator]() {
|
|
327
|
+
return { next: async () => new Promise((resolve) => {
|
|
328
|
+
const backlogEntry = revocationBacklog.shift();
|
|
329
|
+
if (backlogEntry) resolve({
|
|
330
|
+
value: backlogEntry,
|
|
331
|
+
done: false
|
|
332
|
+
});
|
|
333
|
+
else revocationWaiters.push((entry) => {
|
|
334
|
+
resolve({
|
|
335
|
+
value: entry,
|
|
336
|
+
done: false
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
}) };
|
|
340
|
+
} },
|
|
341
|
+
async connect(address, localDomains) {
|
|
342
|
+
if (connection !== null) throw new Error("a session connects once; create a new one to reconnect");
|
|
343
|
+
attempt = 0;
|
|
344
|
+
await doConnect(address, localDomains);
|
|
345
|
+
},
|
|
346
|
+
async sendPing() {
|
|
347
|
+
if (connection === null || state.status !== "connected") throw new Error("not connected");
|
|
348
|
+
const ping = { type: "ping" };
|
|
349
|
+
frameLog.push({
|
|
350
|
+
direction: "sent",
|
|
351
|
+
frame: ping
|
|
352
|
+
});
|
|
353
|
+
await connection.send(ping);
|
|
354
|
+
emit();
|
|
355
|
+
},
|
|
356
|
+
setToken(token) {
|
|
357
|
+
currentToken = token;
|
|
358
|
+
},
|
|
359
|
+
async sendManageRequest(command, scope, targetDevice, token) {
|
|
360
|
+
if (connection === null || state.status !== "connected") throw new Error("not connected");
|
|
361
|
+
if (targetDevice !== void 0) await ensureRelayPairing(targetDevice);
|
|
362
|
+
const frame = buildManageRequest(command, scope, token);
|
|
363
|
+
const outcome = new Promise((resolve, reject) => {
|
|
364
|
+
pendingManageRequests.set(frame["request-id"], {
|
|
365
|
+
resolve,
|
|
366
|
+
reject
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
frameLog.push({
|
|
370
|
+
direction: "sent",
|
|
371
|
+
frame
|
|
372
|
+
});
|
|
373
|
+
await transmit(frame, targetDevice !== void 0);
|
|
374
|
+
emit();
|
|
375
|
+
return outcome;
|
|
376
|
+
},
|
|
377
|
+
async sendRevocationAnnounce(entries) {
|
|
378
|
+
if (connection === null || state.status !== "connected") throw new Error("not connected");
|
|
379
|
+
const frame = {
|
|
380
|
+
type: "revocation-announce",
|
|
381
|
+
entries: [...entries]
|
|
382
|
+
};
|
|
383
|
+
frameLog.push({
|
|
384
|
+
direction: "sent",
|
|
385
|
+
frame
|
|
386
|
+
});
|
|
387
|
+
await transmit(frame, false);
|
|
388
|
+
emit();
|
|
389
|
+
},
|
|
390
|
+
async close() {
|
|
391
|
+
feedCancelled = true;
|
|
392
|
+
if (handshakeTimer !== null) {
|
|
393
|
+
clearTimeout(handshakeTimer);
|
|
394
|
+
handshakeTimer = null;
|
|
395
|
+
}
|
|
396
|
+
if (reconnectTimer !== null) {
|
|
397
|
+
clearTimeout(reconnectTimer);
|
|
398
|
+
reconnectTimer = null;
|
|
399
|
+
}
|
|
400
|
+
rejectPendingManageRequests("connection closed before a response arrived");
|
|
401
|
+
if (connection !== null) await connection.close();
|
|
402
|
+
if (state.status === "connected" || state.status === "connecting" || state.status === "reconnecting") state = {
|
|
403
|
+
status: "closed",
|
|
404
|
+
address: state.address,
|
|
405
|
+
reason: "closed by you"
|
|
406
|
+
};
|
|
407
|
+
emit();
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
function createMeshSession(transport, identity, clock = { now: () => Date.now() }, reconnect = null) {
|
|
413
|
+
const { session } = createSessionCore(identity, clock, reconnect, async (address) => transport.connect(address));
|
|
414
|
+
return session;
|
|
415
|
+
}
|
|
416
|
+
/** Wires an already-accepted Connection up as a full MeshSession, mirroring exactly what createMeshSession's own dial path does once a connection exists (send handshake, send self-advert, negotiate, consume frames) -- the wire-mesh#45 prerequisite agent-comms needs, since its peers both listen and dial rather than only ever dialing the way web-console's own console UI does. Reconnect does not apply here: if this connection drops, only the remote redialing and being accepted again produces a new connection, and therefore a new session -- there is nothing on this side to retry. */
|
|
417
|
+
async function acceptMeshSession(connection, identity, localDomains, options = {}) {
|
|
418
|
+
const clock = options.clock ?? { now: () => Date.now() };
|
|
419
|
+
let resolvePeerDeviceId = null;
|
|
420
|
+
const peerDeviceId = new Promise((resolve) => {
|
|
421
|
+
resolvePeerDeviceId = resolve;
|
|
422
|
+
});
|
|
423
|
+
let peerDeviceIdResolved = false;
|
|
424
|
+
const { session, wireUpConnection } = createSessionCore(identity, clock, null, null, (advert) => {
|
|
425
|
+
if (peerDeviceIdResolved) return;
|
|
426
|
+
peerDeviceIdResolved = true;
|
|
427
|
+
resolvePeerDeviceId?.(advert.device);
|
|
428
|
+
});
|
|
429
|
+
await wireUpConnection(connection, options.label ?? "accepted", localDomains);
|
|
430
|
+
return {
|
|
431
|
+
...session,
|
|
432
|
+
peerDeviceId
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
//#endregion
|
|
436
|
+
export { HANDSHAKE_TIMEOUT_MS, acceptMeshSession, createMeshSession };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/domain/relay-hub.ts
|
|
3
|
+
const HEX_RADIX = 16;
|
|
4
|
+
const HEX_DIGITS_PER_BYTE = 2;
|
|
5
|
+
function deviceKey(device) {
|
|
6
|
+
let key = "";
|
|
7
|
+
for (const byte of device) key += byte.toString(HEX_RADIX).padStart(HEX_DIGITS_PER_BYTE, "0");
|
|
8
|
+
return key;
|
|
9
|
+
}
|
|
10
|
+
function createRelayHub() {
|
|
11
|
+
const devices = /* @__PURE__ */ new Map();
|
|
12
|
+
const connectionDevice = /* @__PURE__ */ new Map();
|
|
13
|
+
const pairings = /* @__PURE__ */ new Map();
|
|
14
|
+
const mostRecentPairing = /* @__PURE__ */ new Map();
|
|
15
|
+
function pairingsOf(connection) {
|
|
16
|
+
const existing = pairings.get(connection);
|
|
17
|
+
if (existing) return existing;
|
|
18
|
+
const created = /* @__PURE__ */ new Map();
|
|
19
|
+
pairings.set(connection, created);
|
|
20
|
+
return created;
|
|
21
|
+
}
|
|
22
|
+
function addPairing(a, aDevice, b, bDevice) {
|
|
23
|
+
pairingsOf(a).set(deviceKey(bDevice), b);
|
|
24
|
+
pairingsOf(b).set(deviceKey(aDevice), a);
|
|
25
|
+
mostRecentPairing.set(a, b);
|
|
26
|
+
mostRecentPairing.set(b, a);
|
|
27
|
+
}
|
|
28
|
+
function forgetConnection(connection) {
|
|
29
|
+
for (const [key, registration] of devices) if (registration.connection === connection) devices.delete(key);
|
|
30
|
+
const ownDevice = connectionDevice.get(connection);
|
|
31
|
+
connectionDevice.delete(connection);
|
|
32
|
+
mostRecentPairing.delete(connection);
|
|
33
|
+
const own = pairings.get(connection);
|
|
34
|
+
if (own) {
|
|
35
|
+
for (const peer of own.values()) {
|
|
36
|
+
const peerOwn = pairings.get(peer);
|
|
37
|
+
if (peerOwn && ownDevice !== void 0) peerOwn.delete(deviceKey(ownDevice));
|
|
38
|
+
if (mostRecentPairing.get(peer) === connection) mostRecentPairing.delete(peer);
|
|
39
|
+
}
|
|
40
|
+
pairings.delete(connection);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function handleFrame(connection, frame) {
|
|
44
|
+
if (frame.type === "gossip") {
|
|
45
|
+
for (const advert of frame.peers) devices.set(deviceKey(advert.device), {
|
|
46
|
+
connection,
|
|
47
|
+
device: advert.device
|
|
48
|
+
});
|
|
49
|
+
for (const registration of devices.values()) if (registration.connection === connection) {
|
|
50
|
+
connectionDevice.set(connection, registration.device);
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (frame.type === "relay-connect") {
|
|
56
|
+
const registration = devices.get(deviceKey(frame["target-device"]));
|
|
57
|
+
if (!registration || registration.connection === connection) return;
|
|
58
|
+
const initiatorDevice = connectionDevice.get(connection);
|
|
59
|
+
if (initiatorDevice === void 0) return;
|
|
60
|
+
addPairing(connection, initiatorDevice, registration.connection, registration.device);
|
|
61
|
+
await registration.connection.send({
|
|
62
|
+
type: "relay-inbound",
|
|
63
|
+
"source-device": initiatorDevice
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (frame.type === "relay-data") {
|
|
68
|
+
const own = pairings.get(connection);
|
|
69
|
+
const toDevice = frame["to-device"];
|
|
70
|
+
const peer = toDevice !== void 0 ? own?.get(deviceKey(toDevice)) : mostRecentPairing.get(connection);
|
|
71
|
+
if (!peer) return;
|
|
72
|
+
const senderDevice = connectionDevice.get(connection);
|
|
73
|
+
if (senderDevice === void 0) return;
|
|
74
|
+
await peer.send({
|
|
75
|
+
type: "relay-data",
|
|
76
|
+
payload: frame.payload,
|
|
77
|
+
"from-device": senderDevice
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
async handleConnection(connection) {
|
|
84
|
+
try {
|
|
85
|
+
for await (const frame of connection.receive()) await handleFrame(connection, frame);
|
|
86
|
+
} catch {} finally {
|
|
87
|
+
forgetConnection(connection);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
stop() {
|
|
91
|
+
devices.clear();
|
|
92
|
+
connectionDevice.clear();
|
|
93
|
+
pairings.clear();
|
|
94
|
+
mostRecentPairing.clear();
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
99
|
+
exports.createRelayHub = createRelayHub;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Connection } from "../ports/transport.cjs";
|
|
2
|
+
//#region src/domain/relay-hub.d.ts
|
|
3
|
+
export interface RelayHub {
|
|
4
|
+
/** Drives one accepted connection until it closes: registers gossip-advertised devices, answers relay-connect by pairing and notifying the target, and forwards relay-data within established pairings. Resolves when the connection's frame stream ends. */
|
|
5
|
+
handleConnection: (connection: Readonly<Connection>) => Promise<void>;
|
|
6
|
+
/** Drops all registry and pairing state -- used by tests and by transport teardown. */
|
|
7
|
+
stop: () => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function createRelayHub(): RelayHub;
|
|
10
|
+
//#endregion
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Connection } from "../ports/transport.mjs";
|
|
2
|
+
//#region src/domain/relay-hub.d.ts
|
|
3
|
+
export interface RelayHub {
|
|
4
|
+
/** Drives one accepted connection until it closes: registers gossip-advertised devices, answers relay-connect by pairing and notifying the target, and forwards relay-data within established pairings. Resolves when the connection's frame stream ends. */
|
|
5
|
+
handleConnection: (connection: Readonly<Connection>) => Promise<void>;
|
|
6
|
+
/** Drops all registry and pairing state -- used by tests and by transport teardown. */
|
|
7
|
+
stop: () => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function createRelayHub(): RelayHub;
|
|
10
|
+
//#endregion
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
//#region src/domain/relay-hub.ts
|
|
2
|
+
const HEX_RADIX = 16;
|
|
3
|
+
const HEX_DIGITS_PER_BYTE = 2;
|
|
4
|
+
function deviceKey(device) {
|
|
5
|
+
let key = "";
|
|
6
|
+
for (const byte of device) key += byte.toString(HEX_RADIX).padStart(HEX_DIGITS_PER_BYTE, "0");
|
|
7
|
+
return key;
|
|
8
|
+
}
|
|
9
|
+
function createRelayHub() {
|
|
10
|
+
const devices = /* @__PURE__ */ new Map();
|
|
11
|
+
const connectionDevice = /* @__PURE__ */ new Map();
|
|
12
|
+
const pairings = /* @__PURE__ */ new Map();
|
|
13
|
+
const mostRecentPairing = /* @__PURE__ */ new Map();
|
|
14
|
+
function pairingsOf(connection) {
|
|
15
|
+
const existing = pairings.get(connection);
|
|
16
|
+
if (existing) return existing;
|
|
17
|
+
const created = /* @__PURE__ */ new Map();
|
|
18
|
+
pairings.set(connection, created);
|
|
19
|
+
return created;
|
|
20
|
+
}
|
|
21
|
+
function addPairing(a, aDevice, b, bDevice) {
|
|
22
|
+
pairingsOf(a).set(deviceKey(bDevice), b);
|
|
23
|
+
pairingsOf(b).set(deviceKey(aDevice), a);
|
|
24
|
+
mostRecentPairing.set(a, b);
|
|
25
|
+
mostRecentPairing.set(b, a);
|
|
26
|
+
}
|
|
27
|
+
function forgetConnection(connection) {
|
|
28
|
+
for (const [key, registration] of devices) if (registration.connection === connection) devices.delete(key);
|
|
29
|
+
const ownDevice = connectionDevice.get(connection);
|
|
30
|
+
connectionDevice.delete(connection);
|
|
31
|
+
mostRecentPairing.delete(connection);
|
|
32
|
+
const own = pairings.get(connection);
|
|
33
|
+
if (own) {
|
|
34
|
+
for (const peer of own.values()) {
|
|
35
|
+
const peerOwn = pairings.get(peer);
|
|
36
|
+
if (peerOwn && ownDevice !== void 0) peerOwn.delete(deviceKey(ownDevice));
|
|
37
|
+
if (mostRecentPairing.get(peer) === connection) mostRecentPairing.delete(peer);
|
|
38
|
+
}
|
|
39
|
+
pairings.delete(connection);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function handleFrame(connection, frame) {
|
|
43
|
+
if (frame.type === "gossip") {
|
|
44
|
+
for (const advert of frame.peers) devices.set(deviceKey(advert.device), {
|
|
45
|
+
connection,
|
|
46
|
+
device: advert.device
|
|
47
|
+
});
|
|
48
|
+
for (const registration of devices.values()) if (registration.connection === connection) {
|
|
49
|
+
connectionDevice.set(connection, registration.device);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (frame.type === "relay-connect") {
|
|
55
|
+
const registration = devices.get(deviceKey(frame["target-device"]));
|
|
56
|
+
if (!registration || registration.connection === connection) return;
|
|
57
|
+
const initiatorDevice = connectionDevice.get(connection);
|
|
58
|
+
if (initiatorDevice === void 0) return;
|
|
59
|
+
addPairing(connection, initiatorDevice, registration.connection, registration.device);
|
|
60
|
+
await registration.connection.send({
|
|
61
|
+
type: "relay-inbound",
|
|
62
|
+
"source-device": initiatorDevice
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (frame.type === "relay-data") {
|
|
67
|
+
const own = pairings.get(connection);
|
|
68
|
+
const toDevice = frame["to-device"];
|
|
69
|
+
const peer = toDevice !== void 0 ? own?.get(deviceKey(toDevice)) : mostRecentPairing.get(connection);
|
|
70
|
+
if (!peer) return;
|
|
71
|
+
const senderDevice = connectionDevice.get(connection);
|
|
72
|
+
if (senderDevice === void 0) return;
|
|
73
|
+
await peer.send({
|
|
74
|
+
type: "relay-data",
|
|
75
|
+
payload: frame.payload,
|
|
76
|
+
"from-device": senderDevice
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
async handleConnection(connection) {
|
|
83
|
+
try {
|
|
84
|
+
for await (const frame of connection.receive()) await handleFrame(connection, frame);
|
|
85
|
+
} catch {} finally {
|
|
86
|
+
forgetConnection(connection);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
stop() {
|
|
90
|
+
devices.clear();
|
|
91
|
+
connectionDevice.clear();
|
|
92
|
+
pairings.clear();
|
|
93
|
+
mostRecentPairing.clear();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
export { createRelayHub };
|