toolcraft 0.0.760 → 0.0.762
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 +9 -0
- package/node_modules/@poe-code/safe-fs/dist/core.d.ts +2 -0
- package/node_modules/@poe-code/safe-fs/dist/core.js +2 -0
- package/node_modules/@poe-code/safe-fs/dist/fs/mount/index.js +6 -2
- package/node_modules/@poe-code/safe-fs/dist/fs/read-file-stream.d.ts +7 -0
- package/node_modules/@poe-code/safe-fs/dist/fs/read-file-stream.js +77 -0
- package/node_modules/@poe-code/safe-fs/dist/index.d.ts +1 -0
- package/node_modules/@poe-code/safe-fs/dist/index.js +1 -0
- package/node_modules/@poe-code/safe-fs/dist/runtime-core.d.ts +1 -0
- package/node_modules/@poe-code/safe-fs/dist/runtime-core.js +1 -0
- 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.762",
|
|
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.762",
|
|
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.762",
|
|
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.762",
|
|
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.
|
|
@@ -303,6 +310,8 @@ For Workers, `externalSpool` must store payloads outside isolate RAM and satisfy
|
|
|
303
310
|
|
|
304
311
|
The reusable metadata-only fixture in [`tests/fixtures/bounded-filesystem.ts`](tests/fixtures/bounded-filesystem.ts) generates file ranges and consumes slow writes without storing payload bytes. [`tests/overlay-bounded.test.ts`](tests/overlay-bounded.test.ts) checks fixed transfer sizes, source backpressure, cancellation/error cleanup, byte results, and Memory publication allocation/identity. These deterministic conformance checks do not qualify a deployed Worker backend; follow the [Worker QA plan](../../docs/plans/safe-fs-bounded-overlay-qa.md) for runtime measurements.
|
|
305
312
|
|
|
313
|
+
`readFileStream(fs, path, options)` reads through the caller's native stream or retained range handle without calling `readFile`. It accepts the stream range/window options above, defaults to a 64 KiB read window, and closes owned readers on completion, failure or cancellation. `skipStream: true` selects retained ranges after an already-attempted stream reports `ENOTSUP`. Chunks are borrowed until the iterator advances. Native adapters must honor the requested window and backpressure. Whole-file-only backends receive `ENOTSUP`; use a streaming or retained-range backend for sequential Worker operations. This helper bounds its I/O window, not the consuming command's parser or output capture.
|
|
314
|
+
|
|
306
315
|
`collectBytes(source, { maxBytes, maxMemoryBytes, signal })` snapshots streamed chunks into one growing buffer. `maxBytes` and `maxMemoryBytes` accept explicit `Infinity`. `maxMemoryBytes` limits owned capacity, the current input's full backing buffer, and overlapping allocations during growth; exhaustion throws `EFBIG`. Collection limits are disabled by default on Node, browser, and Worker targets. Each call owns its budget; concurrent collections do not consume one another’s allowance. The returned view may retain geometric spare capacity. Configured budgets cover collection, not caller-retained results, transport buffering, archive decoding, strings, or the rest of the runtime; use streaming APIs and limit concurrent workloads for larger inputs.
|
|
307
316
|
|
|
308
317
|
<details>
|
|
@@ -17,3 +17,5 @@ export * from "./fs/s3/http/index.js";
|
|
|
17
17
|
export { createFileSystem, readConfigRecord, validateFileSystemConfig } from "./config.js";
|
|
18
18
|
export type { FileSystemConfig, FileSystemAdapterDescriptor, FileSystemAdapterRegistry } from "./config.js";
|
|
19
19
|
export { createPortableFileSystemAdapterRegistry } from "./config.portable.js";
|
|
20
|
+
export { readFileStream } from "./fs/read-file-stream.js";
|
|
21
|
+
export { PagedStorage, IntegerTable, type PagedStorageContext } from "./storage.js";
|
|
@@ -15,3 +15,5 @@ export * from "./fs/s3/index.js";
|
|
|
15
15
|
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
|
+
export { readFileStream } from "./fs/read-file-stream.js";
|
|
19
|
+
export { PagedStorage, IntegerTable } from "./storage.js";
|
|
@@ -219,7 +219,7 @@ export class MountFileSystem {
|
|
|
219
219
|
&& await this.supportsDirectoryValidation(location.path.slice(0, location.path.lastIndexOf("/")) || "/", options, true)
|
|
220
220
|
: this.capabilities.conditionalChmod;
|
|
221
221
|
const { synchronousDirectoryValidation: ignoredValidation, synchronousStagingResolution: ignoredResolution, ...ordinary } = resize;
|
|
222
|
-
const withOpen = { ...ordinary,
|
|
222
|
+
const withOpen = { ...ordinary, ...(conditionalChmod === undefined ? {} : { conditionalChmod }), ...(resolution === undefined ? {} : { synchronousStagingResolution: resolution }), ...(validation === undefined ? {} : { synchronousDirectoryValidation: validation }), atomicStagingAncestry: ancestry, ...(typeof location.mount.backend.open === "function" ? {} : { open: false }) };
|
|
223
223
|
const capabilities = location.synthetic ? { ...withOpen, open: false, retainedRead: false }
|
|
224
224
|
: retainedResizeCapabilities(location.mount.backend, retainedReadCapabilities(location.mount.backend, withOpen));
|
|
225
225
|
if (location.synthetic)
|
|
@@ -973,7 +973,11 @@ export class MountFileSystem {
|
|
|
973
973
|
return Object.freeze(staging);
|
|
974
974
|
const backendCleanup = receipt.cleanup;
|
|
975
975
|
const cleanup = createStagingCleanup(staging.directory.path, controls => this.operation("removeStagedFile", staging.directory.path, controls, () => backendCleanup.remove(controls), undefined, true), () => backendCleanup.close());
|
|
976
|
-
|
|
976
|
+
const writer = receipt.writer;
|
|
977
|
+
return Object.freeze({ ...staging, cleanup, ...(writer ? { writer: Object.freeze({
|
|
978
|
+
write: (bytes, controls = {}) => this.operation("stagedWrite", staging.file.path, controls, () => writer.write(bytes, controls), undefined, true),
|
|
979
|
+
finish: (controls = {}) => this.operation("stagedFinish", staging.file.path, controls, async () => snapshotStat(await writer.finish(controls)), undefined, true),
|
|
980
|
+
}) } : {}) });
|
|
977
981
|
}
|
|
978
982
|
catch (error) {
|
|
979
983
|
await finishCleanup(() => receipt.cleanup?.close(), true);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FileSystem, ReadStreamOptions } from "../contracts/filesystem.js";
|
|
2
|
+
/** Sequential payload access without a whole-file fallback. The injected backend
|
|
3
|
+
* must supply streaming reads or retained range reads; no private storage is used.
|
|
4
|
+
* Chunks are borrowed until the consumer advances the iterator. */
|
|
5
|
+
export declare function readFileStream(fs: FileSystem, path: string, options?: ReadStreamOptions & {
|
|
6
|
+
readonly skipStream?: boolean;
|
|
7
|
+
}): AsyncGenerator<Uint8Array>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { FsError } from "../contracts/errors.js";
|
|
2
|
+
import { finishCleanup } from "../contracts/cleanup.js";
|
|
3
|
+
import { openRetainedReadFile } from "./capabilities.js";
|
|
4
|
+
const byteLength = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Uint8Array.prototype), "byteLength").get;
|
|
5
|
+
const byteKind = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Uint8Array.prototype), Symbol.toStringTag).get;
|
|
6
|
+
/** Sequential payload access without a whole-file fallback. The injected backend
|
|
7
|
+
* must supply streaming reads or retained range reads; no private storage is used.
|
|
8
|
+
* Chunks are borrowed until the consumer advances the iterator. */
|
|
9
|
+
export async function* readFileStream(fs, path, options = {}) {
|
|
10
|
+
const { signal } = options;
|
|
11
|
+
signal?.throwIfAborted();
|
|
12
|
+
const chunkSize = options.chunkSize ?? 65536;
|
|
13
|
+
let position = options.start ?? 0;
|
|
14
|
+
const end = options.endExclusive ?? Number.MAX_SAFE_INTEGER;
|
|
15
|
+
if (!Number.isSafeInteger(chunkSize) || chunkSize <= 0 || !Number.isSafeInteger(position) || position < 0
|
|
16
|
+
|| !Number.isSafeInteger(end) || end < position)
|
|
17
|
+
throw new FsError("EINVAL", { path, syscall: "readStream" });
|
|
18
|
+
const capabilities = await fs.capabilitiesFor?.(path, options) ?? fs.capabilities;
|
|
19
|
+
signal?.throwIfAborted();
|
|
20
|
+
if (!options.skipStream && fs.readStream && fs.capabilities?.streamingRead !== false && capabilities?.streamingRead !== false) {
|
|
21
|
+
let emitted = false;
|
|
22
|
+
let reading = true;
|
|
23
|
+
try {
|
|
24
|
+
const source = fs.readStream(path, { ...options, chunkSize });
|
|
25
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
26
|
+
let done = false, failed = true;
|
|
27
|
+
try {
|
|
28
|
+
while (true) {
|
|
29
|
+
signal?.throwIfAborted();
|
|
30
|
+
const item = await iterator.next();
|
|
31
|
+
signal?.throwIfAborted();
|
|
32
|
+
if (item.done) {
|
|
33
|
+
done = true;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
reading = false;
|
|
37
|
+
if (byteLength.call(item.value))
|
|
38
|
+
emitted = true;
|
|
39
|
+
yield item.value;
|
|
40
|
+
reading = true;
|
|
41
|
+
}
|
|
42
|
+
failed = false;
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
if (!done)
|
|
46
|
+
await finishCleanup(() => iterator.return?.(), failed);
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
signal?.throwIfAborted();
|
|
52
|
+
if (!reading || emitted || !(error instanceof FsError) || error.code !== "ENOTSUP")
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const handle = await openRetainedReadFile(fs, path, options);
|
|
57
|
+
let failed = true;
|
|
58
|
+
try {
|
|
59
|
+
while (position < end) {
|
|
60
|
+
signal?.throwIfAborted();
|
|
61
|
+
const maximum = Math.min(chunkSize, end - position);
|
|
62
|
+
const bytes = await handle.read(position, maximum, options);
|
|
63
|
+
signal?.throwIfAborted();
|
|
64
|
+
if (byteKind.call(bytes) !== "Uint8Array" || byteLength.call(bytes) > maximum)
|
|
65
|
+
throw new FsError("EIO", { path, syscall: "read" });
|
|
66
|
+
const length = byteLength.call(bytes);
|
|
67
|
+
if (!length)
|
|
68
|
+
break;
|
|
69
|
+
position += length;
|
|
70
|
+
yield bytes;
|
|
71
|
+
}
|
|
72
|
+
failed = false;
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
await finishCleanup(() => handle.close(), failed);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -23,3 +23,4 @@ export { createFileSystem, readConfigRecord, validateFileSystemConfig } from "./
|
|
|
23
23
|
export type { FileSystemConfig, FileSystemAdapterDescriptor, FileSystemAdapterRegistry } from "./config.js";
|
|
24
24
|
export { createNodeFileSystemAdapterRegistry } from "./config.node.js";
|
|
25
25
|
export { createPortableFileSystemAdapterRegistry } from "./config.portable.js";
|
|
26
|
+
export { readFileStream } from "./fs/read-file-stream.js";
|
|
@@ -19,3 +19,4 @@ export * from "./node/index.js";
|
|
|
19
19
|
export { createFileSystem, readConfigRecord, validateFileSystemConfig } from "./config.js";
|
|
20
20
|
export { createNodeFileSystemAdapterRegistry } from "./config.node.js";
|
|
21
21
|
export { createPortableFileSystemAdapterRegistry } from "./config.portable.js";
|
|
22
|
+
export { readFileStream } from "./fs/read-file-stream.js";
|
|
@@ -25,3 +25,4 @@ export declare function tryWriteMemoryFileSync(fs: FileSystem, path: string, dat
|
|
|
25
25
|
export declare function tryMkdirMemorySync(fs: FileSystem, path: string, recursive: boolean, mode: number, signal?: AbortSignal, dirPrefixHint?: string): boolean;
|
|
26
26
|
export declare function tryRmRfMemorySync(fs: FileSystem, path: string, signal?: AbortSignal): boolean;
|
|
27
27
|
export declare function tryWriteMemoryFileInDirSync(fs: FileSystem, dirPrefix: string, name: string, data: Uint8Array, append: boolean, mode: number, signal?: AbortSignal): boolean;
|
|
28
|
+
export { readFileStream } from "./fs/read-file-stream.js";
|
|
@@ -55,3 +55,4 @@ export function tryRmRfMemorySync(fs, path, signal) {
|
|
|
55
55
|
export function tryWriteMemoryFileInDirSync(fs, dirPrefix, name, data, append, mode, signal) {
|
|
56
56
|
return Boolean((fs && _getMemHooks()?.byProto?.get(Object.getPrototypeOf(fs))?.tryWriteMemoryFileInDirSync)?.(fs, dirPrefix, name, data, append, mode, signal));
|
|
57
57
|
}
|
|
58
|
+
export { readFileStream } from "./fs/read-file-stream.js";
|
|
@@ -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.762",
|
|
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.762",
|
|
179
179
|
"toolcraft-design": "*",
|
|
180
180
|
"@poe-code/frontmatter": "*",
|
|
181
181
|
"@poe-code/agent-mcp-config": "*",
|