@peerbit/canonical-client 0.0.0-e209d2e
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/LICENSE +202 -0
- package/dist/src/auto.d.ts +41 -0
- package/dist/src/auto.d.ts.map +1 -0
- package/dist/src/auto.js +104 -0
- package/dist/src/auto.js.map +1 -0
- package/dist/src/client.d.ts +76 -0
- package/dist/src/client.d.ts.map +1 -0
- package/dist/src/client.js +709 -0
- package/dist/src/client.js.map +1 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +5 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/peerbit.d.ts +51 -0
- package/dist/src/peerbit.d.ts.map +1 -0
- package/dist/src/peerbit.js +253 -0
- package/dist/src/peerbit.js.map +1 -0
- package/dist/src/service-worker.d.ts +19 -0
- package/dist/src/service-worker.d.ts.map +1 -0
- package/dist/src/service-worker.js +89 -0
- package/dist/src/service-worker.js.map +1 -0
- package/dist/src/window.d.ts +17 -0
- package/dist/src/window.d.ts.map +1 -0
- package/dist/src/window.js +80 -0
- package/dist/src/window.js.map +1 -0
- package/package.json +90 -0
- package/src/auto.ts +166 -0
- package/src/client.ts +722 -0
- package/src/index.ts +9 -0
- package/src/peerbit.ts +354 -0
- package/src/service-worker.ts +118 -0
- package/src/window.ts +112 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
import { deserialize, field, serialize, variant } from "@dao-xyz/borsh";
|
|
2
|
+
import {
|
|
3
|
+
CanonicalBootstrapRequest,
|
|
4
|
+
CanonicalConnection,
|
|
5
|
+
CanonicalControlRequest,
|
|
6
|
+
CanonicalControlResponse,
|
|
7
|
+
CanonicalLoadProgramRequest,
|
|
8
|
+
CanonicalSignRequest,
|
|
9
|
+
createMessagePortTransport as createControlTransport,
|
|
10
|
+
createRpcTransport as createRpcTransportBase,
|
|
11
|
+
} from "@peerbit/canonical-transport";
|
|
12
|
+
import type {
|
|
13
|
+
CanonicalChannel,
|
|
14
|
+
CanonicalRpcTransport,
|
|
15
|
+
CanonicalTransport,
|
|
16
|
+
} from "@peerbit/canonical-transport";
|
|
17
|
+
import {
|
|
18
|
+
type Identity,
|
|
19
|
+
PreHash,
|
|
20
|
+
PublicSignKey,
|
|
21
|
+
SignatureWithKey,
|
|
22
|
+
} from "@peerbit/crypto";
|
|
23
|
+
import type { CanonicalOpenAdapter, CanonicalOpenMode } from "./auto.js";
|
|
24
|
+
import type { PeerbitCanonicalClient } from "./peerbit.js";
|
|
25
|
+
|
|
26
|
+
export type { CanonicalChannel };
|
|
27
|
+
|
|
28
|
+
export type { CanonicalRpcTransport };
|
|
29
|
+
|
|
30
|
+
abstract class RpcMessage {}
|
|
31
|
+
|
|
32
|
+
class RpcRequestHeader {
|
|
33
|
+
@field({ type: "u32" })
|
|
34
|
+
id: number;
|
|
35
|
+
|
|
36
|
+
@field({ type: "string" })
|
|
37
|
+
method: string;
|
|
38
|
+
|
|
39
|
+
constructor(properties: { id: number; method: string }) {
|
|
40
|
+
this.id = properties.id;
|
|
41
|
+
this.method = properties.method;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class RpcResponseHeader {
|
|
46
|
+
@field({ type: "u32" })
|
|
47
|
+
id: number;
|
|
48
|
+
|
|
49
|
+
@field({ type: "string" })
|
|
50
|
+
method: string;
|
|
51
|
+
|
|
52
|
+
constructor(properties: { id: number; method: string }) {
|
|
53
|
+
this.id = properties.id;
|
|
54
|
+
this.method = properties.method;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@variant(0)
|
|
59
|
+
class RpcRequest extends RpcMessage {
|
|
60
|
+
@field({ type: RpcRequestHeader })
|
|
61
|
+
header: RpcRequestHeader;
|
|
62
|
+
|
|
63
|
+
@field({ type: Uint8Array })
|
|
64
|
+
payload: Uint8Array;
|
|
65
|
+
|
|
66
|
+
constructor(properties: { header: RpcRequestHeader; payload: Uint8Array }) {
|
|
67
|
+
super();
|
|
68
|
+
this.header = properties.header;
|
|
69
|
+
this.payload = properties.payload;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@variant(1)
|
|
74
|
+
class RpcOk extends RpcMessage {
|
|
75
|
+
@field({ type: RpcResponseHeader })
|
|
76
|
+
header: RpcResponseHeader;
|
|
77
|
+
|
|
78
|
+
@field({ type: Uint8Array })
|
|
79
|
+
payload: Uint8Array;
|
|
80
|
+
|
|
81
|
+
constructor(properties: { header: RpcResponseHeader; payload: Uint8Array }) {
|
|
82
|
+
super();
|
|
83
|
+
this.header = properties.header;
|
|
84
|
+
this.payload = properties.payload;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@variant(2)
|
|
89
|
+
class RpcErr extends RpcMessage {
|
|
90
|
+
@field({ type: RpcResponseHeader })
|
|
91
|
+
header: RpcResponseHeader;
|
|
92
|
+
|
|
93
|
+
@field({ type: "string" })
|
|
94
|
+
message: string;
|
|
95
|
+
|
|
96
|
+
constructor(properties: { header: RpcResponseHeader; message: string }) {
|
|
97
|
+
super();
|
|
98
|
+
this.header = properties.header;
|
|
99
|
+
this.message = properties.message;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@variant(3)
|
|
104
|
+
class RpcStream extends RpcMessage {
|
|
105
|
+
@field({ type: RpcResponseHeader })
|
|
106
|
+
header: RpcResponseHeader;
|
|
107
|
+
|
|
108
|
+
@field({ type: Uint8Array })
|
|
109
|
+
payload: Uint8Array;
|
|
110
|
+
|
|
111
|
+
constructor(properties: { header: RpcResponseHeader; payload: Uint8Array }) {
|
|
112
|
+
super();
|
|
113
|
+
this.header = properties.header;
|
|
114
|
+
this.payload = properties.payload;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@variant(4)
|
|
119
|
+
class RpcStreamEnd extends RpcMessage {
|
|
120
|
+
@field({ type: RpcResponseHeader })
|
|
121
|
+
header: RpcResponseHeader;
|
|
122
|
+
|
|
123
|
+
constructor(properties: { header: RpcResponseHeader }) {
|
|
124
|
+
super();
|
|
125
|
+
this.header = properties.header;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@variant(5)
|
|
130
|
+
class RpcStreamErr extends RpcMessage {
|
|
131
|
+
@field({ type: RpcResponseHeader })
|
|
132
|
+
header: RpcResponseHeader;
|
|
133
|
+
|
|
134
|
+
@field({ type: "string" })
|
|
135
|
+
message: string;
|
|
136
|
+
|
|
137
|
+
constructor(properties: { header: RpcResponseHeader; message: string }) {
|
|
138
|
+
super();
|
|
139
|
+
this.header = properties.header;
|
|
140
|
+
this.message = properties.message;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
type RpcTransportLike = {
|
|
145
|
+
send: (data: Uint8Array) => void;
|
|
146
|
+
onMessage: (handler: (data: Uint8Array) => void) => () => void;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export type RpcRequestTimeoutMs =
|
|
150
|
+
| number
|
|
151
|
+
| ((method: string) => number | undefined);
|
|
152
|
+
|
|
153
|
+
export type CanonicalIdentity = Identity<PublicSignKey> & {
|
|
154
|
+
signer: (
|
|
155
|
+
prehash?: PreHash,
|
|
156
|
+
) => (data: Uint8Array) => Promise<SignatureWithKey>;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const isMessagePort = (
|
|
160
|
+
value: MessagePort | CanonicalChannel,
|
|
161
|
+
): value is MessagePort =>
|
|
162
|
+
typeof (value as MessagePort).postMessage === "function";
|
|
163
|
+
|
|
164
|
+
const toRpcMessage = (data: Uint8Array): RpcMessage | undefined => {
|
|
165
|
+
try {
|
|
166
|
+
return deserialize(data, RpcMessage) as RpcMessage;
|
|
167
|
+
} catch {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const createCloseAwareRpcTransport = (
|
|
173
|
+
base: RpcTransportLike,
|
|
174
|
+
options: {
|
|
175
|
+
onClose?: (handler: () => void) => () => void;
|
|
176
|
+
closeErrorMessage?: string;
|
|
177
|
+
requestTimeoutMs?: RpcRequestTimeoutMs;
|
|
178
|
+
ignoreTimeoutForMethod?: (method: string) => boolean;
|
|
179
|
+
},
|
|
180
|
+
): CanonicalRpcTransport => {
|
|
181
|
+
const closeErrorMessage = options.closeErrorMessage ?? "RPC transport closed";
|
|
182
|
+
const handlers = new Set<(data: Uint8Array) => void>();
|
|
183
|
+
const inFlight = new Map<
|
|
184
|
+
number,
|
|
185
|
+
{
|
|
186
|
+
method: string;
|
|
187
|
+
timeout?: ReturnType<typeof setTimeout>;
|
|
188
|
+
}
|
|
189
|
+
>();
|
|
190
|
+
let closed = false;
|
|
191
|
+
let offClose: (() => void) | undefined;
|
|
192
|
+
|
|
193
|
+
const ignoreTimeoutForMethod =
|
|
194
|
+
options.ignoreTimeoutForMethod ??
|
|
195
|
+
((method: string) =>
|
|
196
|
+
method.startsWith("$events:") ||
|
|
197
|
+
method.startsWith("$watch:") ||
|
|
198
|
+
method.startsWith("$presentWatch:"));
|
|
199
|
+
|
|
200
|
+
const resolveTimeoutMs = (method: string): number | undefined => {
|
|
201
|
+
if (ignoreTimeoutForMethod(method)) return undefined;
|
|
202
|
+
const configured = options.requestTimeoutMs;
|
|
203
|
+
if (configured == null) return undefined;
|
|
204
|
+
const value =
|
|
205
|
+
typeof configured === "function" ? configured(method) : configured;
|
|
206
|
+
if (value == null || value <= 0) return undefined;
|
|
207
|
+
return value;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const emit = (data: Uint8Array) => {
|
|
211
|
+
for (const handler of handlers) {
|
|
212
|
+
try {
|
|
213
|
+
handler(data);
|
|
214
|
+
} catch {}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const unsubscribeBase = base.onMessage((data) => {
|
|
219
|
+
const msg = toRpcMessage(data);
|
|
220
|
+
if (
|
|
221
|
+
msg instanceof RpcOk ||
|
|
222
|
+
msg instanceof RpcErr ||
|
|
223
|
+
msg instanceof RpcStream ||
|
|
224
|
+
msg instanceof RpcStreamEnd ||
|
|
225
|
+
msg instanceof RpcStreamErr
|
|
226
|
+
) {
|
|
227
|
+
const id = msg.header.id;
|
|
228
|
+
const entry = inFlight.get(id);
|
|
229
|
+
if (entry?.timeout) {
|
|
230
|
+
clearTimeout(entry.timeout);
|
|
231
|
+
entry.timeout = undefined;
|
|
232
|
+
}
|
|
233
|
+
if (
|
|
234
|
+
msg instanceof RpcOk ||
|
|
235
|
+
msg instanceof RpcErr ||
|
|
236
|
+
msg instanceof RpcStreamEnd ||
|
|
237
|
+
msg instanceof RpcStreamErr
|
|
238
|
+
) {
|
|
239
|
+
inFlight.delete(id);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
emit(data);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const close = (message: string = closeErrorMessage) => {
|
|
246
|
+
if (closed) return;
|
|
247
|
+
closed = true;
|
|
248
|
+
if (offClose) offClose();
|
|
249
|
+
unsubscribeBase();
|
|
250
|
+
for (const [id, entry] of inFlight) {
|
|
251
|
+
if (entry.timeout) clearTimeout(entry.timeout);
|
|
252
|
+
emit(
|
|
253
|
+
serialize(
|
|
254
|
+
new RpcErr({
|
|
255
|
+
header: new RpcResponseHeader({ id, method: entry.method }),
|
|
256
|
+
message,
|
|
257
|
+
}),
|
|
258
|
+
),
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
inFlight.clear();
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
if (options.onClose) {
|
|
265
|
+
offClose = options.onClose(() => close(closeErrorMessage));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
send: (data) => {
|
|
270
|
+
const msg = toRpcMessage(data);
|
|
271
|
+
if (closed) {
|
|
272
|
+
if (msg instanceof RpcRequest) {
|
|
273
|
+
emit(
|
|
274
|
+
serialize(
|
|
275
|
+
new RpcErr({
|
|
276
|
+
header: new RpcResponseHeader({
|
|
277
|
+
id: msg.header.id,
|
|
278
|
+
method: msg.header.method,
|
|
279
|
+
}),
|
|
280
|
+
message: closeErrorMessage,
|
|
281
|
+
}),
|
|
282
|
+
),
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (msg instanceof RpcRequest) {
|
|
288
|
+
const method = msg.header.method;
|
|
289
|
+
const timeoutMs = resolveTimeoutMs(method);
|
|
290
|
+
const id = msg.header.id;
|
|
291
|
+
const entry = {
|
|
292
|
+
method,
|
|
293
|
+
timeout: timeoutMs
|
|
294
|
+
? setTimeout(() => {
|
|
295
|
+
const current = inFlight.get(id);
|
|
296
|
+
if (!current) return;
|
|
297
|
+
inFlight.delete(id);
|
|
298
|
+
emit(
|
|
299
|
+
serialize(
|
|
300
|
+
new RpcErr({
|
|
301
|
+
header: new RpcResponseHeader({
|
|
302
|
+
id,
|
|
303
|
+
method: current.method,
|
|
304
|
+
}),
|
|
305
|
+
message: `RPC request timeout (${current.method})`,
|
|
306
|
+
}),
|
|
307
|
+
),
|
|
308
|
+
);
|
|
309
|
+
}, timeoutMs)
|
|
310
|
+
: undefined,
|
|
311
|
+
};
|
|
312
|
+
inFlight.set(id, entry);
|
|
313
|
+
}
|
|
314
|
+
base.send(data);
|
|
315
|
+
},
|
|
316
|
+
onMessage: (handler) => {
|
|
317
|
+
handlers.add(handler);
|
|
318
|
+
return () => {
|
|
319
|
+
handlers.delete(handler);
|
|
320
|
+
};
|
|
321
|
+
},
|
|
322
|
+
close,
|
|
323
|
+
} as CanonicalRpcTransport;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
export const createMessagePortTransport = (
|
|
327
|
+
port: MessagePort | CanonicalChannel,
|
|
328
|
+
options?: {
|
|
329
|
+
requestTimeoutMs?: RpcRequestTimeoutMs;
|
|
330
|
+
ignoreTimeoutForMethod?: (method: string) => boolean;
|
|
331
|
+
closeErrorMessage?: string;
|
|
332
|
+
},
|
|
333
|
+
): CanonicalRpcTransport => {
|
|
334
|
+
const base = createRpcTransportBase(port);
|
|
335
|
+
const shouldWrap =
|
|
336
|
+
typeof options?.requestTimeoutMs !== "undefined" ||
|
|
337
|
+
typeof options?.ignoreTimeoutForMethod === "function" ||
|
|
338
|
+
typeof options?.closeErrorMessage === "string";
|
|
339
|
+
if (isMessagePort(port)) {
|
|
340
|
+
if (!shouldWrap) return base;
|
|
341
|
+
return createCloseAwareRpcTransport(base, {
|
|
342
|
+
requestTimeoutMs: options?.requestTimeoutMs,
|
|
343
|
+
ignoreTimeoutForMethod: options?.ignoreTimeoutForMethod,
|
|
344
|
+
closeErrorMessage: options?.closeErrorMessage,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const channel = port as CanonicalChannel;
|
|
349
|
+
if (typeof channel.onClose !== "function") {
|
|
350
|
+
if (!shouldWrap) return base;
|
|
351
|
+
return createCloseAwareRpcTransport(base, {
|
|
352
|
+
requestTimeoutMs: options?.requestTimeoutMs,
|
|
353
|
+
ignoreTimeoutForMethod: options?.ignoreTimeoutForMethod,
|
|
354
|
+
closeErrorMessage: options?.closeErrorMessage,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return createCloseAwareRpcTransport(base, {
|
|
359
|
+
onClose: (handler) => channel.onClose?.(handler) ?? (() => {}),
|
|
360
|
+
requestTimeoutMs: options?.requestTimeoutMs,
|
|
361
|
+
ignoreTimeoutForMethod: options?.ignoreTimeoutForMethod,
|
|
362
|
+
closeErrorMessage: options?.closeErrorMessage,
|
|
363
|
+
});
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
type Pending<T> = {
|
|
367
|
+
resolve: (value: T) => void;
|
|
368
|
+
reject: (error: Error) => void;
|
|
369
|
+
timeout?: ReturnType<typeof setTimeout>;
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
export class CanonicalClient {
|
|
373
|
+
private nextId = 1;
|
|
374
|
+
private readonly pending = new Map<number, Pending<any>>();
|
|
375
|
+
private readonly connection: CanonicalConnection;
|
|
376
|
+
private readonly unsubscribe: () => void;
|
|
377
|
+
private keepAliveTimer?: ReturnType<typeof setInterval>;
|
|
378
|
+
private keepAliveInFlight?: Promise<void>;
|
|
379
|
+
private readonly requestTimeoutMs?: number;
|
|
380
|
+
private cachedPeerId?: string;
|
|
381
|
+
private cachedPublicKey?: PublicSignKey;
|
|
382
|
+
private cachedMultiaddrs?: string[];
|
|
383
|
+
private cachedIdentity?: CanonicalIdentity;
|
|
384
|
+
|
|
385
|
+
constructor(
|
|
386
|
+
control: MessagePort | CanonicalTransport,
|
|
387
|
+
options?: { requestTimeoutMs?: number },
|
|
388
|
+
) {
|
|
389
|
+
const transport =
|
|
390
|
+
typeof (control as MessagePort).postMessage === "function"
|
|
391
|
+
? createControlTransport(control as MessagePort)
|
|
392
|
+
: (control as CanonicalTransport);
|
|
393
|
+
this.connection = new CanonicalConnection(transport);
|
|
394
|
+
this.requestTimeoutMs = options?.requestTimeoutMs;
|
|
395
|
+
this.unsubscribe = this.connection.onControl((frame) => {
|
|
396
|
+
if (frame instanceof CanonicalControlResponse) {
|
|
397
|
+
this.onMessage(frame);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
static async create(
|
|
403
|
+
control: MessagePort | CanonicalTransport,
|
|
404
|
+
options?: { requestTimeoutMs?: number },
|
|
405
|
+
): Promise<CanonicalClient> {
|
|
406
|
+
const client = new CanonicalClient(control, options);
|
|
407
|
+
await client.init();
|
|
408
|
+
return client;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async init(): Promise<void> {
|
|
412
|
+
if (this.cachedPublicKey) return;
|
|
413
|
+
await this.peerInfo();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
close(): void {
|
|
417
|
+
if (this.keepAliveTimer) {
|
|
418
|
+
clearInterval(this.keepAliveTimer);
|
|
419
|
+
this.keepAliveTimer = undefined;
|
|
420
|
+
}
|
|
421
|
+
this.keepAliveInFlight = undefined;
|
|
422
|
+
this.unsubscribe();
|
|
423
|
+
this.connection.close();
|
|
424
|
+
for (const [id, p] of this.pending) {
|
|
425
|
+
if (p.timeout) clearTimeout(p.timeout);
|
|
426
|
+
p.reject(new Error("CanonicalClient closed"));
|
|
427
|
+
this.pending.delete(id);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async peerId(): Promise<string> {
|
|
432
|
+
if (this.cachedPeerId) return this.cachedPeerId;
|
|
433
|
+
const resp = await this.request({ op: "peerId" });
|
|
434
|
+
if (!resp.peerId) {
|
|
435
|
+
throw new Error("Canonical peerId response missing peerId");
|
|
436
|
+
}
|
|
437
|
+
this.cachedPeerId = resp.peerId;
|
|
438
|
+
return resp.peerId;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async peerInfo(): Promise<{
|
|
442
|
+
peerId: string;
|
|
443
|
+
publicKey: PublicSignKey;
|
|
444
|
+
multiaddrs: string[];
|
|
445
|
+
}> {
|
|
446
|
+
const resp = await this.request({ op: "peerInfo" });
|
|
447
|
+
if (!resp.peerId) {
|
|
448
|
+
throw new Error("Canonical peerInfo response missing peerId");
|
|
449
|
+
}
|
|
450
|
+
if (!resp.payload) {
|
|
451
|
+
throw new Error("Canonical peerInfo response missing payload");
|
|
452
|
+
}
|
|
453
|
+
const publicKey = deserialize(resp.payload, PublicSignKey) as PublicSignKey;
|
|
454
|
+
const multiaddrs = resp.strings ?? [];
|
|
455
|
+
this.cachedPeerId = resp.peerId;
|
|
456
|
+
this.cachedPublicKey = publicKey;
|
|
457
|
+
this.cachedMultiaddrs = multiaddrs;
|
|
458
|
+
this.cachedIdentity = undefined;
|
|
459
|
+
return { peerId: resp.peerId, publicKey, multiaddrs };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async multiaddrs(): Promise<string[]> {
|
|
463
|
+
if (this.cachedMultiaddrs) return this.cachedMultiaddrs;
|
|
464
|
+
const info = await this.peerInfo();
|
|
465
|
+
return info.multiaddrs;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
getMultiaddrs(): Promise<string[]> {
|
|
469
|
+
return this.multiaddrs();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async dial(address: string | { toString(): string }): Promise<boolean> {
|
|
473
|
+
const value = typeof address === "string" ? address : address.toString();
|
|
474
|
+
await this.request({ op: "dial", name: value });
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async hangUp(address: string | { toString(): string }): Promise<void> {
|
|
479
|
+
const value = typeof address === "string" ? address : address.toString();
|
|
480
|
+
await this.request({ op: "hangUp", name: value });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async start(): Promise<void> {
|
|
484
|
+
await this.request({ op: "start" });
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async stop(): Promise<void> {
|
|
488
|
+
await this.request({ op: "stop" });
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async bootstrap(addresses?: string[]): Promise<void> {
|
|
492
|
+
const payload =
|
|
493
|
+
addresses && addresses.length > 0
|
|
494
|
+
? serialize(new CanonicalBootstrapRequest({ addresses }))
|
|
495
|
+
: undefined;
|
|
496
|
+
await this.request({ op: "bootstrap", payload });
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async loadProgram(
|
|
500
|
+
address: string,
|
|
501
|
+
options?: { timeoutMs?: number },
|
|
502
|
+
): Promise<Uint8Array> {
|
|
503
|
+
if (!address) {
|
|
504
|
+
throw new Error("Canonical loadProgram requires address to be set");
|
|
505
|
+
}
|
|
506
|
+
const payload =
|
|
507
|
+
options?.timeoutMs != null
|
|
508
|
+
? serialize(
|
|
509
|
+
new CanonicalLoadProgramRequest({ timeoutMs: options.timeoutMs }),
|
|
510
|
+
)
|
|
511
|
+
: undefined;
|
|
512
|
+
const resp = await this.request({
|
|
513
|
+
op: "loadProgram",
|
|
514
|
+
name: address,
|
|
515
|
+
payload,
|
|
516
|
+
});
|
|
517
|
+
if (!resp.payload) {
|
|
518
|
+
throw new Error("Canonical loadProgram response missing payload");
|
|
519
|
+
}
|
|
520
|
+
return resp.payload;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async sign(
|
|
524
|
+
data: Uint8Array,
|
|
525
|
+
prehash: PreHash = PreHash.NONE,
|
|
526
|
+
): Promise<SignatureWithKey> {
|
|
527
|
+
const resp = await this.request({
|
|
528
|
+
op: "sign",
|
|
529
|
+
payload: serialize(
|
|
530
|
+
new CanonicalSignRequest({
|
|
531
|
+
data,
|
|
532
|
+
prehash,
|
|
533
|
+
}),
|
|
534
|
+
),
|
|
535
|
+
});
|
|
536
|
+
if (!resp.payload) {
|
|
537
|
+
throw new Error("Canonical sign response missing payload");
|
|
538
|
+
}
|
|
539
|
+
return deserialize(resp.payload, SignatureWithKey) as SignatureWithKey;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
get identity(): CanonicalIdentity {
|
|
543
|
+
if (!this.cachedPublicKey) {
|
|
544
|
+
throw new Error(
|
|
545
|
+
"CanonicalClient not initialized (missing publicKey); call await CanonicalClient.create(...) or await client.init()",
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
if (this.cachedIdentity) return this.cachedIdentity;
|
|
549
|
+
const publicKey = this.cachedPublicKey;
|
|
550
|
+
this.cachedIdentity = {
|
|
551
|
+
publicKey,
|
|
552
|
+
sign: (data, prehash) => this.sign(data, prehash ?? PreHash.NONE),
|
|
553
|
+
signer:
|
|
554
|
+
(prehash = PreHash.NONE) =>
|
|
555
|
+
(data) =>
|
|
556
|
+
this.sign(data, prehash),
|
|
557
|
+
};
|
|
558
|
+
return this.cachedIdentity;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async openPort(name: string, payload: Uint8Array): Promise<CanonicalChannel> {
|
|
562
|
+
const resp = await this.request({
|
|
563
|
+
op: "open",
|
|
564
|
+
name,
|
|
565
|
+
payload,
|
|
566
|
+
});
|
|
567
|
+
if (resp.channelId == null) {
|
|
568
|
+
throw new Error("Canonical open response missing channelId");
|
|
569
|
+
}
|
|
570
|
+
return this.connection.createChannel(resp.channelId);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async ping(): Promise<void> {
|
|
574
|
+
await this.request({ op: "ping" });
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
startKeepAlive(options?: {
|
|
578
|
+
intervalMs?: number;
|
|
579
|
+
timeoutMs?: number;
|
|
580
|
+
closeOnFail?: boolean;
|
|
581
|
+
maxFailures?: number;
|
|
582
|
+
}): () => void {
|
|
583
|
+
if (this.keepAliveTimer) {
|
|
584
|
+
clearInterval(this.keepAliveTimer);
|
|
585
|
+
}
|
|
586
|
+
const interval = Math.max(1000, options?.intervalMs ?? 30_000);
|
|
587
|
+
const timeoutMs = Math.max(
|
|
588
|
+
1000,
|
|
589
|
+
options?.timeoutMs ?? this.requestTimeoutMs ?? 10_000,
|
|
590
|
+
);
|
|
591
|
+
const closeOnFail = options?.closeOnFail ?? true;
|
|
592
|
+
const maxFailures = Math.max(1, options?.maxFailures ?? 2);
|
|
593
|
+
let failures = 0;
|
|
594
|
+
this.keepAliveTimer = setInterval(() => {
|
|
595
|
+
if (this.keepAliveInFlight) return;
|
|
596
|
+
this.keepAliveInFlight = this.request({ op: "ping" }, { timeoutMs })
|
|
597
|
+
.then(() => {
|
|
598
|
+
failures = 0;
|
|
599
|
+
})
|
|
600
|
+
.catch(() => {
|
|
601
|
+
failures += 1;
|
|
602
|
+
if (closeOnFail && failures >= maxFailures) {
|
|
603
|
+
this.close();
|
|
604
|
+
}
|
|
605
|
+
})
|
|
606
|
+
.finally(() => {
|
|
607
|
+
this.keepAliveInFlight = undefined;
|
|
608
|
+
});
|
|
609
|
+
}, interval);
|
|
610
|
+
return () => {
|
|
611
|
+
if (this.keepAliveTimer) {
|
|
612
|
+
clearInterval(this.keepAliveTimer);
|
|
613
|
+
this.keepAliveTimer = undefined;
|
|
614
|
+
}
|
|
615
|
+
this.keepAliveInFlight = undefined;
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
private onMessage(message: CanonicalControlResponse) {
|
|
620
|
+
const entry = this.pending.get(message.id);
|
|
621
|
+
if (!entry) return;
|
|
622
|
+
this.pending.delete(message.id);
|
|
623
|
+
if (entry.timeout) clearTimeout(entry.timeout);
|
|
624
|
+
|
|
625
|
+
if (message.ok === false) {
|
|
626
|
+
entry.reject(new Error(message.error ?? "Unknown error"));
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
entry.resolve(message);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
private request(
|
|
634
|
+
message:
|
|
635
|
+
| {
|
|
636
|
+
op: "peerId";
|
|
637
|
+
}
|
|
638
|
+
| {
|
|
639
|
+
op: "peerInfo";
|
|
640
|
+
}
|
|
641
|
+
| {
|
|
642
|
+
op: "dial";
|
|
643
|
+
name: string;
|
|
644
|
+
}
|
|
645
|
+
| {
|
|
646
|
+
op: "hangUp";
|
|
647
|
+
name: string;
|
|
648
|
+
}
|
|
649
|
+
| {
|
|
650
|
+
op: "start";
|
|
651
|
+
}
|
|
652
|
+
| {
|
|
653
|
+
op: "stop";
|
|
654
|
+
}
|
|
655
|
+
| {
|
|
656
|
+
op: "bootstrap";
|
|
657
|
+
payload?: Uint8Array;
|
|
658
|
+
}
|
|
659
|
+
| {
|
|
660
|
+
op: "loadProgram";
|
|
661
|
+
name: string;
|
|
662
|
+
payload?: Uint8Array;
|
|
663
|
+
}
|
|
664
|
+
| {
|
|
665
|
+
op: "sign";
|
|
666
|
+
payload: Uint8Array;
|
|
667
|
+
}
|
|
668
|
+
| {
|
|
669
|
+
op: "ping";
|
|
670
|
+
}
|
|
671
|
+
| {
|
|
672
|
+
op: "open";
|
|
673
|
+
name: string;
|
|
674
|
+
payload: Uint8Array;
|
|
675
|
+
},
|
|
676
|
+
options?: { timeoutMs?: number },
|
|
677
|
+
): Promise<CanonicalControlResponse> {
|
|
678
|
+
const id = this.nextId++;
|
|
679
|
+
const timeoutMs = options?.timeoutMs ?? this.requestTimeoutMs;
|
|
680
|
+
return new Promise((resolve, reject) => {
|
|
681
|
+
const entry: Pending<CanonicalControlResponse> = { resolve, reject };
|
|
682
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
683
|
+
entry.timeout = setTimeout(() => {
|
|
684
|
+
if (!this.pending.has(id)) return;
|
|
685
|
+
this.pending.delete(id);
|
|
686
|
+
reject(new Error(`Canonical request timeout (${message.op})`));
|
|
687
|
+
}, timeoutMs);
|
|
688
|
+
}
|
|
689
|
+
this.pending.set(id, entry);
|
|
690
|
+
try {
|
|
691
|
+
this.connection.sendControl(
|
|
692
|
+
new CanonicalControlRequest({
|
|
693
|
+
id,
|
|
694
|
+
op: message.op,
|
|
695
|
+
...("name" in message ? { name: message.name } : {}),
|
|
696
|
+
...("payload" in message ? { payload: message.payload } : {}),
|
|
697
|
+
}),
|
|
698
|
+
);
|
|
699
|
+
} catch (e: any) {
|
|
700
|
+
if (entry.timeout) clearTimeout(entry.timeout);
|
|
701
|
+
this.pending.delete(id);
|
|
702
|
+
reject(e);
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
export const connectSharedWorker = async (
|
|
709
|
+
worker: SharedWorker,
|
|
710
|
+
options?: { requestTimeoutMs?: number },
|
|
711
|
+
): Promise<CanonicalClient> => {
|
|
712
|
+
return CanonicalClient.create(worker.port, options);
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
export const connectSharedWorkerPeerbit = async (
|
|
716
|
+
worker: SharedWorker,
|
|
717
|
+
options?: { adapters?: CanonicalOpenAdapter[]; mode?: CanonicalOpenMode },
|
|
718
|
+
): Promise<PeerbitCanonicalClient> => {
|
|
719
|
+
const canonical = await connectSharedWorker(worker);
|
|
720
|
+
const { PeerbitCanonicalClient } = await import("./peerbit.js");
|
|
721
|
+
return PeerbitCanonicalClient.create(canonical, options);
|
|
722
|
+
};
|