@componentor/fs 1.2.6 → 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/dist/index.js +133 -69
- package/dist/index.js.map +1 -1
- package/dist/opfs-hybrid.js +133 -69
- package/dist/opfs-hybrid.js.map +1 -1
- package/dist/opfs-worker.js +133 -69
- package/dist/opfs-worker.js.map +1 -1
- package/package.json +1 -1
- package/src/handle-manager.ts +52 -0
- package/src/packed-storage.ts +107 -81
- package/src/symlink-manager.ts +12 -6
package/package.json
CHANGED
package/src/handle-manager.ts
CHANGED
|
@@ -16,6 +16,58 @@ export interface GetHandleOptions {
|
|
|
16
16
|
const FILE_HANDLE_POOL_SIZE = 50
|
|
17
17
|
const DIR_CACHE_MAX_SIZE = 200
|
|
18
18
|
|
|
19
|
+
/**
|
|
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).
|
|
25
|
+
*/
|
|
26
|
+
class FileLock {
|
|
27
|
+
private active = new Set<string>()
|
|
28
|
+
private queues = new Map<string, Array<() => void>>()
|
|
29
|
+
|
|
30
|
+
async acquire(path: string): Promise<() => void> {
|
|
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)
|
|
35
|
+
}
|
|
36
|
+
|
|
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
|
+
})
|
|
46
|
+
|
|
47
|
+
return this.createRelease(path)
|
|
48
|
+
}
|
|
49
|
+
|
|
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
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Global file lock instance for sync access handle serialization */
|
|
69
|
+
export const fileLock = new FileLock()
|
|
70
|
+
|
|
19
71
|
/**
|
|
20
72
|
* Manages OPFS handles with caching for improved performance
|
|
21
73
|
*/
|
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 { fileLock } from './handle-manager.js'
|
|
18
19
|
import { createECORRUPTED } from './errors.js'
|
|
19
20
|
|
|
20
21
|
// ============ Compression ============
|
|
@@ -173,38 +174,43 @@ export class PackedStorage {
|
|
|
173
174
|
}
|
|
174
175
|
|
|
175
176
|
if (this.useSync) {
|
|
176
|
-
const
|
|
177
|
+
const release = await fileLock.acquire(PACK_FILE)
|
|
177
178
|
try {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
179
|
+
const access = await fileHandle.createSyncAccessHandle()
|
|
180
|
+
try {
|
|
181
|
+
const size = access.getSize()
|
|
182
|
+
if (size < 8) {
|
|
183
|
+
return {}
|
|
184
|
+
}
|
|
182
185
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
+
}
|
|
200
204
|
}
|
|
201
|
-
}
|
|
202
205
|
|
|
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()
|
|
211
|
+
}
|
|
206
212
|
} finally {
|
|
207
|
-
|
|
213
|
+
release()
|
|
208
214
|
}
|
|
209
215
|
} else {
|
|
210
216
|
const file = await fileHandle.getFile()
|
|
@@ -268,12 +274,17 @@ export class PackedStorage {
|
|
|
268
274
|
let buffer: Uint8Array
|
|
269
275
|
|
|
270
276
|
if (this.useSync) {
|
|
271
|
-
const
|
|
277
|
+
const release = await fileLock.acquire(PACK_FILE)
|
|
272
278
|
try {
|
|
273
|
-
|
|
274
|
-
|
|
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
|
+
}
|
|
275
286
|
} finally {
|
|
276
|
-
|
|
287
|
+
release()
|
|
277
288
|
}
|
|
278
289
|
} else {
|
|
279
290
|
const file = await fileHandle.getFile()
|
|
@@ -325,21 +336,26 @@ export class PackedStorage {
|
|
|
325
336
|
const decompressPromises: Array<{ path: string; promise: Promise<Uint8Array> }> = []
|
|
326
337
|
|
|
327
338
|
if (this.useSync) {
|
|
328
|
-
const
|
|
339
|
+
const release = await fileLock.acquire(PACK_FILE)
|
|
329
340
|
try {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
+
}
|
|
339
353
|
}
|
|
354
|
+
} finally {
|
|
355
|
+
access.close()
|
|
340
356
|
}
|
|
341
357
|
} finally {
|
|
342
|
-
|
|
358
|
+
release()
|
|
343
359
|
}
|
|
344
360
|
} else {
|
|
345
361
|
const file = await fileHandle.getFile()
|
|
@@ -459,12 +475,17 @@ export class PackedStorage {
|
|
|
459
475
|
if (!fileHandle) return
|
|
460
476
|
|
|
461
477
|
if (this.useSync) {
|
|
462
|
-
const
|
|
478
|
+
const release = await fileLock.acquire(PACK_FILE)
|
|
463
479
|
try {
|
|
464
|
-
access.
|
|
465
|
-
|
|
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
|
+
}
|
|
466
487
|
} finally {
|
|
467
|
-
|
|
488
|
+
release()
|
|
468
489
|
}
|
|
469
490
|
} else {
|
|
470
491
|
const writable = await fileHandle.createWritable()
|
|
@@ -491,48 +512,53 @@ export class PackedStorage {
|
|
|
491
512
|
const newIndexBuf = encoder.encode(JSON.stringify(index))
|
|
492
513
|
|
|
493
514
|
if (this.useSync) {
|
|
494
|
-
const
|
|
515
|
+
const release = await fileLock.acquire(PACK_FILE)
|
|
495
516
|
try {
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
}
|
|
517
|
+
const access = await fileHandle.createSyncAccessHandle()
|
|
518
|
+
try {
|
|
519
|
+
const size = access.getSize()
|
|
510
520
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
+
}
|
|
533
|
+
|
|
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
|
+
}
|
|
517
540
|
|
|
518
|
-
|
|
519
|
-
|
|
541
|
+
// Calculate new CRC32 if enabled
|
|
542
|
+
const checksum = this.useChecksum ? crc32(newContent) : 0
|
|
520
543
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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)
|
|
526
549
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
550
|
+
// Write new file
|
|
551
|
+
const newFile = new Uint8Array(8 + newContent.length)
|
|
552
|
+
newFile.set(newHeader, 0)
|
|
553
|
+
newFile.set(newContent, 8)
|
|
531
554
|
|
|
532
|
-
|
|
533
|
-
|
|
555
|
+
access.truncate(newFile.length)
|
|
556
|
+
access.write(newFile, { at: 0 })
|
|
557
|
+
} finally {
|
|
558
|
+
access.close()
|
|
559
|
+
}
|
|
534
560
|
} finally {
|
|
535
|
-
|
|
561
|
+
release()
|
|
536
562
|
}
|
|
537
563
|
} else {
|
|
538
564
|
// For non-sync, rewrite the whole file
|
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 { fileLock } from './handle-manager.js'
|
|
3
4
|
import { normalize } from './path-utils.js'
|
|
4
5
|
import { createELOOP, createEINVAL, createEEXIST } from './errors.js'
|
|
5
6
|
|
|
@@ -101,15 +102,20 @@ export class SymlinkManager {
|
|
|
101
102
|
const buffer = new TextEncoder().encode(data)
|
|
102
103
|
|
|
103
104
|
if (this.useSync) {
|
|
104
|
-
const
|
|
105
|
+
const release = await fileLock.acquire(SYMLINK_FILE)
|
|
105
106
|
try {
|
|
106
|
-
access.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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()
|
|
110
116
|
}
|
|
111
117
|
} finally {
|
|
112
|
-
|
|
118
|
+
release()
|
|
113
119
|
}
|
|
114
120
|
} else {
|
|
115
121
|
const writable = await fileHandle.createWritable()
|