toolcraft 0.0.759 → 0.0.760

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.759",
136
+ "version": "0.0.760",
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.759",
146
+ "version": "0.0.760",
147
147
  "license": "MIT"
148
148
  },
149
149
  {
@@ -133,7 +133,7 @@
133
133
  },
134
134
  {
135
135
  "name": "toolcraft",
136
- "version": "0.0.759",
136
+ "version": "0.0.760",
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.759",
146
+ "version": "0.0.760",
147
147
  "license": "MIT"
148
148
  },
149
149
  {
@@ -295,6 +295,14 @@ Every raw filesystem operation accepts an optional `signal`. Additional fields a
295
295
 
296
296
  `access` takes a separate mode bitmask from `ACCESS_MODES`. `chmod` takes a mode, `utimes` takes millisecond timestamps, and `truncate` takes a byte length (default 0). For conditional chmod, check `capabilitiesFor(path, { conditionalChmod: true })` (or `capabilities`) and require `conditionalChmod: true`; supply `parent`, `expected`, and complete root-to-parent `ancestors` together. An optional mutation-free `commitGuard` must return literal `true` synchronously. Memory validates at its metadata commit; mount and supported Memory overlays preserve wrapper ancestry. Real uses its existing externally isolated host-tree boundary and does not prevent races with other processes. Backend limits still apply. Node-shaped bridge methods translate their own options rather than accepting these raw option objects; see the [bridge signatures](src/bridge/filesystem.ts).
297
297
 
298
+ Overlay copy-up, copy, stream replacement/append and resize transfer retained ranges in 64 KiB chunks through the injected upper's stream writer. Resize streams the retained prefix and bounded zero chunks. Stream writes stage input before publication; cancellation or transfer failure leaves the destination unchanged and cleans staging. Stock Memory overlays transfer staged storage into existing upper inodes, preserving hardlinks without an additional whole-file buffer. `readFile` and `writeFile` remain buffering convenience APIs; use `readStream`/`writeStream` for large payloads. `maxBufferBytes` retains its existing optional total-byte admission semantics, not a working-memory guarantee.
299
+
300
+ A streaming overlay needs an upper with `atomicRename`, `streamingWrite`, retained reads (`retainedRead` plus `openReadFile`), exclusive create, directory mutation, permissions and timestamps. Both layers must provide retained, bounded range reads with stable file and directory identities. For example, inject these host-authorized adapters with `createOverlayFileSystem({ upper: externalSpool, lower: retainedSource })`, then use `overlay.writeStream(path, source)`. The upper must await each consumed chunk, keep transport queues bounded, and preserve ownership until publication. Supply bounded chunks at the input boundary; the overlay cannot undo a payload-wide allocation already made by its caller. Whole-file-only uppers explicitly reject transfer operations with `ENOTSUP`; they do not silently collect streams.
301
+
302
+ For Workers, `externalSpool` must store payloads outside isolate RAM and satisfy that contract, including atomic rename and retained identity. Memory and the S3 mock store payloads in RAM and are not external spools. Stock S3/WebDAV do not provide the atomic rename/retained-read contract needed for an overlay upper; setting a capability flag does not add it. A host must inject a qualified external adapter. On Node, rooted Real adapters provide the streaming overlay configuration under their documented external-tree isolation requirement. Mount and read-only wrappers forward the selected backend's capabilities; quota streaming additionally requires a real streaming-append route. S3's buffered append convenience path reports `streamingAppend: false`.
303
+
304
+ 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
+
298
306
  `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.
299
307
 
300
308
  <details>
@@ -141,8 +141,8 @@ export function readOnlyCapabilities(capabilities) {
141
141
  });
142
142
  }
143
143
  export function quotaCapabilities(capabilities) {
144
- const streamingWrite = requireCapabilities(capabilities.write, capabilities.append, !capabilities.readOnly);
145
- const streamingAppend = requireCapabilities(capabilities.append, !capabilities.readOnly);
144
+ const streamingWrite = requireCapabilities(capabilities.write, capabilities.append, capabilities.streamingAppend, !capabilities.readOnly);
145
+ const streamingAppend = requireCapabilities(capabilities.append, capabilities.streamingAppend, !capabilities.readOnly);
146
146
  const { streamingWrite: ignoredWrite, streamingAppend: ignoredAppend, ...rest } = capabilities;
147
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,
148
148
  ...(streamingWrite === undefined ? {} : { streamingWrite }),
@@ -1,6 +1,7 @@
1
1
  import type { FileStat, FileSystem } from "../../contracts/filesystem.js";
2
2
  /** Internal authority: inspections and stock mutations share one JavaScript turn. */
3
3
  export interface MemoryAtomicView {
4
+ replaceContents(source: string, destination: string): void;
4
5
  stat(path: string): FileStat | undefined;
5
6
  names(path: string): readonly string[];
6
7
  }
@@ -656,6 +656,26 @@ export class MemoryFileSystem {
656
656
  }
657
657
  }
658
658
  return owner.atomicView ??= {
659
+ replaceContents: (source, destination) => {
660
+ const from = this.entry(source, "overlayAtomicView").node;
661
+ const to = this.entry(destination, "overlayAtomicView").node;
662
+ if (from?.type !== "file" || to?.type !== "file" || from === to || from.nlink !== 1)
663
+ this.fail("EINVAL", "overlayAtomicView", destination);
664
+ this.permission(to, 2, "overlayAtomicView", destination);
665
+ // Exchange owned storage, preserving destination inode/hardlinks and
666
+ // retained allocation snapshots. Stage cleanup releases the old contents.
667
+ const allocation = to.allocation, length = to.byteLength;
668
+ to.allocation = from.allocation;
669
+ to.byteLength = from.byteLength;
670
+ from.allocation = allocation;
671
+ from.byteLength = length;
672
+ to.view = undefined;
673
+ from.view = undefined;
674
+ to.sourceRef = undefined;
675
+ from.sourceRef = undefined;
676
+ this.changed(to);
677
+ this.changed(from);
678
+ },
659
679
  stat: (path) => {
660
680
  this.validatePath(path, "overlayAtomicView");
661
681
  let node = this.root;
@@ -82,6 +82,7 @@ export declare class OverlayFileSystem implements FileSystem {
82
82
  readStream(path: string, options?: ReadStreamOptions): ByteSource;
83
83
  private bounded;
84
84
  private streamToUpper;
85
+ private entryBytes;
85
86
  writeStream(path: string, source: ByteSource, options?: WriteFileOptions): Promise<void>;
86
87
  }
87
88
  export declare function createOverlayFileSystem(options: OverlayFileSystemOptions): OverlayFileSystem;
@@ -95,8 +95,8 @@ export class OverlayFileSystem {
95
95
  : readable.every((capability) => capability === false) ? false : undefined;
96
96
  const retainedRead = streamingRead;
97
97
  const streamingWrite = writable && this.#upper.capabilities.streamingWrite === true
98
- && this.#upper.capabilities.streamingRead === true
99
- && typeof this.#upper.writeStream === "function" && typeof this.#upper.readStream === "function"
98
+ && retainedReadCapabilities(this.#upper).retainedRead === true
99
+ && typeof this.#upper.writeStream === "function"
100
100
  ? readable.every((capability) => capability === true) ? true : undefined : false;
101
101
  const append = !writable || this.#upper.capabilities.append === false ? false
102
102
  : this.#upper.capabilities.append === true ? true : undefined;
@@ -117,8 +117,8 @@ export class OverlayFileSystem {
117
117
  ["rename", requireCapabilities(mutation, upper.rename, upper.write)],
118
118
  ["copy", requireCapabilities(mutation, upper.write)],
119
119
  ["exclusiveCopy", requireCapabilities(mutation, upper.exclusiveCreate)],
120
- ["truncate", requireCapabilities(mutation, upper.truncate)],
121
- ["streamingAppend", requireCapabilities(mutation, streamingWrite, upper.streamingAppend)],
120
+ ["truncate", requireCapabilities(mutation, streamingWrite)],
121
+ ["streamingAppend", requireCapabilities(mutation, streamingWrite)],
122
122
  ["randomAccessWrite", requireCapabilities(mutation, upper.randomAccessWrite)],
123
123
  ["explicitDirectories", requireCapabilities(upper.explicitDirectories, this.#lower.capabilities.explicitDirectories)],
124
124
  ].filter(([, value]) => value !== undefined));
@@ -578,7 +578,7 @@ export class OverlayFileSystem {
578
578
  fail("ENOTSUP", path, "upper cannot preserve timestamps during copy-up");
579
579
  await this.#upper.utimes(path, stat.atimeMs, stat.mtimeMs, options);
580
580
  }
581
- async clone(entry, destination, options, streaming = false) {
581
+ async clone(entry, destination, options) {
582
582
  if (entry.stat.type !== "directory" && ((entry.stat.nlink ?? 1) > 1
583
583
  || (entry.backend.capabilities.hardlinks === true && entry.stat.nlink === undefined))) {
584
584
  fail("ENOTSUP", entry.path, "copy-up cannot preserve hardlink identity");
@@ -591,23 +591,14 @@ export class OverlayFileSystem {
591
591
  }
592
592
  if (entry.stat.type === "directory")
593
593
  await this.#upper.mkdir(destination, { ...options, mode: entry.stat.mode & 0o7777 });
594
- else if (streaming && entry.backend.readStream && entry.backend.capabilities.streamingRead !== false && this.#upper.writeStream) {
594
+ else {
595
+ if (this.#upper.capabilities.streamingWrite !== true || !this.#upper.writeStream)
596
+ fail("ENOTSUP", destination, "copy-up requires a streaming upper");
595
597
  this.permission(entry, 4);
596
598
  if (entry.stat.size > this.maxBufferBytes)
597
599
  fail("EFBIG", entry.path);
598
- const handle = await this.pinRead(entry, options);
599
- let failed = true;
600
- try {
601
- const source = this.bounded(this.handleBytes(handle, options), entry.path, options);
602
- await this.streamToUpper(destination, source, { ...options, mode: 0o600, flag: "wx" });
603
- failed = false;
604
- }
605
- finally {
606
- await finishCleanup(() => handle.close(), failed);
607
- }
600
+ await this.streamToUpper(destination, this.bounded(this.entryBytes(entry, options), entry.path, options), { ...options, mode: 0o600, flag: "wx" });
608
601
  }
609
- else
610
- await this.#upper.writeFile(destination, await this.bytes(entry, options), { ...options, mode: 0o600, flag: "wx" });
611
602
  await this.preserve(destination, entry.stat, options);
612
603
  }
613
604
  rememberLink(entry, destination) {
@@ -686,11 +677,11 @@ export class OverlayFileSystem {
686
677
  }
687
678
  return location;
688
679
  }
689
- async replace(location, options, operation, streaming = false, commitGuard) {
680
+ async replace(location, options, operation, commitGuard) {
690
681
  await this.parent(location.path, options);
691
682
  await this.staged(options, async (temporary) => {
692
683
  if (location.entry)
693
- await this.clone(location.entry, temporary, options, streaming);
684
+ await this.clone(location.entry, temporary, options);
694
685
  await operation(temporary);
695
686
  options.signal?.throwIfAborted();
696
687
  commitGuard?.();
@@ -983,9 +974,10 @@ export class OverlayFileSystem {
983
974
  }
984
975
  }
985
976
  await this.cleanGarbage(false);
986
- const bytes = await this.bytes(original, options);
977
+ if (original.stat.size > this.maxBufferBytes)
978
+ fail("EFBIG", source);
987
979
  await this.replace(target, options, async (temporary) => {
988
- await this.#upper.writeFile(temporary, bytes, { ...options, mode: original.stat.mode & 0o7777, flag: "w" });
980
+ await this.streamToUpper(temporary, this.entryBytes(original, options), { ...options, mode: original.stat.mode & 0o7777, flag: "w" });
989
981
  if (this.#upper.chmod)
990
982
  await this.#upper.chmod(temporary, original.stat.mode & 0o7777, options);
991
983
  else if (((await this.#upper.lstat(temporary, options)).mode & 0o7777) !== (original.stat.mode & 0o7777))
@@ -1057,7 +1049,7 @@ export class OverlayFileSystem {
1057
1049
  await this.#upper.chmod(entry.path, permissions, controls);
1058
1050
  }
1059
1051
  else {
1060
- await this.replace({ path: entry.path, entry }, controls, temporary => this.#upper.chmod(temporary, permissions, controls), false, guard);
1052
+ await this.replace({ path: entry.path, entry }, controls, temporary => this.#upper.chmod(temporary, permissions, controls), guard);
1061
1053
  }
1062
1054
  });
1063
1055
  }
@@ -1079,15 +1071,30 @@ export class OverlayFileSystem {
1079
1071
  if (entry.stat.type !== "file")
1080
1072
  fail("EISDIR", path);
1081
1073
  this.permission(entry, 2);
1082
- await this.replace({ path: entry.path, entry }, options, async (temporary) => {
1083
- if (this.#upper.truncate)
1084
- await this.#upper.truncate(temporary, length, options);
1085
- else {
1086
- const previous = await this.#upper.readFile(temporary, { ...options, ...(this.maxBufferBytes === Infinity ? {} : { maxBytes: this.maxBufferBytes }) });
1087
- const bytes = new Uint8Array(length);
1088
- bytes.set(previous.subarray(0, length));
1089
- await this.#upper.writeFile(temporary, bytes, options);
1090
- }
1074
+ if ((entry.stat.nlink ?? 1) > 1 || (entry.backend.capabilities.hardlinks === true && entry.stat.nlink === undefined)) {
1075
+ fail("ENOTSUP", path, "copy-up cannot preserve hardlink identity");
1076
+ }
1077
+ await this.parent(entry.path, options);
1078
+ await this.staged(options, async (temporary) => {
1079
+ const prefix = this.entryBytes(entry, { ...options, endExclusive: length });
1080
+ const source = async function* () {
1081
+ let written = 0;
1082
+ for await (const chunk of prefix) {
1083
+ written += chunk.byteLength;
1084
+ yield chunk;
1085
+ }
1086
+ while (written < length) {
1087
+ options.signal?.throwIfAborted();
1088
+ const zero = new Uint8Array(Math.min(64 * 1024, length - written));
1089
+ written += zero.byteLength;
1090
+ yield zero;
1091
+ }
1092
+ };
1093
+ await this.streamToUpper(temporary, source(), { ...options, mode: 0o600, flag: "wx" });
1094
+ await this.preserve(temporary, entry.stat, options);
1095
+ await this.#upper.utimes(temporary, entry.stat.atimeMs, Date.now(), options);
1096
+ options.signal?.throwIfAborted();
1097
+ await this.#upper.rename(temporary, entry.path, options);
1091
1098
  });
1092
1099
  });
1093
1100
  }
@@ -1130,7 +1137,7 @@ export class OverlayFileSystem {
1130
1137
  }
1131
1138
  }
1132
1139
  async streamToUpper(path, source, options) {
1133
- if (!this.#upper.writeStream)
1140
+ if (!this.#upper.writeStream || this.#upper.capabilities.streamingWrite !== true)
1134
1141
  fail("ENOTSUP", path);
1135
1142
  const input = readBytes(source, options.signal);
1136
1143
  let failed = false;
@@ -1145,39 +1152,74 @@ export class OverlayFileSystem {
1145
1152
  await finishCleanup(() => input.return(undefined), failed);
1146
1153
  }
1147
1154
  }
1155
+ async *entryBytes(entry, options) {
1156
+ this.permission(entry, 4);
1157
+ const handle = this.activeStages.has(dirname(entry.path)) && entry.backend === this.#upper
1158
+ ? await openRetainedReadFile(this.#upper, entry.path, options) : await this.pinRead(entry, options);
1159
+ let failed = true;
1160
+ try {
1161
+ if (compareIdentity(await handle.stat(options), entry.stat) !== "same")
1162
+ fail("EAGAIN", entry.path);
1163
+ yield* this.handleBytes(handle, options);
1164
+ failed = false;
1165
+ }
1166
+ finally {
1167
+ await finishCleanup(() => handle.close(), failed);
1168
+ }
1169
+ }
1148
1170
  async writeStream(path, source, options = {}) {
1149
1171
  options.signal?.throwIfAborted();
1150
1172
  this.writeOptions(path, options);
1173
+ if (this.capabilities.streamingWrite === false)
1174
+ fail("ENOTSUP", path, "streaming requires retained reads and a streaming upper");
1151
1175
  await this.run(options, () => this.writeLocation(path, options));
1152
- if (this.capabilities.streamingWrite !== false) {
1153
- await this.staged(options, async (incoming) => {
1154
- const bounded = this.bounded(source, path, options);
1155
- await this.streamToUpper(incoming, bounded, { ...options, flag: "wx", mode: 0o600 });
1156
- const size = (await this.#upper.stat(incoming, options)).size;
1157
- await this.run(options, async () => {
1158
- const location = await this.writeLocation(path, options);
1159
- const append = options.flag === "a" || options.flag === "ax";
1160
- if (append && (location.entry?.stat.size ?? 0) + size > this.maxBufferBytes)
1161
- fail("EFBIG", path);
1162
- if (this.publication.supported() && location.entry?.backend === this.#upper) {
1163
- const bytes = await this.#upper.readFile(incoming, options);
1164
- const parent = await this.required(dirname(location.path), options);
1165
- await this.publication.writeFile(location.path, bytes, {
1166
- ...options, parent: this.publication.stat(parent.path, parent.stat),
1167
- expected: this.publication.stat(location.path, location.entry.stat), append,
1168
- }, this.maxBufferBytes);
1176
+ await this.staged(options, async (incoming) => {
1177
+ await this.streamToUpper(incoming, this.bounded(source, path, options), { ...options, flag: "wx", mode: 0o600 });
1178
+ const incomingEntry = { path: incoming, backend: this.#upper, stat: await this.#upper.lstat(incoming, options) };
1179
+ await this.run(options, async () => {
1180
+ const location = await this.writeLocation(path, options);
1181
+ const append = options.flag === "a" || options.flag === "ax";
1182
+ if (append && (location.entry?.stat.size ?? 0) + incomingEntry.stat.size > this.maxBufferBytes)
1183
+ fail("EFBIG", path);
1184
+ if (this.publication.supported() && location.entry?.backend === this.#upper) {
1185
+ const parent = await this.required(dirname(location.path), options);
1186
+ const controls = { ...options, parent: this.publication.stat(parent.path, parent.stat),
1187
+ expected: this.publication.stat(location.path, location.entry.stat) };
1188
+ const guard = this.publication.prepareWrite(location.path, controls);
1189
+ if (append) {
1190
+ await this.staged(options, async (temporary) => {
1191
+ const previous = this.entryBytes(location.entry, options);
1192
+ const added = this.entryBytes(incomingEntry, options);
1193
+ await this.streamToUpper(temporary, (async function* () { yield* previous; yield* added; })(), { ...options, flag: "wx", mode: 0o600 });
1194
+ this.publication.replaceContents(temporary, location.path, guard);
1195
+ });
1169
1196
  }
1170
- else {
1171
- await this.replace(location, options, async (temporary) => {
1172
- await this.streamToUpper(temporary, this.#upper.readStream(incoming, options), { ...options, flag: append ? "a" : "w" });
1173
- }, true);
1197
+ else
1198
+ this.publication.replaceContents(incoming, location.path, guard);
1199
+ }
1200
+ else {
1201
+ if (location.entry && ((location.entry.stat.nlink ?? 1) > 1
1202
+ || (location.entry.backend.capabilities.hardlinks === true && location.entry.stat.nlink === undefined))) {
1203
+ fail("ENOTSUP", path, "copy-up cannot preserve hardlink identity");
1174
1204
  }
1175
- });
1205
+ await this.parent(location.path, options);
1206
+ await this.staged(options, async (temporary) => {
1207
+ const previous = append && location.entry ? this.entryBytes(location.entry, options) : undefined;
1208
+ const added = this.entryBytes(incomingEntry, options);
1209
+ await this.streamToUpper(temporary, (async function* () { if (previous)
1210
+ yield* previous; yield* added; })(), { ...options, flag: "wx", mode: location.entry ? 0o600 : options.mode ?? 0o666 });
1211
+ if (location.entry) {
1212
+ await this.preserve(temporary, location.entry.stat, options);
1213
+ await this.#upper.utimes(temporary, location.entry.stat.atimeMs, Date.now(), options);
1214
+ }
1215
+ options.signal?.throwIfAborted();
1216
+ await this.#upper.rename(temporary, location.path, options);
1217
+ this.linkMetadata.delete(location.path);
1218
+ this.linkOrigins.delete(location.path);
1219
+ });
1220
+ }
1176
1221
  });
1177
- return;
1178
- }
1179
- const bytes = await collectBytes(source, { ...(this.maxBufferBytes === Infinity ? {} : { maxBytes: this.maxBufferBytes }), ...(options.signal ? { signal: options.signal } : {}) });
1180
- await this.writeFile(path, bytes, options);
1222
+ });
1181
1223
  }
1182
1224
  }
1183
1225
  export function createOverlayFileSystem(options) {
@@ -29,6 +29,7 @@ export declare class OverlayMemoryPublication {
29
29
  private check;
30
30
  prepareChmod(path: string, options: ChmodOptions): (() => true) | undefined;
31
31
  prepareWrite(path: string, options: ConditionalWriteFileOptions): () => true;
32
+ replaceContents(source: string, destination: string, guard: () => true): void;
32
33
  writeFile(path: string, data: Uint8Array, options: ConditionalWriteFileOptions, maxBytes: number, guard?: () => void): Promise<FileStat>;
33
34
  removeFile(path: string, options: ConditionalRemoveFileOptions): Promise<void>;
34
35
  prepareDirectory(path: string, options: PrepareDirectoryOptions, guard?: () => void): Promise<FileStat>;
@@ -203,6 +203,11 @@ export class OverlayMemoryPublication {
203
203
  return true;
204
204
  };
205
205
  }
206
+ replaceContents(source, destination, guard) {
207
+ const { upper } = this.stores();
208
+ guard();
209
+ upper.replaceContents(source, destination);
210
+ }
206
211
  async writeFile(path, data, options, maxBytes, guard) {
207
212
  const check = this.prepareWrite(path, options);
208
213
  check();
@@ -1,5 +1,6 @@
1
1
  import { FsError } from "../../contracts/errors.js";
2
2
  import { finishCleanup } from "../../contracts/cleanup.js";
3
+ import { readBytes } from "../../contracts/io.js";
3
4
  import { openRetainedReadFile, openRetainedResizeFile, quotaCapabilities, retainedReadCapabilities, retainedResizeCapabilities } from "../capabilities.js";
4
5
  import { admitDirectoryEntries } from "../directory-admission.js";
5
6
  import { forwardFileDescriptor, openFileDescriptor } from "../descriptor.js";
@@ -485,12 +486,16 @@ export function withFileSystemQuota(fs, options) {
485
486
  },
486
487
  writeStream(path, source, writeOptions) {
487
488
  return mutate(async () => {
489
+ if (fs.capabilities.streamingAppend !== true || !fs.writeStream)
490
+ throw new FsError("ENOTSUP", { syscall: "writeStream", path });
488
491
  const append = writeOptions?.flag === "a" || writeOptions?.flag === "ax";
489
492
  if (!append)
490
493
  await fs.writeFile(path, new Uint8Array(), writeOptions);
491
- for await (const chunk of source) {
494
+ else
495
+ await fs.writeStream(path, (async function* () { })(), writeOptions);
496
+ for await (const chunk of readBytes(source, writeOptions?.signal)) {
492
497
  await assertDelta(path, await existingBytes(fs, path, writeOptions) + chunk.length, writeOptions);
493
- await fs.appendFile(path, chunk, writeOptions);
498
+ await fs.writeStream(path, (async function* () { yield chunk; })(), { ...writeOptions, flag: "a" });
494
499
  }
495
500
  });
496
501
  },
@@ -502,11 +507,11 @@ export function withFileSystemQuota(fs, options) {
502
507
  if (property === "publishStagedFileSet" || property === "prepareStagingResolution" || property === "prepareDirectoryAncestry" || property === "publishFileConditional" || property === "removeEntryConditional" || property === "writeFileConditional" || property === "removeFileConditional" || property === "resizeFile" || property === "canonicalizeMissingTarget" || property === "createStagedFile" || property === "publishStagedFile" || property === "removeStagedFile" || property === "prepareDirectory")
503
508
  return undefined;
504
509
  if (property === "capabilities")
505
- return quotaCapabilities({ ...retainedResizeCapabilities(fs, retainedReadCapabilities(fs)), ...(typeof fs.open === "function" ? {} : { open: false }) });
510
+ return quotaCapabilities({ ...retainedResizeCapabilities(fs, retainedReadCapabilities(fs)), ...(typeof fs.open === "function" ? {} : { open: false }), ...(typeof fs.writeStream === "function" ? {} : { streamingAppend: false }) });
506
511
  if (property === "capabilitiesFor")
507
512
  return async (path, fsOptions) => {
508
513
  const capabilities = await fs.capabilitiesFor?.(path, fsOptions) ?? fs.capabilities;
509
- return quotaCapabilities({ ...retainedResizeCapabilities(fs, retainedReadCapabilities(fs, capabilities)), ...(typeof fs.open === "function" ? {} : { open: false }) });
514
+ return quotaCapabilities({ ...retainedResizeCapabilities(fs, retainedReadCapabilities(fs, capabilities)), ...(typeof fs.open === "function" ? {} : { open: false }), ...(typeof fs.writeStream === "function" ? {} : { streamingAppend: false }) });
510
515
  };
511
516
  if (property === "openReadFile")
512
517
  return (path, fsOptions = {}) => openRetainedReadFile(fs, path, fsOptions);
@@ -53,7 +53,7 @@ export declare class S3FileSystem implements FileSystem {
53
53
  truncate: boolean;
54
54
  atomicRenameNoReplace: false;
55
55
  rename: boolean;
56
- streamingAppend: boolean;
56
+ streamingAppend: false;
57
57
  randomAccessWrite: false;
58
58
  readOnly: boolean;
59
59
  symlinks: false;
@@ -152,8 +152,8 @@ export class S3FileSystem {
152
152
  atomicRenameNoReplace: false,
153
153
  rename: this.allowRename && options.transport.capabilities?.conditionalDelete === true
154
154
  && (options.transport.capabilities?.conditionalCopy === true || options.transport.capabilities?.conditionalPut === true),
155
- streamingAppend: options.transport.capabilities?.streamingWrite === true
156
- && options.transport.capabilities?.conditionalPut === true && typeof options.transport.putObjectStream === "function",
155
+ // Conditional append currently collects the old and incoming bodies.
156
+ streamingAppend: false,
157
157
  randomAccessWrite: false,
158
158
  readOnly: options.readOnly ?? false,
159
159
  symlinks: false, hardlinks: false, permissions: false,
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "name": "toolcraft-schema",
11
- "version": "0.0.759",
11
+ "version": "0.0.760",
12
12
  "license": "MIT"
13
13
  }
14
14
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-schema",
3
- "version": "0.0.759",
3
+ "version": "0.0.760",
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.759",
3
+ "version": "0.0.760",
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.759",
178
+ "toolcraft-schema": "0.0.760",
179
179
  "toolcraft-design": "*",
180
180
  "@poe-code/frontmatter": "*",
181
181
  "@poe-code/agent-mcp-config": "*",