@peerbit/react 0.0.48 → 0.0.49-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/usePeer.tsx CHANGED
@@ -1,22 +1,22 @@
1
- import { noise } from "@chainsafe/libp2p-noise";
2
- import { yamux } from "@chainsafe/libp2p-yamux";
3
- import { keys } from "@libp2p/crypto";
4
- import { webSockets } from "@libp2p/websockets";
1
+ import type { PeerId } from "@libp2p/interface";
5
2
  import type { Multiaddr } from "@multiformats/multiaddr";
3
+ import type {
4
+ CanonicalClient,
5
+ CanonicalOpenAdapter,
6
+ CanonicalOpenMode,
7
+ ConnectServiceWorkerOptions,
8
+ ConnectWindowOptions,
9
+ } from "@peerbit/canonical-client";
10
+ import type { Identity, PublicSignKey } from "@peerbit/crypto";
6
11
  import { Ed25519Keypair } from "@peerbit/crypto";
7
12
  import type { Indices } from "@peerbit/indexer-interface";
8
13
  import { logger as createLogger } from "@peerbit/logger";
9
- import type { ProgramClient } from "@peerbit/program";
10
- import { createClient, createHost } from "@peerbit/proxy-window";
14
+ import type { Address, OpenOptions, Program } from "@peerbit/program";
11
15
  import { waitFor } from "@peerbit/time";
12
- import { detectIncognito } from "detectincognitojs";
13
- import sodium from "libsodium-wrappers";
14
- import { Peerbit } from "peerbit";
15
16
  import * as React from "react";
16
17
  import type { JSX } from "react";
17
18
  import { v4 as uuid } from "uuid";
18
19
  import { FastMutex } from "./lockstorage.ts";
19
- import { useMount } from "./useMount.ts";
20
20
  import {
21
21
  cookiesWhereClearedJustNow,
22
22
  getClientId,
@@ -50,31 +50,84 @@ export type ConnectionStatus =
50
50
  | "connecting"
51
51
  | "failed";
52
52
 
53
- /** Discriminated union for PeerContext */
54
- export type IPeerContext = (ProxyPeerContext | NodePeerContext) & {
55
- error?: Error;
53
+ export type PeerbitLike = {
54
+ peerId: PeerId;
55
+ identity: Identity<PublicSignKey>;
56
+ getMultiaddrs: () => Multiaddr[];
57
+ dial: (address: string | Multiaddr | Multiaddr[]) => Promise<boolean>;
58
+ hangUp: (
59
+ address: PeerId | PublicSignKey | string | Multiaddr,
60
+ ) => Promise<void>;
61
+ start: () => Promise<void>;
62
+ stop: () => Promise<void>;
63
+ bootstrap?: (addresses?: string[] | Multiaddr[]) => Promise<void>;
64
+ open: <S extends Program<any>>(
65
+ storeOrAddress: S | Address | string,
66
+ options?: OpenOptions<S>,
67
+ ) => Promise<S>;
56
68
  };
57
69
 
58
- export interface ProxyPeerContext {
59
- type: "proxy";
60
- peer: ProgramClient | undefined;
61
- promise: Promise<void> | undefined;
62
- loading: boolean;
63
- status: ConnectionStatus;
64
- persisted: boolean | undefined;
65
- /** Present only in proxy (iframe) mode */
66
- targetOrigin: string;
67
- }
70
+ export type PeerRuntime = "node" | "canonical";
71
+
72
+ export type NodePeerProviderConfig = {
73
+ runtime: "node";
74
+ network: "local" | "remote" | NetworkOption;
75
+ waitForConnected?: boolean | "in-flight";
76
+ keypair?: Ed25519Keypair;
77
+ singleton?: boolean;
78
+ indexer?: (directory?: string) => Promise<Indices> | Indices;
79
+ inMemory?: boolean;
80
+ };
81
+
82
+ export type CanonicalPeerProviderConfig = {
83
+ runtime: "canonical";
84
+ transport:
85
+ | { kind: "service-worker"; options: ConnectServiceWorkerOptions }
86
+ | {
87
+ kind: "shared-worker";
88
+ worker:
89
+ | SharedWorker
90
+ | (() => SharedWorker)
91
+ | {
92
+ url: string | URL;
93
+ name?: string;
94
+ type?: WorkerOptions["type"];
95
+ };
96
+ }
97
+ | { kind: "window"; options?: ConnectWindowOptions }
98
+ | {
99
+ kind: "custom";
100
+ connect: () => Promise<CanonicalClient>;
101
+ };
102
+ open?: { adapters?: CanonicalOpenAdapter[]; mode?: CanonicalOpenMode };
103
+ keepAlive?: false | Parameters<CanonicalClient["startKeepAlive"]>[0];
104
+ };
105
+
106
+ export type PeerProviderEnv = {
107
+ inIframe: boolean;
108
+ };
109
+
110
+ export type PeerProviderConfig =
111
+ | NodePeerProviderConfig
112
+ | CanonicalPeerProviderConfig;
68
113
 
69
- export interface NodePeerContext {
70
- type: "node";
71
- peer: ProgramClient | undefined;
114
+ export type PeerProviderConfigSelector =
115
+ | PeerProviderConfig
116
+ | ((env: PeerProviderEnv) => PeerProviderConfig);
117
+
118
+ export type IPeerContext = {
119
+ runtime: PeerRuntime;
120
+ peer: PeerbitLike | undefined;
72
121
  promise: Promise<void> | undefined;
73
122
  loading: boolean;
74
123
  status: ConnectionStatus;
75
- persisted: boolean | undefined;
76
- tabIndex: number;
77
- }
124
+ persisted?: boolean;
125
+ tabIndex?: number;
126
+ error?: Error;
127
+ canonical?: {
128
+ client: CanonicalClient;
129
+ };
130
+ };
78
131
 
79
132
  if (!window.name) {
80
133
  window.name = uuid();
@@ -83,11 +136,6 @@ if (!window.name) {
83
136
  export const PeerContext = React.createContext<IPeerContext>({} as any);
84
137
  export const usePeer = () => React.useContext(PeerContext);
85
138
 
86
- type IFrameOptions = {
87
- type: "proxy";
88
- targetOrigin: string;
89
- };
90
-
91
139
  /**
92
140
  * Network configuration for the node client.
93
141
  *
@@ -99,382 +147,348 @@ export type NetworkOption =
99
147
  | { type: "remote" }
100
148
  | { type?: "explicit"; bootstrap: (Multiaddr | string)[] };
101
149
 
102
- type NodeOptions = {
103
- type?: "node";
104
- network: "local" | "remote" | NetworkOption;
105
- waitForConnnected?: boolean | "in-flight";
106
- keypair?: Ed25519Keypair;
107
- host?: boolean;
108
- singleton?: boolean;
109
- indexer?: (directory?: string) => Promise<Indices> | Indices;
110
- };
111
-
112
- type TopOptions = NodeOptions & { inMemory?: boolean };
113
- type TopAndIframeOptions = {
114
- iframe: IFrameOptions | NodeOptions;
115
- top: TopOptions;
116
- };
117
- type WithChildren = {
118
- children: JSX.Element;
119
- };
120
- type PeerOptions = (TopAndIframeOptions | TopOptions) & WithChildren;
121
-
122
150
  const subscribeToUnload = (fn: () => any) => {
123
151
  window.addEventListener("pagehide", fn);
124
152
  window.addEventListener("beforeunload", fn);
153
+ return () => {
154
+ window.removeEventListener("pagehide", fn);
155
+ window.removeEventListener("beforeunload", fn);
156
+ };
125
157
  };
126
158
 
127
- export const PeerProvider = (options: PeerOptions) => {
128
- const [peer, setPeer] = React.useState<ProgramClient | undefined>(undefined);
159
+ export type PeerProviderProps = {
160
+ config: PeerProviderConfigSelector;
161
+ children: JSX.Element;
162
+ };
163
+
164
+ const resolveConfig = (
165
+ input: PeerProviderConfigSelector,
166
+ ): PeerProviderConfig =>
167
+ typeof input === "function" ? input({ inIframe: inIframe() }) : input;
168
+
169
+ export const PeerProvider = ({ config, children }: PeerProviderProps) => {
170
+ const [runtime, setRuntime] = React.useState<PeerRuntime>("node");
171
+ const [peer, setPeer] = React.useState<PeerbitLike | undefined>(undefined);
172
+ const [canonicalClient, setCanonicalClient] = React.useState<
173
+ CanonicalClient | undefined
174
+ >(undefined);
129
175
  const [promise, setPromise] = React.useState<Promise<void> | undefined>(
130
176
  undefined,
131
177
  );
132
- const [persisted, setPersisted] = React.useState<boolean>(false);
178
+ const [persisted, setPersisted] = React.useState<boolean | undefined>(
179
+ undefined,
180
+ );
133
181
  const [loading, setLoading] = React.useState<boolean>(true);
134
182
  const [connectionState, setConnectionState] =
135
183
  React.useState<ConnectionStatus>("disconnected");
136
-
137
- const [tabIndex, setTabIndex] = React.useState<number>(-1);
138
-
139
- const [error, setError] = React.useState<Error | undefined>(undefined); // <-- error state
140
-
141
- // Decide which options to use based on whether we're in an iframe.
142
- // If options.top is defined, assume we have separate settings for iframe vs. host.
143
- const nodeOptions: IFrameOptions | TopOptions = (
144
- options as TopAndIframeOptions
145
- ).top
146
- ? inIframe()
147
- ? (options as TopAndIframeOptions).iframe
148
- : { ...options, ...(options as TopAndIframeOptions).top } // we merge root and top options, TODO should this be made in a different way to prevent confusion about top props?
149
- : (options as TopOptions);
150
-
151
- // If running as a proxy (iframe), expect a targetOrigin.
152
- const computedTargetOrigin =
153
- nodeOptions.type === "proxy"
154
- ? (nodeOptions as IFrameOptions).targetOrigin
155
- : undefined;
184
+ const [tabIndex, setTabIndex] = React.useState<number | undefined>(undefined);
185
+ const [error, setError] = React.useState<Error | undefined>(undefined);
156
186
 
157
187
  const memo = React.useMemo<IPeerContext>(() => {
158
- if (nodeOptions.type === "proxy") {
159
- return {
160
- type: "proxy",
161
- peer,
162
- promise,
163
- loading,
164
- status: connectionState,
165
- persisted,
166
- targetOrigin: computedTargetOrigin as string,
167
- error,
168
- };
169
- } else {
170
- return {
171
- type: "node",
172
- peer,
173
- promise,
174
- loading,
175
- status: connectionState,
176
- persisted,
177
- tabIndex,
178
- error,
179
- };
180
- }
188
+ return {
189
+ runtime,
190
+ peer,
191
+ promise,
192
+ loading,
193
+ status: connectionState,
194
+ persisted,
195
+ tabIndex,
196
+ error,
197
+ canonical: canonicalClient ? { client: canonicalClient } : undefined,
198
+ };
181
199
  }, [
182
- loading,
183
- promise,
200
+ canonicalClient,
184
201
  connectionState,
202
+ error,
203
+ loading,
185
204
  peer,
186
205
  persisted,
206
+ promise,
207
+ runtime,
187
208
  tabIndex,
188
- computedTargetOrigin,
189
- error,
190
209
  ]);
191
210
 
192
- useMount(() => {
211
+ React.useEffect(() => {
212
+ let unmounted = false;
213
+ let unloadUnsubscribe: (() => void) | undefined;
214
+ let stopKeepAlive: (() => void) | undefined;
215
+ let closePeer: (() => void) | undefined;
216
+
217
+ const selected = resolveConfig(config);
218
+ setRuntime(selected.runtime);
219
+ setConnectionState("connecting");
193
220
  setLoading(true);
221
+ setError(undefined);
222
+
194
223
  const fn = async () => {
224
+ if (selected.runtime === "canonical") {
225
+ const {
226
+ connectServiceWorker,
227
+ connectSharedWorker,
228
+ connectWindow,
229
+ PeerbitCanonicalClient,
230
+ } = await import("@peerbit/canonical-client");
231
+
232
+ let canonical: CanonicalClient;
233
+ if (selected.transport.kind === "service-worker") {
234
+ canonical = await connectServiceWorker(selected.transport.options);
235
+ } else if (selected.transport.kind === "window") {
236
+ canonical = await connectWindow(selected.transport.options ?? {});
237
+ } else if (selected.transport.kind === "shared-worker") {
238
+ const workerSpec = selected.transport.worker;
239
+ const worker =
240
+ typeof workerSpec === "function"
241
+ ? workerSpec()
242
+ : workerSpec instanceof SharedWorker
243
+ ? workerSpec
244
+ : new SharedWorker(workerSpec.url, {
245
+ name: workerSpec.name,
246
+ type: workerSpec.type,
247
+ });
248
+ canonical = await connectSharedWorker(worker);
249
+ } else {
250
+ canonical = await selected.transport.connect();
251
+ }
252
+
253
+ setCanonicalClient(canonical);
254
+ stopKeepAlive =
255
+ selected.keepAlive === false
256
+ ? undefined
257
+ : canonical.startKeepAlive(selected.keepAlive);
258
+
259
+ const peer = await PeerbitCanonicalClient.create(
260
+ canonical,
261
+ selected.open,
262
+ );
263
+ closePeer = () => {
264
+ stopKeepAlive?.();
265
+ try {
266
+ peer.close();
267
+ } catch {}
268
+ };
269
+ unloadUnsubscribe = subscribeToUnload(() => closePeer?.());
270
+ if (unmounted) {
271
+ closePeer();
272
+ return;
273
+ }
274
+ setPeer(peer as unknown as PeerbitLike);
275
+ setConnectionState("connected");
276
+ setLoading(false);
277
+ return;
278
+ }
279
+
280
+ const nodeOptions = selected as NodePeerProviderConfig;
281
+
282
+ const [
283
+ { detectIncognito },
284
+ sodiumModule,
285
+ { Peerbit },
286
+ { noise },
287
+ { yamux },
288
+ { webSockets },
289
+ { keys },
290
+ ] = await Promise.all([
291
+ import("detectincognitojs"),
292
+ import("libsodium-wrappers"),
293
+ import("peerbit"),
294
+ import("@chainsafe/libp2p-noise"),
295
+ import("@chainsafe/libp2p-yamux"),
296
+ import("@libp2p/websockets"),
297
+ import("@libp2p/crypto"),
298
+ ]);
299
+
300
+ const sodium = (sodiumModule as any).default ?? sodiumModule;
195
301
  await sodium.ready;
196
- let newPeer: ProgramClient;
197
- // Track resolved persistence status during client creation
302
+
303
+ let newPeer: PeerbitLike;
198
304
  let persistedResolved = false;
199
- // Controls how long we keep locks alive; flipped to false on close/hidden
200
305
  const keepAliveRef = { current: true } as { current: boolean };
201
306
 
202
- if (nodeOptions.type !== "proxy") {
203
- const releaseFirstLock = cookiesWhereClearedJustNow();
307
+ const releaseFirstLock = cookiesWhereClearedJustNow();
308
+ const sessionId = getClientId("session");
309
+ const mutex = new FastMutex({ clientId: sessionId, timeout: 1e3 });
204
310
 
205
- const sessionId = getClientId("session");
206
- const mutex = new FastMutex({
207
- clientId: sessionId,
208
- timeout: 1e3,
209
- });
210
- if (nodeOptions.singleton) {
211
- singletonLog("acquiring lock");
212
- const localId = getClientId("local");
213
- try {
214
- const lockKey = localId + "-singleton";
215
- subscribeToUnload(function () {
216
- // Immediate release on page close
217
- keepAliveRef.current = false;
218
- mutex.release(lockKey);
219
- });
220
- // Also release when page is hidden to reduce flakiness between sequential tests
221
- const onVisibility = () => {
222
- if (document.visibilityState === "hidden") {
223
- keepAliveRef.current = false;
224
- // Mark expired and remove proactively
225
- try {
226
- mutex.release(lockKey);
227
- } catch {}
228
- }
229
- };
230
- document.addEventListener("visibilitychange", onVisibility);
231
- if (isInStandaloneMode()) {
232
- // PWA issue fix (? TODO is this needed ?
311
+ if (nodeOptions.singleton) {
312
+ singletonLog("acquiring lock");
313
+ const localId = getClientId("local");
314
+ try {
315
+ const lockKey = localId + "-singleton";
316
+ const unsubscribeUnload = subscribeToUnload(() => {
317
+ keepAliveRef.current = false;
318
+ mutex.release(lockKey);
319
+ });
320
+ const onVisibility = () => {
321
+ if (document.visibilityState === "hidden") {
233
322
  keepAliveRef.current = false;
234
- mutex.release(lockKey);
323
+ try {
324
+ mutex.release(lockKey);
325
+ } catch {}
235
326
  }
236
- await mutex.lock(lockKey, () => keepAliveRef.current, {
237
- replaceIfSameClient: true,
238
- });
239
- singletonLog("lock acquired");
240
- } catch (error) {
241
- console.error("Failed to lock singleton client", error);
242
- throw new ClientBusyError("Failed to lock single client");
243
- }
244
- }
245
-
246
- let nodeId: Ed25519Keypair;
247
- if (nodeOptions.keypair) {
248
- nodeId = nodeOptions.keypair;
249
- } else {
250
- keypairLog("acquiring lock");
251
- const kp = await getFreeKeypair(
252
- "",
253
- mutex,
254
- () => keepAliveRef.current,
255
- {
256
- releaseFirstLock,
257
- releaseLockIfSameId: true,
258
- },
259
- );
260
- keypairLog("lock acquired", { index: kp.index });
261
- subscribeToUnload(function () {
327
+ };
328
+ document.addEventListener("visibilitychange", onVisibility);
329
+ if (isInStandaloneMode()) {
262
330
  keepAliveRef.current = false;
263
- mutex.release(kp.path);
264
- });
265
- nodeId = kp.key;
266
- setTabIndex(kp.index);
267
- }
268
- const peerId = nodeId.toPeerId();
269
- const privateKey = keys.privateKeyFromRaw(nodeId.privateKeyPublicKey);
270
-
271
- let directory: string | undefined = undefined;
272
- if (
273
- !(nodeOptions as TopOptions).inMemory &&
274
- !(await detectIncognito()).isPrivate
275
- ) {
276
- storageLog("requesting persist");
277
- const persisted = await navigator.storage.persist();
278
- setPersisted(persisted);
279
- persistedResolved = persisted;
280
- if (!persisted) {
281
- setPersisted(false);
282
- console.error(
283
- "Request persistence but permission was not granted by browser.",
284
- );
285
- } else {
286
- directory = `./repo/${peerId.toString()}/`;
331
+ mutex.release(lockKey);
287
332
  }
333
+ await mutex.lock(lockKey, () => keepAliveRef.current, {
334
+ replaceIfSameClient: true,
335
+ });
336
+ singletonLog("lock acquired");
337
+ void unsubscribeUnload;
338
+ } catch (error) {
339
+ console.error("Failed to lock singleton client", error);
340
+ throw new ClientBusyError("Failed to lock single client");
288
341
  }
342
+ }
289
343
 
290
- clientLog("create", { directory });
291
- newPeer = await Peerbit.create({
292
- libp2p: {
293
- addresses: {
294
- listen: [
295
- /* "/p2p-circuit" */
296
- ],
297
- },
298
- streamMuxers: [yamux()],
299
- connectionEncrypters: [noise()],
300
- privateKey,
301
- connectionManager: { maxConnections: 100 },
302
- connectionMonitor: { enabled: false },
303
- ...(nodeOptions.network === "local"
304
- ? {
305
- connectionGater: {
306
- denyDialMultiaddr: () => false,
307
- },
308
- transports: [
309
- webSockets({}) /* ,
310
- circuitRelayTransport(), */,
311
- ],
312
- }
313
- : {
314
- connectionGater: {
315
- denyDialMultiaddr: () => false, // TODO do right here, dont allow local dials except bootstrap
316
- },
317
- transports: [
318
- webSockets() /* ,
319
- circuitRelayTransport(), */,
320
- ],
321
- }) /*
322
- services: {
323
- pubsub: (c) =>
324
- new DirectSub(c, { canRelayMessage: true }),
325
- identify: identify(),
326
- }, */,
327
- },
328
- directory,
329
- indexer: nodeOptions.indexer,
344
+ let nodeId: Ed25519Keypair;
345
+ if (nodeOptions.keypair) {
346
+ nodeId = nodeOptions.keypair;
347
+ } else {
348
+ keypairLog("acquiring lock");
349
+ const kp = await getFreeKeypair("", mutex, () => keepAliveRef.current, {
350
+ releaseFirstLock,
351
+ releaseLockIfSameId: true,
330
352
  });
331
- clientLog("created", {
332
- directory,
333
- peerHash: newPeer?.identity.publicKey.hashcode(),
334
- network: nodeOptions.network === "local" ? "local" : "remote",
353
+ keypairLog("lock acquired", { index: kp.index });
354
+ subscribeToUnload(() => {
355
+ keepAliveRef.current = false;
356
+ mutex.release(kp.path);
335
357
  });
358
+ nodeId = kp.key;
359
+ setTabIndex(kp.index);
360
+ }
336
361
 
337
- (window as any).__peerInfo = {
338
- peerHash: newPeer?.identity.publicKey.hashcode(),
339
- persisted: persistedResolved,
340
- };
341
- window.dispatchEvent(
342
- new CustomEvent("peer:ready", {
343
- detail: (window as any).__peerInfo,
344
- }),
345
- );
362
+ const peerId = nodeId.toPeerId();
363
+ const privateKey = keys.privateKeyFromRaw(nodeId.privateKeyPublicKey);
364
+
365
+ let directory: string | undefined;
366
+ if (!nodeOptions.inMemory && !(await detectIncognito()).isPrivate) {
367
+ storageLog("requesting persist");
368
+ const persistedValue = await navigator.storage.persist();
369
+ setPersisted(persistedValue);
370
+ persistedResolved = persistedValue;
371
+ if (!persistedValue) {
372
+ console.error(
373
+ "Request persistence but permission was not granted by browser.",
374
+ );
375
+ } else {
376
+ directory = `./repo/${peerId.toString()}/`;
377
+ }
378
+ }
346
379
 
347
- setConnectionState("connecting");
380
+ clientLog("create", { directory });
381
+ const created = await Peerbit.create({
382
+ libp2p: {
383
+ privateKey,
384
+ addresses: { listen: [] },
385
+ streamMuxers: [yamux()],
386
+ connectionEncrypters: [noise()],
387
+ connectionManager: { maxConnections: 100 },
388
+ connectionMonitor: { enabled: false },
389
+ ...(nodeOptions.network === "local"
390
+ ? {
391
+ connectionGater: { denyDialMultiaddr: () => false },
392
+ transports: [webSockets({})],
393
+ }
394
+ : {
395
+ connectionGater: { denyDialMultiaddr: () => false },
396
+ transports: [webSockets()],
397
+ }),
398
+ },
399
+ directory,
400
+ indexer: nodeOptions.indexer,
401
+ });
402
+
403
+ newPeer = created as unknown as PeerbitLike;
404
+
405
+ (window as any).__peerInfo = {
406
+ peerHash: created?.identity?.publicKey?.hashcode?.(),
407
+ persisted: persistedResolved,
408
+ };
409
+ window.dispatchEvent(
410
+ new CustomEvent("peer:ready", { detail: (window as any).__peerInfo }),
411
+ );
348
412
 
349
- const connectFn = async () => {
350
- try {
351
- const network = nodeOptions.network;
352
-
353
- // 1) Explicit bootstrap addresses take precedence
354
- if (
355
- typeof network !== "string" &&
356
- (network as any)?.bootstrap !== undefined
357
- ) {
358
- const list = (network as any).bootstrap as (Multiaddr | string)[];
359
- if (list.length === 0) {
360
- // Explicit offline mode: skip dialing and mark as connected (no relays)
361
- bootstrapLog("offline: skipping relay dialing");
362
- } else {
363
- for (const addr of list) {
364
- await newPeer.dial(addr);
365
- }
413
+ const connectFn = async () => {
414
+ try {
415
+ const network = nodeOptions.network;
416
+ if (
417
+ typeof network !== "string" &&
418
+ (network as any)?.bootstrap !== undefined
419
+ ) {
420
+ const list = (network as any).bootstrap as (Multiaddr | string)[];
421
+ if (list.length === 0) {
422
+ bootstrapLog("offline: skipping relay dialing");
423
+ } else {
424
+ for (const addr of list) {
425
+ await created.dial(addr as any);
366
426
  }
367
427
  }
368
- // 2) Local development: dial local relay service
369
- else if (
370
- network === "local" ||
371
- (typeof network !== "string" &&
372
- (network as any)?.type === "local")
373
- ) {
374
- const localAddress =
375
- "/ip4/127.0.0.1/tcp/8002/ws/p2p/" +
376
- (await (await fetch("http://localhost:8082/peer/id")).text());
377
- bootstrapLog("dialing local address", localAddress);
378
- await newPeer.dial(localAddress);
379
- }
380
- // 3) Remote default: use bootstrap service (no explicit bootstrap provided)
381
- else {
382
- await (newPeer as Peerbit).bootstrap?.();
383
- }
384
- setConnectionState("connected");
385
- } catch (err: any) {
386
- console.error("Failed to bootstrap:", err);
387
- setConnectionState("failed");
388
- }
389
-
390
- if (nodeOptions.host) {
391
- newPeer = await createHost(newPeer as Peerbit);
428
+ } else if (
429
+ network === "local" ||
430
+ (typeof network !== "string" && (network as any)?.type === "local")
431
+ ) {
432
+ const localAddress =
433
+ "/ip4/127.0.0.1/tcp/8002/ws/p2p/" +
434
+ (await (await fetch("http://localhost:8082/peer/id")).text());
435
+ bootstrapLog("dialing local address", localAddress);
436
+ await created.dial(localAddress);
437
+ } else {
438
+ await created.bootstrap?.();
392
439
  }
393
- };
394
-
395
- const perfEnabled = new URLSearchParams(window.location.search).get(
396
- "perf",
397
- );
398
- const t0 = performance.now();
399
- const marks: Record<string, number> = {};
400
- const perfMark = (label: string) => {
401
- marks[label] = performance.now() - t0;
402
- };
440
+ setConnectionState("connected");
441
+ } catch (err: any) {
442
+ console.error("Failed to bootstrap:", err);
443
+ setConnectionState("failed");
444
+ }
445
+ };
403
446
 
404
- bootstrapLog("start...");
405
- const promise = connectFn().then(() => {
406
- perfMark("dialComplete");
447
+ const promise = connectFn();
448
+ if (nodeOptions.waitForConnected === true) {
449
+ await promise;
450
+ } else if (nodeOptions.waitForConnected === "in-flight") {
451
+ let isDone = false;
452
+ promise.finally(() => {
453
+ isDone = true;
407
454
  });
408
- promise.then(() => {
409
- bootstrapLog("done");
410
- try {
411
- if (perfEnabled) {
412
- const payload = { ...marks } as any;
413
- console.info("[Perf] peer bootstrap", payload);
414
- window.dispatchEvent(
415
- new CustomEvent("perf:peer", {
416
- detail: payload,
417
- }),
418
- );
419
- }
420
- } catch {}
455
+ await waitFor(() => {
456
+ if (isDone) return true;
457
+ const libp2p = created as any;
458
+ if (libp2p.libp2p?.getDialQueue?.()?.length > 0) return true;
459
+ if (libp2p.libp2p?.getConnections?.()?.length > 0) return true;
460
+ return false;
421
461
  });
422
- if (nodeOptions.waitForConnnected === true) {
423
- await promise;
424
- } else if (nodeOptions.waitForConnnected === "in-flight") {
425
- // wait for dialQueue to not be empty or connections to contains the peerId
426
- // or done
427
- let isDone = false;
428
- promise.finally(() => {
429
- isDone = true;
430
- });
431
- await waitFor(() => {
432
- if (isDone) {
433
- return true;
434
- }
435
- const libp2p = newPeer as Peerbit;
436
- if (libp2p.libp2p.getDialQueue().length > 0) {
437
- return true;
438
- }
439
- if (libp2p.libp2p.getConnections().length > 0) {
440
- return true;
441
- }
442
- return false;
443
- });
444
- }
445
- } else {
446
- // When in proxy mode (iframe), use the provided targetOrigin.
447
- newPeer = await createClient(
448
- (nodeOptions as IFrameOptions).targetOrigin,
449
- );
462
+ }
463
+
464
+ if (unmounted) {
450
465
  try {
451
- (window as any).__peerInfo = {
452
- peerHash: newPeer?.identity.publicKey.hashcode(),
453
- persisted: false,
454
- };
455
- window.dispatchEvent(
456
- new CustomEvent("peer:ready", {
457
- detail: (window as any).__peerInfo,
458
- }),
459
- );
466
+ await (created as any)?.stop?.();
460
467
  } catch {}
468
+ return;
461
469
  }
462
-
463
470
  setPeer(newPeer);
464
471
  setLoading(false);
465
472
  };
473
+
466
474
  const fnWithErrorHandling = async () => {
467
475
  try {
468
476
  await fn();
469
477
  } catch (error: any) {
470
478
  setError(error);
479
+ setConnectionState("failed");
471
480
  setLoading(false);
472
481
  }
473
482
  };
474
- setPromise(fnWithErrorHandling());
475
- });
476
483
 
477
- return (
478
- <PeerContext.Provider value={memo}>{options.children}</PeerContext.Provider>
479
- );
484
+ const p = fnWithErrorHandling();
485
+ setPromise(p);
486
+ return () => {
487
+ unmounted = true;
488
+ unloadUnsubscribe?.();
489
+ closePeer?.();
490
+ };
491
+ }, [config]);
492
+
493
+ return <PeerContext.Provider value={memo}>{children}</PeerContext.Provider>;
480
494
  };