@effect-vfs/core 0.6.0 → 0.7.1

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 (39) hide show
  1. package/README.md +14 -9
  2. package/dist/BytePath.d.ts +2 -1
  3. package/dist/BytePath.d.ts.map +1 -1
  4. package/dist/LiveVolume.d.ts +6 -3
  5. package/dist/LiveVolume.d.ts.map +1 -1
  6. package/dist/LiveVolume.js +2 -0
  7. package/dist/Snapshot.d.ts +2 -1
  8. package/dist/Snapshot.d.ts.map +1 -1
  9. package/dist/Snapshot.js +2 -1
  10. package/dist/Testing.d.ts +9 -5
  11. package/dist/Testing.d.ts.map +1 -1
  12. package/dist/Testing.js +7 -18
  13. package/dist/VfsError.d.ts +2 -1
  14. package/dist/VfsError.d.ts.map +1 -1
  15. package/dist/VfsError.js +2 -1
  16. package/dist/VirtualFileSystem.d.ts +89 -47
  17. package/dist/VirtualFileSystem.d.ts.map +1 -1
  18. package/dist/VirtualFileSystem.js +62 -29
  19. package/dist/internal/canonicalBase64.js +3 -3
  20. package/dist/internal/fixture.d.ts +1 -1
  21. package/dist/internal/fixture.d.ts.map +1 -1
  22. package/dist/internal/overlayDiff.d.ts.map +1 -1
  23. package/dist/internal/overlayDiff.js +6 -14
  24. package/dist/internal/path.d.ts.map +1 -1
  25. package/dist/internal/path.js +3 -3
  26. package/dist/internal/snapshotDelta.d.ts.map +1 -1
  27. package/dist/internal/snapshotDelta.js +9 -15
  28. package/dist/internal/tree.d.ts.map +1 -1
  29. package/dist/internal/tree.js +14 -14
  30. package/dist/internal/virtualFileSystem.d.ts +13 -7
  31. package/dist/internal/virtualFileSystem.d.ts.map +1 -1
  32. package/dist/internal/virtualFileSystem.js +442 -399
  33. package/dist/internal/volumeState.d.ts +3 -2
  34. package/dist/internal/volumeState.d.ts.map +1 -1
  35. package/dist/internal/volumeState.js +28 -30
  36. package/package.json +7 -7
  37. package/dist/internal/stagedState.d.ts +0 -18
  38. package/dist/internal/stagedState.d.ts.map +0 -1
  39. package/dist/internal/stagedState.js +0 -18
@@ -1,13 +1,16 @@
1
+ import * as Arr from "effect/Array";
1
2
  import * as ByteSize from "effect/ByteSize";
2
3
  import * as Clock from "effect/Clock";
4
+ import * as Context from "effect/Context";
3
5
  import * as Crypto from "effect/Crypto";
4
6
  import * as Data from "effect/Data";
5
7
  import * as Effect from "effect/Effect";
6
- import * as Encoding from "effect/Encoding";
8
+ import * as Hex from "effect/encoding/Hex";
7
9
  import * as Exit from "effect/Exit";
10
+ import * as Match from "effect/Match";
11
+ import * as MutableRef from "effect/MutableRef";
8
12
  import * as Option from "effect/Option";
9
13
  import * as Predicate from "effect/Predicate";
10
- import * as Random from "effect/Random";
11
14
  import * as Result from "effect/Result";
12
15
  import * as Schema from "effect/Schema";
13
16
  import * as Scope from "effect/Scope";
@@ -31,10 +34,9 @@ import * as LiveImage from "./liveImage.js";
31
34
  import * as MetadataDomain from "./metadata.js";
32
35
  import { compareOverlay } from "./overlayDiff.js";
33
36
  import { DOT_DOT_HEX, DOT_HEX, inputBytes, isAttachedBytes, isDotComponent, isWellFormed, joinPath, MAX_NAME_BYTES, MAX_SYMLINK_TRAVERSALS, nameBytes, ownedPath, preparePath, ROOT_PATH, SLASH_BYTE, SLASH_HEX } from "./path.js";
34
- import { offerCommit } from "./stagedState.js";
35
37
  import { VolumeTestSeams } from "./testSeams.js";
36
38
  import { makeTurnstile } from "./turnstile.js";
37
- import { directoryMetadata, emptyRoot, getNode, Ino, MAX_FILE_BYTES, reachableValue, ROOT_INO, storedMetadata, WALK_YIELD_INTERVAL } from "./volumeState.js";
39
+ import { byEntryName, directoryMetadata, emptyRoot, getNode, Ino, MAX_FILE_BYTES, reachableValue, ROOT_INO, storedMetadata, WALK_YIELD_INTERVAL } from "./volumeState.js";
38
40
  import * as WatchHub from "./watchHub.js";
39
41
  const EXECUTE = 0o1;
40
42
  const WRITE = 0o2;
@@ -43,6 +45,20 @@ const ANY_EXECUTE = 0o111;
43
45
  const SET_ID_BITS = 0o6000;
44
46
  const STICKY_BIT = 0o1000;
45
47
  const RELATIME_INTERVAL_NS = 86400000000000n; // relatime, as Linux mounts by default: a read refreshes an access time at least this old, 24 hours.
48
+ // An unknown commit outcome stops the volume; definite rejection stops it only during cleanup.
49
+ const offerCommit = (provider, operation, candidate, cleanup) => Effect.map(Effect.exit(Effect.suspend(() => provider.commit(candidate))), (committed) => {
50
+ // TODO(#186): Preserve the cause of a provider defect or interruption.
51
+ if (Exit.isFailure(committed)) {
52
+ return { available: false, failure: fsFailure("OutcomeUnknown", operation) };
53
+ }
54
+ if (committed.value === "rejected") {
55
+ return { available: !cleanup, failure: fsFailure("StorageRejected", operation) };
56
+ }
57
+ if (committed.value === "unknown") {
58
+ return { available: false, failure: fsFailure("OutcomeUnknown", operation) };
59
+ }
60
+ return { available: true, failure: undefined };
61
+ });
46
62
  const WALK_CHUNK_ENTRIES = 128;
47
63
  const MAX_FILE_OFFSET = 0x7fffffffffffffffn;
48
64
  const isMode = Schema.is(Mode);
@@ -55,7 +71,7 @@ const decodeOwnerUpdate = Schema.decodeEffect(OwnerUpdate, { onExcessProperty: "
55
71
  const decodeTimes = Schema.decodeEffect(Times, { onExcessProperty: "error" });
56
72
  const decodeExpected = Schema.decodeEffect(SetattrOptions.fields.expected.schema, { onExcessProperty: "error" });
57
73
  const SETATTR_FIELDS = Object.keys(SetattrOptions.fields);
58
- const timeAt = (update, value, now) => update === undefined || update.kind === "omit" ? value : update.kind === "now" ? now : update.nanoseconds;
74
+ const timeAt = (update, value, now) => update === undefined ? value : Match.value(update).pipe(Match.discriminator("kind")("omit", () => value), Match.discriminator("kind")("now", () => now), Match.discriminator("kind")("value", ({ nanoseconds }) => nanoseconds), Match.exhaustive);
59
75
  const decodeWriteFileOptions = Schema.decodeEffect(WriteFileOptions, { onExcessProperty: "error" });
60
76
  const decodeOpenOptions = Schema.decodeEffect(OpenOptions, { onExcessProperty: "error" });
61
77
  const decodeMkdirOptions = Schema.decodeEffect(MkdirOptions, { onExcessProperty: "error" });
@@ -70,100 +86,115 @@ const encoder = new TextEncoder();
70
86
  const accessDue = (metadata, now) => now !== metadata.atimeNs && (metadata.atimeNs <= metadata.mtimeNs || metadata.atimeNs <= metadata.ctimeNs ||
71
87
  now - metadata.atimeNs >= RELATIME_INTERVAL_NS);
72
88
  const refreshDue = (read) => read.access !== undefined && accessDue(read.access.node.metadata, read.access.now);
73
- // Builds the next volume value for one transition. Reads see pending writes; a discarded draft leaves the
74
- // base untouched, so an interrupted or failed transition needs no rollback.
75
- class Draft {
76
- base;
77
- owner = Symbol();
78
- pending = new Map();
79
- opens;
80
- nextInode;
81
- changed = false;
82
- finished;
83
- revision;
84
- entries;
85
- usedBytes;
86
- events = [];
87
- after = [];
88
- removed = [];
89
- constructor(base) {
90
- this.base = base;
91
- this.nextInode = base.nextInode;
92
- this.revision = base.revision + 1n;
93
- this.entries = base.entries;
94
- this.usedBytes = base.usedBytes;
95
- }
96
- get(ino) {
97
- const pending = this.pending.get(ino);
98
- return pending !== undefined || this.pending.has(ino) ? pending : InodeTable.get(this.base.inodes, ino);
99
- }
100
- put(node) {
101
- this.changed = true;
102
- // SAFETY: callers construct the value they pass, so stamping it in place replaces a spread on every write.
103
- const stamped = node;
104
- stamped.revision = this.revision;
105
- this.pending.set(node.ino, node);
106
- }
107
- putQuiet(node) {
108
- this.pending.set(node.ino, node);
109
- }
110
- get unchanged() {
111
- return this.pending.size === 0 && this.opens === undefined && this.nextInode === this.base.nextInode &&
112
- this.entries === this.base.entries && this.usedBytes === this.base.usedBytes;
113
- }
114
- remove(ino) {
115
- this.removed.push(ino);
116
- this.pending.set(ino, undefined);
117
- }
118
- allocate() {
119
- const ino = this.nextInode;
120
- this.nextInode = Ino(ino + 1);
121
- return ino;
122
- }
123
- // Whether another inode can be allocated. The allocator itself must stay exactly representable, since a live
124
- // image refuses one past the largest safe integer, so the last inode it hands out is one below that.
125
- get canAllocate() {
126
- return this.nextInode < Number.MAX_SAFE_INTEGER;
127
- }
128
- openCount(ino) {
129
- return this.opens?.get(ino) ?? this.base.open.get(ino) ?? 0;
130
- }
131
- retain(ino) {
132
- this.opens = (this.opens ?? new Map()).set(ino, this.openCount(ino) + 1);
133
- }
134
- release(ino) {
135
- const count = this.openCount(ino) - 1;
136
- this.opens = (this.opens ?? new Map()).set(ino, count < 0 ? 0 : count);
137
- return count;
138
- }
139
- finish() {
140
- if (this.finished !== undefined)
141
- return this.finished;
142
- const base = this.base;
143
- let inodes = base.inodes;
144
- for (const [ino, node] of this.pending)
145
- inodes = InodeTable.set(inodes, ino, node, this.owner);
146
- let open = base.open;
147
- if (this.opens !== undefined) {
148
- const next = new Map(base.open);
149
- for (const [ino, count] of this.opens) {
150
- if (count > 0)
151
- next.set(ino, count);
152
- else
153
- next.delete(ino);
154
- }
155
- open = next;
156
- }
157
- this.finished = {
158
- inodes,
159
- open,
160
- nextInode: this.nextInode,
161
- revision: this.changed ? this.revision : base.revision,
162
- entries: this.entries,
163
- usedBytes: this.usedBytes
89
+ class Draft extends Context.Service()("@effect-vfs/core/internal/virtualFileSystem/Draft", {
90
+ make: Effect.fnUntraced(function* (base) {
91
+ const revision = base.revision + 1n;
92
+ const cell = MutableRef.make({
93
+ owner: Symbol(),
94
+ pending: new Map(),
95
+ opens: undefined,
96
+ nextInode: base.nextInode,
97
+ changed: false,
98
+ finished: undefined,
99
+ entries: base.entries,
100
+ usedBytes: base.usedBytes,
101
+ events: [],
102
+ after: [],
103
+ removed: []
104
+ });
105
+ const get = (ino) => Effect.sync(() => {
106
+ const draft = MutableRef.get(cell);
107
+ return draft.pending.has(ino) ? draft.pending.get(ino) : InodeTable.get(base.inodes, ino);
108
+ });
109
+ const openCount = (ino) => Effect.sync(() => MutableRef.get(cell).opens?.get(ino) ?? base.open.get(ino) ?? 0);
110
+ const put = (node, changed) => Effect.sync(() => {
111
+ MutableRef.update(cell, (draft) => {
112
+ // This map belongs only to this transition. Copying it on every inode write makes recursive changes quadratic.
113
+ draft.pending.set(node.ino, changed ? { ...node, revision } : node);
114
+ return { ...draft, changed: draft.changed || changed };
115
+ });
116
+ });
117
+ return {
118
+ revision,
119
+ get,
120
+ put: (node) => put(node, true),
121
+ putQuiet: (node) => put(node, false),
122
+ unchanged: Effect.sync(() => {
123
+ const draft = MutableRef.get(cell);
124
+ return draft.pending.size === 0 && draft.opens === undefined && draft.nextInode === base.nextInode &&
125
+ draft.entries === base.entries && draft.usedBytes === base.usedBytes;
126
+ }),
127
+ remove: (ino) => Effect.sync(() => {
128
+ const draft = MutableRef.get(cell);
129
+ draft.pending.set(ino, undefined);
130
+ draft.removed.push(ino);
131
+ }),
132
+ allocate: Effect.sync(() => {
133
+ const next = MutableRef.get(cell).nextInode;
134
+ MutableRef.update(cell, (draft) => ({ ...draft, nextInode: Ino(next + 1) }));
135
+ return next;
136
+ }),
137
+ canAllocate: Effect.sync(() => MutableRef.get(cell).nextInode < Number.MAX_SAFE_INTEGER),
138
+ openCount,
139
+ retain: (ino) => Effect.sync(() => {
140
+ const count = MutableRef.get(cell).opens?.get(ino) ?? base.open.get(ino) ?? 0;
141
+ MutableRef.update(cell, (draft) => ({ ...draft, opens: new Map(draft.opens).set(ino, count + 1) }));
142
+ }),
143
+ release: (ino) => Effect.sync(() => {
144
+ const count = (MutableRef.get(cell).opens?.get(ino) ?? base.open.get(ino) ?? 0) - 1;
145
+ MutableRef.update(cell, (draft) => ({ ...draft, opens: new Map(draft.opens).set(ino, Math.max(0, count)) }));
146
+ return count;
147
+ }),
148
+ entries: Effect.sync(() => MutableRef.get(cell).entries),
149
+ addEntries: (amount) => Effect.sync(() => {
150
+ MutableRef.update(cell, (draft) => ({ ...draft, entries: draft.entries + amount }));
151
+ }),
152
+ usedBytes: Effect.sync(() => MutableRef.get(cell).usedBytes),
153
+ addUsedBytes: (amount) => Effect.sync(() => {
154
+ MutableRef.update(cell, (draft) => ({ ...draft, usedBytes: draft.usedBytes + amount }));
155
+ }),
156
+ publish: (event) => Effect.sync(() => {
157
+ MutableRef.get(cell).events.push(event);
158
+ }),
159
+ afterInstall: (action) => Effect.sync(() => {
160
+ MutableRef.get(cell).after.push(action);
161
+ }),
162
+ finish: Effect.sync(() => {
163
+ const draft = MutableRef.get(cell);
164
+ if (draft.finished !== undefined)
165
+ return draft.finished;
166
+ let inodes = base.inodes;
167
+ for (const [ino, node] of draft.pending)
168
+ inodes = InodeTable.set(inodes, ino, node, draft.owner);
169
+ let open = base.open;
170
+ if (draft.opens !== undefined) {
171
+ const next = new Map(base.open);
172
+ for (const [ino, count] of draft.opens) {
173
+ if (count > 0)
174
+ next.set(ino, count);
175
+ else
176
+ next.delete(ino);
177
+ }
178
+ open = next;
179
+ }
180
+ const finished = {
181
+ inodes,
182
+ open,
183
+ nextInode: draft.nextInode,
184
+ revision: draft.changed ? revision : base.revision,
185
+ entries: draft.entries,
186
+ usedBytes: draft.usedBytes
187
+ };
188
+ MutableRef.update(cell, (current) => ({ ...current, finished }));
189
+ return finished;
190
+ }),
191
+ actions: Effect.sync(() => {
192
+ const { events, after, removed } = MutableRef.get(cell);
193
+ return { events, after, removed };
194
+ })
164
195
  };
165
- return this.finished;
166
- }
196
+ })
197
+ }) {
167
198
  }
168
199
  const objectReferences = new WeakMap();
169
200
  const isFileHandle = (value) => Predicate.hasProperty(FileHandleId)(value);
@@ -212,28 +243,14 @@ const observeChanges = Effect.fnUntraced(function* (captured) {
212
243
  return observation;
213
244
  });
214
245
  const randomHex128 = Effect.gen(function* () {
215
- const crypto = yield* Effect.serviceOption(Crypto.Crypto);
216
- if (Option.isSome(crypto))
217
- return Encoding.encodeHex(yield* Effect.orDie(crypto.value.randomBytes(16)));
218
- let hex = "";
219
- for (let draw = 0; draw < 4; draw++) {
220
- hex += (yield* Random.nextIntBetween(0, 0x100000000, { halfOpen: true })).toString(16).padStart(8, "0");
221
- }
222
- return hex;
223
- });
224
- // The 128-bit secret behind every key's tag, always from the platform's cryptographically secure generator. Unlike an
225
- // identity or an epoch it must not follow a seedable Random: every key hands out the identity and epoch, so a
226
- // reproducible secret would let one key forge the tag of any other inode. Web Crypto is global in every supported
227
- // runtime, so reading it adds no service requirement; a runtime without it is a defect, not a recoverable failure.
228
- const randomKeySecret = Effect.sync(() => {
229
- const webCrypto = globalThis.crypto;
230
- if (webCrypto === undefined) {
231
- throw new Error("globalThis.crypto is unavailable, so a volume cannot draw its reference-key secret");
232
- }
233
- return Encoding.encodeHex(webCrypto.getRandomValues(new Uint8Array(16)));
234
- });
246
+ const crypto = yield* Crypto.Crypto;
247
+ return Hex.encode(yield* crypto.randomBytes(16));
248
+ }).pipe(Effect.orDie);
235
249
  /** @internal */
236
250
  export const makeVolume = Effect.fnUntraced(function* (source, options, commitProvider, captureInitial, durability = "memory-only") {
251
+ const services = yield* Effect.context();
252
+ // These accessors run inside synchronous transition callbacks. The runner is reached only when an invariant fails.
253
+ const dieInvariant = (message) => Effect.runSyncWith(services)(Effect.die(message));
237
254
  const live = Predicate.isTagged("Live")(source) ? source.restored : undefined;
238
255
  let restoredOptions = options;
239
256
  if (live !== undefined) {
@@ -255,10 +272,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
255
272
  : VolumeIdentity.make(settings.identity);
256
273
  const incarnation = VolumeIncarnation.make(yield* randomHex128);
257
274
  const epoch = live === undefined ? VolumeEpoch.make(yield* randomHex128) : live.epoch;
258
- const keySecret = live === undefined ? KeySecret.make(yield* randomKeySecret) : 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));
275
+ const keySecret = live === undefined ? KeySecret.make(yield* randomHex128) : live.keySecret;
276
+ const identityBytes = Result.getOrThrow(Hex.decode(identity));
277
+ const epochBytes = Result.getOrThrow(Hex.decode(epoch));
278
+ const keySecretBytes = Result.getOrThrow(Hex.decode(keySecret));
262
279
  const clock = yield* Clock.clockWith(Effect.succeed);
263
280
  const initialTime = clock.currentTimeNanosUnsafe();
264
281
  if (!isTimestamp(initialTime)) {
@@ -339,91 +356,76 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
339
356
  const initialImage = captureInitial === undefined
340
357
  ? undefined
341
358
  : yield* captureInitial(state, { identity, epoch, keySecret }, limits);
342
- let draft;
343
- const view = (ino) => draft === undefined ? getNode(state, ino) : draft.get(ino);
359
+ const view = (ino) => Effect.flatMap(Effect.serviceOption(Draft), (draft) => Option.isSome(draft) ? draft.value.get(ino) : Effect.sync(() => getNode(state, ino)));
344
360
  let available = true;
345
361
  const checkAvailable = (operation) => Effect.suspend(() => available ? Effect.void : Effect.fail(fsFailure("VolumeUnavailable", operation)));
346
362
  const watchCoordinate = (effect) => changing(effect);
347
363
  const watchHub = yield* WatchHub.make(watchCoordinate, settings.maxWatchEvents ?? 256);
348
- const install = (finished) => {
364
+ const install = Effect.fnUntraced(function* (finished) {
349
365
  const before = state;
350
- state = finished.finish();
351
- for (const ino of finished.removed)
366
+ state = yield* finished.finish;
367
+ const actions = yield* finished.actions;
368
+ for (const ino of actions.removed)
352
369
  tokens.delete(ino);
353
- for (const apply of finished.after)
370
+ for (const apply of actions.after)
354
371
  apply();
355
372
  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]));
373
+ watchHub.publishUnsafe(() => actions.events.flatMap((events) => Array.from(events(after))), { before, after });
374
+ });
375
+ const transition = Effect.fnUntraced(function* (change) {
376
+ const current = yield* Draft.make(state);
377
+ const value = yield* Effect.provideService(change, Draft, current);
378
+ return [value, current];
379
+ });
380
+ const applyDirect = Effect.fnUntraced(function* (change) {
381
+ const current = yield* Draft.make(state);
382
+ yield* Effect.provideService(change(current), Draft, current);
383
+ yield* install(current);
364
384
  });
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
385
  // Offers the finished draft's value to the store before installing it. A rejected candidate is discarded and
377
386
  // the volume stays available; an uncertain answer stops the volume. A change that is a cleanup runs its
378
387
  // release even when the store refuses, since the handle must not stay open.
379
388
  const committed = Effect.fnUntraced(function* (op, provider, finished, onStorageFailure) {
380
- if (finished.unchanged)
381
- return install(finished);
382
- const next = finished.finish();
389
+ if (yield* finished.unchanged)
390
+ return yield* install(finished);
391
+ const next = yield* finished.finish;
383
392
  if (provider.prepare !== undefined)
384
393
  yield* provider.prepare(next);
385
394
  const answer = yield* offerCommit(provider, op.operation, next, onStorageFailure !== undefined);
386
395
  if (!answer.available)
387
396
  available = false;
388
397
  if (answer.failure !== undefined) {
389
- if (!answer.available)
390
- onStorageFailure?.();
398
+ if (!answer.available && onStorageFailure !== undefined)
399
+ yield* onStorageFailure();
391
400
  return yield* answer.failure;
392
401
  }
393
- install(finished);
402
+ yield* install(finished);
394
403
  });
395
404
  const annotateFailure = (error) => Effect.annotateCurrentSpan({ operation: error.operation, code: error.code });
396
405
  // Permit waits stay interruptible. A change and its publication run under every permit; with a store, the
397
406
  // change itself stays interruptible and only the commit and installation are not.
398
407
  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
- }))
408
+ ? Effect.uninterruptible(Effect.flatMap(transition(Effect.andThen(checkAvailable(op.operation), effect)), ([value, finished]) => Effect.as(install(finished), value)))
403
409
  : Effect.uninterruptibleMask((restore) => Effect.flatMap(restore(transition(Effect.andThen(checkAvailable(op.operation), effect))), ([value, finished]) => Effect.as(committed(op, commitProvider, finished, onStorageFailure), value))))).pipe(Effect.tapError((error) => Schema.is(VfsError)(error) ? annotateFailure(error) : Effect.void));
404
410
  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) => {
411
+ const refreshAccess = Effect.fnUntraced(function* (read) {
406
412
  if (refreshDue(read)) {
407
413
  const { node, now } = read.access;
408
- current().putQuiet({ ...node, metadata: { ...node.metadata, atimeNs: now } });
414
+ const draft = yield* Draft;
415
+ yield* draft.putQuiet({ ...node, metadata: { ...node.metadata, atimeNs: now } });
409
416
  }
410
417
  return read.value;
411
- };
418
+ });
412
419
  // A read that may refresh an access time. It observes under one permit, so reads run beside each other, and
413
420
  // only when the access time is due does it run again as a change. The change repeats every check and the rule,
414
421
  // since another read may have refreshed the time meanwhile, and its result is the one returned.
415
422
  const accessing = (op, read) => Effect.flatMap(coordinatedRead(op, read), (observed) => refreshDue(observed)
416
- ? coordinated(op, Effect.map(read, refreshAccess))
423
+ ? coordinated(op, Effect.flatMap(read, refreshAccess))
417
424
  : Effect.succeed(observed.value));
418
425
  const coordinatedCleanup = (effect) => changing(Effect.uninterruptible(effect));
419
426
  const shutdown = commitProvider === undefined ? undefined : changing(Effect.sync(() => {
420
427
  available = false;
421
428
  }));
422
- const current = () => {
423
- if (draft === undefined)
424
- throw new Error("Volume mutation outside a transition");
425
- return draft;
426
- };
427
429
  const pathOf = (get, ino) => {
428
430
  const names = [];
429
431
  let node = get(ino);
@@ -438,19 +440,33 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
438
440
  }
439
441
  return SLASH_HEX + names.reverse().join(SLASH_HEX);
440
442
  };
443
+ const pathOfCurrent = Effect.fnUntraced(function* (ino) {
444
+ const names = [];
445
+ const first = yield* view(ino);
446
+ if (first?.kind !== "directory" || first.metadata.nlink === 0)
447
+ return undefined;
448
+ let node = first;
449
+ while (node.ino !== ROOT_INO) {
450
+ names.push(node.name);
451
+ const parent = yield* view(node.parent);
452
+ if (parent?.kind !== "directory" || parent.metadata.nlink === 0)
453
+ return undefined;
454
+ node = parent;
455
+ }
456
+ return SLASH_HEX + names.reverse().join(SLASH_HEX);
457
+ });
441
458
  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);
459
+ const publishEntry = (_tag, parent, name, ino) => Effect.flatMap(Draft, (draft) => draft.publish((installed) => {
460
+ const prefix = pathOf((at) => getNode(installed, at), parent);
461
+ return prefix === undefined ? [] : [{ change: { _tag, path: entryPath(prefix, name) }, parent, ino }];
462
+ }));
463
+ const publishNode = Effect.fnUntraced(function* (ino) {
464
+ const draft = yield* Draft;
465
+ const target = yield* draft.get(ino);
450
466
  if (target === undefined)
451
467
  return;
452
468
  if (target.kind === "directory") {
453
- current().events.push((installed) => {
469
+ yield* draft.publish((installed) => {
454
470
  const directory = getNode(installed, ino);
455
471
  const path = pathOf((at) => getNode(installed, at), ino);
456
472
  return path === undefined || directory?.kind !== "directory"
@@ -460,7 +476,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
460
476
  return;
461
477
  }
462
478
  const links = target.links;
463
- current().events.push((installed) => {
479
+ yield* draft.publish((installed) => {
464
480
  const events = [];
465
481
  for (const link of links) {
466
482
  const prefix = pathOf((at) => getNode(installed, at), link.parent);
@@ -474,7 +490,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
474
490
  }
475
491
  return events;
476
492
  });
477
- };
493
+ });
478
494
  const pathsOf = (installed, ino) => {
479
495
  const node = getNode(installed, ino);
480
496
  if (node === undefined)
@@ -573,7 +589,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
573
589
  const holdDirectory = (ino) => {
574
590
  directoryHolds.set(ino, (directoryHolds.get(ino) ?? 0) + 1);
575
591
  };
576
- const unholdDirectory = (ino) => {
592
+ const unholdDirectory = Effect.fnUntraced(function* (ino) {
577
593
  const count = (directoryHolds.get(ino) ?? 1) - 1;
578
594
  if (count > 0) {
579
595
  directoryHolds.set(ino, count);
@@ -581,19 +597,20 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
581
597
  }
582
598
  directoryHolds.delete(ino);
583
599
  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) => {
600
+ if (node?.kind === "directory" && node.metadata.nlink === 0) {
601
+ yield* applyDirect((d) => d.remove(ino));
602
+ }
603
+ });
604
+ const releaseDirectory = Effect.fnUntraced(function* (reference) {
588
605
  const ino = reference.ino;
589
606
  reference.ino = undefined;
590
607
  reference.closed = true;
591
608
  if (ino !== undefined)
592
- unholdDirectory(ino);
593
- };
609
+ yield* unholdDirectory(ino);
610
+ });
594
611
  const finalizeDirectory = (reference) => Effect.uninterruptible(Effect.suspend(() => reference.closed
595
612
  ? Effect.void
596
- : coordinatedCleanup(Effect.sync(() => releaseDirectory(reference)))));
613
+ : coordinatedCleanup(releaseDirectory(reference))));
597
614
  const closeReleasedScope = (reference) => Effect.suspend(() => !reference.closed || reference.scope === undefined ? Effect.void : Scope.close(reference.scope, Exit.void));
598
615
  const permitted = (node, identity) => {
599
616
  const metadata = node.metadata;
@@ -607,33 +624,45 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
607
624
  const authorize = (node, identity, bits, op) => identity.privileged || (permitted(node, identity) & bits) === bits
608
625
  ? Effect.void
609
626
  : Effect.fail(op.fail("AccessDenied"));
610
- const reclaim = (d, ino) => {
611
- const node = d.get(ino);
627
+ const reclaim = Effect.fnUntraced(function* (d, ino) {
628
+ const node = yield* d.get(ino);
612
629
  if (node === undefined)
613
630
  return;
614
631
  if (node.kind === "file") {
615
- if (node.links.length > 0 || d.openCount(ino) > 0)
632
+ if (node.links.length > 0 || (yield* d.openCount(ino)) > 0)
616
633
  return;
617
- d.usedBytes -= BigInt(node.data.length);
634
+ yield* d.addUsedBytes(-BigInt(node.data.length));
618
635
  }
619
636
  else if (node.kind === "symlink") {
620
637
  if (node.links.length > 0)
621
638
  return;
622
- d.usedBytes -= BigInt(node.target.length);
639
+ yield* d.addUsedBytes(-BigInt(node.target.length));
623
640
  }
624
641
  else if (node.metadata.nlink > 0 || directoryHolds.has(ino))
625
642
  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({
643
+ yield* d.remove(ino);
644
+ });
645
+ const atEntryLimit = Effect.gen(function* () {
646
+ const draft = yield* Draft;
647
+ return settings.maxEntries !== undefined && (yield* draft.entries) >= settings.maxEntries;
648
+ });
649
+ const reserveEntry = Effect.fnUntraced(function* (op) {
650
+ const draft = yield* Draft;
651
+ if ((yield* atEntryLimit) || !(yield* draft.canAllocate))
652
+ return yield* op.fail("NoSpace");
653
+ });
654
+ const reserveInode = (op) => Effect.flatMap(Draft, (draft) => Effect.flatMap(draft.canAllocate, (available) => available ? Effect.void : Effect.fail(op.fail("NoSpace"))));
655
+ const reserveBytes = Effect.fnUntraced(function* (op, bytes) {
656
+ if (settings.maxBytes === undefined)
657
+ return;
658
+ const draft = yield* Draft;
659
+ if (bytes > ByteSize.toBigInt(settings.maxBytes) - (yield* draft.usedBytes)) {
660
+ return yield* op.fail("NoSpace");
661
+ }
662
+ });
663
+ const attach = Effect.fnUntraced(function* (parent, name, child, now) {
664
+ const d = yield* Draft;
665
+ yield* d.put(withEntries({
637
666
  ...parent,
638
667
  metadata: {
639
668
  ...parent.metadata,
@@ -643,51 +672,49 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
643
672
  }
644
673
  }, (entries) => entries.set(name, child.ino)));
645
674
  if (child.kind === "directory")
646
- d.put({ ...child, parent: parent.ino, name });
675
+ yield* d.put({ ...child, parent: parent.ino, name });
647
676
  else {
648
- d.put({
677
+ yield* d.put({
649
678
  ...child,
650
679
  links: [...child.links, { parent: parent.ino, name }],
651
680
  metadata: { ...child.metadata, nlink: child.links.length + 1, ctimeNs: now }
652
681
  });
653
682
  }
654
- };
655
- const detach = (child, parent, name, now) => {
656
- const d = current();
683
+ });
684
+ const detach = Effect.fnUntraced(function* (child, parent, name, now) {
685
+ const d = yield* Draft;
657
686
  if (child.kind === "directory") {
658
- d.put({ ...child, metadata: { ...child.metadata, nlink: 0, ctimeNs: now } });
687
+ yield* d.put({ ...child, metadata: { ...child.metadata, nlink: 0, ctimeNs: now } });
659
688
  }
660
689
  else {
661
690
  const links = withoutLink(child.links, parent, name);
662
- d.put({ ...child, links, metadata: { ...child.metadata, nlink: links.length, ctimeNs: now } });
691
+ yield* d.put({ ...child, links, metadata: { ...child.metadata, nlink: links.length, ctimeNs: now } });
663
692
  }
664
- reclaim(d, child.ino);
665
- };
666
- const releaseFile = (ref) => {
693
+ yield* reclaim(d, child.ino);
694
+ });
695
+ const releaseFile = Effect.fnUntraced(function* (ref) {
667
696
  const ino = ref.ino;
668
697
  if (ino !== undefined) {
669
- const d = current();
670
- d.release(ino);
671
- reclaim(d, ino);
698
+ const d = yield* Draft;
699
+ yield* d.release(ino);
700
+ yield* reclaim(d, ino);
672
701
  }
673
702
  ref.ino = undefined;
674
703
  ref.closed = true;
675
- };
676
- const releaseOpenFile = (ref) => coordinatedCleanup(Effect.sync(() => {
677
- if (!ref.closed)
678
- applyDirect(() => releaseFile(ref));
679
- }));
704
+ });
705
+ const releaseOpenFile = (ref) => coordinatedCleanup(Effect.suspend(() => ref.closed ? Effect.void : applyDirect(() => releaseFile(ref))));
680
706
  // Explicit close and scope cleanup share one release. A release whose commit fails still completes as
681
707
  // cleanup, so the handle never stays open; only an explicit close reports the failure. A close refused
682
708
  // 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(() => {
709
+ const closeFile = (ref, op, check) => coordinated(op, Effect.andThen(check, Effect.gen(function* () {
684
710
  const ino = ref.ino;
685
711
  if (ino !== undefined) {
686
- const d = current();
687
- d.release(ino);
688
- reclaim(d, ino);
712
+ const d = yield* Draft;
713
+ yield* d.release(ino);
714
+ yield* reclaim(d, ino);
689
715
  }
690
- current().after.push(() => {
716
+ const d = yield* Draft;
717
+ yield* d.afterInstall(() => {
691
718
  ref.ino = undefined;
692
719
  ref.closed = true;
693
720
  });
@@ -713,19 +740,16 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
713
740
  return yield* coordinate(Effect.suspend(() => closed() ? Effect.interrupt : acquire).pipe(Effect.tap(() => Effect.suspend(() => {
714
741
  if (!closed())
715
742
  return Effect.void;
716
- releaseAcquired();
717
- return Effect.interrupt;
743
+ return Effect.andThen(releaseAcquired(), Effect.interrupt);
718
744
  })))).pipe(Effect.tap(() => Effect.suspend(() => closed() ? Effect.interrupt : Effect.void)), Effect.onError(() => Effect.andThen(finalize, Scope.close(scope, Exit.void))));
719
745
  });
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({
746
+ const bindFile = (ref, ino) => Effect.flatMap(Draft, (draft) => draft.afterInstall(() => {
747
+ ref.ino = ino;
748
+ }));
749
+ const replaceContent = Effect.fnUntraced(function* (file, data, now, publish = true) {
750
+ const d = yield* Draft;
751
+ yield* d.addUsedBytes(BigInt(data.length - file.data.length));
752
+ yield* d.put({
729
753
  ...file,
730
754
  data,
731
755
  metadata: {
@@ -737,8 +761,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
737
761
  }
738
762
  });
739
763
  if (publish)
740
- publishNode(file.ino);
741
- };
764
+ yield* publishNode(file.ino);
765
+ });
742
766
  const resizeAt = Effect.fnUntraced(function* (file, length, now, op) {
743
767
  if (length > BigInt(maxFileBytes))
744
768
  return yield* op.fail("FileTooLarge");
@@ -746,22 +770,22 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
746
770
  yield* reserveBytes(op, BigInt(size - file.data.length));
747
771
  const data = new Uint8Array(size);
748
772
  data.set(file.data.subarray(0, size));
749
- replaceContent(file, data, now, false);
773
+ yield* replaceContent(file, data, now, false);
750
774
  });
751
775
  const resize = Effect.fnUntraced(function* (file, length, op) {
752
776
  if (!isLength(length))
753
777
  return yield* op.fail("InvalidArgument");
754
778
  yield* resizeAt(file, length, yield* timestamp(op), op);
755
- publishNode(file.ino);
779
+ yield* publishNode(file.ino);
756
780
  });
757
781
  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
- };
782
+ const get = Effect.fnUntraced(function* (op, access) {
783
+ const node = ref.ino === undefined ? undefined : yield* view(ref.ino);
784
+ if (node?.kind !== "file" || (access === "read" && ref.access === "write") ||
785
+ (access === "write" && ref.access === "read"))
786
+ return yield* op.fail("InvalidHandle");
787
+ return node;
788
+ });
765
789
  const read = (maximum, position) => {
766
790
  const op = OpContext.make(position === undefined ? "read" : "pread");
767
791
  const readOp = OpContext.make("read");
@@ -783,9 +807,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
783
807
  // other would start at the same offset.
784
808
  if (position !== undefined)
785
809
  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(() => {
810
+ return coordinated(op, Effect.gen(function* () {
811
+ const { bytes, eof, next } = yield* refreshAccess(yield* body);
812
+ const draft = yield* Draft;
813
+ yield* draft.afterInstall(() => {
789
814
  ref.offset = next;
790
815
  });
791
816
  return { bytes, eof };
@@ -809,9 +834,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
809
834
  if (offset >= BigInt(maxFileBytes))
810
835
  return yield* writeOp.fail("FileTooLarge");
811
836
  const start = Number(offset);
837
+ const draft = yield* Draft;
812
838
  const free = settings.maxBytes === undefined
813
839
  ? BigInt(maxFileBytes)
814
- : ByteSize.toBigInt(settings.maxBytes) - current().usedBytes;
840
+ : ByteSize.toBigInt(settings.maxBytes) - (yield* draft.usedBytes);
815
841
  const maximumEnd = BigInt(file.data.length) + free;
816
842
  const end = Number(BigInt(maxFileBytes) < maximumEnd ? BigInt(maxFileBytes) : maximumEnd);
817
843
  const count = Math.min(bytes.length, Math.max(0, end - start));
@@ -824,10 +850,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
824
850
  data.set(file.data);
825
851
  const now = yield* timestamp(writeOp);
826
852
  data.set(bytes.subarray(0, count), start);
827
- replaceContent(file, data, now);
853
+ yield* replaceContent(file, data, now);
828
854
  if (position === undefined) {
829
855
  const next = offset + BigInt(count);
830
- current().after.push(() => {
856
+ yield* draft.afterInstall(() => {
831
857
  ref.offset = next;
832
858
  });
833
859
  }
@@ -901,34 +927,38 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
901
927
  if (known.volume !== volumeIdentity) {
902
928
  return yield* op.fail("ForeignReference");
903
929
  }
904
- const node = view(known.ino);
930
+ const node = yield* view(known.ino);
905
931
  if (node === undefined || (node.kind === "directory" && node.metadata.nlink === 0)) {
906
932
  return yield* op.fail("StaleReference");
907
933
  }
908
934
  return node;
909
935
  });
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);
936
+ const callerDirectory = Effect.fnUntraced(function* (op) {
937
+ const node = reference.ino === undefined ? undefined : yield* view(reference.ino);
916
938
  if (node?.kind !== "directory")
917
- throw new Error("Directory left the inode table during a transition");
939
+ return yield* op.fail("ClosedCaller");
918
940
  return node;
919
- };
920
- const nodeNow = (ino) => {
921
- const node = view(ino);
941
+ });
942
+ const directoryNow = Effect.fnUntraced(function* (ino) {
943
+ const node = yield* view(ino);
944
+ if (node?.kind !== "directory") {
945
+ return dieInvariant("Directory left the inode table during a transition");
946
+ }
947
+ return node;
948
+ });
949
+ const nodeNow = Effect.fnUntraced(function* (ino) {
950
+ const node = yield* view(ino);
922
951
  if (node === undefined)
923
- throw new Error("Inode left the table during a transition");
952
+ return dieInvariant("Inode left the table during a transition");
924
953
  return node;
925
- };
954
+ });
926
955
  const creationTimes = (times, now) => ({
927
956
  atimeNs: times?.access.kind === "value" ? times.access.nanoseconds : now,
928
957
  mtimeNs: times?.modification.kind === "value" ? times.modification.nanoseconds : now
929
958
  });
930
- const newDirectory = (parent, mode, now, times) => {
931
- const ino = current().allocate();
959
+ const newDirectory = Effect.fnUntraced(function* (parent, mode, now, times) {
960
+ const draft = yield* Draft;
961
+ const ino = yield* draft.allocate;
932
962
  return {
933
963
  kind: "directory",
934
964
  ino,
@@ -941,9 +971,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
941
971
  },
942
972
  revision: 0n
943
973
  };
944
- };
945
- const newFile = (parent, data, mode, now, owner, times) => {
946
- const ino = current().allocate();
974
+ });
975
+ const newFile = Effect.fnUntraced(function* (parent, data, mode, now, owner, times) {
976
+ const draft = yield* Draft;
977
+ const ino = yield* draft.allocate;
947
978
  return {
948
979
  kind: "file",
949
980
  ino,
@@ -958,9 +989,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
958
989
  },
959
990
  revision: 0n
960
991
  };
961
- };
962
- const newSymlink = (parent, target, now, times) => {
963
- const ino = current().allocate();
992
+ });
993
+ const newSymlink = Effect.fnUntraced(function* (parent, target, now, times) {
994
+ const draft = yield* Draft;
995
+ const ino = yield* draft.allocate;
964
996
  return {
965
997
  kind: "symlink",
966
998
  ino,
@@ -975,14 +1007,14 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
975
1007
  },
976
1008
  revision: 0n
977
1009
  };
978
- };
1010
+ });
979
1011
  const lookup = Effect.fnUntraced(function* (path, base, op, options = {}, referencedBase) {
980
1012
  const pathOp = op.at(path.input);
981
1013
  const { allowMissing = false, createMissing, followFinalSymlink = true, parentOnly = false } = options;
982
1014
  if (reference.ino === undefined) {
983
1015
  return yield* pathOp.fail("ClosedCaller");
984
1016
  }
985
- let current = path.absolute ? nodeNow(ROOT_INO) : yield* callerDirectory(pathOp);
1017
+ let current = path.absolute ? (yield* nodeNow(ROOT_INO)) : yield* callerDirectory(pathOp);
986
1018
  if (!path.absolute && referencedBase !== undefined) {
987
1019
  current = referencedBase;
988
1020
  yield* authorize(current, identity, EXECUTE, op);
@@ -995,7 +1027,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
995
1027
  if (target.volume !== volumeIdentity) {
996
1028
  return yield* pathOp.fail("ForeignHandle");
997
1029
  }
998
- const directory = target.ino === undefined ? undefined : view(target.ino);
1030
+ const directory = target.ino === undefined ? undefined : (yield* view(target.ino));
999
1031
  if (directory?.kind !== "directory") {
1000
1032
  return yield* pathOp.fail("InvalidHandle");
1001
1033
  }
@@ -1023,7 +1055,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1023
1055
  if (component === DOT_HEX)
1024
1056
  continue;
1025
1057
  if (component === DOT_DOT_HEX) {
1026
- current = directoryNow(current.parent);
1058
+ current = yield* directoryNow(current.parent);
1027
1059
  parent = undefined;
1028
1060
  name = undefined;
1029
1061
  continue;
@@ -1031,7 +1063,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1031
1063
  parent = current;
1032
1064
  name = component;
1033
1065
  const childIno = current.entries.get(component);
1034
- const child = childIno === undefined ? undefined : view(childIno);
1066
+ const child = childIno === undefined ? undefined : (yield* view(childIno));
1035
1067
  if (child === undefined) {
1036
1068
  if (allowMissing && index === work.components.length - 1) {
1037
1069
  return { node: undefined, parent, name };
@@ -1068,7 +1100,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1068
1100
  work = expanded.success;
1069
1101
  linked = work.components.length - remaining + Math.max(0, linked - index - 1);
1070
1102
  if (work.absolute)
1071
- current = nodeNow(ROOT_INO);
1103
+ current = yield* nodeNow(ROOT_INO);
1072
1104
  index = -1;
1073
1105
  }
1074
1106
  else
@@ -1136,7 +1168,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1136
1168
  const resolving = resolve();
1137
1169
  return coordinated(op, Effect.gen(function* () {
1138
1170
  const resolved = yield* resolving;
1139
- const node = nodeNow(resolved.ino);
1171
+ const node = yield* nodeNow(resolved.ino);
1140
1172
  if (expected !== undefined && node.revision !== expected) {
1141
1173
  return yield* resolved.op.fail("StaleReference", { field: "expected" });
1142
1174
  }
@@ -1162,11 +1194,11 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1162
1194
  const now = yield* timestamp(op);
1163
1195
  if (size !== undefined && node.kind === "file")
1164
1196
  yield* resizeAt(node, size, now, op);
1165
- const sized = nodeNow(resolved.ino);
1197
+ const sized = yield* nodeNow(resolved.ino);
1166
1198
  const gid = owner?.gid ?? sized.metadata.gid;
1167
1199
  const cleared = chowned && sized.kind === "file" ? sized.metadata.mode & ~SET_ID_BITS : sized.metadata.mode;
1168
1200
  if (chowned || mode !== undefined || timed) {
1169
- current().put({
1201
+ yield* (yield* Draft).put({
1170
1202
  ...sized,
1171
1203
  metadata: {
1172
1204
  ...sized.metadata,
@@ -1179,7 +1211,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1179
1211
  }
1180
1212
  });
1181
1213
  }
1182
- publishNode(node.ino);
1214
+ yield* publishNode(node.ino);
1183
1215
  }));
1184
1216
  };
1185
1217
  const authorizeRemoval = (parent, child, op) => (parent.metadata.mode & STICKY_BIT) !== 0 && !identity.privileged &&
@@ -1187,7 +1219,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1187
1219
  ? Effect.fail(op.fail("NotPermitted"))
1188
1220
  : Effect.void;
1189
1221
  const claimName = Effect.fnUntraced(function* (entry, trailingSlash = "allowed") {
1190
- const parent = directoryNow(entry.parent);
1222
+ const parent = yield* directoryNow(entry.parent);
1191
1223
  if (isDotComponent(entry.name)) {
1192
1224
  return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : "AlreadyExists");
1193
1225
  }
@@ -1202,17 +1234,18 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1202
1234
  const makeDirectory = Effect.fnUntraced(function* (entry, request, op) {
1203
1235
  const name = yield* claimName(entry);
1204
1236
  yield* reserveEntry(entry.op);
1205
- const parent = directoryNow(entry.parent);
1237
+ const parent = yield* directoryNow(entry.parent);
1206
1238
  const before = parent.revision;
1207
1239
  const now = yield* timestamp(op);
1208
1240
  const mode = request.exactMode
1209
1241
  ? yield* permittedMode({ kind: "directory", uid: identity.uid, gid: parent.metadata.gid }, request.mode, op)
1210
1242
  : (request.mode & 0o777 & ~umask) | (request.mode & STICKY_BIT);
1211
- const child = newDirectory(parent, mode, now, request.times);
1212
- attach(parent, name, child, now);
1213
- current().entries += 1;
1214
- publishEntry("Create", parent.ino, name, child.ino);
1215
- return { child: child.ino, directory: { before, after: current().revision } };
1243
+ const child = yield* newDirectory(parent, mode, now, request.times);
1244
+ yield* attach(parent, name, child, now);
1245
+ const draft = yield* Draft;
1246
+ yield* draft.addEntries(1);
1247
+ yield* publishEntry("Create", parent.ino, name, child.ino);
1248
+ return { child: child.ino, directory: { before, after: draft.revision } };
1216
1249
  });
1217
1250
  // Creates every missing directory a path names in the running transition, so a failure anywhere discards
1218
1251
  // them all. The mode applies to each directory created and the times to the one in the final position. An
@@ -1228,15 +1261,17 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1228
1261
  const made = yield* Effect.result(makeDirectory(entry, request, op));
1229
1262
  if (Result.isSuccess(made))
1230
1263
  return made.success;
1231
- const existingIno = entry.name === undefined ? undefined : directoryNow(entry.parent).entries.get(entry.name);
1232
- node = existingIno === undefined ? undefined : view(existingIno);
1264
+ const existingIno = entry.name === undefined
1265
+ ? undefined
1266
+ : (yield* directoryNow(entry.parent)).entries.get(entry.name);
1267
+ node = existingIno === undefined ? undefined : (yield* view(existingIno));
1233
1268
  if (made.failure.code !== "AlreadyExists" || node?.kind !== "directory")
1234
1269
  return yield* made.failure;
1235
1270
  }
1236
1271
  else {
1237
1272
  entryOp = op.at(prepared.path.input);
1238
1273
  const resolved = yield* lookup(prepared.path, prepared.base, op, {
1239
- 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) => {
1274
+ 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) => {
1240
1275
  if (!revisionsBefore.has(parent.ino))
1241
1276
  revisionsBefore.set(parent.ino, made.directory.before);
1242
1277
  return directoryNow(made.child);
@@ -1246,39 +1281,41 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1246
1281
  }
1247
1282
  if (node?.kind !== "directory")
1248
1283
  return yield* entryOp.fail("AlreadyExists");
1249
- const after = directoryNow(node.parent).revision;
1284
+ const after = (yield* directoryNow(node.parent)).revision;
1250
1285
  return { child: node.ino, directory: { before: revisionsBefore.get(node.parent) ?? after, after } };
1251
1286
  });
1252
1287
  const linkNode = Effect.fnUntraced(function* (node, entry, op) {
1253
1288
  const name = yield* claimName(entry, "rejected");
1254
1289
  yield* reserveEntry(entry.op);
1255
- const parent = directoryNow(entry.parent);
1290
+ const parent = yield* directoryNow(entry.parent);
1256
1291
  const before = parent.revision;
1257
1292
  const now = yield* timestamp(op);
1258
- attach(parent, name, node, now);
1259
- current().entries += 1;
1260
- publishEntry("Create", parent.ino, name, node.ino);
1261
- return { before, after: current().revision };
1293
+ yield* attach(parent, name, node, now);
1294
+ const draft = yield* Draft;
1295
+ yield* draft.addEntries(1);
1296
+ yield* publishEntry("Create", parent.ino, name, node.ino);
1297
+ return { before, after: draft.revision };
1262
1298
  });
1263
1299
  const makeSymlink = Effect.fnUntraced(function* (entry, target, times, op) {
1264
1300
  const name = yield* claimName(entry, "rejected");
1265
1301
  yield* reserveEntry(entry.op);
1266
1302
  yield* reserveBytes(entry.op, BigInt(target.length));
1267
- const parent = directoryNow(entry.parent);
1303
+ const parent = yield* directoryNow(entry.parent);
1268
1304
  const before = parent.revision;
1269
1305
  const now = yield* timestamp(op);
1270
- const child = newSymlink(parent, target, now, times);
1271
- attach(parent, name, child, now);
1272
- current().entries += 1;
1273
- current().usedBytes += BigInt(target.length);
1274
- publishEntry("Create", parent.ino, name, child.ino);
1275
- return { child: child.ino, directory: { before, after: current().revision } };
1306
+ const child = yield* newSymlink(parent, target, now, times);
1307
+ yield* attach(parent, name, child, now);
1308
+ const draft = yield* Draft;
1309
+ yield* draft.addEntries(1);
1310
+ yield* draft.addUsedBytes(BigInt(target.length));
1311
+ yield* publishEntry("Create", parent.ino, name, child.ino);
1312
+ return { child: child.ino, directory: { before, after: draft.revision } };
1276
1313
  });
1277
1314
  const removeChild = Effect.fnUntraced(function* (parent, name, child, op) {
1278
1315
  const before = parent.revision;
1279
1316
  const now = yield* timestamp(op);
1280
- const d = current();
1281
- d.put(withEntries({
1317
+ const d = yield* Draft;
1318
+ yield* d.put(withEntries({
1282
1319
  ...parent,
1283
1320
  metadata: {
1284
1321
  ...parent.metadata,
@@ -1287,22 +1324,22 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1287
1324
  ctimeNs: now
1288
1325
  }
1289
1326
  }, (entries) => entries.delete(name)));
1290
- publishEntry("Remove", parent.ino, name, child.ino);
1291
- detach(child, parent.ino, name, now);
1292
- d.entries -= 1;
1327
+ yield* publishEntry("Remove", parent.ino, name, child.ino);
1328
+ yield* detach(child, parent.ino, name, now);
1329
+ yield* d.addEntries(-1);
1293
1330
  return { before, after: d.revision };
1294
1331
  });
1295
1332
  // Authorizes removing from the entry's directory and returns the named child. Only a path can name a dot
1296
1333
  // entry, and each verb reports it with its own code. Search permission comes before the lookup and write
1297
1334
  // permission after it, as on Linux; `beforeWrite` holds the verb's own checks that Linux makes in between.
1298
1335
  const removalTarget = Effect.fnUntraced(function* (entry, dotNameCode, beforeWrite) {
1299
- const parent = directoryNow(entry.parent);
1336
+ const parent = yield* directoryNow(entry.parent);
1300
1337
  if (isDotComponent(entry.name)) {
1301
1338
  return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : dotNameCode);
1302
1339
  }
1303
1340
  yield* authorize(parent, identity, EXECUTE, entry.op);
1304
1341
  const childIno = parent.entries.get(entry.name);
1305
- const child = childIno === undefined ? undefined : view(childIno);
1342
+ const child = childIno === undefined ? undefined : (yield* view(childIno));
1306
1343
  if (child === undefined)
1307
1344
  return yield* entry.op.fail("NotFound");
1308
1345
  const rejected = beforeWrite?.(child);
@@ -1337,8 +1374,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1337
1374
  return yield* removeChild(parent, name, child, op);
1338
1375
  });
1339
1376
  const renameEntry = Effect.fnUntraced(function* (source, destination, op) {
1340
- const sourceDirectory = directoryNow(source.parent);
1341
- const destinationDirectory = directoryNow(destination.parent);
1377
+ const sourceDirectory = yield* directoryNow(source.parent);
1378
+ const destinationDirectory = yield* directoryNow(destination.parent);
1342
1379
  const sameDirectory = source.parent === destination.parent;
1343
1380
  const sourceName = source.name;
1344
1381
  const destinationName = destination.name;
@@ -1352,11 +1389,11 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1352
1389
  const sourceBefore = sourceDirectory.revision;
1353
1390
  const destinationBefore = destinationDirectory.revision;
1354
1391
  const childIno = sourceDirectory.entries.get(sourceName);
1355
- const child = childIno === undefined ? undefined : view(childIno);
1392
+ const child = childIno === undefined ? undefined : (yield* view(childIno));
1356
1393
  if (child === undefined)
1357
1394
  return yield* source.op.fail("NotFound");
1358
1395
  const replacedIno = destinationDirectory.entries.get(destinationName);
1359
- const replaced = replacedIno === undefined ? undefined : view(replacedIno);
1396
+ const replaced = replacedIno === undefined ? undefined : (yield* view(replacedIno));
1360
1397
  // A trailing slash on either side asks for a directory; a missing slashed destination is fine when the
1361
1398
  // source is one, as Linux allows.
1362
1399
  if ((source.trailingSlash || destination.trailingSlash) && child.kind !== "directory") {
@@ -1365,24 +1402,29 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1365
1402
  if (destination.trailingSlash && replaced !== undefined && replaced.kind !== "directory") {
1366
1403
  return yield* destination.op.fail("NotDirectory");
1367
1404
  }
1368
- const result = () => sameDirectory
1369
- ? {
1370
- _tag: "SameDirectory",
1371
- directory: { before: sourceBefore, after: directoryNow(source.parent).revision }
1372
- }
1373
- : {
1374
- _tag: "DifferentDirectories",
1375
- sourceDirectory: { before: sourceBefore, after: directoryNow(source.parent).revision },
1376
- destinationDirectory: { before: destinationBefore, after: directoryNow(destination.parent).revision }
1377
- };
1378
- for (let ancestor = destinationDirectory;; ancestor = directoryNow(ancestor.parent)) {
1405
+ const result = Effect.fnUntraced(function* () {
1406
+ return sameDirectory
1407
+ ? {
1408
+ _tag: "SameDirectory",
1409
+ directory: { before: sourceBefore, after: (yield* directoryNow(source.parent)).revision }
1410
+ }
1411
+ : {
1412
+ _tag: "DifferentDirectories",
1413
+ sourceDirectory: { before: sourceBefore, after: (yield* directoryNow(source.parent)).revision },
1414
+ destinationDirectory: {
1415
+ before: destinationBefore,
1416
+ after: (yield* directoryNow(destination.parent)).revision
1417
+ }
1418
+ };
1419
+ });
1420
+ for (let ancestor = destinationDirectory;; ancestor = yield* directoryNow(ancestor.parent)) {
1379
1421
  if (ancestor.ino === child.ino)
1380
1422
  return yield* destination.op.fail("InvalidArgument");
1381
1423
  if (ancestor.ino === ROOT_INO)
1382
1424
  break;
1383
1425
  }
1384
1426
  if (child.ino === replaced?.ino)
1385
- return result();
1427
+ return yield* result();
1386
1428
  yield* authorize(sourceDirectory, identity, WRITE, source.op);
1387
1429
  yield* authorize(destinationDirectory, identity, WRITE, destination.op);
1388
1430
  yield* authorizeRemoval(sourceDirectory, child, source.op);
@@ -1399,9 +1441,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1399
1441
  }
1400
1442
  }
1401
1443
  const now = yield* timestamp(op);
1402
- const d = current();
1403
- publishEntry("Remove", sourceDirectory.ino, sourceName, child.ino);
1404
- d.put(withEntries({
1444
+ const d = yield* Draft;
1445
+ yield* publishEntry("Remove", sourceDirectory.ino, sourceName, child.ino);
1446
+ yield* d.put(withEntries({
1405
1447
  ...sourceDirectory,
1406
1448
  metadata: {
1407
1449
  ...sourceDirectory.metadata,
@@ -1410,8 +1452,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1410
1452
  ctimeNs: now
1411
1453
  }
1412
1454
  }, (entries) => entries.delete(sourceName)));
1413
- const destinationNow = directoryNow(destination.parent);
1414
- d.put(withEntries({
1455
+ const destinationNow = yield* directoryNow(destination.parent);
1456
+ yield* d.put(withEntries({
1415
1457
  ...destinationNow,
1416
1458
  metadata: {
1417
1459
  ...destinationNow.metadata,
@@ -1420,9 +1462,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1420
1462
  ctimeNs: now
1421
1463
  }
1422
1464
  }, (entries) => entries.set(destinationName, child.ino)));
1423
- const moved = nodeNow(child.ino);
1465
+ const moved = yield* nodeNow(child.ino);
1424
1466
  if (moved.kind === "directory") {
1425
- d.put({
1467
+ yield* d.put({
1426
1468
  ...moved,
1427
1469
  parent: destination.parent,
1428
1470
  name: destinationName,
@@ -1430,7 +1472,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1430
1472
  });
1431
1473
  }
1432
1474
  else {
1433
- d.put({
1475
+ yield* d.put({
1434
1476
  ...moved,
1435
1477
  links: [
1436
1478
  ...withoutLink(moved.links, source.parent, sourceName),
@@ -1440,11 +1482,11 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1440
1482
  });
1441
1483
  }
1442
1484
  if (replaced !== undefined) {
1443
- detach(nodeNow(replaced.ino), destination.parent, destinationName, now);
1444
- d.entries -= 1;
1485
+ yield* detach(yield* nodeNow(replaced.ino), destination.parent, destinationName, now);
1486
+ yield* d.addEntries(-1);
1445
1487
  }
1446
- publishEntry("Create", destination.parent, destinationName, child.ino);
1447
- return result();
1488
+ yield* publishEntry("Create", destination.parent, destinationName, child.ino);
1489
+ return yield* result();
1448
1490
  });
1449
1491
  const openExisting = Effect.fnUntraced(function* (file, request, at, op) {
1450
1492
  yield* authorize(file, identity, request.access === "read" ? READ : request.access === "write" ? WRITE : READ | WRITE, at);
@@ -1453,7 +1495,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1453
1495
  });
1454
1496
  const openFile = Effect.fnUntraced(function* (entry, found, request, acquired, op) {
1455
1497
  const { name } = entry;
1456
- const parent = directoryNow(entry.parent);
1498
+ const parent = yield* directoryNow(entry.parent);
1457
1499
  const before = parent.revision;
1458
1500
  let file = found;
1459
1501
  let created = false;
@@ -1480,12 +1522,13 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1480
1522
  const mode = request.exactMode
1481
1523
  ? yield* permittedMode({ kind: "file", uid: owner?.uid ?? identity.uid, gid: owner?.gid ?? parent.metadata.gid }, request.mode, op)
1482
1524
  : (request.mode ?? 0o666) & 0o777 & ~umask;
1483
- file = newFile(parent, new Uint8Array(Number(size)), mode, now, owner, request.times);
1484
- attach(parent, name, file, now);
1485
- current().entries += 1;
1486
- current().usedBytes += size;
1525
+ file = yield* newFile(parent, new Uint8Array(Number(size)), mode, now, owner, request.times);
1526
+ yield* attach(parent, name, file, now);
1527
+ const draft = yield* Draft;
1528
+ yield* draft.addEntries(1);
1529
+ yield* draft.addUsedBytes(size);
1487
1530
  created = true;
1488
- publishEntry("Create", parent.ino, name, file.ino);
1531
+ yield* publishEntry("Create", parent.ino, name, file.ino);
1489
1532
  }
1490
1533
  else {
1491
1534
  if (file.kind === "symlink")
@@ -1494,17 +1537,18 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1494
1537
  return yield* entry.op.fail("IsDirectory");
1495
1538
  yield* openExisting(file, request, entry.op, op);
1496
1539
  }
1497
- current().retain(file.ino);
1498
- bindFile(acquired, file.ino);
1499
- return { ino: file.ino, created, directory: { before, after: directoryNow(entry.parent).revision } };
1540
+ yield* (yield* Draft).retain(file.ino);
1541
+ yield* bindFile(acquired, file.ino);
1542
+ return { ino: file.ino, created, directory: { before, after: (yield* directoryNow(entry.parent)).revision } };
1500
1543
  });
1501
- const releasePending = (opened) => () => {
1544
+ const releasePending = (opened) => () => Effect.gen(function* () {
1502
1545
  const ino = opened();
1503
1546
  if (ino === undefined)
1504
1547
  return;
1505
- current().release(ino);
1506
- reclaim(current(), ino);
1507
- };
1548
+ const draft = yield* Draft;
1549
+ yield* draft.release(ino);
1550
+ yield* reclaim(draft, ino);
1551
+ });
1508
1552
  const acquireOpenedFile = (ref, op, acquire) => {
1509
1553
  let opened;
1510
1554
  return acquireHandle(ref, (effect) => coordinated(op, effect), acquire((ino) => {
@@ -1520,7 +1564,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1520
1564
  return yield* op.fail("InvalidHandle");
1521
1565
  if (ref.volume !== volumeIdentity)
1522
1566
  return yield* op.fail("ForeignHandle");
1523
- const node = ref.ino === undefined ? undefined : view(ref.ino);
1567
+ const node = ref.ino === undefined ? undefined : (yield* view(ref.ino));
1524
1568
  if (node === undefined)
1525
1569
  return yield* op.fail("InvalidHandle");
1526
1570
  return node;
@@ -1560,7 +1604,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1560
1604
  if (!isAttachedBytes(bytes) || bytes.length === 0 || bytes.length > MAX_NAME_BYTES || bytes.includes(0) ||
1561
1605
  bytes.includes(SLASH_BYTE))
1562
1606
  return Result.fail(op.fail("InvalidArgument"));
1563
- return Result.succeed(Encoding.encodeHex(new Uint8Array(bytes)));
1607
+ return Result.succeed(Hex.encode(new Uint8Array(bytes)));
1564
1608
  };
1565
1609
  const prepareEntry = (input, op) => isEntry(input)
1566
1610
  ? Result.map(entryName(input.name, op), (name) => ({ kind: "entry", directory: input.directory, name }))
@@ -1593,7 +1637,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1593
1637
  }),
1594
1638
  // Nothing to undo here: the finalizer that follows an interrupted acquisition releases the hold under
1595
1639
  // every permit, where a detached directory may leave the table.
1596
- () => { }, finalizeDirectory(acquired));
1640
+ () => Effect.void, finalizeDirectory(acquired));
1597
1641
  });
1598
1642
  const fail = (op, cause, field) => op.fail("InvalidArgument", field === undefined ? { cause } : { cause, field });
1599
1643
  const preparedOp = (prepared, op) => prepared.kind === "path" ? op.at(prepared.path.input) : op;
@@ -1655,8 +1699,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1655
1699
  if (node.metadata.nlink === 0)
1656
1700
  return yield* resolved.op.fail("StaleReference");
1657
1701
  yield* openExisting(node, chosen, resolved.op, op);
1658
- current().retain(node.ino);
1659
- bindFile(acquired, node.ino);
1702
+ const draft = yield* Draft;
1703
+ yield* draft.retain(node.ino);
1704
+ yield* bindFile(acquired, node.ino);
1660
1705
  opened(node.ino);
1661
1706
  return fileHandle(acquired);
1662
1707
  }));
@@ -1683,7 +1728,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1683
1728
  return yield* op.fail("InvalidArgument");
1684
1729
  yield* authorize(parent, identity, EXECUTE, op);
1685
1730
  const directIno = parent.entries.get(name);
1686
- const direct = directIno === undefined ? undefined : view(directIno);
1731
+ const direct = directIno === undefined ? undefined : (yield* view(directIno));
1687
1732
  if (expected !== undefined) {
1688
1733
  let expectedIno;
1689
1734
  if (expected !== null) {
@@ -1738,12 +1783,11 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1738
1783
  };
1739
1784
  }));
1740
1785
  });
1741
- const walkChildren = (directory, path, depth, up) => {
1786
+ const walkChildren = Effect.fnUntraced(function* (directory, path, depth, up) {
1742
1787
  const listed = referenceFor(directory.ino);
1743
1788
  const frames = [];
1744
- for (const name of [...directory.entries.keys()].sort()) {
1745
- const childIno = directory.entries.get(name);
1746
- const child = childIno === undefined ? undefined : view(childIno);
1789
+ for (const [name, childIno] of Arr.sort(directory.entries, byEntryName)) {
1790
+ const child = yield* view(childIno);
1747
1791
  if (child === undefined)
1748
1792
  continue;
1749
1793
  const bytes = nameBytes(name);
@@ -1763,7 +1807,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1763
1807
  });
1764
1808
  }
1765
1809
  return frames;
1766
- };
1810
+ });
1767
1811
  const anchorFrame = (parent, key, root) => ({
1768
1812
  ino: root.ino,
1769
1813
  kind: root.kind,
@@ -1784,13 +1828,12 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1784
1828
  const prepared = prepare(target.path, op);
1785
1829
  if (Result.isFailure(prepared))
1786
1830
  return Effect.undefined;
1787
- return ResolvedEntry.fromPath(prepared.success, target.relativeTo, op).pipe(Effect.map((entry) => {
1788
- const parent = view(entry.parent);
1831
+ return ResolvedEntry.fromPath(prepared.success, target.relativeTo, op).pipe(Effect.flatMap((entry) => Effect.map(view(entry.parent), (parent) => {
1789
1832
  return entry.name !== undefined && parent?.kind === "directory" &&
1790
1833
  parent.entries.get(entry.name) === root.ino
1791
1834
  ? anchorFrame(parent, entry.name, root)
1792
1835
  : undefined;
1793
- }), Effect.catch(() => Effect.undefined));
1836
+ })), Effect.catch(() => Effect.undefined));
1794
1837
  };
1795
1838
  const holds = (parent, frame) => parent?.kind === "directory" && parent.metadata.nlink > 0 && parent.entries.get(frame.key) === frame.ino;
1796
1839
  // The node a frame names, reached by name from the walk's root as a path lookup reaches it: every directory on
@@ -1803,13 +1846,13 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1803
1846
  const chain = [];
1804
1847
  for (let link = frame; link !== undefined; link = link.up)
1805
1848
  chain.push(link);
1806
- let node = view((chain.at(-1) ?? frame).parent);
1849
+ let node = yield* view((chain.at(-1) ?? frame).parent);
1807
1850
  for (let index = chain.length - 1; index >= 0; index--) {
1808
1851
  const link = chain[index];
1809
1852
  if (link === undefined || node?.kind !== "directory" || node.metadata.nlink === 0)
1810
1853
  return undefined;
1811
1854
  yield* authorize(node, identity, EXECUTE, at);
1812
- node = node.entries.get(link.key) === link.ino ? view(link.ino) : undefined;
1855
+ node = node.entries.get(link.key) === link.ino ? (yield* view(link.ino)) : undefined;
1813
1856
  }
1814
1857
  return node;
1815
1858
  });
@@ -1830,7 +1873,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1830
1873
  const at = op.at(locate(frame.path));
1831
1874
  return Effect.flatMap(reachFrame(frame, at), (node) => node?.kind !== "directory" || node.metadata.nlink === 0
1832
1875
  ? Effect.succeed([])
1833
- : Effect.as(listable(node, at), walkChildren(node, frame.path, frame.depth, frame)));
1876
+ : Effect.andThen(listable(node, at), walkChildren(node, frame.path, frame.depth, frame)));
1834
1877
  }));
1835
1878
  const exceeded = (frame, field) => makeError({ code: "LimitExceeded", operation: op.operation, field, path: errorPath(locate(frame.path)) });
1836
1879
  const admit = (frame) => {
@@ -1902,10 +1945,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1902
1945
  let targetAt = preparedOp(prepared, op);
1903
1946
  const first = coordinatedRead(op, Effect.gen(function* () {
1904
1947
  const entry = yield* resolveEntry(prepared, op);
1905
- const parent = directoryNow(entry.parent);
1948
+ const parent = yield* directoryNow(entry.parent);
1906
1949
  yield* authorize(parent, identity, EXECUTE, entry.op);
1907
1950
  const childIno = entry.name === undefined ? undefined : parent.entries.get(entry.name);
1908
- const child = childIno === undefined ? undefined : view(childIno);
1951
+ const child = childIno === undefined ? undefined : (yield* view(childIno));
1909
1952
  if (entry.name === undefined || child === undefined)
1910
1953
  return yield* entry.op.fail("NotFound");
1911
1954
  if (child.kind !== "directory")
@@ -1913,16 +1956,16 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1913
1956
  yield* listable(child, entry.op);
1914
1957
  anchor = anchorFrame(parent, entry.name, child);
1915
1958
  targetAt = entry.op;
1916
- return walkChildren(child, new Uint8Array(0), 0, anchor);
1959
+ return yield* walkChildren(child, new Uint8Array(0), 0, anchor);
1917
1960
  })).pipe(Effect.catchIf((error) => force && error.code === "NotFound", () => Effect.succeed([])));
1918
1961
  const seams = yield* VolumeTestSeams;
1919
1962
  const removeFrame = (frame) => {
1920
1963
  const at = op.at(locate(frame.path));
1921
- return Effect.andThen(seams.beforeTreeRemoval, coordinated(op, Effect.suspend(() => {
1922
- if (anchor !== undefined && !holds(view(anchor.parent), anchor)) {
1923
- return force ? Effect.void : Effect.fail(targetAt.fail("NotFound"));
1964
+ return Effect.andThen(seams.beforeTreeRemoval, coordinated(op, Effect.gen(function* () {
1965
+ if (anchor !== undefined && !holds(yield* view(anchor.parent), anchor)) {
1966
+ return yield* (force ? Effect.void : Effect.fail(targetAt.fail("NotFound")));
1924
1967
  }
1925
- return Effect.flatMap(frame.up === undefined ? Effect.succeed(view(frame.parent)) : reachFrame(frame.up, at), (parent) => !holds(parent, frame)
1968
+ return yield* Effect.flatMap(frame.up === undefined ? view(frame.parent) : reachFrame(frame.up, at), (parent) => !holds(parent, frame)
1926
1969
  ? Effect.fail(at.fail("NotFound"))
1927
1970
  : removeEntry({
1928
1971
  parent: parent.ino,
@@ -1952,7 +1995,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1952
1995
  const prepared = yield* Effect.fromResult(prepareEntry(input, op));
1953
1996
  return yield* coordinatedRead(op, Effect.gen(function* () {
1954
1997
  const entry = yield* resolveEntry(prepared, op);
1955
- const directory = directoryNow(entry.parent);
1998
+ const directory = yield* directoryNow(entry.parent);
1956
1999
  if (isDotComponent(entry.name)) {
1957
2000
  return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : "NotFound");
1958
2001
  }
@@ -1999,7 +2042,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1999
2042
  const first = coordinatedRead(op, Effect.gen(function* () {
2000
2043
  const directory = yield* resolveDirectory(target, op);
2001
2044
  yield* authorize(directory.node, identity, READ, directory.op);
2002
- return walkChildren(directory.node, new Uint8Array(0), 0, yield* pathAnchor(target, directory.node, op));
2045
+ return yield* walkChildren(directory.node, new Uint8Array(0), 0, yield* pathAnchor(target, directory.node, op));
2003
2046
  }));
2004
2047
  const plan = {
2005
2048
  order: chosen.order ?? "pre",
@@ -2035,7 +2078,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2035
2078
  const pathOp = op.at(target.path);
2036
2079
  const result = yield* lookup(yield* Effect.fromResult(prepare(target.path, op)), target.relativeTo, op);
2037
2080
  const directory = result.node?.kind === "directory" ? result.node : result.parent;
2038
- const prefix = directory === undefined ? SLASH_HEX : pathOf(view, directory.ino);
2081
+ const prefix = directory === undefined ? SLASH_HEX : yield* pathOfCurrent(directory.ino);
2039
2082
  if (prefix === undefined)
2040
2083
  return yield* pathOp.fail("NotFound");
2041
2084
  if (result.node?.kind === "directory" || result.name === undefined)
@@ -2043,7 +2086,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2043
2086
  return ownedPath(nameBytes(prefix + (prefix === SLASH_HEX ? "" : SLASH_HEX) + result.name));
2044
2087
  }
2045
2088
  const resolved = yield* resolveTarget(target, op);
2046
- const path = pathOf(view, resolved.node.ino);
2089
+ const path = yield* pathOfCurrent(resolved.node.ino);
2047
2090
  if (path === undefined)
2048
2091
  return yield* resolved.op.fail("NotFound");
2049
2092
  return ownedPath(nameBytes(path));
@@ -2128,7 +2171,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2128
2171
  parent = directory.node;
2129
2172
  name = prepared.name;
2130
2173
  const childIno = parent.entries.get(name);
2131
- let child = childIno === undefined ? undefined : view(childIno);
2174
+ let child = childIno === undefined ? undefined : (yield* view(childIno));
2132
2175
  if (child?.kind === "symlink" && chosen.replaceFinalSymlink !== true &&
2133
2176
  chosen.followFinalSymlink !== false) {
2134
2177
  const resolved = yield* lookup(yield* Effect.fromResult(preparePath(ownedPath(nameBytes(name)), op.operation, undefined)), undefined, op, {
@@ -2163,7 +2206,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2163
2206
  yield* authorize(parent, identity, WRITE | EXECUTE, entryOp);
2164
2207
  if (replaced !== undefined)
2165
2208
  yield* authorizeRemoval(parent, replaced, entryOp);
2166
- yield* replaced === undefined ? reserveEntry(entryOp) : reserveInode(entryOp);
2209
+ yield* (replaced === undefined ? reserveEntry(entryOp) : reserveInode(entryOp));
2167
2210
  }
2168
2211
  else {
2169
2212
  yield* authorize(file, identity, chosen.access === "readWrite" ? READ | WRITE : WRITE, entryOp);
@@ -2188,8 +2231,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2188
2231
  data.set(captured, position);
2189
2232
  }
2190
2233
  const now = yield* timestamp(op);
2191
- const d = current();
2192
- const node = file ?? newFile(parent, new Uint8Array(0), (chosen.mode ?? 0o666) & 0o777 & ~umask, now);
2234
+ const d = yield* Draft;
2235
+ const node = file ??
2236
+ (yield* newFile(parent, new Uint8Array(0), (chosen.mode ?? 0o666) & 0o777 & ~umask, now));
2193
2237
  const written = {
2194
2238
  ...node,
2195
2239
  data,
@@ -2201,18 +2245,18 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2201
2245
  ctimeNs: now
2202
2246
  }
2203
2247
  };
2204
- d.usedBytes += BigInt(size - previous);
2248
+ yield* d.addUsedBytes(BigInt(size - previous));
2205
2249
  if (file === undefined) {
2206
2250
  if (replaced !== undefined)
2207
- detach(replaced, parent.ino, name, now);
2208
- attach(directoryNow(parent.ino), name, written, now);
2251
+ yield* detach(replaced, parent.ino, name, now);
2252
+ yield* attach(yield* directoryNow(parent.ino), name, written, now);
2209
2253
  if (replaced === undefined)
2210
- d.entries += 1;
2211
- publishEntry(replaced === undefined ? "Create" : "Update", parent.ino, name, written.ino);
2254
+ yield* d.addEntries(1);
2255
+ yield* publishEntry(replaced === undefined ? "Create" : "Update", parent.ino, name, written.ino);
2212
2256
  }
2213
2257
  else {
2214
- d.put(written);
2215
- publishNode(written.ino);
2258
+ yield* d.put(written);
2259
+ yield* publishNode(written.ino);
2216
2260
  }
2217
2261
  }));
2218
2262
  }),
@@ -2373,18 +2417,17 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2373
2417
  const statOp = OpContext.make("stat");
2374
2418
  const handle = Object.freeze({
2375
2419
  [DirectoryHandleId]: true,
2376
- stat: coordinatedRead(statOp, Effect.suspend(() => {
2377
- const node = acquired.ino === undefined ? undefined : view(acquired.ino);
2378
- return node === undefined
2420
+ stat: coordinatedRead(statOp, Effect.gen(function* () {
2421
+ const node = acquired.ino === undefined ? undefined : yield* view(acquired.ino);
2422
+ return yield* (node === undefined
2379
2423
  ? Effect.fail(statOp.fail("InvalidHandle"))
2380
- : Effect.succeed(withMetadata(node));
2424
+ : Effect.succeed(withMetadata(node)));
2381
2425
  })).pipe(Effect.withSpan("DirectoryHandle.stat")),
2382
2426
  close: coordinatedCleanup(Effect.suspend(() => {
2383
2427
  if (acquired.ino === undefined) {
2384
2428
  return Effect.fail(OpContext.make("close").fail("InvalidHandle"));
2385
2429
  }
2386
- releaseDirectory(acquired);
2387
- return Effect.void;
2430
+ return releaseDirectory(acquired);
2388
2431
  })).pipe(Effect.ensuring(closeReleasedScope(acquired)), Effect.withSpan("DirectoryHandle.close"))
2389
2432
  });
2390
2433
  handles.set(handle, acquired);