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 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | 1x 1x 1x 1x 1x 1x 397x 397x 397x 397x 1x 397x 397x 397x 140x 810x 810x 810x 397x 8x 8x 8x 8x 8x 397x 1x 427x 427x 427x 1x 15658x 15658x 15658x 264426x 264426x 264426x 264426x 264426x 264426x 264426x 264426x 25693x 25693x 25693x 25693x 25693x 264426x 15658x 15658x 1x 3025x 3025x 2387x 3025x 1x 6185x 6185x 1x 1032x 1032x 1032x 391x 391x 391x 641x 1032x 1x 466x 466x 5229x 5229x 466x 2103x 2103x 466x 1x 56x 56x 56x 56x 1x 18x 18x 1x 11x 11x 11x 63x 63x 11x 8x 8x 11x 11x 1x 314x 314x 314x 314x 310x 310x 310x 310x 241x 25x 3x 3x 3x 3x 241x 18x 2x 2x 2x 314x 1x 4291x 4291x 4291x 4291x 7489x 7489x 4291x 4291x 4291x 1x 2x 2x 2x 1x 566x 566x 5320x 1749x 1749x 566x 566x 1x | import { isInside, normalizePath } from '../path.js';
import { chunked, concat } from '../stream.js';
import type { ByteRange, VFSAdapter, VFSListEntry, VFSStat } from '../types.js';
export interface MemoryAdapterOptions {
/**
* Defaults to a monotonic wall clock: every write gets a strictly greater
* `mtime` than the last, which keeps ordering unambiguous in tests and demos.
*/
clock?: () => number;
/** Initial contents. Values are decoded as UTF-8 when given as strings. */
files?: Record<string, string | Uint8Array>;
}
const encoder = new TextEncoder();
const decoder = new TextDecoder();
/** In-memory backend — the reference implementation, and what tests run on. */
export class MemoryAdapter implements VFSAdapter {
readonly name: string;
private readonly entries = new Map<string, { data: Uint8Array; mtime: number }>();
/** Directories created empty via mkdir(); implicit parents are not tracked. */
private readonly dirs = new Set<string>();
private readonly clock: () => number;
private last = 0;
constructor(name = 'memory', options: MemoryAdapterOptions = {}) {
this.name = name;
this.clock =
options.clock ??
(() => {
this.last = Math.max(Date.now(), this.last + 1);
return this.last;
});
for (const [path, value] of Object.entries(options.files ?? {})) {
this.entries.set(normalizePath(path), {
data: typeof value === 'string' ? encoder.encode(value) : value,
mtime: this.clock(),
});
}
}
async mkdir(path: string): Promise<void> {
const parts = normalizePath(path).split('/').filter(Boolean);
for (let i = 1; i <= parts.length; i++) this.dirs.add(parts.slice(0, i).join('/'));
}
async list(path: string): Promise<VFSListEntry[]> {
const dir = normalizePath(path);
const seen = new Map<string, VFSListEntry>();
for (const key of [...this.entries.keys(), ...this.dirs]) {
if (!isInside(key, dir) || key === dir) continue;
const rest = dir ? key.slice(dir.length + 1) : key;
const slash = rest.indexOf('/');
const name = slash === -1 ? rest : rest.slice(0, slash);
const child = dir ? `${dir}/${name}` : name;
const isDir = slash !== -1 || this.dirs.has(key);
const known = seen.get(child);
if (!known || (isDir && known.kind === 'file')) {
const entry: VFSListEntry = { name, path: child, kind: isDir ? 'directory' : 'file' };
// The map holds the bytes and the mtime already, so a caller that would
// stat every entry (walk, a file browser) needs no second pass.
const held = isDir ? undefined : this.entries.get(child);
if (held) entry.stat = { kind: 'file', size: held.data.byteLength, mtime: held.mtime };
seen.set(child, entry);
}
}
return [...seen.values()];
}
async read(path: string): Promise<Uint8Array> {
const entry = this.entries.get(normalizePath(path));
if (!entry) throw new Error(`ENOENT: ${path}`);
return entry.data.slice();
}
async write(path: string, data: Uint8Array): Promise<void> {
this.entries.set(normalizePath(path), { data: data.slice(), mtime: this.clock() });
}
async append(path: string, data: Uint8Array): Promise<void> {
const target = normalizePath(path);
const held = this.entries.get(target);
if (!held) {
await this.write(target, data);
return;
}
this.entries.set(target, { data: concat([held.data, data.slice()]), mtime: this.clock() });
}
async delete(path: string): Promise<void> {
const target = normalizePath(path);
for (const key of [...this.entries.keys()]) {
if (key === target || key.startsWith(`${target}/`)) this.entries.delete(key);
}
for (const dir of [...this.dirs]) {
if (dir === target || dir.startsWith(`${target}/`)) this.dirs.delete(dir);
}
}
// Streaming here can never save memory — the file *is* memory. It exists so
// that the engine's streaming paths are exercised by the adapter the tests
// run on, and so range reads stay cheap.
async readRange(path: string, range: ByteRange = {}): Promise<Uint8Array> {
const entry = this.entries.get(normalizePath(path));
if (!entry) throw new Error(`ENOENT: ${path}`);
return entry.data.slice(range.start ?? 0, range.end ?? entry.data.byteLength);
}
async readStream(path: string, range: ByteRange = {}): Promise<ReadableStream<Uint8Array>> {
return chunked(await this.readRange(path, range));
}
async writeStream(path: string): Promise<WritableStream<Uint8Array>> {
const chunks: Uint8Array[] = [];
return new WritableStream<Uint8Array>({
write: (chunk) => {
chunks.push(chunk.slice());
},
close: async () => {
await this.write(path, concat(chunks));
},
});
}
async rename(oldPath: string, newPath: string): Promise<void> {
const from = normalizePath(oldPath);
const to = normalizePath(newPath);
const entry = this.entries.get(from);
if (entry) {
this.entries.delete(from);
this.entries.set(to, entry);
return;
}
for (const key of [...this.entries.keys()]) {
if (!key.startsWith(`${from}/`)) continue;
const moved = this.entries.get(key) as { data: Uint8Array; mtime: number };
this.entries.delete(key);
this.entries.set(`${to}${key.slice(from.length)}`, moved);
}
// Empty folders are tracked apart from the files, so they have to move too.
// Left behind, the directory would still stat at the old path and stat as
// missing at the new one — and v2 syncs empty directories.
for (const key of [...this.dirs]) {
if (key !== from && !key.startsWith(`${from}/`)) continue;
this.dirs.delete(key);
this.dirs.add(`${to}${key.slice(from.length)}`);
}
}
async stat(path: string): Promise<VFSStat | null> {
const target = normalizePath(path);
const entry = this.entries.get(target);
if (entry) return { kind: 'file', size: entry.data.byteLength, mtime: entry.mtime };
for (const key of this.entries.keys()) {
if (key.startsWith(`${target}/`)) return { kind: 'directory', size: 0, mtime: 0 };
}
if (this.dirs.has(target)) return { kind: 'directory', size: 0, mtime: 0 };
return target === '' ? { kind: 'directory', size: 0, mtime: 0 } : null;
}
// ------------------------------------------------------------- test aids
/** Forces an `mtime`, to script clock skew or simultaneous edits. */
setMtime(path: string, mtime: number): void {
const entry = this.entries.get(normalizePath(path));
if (entry) entry.mtime = mtime;
}
/** Working-folder contents as text, control folder excluded. */
snapshot(): Record<string, string> {
const out: Record<string, string> = {};
for (const [path, entry] of this.entries) {
if (path === '.vfs' || path.startsWith('.vfs/')) continue;
out[path] = decoder.decode(entry.data);
}
return out;
}
}
|