@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.
- package/LICENSE +202 -0
- package/README.md +1 -0
- package/lib/esm/__tests__/lockstorage.test.d.ts +1 -0
- package/lib/esm/__tests__/lockstorage.test.js +225 -0
- package/lib/esm/__tests__/lockstorage.test.js.map +1 -0
- package/lib/esm/__tests__/utils.test.d.ts +1 -0
- package/lib/esm/__tests__/utils.test.js +61 -0
- package/lib/esm/__tests__/utils.test.js.map +1 -0
- package/lib/esm/index.d.ts +3 -0
- package/lib/esm/index.js +4 -0
- package/lib/esm/index.js.map +1 -0
- package/lib/esm/lockstorage.d.ts +32 -0
- package/lib/esm/lockstorage.js +178 -0
- package/lib/esm/lockstorage.js.map +1 -0
- package/lib/esm/package.json +3 -0
- package/lib/esm/useMount.d.ts +2 -0
- package/lib/esm/useMount.js +12 -0
- package/lib/esm/useMount.js.map +1 -0
- package/lib/esm/usePeer.d.ts +25 -0
- package/lib/esm/usePeer.js +180 -0
- package/lib/esm/usePeer.js.map +1 -0
- package/lib/esm/utils.d.ts +11 -0
- package/lib/esm/utils.js +96 -0
- package/lib/esm/utils.js.map +1 -0
- package/package.json +76 -0
- package/src/__tests__/lockstorage.test.ts +264 -0
- package/src/__tests__/utils.test.ts +83 -0
- package/src/index.ts +8 -0
- package/src/lockstorage.ts +249 -0
- package/src/useMount.tsx +15 -0
- package/src/usePeer.tsx +270 -0
- package/src/utils.ts +116 -0
|
@@ -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,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
|
+
}
|
package/src/useMount.tsx
ADDED
|
@@ -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
|
+
};
|