@peerbit/react 0.0.13 → 0.0.14

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.
@@ -45,8 +45,13 @@ export class FastMutex {
45
45
 
46
46
  lock(
47
47
  key: string,
48
- keepLocked?: () => boolean
49
- ): Promise<{ restartCount: number; contentionCount; locksLost: number }> {
48
+ keepLocked?: () => boolean,
49
+ options?: { replaceIfSameClient?: boolean }
50
+ ): Promise<{
51
+ restartCount: number;
52
+ contentionCount: number;
53
+ locksLost: number;
54
+ }> {
50
55
  debug(
51
56
  'Attempting to acquire Lock on "%s" using FastMutex instance "%s"',
52
57
  key,
@@ -54,25 +59,47 @@ export class FastMutex {
54
59
  );
55
60
  const x = this.xPrefix + key;
56
61
  const y = this.yPrefix + key;
57
- let acquireStart = +new Date();
62
+ let acquireStart = Date.now();
58
63
  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
64
  let restartCount = 0;
64
65
  let contentionCount = 0;
65
66
  let locksLost = 0;
66
- const acquireLock = (key) => {
67
+
68
+ const acquireLock = (key: string) => {
69
+ // If the option is set and the same client already holds both keys,
70
+ // update the expiry and resolve immediately.
71
+ if (options?.replaceIfSameClient) {
72
+ const currentX = this.getItem(x);
73
+ const currentY = this.getItem(y);
74
+ if (
75
+ currentX === this.clientId &&
76
+ currentY === this.clientId
77
+ ) {
78
+ // Update expiry so that the lock is effectively "replaced"
79
+ this.setItem(x, this.clientId, keepLocked);
80
+ this.setItem(y, this.clientId, keepLocked);
81
+ debug(
82
+ 'FastMutex client "%s" replaced its own lock on "%s".',
83
+ this.clientId,
84
+ key
85
+ );
86
+ return resolve({
87
+ restartCount,
88
+ contentionCount,
89
+ locksLost,
90
+ });
91
+ }
92
+ }
93
+
94
+ // Check for overall retries/timeouts.
67
95
  if (
68
96
  restartCount > 1000 ||
69
97
  contentionCount > 1000 ||
70
98
  locksLost > 1000
71
99
  ) {
72
- reject("Failed to resolve lock");
100
+ return reject("Failed to resolve lock");
73
101
  }
74
-
75
- const elapsedTime = new Date().getTime() - acquireStart;
102
+ const elapsedTime = Date.now() - acquireStart;
76
103
  if (elapsedTime >= this.timeout) {
77
104
  debug(
78
105
  'Lock on "%s" could not be acquired within %sms by FastMutex client "%s"',
@@ -87,43 +114,40 @@ export class FastMutex {
87
114
  );
88
115
  }
89
116
 
117
+ // First, set key X.
90
118
  this.setItem(x, this.clientId, keepLocked);
91
119
 
92
- // if y exists, another client is getting a lock, so retry in a bit
120
+ // Check if key Y exists (another client may be acquiring the lock)
93
121
  let lsY = this.getItem(y);
94
122
  if (lsY) {
95
123
  debug("Lock exists on Y (%s), restarting...", lsY);
96
124
  restartCount++;
97
- setTimeout(() => acquireLock(key));
125
+ setTimeout(() => acquireLock(key), 10);
98
126
  return;
99
127
  }
100
128
 
101
- // ask for inner lock
129
+ // Request the inner lock by setting Y.
102
130
  this.setItem(y, this.clientId, keepLocked);
103
131
 
104
- // if x was changed, another client is contending for an inner lock
132
+ // Re-check X; if it was changed, we have contention.
105
133
  let lsX = this.getItem(x);
106
134
  if (lsX !== this.clientId) {
107
135
  contentionCount++;
108
136
  debug('Lock contention detected. X="%s"', lsX);
109
-
110
- // Give enough time for critical section:
111
137
  setTimeout(() => {
112
138
  lsY = this.getItem(y);
113
139
  if (lsY === this.clientId) {
114
- // we have a lock
115
140
  debug(
116
141
  'FastMutex client "%s" won the lock contention on "%s"',
117
142
  this.clientId,
118
143
  key
119
144
  );
120
145
  resolve({
146
+ restartCount,
121
147
  contentionCount,
122
148
  locksLost,
123
- restartCount,
124
149
  });
125
150
  } else {
126
- // we lost the lock, restart the process again
127
151
  restartCount++;
128
152
  locksLost++;
129
153
  debug(
@@ -132,19 +156,19 @@ export class FastMutex {
132
156
  key,
133
157
  lsY
134
158
  );
135
- setTimeout(() => acquireLock(key));
159
+ setTimeout(() => acquireLock(key), 10);
136
160
  }
137
161
  }, 50);
138
162
  return;
139
163
  }
140
164
 
141
- // no contention:
165
+ // No contention: lock is acquired.
142
166
  debug(
143
167
  'FastMutex client "%s" acquired a lock on "%s" with no contention',
144
168
  this.clientId,
145
169
  key
146
170
  );
147
- resolve({ contentionCount, locksLost, restartCount });
171
+ resolve({ restartCount, contentionCount, locksLost });
148
172
  };
149
173
 
150
174
  acquireLock(key);
@@ -0,0 +1,60 @@
1
+ import { ClosedError, Documents, WithContext } from "@peerbit/document";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import * as indexerTypes from "@peerbit/indexer-interface";
4
+ import { debounceLeadingTrailing } from "./utils";
5
+
6
+ type QueryOptons = {
7
+ query: indexerTypes.Query[] | indexerTypes.QueryLike;
8
+ id: string;
9
+ };
10
+ export const useCount = <T extends Record<string, any>>(
11
+ db?: Documents<T, any, any>,
12
+ options?: {
13
+ debounce?: number;
14
+ debug?: boolean; // add debug option here
15
+ } & QueryOptons
16
+ ) => {
17
+ const [count, setCount] = useState<number>(0);
18
+ const countRef = useRef<number>(0);
19
+
20
+ useEffect(() => {
21
+ if (!db || db.closed) {
22
+ return;
23
+ }
24
+
25
+ const _l = async (args?: any) => {
26
+ try {
27
+ const count = await db.count({
28
+ query: options?.query,
29
+ approximate: true,
30
+ });
31
+ countRef.current = count;
32
+ setCount(count);
33
+ } catch (error) {
34
+ if (error instanceof ClosedError) {
35
+ return;
36
+ }
37
+ throw error;
38
+ }
39
+ };
40
+
41
+ const debounced = debounceLeadingTrailing(
42
+ _l,
43
+ options?.debounce ?? 1000
44
+ );
45
+
46
+ const handleChange = () => {
47
+ debounced();
48
+ };
49
+
50
+ debounced();
51
+ db.events.addEventListener("change", handleChange);
52
+
53
+ return () => {
54
+ db.events.removeEventListener("change", handleChange);
55
+ debounced.cancel();
56
+ };
57
+ }, [db?.closed ? undefined : db?.rootAddress, options?.id]);
58
+
59
+ return count;
60
+ };
package/src/useLocal.tsx CHANGED
@@ -1,35 +1,106 @@
1
- import { ClosedError, Documents, SearchRequest } from "@peerbit/document";
2
- import { useEffect, useState } from "react";
3
- export const useLocal = <T extends Record<string, any>>(
4
- db?: Documents<T, any>
1
+ import { ClosedError, Documents, WithContext } from "@peerbit/document";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import * as indexerTypes from "@peerbit/indexer-interface";
4
+ import { debounceLeadingTrailing } from "./utils";
5
+
6
+ type QueryLike = {
7
+ query?: indexerTypes.Query[] | indexerTypes.QueryLike;
8
+ sort?: indexerTypes.Sort[] | indexerTypes.Sort | indexerTypes.SortLike;
9
+ };
10
+ type QueryOptons = {
11
+ query: QueryLike;
12
+ id: string;
13
+ };
14
+
15
+ export const useLocal = <
16
+ T extends Record<string, any>,
17
+ I extends Record<string, any>,
18
+ R extends boolean | undefined = true,
19
+ RT = R extends false ? WithContext<I> : WithContext<T>
20
+ >(
21
+ db?: Documents<T, I>,
22
+ options?: {
23
+ resolve?: R;
24
+ transform?: (result: RT) => Promise<RT>;
25
+ onChanges?: (all: RT[]) => void;
26
+ debounce?: number;
27
+ debug?: boolean; // add debug option here
28
+ } & QueryOptons
5
29
  ) => {
6
- const [all, setAll] = useState<T[]>([]);
30
+ const [all, setAll] = useState<RT[]>([]);
31
+ const emptyResultsRef = useRef(false);
32
+
7
33
  useEffect(() => {
8
34
  if (!db || db.closed) {
9
35
  return;
10
36
  }
11
37
 
12
- const changeListener = async () => {
38
+ const _l = async (args?: any) => {
13
39
  try {
14
- setAll(
15
- await db.index.search(new SearchRequest(), {
16
- local: true,
17
- remote: false,
18
- })
19
- );
40
+ const iterator = db.index.iterate(options?.query ?? {}, {
41
+ local: true,
42
+ remote: false,
43
+ resolve: options?.resolve as any,
44
+ });
45
+
46
+ let results: RT[] = (await iterator.all()) as any;
47
+
48
+ if (options?.transform) {
49
+ results = await Promise.all(
50
+ results.map((x) => options.transform!(x))
51
+ );
52
+ }
53
+ emptyResultsRef.current = results.length === 0;
54
+ setAll(() => {
55
+ options?.onChanges?.(results);
56
+ return results;
57
+ });
20
58
  } catch (error) {
21
59
  if (error instanceof ClosedError) {
22
- // ignore
23
60
  return;
24
61
  }
25
62
  throw error;
26
63
  }
27
64
  };
28
65
 
29
- changeListener();
30
- db.events.addEventListener("change", changeListener);
66
+ const debounced = debounceLeadingTrailing(
67
+ _l,
68
+ options?.debounce ?? 1000
69
+ );
70
+
71
+ let ts = setTimeout(() => {
72
+ _l();
73
+ }, 3000);
74
+
75
+ const handleChange = () => {
76
+ if (emptyResultsRef.current) {
77
+ debounced.cancel();
78
+ if (options?.debug) {
79
+ console.log(
80
+ "Empty results detected. Bypassing debounce for immediate search."
81
+ );
82
+ }
83
+ _l();
84
+ } else {
85
+ debounced();
86
+ }
87
+ };
88
+
89
+ debounced();
90
+ db.events.addEventListener("change", handleChange);
91
+
92
+ return () => {
93
+ db.events.removeEventListener("change", handleChange);
94
+ debounced.cancel();
95
+ clearTimeout(ts);
96
+ };
97
+ }, [
98
+ db?.closed ? undefined : db?.rootAddress,
99
+ options?.id,
100
+ options?.resolve,
101
+ options?.onChanges,
102
+ options?.transform,
103
+ ]);
31
104
 
32
- return () => db.events.addEventListener("change", changeListener);
33
- }, [db?.address, db?.closed]);
34
105
  return all;
35
106
  };
@@ -0,0 +1,66 @@
1
+ import { Program, OpenOptions, ProgramEvents } from "@peerbit/program";
2
+ import { PublicSignKey } from "@peerbit/crypto";
3
+ import { useEffect, useState } from "react";
4
+ const addressOrDefined = <A, B extends ProgramEvents, P extends Program<A, B>>(
5
+ p?: P
6
+ ) => {
7
+ try {
8
+ return p?.rootAddress;
9
+ } catch (error) {
10
+ return !!p;
11
+ }
12
+ };
13
+ type ExtractArgs<T> = T extends Program<infer Args> ? Args : never;
14
+ type ExtractEvents<T> = T extends Program<any, infer Events> ? Events : never;
15
+
16
+ export const useOnline = <
17
+ P extends Program<ExtractArgs<P>, ExtractEvents<P>> &
18
+ Program<any, ProgramEvents>
19
+ >(
20
+ program?: P,
21
+ options?: { id?: string }
22
+ ) => {
23
+ const [peers, setPeers] = useState<PublicSignKey[]>([]);
24
+
25
+ useEffect(() => {
26
+ if (!program || program.closed) {
27
+ return;
28
+ }
29
+ let changeListener: () => void;
30
+
31
+ let closed = false;
32
+ const p = program;
33
+ changeListener = () => {
34
+ p.getReady()
35
+ .then((set) => {
36
+ setPeers([...set.values()]);
37
+ })
38
+ .catch((e) => {
39
+ console.error(e, closed);
40
+ });
41
+ };
42
+ p.events.addEventListener("join", changeListener);
43
+ p.events.addEventListener("leave", changeListener);
44
+ p.getReady()
45
+ .then((set) => {
46
+ setPeers([...set.values()]);
47
+ })
48
+ .catch((e) => {
49
+ console.log("Error getReady()", {
50
+ closed,
51
+ pClosed: p.closed,
52
+ e,
53
+ });
54
+ });
55
+ // TODO AbortController?
56
+
57
+ return () => {
58
+ closed = true;
59
+ p.events.removeEventListener("join", changeListener);
60
+ p.events.removeEventListener("leave", changeListener);
61
+ };
62
+ }, [options?.id, addressOrDefined(program)]);
63
+ return {
64
+ peers,
65
+ };
66
+ };