@effect-vfs/core 0.7.1 → 0.8.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 (56) hide show
  1. package/README.md +15 -238
  2. package/dist/LiveVolume.d.ts.map +1 -1
  3. package/dist/LiveVolume.js +9 -9
  4. package/dist/Search.d.ts +730 -0
  5. package/dist/Search.d.ts.map +1 -0
  6. package/dist/Search.js +568 -0
  7. package/dist/SnapshotDelta.d.ts +234 -0
  8. package/dist/SnapshotDelta.d.ts.map +1 -1
  9. package/dist/SnapshotDelta.js +107 -0
  10. package/dist/VirtualFileSystem.d.ts +53 -1
  11. package/dist/VirtualFileSystem.d.ts.map +1 -1
  12. package/dist/VirtualFileSystem.js +59 -1
  13. package/dist/index.d.ts +7 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +7 -0
  16. package/dist/internal/fixture.js +3 -3
  17. package/dist/internal/glob.d.ts +66 -0
  18. package/dist/internal/glob.d.ts.map +1 -0
  19. package/dist/internal/glob.js +248 -0
  20. package/dist/internal/image.d.ts +7 -1
  21. package/dist/internal/image.d.ts.map +1 -1
  22. package/dist/internal/image.js +9 -7
  23. package/dist/internal/liveImage.d.ts +1 -1
  24. package/dist/internal/liveImage.d.ts.map +1 -1
  25. package/dist/internal/liveImage.js +2 -10
  26. package/dist/internal/resolution.d.ts +135 -0
  27. package/dist/internal/resolution.d.ts.map +1 -0
  28. package/dist/internal/resolution.js +277 -0
  29. package/dist/internal/search.d.ts +24 -0
  30. package/dist/internal/search.d.ts.map +1 -0
  31. package/dist/internal/search.js +78 -0
  32. package/dist/internal/searchContent.d.ts +77 -0
  33. package/dist/internal/searchContent.d.ts.map +1 -0
  34. package/dist/internal/searchContent.js +223 -0
  35. package/dist/internal/searchModel.d.ts +83 -0
  36. package/dist/internal/searchModel.d.ts.map +1 -0
  37. package/dist/internal/searchModel.js +23 -0
  38. package/dist/internal/snapshotDelta.d.ts +470 -1
  39. package/dist/internal/snapshotDelta.d.ts.map +1 -1
  40. package/dist/internal/snapshotDelta.js +59 -25
  41. package/dist/internal/snapshotMerge.d.ts +54 -0
  42. package/dist/internal/snapshotMerge.d.ts.map +1 -0
  43. package/dist/internal/snapshotMerge.js +959 -0
  44. package/dist/internal/tokenRegistry.d.ts +77 -0
  45. package/dist/internal/tokenRegistry.d.ts.map +1 -0
  46. package/dist/internal/tokenRegistry.js +128 -0
  47. package/dist/internal/virtualFileSystem.d.ts +32 -17
  48. package/dist/internal/virtualFileSystem.d.ts.map +1 -1
  49. package/dist/internal/virtualFileSystem.js +263 -661
  50. package/dist/internal/volumeLimits.d.ts +53 -0
  51. package/dist/internal/volumeLimits.d.ts.map +1 -0
  52. package/dist/internal/volumeLimits.js +63 -0
  53. package/dist/internal/volumeState.d.ts +2 -0
  54. package/dist/internal/volumeState.d.ts.map +1 -1
  55. package/dist/internal/volumeState.js +9 -0
  56. package/package.json +5 -1
@@ -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,16 +14,15 @@ 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";
25
+ import { OverlayChange, OverlayChangesOptions, ReferenceKey, VolumeId, VolumeIdentity, VolumeIncarnation } from "../Volume.js";
26
26
  import { WatchOptions } from "../Watch.js";
27
27
  import { sameBytes } from "./bytes.js";
28
28
  import { argumentFailure, decodeConfiguration, errorPath, fsFailure, imageFailure, OpContext, retargetFailure, VfsError } from "./errors.js";
@@ -33,10 +33,13 @@ import * as InodeTable from "./inodeTable.js";
33
33
  import * as LiveImage from "./liveImage.js";
34
34
  import * as MetadataDomain from "./metadata.js";
35
35
  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";
36
+ import { inputBytes, isAttachedBytes, isDotComponent, joinPath, nameBytes, ownedPath, ROOT_PATH, SLASH_BYTE, SLASH_HEX } from "./path.js";
37
+ import * as Resolution from "./resolution.js";
37
38
  import { VolumeTestSeams } from "./testSeams.js";
39
+ import * as TokenRegistry from "./tokenRegistry.js";
38
40
  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";
41
+ import * as Limits from "./volumeLimits.js";
42
+ import { byEntryName, directoryMetadata, emptyState, getNode, Ino, reachableValue, ROOT_INO, storedMetadata, WALK_YIELD_INTERVAL } from "./volumeState.js";
40
43
  import * as WatchHub from "./watchHub.js";
41
44
  const EXECUTE = 0o1;
42
45
  const WRITE = 0o2;
@@ -45,11 +48,23 @@ const ANY_EXECUTE = 0o111;
45
48
  const SET_ID_BITS = 0o6000;
46
49
  const STICKY_BIT = 0o1000;
47
50
  const RELATIME_INTERVAL_NS = 86400000000000n; // relatime, as Linux mounts by default: a read refreshes an access time at least this old, 24 hours.
51
+ /** @internal */
52
+ export const memoryCommitProvider = Object.freeze({
53
+ mode: "memory",
54
+ commit: () => Effect.succeed("committed"),
55
+ shutdown: Effect.void
56
+ });
48
57
  // An unknown commit outcome stops the volume; definite rejection stops it only during cleanup.
49
58
  const offerCommit = (provider, operation, candidate, cleanup) => Effect.map(Effect.exit(Effect.suspend(() => provider.commit(candidate))), (committed) => {
50
59
  // TODO(#186): Preserve the cause of a provider defect or interruption.
51
60
  if (Exit.isFailure(committed)) {
52
- return { available: false, failure: fsFailure("OutcomeUnknown", operation) };
61
+ if (Cause.hasDies(committed.cause) || Cause.hasInterrupts(committed.cause)) {
62
+ return { available: false, failure: fsFailure("OutcomeUnknown", operation) };
63
+ }
64
+ const failure = Cause.findErrorOption(committed.cause);
65
+ return Option.isSome(failure)
66
+ ? { available: !cleanup, failure: failure.value }
67
+ : { available: false, failure: fsFailure("OutcomeUnknown", operation) };
53
68
  }
54
69
  if (committed.value === "rejected") {
55
70
  return { available: !cleanup, failure: fsFailure("StorageRejected", operation) };
@@ -64,7 +79,7 @@ const MAX_FILE_OFFSET = 0x7fffffffffffffffn;
64
79
  const isMode = Schema.is(Mode);
65
80
  const isNatural = Schema.is(Schema.Natural);
66
81
  const isLength = Schema.is(SetattrOptions.fields.size.schema);
67
- const inGroup = (identity, gid) => identity.gid === gid || identity.groups.includes(gid);
82
+ const inGroup = Resolution.inGroup;
68
83
  const isTimestamp = Schema.is(MetadataDomain.Timestamp);
69
84
  const isSeekMode = Schema.is(SeekMode);
70
85
  const decodeOwnerUpdate = Schema.decodeEffect(OwnerUpdate, { onExcessProperty: "error" });
@@ -79,7 +94,6 @@ const decodeSymlinkOptions = Schema.decodeEffect(SymlinkOptions, { onExcessPrope
79
94
  const decodeWalkOptions = Schema.decodeEffect(WalkOptions, { onExcessProperty: "error" });
80
95
  const decodeRemoveOptions = Schema.decodeEffect(RemoveOptions, { onExcessProperty: "error" });
81
96
  const decodeOpenEntryOptions = Schema.decodeEffect(OpenEntryOptions, { onExcessProperty: "error" });
82
- const encoder = new TextEncoder();
83
97
  // Whether a read at `now` refreshes a node's access time: when it is not newer than the last modification or
84
98
  // status change, or when it is at least a day old. A time already equal to `now` is never refreshed, as Linux
85
99
  // skips it, so reads at one instant (a frozen or coarse clock) store nothing.
@@ -196,10 +210,6 @@ class Draft extends Context.Service()("@effect-vfs/core/internal/virtualFileSyst
196
210
  })
197
211
  }) {
198
212
  }
199
- const objectReferences = new WeakMap();
200
- const isFileHandle = (value) => Predicate.hasProperty(FileHandleId)(value);
201
- const files = new WeakMap();
202
- const handles = new WeakMap();
203
213
  const withMetadata = (node) => ({ ...node.metadata, revision: node.revision });
204
214
  const withEntries = (directory, edit) => {
205
215
  const entries = new Map(directory.entries);
@@ -247,29 +257,17 @@ const randomHex128 = Effect.gen(function* () {
247
257
  return Hex.encode(yield* crypto.randomBytes(16));
248
258
  }).pipe(Effect.orDie);
249
259
  /** @internal */
250
- export const makeVolume = Effect.fnUntraced(function* (source, options, commitProvider, captureInitial, durability = "memory-only") {
260
+ export const makeVolume = Effect.fnUntraced(function* (source, commitProvider, options, durability = "memory-only") {
251
261
  const services = yield* Effect.context();
252
262
  // These accessors run inside synchronous transition callbacks. The runner is reached only when an invariant fails.
253
263
  const dieInvariant = (message) => Effect.runSyncWith(services)(Effect.die(message));
254
264
  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
265
+ const decoded = yield* Effect.fromResult(Limits.configuration(options));
266
+ const requested = decoded.limits;
267
+ const limits = live === undefined ? requested : yield* Effect.fromResult(Limits.fromStored(live.limits, requested));
268
+ const identity = live?.identity ?? (decoded.identity === undefined
271
269
  ? VolumeIdentity.make(yield* randomHex128)
272
- : VolumeIdentity.make(settings.identity);
270
+ : decoded.identity);
273
271
  const incarnation = VolumeIncarnation.make(yield* randomHex128);
274
272
  const epoch = live === undefined ? VolumeEpoch.make(yield* randomHex128) : live.epoch;
275
273
  const keySecret = live === undefined ? KeySecret.make(yield* randomHex128) : live.keySecret;
@@ -288,24 +286,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
288
286
  : Effect.fail(op.fail("InvalidArgument"));
289
287
  });
290
288
  const volumeIdentity = Symbol();
291
- const tokens = new Map();
292
289
  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;
290
+ const maxPendingOperations = limits.maxPendingOperations;
309
291
  const permits = maxPendingOperations + 1;
310
292
  const gate = Semaphore.makeUnsafe(permits);
311
293
  // The gate hands permits to whichever waiter it can satisfy, so a change waiting for every permit would be
@@ -322,51 +304,36 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
322
304
  onNone: () => op.fail("VolumeBusy"),
323
305
  onSome: Effect.succeed
324
306
  })));
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
- });
307
+ let state = emptyState(initialTime);
308
+ const maxFileBytes = Number(ByteSize.toBigInt(limits.maxFileBytes));
343
309
  if (Predicate.isTagged("Restored")(source)) {
344
310
  const restored = yield* reachableValue(source.value);
345
311
  if (restored.largestFile > maxFileBytes) {
346
312
  return yield* imageFailure("snapshot", "LimitExceeded", { field: "maxFileBytes" });
347
313
  }
348
- if ((settings.maxEntries !== undefined && restored.state.entries > settings.maxEntries) ||
349
- (settings.maxBytes !== undefined && restored.state.usedBytes > ByteSize.toBigInt(settings.maxBytes))) {
314
+ if ((limits.maxEntries !== undefined && restored.state.entries > limits.maxEntries) ||
315
+ (limits.maxBytes !== undefined && restored.state.usedBytes > ByteSize.toBigInt(limits.maxBytes))) {
350
316
  return yield* imageFailure("snapshot", "LimitExceeded", { field: "volume" });
351
317
  }
352
318
  state = restored.state;
353
319
  }
354
320
  if (live !== undefined)
355
321
  state = live.value;
356
- const initialImage = captureInitial === undefined
357
- ? undefined
358
- : yield* captureInitial(state, { identity, epoch, keySecret }, limits);
359
322
  const view = (ino) => Effect.flatMap(Effect.serviceOption(Draft), (draft) => Option.isSome(draft) ? draft.value.get(ino) : Effect.sync(() => getNode(state, ino)));
323
+ const registry = TokenRegistry.make(volumeIdentity, view);
324
+ const referenceFor = registry.referenceFor;
325
+ const makeFileReference = registry.file;
326
+ const makeDirectoryReference = registry.directory;
360
327
  let available = true;
361
328
  const checkAvailable = (operation) => Effect.suspend(() => available ? Effect.void : Effect.fail(fsFailure("VolumeUnavailable", operation)));
362
329
  const watchCoordinate = (effect) => changing(effect);
363
- const watchHub = yield* WatchHub.make(watchCoordinate, settings.maxWatchEvents ?? 256);
330
+ const watchHub = yield* WatchHub.make(watchCoordinate, limits.maxWatchEvents);
364
331
  const install = Effect.fnUntraced(function* (finished) {
365
332
  const before = state;
366
333
  state = yield* finished.finish;
367
334
  const actions = yield* finished.actions;
368
335
  for (const ino of actions.removed)
369
- tokens.delete(ino);
336
+ registry.forget(ino);
370
337
  for (const apply of actions.after)
371
338
  apply();
372
339
  const after = state;
@@ -377,36 +344,33 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
377
344
  const value = yield* Effect.provideService(change, Draft, current);
378
345
  return [value, current];
379
346
  });
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)
347
+ // Cleanup releases local ownership even when storage refuses; that divergence disables the volume.
348
+ const committed = Effect.fnUntraced(function* (op, finished, cleanup) {
349
+ if ((yield* finished.unchanged) || (cleanup && !available))
390
350
  return yield* install(finished);
391
351
  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);
352
+ const answer = yield* offerCommit(commitProvider, op.operation, next, cleanup);
395
353
  if (!answer.available)
396
354
  available = false;
397
355
  if (answer.failure !== undefined) {
398
- if (!answer.available && onStorageFailure !== undefined)
399
- yield* onStorageFailure();
356
+ if (cleanup)
357
+ yield* install(finished);
400
358
  return yield* answer.failure;
401
359
  }
402
360
  yield* install(finished);
403
361
  });
362
+ const applyCleanup = Effect.fnUntraced(function* (change) {
363
+ const current = yield* Draft.make(state);
364
+ yield* Effect.provideService(change(current), Draft, current);
365
+ yield* committed(OpContext.make("close"), current, true);
366
+ });
404
367
  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));
368
+ // Permit waits stay interruptible. Memory transitions are masked; durable transitions remain
369
+ // interruptible until the candidate is ready. Every provider commit and installation is masked.
370
+ const coordinated = (op, effect, cleanup = false) => admit(op, changing(Effect.uninterruptibleMask((restore) => {
371
+ const staged = transition(Effect.andThen(checkAvailable(op.operation), effect));
372
+ return Effect.flatMap(commitProvider.mode === "memory" ? staged : restore(staged), ([value, finished]) => Effect.as(committed(op, finished, cleanup), value));
373
+ }))).pipe(Effect.tapError((error) => Schema.is(VfsError)(error) ? annotateFailure(error) : Effect.void));
410
374
  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
375
  const refreshAccess = Effect.fnUntraced(function* (read) {
412
376
  if (refreshDue(read)) {
@@ -423,9 +387,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
423
387
  ? coordinated(op, Effect.flatMap(read, refreshAccess))
424
388
  : Effect.succeed(observed.value));
425
389
  const coordinatedCleanup = (effect) => changing(Effect.uninterruptible(effect));
426
- const shutdown = commitProvider === undefined ? undefined : changing(Effect.sync(() => {
390
+ const shutdown = commitProvider.mode === "memory" ? commitProvider.shutdown : changing(Effect.uninterruptible(Effect.andThen(Effect.sync(() => {
427
391
  available = false;
428
- }));
392
+ }), commitProvider.shutdown)));
429
393
  const pathOf = (get, ino) => {
430
394
  const names = [];
431
395
  let node = get(ino);
@@ -524,16 +488,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
524
488
  includes: () => true,
525
489
  rescan: () => ({ change: RescanChange.make({ path: rootPath() }), parent: ROOT_INO, ino: ROOT_INO })
526
490
  };
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);
491
+ const scopeRoot = Effect.fnUntraced(function* (scope, op) {
492
+ const node = yield* TokenRegistry.nodeOrFail(yield* registry.resolve(scope, "reference"), "reference", op);
493
+ return node.metadata.nlink === 0 ? yield* op.fail("StaleReference") : node.ino;
537
494
  });
538
495
  // A watch of one object, and of its subtree when recursive. Membership is read from the installed tree when
539
496
  // each event is offered, so it follows renames of the scope and its ancestors. When the object's last name is
@@ -566,10 +523,6 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
566
523
  yield* (yield* VolumeTestSeams).betweenSnapshotAndSummary;
567
524
  return { snapshot, observation: yield* observeChanges(captured) };
568
525
  });
569
- const addressable = (ino) => {
570
- const node = getNode(state, ino);
571
- return node !== undefined && !(node.kind === "directory" && node.metadata.nlink === 0);
572
- };
573
526
  const keyTag = (ino) => {
574
527
  const message = new Uint8Array(40);
575
528
  message.set(identityBytes);
@@ -577,15 +530,6 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
577
530
  new DataView(message.buffer).setBigUint64(32, ino);
578
531
  return hmacSha256(keySecretBytes, message).subarray(0, 16);
579
532
  };
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
533
  const holdDirectory = (ino) => {
590
534
  directoryHolds.set(ino, (directoryHolds.get(ino) ?? 0) + 1);
591
535
  };
@@ -598,32 +542,17 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
598
542
  directoryHolds.delete(ino);
599
543
  const node = getNode(state, ino);
600
544
  if (node?.kind === "directory" && node.metadata.nlink === 0) {
601
- yield* applyDirect((d) => d.remove(ino));
545
+ yield* applyCleanup((d) => d.remove(ino));
602
546
  }
603
547
  });
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);
548
+ const releaseDirectory = (reference) => TokenRegistry.release(reference, unholdDirectory, (action) => Effect.sync(action));
549
+ const finalizeDirectory = (reference) => {
550
+ const close = coordinatedCleanup(releaseDirectory(reference));
551
+ return TokenRegistry.finalize(reference, close, Effect.ignore(close));
623
552
  };
624
- const authorize = (node, identity, bits, op) => identity.privileged || (permitted(node, identity) & bits) === bits
625
- ? Effect.void
626
- : Effect.fail(op.fail("AccessDenied"));
553
+ const closeReleasedScope = TokenRegistry.closeReleasedScope;
554
+ const permitted = Resolution.permitted;
555
+ const authorize = Resolution.authorize;
627
556
  const reclaim = Effect.fnUntraced(function* (d, ino) {
628
557
  const node = yield* d.get(ino);
629
558
  if (node === undefined)
@@ -644,7 +573,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
644
573
  });
645
574
  const atEntryLimit = Effect.gen(function* () {
646
575
  const draft = yield* Draft;
647
- return settings.maxEntries !== undefined && (yield* draft.entries) >= settings.maxEntries;
576
+ return limits.maxEntries !== undefined && (yield* draft.entries) >= limits.maxEntries;
648
577
  });
649
578
  const reserveEntry = Effect.fnUntraced(function* (op) {
650
579
  const draft = yield* Draft;
@@ -653,10 +582,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
653
582
  });
654
583
  const reserveInode = (op) => Effect.flatMap(Draft, (draft) => Effect.flatMap(draft.canAllocate, (available) => available ? Effect.void : Effect.fail(op.fail("NoSpace"))));
655
584
  const reserveBytes = Effect.fnUntraced(function* (op, bytes) {
656
- if (settings.maxBytes === undefined)
585
+ if (limits.maxBytes === undefined)
657
586
  return;
658
587
  const draft = yield* Draft;
659
- if (bytes > ByteSize.toBigInt(settings.maxBytes) - (yield* draft.usedBytes)) {
588
+ if (bytes > ByteSize.toBigInt(limits.maxBytes) - (yield* draft.usedBytes)) {
660
589
  return yield* op.fail("NoSpace");
661
590
  }
662
591
  });
@@ -692,60 +621,19 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
692
621
  }
693
622
  yield* reclaim(d, child.ino);
694
623
  });
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
- }
624
+ const releaseFileInode = Effect.fnUntraced(function* (ino) {
716
625
  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))));
626
+ yield* d.release(ino);
627
+ yield* reclaim(d, ino);
745
628
  });
746
- const bindFile = (ref, ino) => Effect.flatMap(Draft, (draft) => draft.afterInstall(() => {
747
- ref.ino = ino;
748
- }));
629
+ const releaseFile = (ref) => TokenRegistry.release(ref, releaseFileInode, (action) => Effect.sync(action));
630
+ const releaseOpenFile = (ref) => Effect.ignore(coordinatedCleanup(Effect.suspend(() => TokenRegistry.inode(ref) === undefined ? Effect.void : applyCleanup(() => releaseFile(ref)))));
631
+ // Admission refusal leaves the handle open. Every admitted close shares the registry release,
632
+ // and a storage failure still completes cleanup while only explicit close reports the failure.
633
+ 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)));
634
+ const finalizeFile = (ref) => TokenRegistry.finalize(ref, closeFile(ref, OpContext.make("close"), Effect.void), releaseOpenFile(ref));
635
+ const acquireHandle = TokenRegistry.acquire;
636
+ const bindFile = (ref, ino) => Effect.flatMap(Draft, (draft) => draft.afterInstall(() => TokenRegistry.publish(ref, ino)));
749
637
  const replaceContent = Effect.fnUntraced(function* (file, data, now, publish = true) {
750
638
  const d = yield* Draft;
751
639
  yield* d.addUsedBytes(BigInt(data.length - file.data.length));
@@ -780,7 +668,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
780
668
  });
781
669
  const fileHandle = (ref) => {
782
670
  const get = Effect.fnUntraced(function* (op, access) {
783
- const node = ref.ino === undefined ? undefined : yield* view(ref.ino);
671
+ const ino = TokenRegistry.inode(ref);
672
+ const node = ino === undefined ? undefined : yield* view(ino);
784
673
  if (node?.kind !== "file" || (access === "read" && ref.access === "write") ||
785
674
  (access === "write" && ref.access === "read"))
786
675
  return yield* op.fail("InvalidHandle");
@@ -835,9 +724,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
835
724
  return yield* writeOp.fail("FileTooLarge");
836
725
  const start = Number(offset);
837
726
  const draft = yield* Draft;
838
- const free = settings.maxBytes === undefined
727
+ const free = limits.maxBytes === undefined
839
728
  ? BigInt(maxFileBytes)
840
- : ByteSize.toBigInt(settings.maxBytes) - (yield* draft.usedBytes);
729
+ : ByteSize.toBigInt(limits.maxBytes) - (yield* draft.usedBytes);
841
730
  const maximumEnd = BigInt(file.data.length) + free;
842
731
  const end = Number(BigInt(maxFileBytes) < maximumEnd ? BigInt(maxFileBytes) : maximumEnd);
843
732
  const count = Math.min(bytes.length, Math.max(0, end - start));
@@ -912,33 +801,24 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
912
801
  sync: coordinatedRead(syncOp, Effect.suspend(() => Effect.asVoid(get(syncOp)))).pipe(Effect.withSpan("FileHandle.sync")),
913
802
  close: closeFile(ref, closeOp, Effect.suspend(() => get(closeOp))).pipe(Effect.ensuring(closeReleasedScope(ref)), Effect.withSpan("FileHandle.close"))
914
803
  });
915
- files.set(handle, ref);
804
+ registry.registerFile(handle, ref);
916
805
  return handle;
917
806
  };
918
807
  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;
935
- });
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;
808
+ const resolver = Resolution.make({
809
+ caller: reference,
810
+ get: view,
811
+ identity,
812
+ maxPathBytes: limits.maxPathBytes,
813
+ registry
941
814
  });
815
+ const prepare = resolver.prepare;
816
+ const prepareEntry = resolver.prepareEntry;
817
+ const entryName = Resolution.entryName;
818
+ const resolveTarget = (target, op, options) => resolver.resolve(target, { kind: "Node", ...options }, op);
819
+ const resolveDirectory = resolver.directory;
820
+ const resolveEntry = (prepared, op) => resolver.resolve(prepared, { kind: "Parent" }, op);
821
+ const referencedNode = (target, op) => Effect.map(resolveTarget(Target.Reference({ reference: target }), op), (resolved) => resolved.node);
942
822
  const directoryNow = Effect.fnUntraced(function* (ino) {
943
823
  const node = yield* view(ino);
944
824
  if (node?.kind !== "directory") {
@@ -1008,141 +888,6 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1008
888
  revision: 0n
1009
889
  };
1010
890
  });
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
891
  const grantedMode = (metadata, mode) => !identity.privileged && metadata.kind === "file" && !inGroup(identity, metadata.gid) ? mode & ~0o2000 : mode;
1147
892
  const permittedMode = (metadata, mode, op) => !identity.privileged && identity.uid !== metadata.uid
1148
893
  ? Effect.fail(op.fail("NotPermitted"))
@@ -1270,13 +1015,16 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1270
1015
  }
1271
1016
  else {
1272
1017
  entryOp = op.at(prepared.path.input);
1273
- const resolved = yield* lookup(prepared.path, prepared.base, op, {
1018
+ const resolved = yield* resolver.resolve(Target.Path(prepared.base === undefined
1019
+ ? { path: prepared.path.input }
1020
+ : { path: prepared.path.input, relativeTo: prepared.base }), {
1021
+ kind: "Node",
1274
1022
  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
1023
  if (!revisionsBefore.has(parent.ino))
1276
1024
  revisionsBefore.set(parent.ino, made.directory.before);
1277
1025
  return directoryNow(made.child);
1278
1026
  })
1279
- });
1027
+ }, op);
1280
1028
  node = resolved.node;
1281
1029
  }
1282
1030
  if (node?.kind !== "directory")
@@ -1545,9 +1293,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1545
1293
  const ino = opened();
1546
1294
  if (ino === undefined)
1547
1295
  return;
1548
- const draft = yield* Draft;
1549
- yield* draft.release(ino);
1550
- yield* reclaim(draft, ino);
1296
+ yield* releaseFileInode(ino);
1551
1297
  });
1552
1298
  const acquireOpenedFile = (ref, op, acquire) => {
1553
1299
  let opened;
@@ -1555,75 +1301,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1555
1301
  opened = ino;
1556
1302
  }), releasePending(() => opened), finalizeFile(ref));
1557
1303
  };
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
1304
  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
1305
  const asTarget = (input) => Target.of(input);
1628
1306
  const acquireDirectory = Effect.fnUntraced(function* (input, op) {
1629
1307
  const target = asTarget(input);
@@ -1632,7 +1310,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1632
1310
  const directory = yield* resolveDirectory(target, op);
1633
1311
  yield* authorize(directory.node, identity, EXECUTE, directory.op);
1634
1312
  holdDirectory(directory.node.ino);
1635
- acquired.ino = directory.node.ino;
1313
+ TokenRegistry.publish(acquired, directory.node.ino);
1636
1314
  return acquired;
1637
1315
  }),
1638
1316
  // Nothing to undo here: the finalizer that follows an interrupted acquisition releases the hold under
@@ -1655,29 +1333,16 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1655
1333
  const follow = target.followFinalSymlink ?? true;
1656
1334
  return yield* acquireOpenedFile(acquired, op, (opened) => Effect.gen(function* () {
1657
1335
  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)
1336
+ const resolved = yield* resolver.resolve({ kind: "path", path, base: target.relativeTo }, {
1337
+ kind: "OrCreate",
1338
+ create: chosen.create ?? "never",
1339
+ finalSymlink: follow ? "follow" : "preserve"
1340
+ }, op);
1341
+ const { parent, name } = resolved;
1342
+ if (resolved.kind === "missing" && path.trailingSlash)
1676
1343
  return yield* pathOp.fail("IsDirectory");
1677
1344
  yield* authorize(parent, identity, EXECUTE, pathOp);
1678
- const file = resolved.node;
1679
- if (file !== undefined && chosen.create === "exclusive")
1680
- return yield* pathOp.fail("AlreadyExists");
1345
+ const file = resolved.kind === "existing" ? resolved.node : undefined;
1681
1346
  const result = yield* openFile({ parent: parent.ino, name, trailingSlash: path.trailingSlash, addressing: "path", op: pathOp }, file, chosen, acquired, op);
1682
1347
  opened(result.ino);
1683
1348
  return fileHandle(acquired);
@@ -1719,60 +1384,44 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1719
1384
  }
1720
1385
  if (chosen.exactMode && chosen.mode === undefined)
1721
1386
  return yield* op.fail("InvalidArgument");
1722
- const relativePath = preparePath(ownedPath(nameBytes(name)), op.operation, undefined);
1723
1387
  const acquired = makeFileReference(chosen.access, chosen.append ?? false);
1724
1388
  const expected = chosen.expected;
1725
1389
  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 });
1390
+ const resolved = yield* resolver.resolve({ kind: "entry", directory: entry.directory, name }, {
1391
+ kind: "OrCreate",
1392
+ create: chosen.create ?? "never",
1393
+ finalSymlink: chosen.followFinalSymlink !== false ? "follow" : "preserve",
1394
+ beforeFollow: Effect.fnUntraced(function* (_, direct) {
1395
+ if (expected !== undefined) {
1396
+ let expectedIno;
1397
+ if (expected !== null) {
1398
+ const observed = yield* Effect.result(referencedNode(expected, op));
1399
+ if (Result.isFailure(observed)) {
1400
+ return yield* op.fail("VolumeBusy", { cause: observed.failure });
1401
+ }
1402
+ expectedIno = observed.success.ino;
1403
+ }
1404
+ if (direct?.ino !== expectedIno)
1405
+ return yield* op.fail("VolumeBusy");
1738
1406
  }
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");
1407
+ if (chosen.expectedChild === null) {
1408
+ if (direct !== undefined)
1409
+ return yield* op.fail("StaleReference");
1771
1410
  }
1772
- mutationParent = resolved.parent.ino;
1773
- mutationName = resolved.name;
1774
- }
1775
- }
1411
+ else if (chosen.expectedChild !== undefined) {
1412
+ const expectedChild = chosen.expectedChild;
1413
+ const observed = yield* referencedNode(expectedChild.reference, op);
1414
+ if (direct?.ino !== observed.ino || observed.revision !== expectedChild.revision ||
1415
+ observed.metadata.atimeNs !== expectedChild.atimeNs ||
1416
+ observed.metadata.mtimeNs !== expectedChild.mtimeNs) {
1417
+ return yield* op.fail("StaleReference");
1418
+ }
1419
+ }
1420
+ })
1421
+ }, op);
1422
+ const file = resolved.kind === "existing" ? resolved.node : undefined;
1423
+ const mutationParent = resolved.kind === "missing" ? resolved.parent.ino : resolved.origin.parent.ino;
1424
+ const mutationName = resolved.kind === "missing" ? resolved.name : resolved.origin.name;
1776
1425
  const result = yield* openFile({ parent: mutationParent, name: mutationName, trailingSlash: false, addressing: "entry", op }, file, { ...chosen, initialSize: chosen.initialSize ?? 0n }, acquired, op);
1777
1426
  opened(result.ino);
1778
1427
  return {
@@ -1828,7 +1477,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1828
1477
  const prepared = prepare(target.path, op);
1829
1478
  if (Result.isFailure(prepared))
1830
1479
  return Effect.undefined;
1831
- return ResolvedEntry.fromPath(prepared.success, target.relativeTo, op).pipe(Effect.flatMap((entry) => Effect.map(view(entry.parent), (parent) => {
1480
+ return resolveEntry({ kind: "path", path: prepared.success, base: target.relativeTo }, op).pipe(Effect.flatMap((entry) => Effect.map(view(entry.parent), (parent) => {
1832
1481
  return entry.name !== undefined && parent?.kind === "directory" &&
1833
1482
  parent.entries.get(entry.name) === root.ino
1834
1483
  ? anchorFrame(parent, entry.name, root)
@@ -1868,27 +1517,23 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1868
1517
  let bytes = 0n;
1869
1518
  let failure;
1870
1519
  const list = (frame) => coordinatedRead(op, Effect.suspend(() => {
1871
- if (reference.ino === undefined)
1520
+ if (TokenRegistry.inode(reference) === undefined)
1872
1521
  return Effect.fail(op.fail("ClosedCaller"));
1873
1522
  const at = op.at(locate(frame.path));
1874
1523
  return Effect.flatMap(reachFrame(frame, at), (node) => node?.kind !== "directory" || node.metadata.nlink === 0
1875
1524
  ? Effect.succeed([])
1876
1525
  : Effect.andThen(listable(node, at), walkChildren(node, frame.path, frame.depth, frame)));
1877
1526
  }));
1878
- const exceeded = (frame, field) => makeError({ code: "LimitExceeded", operation: op.operation, field, path: errorPath(locate(frame.path)) });
1879
1527
  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");
1528
+ entries++;
1884
1529
  bytes += frame.bytes;
1885
- if (plan.maxBytes !== undefined && bytes > plan.maxBytes)
1886
- return exceeded(frame, "maxBytes");
1887
- return undefined;
1530
+ return plan.limit(frame, { entries, bytes }, "entry");
1888
1531
  };
1889
1532
  const step = Effect.gen(function* () {
1533
+ // Preserve the generic limit error E; yielding the error directly widens it to WalkFailure.
1534
+ // oxlint-disable-next-line effecttsgo/unnecessary-fail-yieldable-error -- E must stay narrow for unbounded removal.
1890
1535
  if (failure !== undefined)
1891
- return yield* failure;
1536
+ return yield* Effect.fail(failure);
1892
1537
  if (pending === undefined)
1893
1538
  pending = (yield* first).reverse();
1894
1539
  const out = [];
@@ -1910,9 +1555,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1910
1555
  out.push(frame);
1911
1556
  }
1912
1557
  if (unlisted) {
1913
- if (plan.maxDepth !== undefined && frame.depth > plan.maxDepth) {
1914
- return yield* stop(exceeded(frame, "maxDepth"));
1915
- }
1558
+ const rejected = plan.limit(frame, { entries, bytes }, "directory");
1559
+ if (rejected !== undefined)
1560
+ return yield* stop(rejected);
1916
1561
  const listed = yield* Effect.result(list(frame));
1917
1562
  if (Result.isFailure(listed))
1918
1563
  return yield* stop(listed.failure);
@@ -1976,7 +1621,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1976
1621
  }, op));
1977
1622
  })));
1978
1623
  };
1979
- const plan = { order: "post", maxDepth: undefined, maxEntries: undefined, maxBytes: undefined };
1624
+ const plan = { order: "post", limit: () => undefined };
1980
1625
  return yield* Stream.runForEach(walkFrames(op, first, plan, locate, listable), removeFrame);
1981
1626
  });
1982
1627
  const rootOp = OpContext.make("root");
@@ -1985,9 +1630,40 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1985
1630
  const prepared = yield* Effect.fromResult(prepareEntry(input, op));
1986
1631
  return yield* coordinated(op, Effect.flatMap(resolveEntry(prepared, op), (entry) => body(entry, op)));
1987
1632
  });
1633
+ const openDispatch = Effect.fn("Caller.open")(function* (input, options) {
1634
+ const op = OpContext.make("open");
1635
+ if (isEntry(input))
1636
+ return yield* openEntry(input, options, op);
1637
+ const target = asTarget(input);
1638
+ if (Target.$is("Path")(target))
1639
+ return yield* openPath(target, options, op);
1640
+ return yield* openNode(target, options, op);
1641
+ });
1642
+ function open(input, options) {
1643
+ return openDispatch(input, options);
1644
+ }
1645
+ const removeDispatch = Effect.fn("Caller.remove")(function* (input, options) {
1646
+ const op = OpContext.make("remove");
1647
+ const prepared = yield* Effect.fromResult(prepareEntry(input, op));
1648
+ const chosen = yield* decodeRemoveOptions(options ?? {}).pipe(Effect.mapError((cause) => fail(preparedOp(prepared, op), cause)));
1649
+ const once = coordinated(op, Effect.flatMap(resolveEntry(prepared, op), (entry) => removeEntry(entry, op)));
1650
+ const target = (effect) => chosen.force === true
1651
+ ? Effect.catchIf(effect, (error) => error.code === "NotFound", () => Effect.undefined)
1652
+ : effect;
1653
+ const removed = yield* Effect.result(target(once));
1654
+ if (Result.isSuccess(removed))
1655
+ return removed.success;
1656
+ if (chosen.recursive !== true || removed.failure.code !== "NotEmpty")
1657
+ return yield* removed.failure;
1658
+ yield* emptyDirectory(prepared, op, chosen.force === true);
1659
+ return yield* target(once);
1660
+ });
1661
+ function remove(input, options) {
1662
+ return removeDispatch(input, options);
1663
+ }
1988
1664
  const caller = Object.freeze({
1989
1665
  [CallerId]: true,
1990
- root: coordinatedRead(rootOp, Effect.suspend(() => reference.ino === undefined
1666
+ root: coordinatedRead(rootOp, Effect.suspend(() => TokenRegistry.inode(reference) === undefined
1991
1667
  ? Effect.fail(rootOp.fail("ClosedCaller"))
1992
1668
  : Effect.succeed(referenceFor(ROOT_INO)))).pipe(Effect.withSpan("Caller.root")),
1993
1669
  lookup: Effect.fn("Caller.lookup")(function* (input) {
@@ -2044,11 +1720,20 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2044
1720
  yield* authorize(directory.node, identity, READ, directory.op);
2045
1721
  return yield* walkChildren(directory.node, new Uint8Array(0), 0, yield* pathAnchor(target, directory.node, op));
2046
1722
  }));
1723
+ const exceeded = (frame, field) => makeError({ code: "LimitExceeded", operation: op.operation, field, path: errorPath(locate(frame.path)) });
1724
+ const maxBytes = chosen.maxBytes === undefined ? undefined : ByteSize.toBigInt(chosen.maxBytes);
2047
1725
  const plan = {
2048
1726
  order: chosen.order ?? "pre",
2049
- maxDepth: chosen.maxDepth,
2050
- maxEntries: chosen.maxEntries,
2051
- maxBytes: chosen.maxBytes === undefined ? undefined : ByteSize.toBigInt(chosen.maxBytes)
1727
+ limit: (frame, progress, phase) => {
1728
+ if (chosen.maxDepth !== undefined && frame.depth > chosen.maxDepth)
1729
+ return exceeded(frame, "maxDepth");
1730
+ if (phase === "directory")
1731
+ return undefined;
1732
+ if (chosen.maxEntries !== undefined && progress.entries > chosen.maxEntries) {
1733
+ return exceeded(frame, "maxEntries");
1734
+ }
1735
+ return maxBytes !== undefined && progress.bytes > maxBytes ? exceeded(frame, "maxBytes") : undefined;
1736
+ }
2052
1737
  };
2053
1738
  return Stream.map(walkFrames(op, first, plan, locate, readable), (frame) => Object.freeze({
2054
1739
  path: ownedPath(frame.path),
@@ -2076,12 +1761,16 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2076
1761
  return yield* coordinatedRead(op, Effect.gen(function* () {
2077
1762
  if (Target.$is("Path")(target)) {
2078
1763
  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;
1764
+ const result = yield* resolveTarget(target, op, { final: true });
1765
+ const directory = result.node.kind === "directory"
1766
+ ? result.node
1767
+ : result.kind === "entry"
1768
+ ? result.parent
1769
+ : undefined;
2081
1770
  const prefix = directory === undefined ? SLASH_HEX : yield* pathOfCurrent(directory.ino);
2082
1771
  if (prefix === undefined)
2083
1772
  return yield* pathOp.fail("NotFound");
2084
- if (result.node?.kind === "directory" || result.name === undefined)
1773
+ if (result.node.kind === "directory" || result.kind === "node")
2085
1774
  return ownedPath(nameBytes(prefix));
2086
1775
  return ownedPath(nameBytes(prefix + (prefix === SLASH_HEX ? "" : SLASH_HEX) + result.name));
2087
1776
  }
@@ -2136,66 +1825,19 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2136
1825
  const captured = new Uint8Array(bytes);
2137
1826
  const chosen = yield* decodeWriteFileOptions(options).pipe(Effect.mapError((cause) => fail(optionsOp, cause)));
2138
1827
  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)
1828
+ const resolved = yield* resolver.resolve(prepared, {
1829
+ kind: "OrCreate",
1830
+ create: chosen.create ?? "never",
1831
+ finalSymlink: chosen.replaceFinalSymlink !== true && chosen.followFinalSymlink !== false
1832
+ ? "follow"
1833
+ : "preserve",
1834
+ entryExclusive: "afterSymlink"
1835
+ }, op);
1836
+ const { parent, name, trailingSlash, op: entryOp } = resolved;
1837
+ const found = resolved.kind === "existing" ? resolved.node : undefined;
1838
+ if (found?.kind === "directory" || (found === undefined && trailingSlash)) {
2198
1839
  return yield* entryOp.fail("IsDirectory");
1840
+ }
2199
1841
  const replaced = found?.kind === "symlink" ? found : undefined;
2200
1842
  if (replaced !== undefined && !chosen.replaceFinalSymlink)
2201
1843
  return yield* entryOp.fail("SymlinkLoop");
@@ -2260,17 +1902,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2260
1902
  }
2261
1903
  }));
2262
1904
  }),
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
- }),
1905
+ open,
2274
1906
  mkdir: Effect.fn("Caller.mkdir")(function* (input, options = {}) {
2275
1907
  const op = OpContext.make("mkdir");
2276
1908
  const prepared = yield* Effect.fromResult(prepareEntry(input, op));
@@ -2328,23 +1960,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2328
1960
  rmdir: Effect.fn("Caller.rmdir")(function* (input) {
2329
1961
  return yield* entryVerb("rmdir", input, rmdirEntry);
2330
1962
  }),
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
- }),
1963
+ remove,
2348
1964
  rename: Effect.fn("Caller.rename")(function* (fromInput, toInput) {
2349
1965
  const op = OpContext.make("rename");
2350
1966
  const from = yield* Effect.fromResult(prepareEntry(fromInput, op));
@@ -2418,19 +2034,20 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2418
2034
  const handle = Object.freeze({
2419
2035
  [DirectoryHandleId]: true,
2420
2036
  stat: coordinatedRead(statOp, Effect.gen(function* () {
2421
- const node = acquired.ino === undefined ? undefined : yield* view(acquired.ino);
2037
+ const ino = TokenRegistry.inode(acquired);
2038
+ const node = ino === undefined ? undefined : yield* view(ino);
2422
2039
  return yield* (node === undefined
2423
2040
  ? Effect.fail(statOp.fail("InvalidHandle"))
2424
2041
  : Effect.succeed(withMetadata(node)));
2425
2042
  })).pipe(Effect.withSpan("DirectoryHandle.stat")),
2426
2043
  close: coordinatedCleanup(Effect.suspend(() => {
2427
- if (acquired.ino === undefined) {
2044
+ if (TokenRegistry.inode(acquired) === undefined) {
2428
2045
  return Effect.fail(OpContext.make("close").fail("InvalidHandle"));
2429
2046
  }
2430
2047
  return releaseDirectory(acquired);
2431
2048
  })).pipe(Effect.ensuring(closeReleasedScope(acquired)), Effect.withSpan("DirectoryHandle.close"))
2432
2049
  });
2433
- handles.set(handle, acquired);
2050
+ registry.registerDirectory(handle, acquired);
2434
2051
  return handle;
2435
2052
  })
2436
2053
  });
@@ -2461,32 +2078,27 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2461
2078
  snapshot: coordinatedRead(OpContext.make("snapshot"), Effect.sync(() => Image.make(state))).pipe(Effect.withSpan("Volume.snapshot")),
2462
2079
  referenceKey: Effect.fn("Volume.referenceKey")(function* (reference) {
2463
2080
  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) });
2081
+ return yield* coordinatedRead(op, Effect.gen(function* () {
2082
+ const node = yield* TokenRegistry.nodeOrFail(yield* registry.resolve(reference, "reference"), "reference", op);
2083
+ const ino = BigInt(node.ino);
2084
+ return { identity: identityBytes.slice(), epoch: epochBytes.slice(), ino, tag: keyTag(ino) };
2474
2085
  }));
2475
2086
  }),
2476
2087
  resolveReferenceKey: Effect.fn("Volume.resolveReferenceKey")(function* (key) {
2477
2088
  const op = OpContext.make("resolveReferenceKey");
2478
- return yield* coordinatedRead(op, Effect.suspend(() => {
2089
+ return yield* coordinatedRead(op, Effect.gen(function* () {
2479
2090
  if (!Schema.is(ReferenceKey)(key))
2480
- return Effect.fail(op.fail("InvalidReference"));
2091
+ return yield* op.fail("InvalidReference");
2481
2092
  // Another epoch numbered its objects on its own, so its key is another volume's even under this identity.
2482
2093
  if (!sameBytes(key.identity, identityBytes) || !sameBytes(key.epoch, epochBytes)) {
2483
- return Effect.fail(op.fail("ForeignReference"));
2094
+ return yield* op.fail("ForeignReference");
2484
2095
  }
2485
2096
  // Checked before the inode is looked up, so a guessed key learns nothing about which numbers are in use.
2486
2097
  if (!sameTag(key.tag, keyTag(key.ino)))
2487
- return Effect.fail(op.fail("InvalidReference"));
2098
+ return yield* op.fail("InvalidReference");
2488
2099
  const ino = Ino(Number(key.ino));
2489
- return addressable(ino) ? Effect.succeed(referenceFor(ino)) : Effect.fail(op.fail("StaleReference"));
2100
+ yield* TokenRegistry.nodeOrFail(yield* registry.resolveInode(ino), "reference", op);
2101
+ return referenceFor(ino);
2490
2102
  }));
2491
2103
  }),
2492
2104
  caller: Effect.fn("Volume.caller")(function* (options) {
@@ -2500,47 +2112,37 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2500
2112
  changes: coordinatedRead(OpContext.make("changes"), Effect.suspend(() => observeChanges(state))),
2501
2113
  capture: coordinatedRead(OpContext.make("capture"), captureState())
2502
2114
  });
2503
- return Object.freeze({ volume, shutdown, initialImage, observe });
2115
+ return Object.freeze({ volume, shutdown, observe });
2504
2116
  });
2505
2117
  /** @internal */
2506
2118
  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;
2119
+ return (yield* makeVolume(VolumeSource.Empty(), memoryCommitProvider, options).pipe(Effect.catchIf((error) => Schema.is(VfsError)(error) && error.code !== "InvalidArgument", Effect.die))).volume;
2508
2120
  });
2509
2121
  /** @internal */
2510
2122
  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;
2123
+ const decoded = yield* Effect.fromResult(Limits.configuration(options, "prepareEmptyImage"));
2124
+ const limits = decoded.limits;
2125
+ const identity = decoded.identity ?? VolumeIdentity.make(yield* randomHex128);
2126
+ const epoch = VolumeEpoch.make(yield* randomHex128);
2127
+ const keySecret = KeySecret.make(yield* randomHex128);
2128
+ const now = yield* Clock.currentTimeNanos;
2129
+ if (!isTimestamp(now))
2130
+ return yield* argumentFailure("prepareEmptyImage", "clock.currentTimeNanos");
2131
+ return yield* LiveImage.encode(emptyState(now), { identity, epoch, keySecret }, limits).pipe(Effect.mapError((error) => retargetFailure("prepareEmptyImage", error)));
2515
2132
  });
2516
2133
  /** @internal */
2517
- export const openImageVolume = Effect.fnUntraced(function* (image, maxImageBytes, commit, durability = "memory-only") {
2134
+ export const openImageVolume = Effect.fnUntraced(function* (image, maxImageBytes, commit, durability = "memory-only", options) {
2518
2135
  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
2136
+ const requested = yield* Effect.fromResult(Limits.fromOptions(options, "openImage"));
2137
+ const limits = yield* Effect.fromResult(Limits.fromStored(restored.limits, requested));
2138
+ const provider = {
2139
+ mode: "durable",
2140
+ shutdown: Effect.void,
2141
+ 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)
2142
+ ? Effect.fail(makeError({ code: "StorageRejected", operation: "commit" }))
2143
+ : commit(bytes)))
2529
2144
  };
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" });
2145
+ const { volume, shutdown } = yield* makeVolume(VolumeSource.Live({ restored }), provider, options, durability);
2544
2146
  return Object.freeze({ volume, shutdown });
2545
2147
  });
2546
2148
  /** @internal */
@@ -2553,12 +2155,12 @@ const changeOptions = (options) => {
2553
2155
  };
2554
2156
  /** @internal */
2555
2157
  export const fromSnapshot = Effect.fn("VirtualFileSystem.fromSnapshot")(function* (snapshot, options) {
2556
- return (yield* makeVolume(VolumeSource.Restored({ value: yield* Image.valueOf(snapshot) }), options)).volume;
2158
+ return (yield* makeVolume(VolumeSource.Restored({ value: yield* Image.valueOf(snapshot) }), memoryCommitProvider, options)).volume;
2557
2159
  }, Effect.mapError((error) => retargetFailure("fromSnapshot", error)));
2558
2160
  /** @internal */
2559
2161
  export const makeOverlay = Effect.fn("VirtualFileSystem.makeOverlay")(function* (base, options) {
2560
2162
  const value = yield* Image.valueOf(base);
2561
- const made = yield* Effect.mapError(makeVolume(VolumeSource.Restored({ value }), options), (error) => retargetFailure("makeOverlay", error));
2163
+ const made = yield* Effect.mapError(makeVolume(VolumeSource.Restored({ value }), memoryCommitProvider, options), (error) => retargetFailure("makeOverlay", error));
2562
2164
  const baseObservation = yield* observeChanges(value);
2563
2165
  const overlay = Object.freeze({
2564
2166
  ...made.volume,