@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/src/peerbit.ts ADDED
@@ -0,0 +1,354 @@
1
+ import { deserialize } from "@dao-xyz/borsh";
2
+ import type { PeerId } from "@libp2p/interface";
3
+ import { peerIdFromString } from "@libp2p/peer-id";
4
+ import {
5
+ type Multiaddr,
6
+ isMultiaddr,
7
+ multiaddr,
8
+ } from "@multiformats/multiaddr";
9
+ import type {
10
+ Identity,
11
+ PreHash,
12
+ PublicSignKey,
13
+ SignatureWithKey,
14
+ } from "@peerbit/crypto";
15
+ import { Program } from "@peerbit/program";
16
+ import type { Address, OpenOptions, ProgramClient } from "@peerbit/program";
17
+ import {
18
+ type CanonicalOpenAdapter,
19
+ type CanonicalOpenMode,
20
+ createManagedProxy,
21
+ } from "./auto.js";
22
+ import type { CanonicalChannel } from "./client.js";
23
+ import type { CanonicalClient } from "./index.js";
24
+
25
+ type IdentityProxy = Identity<PublicSignKey> & {
26
+ signer: (
27
+ prehash?: PreHash,
28
+ ) => (data: Uint8Array) => Promise<SignatureWithKey>;
29
+ };
30
+
31
+ export type PeerbitCanonicalPeerInfo = {
32
+ peerId: PeerId;
33
+ identity: IdentityProxy;
34
+ multiaddrs: Multiaddr[];
35
+ };
36
+
37
+ const asMultiaddrs = (value: unknown): Multiaddr[] | undefined => {
38
+ if (Array.isArray(value) && value.every((x) => isMultiaddr(x))) {
39
+ return value as Multiaddr[];
40
+ }
41
+ return undefined;
42
+ };
43
+
44
+ export class PeerbitCanonicalClient {
45
+ private _peerId: PeerId;
46
+ private _identity: IdentityProxy;
47
+ private _multiaddrs: Multiaddr[];
48
+ private openState?: {
49
+ adapters: CanonicalOpenAdapter[];
50
+ mode: CanonicalOpenMode;
51
+ adapterCaches: Map<CanonicalOpenAdapter, Map<string, Promise<any>>>;
52
+ proxies: Set<any>;
53
+ };
54
+
55
+ private constructor(
56
+ readonly canonical: CanonicalClient,
57
+ info: PeerbitCanonicalPeerInfo,
58
+ ) {
59
+ this._peerId = info.peerId;
60
+ this._identity = info.identity;
61
+ this._multiaddrs = info.multiaddrs;
62
+ }
63
+
64
+ static async create(
65
+ canonical: CanonicalClient,
66
+ options?: { adapters?: CanonicalOpenAdapter[]; mode?: CanonicalOpenMode },
67
+ ): Promise<PeerbitCanonicalClient> {
68
+ await canonical.init();
69
+ const peer = new PeerbitCanonicalClient(canonical, {
70
+ peerId: peerIdFromString(await canonical.peerId()),
71
+ identity: canonical.identity,
72
+ multiaddrs: (await canonical.multiaddrs()).map((a) => multiaddr(a)),
73
+ });
74
+ if (options?.adapters?.length) {
75
+ peer.enableOpen({ adapters: options.adapters, mode: options.mode });
76
+ }
77
+ return peer;
78
+ }
79
+
80
+ get peerId(): PeerId {
81
+ return this._peerId;
82
+ }
83
+
84
+ get identity(): IdentityProxy {
85
+ return this._identity;
86
+ }
87
+
88
+ getMultiaddrs(): Multiaddr[] {
89
+ return this._multiaddrs.slice();
90
+ }
91
+
92
+ private static attachParent(
93
+ child: { parents?: any[]; children?: any[] },
94
+ parent?: any,
95
+ ) {
96
+ if (child.parents && child.parents.includes(parent) && parent == null) {
97
+ return;
98
+ }
99
+ (child.parents || (child.parents = [])).push(parent);
100
+ if (parent) {
101
+ (parent.children || (parent.children = [])).push(child);
102
+ }
103
+ }
104
+
105
+ private async closeOpenProxies(): Promise<void> {
106
+ const state = this.openState;
107
+ if (!state || state.proxies.size === 0) return;
108
+ const targets = [...state.proxies];
109
+ await Promise.allSettled(
110
+ targets.map((proxy) =>
111
+ typeof proxy?.close === "function" ? proxy.close() : undefined,
112
+ ),
113
+ );
114
+ state.proxies.clear();
115
+ for (const cache of state.adapterCaches.values()) {
116
+ cache.clear();
117
+ }
118
+ }
119
+
120
+ enableOpen(options: {
121
+ adapters: CanonicalOpenAdapter[];
122
+ mode?: CanonicalOpenMode;
123
+ }): void {
124
+ if (!this.openState) {
125
+ this.openState = {
126
+ adapters: [...options.adapters],
127
+ mode: options.mode ?? "canonical",
128
+ adapterCaches: new Map(),
129
+ proxies: new Set(),
130
+ };
131
+ return;
132
+ }
133
+ this.openState.adapters.push(...options.adapters);
134
+ this.openState.mode = options.mode ?? this.openState.mode;
135
+ }
136
+
137
+ /**
138
+ * Refreshes peerId/publicKey/multiaddrs from the canonical host.
139
+ */
140
+ async refreshPeerInfo(): Promise<PeerbitCanonicalPeerInfo> {
141
+ const info = await this.canonical.peerInfo();
142
+ this._peerId = peerIdFromString(info.peerId);
143
+ this._identity = this.canonical.identity;
144
+ this._multiaddrs = (info.multiaddrs ?? []).map((a) => multiaddr(a));
145
+ return {
146
+ peerId: this._peerId,
147
+ identity: this._identity,
148
+ multiaddrs: this._multiaddrs.slice(),
149
+ };
150
+ }
151
+
152
+ async dial(
153
+ address: string | Multiaddr | Multiaddr[] | ProgramClient,
154
+ ): Promise<boolean> {
155
+ if (typeof address === "string") {
156
+ return this.canonical.dial(address);
157
+ }
158
+ if (isMultiaddr(address)) {
159
+ return this.canonical.dial(address.toString());
160
+ }
161
+ const many = asMultiaddrs(address);
162
+ if (many) {
163
+ if (many.length === 0) {
164
+ throw new Error("Canonical dial requires at least one multiaddr");
165
+ }
166
+ let lastError: unknown;
167
+ for (const addr of many) {
168
+ try {
169
+ await this.canonical.dial(addr.toString());
170
+ return true;
171
+ } catch (e) {
172
+ lastError = e;
173
+ }
174
+ }
175
+ throw lastError ?? new Error("Canonical dial failed");
176
+ }
177
+
178
+ const candidate = address as any;
179
+ if (candidate && typeof candidate.getMultiaddrs === "function") {
180
+ const addrs = await Promise.resolve(candidate.getMultiaddrs());
181
+ return this.dial(addrs as any);
182
+ }
183
+
184
+ if (candidate && typeof candidate.toString === "function") {
185
+ return this.canonical.dial(candidate.toString());
186
+ }
187
+
188
+ throw new Error("Unsupported dial address");
189
+ }
190
+
191
+ async hangUp(
192
+ address: PeerId | PublicSignKey | string | Multiaddr,
193
+ ): Promise<void> {
194
+ if (typeof address === "string") {
195
+ await this.canonical.hangUp(address);
196
+ return;
197
+ }
198
+ if (isMultiaddr(address)) {
199
+ await this.canonical.hangUp(address.toString());
200
+ return;
201
+ }
202
+
203
+ const candidate = address as any;
204
+ if (candidate && typeof candidate.toPeerId === "function") {
205
+ const peerId = await Promise.resolve(candidate.toPeerId());
206
+ await this.canonical.hangUp(peerId.toString());
207
+ return;
208
+ }
209
+
210
+ if (candidate && typeof candidate.toString === "function") {
211
+ await this.canonical.hangUp(candidate.toString());
212
+ return;
213
+ }
214
+
215
+ throw new Error("Unsupported hangUp address");
216
+ }
217
+
218
+ async start(): Promise<void> {
219
+ await this.canonical.start();
220
+ await this.refreshPeerInfo();
221
+ }
222
+
223
+ async stop(): Promise<void> {
224
+ await this.closeOpenProxies();
225
+ await this.canonical.stop();
226
+ }
227
+
228
+ async bootstrap(addresses?: string[] | Multiaddr[]): Promise<void> {
229
+ if (!addresses) {
230
+ await this.canonical.bootstrap();
231
+ return;
232
+ }
233
+ const normalized = addresses.map((addr) =>
234
+ typeof addr === "string" ? addr : addr.toString(),
235
+ );
236
+ await this.canonical.bootstrap(normalized);
237
+ }
238
+
239
+ openPort(name: string, payload: Uint8Array): Promise<CanonicalChannel> {
240
+ return this.canonical.openPort(name, payload);
241
+ }
242
+
243
+ async open<S extends Program<any>>(
244
+ storeOrAddress: S | Address,
245
+ openOptions: OpenOptions<S> = {},
246
+ ): Promise<S> {
247
+ const state = this.openState;
248
+ if (!state || state.adapters.length === 0) {
249
+ throw new Error(
250
+ "Canonical open is not enabled. Pass adapters to PeerbitCanonicalClient.create(..., { adapters }) or call peer.enableOpen({ adapters }).",
251
+ );
252
+ }
253
+
254
+ const mode = openOptions?.mode ?? state.mode;
255
+ if (mode === "local") {
256
+ throw new Error(
257
+ "PeerbitCanonicalClient has no local Peerbit; use mode:'canonical' or omit mode.",
258
+ );
259
+ }
260
+
261
+ if (typeof storeOrAddress === "string") {
262
+ const address = storeOrAddress;
263
+ const bytes = await this.canonical.loadProgram(address, {
264
+ timeoutMs: openOptions.timeout,
265
+ });
266
+ const loaded = deserialize(bytes, Program) as Program<any>;
267
+ loaded.address = address;
268
+ return this.open(loaded as any, openOptions as any) as Promise<S>;
269
+ }
270
+
271
+ const program = storeOrAddress as Program<any>;
272
+ const adapter = state.adapters.find((candidate) =>
273
+ candidate.canOpen(program),
274
+ );
275
+ if (!adapter) {
276
+ throw new Error(
277
+ `No canonical adapter registered for ${program.constructor?.name ?? "program"}`,
278
+ );
279
+ }
280
+
281
+ const key = adapter.getKey?.(program as any, openOptions);
282
+ if (adapter.getKey && key === undefined) {
283
+ throw new Error(
284
+ `Canonical adapter '${adapter.name}' requires a cache key (adapter.getKey returned undefined)`,
285
+ );
286
+ }
287
+ const existingMode = openOptions?.existing ?? "reject";
288
+ let cache = key ? state.adapterCaches.get(adapter) : undefined;
289
+ if (key && !cache) {
290
+ cache = new Map();
291
+ state.adapterCaches.set(adapter, cache);
292
+ }
293
+
294
+ if (key && cache?.has(key)) {
295
+ if (existingMode === "reject") {
296
+ throw new Error(`Program already open for adapter '${adapter.name}'`);
297
+ }
298
+ if (existingMode === "replace") {
299
+ const prev = await cache.get(key);
300
+ if (prev && typeof (prev as any).close === "function") {
301
+ await (prev as any).close();
302
+ }
303
+ cache.delete(key);
304
+ } else {
305
+ const existingProxy = await cache.get(key);
306
+ if (openOptions?.parent) {
307
+ PeerbitCanonicalClient.attachParent(
308
+ existingProxy as any,
309
+ openOptions.parent,
310
+ );
311
+ }
312
+ return existingProxy as S;
313
+ }
314
+ }
315
+
316
+ const peer = this as any as ProgramClient;
317
+ const openPromise = (async () => {
318
+ const result = await adapter.open({
319
+ program: program as any,
320
+ options: openOptions,
321
+ peer,
322
+ client: this.canonical,
323
+ });
324
+
325
+ let managed: any;
326
+ managed = createManagedProxy(result.proxy as any, {
327
+ address: result.address,
328
+ node: peer,
329
+ onClose: () => {
330
+ if (key && cache?.get(key) === openPromise) {
331
+ cache.delete(key);
332
+ }
333
+ this.openState?.proxies.delete(managed);
334
+ },
335
+ });
336
+
337
+ this.openState?.proxies.add(managed);
338
+ if (openOptions?.parent) {
339
+ PeerbitCanonicalClient.attachParent(managed, openOptions.parent);
340
+ }
341
+ return managed as S;
342
+ })();
343
+
344
+ if (key && cache) {
345
+ cache.set(key, openPromise);
346
+ }
347
+ return openPromise as Promise<S>;
348
+ }
349
+
350
+ close(): void {
351
+ void this.closeOpenProxies();
352
+ this.canonical.close();
353
+ }
354
+ }
@@ -0,0 +1,118 @@
1
+ import type { CanonicalOpenAdapter, CanonicalOpenMode } from "./auto.js";
2
+ import { CanonicalClient } from "./client.js";
3
+ import { PeerbitCanonicalClient } from "./peerbit.js";
4
+
5
+ export const CANONICAL_SERVICE_WORKER_CONNECT_OP =
6
+ "__peerbit_canonical_connect__";
7
+ export const CANONICAL_SERVICE_WORKER_READY_KEY = "__peerbit_canonical_ready__";
8
+
9
+ export type ConnectServiceWorkerOptions = {
10
+ serviceWorker?: ServiceWorker;
11
+ registration?: ServiceWorkerRegistration;
12
+ url?: string | URL;
13
+ scope?: string;
14
+ type?: RegistrationOptions["type"];
15
+ timeoutMs?: number;
16
+ };
17
+
18
+ const waitForActivated = async (
19
+ registration: ServiceWorkerRegistration,
20
+ timeoutMs: number,
21
+ ): Promise<ServiceWorker> => {
22
+ const candidate =
23
+ registration.active ?? registration.installing ?? registration.waiting;
24
+ if (!candidate) {
25
+ throw new Error("Service worker not installing");
26
+ }
27
+ if (candidate.state === "activated") {
28
+ return candidate;
29
+ }
30
+
31
+ return new Promise<ServiceWorker>((resolve, reject) => {
32
+ let timeout: ReturnType<typeof setTimeout> | undefined;
33
+ const onState = () => {
34
+ if (candidate.state !== "activated") return;
35
+ if (timeout) clearTimeout(timeout);
36
+ candidate.removeEventListener("statechange", onState);
37
+ resolve(candidate);
38
+ };
39
+ timeout = setTimeout(() => {
40
+ candidate.removeEventListener("statechange", onState);
41
+ reject(new Error("Service worker activation timeout"));
42
+ }, timeoutMs);
43
+ candidate.addEventListener("statechange", onState);
44
+ });
45
+ };
46
+
47
+ const waitForReady = async (
48
+ port: MessagePort,
49
+ timeoutMs: number,
50
+ ): Promise<void> => {
51
+ port.start();
52
+ return new Promise<void>((resolve, reject) => {
53
+ let timeout: ReturnType<typeof setTimeout> | undefined;
54
+ const cleanup = () => {
55
+ if (timeout) clearTimeout(timeout);
56
+ port.removeEventListener("message", onMessage);
57
+ };
58
+ const onMessage = (event: MessageEvent) => {
59
+ const data = event.data as any;
60
+ if (data?.[CANONICAL_SERVICE_WORKER_READY_KEY] == null) return;
61
+ cleanup();
62
+ if (data[CANONICAL_SERVICE_WORKER_READY_KEY] === false) {
63
+ reject(new Error(data.error ?? "Service worker canonical host failed"));
64
+ } else {
65
+ resolve();
66
+ }
67
+ };
68
+ timeout = setTimeout(() => {
69
+ cleanup();
70
+ reject(new Error("Service worker canonical connect timeout"));
71
+ }, timeoutMs);
72
+ port.addEventListener("message", onMessage);
73
+ });
74
+ };
75
+
76
+ export const connectServiceWorker = async (
77
+ options: ConnectServiceWorkerOptions,
78
+ ): Promise<CanonicalClient> => {
79
+ if (!("serviceWorker" in navigator)) {
80
+ throw new Error("Service workers not supported");
81
+ }
82
+
83
+ const timeoutMs = options.timeoutMs ?? 10_000;
84
+ let target = options.serviceWorker;
85
+
86
+ if (!target) {
87
+ const registration =
88
+ options.registration ??
89
+ (options.url
90
+ ? await navigator.serviceWorker.register(options.url, {
91
+ scope: options.scope,
92
+ type: options.type ?? "module",
93
+ })
94
+ : undefined);
95
+ if (!registration) {
96
+ throw new Error(
97
+ "connectServiceWorker requires url, registration, or serviceWorker",
98
+ );
99
+ }
100
+ target = await waitForActivated(registration, timeoutMs);
101
+ }
102
+
103
+ const channel = new MessageChannel();
104
+ const ready = waitForReady(channel.port1, timeoutMs);
105
+ target.postMessage({ op: CANONICAL_SERVICE_WORKER_CONNECT_OP }, [
106
+ channel.port2,
107
+ ]);
108
+ await ready;
109
+ return CanonicalClient.create(channel.port1);
110
+ };
111
+
112
+ export const connectServiceWorkerPeerbit = async (
113
+ options: ConnectServiceWorkerOptions,
114
+ peerOptions?: { adapters?: CanonicalOpenAdapter[]; mode?: CanonicalOpenMode },
115
+ ): Promise<PeerbitCanonicalClient> => {
116
+ const canonical = await connectServiceWorker(options);
117
+ return PeerbitCanonicalClient.create(canonical, peerOptions);
118
+ };
package/src/window.ts ADDED
@@ -0,0 +1,112 @@
1
+ import { createWindowTransport } from "@peerbit/canonical-transport";
2
+ import type { CanonicalOpenAdapter, CanonicalOpenMode } from "./auto.js";
3
+ import { CanonicalClient } from "./client.js";
4
+ import { PeerbitCanonicalClient } from "./peerbit.js";
5
+
6
+ export const CANONICAL_WINDOW_CONNECT_OP =
7
+ "__peerbit_canonical_window_connect__";
8
+ export const CANONICAL_WINDOW_READY_KEY = "__peerbit_canonical_window_ready__";
9
+
10
+ export type ConnectWindowOptions = {
11
+ target?: Window;
12
+ channel?: string;
13
+ targetOrigin?: string;
14
+ timeoutMs?: number;
15
+ };
16
+
17
+ const randomId = (): string => {
18
+ if (
19
+ typeof crypto !== "undefined" &&
20
+ typeof crypto.randomUUID === "function"
21
+ ) {
22
+ return crypto.randomUUID();
23
+ }
24
+ return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
25
+ };
26
+
27
+ const waitForReady = async (options: {
28
+ target: Window;
29
+ channel: string;
30
+ requestId: string;
31
+ timeoutMs: number;
32
+ expectedOrigin?: string;
33
+ }): Promise<void> => {
34
+ return new Promise<void>((resolve, reject) => {
35
+ let timeout: ReturnType<typeof setTimeout> | undefined;
36
+ const cleanup = () => {
37
+ if (timeout) clearTimeout(timeout);
38
+ globalThis.removeEventListener("message", onMessage);
39
+ };
40
+ const onMessage = (event: MessageEvent) => {
41
+ if (event.source !== options.target) return;
42
+ if (options.expectedOrigin && event.origin !== options.expectedOrigin)
43
+ return;
44
+ const data = event.data as any;
45
+ if (data?.[CANONICAL_WINDOW_READY_KEY] == null) return;
46
+ if (data.channel && String(data.channel) !== options.channel) return;
47
+ if (data.requestId && String(data.requestId) !== options.requestId)
48
+ return;
49
+ cleanup();
50
+ if (data[CANONICAL_WINDOW_READY_KEY] === false) {
51
+ reject(new Error(data.error ?? "Window canonical host failed"));
52
+ return;
53
+ }
54
+ resolve();
55
+ };
56
+ timeout = setTimeout(() => {
57
+ cleanup();
58
+ reject(new Error("Window canonical connect timeout"));
59
+ }, options.timeoutMs);
60
+ globalThis.addEventListener("message", onMessage);
61
+ });
62
+ };
63
+
64
+ export const connectWindow = async (
65
+ options: ConnectWindowOptions = {},
66
+ ): Promise<CanonicalClient> => {
67
+ const parent = window.parent;
68
+ const opener = window.opener;
69
+ const inferredTarget = parent && parent !== window ? parent : opener;
70
+ const target = options.target ?? inferredTarget;
71
+ if (!target || target === window) {
72
+ throw new Error(
73
+ "connectWindow requires a parent/opener window or options.target",
74
+ );
75
+ }
76
+
77
+ const channel = options.channel ?? "peerbit-canonical";
78
+ const targetOrigin = options.targetOrigin ?? "*";
79
+ const timeoutMs = options.timeoutMs ?? 10_000;
80
+ const requestId = randomId();
81
+
82
+ const expectedOrigin = targetOrigin !== "*" ? targetOrigin : undefined;
83
+ const ready = waitForReady({
84
+ target,
85
+ channel,
86
+ requestId,
87
+ timeoutMs,
88
+ expectedOrigin,
89
+ });
90
+
91
+ target.postMessage(
92
+ { op: CANONICAL_WINDOW_CONNECT_OP, channel, requestId },
93
+ targetOrigin,
94
+ );
95
+
96
+ await ready;
97
+
98
+ const transport = createWindowTransport(target, {
99
+ channel,
100
+ source: target,
101
+ targetOrigin,
102
+ });
103
+ return CanonicalClient.create(transport);
104
+ };
105
+
106
+ export const connectWindowPeerbit = async (
107
+ options: ConnectWindowOptions = {},
108
+ peerOptions?: { adapters?: CanonicalOpenAdapter[]; mode?: CanonicalOpenMode },
109
+ ): Promise<PeerbitCanonicalClient> => {
110
+ const canonical = await connectWindow(options);
111
+ return PeerbitCanonicalClient.create(canonical, peerOptions);
112
+ };