@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.
@@ -15,6 +15,7 @@
15
15
  */
16
16
 
17
17
  import type { HandleManager } from './handle-manager.js'
18
+ import { fileLockManager } from './handle-manager.js'
18
19
  import { createECORRUPTED } from './errors.js'
19
20
 
20
21
  // ============ Compression ============
@@ -128,7 +129,6 @@ export class PackedStorage {
128
129
  private useChecksum: boolean
129
130
  private index: PackIndex | null = null
130
131
  private indexLoaded = false
131
- private lockPromise: Promise<void> | null = null
132
132
 
133
133
  constructor(handleManager: HandleManager, useSync: boolean, useCompression = false, useChecksum = true) {
134
134
  this.handleManager = handleManager
@@ -138,23 +138,6 @@ export class PackedStorage {
138
138
  this.useChecksum = useChecksum
139
139
  }
140
140
 
141
- /**
142
- * Acquire lock for pack file access (prevents concurrent handle conflicts)
143
- */
144
- private async acquireLock(): Promise<() => void> {
145
- while (this.lockPromise) {
146
- await this.lockPromise
147
- }
148
- let release: () => void
149
- this.lockPromise = new Promise(resolve => {
150
- release = () => {
151
- this.lockPromise = null
152
- resolve()
153
- }
154
- })
155
- return release!
156
- }
157
-
158
141
  /**
159
142
  * Reset pack storage state (memory only)
160
143
  */
@@ -191,38 +174,43 @@ export class PackedStorage {
191
174
  }
192
175
 
193
176
  if (this.useSync) {
194
- const access = await fileHandle.createSyncAccessHandle()
177
+ const releaseLock = await fileLockManager.acquire(PACK_FILE)
195
178
  try {
196
- const size = access.getSize()
197
- if (size < 8) {
198
- return {}
199
- }
179
+ const access = await fileHandle.createSyncAccessHandle()
180
+ try {
181
+ const size = access.getSize()
182
+ if (size < 8) {
183
+ return {}
184
+ }
200
185
 
201
- // Read header: index length + CRC32
202
- const header = new Uint8Array(8)
203
- access.read(header, { at: 0 })
204
- const view = new DataView(header.buffer)
205
- const indexLen = view.getUint32(0, true)
206
- const storedCrc = view.getUint32(4, true)
207
-
208
- // Read everything after header (index + data) for CRC verification
209
- const contentSize = size - 8
210
- const content = new Uint8Array(contentSize)
211
- access.read(content, { at: 8 })
212
-
213
- // Verify CRC32 if enabled
214
- if (this.useChecksum && storedCrc !== 0) {
215
- const calculatedCrc = crc32(content)
216
- if (calculatedCrc !== storedCrc) {
217
- throw createECORRUPTED(PACK_FILE)
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
+ }
218
204
  }
219
- }
220
205
 
221
- // Parse index from content
222
- const indexJson = new TextDecoder().decode(content.subarray(0, indexLen))
223
- return JSON.parse(indexJson)
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
+ }
224
212
  } finally {
225
- access.close()
213
+ releaseLock()
226
214
  }
227
215
  } else {
228
216
  const file = await fileHandle.getFile()
@@ -256,13 +244,8 @@ export class PackedStorage {
256
244
  * Check if a path exists in the pack
257
245
  */
258
246
  async has(path: string): Promise<boolean> {
259
- const release = await this.acquireLock()
260
- try {
261
- const index = await this.loadIndex()
262
- return path in index
263
- } finally {
264
- release()
265
- }
247
+ const index = await this.loadIndex()
248
+ return path in index
266
249
  }
267
250
 
268
251
  /**
@@ -270,15 +253,10 @@ export class PackedStorage {
270
253
  * Returns originalSize if compressed, otherwise size
271
254
  */
272
255
  async getSize(path: string): Promise<number | null> {
273
- const release = await this.acquireLock()
274
- try {
275
- const index = await this.loadIndex()
276
- const entry = index[path]
277
- if (!entry) return null
278
- return entry.originalSize ?? entry.size
279
- } finally {
280
- release()
281
- }
256
+ const index = await this.loadIndex()
257
+ const entry = index[path]
258
+ if (!entry) return null
259
+ return entry.originalSize ?? entry.size
282
260
  }
283
261
 
284
262
  /**
@@ -286,18 +264,18 @@ export class PackedStorage {
286
264
  * Handles decompression if file was stored compressed
287
265
  */
288
266
  async read(path: string): Promise<Uint8Array | null> {
289
- const release = await this.acquireLock()
290
- try {
291
- const index = await this.loadIndex()
292
- const entry = index[path]
293
- if (!entry) return null
267
+ const index = await this.loadIndex()
268
+ const entry = index[path]
269
+ if (!entry) return null
294
270
 
295
- const { fileHandle } = await this.handleManager.getHandle(PACK_FILE)
296
- if (!fileHandle) return null
271
+ const { fileHandle } = await this.handleManager.getHandle(PACK_FILE)
272
+ if (!fileHandle) return null
297
273
 
298
- let buffer: Uint8Array
274
+ let buffer: Uint8Array
299
275
 
300
- if (this.useSync) {
276
+ if (this.useSync) {
277
+ const releaseLock = await fileLockManager.acquire(PACK_FILE)
278
+ try {
301
279
  const access = await fileHandle.createSyncAccessHandle()
302
280
  try {
303
281
  buffer = new Uint8Array(entry.size)
@@ -305,21 +283,21 @@ export class PackedStorage {
305
283
  } finally {
306
284
  access.close()
307
285
  }
308
- } else {
309
- const file = await fileHandle.getFile()
310
- const data = new Uint8Array(await file.arrayBuffer())
311
- buffer = data.slice(entry.offset, entry.offset + entry.size)
312
- }
313
-
314
- // Decompress if needed
315
- if (entry.originalSize !== undefined) {
316
- return decompress(buffer)
286
+ } finally {
287
+ releaseLock()
317
288
  }
289
+ } else {
290
+ const file = await fileHandle.getFile()
291
+ const data = new Uint8Array(await file.arrayBuffer())
292
+ buffer = data.slice(entry.offset, entry.offset + entry.size)
293
+ }
318
294
 
319
- return buffer
320
- } finally {
321
- release()
295
+ // Decompress if needed
296
+ if (entry.originalSize !== undefined) {
297
+ return decompress(buffer)
322
298
  }
299
+
300
+ return buffer
323
301
  }
324
302
 
325
303
  /**
@@ -331,35 +309,35 @@ export class PackedStorage {
331
309
  const results = new Map<string, Uint8Array | null>()
332
310
  if (paths.length === 0) return results
333
311
 
334
- const release = await this.acquireLock()
335
- try {
336
- const index = await this.loadIndex()
337
-
338
- // Find which paths are in the pack
339
- const toRead: Array<{ path: string; offset: number; size: number; originalSize?: number }> = []
340
- for (const path of paths) {
341
- const entry = index[path]
342
- if (entry) {
343
- toRead.push({ path, offset: entry.offset, size: entry.size, originalSize: entry.originalSize })
344
- } else {
345
- results.set(path, null)
346
- }
312
+ const index = await this.loadIndex()
313
+
314
+ // Find which paths are in the pack
315
+ const toRead: Array<{ path: string; offset: number; size: number; originalSize?: number }> = []
316
+ for (const path of paths) {
317
+ const entry = index[path]
318
+ if (entry) {
319
+ toRead.push({ path, offset: entry.offset, size: entry.size, originalSize: entry.originalSize })
320
+ } else {
321
+ results.set(path, null)
347
322
  }
323
+ }
348
324
 
349
- if (toRead.length === 0) return results
325
+ if (toRead.length === 0) return results
350
326
 
351
- const { fileHandle } = await this.handleManager.getHandle(PACK_FILE)
352
- if (!fileHandle) {
353
- for (const { path } of toRead) {
354
- results.set(path, null)
355
- }
356
- return results
327
+ const { fileHandle } = await this.handleManager.getHandle(PACK_FILE)
328
+ if (!fileHandle) {
329
+ for (const { path } of toRead) {
330
+ results.set(path, null)
357
331
  }
332
+ return results
333
+ }
358
334
 
359
- // Read all files
360
- const decompressPromises: Array<{ path: string; promise: Promise<Uint8Array> }> = []
335
+ // Read all files
336
+ const decompressPromises: Array<{ path: string; promise: Promise<Uint8Array> }> = []
361
337
 
362
- if (this.useSync) {
338
+ if (this.useSync) {
339
+ const releaseLock = await fileLockManager.acquire(PACK_FILE)
340
+ try {
363
341
  const access = await fileHandle.createSyncAccessHandle()
364
342
  try {
365
343
  for (const { path, offset, size, originalSize } of toRead) {
@@ -376,29 +354,29 @@ export class PackedStorage {
376
354
  } finally {
377
355
  access.close()
378
356
  }
379
- } else {
380
- const file = await fileHandle.getFile()
381
- const data = new Uint8Array(await file.arrayBuffer())
382
- for (const { path, offset, size, originalSize } of toRead) {
383
- const buffer = data.slice(offset, offset + size)
384
-
385
- if (originalSize !== undefined) {
386
- decompressPromises.push({ path, promise: decompress(buffer) })
387
- } else {
388
- results.set(path, buffer)
389
- }
390
- }
357
+ } finally {
358
+ releaseLock()
391
359
  }
360
+ } else {
361
+ const file = await fileHandle.getFile()
362
+ const data = new Uint8Array(await file.arrayBuffer())
363
+ for (const { path, offset, size, originalSize } of toRead) {
364
+ const buffer = data.slice(offset, offset + size)
392
365
 
393
- // Wait for all decompressions
394
- for (const { path, promise } of decompressPromises) {
395
- results.set(path, await promise)
366
+ if (originalSize !== undefined) {
367
+ decompressPromises.push({ path, promise: decompress(buffer) })
368
+ } else {
369
+ results.set(path, buffer)
370
+ }
396
371
  }
372
+ }
397
373
 
398
- return results
399
- } finally {
400
- release()
374
+ // Wait for all decompressions
375
+ for (const { path, promise } of decompressPromises) {
376
+ results.set(path, await promise)
401
377
  }
378
+
379
+ return results
402
380
  }
403
381
 
404
382
  /**
@@ -411,86 +389,81 @@ export class PackedStorage {
411
389
  async writeBatch(entries: Array<{ path: string; data: Uint8Array }>): Promise<void> {
412
390
  if (entries.length === 0) return
413
391
 
414
- const release = await this.acquireLock()
415
- try {
416
- const encoder = new TextEncoder()
417
-
418
- // Compress data if enabled
419
- let processedEntries: Array<{ path: string; data: Uint8Array; originalSize?: number }>
420
- if (this.useCompression) {
421
- processedEntries = await Promise.all(
422
- entries.map(async ({ path, data }) => {
423
- const compressed = await compress(data)
424
- // Only use compressed if it's actually smaller
425
- if (compressed.length < data.length) {
426
- return { path, data: compressed, originalSize: data.length }
427
- }
428
- return { path, data }
429
- })
430
- )
431
- } else {
432
- processedEntries = entries
433
- }
392
+ const encoder = new TextEncoder()
393
+
394
+ // Compress data if enabled
395
+ let processedEntries: Array<{ path: string; data: Uint8Array; originalSize?: number }>
396
+ if (this.useCompression) {
397
+ processedEntries = await Promise.all(
398
+ entries.map(async ({ path, data }) => {
399
+ const compressed = await compress(data)
400
+ // Only use compressed if it's actually smaller
401
+ if (compressed.length < data.length) {
402
+ return { path, data: compressed, originalSize: data.length }
403
+ }
404
+ return { path, data }
405
+ })
406
+ )
407
+ } else {
408
+ processedEntries = entries
409
+ }
434
410
 
435
- // Calculate total data size (using compressed sizes where applicable)
436
- let totalDataSize = 0
437
- for (const { data } of processedEntries) {
438
- totalDataSize += data.length
439
- }
411
+ // Calculate total data size (using compressed sizes where applicable)
412
+ let totalDataSize = 0
413
+ for (const { data } of processedEntries) {
414
+ totalDataSize += data.length
415
+ }
440
416
 
441
- // Build index - iterate until offsets stabilize
442
- // (offset changes -> JSON length changes -> header size changes -> offset changes)
443
- // Header format: [index length: 4][CRC32: 4][JSON index][file data...]
444
- const newIndex: PackIndex = {}
445
- let headerSize = 8 // 4 bytes index length + 4 bytes CRC32
446
- let prevHeaderSize = 0
447
-
448
- // Iterate until stable (usually 2-3 iterations)
449
- while (headerSize !== prevHeaderSize) {
450
- prevHeaderSize = headerSize
451
-
452
- let currentOffset = headerSize
453
- for (const { path, data, originalSize } of processedEntries) {
454
- const entry: PackIndexEntry = { offset: currentOffset, size: data.length }
455
- if (originalSize !== undefined) {
456
- entry.originalSize = originalSize
457
- }
458
- newIndex[path] = entry
459
- currentOffset += data.length
417
+ // Build index - iterate until offsets stabilize
418
+ // (offset changes -> JSON length changes -> header size changes -> offset changes)
419
+ // Header format: [index length: 4][CRC32: 4][JSON index][file data...]
420
+ const newIndex: PackIndex = {}
421
+ let headerSize = 8 // 4 bytes index length + 4 bytes CRC32
422
+ let prevHeaderSize = 0
423
+
424
+ // Iterate until stable (usually 2-3 iterations)
425
+ while (headerSize !== prevHeaderSize) {
426
+ prevHeaderSize = headerSize
427
+
428
+ let currentOffset = headerSize
429
+ for (const { path, data, originalSize } of processedEntries) {
430
+ const entry: PackIndexEntry = { offset: currentOffset, size: data.length }
431
+ if (originalSize !== undefined) {
432
+ entry.originalSize = originalSize
460
433
  }
461
-
462
- const indexBuf = encoder.encode(JSON.stringify(newIndex))
463
- headerSize = 8 + indexBuf.length
434
+ newIndex[path] = entry
435
+ currentOffset += data.length
464
436
  }
465
437
 
466
- // Build the complete pack file
467
- const finalIndexBuf = encoder.encode(JSON.stringify(newIndex))
468
- const totalSize = headerSize + totalDataSize
469
- const packBuffer = new Uint8Array(totalSize)
470
- const view = new DataView(packBuffer.buffer)
438
+ const indexBuf = encoder.encode(JSON.stringify(newIndex))
439
+ headerSize = 8 + indexBuf.length
440
+ }
471
441
 
472
- // Write index JSON at offset 8
473
- packBuffer.set(finalIndexBuf, 8)
442
+ // Build the complete pack file
443
+ const finalIndexBuf = encoder.encode(JSON.stringify(newIndex))
444
+ const totalSize = headerSize + totalDataSize
445
+ const packBuffer = new Uint8Array(totalSize)
446
+ const view = new DataView(packBuffer.buffer)
474
447
 
475
- // Write data at correct offsets
476
- for (const { path, data } of processedEntries) {
477
- const entry = newIndex[path]
478
- packBuffer.set(data, entry.offset)
479
- }
448
+ // Write index JSON at offset 8
449
+ packBuffer.set(finalIndexBuf, 8)
480
450
 
481
- // Calculate CRC32 over content (index + data, everything after header) if enabled
482
- const content = packBuffer.subarray(8)
483
- const checksum = this.useChecksum ? crc32(content) : 0
451
+ // Write data at correct offsets
452
+ for (const { path, data } of processedEntries) {
453
+ const entry = newIndex[path]
454
+ packBuffer.set(data, entry.offset)
455
+ }
484
456
 
485
- // Write header (index length + CRC32)
486
- view.setUint32(0, finalIndexBuf.length, true)
487
- view.setUint32(4, checksum, true)
457
+ // Calculate CRC32 over content (index + data, everything after header) if enabled
458
+ const content = packBuffer.subarray(8)
459
+ const checksum = this.useChecksum ? crc32(content) : 0
488
460
 
489
- await this.writePackFile(packBuffer)
490
- this.index = newIndex
491
- } finally {
492
- release()
493
- }
461
+ // Write header (index length + CRC32)
462
+ view.setUint32(0, finalIndexBuf.length, true)
463
+ view.setUint32(4, checksum, true)
464
+
465
+ await this.writePackFile(packBuffer)
466
+ this.index = newIndex
494
467
  }
495
468
 
496
469
  /**
@@ -502,12 +475,17 @@ export class PackedStorage {
502
475
  if (!fileHandle) return
503
476
 
504
477
  if (this.useSync) {
505
- const access = await fileHandle.createSyncAccessHandle()
478
+ const releaseLock = await fileLockManager.acquire(PACK_FILE)
506
479
  try {
507
- access.truncate(data.length)
508
- access.write(data, { at: 0 })
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
+ }
509
487
  } finally {
510
- access.close()
488
+ releaseLock()
511
489
  }
512
490
  } else {
513
491
  const writable = await fileHandle.createWritable()
@@ -521,21 +499,21 @@ export class PackedStorage {
521
499
  * Note: Doesn't reclaim space, just removes from index and recalculates CRC32
522
500
  */
523
501
  async remove(path: string): Promise<boolean> {
524
- const release = await this.acquireLock()
525
- try {
526
- const index = await this.loadIndex()
527
- if (!(path in index)) return false
502
+ const index = await this.loadIndex()
503
+ if (!(path in index)) return false
528
504
 
529
- delete index[path]
505
+ delete index[path]
530
506
 
531
- const { fileHandle } = await this.handleManager.getHandle(PACK_FILE)
532
- if (!fileHandle) return true
507
+ const { fileHandle } = await this.handleManager.getHandle(PACK_FILE)
508
+ if (!fileHandle) return true
533
509
 
534
- // Need to read existing file to recalculate CRC32
535
- const encoder = new TextEncoder()
536
- const newIndexBuf = encoder.encode(JSON.stringify(index))
510
+ // Need to read existing file to recalculate CRC32
511
+ const encoder = new TextEncoder()
512
+ const newIndexBuf = encoder.encode(JSON.stringify(index))
537
513
 
538
- if (this.useSync) {
514
+ if (this.useSync) {
515
+ const releaseLock = await fileLockManager.acquire(PACK_FILE)
516
+ try {
539
517
  const access = await fileHandle.createSyncAccessHandle()
540
518
  try {
541
519
  const size = access.getSize()
@@ -579,53 +557,48 @@ export class PackedStorage {
579
557
  } finally {
580
558
  access.close()
581
559
  }
582
- } else {
583
- // For non-sync, rewrite the whole file
584
- const file = await fileHandle.getFile()
585
- const oldData = new Uint8Array(await file.arrayBuffer())
586
-
587
- if (oldData.length < 8) return true
560
+ } finally {
561
+ releaseLock()
562
+ }
563
+ } else {
564
+ // For non-sync, rewrite the whole file
565
+ const file = await fileHandle.getFile()
566
+ const oldData = new Uint8Array(await file.arrayBuffer())
588
567
 
589
- const oldIndexLen = new DataView(oldData.buffer).getUint32(0, true)
590
- const dataStart = 8 + oldIndexLen
591
- const dataPortion = oldData.subarray(dataStart)
568
+ if (oldData.length < 8) return true
592
569
 
593
- // Build new content
594
- const newContent = new Uint8Array(newIndexBuf.length + dataPortion.length)
595
- newContent.set(newIndexBuf, 0)
596
- newContent.set(dataPortion, newIndexBuf.length)
570
+ const oldIndexLen = new DataView(oldData.buffer).getUint32(0, true)
571
+ const dataStart = 8 + oldIndexLen
572
+ const dataPortion = oldData.subarray(dataStart)
597
573
 
598
- // Calculate CRC32 if enabled
599
- const checksum = this.useChecksum ? crc32(newContent) : 0
574
+ // Build new content
575
+ const newContent = new Uint8Array(newIndexBuf.length + dataPortion.length)
576
+ newContent.set(newIndexBuf, 0)
577
+ newContent.set(dataPortion, newIndexBuf.length)
600
578
 
601
- // Build new file
602
- const newFile = new Uint8Array(8 + newContent.length)
603
- const view = new DataView(newFile.buffer)
604
- view.setUint32(0, newIndexBuf.length, true)
605
- view.setUint32(4, checksum, true)
606
- newFile.set(newContent, 8)
579
+ // Calculate CRC32 if enabled
580
+ const checksum = this.useChecksum ? crc32(newContent) : 0
607
581
 
608
- const writable = await fileHandle.createWritable()
609
- await writable.write(newFile)
610
- await writable.close()
611
- }
582
+ // Build new file
583
+ const newFile = new Uint8Array(8 + newContent.length)
584
+ const view = new DataView(newFile.buffer)
585
+ view.setUint32(0, newIndexBuf.length, true)
586
+ view.setUint32(4, checksum, true)
587
+ newFile.set(newContent, 8)
612
588
 
613
- return true
614
- } finally {
615
- release()
589
+ const writable = await fileHandle.createWritable()
590
+ await writable.write(newFile)
591
+ await writable.close()
616
592
  }
593
+
594
+ return true
617
595
  }
618
596
 
619
597
  /**
620
598
  * Check if pack file is being used (has entries)
621
599
  */
622
600
  async isEmpty(): Promise<boolean> {
623
- const release = await this.acquireLock()
624
- try {
625
- const index = await this.loadIndex()
626
- return Object.keys(index).length === 0
627
- } finally {
628
- release()
629
- }
601
+ const index = await this.loadIndex()
602
+ return Object.keys(index).length === 0
630
603
  }
631
604
  }
@@ -1,5 +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
4
  import { normalize } from './path-utils.js'
4
5
  import { createELOOP, createEINVAL, createEEXIST } from './errors.js'
5
6
 
@@ -101,13 +102,21 @@ export class SymlinkManager {
101
102
  const buffer = new TextEncoder().encode(data)
102
103
 
103
104
  if (this.useSync) {
104
- const access = await fileHandle.createSyncAccessHandle()
105
- access.truncate(0)
106
- let written = 0
107
- while (written < buffer.length) {
108
- written += access.write(buffer.subarray(written), { at: written })
105
+ const releaseLock = await fileLockManager.acquire(SYMLINK_FILE)
106
+ 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()
116
+ }
117
+ } finally {
118
+ releaseLock()
109
119
  }
110
- access.close()
111
120
  } else {
112
121
  const writable = await fileHandle.createWritable()
113
122
  await writable.write(buffer)