@sema-lang/sema 1.9.0

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/README.md ADDED
@@ -0,0 +1,210 @@
1
+ # @sema-lang/sema
2
+
3
+ Sema Lisp interpreter for JavaScript — a client-side scripting engine powered by WebAssembly.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @sema-lang/sema
9
+ ```
10
+
11
+ Or use directly from a CDN:
12
+
13
+ ```html
14
+ <script type="module">
15
+ import { SemaInterpreter } from "https://cdn.jsdelivr.net/npm/@sema-lang/sema/+esm";
16
+
17
+ const sema = await SemaInterpreter.create();
18
+ console.log(sema.evalStr("(+ 1 2 3)").value); // "6"
19
+ </script>
20
+ ```
21
+
22
+ ## Quick Start
23
+
24
+ ```js
25
+ import { SemaInterpreter } from "@sema-lang/sema";
26
+
27
+ const sema = await SemaInterpreter.create();
28
+
29
+ // Evaluate expressions
30
+ const r = sema.evalStr("(+ 1 2 3)");
31
+ console.log(r.value); // "6"
32
+
33
+ // Definitions persist
34
+ sema.evalStr("(define (square x) (* x x))");
35
+ sema.evalStr("(square 7)"); // => "49"
36
+
37
+ // Register JS functions — args are native JS values
38
+ sema.registerFunction("greet", (name) => `Hello, ${name}!`);
39
+ sema.evalStr('(greet "world")'); // => "Hello, world!"
40
+
41
+ // Preload modules
42
+ sema.preloadModule("utils", "(define (double x) (* x 2))");
43
+ sema.evalStr('(import "utils")');
44
+ sema.evalStr("(double 21)"); // => "42"
45
+ ```
46
+
47
+ ## API
48
+
49
+ ### `SemaInterpreter.create(opts?)`
50
+
51
+ Create a new interpreter. Options:
52
+
53
+ | Option | Default | Description |
54
+ |--------|---------|-------------|
55
+ | `wasmUrl` | auto | URL to the `.wasm` binary |
56
+ | `stdlib` | `true` | Include the standard library |
57
+ | `deny` | `[]` | Capabilities to deny: `"network"`, `"fs-read"`, `"fs-write"` |
58
+ | `vfs` | none | VFS backend for persistence: `MemoryBackend`, `LocalStorageBackend`, `SessionStorageBackend`, `IndexedDBBackend` |
59
+
60
+ ### `evalStr(code)` → `EvalResult`
61
+
62
+ Evaluate Sema code synchronously. Returns `{ value, output, error }`.
63
+
64
+ ### `evalStrAsync(code)` → `Promise<EvalResult>`
65
+
66
+ Evaluate code that may use `http/get` or other async operations.
67
+
68
+ ### `registerFunction(name, fn)`
69
+
70
+ Register a JS function callable from Sema. Args are passed as native JS values.
71
+
72
+ ### `preloadModule(name, source)`
73
+
74
+ Inject a virtual module for use with `(import "name")`.
75
+
76
+ ### `version()` → `string`
77
+
78
+ Returns the interpreter version.
79
+
80
+ ### `readFile(path)` → `string | null`
81
+
82
+ Read a file from the virtual filesystem. Returns `null` if the file doesn't exist.
83
+
84
+ ### `writeFile(path, content)`
85
+
86
+ Write a file to the virtual filesystem (1 MB per file, 16 MB total, 256 files max).
87
+
88
+ ### `deleteFile(path)` → `boolean`
89
+
90
+ Delete a file from the VFS. Returns `true` if the file existed.
91
+
92
+ ### `listFiles(dir?)` → `string[]`
93
+
94
+ List entries in a VFS directory.
95
+
96
+ ### `fileExists(path)` → `boolean`
97
+
98
+ Check if a path exists in the VFS.
99
+
100
+ ### `mkdir(path)`
101
+
102
+ Create a directory (and parent directories) in the VFS.
103
+
104
+ ### `isDirectory(path)` → `boolean`
105
+
106
+ Check if a path is a directory in the VFS.
107
+
108
+ ### `vfsStats()` → `VFSStats`
109
+
110
+ Get VFS usage statistics: `{ files, bytes, maxFiles, maxBytes, maxFileBytes }`.
111
+
112
+ ### `resetVFS()`
113
+
114
+ Clear all files and directories from the VFS.
115
+
116
+ ### `flushVFS()` → `Promise<void>`
117
+
118
+ Persist VFS changes to the configured backend. No-op if no backend was provided.
119
+
120
+ ### `resetVFSAndBackend()` → `Promise<void>`
121
+
122
+ Clear the VFS and the persistent backend storage.
123
+
124
+ ### `dispose()`
125
+
126
+ Free WASM memory. Interpreter cannot be used after this.
127
+
128
+ ## Virtual Filesystem
129
+
130
+ ```js
131
+ // Seed files from JS
132
+ sema.writeFile("/lib/utils.sema", "(define (double x) (* x 2))");
133
+
134
+ // Build a file browser
135
+ const files = sema.listFiles("/"); // ["lib"]
136
+ const libFiles = sema.listFiles("/lib"); // ["utils.sema"]
137
+
138
+ // Read back
139
+ const source = sema.readFile("/lib/utils.sema");
140
+
141
+ // Check quota usage
142
+ const stats = sema.vfsStats();
143
+ // { files: 1, bytes: 28, maxFiles: 256, maxBytes: 16777216, maxFileBytes: 1048576 }
144
+
145
+ // Clean up
146
+ sema.resetVFS();
147
+ ```
148
+
149
+ ## VFS Persistence
150
+
151
+ By default, VFS files are lost on page reload. Use a backend to persist them:
152
+
153
+ ```js
154
+ import { SemaInterpreter, IndexedDBBackend } from "@sema-lang/sema";
155
+
156
+ const sema = await SemaInterpreter.create({
157
+ vfs: new IndexedDBBackend({ namespace: "my-project" }),
158
+ });
159
+
160
+ // Files written by Sema code are persisted after flush
161
+ await sema.evalStrAsync('(file/write "/hello.txt" "Hello!")');
162
+ await sema.flushVFS();
163
+
164
+ // On next page load, files are automatically restored
165
+ ```
166
+
167
+ ### Built-in Backends
168
+
169
+ | Backend | Persistence | Size Limit | Best For |
170
+ |---------|-------------|------------|----------|
171
+ | `MemoryBackend` | None (lost on reload) | WASM quota only | Testing, ephemeral sandboxes |
172
+ | `LocalStorageBackend` | Across page loads | ~5–10 MB per origin | Small projects |
173
+ | `SessionStorageBackend` | Within tab session | ~5–10 MB per origin | Scratch work, drafts |
174
+ | `IndexedDBBackend` | Across page loads | Hundreds of MB | **Production use** |
175
+
176
+ All backends accept a `{ namespace }` option (default: `"sema-vfs"`) to isolate storage between different apps or interpreter instances.
177
+
178
+ ### Custom Backends
179
+
180
+ Implement the `VFSBackend` interface to use any storage mechanism:
181
+
182
+ ```ts
183
+ import type { VFSBackend, VFSHost } from "@sema-lang/sema";
184
+
185
+ class MyBackend implements VFSBackend {
186
+ async init() { /* open connections */ }
187
+ async hydrate(host: VFSHost) { /* restore files into host */ }
188
+ async flush(host: VFSHost) { /* save files from host */ }
189
+ async reset() { /* clear storage */ }
190
+ }
191
+ ```
192
+
193
+ ## Sandbox
194
+
195
+ ```js
196
+ const sema = await SemaInterpreter.create({
197
+ deny: ["network"], // deny HTTP access
198
+ });
199
+
200
+ sema.evalStr('(http/get "https://example.com")'); // => PermissionDenied error
201
+ sema.evalStr("(+ 1 2)"); // => works fine
202
+ ```
203
+
204
+ ## Documentation
205
+
206
+ Full documentation: [sema-lang.com/docs/embedding-js](https://sema-lang.com/docs/embedding-js.html)
207
+
208
+ ## License
209
+
210
+ MIT
@@ -0,0 +1,62 @@
1
+ import type { VFSBackend, VFSHost } from "../vfs.js";
2
+ /** Options for {@link IndexedDBBackend}. */
3
+ export interface IndexedDBBackendOptions {
4
+ /**
5
+ * IndexedDB database name.
6
+ * @default "sema-vfs"
7
+ */
8
+ namespace?: string;
9
+ }
10
+ /**
11
+ * VFS backend that persists files to IndexedDB.
12
+ *
13
+ * Unlike the localStorage-based backends, IndexedDB supports large blobs and
14
+ * doesn't share its quota with other synchronous storage. All reads and writes
15
+ * are async, which avoids blocking the main thread.
16
+ *
17
+ * Uses a single object store (`"files"`) keyed by `path`.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { SemaInterpreter, IndexedDBBackend } from "@sema-lang/sema";
22
+ *
23
+ * const sema = await SemaInterpreter.create({
24
+ * vfs: new IndexedDBBackend({ namespace: "my-project" }),
25
+ * });
26
+ * await sema.evalStrAsync(code);
27
+ * await sema.flushVFS();
28
+ * ```
29
+ */
30
+ export declare class IndexedDBBackend implements VFSBackend {
31
+ private dbName;
32
+ private db;
33
+ constructor(opts?: IndexedDBBackendOptions);
34
+ /** Open (or create) the IndexedDB database and cache the connection. */
35
+ init(): Promise<void>;
36
+ /**
37
+ * Populate the in-memory WASM VFS from IndexedDB.
38
+ *
39
+ * Directories are restored first (sorted by depth so parents are created
40
+ * before children), then files.
41
+ */
42
+ hydrate(host: VFSHost): Promise<void>;
43
+ /**
44
+ * Persist the current in-memory VFS state to IndexedDB.
45
+ *
46
+ * Clears the object store and writes all files and directories in a single
47
+ * readwrite transaction.
48
+ */
49
+ flush(host: VFSHost): Promise<void>;
50
+ /** Clear all persisted data from the object store. */
51
+ reset(): Promise<void>;
52
+ /** Open the IndexedDB database, creating the object store if needed. */
53
+ private openDB;
54
+ /** Read all records from the `"files"` object store. */
55
+ private getAll;
56
+ /** Wait for a transaction to complete. */
57
+ private txComplete;
58
+ /** Recursively collect all file paths. */
59
+ private collectFiles;
60
+ /** Recursively collect all directory paths. */
61
+ private collectDirs;
62
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * VFS backend that persists files to IndexedDB.
3
+ *
4
+ * Unlike the localStorage-based backends, IndexedDB supports large blobs and
5
+ * doesn't share its quota with other synchronous storage. All reads and writes
6
+ * are async, which avoids blocking the main thread.
7
+ *
8
+ * Uses a single object store (`"files"`) keyed by `path`.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { SemaInterpreter, IndexedDBBackend } from "@sema-lang/sema";
13
+ *
14
+ * const sema = await SemaInterpreter.create({
15
+ * vfs: new IndexedDBBackend({ namespace: "my-project" }),
16
+ * });
17
+ * await sema.evalStrAsync(code);
18
+ * await sema.flushVFS();
19
+ * ```
20
+ */
21
+ export class IndexedDBBackend {
22
+ constructor(opts) {
23
+ this.db = null;
24
+ this.dbName = opts?.namespace ?? "sema-vfs";
25
+ }
26
+ /** Open (or create) the IndexedDB database and cache the connection. */
27
+ async init() {
28
+ this.db = await this.openDB();
29
+ }
30
+ /**
31
+ * Populate the in-memory WASM VFS from IndexedDB.
32
+ *
33
+ * Directories are restored first (sorted by depth so parents are created
34
+ * before children), then files.
35
+ */
36
+ async hydrate(host) {
37
+ const db = this.db ?? await this.openDB();
38
+ const records = await this.getAll(db);
39
+ // Restore directories first, shallowest to deepest
40
+ const dirs = records
41
+ .filter((r) => r.isDir)
42
+ .sort((a, b) => a.path.split("/").length - b.path.split("/").length);
43
+ for (const rec of dirs) {
44
+ host.mkdir(rec.path);
45
+ }
46
+ // Restore files
47
+ for (const rec of records) {
48
+ if (!rec.isDir && rec.content !== undefined) {
49
+ host.writeFile(rec.path, rec.content);
50
+ }
51
+ }
52
+ }
53
+ /**
54
+ * Persist the current in-memory VFS state to IndexedDB.
55
+ *
56
+ * Clears the object store and writes all files and directories in a single
57
+ * readwrite transaction.
58
+ */
59
+ async flush(host) {
60
+ const db = this.db ?? await this.openDB();
61
+ const tx = db.transaction("files", "readwrite");
62
+ const store = tx.objectStore("files");
63
+ store.clear();
64
+ // Write directories
65
+ const dirs = this.collectDirs(host, "/");
66
+ for (const dir of dirs) {
67
+ store.put({ path: dir, isDir: true });
68
+ }
69
+ // Write files
70
+ const files = this.collectFiles(host, "/");
71
+ for (const filePath of files) {
72
+ const content = host.readFile(filePath);
73
+ if (content !== null) {
74
+ store.put({
75
+ path: filePath,
76
+ content,
77
+ isDir: false,
78
+ });
79
+ }
80
+ }
81
+ await this.txComplete(tx);
82
+ }
83
+ /** Clear all persisted data from the object store. */
84
+ async reset() {
85
+ const db = this.db ?? await this.openDB();
86
+ const tx = db.transaction("files", "readwrite");
87
+ tx.objectStore("files").clear();
88
+ await this.txComplete(tx);
89
+ }
90
+ // ---------------------------------------------------------------------------
91
+ // Private helpers
92
+ // ---------------------------------------------------------------------------
93
+ /** Open the IndexedDB database, creating the object store if needed. */
94
+ openDB() {
95
+ return new Promise((resolve, reject) => {
96
+ const req = indexedDB.open(this.dbName, 1);
97
+ req.onupgradeneeded = () => {
98
+ const db = req.result;
99
+ if (!db.objectStoreNames.contains("files")) {
100
+ db.createObjectStore("files", { keyPath: "path" });
101
+ }
102
+ };
103
+ req.onsuccess = () => resolve(req.result);
104
+ req.onerror = () => reject(req.error);
105
+ });
106
+ }
107
+ /** Read all records from the `"files"` object store. */
108
+ getAll(db) {
109
+ return new Promise((resolve, reject) => {
110
+ const tx = db.transaction("files", "readonly");
111
+ const req = tx.objectStore("files").getAll();
112
+ req.onsuccess = () => resolve(req.result);
113
+ req.onerror = () => reject(req.error);
114
+ });
115
+ }
116
+ /** Wait for a transaction to complete. */
117
+ txComplete(tx) {
118
+ return new Promise((resolve, reject) => {
119
+ tx.oncomplete = () => resolve();
120
+ tx.onerror = () => reject(tx.error);
121
+ });
122
+ }
123
+ /** Recursively collect all file paths. */
124
+ collectFiles(host, dir) {
125
+ const result = [];
126
+ const entries = host.listFiles(dir);
127
+ for (const name of entries) {
128
+ const full = dir === "/" ? "/" + name : dir + "/" + name;
129
+ if (host.isDirectory(full)) {
130
+ result.push(...this.collectFiles(host, full));
131
+ }
132
+ else {
133
+ result.push(full);
134
+ }
135
+ }
136
+ return result;
137
+ }
138
+ /** Recursively collect all directory paths. */
139
+ collectDirs(host, dir) {
140
+ const result = [];
141
+ const entries = host.listFiles(dir);
142
+ for (const name of entries) {
143
+ const full = dir === "/" ? "/" + name : dir + "/" + name;
144
+ if (host.isDirectory(full)) {
145
+ result.push(full);
146
+ result.push(...this.collectDirs(host, full));
147
+ }
148
+ }
149
+ return result;
150
+ }
151
+ }
@@ -0,0 +1,20 @@
1
+ import { WebStorageBackend } from "./web-storage.js";
2
+ import type { WebStorageBackendOptions } from "./web-storage.js";
3
+ /** Options for LocalStorageBackend. */
4
+ export type LocalStorageBackendOptions = WebStorageBackendOptions;
5
+ /**
6
+ * VFS backend that persists files to localStorage.
7
+ *
8
+ * Simple and synchronous — good for small projects (&lt; 5 MB).
9
+ * localStorage has a ~5–10 MB limit per origin in most browsers.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const sema = await SemaInterpreter.create({
14
+ * vfs: new LocalStorageBackend({ namespace: "my-project" }),
15
+ * });
16
+ * ```
17
+ */
18
+ export declare class LocalStorageBackend extends WebStorageBackend {
19
+ constructor(opts?: LocalStorageBackendOptions);
20
+ }
@@ -0,0 +1,19 @@
1
+ import { WebStorageBackend } from "./web-storage.js";
2
+ /**
3
+ * VFS backend that persists files to localStorage.
4
+ *
5
+ * Simple and synchronous — good for small projects (&lt; 5 MB).
6
+ * localStorage has a ~5–10 MB limit per origin in most browsers.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * const sema = await SemaInterpreter.create({
11
+ * vfs: new LocalStorageBackend({ namespace: "my-project" }),
12
+ * });
13
+ * ```
14
+ */
15
+ export class LocalStorageBackend extends WebStorageBackend {
16
+ constructor(opts) {
17
+ super(localStorage, opts);
18
+ }
19
+ }
@@ -0,0 +1,21 @@
1
+ import type { VFSBackend, VFSHost } from "../vfs.js";
2
+ /**
3
+ * Ephemeral VFS backend — no persistence.
4
+ *
5
+ * Files exist only in the WASM memory and are lost on page reload.
6
+ * Use this when you don't need persistence, or for testing.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { SemaInterpreter, MemoryBackend } from "@sema-lang/sema";
11
+ *
12
+ * const sema = await SemaInterpreter.create({
13
+ * vfs: new MemoryBackend(),
14
+ * });
15
+ * ```
16
+ */
17
+ export declare class MemoryBackend implements VFSBackend {
18
+ hydrate(_host: VFSHost): Promise<void>;
19
+ flush(_host: VFSHost): Promise<void>;
20
+ reset(): Promise<void>;
21
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Ephemeral VFS backend — no persistence.
3
+ *
4
+ * Files exist only in the WASM memory and are lost on page reload.
5
+ * Use this when you don't need persistence, or for testing.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { SemaInterpreter, MemoryBackend } from "@sema-lang/sema";
10
+ *
11
+ * const sema = await SemaInterpreter.create({
12
+ * vfs: new MemoryBackend(),
13
+ * });
14
+ * ```
15
+ */
16
+ export class MemoryBackend {
17
+ async hydrate(_host) { }
18
+ async flush(_host) { }
19
+ async reset() { }
20
+ }
@@ -0,0 +1,24 @@
1
+ import { WebStorageBackend } from "./web-storage.js";
2
+ import type { WebStorageBackendOptions } from "./web-storage.js";
3
+ /** Options for SessionStorageBackend. */
4
+ export type SessionStorageBackendOptions = WebStorageBackendOptions;
5
+ /**
6
+ * VFS backend that persists files to sessionStorage.
7
+ *
8
+ * Data persists within the current browser tab/window session only —
9
+ * it is cleared when the tab is closed. Works well for scratch pads,
10
+ * playground-style editors, or any context where cross-session
11
+ * persistence is unnecessary.
12
+ *
13
+ * Like {@link LocalStorageBackend}, the ~5–10 MB per-origin limit applies.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * const sema = await SemaInterpreter.create({
18
+ * vfs: new SessionStorageBackend({ namespace: "playground" }),
19
+ * });
20
+ * ```
21
+ */
22
+ export declare class SessionStorageBackend extends WebStorageBackend {
23
+ constructor(opts?: SessionStorageBackendOptions);
24
+ }
@@ -0,0 +1,23 @@
1
+ import { WebStorageBackend } from "./web-storage.js";
2
+ /**
3
+ * VFS backend that persists files to sessionStorage.
4
+ *
5
+ * Data persists within the current browser tab/window session only —
6
+ * it is cleared when the tab is closed. Works well for scratch pads,
7
+ * playground-style editors, or any context where cross-session
8
+ * persistence is unnecessary.
9
+ *
10
+ * Like {@link LocalStorageBackend}, the ~5–10 MB per-origin limit applies.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const sema = await SemaInterpreter.create({
15
+ * vfs: new SessionStorageBackend({ namespace: "playground" }),
16
+ * });
17
+ * ```
18
+ */
19
+ export class SessionStorageBackend extends WebStorageBackend {
20
+ constructor(opts) {
21
+ super(sessionStorage, opts);
22
+ }
23
+ }
@@ -0,0 +1,32 @@
1
+ import type { VFSBackend, VFSHost } from "../vfs.js";
2
+ /** Options for WebStorageBackend and its subclasses. */
3
+ export interface WebStorageBackendOptions {
4
+ /**
5
+ * Namespace prefix for storage keys.
6
+ * Each file is stored as `${namespace}:f:${path}`.
7
+ * Directories are stored in a manifest key `${namespace}:__dirs__`.
8
+ * @default "sema-vfs"
9
+ */
10
+ namespace?: string;
11
+ }
12
+ /**
13
+ * Base class for VFS backends backed by a Web Storage API (`Storage`) object.
14
+ *
15
+ * Handles hydrate/flush/reset using namespace-prefixed keys and a directory
16
+ * manifest. Subclasses only need to pass the concrete `Storage` instance
17
+ * (e.g. `localStorage` or `sessionStorage`).
18
+ */
19
+ export declare class WebStorageBackend implements VFSBackend {
20
+ private storage;
21
+ private ns;
22
+ private filePrefix;
23
+ private dirsKey;
24
+ constructor(storage: Storage, opts?: WebStorageBackendOptions);
25
+ hydrate(host: VFSHost): Promise<void>;
26
+ flush(host: VFSHost): Promise<void>;
27
+ reset(): Promise<void>;
28
+ /** Recursively collect all file paths. */
29
+ private collectFiles;
30
+ /** Recursively collect all directory paths. */
31
+ private collectDirs;
32
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Base class for VFS backends backed by a Web Storage API (`Storage`) object.
3
+ *
4
+ * Handles hydrate/flush/reset using namespace-prefixed keys and a directory
5
+ * manifest. Subclasses only need to pass the concrete `Storage` instance
6
+ * (e.g. `localStorage` or `sessionStorage`).
7
+ */
8
+ export class WebStorageBackend {
9
+ constructor(storage, opts) {
10
+ this.storage = storage;
11
+ this.ns = opts?.namespace ?? "sema-vfs";
12
+ this.filePrefix = this.ns + ":f:";
13
+ this.dirsKey = this.ns + ":__dirs__";
14
+ }
15
+ async hydrate(host) {
16
+ // Restore directories first (so file writes into them work)
17
+ const dirsJson = this.storage.getItem(this.dirsKey);
18
+ if (dirsJson) {
19
+ try {
20
+ const dirs = JSON.parse(dirsJson);
21
+ for (const dir of dirs) {
22
+ host.mkdir(dir);
23
+ }
24
+ }
25
+ catch { /* ignore corrupt data */ }
26
+ }
27
+ // Restore files
28
+ for (let i = 0; i < this.storage.length; i++) {
29
+ const key = this.storage.key(i);
30
+ if (key && key.startsWith(this.filePrefix)) {
31
+ const path = key.slice(this.filePrefix.length);
32
+ const content = this.storage.getItem(key);
33
+ if (content !== null) {
34
+ host.writeFile(path, content);
35
+ }
36
+ }
37
+ }
38
+ }
39
+ async flush(host) {
40
+ // Clear old entries for this namespace
41
+ const toRemove = [];
42
+ for (let i = 0; i < this.storage.length; i++) {
43
+ const key = this.storage.key(i);
44
+ if (key && (key.startsWith(this.filePrefix) || key === this.dirsKey)) {
45
+ toRemove.push(key);
46
+ }
47
+ }
48
+ for (const key of toRemove) {
49
+ this.storage.removeItem(key);
50
+ }
51
+ // Write current files
52
+ const allFiles = this.collectFiles(host, "/");
53
+ for (const path of allFiles) {
54
+ const content = host.readFile(path);
55
+ if (content !== null) {
56
+ this.storage.setItem(this.filePrefix + path, content);
57
+ }
58
+ }
59
+ // Write directory manifest
60
+ const dirs = this.collectDirs(host, "/");
61
+ this.storage.setItem(this.dirsKey, JSON.stringify(dirs));
62
+ }
63
+ async reset() {
64
+ const toRemove = [];
65
+ for (let i = 0; i < this.storage.length; i++) {
66
+ const key = this.storage.key(i);
67
+ if (key && (key.startsWith(this.filePrefix) || key === this.dirsKey)) {
68
+ toRemove.push(key);
69
+ }
70
+ }
71
+ for (const key of toRemove) {
72
+ this.storage.removeItem(key);
73
+ }
74
+ }
75
+ /** Recursively collect all file paths. */
76
+ collectFiles(host, dir) {
77
+ const result = [];
78
+ const entries = host.listFiles(dir);
79
+ for (const name of entries) {
80
+ const full = dir === "/" ? "/" + name : dir + "/" + name;
81
+ if (host.isDirectory(full)) {
82
+ result.push(...this.collectFiles(host, full));
83
+ }
84
+ else {
85
+ result.push(full);
86
+ }
87
+ }
88
+ return result;
89
+ }
90
+ /** Recursively collect all directory paths. */
91
+ collectDirs(host, dir) {
92
+ const result = [];
93
+ const entries = host.listFiles(dir);
94
+ for (const name of entries) {
95
+ const full = dir === "/" ? "/" + name : dir + "/" + name;
96
+ if (host.isDirectory(full)) {
97
+ result.push(full);
98
+ result.push(...this.collectDirs(host, full));
99
+ }
100
+ }
101
+ return result;
102
+ }
103
+ }