toolcraft 0.0.767 → 0.0.769

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.767",
136
+ "version": "0.0.769",
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.767",
146
+ "version": "0.0.769",
147
147
  "license": "MIT"
148
148
  },
149
149
  {
@@ -133,7 +133,7 @@
133
133
  },
134
134
  {
135
135
  "name": "toolcraft",
136
- "version": "0.0.767",
136
+ "version": "0.0.769",
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.767",
146
+ "version": "0.0.769",
147
147
  "license": "MIT"
148
148
  },
149
149
  {
@@ -10,6 +10,10 @@ working data. Supply `{fs, cwd, env, signal}` and an optional page-cache count
10
10
  `env.TMPDIR` or `cwd`, with conditional unlink and cleanup through `close()`.
11
11
  Providers must support those capabilities; there is no host fallback. Use an
12
12
  external backend for large data because memory filesystems retain spilled bytes.
13
+ `IntegerTable.entries()` replays records in unsigned key order with a fixed
14
+ 16-level node stack and batches of at most 128 records. It accepts zero-filled
15
+ caller storage with `allocate`, `read` and `write`; mutation during traversal
16
+ is rejected. Branch pointers use a separate fixed 128-entry cache.
13
17
 
14
18
  ## Quickstart
15
19
 
@@ -86,7 +90,7 @@ cause. Preserve cancellation/control exceptions separately. Cross-family
86
90
  | Change the namespace | `mkdir`, `rm`, `rename`, `copyFile` |
87
91
  | Remove an empty directory | Optional `rmdir`; never a recursive-delete fallback |
88
92
  | Links and metadata | Optional `readlink`, `symlink`, `link`, `chmod`, `utimes`, `truncate` |
89
- | Enumerate directories lazily | Optional `iterateDirectory`; memory and native backends, with mount/overlay/readonly/quota/device/scoped delegation; backend order, no sorted-array fallback |
93
+ | Enumerate directories lazily | Optional `iterateDirectory`; memory, native and paged S3 backends, with mount/overlay/readonly/quota/device/scoped delegation; backend order, no sorted-array fallback |
90
94
  | Stream bytes | Optional `readStream`, `writeStream`, using async iterables of byte chunks |
91
95
  | 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 |
92
96
  | Publish immutable objects atomically | Optional `publishFileConditional` with opaque identity/version stats and authoritative compare-and-publish |
@@ -133,7 +137,11 @@ For temporary staging that must be cleaned up after its parent moves, adapters
133
137
  advertising `retainedStagingCleanup` accept `createStagedFile(..., { parent,
134
138
  retainCleanup: true })`. Use the returned `staging.cleanup.remove()` and always
135
139
  call `staging.cleanup.close()` in `finally`. Memory and its supported wrappers
136
- retain only the owned staging entries; replacement entries remain protected.
140
+ retain only the owned staging entries; replacement entries remain protected. When
141
+ `atomicStagedFileMutation` is available, `publishStagedFile(..., { preserveIdentity: true,
142
+ parent, destination })` updates an existing file without breaking hardlink aliases.
143
+ Memory and its supported wrappers adopt staged bytes and timestamps while keeping
144
+ the destination identity and mode; unsupported adapters reject this option.
137
145
  Overlays backed by stock Memory filesystems support conditional file mutations,
138
146
  directory preparation, and confined staging. Publication may omit `ancestors`
139
147
  when the destination shares the staging parent; the overlay checks the ancestry
@@ -76,6 +76,8 @@ export interface FileSystemCapabilities {
76
76
  /** Owned staging serialized within a trusted host; requires external tree isolation. */
77
77
  readonly trustedOwnedStaging?: boolean;
78
78
  readonly atomicFileStaging?: boolean;
79
+ /** Staged payload publication can atomically preserve an existing inode. */
80
+ readonly atomicStagedFileMutation?: boolean;
79
81
  /** Creates a cleanup handle bound to owned staging entries, independent of ancestor paths. */
80
82
  readonly retainedStagingCleanup?: boolean;
81
83
  readonly retainedStagingWrite?: boolean;
@@ -277,6 +279,11 @@ export interface CreateStagedFileOptions extends FsOptions {
277
279
  readonly mtimeMs?: number;
278
280
  }
279
281
  export interface PublishStagedFileOptions extends FsOptions {
282
+ /** Requires atomicStagedFileMutation and an existing regular destination.
283
+ * Atomically adopts staged bytes and timestamps while preserving destination
284
+ * identity, links and mode. Consumes the staged pathname as ordinary publication.
285
+ * Unsupported hosts must reject before effects, never silently rename. */
286
+ readonly preserveIdentity?: boolean;
280
287
  readonly ancestors?: readonly FileStagingEntry[];
281
288
  /** Trusted, mutation-free validation. Must return literal true synchronously;
282
289
  * a promise is not acceptance. Requires guardedStagingPublication. */
@@ -7,4 +7,4 @@ export declare function openRetainedResizeFile(filesystem: FileSystem, path: str
7
7
  export declare function readOnlyCapabilities(capabilities: FileSystemCapabilities): FileSystemCapabilities;
8
8
  export declare function quotaCapabilities(capabilities: FileSystemCapabilities): FileSystemCapabilities;
9
9
  export declare function ownedMutationCapabilities(filesystem: FileSystem, capabilities?: FileSystemCapabilities): FileSystemCapabilities;
10
- export declare function requireOwnedMutation(filesystem: FileSystem, path: string, capability: "retainedStagingCleanup" | "atomicFilePublication" | "atomicEntryRemoval" | "atomicEntryRemovalReceipt" | "atomicTreeRemoval" | "atomicFileMutation" | "atomicFileStaging" | "guardedStagingPublication" | "atomicStagingAncestry" | "atomicDirectoryMetadata", options: FsOptions, create?: boolean): Promise<void>;
10
+ export declare function requireOwnedMutation(filesystem: FileSystem, path: string, capability: "retainedStagingCleanup" | "atomicFilePublication" | "atomicEntryRemoval" | "atomicEntryRemovalReceipt" | "atomicTreeRemoval" | "atomicFileMutation" | "atomicFileStaging" | "atomicStagedFileMutation" | "guardedStagingPublication" | "atomicStagingAncestry" | "atomicDirectoryMetadata", options: FsOptions, create?: boolean): Promise<void>;
@@ -136,7 +136,7 @@ export function readOnlyCapabilities(capabilities) {
136
136
  mkdir: false, recursiveMkdir: false, remove: false, removeDirectory: false, recursiveRemove: false,
137
137
  rename: false, copy: false, exclusiveCopy: false, truncate: false, streamingAppend: false,
138
138
  randomAccessWrite: false, hardlinks: false, permissions: false, conditionalChmod: false, timestamps: false,
139
- descriptorWriteStream: false, atomicResize: false, retainedResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicTreeRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
139
+ descriptorWriteStream: false, atomicResize: false, retainedResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicTreeRemoval: false, atomicStagedFileMutation: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
140
140
  atomicFilePublication: false, atomicRename: false, atomicRenameNoReplace: false, streamingWrite: false,
141
141
  });
142
142
  }
@@ -144,7 +144,7 @@ export function quotaCapabilities(capabilities) {
144
144
  const streamingWrite = requireCapabilities(capabilities.write, capabilities.append, capabilities.streamingAppend, !capabilities.readOnly);
145
145
  const streamingAppend = requireCapabilities(capabilities.append, capabilities.streamingAppend, !capabilities.readOnly);
146
146
  const { streamingWrite: ignoredWrite, streamingAppend: ignoredAppend, ...rest } = capabilities;
147
- return Object.freeze({ ...rest, synchronousStagingResolution: false, retainedStagingCleanup: false, retainedStagingWrite: false, atomicStagingAncestry: false, synchronousDirectoryValidation: false, guardedStagingPublication: false, atomicFilePublication: false, descriptorWriteStream: false, atomicResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
147
+ return Object.freeze({ ...rest, synchronousStagingResolution: false, retainedStagingCleanup: false, retainedStagingWrite: false, atomicStagingAncestry: false, synchronousDirectoryValidation: false, guardedStagingPublication: false, atomicFilePublication: false, descriptorWriteStream: false, atomicResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicStagedFileMutation: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
148
148
  ...(streamingWrite === undefined ? {} : { streamingWrite }),
149
149
  ...(streamingAppend === undefined ? {} : { streamingAppend }),
150
150
  });
@@ -167,6 +167,8 @@ export function ownedMutationCapabilities(filesystem, capabilities = filesystem.
167
167
  if (capabilities.atomicFileStaging === true && (capabilities.readOnly === true
168
168
  || typeof filesystem.createStagedFile !== "function" || typeof filesystem.publishStagedFile !== "function" || typeof filesystem.removeStagedFile !== "function"))
169
169
  (unavailable ??= {}).atomicFileStaging = false;
170
+ if (capabilities.atomicStagedFileMutation === true && (capabilities.atomicFileStaging !== true || unavailable?.atomicFileStaging === false))
171
+ (unavailable ??= {}).atomicStagedFileMutation = false;
170
172
  if (capabilities.retainedStagingCleanup === true && (capabilities.atomicFileStaging !== true || unavailable?.atomicFileStaging === false))
171
173
  (unavailable ??= {}).retainedStagingCleanup = false;
172
174
  if (capabilities.retainedStagingWrite === true && (capabilities.retainedStagingCleanup !== true || unavailable?.retainedStagingCleanup === false))
@@ -20,7 +20,7 @@ const deviceCapabilities = Object.freeze({
20
20
  remove: false, removeDirectory: false, recursiveRemove: false, rename: false,
21
21
  mkdir: false, recursiveMkdir: false, symlinks: false, hardlinks: false, readlink: false,
22
22
  permissions: false, timestamps: false, truncate: false, randomAccessWrite: false,
23
- open: true, retainedStagingCleanup: false, atomicFilePublication: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicTreeRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
23
+ open: true, retainedStagingCleanup: false, atomicFilePublication: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicTreeRemoval: false, atomicStagedFileMutation: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
24
24
  atomicRename: false, atomicRenameNoReplace: false, descriptorWriteStream: true, retainedResize: true, atomicResize: false,
25
25
  });
26
26
  function globalCapabilities(filesystem) {
@@ -31,6 +31,7 @@ function globalCapabilities(filesystem) {
31
31
  synchronousDirectoryValidation: ["prepareDirectoryAncestry"], guardedStagingPublication: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
32
32
  synchronousStagingResolution: ["prepareStagingResolution"],
33
33
  trustedOwnedStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile", "writeFileConditional", "removeFileConditional", "prepareDirectory"],
34
+ atomicStagedFileMutation: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
34
35
  atomicFilePublication: ["publishFileConditional"], atomicEntryRemoval: ["removeEntryConditional"], atomicEntryRemovalReceipt: ["removeEntryConditional"], atomicTreeRemoval: ["removeTreeConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
35
36
  streamingRead: ["readStream"], streamingWrite: ["writeStream"], retainedRead: ["openReadFile"],
36
37
  streamingAppend: ["writeStream"], descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"], atomicResize: ["resizeFile"],
@@ -667,6 +668,8 @@ export class DeviceFileSystem {
667
668
  await this.#mutable(path, options, false);
668
669
  await requireOwnedMutation(this.#filesystem, staging.directory.path, "atomicFileStaging", options);
669
670
  await requireOwnedMutation(this.#filesystem, destination, "atomicFileStaging", options, options.destination === null);
671
+ if (options.preserveIdentity)
672
+ await requireOwnedMutation(this.#filesystem, destination, "atomicStagedFileMutation", options);
670
673
  if (!this.#filesystem.publishStagedFile)
671
674
  throw new FsError("ENOTSUP", { path: destination });
672
675
  if (options.commitGuard !== undefined)
@@ -584,7 +584,7 @@ const SHARED_STOCK_MEMORY_CAPABILITIES = Object.freeze({
584
584
  permissions: true,
585
585
  timestamps: true,
586
586
  atomicRename: true,
587
- atomicFileStaging: true, retainedStagingCleanup: true, retainedStagingWrite: true, atomicStagingAncestry: true, atomicFileMutation: true, atomicEntryRemoval: true, atomicEntryRemovalReceipt: true, atomicTreeRemoval: true,
587
+ atomicStagedFileMutation: true, atomicFileStaging: true, retainedStagingCleanup: true, retainedStagingWrite: true, atomicStagingAncestry: true, atomicFileMutation: true, atomicEntryRemoval: true, atomicEntryRemovalReceipt: true, atomicTreeRemoval: true,
588
588
  synchronousDirectoryValidation: true, synchronousStagingResolution: true, guardedStagingPublication: true,
589
589
  atomicDirectoryMetadata: true,
590
590
  streamingRead: true,
@@ -2245,6 +2245,7 @@ export class MemoryFileSystem {
2245
2245
  }
2246
2246
  async publishStagedFile(receipt, destination, options) {
2247
2247
  const signal = options.signal;
2248
+ const preserveIdentity = options.preserveIdentity === true;
2248
2249
  const guard = options.commitGuard;
2249
2250
  const ancestors = options.ancestors === undefined ? undefined : snapshotDirectoryAncestry(options.ancestors);
2250
2251
  const snapshot = (entry) => ({ path: entry.path, stat: { ...entry.stat } });
@@ -2272,9 +2273,48 @@ export class MemoryFileSystem {
2272
2273
  this.fail("EINVAL", "publishStagedFile", destination);
2273
2274
  if (target.node && target.node.type !== "file")
2274
2275
  this.fail("EAGAIN", "publishStagedFile", destination);
2276
+ if (preserveIdentity) {
2277
+ if (this.capabilities.atomicStagedFileMutation !== true)
2278
+ this.fail("ENOTSUP", "publishStagedFile", destination);
2279
+ if (file.node?.type !== "file" || target.node?.type !== "file" || expected === null)
2280
+ this.fail("EINVAL", "publishStagedFile", destination);
2281
+ this.permission(file.parent, 3, "publishStagedFile", staging.file.path);
2282
+ this.permission(target.parent, 3, "publishStagedFile", destination);
2283
+ this.permission(target.node, 2, "publishStagedFile", destination);
2284
+ // Provider-owned storage is copied directly, without assembling an archive
2285
+ // in caller memory. Preserve retained staging readers and independent writes.
2286
+ this.admitSize(target.node, file.node.byteLength, "publishStagedFile", destination);
2287
+ const allocation = this.allocate(file.node.byteLength, "publishStagedFile", destination);
2288
+ try {
2289
+ for (let offset = 0; offset < file.node.byteLength; offset += 65536) {
2290
+ allocation.data.set(file.node.data.subarray(offset, offset + 65536), offset);
2291
+ }
2292
+ }
2293
+ catch (error) {
2294
+ allocation.release();
2295
+ throw error;
2296
+ }
2297
+ // No awaits or overridable operations occur between admission and commit.
2298
+ const now = Date.now();
2299
+ this.replaceData(target.node, allocation);
2300
+ this.changed(target.node, now);
2301
+ target.node.atimeMs = file.node.atimeMs;
2302
+ target.node.mtimeMs = file.node.mtimeMs;
2303
+ file.parent.entries.delete(file.name);
2304
+ this.ledger.release(file.name.length * 2, 1);
2305
+ file.node.nlink--;
2306
+ file.node.ctimeMs = now;
2307
+ file.node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, file.node.revision + 1);
2308
+ this.releaseNode(file.node);
2309
+ this.changed(file.parent, now);
2310
+ memoryCaches.get(this.ledger).clearWrites();
2311
+ return;
2312
+ }
2275
2313
  return MemoryFileSystem.prototype.rename.call(this, staging.file.path, destination, { ...controls, noReplace: expected === null });
2276
2314
  }
2277
2315
  async publishStagedFileSet(receipt, destination, options) {
2316
+ if (options.preserveIdentity)
2317
+ this.fail("ENOTSUP", "publishStagedFileSet", destination);
2278
2318
  const signal = options.signal;
2279
2319
  const guard = options.commitGuard;
2280
2320
  const ancestors = options.ancestors === undefined ? undefined : snapshotDirectoryAncestry(options.ancestors);
@@ -109,6 +109,7 @@ export class MountFileSystem {
109
109
  retainedStagingCleanup: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
110
110
  synchronousDirectoryValidation: ["prepareDirectoryAncestry"], guardedStagingPublication: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
111
111
  trustedOwnedStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile", "writeFileConditional", "removeFileConditional", "prepareDirectory"],
112
+ atomicStagedFileMutation: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
112
113
  atomicFilePublication: ["publishFileConditional"], atomicEntryRemoval: ["removeEntryConditional"], atomicEntryRemovalReceipt: ["removeEntryConditional"], atomicTreeRemoval: ["removeTreeConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
113
114
  symlinks: ["symlink", "readlink"], hardlinks: ["link"], permissions: ["chmod"], timestamps: ["utimes"], readlink: ["readlink"],
114
115
  descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"], atomicResize: ["resizeFile"],
@@ -128,7 +129,7 @@ export class MountFileSystem {
128
129
  return values.every(value => value === true) ? true : values.every(value => value === false) ? false : undefined;
129
130
  };
130
131
  const semantics = Object.fromEntries([
131
- "guardedStagingPublication", "retainedStagingCleanup", "atomicEntryRemovalReceipt",
132
+ "guardedStagingPublication", "retainedStagingCleanup", "atomicEntryRemovalReceipt", "atomicStagedFileMutation",
132
133
  "trustedOwnedStaging", "atomicFilePublication", "atomicEntryRemoval", "atomicTreeRemoval", "atomicFileMutation", "atomicFileStaging", "atomicDirectoryMetadata", "read", "stat", "readdir", "realpath", "access", "open", "versionedDescriptors",
133
134
  "write", "append", "exclusiveCreate", "explicitDirectories", "implicitDirectories", "mkdir", "recursiveMkdir",
134
135
  "remove", "removeDirectory", "recursiveRemove", "rename", "atomicRenameNoReplace", "copy", "exclusiveCopy", "readlink", "truncate",
@@ -1096,6 +1097,8 @@ export class MountFileSystem {
1096
1097
  await requireOwnedMutation(local.mount.backend, target.local, "atomicFileStaging", options, options.destination === null);
1097
1098
  if (!local.mount.backend.publishStagedFile)
1098
1099
  fail("ENOTSUP");
1100
+ if (options.preserveIdentity)
1101
+ await requireOwnedMutation(local.mount.backend, target.local, "atomicStagedFileMutation", options);
1099
1102
  if (ancestors || callerGuard !== undefined)
1100
1103
  await requireOwnedMutation(local.mount.backend, target.local, "guardedStagingPublication", options, options.destination === null);
1101
1104
  if (!ancestors) {
@@ -384,6 +384,8 @@ export class OverlayMemoryPublication {
384
384
  return staging;
385
385
  }
386
386
  async publish(staging, path, options) {
387
+ if (options.preserveIdentity)
388
+ throw new FsError("ENOTSUP", { path });
387
389
  options.signal?.throwIfAborted();
388
390
  this.path(path);
389
391
  const owned = this.stages.get(staging.directory);
@@ -413,6 +413,8 @@ export class RealFileSystem {
413
413
  throw new FsError("EAGAIN");
414
414
  }
415
415
  async publishStagedFile(staging, destination, options) {
416
+ if (options.preserveIdentity)
417
+ throw new FsError("ENOTSUP", { path: destination });
416
418
  return this.operation("publishStagedFile", staging.file.path, options, async () => {
417
419
  options.signal?.throwIfAborted();
418
420
  if (options.ancestors !== undefined || options.commitGuard !== undefined)
@@ -109,6 +109,7 @@ export declare class S3FileSystem implements FileSystem {
109
109
  appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
110
110
  mkdir(input: string, options?: MkdirOptions): Promise<void>;
111
111
  readdir(input: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
112
+ iterateDirectory(input: string, options?: FsOptions): AsyncIterable<DirectoryEntry>;
112
113
  private tree;
113
114
  rmdir(input: string, options?: FsOptions): Promise<void>;
114
115
  rm(input: string, options?: RemoveOptions): Promise<void>;
@@ -341,25 +341,33 @@ export class S3FileSystem {
341
341
  if (key.endsWith("/") && object.Size !== 0)
342
342
  fail("ENOTSUP", "listObjectsV2", path, "nonempty slash-suffixed objects cannot be treated as directories");
343
343
  }
344
- async *pages(prefix, path, options, delimiter, maxKeys = this.pageSize) {
345
- const tokens = new Set();
346
- let token;
347
- let entries = 0;
344
+ async *pages(prefix, path, options, delimiter, maxKeys = this.pageSize, rememberTokens = true) {
345
+ // Buffered callers preserve exact repeated-token rejection. Lazy traversal
346
+ // detects continuation cycles without retaining the whole token history.
347
+ const tokens = rememberTokens ? new Set() : undefined;
348
+ let token, checkpoint;
349
+ let entries = 0, pageCount = 0, span = 0, power = 1;
348
350
  do {
349
351
  const page = await this.page(prefix, path, options, delimiter, token, maxKeys);
350
352
  entries += (page.Contents?.length ?? 0) + (page.CommonPrefixes?.length ?? 0);
351
- if (entries > this.maxListEntries || tokens.size >= this.maxListEntries)
353
+ if (entries > this.maxListEntries || ++pageCount > this.maxListEntries)
352
354
  fail("EFBIG", "listObjectsV2", path, "listing exceeds maxListEntries");
355
+ const truncated = page.IsTruncated, next = page.NextContinuationToken;
353
356
  yield page;
354
- if (!page.IsTruncated)
357
+ if (!truncated)
355
358
  return;
356
- token = page.NextContinuationToken;
357
- if (typeof token !== "string" || !token || tokens.has(token))
359
+ if (typeof next !== "string" || !next || next === token || next === checkpoint || tokens?.has(next))
358
360
  fail("EIO", "listObjectsV2", path, "missing or repeated continuation token");
359
- tokens.add(token);
361
+ if (++span === power) {
362
+ checkpoint = next;
363
+ span = 0;
364
+ power = Math.min(Number.MAX_SAFE_INTEGER, power * 2);
365
+ }
366
+ tokens?.add(next);
367
+ token = next;
360
368
  } while (true);
361
369
  }
362
- async inspect(path, options) {
370
+ async inspect(path, options, rememberTokens = true) {
363
371
  if (path === "/") {
364
372
  const listing = await this.page(this.prefix, path, options, undefined, undefined, 1);
365
373
  const marker = this.prefix && listing.Contents?.some(object => object.Key === this.prefix)
@@ -375,7 +383,7 @@ export class S3FileSystem {
375
383
  const directoryKey = this.directoryKey(path);
376
384
  let directory = false;
377
385
  let hasMarker = false;
378
- for await (const children of this.pages(directoryKey, path, options, undefined, 1)) {
386
+ for await (const children of this.pages(directoryKey, path, options, undefined, 1, rememberTokens)) {
379
387
  if ((children.Contents?.length ?? 0) > 0) {
380
388
  directory = true;
381
389
  hasMarker = children.Contents.some(object => object.Key === directoryKey);
@@ -393,7 +401,7 @@ export class S3FileSystem {
393
401
  return { stat: this.makeStat("directory", marker), ...(marker ? { metadata: marker } : {}) };
394
402
  return undefined;
395
403
  }
396
- async lookup(path, options) {
404
+ async lookup(path, options, rememberTokens = true) {
397
405
  const ancestors = [];
398
406
  for (let parent = posix.dirname(path); path !== "/"; parent = posix.dirname(parent)) {
399
407
  ancestors.unshift(parent);
@@ -403,13 +411,13 @@ export class S3FileSystem {
403
411
  for (const parent of ancestors) {
404
412
  if (parent === "/" && this.prefix === "")
405
413
  continue;
406
- const ancestor = await this.inspect(parent, options);
414
+ const ancestor = await this.inspect(parent, options, rememberTokens);
407
415
  if (!ancestor)
408
416
  fail("ENOENT", "stat", parent);
409
417
  if (ancestor.stat.type !== "directory")
410
418
  fail("ENOTDIR", "stat", parent);
411
419
  }
412
- return this.inspect(path, options);
420
+ return this.inspect(path, options, rememberTokens);
413
421
  }
414
422
  requireDirectorySuffix(input, info) {
415
423
  if (input.endsWith("/") && info?.stat.type === "file")
@@ -596,6 +604,37 @@ export class S3FileSystem {
596
604
  }
597
605
  return [...entries.values()].sort((left, right) => compareKeys(left.name, right.name));
598
606
  }
607
+ async *iterateDirectory(input, options = {}) {
608
+ const path = this.path(input);
609
+ const directory = await this.lookup(path, options, false);
610
+ if (!directory)
611
+ fail("ENOENT", "iterateDirectory", path);
612
+ if (directory.stat.type !== "directory")
613
+ fail("ENOTDIR", "iterateDirectory", path);
614
+ const prefix = this.directoryKey(path);
615
+ for await (const page of this.pages(prefix, path, options, "/", this.pageSize, false)) {
616
+ // Snapshot one bounded service page before yielding to caller code.
617
+ if ((page.Contents?.length ?? 0) + (page.CommonPrefixes?.length ?? 0) > this.pageSize)
618
+ fail("EIO", "iterateDirectory", path, "transport exceeded requested page size");
619
+ const entries = [];
620
+ for (const item of page.Contents ?? [])
621
+ if (item.Key !== prefix)
622
+ entries.push({ name: item.Key.slice(prefix.length), type: "file" });
623
+ for (const item of page.CommonPrefixes ?? [])
624
+ entries.push({ name: item.Prefix.slice(prefix.length, -1), type: "directory" });
625
+ for (const entry of entries) {
626
+ this.checkAbort(options, "iterateDirectory", path);
627
+ if (!entry.name || entry.name.includes("/"))
628
+ fail("EIO", "iterateDirectory", path, "invalid direct child in delimited listing");
629
+ // Revalidation preserves file/prefix collision detection across pages
630
+ // and follows the backend's live namespace without retaining all names.
631
+ const child = await this.lookup(path === "/" ? `/${entry.name}` : `${path}/${entry.name}`, options, false);
632
+ this.checkAbort(options, "iterateDirectory", path);
633
+ if (child)
634
+ yield { name: entry.name, type: child.stat.type };
635
+ }
636
+ }
637
+ }
599
638
  async tree(path, options) {
600
639
  const objects = new Map();
601
640
  for await (const page of this.pages(this.directoryKey(path), path, options)) {
@@ -343,6 +343,8 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
343
343
  if (property === "publishStagedFile") {
344
344
  const publishOptions = (options ?? {});
345
345
  await requireOwnedMutation(original, args[1], "atomicFileStaging", publishOptions, publishOptions.destination === null);
346
+ if (publishOptions.preserveIdentity)
347
+ await requireOwnedMutation(original, args[1], "atomicStagedFileMutation", publishOptions);
346
348
  const query = options.destination === null ? { ...options, create: true } : options;
347
349
  const declared = ownedMutationCapabilities(original, await original.capabilitiesFor?.(args[1], query) ?? original.capabilities);
348
350
  assertReady();
@@ -444,7 +446,7 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
444
446
  return executeDispatch(args);
445
447
  if (operations.has(property)) {
446
448
  const optionIndex = ["compareEntry", "createStagedFile", "utimes"].includes(String(property)) ? 3
447
- : ["writeFile", "appendFile", "writeStream", "rename", "copyFile", "link", "symlink", "access", "chmod", "truncate", "resizeFile", "publishFileConditional", "writeFileConditional", "publishStagedFile"].includes(String(property)) ? 2 : 1;
449
+ : ["writeFile", "appendFile", "writeStream", "rename", "copyFile", "link", "symlink", "access", "chmod", "truncate", "resizeFile", "publishFileConditional", "writeFileConditional", "publishStagedFile", "publishStagedFileSet"].includes(String(property)) ? 2 : 1;
448
450
  args[optionIndex] = scopeTransportOptions((args[optionIndex] ?? {}), admit, credit);
449
451
  }
450
452
  return runScopedTransportBudget(admit, () => executeDispatch(args), credit);
@@ -39,11 +39,19 @@ export declare class IntegerTable {
39
39
  private readonly storage;
40
40
  private readonly maxEntries;
41
41
  private root;
42
+ private revision;
43
+ private readonly branches;
42
44
  private readonly cache;
43
- constructor(storage: PagedStorage, maxEntries?: number);
45
+ constructor(storage: Pick<PagedStorage, "allocate" | "read" | "write">, maxEntries?: number);
44
46
  private retain;
45
47
  get(key: bigint): Promise<bigint | undefined>;
46
48
  set(key: bigint, value: bigint): Promise<void>;
49
+ /** Ordered replay with a fixed 16-level radix stack (at most 2 KiB of
50
+ * owned node bytes) and 128 output records. Batches avoid alternating sparse
51
+ * index reads with consumer writes on every record. Mutation is not permitted. */
52
+ entries(): AsyncGenerator<readonly [bigint, bigint]>;
53
+ /** Hot radix paths must not evict dirty leaf pages in a one-page backend. */
54
+ private branch;
47
55
  private lookup;
48
56
  private persist;
49
57
  }
@@ -192,6 +192,8 @@ export class IntegerTable {
192
192
  storage;
193
193
  maxEntries;
194
194
  root = 0;
195
+ revision = 0;
196
+ branches = new Map();
195
197
  cache = new Map();
196
198
  constructor(storage, maxEntries = 4096) {
197
199
  this.storage = storage;
@@ -224,19 +226,92 @@ export class IntegerTable {
224
226
  async set(key, value) {
225
227
  if (key < 0n || key > 0xffffffffffffffffn || value < 0n || value >= 0xffffffffffffffffn)
226
228
  throw new RangeError("Invalid storage integer table value");
229
+ this.revision++;
227
230
  await this.retain(key, value, true);
228
231
  }
232
+ /** Ordered replay with a fixed 16-level radix stack (at most 2 KiB of
233
+ * owned node bytes) and 128 output records. Batches avoid alternating sparse
234
+ * index reads with consumer writes on every record. Mutation is not permitted. */
235
+ async *entries() {
236
+ const revision = this.revision;
237
+ const check = () => { if (revision !== this.revision)
238
+ throw new Error("Integer table changed during iteration"); };
239
+ for (const [key, entry] of this.cache)
240
+ if (entry.dirty) {
241
+ check();
242
+ await this.persist(key, entry.value);
243
+ check();
244
+ entry.dirty = false;
245
+ }
246
+ const storage = this.storage;
247
+ async function* visit(position, shift, prefix) {
248
+ check();
249
+ if (!position)
250
+ return;
251
+ const bytes = new Uint8Array(await storage.read(position, 128));
252
+ check();
253
+ if (bytes.length !== 128)
254
+ throw new RangeError("Truncated integer table node");
255
+ const node = new DataView(bytes.buffer);
256
+ for (let slot = 0; slot < 16; slot++) {
257
+ check();
258
+ const value = node.getBigUint64(slot * 8, true);
259
+ if (!value)
260
+ continue;
261
+ const key = prefix | BigInt(slot) << shift;
262
+ if (shift === 0n)
263
+ yield [key, value - 1n];
264
+ else
265
+ yield* visit(Number(value), shift - 4n, key);
266
+ }
267
+ }
268
+ const batch = [];
269
+ for await (const entry of visit(this.root, 60n, 0n)) {
270
+ batch.push(entry);
271
+ if (batch.length === 128) {
272
+ for (const entry of batch) {
273
+ check();
274
+ yield entry;
275
+ }
276
+ batch.length = 0;
277
+ }
278
+ }
279
+ for (const entry of batch) {
280
+ check();
281
+ yield entry;
282
+ }
283
+ check();
284
+ }
285
+ /** Hot radix paths must not evict dirty leaf pages in a one-page backend. */
286
+ branch(key, shift, position) {
287
+ const prefix = key >> shift | 1n << (64n - shift);
288
+ const value = position ?? this.branches.get(prefix);
289
+ if (value !== undefined) {
290
+ this.branches.delete(prefix);
291
+ this.branches.set(prefix, value);
292
+ if (this.branches.size > 128)
293
+ this.branches.delete(this.branches.keys().next().value);
294
+ }
295
+ return value;
296
+ }
229
297
  async lookup(key) {
230
298
  let node = this.root;
231
299
  for (let shift = 60n; shift >= 0n; shift -= 4n) {
232
300
  if (!node)
233
301
  return undefined;
302
+ const cached = shift === 0n ? undefined : this.branch(key, shift);
303
+ if (cached !== undefined) {
304
+ node = cached;
305
+ continue;
306
+ }
234
307
  const slot = node + Number((key >> shift) & 15n) * 8;
235
308
  const bytes = await this.storage.read(slot, 8);
236
309
  const value = new DataView(bytes.buffer, bytes.byteOffset, 8).getBigUint64(0, true);
237
310
  if (shift === 0n)
238
311
  return value === 0n ? undefined : value - 1n;
239
312
  node = Number(value);
313
+ if (node)
314
+ this.branch(key, shift, node);
240
315
  }
241
316
  return undefined;
242
317
  }
@@ -245,6 +320,11 @@ export class IntegerTable {
245
320
  this.root = this.storage.allocate(128);
246
321
  let node = this.root;
247
322
  for (let shift = 60n; shift >= 0n; shift -= 4n) {
323
+ const cached = shift === 0n ? undefined : this.branch(key, shift);
324
+ if (cached !== undefined) {
325
+ node = cached;
326
+ continue;
327
+ }
248
328
  const slot = node + Number((key >> shift) & 15n) * 8;
249
329
  const bytes = await this.storage.read(slot, 8);
250
330
  const view = new DataView(bytes.buffer, bytes.byteOffset, 8);
@@ -260,6 +340,7 @@ export class IntegerTable {
260
340
  await this.storage.write(slot, bytes);
261
341
  }
262
342
  node = child;
343
+ this.branch(key, shift, child);
263
344
  }
264
345
  }
265
346
  }
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "name": "toolcraft-schema",
11
- "version": "0.0.767",
11
+ "version": "0.0.769",
12
12
  "license": "MIT"
13
13
  }
14
14
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-schema",
3
- "version": "0.0.767",
3
+ "version": "0.0.769",
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.767",
3
+ "version": "0.0.769",
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.767",
178
+ "toolcraft-schema": "0.0.769",
179
179
  "toolcraft-design": "*",
180
180
  "@poe-code/frontmatter": "*",
181
181
  "@poe-code/agent-mcp-config": "*",