@peerbit/react 0.0.47 → 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/dist/src/usePeer.d.ts +67 -44
- package/dist/src/usePeer.d.ts.map +1 -1
- package/dist/src/usePeer.js +238 -270
- package/dist/src/usePeer.js.map +1 -1
- package/package.json +11 -11
- package/src/usePeer.tsx +367 -353
package/src/usePeer.tsx
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
76
|
-
tabIndex
|
|
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
|
|
128
|
-
|
|
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>(
|
|
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 [
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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
|
-
|
|
203
|
-
|
|
307
|
+
const releaseFirstLock = cookiesWhereClearedJustNow();
|
|
308
|
+
const sessionId = getClientId("session");
|
|
309
|
+
const mutex = new FastMutex({ clientId: sessionId, timeout: 1e3 });
|
|
204
310
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
323
|
+
try {
|
|
324
|
+
mutex.release(lockKey);
|
|
325
|
+
} catch {}
|
|
235
326
|
}
|
|
236
|
-
|
|
237
|
-
|
|
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(
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
) {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
396
|
-
"
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
478
|
-
|
|
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
|
};
|