@peerbit/react 0.0.13 → 0.0.15
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/lib/esm/index.d.ts +4 -1
- package/lib/esm/index.js +4 -1
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/lockstorage.d.ts +4 -2
- package/lib/esm/lockstorage.js +32 -19
- package/lib/esm/lockstorage.js.map +1 -1
- package/lib/esm/useCount.d.ts +11 -0
- package/lib/esm/useCount.js +40 -0
- package/lib/esm/useCount.js.map +1 -0
- package/lib/esm/useLocal.d.ts +18 -2
- package/lib/esm/useLocal.js +45 -11
- package/lib/esm/useLocal.js.map +1 -1
- package/lib/esm/useOnline.d.ts +10 -0
- package/lib/esm/useOnline.js +52 -0
- package/lib/esm/useOnline.js.map +1 -0
- package/lib/esm/usePeer.d.ts +24 -5
- package/lib/esm/usePeer.js +106 -71
- package/lib/esm/usePeer.js.map +1 -1
- package/lib/esm/useProgram.d.ts +7 -2
- package/lib/esm/useProgram.js +36 -12
- package/lib/esm/useProgram.js.map +1 -1
- package/lib/esm/utils.d.ts +7 -3
- package/lib/esm/utils.js +58 -9
- package/lib/esm/utils.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +9 -1
- package/src/lockstorage.ts +47 -23
- package/src/useCount.tsx +60 -0
- package/src/useLocal.tsx +83 -17
- package/src/useOnline.tsx +66 -0
- package/src/usePeer.tsx +149 -103
- package/src/useProgram.tsx +43 -24
- package/src/utils.ts +75 -20
package/src/usePeer.tsx
CHANGED
|
@@ -5,7 +5,7 @@ import { DirectSub } from "@peerbit/pubsub";
|
|
|
5
5
|
import { yamux } from "@chainsafe/libp2p-yamux";
|
|
6
6
|
import {
|
|
7
7
|
getFreeKeypair,
|
|
8
|
-
|
|
8
|
+
getClientId,
|
|
9
9
|
inIframe,
|
|
10
10
|
cookiesWhereClearedJustNow,
|
|
11
11
|
} from "./utils.js";
|
|
@@ -21,21 +21,46 @@ import { ProgramClient } from "@peerbit/program";
|
|
|
21
21
|
import { identify } from "@libp2p/identify";
|
|
22
22
|
import { webSockets } from "@libp2p/websockets";
|
|
23
23
|
import { circuitRelayTransport } from "@libp2p/circuit-relay-v2";
|
|
24
|
-
|
|
25
24
|
import * as filters from "@libp2p/websockets/filters";
|
|
26
25
|
import { detectIncognito } from "detectincognitojs";
|
|
27
26
|
|
|
27
|
+
export class ClientBusyError extends Error {
|
|
28
|
+
constructor(message: string) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = "CreateClientError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
28
34
|
export type ConnectionStatus =
|
|
29
35
|
| "disconnected"
|
|
30
36
|
| "connected"
|
|
31
37
|
| "connecting"
|
|
32
38
|
| "failed";
|
|
33
|
-
|
|
39
|
+
|
|
40
|
+
/** Discriminated union for PeerContext */
|
|
41
|
+
export type IPeerContext = (ProxyPeerContext | NodePeerContext) & {
|
|
42
|
+
error?: Error;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export interface ProxyPeerContext {
|
|
46
|
+
type: "proxy";
|
|
47
|
+
peer: ProgramClient | undefined;
|
|
48
|
+
promise: Promise<void> | undefined;
|
|
49
|
+
loading: boolean;
|
|
50
|
+
status: ConnectionStatus;
|
|
51
|
+
persisted: boolean | undefined;
|
|
52
|
+
/** Present only in proxy (iframe) mode */
|
|
53
|
+
targetOrigin: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface NodePeerContext {
|
|
57
|
+
type: "node";
|
|
34
58
|
peer: ProgramClient | undefined;
|
|
35
59
|
promise: Promise<void> | undefined;
|
|
36
60
|
loading: boolean;
|
|
37
61
|
status: ConnectionStatus;
|
|
38
62
|
persisted: boolean | undefined;
|
|
63
|
+
tabIndex: number;
|
|
39
64
|
}
|
|
40
65
|
|
|
41
66
|
if (!window.name) {
|
|
@@ -44,6 +69,7 @@ if (!window.name) {
|
|
|
44
69
|
|
|
45
70
|
export const PeerContext = React.createContext<IPeerContext>({} as any);
|
|
46
71
|
export const usePeer = () => useContext(PeerContext);
|
|
72
|
+
|
|
47
73
|
type IFrameOptions = {
|
|
48
74
|
type: "proxy";
|
|
49
75
|
targetOrigin: string;
|
|
@@ -56,15 +82,14 @@ type NodeOptions = {
|
|
|
56
82
|
keypair?: Ed25519Keypair;
|
|
57
83
|
bootstrap?: (Multiaddr | string)[];
|
|
58
84
|
host?: boolean;
|
|
85
|
+
singleton?: boolean;
|
|
59
86
|
};
|
|
60
|
-
|
|
87
|
+
|
|
88
|
+
type TopOptions = NodeOptions & { inMemory?: boolean };
|
|
61
89
|
type TopAndIframeOptions = {
|
|
62
90
|
iframe: IFrameOptions | NodeOptions;
|
|
63
91
|
top: TopOptions;
|
|
64
92
|
};
|
|
65
|
-
type WithMemory = {
|
|
66
|
-
inMemory?: boolean;
|
|
67
|
-
};
|
|
68
93
|
type WithChildren = {
|
|
69
94
|
children: JSX.Element;
|
|
70
95
|
};
|
|
@@ -77,147 +102,161 @@ export const PeerProvider = (options: PeerOptions) => {
|
|
|
77
102
|
const [promise, setPromise] = React.useState<Promise<void> | undefined>(
|
|
78
103
|
undefined
|
|
79
104
|
);
|
|
80
|
-
|
|
81
105
|
const [persisted, setPersisted] = React.useState<boolean | undefined>(
|
|
82
106
|
undefined
|
|
83
107
|
);
|
|
84
|
-
|
|
85
108
|
const [loading, setLoading] = React.useState<boolean>(true);
|
|
86
109
|
const [connectionState, setConnectionState] =
|
|
87
110
|
React.useState<ConnectionStatus>("disconnected");
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
111
|
+
|
|
112
|
+
const [tabIndex, setTabIndex] = React.useState<number>(-1);
|
|
113
|
+
|
|
114
|
+
const [error, setError] = React.useState<Error | undefined>(undefined); // <-- error state
|
|
115
|
+
|
|
116
|
+
// Decide which options to use based on whether we're in an iframe.
|
|
117
|
+
// If options.top is defined, assume we have separate settings for iframe vs. host.
|
|
118
|
+
const nodeOptions: IFrameOptions | TopOptions = (
|
|
119
|
+
options as TopAndIframeOptions
|
|
120
|
+
).top
|
|
121
|
+
? inIframe()
|
|
122
|
+
? (options as TopAndIframeOptions).iframe
|
|
123
|
+
: { ...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?
|
|
124
|
+
: (options as TopOptions);
|
|
125
|
+
|
|
126
|
+
// If running as a proxy (iframe), expect a targetOrigin.
|
|
127
|
+
const computedTargetOrigin =
|
|
128
|
+
nodeOptions.type === "proxy"
|
|
129
|
+
? (nodeOptions as IFrameOptions).targetOrigin
|
|
130
|
+
: undefined;
|
|
131
|
+
|
|
132
|
+
const memo = React.useMemo<IPeerContext>(() => {
|
|
133
|
+
if (nodeOptions.type === "proxy") {
|
|
134
|
+
return {
|
|
135
|
+
type: "proxy",
|
|
136
|
+
peer,
|
|
137
|
+
promise,
|
|
138
|
+
loading,
|
|
139
|
+
status: connectionState,
|
|
140
|
+
persisted,
|
|
141
|
+
targetOrigin: computedTargetOrigin as string,
|
|
142
|
+
error,
|
|
143
|
+
};
|
|
144
|
+
} else {
|
|
145
|
+
return {
|
|
146
|
+
type: "node",
|
|
147
|
+
peer,
|
|
148
|
+
promise,
|
|
149
|
+
loading,
|
|
150
|
+
status: connectionState,
|
|
151
|
+
persisted,
|
|
152
|
+
tabIndex,
|
|
153
|
+
error,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}, [
|
|
157
|
+
loading,
|
|
158
|
+
promise,
|
|
159
|
+
connectionState,
|
|
160
|
+
peer,
|
|
161
|
+
persisted,
|
|
162
|
+
tabIndex,
|
|
163
|
+
computedTargetOrigin,
|
|
164
|
+
error,
|
|
165
|
+
]);
|
|
105
166
|
|
|
106
167
|
useMount(() => {
|
|
107
168
|
setLoading(true);
|
|
108
169
|
const fn = async () => {
|
|
109
170
|
await sodium.ready;
|
|
110
|
-
if (peer) {
|
|
111
|
-
await peer.stop();
|
|
112
|
-
setPeer(undefined);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
171
|
let newPeer: ProgramClient;
|
|
116
|
-
const nodeOptions = (options as TopAndIframeOptions).top
|
|
117
|
-
? inIframe()
|
|
118
|
-
? (options as TopAndIframeOptions).iframe
|
|
119
|
-
: (options as TopAndIframeOptions).top
|
|
120
|
-
: (options as TopOptions);
|
|
121
172
|
|
|
122
173
|
if (nodeOptions.type !== "proxy") {
|
|
123
174
|
const releaseFirstLock = cookiesWhereClearedJustNow();
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
175
|
+
|
|
176
|
+
const sessionId = getClientId("session");
|
|
177
|
+
const mutex = new FastMutex({
|
|
178
|
+
clientId: sessionId,
|
|
179
|
+
timeout: 1e3,
|
|
180
|
+
});
|
|
181
|
+
if (nodeOptions.singleton) {
|
|
182
|
+
const localId = getClientId("local");
|
|
183
|
+
try {
|
|
184
|
+
const lockKey = localId + "-singleton";
|
|
185
|
+
globalThis.onbeforeunload = function () {
|
|
186
|
+
mutex.release(lockKey);
|
|
187
|
+
};
|
|
188
|
+
await mutex.lock(lockKey, () => true, {
|
|
189
|
+
replaceIfSameClient: true,
|
|
190
|
+
});
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error("Failed to lock singleton client", error);
|
|
193
|
+
throw new ClientBusyError(
|
|
194
|
+
"Failed to lock single client"
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let nodeId: Ed25519Keypair;
|
|
200
|
+
if (nodeOptions.keypair) {
|
|
201
|
+
nodeId = nodeOptions.keypair;
|
|
202
|
+
} else {
|
|
203
|
+
const kp = await getFreeKeypair("", mutex, undefined, {
|
|
204
|
+
releaseFirstLock,
|
|
205
|
+
releaseLockIfSameId: true,
|
|
206
|
+
});
|
|
207
|
+
globalThis.onbeforeunload = function () {
|
|
208
|
+
mutex.release(kp.path);
|
|
209
|
+
};
|
|
210
|
+
nodeId = kp.key;
|
|
211
|
+
setTabIndex(kp.index);
|
|
212
|
+
}
|
|
140
213
|
const peerId = nodeId.toPeerId();
|
|
141
214
|
|
|
142
215
|
let directory: string | undefined = undefined;
|
|
143
216
|
if (
|
|
144
|
-
!(nodeOptions as
|
|
217
|
+
!(nodeOptions as TopOptions).inMemory &&
|
|
145
218
|
!(await detectIncognito()).isPrivate
|
|
146
219
|
) {
|
|
147
220
|
const persisted = await navigator.storage.persist();
|
|
148
221
|
setPersisted(persisted);
|
|
149
222
|
if (!persisted) {
|
|
150
223
|
setPersisted(false);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
);
|
|
155
|
-
} else {
|
|
156
|
-
console.error(
|
|
157
|
-
"Request persistance but was not given permission by browser."
|
|
158
|
-
);
|
|
159
|
-
}
|
|
224
|
+
console.error(
|
|
225
|
+
"Request persistence but permission was not granted by browser."
|
|
226
|
+
);
|
|
160
227
|
} else {
|
|
161
228
|
directory = `./repo/${peerId.toString()}/`;
|
|
162
229
|
}
|
|
163
230
|
}
|
|
164
231
|
|
|
165
|
-
// We create a new directrory to make tab to tab communication go smoothly
|
|
166
232
|
console.log("Create client");
|
|
167
233
|
newPeer = await Peerbit.create({
|
|
168
234
|
libp2p: {
|
|
169
|
-
addresses: {
|
|
170
|
-
listen: [
|
|
171
|
-
"/p2p-circuit",
|
|
172
|
-
/* "/webrtc" */
|
|
173
|
-
], // TMP disable because flaky behaviour with libp2p 1.8.1
|
|
174
|
-
},
|
|
235
|
+
addresses: { listen: ["/p2p-circuit"] },
|
|
175
236
|
connectionEncrypters: [noise()],
|
|
176
|
-
peerId,
|
|
177
|
-
connectionManager: {
|
|
178
|
-
|
|
179
|
-
},
|
|
180
|
-
connectionMonitor: {
|
|
181
|
-
enabled: false,
|
|
182
|
-
},
|
|
183
|
-
|
|
237
|
+
peerId,
|
|
238
|
+
connectionManager: { maxConnections: 100 },
|
|
239
|
+
connectionMonitor: { enabled: false },
|
|
184
240
|
streamMuxers: [yamux()],
|
|
185
241
|
...(nodeOptions.network === "local"
|
|
186
242
|
? {
|
|
187
243
|
connectionGater: {
|
|
188
|
-
denyDialMultiaddr: () =>
|
|
189
|
-
// by default we refuse to dial local addresses from the browser since they
|
|
190
|
-
// are usually sent by remote peers broadcasting undialable multiaddrs but
|
|
191
|
-
// here we are explicitly connecting to a local node so do not deny dialing
|
|
192
|
-
// any discovered address
|
|
193
|
-
return false;
|
|
194
|
-
},
|
|
244
|
+
denyDialMultiaddr: () => false,
|
|
195
245
|
},
|
|
196
246
|
transports: [
|
|
197
|
-
|
|
198
|
-
webSockets({
|
|
199
|
-
filter: filters.all,
|
|
200
|
-
}),
|
|
247
|
+
webSockets({ filter: filters.all }),
|
|
201
248
|
circuitRelayTransport(),
|
|
202
|
-
/* webRTC(), */ // TMP disable because flaky behaviour with libp2p 1.8.1
|
|
203
249
|
],
|
|
204
250
|
}
|
|
205
251
|
: {
|
|
206
252
|
transports: [
|
|
207
253
|
webSockets({ filter: filters.wss }),
|
|
208
254
|
circuitRelayTransport(),
|
|
209
|
-
/* webRTC(), */ // TMP disable because flaky behaviour with libp2p 1.8.1
|
|
210
255
|
],
|
|
211
256
|
}),
|
|
212
|
-
|
|
213
257
|
services: {
|
|
214
258
|
pubsub: (c) =>
|
|
215
|
-
new DirectSub(c, {
|
|
216
|
-
canRelayMessage: true,
|
|
217
|
-
/* connectionManager: {
|
|
218
|
-
autoDial: false,
|
|
219
|
-
}, */
|
|
220
|
-
}),
|
|
259
|
+
new DirectSub(c, { canRelayMessage: true }),
|
|
221
260
|
identify: identify(),
|
|
222
261
|
},
|
|
223
262
|
},
|
|
@@ -232,7 +271,6 @@ export const PeerProvider = (options: PeerOptions) => {
|
|
|
232
271
|
|
|
233
272
|
setConnectionState("connecting");
|
|
234
273
|
|
|
235
|
-
// Resolve bootstrap nodes async (we want to return before this is done)
|
|
236
274
|
const connectFn = async () => {
|
|
237
275
|
try {
|
|
238
276
|
if (nodeOptions.network === "local") {
|
|
@@ -245,7 +283,6 @@ export const PeerProvider = (options: PeerOptions) => {
|
|
|
245
283
|
).text())
|
|
246
284
|
);
|
|
247
285
|
} else {
|
|
248
|
-
// TODO fix types. When proxy client this will not be available
|
|
249
286
|
if (nodeOptions.bootstrap) {
|
|
250
287
|
for (const addr of nodeOptions.bootstrap) {
|
|
251
288
|
await newPeer.dial(addr);
|
|
@@ -272,19 +309,28 @@ export const PeerProvider = (options: PeerOptions) => {
|
|
|
272
309
|
promise.then(() => {
|
|
273
310
|
console.log("Bootstrap done");
|
|
274
311
|
});
|
|
275
|
-
// Make sure data flow as expected between tabs and windows locally (offline states)
|
|
276
|
-
|
|
277
312
|
if (nodeOptions.waitForConnnected !== false) {
|
|
278
313
|
await promise;
|
|
279
314
|
}
|
|
280
315
|
} else {
|
|
281
|
-
|
|
316
|
+
// When in proxy mode (iframe), use the provided targetOrigin.
|
|
317
|
+
newPeer = await createClient(
|
|
318
|
+
(nodeOptions as IFrameOptions).targetOrigin
|
|
319
|
+
);
|
|
282
320
|
}
|
|
283
321
|
|
|
284
322
|
setPeer(newPeer);
|
|
285
323
|
setLoading(false);
|
|
286
324
|
};
|
|
287
|
-
|
|
325
|
+
const fnWithErrorHandling = async () => {
|
|
326
|
+
try {
|
|
327
|
+
await fn();
|
|
328
|
+
} catch (error: any) {
|
|
329
|
+
setError(error);
|
|
330
|
+
setLoading(false);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
setPromise(fnWithErrorHandling());
|
|
288
334
|
});
|
|
289
335
|
|
|
290
336
|
return (
|
package/src/useProgram.tsx
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import { Program, OpenOptions, ProgramEvents } from "@peerbit/program";
|
|
2
2
|
import { usePeer } from "./usePeer.js";
|
|
3
|
+
import { PublicSignKey } from "@peerbit/crypto";
|
|
3
4
|
import { useEffect, useReducer, useRef, useState } from "react";
|
|
4
|
-
const
|
|
5
|
-
A,
|
|
6
|
-
B extends ProgramEvents,
|
|
7
|
-
P extends Program<A, B>
|
|
8
|
-
>(
|
|
5
|
+
const addressOrDefined = <A, B extends ProgramEvents, P extends Program<A, B>>(
|
|
9
6
|
p?: P
|
|
10
7
|
) => {
|
|
11
8
|
try {
|
|
12
9
|
return p?.address;
|
|
13
10
|
} catch (error) {
|
|
14
|
-
return
|
|
11
|
+
return !!p;
|
|
15
12
|
}
|
|
16
13
|
};
|
|
17
14
|
type ExtractArgs<T> = T extends Program<infer Args> ? Args : never;
|
|
@@ -22,16 +19,20 @@ export const useProgram = <
|
|
|
22
19
|
Program<any, ProgramEvents>
|
|
23
20
|
>(
|
|
24
21
|
addressOrOpen?: P | string,
|
|
25
|
-
options?: OpenOptions<P>
|
|
22
|
+
options?: OpenOptions<P> & { id?: string; keepOpenOnUnmount?: boolean }
|
|
26
23
|
) => {
|
|
27
24
|
const { peer } = usePeer();
|
|
28
25
|
let [program, setProgram] = useState<P | undefined>();
|
|
26
|
+
const [id, setId] = useState<string | undefined>(options?.id);
|
|
29
27
|
let [loading, setLoading] = useState(true);
|
|
30
28
|
const [session, forceUpdate] = useReducer((x) => x + 1, 0);
|
|
31
29
|
let programLoadingRef = useRef<Promise<P>>();
|
|
32
|
-
const [
|
|
33
|
-
let closingRef = useRef<Promise<any>>(Promise.resolve());
|
|
30
|
+
const [peers, setPeers] = useState<PublicSignKey[]>([]);
|
|
34
31
|
|
|
32
|
+
let closingRef = useRef<Promise<any>>(Promise.resolve());
|
|
33
|
+
/* if (options?.debug) {
|
|
34
|
+
console.log("useProgram", addressOrOpen, options);
|
|
35
|
+
} */
|
|
35
36
|
useEffect(() => {
|
|
36
37
|
if (!peer || !addressOrOpen) {
|
|
37
38
|
return;
|
|
@@ -45,19 +46,29 @@ export const useProgram = <
|
|
|
45
46
|
.then((p) => {
|
|
46
47
|
changeListener = () => {
|
|
47
48
|
p.getReady().then((set) => {
|
|
48
|
-
|
|
49
|
+
setPeers([...set.values()]);
|
|
49
50
|
});
|
|
50
51
|
};
|
|
51
52
|
p.events.addEventListener("join", changeListener);
|
|
52
53
|
p.events.addEventListener("leave", changeListener);
|
|
53
|
-
p.getReady()
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
p.getReady()
|
|
55
|
+
.then((set) => {
|
|
56
|
+
setPeers([...set.values()]);
|
|
57
|
+
})
|
|
58
|
+
.catch((e) => {
|
|
59
|
+
console.log("Error getReady()", e);
|
|
60
|
+
});
|
|
56
61
|
setProgram(p);
|
|
57
62
|
forceUpdate();
|
|
58
|
-
|
|
63
|
+
if (options?.id) {
|
|
64
|
+
setId(p.address);
|
|
65
|
+
}
|
|
59
66
|
return p;
|
|
60
67
|
})
|
|
68
|
+
.catch((e) => {
|
|
69
|
+
console.error("failed to open", e);
|
|
70
|
+
throw e;
|
|
71
|
+
})
|
|
61
72
|
.finally(() => {
|
|
62
73
|
setLoading(false);
|
|
63
74
|
});
|
|
@@ -70,8 +81,8 @@ export const useProgram = <
|
|
|
70
81
|
// TODO don't close on reopen the same db?
|
|
71
82
|
if (programLoadingRef.current) {
|
|
72
83
|
closingRef.current =
|
|
73
|
-
programLoadingRef.current.then((p) =>
|
|
74
|
-
|
|
84
|
+
programLoadingRef.current.then((p) => {
|
|
85
|
+
const unsubscribe = () => {
|
|
75
86
|
p.events.removeEventListener(
|
|
76
87
|
"join",
|
|
77
88
|
changeListener
|
|
@@ -80,26 +91,34 @@ export const useProgram = <
|
|
|
80
91
|
"leave",
|
|
81
92
|
changeListener
|
|
82
93
|
);
|
|
94
|
+
};
|
|
95
|
+
if (options?.keepOpenOnUnmount) {
|
|
96
|
+
unsubscribe();
|
|
97
|
+
}
|
|
83
98
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
99
|
+
if (programLoadingRef.current === startRef) {
|
|
100
|
+
setProgram(undefined);
|
|
101
|
+
programLoadingRef.current = undefined;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return p.close().then(unsubscribe);
|
|
106
|
+
}) || Promise.resolve();
|
|
90
107
|
}
|
|
91
108
|
};
|
|
92
109
|
}, [
|
|
93
110
|
peer?.identity.publicKey.hashcode(),
|
|
111
|
+
options?.id,
|
|
94
112
|
typeof addressOrOpen === "string"
|
|
95
113
|
? addressOrOpen
|
|
96
|
-
:
|
|
114
|
+
: addressOrDefined(addressOrOpen),
|
|
97
115
|
]);
|
|
98
116
|
return {
|
|
99
117
|
program,
|
|
100
118
|
session,
|
|
101
119
|
loading,
|
|
102
120
|
promise: programLoadingRef.current,
|
|
103
|
-
|
|
121
|
+
peers,
|
|
122
|
+
id,
|
|
104
123
|
};
|
|
105
124
|
};
|
package/src/utils.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { FastMutex } from "./lockstorage";
|
|
|
4
4
|
import { v4 as uuid } from "uuid";
|
|
5
5
|
import sodium from "libsodium-wrappers";
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const CLIENT_ID_STORAGE_KEY = "CLIENT_ID";
|
|
8
8
|
export const cookiesWhereClearedJustNow = () => {
|
|
9
9
|
const lastPersistedAt = localStorage.getItem("lastPersistedAt");
|
|
10
10
|
if (lastPersistedAt) {
|
|
@@ -14,13 +14,14 @@ export const cookiesWhereClearedJustNow = () => {
|
|
|
14
14
|
return true;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
export const
|
|
18
|
-
const
|
|
17
|
+
export const getClientId = (type: "session" | "local") => {
|
|
18
|
+
const storage = type === "session" ? sessionStorage : localStorage;
|
|
19
|
+
const idFromStorage = storage.getItem(CLIENT_ID_STORAGE_KEY);
|
|
19
20
|
if (idFromStorage) {
|
|
20
21
|
return idFromStorage;
|
|
21
22
|
} else {
|
|
22
23
|
const id = uuid(); // generate unique UUID
|
|
23
|
-
|
|
24
|
+
storage.setItem(CLIENT_ID_STORAGE_KEY, id);
|
|
24
25
|
return id;
|
|
25
26
|
}
|
|
26
27
|
};
|
|
@@ -29,16 +30,13 @@ const ID_COUNTER_KEY = "idc/";
|
|
|
29
30
|
|
|
30
31
|
const getKeyId = (prefix: string, id: number) => prefix + "/" + id;
|
|
31
32
|
|
|
32
|
-
export const releaseKey = (
|
|
33
|
-
path: string,
|
|
34
|
-
lock: FastMutex = new FastMutex({ clientId: getTabId() })
|
|
35
|
-
) => {
|
|
33
|
+
export const releaseKey = (path: string, lock: FastMutex) => {
|
|
36
34
|
lock.release(path);
|
|
37
35
|
};
|
|
38
36
|
|
|
39
37
|
export const getFreeKeypair = async (
|
|
40
38
|
id: string = "",
|
|
41
|
-
lock: FastMutex
|
|
39
|
+
lock: FastMutex,
|
|
42
40
|
lockCondition: () => boolean = () => true,
|
|
43
41
|
options?: {
|
|
44
42
|
releaseLockIfSameId?: boolean;
|
|
@@ -52,16 +50,16 @@ export const getFreeKeypair = async (
|
|
|
52
50
|
for (let i = 0; i < 10000; i++) {
|
|
53
51
|
const key = getKeyId(id, i);
|
|
54
52
|
let lockedInfo = lock.getLockedInfo(key);
|
|
55
|
-
console.log(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
53
|
+
/* console.log(
|
|
54
|
+
"KEY KEY AT",
|
|
55
|
+
key,
|
|
56
|
+
id,
|
|
57
|
+
i,
|
|
58
|
+
lockedInfo,
|
|
59
|
+
lockedInfo === lock.clientId,
|
|
60
|
+
options
|
|
61
|
+
);
|
|
62
|
+
*/
|
|
65
63
|
if (lockedInfo) {
|
|
66
64
|
if (
|
|
67
65
|
(lockedInfo === lock.clientId &&
|
|
@@ -73,7 +71,7 @@ export const getFreeKeypair = async (
|
|
|
73
71
|
continue;
|
|
74
72
|
}
|
|
75
73
|
}
|
|
76
|
-
|
|
74
|
+
|
|
77
75
|
await lock.lock(key, lockCondition);
|
|
78
76
|
|
|
79
77
|
localStorage.setItem(
|
|
@@ -82,6 +80,7 @@ export const getFreeKeypair = async (
|
|
|
82
80
|
);
|
|
83
81
|
await lock.release(idCounterKey);
|
|
84
82
|
return {
|
|
83
|
+
index: i,
|
|
85
84
|
path: key,
|
|
86
85
|
key: await getKeypair(key),
|
|
87
86
|
};
|
|
@@ -141,3 +140,59 @@ export const inIframe = () => {
|
|
|
141
140
|
return true;
|
|
142
141
|
}
|
|
143
142
|
};
|
|
143
|
+
|
|
144
|
+
export function debounceLeadingTrailing<
|
|
145
|
+
T extends (this: any, ...args: any[]) => void
|
|
146
|
+
>(
|
|
147
|
+
func: T,
|
|
148
|
+
delay: number
|
|
149
|
+
): ((this: ThisParameterType<T>, ...args: Parameters<T>) => void) & {
|
|
150
|
+
cancel: () => void;
|
|
151
|
+
} {
|
|
152
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
153
|
+
let lastArgs: Parameters<T> | null = null;
|
|
154
|
+
let lastThis: any;
|
|
155
|
+
let pendingTrailing = false;
|
|
156
|
+
|
|
157
|
+
const debounced = function (
|
|
158
|
+
this: ThisParameterType<T>,
|
|
159
|
+
...args: Parameters<T>
|
|
160
|
+
) {
|
|
161
|
+
if (!timeoutId) {
|
|
162
|
+
// Leading call: no timer means this is the first call in this period.
|
|
163
|
+
func.apply(this, args);
|
|
164
|
+
} else {
|
|
165
|
+
// Subsequent calls during the delay mark that a trailing call is needed.
|
|
166
|
+
pendingTrailing = true;
|
|
167
|
+
}
|
|
168
|
+
// Always update with the most recent context and arguments.
|
|
169
|
+
lastArgs = args;
|
|
170
|
+
lastThis = this;
|
|
171
|
+
|
|
172
|
+
// Reset the timer.
|
|
173
|
+
if (timeoutId) {
|
|
174
|
+
clearTimeout(timeoutId);
|
|
175
|
+
}
|
|
176
|
+
timeoutId = setTimeout(() => {
|
|
177
|
+
timeoutId = null;
|
|
178
|
+
// If there were any calls during the delay, call the function on the trailing edge.
|
|
179
|
+
if (pendingTrailing && lastArgs) {
|
|
180
|
+
func.apply(lastThis, lastArgs);
|
|
181
|
+
}
|
|
182
|
+
// Reset the trailing flag after the trailing call.
|
|
183
|
+
pendingTrailing = false;
|
|
184
|
+
}, delay);
|
|
185
|
+
} as ((this: ThisParameterType<T>, ...args: Parameters<T>) => void) & {
|
|
186
|
+
cancel: () => void;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
debounced.cancel = () => {
|
|
190
|
+
if (timeoutId) {
|
|
191
|
+
clearTimeout(timeoutId);
|
|
192
|
+
timeoutId = null;
|
|
193
|
+
}
|
|
194
|
+
pendingTrailing = false;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
return debounced;
|
|
198
|
+
}
|