@componentor/fs 1.2.5 → 1.2.7

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.5",
3
+ "version": "1.2.7",
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",
@@ -17,68 +17,56 @@ const FILE_HANDLE_POOL_SIZE = 50
17
17
  const DIR_CACHE_MAX_SIZE = 200
18
18
 
19
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.
20
+ * Simple in-memory lock for preventing concurrent sync access handle creation
21
+ * within the same JavaScript context. This is needed because sync access handles
22
+ * are exclusive per file - only one can exist at a time.
23
+ *
24
+ * Optimized for the uncontended case (no Promise creation when lock is free).
22
25
  */
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()
26
+ class FileLock {
27
+ private active = new Set<string>()
28
+ private queues = new Map<string, Array<() => void>>()
27
29
 
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
30
  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)
31
+ if (!this.active.has(path)) {
32
+ // Fast path: no contention, just mark as active
33
+ this.active.add(path)
34
+ return this.createRelease(path)
39
35
  }
40
36
 
41
- // Create new lock
42
- let resolve: () => void
43
- const lockPromise = new Promise<void>(r => {
44
- resolve = r
37
+ // Slow path: wait in queue
38
+ await new Promise<void>(resolve => {
39
+ let queue = this.queues.get(path)
40
+ if (!queue) {
41
+ queue = []
42
+ this.queues.set(path, queue)
43
+ }
44
+ queue.push(resolve)
45
45
  })
46
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))
47
+ return this.createRelease(path)
64
48
  }
65
49
 
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()
50
+ private createRelease(path: string): () => void {
51
+ return () => {
52
+ const queue = this.queues.get(path)
53
+ if (queue && queue.length > 0) {
54
+ // Pass ownership to next waiter
55
+ const next = queue.shift()!
56
+ if (queue.length === 0) {
57
+ this.queues.delete(path)
58
+ }
59
+ next()
60
+ } else {
61
+ // No waiters, release the lock
62
+ this.active.delete(path)
63
+ }
73
64
  }
74
- this.locks.clear()
75
- this.lockResolvers.clear()
76
- this.waitQueues.clear()
77
65
  }
78
66
  }
79
67
 
80
- // Global file lock manager instance
81
- export const fileLockManager = new FileLockManager()
68
+ /** Global file lock instance for sync access handle serialization */
69
+ export const fileLock = new FileLock()
82
70
 
83
71
  /**
84
72
  * Manages OPFS handles with caching for improved performance
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, fileLockManager } from './handle-manager.js'
27
+ import { HandleManager } 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,18 +187,13 @@ export default class OPFS {
187
187
  let buffer: Uint8Array
188
188
 
189
189
  if (this.useSync) {
190
- const releaseLock = await fileLockManager.acquire(resolvedPath)
190
+ const access = await fileHandle.createSyncAccessHandle()
191
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
- }
192
+ const size = access.getSize()
193
+ buffer = new Uint8Array(size)
194
+ access.read(buffer)
200
195
  } finally {
201
- releaseLock()
196
+ access.close()
202
197
  }
203
198
  } else {
204
199
  const file = await fileHandle.getFile()
@@ -277,18 +272,13 @@ export default class OPFS {
277
272
 
278
273
  let buffer: Uint8Array
279
274
  if (this.useSync) {
280
- const releaseLock = await fileLockManager.acquire(resolvedPath)
275
+ const access = await fileHandle.createSyncAccessHandle()
281
276
  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
- }
277
+ const size = access.getSize()
278
+ buffer = new Uint8Array(size)
279
+ access.read(buffer)
290
280
  } finally {
291
- releaseLock()
281
+ access.close()
292
282
  }
293
283
  } else {
294
284
  const file = await fileHandle.getFile()
@@ -326,18 +316,13 @@ export default class OPFS {
326
316
  const buffer = typeof data === 'string' ? new TextEncoder().encode(data) : data
327
317
 
328
318
  if (this.useSync) {
329
- const releaseLock = await fileLockManager.acquire(resolvedPath)
319
+ const access = await fileHandle!.createSyncAccessHandle()
330
320
  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
- }
321
+ // Set exact size (more efficient than truncate(0) + write)
322
+ access.truncate(buffer.length)
323
+ access.write(buffer, { at: 0 })
339
324
  } finally {
340
- releaseLock()
325
+ access.close()
341
326
  }
342
327
  } else {
343
328
  const writable = await fileHandle!.createWritable()
@@ -1060,16 +1045,11 @@ export default class OPFS {
1060
1045
  if (!fileHandle) throw createENOENT(path)
1061
1046
 
1062
1047
  if (this.useSync) {
1063
- const releaseLock = await fileLockManager.acquire(resolvedPath)
1048
+ const access = await fileHandle.createSyncAccessHandle()
1064
1049
  try {
1065
- const access = await fileHandle.createSyncAccessHandle()
1066
- try {
1067
- access.truncate(len)
1068
- } finally {
1069
- access.close()
1070
- }
1050
+ access.truncate(len)
1071
1051
  } finally {
1072
- releaseLock()
1052
+ access.close()
1073
1053
  }
1074
1054
  } else {
1075
1055
  const file = await fileHandle.getFile()
@@ -15,7 +15,7 @@
15
15
  */
16
16
 
17
17
  import type { HandleManager } from './handle-manager.js'
18
- import { fileLockManager } from './handle-manager.js'
18
+ import { fileLock } from './handle-manager.js'
19
19
  import { createECORRUPTED } from './errors.js'
20
20
 
21
21
  // ============ Compression ============
@@ -174,7 +174,7 @@ export class PackedStorage {
174
174
  }
175
175
 
176
176
  if (this.useSync) {
177
- const releaseLock = await fileLockManager.acquire(PACK_FILE)
177
+ const release = await fileLock.acquire(PACK_FILE)
178
178
  try {
179
179
  const access = await fileHandle.createSyncAccessHandle()
180
180
  try {
@@ -210,7 +210,7 @@ export class PackedStorage {
210
210
  access.close()
211
211
  }
212
212
  } finally {
213
- releaseLock()
213
+ release()
214
214
  }
215
215
  } else {
216
216
  const file = await fileHandle.getFile()
@@ -274,7 +274,7 @@ export class PackedStorage {
274
274
  let buffer: Uint8Array
275
275
 
276
276
  if (this.useSync) {
277
- const releaseLock = await fileLockManager.acquire(PACK_FILE)
277
+ const release = await fileLock.acquire(PACK_FILE)
278
278
  try {
279
279
  const access = await fileHandle.createSyncAccessHandle()
280
280
  try {
@@ -284,7 +284,7 @@ export class PackedStorage {
284
284
  access.close()
285
285
  }
286
286
  } finally {
287
- releaseLock()
287
+ release()
288
288
  }
289
289
  } else {
290
290
  const file = await fileHandle.getFile()
@@ -336,7 +336,7 @@ export class PackedStorage {
336
336
  const decompressPromises: Array<{ path: string; promise: Promise<Uint8Array> }> = []
337
337
 
338
338
  if (this.useSync) {
339
- const releaseLock = await fileLockManager.acquire(PACK_FILE)
339
+ const release = await fileLock.acquire(PACK_FILE)
340
340
  try {
341
341
  const access = await fileHandle.createSyncAccessHandle()
342
342
  try {
@@ -355,7 +355,7 @@ export class PackedStorage {
355
355
  access.close()
356
356
  }
357
357
  } finally {
358
- releaseLock()
358
+ release()
359
359
  }
360
360
  } else {
361
361
  const file = await fileHandle.getFile()
@@ -475,7 +475,7 @@ export class PackedStorage {
475
475
  if (!fileHandle) return
476
476
 
477
477
  if (this.useSync) {
478
- const releaseLock = await fileLockManager.acquire(PACK_FILE)
478
+ const release = await fileLock.acquire(PACK_FILE)
479
479
  try {
480
480
  const access = await fileHandle.createSyncAccessHandle()
481
481
  try {
@@ -485,7 +485,7 @@ export class PackedStorage {
485
485
  access.close()
486
486
  }
487
487
  } finally {
488
- releaseLock()
488
+ release()
489
489
  }
490
490
  } else {
491
491
  const writable = await fileHandle.createWritable()
@@ -512,7 +512,7 @@ export class PackedStorage {
512
512
  const newIndexBuf = encoder.encode(JSON.stringify(index))
513
513
 
514
514
  if (this.useSync) {
515
- const releaseLock = await fileLockManager.acquire(PACK_FILE)
515
+ const release = await fileLock.acquire(PACK_FILE)
516
516
  try {
517
517
  const access = await fileHandle.createSyncAccessHandle()
518
518
  try {
@@ -558,7 +558,7 @@ export class PackedStorage {
558
558
  access.close()
559
559
  }
560
560
  } finally {
561
- releaseLock()
561
+ release()
562
562
  }
563
563
  } else {
564
564
  // For non-sync, rewrite the whole file
@@ -1,6 +1,6 @@
1
1
  import type { SymlinkCache, SymlinkDefinition } from './types.js'
2
2
  import type { HandleManager } from './handle-manager.js'
3
- import { fileLockManager } from './handle-manager.js'
3
+ import { fileLock } from './handle-manager.js'
4
4
  import { normalize } from './path-utils.js'
5
5
  import { createELOOP, createEINVAL, createEEXIST } from './errors.js'
6
6
 
@@ -102,7 +102,7 @@ export class SymlinkManager {
102
102
  const buffer = new TextEncoder().encode(data)
103
103
 
104
104
  if (this.useSync) {
105
- const releaseLock = await fileLockManager.acquire(SYMLINK_FILE)
105
+ const release = await fileLock.acquire(SYMLINK_FILE)
106
106
  try {
107
107
  const access = await fileHandle.createSyncAccessHandle()
108
108
  try {
@@ -115,7 +115,7 @@ export class SymlinkManager {
115
115
  access.close()
116
116
  }
117
117
  } finally {
118
- releaseLock()
118
+ release()
119
119
  }
120
120
  } else {
121
121
  const writable = await fileHandle.createWritable()