toolcraft 0.0.761 → 0.0.763
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/composition.json +2 -2
- package/dist/composition.json +2 -2
- package/node_modules/@poe-code/safe-fs/README.md +8 -0
- package/node_modules/@poe-code/safe-fs/dist/contracts/filesystem.d.ts +5 -0
- package/node_modules/@poe-code/safe-fs/dist/core.d.ts +1 -0
- package/node_modules/@poe-code/safe-fs/dist/core.js +1 -0
- package/node_modules/@poe-code/safe-fs/dist/fs/devices/index.d.ts +1 -0
- package/node_modules/@poe-code/safe-fs/dist/fs/devices/index.js +34 -0
- package/node_modules/@poe-code/safe-fs/dist/fs/memory/index.d.ts +1 -0
- package/node_modules/@poe-code/safe-fs/dist/fs/memory/index.js +20 -0
- package/node_modules/@poe-code/safe-fs/dist/fs/readonly/index.d.ts +1 -0
- package/node_modules/@poe-code/safe-fs/dist/fs/readonly/index.js +10 -0
- package/node_modules/@poe-code/safe-fs/dist/fs/real/index.d.ts +1 -0
- package/node_modules/@poe-code/safe-fs/dist/fs/real/index.js +27 -0
- package/node_modules/@poe-code/safe-fs/dist/fs/scoped.js +9 -3
- package/node_modules/@poe-code/safe-fs/dist/storage.d.ts +49 -0
- package/node_modules/@poe-code/safe-fs/dist/storage.js +265 -0
- package/node_modules/@poe-code/safe-fs/package.json +7 -0
- package/node_modules/tiny-stdio-mcp-server/dist/composition.json +1 -1
- package/node_modules/toolcraft-schema/package.json +1 -1
- package/package.json +2 -2
package/composition.json
CHANGED
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
},
|
|
134
134
|
{
|
|
135
135
|
"name": "toolcraft",
|
|
136
|
-
"version": "0.0.
|
|
136
|
+
"version": "0.0.763",
|
|
137
137
|
"license": "MIT"
|
|
138
138
|
},
|
|
139
139
|
{
|
|
@@ -143,7 +143,7 @@
|
|
|
143
143
|
},
|
|
144
144
|
{
|
|
145
145
|
"name": "toolcraft-schema",
|
|
146
|
-
"version": "0.0.
|
|
146
|
+
"version": "0.0.763",
|
|
147
147
|
"license": "MIT"
|
|
148
148
|
},
|
|
149
149
|
{
|
package/dist/composition.json
CHANGED
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
},
|
|
134
134
|
{
|
|
135
135
|
"name": "toolcraft",
|
|
136
|
-
"version": "0.0.
|
|
136
|
+
"version": "0.0.763",
|
|
137
137
|
"license": "MIT"
|
|
138
138
|
},
|
|
139
139
|
{
|
|
@@ -143,7 +143,7 @@
|
|
|
143
143
|
},
|
|
144
144
|
{
|
|
145
145
|
"name": "toolcraft-schema",
|
|
146
|
-
"version": "0.0.
|
|
146
|
+
"version": "0.0.763",
|
|
147
147
|
"license": "MIT"
|
|
148
148
|
},
|
|
149
149
|
{
|
|
@@ -4,6 +4,13 @@ An asynchronous filesystem interface for memory, rooted host directories, S3, an
|
|
|
4
4
|
|
|
5
5
|
Under `workerd` and `browser` conditions, the root export includes portable filesystems, `S3FileSystem`, and `MockS3Client`. Supply an S3 transport to use object storage without Node globals.
|
|
6
6
|
|
|
7
|
+
The `storage` subpath exposes `PagedStorage` and `IntegerTable` for temporary
|
|
8
|
+
working data. Supply `{fs, cwd, env, signal}` and an optional page-cache count
|
|
9
|
+
(default 64 pages of 16 KiB). Scratch data uses retained positioned handles in
|
|
10
|
+
`env.TMPDIR` or `cwd`, with conditional unlink and cleanup through `close()`.
|
|
11
|
+
Providers must support those capabilities; there is no host fallback. Use an
|
|
12
|
+
external backend for large data because memory filesystems retain spilled bytes.
|
|
13
|
+
|
|
7
14
|
## Quickstart
|
|
8
15
|
|
|
9
16
|
Install the standalone package; it does not depend on SafeJS, Safe Bash, or the `poe-code` CLI. These examples use TypeScript in a Node ESM application.
|
|
@@ -79,6 +86,7 @@ cause. Preserve cancellation/control exceptions separately. Cross-family
|
|
|
79
86
|
| Change the namespace | `mkdir`, `rm`, `rename`, `copyFile` |
|
|
80
87
|
| Remove an empty directory | Optional `rmdir`; never a recursive-delete fallback |
|
|
81
88
|
| Links and metadata | Optional `readlink`, `symlink`, `link`, `chmod`, `utimes`, `truncate` |
|
|
89
|
+
| Enumerate directories lazily | Optional `iterateDirectory`; memory and native backends, with readonly/quota/device/scoped delegation; backend order, no sorted-array fallback |
|
|
82
90
|
| Stream bytes | Optional `readStream`, `writeStream`, using async iterables of byte chunks |
|
|
83
91
|
| Retain an open file | Optional `open` with positioned I/O and synchronization; memory and real backends support `noFollow: true` to atomically refuse a final symlink |
|
|
84
92
|
| Publish immutable objects atomically | Optional `publishFileConditional` with opaque identity/version stats and authoritative compare-and-publish |
|
|
@@ -352,6 +352,11 @@ export interface FileSystem {
|
|
|
352
352
|
lstat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
353
353
|
compareEntry?(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<EntryComparison>;
|
|
354
354
|
readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
|
|
355
|
+
/** Optional bounded, lazy directory enumeration in backend-defined order.
|
|
356
|
+
* No complete listing/sort fallback. Consumers must retire the iterator on
|
|
357
|
+
* early exit. Namespace changes may reject or follow the backend's live view;
|
|
358
|
+
* this is not a tree snapshot or a retained path-authority grant. */
|
|
359
|
+
iterateDirectory?(path: string, options?: FsOptions): AsyncIterable<DirectoryEntry>;
|
|
355
360
|
mkdir(path: string, options?: MkdirOptions): Promise<void>;
|
|
356
361
|
rm(path: string, options?: RemoveOptions): Promise<void>;
|
|
357
362
|
/** Atomic final-entry removal; every directory is refused, including raced replacements. */
|
|
@@ -18,3 +18,4 @@ export { createFileSystem, readConfigRecord, validateFileSystemConfig } from "./
|
|
|
18
18
|
export type { FileSystemConfig, FileSystemAdapterDescriptor, FileSystemAdapterRegistry } from "./config.js";
|
|
19
19
|
export { createPortableFileSystemAdapterRegistry } from "./config.portable.js";
|
|
20
20
|
export { readFileStream } from "./fs/read-file-stream.js";
|
|
21
|
+
export { PagedStorage, IntegerTable, type PagedStorageContext } from "./storage.js";
|
|
@@ -16,3 +16,4 @@ export * from "./fs/s3/http/index.js";
|
|
|
16
16
|
export { createFileSystem, readConfigRecord, validateFileSystemConfig } from "./config.js";
|
|
17
17
|
export { createPortableFileSystemAdapterRegistry } from "./config.portable.js";
|
|
18
18
|
export { readFileStream } from "./fs/read-file-stream.js";
|
|
19
|
+
export { PagedStorage, IntegerTable } from "./storage.js";
|
|
@@ -25,6 +25,7 @@ export declare class DeviceFileSystem implements FileSystem {
|
|
|
25
25
|
writeFile(path: string, data: Uint8Array, options?: WriteFileOptions): Promise<void>;
|
|
26
26
|
appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
|
|
27
27
|
writeStream(path: string, source: ByteSource, options?: WriteFileOptions): Promise<void>;
|
|
28
|
+
iterateDirectory(path: string, options?: FsOptions): AsyncIterable<DirectoryEntry>;
|
|
28
29
|
readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
|
|
29
30
|
mkdir(path: string, options?: MkdirOptions): Promise<void>;
|
|
30
31
|
rm(path: string, options?: RemoveOptions): Promise<void>;
|
|
@@ -490,6 +490,40 @@ export class DeviceFileSystem {
|
|
|
490
490
|
throw new FsError("ENOTSUP", { path });
|
|
491
491
|
await this.#filesystem.writeStream(path, source, options);
|
|
492
492
|
}
|
|
493
|
+
async *iterateDirectory(path, options = {}) {
|
|
494
|
+
const resolved = await this.#resolve(path, options);
|
|
495
|
+
if (resolved === nullPath)
|
|
496
|
+
throw new FsError("ENOTDIR", { syscall: "iterateDirectory", path });
|
|
497
|
+
const virtual = resolved === "/" ? "dev" : resolved === deviceDirectory ? "null" : undefined;
|
|
498
|
+
if (!this.#filesystem.iterateDirectory)
|
|
499
|
+
throw new FsError("ENOTSUP", { syscall: "iterateDirectory", path });
|
|
500
|
+
let seen = false;
|
|
501
|
+
try {
|
|
502
|
+
for await (const entry of this.#filesystem.iterateDirectory(virtual ? resolved : path, options)) {
|
|
503
|
+
options.signal?.throwIfAborted();
|
|
504
|
+
if (entry.name === "." || entry.name === "..")
|
|
505
|
+
continue;
|
|
506
|
+
if (!entry.name || entry.name.includes("/") || entry.name.includes("\0"))
|
|
507
|
+
throw new FsError("EIO", { path, message: "invalid directory entry" });
|
|
508
|
+
if (entry.name === virtual) {
|
|
509
|
+
if (seen)
|
|
510
|
+
continue;
|
|
511
|
+
seen = true;
|
|
512
|
+
yield { name: virtual, type: virtual === "dev" ? "directory" : "character" };
|
|
513
|
+
}
|
|
514
|
+
else
|
|
515
|
+
yield { name: entry.name, type: entry.type };
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
catch (error) {
|
|
519
|
+
options.signal?.throwIfAborted();
|
|
520
|
+
if (!virtual || !isFsError(error, "ENOENT") && !isFsError(error, "ENOTDIR"))
|
|
521
|
+
throw error;
|
|
522
|
+
}
|
|
523
|
+
options.signal?.throwIfAborted();
|
|
524
|
+
if (virtual && !seen)
|
|
525
|
+
yield { name: virtual, type: virtual === "dev" ? "directory" : "character" };
|
|
526
|
+
}
|
|
493
527
|
readdir(path, options = {}) {
|
|
494
528
|
const fast = this.#tryResolveSync(path, options);
|
|
495
529
|
if (fast !== undefined && fast !== "/") {
|
|
@@ -134,6 +134,7 @@ export declare class MemoryFileSystem implements FileSystem {
|
|
|
134
134
|
appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
|
|
135
135
|
stat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
136
136
|
lstat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
137
|
+
iterateDirectory(path: string, options?: FsOptions): AsyncIterable<DirectoryEntry>;
|
|
137
138
|
readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
|
|
138
139
|
tryMkdirFastSync(path: string, recursive: boolean, mode: number | undefined): boolean;
|
|
139
140
|
tryRmFastSync(path: string, recursive: boolean, force: boolean): boolean;
|
|
@@ -2508,6 +2508,26 @@ export class MemoryFileSystem {
|
|
|
2508
2508
|
ownedStats.set(stat, { filesystem: this, path, root: this.root });
|
|
2509
2509
|
return stat;
|
|
2510
2510
|
}
|
|
2511
|
+
async *iterateDirectory(path, options = {}) {
|
|
2512
|
+
options.signal?.throwIfAborted();
|
|
2513
|
+
const root = this.root, node = this.resolveNode(path, "iterateDirectory", true);
|
|
2514
|
+
if (node.type !== "directory")
|
|
2515
|
+
this.fail("ENOTDIR", "iterateDirectory", path);
|
|
2516
|
+
this.permission(node, 4, "iterateDirectory", path);
|
|
2517
|
+
const revision = node.revision, ino = node.ino;
|
|
2518
|
+
const check = () => {
|
|
2519
|
+
options.signal?.throwIfAborted();
|
|
2520
|
+
if (this.root !== root || node.ino !== ino || node.revision !== revision || this.resolveNode(path, "iterateDirectory", true) !== node)
|
|
2521
|
+
this.fail("EBUSY", "iterateDirectory", path);
|
|
2522
|
+
this.permission(node, 4, "iterateDirectory", path);
|
|
2523
|
+
};
|
|
2524
|
+
node.atimeMs = Date.now();
|
|
2525
|
+
for (const [name, entry] of node.entries) {
|
|
2526
|
+
check();
|
|
2527
|
+
yield { name, type: entry.type };
|
|
2528
|
+
}
|
|
2529
|
+
check();
|
|
2530
|
+
}
|
|
2511
2531
|
async readdir(path, options = {}) {
|
|
2512
2532
|
const limit = directoryEntryLimit(options, path);
|
|
2513
2533
|
const node = this.resolveNode(path, "readdir", true);
|
|
@@ -13,6 +13,7 @@ export declare class ReadOnlyFileSystem implements FileSystem {
|
|
|
13
13
|
stat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
14
14
|
lstat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
15
15
|
compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<import("../../contracts/filesystem.js").EntryComparison>;
|
|
16
|
+
iterateDirectory(path: string, options?: FsOptions): AsyncIterable<DirectoryEntry>;
|
|
16
17
|
readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
|
|
17
18
|
realpath(path: string, options?: FsOptions): Promise<string>;
|
|
18
19
|
access(path: string, mode?: number, options?: FsOptions): Promise<void>;
|
|
@@ -122,6 +122,16 @@ export class ReadOnlyFileSystem {
|
|
|
122
122
|
compareEntry(path, peer, peerPath, options = {}) {
|
|
123
123
|
return compareEntries(this, path, peer, peerPath, options);
|
|
124
124
|
}
|
|
125
|
+
async *iterateDirectory(path, options = {}) {
|
|
126
|
+
options.signal?.throwIfAborted();
|
|
127
|
+
if (!this.#filesystem.iterateDirectory)
|
|
128
|
+
throw new FsError("ENOTSUP", { syscall: "iterateDirectory", path });
|
|
129
|
+
for await (const entry of this.#filesystem.iterateDirectory(path, options)) {
|
|
130
|
+
options.signal?.throwIfAborted();
|
|
131
|
+
yield { name: entry.name, type: entry.type };
|
|
132
|
+
}
|
|
133
|
+
options.signal?.throwIfAborted();
|
|
134
|
+
}
|
|
125
135
|
async readdir(path, options) {
|
|
126
136
|
const limit = options?.maxEntries === undefined ? undefined : directoryEntryLimit(options, path);
|
|
127
137
|
const entries = await this.#filesystem.readdir(path, options);
|
|
@@ -81,6 +81,7 @@ export declare class RealFileSystem implements FileSystem {
|
|
|
81
81
|
appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
|
|
82
82
|
stat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
83
83
|
lstat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
84
|
+
iterateDirectory(path: string, options?: FsOptions): AsyncIterable<DirectoryEntry>;
|
|
84
85
|
readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
|
|
85
86
|
mkdir(path: string, options?: MkdirOptions): Promise<void>;
|
|
86
87
|
rmdir(path: string, options?: FsOptions): Promise<void>;
|
|
@@ -638,6 +638,33 @@ export class RealFileSystem {
|
|
|
638
638
|
return fileStat(await native.lstat(target, { bigint: true }));
|
|
639
639
|
});
|
|
640
640
|
}
|
|
641
|
+
async *iterateDirectory(path, options = {}) {
|
|
642
|
+
let handle, failed = false;
|
|
643
|
+
try {
|
|
644
|
+
// Preserve the acquired handle even if cancellation arrives during open:
|
|
645
|
+
// the following read admission will observe it and finally will close it.
|
|
646
|
+
handle = await this.operation("iterateDirectory", path, options, async () => {
|
|
647
|
+
const target = await this.path(path, options);
|
|
648
|
+
options.signal?.throwIfAborted();
|
|
649
|
+
return native.opendir(target, { bufferSize: 1 });
|
|
650
|
+
}, undefined, true);
|
|
651
|
+
while (true) {
|
|
652
|
+
const entry = await this.operation("iterateDirectory", path, options, () => handle.read());
|
|
653
|
+
if (entry === null)
|
|
654
|
+
break;
|
|
655
|
+
yield { name: entry.name, type: fileType(entry) };
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
catch (error) {
|
|
659
|
+
failed = true;
|
|
660
|
+
options.signal?.throwIfAborted();
|
|
661
|
+
throw error;
|
|
662
|
+
}
|
|
663
|
+
finally {
|
|
664
|
+
if (handle)
|
|
665
|
+
await finishCleanup(() => handle.close(), failed);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
641
668
|
async readdir(path, options = {}) {
|
|
642
669
|
const limit = directoryEntryLimit(options, path);
|
|
643
670
|
return this.operation("readdir", path, options, async () => {
|
|
@@ -21,7 +21,7 @@ const operations = new Set([
|
|
|
21
21
|
"prepareDirectoryAncestry", "prepareStagingResolution", "publishStagedFileSet",
|
|
22
22
|
"publishFileConditional", "removeEntryConditional", "removeTreeConditional", "writeFileConditional", "removeFileConditional", "createStagedFile", "publishStagedFile", "removeStagedFile", "prepareDirectory",
|
|
23
23
|
"confineExtraction", "access", "appendFile", "canonicalizeMissingTarget", "capabilitiesFor", "chmod", "compareEntry",
|
|
24
|
-
"copyFile", "link", "lstat", "mkdir", "openReadFile", "openResizeFile", "readFile", "readStream", "readdir",
|
|
24
|
+
"copyFile", "link", "lstat", "mkdir", "openReadFile", "openResizeFile", "readFile", "readStream", "readdir", "iterateDirectory",
|
|
25
25
|
"readlink", "realpath", "rename", "resizeFile", "rm", "rmdir", "unlink", "stat", "symlink", "truncate", "utimes",
|
|
26
26
|
"writeFile", "writeStream",
|
|
27
27
|
]);
|
|
@@ -188,7 +188,7 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
|
|
|
188
188
|
close: (options = {}) => closing ??= Promise.resolve().then(() => descriptor.close(options)),
|
|
189
189
|
};
|
|
190
190
|
};
|
|
191
|
-
const wrapStream = (source, options) => {
|
|
191
|
+
const wrapStream = (source, options, chargeNext = false) => {
|
|
192
192
|
const budgetFrames = getScopedTransportBudget();
|
|
193
193
|
return {
|
|
194
194
|
[Symbol.asyncIterator]() {
|
|
@@ -203,6 +203,8 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
|
|
|
203
203
|
await closing;
|
|
204
204
|
return { done: true, value: undefined };
|
|
205
205
|
}
|
|
206
|
+
if (chargeNext)
|
|
207
|
+
admit(options);
|
|
206
208
|
const result = await withScopedTransportBudget(budgetFrames, operation);
|
|
207
209
|
assertOpen(options);
|
|
208
210
|
if (closing) {
|
|
@@ -430,7 +432,11 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
|
|
|
430
432
|
});
|
|
431
433
|
return Reflect.apply(method, original, args);
|
|
432
434
|
})();
|
|
435
|
+
if (property === "iterateDirectory")
|
|
436
|
+
args[1] = { ...args[1], signal: combineSignal(args[1]?.signal) };
|
|
433
437
|
const result = Reflect.apply(method, original, args);
|
|
438
|
+
if (property === "iterateDirectory")
|
|
439
|
+
return wrapStream(result, args[1], true);
|
|
434
440
|
return property === "readStream" ? wrapStream(result, args[1]) : result;
|
|
435
441
|
};
|
|
436
442
|
const dispatch = (...args) => {
|
|
@@ -528,7 +534,7 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
|
|
|
528
534
|
? async (...args) => ownedMutationCapabilities(original, retainedResizeCapabilities(original, await dispatch(...args)))
|
|
529
535
|
: property === "openReadFile"
|
|
530
536
|
? async (...args) => wrapHandle(await dispatch(...args))
|
|
531
|
-
: operations.has(property) && property !== "canonicalizeMissingTarget" && property !== "readStream"
|
|
537
|
+
: operations.has(property) && property !== "canonicalizeMissingTarget" && property !== "readStream" && property !== "iterateDirectory"
|
|
532
538
|
? (...args) => {
|
|
533
539
|
try {
|
|
534
540
|
const res = dispatch(...args);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { FileSystem } from "./contracts/filesystem.js";
|
|
2
|
+
export interface PagedStorageContext {
|
|
3
|
+
readonly fs: FileSystem;
|
|
4
|
+
readonly cwd: string;
|
|
5
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
6
|
+
readonly signal: AbortSignal;
|
|
7
|
+
}
|
|
8
|
+
/** A bounded page cache backed only by the caller filesystem. The default is
|
|
9
|
+
* one MiB; callers own close() on every outcome. Memory filesystems still retain
|
|
10
|
+
* spilled data in RAM, so large workloads require an external backing provider. */
|
|
11
|
+
export declare class PagedStorage {
|
|
12
|
+
private readonly context;
|
|
13
|
+
private readonly maxPages;
|
|
14
|
+
private readonly pages;
|
|
15
|
+
private readonly controller;
|
|
16
|
+
private readonly signal;
|
|
17
|
+
private descriptor;
|
|
18
|
+
private parent;
|
|
19
|
+
private initial;
|
|
20
|
+
private path;
|
|
21
|
+
private diskLength;
|
|
22
|
+
private unlinked;
|
|
23
|
+
private end;
|
|
24
|
+
private active;
|
|
25
|
+
private closing;
|
|
26
|
+
constructor(context: PagedStorageContext, maxPages?: number);
|
|
27
|
+
allocate(length: number): number;
|
|
28
|
+
private operation;
|
|
29
|
+
private spill;
|
|
30
|
+
private flush;
|
|
31
|
+
private page;
|
|
32
|
+
write(position: number, bytes: Uint8Array): Promise<void>;
|
|
33
|
+
append(bytes: Uint8Array): Promise<number>;
|
|
34
|
+
read(position: number, length: number): Promise<Uint8Array>;
|
|
35
|
+
close(): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
/** Fixed-depth radix index: both dense and sparse 64-bit keys use bounded RAM. */
|
|
38
|
+
export declare class IntegerTable {
|
|
39
|
+
private readonly storage;
|
|
40
|
+
private readonly maxEntries;
|
|
41
|
+
private root;
|
|
42
|
+
private readonly cache;
|
|
43
|
+
constructor(storage: PagedStorage, maxEntries?: number);
|
|
44
|
+
private retain;
|
|
45
|
+
get(key: bigint): Promise<bigint | undefined>;
|
|
46
|
+
set(key: bigint, value: bigint): Promise<void>;
|
|
47
|
+
private lookup;
|
|
48
|
+
private persist;
|
|
49
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { FsError, isFsError } from "./contracts/errors.js";
|
|
2
|
+
const pageBytes = 16 * 1024;
|
|
3
|
+
let serial = 0;
|
|
4
|
+
/** A bounded page cache backed only by the caller filesystem. The default is
|
|
5
|
+
* one MiB; callers own close() on every outcome. Memory filesystems still retain
|
|
6
|
+
* spilled data in RAM, so large workloads require an external backing provider. */
|
|
7
|
+
export class PagedStorage {
|
|
8
|
+
context;
|
|
9
|
+
maxPages;
|
|
10
|
+
pages = new Map();
|
|
11
|
+
controller = new AbortController();
|
|
12
|
+
signal;
|
|
13
|
+
descriptor;
|
|
14
|
+
parent;
|
|
15
|
+
initial;
|
|
16
|
+
path;
|
|
17
|
+
diskLength = 0;
|
|
18
|
+
unlinked = false;
|
|
19
|
+
end = 8; // Zero is the absent-pointer sentinel.
|
|
20
|
+
active = Promise.resolve();
|
|
21
|
+
closing;
|
|
22
|
+
constructor(context, maxPages = 64) {
|
|
23
|
+
this.context = context;
|
|
24
|
+
this.maxPages = maxPages;
|
|
25
|
+
if (!Number.isSafeInteger(maxPages) || maxPages < 1)
|
|
26
|
+
throw new RangeError("Invalid storage page cache size");
|
|
27
|
+
this.signal = AbortSignal.any([context.signal, this.controller.signal]);
|
|
28
|
+
}
|
|
29
|
+
allocate(length) {
|
|
30
|
+
this.signal.throwIfAborted();
|
|
31
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > Number.MAX_SAFE_INTEGER - this.end)
|
|
32
|
+
throw new FsError("EFBIG");
|
|
33
|
+
const offset = this.end;
|
|
34
|
+
this.end += length;
|
|
35
|
+
return offset;
|
|
36
|
+
}
|
|
37
|
+
operation(action) {
|
|
38
|
+
this.signal.throwIfAborted();
|
|
39
|
+
const work = this.active.then(() => {
|
|
40
|
+
this.signal.throwIfAborted();
|
|
41
|
+
return action();
|
|
42
|
+
});
|
|
43
|
+
this.active = work.then(() => undefined, () => undefined);
|
|
44
|
+
return work;
|
|
45
|
+
}
|
|
46
|
+
async spill() {
|
|
47
|
+
const { fs, cwd, env } = this.context;
|
|
48
|
+
if (!fs.open || !fs.removeFileConditional)
|
|
49
|
+
throw new FsError("ENOTSUP", { message: "spill storage requires retained read/write handles and conditional removal" });
|
|
50
|
+
const selected = env.TMPDIR || cwd;
|
|
51
|
+
const directory = selected.startsWith("/") ? selected : `${cwd}/${selected}`;
|
|
52
|
+
this.parent = await fs.stat(directory, { signal: this.signal });
|
|
53
|
+
this.signal.throwIfAborted();
|
|
54
|
+
if (this.parent.type !== "directory")
|
|
55
|
+
throw new FsError("ENOTDIR", { path: directory });
|
|
56
|
+
for (let attempt = 0; attempt < 128; attempt++) {
|
|
57
|
+
const path = `${directory.endsWith("/") ? directory : directory + "/"}.storage-${++serial}`;
|
|
58
|
+
try {
|
|
59
|
+
this.descriptor = await fs.open(path, { access: "readwrite", creation: "exclusive", mode: 0o600, signal: this.signal });
|
|
60
|
+
this.path = path;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
this.signal.throwIfAborted();
|
|
65
|
+
if (!isFsError(error) || error.code !== "EEXIST")
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!this.descriptor)
|
|
70
|
+
throw new FsError("EEXIST", { path: directory });
|
|
71
|
+
// Retain the acquisition receipt even if cancellation wins immediately after open.
|
|
72
|
+
this.initial = await this.descriptor.stat();
|
|
73
|
+
this.signal.throwIfAborted();
|
|
74
|
+
if (!this.descriptor.capabilities.positionedRead || !this.descriptor.capabilities.positionedWrite)
|
|
75
|
+
throw new FsError("ENOTSUP");
|
|
76
|
+
// Detach the name before storing input. A later output or random-source path
|
|
77
|
+
// cannot alias our scratch file; the retained descriptor owns its lifetime.
|
|
78
|
+
await fs.removeFileConditional(this.path, { parent: this.parent, expected: this.initial, signal: this.signal });
|
|
79
|
+
this.unlinked = true;
|
|
80
|
+
for (const [number, page] of this.pages)
|
|
81
|
+
await this.flush(number, page);
|
|
82
|
+
}
|
|
83
|
+
async flush(number, page) {
|
|
84
|
+
if (!page.dirty)
|
|
85
|
+
return;
|
|
86
|
+
let offset = 0;
|
|
87
|
+
while (offset < pageBytes) {
|
|
88
|
+
this.signal.throwIfAborted();
|
|
89
|
+
const count = await this.descriptor.write(page.bytes.subarray(offset), number * pageBytes + offset, { signal: this.signal });
|
|
90
|
+
this.signal.throwIfAborted();
|
|
91
|
+
if (!Number.isSafeInteger(count) || count <= 0 || count > pageBytes - offset)
|
|
92
|
+
throw new FsError("EIO");
|
|
93
|
+
offset += count;
|
|
94
|
+
}
|
|
95
|
+
this.diskLength = Math.max(this.diskLength, (number + 1) * pageBytes);
|
|
96
|
+
page.dirty = false;
|
|
97
|
+
}
|
|
98
|
+
async page(number) {
|
|
99
|
+
this.signal.throwIfAborted();
|
|
100
|
+
const cached = this.pages.get(number);
|
|
101
|
+
if (cached) {
|
|
102
|
+
this.pages.delete(number);
|
|
103
|
+
this.pages.set(number, cached);
|
|
104
|
+
return cached;
|
|
105
|
+
}
|
|
106
|
+
if (this.pages.size === this.maxPages) {
|
|
107
|
+
if (!this.descriptor)
|
|
108
|
+
await this.spill();
|
|
109
|
+
const [oldNumber, oldPage] = this.pages.entries().next().value;
|
|
110
|
+
await this.flush(oldNumber, oldPage);
|
|
111
|
+
this.pages.delete(oldNumber);
|
|
112
|
+
}
|
|
113
|
+
const page = { bytes: new Uint8Array(pageBytes), dirty: false };
|
|
114
|
+
if (number * pageBytes < this.diskLength) {
|
|
115
|
+
let offset = 0;
|
|
116
|
+
while (offset < pageBytes) {
|
|
117
|
+
const count = await this.descriptor.read(page.bytes.subarray(offset), number * pageBytes + offset, { signal: this.signal });
|
|
118
|
+
this.signal.throwIfAborted();
|
|
119
|
+
if (!Number.isSafeInteger(count) || count <= 0 || count > pageBytes - offset)
|
|
120
|
+
throw new FsError("EIO");
|
|
121
|
+
offset += count;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
this.pages.set(number, page);
|
|
125
|
+
return page;
|
|
126
|
+
}
|
|
127
|
+
write(position, bytes) {
|
|
128
|
+
if (!Number.isSafeInteger(position) || position < 8 || position + bytes.length > this.end)
|
|
129
|
+
throw new RangeError("Invalid paged storage range");
|
|
130
|
+
return this.operation(async () => {
|
|
131
|
+
for (let offset = 0; offset < bytes.length;) {
|
|
132
|
+
const target = position + offset;
|
|
133
|
+
const page = await this.page(Math.floor(target / pageBytes));
|
|
134
|
+
const count = Math.min(bytes.length - offset, pageBytes - target % pageBytes);
|
|
135
|
+
page.bytes.set(bytes.subarray(offset, offset + count), target % pageBytes);
|
|
136
|
+
page.dirty = true;
|
|
137
|
+
offset += count;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
async append(bytes) {
|
|
142
|
+
const position = this.allocate(bytes.length);
|
|
143
|
+
await this.write(position, bytes);
|
|
144
|
+
return position;
|
|
145
|
+
}
|
|
146
|
+
read(position, length) {
|
|
147
|
+
if (!Number.isSafeInteger(position) || position < 8 || !Number.isSafeInteger(length) || length < 0 || length > pageBytes || position + length > this.end)
|
|
148
|
+
throw new RangeError("Invalid paged storage range");
|
|
149
|
+
return this.operation(async () => {
|
|
150
|
+
const bytes = new Uint8Array(length);
|
|
151
|
+
for (let offset = 0; offset < length;) {
|
|
152
|
+
const source = position + offset;
|
|
153
|
+
const page = await this.page(Math.floor(source / pageBytes));
|
|
154
|
+
const count = Math.min(length - offset, pageBytes - source % pageBytes);
|
|
155
|
+
bytes.set(page.bytes.subarray(source % pageBytes, source % pageBytes + count), offset);
|
|
156
|
+
offset += count;
|
|
157
|
+
}
|
|
158
|
+
return bytes;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
close() {
|
|
162
|
+
this.controller.abort(new FsError("ECANCELED"));
|
|
163
|
+
return this.closing ??= (async () => {
|
|
164
|
+
await this.active.catch(() => { });
|
|
165
|
+
this.pages.clear();
|
|
166
|
+
const descriptor = this.descriptor;
|
|
167
|
+
if (!descriptor)
|
|
168
|
+
return;
|
|
169
|
+
const cancellation = new FsError("ECANCELED");
|
|
170
|
+
try {
|
|
171
|
+
if (!this.unlinked) {
|
|
172
|
+
const expected = this.initial ?? await descriptor.stat();
|
|
173
|
+
await this.context.fs.removeFileConditional(this.path, { parent: this.parent, expected }).catch(error => {
|
|
174
|
+
if (!isFsError(error) || error.code !== "ENOENT")
|
|
175
|
+
throw error;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
// Conditional descriptors must discard their private staged writes, not
|
|
181
|
+
// publish the scratch data when the command retires its handle.
|
|
182
|
+
await descriptor.close({ signal: AbortSignal.abort(cancellation) }).catch(error => {
|
|
183
|
+
if (error !== cancellation)
|
|
184
|
+
throw error;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
})();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/** Fixed-depth radix index: both dense and sparse 64-bit keys use bounded RAM. */
|
|
191
|
+
export class IntegerTable {
|
|
192
|
+
storage;
|
|
193
|
+
maxEntries;
|
|
194
|
+
root = 0;
|
|
195
|
+
cache = new Map();
|
|
196
|
+
constructor(storage, maxEntries = 4096) {
|
|
197
|
+
this.storage = storage;
|
|
198
|
+
this.maxEntries = maxEntries;
|
|
199
|
+
if (!Number.isSafeInteger(maxEntries) || maxEntries < 1)
|
|
200
|
+
throw new RangeError("Invalid integer table cache size");
|
|
201
|
+
}
|
|
202
|
+
async retain(key, value, dirty) {
|
|
203
|
+
if (this.cache.size === this.maxEntries && !this.cache.has(key)) {
|
|
204
|
+
const [oldKey, old] = this.cache.entries().next().value;
|
|
205
|
+
if (old.dirty)
|
|
206
|
+
await this.persist(oldKey, old.value);
|
|
207
|
+
this.cache.delete(oldKey);
|
|
208
|
+
}
|
|
209
|
+
this.cache.delete(key);
|
|
210
|
+
this.cache.set(key, { value, dirty });
|
|
211
|
+
}
|
|
212
|
+
async get(key) {
|
|
213
|
+
const cached = this.cache.get(key);
|
|
214
|
+
if (cached) {
|
|
215
|
+
this.cache.delete(key);
|
|
216
|
+
this.cache.set(key, cached);
|
|
217
|
+
return cached.value;
|
|
218
|
+
}
|
|
219
|
+
const value = await this.lookup(key);
|
|
220
|
+
if (value !== undefined)
|
|
221
|
+
await this.retain(key, value, false);
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
async set(key, value) {
|
|
225
|
+
if (key < 0n || key > 0xffffffffffffffffn || value < 0n || value >= 0xffffffffffffffffn)
|
|
226
|
+
throw new RangeError("Invalid storage integer table value");
|
|
227
|
+
await this.retain(key, value, true);
|
|
228
|
+
}
|
|
229
|
+
async lookup(key) {
|
|
230
|
+
let node = this.root;
|
|
231
|
+
for (let shift = 60n; shift >= 0n; shift -= 4n) {
|
|
232
|
+
if (!node)
|
|
233
|
+
return undefined;
|
|
234
|
+
const slot = node + Number((key >> shift) & 15n) * 8;
|
|
235
|
+
const bytes = await this.storage.read(slot, 8);
|
|
236
|
+
const value = new DataView(bytes.buffer, bytes.byteOffset, 8).getBigUint64(0, true);
|
|
237
|
+
if (shift === 0n)
|
|
238
|
+
return value === 0n ? undefined : value - 1n;
|
|
239
|
+
node = Number(value);
|
|
240
|
+
}
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
async persist(key, value) {
|
|
244
|
+
if (!this.root)
|
|
245
|
+
this.root = this.storage.allocate(128);
|
|
246
|
+
let node = this.root;
|
|
247
|
+
for (let shift = 60n; shift >= 0n; shift -= 4n) {
|
|
248
|
+
const slot = node + Number((key >> shift) & 15n) * 8;
|
|
249
|
+
const bytes = await this.storage.read(slot, 8);
|
|
250
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, 8);
|
|
251
|
+
if (shift === 0n) {
|
|
252
|
+
view.setBigUint64(0, value + 1n, true);
|
|
253
|
+
await this.storage.write(slot, bytes);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
let child = Number(view.getBigUint64(0, true));
|
|
257
|
+
if (!child) {
|
|
258
|
+
child = this.storage.allocate(128);
|
|
259
|
+
view.setBigUint64(0, BigInt(child), true);
|
|
260
|
+
await this.storage.write(slot, bytes);
|
|
261
|
+
}
|
|
262
|
+
node = child;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -161,6 +161,13 @@
|
|
|
161
161
|
"./bridge": {
|
|
162
162
|
"types": "./dist/bridge/index.d.ts",
|
|
163
163
|
"import": "./dist/bridge/index.js"
|
|
164
|
+
},
|
|
165
|
+
"./storage": {
|
|
166
|
+
"types": "./dist/storage.d.ts",
|
|
167
|
+
"workerd": "./dist/storage.js",
|
|
168
|
+
"browser": "./dist/storage.js",
|
|
169
|
+
"worker": "./dist/storage.js",
|
|
170
|
+
"import": "./dist/storage.js"
|
|
164
171
|
}
|
|
165
172
|
},
|
|
166
173
|
"files": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolcraft",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.763",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -175,7 +175,7 @@
|
|
|
175
175
|
"@poe-code/terminal-text"
|
|
176
176
|
],
|
|
177
177
|
"optionalDependencies": {
|
|
178
|
-
"toolcraft-schema": "0.0.
|
|
178
|
+
"toolcraft-schema": "0.0.763",
|
|
179
179
|
"toolcraft-design": "*",
|
|
180
180
|
"@poe-code/frontmatter": "*",
|
|
181
181
|
"@poe-code/agent-mcp-config": "*",
|