sliftutils 1.4.63 → 1.4.64

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 CHANGED
@@ -1823,6 +1823,10 @@ declare module "sliftutils/storage/FileFolderAPI" {
1823
1823
  }): Promise<DirectoryWrapper>;
1824
1824
  [Symbol.asyncIterator](): AsyncIterableIterator<[string, FileWrapper | DirectoryWrapper]>;
1825
1825
  }
1826
+ export declare function usePrivateFileSystem(): void;
1827
+ export declare function isPrivateFileSystemActive(): boolean;
1828
+ export declare function listPrivateFolders(): Promise<string[]>;
1829
+ export declare function pickPrivateFolder(name: string): void;
1826
1830
  export declare const getDirectoryHandle: {
1827
1831
  (): Promise<DirectoryWrapper>;
1828
1832
  reset(): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sliftutils",
3
- "version": "1.4.63",
3
+ "version": "1.4.64",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -106,6 +106,10 @@ export declare class NodeJSDirectoryHandleWrapper implements DirectoryWrapper {
106
106
  }): Promise<DirectoryWrapper>;
107
107
  [Symbol.asyncIterator](): AsyncIterableIterator<[string, FileWrapper | DirectoryWrapper]>;
108
108
  }
109
+ export declare function usePrivateFileSystem(): void;
110
+ export declare function isPrivateFileSystemActive(): boolean;
111
+ export declare function listPrivateFolders(): Promise<string[]>;
112
+ export declare function pickPrivateFolder(name: string): void;
109
113
  export declare const getDirectoryHandle: {
110
114
  (): Promise<DirectoryWrapper>;
111
115
  reset(): void;
@@ -372,6 +372,71 @@ export class NodeJSDirectoryHandleWrapper implements DirectoryWrapper {
372
372
  }
373
373
 
374
374
 
375
+ // When set, getDirectoryHandle skips every prompt + remote check and serves from the browser's Origin
376
+ // Private File System (OPFS). One named subfolder is "current" per fileAPIKey; pickPrivateFolder /
377
+ // resetStorageLocation switch which one. Set this in app startup BEFORE the first getDirectoryHandle.
378
+ let opfsEnabled = false;
379
+ const OPFS_FOLDERS_DIR = "folders";
380
+ const OPFS_DEFAULT_FOLDER = "default";
381
+ function opfsCurrentKey() { return getFileAPIKey() + ":opfs:current"; }
382
+ function getCurrentOpfsFolder(): string {
383
+ return localStorage.getItem(opfsCurrentKey()) || OPFS_DEFAULT_FOLDER;
384
+ }
385
+ function setCurrentOpfsFolder(name: string): void {
386
+ localStorage.setItem(opfsCurrentKey(), name);
387
+ }
388
+
389
+ // Switches getDirectoryHandle to the Origin Private File System. No directory picker, no permission
390
+ // prompt, no remote server check. Persists nothing — call on every app start before reading storage.
391
+ export function usePrivateFileSystem(): void {
392
+ opfsEnabled = true;
393
+ }
394
+
395
+ // Whether the OPFS branch is in effect (so other callers can adapt — e.g. skip the "find data subdir"
396
+ // hack in getFileStorageNested2 since OPFS is always a clean root we own end-to-end).
397
+ export function isPrivateFileSystemActive(): boolean {
398
+ return opfsEnabled;
399
+ }
400
+
401
+ async function getOpfsFoldersDir(): Promise<FileSystemDirectoryHandle> {
402
+ if (!navigator.storage?.getDirectory) {
403
+ throw new Error("Private File System Access API not supported in this browser");
404
+ }
405
+ if (location.protocol === "file:") {
406
+ throw new Error("Private File System API is disallowed in file:// locations. Host on an actual origin.");
407
+ }
408
+ const root = await navigator.storage.getDirectory();
409
+ const keyed = await root.getDirectoryHandle(getFileAPIKey(), { create: true });
410
+ return await keyed.getDirectoryHandle(OPFS_FOLDERS_DIR, { create: true });
411
+ }
412
+
413
+ async function getCurrentOpfsHandle(): Promise<DirectoryWrapper> {
414
+ const folders = await getOpfsFoldersDir();
415
+ const folder = await folders.getDirectoryHandle(getCurrentOpfsFolder(), { create: true });
416
+ return folder as unknown as DirectoryWrapper;
417
+ }
418
+
419
+ // All previously-used OPFS subfolders under this fileAPIKey, sorted alphabetically (timestamp-named
420
+ // auto-generated folders therefore sort chronologically). Use this to surface a list, then call
421
+ // pickPrivateFolder(name) to switch.
422
+ export async function listPrivateFolders(): Promise<string[]> {
423
+ if (!navigator.storage?.getDirectory) return [];
424
+ let folders: FileSystemDirectoryHandle;
425
+ try { folders = await getOpfsFoldersDir(); } catch { return []; }
426
+ const names: string[] = [];
427
+ for await (const [name, handle] of (folders as unknown as AsyncIterable<[string, { kind: string }]>)) {
428
+ if (handle.kind === "directory") names.push(name);
429
+ }
430
+ return names.sort();
431
+ }
432
+
433
+ // Switch to a specific OPFS subfolder and reload so getDirectoryHandle re-resolves to it. The folder
434
+ // is created if it doesn't exist yet.
435
+ export function pickPrivateFolder(name: string): void {
436
+ setCurrentOpfsFolder(name);
437
+ window.location.reload();
438
+ }
439
+
375
440
  // Returns the directory handle to use — local (Node / picked folder) OR a remote server, both as the
376
441
  // same DirectoryWrapper, so callers don't know or care which it is. A remembered server is VERIFIED
377
442
  // (we actually connect) before use; if it no longer works we re-prompt, just like a local folder whose
@@ -380,6 +445,9 @@ export const getDirectoryHandle = lazy(async function getDirectoryHandle(): Prom
380
445
  if (isNode()) {
381
446
  return new NodeJSDirectoryHandleWrapper(path.resolve("./data/"));
382
447
  }
448
+ if (opfsEnabled) {
449
+ return getCurrentOpfsHandle();
450
+ }
383
451
 
384
452
  // A server connected in a previous session: only reuse it if it still actually works.
385
453
  const savedRemote = loadRemoteConfig();
@@ -479,17 +547,21 @@ export const getFileStorageNested2 = cache(async function getFileStorage(pathStr
479
547
  base = new NodeJSDirectoryHandleWrapper(path.resolve("./data/"));
480
548
  } else {
481
549
  base = await getDirectoryHandle();
482
- let dirs: string[] = [];
483
- let alls: string[] = [];
484
- for await (const [name, entry] of base) {
485
- if (entry.kind === "directory") {
486
- dirs.push(name);
550
+ // Skip the "find data subdir" hack under OPFS — that's for picked directories where the user
551
+ // might have selected a folder with unrelated files; OPFS is a clean root we own end-to-end.
552
+ if (!opfsEnabled) {
553
+ let dirs: string[] = [];
554
+ let alls: string[] = [];
555
+ for await (const [name, entry] of base) {
556
+ if (entry.kind === "directory") {
557
+ dirs.push(name);
558
+ }
559
+ alls.push(name);
560
+ }
561
+ // HACK: If there are enough files, it's almost certainly not a directory that contains collections. It's probably the user's data instead
562
+ if (dirs.includes(".git") || dirs.includes("data") || alls.length > 100) {
563
+ base = await base.getDirectoryHandle("data", { create: true });
487
564
  }
488
- alls.push(name);
489
- }
490
- // HACK: If there are enough files, it's almost certainly not a directory that contains collections. It's probably the user's data instead
491
- if (dirs.includes(".git") || dirs.includes("data") || alls.length > 100) {
492
- base = await base.getDirectoryHandle("data", { create: true });
493
565
  }
494
566
  }
495
567
  for (let part of pathStr.split("/")) {
@@ -506,6 +578,13 @@ export const getFileStorage = lazy(async function getFileStorage(): Promise<File
506
578
  return wrapHandle(handle);
507
579
  });
508
580
  export function resetStorageLocation() {
581
+ if (opfsEnabled) {
582
+ // Don't delete previous folder — listPrivateFolders / pickPrivateFolder still need it. Just point
583
+ // "current" at a fresh timestamp-named one so the next reload starts clean.
584
+ setCurrentOpfsFolder(new Date().toISOString().replace(/[:.]/g, "-"));
585
+ window.location.reload();
586
+ return;
587
+ }
509
588
  localStorage.removeItem(getFileAPIKey());
510
589
  try { localStorage.removeItem(remoteConfigKey()); } catch { /* ignore */ }
511
590
  window.location.reload();