@effect-vfs/core 0.3.0 → 0.4.0

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.
Files changed (69) hide show
  1. package/README.md +36 -149
  2. package/dist/BytePath.d.ts +34 -2
  3. package/dist/BytePath.d.ts.map +1 -1
  4. package/dist/BytePath.js +1 -1
  5. package/dist/LiveVolume.d.ts +101 -0
  6. package/dist/LiveVolume.d.ts.map +1 -0
  7. package/dist/LiveVolume.js +72 -0
  8. package/dist/Snapshot.d.ts +60 -2
  9. package/dist/Snapshot.d.ts.map +1 -1
  10. package/dist/Snapshot.js +59 -1
  11. package/dist/SnapshotDelta.d.ts +113 -3
  12. package/dist/SnapshotDelta.d.ts.map +1 -1
  13. package/dist/SnapshotDelta.js +112 -2
  14. package/dist/VirtualFileSystem.d.ts +1224 -90
  15. package/dist/VirtualFileSystem.d.ts.map +1 -1
  16. package/dist/VirtualFileSystem.js +678 -20
  17. package/dist/VirtualFileSystemError.d.ts +54 -0
  18. package/dist/VirtualFileSystemError.d.ts.map +1 -0
  19. package/dist/VirtualFileSystemError.js +49 -0
  20. package/dist/index.d.ts +15 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +15 -1
  23. package/dist/internal/bytePath.d.ts.map +1 -1
  24. package/dist/internal/bytePath.js +5 -10
  25. package/dist/internal/bytes.d.ts +6 -0
  26. package/dist/internal/bytes.d.ts.map +1 -0
  27. package/dist/internal/bytes.js +17 -0
  28. package/dist/internal/canonicalBase64.d.ts +13 -5
  29. package/dist/internal/canonicalBase64.d.ts.map +1 -1
  30. package/dist/internal/canonicalBase64.js +28 -6
  31. package/dist/internal/image.d.ts +76 -90
  32. package/dist/internal/image.d.ts.map +1 -1
  33. package/dist/internal/image.js +98 -89
  34. package/dist/internal/liveImage.d.ts +286 -0
  35. package/dist/internal/liveImage.d.ts.map +1 -0
  36. package/dist/internal/liveImage.js +176 -0
  37. package/dist/internal/metadata.d.ts +28 -0
  38. package/dist/internal/metadata.d.ts.map +1 -0
  39. package/dist/internal/metadata.js +29 -0
  40. package/dist/internal/snapshotDelta.d.ts.map +1 -1
  41. package/dist/internal/snapshotDelta.js +346 -300
  42. package/dist/internal/snapshotDeltaModel.d.ts +2 -2
  43. package/dist/internal/snapshotDeltaModel.d.ts.map +1 -1
  44. package/dist/internal/snapshotDeltaModel.js +3 -1
  45. package/dist/internal/virtualFileSystem/errors.d.ts +6 -20
  46. package/dist/internal/virtualFileSystem/errors.d.ts.map +1 -1
  47. package/dist/internal/virtualFileSystem/errors.js +22 -7
  48. package/dist/internal/virtualFileSystem/fixture.d.ts +4 -1
  49. package/dist/internal/virtualFileSystem/fixture.d.ts.map +1 -1
  50. package/dist/internal/virtualFileSystem/fixture.js +80 -74
  51. package/dist/internal/virtualFileSystem/overlayContent.d.ts.map +1 -1
  52. package/dist/internal/virtualFileSystem/overlayContent.js +9 -2
  53. package/dist/internal/virtualFileSystem/overlayDiff.d.ts +2 -11
  54. package/dist/internal/virtualFileSystem/overlayDiff.d.ts.map +1 -1
  55. package/dist/internal/virtualFileSystem/overlayDiff.js +45 -71
  56. package/dist/internal/virtualFileSystem/path.d.ts +15 -14
  57. package/dist/internal/virtualFileSystem/path.d.ts.map +1 -1
  58. package/dist/internal/virtualFileSystem/path.js +57 -54
  59. package/dist/internal/virtualFileSystem/stagedState.d.ts +22 -0
  60. package/dist/internal/virtualFileSystem/stagedState.d.ts.map +1 -0
  61. package/dist/internal/virtualFileSystem/stagedState.js +62 -0
  62. package/dist/internal/virtualFileSystem/testHooks.d.ts.map +1 -1
  63. package/dist/internal/virtualFileSystem/watchHub.d.ts +3 -3
  64. package/dist/internal/virtualFileSystem/watchHub.d.ts.map +1 -1
  65. package/dist/internal/virtualFileSystem/watchHub.js +45 -24
  66. package/dist/internal/virtualFileSystem.d.ts +307 -23
  67. package/dist/internal/virtualFileSystem.d.ts.map +1 -1
  68. package/dist/internal/virtualFileSystem.js +1738 -525
  69. package/package.json +23 -5
@@ -1,20 +1,27 @@
1
1
  // Runtime definitions and cohesive live virtual filesystem engine.
2
2
  import * as ByteSize from "effect/ByteSize";
3
3
  import * as Clock from "effect/Clock";
4
- import * as Context from "effect/Context";
4
+ import * as Crypto from "effect/Crypto";
5
+ import * as Data from "effect/Data";
5
6
  import * as Effect from "effect/Effect";
6
7
  import * as Encoding from "effect/Encoding";
8
+ import * as Exit from "effect/Exit";
9
+ import * as Order from "effect/Order";
10
+ import * as Predicate from "effect/Predicate";
7
11
  import * as Result from "effect/Result";
8
12
  import * as Schema from "effect/Schema";
9
13
  import * as Semaphore from "effect/Semaphore";
10
14
  import { BytePath } from "../BytePath.js";
11
15
  import { ImageError } from "../Snapshot.js";
12
- import { getBytes as getBytePathBytes } from "./bytePath.js";
16
+ import { CanonicalBase64 } from "./canonicalBase64.js";
13
17
  import * as Image from "./image.js";
14
- import { ConfigurationError, FsCode as FsCodeSchema, FsError } from "./virtualFileSystem/errors.js";
18
+ import * as LiveImage from "./liveImage.js";
19
+ import * as MetadataDomain from "./metadata.js";
20
+ import { ConfigurationError, decodeConfiguration, FsCode as FsCodeSchema, FsError } from "./virtualFileSystem/errors.js";
15
21
  import * as Content from "./virtualFileSystem/overlayContent.js";
16
22
  import { compareOverlay } from "./virtualFileSystem/overlayDiff.js";
17
- import { attachedBuffer, decodeConfiguration, failure, nameBytes, ownedPath, preparePath, strictString, wellFormed } from "./virtualFileSystem/path.js";
23
+ import { DOT_DOT_HEX, DOT_HEX, inputBytes, isAttachedBytes, isDotComponent, MAX_NAME_BYTES, nameBytes, ownedPath, preparePath, SLASH_BYTE, SLASH_HEX, strictString } from "./virtualFileSystem/path.js";
24
+ import { makeStagedState } from "./virtualFileSystem/stagedState.js";
18
25
  import * as TestHooks from "./virtualFileSystem/testHooks.js";
19
26
  import * as WatchHub from "./virtualFileSystem/watchHub.js";
20
27
  /** @internal */
@@ -29,8 +36,22 @@ export const DirectoryHandleId = Symbol("@effect-vfs/core/DirectoryHandle");
29
36
  export const ObjectReferenceId = Symbol("@effect-vfs/core/ObjectReference");
30
37
  /** @internal */
31
38
  export { ConfigurationError, FsCodeSchema as FsCode, FsError };
32
- /** @internal */
33
- export const Mode = Schema.Natural.check(Schema.isLessThanOrEqualTo(0o7777));
39
+ // POSIX permission bits, masked against mode once it is shifted to the caller's class.
40
+ const EXECUTE = 0o1;
41
+ const WRITE = 0o2;
42
+ const READ = 0o4;
43
+ // Execute for every class; a file with none is not executable by anyone.
44
+ const ANY_EXECUTE = 0o111;
45
+ // setuid and setgid, cleared whenever a file's contents or ownership change.
46
+ const SET_ID_BITS = 0o6000;
47
+ // Sticky: only the owner of an entry or of its directory may remove it.
48
+ const STICKY_BIT = 0o1000;
49
+ const WALK_YIELD_INTERVAL = 128;
50
+ // Largest signed 64-bit file offset, as POSIX off_t.
51
+ const MAX_FILE_OFFSET = 0x7fffffffffffffffn;
52
+ const Mode = Schema.Natural.check(Schema.isLessThanOrEqualTo(0o7777));
53
+ const isMode = Schema.is(Mode);
54
+ const isNatural = Schema.is(Schema.Natural);
34
55
  /** @internal */
35
56
  export const Identity = Schema.Struct({
36
57
  uid: Schema.Natural,
@@ -43,17 +64,39 @@ export const RootCallerOptions = Schema.Struct({
43
64
  identity: Schema.optionalKey(Identity),
44
65
  umask: Schema.optionalKey(Schema.Natural.check(Schema.isLessThanOrEqualTo(0o777)))
45
66
  });
67
+ const Hex128 = Schema.String.check(Schema.isPattern(/^[0-9a-f]{32}$/));
68
+ /** @internal */
69
+ export const VolumeDurability = Schema.Literals([
70
+ "memory-only",
71
+ "survives-process-crash",
72
+ "survives-operating-system-crash",
73
+ "survives-power-loss"
74
+ ]);
75
+ const durabilityRank = {
76
+ "memory-only": 0,
77
+ "survives-process-crash": 1,
78
+ "survives-operating-system-crash": 2,
79
+ "survives-power-loss": 3
80
+ };
81
+ /** @internal */
82
+ export const VolumeDurabilityOrder = Order.mapInput(Order.Number, (durability) => durabilityRank[durability]);
83
+ /** @internal */
84
+ export const isVolumeDurabilityAtLeast = (actual, required) => VolumeDurabilityOrder(actual, required) >= 0;
85
+ /** @internal */
86
+ export const VolumeIdentity = Hex128.pipe(Schema.brand("@effect-vfs/core/VolumeIdentity"));
87
+ /** @internal */
88
+ export const VolumeIncarnation = Hex128.pipe(Schema.brand("@effect-vfs/core/VolumeIncarnation"));
46
89
  /** @internal */
47
90
  export const VolumeOptions = Schema.Struct({
91
+ identity: Schema.optionalKey(VolumeIdentity),
48
92
  maxEntries: Schema.optionalKey(Schema.Natural),
93
+ maxPendingOperations: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))),
94
+ maxWatchEvents: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(2))),
49
95
  maxBytes: Schema.optionalKey(Schema.ByteSize),
50
96
  maxFileBytes: Schema.optionalKey(Schema.ByteSize.check(Schema.makeFilter((size) => ByteSize.isLessThanOrEqualTo(size, ByteSize.bytes(0xffffffff)) ? undefined : "must be at most 4294967295 bytes"))),
51
97
  maxPathBytes: Schema.optionalKey(Schema.ByteSize.check(Schema.makeFilter((size) => ByteSize.isGreaterThanOrEqualTo(size, ByteSize.bytes(1)) ? undefined : "must be at least 1 byte")))
52
98
  });
53
- // Match snapshot v1's canonical signed decimal timestamp domain.
54
- const timestampLimit = 10n ** 128n - 1n;
55
- /** @internal */
56
- export const Timestamp = Schema.BigInt.check(Schema.isGreaterThanOrEqualToBigInt(-timestampLimit), Schema.isLessThanOrEqualToBigInt(timestampLimit));
99
+ const isTimestamp = Schema.is(MetadataDomain.Timestamp);
57
100
  /** @internal */
58
101
  export const Metadata = Schema.Struct({
59
102
  kind: Schema.Literals(["directory", "file", "symlink"]),
@@ -63,10 +106,10 @@ export const Metadata = Schema.Struct({
63
106
  uid: Schema.Natural,
64
107
  gid: Schema.Natural,
65
108
  mode: Mode,
66
- atimeNs: Timestamp,
67
- mtimeNs: Timestamp,
68
- ctimeNs: Timestamp,
69
- birthtimeNs: Timestamp
109
+ atimeNs: MetadataDomain.Timestamp,
110
+ mtimeNs: MetadataDomain.Timestamp,
111
+ ctimeNs: MetadataDomain.Timestamp,
112
+ birthtimeNs: MetadataDomain.Timestamp
70
113
  });
71
114
  /** @internal */
72
115
  export const OwnerUpdate = Schema.Struct({
@@ -77,12 +120,13 @@ export const OwnerUpdate = Schema.Struct({
77
120
  export const TimeUpdate = Schema.Union([
78
121
  Schema.Struct({ kind: Schema.Literal("now") }),
79
122
  Schema.Struct({ kind: Schema.Literal("omit") }),
80
- Schema.Struct({ kind: Schema.Literal("value"), nanoseconds: Timestamp })
123
+ Schema.Struct({ kind: Schema.Literal("value"), nanoseconds: MetadataDomain.Timestamp })
81
124
  ]);
82
125
  /** @internal */
83
126
  export const Times = Schema.Struct({ access: TimeUpdate, modification: TimeUpdate });
84
127
  /** @internal */
85
128
  export const SeekMode = Schema.Literals(["start", "current", "end", "data", "hole"]);
129
+ const isSeekMode = Schema.is(SeekMode);
86
130
  /** @internal */
87
131
  export const OpenSettings = Schema.Struct({
88
132
  access: Schema.Literals(["read", "write", "readWrite"]),
@@ -93,11 +137,61 @@ export const OpenSettings = Schema.Struct({
93
137
  followFinalSymlink: Schema.optionalKey(Schema.Boolean)
94
138
  });
95
139
  /** @internal */
96
- export const WriteFileSettings = Schema.Struct({
140
+ export const DirectoryChange = Schema.Struct({
141
+ before: Schema.BigInt,
142
+ after: Schema.BigInt
143
+ });
144
+ /** @internal */
145
+ export const RenameReferenceResult = Schema.TaggedUnion({
146
+ SameDirectory: { directory: DirectoryChange },
147
+ DifferentDirectories: {
148
+ sourceDirectory: DirectoryChange,
149
+ destinationDirectory: DirectoryChange
150
+ }
151
+ });
152
+ /** @internal */
153
+ export const MkdirReferenceSettings = Schema.Struct({
154
+ mode: Schema.optionalKey(Mode),
155
+ exactMode: Schema.optionalKey(Schema.Boolean),
156
+ times: Schema.optionalKey(Times)
157
+ });
158
+ /** @internal */
159
+ export const SymlinkReferenceSettings = Schema.Struct({
160
+ times: Schema.optionalKey(Times)
161
+ });
162
+ /** @internal */
163
+ export const OpenReferenceSettings = Schema.Struct({
164
+ access: Schema.Literals(["read", "write", "readWrite"]),
165
+ append: Schema.optionalKey(Schema.Boolean),
166
+ truncate: Schema.optionalKey(Schema.Boolean)
167
+ });
168
+ /** @internal */
169
+ export const OpenChildReferenceSettings = Schema.Struct({
170
+ ...OpenSettings.fields,
171
+ times: Schema.optionalKey(Times),
172
+ initialSize: Schema.optionalKey(Schema.BigInt),
173
+ exactMode: Schema.optionalKey(Schema.Boolean),
174
+ owner: Schema.optionalKey(OwnerUpdate),
175
+ expectedChild: Schema.optionalKey(Schema.NullOr(Schema.Struct({
176
+ reference: Schema.declare((input) => Predicate.hasProperty(ObjectReferenceId)(input) && input[ObjectReferenceId] === true),
177
+ revision: Schema.BigInt,
178
+ atimeNs: MetadataDomain.Timestamp,
179
+ mtimeNs: MetadataDomain.Timestamp
180
+ })))
181
+ });
182
+ const WriteFileSettings = Schema.Struct({
97
183
  ...OpenSettings.fields,
98
184
  replaceFinalSymlink: Schema.optionalKey(Schema.Boolean),
99
185
  finalMode: Schema.optionalKey(Mode)
100
186
  });
187
+ const decodeOwnerUpdate = Schema.decodeResult(OwnerUpdate, { onExcessProperty: "error" });
188
+ const decodeTimes = Schema.decodeResult(Times, { onExcessProperty: "error" });
189
+ const decodeWriteFileSettings = Schema.decodeResult(WriteFileSettings, { onExcessProperty: "error" });
190
+ const decodeOpenSettings = Schema.decodeResult(OpenSettings, { onExcessProperty: "error" });
191
+ const decodeMkdirReferenceSettings = Schema.decodeResult(MkdirReferenceSettings, { onExcessProperty: "error" });
192
+ const decodeSymlinkReferenceSettings = Schema.decodeResult(SymlinkReferenceSettings, { onExcessProperty: "error" });
193
+ const decodeOpenReferenceSettings = Schema.decodeResult(OpenReferenceSettings, { onExcessProperty: "error" });
194
+ const decodeOpenChildReferenceSettings = Schema.decodeResult(OpenChildReferenceSettings, { onExcessProperty: "error" });
101
195
  /** @internal */
102
196
  export const OverlayNodeKind = Schema.Literals(["directory", "file", "symlink"]);
103
197
  /** @internal */
@@ -136,47 +230,112 @@ export const OverlayChangesOptions = Schema.Struct({
136
230
  includeTimestamps: Schema.optionalKey(Schema.Boolean)
137
231
  });
138
232
  /** @internal */
139
- export class CurrentFileSystem extends Context.Service()("@effect-vfs/core/CurrentFileSystem") {
140
- }
141
- /** @internal */
142
233
  export const FixtureMetadata = Schema.Struct({
143
234
  uid: Schema.optionalKey(Schema.Natural),
144
235
  gid: Schema.optionalKey(Schema.Natural),
145
236
  mode: Schema.optionalKey(Mode),
146
- atimeNs: Schema.optionalKey(Timestamp),
147
- mtimeNs: Schema.optionalKey(Timestamp),
148
- ctimeNs: Schema.optionalKey(Timestamp),
149
- birthtimeNs: Schema.optionalKey(Timestamp)
237
+ atimeNs: Schema.optionalKey(MetadataDomain.Timestamp),
238
+ mtimeNs: Schema.optionalKey(MetadataDomain.Timestamp),
239
+ ctimeNs: Schema.optionalKey(MetadataDomain.Timestamp),
240
+ birthtimeNs: Schema.optionalKey(MetadataDomain.Timestamp)
150
241
  });
151
242
  const FixturePath = Schema.Union([
152
243
  Schema.String,
153
244
  BytePath
154
245
  ]);
155
246
  /** @internal */
247
+ export const FixtureEntry = Schema.Union([
248
+ Schema.Struct({
249
+ kind: Schema.Literal("directory"),
250
+ path: FixturePath,
251
+ metadata: Schema.optionalKey(FixtureMetadata)
252
+ }),
253
+ Schema.Struct({
254
+ kind: Schema.Literal("file"),
255
+ path: FixturePath,
256
+ bytes: Schema.Uint8Array,
257
+ metadata: Schema.optionalKey(FixtureMetadata)
258
+ }),
259
+ Schema.Struct({
260
+ kind: Schema.Literal("symlink"),
261
+ path: FixturePath,
262
+ target: FixturePath,
263
+ metadata: Schema.optionalKey(FixtureMetadata)
264
+ }),
265
+ Schema.Struct({ kind: Schema.Literal("hardLink"), path: FixturePath, target: FixturePath })
266
+ ]).pipe(Schema.toTaggedUnion("kind"));
267
+ /** @internal */
156
268
  export const Fixture = Schema.Struct({
157
269
  rootMetadata: Schema.optionalKey(FixtureMetadata),
158
- entries: Schema.Array(Schema.Union([
159
- Schema.Struct({
160
- kind: Schema.Literal("directory"),
161
- path: FixturePath,
162
- metadata: Schema.optionalKey(FixtureMetadata)
163
- }),
164
- Schema.Struct({
165
- kind: Schema.Literal("file"),
166
- path: FixturePath,
167
- bytes: Schema.Uint8Array,
168
- metadata: Schema.optionalKey(FixtureMetadata)
169
- }),
170
- Schema.Struct({
171
- kind: Schema.Literal("symlink"),
172
- path: FixturePath,
173
- target: FixturePath,
174
- metadata: Schema.optionalKey(FixtureMetadata)
175
- }),
176
- Schema.Struct({ kind: Schema.Literal("hardLink"), path: FixturePath, target: FixturePath })
177
- ]))
270
+ entries: Schema.Array(FixtureEntry)
271
+ });
272
+ /** @internal */
273
+ export const captureLiveImage = Effect.fnUntraced(function* (state, identity, limits) {
274
+ const records = [];
275
+ const visited = new Set();
276
+ const pending = [state.root, ...state.retainedFiles.values()];
277
+ for (let index = 0; index < pending.length; index++) {
278
+ if (index % WALK_YIELD_INTERVAL === 0)
279
+ yield* Effect.yieldNow;
280
+ const node = pending[index];
281
+ if (node === undefined || visited.has(node.metadata.ino))
282
+ continue;
283
+ visited.add(node.metadata.ino);
284
+ const common = {
285
+ ino: node.metadata.ino,
286
+ revision: node.revision,
287
+ metadata: {
288
+ ...storedMetadata(node.metadata),
289
+ nlink: node.metadata.nlink,
290
+ size: node.metadata.size
291
+ }
292
+ };
293
+ if (node.lineage !== undefined)
294
+ common.lineage = node.lineage;
295
+ if (node.kind === "directory") {
296
+ const entries = [];
297
+ for (const [name, child] of node.entries) {
298
+ entries.push({ name: CanonicalBase64.encode(nameBytes(name)), target: child.metadata.ino });
299
+ pending.push(child);
300
+ }
301
+ records.push(LiveImage.Record.cases.directory.make({ ...common, entries }));
302
+ }
303
+ else if (node.kind === "file") {
304
+ records.push(LiveImage.Record.cases.file.make({ ...common, data: CanonicalBase64.encode(node.data.bytes) }));
305
+ }
306
+ else {
307
+ records.push(LiveImage.Record.cases.symlink.make({ ...common, target: CanonicalBase64.encode(node.target) }));
308
+ }
309
+ }
310
+ const storedLimits = {};
311
+ if (limits.maxEntries !== undefined)
312
+ storedLimits.maxEntries = limits.maxEntries;
313
+ if (limits.maxBytes !== undefined)
314
+ storedLimits.maxBytes = ByteSize.toBigInt(limits.maxBytes);
315
+ if (limits.maxFileBytes !== undefined)
316
+ storedLimits.maxFileBytes = ByteSize.toBigInt(limits.maxFileBytes);
317
+ if (limits.maxPathBytes !== undefined)
318
+ storedLimits.maxPathBytes = ByteSize.toBigInt(limits.maxPathBytes);
319
+ const document = {
320
+ format: "effect-vfs-live",
321
+ version: 1,
322
+ identity,
323
+ root: state.root.metadata.ino,
324
+ nextInode: state.nextInode,
325
+ revisionCounter: state.revisionCounter,
326
+ entries: state.entries,
327
+ usedBytes: state.usedBytes,
328
+ limits: storedLimits,
329
+ retainedFiles: [...state.retainedFiles.keys()],
330
+ records
331
+ };
332
+ return yield* LiveImage.encode(document);
178
333
  });
179
334
  const objectReferences = new WeakMap();
335
+ const isFileHandle = (value) => Predicate.hasProperty(FileHandleId)(value);
336
+ // Only a PathInput target names a path; a handle-based call has none to report in its error.
337
+ const pathOf = (target) => isFileHandle(target) || isDirectoryHandle(target) ? undefined : target;
338
+ const isDirectoryHandle = (value) => Predicate.hasProperty(DirectoryHandleId)(value);
180
339
  const files = new WeakMap();
181
340
  const handles = new WeakMap();
182
341
  const directoryMetadata = (ino, uid, gid, mode, now) => ({
@@ -196,58 +355,235 @@ const storedMetadata = (metadata) => ({
196
355
  uid: metadata.uid,
197
356
  gid: metadata.gid,
198
357
  mode: metadata.mode,
199
- atimeNs: String(metadata.atimeNs),
200
- mtimeNs: String(metadata.mtimeNs),
201
- ctimeNs: String(metadata.ctimeNs),
202
- birthtimeNs: String(metadata.birthtimeNs)
358
+ atimeNs: metadata.atimeNs,
359
+ mtimeNs: metadata.mtimeNs,
360
+ ctimeNs: metadata.ctimeNs,
361
+ birthtimeNs: metadata.birthtimeNs
203
362
  });
363
+ /** @internal */
364
+ export const VolumeSource = Data.taggedEnum();
365
+ const UpdateChange = Schema.TaggedStruct("Update", { path: BytePath });
366
+ const RescanChange = Schema.TaggedStruct("Rescan", { path: BytePath });
204
367
  // Each execution constructs a fresh volume and captures its Clock.
205
368
  /** @internal */
206
- export const makeVolume = Effect.fnUntraced(function* (source, options) {
207
- const image = source._tag === "Empty" ? undefined : source.image;
208
- const decoded = decodeConfiguration(VolumeOptions, options === undefined ? {} : options);
369
+ export const makeVolume = Effect.fnUntraced(function* (source, options, commitProvider, captureInitial) {
370
+ const image = "image" in source ? source.image : undefined;
371
+ const live = Predicate.isTagged("Live")(source) ? source.document : undefined;
372
+ let restoredOptions = options;
373
+ if (live !== undefined) {
374
+ const recovered = { identity: VolumeIdentity.make(live.identity) };
375
+ if (live.limits.maxEntries !== undefined)
376
+ recovered.maxEntries = live.limits.maxEntries;
377
+ if (live.limits.maxBytes !== undefined)
378
+ recovered.maxBytes = ByteSize.bytes(live.limits.maxBytes);
379
+ if (live.limits.maxFileBytes !== undefined)
380
+ recovered.maxFileBytes = ByteSize.bytes(live.limits.maxFileBytes);
381
+ if (live.limits.maxPathBytes !== undefined)
382
+ recovered.maxPathBytes = ByteSize.bytes(live.limits.maxPathBytes);
383
+ restoredOptions = recovered;
384
+ }
385
+ const decoded = decodeConfiguration(VolumeOptions, restoredOptions === undefined ? {} : restoredOptions);
209
386
  if (Result.isFailure(decoded))
210
387
  return yield* decoded.failure;
211
388
  const settings = { ...decoded.success };
389
+ const crypto = yield* Crypto.Crypto;
390
+ const identity = settings.identity === undefined
391
+ ? VolumeIdentity.make(Encoding.encodeHex(yield* crypto.randomBytes(16)))
392
+ : VolumeIdentity.make(settings.identity);
393
+ const incarnation = VolumeIncarnation.make(Encoding.encodeHex(yield* crypto.randomBytes(16)));
212
394
  const clock = yield* Clock.clockWith(Effect.succeed);
213
395
  const initialTime = clock.currentTimeNanosUnsafe();
214
- if (!Schema.is(Timestamp)(initialTime)) {
396
+ if (!isTimestamp(initialTime)) {
215
397
  return yield* new ConfigurationError({ field: "clock.currentTimeNanos" });
216
398
  }
217
399
  const timestamp = (operation) => Effect.suspend(() => {
218
400
  const now = clock.currentTimeNanosUnsafe();
219
- return Schema.is(Timestamp)(now) ? Effect.succeed(now) : Effect.fail(failure("InvalidArgument", operation));
401
+ return isTimestamp(now)
402
+ ? Effect.succeed(now)
403
+ : Effect.fail(new FsError({ code: "InvalidArgument", operation: operation }));
220
404
  });
221
405
  const volumeIdentity = Symbol();
406
+ let activeStage;
407
+ const fileReferences = new Set();
408
+ const directoryReferences = new Set();
409
+ // A capability holds a cell instead of a particular node object. Publication can replace
410
+ // the node behind the cell without replacing the capability held by a caller.
411
+ const cells = new WeakMap();
412
+ const cellFor = (node) => {
413
+ const existing = cells.get(node);
414
+ if (existing !== undefined)
415
+ return existing;
416
+ const previous = activeStage?.previous.get(node);
417
+ const retained = previous === undefined ? undefined : cells.get(previous);
418
+ if (retained !== undefined) {
419
+ cells.set(node, retained);
420
+ return retained;
421
+ }
422
+ let current = node;
423
+ const cell = {
424
+ get node() {
425
+ return activeStage?.nodes.get(current) ?? current;
426
+ },
427
+ set node(next) {
428
+ current = next;
429
+ }
430
+ };
431
+ cells.set(node, cell);
432
+ return cell;
433
+ };
434
+ const makeFileReference = (access, append) => {
435
+ const live = { cell: undefined, closed: false, offset: 0n };
436
+ let reference;
437
+ const record = () => {
438
+ if (activeStage === undefined)
439
+ return live;
440
+ let staged = activeStage.files.get(reference);
441
+ if (staged === undefined) {
442
+ staged = { ...live };
443
+ activeStage.files.set(reference, staged);
444
+ const pending = staged;
445
+ activeStage.apply.push(() => {
446
+ Object.assign(live, pending);
447
+ if (live.cell === undefined)
448
+ fileReferences.delete(reference);
449
+ else
450
+ fileReferences.add(reference);
451
+ });
452
+ }
453
+ return staged;
454
+ };
455
+ reference = {
456
+ volume: volumeIdentity,
457
+ get file() {
458
+ const node = record().cell?.node;
459
+ return node?.kind === "file" ? node : undefined;
460
+ },
461
+ set file(file) {
462
+ record().cell = file === undefined ? undefined : cellFor(file);
463
+ if (activeStage === undefined) {
464
+ if (file === undefined)
465
+ fileReferences.delete(reference);
466
+ else
467
+ fileReferences.add(reference);
468
+ }
469
+ },
470
+ get closed() {
471
+ return record().closed;
472
+ },
473
+ set closed(value) {
474
+ record().closed = value;
475
+ },
476
+ get offset() {
477
+ return record().offset;
478
+ },
479
+ set offset(value) {
480
+ record().offset = value;
481
+ },
482
+ access,
483
+ append
484
+ };
485
+ return reference;
486
+ };
487
+ const makeDirectoryReference = (directory) => {
488
+ const live = {
489
+ cell: directory === undefined ? undefined : cellFor(directory),
490
+ closed: false
491
+ };
492
+ let reference;
493
+ const record = () => {
494
+ if (activeStage === undefined)
495
+ return live;
496
+ let staged = activeStage.directories.get(reference);
497
+ if (staged === undefined) {
498
+ staged = { ...live };
499
+ activeStage.directories.set(reference, staged);
500
+ const pending = staged;
501
+ activeStage.apply.push(() => {
502
+ Object.assign(live, pending);
503
+ if (live.cell === undefined || live.cell.node === state.root)
504
+ directoryReferences.delete(reference);
505
+ else
506
+ directoryReferences.add(reference);
507
+ });
508
+ }
509
+ return staged;
510
+ };
511
+ reference = {
512
+ volume: volumeIdentity,
513
+ get directory() {
514
+ const node = record().cell?.node;
515
+ return node?.kind === "directory" ? node : undefined;
516
+ },
517
+ set directory(directory) {
518
+ record().cell = directory === undefined ? undefined : cellFor(directory);
519
+ if (activeStage === undefined) {
520
+ if (directory === undefined || directory === state.root)
521
+ directoryReferences.delete(reference);
522
+ else
523
+ directoryReferences.add(reference);
524
+ }
525
+ },
526
+ get closed() {
527
+ return record().closed;
528
+ },
529
+ set closed(value) {
530
+ record().closed = value;
531
+ }
532
+ };
533
+ if (directory !== undefined && directory !== state.root && activeStage === undefined) {
534
+ directoryReferences.add(reference);
535
+ }
536
+ return reference;
537
+ };
222
538
  const gate = Semaphore.makeUnsafe(1);
223
- let revisionCounter = 0n;
224
- const nextRevision = () => ++revisionCounter;
225
- const root = {
226
- kind: "directory",
227
- lineage: image?.root,
228
- parent: undefined,
229
- entries: new Map(),
230
- metadata: directoryMetadata(1n, 0, 0, 0o755, initialTime),
231
- revision: nextRevision(),
232
- objectReference: undefined
539
+ const maxPendingOperations = settings.maxPendingOperations ?? 64;
540
+ let admitted = 0;
541
+ const admit = (operation, effect) => Effect.suspend(() => {
542
+ if (admitted >= maxPendingOperations + 1) {
543
+ return Effect.fail(new FsError({ code: "VolumeBusy", operation }));
544
+ }
545
+ admitted += 1;
546
+ return effect.pipe(Effect.ensuring(Effect.sync(() => {
547
+ admitted -= 1;
548
+ })));
549
+ });
550
+ let state = {
551
+ root: {
552
+ kind: "directory",
553
+ lineage: image?.root,
554
+ parent: undefined,
555
+ entries: new Map(),
556
+ metadata: directoryMetadata(1n, 0, 0, 0o755, initialTime),
557
+ revision: 1n,
558
+ objectReference: undefined
559
+ },
560
+ retainedFiles: new Map(),
561
+ revisionCounter: 1n,
562
+ nextInode: 2n,
563
+ entries: 0,
564
+ usedBytes: 0n
233
565
  };
234
- let nextInode = 2n;
235
- let entries = 0;
236
- let usedBytes = 0n;
566
+ const nextRevision = () => ++state.revisionCounter;
237
567
  // The schema caps this value at uint32, so this boundary conversion is exact.
238
568
  const maxFileBytes = Number(ByteSize.toBigInt(settings.maxFileBytes ?? ByteSize.bytes(0xffffffff)));
239
- // Permit waits stay interruptible. State transitions and resource registration do not.
240
- const coordinated = (effect) => gate.withPermit(Effect.uninterruptible(effect));
569
+ const limits = Object.freeze({
570
+ maxBytes: settings.maxBytes,
571
+ maxFileBytes: ByteSize.bytes(maxFileBytes),
572
+ maxEntries: settings.maxEntries,
573
+ maxPathBytes: settings.maxPathBytes,
574
+ maxPendingOperations: settings.maxPendingOperations ?? 64,
575
+ maxWatchEvents: settings.maxWatchEvents ?? 256
576
+ });
241
577
  if (image !== undefined) {
242
578
  const incoming = new Map();
243
579
  let content = 0n;
244
580
  let count = 0;
245
581
  for (const record of image.records) {
246
- if (record.kind === "directory")
582
+ if (Image.Record.guards.directory(record))
247
583
  count += record.entries.length;
248
584
  else {
249
- const length = Image.decodedLength(record.kind === "file" ? record.data : record.target);
250
- if (record.kind === "file" && length > maxFileBytes) {
585
+ const length = CanonicalBase64.decodedLength(Image.Record.guards.file(record) ? record.data : record.target);
586
+ if (Image.Record.guards.file(record) && length > maxFileBytes) {
251
587
  return yield* new ImageError({ code: "LimitExceeded", field: "maxFileBytes" });
252
588
  }
253
589
  content += BigInt(length);
@@ -257,22 +593,22 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
257
593
  (settings.maxBytes !== undefined && content > ByteSize.toBigInt(settings.maxBytes))) {
258
594
  return yield* new ImageError({ code: "LimitExceeded", field: "volume" });
259
595
  }
260
- const baseContents = source._tag === "Overlay" ? Content.forOverlay(source.base, image) : undefined;
596
+ const baseContents = Predicate.isTagged("Overlay")(source) ? Content.forOverlay(source.base, image) : undefined;
261
597
  for (const record of image.records) {
262
598
  const metadata = {
263
599
  ...record.metadata,
264
- kind: record.kind,
265
- ino: record.id === image.root ? 1n : nextInode++,
266
- nlink: record.kind === "directory" ? 2 : 0,
600
+ kind: record._tag,
601
+ ino: record.id === image.root ? 1n : state.nextInode++,
602
+ nlink: Image.Record.guards.directory(record) ? 2 : 0,
267
603
  size: 0n,
268
- atimeNs: BigInt(record.metadata.atimeNs),
269
- mtimeNs: BigInt(record.metadata.mtimeNs),
270
- ctimeNs: BigInt(record.metadata.ctimeNs),
271
- birthtimeNs: BigInt(record.metadata.birthtimeNs)
604
+ atimeNs: record.metadata.atimeNs,
605
+ mtimeNs: record.metadata.mtimeNs,
606
+ ctimeNs: record.metadata.ctimeNs,
607
+ birthtimeNs: record.metadata.birthtimeNs
272
608
  };
273
- if (record.kind === "directory") {
609
+ if (Image.Record.guards.directory(record)) {
274
610
  const node = record.id === image.root
275
- ? root
611
+ ? state.root
276
612
  : {
277
613
  kind: "directory",
278
614
  lineage: record.id,
@@ -285,8 +621,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
285
621
  node.metadata = metadata;
286
622
  incoming.set(record.id, node);
287
623
  }
288
- else if (record.kind === "file") {
289
- const data = baseContents?.get(record.id) ?? Content.make(Image.bytes(record.data));
624
+ else if (Image.Record.guards.file(record)) {
625
+ const data = baseContents?.get(record.id) ?? Content.make(yield* CanonicalBase64.decode(record.data));
290
626
  incoming.set(record.id, {
291
627
  kind: "file",
292
628
  lineage: record.id,
@@ -298,7 +634,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
298
634
  });
299
635
  }
300
636
  else {
301
- const target = Image.bytes(record.target);
637
+ const target = yield* CanonicalBase64.decode(record.target);
302
638
  incoming.set(record.id, {
303
639
  kind: "symlink",
304
640
  lineage: record.id,
@@ -310,7 +646,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
310
646
  }
311
647
  }
312
648
  for (const record of image.records) {
313
- if (record.kind !== "directory")
649
+ if (!Image.Record.guards.directory(record))
314
650
  continue;
315
651
  const parent = incoming.get(record.id);
316
652
  if (parent?.kind !== "directory")
@@ -319,7 +655,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
319
655
  const node = incoming.get(entry.target);
320
656
  if (node === undefined)
321
657
  return yield* new ImageError({ code: "InvalidStructure" });
322
- parent.entries.set(Encoding.encodeHex(Image.bytes(entry.name)), node);
658
+ parent.entries.set(Encoding.encodeHex(yield* CanonicalBase64.decode(entry.name)), node);
323
659
  if (node.kind === "directory") {
324
660
  node.parent = parent;
325
661
  parent.metadata = { ...parent.metadata, nlink: parent.metadata.nlink + 1 };
@@ -328,10 +664,191 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
328
664
  node.metadata = { ...node.metadata, nlink: node.metadata.nlink + 1 };
329
665
  }
330
666
  }
331
- entries = count;
332
- usedBytes = content;
667
+ state.entries = count;
668
+ state.usedBytes = content;
333
669
  }
334
- const watchHub = yield* WatchHub.make(coordinated);
670
+ if (live !== undefined) {
671
+ const incoming = new Map();
672
+ for (const record of live.records) {
673
+ const metadata = {
674
+ ...record.metadata,
675
+ kind: record._tag,
676
+ ino: record.ino
677
+ };
678
+ if (LiveImage.Record.guards.directory(record)) {
679
+ incoming.set(record.ino, {
680
+ kind: "directory",
681
+ lineage: record.lineage,
682
+ parent: undefined,
683
+ entries: new Map(),
684
+ metadata,
685
+ revision: record.revision,
686
+ objectReference: undefined
687
+ });
688
+ }
689
+ else if (LiveImage.Record.guards.file(record)) {
690
+ incoming.set(record.ino, {
691
+ kind: "file",
692
+ lineage: record.lineage,
693
+ data: Content.make(yield* CanonicalBase64.decode(record.data)),
694
+ openCount: 0,
695
+ metadata,
696
+ revision: record.revision,
697
+ objectReference: undefined
698
+ });
699
+ }
700
+ else {
701
+ incoming.set(record.ino, {
702
+ kind: "symlink",
703
+ lineage: record.lineage,
704
+ target: yield* CanonicalBase64.decode(record.target),
705
+ metadata,
706
+ revision: record.revision,
707
+ objectReference: undefined
708
+ });
709
+ }
710
+ }
711
+ for (const record of live.records) {
712
+ if (!LiveImage.Record.guards.directory(record))
713
+ continue;
714
+ const parent = incoming.get(record.ino);
715
+ if (parent?.kind !== "directory")
716
+ return yield* new ImageError({ code: "InvalidStructure" });
717
+ for (const entry of record.entries) {
718
+ const child = incoming.get(entry.target);
719
+ if (child === undefined)
720
+ return yield* new ImageError({ code: "InvalidStructure" });
721
+ parent.entries.set(Encoding.encodeHex(yield* CanonicalBase64.decode(entry.name)), child);
722
+ if (child.kind === "directory")
723
+ child.parent = parent;
724
+ }
725
+ }
726
+ const root = incoming.get(live.root);
727
+ if (root?.kind !== "directory")
728
+ return yield* new ImageError({ code: "InvalidStructure" });
729
+ state.root = root;
730
+ state.nextInode = live.nextInode;
731
+ state.revisionCounter = live.revisionCounter;
732
+ state.entries = live.entries;
733
+ state.usedBytes = live.usedBytes;
734
+ // A previous process's handles no longer exist. Their zero-link files
735
+ // remain in the stored image but are reclaimed from this runtime state.
736
+ for (const ino of live.retainedFiles) {
737
+ const orphan = incoming.get(ino);
738
+ if (orphan?.kind !== "file")
739
+ return yield* new ImageError({ code: "InvalidStructure" });
740
+ state.usedBytes -= BigInt(orphan.data.bytes.length);
741
+ }
742
+ }
743
+ if (captureInitial !== undefined)
744
+ yield* captureInitial(state, identity, limits);
745
+ const contexts = new WeakMap();
746
+ const copyState = (current) => Effect.sync(() => {
747
+ const nodes = new Map();
748
+ const copyDirectory = (node) => {
749
+ const existing = nodes.get(node);
750
+ if (existing?.kind === "directory")
751
+ return existing;
752
+ const copy = { ...node, entries: new Map(), metadata: { ...node.metadata }, parent: undefined };
753
+ nodes.set(node, copy);
754
+ copy.parent = node.parent === undefined ? undefined : copyDirectory(node.parent);
755
+ for (const [name, child] of node.entries)
756
+ copy.entries.set(name, copyNode(child));
757
+ return copy;
758
+ };
759
+ const copyNode = (node) => {
760
+ if (node.kind === "directory")
761
+ return copyDirectory(node);
762
+ const existing = nodes.get(node);
763
+ if (existing !== undefined)
764
+ return existing;
765
+ const copy = { ...node, metadata: { ...node.metadata } };
766
+ nodes.set(node, copy);
767
+ return copy;
768
+ };
769
+ const candidate = { ...current, root: copyDirectory(current.root), retainedFiles: new Map() };
770
+ for (const [ino, file] of current.retainedFiles) {
771
+ const copy = copyNode(file);
772
+ if (copy.kind === "file")
773
+ candidate.retainedFiles.set(ino, copy);
774
+ }
775
+ // Open resources keep zero-link objects alive after the namespace stops reaching them.
776
+ for (const reference of fileReferences) {
777
+ const file = reference.file;
778
+ if (file !== undefined)
779
+ copyNode(file);
780
+ }
781
+ for (const reference of directoryReferences) {
782
+ const directory = reference.directory;
783
+ if (directory !== undefined)
784
+ copyDirectory(directory);
785
+ }
786
+ contexts.set(candidate, {
787
+ nodes,
788
+ previous: new Map([...nodes].map(([prior, next]) => [next, prior])),
789
+ files: new Map(),
790
+ directories: new Map(),
791
+ invalidated: new Set(),
792
+ events: [],
793
+ apply: []
794
+ });
795
+ return candidate;
796
+ });
797
+ const staged = commitProvider === undefined ? undefined : makeStagedState(state, copyState, commitProvider, (candidate, events) => {
798
+ const context = contexts.get(candidate);
799
+ if (context === undefined)
800
+ throw new Error("Missing staged engine state");
801
+ state = candidate;
802
+ for (const [previous, next] of context.nodes) {
803
+ const cell = cells.get(previous);
804
+ if (cell !== undefined) {
805
+ cell.node = next;
806
+ cells.set(next, cell);
807
+ }
808
+ }
809
+ for (const apply of context.apply)
810
+ apply();
811
+ for (const reference of context.invalidated) {
812
+ reference.active = false;
813
+ reference.cell = undefined;
814
+ }
815
+ for (const event of events)
816
+ event();
817
+ });
818
+ if (staged !== undefined)
819
+ commitProvider?.onReady?.(staged.shutdown);
820
+ // Permit waits stay interruptible. Changes and their publication run under one permit.
821
+ const coordinated = (operation, effect, onStorageFailure) => admit(operation, staged === undefined
822
+ ? gate.withPermit(Effect.uninterruptible(effect))
823
+ : staged.mutate(operation, (candidate, emit) => Effect.gen(function* () {
824
+ const previous = state;
825
+ const context = contexts.get(candidate);
826
+ if (context === undefined)
827
+ return yield* Effect.die("Missing staged engine state");
828
+ state = candidate;
829
+ activeStage = context;
830
+ const result = yield* Effect.exit(effect);
831
+ state = previous;
832
+ activeStage = undefined;
833
+ if (Exit.isFailure(result))
834
+ return yield* Effect.failCause(result.cause);
835
+ for (const event of context.events)
836
+ emit(event);
837
+ return result.value;
838
+ }), onStorageFailure));
839
+ // Pure observations share the permit without making a candidate or calling the provider.
840
+ const coordinatedRead = (operation, effect) => admit(operation, staged === undefined ? gate.withPermit(effect) : staged.read(operation, () => effect));
841
+ const coordinatedCleanup = (effect) => staged === undefined
842
+ ? gate.withPermit(Effect.uninterruptible(effect))
843
+ : staged.coordinate(Effect.uninterruptible(effect));
844
+ const watchCoordinate = (effect) => staged === undefined ? gate.withPermit(effect) : staged.coordinate(effect);
845
+ const watchHub = yield* WatchHub.make(watchCoordinate, settings.maxWatchEvents ?? 256, () => RescanChange.make({ path: ownedPath(new Uint8Array([47])) }), staged?.checkAvailable("watch"));
846
+ const queuePublish = (publish) => {
847
+ if (activeStage === undefined)
848
+ publish();
849
+ else
850
+ activeStage.events.push(publish);
851
+ };
335
852
  const directoryHex = (directory) => {
336
853
  const names = [];
337
854
  let current = directory;
@@ -343,22 +860,30 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
343
860
  names.push(found[0]);
344
861
  current = parent;
345
862
  }
346
- return "2f" + names.reverse().join("2f");
863
+ return SLASH_HEX + names.reverse().join(SLASH_HEX);
347
864
  };
348
865
  const publishEntry = (_tag, parent, name) => {
349
- watchHub.publishUnsafe(() => {
866
+ queuePublish(() => watchHub.publishUnsafe(() => {
350
867
  const prefix = directoryHex(parent);
351
- return { _tag, path: ownedPath(nameBytes(prefix + (prefix === "2f" ? "" : "2f") + name)) };
352
- });
868
+ return { _tag, path: ownedPath(nameBytes(prefix + (prefix === SLASH_HEX ? "" : SLASH_HEX) + name)) };
869
+ }));
353
870
  };
354
871
  const publishNode = (target) => {
355
- watchHub.publishManyUnsafe(() => {
872
+ // Directories are never hard linked, so one name reaches them and the parent chain resolves it.
873
+ if (target.kind === "directory") {
874
+ // Removal and rename displacement drop the link count while open handles keep reaching the node.
875
+ // No name resolves it any more, so its empty parent chain would otherwise read as the root path.
876
+ // The file scan below stops on the same signal, and `lookup` already reads it as NotFound.
877
+ if (target.metadata.nlink === 0)
878
+ return;
879
+ queuePublish(() => watchHub.publishUnsafe(() => UpdateChange.make({ path: ownedPath(nameBytes(directoryHex(target))) })));
880
+ return;
881
+ }
882
+ queuePublish(() => watchHub.publishManyUnsafe(() => {
356
883
  const changes = [];
357
- if (target === root) {
358
- changes.push({ _tag: "Update", path: ownedPath(new Uint8Array([47])) });
359
- }
360
- const pending = [[root, "2f"]];
361
- while (pending.length > 0) {
884
+ const pending = [[state.root, SLASH_HEX]];
885
+ // nlink counts the names bound to this node, so the scan stops once it has found them all.
886
+ while (pending.length > 0 && changes.length < target.metadata.nlink) {
362
887
  const next = pending.pop();
363
888
  if (next === undefined)
364
889
  break;
@@ -366,20 +891,22 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
366
891
  for (const [name, node] of directory.entries) {
367
892
  const path = prefix + name;
368
893
  if (node === target) {
369
- changes.push({ _tag: "Update", path: ownedPath(nameBytes(path)) });
894
+ changes.push(UpdateChange.make({ path: ownedPath(nameBytes(path)) }));
370
895
  }
371
896
  if (node.kind === "directory")
372
- pending.push([node, path + "2f"]);
897
+ pending.push([node, path + SLASH_HEX]);
373
898
  }
374
899
  }
375
900
  return changes;
376
- });
901
+ }));
377
902
  };
378
903
  const captureSnapshot = Effect.fnUntraced(function* () {
379
- const ids = new Map([[root, "0"]]);
380
- const pending = [root];
904
+ const ids = new Map([[state.root, "0"]]);
905
+ const pending = [state.root];
381
906
  const records = [];
382
907
  for (let index = 0; index < pending.length; index++) {
908
+ if (index % WALK_YIELD_INTERVAL === 0)
909
+ yield* Effect.yieldNow;
383
910
  const node = pending[index];
384
911
  if (node === undefined)
385
912
  continue;
@@ -396,22 +923,25 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
396
923
  ids.set(child, target);
397
924
  pending.push(child);
398
925
  }
399
- children.push({ name: Image.base64(nameBytes(name)), target });
926
+ children.push({ name: CanonicalBase64.encode(nameBytes(name)), target });
400
927
  }
401
- records.push({ id, kind: "directory", metadata, entries: children });
928
+ records.push(Image.Record.cases.directory.make({ id, metadata, entries: children }));
402
929
  }
403
930
  else if (node.kind === "file") {
404
- records.push({ id, kind: "file", metadata, data: Image.base64(node.data.bytes) });
931
+ records.push(Image.Record.cases.file.make({ id, metadata, data: CanonicalBase64.encode(node.data.bytes) }));
932
+ }
933
+ else {
934
+ records.push(Image.Record.cases.symlink.make({ id, metadata, target: CanonicalBase64.encode(node.target) }));
405
935
  }
406
- else
407
- records.push({ id, kind: "symlink", metadata, target: Image.base64(node.target) });
408
936
  }
409
- return yield* Image.capture({ format: "effect-vfs", version: 1, root: "0", records });
937
+ return yield* Image.capture({ format: "effect-vfs", version: 1, root: "0", records }, undefined, true);
410
938
  });
411
- const observeChanges = () => {
939
+ const observeChanges = Effect.fnUntraced(function* () {
412
940
  const observation = [];
413
- const paths = [[root, new Uint8Array([47])]];
941
+ const paths = [[state.root, new Uint8Array([SLASH_BYTE])]];
414
942
  for (let index = 0; index < paths.length; index++) {
943
+ if (index % WALK_YIELD_INTERVAL === 0)
944
+ yield* Effect.yieldNow;
415
945
  const current = paths[index];
416
946
  if (current === undefined)
417
947
  continue;
@@ -431,18 +961,18 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
431
961
  childPath.set(path);
432
962
  let offset = path.length;
433
963
  if (path.length !== 1)
434
- childPath[offset++] = 47;
964
+ childPath[offset++] = SLASH_BYTE;
435
965
  childPath.set(bytes, offset);
436
966
  paths.push([child, childPath]);
437
967
  }
438
968
  }
439
969
  return observation;
440
- };
970
+ });
441
971
  const captureState = Effect.fnUntraced(function* (hook) {
442
972
  const snapshot = yield* captureSnapshot();
443
973
  if (hook !== undefined)
444
974
  yield* hook.betweenSnapshotAndSummary;
445
- return { snapshot, observation: observeChanges() };
975
+ return { snapshot, observation: yield* observeChanges() };
446
976
  });
447
977
  const advanceRevision = (node) => {
448
978
  node.revision = nextRevision();
@@ -451,7 +981,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
451
981
  if (node.objectReference !== undefined)
452
982
  return node.objectReference;
453
983
  const reference = Object.freeze({ [ObjectReferenceId]: true });
454
- objectReferences.set(reference, { volume: volumeIdentity, node });
984
+ objectReferences.set(reference, { volume: volumeIdentity, cell: cellFor(node), active: true });
455
985
  node.objectReference = reference;
456
986
  return reference;
457
987
  };
@@ -460,18 +990,24 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
460
990
  if (reference === undefined)
461
991
  return;
462
992
  const state = objectReferences.get(reference);
463
- if (state !== undefined)
464
- state.node = undefined;
993
+ if (state !== undefined) {
994
+ if (activeStage === undefined) {
995
+ state.active = false;
996
+ state.cell = undefined;
997
+ }
998
+ else
999
+ activeStage.invalidated.add(state);
1000
+ }
465
1001
  node.objectReference = undefined;
466
1002
  };
467
- const release = (reference) => coordinated(Effect.sync(() => {
1003
+ const release = (reference) => coordinatedCleanup(Effect.sync(() => {
468
1004
  reference.directory = undefined;
469
1005
  reference.closed = true;
470
1006
  }));
471
- const authorize = (directory, identity, bits, operation, path) => {
1007
+ const authorize = (node, identity, bits, operation, path) => {
472
1008
  if (identity.privileged)
473
1009
  return Effect.void;
474
- const metadata = directory.metadata;
1010
+ const metadata = node.metadata;
475
1011
  const shift = metadata.uid === identity.uid ?
476
1012
  6
477
1013
  : metadata.gid === identity.gid || identity.groups.includes(metadata.gid)
@@ -479,15 +1015,31 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
479
1015
  : 0;
480
1016
  return ((metadata.mode >> shift) & bits) === bits
481
1017
  ? Effect.void
482
- : Effect.fail(failure("AccessDenied", operation, path));
1018
+ : Effect.fail(path === undefined
1019
+ ? new FsError({ code: "AccessDenied", operation })
1020
+ : new FsError({ code: "AccessDenied", operation, path }));
483
1021
  };
484
1022
  const reclaim = (file) => {
485
1023
  if (file.metadata.nlink === 0 && file.openCount === 0) {
486
- usedBytes -= BigInt(file.data.bytes.length);
1024
+ state.usedBytes -= BigInt(file.data.bytes.length);
487
1025
  file.data = Content.empty();
1026
+ state.retainedFiles.delete(file.metadata.ino);
488
1027
  invalidateReference(file);
489
1028
  }
490
1029
  };
1030
+ // Whether the volume's entry quota leaves room for one more name.
1031
+ const atEntryLimit = () => settings.maxEntries !== undefined && state.entries >= settings.maxEntries;
1032
+ // A new subdirectory's ".." entry is a second link to the parent; other node kinds add none.
1033
+ const attach = (parent, name, node, now) => {
1034
+ parent.entries.set(name, node);
1035
+ parent.metadata = {
1036
+ ...parent.metadata,
1037
+ nlink: parent.metadata.nlink + (node.kind === "directory" ? 1 : 0),
1038
+ mtimeNs: now,
1039
+ ctimeNs: now
1040
+ };
1041
+ advanceRevision(parent);
1042
+ };
491
1043
  const detach = (node, now) => {
492
1044
  if (node.kind === "directory") {
493
1045
  node.parent = undefined;
@@ -498,10 +1050,13 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
498
1050
  else {
499
1051
  node.metadata = { ...node.metadata, nlink: node.metadata.nlink - 1, ctimeNs: now };
500
1052
  advanceRevision(node);
501
- if (node.kind === "file")
1053
+ if (node.kind === "file") {
1054
+ if (node.metadata.nlink === 0 && node.openCount > 0)
1055
+ state.retainedFiles.set(node.metadata.ino, node);
502
1056
  reclaim(node);
1057
+ }
503
1058
  else if (node.metadata.nlink === 0) {
504
- usedBytes -= BigInt(node.target.length);
1059
+ state.usedBytes -= BigInt(node.target.length);
505
1060
  invalidateReference(node);
506
1061
  }
507
1062
  }
@@ -514,43 +1069,58 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
514
1069
  }
515
1070
  ref.closed = true;
516
1071
  };
517
- const resize = Effect.fnUntraced(function* (file, length, operation) {
518
- if (typeof length !== "bigint" || length < 0n)
519
- return yield* failure("InvalidArgument", operation);
520
- if (length > BigInt(maxFileBytes))
521
- return yield* failure("FileTooLarge", operation);
522
- const size = Number(length);
523
- if (settings.maxBytes !== undefined &&
524
- BigInt(size - file.data.bytes.length) > ByteSize.toBigInt(settings.maxBytes) - usedBytes) {
525
- return yield* failure("NoSpace", operation);
526
- }
527
- const data = new Uint8Array(size);
528
- data.set(file.data.bytes.subarray(0, size));
529
- const now = yield* timestamp(operation);
530
- usedBytes += BigInt(size - file.data.bytes.length);
1072
+ const finalizeFile = (ref) => Effect.suspend(() => ref.closed
1073
+ ? Effect.void
1074
+ : ref.file === undefined
1075
+ ? Effect.sync(() => {
1076
+ ref.closed = true;
1077
+ })
1078
+ : coordinated("close", Effect.sync(() => releaseFile(ref)), () => releaseFile(ref)).pipe(Effect.catch(() => coordinatedCleanup(Effect.sync(() => {
1079
+ if (!ref.closed)
1080
+ releaseFile(ref);
1081
+ })))));
1082
+ // Replacing a payload clears setuid and setgid, and charges the volume for the size delta.
1083
+ const replaceContent = (file, data, now, publish = true) => {
1084
+ state.usedBytes += BigInt(data.length - file.data.bytes.length);
531
1085
  file.data = Content.make(data);
532
1086
  file.metadata = {
533
1087
  ...file.metadata,
534
- size: length,
535
- mode: file.metadata.mode & ~0o6000,
1088
+ size: BigInt(data.length),
1089
+ mode: file.metadata.mode & ~SET_ID_BITS,
536
1090
  mtimeNs: now,
537
1091
  ctimeNs: now
538
1092
  };
539
1093
  advanceRevision(file);
540
- publishNode(file);
1094
+ if (publish)
1095
+ publishNode(file);
1096
+ };
1097
+ const resize = Effect.fnUntraced(function* (file, length, operation, publish = true) {
1098
+ if (!Predicate.isBigInt(length) || length < 0n) {
1099
+ return yield* new FsError({ code: "InvalidArgument", operation: operation });
1100
+ }
1101
+ if (length > BigInt(maxFileBytes))
1102
+ return yield* new FsError({ code: "FileTooLarge", operation: operation });
1103
+ const size = Number(length);
1104
+ if (settings.maxBytes !== undefined &&
1105
+ BigInt(size - file.data.bytes.length) > ByteSize.toBigInt(settings.maxBytes) - state.usedBytes) {
1106
+ return yield* new FsError({ code: "NoSpace", operation: operation });
1107
+ }
1108
+ const data = new Uint8Array(size);
1109
+ data.set(file.data.bytes.subarray(0, size));
1110
+ replaceContent(file, data, yield* timestamp(operation), publish);
541
1111
  });
542
1112
  const fileHandle = (ref) => {
543
1113
  const get = (operation, access) => ref.file === undefined || (access === "read" && ref.access === "write") ||
544
1114
  (access === "write" && ref.access === "read")
545
- ? Effect.fail(failure("InvalidHandle", operation))
1115
+ ? Effect.fail(new FsError({ code: "InvalidHandle", operation: operation }))
546
1116
  : Effect.succeed(ref.file);
547
- const read = Effect.fnUntraced(function* (maximum, position) {
1117
+ const read = (maximum, position) => coordinated(position === undefined ? "read" : "pread", Effect.gen(function* () {
548
1118
  const file = yield* get(position === undefined ? "read" : "pread", "read");
549
- if (!Schema.is(Schema.Natural)(maximum))
550
- return yield* failure("InvalidArgument", "read");
1119
+ if (!isNatural(maximum))
1120
+ return yield* new FsError({ code: "InvalidArgument", operation: "read" });
551
1121
  const offset = position ?? ref.offset;
552
- if (typeof offset !== "bigint" || offset < 0n || offset > 0x7fffffffffffffffn) {
553
- return yield* failure("InvalidArgument", "read");
1122
+ if (!Predicate.isBigInt(offset) || offset < 0n || offset > MAX_FILE_OFFSET) {
1123
+ return yield* new FsError({ code: "InvalidArgument", operation: "read" });
554
1124
  }
555
1125
  const start = Number(offset > file.metadata.size ? file.metadata.size : offset);
556
1126
  const data = file.data.bytes.slice(start, start + Math.min(maximum, file.data.bytes.length - start));
@@ -560,32 +1130,31 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
560
1130
  if (position === undefined)
561
1131
  ref.offset += BigInt(data.length);
562
1132
  return data;
563
- }, coordinated);
1133
+ }));
564
1134
  const write = Effect.fnUntraced(function* (input, position) {
565
- if (!(input instanceof Uint8Array) || !(input.buffer instanceof ArrayBuffer) || !attachedBuffer(input)) {
566
- return yield* failure("InvalidArgument", "write");
567
- }
1135
+ if (!isAttachedBytes(input))
1136
+ return yield* new FsError({ code: "InvalidArgument", operation: "write" });
568
1137
  const bytes = new Uint8Array(input);
569
- return yield* coordinated(Effect.gen(function* () {
1138
+ return yield* coordinated(position === undefined ? "write" : "pwrite", Effect.gen(function* () {
570
1139
  const file = yield* get(position === undefined ? "write" : "pwrite", "write");
571
1140
  const offset = position ?? (ref.append ? file.metadata.size : ref.offset);
572
- if (typeof offset !== "bigint" || offset < 0n || offset > 0x7fffffffffffffffn) {
573
- return yield* failure("InvalidArgument", "write");
1141
+ if (!Predicate.isBigInt(offset) || offset < 0n || offset > MAX_FILE_OFFSET) {
1142
+ return yield* new FsError({ code: "InvalidArgument", operation: "write" });
574
1143
  }
575
1144
  if (bytes.length === 0) {
576
1145
  return 0;
577
1146
  }
578
1147
  if (offset >= BigInt(maxFileBytes))
579
- return yield* failure("FileTooLarge", "write");
1148
+ return yield* new FsError({ code: "FileTooLarge", operation: "write" });
580
1149
  const start = Number(offset);
581
1150
  const free = settings.maxBytes === undefined
582
1151
  ? BigInt(maxFileBytes)
583
- : ByteSize.toBigInt(settings.maxBytes) - usedBytes;
1152
+ : ByteSize.toBigInt(settings.maxBytes) - state.usedBytes;
584
1153
  const maximumEnd = BigInt(file.data.bytes.length) + free;
585
1154
  const end = Number(BigInt(maxFileBytes) < maximumEnd ? BigInt(maxFileBytes) : maximumEnd);
586
1155
  const count = Math.min(bytes.length, Math.max(0, end - start));
587
1156
  if (count === 0)
588
- return yield* failure("NoSpace", "write");
1157
+ return yield* new FsError({ code: "NoSpace", operation: "write" });
589
1158
  const size = Math.max(file.data.bytes.length, start + count);
590
1159
  // Always detach before mutation. A same-sized write is the critical
591
1160
  // case: the current payload may belong to the base or a prior capture.
@@ -593,17 +1162,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
593
1162
  data.set(file.data.bytes);
594
1163
  const now = yield* timestamp("write");
595
1164
  data.set(bytes.subarray(0, count), start);
596
- usedBytes += BigInt(size - file.data.bytes.length);
597
- file.data = Content.make(data);
598
- file.metadata = {
599
- ...file.metadata,
600
- size: BigInt(size),
601
- mode: file.metadata.mode & ~0o6000,
602
- mtimeNs: now,
603
- ctimeNs: now
604
- };
605
- advanceRevision(file);
606
- publishNode(file);
1165
+ replaceContent(file, data, now);
607
1166
  if (position === undefined)
608
1167
  ref.offset = offset + BigInt(count);
609
1168
  return count;
@@ -624,37 +1183,45 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
624
1183
  return yield* write(bytes, offset);
625
1184
  }),
626
1185
  seek: Effect.fn("FileHandle.seek")(function* (offset, mode) {
627
- return yield* coordinated(Effect.gen(function* () {
1186
+ return yield* coordinatedRead("seek", Effect.uninterruptible(Effect.gen(function* () {
628
1187
  const file = yield* get("seek");
629
- if (typeof offset !== "bigint" || !Schema.is(SeekMode)(mode)) {
630
- return yield* failure("InvalidArgument", "seek");
1188
+ if (!Predicate.isBigInt(offset) || !isSeekMode(mode)) {
1189
+ return yield* new FsError({ code: "InvalidArgument", operation: "seek" });
1190
+ }
1191
+ let next = mode === "current"
1192
+ ? ref.offset + offset
1193
+ : mode === "end"
1194
+ ? file.metadata.size + offset
1195
+ : offset;
1196
+ if (next < 0n || next > MAX_FILE_OFFSET) {
1197
+ return yield* new FsError({ code: "InvalidArgument", operation: "seek" });
631
1198
  }
632
- let next = mode === "current" ? ref.offset + offset : mode === "end" ? file.metadata.size + offset : offset;
633
- if (next < 0n || next > 0x7fffffffffffffffn)
634
- return yield* failure("InvalidArgument", "seek");
635
1199
  if (mode === "data" || mode === "hole") {
636
1200
  if (offset >= file.metadata.size)
637
- return yield* failure("NoData", "seek");
1201
+ return yield* new FsError({ code: "NoData", operation: "seek" });
638
1202
  if (mode === "hole")
639
1203
  next = file.metadata.size;
640
1204
  }
641
1205
  ref.offset = next;
642
1206
  return next;
643
- }));
1207
+ })));
644
1208
  }),
645
1209
  truncate: Effect.fn("FileHandle.truncate")(function* (length) {
646
- return yield* coordinated(Effect.gen(function* () {
1210
+ return yield* coordinated("truncate", Effect.gen(function* () {
647
1211
  yield* resize(yield* get("truncate", "write"), length, "truncate");
648
1212
  }));
649
1213
  }),
650
- stat: coordinated(Effect.gen(function* () {
1214
+ stat: coordinatedRead("stat", Effect.gen(function* () {
651
1215
  return { ...(yield* get("stat")).metadata };
652
1216
  })).pipe(Effect.withSpan("FileHandle.stat")),
653
- sync: coordinated(Effect.suspend(() => Effect.asVoid(get("sync")))).pipe(Effect.withSpan("FileHandle.sync")),
654
- close: coordinated(Effect.gen(function* () {
1217
+ sync: coordinatedRead("sync", Effect.suspend(() => Effect.asVoid(get("sync")))).pipe(Effect.withSpan("FileHandle.sync")),
1218
+ close: coordinated("close", Effect.gen(function* () {
655
1219
  yield* get("close");
656
1220
  releaseFile(ref);
657
- })).pipe(Effect.withSpan("FileHandle.close"))
1221
+ }), () => releaseFile(ref)).pipe(Effect.catch((error) => coordinatedCleanup(Effect.sync(() => {
1222
+ if (!ref.closed)
1223
+ releaseFile(ref);
1224
+ })).pipe(Effect.andThen(Effect.fail(error)))), Effect.withSpan("FileHandle.close"))
658
1225
  });
659
1226
  files.set(handle, ref);
660
1227
  return handle;
@@ -662,50 +1229,84 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
662
1229
  const createCaller = (reference, identity, umask) => {
663
1230
  const referencedNode = Effect.fnUntraced(function* (target, operation) {
664
1231
  if (reference.directory === undefined)
665
- return yield* failure("ClosedCaller", operation);
666
- if (typeof target !== "object" || target === null)
667
- return yield* failure("InvalidReference", operation);
1232
+ return yield* new FsError({ code: "ClosedCaller", operation: operation });
1233
+ if (!Predicate.isObject(target))
1234
+ return yield* new FsError({ code: "InvalidReference", operation: operation });
668
1235
  const state = objectReferences.get(target);
669
1236
  if (state === undefined)
670
- return yield* failure("InvalidReference", operation);
671
- if (state.volume !== volumeIdentity)
672
- return yield* failure("ForeignReference", operation);
673
- if (state.node === undefined)
674
- return yield* failure("StaleReference", operation);
675
- return state.node;
1237
+ return yield* new FsError({ code: "InvalidReference", operation: operation });
1238
+ if (state.volume !== volumeIdentity) {
1239
+ return yield* new FsError({ code: "ForeignReference", operation: operation });
1240
+ }
1241
+ if (!state.active || activeStage?.invalidated.has(state)) {
1242
+ return yield* new FsError({ code: "StaleReference", operation: operation });
1243
+ }
1244
+ const node = state.cell?.node;
1245
+ if (node === undefined)
1246
+ return yield* new FsError({ code: "StaleReference", operation });
1247
+ return node;
1248
+ });
1249
+ const referencedName = Effect.fnUntraced(function* (input, operation) {
1250
+ if (!isAttachedBytes(input) || input.length === 0 || input.length > MAX_NAME_BYTES || input.includes(0) ||
1251
+ input.includes(SLASH_BYTE))
1252
+ return yield* new FsError({ code: "InvalidArgument", operation });
1253
+ const name = Encoding.encodeHex(new Uint8Array(input));
1254
+ if (isDotComponent(name))
1255
+ return yield* new FsError({ code: "InvalidArgument", operation });
1256
+ return name;
676
1257
  });
677
- const lookup = Effect.fnUntraced(function* (path, base, operation, options = {}) {
1258
+ const referencedDirectory = Effect.fnUntraced(function* (target, operation) {
1259
+ const node = yield* referencedNode(target, operation);
1260
+ if (node.kind !== "directory")
1261
+ return yield* new FsError({ code: "NotDirectory", operation });
1262
+ return node;
1263
+ });
1264
+ const creationTimes = (times, now) => ({
1265
+ atimeNs: times?.access.kind === "value" ? times.access.nanoseconds : now,
1266
+ mtimeNs: times?.modification.kind === "value" ? times.modification.nanoseconds : now
1267
+ });
1268
+ const lookup = Effect.fnUntraced(function* (path, base, operation, options = {}, referencedBase) {
678
1269
  const { followFinalSymlink = true, allowMissing = false, parentOnly = false } = options;
679
- if (reference.directory === undefined)
680
- return yield* failure("ClosedCaller", operation, path.input);
681
- let current = path.absolute ? root : reference.directory;
682
- if (!path.absolute && base !== undefined) {
1270
+ if (reference.directory === undefined) {
1271
+ return yield* new FsError({ code: "ClosedCaller", operation: operation, path: path.input });
1272
+ }
1273
+ let current = path.absolute ? state.root : reference.directory;
1274
+ if (!path.absolute && referencedBase !== undefined) {
1275
+ current = referencedBase;
1276
+ yield* authorize(current, identity, EXECUTE, operation);
1277
+ }
1278
+ else if (!path.absolute && base !== undefined) {
683
1279
  const target = handles.get(base);
684
- if (target === undefined)
685
- return yield* failure("InvalidHandle", operation, path.input);
686
- if (target.volume !== volumeIdentity)
687
- return yield* failure("ForeignHandle", operation, path.input);
688
- if (target.directory === undefined)
689
- return yield* failure("InvalidHandle", operation, path.input);
1280
+ if (target === undefined) {
1281
+ return yield* new FsError({ code: "InvalidHandle", operation: operation, path: path.input });
1282
+ }
1283
+ if (target.volume !== volumeIdentity) {
1284
+ return yield* new FsError({ code: "ForeignHandle", operation: operation, path: path.input });
1285
+ }
1286
+ if (target.directory === undefined) {
1287
+ return yield* new FsError({ code: "InvalidHandle", operation: operation, path: path.input });
1288
+ }
690
1289
  current = target.directory;
691
- yield* authorize(current, identity, 1, operation, path.input);
1290
+ yield* authorize(current, identity, EXECUTE, operation, path.input);
1291
+ }
1292
+ if (current.metadata.nlink === 0) {
1293
+ return yield* new FsError({ code: "NotFound", operation: operation, path: path.input });
692
1294
  }
693
- if (current.metadata.nlink === 0)
694
- return yield* failure("NotFound", operation, path.input);
695
1295
  let work = path;
696
1296
  let parent;
697
1297
  let name;
698
1298
  let traversals = 0;
699
1299
  for (let index = 0; index < work.components.length - (parentOnly ? 1 : 0); index++) {
700
- if (current.kind !== "directory")
701
- return yield* failure("NotDirectory", operation, path.input);
702
- yield* authorize(current, identity, 1, operation, path.input);
1300
+ if (current.kind !== "directory") {
1301
+ return yield* new FsError({ code: "NotDirectory", operation: operation, path: path.input });
1302
+ }
1303
+ yield* authorize(current, identity, EXECUTE, operation, path.input);
703
1304
  const component = work.components[index];
704
1305
  if (component === undefined)
705
1306
  break;
706
- if (component === "2e")
1307
+ if (component === DOT_HEX)
707
1308
  continue;
708
- if (component === "2e2e") {
1309
+ if (component === DOT_DOT_HEX) {
709
1310
  current = current.parent ?? current;
710
1311
  parent = undefined;
711
1312
  name = undefined;
@@ -718,72 +1319,83 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
718
1319
  if (allowMissing && index === work.components.length - 1 && !work.trailingSlash) {
719
1320
  return { node: undefined, parent, name };
720
1321
  }
721
- return yield* failure("NotFound", operation, path.input);
1322
+ return yield* new FsError({ code: "NotFound", operation: operation, path: path.input });
722
1323
  }
723
1324
  if (child.kind === "symlink" && (followFinalSymlink || index < work.components.length - 1 || work.trailingSlash)) {
724
- if (child.target.length === 0)
725
- return yield* failure("NotFound", operation, path.input);
726
- if (++traversals > 40)
727
- return yield* failure("SymlinkLoop", operation, path.input);
1325
+ if (child.target.length === 0) {
1326
+ return yield* new FsError({ code: "NotFound", operation: operation, path: path.input });
1327
+ }
1328
+ if (++traversals > 40) {
1329
+ return yield* new FsError({ code: "SymlinkLoop", operation: operation, path: path.input });
1330
+ }
728
1331
  const suffix = work.suffixes[index] ?? new Uint8Array(0);
729
1332
  if (settings.maxPathBytes !== undefined &&
730
1333
  ByteSize.isGreaterThan(ByteSize.bytes(child.target.length + suffix.length), settings.maxPathBytes)) {
731
- return yield* failure("PathTooLong", operation, path.input);
1334
+ return yield* new FsError({ code: "PathTooLong", operation: operation, path: path.input });
732
1335
  }
733
1336
  const expansion = new Uint8Array(child.target.length + suffix.length);
734
1337
  expansion.set(child.target);
735
1338
  expansion.set(suffix, child.target.length);
736
- work = yield* Effect.fromResult(preparePath(ownedPath(expansion), operation, settings.maxPathBytes));
1339
+ const expanded = preparePath(ownedPath(expansion), operation, settings.maxPathBytes);
1340
+ // The expansion is synthetic: its per-component limits are the caller's to hear about,
1341
+ // but the path in the error has to be the one the caller passed in.
1342
+ if (Result.isFailure(expanded)) {
1343
+ return yield* new FsError({ code: expanded.failure.code, operation: operation, path: path.input });
1344
+ }
1345
+ work = expanded.success;
737
1346
  if (work.absolute)
738
- current = root;
1347
+ current = state.root;
739
1348
  index = -1;
740
1349
  }
741
1350
  else
742
1351
  current = child;
743
1352
  }
744
1353
  if (!parentOnly && work.trailingSlash && current.kind !== "directory") {
745
- return yield* failure("NotDirectory", operation, path.input);
1354
+ return yield* new FsError({ code: "NotDirectory", operation: operation, path: path.input });
746
1355
  }
747
1356
  return { node: current, parent, name };
748
1357
  });
749
1358
  const resolveNode = Effect.fnUntraced(function* (path, base, operation, options) {
750
1359
  const result = yield* lookup(path, base, operation, options);
751
- if (result.node === undefined)
752
- return yield* failure("NotFound", operation, path.input);
1360
+ if (result.node === undefined) {
1361
+ return yield* new FsError({ code: "NotFound", operation: operation, path: path.input });
1362
+ }
753
1363
  return result.node;
754
1364
  });
755
1365
  const locate = Effect.fnUntraced(function* (path, base, operation, options) {
756
1366
  const result = yield* lookup(path, base, operation, options);
757
1367
  const node = result.node;
758
- if (node === undefined)
759
- return yield* failure("NotFound", operation, path.input);
760
- if (node.kind !== "directory")
761
- return yield* failure("NotDirectory", operation, path.input);
1368
+ if (node === undefined) {
1369
+ return yield* new FsError({ code: "NotFound", operation: operation, path: path.input });
1370
+ }
1371
+ if (node.kind !== "directory") {
1372
+ return yield* new FsError({ code: "NotDirectory", operation: operation, path: path.input });
1373
+ }
762
1374
  return node;
763
1375
  });
764
1376
  const acquireDirectory = Effect.fnUntraced(function* (input, options, operation) {
765
1377
  const prepared = preparePath(input, operation, settings.maxPathBytes);
766
1378
  const base = options?.relativeTo;
767
- const acquired = { volume: volumeIdentity, directory: undefined, closed: false };
1379
+ const acquired = makeDirectoryReference();
768
1380
  // Register before retaining a directory. Closed scopes can run this immediately,
769
1381
  // so registration must not happen while holding the volume permit.
770
1382
  yield* Effect.addFinalizer(() => release(acquired));
771
- return yield* coordinated(Effect.gen(function* () {
1383
+ return yield* coordinatedRead(operation, Effect.uninterruptible(Effect.gen(function* () {
772
1384
  if (acquired.closed)
773
1385
  return yield* Effect.interrupt;
774
1386
  const path = yield* Effect.fromResult(prepared);
775
1387
  const directory = yield* locate(path, base, operation);
776
- yield* authorize(directory, identity, 1, operation, input);
1388
+ yield* authorize(directory, identity, EXECUTE, operation, input);
777
1389
  acquired.directory = directory;
778
1390
  return acquired;
779
- }));
1391
+ })));
780
1392
  });
781
1393
  const list = Effect.fnUntraced(function* (input, options) {
782
1394
  const prepared = preparePath(input, "readDirectory", settings.maxPathBytes);
783
1395
  const base = options?.relativeTo;
784
- return yield* coordinated(Effect.gen(function* () {
1396
+ return yield* coordinated("readDirectory", Effect.gen(function* () {
785
1397
  const directory = yield* locate(yield* Effect.fromResult(prepared), base, "readDirectory");
786
- yield* authorize(directory, identity, 4, "readDirectory", input);
1398
+ yield* authorize(directory, identity, READ, "readDirectory", input);
787
1399
  const result = [...directory.entries.keys()].map(nameBytes);
788
1400
  directory.metadata = { ...directory.metadata, atimeNs: (yield* timestamp("readDirectory")) };
789
1401
  return result;
@@ -792,19 +1404,20 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
792
1404
  const readTarget = Effect.fnUntraced(function* (input, options) {
793
1405
  const prepared = preparePath(input, "readLink", settings.maxPathBytes);
794
1406
  const base = options?.relativeTo;
795
- return yield* coordinated(Effect.gen(function* () {
1407
+ return yield* coordinatedRead("readLink", Effect.gen(function* () {
796
1408
  const node = yield* resolveNode(yield* Effect.fromResult(prepared), base, "readLink", {
797
1409
  followFinalSymlink: false
798
1410
  });
799
- if (node.kind !== "symlink")
800
- return yield* failure("InvalidArgument", "readLink", input);
1411
+ if (node.kind !== "symlink") {
1412
+ return yield* new FsError({ code: "InvalidArgument", operation: "readLink", path: input });
1413
+ }
801
1414
  return new Uint8Array(node.target);
802
1415
  }));
803
1416
  });
804
1417
  const canonical = Effect.fnUntraced(function* (input, options) {
805
1418
  const prepared = preparePath(input, "realPath", settings.maxPathBytes);
806
1419
  const base = options?.relativeTo;
807
- return yield* coordinated(Effect.gen(function* () {
1420
+ return yield* coordinatedRead("realPath", Effect.gen(function* () {
808
1421
  const result = yield* lookup(yield* Effect.fromResult(prepared), base, "realPath");
809
1422
  const components = [];
810
1423
  if (result.node?.kind !== "directory" && result.name !== undefined)
@@ -813,26 +1426,29 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
813
1426
  while (directory !== undefined && directory.parent !== undefined) {
814
1427
  const parent = directory.parent;
815
1428
  const entry = [...parent.entries].find(([, child]) => child === directory);
816
- if (entry === undefined)
817
- return yield* failure("NotFound", "realPath", input);
1429
+ if (entry === undefined) {
1430
+ return yield* new FsError({ code: "NotFound", operation: "realPath", path: input });
1431
+ }
818
1432
  components.push(entry[0]);
819
1433
  directory = parent;
820
1434
  }
821
- return nameBytes("2f" + components.reverse().join("2f"));
1435
+ return nameBytes(SLASH_HEX + components.reverse().join(SLASH_HEX));
822
1436
  }));
823
1437
  });
824
1438
  const metadataNode = Effect.fnUntraced(function* (target, options, operation) {
825
- if (reference.directory === undefined)
826
- return yield* failure("ClosedCaller", operation);
827
- if (typeof target === "object" && target !== null && (FileHandleId in target || DirectoryHandleId in target)) {
828
- const ref = FileHandleId in target ? files.get(target) : handles.get(target);
1439
+ if (reference.directory === undefined) {
1440
+ return yield* new FsError({ code: "ClosedCaller", operation: operation });
1441
+ }
1442
+ if (isFileHandle(target) || isDirectoryHandle(target)) {
1443
+ const ref = isFileHandle(target) ? files.get(target) : handles.get(target);
829
1444
  if (ref === undefined)
830
- return yield* failure("InvalidHandle", operation);
831
- if (ref.volume !== volumeIdentity)
832
- return yield* failure("ForeignHandle", operation);
1445
+ return yield* new FsError({ code: "InvalidHandle", operation: operation });
1446
+ if (ref.volume !== volumeIdentity) {
1447
+ return yield* new FsError({ code: "ForeignHandle", operation: operation });
1448
+ }
833
1449
  const node = "file" in ref ? ref.file : ref.directory;
834
1450
  if (node === undefined)
835
- return yield* failure("InvalidHandle", operation);
1451
+ return yield* new FsError({ code: "InvalidHandle", operation: operation });
836
1452
  return node;
837
1453
  }
838
1454
  const path = yield* Effect.fromResult(preparePath(target, operation, settings.maxPathBytes));
@@ -842,16 +1458,18 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
842
1458
  });
843
1459
  const permittedMode = (metadata, mode, operation, path) => {
844
1460
  if (!identity.privileged && identity.uid !== metadata.uid) {
845
- return Effect.fail(failure("AccessDenied", operation, path));
1461
+ return Effect.fail(path === undefined
1462
+ ? new FsError({ code: "AccessDenied", operation })
1463
+ : new FsError({ code: "AccessDenied", operation, path }));
846
1464
  }
847
1465
  const group = identity.gid === metadata.gid || identity.groups.includes(metadata.gid);
848
1466
  return Effect.succeed(!identity.privileged && metadata.kind === "file" && !group ? mode & ~0o2000 : mode);
849
1467
  };
850
1468
  const changeMode = Effect.fnUntraced(function* (target, mode, options) {
851
- if (!Schema.is(Mode)(mode))
852
- return yield* failure("InvalidArgument", "chmod");
1469
+ if (!isMode(mode))
1470
+ return yield* new FsError({ code: "InvalidArgument", operation: "chmod" });
853
1471
  const chosen = options === undefined ? undefined : { ...options };
854
- return yield* coordinated(Effect.gen(function* () {
1472
+ return yield* coordinated("chmod", Effect.gen(function* () {
855
1473
  const node = yield* metadataNode(target, chosen, "chmod");
856
1474
  const permitted = yield* permittedMode(node.metadata, mode, "chmod");
857
1475
  node.metadata = {
@@ -864,17 +1482,17 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
864
1482
  }));
865
1483
  });
866
1484
  const changeOwner = Effect.fnUntraced(function* (target, owner, options) {
867
- const decoded = Schema.decodeResult(OwnerUpdate, { onExcessProperty: "error" })(owner);
1485
+ const decoded = decodeOwnerUpdate(owner);
868
1486
  if (Result.isFailure(decoded))
869
- return yield* failure("InvalidArgument", "chown");
1487
+ return yield* new FsError({ code: "InvalidArgument", operation: "chown" });
870
1488
  const update = { ...decoded.success };
871
1489
  const chosen = options === undefined ? undefined : { ...options };
872
- return yield* coordinated(Effect.gen(function* () {
1490
+ return yield* coordinated("chown", Effect.gen(function* () {
873
1491
  const node = yield* metadataNode(target, chosen, "chown");
874
1492
  if (!identity.privileged && (identity.uid !== node.metadata.uid ||
875
1493
  (update.uid !== undefined && update.uid !== node.metadata.uid) ||
876
1494
  (update.gid !== undefined && update.gid !== identity.gid && !identity.groups.includes(update.gid)))) {
877
- return yield* failure("AccessDenied", "chown");
1495
+ return yield* new FsError({ code: "AccessDenied", operation: "chown" });
878
1496
  }
879
1497
  if (update.uid === undefined && update.gid === undefined)
880
1498
  return;
@@ -882,7 +1500,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
882
1500
  ...node.metadata,
883
1501
  uid: update.uid ?? node.metadata.uid,
884
1502
  gid: update.gid ?? node.metadata.gid,
885
- mode: node.kind === "file" ? node.metadata.mode & ~0o6000 : node.metadata.mode,
1503
+ mode: node.kind === "file" ? node.metadata.mode & ~SET_ID_BITS : node.metadata.mode,
886
1504
  ctimeNs: (yield* timestamp("chown"))
887
1505
  };
888
1506
  advanceRevision(node);
@@ -890,20 +1508,26 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
890
1508
  }));
891
1509
  });
892
1510
  const changeTimes = Effect.fnUntraced(function* (target, times, options) {
893
- const decoded = Schema.decodeResult(Times, { onExcessProperty: "error" })(times);
1511
+ const decoded = decodeTimes(times);
894
1512
  if (Result.isFailure(decoded))
895
- return yield* failure("InvalidArgument", "utimes");
1513
+ return yield* new FsError({ code: "InvalidArgument", operation: "utimes" });
896
1514
  const access = { ...decoded.success.access };
897
1515
  const modification = { ...decoded.success.modification };
898
1516
  const chosen = options === undefined ? undefined : { ...options };
899
- return yield* coordinated(Effect.gen(function* () {
1517
+ return yield* coordinated("utimes", Effect.gen(function* () {
900
1518
  const node = yield* metadataNode(target, chosen, "utimes");
901
1519
  if (access.kind === "omit" && modification.kind === "omit")
902
1520
  return;
1521
+ // POSIX grants write access only when both times are UTIME_NOW; both UTIME_OMIT
1522
+ // returned above. Every other combination, mixed ones included, needs ownership.
903
1523
  if (!identity.privileged && identity.uid !== node.metadata.uid) {
904
- if (access.kind !== "now" || modification.kind !== "now")
905
- return yield* failure("AccessDenied", "utimes");
906
- yield* authorize(node, identity, 2, "utimes", "/");
1524
+ if (access.kind !== "now" || modification.kind !== "now") {
1525
+ const path = pathOf(target);
1526
+ return yield* path === undefined
1527
+ ? new FsError({ code: "AccessDenied", operation: "utimes" })
1528
+ : new FsError({ code: "AccessDenied", operation: "utimes", path });
1529
+ }
1530
+ yield* authorize(node, identity, WRITE, "utimes", pathOf(target));
907
1531
  }
908
1532
  const now = yield* timestamp("utimes");
909
1533
  node.metadata = {
@@ -924,100 +1548,652 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
924
1548
  publishNode(node);
925
1549
  }));
926
1550
  });
927
- const authorizeRemoval = (parent, child, operation, input) => (parent.metadata.mode & 0o1000) !== 0 && !identity.privileged &&
1551
+ const authorizeRemoval = (parent, child, operation, input) => (parent.metadata.mode & STICKY_BIT) !== 0 && !identity.privileged &&
928
1552
  identity.uid !== parent.metadata.uid && identity.uid !== child.metadata.uid
929
- ? Effect.fail(failure("AccessDenied", operation, input))
1553
+ ? Effect.fail(input === undefined
1554
+ ? new FsError({ code: "AccessDenied", operation })
1555
+ : new FsError({ code: "AccessDenied", operation, path: input }))
930
1556
  : Effect.void;
931
1557
  return Object.freeze({
932
1558
  [CallerId]: true,
933
- rootReference: coordinated(Effect.gen(function* () {
934
- if (reference.directory === undefined)
935
- return yield* failure("ClosedCaller", "rootReference");
936
- return referenceFor(root);
1559
+ rootReference: coordinatedRead("rootReference", Effect.gen(function* () {
1560
+ if (reference.directory === undefined) {
1561
+ return yield* new FsError({ code: "ClosedCaller", operation: "rootReference" });
1562
+ }
1563
+ return referenceFor(state.root);
937
1564
  })).pipe(Effect.withSpan("Caller.rootReference")),
938
1565
  lookupReference: Effect.fn("Caller.lookupReference")(function* (directoryReference, name) {
939
- if (!(name instanceof Uint8Array) || !(name.buffer instanceof ArrayBuffer) || !attachedBuffer(name) ||
940
- name.length === 0 || name.length > 255 || name.includes(0) || name.includes(47))
941
- return yield* failure("InvalidArgument", "lookupReference");
1566
+ if (!isAttachedBytes(name) || name.length === 0 || name.length > MAX_NAME_BYTES || name.includes(0) ||
1567
+ name.includes(SLASH_BYTE))
1568
+ return yield* new FsError({ code: "InvalidArgument", operation: "lookupReference" });
942
1569
  const key = Encoding.encodeHex(new Uint8Array(name));
943
- if (key === "2e" || key === "2e2e")
944
- return yield* failure("InvalidArgument", "lookupReference");
945
- return yield* coordinated(Effect.gen(function* () {
1570
+ if (isDotComponent(key))
1571
+ return yield* new FsError({ code: "InvalidArgument", operation: "lookupReference" });
1572
+ return yield* coordinatedRead("lookupReference", Effect.gen(function* () {
946
1573
  const directory = yield* referencedNode(directoryReference, "lookupReference");
947
- if (directory.kind !== "directory")
948
- return yield* failure("NotDirectory", "lookupReference");
949
- yield* authorize(directory, identity, 1, "lookupReference", "/");
1574
+ if (directory.kind !== "directory") {
1575
+ return yield* new FsError({ code: "NotDirectory", operation: "lookupReference" });
1576
+ }
1577
+ yield* authorize(directory, identity, EXECUTE, "lookupReference", "/");
950
1578
  const child = directory.entries.get(key);
951
1579
  if (child === undefined)
952
- return yield* failure("NotFound", "lookupReference");
1580
+ return yield* new FsError({ code: "NotFound", operation: "lookupReference" });
953
1581
  return referenceFor(child);
954
1582
  }));
955
1583
  }),
956
1584
  parentReference: Effect.fn("Caller.parentReference")(function* (directoryReference) {
957
- return yield* coordinated(Effect.gen(function* () {
1585
+ return yield* coordinatedRead("parentReference", Effect.gen(function* () {
958
1586
  const directory = yield* referencedNode(directoryReference, "parentReference");
959
- if (directory.kind !== "directory")
960
- return yield* failure("NotDirectory", "parentReference");
961
- yield* authorize(directory, identity, 1, "parentReference", "/");
1587
+ if (directory.kind !== "directory") {
1588
+ return yield* new FsError({ code: "NotDirectory", operation: "parentReference" });
1589
+ }
1590
+ yield* authorize(directory, identity, EXECUTE, "parentReference", "/");
962
1591
  return referenceFor(directory.parent ?? directory);
963
1592
  }));
964
1593
  }),
965
1594
  observeMetadata: Effect.fn("Caller.observeMetadata")(function* (objectReference) {
966
- return yield* coordinated(Effect.gen(function* () {
1595
+ return yield* coordinatedRead("observeMetadata", Effect.gen(function* () {
967
1596
  const node = yield* referencedNode(objectReference, "observeMetadata");
968
1597
  return Object.freeze({ value: Object.freeze({ ...node.metadata }), revision: node.revision });
969
1598
  }));
970
1599
  }),
1600
+ accessReference: Effect.fn("Caller.accessReference")(function* (objectReference, bits = 0) {
1601
+ if (!Number.isInteger(bits) || bits < 0 || bits > (READ | WRITE | EXECUTE)) {
1602
+ return yield* new FsError({ code: "InvalidArgument", operation: "accessReference" });
1603
+ }
1604
+ return yield* coordinatedRead("accessReference", Effect.gen(function* () {
1605
+ const node = yield* referencedNode(objectReference, "accessReference");
1606
+ if (node.kind === "file" && (bits & EXECUTE) !== 0 && (node.metadata.mode & ANY_EXECUTE) === 0) {
1607
+ return yield* new FsError({ code: "AccessDenied", operation: "accessReference" });
1608
+ }
1609
+ yield* authorize(node, identity, bits, "accessReference");
1610
+ }));
1611
+ }),
971
1612
  observeDirectory: Effect.fn("Caller.observeDirectory")(function* (directoryReference) {
972
- return yield* coordinated(Effect.gen(function* () {
1613
+ return yield* coordinatedRead("observeDirectory", Effect.gen(function* () {
973
1614
  const directory = yield* referencedNode(directoryReference, "observeDirectory");
974
- if (directory.kind !== "directory")
975
- return yield* failure("NotDirectory", "observeDirectory");
976
- yield* authorize(directory, identity, 4, "observeDirectory", "/");
1615
+ if (directory.kind !== "directory") {
1616
+ return yield* new FsError({ code: "NotDirectory", operation: "observeDirectory" });
1617
+ }
1618
+ yield* authorize(directory, identity, READ, "observeDirectory", "/");
977
1619
  const value = Object.freeze([...directory.entries].map(([name, node]) => Object.freeze({ name: nameBytes(name), reference: referenceFor(node) })));
978
1620
  return Object.freeze({ value, revision: directory.revision });
979
1621
  }));
980
1622
  }),
981
1623
  readLinkReference: Effect.fn("Caller.readLinkReference")(function* (objectReference) {
982
- return yield* coordinated(Effect.gen(function* () {
1624
+ return yield* coordinatedRead("readLinkReference", Effect.gen(function* () {
983
1625
  const node = yield* referencedNode(objectReference, "readLinkReference");
984
- if (node.kind !== "symlink")
985
- return yield* failure("InvalidArgument", "readLinkReference");
1626
+ if (node.kind !== "symlink") {
1627
+ return yield* new FsError({ code: "InvalidArgument", operation: "readLinkReference" });
1628
+ }
986
1629
  return new Uint8Array(node.target);
987
1630
  }));
988
1631
  }),
989
- openReference: Effect.fn("Caller.openReference")(function* (objectReference) {
990
- const acquired = {
991
- volume: volumeIdentity,
992
- file: undefined,
993
- closed: false,
994
- offset: 0n,
995
- access: "read",
996
- append: false
997
- };
998
- yield* Effect.addFinalizer(() => coordinated(Effect.sync(() => releaseFile(acquired))));
999
- return yield* coordinated(Effect.gen(function* () {
1632
+ mkdirReference: Effect.fn("Caller.mkdirReference")(function* (directoryReference, input, raw = {}) {
1633
+ const name = yield* referencedName(input, "mkdirReference");
1634
+ const decoded = decodeMkdirReferenceSettings(raw);
1635
+ if (Result.isFailure(decoded)) {
1636
+ return yield* new FsError({ code: "InvalidArgument", operation: "mkdirReference" });
1637
+ }
1638
+ const chosen = { ...decoded.success };
1639
+ if (chosen.exactMode && chosen.mode === undefined) {
1640
+ return yield* new FsError({ code: "InvalidArgument", operation: "mkdirReference" });
1641
+ }
1642
+ const mode = chosen.mode ?? 0o777;
1643
+ return yield* coordinated("mkdirReference", Effect.gen(function* () {
1644
+ const parent = yield* referencedDirectory(directoryReference, "mkdirReference");
1645
+ yield* authorize(parent, identity, WRITE | EXECUTE, "mkdirReference");
1646
+ if (parent.entries.has(name)) {
1647
+ return yield* new FsError({ code: "AlreadyExists", operation: "mkdirReference" });
1648
+ }
1649
+ if (atEntryLimit())
1650
+ return yield* new FsError({ code: "NoSpace", operation: "mkdirReference" });
1651
+ const before = parent.revision;
1652
+ const now = yield* timestamp("mkdirReference");
1653
+ const initial = creationTimes(chosen.times, now);
1654
+ const creationMode = chosen.exactMode
1655
+ ? yield* permittedMode({ kind: "directory", uid: identity.uid, gid: parent.metadata.gid }, mode, "mkdirReference")
1656
+ : (mode & 0o777 & ~umask) | (mode & STICKY_BIT);
1657
+ const child = {
1658
+ kind: "directory",
1659
+ lineage: undefined,
1660
+ parent,
1661
+ entries: new Map(),
1662
+ metadata: {
1663
+ ...directoryMetadata(state.nextInode, identity.uid, parent.metadata.gid, creationMode, now),
1664
+ ...initial
1665
+ },
1666
+ revision: nextRevision(),
1667
+ objectReference: undefined
1668
+ };
1669
+ attach(parent, name, child, now);
1670
+ state.nextInode += 1n;
1671
+ state.entries += 1;
1672
+ publishEntry("Create", parent, name);
1673
+ return { reference: referenceFor(child), directory: { before, after: parent.revision } };
1674
+ }));
1675
+ }),
1676
+ symlinkReference: Effect.fn("Caller.symlinkReference")(function* (target, directoryReference, input, raw = {}) {
1677
+ const name = yield* referencedName(input, "symlinkReference");
1678
+ const decoded = decodeSymlinkReferenceSettings(raw);
1679
+ if (Result.isFailure(decoded)) {
1680
+ return yield* new FsError({ code: "InvalidArgument", operation: "symlinkReference" });
1681
+ }
1682
+ const rawTarget = inputBytes(target);
1683
+ if (Result.isFailure(rawTarget)) {
1684
+ return yield* new FsError({ code: rawTarget.failure, operation: "symlinkReference" });
1685
+ }
1686
+ if (rawTarget.success.includes(0)) {
1687
+ return yield* new FsError({ code: "InvalidArgument", operation: "symlinkReference" });
1688
+ }
1689
+ const targetBytes = new Uint8Array(rawTarget.success);
1690
+ const chosen = { ...decoded.success };
1691
+ return yield* coordinated("symlinkReference", Effect.gen(function* () {
1692
+ const parent = yield* referencedDirectory(directoryReference, "symlinkReference");
1693
+ yield* authorize(parent, identity, WRITE | EXECUTE, "symlinkReference");
1694
+ if (parent.entries.has(name)) {
1695
+ return yield* new FsError({ code: "AlreadyExists", operation: "symlinkReference" });
1696
+ }
1697
+ if (atEntryLimit() ||
1698
+ (settings.maxBytes !== undefined &&
1699
+ BigInt(targetBytes.length) > ByteSize.toBigInt(settings.maxBytes) - state.usedBytes))
1700
+ return yield* new FsError({ code: "NoSpace", operation: "symlinkReference" });
1701
+ const before = parent.revision;
1702
+ const now = yield* timestamp("symlinkReference");
1703
+ const initial = creationTimes(chosen.times, now);
1704
+ const node = {
1705
+ kind: "symlink",
1706
+ lineage: undefined,
1707
+ target: targetBytes,
1708
+ metadata: {
1709
+ ...directoryMetadata(state.nextInode, identity.uid, parent.metadata.gid, 0o777, now),
1710
+ ...initial,
1711
+ kind: "symlink",
1712
+ nlink: 1,
1713
+ size: BigInt(targetBytes.length)
1714
+ },
1715
+ revision: nextRevision(),
1716
+ objectReference: undefined
1717
+ };
1718
+ attach(parent, name, node, now);
1719
+ state.nextInode += 1n;
1720
+ state.entries += 1;
1721
+ state.usedBytes += BigInt(targetBytes.length);
1722
+ publishEntry("Create", parent, name);
1723
+ return { reference: referenceFor(node), directory: { before, after: parent.revision } };
1724
+ }));
1725
+ }),
1726
+ linkReference: Effect.fn("Caller.linkReference")(function* (sourceReference, directoryReference, input) {
1727
+ const name = yield* referencedName(input, "linkReference");
1728
+ return yield* coordinated("linkReference", Effect.gen(function* () {
1729
+ const node = yield* referencedNode(sourceReference, "linkReference");
1730
+ if (node.kind === "directory") {
1731
+ return yield* new FsError({ code: "IsDirectory", operation: "linkReference" });
1732
+ }
1733
+ if (node.metadata.nlink === 0) {
1734
+ return yield* new FsError({ code: "StaleReference", operation: "linkReference" });
1735
+ }
1736
+ const parent = yield* referencedDirectory(directoryReference, "linkReference");
1737
+ yield* authorize(parent, identity, WRITE | EXECUTE, "linkReference");
1738
+ if (parent.entries.has(name)) {
1739
+ return yield* new FsError({ code: "AlreadyExists", operation: "linkReference" });
1740
+ }
1741
+ if (atEntryLimit())
1742
+ return yield* new FsError({ code: "NoSpace", operation: "linkReference" });
1743
+ const before = parent.revision;
1744
+ const now = yield* timestamp("linkReference");
1745
+ attach(parent, name, node, now);
1746
+ node.metadata = { ...node.metadata, nlink: node.metadata.nlink + 1, ctimeNs: now };
1747
+ advanceRevision(node);
1748
+ state.entries += 1;
1749
+ publishEntry("Create", parent, name);
1750
+ return { reference: referenceFor(node), directory: { before, after: parent.revision } };
1751
+ }));
1752
+ }),
1753
+ unlinkReference: Effect.fn("Caller.unlinkReference")(function* (directoryReference, input) {
1754
+ const name = yield* referencedName(input, "unlinkReference");
1755
+ return yield* coordinated("unlinkReference", Effect.gen(function* () {
1756
+ const parent = yield* referencedDirectory(directoryReference, "unlinkReference");
1757
+ yield* authorize(parent, identity, WRITE | EXECUTE, "unlinkReference");
1758
+ const child = parent.entries.get(name);
1759
+ if (child === undefined)
1760
+ return yield* new FsError({ code: "NotFound", operation: "unlinkReference" });
1761
+ if (child.kind === "directory") {
1762
+ return yield* new FsError({ code: "IsDirectory", operation: "unlinkReference" });
1763
+ }
1764
+ yield* authorizeRemoval(parent, child, "unlinkReference");
1765
+ const before = parent.revision;
1766
+ const now = yield* timestamp("unlinkReference");
1767
+ parent.entries.delete(name);
1768
+ parent.metadata = { ...parent.metadata, mtimeNs: now, ctimeNs: now };
1769
+ advanceRevision(parent);
1770
+ detach(child, now);
1771
+ state.entries -= 1;
1772
+ publishEntry("Remove", parent, name);
1773
+ return { before, after: parent.revision };
1774
+ }));
1775
+ }),
1776
+ rmdirReference: Effect.fn("Caller.rmdirReference")(function* (directoryReference, input) {
1777
+ const name = yield* referencedName(input, "rmdirReference");
1778
+ return yield* coordinated("rmdirReference", Effect.gen(function* () {
1779
+ const parent = yield* referencedDirectory(directoryReference, "rmdirReference");
1780
+ yield* authorize(parent, identity, WRITE | EXECUTE, "rmdirReference");
1781
+ const child = parent.entries.get(name);
1782
+ if (child === undefined)
1783
+ return yield* new FsError({ code: "NotFound", operation: "rmdirReference" });
1784
+ yield* authorizeRemoval(parent, child, "rmdirReference");
1785
+ if (child.kind !== "directory") {
1786
+ return yield* new FsError({ code: "NotDirectory", operation: "rmdirReference" });
1787
+ }
1788
+ if (child.entries.size > 0) {
1789
+ return yield* new FsError({ code: "NotEmpty", operation: "rmdirReference" });
1790
+ }
1791
+ const before = parent.revision;
1792
+ const now = yield* timestamp("rmdirReference");
1793
+ parent.entries.delete(name);
1794
+ parent.metadata = { ...parent.metadata, nlink: parent.metadata.nlink - 1, mtimeNs: now, ctimeNs: now };
1795
+ child.parent = undefined;
1796
+ child.metadata = { ...child.metadata, nlink: 0, ctimeNs: now };
1797
+ advanceRevision(parent);
1798
+ advanceRevision(child);
1799
+ invalidateReference(child);
1800
+ state.entries -= 1;
1801
+ publishEntry("Remove", parent, name);
1802
+ return { before, after: parent.revision };
1803
+ }));
1804
+ }),
1805
+ removeReference: Effect.fn("Caller.removeReference")(function* (directoryReference, input) {
1806
+ const name = yield* referencedName(input, "removeReference");
1807
+ return yield* coordinated("removeReference", Effect.gen(function* () {
1808
+ const parent = yield* referencedDirectory(directoryReference, "removeReference");
1809
+ yield* authorize(parent, identity, WRITE | EXECUTE, "removeReference");
1810
+ const child = parent.entries.get(name);
1811
+ if (child === undefined)
1812
+ return yield* new FsError({ code: "NotFound", operation: "removeReference" });
1813
+ yield* authorizeRemoval(parent, child, "removeReference");
1814
+ if (child.kind === "directory" && child.entries.size > 0) {
1815
+ return yield* new FsError({ code: "NotEmpty", operation: "removeReference" });
1816
+ }
1817
+ const before = parent.revision;
1818
+ const now = yield* timestamp("removeReference");
1819
+ parent.entries.delete(name);
1820
+ parent.metadata = {
1821
+ ...parent.metadata,
1822
+ nlink: parent.metadata.nlink - (child.kind === "directory" ? 1 : 0),
1823
+ mtimeNs: now,
1824
+ ctimeNs: now
1825
+ };
1826
+ if (child.kind === "directory") {
1827
+ child.parent = undefined;
1828
+ child.metadata = { ...child.metadata, nlink: 0, ctimeNs: now };
1829
+ advanceRevision(parent);
1830
+ advanceRevision(child);
1831
+ invalidateReference(child);
1832
+ }
1833
+ else {
1834
+ advanceRevision(parent);
1835
+ detach(child, now);
1836
+ }
1837
+ state.entries -= 1;
1838
+ publishEntry("Remove", parent, name);
1839
+ return { before, after: parent.revision };
1840
+ }));
1841
+ }),
1842
+ renameReference: Effect.fn("Caller.renameReference")(function* (sourceDirectoryReference, sourceInput, destinationDirectoryReference, destinationInput) {
1843
+ const sourceName = yield* referencedName(sourceInput, "renameReference");
1844
+ const destinationName = yield* referencedName(destinationInput, "renameReference");
1845
+ return yield* coordinated("renameReference", Effect.gen(function* () {
1846
+ const sourceDirectory = yield* referencedDirectory(sourceDirectoryReference, "renameReference");
1847
+ const destinationDirectory = yield* referencedDirectory(destinationDirectoryReference, "renameReference");
1848
+ yield* authorize(sourceDirectory, identity, WRITE | EXECUTE, "renameReference");
1849
+ yield* authorize(destinationDirectory, identity, WRITE | EXECUTE, "renameReference");
1850
+ const sourceBefore = sourceDirectory.revision;
1851
+ const destinationBefore = destinationDirectory.revision;
1852
+ const child = sourceDirectory.entries.get(sourceName);
1853
+ if (child === undefined) {
1854
+ return yield* new FsError({ code: "NotFound", operation: "renameReference" });
1855
+ }
1856
+ const replaced = destinationDirectory.entries.get(destinationName);
1857
+ if (child === replaced) {
1858
+ return sourceDirectory === destinationDirectory
1859
+ ? { _tag: "SameDirectory", directory: { before: sourceBefore, after: sourceBefore } }
1860
+ : {
1861
+ _tag: "DifferentDirectories",
1862
+ sourceDirectory: { before: sourceBefore, after: sourceBefore },
1863
+ destinationDirectory: { before: destinationBefore, after: destinationBefore }
1864
+ };
1865
+ }
1866
+ yield* authorizeRemoval(sourceDirectory, child, "renameReference");
1867
+ if (replaced !== undefined) {
1868
+ yield* authorizeRemoval(destinationDirectory, replaced, "renameReference");
1869
+ if (child.kind === "directory" && replaced.kind !== "directory") {
1870
+ return yield* new FsError({ code: "NotDirectory", operation: "renameReference" });
1871
+ }
1872
+ if (child.kind !== "directory" && replaced.kind === "directory") {
1873
+ return yield* new FsError({ code: "IsDirectory", operation: "renameReference" });
1874
+ }
1875
+ if (replaced.kind === "directory" && replaced.entries.size > 0) {
1876
+ return yield* new FsError({ code: "NotEmpty", operation: "renameReference" });
1877
+ }
1878
+ }
1879
+ for (let ancestor = destinationDirectory; ancestor !== undefined; ancestor = ancestor.parent) {
1880
+ if (ancestor === child) {
1881
+ return yield* new FsError({ code: "InvalidArgument", operation: "renameReference" });
1882
+ }
1883
+ }
1884
+ const now = yield* timestamp("renameReference");
1885
+ const oldEvent = () => ownedPath(nameBytes(directoryHex(sourceDirectory) + (sourceDirectory === state.root ? "" : SLASH_HEX) + sourceName));
1886
+ sourceDirectory.entries.delete(sourceName);
1887
+ destinationDirectory.entries.set(destinationName, child);
1888
+ if (child.kind === "directory")
1889
+ child.parent = destinationDirectory;
1890
+ sourceDirectory.metadata = {
1891
+ ...sourceDirectory.metadata,
1892
+ nlink: sourceDirectory.metadata.nlink - (child.kind === "directory" ? 1 : 0),
1893
+ mtimeNs: now,
1894
+ ctimeNs: now
1895
+ };
1896
+ destinationDirectory.metadata = {
1897
+ ...destinationDirectory.metadata,
1898
+ nlink: destinationDirectory.metadata.nlink +
1899
+ (child.kind === "directory" && replaced === undefined ? 1 : 0),
1900
+ mtimeNs: now,
1901
+ ctimeNs: now
1902
+ };
1903
+ child.metadata = { ...child.metadata, ctimeNs: now };
1904
+ advanceRevision(sourceDirectory);
1905
+ if (destinationDirectory !== sourceDirectory)
1906
+ advanceRevision(destinationDirectory);
1907
+ advanceRevision(child);
1908
+ if (replaced !== undefined) {
1909
+ detach(replaced, now);
1910
+ state.entries -= 1;
1911
+ }
1912
+ queuePublish(() => watchHub.publishUnsafe(() => ({ _tag: "Remove", path: oldEvent() })));
1913
+ publishEntry("Create", destinationDirectory, destinationName);
1914
+ return sourceDirectory === destinationDirectory
1915
+ ? {
1916
+ _tag: "SameDirectory",
1917
+ directory: { before: sourceBefore, after: sourceDirectory.revision }
1918
+ }
1919
+ : {
1920
+ _tag: "DifferentDirectories",
1921
+ sourceDirectory: { before: sourceBefore, after: sourceDirectory.revision },
1922
+ destinationDirectory: { before: destinationBefore, after: destinationDirectory.revision }
1923
+ };
1924
+ }));
1925
+ }),
1926
+ chmodReference: Effect.fn("Caller.chmodReference")(function* (objectReference, mode) {
1927
+ if (!isMode(mode))
1928
+ return yield* new FsError({ code: "InvalidArgument", operation: "chmodReference" });
1929
+ return yield* coordinated("chmodReference", Effect.gen(function* () {
1930
+ const node = yield* referencedNode(objectReference, "chmodReference");
1931
+ const permitted = yield* permittedMode(node.metadata, mode, "chmodReference");
1932
+ node.metadata = { ...node.metadata, mode: permitted, ctimeNs: (yield* timestamp("chmodReference")) };
1933
+ advanceRevision(node);
1934
+ publishNode(node);
1935
+ }));
1936
+ }),
1937
+ chownReference: Effect.fn("Caller.chownReference")(function* (objectReference, owner) {
1938
+ const decoded = decodeOwnerUpdate(owner);
1939
+ if (Result.isFailure(decoded)) {
1940
+ return yield* new FsError({ code: "InvalidArgument", operation: "chownReference" });
1941
+ }
1942
+ const update = { ...decoded.success };
1943
+ return yield* coordinated("chownReference", Effect.gen(function* () {
1944
+ const node = yield* referencedNode(objectReference, "chownReference");
1945
+ if (!identity.privileged && (identity.uid !== node.metadata.uid ||
1946
+ (update.uid !== undefined && update.uid !== node.metadata.uid) ||
1947
+ (update.gid !== undefined && update.gid !== identity.gid && !identity.groups.includes(update.gid))))
1948
+ return yield* new FsError({ code: "AccessDenied", operation: "chownReference" });
1949
+ if (update.uid === undefined && update.gid === undefined)
1950
+ return;
1951
+ node.metadata = {
1952
+ ...node.metadata,
1953
+ uid: update.uid ?? node.metadata.uid,
1954
+ gid: update.gid ?? node.metadata.gid,
1955
+ mode: node.kind === "file" ? node.metadata.mode & ~SET_ID_BITS : node.metadata.mode,
1956
+ ctimeNs: (yield* timestamp("chownReference"))
1957
+ };
1958
+ advanceRevision(node);
1959
+ publishNode(node);
1960
+ }));
1961
+ }),
1962
+ utimesReference: Effect.fn("Caller.utimesReference")(function* (objectReference, times) {
1963
+ const decoded = decodeTimes(times);
1964
+ if (Result.isFailure(decoded)) {
1965
+ return yield* new FsError({ code: "InvalidArgument", operation: "utimesReference" });
1966
+ }
1967
+ const access = { ...decoded.success.access };
1968
+ const modification = { ...decoded.success.modification };
1969
+ return yield* coordinated("utimesReference", Effect.gen(function* () {
1970
+ const node = yield* referencedNode(objectReference, "utimesReference");
1971
+ if (access.kind === "omit" && modification.kind === "omit")
1972
+ return;
1973
+ if (!identity.privileged && identity.uid !== node.metadata.uid) {
1974
+ if (access.kind !== "now" || modification.kind !== "now") {
1975
+ return yield* new FsError({ code: "AccessDenied", operation: "utimesReference" });
1976
+ }
1977
+ yield* authorize(node, identity, WRITE, "utimesReference");
1978
+ }
1979
+ const now = yield* timestamp("utimesReference");
1980
+ node.metadata = {
1981
+ ...node.metadata,
1982
+ atimeNs: access.kind === "omit"
1983
+ ? node.metadata.atimeNs
1984
+ : access.kind === "now"
1985
+ ? now
1986
+ : access.nanoseconds,
1987
+ mtimeNs: modification.kind === "omit"
1988
+ ? node.metadata.mtimeNs
1989
+ : modification.kind === "now"
1990
+ ? now
1991
+ : modification.nanoseconds,
1992
+ ctimeNs: now
1993
+ };
1994
+ advanceRevision(node);
1995
+ publishNode(node);
1996
+ }));
1997
+ }),
1998
+ truncateReference: Effect.fn("Caller.truncateReference")(function* (objectReference, length) {
1999
+ return yield* coordinated("truncateReference", Effect.gen(function* () {
2000
+ const node = yield* referencedNode(objectReference, "truncateReference");
2001
+ if (node.kind !== "file") {
2002
+ return yield* new FsError({ code: "IsDirectory", operation: "truncateReference" });
2003
+ }
2004
+ yield* authorize(node, identity, WRITE, "truncateReference");
2005
+ yield* resize(node, length, "truncateReference");
2006
+ }));
2007
+ }),
2008
+ openReference: Effect.fn("Caller.openReference")(function* (objectReference, raw = { access: "read" }) {
2009
+ const decoded = decodeOpenReferenceSettings(raw);
2010
+ if (Result.isFailure(decoded)) {
2011
+ return yield* new FsError({ code: "InvalidArgument", operation: "openReference" });
2012
+ }
2013
+ const chosen = { ...decoded.success };
2014
+ if (chosen.access === "read" && (chosen.append || chosen.truncate)) {
2015
+ return yield* new FsError({ code: "InvalidArgument", operation: "openReference" });
2016
+ }
2017
+ const acquired = makeFileReference(chosen.access, chosen.append ?? false);
2018
+ yield* Effect.addFinalizer(() => finalizeFile(acquired));
2019
+ return yield* coordinated("openReference", Effect.gen(function* () {
1000
2020
  if (acquired.closed)
1001
2021
  return yield* Effect.interrupt;
1002
2022
  const node = yield* referencedNode(objectReference, "openReference");
1003
2023
  if (node.kind !== "file")
1004
- return yield* failure("IsDirectory", "openReference");
1005
- if (node.metadata.nlink === 0)
1006
- return yield* failure("StaleReference", "openReference");
1007
- yield* authorize(node, identity, 4, "openReference", "/");
2024
+ return yield* new FsError({ code: "IsDirectory", operation: "openReference" });
2025
+ if (node.metadata.nlink === 0) {
2026
+ return yield* new FsError({ code: "StaleReference", operation: "openReference" });
2027
+ }
2028
+ yield* authorize(node, identity, chosen.access === "read" ? READ : chosen.access === "write" ? WRITE : READ | WRITE, "openReference");
2029
+ if (chosen.truncate)
2030
+ yield* resize(node, 0n, "openReference");
1008
2031
  node.openCount += 1;
1009
2032
  acquired.file = node;
1010
2033
  return fileHandle(acquired);
1011
2034
  }));
1012
2035
  }),
2036
+ openChildReference: Effect.fn("Caller.openChildReference")(function* (directoryReference, input, raw, expected) {
2037
+ const name = yield* referencedName(input, "openChildReference");
2038
+ const decoded = decodeOpenChildReferenceSettings(raw);
2039
+ if (Result.isFailure(decoded)) {
2040
+ return yield* new FsError({ code: "InvalidArgument", operation: "openChildReference" });
2041
+ }
2042
+ const chosen = { ...decoded.success };
2043
+ if (chosen.access === "read" && (chosen.append || chosen.truncate)) {
2044
+ return yield* new FsError({ code: "InvalidArgument", operation: "openChildReference" });
2045
+ }
2046
+ if ((chosen.mode !== undefined || chosen.times !== undefined || chosen.initialSize !== undefined ||
2047
+ chosen.exactMode !== undefined || chosen.owner !== undefined) &&
2048
+ (chosen.create === undefined || chosen.create === "never")) {
2049
+ return yield* new FsError({ code: "InvalidArgument", operation: "openChildReference" });
2050
+ }
2051
+ if (chosen.exactMode && chosen.mode === undefined) {
2052
+ return yield* new FsError({ code: "InvalidArgument", operation: "openChildReference" });
2053
+ }
2054
+ const relativePath = preparePath(ownedPath(nameBytes(name)), "openChildReference", undefined);
2055
+ const acquired = makeFileReference(chosen.access, chosen.append ?? false);
2056
+ yield* Effect.addFinalizer(() => finalizeFile(acquired));
2057
+ return yield* coordinated("openChildReference", Effect.gen(function* () {
2058
+ if (acquired.closed)
2059
+ return yield* Effect.interrupt;
2060
+ const parent = yield* referencedDirectory(directoryReference, "openChildReference");
2061
+ yield* authorize(parent, identity, EXECUTE, "openChildReference");
2062
+ const direct = parent.entries.get(name);
2063
+ if (expected !== undefined) {
2064
+ let expectedNode;
2065
+ if (expected !== null) {
2066
+ const observed = yield* Effect.result(referencedNode(expected, "openChildReference"));
2067
+ if (Result.isFailure(observed)) {
2068
+ return yield* new FsError({ code: "VolumeBusy", operation: "openChildReference" });
2069
+ }
2070
+ expectedNode = observed.success;
2071
+ }
2072
+ if (direct !== expectedNode) {
2073
+ return yield* new FsError({ code: "VolumeBusy", operation: "openChildReference" });
2074
+ }
2075
+ }
2076
+ if (chosen.expectedChild === null) {
2077
+ if (direct !== undefined) {
2078
+ return yield* new FsError({ code: "StaleReference", operation: "openChildReference" });
2079
+ }
2080
+ }
2081
+ else if (chosen.expectedChild !== undefined) {
2082
+ const expected = chosen.expectedChild;
2083
+ const observed = yield* referencedNode(expected.reference, "openChildReference");
2084
+ if (direct !== observed || observed.revision !== expected.revision ||
2085
+ observed.metadata.atimeNs !== expected.atimeNs || observed.metadata.mtimeNs !== expected.mtimeNs) {
2086
+ return yield* new FsError({ code: "StaleReference", operation: "openChildReference" });
2087
+ }
2088
+ }
2089
+ if (direct !== undefined && chosen.create === "exclusive") {
2090
+ return yield* new FsError({ code: "AlreadyExists", operation: "openChildReference" });
2091
+ }
2092
+ let file = direct;
2093
+ let mutationParent = parent;
2094
+ let mutationName = name;
2095
+ if (file?.kind === "symlink" && chosen.followFinalSymlink !== false) {
2096
+ const resolved = yield* lookup(yield* Effect.fromResult(relativePath), undefined, "openChildReference", {
2097
+ followFinalSymlink: true,
2098
+ allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
2099
+ }, parent);
2100
+ file = resolved.node;
2101
+ if (file === undefined) {
2102
+ if (resolved.parent === undefined || resolved.name === undefined) {
2103
+ return yield* new FsError({ code: "IsDirectory", operation: "openChildReference" });
2104
+ }
2105
+ mutationParent = resolved.parent;
2106
+ mutationName = resolved.name;
2107
+ }
2108
+ }
2109
+ const before = mutationParent.revision;
2110
+ let created = false;
2111
+ if (file === undefined) {
2112
+ if (chosen.create === undefined || chosen.create === "never") {
2113
+ return yield* new FsError({ code: "NotFound", operation: "openChildReference" });
2114
+ }
2115
+ yield* authorize(mutationParent, identity, WRITE | EXECUTE, "openChildReference");
2116
+ if (atEntryLimit()) {
2117
+ return yield* new FsError({ code: "NoSpace", operation: "openChildReference" });
2118
+ }
2119
+ const size = chosen.initialSize ?? 0n;
2120
+ if (size < 0n) {
2121
+ return yield* new FsError({ code: "InvalidArgument", operation: "openChildReference" });
2122
+ }
2123
+ if (size > BigInt(maxFileBytes)) {
2124
+ return yield* new FsError({ code: "FileTooLarge", operation: "openChildReference" });
2125
+ }
2126
+ if (settings.maxBytes !== undefined && size > ByteSize.toBigInt(settings.maxBytes) - state.usedBytes) {
2127
+ return yield* new FsError({ code: "NoSpace", operation: "openChildReference" });
2128
+ }
2129
+ if (!identity.privileged &&
2130
+ ((chosen.owner?.uid !== undefined && chosen.owner.uid !== identity.uid) ||
2131
+ (chosen.owner?.gid !== undefined && chosen.owner.gid !== identity.gid &&
2132
+ !identity.groups.includes(chosen.owner.gid)))) {
2133
+ return yield* new FsError({ code: "AccessDenied", operation: "openChildReference" });
2134
+ }
2135
+ const now = yield* timestamp("openChildReference");
2136
+ const initial = creationTimes(chosen.times, now);
2137
+ const creationMode = chosen.exactMode
2138
+ ? yield* permittedMode({
2139
+ kind: "file",
2140
+ uid: chosen.owner?.uid ?? identity.uid,
2141
+ gid: chosen.owner?.gid ?? mutationParent.metadata.gid
2142
+ }, chosen.mode, "openChildReference")
2143
+ : (chosen.mode ?? 0o666) & 0o777 & ~umask;
2144
+ const createdFile = {
2145
+ kind: "file",
2146
+ lineage: undefined,
2147
+ data: Content.make(new Uint8Array(Number(size))),
2148
+ openCount: 0,
2149
+ metadata: {
2150
+ ...directoryMetadata(state.nextInode, chosen.owner?.uid ?? identity.uid, chosen.owner?.gid ?? mutationParent.metadata.gid, creationMode, now),
2151
+ ...initial,
2152
+ kind: "file",
2153
+ size,
2154
+ nlink: 1
2155
+ },
2156
+ revision: nextRevision(),
2157
+ objectReference: undefined
2158
+ };
2159
+ file = createdFile;
2160
+ attach(mutationParent, mutationName, createdFile, now);
2161
+ state.entries += 1;
2162
+ state.usedBytes += size;
2163
+ state.nextInode += 1n;
2164
+ created = true;
2165
+ publishEntry("Create", mutationParent, mutationName);
2166
+ }
2167
+ else {
2168
+ if (file.kind === "symlink") {
2169
+ return yield* new FsError({ code: "SymlinkLoop", operation: "openChildReference" });
2170
+ }
2171
+ if (file.kind !== "file") {
2172
+ return yield* new FsError({ code: "IsDirectory", operation: "openChildReference" });
2173
+ }
2174
+ yield* authorize(file, identity, chosen.access === "read" ? READ : chosen.access === "write" ? WRITE : READ | WRITE, "openChildReference");
2175
+ if (chosen.truncate)
2176
+ yield* resize(file, 0n, "openChildReference");
2177
+ }
2178
+ file.openCount += 1;
2179
+ acquired.file = file;
2180
+ return {
2181
+ handle: fileHandle(acquired),
2182
+ reference: referenceFor(file),
2183
+ created,
2184
+ directory: { before, after: mutationParent.revision }
2185
+ };
2186
+ }));
2187
+ }),
1013
2188
  readFile: Effect.fn("Caller.readFile")(function* (input, options) {
1014
2189
  const prepared = preparePath(input, "readFile", settings.maxPathBytes);
1015
2190
  const base = options?.relativeTo;
1016
- return yield* coordinated(Effect.gen(function* () {
2191
+ return yield* coordinated("readFile", Effect.gen(function* () {
1017
2192
  const node = yield* resolveNode(yield* Effect.fromResult(prepared), base, "readFile");
1018
- if (node.kind !== "file")
1019
- return yield* failure("IsDirectory", "readFile", input);
1020
- yield* authorize(node, identity, 4, "readFile", input);
2193
+ if (node.kind !== "file") {
2194
+ return yield* new FsError({ code: "IsDirectory", operation: "readFile", path: input });
2195
+ }
2196
+ yield* authorize(node, identity, READ, "readFile", input);
1021
2197
  const data = new Uint8Array(node.data.bytes);
1022
2198
  node.metadata = { ...node.metadata, atimeNs: (yield* timestamp("readFile")) };
1023
2199
  return data;
@@ -1025,21 +2201,23 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
1025
2201
  }),
1026
2202
  writeFile: Effect.fn("Caller.writeFile")(function* (input, bytes, options) {
1027
2203
  const prepared = preparePath(input, "writeFile", settings.maxPathBytes);
1028
- if (!(bytes instanceof Uint8Array) || !(bytes.buffer instanceof ArrayBuffer) || !attachedBuffer(bytes)) {
1029
- return yield* failure("InvalidArgument", "writeFile", input);
2204
+ if (!isAttachedBytes(bytes)) {
2205
+ return yield* new FsError({ code: "InvalidArgument", operation: "writeFile", path: input });
1030
2206
  }
1031
2207
  const captured = new Uint8Array(bytes);
1032
2208
  const { relativeTo: base, ...raw } = options;
1033
- const decoded = Schema.decodeResult(WriteFileSettings, { onExcessProperty: "error" })(raw);
1034
- if (Result.isFailure(decoded))
1035
- return yield* failure("InvalidArgument", "writeFile", input);
2209
+ const decoded = decodeWriteFileSettings(raw);
2210
+ if (Result.isFailure(decoded)) {
2211
+ return yield* new FsError({ code: "InvalidArgument", operation: "writeFile", path: input });
2212
+ }
1036
2213
  const chosen = decoded.success;
1037
- return yield* coordinated(Effect.gen(function* () {
2214
+ return yield* coordinated("writeFile", Effect.gen(function* () {
1038
2215
  const path = yield* Effect.fromResult(prepared);
1039
2216
  if (chosen.create === "exclusive") {
1040
2217
  const exists = yield* Effect.result(lookup(path, base, "writeFile", { followFinalSymlink: false }));
1041
- if (Result.isSuccess(exists))
1042
- return yield* failure("AlreadyExists", "writeFile", input);
2218
+ if (Result.isSuccess(exists)) {
2219
+ return yield* new FsError({ code: "AlreadyExists", operation: "writeFile", path: input });
2220
+ }
1043
2221
  if (exists.failure.code !== "NotFound")
1044
2222
  return yield* exists.failure;
1045
2223
  }
@@ -1049,36 +2227,39 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
1049
2227
  });
1050
2228
  const { name, parent } = resolved;
1051
2229
  if (parent === undefined || name === undefined || resolved.node?.kind === "directory") {
1052
- return yield* failure("IsDirectory", "writeFile", input);
2230
+ return yield* new FsError({ code: "IsDirectory", operation: "writeFile", path: input });
1053
2231
  }
1054
2232
  const replaced = resolved.node?.kind === "symlink" ? resolved.node : undefined;
1055
2233
  if (replaced !== undefined && !chosen.replaceFinalSymlink) {
1056
- return yield* failure("SymlinkLoop", "writeFile", input);
2234
+ return yield* new FsError({ code: "SymlinkLoop", operation: "writeFile", path: input });
2235
+ }
2236
+ if (chosen.access === "read") {
2237
+ return yield* new FsError({ code: "InvalidHandle", operation: "writeFile", path: input });
1057
2238
  }
1058
- if (chosen.access === "read")
1059
- return yield* failure("InvalidHandle", "writeFile", input);
1060
2239
  const file = resolved.node?.kind === "file" ? resolved.node : undefined;
1061
2240
  if (file === undefined) {
1062
- yield* authorize(parent, identity, 3, "writeFile", input);
2241
+ yield* authorize(parent, identity, WRITE | EXECUTE, "writeFile", input);
1063
2242
  if (replaced !== undefined)
1064
2243
  yield* authorizeRemoval(parent, replaced, "writeFile", input);
1065
- if (replaced === undefined && settings.maxEntries !== undefined && entries >= settings.maxEntries) {
1066
- return yield* failure("NoSpace", "writeFile", input);
2244
+ if (replaced === undefined && atEntryLimit()) {
2245
+ return yield* new FsError({ code: "NoSpace", operation: "writeFile", path: input });
1067
2246
  }
1068
2247
  }
1069
- else
1070
- yield* authorize(file, identity, chosen.access === "readWrite" ? 6 : 2, "writeFile", input);
2248
+ else {
2249
+ yield* authorize(file, identity, chosen.access === "readWrite" ? READ | WRITE : WRITE, "writeFile", input);
2250
+ }
1071
2251
  const finalMode = chosen.finalMode === undefined ? undefined : yield* permittedMode(file?.metadata ?? { kind: "file", uid: identity.uid, gid: parent.metadata.gid }, chosen.finalMode, "writeFile", input);
1072
2252
  const previous = file?.data.bytes.length ?? 0;
1073
2253
  const initial = chosen.truncate ? 0 : previous;
1074
2254
  const position = chosen.append ? initial : 0;
1075
2255
  const size = Math.max(initial, position + captured.length);
1076
- if (size > maxFileBytes)
1077
- return yield* failure("FileTooLarge", "writeFile", input);
2256
+ if (size > maxFileBytes) {
2257
+ return yield* new FsError({ code: "FileTooLarge", operation: "writeFile", path: input });
2258
+ }
1078
2259
  const reclaimed = replaced !== undefined && replaced.metadata.nlink === 1 ? replaced.target.length : 0;
1079
2260
  if (settings.maxBytes !== undefined &&
1080
- BigInt(size - previous) > ByteSize.toBigInt(settings.maxBytes) - usedBytes + BigInt(reclaimed)) {
1081
- return yield* failure("NoSpace", "writeFile", input);
2261
+ BigInt(size - previous) > ByteSize.toBigInt(settings.maxBytes) - state.usedBytes + BigInt(reclaimed)) {
2262
+ return yield* new FsError({ code: "NoSpace", operation: "writeFile", path: input });
1082
2263
  }
1083
2264
  if (file !== undefined && !chosen.truncate && captured.length === 0 && chosen.finalMode === undefined) {
1084
2265
  return;
@@ -1095,10 +2276,11 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
1095
2276
  {
1096
2277
  kind: "file",
1097
2278
  lineage: undefined,
1098
- data: Content.make(data),
2279
+ // Assigned below on the shared path that also covers an existing file.
2280
+ data: Content.empty(),
1099
2281
  openCount: 0,
1100
2282
  metadata: {
1101
- ...directoryMetadata(nextInode++, identity.uid, parent.metadata.gid, (chosen.mode ?? 0o666) & 0o777 & ~umask, now),
2283
+ ...directoryMetadata(state.nextInode++, identity.uid, parent.metadata.gid, (chosen.mode ?? 0o666) & 0o777 & ~umask, now),
1102
2284
  kind: "file",
1103
2285
  nlink: 1
1104
2286
  },
@@ -1108,21 +2290,19 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
1108
2290
  node.data = Content.make(data);
1109
2291
  node.metadata = {
1110
2292
  ...node.metadata,
1111
- mode: finalMode ?? node.metadata.mode & ~0o6000,
2293
+ mode: finalMode ?? node.metadata.mode & ~SET_ID_BITS,
1112
2294
  size: BigInt(size),
1113
2295
  mtimeNs: now,
1114
2296
  ctimeNs: now
1115
2297
  };
1116
2298
  advanceRevision(node);
1117
- usedBytes += BigInt(size - previous);
2299
+ state.usedBytes += BigInt(size - previous);
1118
2300
  if (file === undefined) {
1119
2301
  if (replaced !== undefined)
1120
2302
  detach(replaced, now);
1121
- parent.entries.set(name, node);
1122
- parent.metadata = { ...parent.metadata, mtimeNs: now, ctimeNs: now };
1123
- advanceRevision(parent);
2303
+ attach(parent, name, node, now);
1124
2304
  if (replaced === undefined)
1125
- entries += 1;
2305
+ state.entries += 1;
1126
2306
  publishEntry(replaced === undefined ? "Create" : "Update", parent, name);
1127
2307
  }
1128
2308
  else
@@ -1150,12 +2330,13 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
1150
2330
  access: Effect.fn("Caller.access")(function* (input, bits = 0, options) {
1151
2331
  const prepared = preparePath(input, "access", settings.maxPathBytes);
1152
2332
  const base = options?.relativeTo;
1153
- if (!Number.isInteger(bits) || bits < 0 || bits > 7)
1154
- return yield* failure("InvalidArgument", "access", input);
1155
- return yield* coordinated(Effect.gen(function* () {
2333
+ if (!Number.isInteger(bits) || bits < 0 || bits > (READ | WRITE | EXECUTE)) {
2334
+ return yield* new FsError({ code: "InvalidArgument", operation: "access", path: input });
2335
+ }
2336
+ return yield* coordinatedRead("access", Effect.gen(function* () {
1156
2337
  const node = yield* resolveNode(yield* Effect.fromResult(prepared), base, "access");
1157
- if (node.kind === "file" && (bits & 1) !== 0 && (node.metadata.mode & 0o111) === 0) {
1158
- return yield* failure("AccessDenied", "access", input);
2338
+ if (node.kind === "file" && (bits & EXECUTE) !== 0 && (node.metadata.mode & ANY_EXECUTE) === 0) {
2339
+ return yield* new FsError({ code: "AccessDenied", operation: "access", path: input });
1159
2340
  }
1160
2341
  yield* authorize(node, identity, bits, "access", input);
1161
2342
  }));
@@ -1163,18 +2344,19 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
1163
2344
  truncate: Effect.fn("Caller.truncate")(function* (input, length, options) {
1164
2345
  const prepared = preparePath(input, "truncate", settings.maxPathBytes);
1165
2346
  const base = options?.relativeTo;
1166
- return yield* coordinated(Effect.gen(function* () {
2347
+ return yield* coordinated("truncate", Effect.gen(function* () {
1167
2348
  const node = yield* resolveNode(yield* Effect.fromResult(prepared), base, "truncate");
1168
- if (node.kind !== "file")
1169
- return yield* failure("IsDirectory", "truncate", input);
1170
- yield* authorize(node, identity, 2, "truncate", input);
2349
+ if (node.kind !== "file") {
2350
+ return yield* new FsError({ code: "IsDirectory", operation: "truncate", path: input });
2351
+ }
2352
+ yield* authorize(node, identity, WRITE, "truncate", input);
1171
2353
  yield* resize(node, length, "truncate");
1172
2354
  }));
1173
2355
  }),
1174
2356
  lstat: Effect.fn("Caller.lstat")(function* (input, options) {
1175
2357
  const prepared = preparePath(input, "lstat", settings.maxPathBytes);
1176
2358
  const base = options?.relativeTo;
1177
- return yield* coordinated(Effect.gen(function* () {
2359
+ return yield* coordinatedRead("lstat", Effect.gen(function* () {
1178
2360
  const node = yield* resolveNode(yield* Effect.fromResult(prepared), base, "lstat", {
1179
2361
  followFinalSymlink: false
1180
2362
  });
@@ -1187,67 +2369,68 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
1187
2369
  const sourceBase = options?.sourceRelativeTo;
1188
2370
  const destinationBase = options?.destinationRelativeTo;
1189
2371
  const follow = options?.followSourceSymlink ?? false;
1190
- return yield* coordinated(Effect.gen(function* () {
2372
+ return yield* coordinated("link", Effect.gen(function* () {
1191
2373
  const node = yield* resolveNode(yield* Effect.fromResult(a), sourceBase, "link", {
1192
2374
  followFinalSymlink: follow
1193
2375
  });
1194
- if (node.kind === "directory")
1195
- return yield* failure("IsDirectory", "link", source);
2376
+ if (node.kind === "directory") {
2377
+ return yield* new FsError({ code: "IsDirectory", operation: "link", path: source });
2378
+ }
1196
2379
  const path = yield* Effect.fromResult(b);
1197
2380
  const parent = yield* locate(path, destinationBase, "link", { parentOnly: true });
1198
- yield* authorize(parent, identity, 3, "link", destination);
2381
+ yield* authorize(parent, identity, WRITE | EXECUTE, "link", destination);
1199
2382
  const name = path.components.at(-1);
1200
- if (name === undefined || name === "2e" || name === "2e2e" || parent.entries.has(name)) {
1201
- return yield* failure("AlreadyExists", "link", destination);
2383
+ if (isDotComponent(name) || parent.entries.has(name)) {
2384
+ return yield* new FsError({ code: "AlreadyExists", operation: "link", path: destination });
1202
2385
  }
1203
- if (path.trailingSlash)
1204
- return yield* failure("NotDirectory", "link", destination);
1205
- if (settings.maxEntries !== undefined && entries >= settings.maxEntries) {
1206
- return yield* failure("NoSpace", "link", destination);
2386
+ if (path.trailingSlash) {
2387
+ return yield* new FsError({ code: "NotDirectory", operation: "link", path: destination });
2388
+ }
2389
+ if (atEntryLimit()) {
2390
+ return yield* new FsError({ code: "NoSpace", operation: "link", path: destination });
1207
2391
  }
1208
2392
  const now = yield* timestamp("link");
1209
- parent.entries.set(name, node);
1210
- parent.metadata = { ...parent.metadata, mtimeNs: now, ctimeNs: now };
2393
+ attach(parent, name, node, now);
1211
2394
  node.metadata = { ...node.metadata, nlink: node.metadata.nlink + 1, ctimeNs: now };
1212
- advanceRevision(parent);
1213
2395
  advanceRevision(node);
1214
- entries += 1;
2396
+ state.entries += 1;
1215
2397
  publishEntry("Create", parent, name);
1216
2398
  }));
1217
2399
  }),
1218
2400
  symlink: Effect.fn("Caller.symlink")(function* (target, input, options) {
1219
2401
  const prepared = preparePath(input, "symlink", settings.maxPathBytes);
1220
- if (typeof target === "string" && !wellFormed(target)) {
1221
- return yield* failure("InvalidPathEncoding", "symlink", target);
2402
+ const rawTarget = inputBytes(target);
2403
+ if (Result.isFailure(rawTarget)) {
2404
+ return yield* new FsError({ code: rawTarget.failure, operation: "symlink", path: target });
1222
2405
  }
1223
- const rawTarget = typeof target === "string" ? new TextEncoder().encode(target) : getBytePathBytes(target);
1224
- if (rawTarget === undefined || rawTarget.includes(0)) {
1225
- return yield* failure("InvalidArgument", "symlink", target);
2406
+ if (rawTarget.success.includes(0)) {
2407
+ return yield* new FsError({ code: "InvalidArgument", operation: "symlink", path: target });
1226
2408
  }
1227
- const targetBytes = new Uint8Array(rawTarget);
2409
+ const targetBytes = new Uint8Array(rawTarget.success);
1228
2410
  const base = options?.relativeTo;
1229
- return yield* coordinated(Effect.gen(function* () {
2411
+ return yield* coordinated("symlink", Effect.gen(function* () {
1230
2412
  const path = yield* Effect.fromResult(prepared);
1231
2413
  const bytes = targetBytes;
1232
2414
  const parent = yield* locate(path, base, "symlink", { parentOnly: true });
1233
- yield* authorize(parent, identity, 3, "symlink", input);
2415
+ yield* authorize(parent, identity, WRITE | EXECUTE, "symlink", input);
1234
2416
  const name = path.components.at(-1);
1235
- if (name === undefined || name === "2e" || name === "2e2e" || parent.entries.has(name)) {
1236
- return yield* failure("AlreadyExists", "symlink", input);
2417
+ if (isDotComponent(name) || parent.entries.has(name)) {
2418
+ return yield* new FsError({ code: "AlreadyExists", operation: "symlink", path: input });
2419
+ }
2420
+ if (path.trailingSlash) {
2421
+ return yield* new FsError({ code: "NotDirectory", operation: "symlink", path: input });
1237
2422
  }
1238
- if (path.trailingSlash)
1239
- return yield* failure("NotDirectory", "symlink", input);
1240
- if ((settings.maxEntries !== undefined && entries >= settings.maxEntries) ||
2423
+ if (atEntryLimit() ||
1241
2424
  (settings.maxBytes !== undefined &&
1242
- BigInt(bytes.length) > ByteSize.toBigInt(settings.maxBytes) - usedBytes))
1243
- return yield* failure("NoSpace", "symlink", input);
2425
+ BigInt(bytes.length) > ByteSize.toBigInt(settings.maxBytes) - state.usedBytes))
2426
+ return yield* new FsError({ code: "NoSpace", operation: "symlink", path: input });
1244
2427
  const now = yield* timestamp("symlink");
1245
2428
  const node = {
1246
2429
  kind: "symlink",
1247
2430
  lineage: undefined,
1248
2431
  target: new Uint8Array(bytes),
1249
2432
  metadata: {
1250
- ...directoryMetadata(nextInode, identity.uid, parent.metadata.gid, 0o777, now),
2433
+ ...directoryMetadata(state.nextInode, identity.uid, parent.metadata.gid, 0o777, now),
1251
2434
  kind: "symlink",
1252
2435
  nlink: 1,
1253
2436
  size: BigInt(bytes.length)
@@ -1255,12 +2438,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
1255
2438
  revision: nextRevision(),
1256
2439
  objectReference: undefined
1257
2440
  };
1258
- parent.entries.set(name, node);
1259
- parent.metadata = { ...parent.metadata, mtimeNs: now, ctimeNs: now };
1260
- advanceRevision(parent);
1261
- nextInode += 1n;
1262
- entries += 1;
1263
- usedBytes += BigInt(bytes.length);
2441
+ attach(parent, name, node, now);
2442
+ state.nextInode += 1n;
2443
+ state.entries += 1;
2444
+ state.usedBytes += BigInt(bytes.length);
1264
2445
  publishEntry("Create", parent, name);
1265
2446
  }));
1266
2447
  }),
@@ -1285,33 +2466,28 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
1285
2466
  open: Effect.fn("Caller.open")(function* (input, options) {
1286
2467
  const prepared = preparePath(input, "open", settings.maxPathBytes);
1287
2468
  const { relativeTo: base, ...raw } = options;
1288
- const decoded = Schema.decodeResult(OpenSettings, { onExcessProperty: "error" })(raw);
1289
- if (Result.isFailure(decoded))
1290
- return yield* failure("InvalidArgument", "open", input);
2469
+ const decoded = decodeOpenSettings(raw);
2470
+ if (Result.isFailure(decoded)) {
2471
+ return yield* new FsError({ code: "InvalidArgument", operation: "open", path: input });
2472
+ }
1291
2473
  const chosen = { ...decoded.success };
1292
2474
  if (chosen.access === "read" && (chosen.append || chosen.truncate)) {
1293
- return yield* failure("InvalidArgument", "open", input);
2475
+ return yield* new FsError({ code: "InvalidArgument", operation: "open", path: input });
1294
2476
  }
1295
2477
  if (chosen.mode !== undefined && (chosen.create === undefined || chosen.create === "never")) {
1296
- return yield* failure("InvalidArgument", "open", input);
2478
+ return yield* new FsError({ code: "InvalidArgument", operation: "open", path: input });
1297
2479
  }
1298
- const acquired = {
1299
- volume: volumeIdentity,
1300
- file: undefined,
1301
- closed: false,
1302
- offset: 0n,
1303
- access: chosen.access,
1304
- append: chosen.append ?? false
1305
- };
1306
- yield* Effect.addFinalizer(() => coordinated(Effect.sync(() => releaseFile(acquired))));
1307
- return yield* coordinated(Effect.gen(function* () {
2480
+ const acquired = makeFileReference(chosen.access, chosen.append ?? false);
2481
+ yield* Effect.addFinalizer(() => finalizeFile(acquired));
2482
+ return yield* coordinated("open", Effect.gen(function* () {
1308
2483
  if (acquired.closed)
1309
2484
  return yield* Effect.interrupt;
1310
2485
  const path = yield* Effect.fromResult(prepared);
1311
2486
  if (chosen.create === "exclusive") {
1312
2487
  const existing = yield* Effect.result(lookup(path, base, "open", { followFinalSymlink: false }));
1313
- if (Result.isSuccess(existing))
1314
- return yield* failure("AlreadyExists", "open", input);
2488
+ if (Result.isSuccess(existing)) {
2489
+ return yield* new FsError({ code: "AlreadyExists", operation: "open", path: input });
2490
+ }
1315
2491
  if (existing.failure.code !== "NotFound")
1316
2492
  return yield* existing.failure;
1317
2493
  }
@@ -1320,24 +2496,25 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
1320
2496
  allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
1321
2497
  });
1322
2498
  const parent = resolved.parent;
1323
- if (parent === undefined)
1324
- return yield* failure("IsDirectory", "open", input);
2499
+ if (parent === undefined) {
2500
+ return yield* new FsError({ code: "IsDirectory", operation: "open", path: input });
2501
+ }
1325
2502
  const name = resolved.name;
1326
- if (name === undefined || name === "2e" || name === "2e2e") {
1327
- return yield* failure("IsDirectory", "open", input);
2503
+ if (isDotComponent(name)) {
2504
+ return yield* new FsError({ code: "IsDirectory", operation: "open", path: input });
1328
2505
  }
1329
- yield* authorize(parent, identity, 1, "open", input);
2506
+ yield* authorize(parent, identity, EXECUTE, "open", input);
1330
2507
  let file = resolved.node;
1331
2508
  if (file !== undefined && chosen.create === "exclusive") {
1332
- return yield* failure("AlreadyExists", "open", input);
2509
+ return yield* new FsError({ code: "AlreadyExists", operation: "open", path: input });
1333
2510
  }
1334
2511
  if (file === undefined) {
1335
2512
  if (chosen.create === undefined || chosen.create === "never" || path.trailingSlash) {
1336
- return yield* failure("NotFound", "open", input);
2513
+ return yield* new FsError({ code: "NotFound", operation: "open", path: input });
1337
2514
  }
1338
- yield* authorize(parent, identity, 3, "open", input);
1339
- if (settings.maxEntries !== undefined && entries >= settings.maxEntries) {
1340
- return yield* failure("NoSpace", "open", input);
2515
+ yield* authorize(parent, identity, WRITE | EXECUTE, "open", input);
2516
+ if (atEntryLimit()) {
2517
+ return yield* new FsError({ code: "NoSpace", operation: "open", path: input });
1341
2518
  }
1342
2519
  const now = yield* timestamp("open");
1343
2520
  file = {
@@ -1346,28 +2523,29 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
1346
2523
  data: Content.empty(),
1347
2524
  openCount: 0,
1348
2525
  metadata: {
1349
- ...directoryMetadata(nextInode, identity.uid, parent.metadata.gid, (chosen.mode ?? 0o666) & 0o777 & ~umask, now),
2526
+ ...directoryMetadata(state.nextInode, identity.uid, parent.metadata.gid, (chosen.mode ?? 0o666) & 0o777 & ~umask, now),
1350
2527
  kind: "file",
1351
2528
  nlink: 1
1352
2529
  },
1353
2530
  revision: nextRevision(),
1354
2531
  objectReference: undefined
1355
2532
  };
1356
- parent.entries.set(name, file);
1357
- parent.metadata = { ...parent.metadata, mtimeNs: now, ctimeNs: now };
1358
- advanceRevision(parent);
1359
- entries += 1;
1360
- nextInode += 1n;
2533
+ attach(parent, name, file, now);
2534
+ state.entries += 1;
2535
+ state.nextInode += 1n;
1361
2536
  publishEntry("Create", parent, name);
1362
2537
  }
1363
2538
  else {
1364
- if (file.kind === "symlink")
1365
- return yield* failure("SymlinkLoop", "open", input);
1366
- if (file.kind !== "file")
1367
- return yield* failure("IsDirectory", "open", input);
1368
- if (path.trailingSlash)
1369
- return yield* failure("NotDirectory", "open", input);
1370
- yield* authorize(file, identity, chosen.access === "read" ? 4 : chosen.access === "write" ? 2 : 6, "open", input);
2539
+ if (file.kind === "symlink") {
2540
+ return yield* new FsError({ code: "SymlinkLoop", operation: "open", path: input });
2541
+ }
2542
+ if (file.kind !== "file") {
2543
+ return yield* new FsError({ code: "IsDirectory", operation: "open", path: input });
2544
+ }
2545
+ if (path.trailingSlash) {
2546
+ return yield* new FsError({ code: "NotDirectory", operation: "open", path: input });
2547
+ }
2548
+ yield* authorize(file, identity, chosen.access === "read" ? READ : chosen.access === "write" ? WRITE : READ | WRITE, "open", input);
1371
2549
  if (chosen.truncate)
1372
2550
  yield* resize(file, 0n, "open");
1373
2551
  }
@@ -1379,28 +2557,30 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
1379
2557
  unlink: Effect.fn("Caller.unlink")(function* (input, options) {
1380
2558
  const prepared = preparePath(input, "unlink", settings.maxPathBytes);
1381
2559
  const base = options?.relativeTo;
1382
- return yield* coordinated(Effect.gen(function* () {
2560
+ return yield* coordinated("unlink", Effect.gen(function* () {
1383
2561
  const path = yield* Effect.fromResult(prepared);
1384
2562
  const parent = yield* locate(path, base, "unlink", { parentOnly: true });
1385
- yield* authorize(parent, identity, 3, "unlink", input);
2563
+ yield* authorize(parent, identity, WRITE | EXECUTE, "unlink", input);
1386
2564
  const name = path.components.at(-1);
1387
- if (name === undefined || name === "2e" || name === "2e2e") {
1388
- return yield* failure("IsDirectory", "unlink", input);
2565
+ if (isDotComponent(name)) {
2566
+ return yield* new FsError({ code: "IsDirectory", operation: "unlink", path: input });
1389
2567
  }
1390
2568
  const child = parent.entries.get(name);
1391
2569
  if (child === undefined)
1392
- return yield* failure("NotFound", "unlink", input);
1393
- if (child.kind === "directory")
1394
- return yield* failure("IsDirectory", "unlink", input);
1395
- if (path.trailingSlash)
1396
- return yield* failure("NotDirectory", "unlink", input);
2570
+ return yield* new FsError({ code: "NotFound", operation: "unlink", path: input });
2571
+ if (child.kind === "directory") {
2572
+ return yield* new FsError({ code: "IsDirectory", operation: "unlink", path: input });
2573
+ }
2574
+ if (path.trailingSlash) {
2575
+ return yield* new FsError({ code: "NotDirectory", operation: "unlink", path: input });
2576
+ }
1397
2577
  yield* authorizeRemoval(parent, child, "unlink", input);
1398
2578
  const now = yield* timestamp("unlink");
1399
2579
  parent.entries.delete(name);
1400
2580
  parent.metadata = { ...parent.metadata, mtimeNs: now, ctimeNs: now };
1401
2581
  advanceRevision(parent);
1402
2582
  detach(child, now);
1403
- entries -= 1;
2583
+ state.entries -= 1;
1404
2584
  publishEntry("Remove", parent, name);
1405
2585
  }));
1406
2586
  }),
@@ -1409,31 +2589,31 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
1409
2589
  const newPrepared = preparePath(destination, "rename", settings.maxPathBytes);
1410
2590
  const oldBase = options?.sourceRelativeTo;
1411
2591
  const newBase = options?.destinationRelativeTo;
1412
- return yield* coordinated(Effect.gen(function* () {
2592
+ return yield* coordinated("rename", Effect.gen(function* () {
1413
2593
  const oldPath = yield* Effect.fromResult(oldPrepared);
1414
2594
  const newPath = yield* Effect.fromResult(newPrepared);
1415
2595
  const oldParent = yield* locate(oldPath, oldBase, "rename", { parentOnly: true });
1416
2596
  const newParent = yield* locate(newPath, newBase, "rename", { parentOnly: true });
1417
- yield* authorize(oldParent, identity, 3, "rename", source);
1418
- yield* authorize(newParent, identity, 3, "rename", destination);
2597
+ yield* authorize(oldParent, identity, WRITE | EXECUTE, "rename", source);
2598
+ yield* authorize(newParent, identity, WRITE | EXECUTE, "rename", destination);
1419
2599
  const oldName = oldPath.components.at(-1);
1420
2600
  const newName = newPath.components.at(-1);
1421
- if (oldName === undefined || newName === undefined || oldName === "2e" || oldName === "2e2e" ||
1422
- newName === "2e" || newName === "2e2e") {
1423
- return yield* failure("InvalidArgument", "rename", source);
2601
+ if (isDotComponent(oldName) || isDotComponent(newName)) {
2602
+ return yield* new FsError({ code: "InvalidArgument", operation: "rename", path: source });
1424
2603
  }
1425
2604
  const child = oldParent.entries.get(oldName);
1426
- if (child === undefined)
1427
- return yield* failure("NotFound", "rename", source);
2605
+ if (child === undefined) {
2606
+ return yield* new FsError({ code: "NotFound", operation: "rename", path: source });
2607
+ }
1428
2608
  const replaced = newParent.entries.get(newName);
1429
2609
  if (newPath.trailingSlash && replaced === undefined) {
1430
- return yield* failure("NotFound", "rename", destination);
2610
+ return yield* new FsError({ code: "NotFound", operation: "rename", path: destination });
1431
2611
  }
1432
2612
  if (oldPath.trailingSlash && child.kind !== "directory") {
1433
- return yield* failure("NotDirectory", "rename", source);
2613
+ return yield* new FsError({ code: "NotDirectory", operation: "rename", path: source });
1434
2614
  }
1435
2615
  if (newPath.trailingSlash && replaced?.kind !== "directory") {
1436
- return yield* failure("NotDirectory", "rename", destination);
2616
+ return yield* new FsError({ code: "NotDirectory", operation: "rename", path: destination });
1437
2617
  }
1438
2618
  if (child === replaced)
1439
2619
  return;
@@ -1441,22 +2621,23 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
1441
2621
  if (replaced !== undefined) {
1442
2622
  yield* authorizeRemoval(newParent, replaced, "rename", destination);
1443
2623
  if (child.kind === "directory" && replaced.kind !== "directory") {
1444
- return yield* failure("NotDirectory", "rename", destination);
2624
+ return yield* new FsError({ code: "NotDirectory", operation: "rename", path: destination });
1445
2625
  }
1446
2626
  if (child.kind !== "directory" && replaced.kind === "directory") {
1447
- return yield* failure("IsDirectory", "rename", destination);
2627
+ return yield* new FsError({ code: "IsDirectory", operation: "rename", path: destination });
1448
2628
  }
1449
2629
  if (replaced.kind === "directory" && replaced.entries.size > 0) {
1450
- return yield* failure("NotEmpty", "rename", destination);
2630
+ return yield* new FsError({ code: "NotEmpty", operation: "rename", path: destination });
1451
2631
  }
1452
2632
  }
1453
2633
  for (let ancestor = newParent; ancestor !== undefined; ancestor = ancestor.parent) {
1454
- if (ancestor === child)
1455
- return yield* failure("InvalidArgument", "rename", destination);
2634
+ if (ancestor === child) {
2635
+ return yield* new FsError({ code: "InvalidArgument", operation: "rename", path: destination });
2636
+ }
1456
2637
  }
1457
2638
  const now = yield* timestamp("rename");
1458
2639
  // All rejection checks precede namespace, ancestry, quota, and metadata publication.
1459
- const oldEvent = () => ownedPath(nameBytes(directoryHex(oldParent) + (oldParent === root ? "" : "2f") + oldName));
2640
+ const oldEvent = () => ownedPath(nameBytes(directoryHex(oldParent) + (oldParent === state.root ? "" : SLASH_HEX) + oldName));
1460
2641
  oldParent.entries.delete(oldName);
1461
2642
  newParent.entries.set(newName, child);
1462
2643
  if (child.kind === "directory")
@@ -1480,31 +2661,33 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
1480
2661
  advanceRevision(child);
1481
2662
  if (replaced !== undefined) {
1482
2663
  detach(replaced, now);
1483
- entries -= 1;
2664
+ state.entries -= 1;
1484
2665
  }
1485
- watchHub.publishUnsafe(() => ({ _tag: "Remove", path: oldEvent() }));
2666
+ queuePublish(() => watchHub.publishUnsafe(() => ({ _tag: "Remove", path: oldEvent() })));
1486
2667
  publishEntry("Create", newParent, newName);
1487
2668
  }));
1488
2669
  }),
1489
2670
  rmdir: Effect.fn("Caller.rmdir")(function* (input, options) {
1490
2671
  const prepared = preparePath(input, "rmdir", settings.maxPathBytes);
1491
2672
  const base = options?.relativeTo;
1492
- return yield* coordinated(Effect.gen(function* () {
2673
+ return yield* coordinated("rmdir", Effect.gen(function* () {
1493
2674
  const path = yield* Effect.fromResult(prepared);
1494
2675
  const parent = yield* locate(path, base, "rmdir", { parentOnly: true });
1495
- yield* authorize(parent, identity, 3, "rmdir", input);
2676
+ yield* authorize(parent, identity, WRITE | EXECUTE, "rmdir", input);
1496
2677
  const name = path.components.at(-1);
1497
- if (name === undefined || name === "2e" || name === "2e2e") {
1498
- return yield* failure("InvalidArgument", "rmdir", input);
2678
+ if (isDotComponent(name)) {
2679
+ return yield* new FsError({ code: "InvalidArgument", operation: "rmdir", path: input });
1499
2680
  }
1500
2681
  const child = parent.entries.get(name);
1501
2682
  if (child === undefined)
1502
- return yield* failure("NotFound", "rmdir", input);
2683
+ return yield* new FsError({ code: "NotFound", operation: "rmdir", path: input });
1503
2684
  yield* authorizeRemoval(parent, child, "rmdir", input);
1504
- if (child.kind !== "directory")
1505
- return yield* failure("NotDirectory", "rmdir", input);
1506
- if (child.entries.size > 0)
1507
- return yield* failure("NotEmpty", "rmdir", input);
2685
+ if (child.kind !== "directory") {
2686
+ return yield* new FsError({ code: "NotDirectory", operation: "rmdir", path: input });
2687
+ }
2688
+ if (child.entries.size > 0) {
2689
+ return yield* new FsError({ code: "NotEmpty", operation: "rmdir", path: input });
2690
+ }
1508
2691
  const now = yield* timestamp("rmdir");
1509
2692
  parent.entries.delete(name);
1510
2693
  parent.metadata = { ...parent.metadata, nlink: parent.metadata.nlink - 1, mtimeNs: now, ctimeNs: now };
@@ -1513,14 +2696,14 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
1513
2696
  advanceRevision(parent);
1514
2697
  advanceRevision(child);
1515
2698
  invalidateReference(child);
1516
- entries -= 1;
2699
+ state.entries -= 1;
1517
2700
  publishEntry("Remove", parent, name);
1518
2701
  }));
1519
2702
  }),
1520
2703
  stat: Effect.fn("Caller.stat")(function* (input, options) {
1521
2704
  const prepared = preparePath(input, "stat", settings.maxPathBytes);
1522
2705
  const base = options?.relativeTo;
1523
- return yield* coordinated(Effect.gen(function* () {
2706
+ return yield* coordinatedRead("stat", Effect.gen(function* () {
1524
2707
  const path = yield* Effect.fromResult(prepared);
1525
2708
  const directory = yield* resolveNode(path, base, "stat");
1526
2709
  return { ...directory.metadata };
@@ -1530,18 +2713,18 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
1530
2713
  const prepared = preparePath(input, "mkdir", settings.maxPathBytes);
1531
2714
  const base = options?.relativeTo;
1532
2715
  const mode = options?.mode === undefined ? 0o777 : options.mode;
1533
- if (!Schema.is(Mode)(mode))
1534
- return yield* failure("InvalidArgument", "mkdir", input);
1535
- return yield* coordinated(Effect.gen(function* () {
2716
+ if (!isMode(mode))
2717
+ return yield* new FsError({ code: "InvalidArgument", operation: "mkdir", path: input });
2718
+ return yield* coordinated("mkdir", Effect.gen(function* () {
1536
2719
  const path = yield* Effect.fromResult(prepared);
1537
2720
  const parent = yield* locate(path, base, "mkdir", { parentOnly: true });
1538
- yield* authorize(parent, identity, 3, "mkdir", input);
2721
+ yield* authorize(parent, identity, WRITE | EXECUTE, "mkdir", input);
1539
2722
  const name = path.components.at(-1);
1540
- if (name === undefined || name === "2e" || name === "2e2e" || parent.entries.has(name)) {
1541
- return yield* failure("AlreadyExists", "mkdir", input);
2723
+ if (isDotComponent(name) || parent.entries.has(name)) {
2724
+ return yield* new FsError({ code: "AlreadyExists", operation: "mkdir", path: input });
1542
2725
  }
1543
- if (settings.maxEntries !== undefined && entries >= settings.maxEntries) {
1544
- return yield* failure("NoSpace", "mkdir", input);
2726
+ if (atEntryLimit()) {
2727
+ return yield* new FsError({ code: "NoSpace", operation: "mkdir", path: input });
1545
2728
  }
1546
2729
  const now = yield* timestamp("mkdir");
1547
2730
  const child = {
@@ -1549,22 +2732,14 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
1549
2732
  lineage: undefined,
1550
2733
  parent,
1551
2734
  entries: new Map(),
1552
- metadata: directoryMetadata(nextInode, identity.uid, parent.metadata.gid, (mode & 0o777 & ~umask) | (mode & 0o1000), now),
2735
+ metadata: directoryMetadata(state.nextInode, identity.uid, parent.metadata.gid, (mode & 0o777 & ~umask) | (mode & STICKY_BIT), now),
1553
2736
  revision: nextRevision(),
1554
2737
  objectReference: undefined
1555
2738
  };
1556
- const parentMetadata = {
1557
- ...parent.metadata,
1558
- nlink: parent.metadata.nlink + 1,
1559
- mtimeNs: now,
1560
- ctimeNs: now
1561
- };
1562
2739
  // No Effect yield or expected failure between these publication writes.
1563
- parent.entries.set(name, child);
1564
- parent.metadata = parentMetadata;
1565
- advanceRevision(parent);
1566
- nextInode += 1n;
1567
- entries += 1;
2740
+ attach(parent, name, child, now);
2741
+ state.nextInode += 1n;
2742
+ state.entries += 1;
1568
2743
  publishEntry("Create", parent, name);
1569
2744
  }));
1570
2745
  }),
@@ -1576,12 +2751,13 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
1576
2751
  const acquired = yield* acquireDirectory(input, options, "openDirectory");
1577
2752
  const handle = Object.freeze({
1578
2753
  [DirectoryHandleId]: true,
1579
- stat: coordinated(Effect.suspend(() => acquired.directory === undefined
1580
- ? Effect.fail(failure("InvalidHandle", "stat"))
2754
+ stat: coordinatedRead("stat", Effect.suspend(() => acquired.directory === undefined
2755
+ ? Effect.fail(new FsError({ code: "InvalidHandle", operation: "stat" }))
1581
2756
  : Effect.succeed({ ...acquired.directory.metadata }))).pipe(Effect.withSpan("DirectoryHandle.stat")),
1582
- close: coordinated(Effect.suspend(() => {
1583
- if (acquired.directory === undefined)
1584
- return Effect.fail(failure("InvalidHandle", "close"));
2757
+ close: coordinatedCleanup(Effect.suspend(() => {
2758
+ if (acquired.directory === undefined) {
2759
+ return Effect.fail(new FsError({ code: "InvalidHandle", operation: "close" }));
2760
+ }
1585
2761
  acquired.directory = undefined;
1586
2762
  acquired.closed = true;
1587
2763
  return Effect.void;
@@ -1592,86 +2768,123 @@ export const makeVolume = Effect.fnUntraced(function* (source, options) {
1592
2768
  })
1593
2769
  });
1594
2770
  };
1595
- const baseObservation = source._tag === "Overlay" ? observeChanges() : undefined;
1596
- const publicChange = (change) => {
1597
- switch (change._tag) {
1598
- case "Added":
1599
- case "Removed":
1600
- case "Updated":
1601
- case "Replaced":
1602
- return Object.freeze({ ...change, path: ownedPath(new Uint8Array(change.path)) });
1603
- case "Renamed":
1604
- return Object.freeze({
1605
- ...change,
1606
- from: ownedPath(new Uint8Array(change.from)),
1607
- to: ownedPath(new Uint8Array(change.to))
1608
- });
1609
- }
1610
- throw new Error("Unknown internal overlay change");
1611
- };
2771
+ const baseObservation = Predicate.isTagged("Overlay")(source) ? yield* observeChanges() : undefined;
2772
+ const publicChange = (change) => Predicate.isTagged("Renamed")(change)
2773
+ ? OverlayChange.make({ ...change, from: ownedPath(change.from), to: ownedPath(change.to) })
2774
+ : OverlayChange.make({ ...change, path: ownedPath(change.path) });
1612
2775
  const publicChanges = (changes) => Object.freeze(changes.map(publicChange));
1613
2776
  const changeOptions = (options) => {
1614
2777
  const decoded = decodeConfiguration(OverlayChangesOptions, options === undefined ? {} : options);
1615
2778
  return Result.isFailure(decoded) ? Effect.fail(decoded.failure) : Effect.succeed(decoded.success);
1616
2779
  };
2780
+ // An overlay is a spread copy of `volume`, so the object the caller holds is not always the one
2781
+ // built here. `watch` runs lazily, so it reads whichever surface was actually handed out.
2782
+ let surface;
1617
2783
  const volume = Object.freeze({
1618
2784
  [VolumeId]: true,
2785
+ durability: "memory-only",
2786
+ identity,
2787
+ incarnation,
2788
+ limits,
2789
+ usage: coordinatedRead("usage", Effect.sync(() => ({ usedBytes: state.usedBytes, entries: state.entries })))
2790
+ .pipe(Effect.withSpan("Volume.usage")),
1619
2791
  watch: Effect.gen(function* () {
1620
- const hook = TestHooks.getRegistrationHook(volume);
1621
- return yield* watchHub.subscribe(hook?.afterSubscribe);
2792
+ const hook = TestHooks.getRegistrationHook(surface);
2793
+ return yield* admit("watch", watchHub.subscribe(hook?.afterSubscribe));
1622
2794
  }).pipe(Effect.withSpan("Volume.watch")),
1623
- snapshot: coordinated(captureSnapshot()).pipe(Effect.withSpan("Volume.snapshot")),
2795
+ snapshot: coordinatedRead("snapshot", captureSnapshot()).pipe(Effect.withSpan("Volume.snapshot")),
1624
2796
  caller: Effect.fn("Volume.caller")(function* (options) {
1625
2797
  const decoded = decodeConfiguration(RootCallerOptions, options === undefined ? {} : options);
1626
2798
  if (Result.isFailure(decoded))
1627
2799
  return yield* decoded.failure;
1628
2800
  const chosen = decoded.success.identity ?? { uid: 0, gid: 0, groups: [], privileged: true };
1629
2801
  const identity = Object.freeze({ ...chosen, groups: Object.freeze([...chosen.groups]) });
1630
- return createCaller({ volume: volumeIdentity, directory: root, closed: false }, identity, decoded.success.umask ?? 0o022);
2802
+ return yield* coordinatedRead("caller", Effect.sync(() => createCaller(makeDirectoryReference(state.root), identity, decoded.success.umask ?? 0o022)));
1631
2803
  })
1632
2804
  });
1633
- if (source._tag !== "Overlay" || baseObservation === undefined) {
1634
- return { _tag: "Volume", volume };
2805
+ surface = volume;
2806
+ if (!Predicate.isTagged("Overlay")(source) || baseObservation === undefined) {
2807
+ // SAFETY: Overlay sources return below, so S is non-Overlay here and VolumeFor<S> is Volume.
2808
+ return volume;
1635
2809
  }
1636
2810
  const hook = TestHooks.getObservationHook(source.base);
1637
2811
  const overlay = Object.freeze({
1638
2812
  ...volume,
1639
2813
  changes: Effect.fn("OverlayVolume.changes")(function* (options) {
1640
2814
  const selected = yield* changeOptions(options);
1641
- const current = yield* coordinated(Effect.sync(observeChanges));
2815
+ const current = yield* coordinatedRead("changes", observeChanges());
1642
2816
  return publicChanges(compareOverlay(baseObservation, current, selected.includeTimestamps ?? false));
1643
2817
  }),
1644
2818
  capture: Effect.fn("OverlayVolume.capture")(function* (options) {
1645
2819
  const selected = yield* changeOptions(options);
1646
- const current = yield* coordinated(captureState(hook));
2820
+ const current = yield* coordinatedRead("capture", captureState(hook));
1647
2821
  return Object.freeze({
1648
2822
  snapshot: current.snapshot,
1649
2823
  changes: publicChanges(compareOverlay(baseObservation, current.observation, selected.includeTimestamps ?? false))
1650
2824
  });
1651
2825
  })
1652
2826
  });
1653
- return { _tag: "Overlay", volume: overlay };
2827
+ surface = overlay;
2828
+ return overlay;
1654
2829
  });
1655
2830
  /** @internal */
1656
2831
  export const make = Effect.fn("VirtualFileSystem.make")(function* (options) {
1657
- const result = yield* makeVolume({ _tag: "Empty" }, options).pipe(Effect.catchTag("ImageError", Effect.die));
1658
- if (result._tag === "Overlay")
1659
- return yield* Effect.die(new Error("empty volume constructed as overlay"));
1660
- return result.volume;
2832
+ return yield* makeVolume(VolumeSource.Empty(), options).pipe(Effect.catchTag("ImageError", Effect.die));
2833
+ });
2834
+ /** @internal */
2835
+ export const prepareEmptyLiveImage = Effect.fnUntraced(function* (options) {
2836
+ let bytes;
2837
+ yield* makeVolume(VolumeSource.Empty(), options, undefined, (initial, identity, limits) => captureLiveImage(initial, identity, limits).pipe(Effect.tap((image) => Effect.sync(() => {
2838
+ bytes = image;
2839
+ })), Effect.asVoid));
2840
+ if (bytes === undefined)
2841
+ return yield* new ImageError({ code: "InvalidStructure", field: "liveImage" });
2842
+ return bytes;
2843
+ });
2844
+ /** @internal */
2845
+ export const openImageVolume = Effect.fnUntraced(function* (image, maxImageBytes, commit) {
2846
+ const document = yield* LiveImage.decode(image, maxImageBytes);
2847
+ const prepared = new WeakMap();
2848
+ const identity = VolumeIdentity.make(document.identity);
2849
+ let shutdown;
2850
+ const limits = {
2851
+ maxEntries: document.limits.maxEntries,
2852
+ maxBytes: document.limits.maxBytes === undefined ? undefined : ByteSize.bytes(document.limits.maxBytes),
2853
+ maxFileBytes: document.limits.maxFileBytes === undefined
2854
+ ? ByteSize.bytes(0xffffffff)
2855
+ : ByteSize.bytes(document.limits.maxFileBytes),
2856
+ maxPathBytes: document.limits.maxPathBytes === undefined ? undefined : ByteSize.bytes(document.limits.maxPathBytes),
2857
+ maxPendingOperations: 64,
2858
+ maxWatchEvents: 256
2859
+ };
2860
+ const volume = yield* makeVolume(VolumeSource.Live({ document }), undefined, {
2861
+ onReady: (effect) => {
2862
+ shutdown = effect;
2863
+ },
2864
+ prepare: (candidate) => captureLiveImage(candidate, identity, limits).pipe(Effect.flatMap((bytes) => ByteSize.isGreaterThan(ByteSize.bytes(bytes.length), maxImageBytes)
2865
+ ? new FsError({ code: "StorageRejected", operation: "commit" })
2866
+ : Effect.sync(() => {
2867
+ prepared.set(candidate, bytes);
2868
+ })), Effect.mapError(() => new FsError({ code: "StorageRejected", operation: "commit" }))),
2869
+ commit: (candidate) => Effect.suspend(() => {
2870
+ const bytes = prepared.get(candidate);
2871
+ if (bytes === undefined)
2872
+ return Effect.succeed("unknown");
2873
+ prepared.delete(candidate);
2874
+ return commit(bytes);
2875
+ })
2876
+ });
2877
+ if (shutdown === undefined)
2878
+ return yield* new ImageError({ code: "InvalidStructure", field: "liveImage" });
2879
+ return Object.freeze({ volume, shutdown });
1661
2880
  });
1662
2881
  /** @internal */
1663
2882
  export const fromSnapshot = Effect.fn("VirtualFileSystem.fromSnapshot")(function* (snapshot, options) {
1664
2883
  const image = yield* Image.inspect(snapshot);
1665
- const result = yield* makeVolume({ _tag: "Snapshot", image }, options);
1666
- if (result._tag === "Overlay")
1667
- return yield* new ImageError({ code: "InvalidStructure" });
1668
- return result.volume;
2884
+ return yield* makeVolume(VolumeSource.Snapshot({ image }), options);
1669
2885
  });
1670
2886
  /** @internal */
1671
2887
  export const makeOverlay = Effect.fn("VirtualFileSystem.makeOverlay")(function* (base, options) {
1672
2888
  const image = yield* Image.inspect(base);
1673
- const result = yield* makeVolume({ _tag: "Overlay", base, image }, options);
1674
- if (result._tag === "Volume")
1675
- return yield* new ImageError({ code: "InvalidStructure" });
1676
- return result.volume;
2889
+ return yield* makeVolume(VolumeSource.Overlay({ base, image }), options);
1677
2890
  });