@effect-vfs/core 0.7.0 → 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 (61) 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/canonicalBase64.js +3 -3
  17. package/dist/internal/fixture.js +3 -3
  18. package/dist/internal/glob.d.ts +66 -0
  19. package/dist/internal/glob.d.ts.map +1 -0
  20. package/dist/internal/glob.js +248 -0
  21. package/dist/internal/image.d.ts +7 -1
  22. package/dist/internal/image.d.ts.map +1 -1
  23. package/dist/internal/image.js +9 -7
  24. package/dist/internal/liveImage.d.ts +1 -1
  25. package/dist/internal/liveImage.d.ts.map +1 -1
  26. package/dist/internal/liveImage.js +2 -10
  27. package/dist/internal/overlayDiff.js +2 -2
  28. package/dist/internal/path.d.ts.map +1 -1
  29. package/dist/internal/path.js +3 -3
  30. package/dist/internal/resolution.d.ts +135 -0
  31. package/dist/internal/resolution.d.ts.map +1 -0
  32. package/dist/internal/resolution.js +277 -0
  33. package/dist/internal/search.d.ts +24 -0
  34. package/dist/internal/search.d.ts.map +1 -0
  35. package/dist/internal/search.js +78 -0
  36. package/dist/internal/searchContent.d.ts +77 -0
  37. package/dist/internal/searchContent.d.ts.map +1 -0
  38. package/dist/internal/searchContent.js +223 -0
  39. package/dist/internal/searchModel.d.ts +83 -0
  40. package/dist/internal/searchModel.d.ts.map +1 -0
  41. package/dist/internal/searchModel.js +23 -0
  42. package/dist/internal/snapshotDelta.d.ts +470 -1
  43. package/dist/internal/snapshotDelta.d.ts.map +1 -1
  44. package/dist/internal/snapshotDelta.js +60 -26
  45. package/dist/internal/snapshotMerge.d.ts +54 -0
  46. package/dist/internal/snapshotMerge.d.ts.map +1 -0
  47. package/dist/internal/snapshotMerge.js +959 -0
  48. package/dist/internal/tokenRegistry.d.ts +77 -0
  49. package/dist/internal/tokenRegistry.d.ts.map +1 -0
  50. package/dist/internal/tokenRegistry.js +128 -0
  51. package/dist/internal/tree.js +2 -2
  52. package/dist/internal/virtualFileSystem.d.ts +32 -17
  53. package/dist/internal/virtualFileSystem.d.ts.map +1 -1
  54. package/dist/internal/virtualFileSystem.js +640 -993
  55. package/dist/internal/volumeLimits.d.ts +53 -0
  56. package/dist/internal/volumeLimits.d.ts.map +1 -0
  57. package/dist/internal/volumeLimits.js +63 -0
  58. package/dist/internal/volumeState.d.ts +2 -0
  59. package/dist/internal/volumeState.d.ts.map +1 -1
  60. package/dist/internal/volumeState.js +9 -0
  61. package/package.json +10 -6
@@ -1,26 +1,28 @@
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";
5
+ import * as Context from "effect/Context";
4
6
  import * as Crypto from "effect/Crypto";
5
7
  import * as Data from "effect/Data";
6
8
  import * as Effect from "effect/Effect";
7
- import * as Encoding from "effect/Encoding";
9
+ import * as Hex from "effect/encoding/Hex";
8
10
  import * as Exit from "effect/Exit";
9
11
  import * as Match from "effect/Match";
12
+ import * as MutableRef from "effect/MutableRef";
10
13
  import * as Option from "effect/Option";
11
14
  import * as Predicate from "effect/Predicate";
12
15
  import * as Result from "effect/Result";
13
16
  import * as Schema from "effect/Schema";
14
- import * as Scope from "effect/Scope";
15
17
  import * as Semaphore from "effect/Semaphore";
16
18
  import * as Stream from "effect/Stream";
17
19
  import { BytePath } from "../BytePath.js";
18
- 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";
19
21
  import { DirectoryHandleId, FileHandleId, SeekMode } from "../FileHandle.js";
20
22
  import { Mode, OwnerUpdate, Times } from "../Metadata.js";
21
- import { isEntry, isTarget, Target } from "../Target.js";
23
+ import { isEntry, Target } from "../Target.js";
22
24
  import { make as makeError } from "../VfsError.js";
23
- import { OverlayChange, OverlayChangesOptions, ReferenceKey, VolumeId, VolumeIdentity, VolumeIncarnation, VolumeOptions } from "../Volume.js";
25
+ import { OverlayChange, OverlayChangesOptions, ReferenceKey, VolumeId, VolumeIdentity, VolumeIncarnation } from "../Volume.js";
24
26
  import { WatchOptions } from "../Watch.js";
25
27
  import { sameBytes } from "./bytes.js";
26
28
  import { argumentFailure, decodeConfiguration, errorPath, fsFailure, imageFailure, OpContext, retargetFailure, VfsError } from "./errors.js";
@@ -31,10 +33,13 @@ import * as InodeTable from "./inodeTable.js";
31
33
  import * as LiveImage from "./liveImage.js";
32
34
  import * as MetadataDomain from "./metadata.js";
33
35
  import { compareOverlay } from "./overlayDiff.js";
34
- 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";
35
38
  import { VolumeTestSeams } from "./testSeams.js";
39
+ import * as TokenRegistry from "./tokenRegistry.js";
36
40
  import { makeTurnstile } from "./turnstile.js";
37
- 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";
38
43
  import * as WatchHub from "./watchHub.js";
39
44
  const EXECUTE = 0o1;
40
45
  const WRITE = 0o2;
@@ -43,11 +48,23 @@ const ANY_EXECUTE = 0o111;
43
48
  const SET_ID_BITS = 0o6000;
44
49
  const STICKY_BIT = 0o1000;
45
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
+ });
46
57
  // An unknown commit outcome stops the volume; definite rejection stops it only during cleanup.
47
58
  const offerCommit = (provider, operation, candidate, cleanup) => Effect.map(Effect.exit(Effect.suspend(() => provider.commit(candidate))), (committed) => {
48
59
  // TODO(#186): Preserve the cause of a provider defect or interruption.
49
60
  if (Exit.isFailure(committed)) {
50
- 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) };
51
68
  }
52
69
  if (committed.value === "rejected") {
53
70
  return { available: !cleanup, failure: fsFailure("StorageRejected", operation) };
@@ -62,7 +79,7 @@ const MAX_FILE_OFFSET = 0x7fffffffffffffffn;
62
79
  const isMode = Schema.is(Mode);
63
80
  const isNatural = Schema.is(Schema.Natural);
64
81
  const isLength = Schema.is(SetattrOptions.fields.size.schema);
65
- const inGroup = (identity, gid) => identity.gid === gid || identity.groups.includes(gid);
82
+ const inGroup = Resolution.inGroup;
66
83
  const isTimestamp = Schema.is(MetadataDomain.Timestamp);
67
84
  const isSeekMode = Schema.is(SeekMode);
68
85
  const decodeOwnerUpdate = Schema.decodeEffect(OwnerUpdate, { onExcessProperty: "error" });
@@ -77,112 +94,122 @@ const decodeSymlinkOptions = Schema.decodeEffect(SymlinkOptions, { onExcessPrope
77
94
  const decodeWalkOptions = Schema.decodeEffect(WalkOptions, { onExcessProperty: "error" });
78
95
  const decodeRemoveOptions = Schema.decodeEffect(RemoveOptions, { onExcessProperty: "error" });
79
96
  const decodeOpenEntryOptions = Schema.decodeEffect(OpenEntryOptions, { onExcessProperty: "error" });
80
- const encoder = new TextEncoder();
81
97
  // Whether a read at `now` refreshes a node's access time: when it is not newer than the last modification or
82
98
  // status change, or when it is at least a day old. A time already equal to `now` is never refreshed, as Linux
83
99
  // skips it, so reads at one instant (a frozen or coarse clock) store nothing.
84
100
  const accessDue = (metadata, now) => now !== metadata.atimeNs && (metadata.atimeNs <= metadata.mtimeNs || metadata.atimeNs <= metadata.ctimeNs ||
85
101
  now - metadata.atimeNs >= RELATIME_INTERVAL_NS);
86
102
  const refreshDue = (read) => read.access !== undefined && accessDue(read.access.node.metadata, read.access.now);
87
- // Builds the next volume value for one transition. Reads see pending writes; a discarded draft leaves the
88
- // base untouched, so an interrupted or failed transition needs no rollback.
89
- class Draft {
90
- base;
91
- owner = Symbol();
92
- pending = new Map();
93
- opens;
94
- nextInode;
95
- changed = false;
96
- finished;
97
- revision;
98
- entries;
99
- usedBytes;
100
- events = [];
101
- after = [];
102
- removed = [];
103
- constructor(base) {
104
- this.base = base;
105
- this.nextInode = base.nextInode;
106
- this.revision = base.revision + 1n;
107
- this.entries = base.entries;
108
- this.usedBytes = base.usedBytes;
109
- }
110
- get(ino) {
111
- const pending = this.pending.get(ino);
112
- return pending !== undefined || this.pending.has(ino) ? pending : InodeTable.get(this.base.inodes, ino);
113
- }
114
- put(node) {
115
- this.changed = true;
116
- // SAFETY: callers construct the value they pass, so stamping it in place replaces a spread on every write.
117
- const stamped = node;
118
- stamped.revision = this.revision;
119
- this.pending.set(node.ino, node);
120
- }
121
- putQuiet(node) {
122
- this.pending.set(node.ino, node);
123
- }
124
- get unchanged() {
125
- return this.pending.size === 0 && this.opens === undefined && this.nextInode === this.base.nextInode &&
126
- this.entries === this.base.entries && this.usedBytes === this.base.usedBytes;
127
- }
128
- remove(ino) {
129
- this.removed.push(ino);
130
- this.pending.set(ino, undefined);
131
- }
132
- allocate() {
133
- const ino = this.nextInode;
134
- this.nextInode = Ino(ino + 1);
135
- return ino;
136
- }
137
- // Whether another inode can be allocated. The allocator itself must stay exactly representable, since a live
138
- // image refuses one past the largest safe integer, so the last inode it hands out is one below that.
139
- get canAllocate() {
140
- return this.nextInode < Number.MAX_SAFE_INTEGER;
141
- }
142
- openCount(ino) {
143
- return this.opens?.get(ino) ?? this.base.open.get(ino) ?? 0;
144
- }
145
- retain(ino) {
146
- this.opens = (this.opens ?? new Map()).set(ino, this.openCount(ino) + 1);
147
- }
148
- release(ino) {
149
- const count = this.openCount(ino) - 1;
150
- this.opens = (this.opens ?? new Map()).set(ino, count < 0 ? 0 : count);
151
- return count;
152
- }
153
- finish() {
154
- if (this.finished !== undefined)
155
- return this.finished;
156
- const base = this.base;
157
- let inodes = base.inodes;
158
- for (const [ino, node] of this.pending)
159
- inodes = InodeTable.set(inodes, ino, node, this.owner);
160
- let open = base.open;
161
- if (this.opens !== undefined) {
162
- const next = new Map(base.open);
163
- for (const [ino, count] of this.opens) {
164
- if (count > 0)
165
- next.set(ino, count);
166
- else
167
- next.delete(ino);
168
- }
169
- open = next;
170
- }
171
- this.finished = {
172
- inodes,
173
- open,
174
- nextInode: this.nextInode,
175
- revision: this.changed ? this.revision : base.revision,
176
- entries: this.entries,
177
- usedBytes: this.usedBytes
103
+ class Draft extends Context.Service()("@effect-vfs/core/internal/virtualFileSystem/Draft", {
104
+ make: Effect.fnUntraced(function* (base) {
105
+ const revision = base.revision + 1n;
106
+ const cell = MutableRef.make({
107
+ owner: Symbol(),
108
+ pending: new Map(),
109
+ opens: undefined,
110
+ nextInode: base.nextInode,
111
+ changed: false,
112
+ finished: undefined,
113
+ entries: base.entries,
114
+ usedBytes: base.usedBytes,
115
+ events: [],
116
+ after: [],
117
+ removed: []
118
+ });
119
+ const get = (ino) => Effect.sync(() => {
120
+ const draft = MutableRef.get(cell);
121
+ return draft.pending.has(ino) ? draft.pending.get(ino) : InodeTable.get(base.inodes, ino);
122
+ });
123
+ const openCount = (ino) => Effect.sync(() => MutableRef.get(cell).opens?.get(ino) ?? base.open.get(ino) ?? 0);
124
+ const put = (node, changed) => Effect.sync(() => {
125
+ MutableRef.update(cell, (draft) => {
126
+ // This map belongs only to this transition. Copying it on every inode write makes recursive changes quadratic.
127
+ draft.pending.set(node.ino, changed ? { ...node, revision } : node);
128
+ return { ...draft, changed: draft.changed || changed };
129
+ });
130
+ });
131
+ return {
132
+ revision,
133
+ get,
134
+ put: (node) => put(node, true),
135
+ putQuiet: (node) => put(node, false),
136
+ unchanged: Effect.sync(() => {
137
+ const draft = MutableRef.get(cell);
138
+ return draft.pending.size === 0 && draft.opens === undefined && draft.nextInode === base.nextInode &&
139
+ draft.entries === base.entries && draft.usedBytes === base.usedBytes;
140
+ }),
141
+ remove: (ino) => Effect.sync(() => {
142
+ const draft = MutableRef.get(cell);
143
+ draft.pending.set(ino, undefined);
144
+ draft.removed.push(ino);
145
+ }),
146
+ allocate: Effect.sync(() => {
147
+ const next = MutableRef.get(cell).nextInode;
148
+ MutableRef.update(cell, (draft) => ({ ...draft, nextInode: Ino(next + 1) }));
149
+ return next;
150
+ }),
151
+ canAllocate: Effect.sync(() => MutableRef.get(cell).nextInode < Number.MAX_SAFE_INTEGER),
152
+ openCount,
153
+ retain: (ino) => Effect.sync(() => {
154
+ const count = MutableRef.get(cell).opens?.get(ino) ?? base.open.get(ino) ?? 0;
155
+ MutableRef.update(cell, (draft) => ({ ...draft, opens: new Map(draft.opens).set(ino, count + 1) }));
156
+ }),
157
+ release: (ino) => Effect.sync(() => {
158
+ const count = (MutableRef.get(cell).opens?.get(ino) ?? base.open.get(ino) ?? 0) - 1;
159
+ MutableRef.update(cell, (draft) => ({ ...draft, opens: new Map(draft.opens).set(ino, Math.max(0, count)) }));
160
+ return count;
161
+ }),
162
+ entries: Effect.sync(() => MutableRef.get(cell).entries),
163
+ addEntries: (amount) => Effect.sync(() => {
164
+ MutableRef.update(cell, (draft) => ({ ...draft, entries: draft.entries + amount }));
165
+ }),
166
+ usedBytes: Effect.sync(() => MutableRef.get(cell).usedBytes),
167
+ addUsedBytes: (amount) => Effect.sync(() => {
168
+ MutableRef.update(cell, (draft) => ({ ...draft, usedBytes: draft.usedBytes + amount }));
169
+ }),
170
+ publish: (event) => Effect.sync(() => {
171
+ MutableRef.get(cell).events.push(event);
172
+ }),
173
+ afterInstall: (action) => Effect.sync(() => {
174
+ MutableRef.get(cell).after.push(action);
175
+ }),
176
+ finish: Effect.sync(() => {
177
+ const draft = MutableRef.get(cell);
178
+ if (draft.finished !== undefined)
179
+ return draft.finished;
180
+ let inodes = base.inodes;
181
+ for (const [ino, node] of draft.pending)
182
+ inodes = InodeTable.set(inodes, ino, node, draft.owner);
183
+ let open = base.open;
184
+ if (draft.opens !== undefined) {
185
+ const next = new Map(base.open);
186
+ for (const [ino, count] of draft.opens) {
187
+ if (count > 0)
188
+ next.set(ino, count);
189
+ else
190
+ next.delete(ino);
191
+ }
192
+ open = next;
193
+ }
194
+ const finished = {
195
+ inodes,
196
+ open,
197
+ nextInode: draft.nextInode,
198
+ revision: draft.changed ? revision : base.revision,
199
+ entries: draft.entries,
200
+ usedBytes: draft.usedBytes
201
+ };
202
+ MutableRef.update(cell, (current) => ({ ...current, finished }));
203
+ return finished;
204
+ }),
205
+ actions: Effect.sync(() => {
206
+ const { events, after, removed } = MutableRef.get(cell);
207
+ return { events, after, removed };
208
+ })
178
209
  };
179
- return this.finished;
180
- }
210
+ })
211
+ }) {
181
212
  }
182
- const objectReferences = new WeakMap();
183
- const isFileHandle = (value) => Predicate.hasProperty(FileHandleId)(value);
184
- const files = new WeakMap();
185
- const handles = new WeakMap();
186
213
  const withMetadata = (node) => ({ ...node.metadata, revision: node.revision });
187
214
  const withEntries = (directory, edit) => {
188
215
  const entries = new Map(directory.entries);
@@ -227,38 +254,26 @@ const observeChanges = Effect.fnUntraced(function* (captured) {
227
254
  });
228
255
  const randomHex128 = Effect.gen(function* () {
229
256
  const crypto = yield* Crypto.Crypto;
230
- return Encoding.encodeHex(yield* crypto.randomBytes(16));
257
+ return Hex.encode(yield* crypto.randomBytes(16));
231
258
  }).pipe(Effect.orDie);
232
259
  /** @internal */
233
- 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") {
234
261
  const services = yield* Effect.context();
235
262
  // These accessors run inside synchronous transition callbacks. The runner is reached only when an invariant fails.
236
263
  const dieInvariant = (message) => Effect.runSyncWith(services)(Effect.die(message));
237
264
  const live = Predicate.isTagged("Live")(source) ? source.restored : undefined;
238
- let restoredOptions = options;
239
- if (live !== undefined) {
240
- const recovered = { identity: live.identity };
241
- if (live.limits.maxEntries !== undefined)
242
- recovered.maxEntries = live.limits.maxEntries;
243
- if (live.limits.maxBytes !== undefined)
244
- recovered.maxBytes = ByteSize.bytes(live.limits.maxBytes);
245
- if (live.limits.maxFileBytes !== undefined)
246
- recovered.maxFileBytes = ByteSize.bytes(live.limits.maxFileBytes);
247
- if (live.limits.maxPathBytes !== undefined)
248
- recovered.maxPathBytes = ByteSize.bytes(live.limits.maxPathBytes);
249
- restoredOptions = recovered;
250
- }
251
- const decoded = yield* Effect.fromResult(decodeConfiguration(VolumeOptions, restoredOptions === undefined ? {} : restoredOptions, "make"));
252
- const settings = { ...decoded };
253
- 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
254
269
  ? VolumeIdentity.make(yield* randomHex128)
255
- : VolumeIdentity.make(settings.identity);
270
+ : decoded.identity);
256
271
  const incarnation = VolumeIncarnation.make(yield* randomHex128);
257
272
  const epoch = live === undefined ? VolumeEpoch.make(yield* randomHex128) : live.epoch;
258
273
  const keySecret = live === undefined ? KeySecret.make(yield* randomHex128) : live.keySecret;
259
- const identityBytes = Result.getOrThrow(Encoding.decodeHex(identity));
260
- const epochBytes = Result.getOrThrow(Encoding.decodeHex(epoch));
261
- const keySecretBytes = Result.getOrThrow(Encoding.decodeHex(keySecret));
274
+ const identityBytes = Result.getOrThrow(Hex.decode(identity));
275
+ const epochBytes = Result.getOrThrow(Hex.decode(epoch));
276
+ const keySecretBytes = Result.getOrThrow(Hex.decode(keySecret));
262
277
  const clock = yield* Clock.clockWith(Effect.succeed);
263
278
  const initialTime = clock.currentTimeNanosUnsafe();
264
279
  if (!isTimestamp(initialTime)) {
@@ -271,24 +286,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
271
286
  : Effect.fail(op.fail("InvalidArgument"));
272
287
  });
273
288
  const volumeIdentity = Symbol();
274
- const tokens = new Map();
275
289
  const directoryHolds = new Map();
276
- const makeFileReference = (access, append) => ({
277
- volume: volumeIdentity,
278
- scope: undefined,
279
- closed: false,
280
- ino: undefined,
281
- offset: 0n,
282
- access,
283
- append
284
- });
285
- const makeDirectoryReference = (ino) => ({
286
- volume: volumeIdentity,
287
- scope: undefined,
288
- closed: false,
289
- ino
290
- });
291
- const maxPendingOperations = settings.maxPendingOperations ?? 64;
290
+ const maxPendingOperations = limits.maxPendingOperations;
292
291
  const permits = maxPendingOperations + 1;
293
292
  const gate = Semaphore.makeUnsafe(permits);
294
293
  // The gate hands permits to whichever waiter it can satisfy, so a change waiting for every permit would be
@@ -305,125 +304,92 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
305
304
  onNone: () => op.fail("VolumeBusy"),
306
305
  onSome: Effect.succeed
307
306
  })));
308
- const root = emptyRoot(initialTime);
309
- let state = {
310
- inodes: InodeTable.set(InodeTable.empty(), ROOT_INO, root),
311
- open: new Map(),
312
- nextInode: Ino(2),
313
- revision: 1n,
314
- entries: 0,
315
- usedBytes: 0n
316
- };
317
- const maxFileBytes = Number(ByteSize.toBigInt(settings.maxFileBytes ?? ByteSize.bytes(MAX_FILE_BYTES)));
318
- const limits = Object.freeze({
319
- maxBytes: settings.maxBytes,
320
- maxFileBytes: ByteSize.bytes(maxFileBytes),
321
- maxEntries: settings.maxEntries,
322
- maxPathBytes: settings.maxPathBytes,
323
- maxPendingOperations: settings.maxPendingOperations ?? 64,
324
- maxWatchEvents: settings.maxWatchEvents ?? 256
325
- });
307
+ let state = emptyState(initialTime);
308
+ const maxFileBytes = Number(ByteSize.toBigInt(limits.maxFileBytes));
326
309
  if (Predicate.isTagged("Restored")(source)) {
327
310
  const restored = yield* reachableValue(source.value);
328
311
  if (restored.largestFile > maxFileBytes) {
329
312
  return yield* imageFailure("snapshot", "LimitExceeded", { field: "maxFileBytes" });
330
313
  }
331
- if ((settings.maxEntries !== undefined && restored.state.entries > settings.maxEntries) ||
332
- (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))) {
333
316
  return yield* imageFailure("snapshot", "LimitExceeded", { field: "volume" });
334
317
  }
335
318
  state = restored.state;
336
319
  }
337
320
  if (live !== undefined)
338
321
  state = live.value;
339
- const initialImage = captureInitial === undefined
340
- ? undefined
341
- : yield* captureInitial(state, { identity, epoch, keySecret }, limits);
342
- let draft;
343
- const view = (ino) => draft === undefined ? getNode(state, ino) : draft.get(ino);
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;
344
327
  let available = true;
345
328
  const checkAvailable = (operation) => Effect.suspend(() => available ? Effect.void : Effect.fail(fsFailure("VolumeUnavailable", operation)));
346
329
  const watchCoordinate = (effect) => changing(effect);
347
- const watchHub = yield* WatchHub.make(watchCoordinate, settings.maxWatchEvents ?? 256);
348
- const install = (finished) => {
330
+ const watchHub = yield* WatchHub.make(watchCoordinate, limits.maxWatchEvents);
331
+ const install = Effect.fnUntraced(function* (finished) {
349
332
  const before = state;
350
- state = finished.finish();
351
- for (const ino of finished.removed)
352
- tokens.delete(ino);
353
- for (const apply of finished.after)
333
+ state = yield* finished.finish;
334
+ const actions = yield* finished.actions;
335
+ for (const ino of actions.removed)
336
+ registry.forget(ino);
337
+ for (const apply of actions.after)
354
338
  apply();
355
339
  const after = state;
356
- watchHub.publishUnsafe(() => finished.events.flatMap((events) => Array.from(events(after))), { before, after });
357
- };
358
- const transition = (change) => Effect.suspend(() => {
359
- const current = new Draft(state);
360
- draft = current;
361
- return Effect.onExit(change, () => Effect.sync(() => {
362
- draft = undefined;
363
- })).pipe(Effect.map((value) => [value, current]));
340
+ watchHub.publishUnsafe(() => actions.events.flatMap((events) => Array.from(events(after))), { before, after });
364
341
  });
365
- const applyDirect = (change) => {
366
- const current = new Draft(state);
367
- draft = current;
368
- try {
369
- change(current);
370
- }
371
- finally {
372
- draft = undefined;
373
- }
374
- install(current);
375
- };
376
- // Offers the finished draft's value to the store before installing it. A rejected candidate is discarded and
377
- // the volume stays available; an uncertain answer stops the volume. A change that is a cleanup runs its
378
- // release even when the store refuses, since the handle must not stay open.
379
- const committed = Effect.fnUntraced(function* (op, provider, finished, onStorageFailure) {
380
- if (finished.unchanged)
381
- return install(finished);
382
- const next = finished.finish();
383
- if (provider.prepare !== undefined)
384
- yield* provider.prepare(next);
385
- const answer = yield* offerCommit(provider, op.operation, next, onStorageFailure !== undefined);
342
+ const transition = Effect.fnUntraced(function* (change) {
343
+ const current = yield* Draft.make(state);
344
+ const value = yield* Effect.provideService(change, Draft, current);
345
+ return [value, current];
346
+ });
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))
350
+ return yield* install(finished);
351
+ const next = yield* finished.finish;
352
+ const answer = yield* offerCommit(commitProvider, op.operation, next, cleanup);
386
353
  if (!answer.available)
387
354
  available = false;
388
355
  if (answer.failure !== undefined) {
389
- if (!answer.available)
390
- onStorageFailure?.();
356
+ if (cleanup)
357
+ yield* install(finished);
391
358
  return yield* answer.failure;
392
359
  }
393
- install(finished);
360
+ yield* install(finished);
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);
394
366
  });
395
367
  const annotateFailure = (error) => Effect.annotateCurrentSpan({ operation: error.operation, code: error.code });
396
- // Permit waits stay interruptible. A change and its publication run under every permit; with a store, the
397
- // change itself stays interruptible and only the commit and installation are not.
398
- const coordinated = (op, effect, onStorageFailure) => admit(op, changing(commitProvider === undefined
399
- ? Effect.uninterruptible(Effect.map(transition(Effect.andThen(checkAvailable(op.operation), effect)), ([value, finished]) => {
400
- install(finished);
401
- return value;
402
- }))
403
- : 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));
404
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));
405
- const refreshAccess = (read) => {
375
+ const refreshAccess = Effect.fnUntraced(function* (read) {
406
376
  if (refreshDue(read)) {
407
377
  const { node, now } = read.access;
408
- current().putQuiet({ ...node, metadata: { ...node.metadata, atimeNs: now } });
378
+ const draft = yield* Draft;
379
+ yield* draft.putQuiet({ ...node, metadata: { ...node.metadata, atimeNs: now } });
409
380
  }
410
381
  return read.value;
411
- };
382
+ });
412
383
  // A read that may refresh an access time. It observes under one permit, so reads run beside each other, and
413
384
  // only when the access time is due does it run again as a change. The change repeats every check and the rule,
414
385
  // since another read may have refreshed the time meanwhile, and its result is the one returned.
415
386
  const accessing = (op, read) => Effect.flatMap(coordinatedRead(op, read), (observed) => refreshDue(observed)
416
- ? coordinated(op, Effect.map(read, refreshAccess))
387
+ ? coordinated(op, Effect.flatMap(read, refreshAccess))
417
388
  : Effect.succeed(observed.value));
418
389
  const coordinatedCleanup = (effect) => changing(Effect.uninterruptible(effect));
419
- const shutdown = commitProvider === undefined ? undefined : changing(Effect.sync(() => {
390
+ const shutdown = commitProvider.mode === "memory" ? commitProvider.shutdown : changing(Effect.uninterruptible(Effect.andThen(Effect.sync(() => {
420
391
  available = false;
421
- }));
422
- const current = () => {
423
- if (draft === undefined)
424
- return dieInvariant("Volume mutation outside a transition");
425
- return draft;
426
- };
392
+ }), commitProvider.shutdown)));
427
393
  const pathOf = (get, ino) => {
428
394
  const names = [];
429
395
  let node = get(ino);
@@ -438,19 +404,33 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
438
404
  }
439
405
  return SLASH_HEX + names.reverse().join(SLASH_HEX);
440
406
  };
407
+ const pathOfCurrent = Effect.fnUntraced(function* (ino) {
408
+ const names = [];
409
+ const first = yield* view(ino);
410
+ if (first?.kind !== "directory" || first.metadata.nlink === 0)
411
+ return undefined;
412
+ let node = first;
413
+ while (node.ino !== ROOT_INO) {
414
+ names.push(node.name);
415
+ const parent = yield* view(node.parent);
416
+ if (parent?.kind !== "directory" || parent.metadata.nlink === 0)
417
+ return undefined;
418
+ node = parent;
419
+ }
420
+ return SLASH_HEX + names.reverse().join(SLASH_HEX);
421
+ });
441
422
  const entryPath = (prefix, name) => ownedPath(nameBytes(prefix + (prefix === SLASH_HEX ? "" : SLASH_HEX) + name));
442
- const publishEntry = (_tag, parent, name, ino) => {
443
- current().events.push((installed) => {
444
- const prefix = pathOf((at) => getNode(installed, at), parent);
445
- return prefix === undefined ? [] : [{ change: { _tag, path: entryPath(prefix, name) }, parent, ino }];
446
- });
447
- };
448
- const publishNode = (ino) => {
449
- const target = current().get(ino);
423
+ const publishEntry = (_tag, parent, name, ino) => Effect.flatMap(Draft, (draft) => draft.publish((installed) => {
424
+ const prefix = pathOf((at) => getNode(installed, at), parent);
425
+ return prefix === undefined ? [] : [{ change: { _tag, path: entryPath(prefix, name) }, parent, ino }];
426
+ }));
427
+ const publishNode = Effect.fnUntraced(function* (ino) {
428
+ const draft = yield* Draft;
429
+ const target = yield* draft.get(ino);
450
430
  if (target === undefined)
451
431
  return;
452
432
  if (target.kind === "directory") {
453
- current().events.push((installed) => {
433
+ yield* draft.publish((installed) => {
454
434
  const directory = getNode(installed, ino);
455
435
  const path = pathOf((at) => getNode(installed, at), ino);
456
436
  return path === undefined || directory?.kind !== "directory"
@@ -460,7 +440,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
460
440
  return;
461
441
  }
462
442
  const links = target.links;
463
- current().events.push((installed) => {
443
+ yield* draft.publish((installed) => {
464
444
  const events = [];
465
445
  for (const link of links) {
466
446
  const prefix = pathOf((at) => getNode(installed, at), link.parent);
@@ -474,7 +454,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
474
454
  }
475
455
  return events;
476
456
  });
477
- };
457
+ });
478
458
  const pathsOf = (installed, ino) => {
479
459
  const node = getNode(installed, ino);
480
460
  if (node === undefined)
@@ -508,16 +488,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
508
488
  includes: () => true,
509
489
  rescan: () => ({ change: RescanChange.make({ path: rootPath() }), parent: ROOT_INO, ino: ROOT_INO })
510
490
  };
511
- const scopeRoot = (scope, op) => Effect.suspend(() => {
512
- const known = objectReferences.get(scope);
513
- if (known === undefined)
514
- return Effect.fail(op.fail("InvalidReference"));
515
- if (known.volume !== volumeIdentity)
516
- return Effect.fail(op.fail("ForeignReference"));
517
- const node = getNode(state, known.ino);
518
- return node === undefined || node.metadata.nlink === 0
519
- ? Effect.fail(op.fail("StaleReference"))
520
- : 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;
521
494
  });
522
495
  // A watch of one object, and of its subtree when recursive. Membership is read from the installed tree when
523
496
  // each event is offered, so it follows renames of the scope and its ancestors. When the object's last name is
@@ -550,10 +523,6 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
550
523
  yield* (yield* VolumeTestSeams).betweenSnapshotAndSummary;
551
524
  return { snapshot, observation: yield* observeChanges(captured) };
552
525
  });
553
- const addressable = (ino) => {
554
- const node = getNode(state, ino);
555
- return node !== undefined && !(node.kind === "directory" && node.metadata.nlink === 0);
556
- };
557
526
  const keyTag = (ino) => {
558
527
  const message = new Uint8Array(40);
559
528
  message.set(identityBytes);
@@ -561,19 +530,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
561
530
  new DataView(message.buffer).setBigUint64(32, ino);
562
531
  return hmacSha256(keySecretBytes, message).subarray(0, 16);
563
532
  };
564
- const referenceFor = (ino) => {
565
- const existing = tokens.get(ino);
566
- if (existing !== undefined)
567
- return existing;
568
- const reference = Object.freeze({ [ObjectReferenceId]: true });
569
- objectReferences.set(reference, { volume: volumeIdentity, ino });
570
- tokens.set(ino, reference);
571
- return reference;
572
- };
573
533
  const holdDirectory = (ino) => {
574
534
  directoryHolds.set(ino, (directoryHolds.get(ino) ?? 0) + 1);
575
535
  };
576
- const unholdDirectory = (ino) => {
536
+ const unholdDirectory = Effect.fnUntraced(function* (ino) {
577
537
  const count = (directoryHolds.get(ino) ?? 1) - 1;
578
538
  if (count > 0) {
579
539
  directoryHolds.set(ino, count);
@@ -581,59 +541,57 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
581
541
  }
582
542
  directoryHolds.delete(ino);
583
543
  const node = getNode(state, ino);
584
- if (node?.kind === "directory" && node.metadata.nlink === 0)
585
- applyDirect((d) => d.remove(ino));
586
- };
587
- const releaseDirectory = (reference) => {
588
- const ino = reference.ino;
589
- reference.ino = undefined;
590
- reference.closed = true;
591
- if (ino !== undefined)
592
- unholdDirectory(ino);
593
- };
594
- const finalizeDirectory = (reference) => Effect.uninterruptible(Effect.suspend(() => reference.closed
595
- ? Effect.void
596
- : coordinatedCleanup(Effect.sync(() => releaseDirectory(reference)))));
597
- const closeReleasedScope = (reference) => Effect.suspend(() => !reference.closed || reference.scope === undefined ? Effect.void : Scope.close(reference.scope, Exit.void));
598
- const permitted = (node, identity) => {
599
- const metadata = node.metadata;
600
- const shift = metadata.uid === identity.uid ?
601
- 6
602
- : inGroup(identity, metadata.gid)
603
- ? 3
604
- : 0;
605
- return (metadata.mode >> shift) & (READ | WRITE | EXECUTE);
544
+ if (node?.kind === "directory" && node.metadata.nlink === 0) {
545
+ yield* applyCleanup((d) => d.remove(ino));
546
+ }
547
+ });
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));
606
552
  };
607
- const authorize = (node, identity, bits, op) => identity.privileged || (permitted(node, identity) & bits) === bits
608
- ? Effect.void
609
- : Effect.fail(op.fail("AccessDenied"));
610
- const reclaim = (d, ino) => {
611
- const node = d.get(ino);
553
+ const closeReleasedScope = TokenRegistry.closeReleasedScope;
554
+ const permitted = Resolution.permitted;
555
+ const authorize = Resolution.authorize;
556
+ const reclaim = Effect.fnUntraced(function* (d, ino) {
557
+ const node = yield* d.get(ino);
612
558
  if (node === undefined)
613
559
  return;
614
560
  if (node.kind === "file") {
615
- if (node.links.length > 0 || d.openCount(ino) > 0)
561
+ if (node.links.length > 0 || (yield* d.openCount(ino)) > 0)
616
562
  return;
617
- d.usedBytes -= BigInt(node.data.length);
563
+ yield* d.addUsedBytes(-BigInt(node.data.length));
618
564
  }
619
565
  else if (node.kind === "symlink") {
620
566
  if (node.links.length > 0)
621
567
  return;
622
- d.usedBytes -= BigInt(node.target.length);
568
+ yield* d.addUsedBytes(-BigInt(node.target.length));
623
569
  }
624
570
  else if (node.metadata.nlink > 0 || directoryHolds.has(ino))
625
571
  return;
626
- d.remove(ino);
627
- };
628
- const atEntryLimit = () => settings.maxEntries !== undefined && current().entries >= settings.maxEntries;
629
- const reserveEntry = (op) => atEntryLimit() || !current().canAllocate ? Effect.fail(op.fail("NoSpace")) : Effect.void;
630
- const reserveInode = (op) => current().canAllocate ? Effect.void : Effect.fail(op.fail("NoSpace"));
631
- const reserveBytes = (op, bytes) => settings.maxBytes !== undefined && bytes > ByteSize.toBigInt(settings.maxBytes) - current().usedBytes
632
- ? Effect.fail(op.fail("NoSpace"))
633
- : Effect.void;
634
- const attach = (parent, name, child, now) => {
635
- const d = current();
636
- d.put(withEntries({
572
+ yield* d.remove(ino);
573
+ });
574
+ const atEntryLimit = Effect.gen(function* () {
575
+ const draft = yield* Draft;
576
+ return limits.maxEntries !== undefined && (yield* draft.entries) >= limits.maxEntries;
577
+ });
578
+ const reserveEntry = Effect.fnUntraced(function* (op) {
579
+ const draft = yield* Draft;
580
+ if ((yield* atEntryLimit) || !(yield* draft.canAllocate))
581
+ return yield* op.fail("NoSpace");
582
+ });
583
+ const reserveInode = (op) => Effect.flatMap(Draft, (draft) => Effect.flatMap(draft.canAllocate, (available) => available ? Effect.void : Effect.fail(op.fail("NoSpace"))));
584
+ const reserveBytes = Effect.fnUntraced(function* (op, bytes) {
585
+ if (limits.maxBytes === undefined)
586
+ return;
587
+ const draft = yield* Draft;
588
+ if (bytes > ByteSize.toBigInt(limits.maxBytes) - (yield* draft.usedBytes)) {
589
+ return yield* op.fail("NoSpace");
590
+ }
591
+ });
592
+ const attach = Effect.fnUntraced(function* (parent, name, child, now) {
593
+ const d = yield* Draft;
594
+ yield* d.put(withEntries({
637
595
  ...parent,
638
596
  metadata: {
639
597
  ...parent.metadata,
@@ -643,89 +601,43 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
643
601
  }
644
602
  }, (entries) => entries.set(name, child.ino)));
645
603
  if (child.kind === "directory")
646
- d.put({ ...child, parent: parent.ino, name });
604
+ yield* d.put({ ...child, parent: parent.ino, name });
647
605
  else {
648
- d.put({
606
+ yield* d.put({
649
607
  ...child,
650
608
  links: [...child.links, { parent: parent.ino, name }],
651
609
  metadata: { ...child.metadata, nlink: child.links.length + 1, ctimeNs: now }
652
610
  });
653
611
  }
654
- };
655
- const detach = (child, parent, name, now) => {
656
- const d = current();
612
+ });
613
+ const detach = Effect.fnUntraced(function* (child, parent, name, now) {
614
+ const d = yield* Draft;
657
615
  if (child.kind === "directory") {
658
- d.put({ ...child, metadata: { ...child.metadata, nlink: 0, ctimeNs: now } });
616
+ yield* d.put({ ...child, metadata: { ...child.metadata, nlink: 0, ctimeNs: now } });
659
617
  }
660
618
  else {
661
619
  const links = withoutLink(child.links, parent, name);
662
- d.put({ ...child, links, metadata: { ...child.metadata, nlink: links.length, ctimeNs: now } });
663
- }
664
- reclaim(d, child.ino);
665
- };
666
- const releaseFile = (ref) => {
667
- const ino = ref.ino;
668
- if (ino !== undefined) {
669
- const d = current();
670
- d.release(ino);
671
- reclaim(d, ino);
620
+ yield* d.put({ ...child, links, metadata: { ...child.metadata, nlink: links.length, ctimeNs: now } });
672
621
  }
673
- ref.ino = undefined;
674
- ref.closed = true;
675
- };
676
- const releaseOpenFile = (ref) => coordinatedCleanup(Effect.sync(() => {
677
- if (!ref.closed)
678
- applyDirect(() => releaseFile(ref));
679
- }));
680
- // Explicit close and scope cleanup share one release. A release whose commit fails still completes as
681
- // cleanup, so the handle never stays open; only an explicit close reports the failure. A close refused
682
- // admission never entered the volume, so it leaves the handle open for a retry.
683
- const closeFile = (ref, op, check) => coordinated(op, Effect.andThen(check, Effect.sync(() => {
684
- const ino = ref.ino;
685
- if (ino !== undefined) {
686
- const d = current();
687
- d.release(ino);
688
- reclaim(d, ino);
689
- }
690
- current().after.push(() => {
691
- ref.ino = undefined;
692
- ref.closed = true;
693
- });
694
- })), () => applyDirect(() => releaseFile(ref))).pipe(Effect.tapError((error) => error.code === "VolumeBusy" ? Effect.void : releaseOpenFile(ref)));
695
- // Keyed on the file rather than the closed flag, so a rerun releases an open that published after a first run.
696
- // Cleanup is uninterruptible and does not need admission, so an interrupted or busy scope close still releases.
697
- const finalizeFile = (ref) => Effect.uninterruptible(Effect.suspend(() => ref.ino === undefined
698
- ? Effect.sync(() => {
699
- ref.closed = true;
700
- })
701
- : Effect.ignore(closeFile(ref, OpContext.make("close"), Effect.void)).pipe(Effect.andThen(Effect.suspend(() => ref.closed ? Effect.void : releaseOpenFile(ref))))));
702
- // Acquires a handle into its own scope, forked from the caller's. The finalizer is registered before waiting,
703
- // since a closed scope runs a new finalizer at once and the permit is not reentrant. A scope that closes
704
- // before or during acquisition interrupts it, and releasing here, under the permit, keeps the acquisition
705
- // from outliving a finalizer that already ran. A scope that closes while the acquisition commits interrupts it
706
- // too. Every exit without a handle reruns the finalizer, which may have run before the commit published and
707
- // which closing an already closed scope would not run again.
708
- const acquireHandle = Effect.fnUntraced(function* (reference, coordinate, acquire, releaseAcquired, finalize) {
709
- const scope = yield* Scope.fork(yield* Effect.scope);
710
- reference.scope = scope;
711
- yield* Scope.addFinalizer(scope, finalize);
712
- const closed = () => Predicate.isTagged(scope.state, "Closed");
713
- return yield* coordinate(Effect.suspend(() => closed() ? Effect.interrupt : acquire).pipe(Effect.tap(() => Effect.suspend(() => {
714
- if (!closed())
715
- return Effect.void;
716
- releaseAcquired();
717
- return Effect.interrupt;
718
- })))).pipe(Effect.tap(() => Effect.suspend(() => closed() ? Effect.interrupt : Effect.void)), Effect.onError(() => Effect.andThen(finalize, Scope.close(scope, Exit.void))));
622
+ yield* reclaim(d, child.ino);
719
623
  });
720
- const bindFile = (ref, ino) => {
721
- current().after.push(() => {
722
- ref.ino = ino;
723
- });
724
- };
725
- const replaceContent = (file, data, now, publish = true) => {
726
- const d = current();
727
- d.usedBytes += BigInt(data.length - file.data.length);
728
- d.put({
624
+ const releaseFileInode = Effect.fnUntraced(function* (ino) {
625
+ const d = yield* Draft;
626
+ yield* d.release(ino);
627
+ yield* reclaim(d, ino);
628
+ });
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)));
637
+ const replaceContent = Effect.fnUntraced(function* (file, data, now, publish = true) {
638
+ const d = yield* Draft;
639
+ yield* d.addUsedBytes(BigInt(data.length - file.data.length));
640
+ yield* d.put({
729
641
  ...file,
730
642
  data,
731
643
  metadata: {
@@ -737,8 +649,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
737
649
  }
738
650
  });
739
651
  if (publish)
740
- publishNode(file.ino);
741
- };
652
+ yield* publishNode(file.ino);
653
+ });
742
654
  const resizeAt = Effect.fnUntraced(function* (file, length, now, op) {
743
655
  if (length > BigInt(maxFileBytes))
744
656
  return yield* op.fail("FileTooLarge");
@@ -746,22 +658,23 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
746
658
  yield* reserveBytes(op, BigInt(size - file.data.length));
747
659
  const data = new Uint8Array(size);
748
660
  data.set(file.data.subarray(0, size));
749
- replaceContent(file, data, now, false);
661
+ yield* replaceContent(file, data, now, false);
750
662
  });
751
663
  const resize = Effect.fnUntraced(function* (file, length, op) {
752
664
  if (!isLength(length))
753
665
  return yield* op.fail("InvalidArgument");
754
666
  yield* resizeAt(file, length, yield* timestamp(op), op);
755
- publishNode(file.ino);
667
+ yield* publishNode(file.ino);
756
668
  });
757
669
  const fileHandle = (ref) => {
758
- const get = (op, access) => {
759
- const node = ref.ino === undefined ? undefined : view(ref.ino);
760
- return node?.kind !== "file" || (access === "read" && ref.access === "write") ||
761
- (access === "write" && ref.access === "read")
762
- ? Effect.fail(op.fail("InvalidHandle"))
763
- : Effect.succeed(node);
764
- };
670
+ const get = Effect.fnUntraced(function* (op, access) {
671
+ const ino = TokenRegistry.inode(ref);
672
+ const node = ino === undefined ? undefined : yield* view(ino);
673
+ if (node?.kind !== "file" || (access === "read" && ref.access === "write") ||
674
+ (access === "write" && ref.access === "read"))
675
+ return yield* op.fail("InvalidHandle");
676
+ return node;
677
+ });
765
678
  const read = (maximum, position) => {
766
679
  const op = OpContext.make(position === undefined ? "read" : "pread");
767
680
  const readOp = OpContext.make("read");
@@ -783,9 +696,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
783
696
  // other would start at the same offset.
784
697
  if (position !== undefined)
785
698
  return Effect.map(accessing(op, body), ({ bytes, eof }) => ({ bytes, eof }));
786
- return coordinated(op, Effect.map(body, (read) => {
787
- const { bytes, eof, next } = refreshAccess(read);
788
- current().after.push(() => {
699
+ return coordinated(op, Effect.gen(function* () {
700
+ const { bytes, eof, next } = yield* refreshAccess(yield* body);
701
+ const draft = yield* Draft;
702
+ yield* draft.afterInstall(() => {
789
703
  ref.offset = next;
790
704
  });
791
705
  return { bytes, eof };
@@ -809,9 +723,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
809
723
  if (offset >= BigInt(maxFileBytes))
810
724
  return yield* writeOp.fail("FileTooLarge");
811
725
  const start = Number(offset);
812
- const free = settings.maxBytes === undefined
726
+ const draft = yield* Draft;
727
+ const free = limits.maxBytes === undefined
813
728
  ? BigInt(maxFileBytes)
814
- : ByteSize.toBigInt(settings.maxBytes) - current().usedBytes;
729
+ : ByteSize.toBigInt(limits.maxBytes) - (yield* draft.usedBytes);
815
730
  const maximumEnd = BigInt(file.data.length) + free;
816
731
  const end = Number(BigInt(maxFileBytes) < maximumEnd ? BigInt(maxFileBytes) : maximumEnd);
817
732
  const count = Math.min(bytes.length, Math.max(0, end - start));
@@ -824,10 +739,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
824
739
  data.set(file.data);
825
740
  const now = yield* timestamp(writeOp);
826
741
  data.set(bytes.subarray(0, count), start);
827
- replaceContent(file, data, now);
742
+ yield* replaceContent(file, data, now);
828
743
  if (position === undefined) {
829
744
  const next = offset + BigInt(count);
830
- current().after.push(() => {
745
+ yield* draft.afterInstall(() => {
831
746
  ref.offset = next;
832
747
  });
833
748
  }
@@ -886,50 +801,44 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
886
801
  sync: coordinatedRead(syncOp, Effect.suspend(() => Effect.asVoid(get(syncOp)))).pipe(Effect.withSpan("FileHandle.sync")),
887
802
  close: closeFile(ref, closeOp, Effect.suspend(() => get(closeOp))).pipe(Effect.ensuring(closeReleasedScope(ref)), Effect.withSpan("FileHandle.close"))
888
803
  });
889
- files.set(handle, ref);
804
+ registry.registerFile(handle, ref);
890
805
  return handle;
891
806
  };
892
807
  const createCaller = (reference, identity, umask) => {
893
- const referencedNode = Effect.fnUntraced(function* (target, op) {
894
- if (reference.ino === undefined)
895
- return yield* op.fail("ClosedCaller");
896
- if (!Predicate.isObject(target))
897
- return yield* op.fail("InvalidReference");
898
- const known = objectReferences.get(target);
899
- if (known === undefined)
900
- return yield* op.fail("InvalidReference");
901
- if (known.volume !== volumeIdentity) {
902
- return yield* op.fail("ForeignReference");
903
- }
904
- const node = view(known.ino);
905
- if (node === undefined || (node.kind === "directory" && node.metadata.nlink === 0)) {
906
- return yield* op.fail("StaleReference");
907
- }
908
- return node;
808
+ const resolver = Resolution.make({
809
+ caller: reference,
810
+ get: view,
811
+ identity,
812
+ maxPathBytes: limits.maxPathBytes,
813
+ registry
909
814
  });
910
- const callerDirectory = (op) => {
911
- const node = reference.ino === undefined ? undefined : view(reference.ino);
912
- return node?.kind === "directory" ? Effect.succeed(node) : Effect.fail(op.fail("ClosedCaller"));
913
- };
914
- const directoryNow = (ino) => {
915
- const node = view(ino);
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);
822
+ const directoryNow = Effect.fnUntraced(function* (ino) {
823
+ const node = yield* view(ino);
916
824
  if (node?.kind !== "directory") {
917
825
  return dieInvariant("Directory left the inode table during a transition");
918
826
  }
919
827
  return node;
920
- };
921
- const nodeNow = (ino) => {
922
- const node = view(ino);
828
+ });
829
+ const nodeNow = Effect.fnUntraced(function* (ino) {
830
+ const node = yield* view(ino);
923
831
  if (node === undefined)
924
832
  return dieInvariant("Inode left the table during a transition");
925
833
  return node;
926
- };
834
+ });
927
835
  const creationTimes = (times, now) => ({
928
836
  atimeNs: times?.access.kind === "value" ? times.access.nanoseconds : now,
929
837
  mtimeNs: times?.modification.kind === "value" ? times.modification.nanoseconds : now
930
838
  });
931
- const newDirectory = (parent, mode, now, times) => {
932
- const ino = current().allocate();
839
+ const newDirectory = Effect.fnUntraced(function* (parent, mode, now, times) {
840
+ const draft = yield* Draft;
841
+ const ino = yield* draft.allocate;
933
842
  return {
934
843
  kind: "directory",
935
844
  ino,
@@ -942,9 +851,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
942
851
  },
943
852
  revision: 0n
944
853
  };
945
- };
946
- const newFile = (parent, data, mode, now, owner, times) => {
947
- const ino = current().allocate();
854
+ });
855
+ const newFile = Effect.fnUntraced(function* (parent, data, mode, now, owner, times) {
856
+ const draft = yield* Draft;
857
+ const ino = yield* draft.allocate;
948
858
  return {
949
859
  kind: "file",
950
860
  ino,
@@ -959,9 +869,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
959
869
  },
960
870
  revision: 0n
961
871
  };
962
- };
963
- const newSymlink = (parent, target, now, times) => {
964
- const ino = current().allocate();
872
+ });
873
+ const newSymlink = Effect.fnUntraced(function* (parent, target, now, times) {
874
+ const draft = yield* Draft;
875
+ const ino = yield* draft.allocate;
965
876
  return {
966
877
  kind: "symlink",
967
878
  ino,
@@ -976,142 +887,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
976
887
  },
977
888
  revision: 0n
978
889
  };
979
- };
980
- const lookup = Effect.fnUntraced(function* (path, base, op, options = {}, referencedBase) {
981
- const pathOp = op.at(path.input);
982
- const { allowMissing = false, createMissing, followFinalSymlink = true, parentOnly = false } = options;
983
- if (reference.ino === undefined) {
984
- return yield* pathOp.fail("ClosedCaller");
985
- }
986
- let current = path.absolute ? nodeNow(ROOT_INO) : yield* callerDirectory(pathOp);
987
- if (!path.absolute && referencedBase !== undefined) {
988
- current = referencedBase;
989
- yield* authorize(current, identity, EXECUTE, op);
990
- }
991
- else if (!path.absolute && base !== undefined) {
992
- const target = handles.get(base);
993
- if (target === undefined) {
994
- return yield* pathOp.fail("InvalidHandle");
995
- }
996
- if (target.volume !== volumeIdentity) {
997
- return yield* pathOp.fail("ForeignHandle");
998
- }
999
- const directory = target.ino === undefined ? undefined : view(target.ino);
1000
- if (directory?.kind !== "directory") {
1001
- return yield* pathOp.fail("InvalidHandle");
1002
- }
1003
- current = directory;
1004
- yield* authorize(current, identity, EXECUTE, pathOp);
1005
- }
1006
- if (current.metadata.nlink === 0) {
1007
- return yield* pathOp.fail("NotFound");
1008
- }
1009
- let work = path;
1010
- let parent;
1011
- let name;
1012
- let traversals = 0;
1013
- // Leading components that came from a symbolic link's target. Only the components the caller wrote are
1014
- // created, so a dangling link stays missing, as it does for mkdir -p.
1015
- let linked = 0;
1016
- for (let index = 0; index < work.components.length - (parentOnly ? 1 : 0); index++) {
1017
- if (current.kind !== "directory") {
1018
- return yield* pathOp.fail("NotDirectory");
1019
- }
1020
- yield* authorize(current, identity, EXECUTE, pathOp);
1021
- const component = work.components[index];
1022
- if (component === undefined)
1023
- break;
1024
- if (component === DOT_HEX)
1025
- continue;
1026
- if (component === DOT_DOT_HEX) {
1027
- current = directoryNow(current.parent);
1028
- parent = undefined;
1029
- name = undefined;
1030
- continue;
1031
- }
1032
- parent = current;
1033
- name = component;
1034
- const childIno = current.entries.get(component);
1035
- const child = childIno === undefined ? undefined : view(childIno);
1036
- if (child === undefined) {
1037
- if (allowMissing && index === work.components.length - 1) {
1038
- return { node: undefined, parent, name };
1039
- }
1040
- if (createMissing !== undefined && index >= linked) {
1041
- current = yield* createMissing(current, component, index === work.components.length - 1);
1042
- continue;
1043
- }
1044
- return yield* pathOp.fail("NotFound");
1045
- }
1046
- if (child.kind === "symlink" && (followFinalSymlink || index < work.components.length - 1 || work.trailingSlash)) {
1047
- if (child.target.length === 0) {
1048
- return yield* pathOp.fail("NotFound");
1049
- }
1050
- if (++traversals > MAX_SYMLINK_TRAVERSALS) {
1051
- return yield* pathOp.fail("SymlinkLoop");
1052
- }
1053
- const suffix = work.suffixes[index] ?? new Uint8Array(0);
1054
- const remaining = work.components.length - index - 1;
1055
- if (settings.maxPathBytes !== undefined &&
1056
- ByteSize.isGreaterThan(ByteSize.bytes(child.target.length + suffix.length), settings.maxPathBytes)) {
1057
- return yield* pathOp.fail("PathTooLong");
1058
- }
1059
- const expansion = new Uint8Array(child.target.length + suffix.length);
1060
- expansion.set(child.target);
1061
- expansion.set(suffix, child.target.length);
1062
- const expanded = preparePath(ownedPath(expansion), op.operation, settings.maxPathBytes);
1063
- // The expansion is synthetic: its per-component limits are the caller's to hear about,
1064
- // but the path in the error has to be the one the caller passed in, so the
1065
- // expansion's own failure is not kept as the cause.
1066
- if (Result.isFailure(expanded)) {
1067
- return yield* pathOp.fail(expanded.failure.code);
1068
- }
1069
- work = expanded.success;
1070
- linked = work.components.length - remaining + Math.max(0, linked - index - 1);
1071
- if (work.absolute)
1072
- current = nodeNow(ROOT_INO);
1073
- index = -1;
1074
- }
1075
- else
1076
- current = child;
1077
- }
1078
- if (!parentOnly && work.trailingSlash && current.kind !== "directory") {
1079
- return yield* pathOp.fail("NotDirectory");
1080
- }
1081
- return { node: current, parent, name };
1082
- });
1083
- const resolveNode = Effect.fnUntraced(function* (path, base, op, options) {
1084
- const pathOp = op.at(path.input);
1085
- const result = yield* lookup(path, base, op, options);
1086
- if (result.node === undefined) {
1087
- return yield* pathOp.fail("NotFound");
1088
- }
1089
- return result.node;
1090
890
  });
1091
- const locate = Effect.fnUntraced(function* (path, base, op, options) {
1092
- const pathOp = op.at(path.input);
1093
- const result = yield* lookup(path, base, op, options);
1094
- const node = result.node;
1095
- if (node === undefined) {
1096
- return yield* pathOp.fail("NotFound");
1097
- }
1098
- if (node.kind !== "directory") {
1099
- return yield* pathOp.fail("NotDirectory");
1100
- }
1101
- return node;
1102
- });
1103
- const ResolvedEntry = {
1104
- fromPath: Effect.fnUntraced(function* (path, base, op) {
1105
- const parent = yield* locate(path, base, op, { parentOnly: true });
1106
- return {
1107
- parent: parent.ino,
1108
- name: path.components.at(-1),
1109
- trailingSlash: path.trailingSlash,
1110
- addressing: "path",
1111
- op: op.at(path.input)
1112
- };
1113
- })
1114
- };
1115
891
  const grantedMode = (metadata, mode) => !identity.privileged && metadata.kind === "file" && !inGroup(identity, metadata.gid) ? mode & ~0o2000 : mode;
1116
892
  const permittedMode = (metadata, mode, op) => !identity.privileged && identity.uid !== metadata.uid
1117
893
  ? Effect.fail(op.fail("NotPermitted"))
@@ -1137,7 +913,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1137
913
  const resolving = resolve();
1138
914
  return coordinated(op, Effect.gen(function* () {
1139
915
  const resolved = yield* resolving;
1140
- const node = nodeNow(resolved.ino);
916
+ const node = yield* nodeNow(resolved.ino);
1141
917
  if (expected !== undefined && node.revision !== expected) {
1142
918
  return yield* resolved.op.fail("StaleReference", { field: "expected" });
1143
919
  }
@@ -1163,11 +939,11 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1163
939
  const now = yield* timestamp(op);
1164
940
  if (size !== undefined && node.kind === "file")
1165
941
  yield* resizeAt(node, size, now, op);
1166
- const sized = nodeNow(resolved.ino);
942
+ const sized = yield* nodeNow(resolved.ino);
1167
943
  const gid = owner?.gid ?? sized.metadata.gid;
1168
944
  const cleared = chowned && sized.kind === "file" ? sized.metadata.mode & ~SET_ID_BITS : sized.metadata.mode;
1169
945
  if (chowned || mode !== undefined || timed) {
1170
- current().put({
946
+ yield* (yield* Draft).put({
1171
947
  ...sized,
1172
948
  metadata: {
1173
949
  ...sized.metadata,
@@ -1180,7 +956,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1180
956
  }
1181
957
  });
1182
958
  }
1183
- publishNode(node.ino);
959
+ yield* publishNode(node.ino);
1184
960
  }));
1185
961
  };
1186
962
  const authorizeRemoval = (parent, child, op) => (parent.metadata.mode & STICKY_BIT) !== 0 && !identity.privileged &&
@@ -1188,7 +964,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1188
964
  ? Effect.fail(op.fail("NotPermitted"))
1189
965
  : Effect.void;
1190
966
  const claimName = Effect.fnUntraced(function* (entry, trailingSlash = "allowed") {
1191
- const parent = directoryNow(entry.parent);
967
+ const parent = yield* directoryNow(entry.parent);
1192
968
  if (isDotComponent(entry.name)) {
1193
969
  return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : "AlreadyExists");
1194
970
  }
@@ -1203,17 +979,18 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1203
979
  const makeDirectory = Effect.fnUntraced(function* (entry, request, op) {
1204
980
  const name = yield* claimName(entry);
1205
981
  yield* reserveEntry(entry.op);
1206
- const parent = directoryNow(entry.parent);
982
+ const parent = yield* directoryNow(entry.parent);
1207
983
  const before = parent.revision;
1208
984
  const now = yield* timestamp(op);
1209
985
  const mode = request.exactMode
1210
986
  ? yield* permittedMode({ kind: "directory", uid: identity.uid, gid: parent.metadata.gid }, request.mode, op)
1211
987
  : (request.mode & 0o777 & ~umask) | (request.mode & STICKY_BIT);
1212
- const child = newDirectory(parent, mode, now, request.times);
1213
- attach(parent, name, child, now);
1214
- current().entries += 1;
1215
- publishEntry("Create", parent.ino, name, child.ino);
1216
- return { child: child.ino, directory: { before, after: current().revision } };
988
+ const child = yield* newDirectory(parent, mode, now, request.times);
989
+ yield* attach(parent, name, child, now);
990
+ const draft = yield* Draft;
991
+ yield* draft.addEntries(1);
992
+ yield* publishEntry("Create", parent.ino, name, child.ino);
993
+ return { child: child.ino, directory: { before, after: draft.revision } };
1217
994
  });
1218
995
  // Creates every missing directory a path names in the running transition, so a failure anywhere discards
1219
996
  // them all. The mode applies to each directory created and the times to the one in the final position. An
@@ -1229,57 +1006,64 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1229
1006
  const made = yield* Effect.result(makeDirectory(entry, request, op));
1230
1007
  if (Result.isSuccess(made))
1231
1008
  return made.success;
1232
- const existingIno = entry.name === undefined ? undefined : directoryNow(entry.parent).entries.get(entry.name);
1233
- node = existingIno === undefined ? undefined : view(existingIno);
1009
+ const existingIno = entry.name === undefined
1010
+ ? undefined
1011
+ : (yield* directoryNow(entry.parent)).entries.get(entry.name);
1012
+ node = existingIno === undefined ? undefined : (yield* view(existingIno));
1234
1013
  if (made.failure.code !== "AlreadyExists" || node?.kind !== "directory")
1235
1014
  return yield* made.failure;
1236
1015
  }
1237
1016
  else {
1238
1017
  entryOp = op.at(prepared.path.input);
1239
- const resolved = yield* lookup(prepared.path, prepared.base, op, {
1240
- createMissing: (parent, name, final) => Effect.map(makeDirectory({ parent: parent.ino, name, trailingSlash: false, addressing: "path", op: entryOp }, final ? request : { mode: request.mode, exactMode: request.exactMode }, op), (made) => {
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",
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) => {
1241
1023
  if (!revisionsBefore.has(parent.ino))
1242
1024
  revisionsBefore.set(parent.ino, made.directory.before);
1243
1025
  return directoryNow(made.child);
1244
1026
  })
1245
- });
1027
+ }, op);
1246
1028
  node = resolved.node;
1247
1029
  }
1248
1030
  if (node?.kind !== "directory")
1249
1031
  return yield* entryOp.fail("AlreadyExists");
1250
- const after = directoryNow(node.parent).revision;
1032
+ const after = (yield* directoryNow(node.parent)).revision;
1251
1033
  return { child: node.ino, directory: { before: revisionsBefore.get(node.parent) ?? after, after } };
1252
1034
  });
1253
1035
  const linkNode = Effect.fnUntraced(function* (node, entry, op) {
1254
1036
  const name = yield* claimName(entry, "rejected");
1255
1037
  yield* reserveEntry(entry.op);
1256
- const parent = directoryNow(entry.parent);
1038
+ const parent = yield* directoryNow(entry.parent);
1257
1039
  const before = parent.revision;
1258
1040
  const now = yield* timestamp(op);
1259
- attach(parent, name, node, now);
1260
- current().entries += 1;
1261
- publishEntry("Create", parent.ino, name, node.ino);
1262
- return { before, after: current().revision };
1041
+ yield* attach(parent, name, node, now);
1042
+ const draft = yield* Draft;
1043
+ yield* draft.addEntries(1);
1044
+ yield* publishEntry("Create", parent.ino, name, node.ino);
1045
+ return { before, after: draft.revision };
1263
1046
  });
1264
1047
  const makeSymlink = Effect.fnUntraced(function* (entry, target, times, op) {
1265
1048
  const name = yield* claimName(entry, "rejected");
1266
1049
  yield* reserveEntry(entry.op);
1267
1050
  yield* reserveBytes(entry.op, BigInt(target.length));
1268
- const parent = directoryNow(entry.parent);
1051
+ const parent = yield* directoryNow(entry.parent);
1269
1052
  const before = parent.revision;
1270
1053
  const now = yield* timestamp(op);
1271
- const child = newSymlink(parent, target, now, times);
1272
- attach(parent, name, child, now);
1273
- current().entries += 1;
1274
- current().usedBytes += BigInt(target.length);
1275
- publishEntry("Create", parent.ino, name, child.ino);
1276
- return { child: child.ino, directory: { before, after: current().revision } };
1054
+ const child = yield* newSymlink(parent, target, now, times);
1055
+ yield* attach(parent, name, child, now);
1056
+ const draft = yield* Draft;
1057
+ yield* draft.addEntries(1);
1058
+ yield* draft.addUsedBytes(BigInt(target.length));
1059
+ yield* publishEntry("Create", parent.ino, name, child.ino);
1060
+ return { child: child.ino, directory: { before, after: draft.revision } };
1277
1061
  });
1278
1062
  const removeChild = Effect.fnUntraced(function* (parent, name, child, op) {
1279
1063
  const before = parent.revision;
1280
1064
  const now = yield* timestamp(op);
1281
- const d = current();
1282
- d.put(withEntries({
1065
+ const d = yield* Draft;
1066
+ yield* d.put(withEntries({
1283
1067
  ...parent,
1284
1068
  metadata: {
1285
1069
  ...parent.metadata,
@@ -1288,22 +1072,22 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1288
1072
  ctimeNs: now
1289
1073
  }
1290
1074
  }, (entries) => entries.delete(name)));
1291
- publishEntry("Remove", parent.ino, name, child.ino);
1292
- detach(child, parent.ino, name, now);
1293
- d.entries -= 1;
1075
+ yield* publishEntry("Remove", parent.ino, name, child.ino);
1076
+ yield* detach(child, parent.ino, name, now);
1077
+ yield* d.addEntries(-1);
1294
1078
  return { before, after: d.revision };
1295
1079
  });
1296
1080
  // Authorizes removing from the entry's directory and returns the named child. Only a path can name a dot
1297
1081
  // entry, and each verb reports it with its own code. Search permission comes before the lookup and write
1298
1082
  // permission after it, as on Linux; `beforeWrite` holds the verb's own checks that Linux makes in between.
1299
1083
  const removalTarget = Effect.fnUntraced(function* (entry, dotNameCode, beforeWrite) {
1300
- const parent = directoryNow(entry.parent);
1084
+ const parent = yield* directoryNow(entry.parent);
1301
1085
  if (isDotComponent(entry.name)) {
1302
1086
  return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : dotNameCode);
1303
1087
  }
1304
1088
  yield* authorize(parent, identity, EXECUTE, entry.op);
1305
1089
  const childIno = parent.entries.get(entry.name);
1306
- const child = childIno === undefined ? undefined : view(childIno);
1090
+ const child = childIno === undefined ? undefined : (yield* view(childIno));
1307
1091
  if (child === undefined)
1308
1092
  return yield* entry.op.fail("NotFound");
1309
1093
  const rejected = beforeWrite?.(child);
@@ -1338,8 +1122,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1338
1122
  return yield* removeChild(parent, name, child, op);
1339
1123
  });
1340
1124
  const renameEntry = Effect.fnUntraced(function* (source, destination, op) {
1341
- const sourceDirectory = directoryNow(source.parent);
1342
- const destinationDirectory = directoryNow(destination.parent);
1125
+ const sourceDirectory = yield* directoryNow(source.parent);
1126
+ const destinationDirectory = yield* directoryNow(destination.parent);
1343
1127
  const sameDirectory = source.parent === destination.parent;
1344
1128
  const sourceName = source.name;
1345
1129
  const destinationName = destination.name;
@@ -1353,11 +1137,11 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1353
1137
  const sourceBefore = sourceDirectory.revision;
1354
1138
  const destinationBefore = destinationDirectory.revision;
1355
1139
  const childIno = sourceDirectory.entries.get(sourceName);
1356
- const child = childIno === undefined ? undefined : view(childIno);
1140
+ const child = childIno === undefined ? undefined : (yield* view(childIno));
1357
1141
  if (child === undefined)
1358
1142
  return yield* source.op.fail("NotFound");
1359
1143
  const replacedIno = destinationDirectory.entries.get(destinationName);
1360
- const replaced = replacedIno === undefined ? undefined : view(replacedIno);
1144
+ const replaced = replacedIno === undefined ? undefined : (yield* view(replacedIno));
1361
1145
  // A trailing slash on either side asks for a directory; a missing slashed destination is fine when the
1362
1146
  // source is one, as Linux allows.
1363
1147
  if ((source.trailingSlash || destination.trailingSlash) && child.kind !== "directory") {
@@ -1366,24 +1150,29 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1366
1150
  if (destination.trailingSlash && replaced !== undefined && replaced.kind !== "directory") {
1367
1151
  return yield* destination.op.fail("NotDirectory");
1368
1152
  }
1369
- const result = () => sameDirectory
1370
- ? {
1371
- _tag: "SameDirectory",
1372
- directory: { before: sourceBefore, after: directoryNow(source.parent).revision }
1373
- }
1374
- : {
1375
- _tag: "DifferentDirectories",
1376
- sourceDirectory: { before: sourceBefore, after: directoryNow(source.parent).revision },
1377
- destinationDirectory: { before: destinationBefore, after: directoryNow(destination.parent).revision }
1378
- };
1379
- for (let ancestor = destinationDirectory;; ancestor = directoryNow(ancestor.parent)) {
1153
+ const result = Effect.fnUntraced(function* () {
1154
+ return sameDirectory
1155
+ ? {
1156
+ _tag: "SameDirectory",
1157
+ directory: { before: sourceBefore, after: (yield* directoryNow(source.parent)).revision }
1158
+ }
1159
+ : {
1160
+ _tag: "DifferentDirectories",
1161
+ sourceDirectory: { before: sourceBefore, after: (yield* directoryNow(source.parent)).revision },
1162
+ destinationDirectory: {
1163
+ before: destinationBefore,
1164
+ after: (yield* directoryNow(destination.parent)).revision
1165
+ }
1166
+ };
1167
+ });
1168
+ for (let ancestor = destinationDirectory;; ancestor = yield* directoryNow(ancestor.parent)) {
1380
1169
  if (ancestor.ino === child.ino)
1381
1170
  return yield* destination.op.fail("InvalidArgument");
1382
1171
  if (ancestor.ino === ROOT_INO)
1383
1172
  break;
1384
1173
  }
1385
1174
  if (child.ino === replaced?.ino)
1386
- return result();
1175
+ return yield* result();
1387
1176
  yield* authorize(sourceDirectory, identity, WRITE, source.op);
1388
1177
  yield* authorize(destinationDirectory, identity, WRITE, destination.op);
1389
1178
  yield* authorizeRemoval(sourceDirectory, child, source.op);
@@ -1400,9 +1189,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1400
1189
  }
1401
1190
  }
1402
1191
  const now = yield* timestamp(op);
1403
- const d = current();
1404
- publishEntry("Remove", sourceDirectory.ino, sourceName, child.ino);
1405
- d.put(withEntries({
1192
+ const d = yield* Draft;
1193
+ yield* publishEntry("Remove", sourceDirectory.ino, sourceName, child.ino);
1194
+ yield* d.put(withEntries({
1406
1195
  ...sourceDirectory,
1407
1196
  metadata: {
1408
1197
  ...sourceDirectory.metadata,
@@ -1411,8 +1200,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1411
1200
  ctimeNs: now
1412
1201
  }
1413
1202
  }, (entries) => entries.delete(sourceName)));
1414
- const destinationNow = directoryNow(destination.parent);
1415
- d.put(withEntries({
1203
+ const destinationNow = yield* directoryNow(destination.parent);
1204
+ yield* d.put(withEntries({
1416
1205
  ...destinationNow,
1417
1206
  metadata: {
1418
1207
  ...destinationNow.metadata,
@@ -1421,9 +1210,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1421
1210
  ctimeNs: now
1422
1211
  }
1423
1212
  }, (entries) => entries.set(destinationName, child.ino)));
1424
- const moved = nodeNow(child.ino);
1213
+ const moved = yield* nodeNow(child.ino);
1425
1214
  if (moved.kind === "directory") {
1426
- d.put({
1215
+ yield* d.put({
1427
1216
  ...moved,
1428
1217
  parent: destination.parent,
1429
1218
  name: destinationName,
@@ -1431,7 +1220,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1431
1220
  });
1432
1221
  }
1433
1222
  else {
1434
- d.put({
1223
+ yield* d.put({
1435
1224
  ...moved,
1436
1225
  links: [
1437
1226
  ...withoutLink(moved.links, source.parent, sourceName),
@@ -1441,11 +1230,11 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1441
1230
  });
1442
1231
  }
1443
1232
  if (replaced !== undefined) {
1444
- detach(nodeNow(replaced.ino), destination.parent, destinationName, now);
1445
- d.entries -= 1;
1233
+ yield* detach(yield* nodeNow(replaced.ino), destination.parent, destinationName, now);
1234
+ yield* d.addEntries(-1);
1446
1235
  }
1447
- publishEntry("Create", destination.parent, destinationName, child.ino);
1448
- return result();
1236
+ yield* publishEntry("Create", destination.parent, destinationName, child.ino);
1237
+ return yield* result();
1449
1238
  });
1450
1239
  const openExisting = Effect.fnUntraced(function* (file, request, at, op) {
1451
1240
  yield* authorize(file, identity, request.access === "read" ? READ : request.access === "write" ? WRITE : READ | WRITE, at);
@@ -1454,7 +1243,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1454
1243
  });
1455
1244
  const openFile = Effect.fnUntraced(function* (entry, found, request, acquired, op) {
1456
1245
  const { name } = entry;
1457
- const parent = directoryNow(entry.parent);
1246
+ const parent = yield* directoryNow(entry.parent);
1458
1247
  const before = parent.revision;
1459
1248
  let file = found;
1460
1249
  let created = false;
@@ -1481,12 +1270,13 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1481
1270
  const mode = request.exactMode
1482
1271
  ? yield* permittedMode({ kind: "file", uid: owner?.uid ?? identity.uid, gid: owner?.gid ?? parent.metadata.gid }, request.mode, op)
1483
1272
  : (request.mode ?? 0o666) & 0o777 & ~umask;
1484
- file = newFile(parent, new Uint8Array(Number(size)), mode, now, owner, request.times);
1485
- attach(parent, name, file, now);
1486
- current().entries += 1;
1487
- current().usedBytes += size;
1273
+ file = yield* newFile(parent, new Uint8Array(Number(size)), mode, now, owner, request.times);
1274
+ yield* attach(parent, name, file, now);
1275
+ const draft = yield* Draft;
1276
+ yield* draft.addEntries(1);
1277
+ yield* draft.addUsedBytes(size);
1488
1278
  created = true;
1489
- publishEntry("Create", parent.ino, name, file.ino);
1279
+ yield* publishEntry("Create", parent.ino, name, file.ino);
1490
1280
  }
1491
1281
  else {
1492
1282
  if (file.kind === "symlink")
@@ -1495,92 +1285,23 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1495
1285
  return yield* entry.op.fail("IsDirectory");
1496
1286
  yield* openExisting(file, request, entry.op, op);
1497
1287
  }
1498
- current().retain(file.ino);
1499
- bindFile(acquired, file.ino);
1500
- return { ino: file.ino, created, directory: { before, after: directoryNow(entry.parent).revision } };
1288
+ yield* (yield* Draft).retain(file.ino);
1289
+ yield* bindFile(acquired, file.ino);
1290
+ return { ino: file.ino, created, directory: { before, after: (yield* directoryNow(entry.parent)).revision } };
1501
1291
  });
1502
- const releasePending = (opened) => () => {
1292
+ const releasePending = (opened) => () => Effect.gen(function* () {
1503
1293
  const ino = opened();
1504
1294
  if (ino === undefined)
1505
1295
  return;
1506
- current().release(ino);
1507
- reclaim(current(), ino);
1508
- };
1296
+ yield* releaseFileInode(ino);
1297
+ });
1509
1298
  const acquireOpenedFile = (ref, op, acquire) => {
1510
1299
  let opened;
1511
1300
  return acquireHandle(ref, (effect) => coordinated(op, effect), acquire((ino) => {
1512
1301
  opened = ino;
1513
1302
  }), releasePending(() => opened), finalizeFile(ref));
1514
1303
  };
1515
- const prepare = (input, op) => preparePath(input, op.operation, settings.maxPathBytes);
1516
- const handleNode = Effect.fnUntraced(function* (handle, op) {
1517
- if (reference.ino === undefined)
1518
- return yield* op.fail("ClosedCaller");
1519
- const ref = isFileHandle(handle) ? files.get(handle) : handles.get(handle);
1520
- if (ref === undefined)
1521
- return yield* op.fail("InvalidHandle");
1522
- if (ref.volume !== volumeIdentity)
1523
- return yield* op.fail("ForeignHandle");
1524
- const node = ref.ino === undefined ? undefined : view(ref.ino);
1525
- if (node === undefined)
1526
- return yield* op.fail("InvalidHandle");
1527
- return node;
1528
- });
1529
- // A path prepares before the walk, so a malformed input fails before any lookup, and names the path on
1530
- // every failure. A reference or handle names none. The verb's `followFinalSymlink` is the default a
1531
- // path's own setting overrides; `final` makes the verb's setting win, for a verb such as `readLink` whose
1532
- // meaning fixes it.
1533
- const resolveTarget = (target, op, options) => Target.$match(target, {
1534
- Path: ({ followFinalSymlink, path, relativeTo }) => Effect.flatMap(Effect.fromResult(prepare(path, op)), (prepared) => Effect.map(resolveNode(prepared, relativeTo, op, {
1535
- followFinalSymlink: options?.final === true
1536
- ? options.followFinalSymlink ?? true
1537
- : followFinalSymlink ?? options?.followFinalSymlink ?? true
1538
- }), (node) => ({ node, op: op.at(path) }))),
1539
- Reference: ({ reference }) => Effect.map(referencedNode(reference, op), (node) => ({ node, op })),
1540
- Handle: ({ handle }) => Effect.map(handleNode(handle, op), (node) => ({ node, op }))
1541
- });
1542
- const resolveDirectory = Effect.fnUntraced(function* (target, op) {
1543
- const resolved = yield* resolveTarget(target, op);
1544
- if (resolved.node.kind !== "directory")
1545
- return yield* resolved.op.fail("NotDirectory");
1546
- return { node: resolved.node, op: resolved.op };
1547
- });
1548
- // The directory an entry names a child of. A removed directory that a handle still holds takes no new
1549
- // children, as on Linux, where creating through a descriptor of a removed directory fails with ENOENT.
1550
- const entryDirectory = Effect.fnUntraced(function* (target, op) {
1551
- const directory = yield* resolveDirectory(target, op);
1552
- if (directory.node.metadata.nlink === 0)
1553
- return yield* directory.op.fail("NotFound");
1554
- return directory;
1555
- });
1556
1304
  const asResolvedNode = (target, op, options) => Effect.map(resolveTarget(target, op, options), (resolved) => ({ ino: resolved.node.ino, op: resolved.op }));
1557
- const entryName = (input, op) => {
1558
- if (Predicate.isString(input) && !isWellFormed(input))
1559
- return Result.fail(op.fail("InvalidPathEncoding"));
1560
- const bytes = Predicate.isString(input) ? encoder.encode(input) : input;
1561
- if (!isAttachedBytes(bytes) || bytes.length === 0 || bytes.length > MAX_NAME_BYTES || bytes.includes(0) ||
1562
- bytes.includes(SLASH_BYTE))
1563
- return Result.fail(op.fail("InvalidArgument"));
1564
- return Result.succeed(Encoding.encodeHex(new Uint8Array(bytes)));
1565
- };
1566
- const prepareEntry = (input, op) => isEntry(input)
1567
- ? Result.map(entryName(input.name, op), (name) => ({ kind: "entry", directory: input.directory, name }))
1568
- : isTarget(input)
1569
- ? Result.map(prepare(input.path, op), (path) => ({ kind: "path", path, base: input.relativeTo }))
1570
- : Result.map(prepare(input, op), (path) => ({ kind: "path", path, base: undefined }));
1571
- const resolveEntry = Effect.fnUntraced(function* (prepared, op) {
1572
- if (prepared.kind === "path") {
1573
- return yield* ResolvedEntry.fromPath(prepared.path, prepared.base, op);
1574
- }
1575
- const directory = yield* entryDirectory(prepared.directory, op);
1576
- return {
1577
- parent: directory.node.ino,
1578
- name: prepared.name,
1579
- trailingSlash: false,
1580
- addressing: "entry",
1581
- op
1582
- };
1583
- });
1584
1305
  const asTarget = (input) => Target.of(input);
1585
1306
  const acquireDirectory = Effect.fnUntraced(function* (input, op) {
1586
1307
  const target = asTarget(input);
@@ -1589,12 +1310,12 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1589
1310
  const directory = yield* resolveDirectory(target, op);
1590
1311
  yield* authorize(directory.node, identity, EXECUTE, directory.op);
1591
1312
  holdDirectory(directory.node.ino);
1592
- acquired.ino = directory.node.ino;
1313
+ TokenRegistry.publish(acquired, directory.node.ino);
1593
1314
  return acquired;
1594
1315
  }),
1595
1316
  // Nothing to undo here: the finalizer that follows an interrupted acquisition releases the hold under
1596
1317
  // every permit, where a detached directory may leave the table.
1597
- () => { }, finalizeDirectory(acquired));
1318
+ () => Effect.void, finalizeDirectory(acquired));
1598
1319
  });
1599
1320
  const fail = (op, cause, field) => op.fail("InvalidArgument", field === undefined ? { cause } : { cause, field });
1600
1321
  const preparedOp = (prepared, op) => prepared.kind === "path" ? op.at(prepared.path.input) : op;
@@ -1612,29 +1333,16 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1612
1333
  const follow = target.followFinalSymlink ?? true;
1613
1334
  return yield* acquireOpenedFile(acquired, op, (opened) => Effect.gen(function* () {
1614
1335
  const path = yield* Effect.fromResult(prepared);
1615
- if (chosen.create === "exclusive") {
1616
- const existing = yield* Effect.result(lookup(path, target.relativeTo, op, { followFinalSymlink: false }));
1617
- if (Result.isSuccess(existing))
1618
- return yield* pathOp.fail("AlreadyExists");
1619
- if (existing.failure.code !== "NotFound")
1620
- return yield* existing.failure;
1621
- }
1622
- const resolved = yield* lookup(path, target.relativeTo, op, {
1623
- followFinalSymlink: follow,
1624
- allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
1625
- });
1626
- const parent = resolved.parent;
1627
- if (parent === undefined)
1628
- return yield* pathOp.fail("IsDirectory");
1629
- const name = resolved.name;
1630
- if (isDotComponent(name))
1631
- return yield* pathOp.fail("IsDirectory");
1632
- 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)
1633
1343
  return yield* pathOp.fail("IsDirectory");
1634
1344
  yield* authorize(parent, identity, EXECUTE, pathOp);
1635
- const file = resolved.node;
1636
- if (file !== undefined && chosen.create === "exclusive")
1637
- return yield* pathOp.fail("AlreadyExists");
1345
+ const file = resolved.kind === "existing" ? resolved.node : undefined;
1638
1346
  const result = yield* openFile({ parent: parent.ino, name, trailingSlash: path.trailingSlash, addressing: "path", op: pathOp }, file, chosen, acquired, op);
1639
1347
  opened(result.ino);
1640
1348
  return fileHandle(acquired);
@@ -1656,8 +1364,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1656
1364
  if (node.metadata.nlink === 0)
1657
1365
  return yield* resolved.op.fail("StaleReference");
1658
1366
  yield* openExisting(node, chosen, resolved.op, op);
1659
- current().retain(node.ino);
1660
- bindFile(acquired, node.ino);
1367
+ const draft = yield* Draft;
1368
+ yield* draft.retain(node.ino);
1369
+ yield* bindFile(acquired, node.ino);
1661
1370
  opened(node.ino);
1662
1371
  return fileHandle(acquired);
1663
1372
  }));
@@ -1675,60 +1384,44 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1675
1384
  }
1676
1385
  if (chosen.exactMode && chosen.mode === undefined)
1677
1386
  return yield* op.fail("InvalidArgument");
1678
- const relativePath = preparePath(ownedPath(nameBytes(name)), op.operation, undefined);
1679
1387
  const acquired = makeFileReference(chosen.access, chosen.append ?? false);
1680
1388
  const expected = chosen.expected;
1681
1389
  return yield* acquireOpenedFile(acquired, op, (opened) => Effect.gen(function* () {
1682
- const parent = (yield* entryDirectory(entry.directory, op)).node;
1683
- if (isDotComponent(name))
1684
- return yield* op.fail("InvalidArgument");
1685
- yield* authorize(parent, identity, EXECUTE, op);
1686
- const directIno = parent.entries.get(name);
1687
- const direct = directIno === undefined ? undefined : view(directIno);
1688
- if (expected !== undefined) {
1689
- let expectedIno;
1690
- if (expected !== null) {
1691
- const observed = yield* Effect.result(referencedNode(expected, op));
1692
- if (Result.isFailure(observed)) {
1693
- 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");
1694
1406
  }
1695
- expectedIno = observed.success.ino;
1696
- }
1697
- if (direct?.ino !== expectedIno)
1698
- return yield* op.fail("VolumeBusy");
1699
- }
1700
- if (chosen.expectedChild === null) {
1701
- if (direct !== undefined)
1702
- return yield* op.fail("StaleReference");
1703
- }
1704
- else if (chosen.expectedChild !== undefined) {
1705
- const expectedChild = chosen.expectedChild;
1706
- const observed = yield* referencedNode(expectedChild.reference, op);
1707
- if (direct?.ino !== observed.ino || observed.revision !== expectedChild.revision ||
1708
- observed.metadata.atimeNs !== expectedChild.atimeNs ||
1709
- observed.metadata.mtimeNs !== expectedChild.mtimeNs) {
1710
- return yield* op.fail("StaleReference");
1711
- }
1712
- }
1713
- if (direct !== undefined && chosen.create === "exclusive")
1714
- return yield* op.fail("AlreadyExists");
1715
- let file = direct;
1716
- let mutationParent = parent.ino;
1717
- let mutationName = name;
1718
- if (file?.kind === "symlink" && chosen.followFinalSymlink !== false) {
1719
- const resolved = yield* lookup(yield* Effect.fromResult(relativePath), undefined, op, {
1720
- followFinalSymlink: true,
1721
- allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
1722
- }, parent);
1723
- file = resolved.node;
1724
- if (file === undefined) {
1725
- if (resolved.parent === undefined || resolved.name === undefined) {
1726
- return yield* op.fail("IsDirectory");
1407
+ if (chosen.expectedChild === null) {
1408
+ if (direct !== undefined)
1409
+ return yield* op.fail("StaleReference");
1727
1410
  }
1728
- mutationParent = resolved.parent.ino;
1729
- mutationName = resolved.name;
1730
- }
1731
- }
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;
1732
1425
  const result = yield* openFile({ parent: mutationParent, name: mutationName, trailingSlash: false, addressing: "entry", op }, file, { ...chosen, initialSize: chosen.initialSize ?? 0n }, acquired, op);
1733
1426
  opened(result.ino);
1734
1427
  return {
@@ -1739,29 +1432,31 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1739
1432
  };
1740
1433
  }));
1741
1434
  });
1742
- const walkChildren = (directory, path, depth, up) => {
1435
+ const walkChildren = Effect.fnUntraced(function* (directory, path, depth, up) {
1743
1436
  const listed = referenceFor(directory.ino);
1744
- return Arr.flatMap(Arr.sort(directory.entries, byEntryName), ([name, childIno]) => {
1745
- const child = view(childIno);
1437
+ const frames = [];
1438
+ for (const [name, childIno] of Arr.sort(directory.entries, byEntryName)) {
1439
+ const child = yield* view(childIno);
1746
1440
  if (child === undefined)
1747
- return [];
1441
+ continue;
1748
1442
  const bytes = nameBytes(name);
1749
- return [{
1750
- ino: child.ino,
1751
- kind: child.kind,
1752
- name: bytes,
1753
- key: name,
1754
- path: joinPath(path, bytes),
1755
- parent: directory.ino,
1756
- up,
1757
- depth: depth + 1,
1758
- bytes: child.kind === "directory" ? 0n : child.metadata.size,
1759
- reference: referenceFor(child.ino),
1760
- directory: listed,
1761
- listed: false
1762
- }];
1763
- });
1764
- };
1443
+ frames.push({
1444
+ ino: child.ino,
1445
+ kind: child.kind,
1446
+ name: bytes,
1447
+ key: name,
1448
+ path: joinPath(path, bytes),
1449
+ parent: directory.ino,
1450
+ up,
1451
+ depth: depth + 1,
1452
+ bytes: child.kind === "directory" ? 0n : child.metadata.size,
1453
+ reference: referenceFor(child.ino),
1454
+ directory: listed,
1455
+ listed: false
1456
+ });
1457
+ }
1458
+ return frames;
1459
+ });
1765
1460
  const anchorFrame = (parent, key, root) => ({
1766
1461
  ino: root.ino,
1767
1462
  kind: root.kind,
@@ -1782,13 +1477,12 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1782
1477
  const prepared = prepare(target.path, op);
1783
1478
  if (Result.isFailure(prepared))
1784
1479
  return Effect.undefined;
1785
- return ResolvedEntry.fromPath(prepared.success, target.relativeTo, op).pipe(Effect.map((entry) => {
1786
- const parent = view(entry.parent);
1480
+ return resolveEntry({ kind: "path", path: prepared.success, base: target.relativeTo }, op).pipe(Effect.flatMap((entry) => Effect.map(view(entry.parent), (parent) => {
1787
1481
  return entry.name !== undefined && parent?.kind === "directory" &&
1788
1482
  parent.entries.get(entry.name) === root.ino
1789
1483
  ? anchorFrame(parent, entry.name, root)
1790
1484
  : undefined;
1791
- }), Effect.catch(() => Effect.undefined));
1485
+ })), Effect.catch(() => Effect.undefined));
1792
1486
  };
1793
1487
  const holds = (parent, frame) => parent?.kind === "directory" && parent.metadata.nlink > 0 && parent.entries.get(frame.key) === frame.ino;
1794
1488
  // The node a frame names, reached by name from the walk's root as a path lookup reaches it: every directory on
@@ -1801,13 +1495,13 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1801
1495
  const chain = [];
1802
1496
  for (let link = frame; link !== undefined; link = link.up)
1803
1497
  chain.push(link);
1804
- let node = view((chain.at(-1) ?? frame).parent);
1498
+ let node = yield* view((chain.at(-1) ?? frame).parent);
1805
1499
  for (let index = chain.length - 1; index >= 0; index--) {
1806
1500
  const link = chain[index];
1807
1501
  if (link === undefined || node?.kind !== "directory" || node.metadata.nlink === 0)
1808
1502
  return undefined;
1809
1503
  yield* authorize(node, identity, EXECUTE, at);
1810
- node = node.entries.get(link.key) === link.ino ? view(link.ino) : undefined;
1504
+ node = node.entries.get(link.key) === link.ino ? (yield* view(link.ino)) : undefined;
1811
1505
  }
1812
1506
  return node;
1813
1507
  });
@@ -1823,27 +1517,23 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1823
1517
  let bytes = 0n;
1824
1518
  let failure;
1825
1519
  const list = (frame) => coordinatedRead(op, Effect.suspend(() => {
1826
- if (reference.ino === undefined)
1520
+ if (TokenRegistry.inode(reference) === undefined)
1827
1521
  return Effect.fail(op.fail("ClosedCaller"));
1828
1522
  const at = op.at(locate(frame.path));
1829
1523
  return Effect.flatMap(reachFrame(frame, at), (node) => node?.kind !== "directory" || node.metadata.nlink === 0
1830
1524
  ? Effect.succeed([])
1831
- : Effect.as(listable(node, at), walkChildren(node, frame.path, frame.depth, frame)));
1525
+ : Effect.andThen(listable(node, at), walkChildren(node, frame.path, frame.depth, frame)));
1832
1526
  }));
1833
- const exceeded = (frame, field) => makeError({ code: "LimitExceeded", operation: op.operation, field, path: errorPath(locate(frame.path)) });
1834
1527
  const admit = (frame) => {
1835
- if (plan.maxDepth !== undefined && frame.depth > plan.maxDepth)
1836
- return exceeded(frame, "maxDepth");
1837
- if (plan.maxEntries !== undefined && ++entries > plan.maxEntries)
1838
- return exceeded(frame, "maxEntries");
1528
+ entries++;
1839
1529
  bytes += frame.bytes;
1840
- if (plan.maxBytes !== undefined && bytes > plan.maxBytes)
1841
- return exceeded(frame, "maxBytes");
1842
- return undefined;
1530
+ return plan.limit(frame, { entries, bytes }, "entry");
1843
1531
  };
1844
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.
1845
1535
  if (failure !== undefined)
1846
- return yield* failure;
1536
+ return yield* Effect.fail(failure);
1847
1537
  if (pending === undefined)
1848
1538
  pending = (yield* first).reverse();
1849
1539
  const out = [];
@@ -1865,9 +1555,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1865
1555
  out.push(frame);
1866
1556
  }
1867
1557
  if (unlisted) {
1868
- if (plan.maxDepth !== undefined && frame.depth > plan.maxDepth) {
1869
- return yield* stop(exceeded(frame, "maxDepth"));
1870
- }
1558
+ const rejected = plan.limit(frame, { entries, bytes }, "directory");
1559
+ if (rejected !== undefined)
1560
+ return yield* stop(rejected);
1871
1561
  const listed = yield* Effect.result(list(frame));
1872
1562
  if (Result.isFailure(listed))
1873
1563
  return yield* stop(listed.failure);
@@ -1900,10 +1590,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1900
1590
  let targetAt = preparedOp(prepared, op);
1901
1591
  const first = coordinatedRead(op, Effect.gen(function* () {
1902
1592
  const entry = yield* resolveEntry(prepared, op);
1903
- const parent = directoryNow(entry.parent);
1593
+ const parent = yield* directoryNow(entry.parent);
1904
1594
  yield* authorize(parent, identity, EXECUTE, entry.op);
1905
1595
  const childIno = entry.name === undefined ? undefined : parent.entries.get(entry.name);
1906
- const child = childIno === undefined ? undefined : view(childIno);
1596
+ const child = childIno === undefined ? undefined : (yield* view(childIno));
1907
1597
  if (entry.name === undefined || child === undefined)
1908
1598
  return yield* entry.op.fail("NotFound");
1909
1599
  if (child.kind !== "directory")
@@ -1911,16 +1601,16 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1911
1601
  yield* listable(child, entry.op);
1912
1602
  anchor = anchorFrame(parent, entry.name, child);
1913
1603
  targetAt = entry.op;
1914
- return walkChildren(child, new Uint8Array(0), 0, anchor);
1604
+ return yield* walkChildren(child, new Uint8Array(0), 0, anchor);
1915
1605
  })).pipe(Effect.catchIf((error) => force && error.code === "NotFound", () => Effect.succeed([])));
1916
1606
  const seams = yield* VolumeTestSeams;
1917
1607
  const removeFrame = (frame) => {
1918
1608
  const at = op.at(locate(frame.path));
1919
- return Effect.andThen(seams.beforeTreeRemoval, coordinated(op, Effect.suspend(() => {
1920
- if (anchor !== undefined && !holds(view(anchor.parent), anchor)) {
1921
- return force ? Effect.void : Effect.fail(targetAt.fail("NotFound"));
1609
+ return Effect.andThen(seams.beforeTreeRemoval, coordinated(op, Effect.gen(function* () {
1610
+ if (anchor !== undefined && !holds(yield* view(anchor.parent), anchor)) {
1611
+ return yield* (force ? Effect.void : Effect.fail(targetAt.fail("NotFound")));
1922
1612
  }
1923
- return Effect.flatMap(frame.up === undefined ? Effect.succeed(view(frame.parent)) : reachFrame(frame.up, at), (parent) => !holds(parent, frame)
1613
+ return yield* Effect.flatMap(frame.up === undefined ? view(frame.parent) : reachFrame(frame.up, at), (parent) => !holds(parent, frame)
1924
1614
  ? Effect.fail(at.fail("NotFound"))
1925
1615
  : removeEntry({
1926
1616
  parent: parent.ino,
@@ -1931,7 +1621,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1931
1621
  }, op));
1932
1622
  })));
1933
1623
  };
1934
- const plan = { order: "post", maxDepth: undefined, maxEntries: undefined, maxBytes: undefined };
1624
+ const plan = { order: "post", limit: () => undefined };
1935
1625
  return yield* Stream.runForEach(walkFrames(op, first, plan, locate, listable), removeFrame);
1936
1626
  });
1937
1627
  const rootOp = OpContext.make("root");
@@ -1940,9 +1630,40 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1940
1630
  const prepared = yield* Effect.fromResult(prepareEntry(input, op));
1941
1631
  return yield* coordinated(op, Effect.flatMap(resolveEntry(prepared, op), (entry) => body(entry, op)));
1942
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
+ }
1943
1664
  const caller = Object.freeze({
1944
1665
  [CallerId]: true,
1945
- root: coordinatedRead(rootOp, Effect.suspend(() => reference.ino === undefined
1666
+ root: coordinatedRead(rootOp, Effect.suspend(() => TokenRegistry.inode(reference) === undefined
1946
1667
  ? Effect.fail(rootOp.fail("ClosedCaller"))
1947
1668
  : Effect.succeed(referenceFor(ROOT_INO)))).pipe(Effect.withSpan("Caller.root")),
1948
1669
  lookup: Effect.fn("Caller.lookup")(function* (input) {
@@ -1950,7 +1671,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1950
1671
  const prepared = yield* Effect.fromResult(prepareEntry(input, op));
1951
1672
  return yield* coordinatedRead(op, Effect.gen(function* () {
1952
1673
  const entry = yield* resolveEntry(prepared, op);
1953
- const directory = directoryNow(entry.parent);
1674
+ const directory = yield* directoryNow(entry.parent);
1954
1675
  if (isDotComponent(entry.name)) {
1955
1676
  return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : "NotFound");
1956
1677
  }
@@ -1997,13 +1718,22 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1997
1718
  const first = coordinatedRead(op, Effect.gen(function* () {
1998
1719
  const directory = yield* resolveDirectory(target, op);
1999
1720
  yield* authorize(directory.node, identity, READ, directory.op);
2000
- return walkChildren(directory.node, new Uint8Array(0), 0, yield* pathAnchor(target, directory.node, op));
1721
+ return yield* walkChildren(directory.node, new Uint8Array(0), 0, yield* pathAnchor(target, directory.node, op));
2001
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);
2002
1725
  const plan = {
2003
1726
  order: chosen.order ?? "pre",
2004
- maxDepth: chosen.maxDepth,
2005
- maxEntries: chosen.maxEntries,
2006
- 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
+ }
2007
1737
  };
2008
1738
  return Stream.map(walkFrames(op, first, plan, locate, readable), (frame) => Object.freeze({
2009
1739
  path: ownedPath(frame.path),
@@ -2031,17 +1761,21 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2031
1761
  return yield* coordinatedRead(op, Effect.gen(function* () {
2032
1762
  if (Target.$is("Path")(target)) {
2033
1763
  const pathOp = op.at(target.path);
2034
- const result = yield* lookup(yield* Effect.fromResult(prepare(target.path, op)), target.relativeTo, op);
2035
- const directory = result.node?.kind === "directory" ? result.node : result.parent;
2036
- const prefix = directory === undefined ? SLASH_HEX : pathOf(view, directory.ino);
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;
1770
+ const prefix = directory === undefined ? SLASH_HEX : yield* pathOfCurrent(directory.ino);
2037
1771
  if (prefix === undefined)
2038
1772
  return yield* pathOp.fail("NotFound");
2039
- if (result.node?.kind === "directory" || result.name === undefined)
1773
+ if (result.node.kind === "directory" || result.kind === "node")
2040
1774
  return ownedPath(nameBytes(prefix));
2041
1775
  return ownedPath(nameBytes(prefix + (prefix === SLASH_HEX ? "" : SLASH_HEX) + result.name));
2042
1776
  }
2043
1777
  const resolved = yield* resolveTarget(target, op);
2044
- const path = pathOf(view, resolved.node.ino);
1778
+ const path = yield* pathOfCurrent(resolved.node.ino);
2045
1779
  if (path === undefined)
2046
1780
  return yield* resolved.op.fail("NotFound");
2047
1781
  return ownedPath(nameBytes(path));
@@ -2091,66 +1825,19 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2091
1825
  const captured = new Uint8Array(bytes);
2092
1826
  const chosen = yield* decodeWriteFileOptions(options).pipe(Effect.mapError((cause) => fail(optionsOp, cause)));
2093
1827
  return yield* coordinated(op, Effect.gen(function* () {
2094
- let entryOp = op;
2095
- let parent;
2096
- let name;
2097
- let found;
2098
- let trailingSlash = false;
2099
- if (prepared.kind === "path") {
2100
- const path = prepared.path;
2101
- entryOp = op.at(path.input);
2102
- if (chosen.create === "exclusive") {
2103
- const exists = yield* Effect.result(lookup(path, prepared.base, op, { followFinalSymlink: false }));
2104
- if (Result.isSuccess(exists))
2105
- return yield* entryOp.fail("AlreadyExists");
2106
- if (exists.failure.code !== "NotFound")
2107
- return yield* exists.failure;
2108
- }
2109
- const resolved = yield* lookup(path, prepared.base, op, {
2110
- followFinalSymlink: chosen.replaceFinalSymlink !== true && chosen.followFinalSymlink !== false,
2111
- allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
2112
- });
2113
- if (resolved.parent === undefined || resolved.name === undefined || resolved.node?.kind === "directory") {
2114
- return yield* entryOp.fail("IsDirectory");
2115
- }
2116
- parent = resolved.parent;
2117
- name = resolved.name;
2118
- found = resolved.node;
2119
- trailingSlash = path.trailingSlash;
2120
- }
2121
- else {
2122
- const directory = yield* entryDirectory(prepared.directory, op);
2123
- if (isDotComponent(prepared.name))
2124
- return yield* op.fail("InvalidArgument");
2125
- yield* authorize(directory.node, identity, EXECUTE, op);
2126
- parent = directory.node;
2127
- name = prepared.name;
2128
- const childIno = parent.entries.get(name);
2129
- let child = childIno === undefined ? undefined : view(childIno);
2130
- if (child?.kind === "symlink" && chosen.replaceFinalSymlink !== true &&
2131
- chosen.followFinalSymlink !== false) {
2132
- const resolved = yield* lookup(yield* Effect.fromResult(preparePath(ownedPath(nameBytes(name)), op.operation, undefined)), undefined, op, {
2133
- followFinalSymlink: true,
2134
- allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
2135
- }, parent);
2136
- if (resolved.parent === undefined || resolved.name === undefined || resolved.node?.kind === "directory") {
2137
- return yield* op.fail("IsDirectory");
2138
- }
2139
- parent = resolved.parent;
2140
- name = resolved.name;
2141
- child = resolved.node;
2142
- }
2143
- if (child === undefined && (chosen.create === undefined || chosen.create === "never")) {
2144
- return yield* op.fail("NotFound");
2145
- }
2146
- if (child !== undefined && chosen.create === "exclusive")
2147
- return yield* op.fail("AlreadyExists");
2148
- if (child?.kind === "directory")
2149
- return yield* op.fail("IsDirectory");
2150
- found = child;
2151
- }
2152
- 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)) {
2153
1839
  return yield* entryOp.fail("IsDirectory");
1840
+ }
2154
1841
  const replaced = found?.kind === "symlink" ? found : undefined;
2155
1842
  if (replaced !== undefined && !chosen.replaceFinalSymlink)
2156
1843
  return yield* entryOp.fail("SymlinkLoop");
@@ -2161,7 +1848,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2161
1848
  yield* authorize(parent, identity, WRITE | EXECUTE, entryOp);
2162
1849
  if (replaced !== undefined)
2163
1850
  yield* authorizeRemoval(parent, replaced, entryOp);
2164
- yield* replaced === undefined ? reserveEntry(entryOp) : reserveInode(entryOp);
1851
+ yield* (replaced === undefined ? reserveEntry(entryOp) : reserveInode(entryOp));
2165
1852
  }
2166
1853
  else {
2167
1854
  yield* authorize(file, identity, chosen.access === "readWrite" ? READ | WRITE : WRITE, entryOp);
@@ -2186,8 +1873,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2186
1873
  data.set(captured, position);
2187
1874
  }
2188
1875
  const now = yield* timestamp(op);
2189
- const d = current();
2190
- const node = file ?? newFile(parent, new Uint8Array(0), (chosen.mode ?? 0o666) & 0o777 & ~umask, now);
1876
+ const d = yield* Draft;
1877
+ const node = file ??
1878
+ (yield* newFile(parent, new Uint8Array(0), (chosen.mode ?? 0o666) & 0o777 & ~umask, now));
2191
1879
  const written = {
2192
1880
  ...node,
2193
1881
  data,
@@ -2199,32 +1887,22 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2199
1887
  ctimeNs: now
2200
1888
  }
2201
1889
  };
2202
- d.usedBytes += BigInt(size - previous);
1890
+ yield* d.addUsedBytes(BigInt(size - previous));
2203
1891
  if (file === undefined) {
2204
1892
  if (replaced !== undefined)
2205
- detach(replaced, parent.ino, name, now);
2206
- attach(directoryNow(parent.ino), name, written, now);
1893
+ yield* detach(replaced, parent.ino, name, now);
1894
+ yield* attach(yield* directoryNow(parent.ino), name, written, now);
2207
1895
  if (replaced === undefined)
2208
- d.entries += 1;
2209
- publishEntry(replaced === undefined ? "Create" : "Update", parent.ino, name, written.ino);
1896
+ yield* d.addEntries(1);
1897
+ yield* publishEntry(replaced === undefined ? "Create" : "Update", parent.ino, name, written.ino);
2210
1898
  }
2211
1899
  else {
2212
- d.put(written);
2213
- publishNode(written.ino);
1900
+ yield* d.put(written);
1901
+ yield* publishNode(written.ino);
2214
1902
  }
2215
1903
  }));
2216
1904
  }),
2217
- // SAFETY: the overloads pair an entry with entry options and a target with open options; the decoders below
2218
- // reject the other shape, so one implementation serves both.
2219
- open: Effect.fn("Caller.open")(function* (input, options) {
2220
- const op = OpContext.make("open");
2221
- if (isEntry(input))
2222
- return yield* openEntry(input, options, op);
2223
- const target = asTarget(input);
2224
- if (Target.$is("Path")(target))
2225
- return yield* openPath(target, options, op);
2226
- return yield* openNode(target, options, op);
2227
- }),
1905
+ open,
2228
1906
  mkdir: Effect.fn("Caller.mkdir")(function* (input, options = {}) {
2229
1907
  const op = OpContext.make("mkdir");
2230
1908
  const prepared = yield* Effect.fromResult(prepareEntry(input, op));
@@ -2282,23 +1960,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2282
1960
  rmdir: Effect.fn("Caller.rmdir")(function* (input) {
2283
1961
  return yield* entryVerb("rmdir", input, rmdirEntry);
2284
1962
  }),
2285
- // SAFETY: the overloads differ only in whether `force` may leave nothing removed; one body serves both.
2286
- remove: Effect.fn("Caller.remove")(function* (input, options) {
2287
- const op = OpContext.make("remove");
2288
- const prepared = yield* Effect.fromResult(prepareEntry(input, op));
2289
- const chosen = yield* decodeRemoveOptions(options ?? {}).pipe(Effect.mapError((cause) => fail(preparedOp(prepared, op), cause)));
2290
- const once = coordinated(op, Effect.flatMap(resolveEntry(prepared, op), (entry) => removeEntry(entry, op)));
2291
- const target = (effect) => chosen.force === true
2292
- ? Effect.catchIf(effect, (error) => error.code === "NotFound", () => Effect.undefined)
2293
- : effect;
2294
- const removed = yield* Effect.result(target(once));
2295
- if (Result.isSuccess(removed))
2296
- return removed.success;
2297
- if (chosen.recursive !== true || removed.failure.code !== "NotEmpty")
2298
- return yield* removed.failure;
2299
- yield* emptyDirectory(prepared, op, chosen.force === true);
2300
- return yield* target(once);
2301
- }),
1963
+ remove,
2302
1964
  rename: Effect.fn("Caller.rename")(function* (fromInput, toInput) {
2303
1965
  const op = OpContext.make("rename");
2304
1966
  const from = yield* Effect.fromResult(prepareEntry(fromInput, op));
@@ -2371,21 +2033,21 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2371
2033
  const statOp = OpContext.make("stat");
2372
2034
  const handle = Object.freeze({
2373
2035
  [DirectoryHandleId]: true,
2374
- stat: coordinatedRead(statOp, Effect.suspend(() => {
2375
- const node = acquired.ino === undefined ? undefined : view(acquired.ino);
2376
- return node === undefined
2036
+ stat: coordinatedRead(statOp, Effect.gen(function* () {
2037
+ const ino = TokenRegistry.inode(acquired);
2038
+ const node = ino === undefined ? undefined : yield* view(ino);
2039
+ return yield* (node === undefined
2377
2040
  ? Effect.fail(statOp.fail("InvalidHandle"))
2378
- : Effect.succeed(withMetadata(node));
2041
+ : Effect.succeed(withMetadata(node)));
2379
2042
  })).pipe(Effect.withSpan("DirectoryHandle.stat")),
2380
2043
  close: coordinatedCleanup(Effect.suspend(() => {
2381
- if (acquired.ino === undefined) {
2044
+ if (TokenRegistry.inode(acquired) === undefined) {
2382
2045
  return Effect.fail(OpContext.make("close").fail("InvalidHandle"));
2383
2046
  }
2384
- releaseDirectory(acquired);
2385
- return Effect.void;
2047
+ return releaseDirectory(acquired);
2386
2048
  })).pipe(Effect.ensuring(closeReleasedScope(acquired)), Effect.withSpan("DirectoryHandle.close"))
2387
2049
  });
2388
- handles.set(handle, acquired);
2050
+ registry.registerDirectory(handle, acquired);
2389
2051
  return handle;
2390
2052
  })
2391
2053
  });
@@ -2416,32 +2078,27 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2416
2078
  snapshot: coordinatedRead(OpContext.make("snapshot"), Effect.sync(() => Image.make(state))).pipe(Effect.withSpan("Volume.snapshot")),
2417
2079
  referenceKey: Effect.fn("Volume.referenceKey")(function* (reference) {
2418
2080
  const op = OpContext.make("referenceKey");
2419
- return yield* coordinatedRead(op, Effect.suspend(() => {
2420
- const known = Predicate.isObject(reference) ? objectReferences.get(reference) : undefined;
2421
- if (known === undefined)
2422
- return Effect.fail(op.fail("InvalidReference"));
2423
- if (known.volume !== volumeIdentity)
2424
- return Effect.fail(op.fail("ForeignReference"));
2425
- if (!addressable(known.ino))
2426
- return Effect.fail(op.fail("StaleReference"));
2427
- const ino = BigInt(known.ino);
2428
- 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) };
2429
2085
  }));
2430
2086
  }),
2431
2087
  resolveReferenceKey: Effect.fn("Volume.resolveReferenceKey")(function* (key) {
2432
2088
  const op = OpContext.make("resolveReferenceKey");
2433
- return yield* coordinatedRead(op, Effect.suspend(() => {
2089
+ return yield* coordinatedRead(op, Effect.gen(function* () {
2434
2090
  if (!Schema.is(ReferenceKey)(key))
2435
- return Effect.fail(op.fail("InvalidReference"));
2091
+ return yield* op.fail("InvalidReference");
2436
2092
  // Another epoch numbered its objects on its own, so its key is another volume's even under this identity.
2437
2093
  if (!sameBytes(key.identity, identityBytes) || !sameBytes(key.epoch, epochBytes)) {
2438
- return Effect.fail(op.fail("ForeignReference"));
2094
+ return yield* op.fail("ForeignReference");
2439
2095
  }
2440
2096
  // Checked before the inode is looked up, so a guessed key learns nothing about which numbers are in use.
2441
2097
  if (!sameTag(key.tag, keyTag(key.ino)))
2442
- return Effect.fail(op.fail("InvalidReference"));
2098
+ return yield* op.fail("InvalidReference");
2443
2099
  const ino = Ino(Number(key.ino));
2444
- 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);
2445
2102
  }));
2446
2103
  }),
2447
2104
  caller: Effect.fn("Volume.caller")(function* (options) {
@@ -2455,47 +2112,37 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2455
2112
  changes: coordinatedRead(OpContext.make("changes"), Effect.suspend(() => observeChanges(state))),
2456
2113
  capture: coordinatedRead(OpContext.make("capture"), captureState())
2457
2114
  });
2458
- return Object.freeze({ volume, shutdown, initialImage, observe });
2115
+ return Object.freeze({ volume, shutdown, observe });
2459
2116
  });
2460
2117
  /** @internal */
2461
2118
  export const make = Effect.fn("VirtualFileSystem.make")(function* (options) {
2462
- 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;
2463
2120
  });
2464
2121
  /** @internal */
2465
2122
  export const prepareEmptyLiveImage = Effect.fnUntraced(function* (options) {
2466
- const { initialImage } = yield* Effect.mapError(makeVolume(VolumeSource.Empty(), options, undefined, LiveImage.encode), (error) => retargetFailure("prepareEmptyImage", error));
2467
- if (initialImage === undefined)
2468
- return yield* imageFailure("openImage", "InvalidStructure", { field: "liveImage" });
2469
- 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)));
2470
2132
  });
2471
2133
  /** @internal */
2472
- 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) {
2473
2135
  const restored = yield* LiveImage.decode(image, maxImageBytes);
2474
- let prepared;
2475
- const { limits: stored } = restored;
2476
- const commitOp = OpContext.make("commit");
2477
- const limits = {
2478
- maxEntries: stored.maxEntries,
2479
- maxBytes: stored.maxBytes === undefined ? undefined : ByteSize.bytes(stored.maxBytes),
2480
- maxFileBytes: ByteSize.bytes(stored.maxFileBytes ?? MAX_FILE_BYTES),
2481
- maxPathBytes: stored.maxPathBytes === undefined ? undefined : ByteSize.bytes(stored.maxPathBytes),
2482
- maxPendingOperations: 64,
2483
- 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)))
2484
2144
  };
2485
- const { volume, shutdown } = yield* makeVolume(VolumeSource.Live({ restored }), undefined, {
2486
- 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)
2487
- ? commitOp.fail("StorageRejected")
2488
- : Effect.sync(() => {
2489
- prepared = bytes;
2490
- }))),
2491
- commit: () => Effect.suspend(() => {
2492
- const bytes = prepared;
2493
- prepared = undefined;
2494
- return bytes === undefined ? Effect.succeed("unknown") : commit(bytes);
2495
- })
2496
- }, undefined, durability);
2497
- if (shutdown === undefined)
2498
- return yield* imageFailure("openImage", "InvalidStructure", { field: "liveImage" });
2145
+ const { volume, shutdown } = yield* makeVolume(VolumeSource.Live({ restored }), provider, options, durability);
2499
2146
  return Object.freeze({ volume, shutdown });
2500
2147
  });
2501
2148
  /** @internal */
@@ -2508,12 +2155,12 @@ const changeOptions = (options) => {
2508
2155
  };
2509
2156
  /** @internal */
2510
2157
  export const fromSnapshot = Effect.fn("VirtualFileSystem.fromSnapshot")(function* (snapshot, options) {
2511
- 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;
2512
2159
  }, Effect.mapError((error) => retargetFailure("fromSnapshot", error)));
2513
2160
  /** @internal */
2514
2161
  export const makeOverlay = Effect.fn("VirtualFileSystem.makeOverlay")(function* (base, options) {
2515
2162
  const value = yield* Image.valueOf(base);
2516
- 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));
2517
2164
  const baseObservation = yield* observeChanges(value);
2518
2165
  const overlay = Object.freeze({
2519
2166
  ...made.volume,