@componentor/fs 1.2.3 → 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/dist/index.js +214 -87
- package/dist/index.js.map +1 -1
- package/dist/opfs-hybrid.js +214 -87
- package/dist/opfs-hybrid.js.map +1 -1
- package/dist/opfs-worker.js +214 -87
- package/dist/opfs-worker.js.map +1 -1
- package/package.json +1 -1
- package/src/handle-manager.ts +64 -0
- package/src/index.ts +51 -19
- package/src/packed-storage.ts +125 -84
- package/src/symlink-manager.ts +15 -6
package/package.json
CHANGED
package/src/handle-manager.ts
CHANGED
|
@@ -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
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
|
1040
|
-
|
|
1041
|
-
|
|
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())
|
package/src/packed-storage.ts
CHANGED
|
@@ -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 ============
|
|
@@ -163,9 +164,9 @@ export class PackedStorage {
|
|
|
163
164
|
/**
|
|
164
165
|
* Load pack index from disk (always reloads to support hybrid mode)
|
|
165
166
|
* Verifies CRC32 checksum for integrity
|
|
167
|
+
* Note: Caller must hold the lock
|
|
166
168
|
*/
|
|
167
169
|
private async loadIndex(): Promise<PackIndex> {
|
|
168
|
-
// Always reload from disk to ensure we see writes from other threads/workers
|
|
169
170
|
try {
|
|
170
171
|
const { fileHandle } = await this.handleManager.getHandle(PACK_FILE)
|
|
171
172
|
if (!fileHandle) {
|
|
@@ -173,37 +174,44 @@ export class PackedStorage {
|
|
|
173
174
|
}
|
|
174
175
|
|
|
175
176
|
if (this.useSync) {
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
177
|
+
const releaseLock = await fileLockManager.acquire(PACK_FILE)
|
|
178
|
+
try {
|
|
179
|
+
const access = await fileHandle.createSyncAccessHandle()
|
|
180
|
+
try {
|
|
181
|
+
const size = access.getSize()
|
|
182
|
+
if (size < 8) {
|
|
183
|
+
return {}
|
|
184
|
+
}
|
|
185
|
+
|
|
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
|
+
}
|
|
204
|
+
}
|
|
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()
|
|
201
211
|
}
|
|
212
|
+
} finally {
|
|
213
|
+
releaseLock()
|
|
202
214
|
}
|
|
203
|
-
|
|
204
|
-
// Parse index from content
|
|
205
|
-
const indexJson = new TextDecoder().decode(content.subarray(0, indexLen))
|
|
206
|
-
return JSON.parse(indexJson)
|
|
207
215
|
} else {
|
|
208
216
|
const file = await fileHandle.getFile()
|
|
209
217
|
const data = new Uint8Array(await file.arrayBuffer())
|
|
@@ -266,10 +274,18 @@ export class PackedStorage {
|
|
|
266
274
|
let buffer: Uint8Array
|
|
267
275
|
|
|
268
276
|
if (this.useSync) {
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
277
|
+
const releaseLock = await fileLockManager.acquire(PACK_FILE)
|
|
278
|
+
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
|
+
}
|
|
286
|
+
} finally {
|
|
287
|
+
releaseLock()
|
|
288
|
+
}
|
|
273
289
|
} else {
|
|
274
290
|
const file = await fileHandle.getFile()
|
|
275
291
|
const data = new Uint8Array(await file.arrayBuffer())
|
|
@@ -320,19 +336,27 @@ export class PackedStorage {
|
|
|
320
336
|
const decompressPromises: Array<{ path: string; promise: Promise<Uint8Array> }> = []
|
|
321
337
|
|
|
322
338
|
if (this.useSync) {
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
339
|
+
const releaseLock = await fileLockManager.acquire(PACK_FILE)
|
|
340
|
+
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
|
+
}
|
|
353
|
+
}
|
|
354
|
+
} finally {
|
|
355
|
+
access.close()
|
|
333
356
|
}
|
|
357
|
+
} finally {
|
|
358
|
+
releaseLock()
|
|
334
359
|
}
|
|
335
|
-
access.close()
|
|
336
360
|
} else {
|
|
337
361
|
const file = await fileHandle.getFile()
|
|
338
362
|
const data = new Uint8Array(await file.arrayBuffer())
|
|
@@ -444,16 +468,25 @@ export class PackedStorage {
|
|
|
444
468
|
|
|
445
469
|
/**
|
|
446
470
|
* Write the pack file to OPFS
|
|
471
|
+
* Note: Caller must hold the lock
|
|
447
472
|
*/
|
|
448
473
|
private async writePackFile(data: Uint8Array): Promise<void> {
|
|
449
474
|
const { fileHandle } = await this.handleManager.getHandle(PACK_FILE, { create: true })
|
|
450
475
|
if (!fileHandle) return
|
|
451
476
|
|
|
452
477
|
if (this.useSync) {
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
478
|
+
const releaseLock = await fileLockManager.acquire(PACK_FILE)
|
|
479
|
+
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
|
+
}
|
|
487
|
+
} finally {
|
|
488
|
+
releaseLock()
|
|
489
|
+
}
|
|
457
490
|
} else {
|
|
458
491
|
const writable = await fileHandle.createWritable()
|
|
459
492
|
await writable.write(data)
|
|
@@ -479,46 +512,54 @@ export class PackedStorage {
|
|
|
479
512
|
const newIndexBuf = encoder.encode(JSON.stringify(index))
|
|
480
513
|
|
|
481
514
|
if (this.useSync) {
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
515
|
+
const releaseLock = await fileLockManager.acquire(PACK_FILE)
|
|
516
|
+
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
|
+
}
|
|
497
533
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
+
}
|
|
504
540
|
|
|
505
|
-
|
|
506
|
-
|
|
541
|
+
// Calculate new CRC32 if enabled
|
|
542
|
+
const checksum = this.useChecksum ? crc32(newContent) : 0
|
|
507
543
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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)
|
|
513
549
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
550
|
+
// Write new file
|
|
551
|
+
const newFile = new Uint8Array(8 + newContent.length)
|
|
552
|
+
newFile.set(newHeader, 0)
|
|
553
|
+
newFile.set(newContent, 8)
|
|
518
554
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
555
|
+
access.truncate(newFile.length)
|
|
556
|
+
access.write(newFile, { at: 0 })
|
|
557
|
+
} finally {
|
|
558
|
+
access.close()
|
|
559
|
+
}
|
|
560
|
+
} finally {
|
|
561
|
+
releaseLock()
|
|
562
|
+
}
|
|
522
563
|
} else {
|
|
523
564
|
// For non-sync, rewrite the whole file
|
|
524
565
|
const file = await fileHandle.getFile()
|
package/src/symlink-manager.ts
CHANGED
|
@@ -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
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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)
|