Skip to content

Commit a31a4e9

Browse files
committed
feat(web): async disposable semaphore
1 parent 3f33f67 commit a31a4e9

File tree

4 files changed

+104
-84
lines changed

4 files changed

+104
-84
lines changed

packages/filen-web/eslint.config.mjs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,16 @@ export default [
5757
"@typescript-eslint/ban-types": "off",
5858
"react/react-in-jsx-scope": "off",
5959
"react/prop-types": "off",
60-
"react-compiler/react-compiler": "error"
60+
"react-compiler/react-compiler": "error",
61+
"@typescript-eslint/no-unused-vars": [
62+
"error",
63+
{
64+
argsIgnorePattern: "^_",
65+
varsIgnorePattern: "^_",
66+
caughtErrorsIgnorePattern: "^_",
67+
destructuredArrayIgnorePattern: "^_"
68+
}
69+
]
6170
}
6271
},
6372
{

packages/filen-web/src/components/socket.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ export const Socket = memo(() => {
1111
const listenerRef = useRef<EventListenerHandle | undefined>(undefined)
1212

1313
const registerListener = useCallback(async () => {
14-
try {
15-
if (listenerRef.current || !sdk) {
16-
return
17-
}
14+
if (listenerRef.current || !sdk) {
15+
return
16+
}
1817

18+
try {
1919
listenerRef.current = await sdk.addSocketListener(null, async e => {
2020
try {
2121
switch (e.type) {
Lines changed: 42 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,47 @@
1-
/**
2-
* Semaphore
3-
*
4-
* @export
5-
* @class Semaphore
6-
* @typedef {Semaphore}
7-
*/
1+
export class DisposeSemaphoreWrapper implements AsyncDisposable {
2+
private readonly semaphore: Semaphore
3+
private released: boolean = false
4+
5+
public constructor(semaphore: Semaphore) {
6+
this.semaphore = semaphore
7+
}
8+
9+
async [Symbol.asyncDispose](): Promise<void> {
10+
if (this.released) {
11+
return Promise.resolve()
12+
}
13+
14+
this.semaphore.release()
15+
16+
this.released = true
17+
18+
return Promise.resolve()
19+
}
20+
}
21+
822
export class Semaphore {
923
private counter: number = 0
1024
private waiting: Array<{
11-
resolve: (value: void | PromiseLike<void>) => void
25+
resolve: (value: DisposeSemaphoreWrapper | PromiseLike<DisposeSemaphoreWrapper>) => void
1226
reject: (reason?: unknown) => void
1327
}> = []
1428
private maxCount: number
1529

16-
/**
17-
* Creates an instance of Semaphore.
18-
*
19-
* @constructor
20-
* @public
21-
* @param {number} [max=1]
22-
*/
2330
public constructor(max: number = 1) {
31+
if (max < 1) {
32+
throw new Error("Max must be at least 1")
33+
}
34+
2435
this.maxCount = max
2536
}
2637

27-
/**
28-
* Acquire a lock.
29-
*
30-
* @public
31-
* @returns {Promise<void>}
32-
*/
33-
public acquire(): Promise<void> {
38+
public acquire(): Promise<DisposeSemaphoreWrapper> {
3439
if (this.counter < this.maxCount) {
3540
this.counter++
3641

37-
return Promise.resolve()
42+
return Promise.resolve(new DisposeSemaphoreWrapper(this))
3843
} else {
39-
return new Promise<void>((resolve, reject) => {
44+
return new Promise<DisposeSemaphoreWrapper>((resolve, reject) => {
4045
this.waiting.push({
4146
resolve,
4247
reject
@@ -45,11 +50,6 @@ export class Semaphore {
4550
}
4651
}
4752

48-
/**
49-
* Release a lock.
50-
*
51-
* @public
52-
*/
5353
public release(): void {
5454
if (this.counter <= 0) {
5555
return
@@ -60,63 +60,29 @@ export class Semaphore {
6060
this.processQueue()
6161
}
6262

63-
/**
64-
* Returns the locks in the queue.
65-
*
66-
* @public
67-
* @returns {number}
68-
*/
69-
public count(): number {
70-
return this.counter
71-
}
72-
73-
/**
74-
* Set max number of concurrent locks.
75-
*
76-
* @public
77-
* @param {number} newMax
78-
*/
79-
public setMax(newMax: number): void {
80-
this.maxCount = newMax
81-
82-
this.processQueue()
83-
}
84-
85-
/**
86-
* Purge all waiting promises.
87-
*
88-
* @public
89-
* @returns {number}
90-
*/
91-
public purge(): number {
92-
const unresolved = this.waiting.length
93-
94-
for (const waiter of this.waiting) {
95-
waiter.reject("Task has been purged")
96-
}
97-
98-
this.counter = 0
99-
this.waiting = []
100-
101-
return unresolved
102-
}
103-
104-
/**
105-
* Internal process queue.
106-
*
107-
* @private
108-
*/
10963
private processQueue(): void {
11064
if (this.waiting.length > 0 && this.counter < this.maxCount) {
11165
this.counter++
11266

11367
const waiter = this.waiting.shift()
11468

11569
if (waiter) {
116-
waiter.resolve()
70+
waiter.resolve(new DisposeSemaphoreWrapper(this))
11771
}
11872
}
11973
}
12074
}
12175

76+
export class Mutex {
77+
private semaphore: Semaphore = new Semaphore(1)
78+
79+
public acquire(): Promise<DisposeSemaphoreWrapper> {
80+
return this.semaphore.acquire()
81+
}
82+
83+
public release(): void {
84+
this.semaphore.release()
85+
}
86+
}
87+
12288
export default Semaphore

packages/filen-web/src/queries/client.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ export const queryClientPersisterKv = {
2323
return await idb.get<T>(`${QUERY_CLIENT_PERSISTER_PREFIX}-${key}`)
2424
},
2525
setItem: async (key: string, value: unknown): Promise<void> => {
26+
if (!value) {
27+
return
28+
}
29+
2630
await persisterMutex.acquire()
2731

2832
try {
@@ -35,7 +39,7 @@ export const queryClientPersisterKv = {
3539
await persisterMutex.acquire()
3640

3741
try {
38-
return await idb.remove(`${QUERY_CLIENT_PERSISTER_PREFIX}-${key}`)
42+
await idb.remove(`${QUERY_CLIENT_PERSISTER_PREFIX}-${key}`)
3943
} finally {
4044
persisterMutex.release()
4145
}
@@ -130,29 +134,70 @@ export const DEFAULT_QUERY_OPTIONS: Pick<
130134
| "refetchIntervalInBackground"
131135
| "retry"
132136
| "retryDelay"
137+
| "networkMode"
138+
| "notifyOnChangeProps"
133139
> = {
134140
refetchOnMount: "always",
135141
refetchOnReconnect: "always",
136142
refetchOnWindowFocus: "always",
137143
staleTime: 0,
138144
gcTime: QUERY_CLIENT_CACHE_TIME,
139145
refetchInterval: false,
140-
experimental_prefetchInRender: true,
146+
experimental_prefetchInRender: false,
141147
refetchIntervalInBackground: false,
142148
retry: true,
143149
retryDelay: 1000,
144150
retryOnMount: true,
151+
networkMode: "always",
145152
throwOnError(err) {
146153
console.error(err)
147154

148155
return false
149156
}
150-
} as const
157+
} as Omit<UseQueryOptions, "queryKey" | "queryFn">
158+
159+
export const DEFAULT_QUERY_OPTIONS_ETERNAL: Pick<
160+
UseQueryOptions,
161+
| "refetchOnMount"
162+
| "refetchOnReconnect"
163+
| "refetchOnWindowFocus"
164+
| "staleTime"
165+
| "gcTime"
166+
| "refetchInterval"
167+
| "throwOnError"
168+
| "retryOnMount"
169+
| "experimental_prefetchInRender"
170+
| "refetchIntervalInBackground"
171+
| "retry"
172+
| "retryDelay"
173+
| "networkMode"
174+
| "notifyOnChangeProps"
175+
> = {
176+
notifyOnChangeProps: undefined,
177+
refetchOnMount: false,
178+
refetchOnReconnect: false,
179+
refetchOnWindowFocus: false,
180+
staleTime: Infinity,
181+
gcTime: Infinity,
182+
refetchInterval: false,
183+
experimental_prefetchInRender: false,
184+
refetchIntervalInBackground: false,
185+
retry: true,
186+
retryDelay: 1000,
187+
retryOnMount: true,
188+
networkMode: "always",
189+
throwOnError(err) {
190+
console.error(err)
191+
192+
return false
193+
}
194+
} as Omit<UseQueryOptions, "queryKey" | "queryFn">
151195

152196
export const queryClient = new QueryClient({
153197
defaultOptions: {
154198
queries: {
155199
...DEFAULT_QUERY_OPTIONS,
200+
persister: queryClientPersister.persisterFn,
156201
queryKeyHashFn: queryKey => pack(queryKey).toString("base64")
157202
}
158203
}

0 commit comments

Comments
 (0)