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 CHANGED
@@ -133,7 +133,7 @@
133
133
  },
134
134
  {
135
135
  "name": "toolcraft",
136
- "version": "0.0.761",
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.761",
146
+ "version": "0.0.763",
147
147
  "license": "MIT"
148
148
  },
149
149
  {
@@ -133,7 +133,7 @@
133
133
  },
134
134
  {
135
135
  "name": "toolcraft",
136
- "version": "0.0.761",
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.761",
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": [
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "name": "toolcraft-schema",
11
- "version": "0.0.761",
11
+ "version": "0.0.763",
12
12
  "license": "MIT"
13
13
  }
14
14
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-schema",
3
- "version": "0.0.761",
3
+ "version": "0.0.763",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft",
3
- "version": "0.0.761",
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.761",
178
+ "toolcraft-schema": "0.0.763",
179
179
  "toolcraft-design": "*",
180
180
  "@poe-code/frontmatter": "*",
181
181
  "@poe-code/agent-mcp-config": "*",