@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 +210 -0
- package/dist/backends/indexed-db.d.ts +62 -0
- package/dist/backends/indexed-db.js +151 -0
- package/dist/backends/local-storage.d.ts +20 -0
- package/dist/backends/local-storage.js +19 -0
- package/dist/backends/memory.d.ts +21 -0
- package/dist/backends/memory.js +20 -0
- package/dist/backends/session-storage.d.ts +24 -0
- package/dist/backends/session-storage.js +23 -0
- package/dist/backends/web-storage.d.ts +32 -0
- package/dist/backends/web-storage.js +103 -0
- package/dist/index.d.ts +299 -0
- package/dist/index.js +324 -0
- package/dist/vfs.d.ts +53 -0
- package/dist/vfs.js +1 -0
- package/package.json +49 -0
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 (< 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 (< 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
|
+
}
|