@componentor/fs 1.2.3 → 1.2.4

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.
@@ -128,6 +128,7 @@ export class PackedStorage {
128
128
  private useChecksum: boolean
129
129
  private index: PackIndex | null = null
130
130
  private indexLoaded = false
131
+ private lockPromise: Promise<void> | null = null
131
132
 
132
133
  constructor(handleManager: HandleManager, useSync: boolean, useCompression = false, useChecksum = true) {
133
134
  this.handleManager = handleManager
@@ -137,6 +138,23 @@ export class PackedStorage {
137
138
  this.useChecksum = useChecksum
138
139
  }
139
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
+
140
158
  /**
141
159
  * Reset pack storage state (memory only)
142
160
  */
@@ -163,9 +181,9 @@ export class PackedStorage {
163
181
  /**
164
182
  * Load pack index from disk (always reloads to support hybrid mode)
165
183
  * Verifies CRC32 checksum for integrity
184
+ * Note: Caller must hold the lock
166
185
  */
167
186
  private async loadIndex(): Promise<PackIndex> {
168
- // Always reload from disk to ensure we see writes from other threads/workers
169
187
  try {
170
188
  const { fileHandle } = await this.handleManager.getHandle(PACK_FILE)
171
189
  if (!fileHandle) {
@@ -174,36 +192,38 @@ export class PackedStorage {
174
192
 
175
193
  if (this.useSync) {
176
194
  const access = await fileHandle.createSyncAccessHandle()
177
- const size = access.getSize()
178
- if (size < 8) {
179
- access.close()
180
- return {}
181
- }
182
-
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
- access.close()
195
+ try {
196
+ const size = access.getSize()
197
+ if (size < 8) {
198
+ return {}
199
+ }
195
200
 
196
- // Verify CRC32 if enabled
197
- if (this.useChecksum && storedCrc !== 0) {
198
- const calculatedCrc = crc32(content)
199
- if (calculatedCrc !== storedCrc) {
200
- throw createECORRUPTED(PACK_FILE)
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)
218
+ }
201
219
  }
202
- }
203
220
 
204
- // Parse index from content
205
- const indexJson = new TextDecoder().decode(content.subarray(0, indexLen))
206
- return JSON.parse(indexJson)
221
+ // Parse index from content
222
+ const indexJson = new TextDecoder().decode(content.subarray(0, indexLen))
223
+ return JSON.parse(indexJson)
224
+ } finally {
225
+ access.close()
226
+ }
207
227
  } else {
208
228
  const file = await fileHandle.getFile()
209
229
  const data = new Uint8Array(await file.arrayBuffer())
@@ -236,8 +256,13 @@ export class PackedStorage {
236
256
  * Check if a path exists in the pack
237
257
  */
238
258
  async has(path: string): Promise<boolean> {
239
- const index = await this.loadIndex()
240
- return path in index
259
+ const release = await this.acquireLock()
260
+ try {
261
+ const index = await this.loadIndex()
262
+ return path in index
263
+ } finally {
264
+ release()
265
+ }
241
266
  }
242
267
 
243
268
  /**
@@ -245,10 +270,15 @@ export class PackedStorage {
245
270
  * Returns originalSize if compressed, otherwise size
246
271
  */
247
272
  async getSize(path: string): Promise<number | null> {
248
- const index = await this.loadIndex()
249
- const entry = index[path]
250
- if (!entry) return null
251
- return entry.originalSize ?? entry.size
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
+ }
252
282
  }
253
283
 
254
284
  /**
@@ -256,32 +286,40 @@ export class PackedStorage {
256
286
  * Handles decompression if file was stored compressed
257
287
  */
258
288
  async read(path: string): Promise<Uint8Array | null> {
259
- const index = await this.loadIndex()
260
- const entry = index[path]
261
- if (!entry) return 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
262
294
 
263
- const { fileHandle } = await this.handleManager.getHandle(PACK_FILE)
264
- if (!fileHandle) return null
295
+ const { fileHandle } = await this.handleManager.getHandle(PACK_FILE)
296
+ if (!fileHandle) return null
265
297
 
266
- let buffer: Uint8Array
298
+ let buffer: Uint8Array
267
299
 
268
- if (this.useSync) {
269
- const access = await fileHandle.createSyncAccessHandle()
270
- buffer = new Uint8Array(entry.size)
271
- access.read(buffer, { at: entry.offset })
272
- access.close()
273
- } else {
274
- const file = await fileHandle.getFile()
275
- const data = new Uint8Array(await file.arrayBuffer())
276
- buffer = data.slice(entry.offset, entry.offset + entry.size)
277
- }
300
+ if (this.useSync) {
301
+ const access = await fileHandle.createSyncAccessHandle()
302
+ try {
303
+ buffer = new Uint8Array(entry.size)
304
+ access.read(buffer, { at: entry.offset })
305
+ } finally {
306
+ access.close()
307
+ }
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
+ }
278
313
 
279
- // Decompress if needed
280
- if (entry.originalSize !== undefined) {
281
- return decompress(buffer)
282
- }
314
+ // Decompress if needed
315
+ if (entry.originalSize !== undefined) {
316
+ return decompress(buffer)
317
+ }
283
318
 
284
- return buffer
319
+ return buffer
320
+ } finally {
321
+ release()
322
+ }
285
323
  }
286
324
 
287
325
  /**
@@ -293,66 +331,74 @@ export class PackedStorage {
293
331
  const results = new Map<string, Uint8Array | null>()
294
332
  if (paths.length === 0) return results
295
333
 
296
- const index = await this.loadIndex()
297
-
298
- // Find which paths are in the pack
299
- const toRead: Array<{ path: string; offset: number; size: number; originalSize?: number }> = []
300
- for (const path of paths) {
301
- const entry = index[path]
302
- if (entry) {
303
- toRead.push({ path, offset: entry.offset, size: entry.size, originalSize: entry.originalSize })
304
- } else {
305
- results.set(path, null)
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
+ }
306
347
  }
307
- }
308
348
 
309
- if (toRead.length === 0) return results
349
+ if (toRead.length === 0) return results
310
350
 
311
- const { fileHandle } = await this.handleManager.getHandle(PACK_FILE)
312
- if (!fileHandle) {
313
- for (const { path } of toRead) {
314
- results.set(path, null)
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
315
357
  }
316
- return results
317
- }
318
358
 
319
- // Read all files
320
- const decompressPromises: Array<{ path: string; promise: Promise<Uint8Array> }> = []
359
+ // Read all files
360
+ const decompressPromises: Array<{ path: string; promise: Promise<Uint8Array> }> = []
321
361
 
322
- if (this.useSync) {
323
- const access = await fileHandle.createSyncAccessHandle()
324
- for (const { path, offset, size, originalSize } of toRead) {
325
- const buffer = new Uint8Array(size)
326
- access.read(buffer, { at: offset })
362
+ if (this.useSync) {
363
+ const access = await fileHandle.createSyncAccessHandle()
364
+ try {
365
+ for (const { path, offset, size, originalSize } of toRead) {
366
+ const buffer = new Uint8Array(size)
367
+ access.read(buffer, { at: offset })
368
+
369
+ if (originalSize !== undefined) {
370
+ // Queue for decompression
371
+ decompressPromises.push({ path, promise: decompress(buffer) })
372
+ } else {
373
+ results.set(path, buffer)
374
+ }
375
+ }
376
+ } finally {
377
+ access.close()
378
+ }
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)
327
384
 
328
- if (originalSize !== undefined) {
329
- // Queue for decompression
330
- decompressPromises.push({ path, promise: decompress(buffer) })
331
- } else {
332
- results.set(path, buffer)
385
+ if (originalSize !== undefined) {
386
+ decompressPromises.push({ path, promise: decompress(buffer) })
387
+ } else {
388
+ results.set(path, buffer)
389
+ }
333
390
  }
334
391
  }
335
- access.close()
336
- } else {
337
- const file = await fileHandle.getFile()
338
- const data = new Uint8Array(await file.arrayBuffer())
339
- for (const { path, offset, size, originalSize } of toRead) {
340
- const buffer = data.slice(offset, offset + size)
341
392
 
342
- if (originalSize !== undefined) {
343
- decompressPromises.push({ path, promise: decompress(buffer) })
344
- } else {
345
- results.set(path, buffer)
346
- }
393
+ // Wait for all decompressions
394
+ for (const { path, promise } of decompressPromises) {
395
+ results.set(path, await promise)
347
396
  }
348
- }
349
397
 
350
- // Wait for all decompressions
351
- for (const { path, promise } of decompressPromises) {
352
- results.set(path, await promise)
398
+ return results
399
+ } finally {
400
+ release()
353
401
  }
354
-
355
- return results
356
402
  }
357
403
 
358
404
  /**
@@ -365,85 +411,91 @@ export class PackedStorage {
365
411
  async writeBatch(entries: Array<{ path: string; data: Uint8Array }>): Promise<void> {
366
412
  if (entries.length === 0) return
367
413
 
368
- const encoder = new TextEncoder()
369
-
370
- // Compress data if enabled
371
- let processedEntries: Array<{ path: string; data: Uint8Array; originalSize?: number }>
372
- if (this.useCompression) {
373
- processedEntries = await Promise.all(
374
- entries.map(async ({ path, data }) => {
375
- const compressed = await compress(data)
376
- // Only use compressed if it's actually smaller
377
- if (compressed.length < data.length) {
378
- return { path, data: compressed, originalSize: data.length }
379
- }
380
- return { path, data }
381
- })
382
- )
383
- } else {
384
- processedEntries = entries
385
- }
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
+ }
386
434
 
387
- // Calculate total data size (using compressed sizes where applicable)
388
- let totalDataSize = 0
389
- for (const { data } of processedEntries) {
390
- totalDataSize += data.length
391
- }
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
+ }
392
440
 
393
- // Build index - iterate until offsets stabilize
394
- // (offset changes -> JSON length changes -> header size changes -> offset changes)
395
- // Header format: [index length: 4][CRC32: 4][JSON index][file data...]
396
- const newIndex: PackIndex = {}
397
- let headerSize = 8 // 4 bytes index length + 4 bytes CRC32
398
- let prevHeaderSize = 0
399
-
400
- // Iterate until stable (usually 2-3 iterations)
401
- while (headerSize !== prevHeaderSize) {
402
- prevHeaderSize = headerSize
403
-
404
- let currentOffset = headerSize
405
- for (const { path, data, originalSize } of processedEntries) {
406
- const entry: PackIndexEntry = { offset: currentOffset, size: data.length }
407
- if (originalSize !== undefined) {
408
- entry.originalSize = originalSize
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
409
460
  }
410
- newIndex[path] = entry
411
- currentOffset += data.length
412
- }
413
461
 
414
- const indexBuf = encoder.encode(JSON.stringify(newIndex))
415
- headerSize = 8 + indexBuf.length
416
- }
462
+ const indexBuf = encoder.encode(JSON.stringify(newIndex))
463
+ headerSize = 8 + indexBuf.length
464
+ }
417
465
 
418
- // Build the complete pack file
419
- const finalIndexBuf = encoder.encode(JSON.stringify(newIndex))
420
- const totalSize = headerSize + totalDataSize
421
- const packBuffer = new Uint8Array(totalSize)
422
- const view = new DataView(packBuffer.buffer)
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)
423
471
 
424
- // Write index JSON at offset 8
425
- packBuffer.set(finalIndexBuf, 8)
472
+ // Write index JSON at offset 8
473
+ packBuffer.set(finalIndexBuf, 8)
426
474
 
427
- // Write data at correct offsets
428
- for (const { path, data } of processedEntries) {
429
- const entry = newIndex[path]
430
- packBuffer.set(data, entry.offset)
431
- }
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
+ }
432
480
 
433
- // Calculate CRC32 over content (index + data, everything after header) if enabled
434
- const content = packBuffer.subarray(8)
435
- const checksum = this.useChecksum ? crc32(content) : 0
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
436
484
 
437
- // Write header (index length + CRC32)
438
- view.setUint32(0, finalIndexBuf.length, true)
439
- view.setUint32(4, checksum, true)
485
+ // Write header (index length + CRC32)
486
+ view.setUint32(0, finalIndexBuf.length, true)
487
+ view.setUint32(4, checksum, true)
440
488
 
441
- await this.writePackFile(packBuffer)
442
- this.index = newIndex
489
+ await this.writePackFile(packBuffer)
490
+ this.index = newIndex
491
+ } finally {
492
+ release()
493
+ }
443
494
  }
444
495
 
445
496
  /**
446
497
  * Write the pack file to OPFS
498
+ * Note: Caller must hold the lock
447
499
  */
448
500
  private async writePackFile(data: Uint8Array): Promise<void> {
449
501
  const { fileHandle } = await this.handleManager.getHandle(PACK_FILE, { create: true })
@@ -451,9 +503,12 @@ export class PackedStorage {
451
503
 
452
504
  if (this.useSync) {
453
505
  const access = await fileHandle.createSyncAccessHandle()
454
- access.truncate(data.length)
455
- access.write(data, { at: 0 })
456
- access.close()
506
+ try {
507
+ access.truncate(data.length)
508
+ access.write(data, { at: 0 })
509
+ } finally {
510
+ access.close()
511
+ }
457
512
  } else {
458
513
  const writable = await fileHandle.createWritable()
459
514
  await writable.write(data)
@@ -466,98 +521,111 @@ export class PackedStorage {
466
521
  * Note: Doesn't reclaim space, just removes from index and recalculates CRC32
467
522
  */
468
523
  async remove(path: string): Promise<boolean> {
469
- const index = await this.loadIndex()
470
- if (!(path in index)) return false
524
+ const release = await this.acquireLock()
525
+ try {
526
+ const index = await this.loadIndex()
527
+ if (!(path in index)) return false
471
528
 
472
- delete index[path]
529
+ delete index[path]
473
530
 
474
- const { fileHandle } = await this.handleManager.getHandle(PACK_FILE)
475
- if (!fileHandle) return true
531
+ const { fileHandle } = await this.handleManager.getHandle(PACK_FILE)
532
+ if (!fileHandle) return true
476
533
 
477
- // Need to read existing file to recalculate CRC32
478
- const encoder = new TextEncoder()
479
- const newIndexBuf = encoder.encode(JSON.stringify(index))
534
+ // Need to read existing file to recalculate CRC32
535
+ const encoder = new TextEncoder()
536
+ const newIndexBuf = encoder.encode(JSON.stringify(index))
480
537
 
481
- if (this.useSync) {
482
- const access = await fileHandle.createSyncAccessHandle()
483
- const size = access.getSize()
484
-
485
- // Read old header to get old index length
486
- const oldHeader = new Uint8Array(8)
487
- access.read(oldHeader, { at: 0 })
488
- const oldIndexLen = new DataView(oldHeader.buffer).getUint32(0, true)
489
-
490
- // Read data portion (after old index)
491
- const dataStart = 8 + oldIndexLen
492
- const dataSize = size - dataStart
493
- const dataPortion = new Uint8Array(dataSize)
494
- if (dataSize > 0) {
495
- access.read(dataPortion, { at: dataStart })
496
- }
538
+ if (this.useSync) {
539
+ const access = await fileHandle.createSyncAccessHandle()
540
+ try {
541
+ const size = access.getSize()
542
+
543
+ // Read old header to get old index length
544
+ const oldHeader = new Uint8Array(8)
545
+ access.read(oldHeader, { at: 0 })
546
+ const oldIndexLen = new DataView(oldHeader.buffer).getUint32(0, true)
547
+
548
+ // Read data portion (after old index)
549
+ const dataStart = 8 + oldIndexLen
550
+ const dataSize = size - dataStart
551
+ const dataPortion = new Uint8Array(dataSize)
552
+ if (dataSize > 0) {
553
+ access.read(dataPortion, { at: dataStart })
554
+ }
497
555
 
498
- // Build new content (new index + data)
499
- const newContent = new Uint8Array(newIndexBuf.length + dataSize)
500
- newContent.set(newIndexBuf, 0)
501
- if (dataSize > 0) {
502
- newContent.set(dataPortion, newIndexBuf.length)
503
- }
556
+ // Build new content (new index + data)
557
+ const newContent = new Uint8Array(newIndexBuf.length + dataSize)
558
+ newContent.set(newIndexBuf, 0)
559
+ if (dataSize > 0) {
560
+ newContent.set(dataPortion, newIndexBuf.length)
561
+ }
504
562
 
505
- // Calculate new CRC32 if enabled
506
- const checksum = this.useChecksum ? crc32(newContent) : 0
563
+ // Calculate new CRC32 if enabled
564
+ const checksum = this.useChecksum ? crc32(newContent) : 0
507
565
 
508
- // Build new header
509
- const newHeader = new Uint8Array(8)
510
- const view = new DataView(newHeader.buffer)
511
- view.setUint32(0, newIndexBuf.length, true)
512
- view.setUint32(4, checksum, true)
566
+ // Build new header
567
+ const newHeader = new Uint8Array(8)
568
+ const view = new DataView(newHeader.buffer)
569
+ view.setUint32(0, newIndexBuf.length, true)
570
+ view.setUint32(4, checksum, true)
513
571
 
514
- // Write new file
515
- const newFile = new Uint8Array(8 + newContent.length)
516
- newFile.set(newHeader, 0)
517
- newFile.set(newContent, 8)
572
+ // Write new file
573
+ const newFile = new Uint8Array(8 + newContent.length)
574
+ newFile.set(newHeader, 0)
575
+ newFile.set(newContent, 8)
518
576
 
519
- access.truncate(newFile.length)
520
- access.write(newFile, { at: 0 })
521
- access.close()
522
- } else {
523
- // For non-sync, rewrite the whole file
524
- const file = await fileHandle.getFile()
525
- const oldData = new Uint8Array(await file.arrayBuffer())
577
+ access.truncate(newFile.length)
578
+ access.write(newFile, { at: 0 })
579
+ } finally {
580
+ access.close()
581
+ }
582
+ } else {
583
+ // For non-sync, rewrite the whole file
584
+ const file = await fileHandle.getFile()
585
+ const oldData = new Uint8Array(await file.arrayBuffer())
526
586
 
527
- if (oldData.length < 8) return true
587
+ if (oldData.length < 8) return true
528
588
 
529
- const oldIndexLen = new DataView(oldData.buffer).getUint32(0, true)
530
- const dataStart = 8 + oldIndexLen
531
- const dataPortion = oldData.subarray(dataStart)
589
+ const oldIndexLen = new DataView(oldData.buffer).getUint32(0, true)
590
+ const dataStart = 8 + oldIndexLen
591
+ const dataPortion = oldData.subarray(dataStart)
532
592
 
533
- // Build new content
534
- const newContent = new Uint8Array(newIndexBuf.length + dataPortion.length)
535
- newContent.set(newIndexBuf, 0)
536
- newContent.set(dataPortion, newIndexBuf.length)
593
+ // Build new content
594
+ const newContent = new Uint8Array(newIndexBuf.length + dataPortion.length)
595
+ newContent.set(newIndexBuf, 0)
596
+ newContent.set(dataPortion, newIndexBuf.length)
537
597
 
538
- // Calculate CRC32 if enabled
539
- const checksum = this.useChecksum ? crc32(newContent) : 0
598
+ // Calculate CRC32 if enabled
599
+ const checksum = this.useChecksum ? crc32(newContent) : 0
540
600
 
541
- // Build new file
542
- const newFile = new Uint8Array(8 + newContent.length)
543
- const view = new DataView(newFile.buffer)
544
- view.setUint32(0, newIndexBuf.length, true)
545
- view.setUint32(4, checksum, true)
546
- newFile.set(newContent, 8)
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)
547
607
 
548
- const writable = await fileHandle.createWritable()
549
- await writable.write(newFile)
550
- await writable.close()
551
- }
608
+ const writable = await fileHandle.createWritable()
609
+ await writable.write(newFile)
610
+ await writable.close()
611
+ }
552
612
 
553
- return true
613
+ return true
614
+ } finally {
615
+ release()
616
+ }
554
617
  }
555
618
 
556
619
  /**
557
620
  * Check if pack file is being used (has entries)
558
621
  */
559
622
  async isEmpty(): Promise<boolean> {
560
- const index = await this.loadIndex()
561
- return Object.keys(index).length === 0
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
+ }
562
630
  }
563
631
  }