@peerbit/react 0.0.2

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.
@@ -0,0 +1,264 @@
1
+ /*
2
+ ISC License (ISC)
3
+ Copyright (c) 2016, Wes Cruver <chieffancypants@gmail.com>
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any purpose
6
+ with or without fee is hereby granted, provided that the above copyright notice
7
+ and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
11
+ FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
13
+ OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
14
+ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
15
+ THIS SOFTWARE.
16
+ */
17
+
18
+ import { FastMutex } from "../lockstorage.js";
19
+ import sinon from "sinon";
20
+
21
+ import nodelocalstorage from "node-localstorage";
22
+ import { jest } from "@jest/globals";
23
+ import { delay } from "@peerbit/time";
24
+ var LocalStorage = nodelocalstorage.LocalStorage;
25
+ var localStorage = new LocalStorage("./tmp/FastMutex");
26
+ globalThis.localStorage = localStorage;
27
+ describe("FastMutex", () => {
28
+ let sandbox;
29
+ beforeEach(() => {
30
+ sandbox = sinon.createSandbox();
31
+ localStorage.clear();
32
+ });
33
+ afterEach(() => {
34
+ sandbox.restore();
35
+ localStorage.clear();
36
+ expect(localStorage.length).toEqual(0);
37
+ });
38
+
39
+ it("should immediately establish a lock when there is no contention", async () => {
40
+ const fm1 = new FastMutex({ localStorage: localStorage });
41
+
42
+ expect(fm1.isLocked("clientId")).toBeFalse();
43
+ const stats = await fm1.lock("clientId");
44
+ expect(fm1.isLocked("clientId")).toBeTrue();
45
+ });
46
+
47
+ it("When another client has a lock (Y is not 0), it should restart to acquire a lock at a later time", function () {
48
+ const fm1 = new FastMutex({
49
+ xPrefix: "xPrefix_",
50
+ yPrefix: "yPrefix_",
51
+ localStorage: localStorage,
52
+ });
53
+
54
+ const key = "clientId";
55
+ fm1.setItem(`yPrefix_${key}`, "someOtherMutexId");
56
+
57
+ setTimeout(() => {
58
+ localStorage.removeItem(`yPrefix_${key}`);
59
+ }, 20);
60
+
61
+ return fm1.lock(key).then(() => {
62
+ expect(fm1.getLockedInfo(key)).toBeDefined();
63
+ });
64
+ });
65
+
66
+ it("when contending for a lock and ultimately losing, it should restart", () => {
67
+ const key = "somekey";
68
+ const fm = new FastMutex({
69
+ localStorage: localStorage,
70
+ clientId: "uniqueId",
71
+ });
72
+ const stub = sandbox.stub(fm, "getItem");
73
+
74
+ // Set up scenario for lock contention where we lost Y
75
+ stub.onCall(0).returns(null); // getItem Y
76
+ stub.onCall(1).returns("lockcontention"); // getItem X
77
+ stub.onCall(2).returns("youLostTheLock"); // getItem Y
78
+
79
+ // fastmutex should have restarted, so let's free up the lock:
80
+ stub.onCall(3).returns(null);
81
+ stub.onCall(4).returns("uniqueId");
82
+
83
+ return fm.lock(key).then((stats) => {
84
+ expect(stats.restartCount).toEqual(1);
85
+ expect(stats.locksLost).toEqual(1);
86
+ expect(stats.contentionCount).toEqual(1);
87
+ });
88
+ });
89
+
90
+ it("When contending for a lock and ultimately winning, it should not restart", () => {
91
+ const key = "somekey";
92
+ const fm = new FastMutex({
93
+ localStorage: localStorage,
94
+ clientId: "uniqueId",
95
+ });
96
+ const stub = sandbox.stub(fm, "getItem");
97
+
98
+ // Set up scenario for lock contention where we lost Y
99
+ stub.onCall(0).returns(null); // getItem Y
100
+ stub.onCall(1).returns("lockContention");
101
+ stub.onCall(2).returns("uniqueId");
102
+
103
+ const spy = sandbox.spy(fm, "lock");
104
+
105
+ return fm.lock(key).then((stats) => {
106
+ expect(stats.restartCount).toEqual(0);
107
+ expect(stats.locksLost).toEqual(0);
108
+ expect(stats.contentionCount).toEqual(1);
109
+ expect(spy.callCount).toEqual(1);
110
+ });
111
+ });
112
+
113
+ // This is just to ensure that the internals of FastMutex have prefixes on the
114
+ // X and Y locks such that two different FM clients can acquire locks on
115
+ // different keys concurrently without clashing.
116
+ it("should not clash with other fastMutex locks", async () => {
117
+ const yPrefix = "yLock";
118
+ const xPrefix = "xLock";
119
+ const opts = { localStorage, yPrefix, xPrefix };
120
+
121
+ const fm1 = new FastMutex(opts);
122
+ const fm2 = new FastMutex(opts);
123
+
124
+ let lock1Acquired = false;
125
+ let lock2Acquired = false;
126
+
127
+ /* eslint-disable jest/valid-expect-in-promise */
128
+ const lock1Promise = fm1.lock("lock1").then((stats) => {
129
+ lock1Acquired = true;
130
+ expect(localStorage.getItem(yPrefix + "lock1")).toBeDefined();
131
+ return stats;
132
+ });
133
+
134
+ /* eslint-disable jest/valid-expect-in-promise */
135
+ const lock2Promise = fm2.lock("lock2").then((stats) => {
136
+ lock2Acquired = true;
137
+ expect(localStorage.getItem(yPrefix + "lock2")).toBeDefined();
138
+ return stats;
139
+ });
140
+
141
+ await Promise.all([lock1Promise, lock2Promise]).then(() => {
142
+ expect(lock1Acquired).toBeTrue();
143
+ expect(lock2Acquired).toBeTrue();
144
+ });
145
+ });
146
+
147
+ it("release() should remove the y lock in localStorage", () => {
148
+ const key = "somekey";
149
+ const fm1 = new FastMutex({
150
+ localStorage: localStorage,
151
+ clientId: "releaseTestId",
152
+ yPrefix: "yLock",
153
+ });
154
+ return fm1
155
+ .lock(key)
156
+ .then(() => {
157
+ expect(fm1.getItem("yLock" + key)).toEqual("releaseTestId");
158
+ return fm1.release(key);
159
+ })
160
+ .then(() => {
161
+ expect(fm1.getItem("yLock" + key)).toBeUndefined();
162
+ });
163
+ });
164
+
165
+ // this is essentially just a better way to test that two locks cannot get
166
+ // an exclusive lock until the other releases. It's a bit more accurate
167
+ // than the test above ("release should remove the y lock in localstorage")
168
+ it("two clients should never get locks at the same time", function () {
169
+ const fm1 = new FastMutex({ localStorage: localStorage });
170
+ const fm2 = new FastMutex({ localStorage: localStorage });
171
+ let fm1LockReleased = false;
172
+ const lockHoldTime = 10;
173
+
174
+ return fm1
175
+ .lock("clientId")
176
+ .then(() => {
177
+ // before the lock is released, try to establish another lock:
178
+ var lock2Promise = fm2.lock("clientId");
179
+ expect(fm1LockReleased).toBeFalse();
180
+
181
+ // in a few milliseconds, release the lock
182
+ setTimeout(() => {
183
+ fm1.release("clientId");
184
+ fm1LockReleased = true;
185
+ }, lockHoldTime);
186
+
187
+ return lock2Promise;
188
+ })
189
+ .then((lock2) => {
190
+ // this will only execute once the other lock was released
191
+ expect(fm1LockReleased).toBeTrue();
192
+ });
193
+ });
194
+
195
+ it("should throw if lock is never acquired after set time period", function () {
196
+ jest.setTimeout(1000);
197
+ // this.slow(500);
198
+
199
+ const fm1 = new FastMutex({ localStorage: localStorage, timeout: 50 });
200
+ const fm2 = new FastMutex({ localStorage: localStorage, timeout: 50 });
201
+
202
+ const p = fm1.lock("timeoutTest").then(() => {
203
+ // fm2 will never get a lock as we're not releasing fm1's lock:
204
+ return fm2.lock("timeoutTest");
205
+ });
206
+
207
+ return expect(p).rejects.toThrowError();
208
+ });
209
+
210
+ it("should ignore expired locks", () => {
211
+ const fm1 = new FastMutex({
212
+ localStorage: localStorage,
213
+ timeout: 5000,
214
+ yPrefix: "yLock",
215
+ clientId: "timeoutClient",
216
+ });
217
+ const expiredRecord = {
218
+ expiresAt: new Date().getTime() - 5000,
219
+ value: "oldclient",
220
+ };
221
+
222
+ localStorage.setItem("yLocktimeoutTest", JSON.stringify(expiredRecord));
223
+ expect(
224
+ JSON.parse(localStorage.getItem("yLocktimeoutTest")).value
225
+ ).toEqual("oldclient");
226
+ return expect(fm1.lock("timeoutTest")).toResolve();
227
+ });
228
+
229
+ it("should reset the client stats after lock is released", async () => {
230
+ // without resetting the stats, the acquireStart will always be set, and
231
+ // after `timeout` ms, will be unable to acquire a lock anymore
232
+ const fm1 = new FastMutex({ localStorage: localStorage, timeout: 50 });
233
+ let keepLock = true;
234
+ let keepLockFn = () => keepLock;
235
+ await fm1.lock("resetStats", keepLockFn);
236
+ expect(fm1.isLocked("resetStats")).toBeTrue();
237
+ keepLock = false;
238
+ await delay(100); // await timeout
239
+ expect(fm1.isLocked("resetStats")).toBeFalse();
240
+ const p = fm1.lock("resetStats").then(() => fm1.release("resetStats"));
241
+ await expect(p).toResolve();
242
+ });
243
+
244
+ it("can keep lock with callback function", async () => {
245
+ const fm1 = new FastMutex({ localStorage: localStorage, timeout: 50 });
246
+ await fm1.lock("x");
247
+ await fm1.release("x");
248
+ expect(fm1.isLocked("x")).toBeFalse();
249
+ await fm1.lock("x").then(() => fm1.release("x"));
250
+ });
251
+
252
+ it("should reset the client stats if the lock has expired", async () => {
253
+ // in the event a lock cannot be acquired within `timeout`, acquireStart
254
+ // will never be reset, and a subsequent call (after the `timeout`) would
255
+ // immediately fail
256
+ const fm1 = new FastMutex({ localStorage: localStorage, timeout: 50 });
257
+
258
+ await fm1.lock("resetStats");
259
+
260
+ // try to acquire a lock after `timeout`:
261
+ await delay(50);
262
+ await expect(fm1.lock("resetStats")).toResolve();
263
+ });
264
+ });
@@ -0,0 +1,83 @@
1
+ import { getAllKeyPairs, getFreeKeypair, releaseKey } from "../utils";
2
+ import nodelocalstorage from "node-localstorage";
3
+ import { FastMutex } from "../lockstorage";
4
+ import { delay } from "@peerbit/time";
5
+ import { default as sodium } from "libsodium-wrappers";
6
+ import { v4 as uuid } from "uuid";
7
+ var LocalStorage = nodelocalstorage.LocalStorage;
8
+ var localStorage = new LocalStorage("./tmp/getKeypair");
9
+ globalThis.localStorage = localStorage;
10
+
11
+ describe("getKeypair", () => {
12
+ beforeAll(async () => {
13
+ await sodium.ready;
14
+ });
15
+
16
+ it("can aquire multiple keypairs", async () => {
17
+ let timeout = 1000;
18
+ let mutex = new FastMutex({ localStorage, timeout });
19
+ let lock = true;
20
+ const lockCondition = () => lock;
21
+ let id = uuid();
22
+ const { key: keypair, path: path1 } = await getFreeKeypair(
23
+ id,
24
+ mutex,
25
+ lockCondition
26
+ );
27
+ const { key: keypair2, path: path2 } = await getFreeKeypair(id, mutex);
28
+ expect(keypair!.equals(keypair2!)).toBeFalse();
29
+ expect(path1).not.toEqual(path2);
30
+ lock = false;
31
+ await delay(timeout);
32
+ const { path: path3, key: keypair3 } = await getFreeKeypair(id, mutex);
33
+ expect(path3).toEqual(path1);
34
+ expect(keypair3.equals(keypair)).toBeTrue();
35
+
36
+ const allKeypair = await getAllKeyPairs(id);
37
+ expect(allKeypair.map((x) => x.publicKey.hashcode())).toEqual([
38
+ keypair3.publicKey.hashcode(),
39
+ keypair2.publicKey.hashcode(),
40
+ ]);
41
+ });
42
+
43
+ it("can release if same id", async () => {
44
+ let timeout = 1000;
45
+ let mutex = new FastMutex({ localStorage, timeout });
46
+ let lock = true;
47
+ const lockCondition = () => lock;
48
+ let id = uuid();
49
+ const { key: keypair, path: path1 } = await getFreeKeypair(
50
+ id,
51
+ mutex,
52
+ lockCondition,
53
+ true
54
+ );
55
+ const { key: keypair2, path: path2 } = await getFreeKeypair(
56
+ id,
57
+ mutex,
58
+ undefined,
59
+ true
60
+ );
61
+ expect(keypair!.equals(keypair2!)).toBeTrue();
62
+ expect(path1).toEqual(path2);
63
+ const allKeypair = await getAllKeyPairs(id);
64
+ expect(allKeypair).toHaveLength(1);
65
+ });
66
+
67
+ it("releases manually", async () => {
68
+ let timeout = 1000;
69
+ let mutex = new FastMutex({ localStorage, timeout });
70
+ const id = uuid();
71
+
72
+ const { key: keypair, path: path1 } = await getFreeKeypair(id, mutex);
73
+
74
+ const { key: keypair2, path: path2 } = await getFreeKeypair(id, mutex);
75
+
76
+ expect(path1).not.toEqual(path2);
77
+ releaseKey(path1, mutex);
78
+ expect(mutex.getLockedInfo(path1)).toBeUndefined();
79
+ const { key: keypair3, path: path3 } = await getFreeKeypair(id, mutex);
80
+
81
+ expect(path1).toEqual(path3); // we can now acquire key at path1 again, since we released it
82
+ });
83
+ });
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export {
2
+ PeerProvider,
3
+ PeerContext,
4
+ usePeer,
5
+ submitKeypairChange,
6
+ } from "./usePeer.js";
7
+ export * from "./utils.js";
8
+ export { FastMutex } from "./lockstorage.js";
@@ -0,0 +1,249 @@
1
+ /*
2
+ ISC License (ISC)
3
+ Copyright (c) 2016, Wes Cruver <chieffancypants@gmail.com>
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any purpose
6
+ with or without fee is hereby granted, provided that the above copyright notice
7
+ and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
11
+ FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
13
+ OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
14
+ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
15
+ THIS SOFTWARE.
16
+ */
17
+
18
+ import debugFn from "debug";
19
+ import { v4 as uuid } from "uuid";
20
+ const debug = debugFn("FastMutex");
21
+
22
+ export class FastMutex {
23
+ clientId: string;
24
+ xPrefix: string;
25
+ yPrefix: string;
26
+ timeout: number;
27
+ localStorage: any;
28
+ intervals: Map<string, any>;
29
+
30
+ constructor({
31
+ clientId = uuid(),
32
+ xPrefix = "_MUTEX_LOCK_X_",
33
+ yPrefix = "_MUTEX_LOCK_Y_",
34
+ timeout = 5000,
35
+ localStorage = undefined,
36
+ } = {}) {
37
+ this.clientId = clientId;
38
+ this.xPrefix = xPrefix;
39
+ this.yPrefix = yPrefix;
40
+ this.timeout = timeout;
41
+ this.intervals = new Map();
42
+
43
+ this.localStorage = localStorage || window.localStorage;
44
+ }
45
+
46
+ lock(
47
+ key: string,
48
+ keepLocked?: () => boolean
49
+ ): Promise<{ restartCount: number; contentionCount; locksLost: number }> {
50
+ debug(
51
+ 'Attempting to acquire Lock on "%s" using FastMutex instance "%s"',
52
+ key,
53
+ this.clientId
54
+ );
55
+ const x = this.xPrefix + key;
56
+ const y = this.yPrefix + key;
57
+ let acquireStart = +new Date();
58
+ return new Promise((resolve, reject) => {
59
+ // we need to differentiate between API calls to lock() and our internal
60
+ // recursive calls so that we can timeout based on the original lock() and
61
+ // not each subsequent call. Therefore, create a new function here within
62
+ // the promise closure that we use for subsequent calls:
63
+ let restartCount = 0;
64
+ let contentionCount = 0;
65
+ let locksLost = 0;
66
+ const acquireLock = (key) => {
67
+ if (
68
+ restartCount > 1000 ||
69
+ contentionCount > 1000 ||
70
+ locksLost > 1000
71
+ ) {
72
+ reject("Failed to resolve lock");
73
+ }
74
+
75
+ const elapsedTime = new Date().getTime() - acquireStart;
76
+ if (elapsedTime >= this.timeout) {
77
+ debug(
78
+ 'Lock on "%s" could not be acquired within %sms by FastMutex client "%s"',
79
+ key,
80
+ this.timeout,
81
+ this.clientId
82
+ );
83
+ return reject(
84
+ new Error(
85
+ `Lock could not be acquired within ${this.timeout}ms`
86
+ )
87
+ );
88
+ }
89
+
90
+ this.setItem(x, this.clientId, keepLocked);
91
+
92
+ // if y exists, another client is getting a lock, so retry in a bit
93
+ let lsY = this.getItem(y);
94
+ if (lsY) {
95
+ debug("Lock exists on Y (%s), restarting...", lsY);
96
+ restartCount++;
97
+ setTimeout(() => acquireLock(key));
98
+ return;
99
+ }
100
+
101
+ // ask for inner lock
102
+ this.setItem(y, this.clientId, keepLocked);
103
+
104
+ // if x was changed, another client is contending for an inner lock
105
+ let lsX = this.getItem(x);
106
+ if (lsX !== this.clientId) {
107
+ contentionCount++;
108
+ debug('Lock contention detected. X="%s"', lsX);
109
+
110
+ // Give enough time for critical section:
111
+ setTimeout(() => {
112
+ lsY = this.getItem(y);
113
+ if (lsY === this.clientId) {
114
+ // we have a lock
115
+ debug(
116
+ 'FastMutex client "%s" won the lock contention on "%s"',
117
+ this.clientId,
118
+ key
119
+ );
120
+ resolve({
121
+ contentionCount,
122
+ locksLost,
123
+ restartCount,
124
+ });
125
+ } else {
126
+ // we lost the lock, restart the process again
127
+ restartCount++;
128
+ locksLost++;
129
+ debug(
130
+ 'FastMutex client "%s" lost the lock contention on "%s" to another process (%s). Restarting...',
131
+ this.clientId,
132
+ key,
133
+ lsY
134
+ );
135
+ setTimeout(() => acquireLock(key));
136
+ }
137
+ }, 50);
138
+ return;
139
+ }
140
+
141
+ // no contention:
142
+ debug(
143
+ 'FastMutex client "%s" acquired a lock on "%s" with no contention',
144
+ this.clientId,
145
+ key
146
+ );
147
+ resolve({ contentionCount, locksLost, restartCount });
148
+ };
149
+
150
+ acquireLock(key);
151
+ });
152
+ }
153
+
154
+ isLocked(key: string) {
155
+ const x = this.xPrefix + key;
156
+ const y = this.yPrefix + key;
157
+ return !!this.getItem(x) || !!this.getItem(y);
158
+ }
159
+
160
+ getLockedInfo(key: string): string | undefined {
161
+ const x = this.xPrefix + key;
162
+ const y = this.yPrefix + key;
163
+ return this.getItem(x) || this.getItem(y);
164
+ }
165
+
166
+ release(key: string) {
167
+ debug(
168
+ 'FastMutex client "%s" is releasing lock on "%s"',
169
+ this.clientId,
170
+ key
171
+ );
172
+ let ps = [this.yPrefix + key, this.xPrefix + key];
173
+ for (const p of ps) {
174
+ clearInterval(this.intervals.get(p));
175
+ this.intervals.delete(p);
176
+ this.localStorage.removeItem(p);
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Helper function to wrap all values in an object that includes the time (so
182
+ * that we can expire it in the future) and json.stringify's it
183
+ */
184
+ setItem(key: string, value: any, keepLocked?: () => boolean) {
185
+ if (!keepLocked) {
186
+ return this.localStorage.setItem(
187
+ key,
188
+ JSON.stringify({
189
+ expiresAt: new Date().getTime() + this.timeout,
190
+ value,
191
+ })
192
+ );
193
+ } else {
194
+ let getExpiry = () => +new Date() + this.timeout;
195
+ const ret = this.localStorage.setItem(
196
+ key,
197
+ JSON.stringify({
198
+ expiresAt: getExpiry(),
199
+ value,
200
+ })
201
+ );
202
+ const interval = setInterval(() => {
203
+ if (!keepLocked()) {
204
+ this.localStorage.setItem(
205
+ // TODO, release directly?
206
+ key,
207
+ JSON.stringify({
208
+ expiresAt: 0,
209
+ value,
210
+ })
211
+ );
212
+ } else {
213
+ this.localStorage.setItem(
214
+ key,
215
+ JSON.stringify({
216
+ expiresAt: getExpiry(), // bump expiry
217
+ value,
218
+ })
219
+ );
220
+ }
221
+ }, this.timeout);
222
+ this.intervals.set(key, interval);
223
+ return ret;
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Helper function to parse JSON encoded values set in localStorage
229
+ */
230
+ getItem(key: string): string | undefined {
231
+ const item = this.localStorage.getItem(key);
232
+ if (!item) return;
233
+
234
+ const parsed = JSON.parse(item);
235
+ if (new Date().getTime() - parsed.expiresAt >= this.timeout) {
236
+ debug(
237
+ 'FastMutex client "%s" removed an expired record on "%s"',
238
+ this.clientId,
239
+ key
240
+ );
241
+ this.localStorage.removeItem(key);
242
+ clearInterval(this.intervals.get(key));
243
+ this.intervals.delete(key);
244
+ return;
245
+ }
246
+
247
+ return JSON.parse(item).value;
248
+ }
249
+ }
@@ -0,0 +1,15 @@
1
+ import { type EffectCallback, useEffect, useRef } from "react";
2
+
3
+ export const useMount = (effect: EffectCallback) => {
4
+ const mounted = useRef(false);
5
+
6
+ useEffect(() => {
7
+ if (!mounted.current) {
8
+ effect();
9
+ }
10
+
11
+ mounted.current = true;
12
+
13
+ return () => {};
14
+ }, [mounted.current]);
15
+ };