@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/dist/index.js +86 -182
- package/dist/index.js.map +1 -1
- package/dist/opfs-hybrid.js +86 -182
- package/dist/opfs-hybrid.js.map +1 -1
- package/dist/opfs-worker.js +86 -182
- package/dist/opfs-worker.js.map +1 -1
- package/package.json +1 -1
- package/src/handle-manager.ts +0 -64
- package/src/index.ts +19 -39
- package/src/packed-storage.ts +81 -107
- package/src/symlink-manager.ts +6 -12
package/package.json
CHANGED
package/src/handle-manager.ts
CHANGED
|
@@ -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
|
|
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
|
|
190
|
+
const access = await fileHandle.createSyncAccessHandle()
|
|
191
191
|
try {
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
|
275
|
+
const access = await fileHandle.createSyncAccessHandle()
|
|
281
276
|
try {
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
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
|
|
319
|
+
const access = await fileHandle!.createSyncAccessHandle()
|
|
330
320
|
try {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
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
|
|
1048
|
+
const access = await fileHandle.createSyncAccessHandle()
|
|
1064
1049
|
try {
|
|
1065
|
-
|
|
1066
|
-
try {
|
|
1067
|
-
access.truncate(len)
|
|
1068
|
-
} finally {
|
|
1069
|
-
access.close()
|
|
1070
|
-
}
|
|
1050
|
+
access.truncate(len)
|
|
1071
1051
|
} finally {
|
|
1072
|
-
|
|
1052
|
+
access.close()
|
|
1073
1053
|
}
|
|
1074
1054
|
} else {
|
|
1075
1055
|
const file = await fileHandle.getFile()
|
package/src/packed-storage.ts
CHANGED
|
@@ -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
|
|
176
|
+
const access = await fileHandle.createSyncAccessHandle()
|
|
178
177
|
try {
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return {}
|
|
184
|
-
}
|
|
178
|
+
const size = access.getSize()
|
|
179
|
+
if (size < 8) {
|
|
180
|
+
return {}
|
|
181
|
+
}
|
|
185
182
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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
|
|
271
|
+
const access = await fileHandle.createSyncAccessHandle()
|
|
278
272
|
try {
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
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
|
|
328
|
+
const access = await fileHandle.createSyncAccessHandle()
|
|
340
329
|
try {
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
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
|
|
462
|
+
const access = await fileHandle.createSyncAccessHandle()
|
|
479
463
|
try {
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
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
|
|
494
|
+
const access = await fileHandle.createSyncAccessHandle()
|
|
516
495
|
try {
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
-
|
|
542
|
-
|
|
518
|
+
// Calculate new CRC32 if enabled
|
|
519
|
+
const checksum = this.useChecksum ? crc32(newContent) : 0
|
|
543
520
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
} finally {
|
|
558
|
-
access.close()
|
|
559
|
-
}
|
|
532
|
+
access.truncate(newFile.length)
|
|
533
|
+
access.write(newFile, { at: 0 })
|
|
560
534
|
} finally {
|
|
561
|
-
|
|
535
|
+
access.close()
|
|
562
536
|
}
|
|
563
537
|
} else {
|
|
564
538
|
// For non-sync, rewrite the whole file
|
package/src/symlink-manager.ts
CHANGED
|
@@ -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
|
|
104
|
+
const access = await fileHandle.createSyncAccessHandle()
|
|
106
105
|
try {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
112
|
+
access.close()
|
|
119
113
|
}
|
|
120
114
|
} else {
|
|
121
115
|
const writable = await fileHandle.createWritable()
|