sliftutils 1.1.40 → 1.1.42
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/index.d.ts +49 -0
- package/misc/getSecret.d.ts +1 -0
- package/misc/getSecret.ts +7 -0
- package/package.json +1 -1
- package/storage/FileFolderAPI.d.ts +48 -0
- package/storage/FileFolderAPI.tsx +66 -19
package/index.d.ts
CHANGED
|
@@ -55,6 +55,7 @@ declare module "sliftutils/misc/getSecret" {
|
|
|
55
55
|
getAllKeys(): string[];
|
|
56
56
|
get(key: string): Promise<string> | undefined;
|
|
57
57
|
};
|
|
58
|
+
export declare function setSecret(key: string, value: string): void;
|
|
58
59
|
|
|
59
60
|
}
|
|
60
61
|
|
|
@@ -986,6 +987,52 @@ declare module "sliftutils/storage/FileFolderAPI" {
|
|
|
986
987
|
]>;
|
|
987
988
|
};
|
|
988
989
|
export declare function setFileAPIKey(key: string): void;
|
|
990
|
+
export declare class NodeJSFileHandleWrapper implements FileWrapper {
|
|
991
|
+
private filePath;
|
|
992
|
+
constructor(filePath: string);
|
|
993
|
+
getFile(): Promise<{
|
|
994
|
+
size: number;
|
|
995
|
+
lastModified: number;
|
|
996
|
+
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
997
|
+
slice: (start: number, end: number) => {
|
|
998
|
+
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
999
|
+
};
|
|
1000
|
+
}>;
|
|
1001
|
+
createWritable(config?: {
|
|
1002
|
+
keepExistingData?: boolean;
|
|
1003
|
+
}): Promise<{
|
|
1004
|
+
seek: (offset: number) => Promise<void>;
|
|
1005
|
+
write: (value: Buffer) => Promise<void>;
|
|
1006
|
+
close: () => Promise<void>;
|
|
1007
|
+
}>;
|
|
1008
|
+
}
|
|
1009
|
+
export declare class NodeJSDirectoryHandleWrapper implements DirectoryWrapper {
|
|
1010
|
+
private rootPath;
|
|
1011
|
+
constructor(rootPath: string);
|
|
1012
|
+
removeEntry(key: string, options?: {
|
|
1013
|
+
recursive?: boolean;
|
|
1014
|
+
}): Promise<void>;
|
|
1015
|
+
getFileHandle(key: string, options?: {
|
|
1016
|
+
create?: boolean;
|
|
1017
|
+
}): Promise<FileWrapper>;
|
|
1018
|
+
getDirectoryHandle(key: string, options?: {
|
|
1019
|
+
create?: boolean;
|
|
1020
|
+
}): Promise<DirectoryWrapper>;
|
|
1021
|
+
[Symbol.asyncIterator](): AsyncIterableIterator<[
|
|
1022
|
+
string,
|
|
1023
|
+
{
|
|
1024
|
+
kind: "file";
|
|
1025
|
+
name: string;
|
|
1026
|
+
getFile(): Promise<FileWrapper>;
|
|
1027
|
+
} | {
|
|
1028
|
+
kind: "directory";
|
|
1029
|
+
name: string;
|
|
1030
|
+
getDirectoryHandle(key: string, options?: {
|
|
1031
|
+
create?: boolean;
|
|
1032
|
+
}): Promise<DirectoryWrapper>;
|
|
1033
|
+
}
|
|
1034
|
+
]>;
|
|
1035
|
+
}
|
|
989
1036
|
export declare const getDirectoryHandle: {
|
|
990
1037
|
(): Promise<DirectoryWrapper>;
|
|
991
1038
|
reset(): void;
|
|
@@ -1014,6 +1061,8 @@ declare module "sliftutils/storage/FileFolderAPI" {
|
|
|
1014
1061
|
export type FileStorage = IStorageRaw & {
|
|
1015
1062
|
folder: NestedFileStorage;
|
|
1016
1063
|
};
|
|
1064
|
+
export declare function wrapHandle(handle: DirectoryWrapper): FileStorage;
|
|
1065
|
+
export declare function tryToLoadPointer(pointer: string): Promise<DirectoryWrapper | undefined>;
|
|
1017
1066
|
export {};
|
|
1018
1067
|
|
|
1019
1068
|
}
|
package/misc/getSecret.d.ts
CHANGED
package/misc/getSecret.ts
CHANGED
|
@@ -96,3 +96,10 @@ export const getSecret = cache(async function getSecret(key: string): Promise<st
|
|
|
96
96
|
});
|
|
97
97
|
});
|
|
98
98
|
});
|
|
99
|
+
|
|
100
|
+
export function setSecret(key: string, value: string) {
|
|
101
|
+
if (isNode()) {
|
|
102
|
+
throw new Error("setSecret is only supported in the browser. We don't want to break the user's file system with setSecret in NodeJS.");
|
|
103
|
+
}
|
|
104
|
+
localStorage.setItem(getStorageKey(key), value);
|
|
105
|
+
}
|
package/package.json
CHANGED
|
@@ -70,6 +70,52 @@ type DirectoryWrapper = {
|
|
|
70
70
|
]>;
|
|
71
71
|
};
|
|
72
72
|
export declare function setFileAPIKey(key: string): void;
|
|
73
|
+
export declare class NodeJSFileHandleWrapper implements FileWrapper {
|
|
74
|
+
private filePath;
|
|
75
|
+
constructor(filePath: string);
|
|
76
|
+
getFile(): Promise<{
|
|
77
|
+
size: number;
|
|
78
|
+
lastModified: number;
|
|
79
|
+
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
80
|
+
slice: (start: number, end: number) => {
|
|
81
|
+
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
82
|
+
};
|
|
83
|
+
}>;
|
|
84
|
+
createWritable(config?: {
|
|
85
|
+
keepExistingData?: boolean;
|
|
86
|
+
}): Promise<{
|
|
87
|
+
seek: (offset: number) => Promise<void>;
|
|
88
|
+
write: (value: Buffer) => Promise<void>;
|
|
89
|
+
close: () => Promise<void>;
|
|
90
|
+
}>;
|
|
91
|
+
}
|
|
92
|
+
export declare class NodeJSDirectoryHandleWrapper implements DirectoryWrapper {
|
|
93
|
+
private rootPath;
|
|
94
|
+
constructor(rootPath: string);
|
|
95
|
+
removeEntry(key: string, options?: {
|
|
96
|
+
recursive?: boolean;
|
|
97
|
+
}): Promise<void>;
|
|
98
|
+
getFileHandle(key: string, options?: {
|
|
99
|
+
create?: boolean;
|
|
100
|
+
}): Promise<FileWrapper>;
|
|
101
|
+
getDirectoryHandle(key: string, options?: {
|
|
102
|
+
create?: boolean;
|
|
103
|
+
}): Promise<DirectoryWrapper>;
|
|
104
|
+
[Symbol.asyncIterator](): AsyncIterableIterator<[
|
|
105
|
+
string,
|
|
106
|
+
{
|
|
107
|
+
kind: "file";
|
|
108
|
+
name: string;
|
|
109
|
+
getFile(): Promise<FileWrapper>;
|
|
110
|
+
} | {
|
|
111
|
+
kind: "directory";
|
|
112
|
+
name: string;
|
|
113
|
+
getDirectoryHandle(key: string, options?: {
|
|
114
|
+
create?: boolean;
|
|
115
|
+
}): Promise<DirectoryWrapper>;
|
|
116
|
+
}
|
|
117
|
+
]>;
|
|
118
|
+
}
|
|
73
119
|
export declare const getDirectoryHandle: {
|
|
74
120
|
(): Promise<DirectoryWrapper>;
|
|
75
121
|
reset(): void;
|
|
@@ -98,4 +144,6 @@ export type NestedFileStorage = {
|
|
|
98
144
|
export type FileStorage = IStorageRaw & {
|
|
99
145
|
folder: NestedFileStorage;
|
|
100
146
|
};
|
|
147
|
+
export declare function wrapHandle(handle: DirectoryWrapper): FileStorage;
|
|
148
|
+
export declare function tryToLoadPointer(pointer: string): Promise<DirectoryWrapper | undefined>;
|
|
101
149
|
export {};
|
|
@@ -96,7 +96,7 @@ class DirectoryPrompter extends preact.Component {
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
class NodeJSFileHandleWrapper implements FileWrapper {
|
|
99
|
+
export class NodeJSFileHandleWrapper implements FileWrapper {
|
|
100
100
|
constructor(private filePath: string) {
|
|
101
101
|
}
|
|
102
102
|
|
|
@@ -159,7 +159,7 @@ class NodeJSFileHandleWrapper implements FileWrapper {
|
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
class NodeJSDirectoryHandleWrapper implements DirectoryWrapper {
|
|
162
|
+
export class NodeJSDirectoryHandleWrapper implements DirectoryWrapper {
|
|
163
163
|
constructor(private rootPath: string) {
|
|
164
164
|
}
|
|
165
165
|
|
|
@@ -241,6 +241,7 @@ class NodeJSDirectoryHandleWrapper implements DirectoryWrapper {
|
|
|
241
241
|
|
|
242
242
|
|
|
243
243
|
// NOTE: Blocks until the user provides a directory
|
|
244
|
+
// NOTE: If you want to override the location, you can explicitly call getDirectoryHandle.set with your own directory wrapper. Which if your server side can be the Node.js directory handle wrapper, or if your client side it can just be a pointer from tryToLoadPointer/fileSystemPointer.ts.
|
|
244
245
|
export const getDirectoryHandle = lazy(async function getDirectoryHandle(): Promise<DirectoryWrapper> {
|
|
245
246
|
if (isNode()) {
|
|
246
247
|
return new NodeJSDirectoryHandleWrapper(path.resolve("./data/"));
|
|
@@ -434,40 +435,78 @@ async function fixedGetFileHandle(config: {
|
|
|
434
435
|
return await config.handle.getFileHandle(config.key, { create: true });
|
|
435
436
|
}
|
|
436
437
|
|
|
438
|
+
// A file that genuinely does not exist. We must NOT retry these, otherwise reading many missing
|
|
439
|
+
// files (a common pattern) gets catastrophically slow.
|
|
440
|
+
function isMissingError(error: unknown): boolean {
|
|
441
|
+
let name = (error as { name?: string })?.name;
|
|
442
|
+
let code = (error as { code?: string })?.code;
|
|
443
|
+
return name === "NotFoundError" || code === "ENOENT";
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const MAX_READ_RETRIES = 6;
|
|
447
|
+
|
|
448
|
+
// The browser File System Access API can transiently fail reads under heavy load (it appears to
|
|
449
|
+
// throttle when a page issues many reads at once). Those failures surface as errors OTHER than
|
|
450
|
+
// "not found", so we retry them with backoff. A real missing file (NotFoundError / ENOENT) returns
|
|
451
|
+
// undefined immediately with no retry.
|
|
452
|
+
async function readWithRetry<T>(label: string, key: string, read: () => Promise<T>): Promise<T | undefined> {
|
|
453
|
+
let backoff = 25;
|
|
454
|
+
for (let attempt = 0; ; attempt++) {
|
|
455
|
+
try {
|
|
456
|
+
return await read();
|
|
457
|
+
} catch (error) {
|
|
458
|
+
if (isMissingError(error)) return undefined;
|
|
459
|
+
let name = (error as { name?: string })?.name || "Error";
|
|
460
|
+
let message = (error as { message?: string })?.message || String(error);
|
|
461
|
+
if (attempt >= MAX_READ_RETRIES) {
|
|
462
|
+
console.warn(`${label} gave up on ${JSON.stringify(key)} after ${attempt} retries (${name}): ${message.slice(0, 200)}`);
|
|
463
|
+
return undefined;
|
|
464
|
+
}
|
|
465
|
+
console.warn(`${label} retrying ${JSON.stringify(key)} (attempt ${attempt + 1}, ${name}): ${message.slice(0, 200)}`);
|
|
466
|
+
await new Promise(resolve => setTimeout(resolve, backoff));
|
|
467
|
+
backoff = Math.min(backoff * 2, 1000);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
437
472
|
function wrapHandleFiles(handle: DirectoryWrapper): IStorageRaw {
|
|
438
473
|
return {
|
|
439
474
|
async getInfo(key: string) {
|
|
440
|
-
|
|
475
|
+
return readWithRetry("getInfo", key, async () => {
|
|
441
476
|
const file = await handle.getFileHandle(key);
|
|
442
477
|
const fileContent = await file.getFile();
|
|
443
478
|
return {
|
|
444
479
|
size: fileContent.size,
|
|
445
480
|
lastModified: fileContent.lastModified,
|
|
446
481
|
};
|
|
447
|
-
}
|
|
448
|
-
return undefined;
|
|
449
|
-
}
|
|
482
|
+
});
|
|
450
483
|
},
|
|
451
484
|
async get(key: string): Promise<Buffer | undefined> {
|
|
452
|
-
|
|
485
|
+
return readWithRetry("get", key, async () => {
|
|
453
486
|
const file = await handle.getFileHandle(key);
|
|
454
487
|
const fileContent = await file.getFile();
|
|
455
488
|
const arrayBuffer = await fileContent.arrayBuffer();
|
|
489
|
+
// Under load the FS Access API can resolve with a truncated buffer and no error.
|
|
490
|
+
// Treat a short read as transient so readWithRetry retries it.
|
|
491
|
+
if (arrayBuffer.byteLength !== fileContent.size) {
|
|
492
|
+
throw Object.assign(new Error(`Short read: got ${arrayBuffer.byteLength} of ${fileContent.size} bytes`), { name: "ShortReadError" });
|
|
493
|
+
}
|
|
456
494
|
return Buffer.from(arrayBuffer);
|
|
457
|
-
}
|
|
458
|
-
return undefined;
|
|
459
|
-
}
|
|
495
|
+
});
|
|
460
496
|
},
|
|
461
497
|
|
|
462
498
|
async getRange(key: string, config: { start: number; end: number }): Promise<Buffer | undefined> {
|
|
463
|
-
|
|
499
|
+
return readWithRetry("getRange", key, async () => {
|
|
464
500
|
const file = await handle.getFileHandle(key);
|
|
465
501
|
const fileContent = await file.getFile();
|
|
502
|
+
const clampedStart = Math.min(Math.max(config.start, 0), fileContent.size);
|
|
503
|
+
const clampedEnd = Math.min(Math.max(config.end, clampedStart), fileContent.size);
|
|
466
504
|
const arrayBuffer = await fileContent.slice(config.start, config.end).arrayBuffer();
|
|
505
|
+
if (arrayBuffer.byteLength !== clampedEnd - clampedStart) {
|
|
506
|
+
throw Object.assign(new Error(`Short range read: got ${arrayBuffer.byteLength} of ${clampedEnd - clampedStart} bytes`), { name: "ShortReadError" });
|
|
507
|
+
}
|
|
467
508
|
return Buffer.from(arrayBuffer);
|
|
468
|
-
}
|
|
469
|
-
return undefined;
|
|
470
|
-
}
|
|
509
|
+
});
|
|
471
510
|
},
|
|
472
511
|
|
|
473
512
|
async append(key: string, value: Buffer): Promise<void> {
|
|
@@ -496,10 +535,18 @@ function wrapHandleFiles(handle: DirectoryWrapper): IStorageRaw {
|
|
|
496
535
|
|
|
497
536
|
async getKeys(includeFolders: boolean = false): Promise<string[]> {
|
|
498
537
|
const keys: string[] = [];
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
538
|
+
try {
|
|
539
|
+
for await (const [name, entry] of handle) {
|
|
540
|
+
if (entry.kind === "file" || includeFolders) {
|
|
541
|
+
keys.push(entry.name);
|
|
542
|
+
}
|
|
502
543
|
}
|
|
544
|
+
} catch (error) {
|
|
545
|
+
let name = (error as { name?: string })?.name || "Error";
|
|
546
|
+
let message = (error as { message?: string })?.message || String(error);
|
|
547
|
+
// A failure mid-iteration would silently truncate the listing, so surface it loudly.
|
|
548
|
+
console.error(`getKeys directory iteration failed after ${keys.length} entries (${name}): ${message.slice(0, 300)}`);
|
|
549
|
+
throw error;
|
|
503
550
|
}
|
|
504
551
|
return keys;
|
|
505
552
|
},
|
|
@@ -544,14 +591,14 @@ function wrapHandleNested(handle: DirectoryWrapper): NestedFileStorage {
|
|
|
544
591
|
};
|
|
545
592
|
}
|
|
546
593
|
|
|
547
|
-
function wrapHandle(handle: DirectoryWrapper): FileStorage {
|
|
594
|
+
export function wrapHandle(handle: DirectoryWrapper): FileStorage {
|
|
548
595
|
return {
|
|
549
596
|
...wrapHandleFiles(handle),
|
|
550
597
|
folder: wrapHandleNested(handle),
|
|
551
598
|
};
|
|
552
599
|
}
|
|
553
600
|
|
|
554
|
-
async function tryToLoadPointer(pointer: string) {
|
|
601
|
+
export async function tryToLoadPointer(pointer: string) {
|
|
555
602
|
let result = await getFileSystemPointer({ pointer });
|
|
556
603
|
if (!result) return;
|
|
557
604
|
let handle = await result?.onUserActivation();
|