@effect-vfs/core 0.7.1 → 0.9.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 (79) hide show
  1. package/README.md +21 -235
  2. package/dist/Caller.d.ts +2 -0
  3. package/dist/Caller.d.ts.map +1 -1
  4. package/dist/Caller.js +3 -1
  5. package/dist/LiveVolume.d.ts +69 -1
  6. package/dist/LiveVolume.d.ts.map +1 -1
  7. package/dist/LiveVolume.js +89 -12
  8. package/dist/Search.d.ts +730 -0
  9. package/dist/Search.d.ts.map +1 -0
  10. package/dist/Search.js +576 -0
  11. package/dist/SnapshotDelta.d.ts +234 -0
  12. package/dist/SnapshotDelta.d.ts.map +1 -1
  13. package/dist/SnapshotDelta.js +107 -0
  14. package/dist/VfsError.d.ts +3 -1
  15. package/dist/VfsError.d.ts.map +1 -1
  16. package/dist/VfsError.js +3 -1
  17. package/dist/VirtualFileSystem.d.ts +78 -2
  18. package/dist/VirtualFileSystem.d.ts.map +1 -1
  19. package/dist/VirtualFileSystem.js +68 -4
  20. package/dist/Watch.d.ts +18 -0
  21. package/dist/Watch.d.ts.map +1 -1
  22. package/dist/Watch.js +12 -0
  23. package/dist/index.d.ts +7 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +7 -0
  26. package/dist/internal/callerWatch.d.ts +32 -0
  27. package/dist/internal/callerWatch.d.ts.map +1 -0
  28. package/dist/internal/callerWatch.js +184 -0
  29. package/dist/internal/confinement.d.ts +16 -0
  30. package/dist/internal/confinement.d.ts.map +1 -0
  31. package/dist/internal/confinement.js +79 -0
  32. package/dist/internal/directoryEntries.d.ts +6 -0
  33. package/dist/internal/directoryEntries.d.ts.map +1 -0
  34. package/dist/internal/directoryEntries.js +79 -0
  35. package/dist/internal/fixture.js +4 -4
  36. package/dist/internal/glob.d.ts +66 -0
  37. package/dist/internal/glob.d.ts.map +1 -0
  38. package/dist/internal/glob.js +248 -0
  39. package/dist/internal/image.d.ts +7 -1
  40. package/dist/internal/image.d.ts.map +1 -1
  41. package/dist/internal/image.js +9 -7
  42. package/dist/internal/liveImage.d.ts +1 -1
  43. package/dist/internal/liveImage.d.ts.map +1 -1
  44. package/dist/internal/liveImage.js +2 -10
  45. package/dist/internal/path.js +2 -2
  46. package/dist/internal/resolution.d.ts +139 -0
  47. package/dist/internal/resolution.d.ts.map +1 -0
  48. package/dist/internal/resolution.js +332 -0
  49. package/dist/internal/search.d.ts +24 -0
  50. package/dist/internal/search.d.ts.map +1 -0
  51. package/dist/internal/search.js +78 -0
  52. package/dist/internal/searchContent.d.ts +77 -0
  53. package/dist/internal/searchContent.d.ts.map +1 -0
  54. package/dist/internal/searchContent.js +223 -0
  55. package/dist/internal/searchModel.d.ts +83 -0
  56. package/dist/internal/searchModel.d.ts.map +1 -0
  57. package/dist/internal/searchModel.js +23 -0
  58. package/dist/internal/snapshotDelta.d.ts +470 -1
  59. package/dist/internal/snapshotDelta.d.ts.map +1 -1
  60. package/dist/internal/snapshotDelta.js +59 -25
  61. package/dist/internal/snapshotMerge.d.ts +54 -0
  62. package/dist/internal/snapshotMerge.d.ts.map +1 -0
  63. package/dist/internal/snapshotMerge.js +959 -0
  64. package/dist/internal/tokenRegistry.d.ts +83 -0
  65. package/dist/internal/tokenRegistry.d.ts.map +1 -0
  66. package/dist/internal/tokenRegistry.js +146 -0
  67. package/dist/internal/virtualFileSystem.d.ts +32 -17
  68. package/dist/internal/virtualFileSystem.d.ts.map +1 -1
  69. package/dist/internal/virtualFileSystem.js +503 -736
  70. package/dist/internal/volumeLimits.d.ts +53 -0
  71. package/dist/internal/volumeLimits.d.ts.map +1 -0
  72. package/dist/internal/volumeLimits.js +63 -0
  73. package/dist/internal/volumeState.d.ts +2 -0
  74. package/dist/internal/volumeState.d.ts.map +1 -1
  75. package/dist/internal/volumeState.js +9 -0
  76. package/dist/internal/watchHub.d.ts +6 -2
  77. package/dist/internal/watchHub.d.ts.map +1 -1
  78. package/dist/internal/watchHub.js +31 -6
  79. package/package.json +9 -3
@@ -1,5 +1,6 @@
1
1
  import * as Arr from "effect/Array";
2
2
  import * as ByteSize from "effect/ByteSize";
3
+ import * as Cause from "effect/Cause";
3
4
  import * as Clock from "effect/Clock";
4
5
  import * as Context from "effect/Context";
5
6
  import * as Crypto from "effect/Crypto";
@@ -13,18 +14,20 @@ import * as Option from "effect/Option";
13
14
  import * as Predicate from "effect/Predicate";
14
15
  import * as Result from "effect/Result";
15
16
  import * as Schema from "effect/Schema";
16
- import * as Scope from "effect/Scope";
17
17
  import * as Semaphore from "effect/Semaphore";
18
18
  import * as Stream from "effect/Stream";
19
19
  import { BytePath } from "../BytePath.js";
20
- import { CallerId, MkdirOptions, ObjectReferenceId, OpenEntryOptions, OpenOptions, RemoveOptions, RootCallerOptions, SetattrOptions, SymlinkOptions, WalkOptions, WriteFileOptions } from "../Caller.js";
20
+ import { CallerId, MkdirOptions, OpenEntryOptions, OpenOptions, RemoveOptions, RootCallerOptions, SetattrOptions, SymlinkOptions, WalkOptions, WriteFileOptions } from "../Caller.js";
21
21
  import { DirectoryHandleId, FileHandleId, SeekMode } from "../FileHandle.js";
22
22
  import { Mode, OwnerUpdate, Times } from "../Metadata.js";
23
- import { isEntry, isTarget, Target } from "../Target.js";
23
+ import { isEntry, Target } from "../Target.js";
24
24
  import { make as makeError } from "../VfsError.js";
25
- import { OverlayChange, OverlayChangesOptions, ReferenceKey, VolumeId, VolumeIdentity, VolumeIncarnation, VolumeOptions } from "../Volume.js";
26
- import { WatchOptions } from "../Watch.js";
25
+ import { OverlayChange, OverlayChangesOptions, ReferenceKey, VolumeId, VolumeIdentity, VolumeIncarnation } from "../Volume.js";
26
+ import { CallerWatchOptions, WatchOptions } from "../Watch.js";
27
27
  import { sameBytes } from "./bytes.js";
28
+ import * as CallerWatch from "./callerWatch.js";
29
+ import * as Confinement from "./confinement.js";
30
+ import * as DirectoryEntries from "./directoryEntries.js";
28
31
  import { argumentFailure, decodeConfiguration, errorPath, fsFailure, imageFailure, OpContext, retargetFailure, VfsError } from "./errors.js";
29
32
  import { KeySecret, VolumeEpoch } from "./hex128.js";
30
33
  import { hmacSha256, sameTag } from "./hmac.js";
@@ -33,10 +36,13 @@ import * as InodeTable from "./inodeTable.js";
33
36
  import * as LiveImage from "./liveImage.js";
34
37
  import * as MetadataDomain from "./metadata.js";
35
38
  import { compareOverlay } from "./overlayDiff.js";
36
- import { DOT_DOT_HEX, DOT_HEX, inputBytes, isAttachedBytes, isDotComponent, isWellFormed, joinPath, MAX_NAME_BYTES, MAX_SYMLINK_TRAVERSALS, nameBytes, ownedPath, preparePath, ROOT_PATH, SLASH_BYTE, SLASH_HEX } from "./path.js";
39
+ import { inputBytes, isAttachedBytes, isDotComponent, joinPath, nameBytes, ownedPath, ROOT_PATH, SLASH_BYTE, SLASH_HEX } from "./path.js";
40
+ import * as Resolution from "./resolution.js";
37
41
  import { VolumeTestSeams } from "./testSeams.js";
42
+ import * as TokenRegistry from "./tokenRegistry.js";
38
43
  import { makeTurnstile } from "./turnstile.js";
39
- import { byEntryName, directoryMetadata, emptyRoot, getNode, Ino, MAX_FILE_BYTES, reachableValue, ROOT_INO, storedMetadata, WALK_YIELD_INTERVAL } from "./volumeState.js";
44
+ import * as Limits from "./volumeLimits.js";
45
+ import { byEntryName, directoryMetadata, emptyState, getNode, Ino, reachableValue, ROOT_INO, storedMetadata, WALK_YIELD_INTERVAL } from "./volumeState.js";
40
46
  import * as WatchHub from "./watchHub.js";
41
47
  const EXECUTE = 0o1;
42
48
  const WRITE = 0o2;
@@ -45,11 +51,23 @@ const ANY_EXECUTE = 0o111;
45
51
  const SET_ID_BITS = 0o6000;
46
52
  const STICKY_BIT = 0o1000;
47
53
  const RELATIME_INTERVAL_NS = 86400000000000n; // relatime, as Linux mounts by default: a read refreshes an access time at least this old, 24 hours.
54
+ /** @internal */
55
+ export const memoryCommitProvider = Object.freeze({
56
+ mode: "memory",
57
+ commit: () => Effect.succeed("committed"),
58
+ shutdown: Effect.void
59
+ });
48
60
  // An unknown commit outcome stops the volume; definite rejection stops it only during cleanup.
49
61
  const offerCommit = (provider, operation, candidate, cleanup) => Effect.map(Effect.exit(Effect.suspend(() => provider.commit(candidate))), (committed) => {
50
62
  // TODO(#186): Preserve the cause of a provider defect or interruption.
51
63
  if (Exit.isFailure(committed)) {
52
- return { available: false, failure: fsFailure("OutcomeUnknown", operation) };
64
+ if (Cause.hasDies(committed.cause) || Cause.hasInterrupts(committed.cause)) {
65
+ return { available: false, failure: fsFailure("OutcomeUnknown", operation) };
66
+ }
67
+ const failure = Cause.findErrorOption(committed.cause);
68
+ return Option.isSome(failure)
69
+ ? { available: !cleanup, failure: failure.value }
70
+ : { available: false, failure: fsFailure("OutcomeUnknown", operation) };
53
71
  }
54
72
  if (committed.value === "rejected") {
55
73
  return { available: !cleanup, failure: fsFailure("StorageRejected", operation) };
@@ -64,7 +82,7 @@ const MAX_FILE_OFFSET = 0x7fffffffffffffffn;
64
82
  const isMode = Schema.is(Mode);
65
83
  const isNatural = Schema.is(Schema.Natural);
66
84
  const isLength = Schema.is(SetattrOptions.fields.size.schema);
67
- const inGroup = (identity, gid) => identity.gid === gid || identity.groups.includes(gid);
85
+ const inGroup = Resolution.inGroup;
68
86
  const isTimestamp = Schema.is(MetadataDomain.Timestamp);
69
87
  const isSeekMode = Schema.is(SeekMode);
70
88
  const decodeOwnerUpdate = Schema.decodeEffect(OwnerUpdate, { onExcessProperty: "error" });
@@ -79,7 +97,6 @@ const decodeSymlinkOptions = Schema.decodeEffect(SymlinkOptions, { onExcessPrope
79
97
  const decodeWalkOptions = Schema.decodeEffect(WalkOptions, { onExcessProperty: "error" });
80
98
  const decodeRemoveOptions = Schema.decodeEffect(RemoveOptions, { onExcessProperty: "error" });
81
99
  const decodeOpenEntryOptions = Schema.decodeEffect(OpenEntryOptions, { onExcessProperty: "error" });
82
- const encoder = new TextEncoder();
83
100
  // Whether a read at `now` refreshes a node's access time: when it is not newer than the last modification or
84
101
  // status change, or when it is at least a day old. A time already equal to `now` is never refreshed, as Linux
85
102
  // skips it, so reads at one instant (a frozen or coarse clock) store nothing.
@@ -98,14 +115,16 @@ class Draft extends Context.Service()("@effect-vfs/core/internal/virtualFileSyst
98
115
  finished: undefined,
99
116
  entries: base.entries,
100
117
  usedBytes: base.usedBytes,
118
+ renames: [],
101
119
  events: [],
102
120
  after: [],
103
121
  removed: []
104
122
  });
105
- const get = (ino) => Effect.sync(() => {
123
+ const lookup = (ino) => {
106
124
  const draft = MutableRef.get(cell);
107
125
  return draft.pending.has(ino) ? draft.pending.get(ino) : InodeTable.get(base.inodes, ino);
108
- });
126
+ };
127
+ const get = (ino) => Effect.sync(() => lookup(ino));
109
128
  const openCount = (ino) => Effect.sync(() => MutableRef.get(cell).opens?.get(ino) ?? base.open.get(ino) ?? 0);
110
129
  const put = (node, changed) => Effect.sync(() => {
111
130
  MutableRef.update(cell, (draft) => {
@@ -116,6 +135,7 @@ class Draft extends Context.Service()("@effect-vfs/core/internal/virtualFileSyst
116
135
  });
117
136
  return {
118
137
  revision,
138
+ lookup,
119
139
  get,
120
140
  put: (node) => put(node, true),
121
141
  putQuiet: (node) => put(node, false),
@@ -153,6 +173,9 @@ class Draft extends Context.Service()("@effect-vfs/core/internal/virtualFileSyst
153
173
  addUsedBytes: (amount) => Effect.sync(() => {
154
174
  MutableRef.update(cell, (draft) => ({ ...draft, usedBytes: draft.usedBytes + amount }));
155
175
  }),
176
+ rename: (rename) => Effect.sync(() => {
177
+ MutableRef.get(cell).renames.push(rename);
178
+ }),
156
179
  publish: (event) => Effect.sync(() => {
157
180
  MutableRef.get(cell).events.push(event);
158
181
  }),
@@ -189,23 +212,15 @@ class Draft extends Context.Service()("@effect-vfs/core/internal/virtualFileSyst
189
212
  return finished;
190
213
  }),
191
214
  actions: Effect.sync(() => {
192
- const { events, after, removed } = MutableRef.get(cell);
193
- return { events, after, removed };
215
+ const { events, after, removed, renames } = MutableRef.get(cell);
216
+ return { events, after, removed, renames };
194
217
  })
195
218
  };
196
219
  })
197
220
  }) {
198
221
  }
199
- const objectReferences = new WeakMap();
200
- const isFileHandle = (value) => Predicate.hasProperty(FileHandleId)(value);
201
- const files = new WeakMap();
202
- const handles = new WeakMap();
203
222
  const withMetadata = (node) => ({ ...node.metadata, revision: node.revision });
204
- const withEntries = (directory, edit) => {
205
- const entries = new Map(directory.entries);
206
- edit(entries);
207
- return { ...directory, entries };
208
- };
223
+ const withEntries = (directory, entries) => ({ ...directory, entries });
209
224
  const withoutLink = (links, parent, name) => {
210
225
  const index = links.findIndex((link) => link.parent === parent && link.name === name);
211
226
  return index < 0 ? links : [...links.slice(0, index), ...links.slice(index + 1)];
@@ -247,29 +262,17 @@ const randomHex128 = Effect.gen(function* () {
247
262
  return Hex.encode(yield* crypto.randomBytes(16));
248
263
  }).pipe(Effect.orDie);
249
264
  /** @internal */
250
- export const makeVolume = Effect.fnUntraced(function* (source, options, commitProvider, captureInitial, durability = "memory-only") {
265
+ export const makeVolume = Effect.fnUntraced(function* (source, commitProvider, options, durability = "memory-only") {
251
266
  const services = yield* Effect.context();
252
267
  // These accessors run inside synchronous transition callbacks. The runner is reached only when an invariant fails.
253
268
  const dieInvariant = (message) => Effect.runSyncWith(services)(Effect.die(message));
254
269
  const live = Predicate.isTagged("Live")(source) ? source.restored : undefined;
255
- let restoredOptions = options;
256
- if (live !== undefined) {
257
- const recovered = { identity: live.identity };
258
- if (live.limits.maxEntries !== undefined)
259
- recovered.maxEntries = live.limits.maxEntries;
260
- if (live.limits.maxBytes !== undefined)
261
- recovered.maxBytes = ByteSize.bytes(live.limits.maxBytes);
262
- if (live.limits.maxFileBytes !== undefined)
263
- recovered.maxFileBytes = ByteSize.bytes(live.limits.maxFileBytes);
264
- if (live.limits.maxPathBytes !== undefined)
265
- recovered.maxPathBytes = ByteSize.bytes(live.limits.maxPathBytes);
266
- restoredOptions = recovered;
267
- }
268
- const decoded = yield* Effect.fromResult(decodeConfiguration(VolumeOptions, restoredOptions === undefined ? {} : restoredOptions, "make"));
269
- const settings = { ...decoded };
270
- const identity = settings.identity === undefined
270
+ const decoded = yield* Effect.fromResult(Limits.configuration(options));
271
+ const requested = decoded.limits;
272
+ const limits = live === undefined ? requested : yield* Effect.fromResult(Limits.fromStored(live.limits, requested));
273
+ const identity = live?.identity ?? (decoded.identity === undefined
271
274
  ? VolumeIdentity.make(yield* randomHex128)
272
- : VolumeIdentity.make(settings.identity);
275
+ : decoded.identity);
273
276
  const incarnation = VolumeIncarnation.make(yield* randomHex128);
274
277
  const epoch = live === undefined ? VolumeEpoch.make(yield* randomHex128) : live.epoch;
275
278
  const keySecret = live === undefined ? KeySecret.make(yield* randomHex128) : live.keySecret;
@@ -288,24 +291,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
288
291
  : Effect.fail(op.fail("InvalidArgument"));
289
292
  });
290
293
  const volumeIdentity = Symbol();
291
- const tokens = new Map();
292
294
  const directoryHolds = new Map();
293
- const makeFileReference = (access, append) => ({
294
- volume: volumeIdentity,
295
- scope: undefined,
296
- closed: false,
297
- ino: undefined,
298
- offset: 0n,
299
- access,
300
- append
301
- });
302
- const makeDirectoryReference = (ino) => ({
303
- volume: volumeIdentity,
304
- scope: undefined,
305
- closed: false,
306
- ino
307
- });
308
- const maxPendingOperations = settings.maxPendingOperations ?? 64;
295
+ const maxPendingOperations = limits.maxPendingOperations;
309
296
  const permits = maxPendingOperations + 1;
310
297
  const gate = Semaphore.makeUnsafe(permits);
311
298
  // The gate hands permits to whichever waiter it can satisfy, so a change waiting for every permit would be
@@ -322,91 +309,79 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
322
309
  onNone: () => op.fail("VolumeBusy"),
323
310
  onSome: Effect.succeed
324
311
  })));
325
- const root = emptyRoot(initialTime);
326
- let state = {
327
- inodes: InodeTable.set(InodeTable.empty(), ROOT_INO, root),
328
- open: new Map(),
329
- nextInode: Ino(2),
330
- revision: 1n,
331
- entries: 0,
332
- usedBytes: 0n
333
- };
334
- const maxFileBytes = Number(ByteSize.toBigInt(settings.maxFileBytes ?? ByteSize.bytes(MAX_FILE_BYTES)));
335
- const limits = Object.freeze({
336
- maxBytes: settings.maxBytes,
337
- maxFileBytes: ByteSize.bytes(maxFileBytes),
338
- maxEntries: settings.maxEntries,
339
- maxPathBytes: settings.maxPathBytes,
340
- maxPendingOperations: settings.maxPendingOperations ?? 64,
341
- maxWatchEvents: settings.maxWatchEvents ?? 256
342
- });
312
+ let state = emptyState(initialTime);
313
+ const maxFileBytes = Number(ByteSize.toBigInt(limits.maxFileBytes));
343
314
  if (Predicate.isTagged("Restored")(source)) {
344
315
  const restored = yield* reachableValue(source.value);
345
316
  if (restored.largestFile > maxFileBytes) {
346
317
  return yield* imageFailure("snapshot", "LimitExceeded", { field: "maxFileBytes" });
347
318
  }
348
- if ((settings.maxEntries !== undefined && restored.state.entries > settings.maxEntries) ||
349
- (settings.maxBytes !== undefined && restored.state.usedBytes > ByteSize.toBigInt(settings.maxBytes))) {
319
+ if ((limits.maxEntries !== undefined && restored.state.entries > limits.maxEntries) ||
320
+ (limits.maxBytes !== undefined && restored.state.usedBytes > ByteSize.toBigInt(limits.maxBytes))) {
350
321
  return yield* imageFailure("snapshot", "LimitExceeded", { field: "volume" });
351
322
  }
352
323
  state = restored.state;
353
324
  }
354
325
  if (live !== undefined)
355
326
  state = live.value;
356
- const initialImage = captureInitial === undefined
357
- ? undefined
358
- : yield* captureInitial(state, { identity, epoch, keySecret }, limits);
359
327
  const view = (ino) => Effect.flatMap(Effect.serviceOption(Draft), (draft) => Option.isSome(draft) ? draft.value.get(ino) : Effect.sync(() => getNode(state, ino)));
328
+ // Select the operation's namespace at execution time; never retain it across operations.
329
+ const authorityView = Effect.map(Effect.serviceOption(Draft), (draft) => Option.isSome(draft) ? draft.value.lookup : (ino) => getNode(state, ino));
330
+ const registry = TokenRegistry.make(volumeIdentity, view);
331
+ const referenceFor = registry.referenceFor;
332
+ const makeFileReference = registry.file;
333
+ const makeDirectoryReference = registry.directory;
360
334
  let available = true;
361
335
  const checkAvailable = (operation) => Effect.suspend(() => available ? Effect.void : Effect.fail(fsFailure("VolumeUnavailable", operation)));
362
336
  const watchCoordinate = (effect) => changing(effect);
363
- const watchHub = yield* WatchHub.make(watchCoordinate, settings.maxWatchEvents ?? 256);
337
+ const watchHub = yield* WatchHub.make(watchCoordinate, limits.maxWatchEvents);
364
338
  const install = Effect.fnUntraced(function* (finished) {
365
339
  const before = state;
366
340
  state = yield* finished.finish;
367
341
  const actions = yield* finished.actions;
368
342
  for (const ino of actions.removed)
369
- tokens.delete(ino);
343
+ registry.forget(ino);
370
344
  for (const apply of actions.after)
371
345
  apply();
372
346
  const after = state;
373
- watchHub.publishUnsafe(() => actions.events.flatMap((events) => Array.from(events(after))), { before, after });
347
+ watchHub.publishUnsafe(() => actions.events.flatMap((events) => Array.from(events(after))), {
348
+ before,
349
+ after,
350
+ renames: actions.renames
351
+ });
374
352
  });
375
353
  const transition = Effect.fnUntraced(function* (change) {
376
354
  const current = yield* Draft.make(state);
377
355
  const value = yield* Effect.provideService(change, Draft, current);
378
356
  return [value, current];
379
357
  });
380
- const applyDirect = Effect.fnUntraced(function* (change) {
381
- const current = yield* Draft.make(state);
382
- yield* Effect.provideService(change(current), Draft, current);
383
- yield* install(current);
384
- });
385
- // Offers the finished draft's value to the store before installing it. A rejected candidate is discarded and
386
- // the volume stays available; an uncertain answer stops the volume. A change that is a cleanup runs its
387
- // release even when the store refuses, since the handle must not stay open.
388
- const committed = Effect.fnUntraced(function* (op, provider, finished, onStorageFailure) {
389
- if (yield* finished.unchanged)
358
+ // Cleanup releases local ownership even when storage refuses; that divergence disables the volume.
359
+ const committed = Effect.fnUntraced(function* (op, finished, cleanup) {
360
+ if ((yield* finished.unchanged) || (cleanup && !available))
390
361
  return yield* install(finished);
391
362
  const next = yield* finished.finish;
392
- if (provider.prepare !== undefined)
393
- yield* provider.prepare(next);
394
- const answer = yield* offerCommit(provider, op.operation, next, onStorageFailure !== undefined);
363
+ const answer = yield* offerCommit(commitProvider, op.operation, next, cleanup);
395
364
  if (!answer.available)
396
365
  available = false;
397
366
  if (answer.failure !== undefined) {
398
- if (!answer.available && onStorageFailure !== undefined)
399
- yield* onStorageFailure();
367
+ if (cleanup)
368
+ yield* install(finished);
400
369
  return yield* answer.failure;
401
370
  }
402
371
  yield* install(finished);
403
372
  });
373
+ const applyCleanup = Effect.fnUntraced(function* (change) {
374
+ const current = yield* Draft.make(state);
375
+ yield* Effect.provideService(change(current), Draft, current);
376
+ yield* committed(OpContext.make("close"), current, true);
377
+ });
404
378
  const annotateFailure = (error) => Effect.annotateCurrentSpan({ operation: error.operation, code: error.code });
405
- // Permit waits stay interruptible. A change and its publication run under every permit; with a store, the
406
- // change itself stays interruptible and only the commit and installation are not.
407
- const coordinated = (op, effect, onStorageFailure) => admit(op, changing(commitProvider === undefined
408
- ? Effect.uninterruptible(Effect.flatMap(transition(Effect.andThen(checkAvailable(op.operation), effect)), ([value, finished]) => Effect.as(install(finished), value)))
409
- : Effect.uninterruptibleMask((restore) => Effect.flatMap(restore(transition(Effect.andThen(checkAvailable(op.operation), effect))), ([value, finished]) => Effect.as(committed(op, commitProvider, finished, onStorageFailure), value))))).pipe(Effect.tapError((error) => Schema.is(VfsError)(error) ? annotateFailure(error) : Effect.void));
379
+ // Permit waits stay interruptible. Memory transitions are masked; durable transitions remain
380
+ // interruptible until the candidate is ready. Every provider commit and installation is masked.
381
+ const coordinated = (op, effect, cleanup = false) => admit(op, changing(Effect.uninterruptibleMask((restore) => {
382
+ const staged = transition(Effect.andThen(checkAvailable(op.operation), effect));
383
+ return Effect.flatMap(commitProvider.mode === "memory" ? staged : restore(staged), ([value, finished]) => Effect.as(committed(op, finished, cleanup), value));
384
+ }))).pipe(Effect.tapError((error) => Schema.is(VfsError)(error) ? annotateFailure(error) : Effect.void));
410
385
  const coordinatedRead = (op, effect) => admit(op, observing(Effect.andThen(checkAvailable(op.operation), effect))).pipe(Effect.tapError((error) => Schema.is(VfsError)(error) ? annotateFailure(error) : Effect.void));
411
386
  const refreshAccess = Effect.fnUntraced(function* (read) {
412
387
  if (refreshDue(read)) {
@@ -423,9 +398,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
423
398
  ? coordinated(op, Effect.flatMap(read, refreshAccess))
424
399
  : Effect.succeed(observed.value));
425
400
  const coordinatedCleanup = (effect) => changing(Effect.uninterruptible(effect));
426
- const shutdown = commitProvider === undefined ? undefined : changing(Effect.sync(() => {
401
+ const shutdown = commitProvider.mode === "memory" ? commitProvider.shutdown : changing(Effect.uninterruptible(Effect.andThen(Effect.sync(() => {
427
402
  available = false;
428
- }));
403
+ }), commitProvider.shutdown)));
429
404
  const pathOf = (get, ino) => {
430
405
  const names = [];
431
406
  let node = get(ino);
@@ -456,10 +431,14 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
456
431
  return SLASH_HEX + names.reverse().join(SLASH_HEX);
457
432
  });
458
433
  const entryPath = (prefix, name) => ownedPath(nameBytes(prefix + (prefix === SLASH_HEX ? "" : SLASH_HEX) + name));
459
- const publishEntry = (_tag, parent, name, ino) => Effect.flatMap(Draft, (draft) => draft.publish((installed) => {
460
- const prefix = pathOf((at) => getNode(installed, at), parent);
461
- return prefix === undefined ? [] : [{ change: { _tag, path: entryPath(prefix, name) }, parent, ino }];
462
- }));
434
+ const publishEntry = Effect.fnUntraced(function* (_tag, parent, name, ino) {
435
+ const before = state;
436
+ const draft = yield* Draft;
437
+ yield* draft.publish((installed) => {
438
+ const prefix = pathOf((at) => getNode(installed, at), parent) ?? pathOf((at) => getNode(before, at), parent);
439
+ return prefix === undefined ? [] : [{ change: { _tag, path: entryPath(prefix, name) }, parent, ino, name }];
440
+ });
441
+ });
463
442
  const publishNode = Effect.fnUntraced(function* (ino) {
464
443
  const draft = yield* Draft;
465
444
  const target = yield* draft.get(ino);
@@ -524,16 +503,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
524
503
  includes: () => true,
525
504
  rescan: () => ({ change: RescanChange.make({ path: rootPath() }), parent: ROOT_INO, ino: ROOT_INO })
526
505
  };
527
- const scopeRoot = (scope, op) => Effect.suspend(() => {
528
- const known = objectReferences.get(scope);
529
- if (known === undefined)
530
- return Effect.fail(op.fail("InvalidReference"));
531
- if (known.volume !== volumeIdentity)
532
- return Effect.fail(op.fail("ForeignReference"));
533
- const node = getNode(state, known.ino);
534
- return node === undefined || node.metadata.nlink === 0
535
- ? Effect.fail(op.fail("StaleReference"))
536
- : Effect.succeed(known.ino);
506
+ const scopeRoot = Effect.fnUntraced(function* (scope, op) {
507
+ const node = yield* TokenRegistry.nodeOrFail(yield* registry.resolve(scope, "reference"), "reference", op);
508
+ return node.metadata.nlink === 0 ? yield* op.fail("StaleReference") : node.ino;
537
509
  });
538
510
  // A watch of one object, and of its subtree when recursive. Membership is read from the installed tree when
539
511
  // each event is offered, so it follows renames of the scope and its ancestors. When the object's last name is
@@ -566,10 +538,6 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
566
538
  yield* (yield* VolumeTestSeams).betweenSnapshotAndSummary;
567
539
  return { snapshot, observation: yield* observeChanges(captured) };
568
540
  });
569
- const addressable = (ino) => {
570
- const node = getNode(state, ino);
571
- return node !== undefined && !(node.kind === "directory" && node.metadata.nlink === 0);
572
- };
573
541
  const keyTag = (ino) => {
574
542
  const message = new Uint8Array(40);
575
543
  message.set(identityBytes);
@@ -577,15 +545,6 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
577
545
  new DataView(message.buffer).setBigUint64(32, ino);
578
546
  return hmacSha256(keySecretBytes, message).subarray(0, 16);
579
547
  };
580
- const referenceFor = (ino) => {
581
- const existing = tokens.get(ino);
582
- if (existing !== undefined)
583
- return existing;
584
- const reference = Object.freeze({ [ObjectReferenceId]: true });
585
- objectReferences.set(reference, { volume: volumeIdentity, ino });
586
- tokens.set(ino, reference);
587
- return reference;
588
- };
589
548
  const holdDirectory = (ino) => {
590
549
  directoryHolds.set(ino, (directoryHolds.get(ino) ?? 0) + 1);
591
550
  };
@@ -598,32 +557,17 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
598
557
  directoryHolds.delete(ino);
599
558
  const node = getNode(state, ino);
600
559
  if (node?.kind === "directory" && node.metadata.nlink === 0) {
601
- yield* applyDirect((d) => d.remove(ino));
560
+ yield* applyCleanup((d) => d.remove(ino));
602
561
  }
603
562
  });
604
- const releaseDirectory = Effect.fnUntraced(function* (reference) {
605
- const ino = reference.ino;
606
- reference.ino = undefined;
607
- reference.closed = true;
608
- if (ino !== undefined)
609
- yield* unholdDirectory(ino);
610
- });
611
- const finalizeDirectory = (reference) => Effect.uninterruptible(Effect.suspend(() => reference.closed
612
- ? Effect.void
613
- : coordinatedCleanup(releaseDirectory(reference))));
614
- const closeReleasedScope = (reference) => Effect.suspend(() => !reference.closed || reference.scope === undefined ? Effect.void : Scope.close(reference.scope, Exit.void));
615
- const permitted = (node, identity) => {
616
- const metadata = node.metadata;
617
- const shift = metadata.uid === identity.uid ?
618
- 6
619
- : inGroup(identity, metadata.gid)
620
- ? 3
621
- : 0;
622
- return (metadata.mode >> shift) & (READ | WRITE | EXECUTE);
563
+ const releaseDirectory = (reference) => TokenRegistry.release(reference, unholdDirectory, (action) => Effect.sync(action));
564
+ const finalizeDirectory = (reference) => {
565
+ const close = coordinatedCleanup(releaseDirectory(reference));
566
+ return TokenRegistry.finalize(reference, close, Effect.ignore(close));
623
567
  };
624
- const authorize = (node, identity, bits, op) => identity.privileged || (permitted(node, identity) & bits) === bits
625
- ? Effect.void
626
- : Effect.fail(op.fail("AccessDenied"));
568
+ const closeReleasedScope = TokenRegistry.closeReleasedScope;
569
+ const permitted = Resolution.permitted;
570
+ const authorize = Resolution.authorize;
627
571
  const reclaim = Effect.fnUntraced(function* (d, ino) {
628
572
  const node = yield* d.get(ino);
629
573
  if (node === undefined)
@@ -644,7 +588,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
644
588
  });
645
589
  const atEntryLimit = Effect.gen(function* () {
646
590
  const draft = yield* Draft;
647
- return settings.maxEntries !== undefined && (yield* draft.entries) >= settings.maxEntries;
591
+ return limits.maxEntries !== undefined && (yield* draft.entries) >= limits.maxEntries;
648
592
  });
649
593
  const reserveEntry = Effect.fnUntraced(function* (op) {
650
594
  const draft = yield* Draft;
@@ -653,10 +597,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
653
597
  });
654
598
  const reserveInode = (op) => Effect.flatMap(Draft, (draft) => Effect.flatMap(draft.canAllocate, (available) => available ? Effect.void : Effect.fail(op.fail("NoSpace"))));
655
599
  const reserveBytes = Effect.fnUntraced(function* (op, bytes) {
656
- if (settings.maxBytes === undefined)
600
+ if (limits.maxBytes === undefined)
657
601
  return;
658
602
  const draft = yield* Draft;
659
- if (bytes > ByteSize.toBigInt(settings.maxBytes) - (yield* draft.usedBytes)) {
603
+ if (bytes > ByteSize.toBigInt(limits.maxBytes) - (yield* draft.usedBytes)) {
660
604
  return yield* op.fail("NoSpace");
661
605
  }
662
606
  });
@@ -670,7 +614,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
670
614
  mtimeNs: now,
671
615
  ctimeNs: now
672
616
  }
673
- }, (entries) => entries.set(name, child.ino)));
617
+ }, DirectoryEntries.set(parent.entries, name, child.ino)));
674
618
  if (child.kind === "directory")
675
619
  yield* d.put({ ...child, parent: parent.ino, name });
676
620
  else {
@@ -692,60 +636,19 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
692
636
  }
693
637
  yield* reclaim(d, child.ino);
694
638
  });
695
- const releaseFile = Effect.fnUntraced(function* (ref) {
696
- const ino = ref.ino;
697
- if (ino !== undefined) {
698
- const d = yield* Draft;
699
- yield* d.release(ino);
700
- yield* reclaim(d, ino);
701
- }
702
- ref.ino = undefined;
703
- ref.closed = true;
704
- });
705
- const releaseOpenFile = (ref) => coordinatedCleanup(Effect.suspend(() => ref.closed ? Effect.void : applyDirect(() => releaseFile(ref))));
706
- // Explicit close and scope cleanup share one release. A release whose commit fails still completes as
707
- // cleanup, so the handle never stays open; only an explicit close reports the failure. A close refused
708
- // admission never entered the volume, so it leaves the handle open for a retry.
709
- const closeFile = (ref, op, check) => coordinated(op, Effect.andThen(check, Effect.gen(function* () {
710
- const ino = ref.ino;
711
- if (ino !== undefined) {
712
- const d = yield* Draft;
713
- yield* d.release(ino);
714
- yield* reclaim(d, ino);
715
- }
639
+ const releaseFileInode = Effect.fnUntraced(function* (ino) {
716
640
  const d = yield* Draft;
717
- yield* d.afterInstall(() => {
718
- ref.ino = undefined;
719
- ref.closed = true;
720
- });
721
- })), () => applyDirect(() => releaseFile(ref))).pipe(Effect.tapError((error) => error.code === "VolumeBusy" ? Effect.void : releaseOpenFile(ref)));
722
- // Keyed on the file rather than the closed flag, so a rerun releases an open that published after a first run.
723
- // Cleanup is uninterruptible and does not need admission, so an interrupted or busy scope close still releases.
724
- const finalizeFile = (ref) => Effect.uninterruptible(Effect.suspend(() => ref.ino === undefined
725
- ? Effect.sync(() => {
726
- ref.closed = true;
727
- })
728
- : Effect.ignore(closeFile(ref, OpContext.make("close"), Effect.void)).pipe(Effect.andThen(Effect.suspend(() => ref.closed ? Effect.void : releaseOpenFile(ref))))));
729
- // Acquires a handle into its own scope, forked from the caller's. The finalizer is registered before waiting,
730
- // since a closed scope runs a new finalizer at once and the permit is not reentrant. A scope that closes
731
- // before or during acquisition interrupts it, and releasing here, under the permit, keeps the acquisition
732
- // from outliving a finalizer that already ran. A scope that closes while the acquisition commits interrupts it
733
- // too. Every exit without a handle reruns the finalizer, which may have run before the commit published and
734
- // which closing an already closed scope would not run again.
735
- const acquireHandle = Effect.fnUntraced(function* (reference, coordinate, acquire, releaseAcquired, finalize) {
736
- const scope = yield* Scope.fork(yield* Effect.scope);
737
- reference.scope = scope;
738
- yield* Scope.addFinalizer(scope, finalize);
739
- const closed = () => Predicate.isTagged(scope.state, "Closed");
740
- return yield* coordinate(Effect.suspend(() => closed() ? Effect.interrupt : acquire).pipe(Effect.tap(() => Effect.suspend(() => {
741
- if (!closed())
742
- return Effect.void;
743
- return Effect.andThen(releaseAcquired(), Effect.interrupt);
744
- })))).pipe(Effect.tap(() => Effect.suspend(() => closed() ? Effect.interrupt : Effect.void)), Effect.onError(() => Effect.andThen(finalize, Scope.close(scope, Exit.void))));
641
+ yield* d.release(ino);
642
+ yield* reclaim(d, ino);
745
643
  });
746
- const bindFile = (ref, ino) => Effect.flatMap(Draft, (draft) => draft.afterInstall(() => {
747
- ref.ino = ino;
748
- }));
644
+ const releaseFile = (ref) => TokenRegistry.release(ref, releaseFileInode, (action) => Effect.sync(action));
645
+ const releaseOpenFile = (ref) => Effect.ignore(coordinatedCleanup(Effect.suspend(() => TokenRegistry.inode(ref) === undefined ? Effect.void : applyCleanup(() => releaseFile(ref)))));
646
+ // Admission refusal leaves the handle open. Every admitted close shares the registry release,
647
+ // and a storage failure still completes cleanup while only explicit close reports the failure.
648
+ const closeFile = (ref, op, check) => coordinated(op, Effect.andThen(check, TokenRegistry.release(ref, releaseFileInode, (action) => Effect.flatMap(Draft, (draft) => draft.afterInstall(action)))), true).pipe(Effect.tapError((error) => error.code === "VolumeBusy" ? Effect.void : releaseOpenFile(ref)));
649
+ const finalizeFile = (ref) => TokenRegistry.finalize(ref, closeFile(ref, OpContext.make("close"), Effect.void), releaseOpenFile(ref));
650
+ const acquireHandle = TokenRegistry.acquire;
651
+ const bindFile = (ref, ino) => Effect.flatMap(Draft, (draft) => draft.afterInstall(() => TokenRegistry.publish(ref, ino)));
749
652
  const replaceContent = Effect.fnUntraced(function* (file, data, now, publish = true) {
750
653
  const d = yield* Draft;
751
654
  yield* d.addUsedBytes(BigInt(data.length - file.data.length));
@@ -779,11 +682,14 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
779
682
  yield* publishNode(file.ino);
780
683
  });
781
684
  const fileHandle = (ref) => {
782
- const get = Effect.fnUntraced(function* (op, access) {
783
- const node = ref.ino === undefined ? undefined : yield* view(ref.ino);
685
+ const get = Effect.fnUntraced(function* (op, access, cleanup = false) {
686
+ const ino = TokenRegistry.inode(ref);
687
+ const node = ino === undefined ? undefined : yield* view(ino);
784
688
  if (node?.kind !== "file" || (access === "read" && ref.access === "write") ||
785
689
  (access === "write" && ref.access === "read"))
786
690
  return yield* op.fail("InvalidHandle");
691
+ if (!cleanup && ref.authority !== undefined)
692
+ yield* ref.authority(node, op);
787
693
  return node;
788
694
  });
789
695
  const read = (maximum, position) => {
@@ -835,9 +741,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
835
741
  return yield* writeOp.fail("FileTooLarge");
836
742
  const start = Number(offset);
837
743
  const draft = yield* Draft;
838
- const free = settings.maxBytes === undefined
744
+ const free = limits.maxBytes === undefined
839
745
  ? BigInt(maxFileBytes)
840
- : ByteSize.toBigInt(settings.maxBytes) - (yield* draft.usedBytes);
746
+ : ByteSize.toBigInt(limits.maxBytes) - (yield* draft.usedBytes);
841
747
  const maximumEnd = BigInt(file.data.length) + free;
842
748
  const end = Number(BigInt(maxFileBytes) < maximumEnd ? BigInt(maxFileBytes) : maximumEnd);
843
749
  const count = Math.min(bytes.length, Math.max(0, end - start));
@@ -865,19 +771,19 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
865
771
  const closeOp = OpContext.make("close");
866
772
  const handle = Object.freeze({
867
773
  [FileHandleId]: true,
868
- read: Effect.fn("FileHandle.read")(function* (maximum) {
774
+ read: Effect.fnUntraced(function* (maximum) {
869
775
  return (yield* read(maximum)).bytes;
870
776
  }),
871
- pread: Effect.fn("FileHandle.pread")(function* (maximum, offset) {
777
+ pread: Effect.fnUntraced(function* (maximum, offset) {
872
778
  return yield* read(maximum, offset);
873
779
  }),
874
- write: Effect.fn("FileHandle.write")(function* (bytes) {
780
+ write: Effect.fnUntraced(function* (bytes) {
875
781
  return yield* write(bytes);
876
782
  }),
877
- pwrite: Effect.fn("FileHandle.pwrite")(function* (bytes, offset) {
783
+ pwrite: Effect.fnUntraced(function* (bytes, offset) {
878
784
  return yield* write(bytes, offset);
879
785
  }),
880
- seek: Effect.fn("FileHandle.seek")(function* (offset, mode) {
786
+ seek: Effect.fnUntraced(function* (offset, mode) {
881
787
  const op = OpContext.make("seek");
882
788
  return yield* coordinatedRead(op, Effect.uninterruptible(Effect.gen(function* () {
883
789
  const file = yield* get(op);
@@ -902,43 +808,66 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
902
808
  return next;
903
809
  })));
904
810
  }),
905
- truncate: Effect.fn("FileHandle.truncate")(function* (length) {
811
+ truncate: Effect.fnUntraced(function* (length) {
906
812
  const op = OpContext.make("truncate");
907
813
  return yield* coordinated(op, Effect.gen(function* () {
908
814
  yield* resize(yield* get(op, "write"), length, op);
909
815
  }));
910
816
  }),
911
- stat: coordinatedRead(statOp, Effect.map(Effect.suspend(() => get(statOp)), withMetadata)).pipe(Effect.withSpan("FileHandle.stat")),
912
- sync: coordinatedRead(syncOp, Effect.suspend(() => Effect.asVoid(get(syncOp)))).pipe(Effect.withSpan("FileHandle.sync")),
913
- close: closeFile(ref, closeOp, Effect.suspend(() => get(closeOp))).pipe(Effect.ensuring(closeReleasedScope(ref)), Effect.withSpan("FileHandle.close"))
817
+ stat: coordinatedRead(statOp, Effect.map(Effect.suspend(() => get(statOp)), withMetadata)),
818
+ sync: coordinatedRead(syncOp, Effect.suspend(() => Effect.asVoid(get(syncOp)))),
819
+ close: closeFile(ref, closeOp, Effect.suspend(() => get(closeOp, undefined, true))).pipe(Effect.ensuring(closeReleasedScope(ref)))
914
820
  });
915
- files.set(handle, ref);
821
+ registry.registerFile(handle, ref);
916
822
  return handle;
917
823
  };
918
- const createCaller = (reference, identity, umask) => {
919
- const referencedNode = Effect.fnUntraced(function* (target, op) {
920
- if (reference.ino === undefined)
921
- return yield* op.fail("ClosedCaller");
922
- if (!Predicate.isObject(target))
923
- return yield* op.fail("InvalidReference");
924
- const known = objectReferences.get(target);
925
- if (known === undefined)
926
- return yield* op.fail("InvalidReference");
927
- if (known.volume !== volumeIdentity) {
928
- return yield* op.fail("ForeignReference");
929
- }
930
- const node = yield* view(known.ino);
931
- if (node === undefined || (node.kind === "directory" && node.metadata.nlink === 0)) {
932
- return yield* op.fail("StaleReference");
933
- }
934
- return node;
824
+ const createCaller = (reference, identity, umask, roots = [], inherited = []) => {
825
+ const boundary = roots.length === 0 ? undefined : Confinement.make(authorityView, roots);
826
+ inherited = inherited.filter((chain, index) => !((chain.length <= roots.length && chain.every((ino, at) => roots[at] === ino)) ||
827
+ inherited.slice(0, index).some((other) => other.length === chain.length && chain.every((ino, at) => other[at] === ino))));
828
+ const additional = inherited.map((chain) => Confinement.make(authorityView, chain));
829
+ const checkAuthority = Effect.fnUntraced(function* (node, op) {
830
+ if (boundary !== undefined)
831
+ yield* boundary.check(node, op);
832
+ for (const authority of additional)
833
+ yield* authority.check(node, op);
935
834
  });
936
- const callerDirectory = Effect.fnUntraced(function* (op) {
937
- const node = reference.ino === undefined ? undefined : yield* view(reference.ino);
938
- if (node?.kind !== "directory")
939
- return yield* op.fail("ClosedCaller");
940
- return node;
835
+ const inheritedFor = (target) => Target.$is("Handle")(target)
836
+ ? registry.handleBoundaries(target.handle)
837
+ : Target.$is("Path")(target) && target.relativeTo !== undefined
838
+ ? registry.handleBoundaries(target.relativeTo)
839
+ : [];
840
+ const boundariesFor = (target) => [...(roots.length > 0 ? [roots] : []), ...inherited, ...(target === undefined ? [] : inheritedFor(target))];
841
+ const assignAuthority = (ref, target) => {
842
+ const chains = boundariesFor(target).filter((chain, index, all) => !all.some((other, at) => at !== index && other.length >= chain.length && chain.every((ino, offset) => other[offset] === ino) &&
843
+ (other.length > chain.length || at < index)));
844
+ if (chains.length === 0)
845
+ return;
846
+ ref.boundaries = chains;
847
+ const authorities = chains.map((chain) => Confinement.make(authorityView, chain));
848
+ ref.authority = Effect.fnUntraced(function* (node, op) {
849
+ for (const authority of authorities)
850
+ yield* authority.check(node, op, true);
851
+ });
852
+ };
853
+ const callerPath = boundary?.path ?? pathOfCurrent;
854
+ const resolver = Resolution.make({
855
+ caller: reference,
856
+ confinement: boundary,
857
+ additional: additional.length === 0 ? undefined : additional,
858
+ get: view,
859
+ authorityView,
860
+ identity,
861
+ maxPathBytes: limits.maxPathBytes,
862
+ registry
941
863
  });
864
+ const prepare = resolver.prepare;
865
+ const prepareEntry = resolver.prepareEntry;
866
+ const entryName = Resolution.entryName;
867
+ const resolveTarget = (target, op, options) => resolver.resolve(target, { kind: "Node", ...options }, op);
868
+ const resolveDirectory = resolver.directory;
869
+ const resolveEntry = (prepared, op) => resolver.resolve(prepared, { kind: "Parent" }, op);
870
+ const referencedNode = (target, op) => Effect.map(resolveTarget(Target.Reference({ reference: target }), op), (resolved) => resolved.node);
942
871
  const directoryNow = Effect.fnUntraced(function* (ino) {
943
872
  const node = yield* view(ino);
944
873
  if (node?.kind !== "directory") {
@@ -1008,141 +937,6 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1008
937
  revision: 0n
1009
938
  };
1010
939
  });
1011
- const lookup = Effect.fnUntraced(function* (path, base, op, options = {}, referencedBase) {
1012
- const pathOp = op.at(path.input);
1013
- const { allowMissing = false, createMissing, followFinalSymlink = true, parentOnly = false } = options;
1014
- if (reference.ino === undefined) {
1015
- return yield* pathOp.fail("ClosedCaller");
1016
- }
1017
- let current = path.absolute ? (yield* nodeNow(ROOT_INO)) : yield* callerDirectory(pathOp);
1018
- if (!path.absolute && referencedBase !== undefined) {
1019
- current = referencedBase;
1020
- yield* authorize(current, identity, EXECUTE, op);
1021
- }
1022
- else if (!path.absolute && base !== undefined) {
1023
- const target = handles.get(base);
1024
- if (target === undefined) {
1025
- return yield* pathOp.fail("InvalidHandle");
1026
- }
1027
- if (target.volume !== volumeIdentity) {
1028
- return yield* pathOp.fail("ForeignHandle");
1029
- }
1030
- const directory = target.ino === undefined ? undefined : (yield* view(target.ino));
1031
- if (directory?.kind !== "directory") {
1032
- return yield* pathOp.fail("InvalidHandle");
1033
- }
1034
- current = directory;
1035
- yield* authorize(current, identity, EXECUTE, pathOp);
1036
- }
1037
- if (current.metadata.nlink === 0) {
1038
- return yield* pathOp.fail("NotFound");
1039
- }
1040
- let work = path;
1041
- let parent;
1042
- let name;
1043
- let traversals = 0;
1044
- // Leading components that came from a symbolic link's target. Only the components the caller wrote are
1045
- // created, so a dangling link stays missing, as it does for mkdir -p.
1046
- let linked = 0;
1047
- for (let index = 0; index < work.components.length - (parentOnly ? 1 : 0); index++) {
1048
- if (current.kind !== "directory") {
1049
- return yield* pathOp.fail("NotDirectory");
1050
- }
1051
- yield* authorize(current, identity, EXECUTE, pathOp);
1052
- const component = work.components[index];
1053
- if (component === undefined)
1054
- break;
1055
- if (component === DOT_HEX)
1056
- continue;
1057
- if (component === DOT_DOT_HEX) {
1058
- current = yield* directoryNow(current.parent);
1059
- parent = undefined;
1060
- name = undefined;
1061
- continue;
1062
- }
1063
- parent = current;
1064
- name = component;
1065
- const childIno = current.entries.get(component);
1066
- const child = childIno === undefined ? undefined : (yield* view(childIno));
1067
- if (child === undefined) {
1068
- if (allowMissing && index === work.components.length - 1) {
1069
- return { node: undefined, parent, name };
1070
- }
1071
- if (createMissing !== undefined && index >= linked) {
1072
- current = yield* createMissing(current, component, index === work.components.length - 1);
1073
- continue;
1074
- }
1075
- return yield* pathOp.fail("NotFound");
1076
- }
1077
- if (child.kind === "symlink" && (followFinalSymlink || index < work.components.length - 1 || work.trailingSlash)) {
1078
- if (child.target.length === 0) {
1079
- return yield* pathOp.fail("NotFound");
1080
- }
1081
- if (++traversals > MAX_SYMLINK_TRAVERSALS) {
1082
- return yield* pathOp.fail("SymlinkLoop");
1083
- }
1084
- const suffix = work.suffixes[index] ?? new Uint8Array(0);
1085
- const remaining = work.components.length - index - 1;
1086
- if (settings.maxPathBytes !== undefined &&
1087
- ByteSize.isGreaterThan(ByteSize.bytes(child.target.length + suffix.length), settings.maxPathBytes)) {
1088
- return yield* pathOp.fail("PathTooLong");
1089
- }
1090
- const expansion = new Uint8Array(child.target.length + suffix.length);
1091
- expansion.set(child.target);
1092
- expansion.set(suffix, child.target.length);
1093
- const expanded = preparePath(ownedPath(expansion), op.operation, settings.maxPathBytes);
1094
- // The expansion is synthetic: its per-component limits are the caller's to hear about,
1095
- // but the path in the error has to be the one the caller passed in, so the
1096
- // expansion's own failure is not kept as the cause.
1097
- if (Result.isFailure(expanded)) {
1098
- return yield* pathOp.fail(expanded.failure.code);
1099
- }
1100
- work = expanded.success;
1101
- linked = work.components.length - remaining + Math.max(0, linked - index - 1);
1102
- if (work.absolute)
1103
- current = yield* nodeNow(ROOT_INO);
1104
- index = -1;
1105
- }
1106
- else
1107
- current = child;
1108
- }
1109
- if (!parentOnly && work.trailingSlash && current.kind !== "directory") {
1110
- return yield* pathOp.fail("NotDirectory");
1111
- }
1112
- return { node: current, parent, name };
1113
- });
1114
- const resolveNode = Effect.fnUntraced(function* (path, base, op, options) {
1115
- const pathOp = op.at(path.input);
1116
- const result = yield* lookup(path, base, op, options);
1117
- if (result.node === undefined) {
1118
- return yield* pathOp.fail("NotFound");
1119
- }
1120
- return result.node;
1121
- });
1122
- const locate = Effect.fnUntraced(function* (path, base, op, options) {
1123
- const pathOp = op.at(path.input);
1124
- const result = yield* lookup(path, base, op, options);
1125
- const node = result.node;
1126
- if (node === undefined) {
1127
- return yield* pathOp.fail("NotFound");
1128
- }
1129
- if (node.kind !== "directory") {
1130
- return yield* pathOp.fail("NotDirectory");
1131
- }
1132
- return node;
1133
- });
1134
- const ResolvedEntry = {
1135
- fromPath: Effect.fnUntraced(function* (path, base, op) {
1136
- const parent = yield* locate(path, base, op, { parentOnly: true });
1137
- return {
1138
- parent: parent.ino,
1139
- name: path.components.at(-1),
1140
- trailingSlash: path.trailingSlash,
1141
- addressing: "path",
1142
- op: op.at(path.input)
1143
- };
1144
- })
1145
- };
1146
940
  const grantedMode = (metadata, mode) => !identity.privileged && metadata.kind === "file" && !inGroup(identity, metadata.gid) ? mode & ~0o2000 : mode;
1147
941
  const permittedMode = (metadata, mode, op) => !identity.privileged && identity.uid !== metadata.uid
1148
942
  ? Effect.fail(op.fail("NotPermitted"))
@@ -1220,6 +1014,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1220
1014
  : Effect.void;
1221
1015
  const claimName = Effect.fnUntraced(function* (entry, trailingSlash = "allowed") {
1222
1016
  const parent = yield* directoryNow(entry.parent);
1017
+ if (boundary !== undefined && parent.ino === boundary.root &&
1018
+ (entry.name === undefined || isDotComponent(entry.name)))
1019
+ return yield* entry.op.fail("NotPermitted");
1223
1020
  if (isDotComponent(entry.name)) {
1224
1021
  return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : "AlreadyExists");
1225
1022
  }
@@ -1270,13 +1067,16 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1270
1067
  }
1271
1068
  else {
1272
1069
  entryOp = op.at(prepared.path.input);
1273
- const resolved = yield* lookup(prepared.path, prepared.base, op, {
1070
+ const resolved = yield* resolver.resolve(Target.Path(prepared.base === undefined
1071
+ ? { path: prepared.path.input }
1072
+ : { path: prepared.path.input, relativeTo: prepared.base }), {
1073
+ kind: "Node",
1274
1074
  createMissing: (parent, name, final) => Effect.flatMap(makeDirectory({ parent: parent.ino, name, trailingSlash: false, addressing: "path", op: entryOp }, final ? request : { mode: request.mode, exactMode: request.exactMode }, op), (made) => {
1275
1075
  if (!revisionsBefore.has(parent.ino))
1276
1076
  revisionsBefore.set(parent.ino, made.directory.before);
1277
1077
  return directoryNow(made.child);
1278
1078
  })
1279
- });
1079
+ }, op);
1280
1080
  node = resolved.node;
1281
1081
  }
1282
1082
  if (node?.kind !== "directory")
@@ -1323,7 +1123,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1323
1123
  mtimeNs: now,
1324
1124
  ctimeNs: now
1325
1125
  }
1326
- }, (entries) => entries.delete(name)));
1126
+ }, DirectoryEntries.remove(parent.entries, name)));
1327
1127
  yield* publishEntry("Remove", parent.ino, name, child.ino);
1328
1128
  yield* detach(child, parent.ino, name, now);
1329
1129
  yield* d.addEntries(-1);
@@ -1332,8 +1132,13 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1332
1132
  // Authorizes removing from the entry's directory and returns the named child. Only a path can name a dot
1333
1133
  // entry, and each verb reports it with its own code. Search permission comes before the lookup and write
1334
1134
  // permission after it, as on Linux; `beforeWrite` holds the verb's own checks that Linux makes in between.
1135
+ const protectsRoot = (parent, name) => boundary !== undefined &&
1136
+ ((parent.ino === boundary.root && (name === undefined || isDotComponent(name))) ||
1137
+ (name === "2e2e" && parent.parent === boundary.root));
1335
1138
  const removalTarget = Effect.fnUntraced(function* (entry, dotNameCode, beforeWrite) {
1336
1139
  const parent = yield* directoryNow(entry.parent);
1140
+ if (protectsRoot(parent, entry.name))
1141
+ return yield* entry.op.fail("NotPermitted");
1337
1142
  if (isDotComponent(entry.name)) {
1338
1143
  return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : dotNameCode);
1339
1144
  }
@@ -1342,6 +1147,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1342
1147
  const child = childIno === undefined ? undefined : (yield* view(childIno));
1343
1148
  if (child === undefined)
1344
1149
  return yield* entry.op.fail("NotFound");
1150
+ if (child.ino === boundary?.root)
1151
+ return yield* entry.op.fail("NotPermitted");
1345
1152
  const rejected = beforeWrite?.(child);
1346
1153
  if (rejected !== undefined)
1347
1154
  return yield* rejected;
@@ -1379,6 +1186,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1379
1186
  const sameDirectory = source.parent === destination.parent;
1380
1187
  const sourceName = source.name;
1381
1188
  const destinationName = destination.name;
1189
+ if (protectsRoot(sourceDirectory, sourceName) || protectsRoot(destinationDirectory, destinationName))
1190
+ return yield* source.op.fail("NotPermitted");
1382
1191
  if (isDotComponent(sourceName) || isDotComponent(destinationName)) {
1383
1192
  return yield* source.op.fail("InvalidArgument");
1384
1193
  }
@@ -1394,6 +1203,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1394
1203
  return yield* source.op.fail("NotFound");
1395
1204
  const replacedIno = destinationDirectory.entries.get(destinationName);
1396
1205
  const replaced = replacedIno === undefined ? undefined : (yield* view(replacedIno));
1206
+ if (boundary !== undefined && (child.ino === boundary.root || replaced?.ino === boundary.root)) {
1207
+ return yield* source.op.fail("NotPermitted");
1208
+ }
1397
1209
  // A trailing slash on either side asks for a directory; a missing slashed destination is fine when the
1398
1210
  // source is one, as Linux allows.
1399
1211
  if ((source.trailingSlash || destination.trailingSlash) && child.kind !== "directory") {
@@ -1442,6 +1254,11 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1442
1254
  }
1443
1255
  const now = yield* timestamp(op);
1444
1256
  const d = yield* Draft;
1257
+ yield* d.rename({
1258
+ ino: child.ino,
1259
+ from: { parent: source.parent, name: sourceName },
1260
+ to: { parent: destination.parent, name: destinationName }
1261
+ });
1445
1262
  yield* publishEntry("Remove", sourceDirectory.ino, sourceName, child.ino);
1446
1263
  yield* d.put(withEntries({
1447
1264
  ...sourceDirectory,
@@ -1451,7 +1268,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1451
1268
  mtimeNs: now,
1452
1269
  ctimeNs: now
1453
1270
  }
1454
- }, (entries) => entries.delete(sourceName)));
1271
+ }, DirectoryEntries.remove(sourceDirectory.entries, sourceName)));
1455
1272
  const destinationNow = yield* directoryNow(destination.parent);
1456
1273
  yield* d.put(withEntries({
1457
1274
  ...destinationNow,
@@ -1461,7 +1278,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1461
1278
  mtimeNs: now,
1462
1279
  ctimeNs: now
1463
1280
  }
1464
- }, (entries) => entries.set(destinationName, child.ino)));
1281
+ }, DirectoryEntries.set(destinationNow.entries, destinationName, child.ino)));
1465
1282
  const moved = yield* nodeNow(child.ino);
1466
1283
  if (moved.kind === "directory") {
1467
1284
  yield* d.put({
@@ -1545,9 +1362,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1545
1362
  const ino = opened();
1546
1363
  if (ino === undefined)
1547
1364
  return;
1548
- const draft = yield* Draft;
1549
- yield* draft.release(ino);
1550
- yield* reclaim(draft, ino);
1365
+ yield* releaseFileInode(ino);
1551
1366
  });
1552
1367
  const acquireOpenedFile = (ref, op, acquire) => {
1553
1368
  let opened;
@@ -1555,75 +1370,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1555
1370
  opened = ino;
1556
1371
  }), releasePending(() => opened), finalizeFile(ref));
1557
1372
  };
1558
- const prepare = (input, op) => preparePath(input, op.operation, settings.maxPathBytes);
1559
- const handleNode = Effect.fnUntraced(function* (handle, op) {
1560
- if (reference.ino === undefined)
1561
- return yield* op.fail("ClosedCaller");
1562
- const ref = isFileHandle(handle) ? files.get(handle) : handles.get(handle);
1563
- if (ref === undefined)
1564
- return yield* op.fail("InvalidHandle");
1565
- if (ref.volume !== volumeIdentity)
1566
- return yield* op.fail("ForeignHandle");
1567
- const node = ref.ino === undefined ? undefined : (yield* view(ref.ino));
1568
- if (node === undefined)
1569
- return yield* op.fail("InvalidHandle");
1570
- return node;
1571
- });
1572
- // A path prepares before the walk, so a malformed input fails before any lookup, and names the path on
1573
- // every failure. A reference or handle names none. The verb's `followFinalSymlink` is the default a
1574
- // path's own setting overrides; `final` makes the verb's setting win, for a verb such as `readLink` whose
1575
- // meaning fixes it.
1576
- const resolveTarget = (target, op, options) => Target.$match(target, {
1577
- Path: ({ followFinalSymlink, path, relativeTo }) => Effect.flatMap(Effect.fromResult(prepare(path, op)), (prepared) => Effect.map(resolveNode(prepared, relativeTo, op, {
1578
- followFinalSymlink: options?.final === true
1579
- ? options.followFinalSymlink ?? true
1580
- : followFinalSymlink ?? options?.followFinalSymlink ?? true
1581
- }), (node) => ({ node, op: op.at(path) }))),
1582
- Reference: ({ reference }) => Effect.map(referencedNode(reference, op), (node) => ({ node, op })),
1583
- Handle: ({ handle }) => Effect.map(handleNode(handle, op), (node) => ({ node, op }))
1584
- });
1585
- const resolveDirectory = Effect.fnUntraced(function* (target, op) {
1586
- const resolved = yield* resolveTarget(target, op);
1587
- if (resolved.node.kind !== "directory")
1588
- return yield* resolved.op.fail("NotDirectory");
1589
- return { node: resolved.node, op: resolved.op };
1590
- });
1591
- // The directory an entry names a child of. A removed directory that a handle still holds takes no new
1592
- // children, as on Linux, where creating through a descriptor of a removed directory fails with ENOENT.
1593
- const entryDirectory = Effect.fnUntraced(function* (target, op) {
1594
- const directory = yield* resolveDirectory(target, op);
1595
- if (directory.node.metadata.nlink === 0)
1596
- return yield* directory.op.fail("NotFound");
1597
- return directory;
1598
- });
1599
1373
  const asResolvedNode = (target, op, options) => Effect.map(resolveTarget(target, op, options), (resolved) => ({ ino: resolved.node.ino, op: resolved.op }));
1600
- const entryName = (input, op) => {
1601
- if (Predicate.isString(input) && !isWellFormed(input))
1602
- return Result.fail(op.fail("InvalidPathEncoding"));
1603
- const bytes = Predicate.isString(input) ? encoder.encode(input) : input;
1604
- if (!isAttachedBytes(bytes) || bytes.length === 0 || bytes.length > MAX_NAME_BYTES || bytes.includes(0) ||
1605
- bytes.includes(SLASH_BYTE))
1606
- return Result.fail(op.fail("InvalidArgument"));
1607
- return Result.succeed(Hex.encode(new Uint8Array(bytes)));
1608
- };
1609
- const prepareEntry = (input, op) => isEntry(input)
1610
- ? Result.map(entryName(input.name, op), (name) => ({ kind: "entry", directory: input.directory, name }))
1611
- : isTarget(input)
1612
- ? Result.map(prepare(input.path, op), (path) => ({ kind: "path", path, base: input.relativeTo }))
1613
- : Result.map(prepare(input, op), (path) => ({ kind: "path", path, base: undefined }));
1614
- const resolveEntry = Effect.fnUntraced(function* (prepared, op) {
1615
- if (prepared.kind === "path") {
1616
- return yield* ResolvedEntry.fromPath(prepared.path, prepared.base, op);
1617
- }
1618
- const directory = yield* entryDirectory(prepared.directory, op);
1619
- return {
1620
- parent: directory.node.ino,
1621
- name: prepared.name,
1622
- trailingSlash: false,
1623
- addressing: "entry",
1624
- op
1625
- };
1626
- });
1627
1374
  const asTarget = (input) => Target.of(input);
1628
1375
  const acquireDirectory = Effect.fnUntraced(function* (input, op) {
1629
1376
  const target = asTarget(input);
@@ -1631,8 +1378,23 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1631
1378
  return yield* acquireHandle(acquired, (acquire) => coordinatedRead(op, Effect.uninterruptible(acquire)), Effect.gen(function* () {
1632
1379
  const directory = yield* resolveDirectory(target, op);
1633
1380
  yield* authorize(directory.node, identity, EXECUTE, directory.op);
1381
+ assignAuthority(acquired, target);
1382
+ const candidates = acquired.boundaries ?? [];
1383
+ if (candidates.length === 1)
1384
+ acquired.namespaceRoots = candidates[0];
1385
+ let current = directory.node;
1386
+ while (candidates.length > 1) {
1387
+ const nearest = candidates.find((chain) => chain.at(-1) === current.ino);
1388
+ if (nearest !== undefined) {
1389
+ acquired.namespaceRoots = nearest;
1390
+ break;
1391
+ }
1392
+ if (current.ino === ROOT_INO)
1393
+ break;
1394
+ current = yield* directoryNow(current.parent);
1395
+ }
1634
1396
  holdDirectory(directory.node.ino);
1635
- acquired.ino = directory.node.ino;
1397
+ TokenRegistry.publish(acquired, directory.node.ino);
1636
1398
  return acquired;
1637
1399
  }),
1638
1400
  // Nothing to undo here: the finalizer that follows an interrupted acquisition releases the hold under
@@ -1655,31 +1417,19 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1655
1417
  const follow = target.followFinalSymlink ?? true;
1656
1418
  return yield* acquireOpenedFile(acquired, op, (opened) => Effect.gen(function* () {
1657
1419
  const path = yield* Effect.fromResult(prepared);
1658
- if (chosen.create === "exclusive") {
1659
- const existing = yield* Effect.result(lookup(path, target.relativeTo, op, { followFinalSymlink: false }));
1660
- if (Result.isSuccess(existing))
1661
- return yield* pathOp.fail("AlreadyExists");
1662
- if (existing.failure.code !== "NotFound")
1663
- return yield* existing.failure;
1664
- }
1665
- const resolved = yield* lookup(path, target.relativeTo, op, {
1666
- followFinalSymlink: follow,
1667
- allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
1668
- });
1669
- const parent = resolved.parent;
1670
- if (parent === undefined)
1671
- return yield* pathOp.fail("IsDirectory");
1672
- const name = resolved.name;
1673
- if (isDotComponent(name))
1674
- return yield* pathOp.fail("IsDirectory");
1675
- if (resolved.node === undefined && path.trailingSlash)
1420
+ const resolved = yield* resolver.resolve({ kind: "path", path, base: target.relativeTo }, {
1421
+ kind: "OrCreate",
1422
+ create: chosen.create ?? "never",
1423
+ finalSymlink: follow ? "follow" : "preserve"
1424
+ }, op);
1425
+ const { parent, name } = resolved;
1426
+ if (resolved.kind === "missing" && path.trailingSlash)
1676
1427
  return yield* pathOp.fail("IsDirectory");
1677
1428
  yield* authorize(parent, identity, EXECUTE, pathOp);
1678
- const file = resolved.node;
1679
- if (file !== undefined && chosen.create === "exclusive")
1680
- return yield* pathOp.fail("AlreadyExists");
1429
+ const file = resolved.kind === "existing" ? resolved.node : undefined;
1681
1430
  const result = yield* openFile({ parent: parent.ino, name, trailingSlash: path.trailingSlash, addressing: "path", op: pathOp }, file, chosen, acquired, op);
1682
1431
  opened(result.ino);
1432
+ assignAuthority(acquired, target);
1683
1433
  return fileHandle(acquired);
1684
1434
  }));
1685
1435
  });
@@ -1703,6 +1453,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1703
1453
  yield* draft.retain(node.ino);
1704
1454
  yield* bindFile(acquired, node.ino);
1705
1455
  opened(node.ino);
1456
+ assignAuthority(acquired, target);
1706
1457
  return fileHandle(acquired);
1707
1458
  }));
1708
1459
  });
@@ -1719,64 +1470,51 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1719
1470
  }
1720
1471
  if (chosen.exactMode && chosen.mode === undefined)
1721
1472
  return yield* op.fail("InvalidArgument");
1722
- const relativePath = preparePath(ownedPath(nameBytes(name)), op.operation, undefined);
1723
1473
  const acquired = makeFileReference(chosen.access, chosen.append ?? false);
1724
1474
  const expected = chosen.expected;
1725
1475
  return yield* acquireOpenedFile(acquired, op, (opened) => Effect.gen(function* () {
1726
- const parent = (yield* entryDirectory(entry.directory, op)).node;
1727
- if (isDotComponent(name))
1728
- return yield* op.fail("InvalidArgument");
1729
- yield* authorize(parent, identity, EXECUTE, op);
1730
- const directIno = parent.entries.get(name);
1731
- const direct = directIno === undefined ? undefined : (yield* view(directIno));
1732
- if (expected !== undefined) {
1733
- let expectedIno;
1734
- if (expected !== null) {
1735
- const observed = yield* Effect.result(referencedNode(expected, op));
1736
- if (Result.isFailure(observed)) {
1737
- return yield* op.fail("VolumeBusy", { cause: observed.failure });
1476
+ const resolved = yield* resolver.resolve({ kind: "entry", directory: entry.directory, name }, {
1477
+ kind: "OrCreate",
1478
+ create: chosen.create ?? "never",
1479
+ finalSymlink: chosen.followFinalSymlink !== false ? "follow" : "preserve",
1480
+ beforeFollow: Effect.fnUntraced(function* (_, direct) {
1481
+ if (expected !== undefined) {
1482
+ let expectedIno;
1483
+ if (expected !== null) {
1484
+ const observed = yield* Effect.result(referencedNode(expected, op));
1485
+ if (Result.isFailure(observed)) {
1486
+ return yield* op.fail("VolumeBusy", { cause: observed.failure });
1487
+ }
1488
+ expectedIno = observed.success.ino;
1489
+ }
1490
+ if (direct?.ino !== expectedIno)
1491
+ return yield* op.fail("VolumeBusy");
1738
1492
  }
1739
- expectedIno = observed.success.ino;
1740
- }
1741
- if (direct?.ino !== expectedIno)
1742
- return yield* op.fail("VolumeBusy");
1743
- }
1744
- if (chosen.expectedChild === null) {
1745
- if (direct !== undefined)
1746
- return yield* op.fail("StaleReference");
1747
- }
1748
- else if (chosen.expectedChild !== undefined) {
1749
- const expectedChild = chosen.expectedChild;
1750
- const observed = yield* referencedNode(expectedChild.reference, op);
1751
- if (direct?.ino !== observed.ino || observed.revision !== expectedChild.revision ||
1752
- observed.metadata.atimeNs !== expectedChild.atimeNs ||
1753
- observed.metadata.mtimeNs !== expectedChild.mtimeNs) {
1754
- return yield* op.fail("StaleReference");
1755
- }
1756
- }
1757
- if (direct !== undefined && chosen.create === "exclusive")
1758
- return yield* op.fail("AlreadyExists");
1759
- let file = direct;
1760
- let mutationParent = parent.ino;
1761
- let mutationName = name;
1762
- if (file?.kind === "symlink" && chosen.followFinalSymlink !== false) {
1763
- const resolved = yield* lookup(yield* Effect.fromResult(relativePath), undefined, op, {
1764
- followFinalSymlink: true,
1765
- allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
1766
- }, parent);
1767
- file = resolved.node;
1768
- if (file === undefined) {
1769
- if (resolved.parent === undefined || resolved.name === undefined) {
1770
- return yield* op.fail("IsDirectory");
1493
+ if (chosen.expectedChild === null) {
1494
+ if (direct !== undefined)
1495
+ return yield* op.fail("StaleReference");
1771
1496
  }
1772
- mutationParent = resolved.parent.ino;
1773
- mutationName = resolved.name;
1774
- }
1775
- }
1497
+ else if (chosen.expectedChild !== undefined) {
1498
+ const expectedChild = chosen.expectedChild;
1499
+ const observed = yield* referencedNode(expectedChild.reference, op);
1500
+ if (direct?.ino !== observed.ino || observed.revision !== expectedChild.revision ||
1501
+ observed.metadata.atimeNs !== expectedChild.atimeNs ||
1502
+ observed.metadata.mtimeNs !== expectedChild.mtimeNs) {
1503
+ return yield* op.fail("StaleReference");
1504
+ }
1505
+ }
1506
+ })
1507
+ }, op);
1508
+ const file = resolved.kind === "existing" ? resolved.node : undefined;
1509
+ const mutationParent = resolved.kind === "missing" ? resolved.parent.ino : resolved.origin.parent.ino;
1510
+ const mutationName = resolved.kind === "missing" ? resolved.name : resolved.origin.name;
1776
1511
  const result = yield* openFile({ parent: mutationParent, name: mutationName, trailingSlash: false, addressing: "entry", op }, file, { ...chosen, initialSize: chosen.initialSize ?? 0n }, acquired, op);
1777
1512
  opened(result.ino);
1778
1513
  return {
1779
- handle: fileHandle(acquired),
1514
+ handle: (() => {
1515
+ assignAuthority(acquired, entry.directory);
1516
+ return fileHandle(acquired);
1517
+ })(),
1780
1518
  reference: referenceFor(result.ino),
1781
1519
  created: result.created,
1782
1520
  directory: result.directory
@@ -1828,7 +1566,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1828
1566
  const prepared = prepare(target.path, op);
1829
1567
  if (Result.isFailure(prepared))
1830
1568
  return Effect.undefined;
1831
- return ResolvedEntry.fromPath(prepared.success, target.relativeTo, op).pipe(Effect.flatMap((entry) => Effect.map(view(entry.parent), (parent) => {
1569
+ return resolveEntry({ kind: "path", path: prepared.success, base: target.relativeTo }, op).pipe(Effect.flatMap((entry) => Effect.map(view(entry.parent), (parent) => {
1832
1570
  return entry.name !== undefined && parent?.kind === "directory" &&
1833
1571
  parent.entries.get(entry.name) === root.ino
1834
1572
  ? anchorFrame(parent, entry.name, root)
@@ -1843,10 +1581,14 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1843
1581
  // through a reference or a handle is held by the object it resolved to, as a descriptor holds it.
1844
1582
  // It steps down the chain in a loop, so a tree of any depth reaches its frames without deepening the stack.
1845
1583
  const reachFrame = (frame, at) => Effect.gen(function* () {
1584
+ if (boundary !== undefined)
1585
+ yield* boundary.alive(at);
1846
1586
  const chain = [];
1847
1587
  for (let link = frame; link !== undefined; link = link.up)
1848
1588
  chain.push(link);
1849
1589
  let node = yield* view((chain.at(-1) ?? frame).parent);
1590
+ if (node !== undefined)
1591
+ yield* checkAuthority(node, at);
1850
1592
  for (let index = chain.length - 1; index >= 0; index--) {
1851
1593
  const link = chain[index];
1852
1594
  if (link === undefined || node?.kind !== "directory" || node.metadata.nlink === 0)
@@ -1868,27 +1610,23 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1868
1610
  let bytes = 0n;
1869
1611
  let failure;
1870
1612
  const list = (frame) => coordinatedRead(op, Effect.suspend(() => {
1871
- if (reference.ino === undefined)
1613
+ if (TokenRegistry.inode(reference) === undefined)
1872
1614
  return Effect.fail(op.fail("ClosedCaller"));
1873
1615
  const at = op.at(locate(frame.path));
1874
1616
  return Effect.flatMap(reachFrame(frame, at), (node) => node?.kind !== "directory" || node.metadata.nlink === 0
1875
1617
  ? Effect.succeed([])
1876
1618
  : Effect.andThen(listable(node, at), walkChildren(node, frame.path, frame.depth, frame)));
1877
1619
  }));
1878
- const exceeded = (frame, field) => makeError({ code: "LimitExceeded", operation: op.operation, field, path: errorPath(locate(frame.path)) });
1879
1620
  const admit = (frame) => {
1880
- if (plan.maxDepth !== undefined && frame.depth > plan.maxDepth)
1881
- return exceeded(frame, "maxDepth");
1882
- if (plan.maxEntries !== undefined && ++entries > plan.maxEntries)
1883
- return exceeded(frame, "maxEntries");
1621
+ entries++;
1884
1622
  bytes += frame.bytes;
1885
- if (plan.maxBytes !== undefined && bytes > plan.maxBytes)
1886
- return exceeded(frame, "maxBytes");
1887
- return undefined;
1623
+ return plan.limit(frame, { entries, bytes }, "entry");
1888
1624
  };
1889
1625
  const step = Effect.gen(function* () {
1626
+ // Preserve the generic limit error E; yielding the error directly widens it to WalkFailure.
1627
+ // oxlint-disable-next-line effecttsgo/unnecessary-fail-yieldable-error -- E must stay narrow for unbounded removal.
1890
1628
  if (failure !== undefined)
1891
- return yield* failure;
1629
+ return yield* Effect.fail(failure);
1892
1630
  if (pending === undefined)
1893
1631
  pending = (yield* first).reverse();
1894
1632
  const out = [];
@@ -1910,9 +1648,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1910
1648
  out.push(frame);
1911
1649
  }
1912
1650
  if (unlisted) {
1913
- if (plan.maxDepth !== undefined && frame.depth > plan.maxDepth) {
1914
- return yield* stop(exceeded(frame, "maxDepth"));
1915
- }
1651
+ const rejected = plan.limit(frame, { entries, bytes }, "directory");
1652
+ if (rejected !== undefined)
1653
+ return yield* stop(rejected);
1916
1654
  const listed = yield* Effect.result(list(frame));
1917
1655
  if (Result.isFailure(listed))
1918
1656
  return yield* stop(listed.failure);
@@ -1937,13 +1675,15 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1937
1675
  // failure, which names the entry's path under the one the caller gave. Removing an empty directory reveals nothing, so only a directory with entries must be readable, and
1938
1676
  // no permission is changed to make one so. With `force`, the entry itself going missing leaves nothing to
1939
1677
  // empty.
1940
- const emptyDirectory = Effect.fnUntraced(function* (prepared, op, force) {
1678
+ const emptyDirectory = Effect.fnUntraced(function* (prepared, op, force, guard) {
1941
1679
  const prefix = prepared.kind === "path" ? prepared.path.bytes : nameBytes(prepared.name);
1942
1680
  const locate = (path) => ownedPath(joinPath(prefix, path));
1943
- const listable = (directory, at) => directory.entries.size === 0 ? Effect.void : authorize(directory, identity, READ, at);
1681
+ const targetGuard = guard.pipe(Effect.catchIf((error) => force && error.code === "NotFound", () => Effect.void));
1682
+ const listable = (directory, at) => Effect.andThen(targetGuard, directory.entries.size === 0 ? Effect.void : authorize(directory, identity, READ, at));
1944
1683
  let anchor;
1945
1684
  let targetAt = preparedOp(prepared, op);
1946
1685
  const first = coordinatedRead(op, Effect.gen(function* () {
1686
+ yield* guard;
1947
1687
  const entry = yield* resolveEntry(prepared, op);
1948
1688
  const parent = yield* directoryNow(entry.parent);
1949
1689
  yield* authorize(parent, identity, EXECUTE, entry.op);
@@ -1962,6 +1702,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1962
1702
  const removeFrame = (frame) => {
1963
1703
  const at = op.at(locate(frame.path));
1964
1704
  return Effect.andThen(seams.beforeTreeRemoval, coordinated(op, Effect.gen(function* () {
1705
+ yield* targetGuard;
1965
1706
  if (anchor !== undefined && !holds(yield* view(anchor.parent), anchor)) {
1966
1707
  return yield* (force ? Effect.void : Effect.fail(targetAt.fail("NotFound")));
1967
1708
  }
@@ -1976,7 +1717,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1976
1717
  }, op));
1977
1718
  })));
1978
1719
  };
1979
- const plan = { order: "post", maxDepth: undefined, maxEntries: undefined, maxBytes: undefined };
1720
+ const plan = { order: "post", limit: () => undefined };
1980
1721
  return yield* Stream.runForEach(walkFrames(op, first, plan, locate, listable), removeFrame);
1981
1722
  });
1982
1723
  const rootOp = OpContext.make("root");
@@ -1985,12 +1726,99 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1985
1726
  const prepared = yield* Effect.fromResult(prepareEntry(input, op));
1986
1727
  return yield* coordinated(op, Effect.flatMap(resolveEntry(prepared, op), (entry) => body(entry, op)));
1987
1728
  });
1729
+ const openDispatch = Effect.fnUntraced(function* (input, options) {
1730
+ const op = OpContext.make("open");
1731
+ if (isEntry(input))
1732
+ return yield* openEntry(input, options, op);
1733
+ const target = asTarget(input);
1734
+ if (Target.$is("Path")(target))
1735
+ return yield* openPath(target, options, op);
1736
+ return yield* openNode(target, options, op);
1737
+ });
1738
+ function open(input, options) {
1739
+ return openDispatch(input, options);
1740
+ }
1741
+ const removeDispatch = Effect.fnUntraced(function* (input, options) {
1742
+ const op = OpContext.make("remove");
1743
+ const prepared = yield* Effect.fromResult(prepareEntry(input, op));
1744
+ const chosen = yield* decodeRemoveOptions(options ?? {}).pipe(Effect.mapError((cause) => fail(preparedOp(prepared, op), cause)));
1745
+ const imported = prepared.kind === "entry"
1746
+ ? inheritedFor(prepared.directory)
1747
+ : prepared.base === undefined
1748
+ ? []
1749
+ : registry.handleBoundaries(prepared.base);
1750
+ const guard = chosen.expected === undefined && imported.length === 0 ? Effect.void : Effect.gen(function* () {
1751
+ const entry = yield* resolveEntry(prepared, op);
1752
+ const parent = yield* directoryNow(entry.parent);
1753
+ if (protectsRoot(parent, entry.name))
1754
+ return yield* entry.op.fail("NotPermitted");
1755
+ if (chosen.expected === undefined)
1756
+ return;
1757
+ const expected = yield* registry.referenceIdentity(chosen.expected, op);
1758
+ const expectedNode = yield* view(expected);
1759
+ if (expectedNode !== undefined && expectedNode.metadata.nlink > 0)
1760
+ yield* checkAuthority(expectedNode, op);
1761
+ yield* authorize(parent, identity, EXECUTE, entry.op);
1762
+ const found = entry.name === undefined ? parent.ino : parent.entries.get(entry.name);
1763
+ if (found === undefined)
1764
+ return yield* entry.op.fail("NotFound");
1765
+ if (found !== expected)
1766
+ return yield* entry.op.fail("VolumeBusy", { field: "expected" });
1767
+ });
1768
+ const once = coordinated(op, Effect.andThen(guard, Effect.flatMap(resolveEntry(prepared, op), (entry) => removeEntry(entry, op))));
1769
+ const target = (effect) => chosen.force === true
1770
+ ? Effect.catchIf(effect, (error) => error.code === "NotFound", () => Effect.undefined)
1771
+ : effect;
1772
+ const removed = yield* Effect.result(target(once));
1773
+ if (Result.isSuccess(removed))
1774
+ return removed.success;
1775
+ if (chosen.recursive !== true || removed.failure.code !== "NotEmpty")
1776
+ return yield* removed.failure;
1777
+ yield* emptyDirectory(prepared, op, chosen.force === true, guard);
1778
+ return yield* target(once);
1779
+ });
1780
+ function remove(input, options) {
1781
+ return removeDispatch(input, options);
1782
+ }
1988
1783
  const caller = Object.freeze({
1989
1784
  [CallerId]: true,
1990
- root: coordinatedRead(rootOp, Effect.suspend(() => reference.ino === undefined
1785
+ limits,
1786
+ watch: Effect.fnUntraced(function* (input, options) {
1787
+ const op = OpContext.make("watch");
1788
+ const decoded = yield* Effect.fromResult(decodeConfiguration(CallerWatchOptions, options ?? {}, "watch"));
1789
+ const target = asTarget(input);
1790
+ const seams = yield* VolumeTestSeams;
1791
+ const select = Effect.gen(function* () {
1792
+ yield* checkAvailable("watch");
1793
+ const resolved = yield* resolveTarget(target, op);
1794
+ yield* authorize(resolved.node, identity, READ, resolved.op);
1795
+ if (resolved.node.metadata.nlink === 0)
1796
+ return yield* resolved.op.fail("NotFound");
1797
+ if (decoded.alias === "resolved" && resolved.kind === "node" && resolved.node.kind !== "directory") {
1798
+ return yield* op.fail("InvalidArgument");
1799
+ }
1800
+ const selection = {
1801
+ roots,
1802
+ additionalRoots: [...inherited, ...inheritedFor(target)],
1803
+ target: resolved.node.ino,
1804
+ recursive: decoded.recursive ?? true
1805
+ };
1806
+ return CallerWatch.make(decoded.alias === "resolved" && resolved.kind === "entry"
1807
+ ? { ...selection, selected: { parent: resolved.parent.ino, name: resolved.name } }
1808
+ : selection);
1809
+ });
1810
+ return Stream.map(yield* admit(op, watchHub.subscribe(select, seams.afterSubscribe)), (event) => event.change);
1811
+ }),
1812
+ root: coordinatedRead(rootOp, Effect.suspend(() => TokenRegistry.inode(reference) === undefined
1991
1813
  ? Effect.fail(rootOp.fail("ClosedCaller"))
1992
- : Effect.succeed(referenceFor(ROOT_INO)))).pipe(Effect.withSpan("Caller.root")),
1993
- lookup: Effect.fn("Caller.lookup")(function* (input) {
1814
+ : Effect.gen(function* () {
1815
+ const node = yield* view(boundary?.root ?? ROOT_INO);
1816
+ if (node === undefined)
1817
+ return yield* rootOp.fail("ClosedCaller");
1818
+ yield* checkAuthority(node, rootOp);
1819
+ return referenceFor(node.ino);
1820
+ }))),
1821
+ lookup: Effect.fnUntraced(function* (input) {
1994
1822
  const op = OpContext.make("lookup");
1995
1823
  const prepared = yield* Effect.fromResult(prepareEntry(input, op));
1996
1824
  return yield* coordinatedRead(op, Effect.gen(function* () {
@@ -2006,21 +1834,24 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2006
1834
  return referenceFor(child);
2007
1835
  }));
2008
1836
  }),
2009
- parent: Effect.fn("Caller.parent")(function* (input) {
1837
+ parent: Effect.fnUntraced(function* (input) {
2010
1838
  const op = OpContext.make("parent");
2011
1839
  const target = asTarget(input);
2012
1840
  return yield* coordinatedRead(op, Effect.gen(function* () {
2013
1841
  const directory = yield* resolveDirectory(target, op);
2014
1842
  yield* authorize(directory.node, identity, EXECUTE, directory.op);
2015
- return referenceFor(directory.node.parent);
1843
+ return referenceFor(directory.node.ino === boundary?.root ||
1844
+ inheritedFor(target).some((chain) => chain.at(-1) === directory.node.ino)
1845
+ ? directory.node.ino
1846
+ : directory.node.parent);
2016
1847
  }));
2017
1848
  }),
2018
- stat: Effect.fn("Caller.stat")(function* (input) {
1849
+ stat: Effect.fnUntraced(function* (input) {
2019
1850
  const op = OpContext.make("stat");
2020
1851
  const target = asTarget(input);
2021
1852
  return yield* coordinatedRead(op, Effect.map(resolveTarget(target, op), (resolved) => withMetadata(resolved.node)));
2022
1853
  }),
2023
- readDirectory: Effect.fn("Caller.readDirectory")(function* (input) {
1854
+ readDirectory: Effect.fnUntraced(function* (input) {
2024
1855
  const op = OpContext.make("readDirectory");
2025
1856
  const target = asTarget(input);
2026
1857
  return yield* accessing(op, Effect.gen(function* () {
@@ -2036,7 +1867,12 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2036
1867
  const target = asTarget(input);
2037
1868
  const prefix = Target.$is("Path")(target) ? Result.getOrUndefined(inputBytes(target.path)) : undefined;
2038
1869
  const locate = (path) => ownedPath(prefix === undefined ? path : joinPath(prefix, path));
2039
- const readable = (directory, at) => authorize(directory, identity, READ, at);
1870
+ const authorities = inheritedFor(target).map((chain) => Confinement.make(authorityView, chain));
1871
+ const readable = Effect.fnUntraced(function* (directory, at) {
1872
+ for (const authority of authorities)
1873
+ yield* authority.check(directory, at, true);
1874
+ yield* authorize(directory, identity, READ, at);
1875
+ });
2040
1876
  return Stream.unwrap(Effect.gen(function* () {
2041
1877
  const chosen = yield* decodeWalkOptions(options ?? {}).pipe(Effect.mapError((cause) => fail(Target.$is("Path")(target) ? op.at(target.path) : op, cause)));
2042
1878
  const first = coordinatedRead(op, Effect.gen(function* () {
@@ -2044,11 +1880,20 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2044
1880
  yield* authorize(directory.node, identity, READ, directory.op);
2045
1881
  return yield* walkChildren(directory.node, new Uint8Array(0), 0, yield* pathAnchor(target, directory.node, op));
2046
1882
  }));
1883
+ const exceeded = (frame, field) => makeError({ code: "LimitExceeded", operation: op.operation, field, path: errorPath(locate(frame.path)) });
1884
+ const maxBytes = chosen.maxBytes === undefined ? undefined : ByteSize.toBigInt(chosen.maxBytes);
2047
1885
  const plan = {
2048
1886
  order: chosen.order ?? "pre",
2049
- maxDepth: chosen.maxDepth,
2050
- maxEntries: chosen.maxEntries,
2051
- maxBytes: chosen.maxBytes === undefined ? undefined : ByteSize.toBigInt(chosen.maxBytes)
1887
+ limit: (frame, progress, phase) => {
1888
+ if (chosen.maxDepth !== undefined && frame.depth > chosen.maxDepth)
1889
+ return exceeded(frame, "maxDepth");
1890
+ if (phase === "directory")
1891
+ return undefined;
1892
+ if (chosen.maxEntries !== undefined && progress.entries > chosen.maxEntries) {
1893
+ return exceeded(frame, "maxEntries");
1894
+ }
1895
+ return maxBytes !== undefined && progress.bytes > maxBytes ? exceeded(frame, "maxBytes") : undefined;
1896
+ }
2052
1897
  };
2053
1898
  return Stream.map(walkFrames(op, first, plan, locate, readable), (frame) => Object.freeze({
2054
1899
  path: ownedPath(frame.path),
@@ -2058,9 +1903,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2058
1903
  kind: frame.kind,
2059
1904
  depth: frame.depth
2060
1905
  }));
2061
- })).pipe(Stream.withSpan("Caller.walk"));
1906
+ }));
2062
1907
  },
2063
- readLink: Effect.fn("Caller.readLink")(function* (input) {
1908
+ readLink: Effect.fnUntraced(function* (input) {
2064
1909
  const op = OpContext.make("readLink");
2065
1910
  const target = asTarget(input);
2066
1911
  return yield* coordinatedRead(op, Effect.gen(function* () {
@@ -2070,29 +1915,33 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2070
1915
  return new Uint8Array(resolved.node.target);
2071
1916
  }));
2072
1917
  }),
2073
- realPath: Effect.fn("Caller.realPath")(function* (input) {
1918
+ realPath: Effect.fnUntraced(function* (input) {
2074
1919
  const op = OpContext.make("realPath");
2075
1920
  const target = asTarget(input);
2076
1921
  return yield* coordinatedRead(op, Effect.gen(function* () {
2077
1922
  if (Target.$is("Path")(target)) {
2078
1923
  const pathOp = op.at(target.path);
2079
- const result = yield* lookup(yield* Effect.fromResult(prepare(target.path, op)), target.relativeTo, op);
2080
- const directory = result.node?.kind === "directory" ? result.node : result.parent;
2081
- const prefix = directory === undefined ? SLASH_HEX : yield* pathOfCurrent(directory.ino);
1924
+ const result = yield* resolveTarget(target, op, { final: true });
1925
+ const directory = result.node.kind === "directory"
1926
+ ? result.node
1927
+ : result.kind === "entry"
1928
+ ? result.parent
1929
+ : undefined;
1930
+ const prefix = directory === undefined ? SLASH_HEX : yield* callerPath(directory.ino);
2082
1931
  if (prefix === undefined)
2083
1932
  return yield* pathOp.fail("NotFound");
2084
- if (result.node?.kind === "directory" || result.name === undefined)
1933
+ if (result.node.kind === "directory" || result.kind === "node")
2085
1934
  return ownedPath(nameBytes(prefix));
2086
1935
  return ownedPath(nameBytes(prefix + (prefix === SLASH_HEX ? "" : SLASH_HEX) + result.name));
2087
1936
  }
2088
1937
  const resolved = yield* resolveTarget(target, op);
2089
- const path = yield* pathOfCurrent(resolved.node.ino);
1938
+ const path = yield* callerPath(resolved.node.ino);
2090
1939
  if (path === undefined)
2091
1940
  return yield* resolved.op.fail("NotFound");
2092
1941
  return ownedPath(nameBytes(path));
2093
1942
  }));
2094
1943
  }),
2095
- access: Effect.fn("Caller.access")(function* (input, bits = 0) {
1944
+ access: Effect.fnUntraced(function* (input, bits = 0) {
2096
1945
  const op = OpContext.make("access");
2097
1946
  const target = asTarget(input);
2098
1947
  if (!Number.isInteger(bits) || bits < 0 || bits > (READ | WRITE | EXECUTE)) {
@@ -2112,7 +1961,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2112
1961
  return granted;
2113
1962
  }));
2114
1963
  }),
2115
- readFile: Effect.fn("Caller.readFile")(function* (input) {
1964
+ readFile: Effect.fnUntraced(function* (input) {
2116
1965
  const op = OpContext.make("readFile");
2117
1966
  const target = asTarget(input);
2118
1967
  return yield* accessing(op, Effect.gen(function* () {
@@ -2127,7 +1976,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2127
1976
  return { value: data, access: { node, now: yield* timestamp(op) } };
2128
1977
  }));
2129
1978
  }),
2130
- writeFile: Effect.fn("Caller.writeFile")(function* (input, bytes, options) {
1979
+ writeFile: Effect.fnUntraced(function* (input, bytes, options) {
2131
1980
  const op = OpContext.make("writeFile");
2132
1981
  const prepared = yield* Effect.fromResult(prepareEntry(input, op));
2133
1982
  const optionsOp = preparedOp(prepared, op);
@@ -2136,66 +1985,19 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2136
1985
  const captured = new Uint8Array(bytes);
2137
1986
  const chosen = yield* decodeWriteFileOptions(options).pipe(Effect.mapError((cause) => fail(optionsOp, cause)));
2138
1987
  return yield* coordinated(op, Effect.gen(function* () {
2139
- let entryOp = op;
2140
- let parent;
2141
- let name;
2142
- let found;
2143
- let trailingSlash = false;
2144
- if (prepared.kind === "path") {
2145
- const path = prepared.path;
2146
- entryOp = op.at(path.input);
2147
- if (chosen.create === "exclusive") {
2148
- const exists = yield* Effect.result(lookup(path, prepared.base, op, { followFinalSymlink: false }));
2149
- if (Result.isSuccess(exists))
2150
- return yield* entryOp.fail("AlreadyExists");
2151
- if (exists.failure.code !== "NotFound")
2152
- return yield* exists.failure;
2153
- }
2154
- const resolved = yield* lookup(path, prepared.base, op, {
2155
- followFinalSymlink: chosen.replaceFinalSymlink !== true && chosen.followFinalSymlink !== false,
2156
- allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
2157
- });
2158
- if (resolved.parent === undefined || resolved.name === undefined || resolved.node?.kind === "directory") {
2159
- return yield* entryOp.fail("IsDirectory");
2160
- }
2161
- parent = resolved.parent;
2162
- name = resolved.name;
2163
- found = resolved.node;
2164
- trailingSlash = path.trailingSlash;
2165
- }
2166
- else {
2167
- const directory = yield* entryDirectory(prepared.directory, op);
2168
- if (isDotComponent(prepared.name))
2169
- return yield* op.fail("InvalidArgument");
2170
- yield* authorize(directory.node, identity, EXECUTE, op);
2171
- parent = directory.node;
2172
- name = prepared.name;
2173
- const childIno = parent.entries.get(name);
2174
- let child = childIno === undefined ? undefined : (yield* view(childIno));
2175
- if (child?.kind === "symlink" && chosen.replaceFinalSymlink !== true &&
2176
- chosen.followFinalSymlink !== false) {
2177
- const resolved = yield* lookup(yield* Effect.fromResult(preparePath(ownedPath(nameBytes(name)), op.operation, undefined)), undefined, op, {
2178
- followFinalSymlink: true,
2179
- allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
2180
- }, parent);
2181
- if (resolved.parent === undefined || resolved.name === undefined || resolved.node?.kind === "directory") {
2182
- return yield* op.fail("IsDirectory");
2183
- }
2184
- parent = resolved.parent;
2185
- name = resolved.name;
2186
- child = resolved.node;
2187
- }
2188
- if (child === undefined && (chosen.create === undefined || chosen.create === "never")) {
2189
- return yield* op.fail("NotFound");
2190
- }
2191
- if (child !== undefined && chosen.create === "exclusive")
2192
- return yield* op.fail("AlreadyExists");
2193
- if (child?.kind === "directory")
2194
- return yield* op.fail("IsDirectory");
2195
- found = child;
2196
- }
2197
- if (found === undefined && trailingSlash)
1988
+ const resolved = yield* resolver.resolve(prepared, {
1989
+ kind: "OrCreate",
1990
+ create: chosen.create ?? "never",
1991
+ finalSymlink: chosen.replaceFinalSymlink !== true && chosen.followFinalSymlink !== false
1992
+ ? "follow"
1993
+ : "preserve",
1994
+ entryExclusive: "afterSymlink"
1995
+ }, op);
1996
+ const { parent, name, trailingSlash, op: entryOp } = resolved;
1997
+ const found = resolved.kind === "existing" ? resolved.node : undefined;
1998
+ if (found?.kind === "directory" || (found === undefined && trailingSlash)) {
2198
1999
  return yield* entryOp.fail("IsDirectory");
2000
+ }
2199
2001
  const replaced = found?.kind === "symlink" ? found : undefined;
2200
2002
  if (replaced !== undefined && !chosen.replaceFinalSymlink)
2201
2003
  return yield* entryOp.fail("SymlinkLoop");
@@ -2260,18 +2062,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2260
2062
  }
2261
2063
  }));
2262
2064
  }),
2263
- // SAFETY: the overloads pair an entry with entry options and a target with open options; the decoders below
2264
- // reject the other shape, so one implementation serves both.
2265
- open: Effect.fn("Caller.open")(function* (input, options) {
2266
- const op = OpContext.make("open");
2267
- if (isEntry(input))
2268
- return yield* openEntry(input, options, op);
2269
- const target = asTarget(input);
2270
- if (Target.$is("Path")(target))
2271
- return yield* openPath(target, options, op);
2272
- return yield* openNode(target, options, op);
2273
- }),
2274
- mkdir: Effect.fn("Caller.mkdir")(function* (input, options = {}) {
2065
+ open,
2066
+ mkdir: Effect.fnUntraced(function* (input, options = {}) {
2275
2067
  const op = OpContext.make("mkdir");
2276
2068
  const prepared = yield* Effect.fromResult(prepareEntry(input, op));
2277
2069
  const optionsOp = preparedOp(prepared, op);
@@ -2287,7 +2079,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2287
2079
  return { reference: referenceFor(child), directory };
2288
2080
  }));
2289
2081
  }),
2290
- symlink: Effect.fn("Caller.symlink")(function* (target, input, options = {}) {
2082
+ symlink: Effect.fnUntraced(function* (target, input, options = {}) {
2291
2083
  const op = OpContext.make("symlink");
2292
2084
  const targetOp = op.at(target);
2293
2085
  const prepared = yield* Effect.fromResult(prepareEntry(input, op));
@@ -2306,7 +2098,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2306
2098
  return { reference: referenceFor(child), directory };
2307
2099
  }));
2308
2100
  }),
2309
- link: Effect.fn("Caller.link")(function* (sourceInput, input) {
2101
+ link: Effect.fnUntraced(function* (sourceInput, input) {
2310
2102
  const op = OpContext.make("link");
2311
2103
  const source = asTarget(sourceInput);
2312
2104
  const prepared = yield* Effect.fromResult(prepareEntry(input, op));
@@ -2322,30 +2114,14 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2322
2114
  return { reference: referenceFor(node.ino), directory };
2323
2115
  }));
2324
2116
  }),
2325
- unlink: Effect.fn("Caller.unlink")(function* (input) {
2117
+ unlink: Effect.fnUntraced(function* (input) {
2326
2118
  return yield* entryVerb("unlink", input, unlinkEntry);
2327
2119
  }),
2328
- rmdir: Effect.fn("Caller.rmdir")(function* (input) {
2120
+ rmdir: Effect.fnUntraced(function* (input) {
2329
2121
  return yield* entryVerb("rmdir", input, rmdirEntry);
2330
2122
  }),
2331
- // SAFETY: the overloads differ only in whether `force` may leave nothing removed; one body serves both.
2332
- remove: Effect.fn("Caller.remove")(function* (input, options) {
2333
- const op = OpContext.make("remove");
2334
- const prepared = yield* Effect.fromResult(prepareEntry(input, op));
2335
- const chosen = yield* decodeRemoveOptions(options ?? {}).pipe(Effect.mapError((cause) => fail(preparedOp(prepared, op), cause)));
2336
- const once = coordinated(op, Effect.flatMap(resolveEntry(prepared, op), (entry) => removeEntry(entry, op)));
2337
- const target = (effect) => chosen.force === true
2338
- ? Effect.catchIf(effect, (error) => error.code === "NotFound", () => Effect.undefined)
2339
- : effect;
2340
- const removed = yield* Effect.result(target(once));
2341
- if (Result.isSuccess(removed))
2342
- return removed.success;
2343
- if (chosen.recursive !== true || removed.failure.code !== "NotEmpty")
2344
- return yield* removed.failure;
2345
- yield* emptyDirectory(prepared, op, chosen.force === true);
2346
- return yield* target(once);
2347
- }),
2348
- rename: Effect.fn("Caller.rename")(function* (fromInput, toInput) {
2123
+ remove,
2124
+ rename: Effect.fnUntraced(function* (fromInput, toInput) {
2349
2125
  const op = OpContext.make("rename");
2350
2126
  const from = yield* Effect.fromResult(prepareEntry(fromInput, op));
2351
2127
  const to = yield* Effect.fromResult(prepareEntry(toInput, op));
@@ -2355,34 +2131,34 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2355
2131
  return yield* renameEntry(source, destination, op);
2356
2132
  }));
2357
2133
  }),
2358
- chmod: Effect.fn("Caller.chmod")(function* (input, mode) {
2134
+ chmod: Effect.fnUntraced(function* (input, mode) {
2359
2135
  const op = OpContext.make("chmod");
2360
2136
  const target = asTarget(input);
2361
2137
  if (!isMode(mode))
2362
2138
  return yield* op.fail("InvalidArgument");
2363
2139
  yield* changeAttributes(() => asResolvedNode(target, op), { mode }, op);
2364
2140
  }),
2365
- chown: Effect.fn("Caller.chown")(function* (input, owner) {
2141
+ chown: Effect.fnUntraced(function* (input, owner) {
2366
2142
  const op = OpContext.make("chown");
2367
2143
  const target = asTarget(input);
2368
2144
  const decoded = yield* decodeOwnerUpdate(owner).pipe(Effect.mapError((cause) => fail(op, cause)));
2369
2145
  yield* changeAttributes(() => asResolvedNode(target, op), { owner: decoded }, op);
2370
2146
  }),
2371
- utimes: Effect.fn("Caller.utimes")(function* (input, times) {
2147
+ utimes: Effect.fnUntraced(function* (input, times) {
2372
2148
  const op = OpContext.make("utimes");
2373
2149
  const target = asTarget(input);
2374
2150
  const decoded = yield* decodeTimes(times).pipe(Effect.mapError((cause) => fail(op, cause)));
2375
2151
  yield* changeAttributes(() => asResolvedNode(target, op), { times: decoded }, op);
2376
2152
  }),
2377
2153
  // A negative length fails before the target resolves, as truncate(2) rejects it before the lookup.
2378
- truncate: Effect.fn("Caller.truncate")(function* (input, length) {
2154
+ truncate: Effect.fnUntraced(function* (input, length) {
2379
2155
  const op = OpContext.make("truncate");
2380
2156
  const target = asTarget(input);
2381
2157
  if (!isLength(length))
2382
2158
  return yield* op.fail("InvalidArgument");
2383
2159
  yield* changeAttributes(() => asResolvedNode(target, op), { size: length }, op);
2384
2160
  }),
2385
- setattr: Effect.fn("Caller.setattr")(function* (input, attributes) {
2161
+ setattr: Effect.fnUntraced(function* (input, attributes) {
2386
2162
  const op = OpContext.make("setattr");
2387
2163
  const target = asTarget(input);
2388
2164
  // A caller outside TypeScript can pass anything, so a non-object fails typed rather than as a defect;
@@ -2408,29 +2184,36 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2408
2184
  : yield* decodeExpected(attributes.expected).pipe(Effect.mapError((cause) => fail(op, cause, "expected")));
2409
2185
  yield* changeAttributes(() => asResolvedNode(target, op), { size: attributes.size, mode: attributes.mode, owner, times, expected }, op);
2410
2186
  }),
2411
- withDirectory: Effect.fn("Caller.withDirectory")(function* (input) {
2187
+ withRoot: Effect.fnUntraced(function* (input) {
2188
+ const acquired = yield* acquireDirectory(input, OpContext.make("withRoot"));
2189
+ return createCaller(acquired, identity, umask, [...roots, TokenRegistry.inode(acquired)], acquired.boundaries ?? []);
2190
+ }),
2191
+ withDirectory: Effect.fnUntraced(function* (input) {
2412
2192
  const acquired = yield* acquireDirectory(input, OpContext.make("withDirectory"));
2413
- return createCaller(acquired, identity, umask);
2193
+ return createCaller(acquired, identity, umask, acquired.namespaceRoots ?? roots, acquired.boundaries ?? []);
2414
2194
  }),
2415
- openDirectory: Effect.fn("Caller.openDirectory")(function* (input) {
2195
+ openDirectory: Effect.fnUntraced(function* (input) {
2416
2196
  const acquired = yield* acquireDirectory(input, OpContext.make("openDirectory"));
2417
2197
  const statOp = OpContext.make("stat");
2418
2198
  const handle = Object.freeze({
2419
2199
  [DirectoryHandleId]: true,
2420
2200
  stat: coordinatedRead(statOp, Effect.gen(function* () {
2421
- const node = acquired.ino === undefined ? undefined : yield* view(acquired.ino);
2201
+ const ino = TokenRegistry.inode(acquired);
2202
+ const node = ino === undefined ? undefined : yield* view(ino);
2203
+ if (node !== undefined && acquired.authority !== undefined)
2204
+ yield* acquired.authority(node, statOp);
2422
2205
  return yield* (node === undefined
2423
2206
  ? Effect.fail(statOp.fail("InvalidHandle"))
2424
2207
  : Effect.succeed(withMetadata(node)));
2425
- })).pipe(Effect.withSpan("DirectoryHandle.stat")),
2208
+ })),
2426
2209
  close: coordinatedCleanup(Effect.suspend(() => {
2427
- if (acquired.ino === undefined) {
2210
+ if (TokenRegistry.inode(acquired) === undefined) {
2428
2211
  return Effect.fail(OpContext.make("close").fail("InvalidHandle"));
2429
2212
  }
2430
2213
  return releaseDirectory(acquired);
2431
- })).pipe(Effect.ensuring(closeReleasedScope(acquired)), Effect.withSpan("DirectoryHandle.close"))
2214
+ })).pipe(Effect.ensuring(closeReleasedScope(acquired)))
2432
2215
  });
2433
- handles.set(handle, acquired);
2216
+ registry.registerDirectory(handle, acquired);
2434
2217
  return handle;
2435
2218
  })
2436
2219
  });
@@ -2442,9 +2225,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2442
2225
  identity,
2443
2226
  incarnation,
2444
2227
  limits,
2445
- usage: coordinatedRead(OpContext.make("usage"), Effect.sync(() => ({ usedBytes: state.usedBytes, entries: state.entries })))
2446
- .pipe(Effect.withSpan("Volume.usage")),
2447
- watch: Effect.fn("Volume.watch")(function* (options) {
2228
+ usage: coordinatedRead(OpContext.make("usage"), Effect.sync(() => ({ usedBytes: state.usedBytes, entries: state.entries }))),
2229
+ watch: Effect.fnUntraced(function* (options) {
2448
2230
  const op = OpContext.make("watch");
2449
2231
  const decoded = yield* Effect.fromResult(decodeConfiguration(WatchOptions, options === undefined ? {} : options, "watch"));
2450
2232
  const recursive = decoded.recursive ?? true;
@@ -2458,38 +2240,33 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2458
2240
  const stream = yield* admit(op, watchHub.subscribe(select, seams.afterSubscribe));
2459
2241
  return Stream.map(stream, (event) => event.change);
2460
2242
  }),
2461
- snapshot: coordinatedRead(OpContext.make("snapshot"), Effect.sync(() => Image.make(state))).pipe(Effect.withSpan("Volume.snapshot")),
2462
- referenceKey: Effect.fn("Volume.referenceKey")(function* (reference) {
2243
+ snapshot: coordinatedRead(OpContext.make("snapshot"), Effect.sync(() => Image.make(state))),
2244
+ referenceKey: Effect.fnUntraced(function* (reference) {
2463
2245
  const op = OpContext.make("referenceKey");
2464
- return yield* coordinatedRead(op, Effect.suspend(() => {
2465
- const known = Predicate.isObject(reference) ? objectReferences.get(reference) : undefined;
2466
- if (known === undefined)
2467
- return Effect.fail(op.fail("InvalidReference"));
2468
- if (known.volume !== volumeIdentity)
2469
- return Effect.fail(op.fail("ForeignReference"));
2470
- if (!addressable(known.ino))
2471
- return Effect.fail(op.fail("StaleReference"));
2472
- const ino = BigInt(known.ino);
2473
- return Effect.succeed({ identity: identityBytes.slice(), epoch: epochBytes.slice(), ino, tag: keyTag(ino) });
2246
+ return yield* coordinatedRead(op, Effect.gen(function* () {
2247
+ const node = yield* TokenRegistry.nodeOrFail(yield* registry.resolve(reference, "reference"), "reference", op);
2248
+ const ino = BigInt(node.ino);
2249
+ return { identity: identityBytes.slice(), epoch: epochBytes.slice(), ino, tag: keyTag(ino) };
2474
2250
  }));
2475
2251
  }),
2476
- resolveReferenceKey: Effect.fn("Volume.resolveReferenceKey")(function* (key) {
2252
+ resolveReferenceKey: Effect.fnUntraced(function* (key) {
2477
2253
  const op = OpContext.make("resolveReferenceKey");
2478
- return yield* coordinatedRead(op, Effect.suspend(() => {
2254
+ return yield* coordinatedRead(op, Effect.gen(function* () {
2479
2255
  if (!Schema.is(ReferenceKey)(key))
2480
- return Effect.fail(op.fail("InvalidReference"));
2256
+ return yield* op.fail("InvalidReference");
2481
2257
  // Another epoch numbered its objects on its own, so its key is another volume's even under this identity.
2482
2258
  if (!sameBytes(key.identity, identityBytes) || !sameBytes(key.epoch, epochBytes)) {
2483
- return Effect.fail(op.fail("ForeignReference"));
2259
+ return yield* op.fail("ForeignReference");
2484
2260
  }
2485
2261
  // Checked before the inode is looked up, so a guessed key learns nothing about which numbers are in use.
2486
2262
  if (!sameTag(key.tag, keyTag(key.ino)))
2487
- return Effect.fail(op.fail("InvalidReference"));
2263
+ return yield* op.fail("InvalidReference");
2488
2264
  const ino = Ino(Number(key.ino));
2489
- return addressable(ino) ? Effect.succeed(referenceFor(ino)) : Effect.fail(op.fail("StaleReference"));
2265
+ yield* TokenRegistry.nodeOrFail(yield* registry.resolveInode(ino), "reference", op);
2266
+ return referenceFor(ino);
2490
2267
  }));
2491
2268
  }),
2492
- caller: Effect.fn("Volume.caller")(function* (options) {
2269
+ caller: Effect.fnUntraced(function* (options) {
2493
2270
  const decoded = yield* Effect.fromResult(decodeConfiguration(RootCallerOptions, options === undefined ? {} : options, "caller"));
2494
2271
  const chosen = decoded.identity ?? { uid: 0, gid: 0, groups: [], privileged: true };
2495
2272
  const identity = Object.freeze({ ...chosen, groups: Object.freeze([...chosen.groups]) });
@@ -2500,47 +2277,37 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2500
2277
  changes: coordinatedRead(OpContext.make("changes"), Effect.suspend(() => observeChanges(state))),
2501
2278
  capture: coordinatedRead(OpContext.make("capture"), captureState())
2502
2279
  });
2503
- return Object.freeze({ volume, shutdown, initialImage, observe });
2280
+ return Object.freeze({ volume, shutdown, observe });
2504
2281
  });
2505
2282
  /** @internal */
2506
- export const make = Effect.fn("VirtualFileSystem.make")(function* (options) {
2507
- return (yield* makeVolume(VolumeSource.Empty(), options).pipe(Effect.catchIf((error) => Schema.is(VfsError)(error) && error.code !== "InvalidArgument", Effect.die))).volume;
2283
+ export const make = Effect.fnUntraced(function* (options) {
2284
+ return (yield* makeVolume(VolumeSource.Empty(), memoryCommitProvider, options).pipe(Effect.catchIf((error) => Schema.is(VfsError)(error) && error.code !== "InvalidArgument", Effect.die))).volume;
2508
2285
  });
2509
2286
  /** @internal */
2510
2287
  export const prepareEmptyLiveImage = Effect.fnUntraced(function* (options) {
2511
- const { initialImage } = yield* Effect.mapError(makeVolume(VolumeSource.Empty(), options, undefined, LiveImage.encode), (error) => retargetFailure("prepareEmptyImage", error));
2512
- if (initialImage === undefined)
2513
- return yield* imageFailure("openImage", "InvalidStructure", { field: "liveImage" });
2514
- return initialImage;
2288
+ const decoded = yield* Effect.fromResult(Limits.configuration(options, "prepareEmptyImage"));
2289
+ const limits = decoded.limits;
2290
+ const identity = decoded.identity ?? VolumeIdentity.make(yield* randomHex128);
2291
+ const epoch = VolumeEpoch.make(yield* randomHex128);
2292
+ const keySecret = KeySecret.make(yield* randomHex128);
2293
+ const now = yield* Clock.currentTimeNanos;
2294
+ if (!isTimestamp(now))
2295
+ return yield* argumentFailure("prepareEmptyImage", "clock.currentTimeNanos");
2296
+ return yield* LiveImage.encode(emptyState(now), { identity, epoch, keySecret }, limits).pipe(Effect.mapError((error) => retargetFailure("prepareEmptyImage", error)));
2515
2297
  });
2516
2298
  /** @internal */
2517
- export const openImageVolume = Effect.fnUntraced(function* (image, maxImageBytes, commit, durability = "memory-only") {
2299
+ export const openImageVolume = Effect.fnUntraced(function* (image, maxImageBytes, commit, durability = "memory-only", options) {
2518
2300
  const restored = yield* LiveImage.decode(image, maxImageBytes);
2519
- let prepared;
2520
- const { limits: stored } = restored;
2521
- const commitOp = OpContext.make("commit");
2522
- const limits = {
2523
- maxEntries: stored.maxEntries,
2524
- maxBytes: stored.maxBytes === undefined ? undefined : ByteSize.bytes(stored.maxBytes),
2525
- maxFileBytes: ByteSize.bytes(stored.maxFileBytes ?? MAX_FILE_BYTES),
2526
- maxPathBytes: stored.maxPathBytes === undefined ? undefined : ByteSize.bytes(stored.maxPathBytes),
2527
- maxPendingOperations: 64,
2528
- maxWatchEvents: 256
2301
+ const requested = yield* Effect.fromResult(Limits.fromOptions(options, "openImage"));
2302
+ const limits = yield* Effect.fromResult(Limits.fromStored(restored.limits, requested));
2303
+ const provider = {
2304
+ mode: "durable",
2305
+ shutdown: Effect.void,
2306
+ commit: (candidate) => LiveImage.encode(candidate, restored, limits).pipe(Effect.mapError((cause) => makeError({ code: "StorageRejected", operation: "commit", cause })), Effect.flatMap((bytes) => ByteSize.isGreaterThan(ByteSize.bytes(bytes.length), maxImageBytes)
2307
+ ? Effect.fail(makeError({ code: "StorageRejected", operation: "commit" }))
2308
+ : commit(bytes)))
2529
2309
  };
2530
- const { volume, shutdown } = yield* makeVolume(VolumeSource.Live({ restored }), undefined, {
2531
- prepare: (candidate) => LiveImage.encode(candidate, restored, limits).pipe(Effect.mapError((cause) => commitOp.fail("StorageRejected", { cause })), Effect.flatMap((bytes) => ByteSize.isGreaterThan(ByteSize.bytes(bytes.length), maxImageBytes)
2532
- ? commitOp.fail("StorageRejected")
2533
- : Effect.sync(() => {
2534
- prepared = bytes;
2535
- }))),
2536
- commit: () => Effect.suspend(() => {
2537
- const bytes = prepared;
2538
- prepared = undefined;
2539
- return bytes === undefined ? Effect.succeed("unknown") : commit(bytes);
2540
- })
2541
- }, undefined, durability);
2542
- if (shutdown === undefined)
2543
- return yield* imageFailure("openImage", "InvalidStructure", { field: "liveImage" });
2310
+ const { volume, shutdown } = yield* makeVolume(VolumeSource.Live({ restored }), provider, options, durability);
2544
2311
  return Object.freeze({ volume, shutdown });
2545
2312
  });
2546
2313
  /** @internal */
@@ -2552,22 +2319,22 @@ const changeOptions = (options) => {
2552
2319
  return Effect.fromResult(decodeConfiguration(OverlayChangesOptions, options === undefined ? {} : options, "changes"));
2553
2320
  };
2554
2321
  /** @internal */
2555
- export const fromSnapshot = Effect.fn("VirtualFileSystem.fromSnapshot")(function* (snapshot, options) {
2556
- return (yield* makeVolume(VolumeSource.Restored({ value: yield* Image.valueOf(snapshot) }), options)).volume;
2322
+ export const fromSnapshot = Effect.fnUntraced(function* (snapshot, options) {
2323
+ return (yield* makeVolume(VolumeSource.Restored({ value: yield* Image.valueOf(snapshot) }), memoryCommitProvider, options)).volume;
2557
2324
  }, Effect.mapError((error) => retargetFailure("fromSnapshot", error)));
2558
2325
  /** @internal */
2559
- export const makeOverlay = Effect.fn("VirtualFileSystem.makeOverlay")(function* (base, options) {
2326
+ export const makeOverlay = Effect.fnUntraced(function* (base, options) {
2560
2327
  const value = yield* Image.valueOf(base);
2561
- const made = yield* Effect.mapError(makeVolume(VolumeSource.Restored({ value }), options), (error) => retargetFailure("makeOverlay", error));
2328
+ const made = yield* Effect.mapError(makeVolume(VolumeSource.Restored({ value }), memoryCommitProvider, options), (error) => retargetFailure("makeOverlay", error));
2562
2329
  const baseObservation = yield* observeChanges(value);
2563
2330
  const overlay = Object.freeze({
2564
2331
  ...made.volume,
2565
- changes: Effect.fn("OverlayVolume.changes")(function* (options) {
2332
+ changes: Effect.fnUntraced(function* (options) {
2566
2333
  const selected = yield* changeOptions(options);
2567
2334
  const current = yield* made.observe.changes;
2568
2335
  return publicChanges(compareOverlay(baseObservation, current, selected.includeTimestamps ?? false));
2569
2336
  }),
2570
- capture: Effect.fn("OverlayVolume.capture")(function* (options) {
2337
+ capture: Effect.fnUntraced(function* (options) {
2571
2338
  const selected = yield* changeOptions(options);
2572
2339
  const current = yield* made.observe.capture;
2573
2340
  return Object.freeze({