All files / src/adapters scoped.ts

100% Statements 76/76
100% Branches 49/49
100% Functions 20/20
100% Lines 76/76

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 1081x                                 1x 1x 42x 42x   42x 42x 42x 42x 42x 42x 42x 42x 42x   1x 42x 42x 42x   42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x     42x 42x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 42x   1x 3x 3x   1x 178x 178x     1x 31x 31x   1x 19x 19x 19x   1x 40x 40x   1x 45x 45x   1x 3x 3x   1x 2x 2x   1x 44x 44x 1x  
import { joinPath, normalizePath } from '../path.js';
import type {
  ByteRange,
  VFSAdapter,
  VFSChangeFeed,
  VFSListEntry,
  VFSStat,
} from '../types.js';
 
/**
 * A view of another adapter rooted at one of its subfolders, so any folder of
 * any backend can host its own `.vfs` store. The handle-based backends can
 * re-root natively; this wrapper gives every other backend the same ability.
 *
 * The optional methods (`fileId`, streaming) are only present when the base
 * adapter has them, so capability checks like `canStream()` see the truth.
 */
export class ScopedAdapter implements VFSAdapter {
  readonly name: string;
  readonly base: VFSAdapter;
  readonly root: string;
 
  mkdir?: (path: string) => Promise<void>;
  fileId?: (path: string) => Promise<string | null>;
  readRange?: (path: string, range?: ByteRange) => Promise<Uint8Array>;
  readStream?: (path: string, range?: ByteRange) => Promise<ReadableStream<Uint8Array>>;
  writeStream?: (path: string) => Promise<WritableStream<Uint8Array>>;
  append?: (path: string, data: Uint8Array) => Promise<void>;
  writeIf?: (path: string, data: Uint8Array, tag: string | null) => Promise<string | null>;
  tag?: (path: string) => Promise<string | null>;
  changes?: (token: string | null) => Promise<VFSChangeFeed>;
 
  constructor(base: VFSAdapter, root: string, name?: string) {
    this.base = base;
    this.root = normalizePath(root);
    this.name = name ?? (this.root ? `${base.name}/${this.root}` : base.name);
 
    const mkdir = base.mkdir?.bind(base);
    if (mkdir) this.mkdir = (path) => mkdir(this.at(path));
    const fileId = base.fileId?.bind(base);
    if (fileId) this.fileId = (path) => fileId(this.at(path));
    const readRange = base.readRange?.bind(base);
    if (readRange) this.readRange = (path, range) => readRange(this.at(path), range);
    const readStream = base.readStream?.bind(base);
    if (readStream) this.readStream = (path, range) => readStream(this.at(path), range);
    const writeStream = base.writeStream?.bind(base);
    if (writeStream) this.writeStream = (path) => writeStream(this.at(path));
    const append = base.append?.bind(base);
    if (append) this.append = (path, data) => append(this.at(path), data);
    const writeIf = base.writeIf?.bind(base);
    if (writeIf) this.writeIf = (path, data, tag) => writeIf(this.at(path), data, tag);
    const tag = base.tag?.bind(base);
    if (tag) this.tag = (path) => tag(this.at(path));
    // The change feed is account-wide on every backend that has one, so it is
    // forwarded whole and filtered by the caller against the paths it knows.
    const changes = base.changes?.bind(base);
    if (changes) {
      this.changes = async (token) => {
        const feed = await changes(token);
        return {
          ...feed,
          changes: feed.changes
            .filter((change) => !change.path || this.inside(change.path))
            .map((change) => (change.path ? { ...change, path: this.un(change.path) } : change)),
        };
      };
    }
  }
 
  private inside(path: string): boolean {
    return !this.root || path === this.root || path.startsWith(`${this.root}/`);
  }
 
  private at(path: string): string {
    return joinPath(this.root, path);
  }
 
  /** Base paths come back prefixed with the scope root; strip it. */
  private un(path: string): string {
    return this.root ? path.slice(this.root.length + 1) : path;
  }
 
  async list(path: string): Promise<VFSListEntry[]> {
    const entries = await this.base.list(this.at(path));
    return entries.map((entry) => ({ ...entry, path: this.un(entry.path) }));
  }
 
  read(path: string): Promise<Uint8Array> {
    return this.base.read(this.at(path));
  }
 
  write(path: string, data: Uint8Array): Promise<void> {
    return this.base.write(this.at(path), data);
  }
 
  delete(path: string): Promise<void> {
    return this.base.delete(this.at(path));
  }
 
  rename(oldPath: string, newPath: string): Promise<void> {
    return this.base.rename(this.at(oldPath), this.at(newPath));
  }
 
  stat(path: string): Promise<VFSStat | null> {
    return this.base.stat(this.at(path));
  }
}