@componentor/fs 1.2.4 → 1.2.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@componentor/fs",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "description": "A blazing-fast, Node.js-compatible filesystem for the browser using OPFS",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -16,6 +16,70 @@ export interface GetHandleOptions {
16
16
  const FILE_HANDLE_POOL_SIZE = 50
17
17
  const DIR_CACHE_MAX_SIZE = 200
18
18
 
19
+ /**
20
+ * Manages file-level locks to prevent concurrent sync access handle creation.
21
+ * OPFS only allows one sync access handle per file at a time.
22
+ */
23
+ export class FileLockManager {
24
+ private locks: Map<string, Promise<void>> = new Map()
25
+ private lockResolvers: Map<string, () => void> = new Map()
26
+ private waitQueues: Map<string, Array<() => void>> = new Map()
27
+
28
+ /**
29
+ * Acquire an exclusive lock on a file path.
30
+ * If the file is already locked, waits until it's released.
31
+ * Returns a release function that MUST be called when done.
32
+ */
33
+ async acquire(path: string): Promise<() => void> {
34
+ const normalizedPath = normalize(path)
35
+
36
+ // If file is currently locked, wait for it
37
+ while (this.locks.has(normalizedPath)) {
38
+ await this.locks.get(normalizedPath)
39
+ }
40
+
41
+ // Create new lock
42
+ let resolve: () => void
43
+ const lockPromise = new Promise<void>(r => {
44
+ resolve = r
45
+ })
46
+
47
+ this.locks.set(normalizedPath, lockPromise)
48
+ this.lockResolvers.set(normalizedPath, resolve!)
49
+
50
+ // Return release function
51
+ return () => {
52
+ const resolver = this.lockResolvers.get(normalizedPath)
53
+ this.locks.delete(normalizedPath)
54
+ this.lockResolvers.delete(normalizedPath)
55
+ if (resolver) resolver()
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Check if a file is currently locked
61
+ */
62
+ isLocked(path: string): boolean {
63
+ return this.locks.has(normalize(path))
64
+ }
65
+
66
+ /**
67
+ * Clear all locks (use with caution, mainly for cleanup)
68
+ */
69
+ clearAll(): void {
70
+ // Resolve all pending locks
71
+ for (const resolver of this.lockResolvers.values()) {
72
+ resolver()
73
+ }
74
+ this.locks.clear()
75
+ this.lockResolvers.clear()
76
+ this.waitQueues.clear()
77
+ }
78
+ }
79
+
80
+ // Global file lock manager instance
81
+ export const fileLockManager = new FileLockManager()
82
+
19
83
  /**
20
84
  * Manages OPFS handles with caching for improved performance
21
85
  */
package/src/index.ts CHANGED
@@ -24,7 +24,7 @@ import type {
24
24
  import { constants, flagsToString } from './constants.js'
25
25
  import { createENOENT, createEEXIST, createEACCES, createEISDIR, wrapError } from './errors.js'
26
26
  import { normalize, dirname, basename, join, isRoot, segments } from './path-utils.js'
27
- import { HandleManager } from './handle-manager.js'
27
+ import { HandleManager, fileLockManager } from './handle-manager.js'
28
28
  import { SymlinkManager } from './symlink-manager.js'
29
29
  import { PackedStorage } from './packed-storage.js'
30
30
  import { createFileHandle } from './file-handle.js'
@@ -187,11 +187,19 @@ export default class OPFS {
187
187
  let buffer: Uint8Array
188
188
 
189
189
  if (this.useSync) {
190
- const access = await fileHandle.createSyncAccessHandle()
191
- const size = access.getSize()
192
- buffer = new Uint8Array(size)
193
- access.read(buffer)
194
- access.close()
190
+ const releaseLock = await fileLockManager.acquire(resolvedPath)
191
+ try {
192
+ const access = await fileHandle.createSyncAccessHandle()
193
+ try {
194
+ const size = access.getSize()
195
+ buffer = new Uint8Array(size)
196
+ access.read(buffer)
197
+ } finally {
198
+ access.close()
199
+ }
200
+ } finally {
201
+ releaseLock()
202
+ }
195
203
  } else {
196
204
  const file = await fileHandle.getFile()
197
205
  buffer = new Uint8Array(await file.arrayBuffer())
@@ -269,11 +277,19 @@ export default class OPFS {
269
277
 
270
278
  let buffer: Uint8Array
271
279
  if (this.useSync) {
272
- const access = await fileHandle.createSyncAccessHandle()
273
- const size = access.getSize()
274
- buffer = new Uint8Array(size)
275
- access.read(buffer)
276
- access.close()
280
+ const releaseLock = await fileLockManager.acquire(resolvedPath)
281
+ try {
282
+ const access = await fileHandle.createSyncAccessHandle()
283
+ try {
284
+ const size = access.getSize()
285
+ buffer = new Uint8Array(size)
286
+ access.read(buffer)
287
+ } finally {
288
+ access.close()
289
+ }
290
+ } finally {
291
+ releaseLock()
292
+ }
277
293
  } else {
278
294
  const file = await fileHandle.getFile()
279
295
  buffer = new Uint8Array(await file.arrayBuffer())
@@ -310,11 +326,19 @@ export default class OPFS {
310
326
  const buffer = typeof data === 'string' ? new TextEncoder().encode(data) : data
311
327
 
312
328
  if (this.useSync) {
313
- const access = await fileHandle!.createSyncAccessHandle()
314
- // Set exact size (more efficient than truncate(0) + write)
315
- access.truncate(buffer.length)
316
- access.write(buffer, { at: 0 })
317
- access.close()
329
+ const releaseLock = await fileLockManager.acquire(resolvedPath)
330
+ try {
331
+ const access = await fileHandle!.createSyncAccessHandle()
332
+ try {
333
+ // Set exact size (more efficient than truncate(0) + write)
334
+ access.truncate(buffer.length)
335
+ access.write(buffer, { at: 0 })
336
+ } finally {
337
+ access.close()
338
+ }
339
+ } finally {
340
+ releaseLock()
341
+ }
318
342
  } else {
319
343
  const writable = await fileHandle!.createWritable()
320
344
  await writable.write(buffer)
@@ -1036,9 +1060,17 @@ export default class OPFS {
1036
1060
  if (!fileHandle) throw createENOENT(path)
1037
1061
 
1038
1062
  if (this.useSync) {
1039
- const access = await fileHandle.createSyncAccessHandle()
1040
- access.truncate(len)
1041
- access.close()
1063
+ const releaseLock = await fileLockManager.acquire(resolvedPath)
1064
+ try {
1065
+ const access = await fileHandle.createSyncAccessHandle()
1066
+ try {
1067
+ access.truncate(len)
1068
+ } finally {
1069
+ access.close()
1070
+ }
1071
+ } finally {
1072
+ releaseLock()
1073
+ }
1042
1074
  } else {
1043
1075
  const file = await fileHandle.getFile()
1044
1076
  const data = new Uint8Array(await file.arrayBuffer())