@componentor/fs 1.2.5 → 1.2.6

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.6",
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,70 +16,6 @@ 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
-
83
19
  /**
84
20
  * Manages OPFS handles with caching for improved performance
85
21
  */
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,6 @@
15
15
  */
16
16
 
17
17
  import type { HandleManager } from './handle-manager.js'
18
- import { fileLockManager } from './handle-manager.js'
19
18
  import { createECORRUPTED } from './errors.js'
20
19
 
21
20
  // ============ Compression ============
@@ -174,43 +173,38 @@ export class PackedStorage {
174
173
  }
175
174
 
176
175
  if (this.useSync) {
177
- const releaseLock = await fileLockManager.acquire(PACK_FILE)
176
+ const access = await fileHandle.createSyncAccessHandle()
178
177
  try {
179
- const access = await fileHandle.createSyncAccessHandle()
180
- try {
181
- const size = access.getSize()
182
- if (size < 8) {
183
- return {}
184
- }
178
+ const size = access.getSize()
179
+ if (size < 8) {
180
+ return {}
181
+ }
185
182
 
186
- // Read header: index length + CRC32
187
- const header = new Uint8Array(8)
188
- access.read(header, { at: 0 })
189
- const view = new DataView(header.buffer)
190
- const indexLen = view.getUint32(0, true)
191
- const storedCrc = view.getUint32(4, true)
192
-
193
- // Read everything after header (index + data) for CRC verification
194
- const contentSize = size - 8
195
- const content = new Uint8Array(contentSize)
196
- access.read(content, { at: 8 })
197
-
198
- // Verify CRC32 if enabled
199
- if (this.useChecksum && storedCrc !== 0) {
200
- const calculatedCrc = crc32(content)
201
- if (calculatedCrc !== storedCrc) {
202
- throw createECORRUPTED(PACK_FILE)
203
- }
183
+ // Read header: index length + CRC32
184
+ const header = new Uint8Array(8)
185
+ access.read(header, { at: 0 })
186
+ const view = new DataView(header.buffer)
187
+ const indexLen = view.getUint32(0, true)
188
+ const storedCrc = view.getUint32(4, true)
189
+
190
+ // Read everything after header (index + data) for CRC verification
191
+ const contentSize = size - 8
192
+ const content = new Uint8Array(contentSize)
193
+ access.read(content, { at: 8 })
194
+
195
+ // Verify CRC32 if enabled
196
+ if (this.useChecksum && storedCrc !== 0) {
197
+ const calculatedCrc = crc32(content)
198
+ if (calculatedCrc !== storedCrc) {
199
+ throw createECORRUPTED(PACK_FILE)
204
200
  }
205
-
206
- // Parse index from content
207
- const indexJson = new TextDecoder().decode(content.subarray(0, indexLen))
208
- return JSON.parse(indexJson)
209
- } finally {
210
- access.close()
211
201
  }
202
+
203
+ // Parse index from content
204
+ const indexJson = new TextDecoder().decode(content.subarray(0, indexLen))
205
+ return JSON.parse(indexJson)
212
206
  } finally {
213
- releaseLock()
207
+ access.close()
214
208
  }
215
209
  } else {
216
210
  const file = await fileHandle.getFile()
@@ -274,17 +268,12 @@ export class PackedStorage {
274
268
  let buffer: Uint8Array
275
269
 
276
270
  if (this.useSync) {
277
- const releaseLock = await fileLockManager.acquire(PACK_FILE)
271
+ const access = await fileHandle.createSyncAccessHandle()
278
272
  try {
279
- const access = await fileHandle.createSyncAccessHandle()
280
- try {
281
- buffer = new Uint8Array(entry.size)
282
- access.read(buffer, { at: entry.offset })
283
- } finally {
284
- access.close()
285
- }
273
+ buffer = new Uint8Array(entry.size)
274
+ access.read(buffer, { at: entry.offset })
286
275
  } finally {
287
- releaseLock()
276
+ access.close()
288
277
  }
289
278
  } else {
290
279
  const file = await fileHandle.getFile()
@@ -336,26 +325,21 @@ export class PackedStorage {
336
325
  const decompressPromises: Array<{ path: string; promise: Promise<Uint8Array> }> = []
337
326
 
338
327
  if (this.useSync) {
339
- const releaseLock = await fileLockManager.acquire(PACK_FILE)
328
+ const access = await fileHandle.createSyncAccessHandle()
340
329
  try {
341
- const access = await fileHandle.createSyncAccessHandle()
342
- try {
343
- for (const { path, offset, size, originalSize } of toRead) {
344
- const buffer = new Uint8Array(size)
345
- access.read(buffer, { at: offset })
346
-
347
- if (originalSize !== undefined) {
348
- // Queue for decompression
349
- decompressPromises.push({ path, promise: decompress(buffer) })
350
- } else {
351
- results.set(path, buffer)
352
- }
330
+ for (const { path, offset, size, originalSize } of toRead) {
331
+ const buffer = new Uint8Array(size)
332
+ access.read(buffer, { at: offset })
333
+
334
+ if (originalSize !== undefined) {
335
+ // Queue for decompression
336
+ decompressPromises.push({ path, promise: decompress(buffer) })
337
+ } else {
338
+ results.set(path, buffer)
353
339
  }
354
- } finally {
355
- access.close()
356
340
  }
357
341
  } finally {
358
- releaseLock()
342
+ access.close()
359
343
  }
360
344
  } else {
361
345
  const file = await fileHandle.getFile()
@@ -475,17 +459,12 @@ export class PackedStorage {
475
459
  if (!fileHandle) return
476
460
 
477
461
  if (this.useSync) {
478
- const releaseLock = await fileLockManager.acquire(PACK_FILE)
462
+ const access = await fileHandle.createSyncAccessHandle()
479
463
  try {
480
- const access = await fileHandle.createSyncAccessHandle()
481
- try {
482
- access.truncate(data.length)
483
- access.write(data, { at: 0 })
484
- } finally {
485
- access.close()
486
- }
464
+ access.truncate(data.length)
465
+ access.write(data, { at: 0 })
487
466
  } finally {
488
- releaseLock()
467
+ access.close()
489
468
  }
490
469
  } else {
491
470
  const writable = await fileHandle.createWritable()
@@ -512,53 +491,48 @@ export class PackedStorage {
512
491
  const newIndexBuf = encoder.encode(JSON.stringify(index))
513
492
 
514
493
  if (this.useSync) {
515
- const releaseLock = await fileLockManager.acquire(PACK_FILE)
494
+ const access = await fileHandle.createSyncAccessHandle()
516
495
  try {
517
- const access = await fileHandle.createSyncAccessHandle()
518
- try {
519
- const size = access.getSize()
520
-
521
- // Read old header to get old index length
522
- const oldHeader = new Uint8Array(8)
523
- access.read(oldHeader, { at: 0 })
524
- const oldIndexLen = new DataView(oldHeader.buffer).getUint32(0, true)
525
-
526
- // Read data portion (after old index)
527
- const dataStart = 8 + oldIndexLen
528
- const dataSize = size - dataStart
529
- const dataPortion = new Uint8Array(dataSize)
530
- if (dataSize > 0) {
531
- access.read(dataPortion, { at: dataStart })
532
- }
496
+ const size = access.getSize()
497
+
498
+ // Read old header to get old index length
499
+ const oldHeader = new Uint8Array(8)
500
+ access.read(oldHeader, { at: 0 })
501
+ const oldIndexLen = new DataView(oldHeader.buffer).getUint32(0, true)
502
+
503
+ // Read data portion (after old index)
504
+ const dataStart = 8 + oldIndexLen
505
+ const dataSize = size - dataStart
506
+ const dataPortion = new Uint8Array(dataSize)
507
+ if (dataSize > 0) {
508
+ access.read(dataPortion, { at: dataStart })
509
+ }
533
510
 
534
- // Build new content (new index + data)
535
- const newContent = new Uint8Array(newIndexBuf.length + dataSize)
536
- newContent.set(newIndexBuf, 0)
537
- if (dataSize > 0) {
538
- newContent.set(dataPortion, newIndexBuf.length)
539
- }
511
+ // Build new content (new index + data)
512
+ const newContent = new Uint8Array(newIndexBuf.length + dataSize)
513
+ newContent.set(newIndexBuf, 0)
514
+ if (dataSize > 0) {
515
+ newContent.set(dataPortion, newIndexBuf.length)
516
+ }
540
517
 
541
- // Calculate new CRC32 if enabled
542
- const checksum = this.useChecksum ? crc32(newContent) : 0
518
+ // Calculate new CRC32 if enabled
519
+ const checksum = this.useChecksum ? crc32(newContent) : 0
543
520
 
544
- // Build new header
545
- const newHeader = new Uint8Array(8)
546
- const view = new DataView(newHeader.buffer)
547
- view.setUint32(0, newIndexBuf.length, true)
548
- view.setUint32(4, checksum, true)
521
+ // Build new header
522
+ const newHeader = new Uint8Array(8)
523
+ const view = new DataView(newHeader.buffer)
524
+ view.setUint32(0, newIndexBuf.length, true)
525
+ view.setUint32(4, checksum, true)
549
526
 
550
- // Write new file
551
- const newFile = new Uint8Array(8 + newContent.length)
552
- newFile.set(newHeader, 0)
553
- newFile.set(newContent, 8)
527
+ // Write new file
528
+ const newFile = new Uint8Array(8 + newContent.length)
529
+ newFile.set(newHeader, 0)
530
+ newFile.set(newContent, 8)
554
531
 
555
- access.truncate(newFile.length)
556
- access.write(newFile, { at: 0 })
557
- } finally {
558
- access.close()
559
- }
532
+ access.truncate(newFile.length)
533
+ access.write(newFile, { at: 0 })
560
534
  } finally {
561
- releaseLock()
535
+ access.close()
562
536
  }
563
537
  } else {
564
538
  // For non-sync, rewrite the whole file
@@ -1,6 +1,5 @@
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'
4
3
  import { normalize } from './path-utils.js'
5
4
  import { createELOOP, createEINVAL, createEEXIST } from './errors.js'
6
5
 
@@ -102,20 +101,15 @@ export class SymlinkManager {
102
101
  const buffer = new TextEncoder().encode(data)
103
102
 
104
103
  if (this.useSync) {
105
- const releaseLock = await fileLockManager.acquire(SYMLINK_FILE)
104
+ const access = await fileHandle.createSyncAccessHandle()
106
105
  try {
107
- const access = await fileHandle.createSyncAccessHandle()
108
- try {
109
- access.truncate(0)
110
- let written = 0
111
- while (written < buffer.length) {
112
- written += access.write(buffer.subarray(written), { at: written })
113
- }
114
- } finally {
115
- access.close()
106
+ access.truncate(0)
107
+ let written = 0
108
+ while (written < buffer.length) {
109
+ written += access.write(buffer.subarray(written), { at: written })
116
110
  }
117
111
  } finally {
118
- releaseLock()
112
+ access.close()
119
113
  }
120
114
  } else {
121
115
  const writable = await fileHandle.createWritable()