@effect-vfs/core 0.7.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.
@@ -1,12 +1,14 @@
1
1
  import * as Arr from "effect/Array";
2
2
  import * as ByteSize from "effect/ByteSize";
3
3
  import * as Clock from "effect/Clock";
4
+ import * as Context from "effect/Context";
4
5
  import * as Crypto from "effect/Crypto";
5
6
  import * as Data from "effect/Data";
6
7
  import * as Effect from "effect/Effect";
7
- import * as Encoding from "effect/Encoding";
8
+ import * as Hex from "effect/encoding/Hex";
8
9
  import * as Exit from "effect/Exit";
9
10
  import * as Match from "effect/Match";
11
+ import * as MutableRef from "effect/MutableRef";
10
12
  import * as Option from "effect/Option";
11
13
  import * as Predicate from "effect/Predicate";
12
14
  import * as Result from "effect/Result";
@@ -84,100 +86,115 @@ const encoder = new TextEncoder();
84
86
  const accessDue = (metadata, now) => now !== metadata.atimeNs && (metadata.atimeNs <= metadata.mtimeNs || metadata.atimeNs <= metadata.ctimeNs ||
85
87
  now - metadata.atimeNs >= RELATIME_INTERVAL_NS);
86
88
  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
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
+ })
178
195
  };
179
- return this.finished;
180
- }
196
+ })
197
+ }) {
181
198
  }
182
199
  const objectReferences = new WeakMap();
183
200
  const isFileHandle = (value) => Predicate.hasProperty(FileHandleId)(value);
@@ -227,7 +244,7 @@ const observeChanges = Effect.fnUntraced(function* (captured) {
227
244
  });
228
245
  const randomHex128 = Effect.gen(function* () {
229
246
  const crypto = yield* Crypto.Crypto;
230
- return Encoding.encodeHex(yield* crypto.randomBytes(16));
247
+ return Hex.encode(yield* crypto.randomBytes(16));
231
248
  }).pipe(Effect.orDie);
232
249
  /** @internal */
233
250
  export const makeVolume = Effect.fnUntraced(function* (source, options, commitProvider, captureInitial, durability = "memory-only") {
@@ -256,9 +273,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
256
273
  const incarnation = VolumeIncarnation.make(yield* randomHex128);
257
274
  const epoch = live === undefined ? VolumeEpoch.make(yield* randomHex128) : live.epoch;
258
275
  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));
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
- return dieInvariant("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,35 +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);
938
+ if (node?.kind !== "directory")
939
+ return yield* op.fail("ClosedCaller");
940
+ return node;
941
+ });
942
+ const directoryNow = Effect.fnUntraced(function* (ino) {
943
+ const node = yield* view(ino);
916
944
  if (node?.kind !== "directory") {
917
945
  return dieInvariant("Directory left the inode table during a transition");
918
946
  }
919
947
  return node;
920
- };
921
- const nodeNow = (ino) => {
922
- const node = view(ino);
948
+ });
949
+ const nodeNow = Effect.fnUntraced(function* (ino) {
950
+ const node = yield* view(ino);
923
951
  if (node === undefined)
924
952
  return dieInvariant("Inode left the table during a transition");
925
953
  return node;
926
- };
954
+ });
927
955
  const creationTimes = (times, now) => ({
928
956
  atimeNs: times?.access.kind === "value" ? times.access.nanoseconds : now,
929
957
  mtimeNs: times?.modification.kind === "value" ? times.modification.nanoseconds : now
930
958
  });
931
- const newDirectory = (parent, mode, now, times) => {
932
- 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;
933
962
  return {
934
963
  kind: "directory",
935
964
  ino,
@@ -942,9 +971,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
942
971
  },
943
972
  revision: 0n
944
973
  };
945
- };
946
- const newFile = (parent, data, mode, now, owner, times) => {
947
- 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;
948
978
  return {
949
979
  kind: "file",
950
980
  ino,
@@ -959,9 +989,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
959
989
  },
960
990
  revision: 0n
961
991
  };
962
- };
963
- const newSymlink = (parent, target, now, times) => {
964
- 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;
965
996
  return {
966
997
  kind: "symlink",
967
998
  ino,
@@ -976,14 +1007,14 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
976
1007
  },
977
1008
  revision: 0n
978
1009
  };
979
- };
1010
+ });
980
1011
  const lookup = Effect.fnUntraced(function* (path, base, op, options = {}, referencedBase) {
981
1012
  const pathOp = op.at(path.input);
982
1013
  const { allowMissing = false, createMissing, followFinalSymlink = true, parentOnly = false } = options;
983
1014
  if (reference.ino === undefined) {
984
1015
  return yield* pathOp.fail("ClosedCaller");
985
1016
  }
986
- let current = path.absolute ? nodeNow(ROOT_INO) : yield* callerDirectory(pathOp);
1017
+ let current = path.absolute ? (yield* nodeNow(ROOT_INO)) : yield* callerDirectory(pathOp);
987
1018
  if (!path.absolute && referencedBase !== undefined) {
988
1019
  current = referencedBase;
989
1020
  yield* authorize(current, identity, EXECUTE, op);
@@ -996,7 +1027,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
996
1027
  if (target.volume !== volumeIdentity) {
997
1028
  return yield* pathOp.fail("ForeignHandle");
998
1029
  }
999
- const directory = target.ino === undefined ? undefined : view(target.ino);
1030
+ const directory = target.ino === undefined ? undefined : (yield* view(target.ino));
1000
1031
  if (directory?.kind !== "directory") {
1001
1032
  return yield* pathOp.fail("InvalidHandle");
1002
1033
  }
@@ -1024,7 +1055,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1024
1055
  if (component === DOT_HEX)
1025
1056
  continue;
1026
1057
  if (component === DOT_DOT_HEX) {
1027
- current = directoryNow(current.parent);
1058
+ current = yield* directoryNow(current.parent);
1028
1059
  parent = undefined;
1029
1060
  name = undefined;
1030
1061
  continue;
@@ -1032,7 +1063,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1032
1063
  parent = current;
1033
1064
  name = component;
1034
1065
  const childIno = current.entries.get(component);
1035
- const child = childIno === undefined ? undefined : view(childIno);
1066
+ const child = childIno === undefined ? undefined : (yield* view(childIno));
1036
1067
  if (child === undefined) {
1037
1068
  if (allowMissing && index === work.components.length - 1) {
1038
1069
  return { node: undefined, parent, name };
@@ -1069,7 +1100,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1069
1100
  work = expanded.success;
1070
1101
  linked = work.components.length - remaining + Math.max(0, linked - index - 1);
1071
1102
  if (work.absolute)
1072
- current = nodeNow(ROOT_INO);
1103
+ current = yield* nodeNow(ROOT_INO);
1073
1104
  index = -1;
1074
1105
  }
1075
1106
  else
@@ -1137,7 +1168,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1137
1168
  const resolving = resolve();
1138
1169
  return coordinated(op, Effect.gen(function* () {
1139
1170
  const resolved = yield* resolving;
1140
- const node = nodeNow(resolved.ino);
1171
+ const node = yield* nodeNow(resolved.ino);
1141
1172
  if (expected !== undefined && node.revision !== expected) {
1142
1173
  return yield* resolved.op.fail("StaleReference", { field: "expected" });
1143
1174
  }
@@ -1163,11 +1194,11 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1163
1194
  const now = yield* timestamp(op);
1164
1195
  if (size !== undefined && node.kind === "file")
1165
1196
  yield* resizeAt(node, size, now, op);
1166
- const sized = nodeNow(resolved.ino);
1197
+ const sized = yield* nodeNow(resolved.ino);
1167
1198
  const gid = owner?.gid ?? sized.metadata.gid;
1168
1199
  const cleared = chowned && sized.kind === "file" ? sized.metadata.mode & ~SET_ID_BITS : sized.metadata.mode;
1169
1200
  if (chowned || mode !== undefined || timed) {
1170
- current().put({
1201
+ yield* (yield* Draft).put({
1171
1202
  ...sized,
1172
1203
  metadata: {
1173
1204
  ...sized.metadata,
@@ -1180,7 +1211,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1180
1211
  }
1181
1212
  });
1182
1213
  }
1183
- publishNode(node.ino);
1214
+ yield* publishNode(node.ino);
1184
1215
  }));
1185
1216
  };
1186
1217
  const authorizeRemoval = (parent, child, op) => (parent.metadata.mode & STICKY_BIT) !== 0 && !identity.privileged &&
@@ -1188,7 +1219,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1188
1219
  ? Effect.fail(op.fail("NotPermitted"))
1189
1220
  : Effect.void;
1190
1221
  const claimName = Effect.fnUntraced(function* (entry, trailingSlash = "allowed") {
1191
- const parent = directoryNow(entry.parent);
1222
+ const parent = yield* directoryNow(entry.parent);
1192
1223
  if (isDotComponent(entry.name)) {
1193
1224
  return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : "AlreadyExists");
1194
1225
  }
@@ -1203,17 +1234,18 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1203
1234
  const makeDirectory = Effect.fnUntraced(function* (entry, request, op) {
1204
1235
  const name = yield* claimName(entry);
1205
1236
  yield* reserveEntry(entry.op);
1206
- const parent = directoryNow(entry.parent);
1237
+ const parent = yield* directoryNow(entry.parent);
1207
1238
  const before = parent.revision;
1208
1239
  const now = yield* timestamp(op);
1209
1240
  const mode = request.exactMode
1210
1241
  ? yield* permittedMode({ kind: "directory", uid: identity.uid, gid: parent.metadata.gid }, request.mode, op)
1211
1242
  : (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 } };
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 } };
1217
1249
  });
1218
1250
  // Creates every missing directory a path names in the running transition, so a failure anywhere discards
1219
1251
  // them all. The mode applies to each directory created and the times to the one in the final position. An
@@ -1229,15 +1261,17 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1229
1261
  const made = yield* Effect.result(makeDirectory(entry, request, op));
1230
1262
  if (Result.isSuccess(made))
1231
1263
  return made.success;
1232
- const existingIno = entry.name === undefined ? undefined : directoryNow(entry.parent).entries.get(entry.name);
1233
- 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));
1234
1268
  if (made.failure.code !== "AlreadyExists" || node?.kind !== "directory")
1235
1269
  return yield* made.failure;
1236
1270
  }
1237
1271
  else {
1238
1272
  entryOp = op.at(prepared.path.input);
1239
1273
  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) => {
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) => {
1241
1275
  if (!revisionsBefore.has(parent.ino))
1242
1276
  revisionsBefore.set(parent.ino, made.directory.before);
1243
1277
  return directoryNow(made.child);
@@ -1247,39 +1281,41 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1247
1281
  }
1248
1282
  if (node?.kind !== "directory")
1249
1283
  return yield* entryOp.fail("AlreadyExists");
1250
- const after = directoryNow(node.parent).revision;
1284
+ const after = (yield* directoryNow(node.parent)).revision;
1251
1285
  return { child: node.ino, directory: { before: revisionsBefore.get(node.parent) ?? after, after } };
1252
1286
  });
1253
1287
  const linkNode = Effect.fnUntraced(function* (node, entry, op) {
1254
1288
  const name = yield* claimName(entry, "rejected");
1255
1289
  yield* reserveEntry(entry.op);
1256
- const parent = directoryNow(entry.parent);
1290
+ const parent = yield* directoryNow(entry.parent);
1257
1291
  const before = parent.revision;
1258
1292
  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 };
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 };
1263
1298
  });
1264
1299
  const makeSymlink = Effect.fnUntraced(function* (entry, target, times, op) {
1265
1300
  const name = yield* claimName(entry, "rejected");
1266
1301
  yield* reserveEntry(entry.op);
1267
1302
  yield* reserveBytes(entry.op, BigInt(target.length));
1268
- const parent = directoryNow(entry.parent);
1303
+ const parent = yield* directoryNow(entry.parent);
1269
1304
  const before = parent.revision;
1270
1305
  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 } };
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 } };
1277
1313
  });
1278
1314
  const removeChild = Effect.fnUntraced(function* (parent, name, child, op) {
1279
1315
  const before = parent.revision;
1280
1316
  const now = yield* timestamp(op);
1281
- const d = current();
1282
- d.put(withEntries({
1317
+ const d = yield* Draft;
1318
+ yield* d.put(withEntries({
1283
1319
  ...parent,
1284
1320
  metadata: {
1285
1321
  ...parent.metadata,
@@ -1288,22 +1324,22 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1288
1324
  ctimeNs: now
1289
1325
  }
1290
1326
  }, (entries) => entries.delete(name)));
1291
- publishEntry("Remove", parent.ino, name, child.ino);
1292
- detach(child, parent.ino, name, now);
1293
- 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);
1294
1330
  return { before, after: d.revision };
1295
1331
  });
1296
1332
  // Authorizes removing from the entry's directory and returns the named child. Only a path can name a dot
1297
1333
  // entry, and each verb reports it with its own code. Search permission comes before the lookup and write
1298
1334
  // permission after it, as on Linux; `beforeWrite` holds the verb's own checks that Linux makes in between.
1299
1335
  const removalTarget = Effect.fnUntraced(function* (entry, dotNameCode, beforeWrite) {
1300
- const parent = directoryNow(entry.parent);
1336
+ const parent = yield* directoryNow(entry.parent);
1301
1337
  if (isDotComponent(entry.name)) {
1302
1338
  return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : dotNameCode);
1303
1339
  }
1304
1340
  yield* authorize(parent, identity, EXECUTE, entry.op);
1305
1341
  const childIno = parent.entries.get(entry.name);
1306
- const child = childIno === undefined ? undefined : view(childIno);
1342
+ const child = childIno === undefined ? undefined : (yield* view(childIno));
1307
1343
  if (child === undefined)
1308
1344
  return yield* entry.op.fail("NotFound");
1309
1345
  const rejected = beforeWrite?.(child);
@@ -1338,8 +1374,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1338
1374
  return yield* removeChild(parent, name, child, op);
1339
1375
  });
1340
1376
  const renameEntry = Effect.fnUntraced(function* (source, destination, op) {
1341
- const sourceDirectory = directoryNow(source.parent);
1342
- const destinationDirectory = directoryNow(destination.parent);
1377
+ const sourceDirectory = yield* directoryNow(source.parent);
1378
+ const destinationDirectory = yield* directoryNow(destination.parent);
1343
1379
  const sameDirectory = source.parent === destination.parent;
1344
1380
  const sourceName = source.name;
1345
1381
  const destinationName = destination.name;
@@ -1353,11 +1389,11 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1353
1389
  const sourceBefore = sourceDirectory.revision;
1354
1390
  const destinationBefore = destinationDirectory.revision;
1355
1391
  const childIno = sourceDirectory.entries.get(sourceName);
1356
- const child = childIno === undefined ? undefined : view(childIno);
1392
+ const child = childIno === undefined ? undefined : (yield* view(childIno));
1357
1393
  if (child === undefined)
1358
1394
  return yield* source.op.fail("NotFound");
1359
1395
  const replacedIno = destinationDirectory.entries.get(destinationName);
1360
- const replaced = replacedIno === undefined ? undefined : view(replacedIno);
1396
+ const replaced = replacedIno === undefined ? undefined : (yield* view(replacedIno));
1361
1397
  // A trailing slash on either side asks for a directory; a missing slashed destination is fine when the
1362
1398
  // source is one, as Linux allows.
1363
1399
  if ((source.trailingSlash || destination.trailingSlash) && child.kind !== "directory") {
@@ -1366,24 +1402,29 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1366
1402
  if (destination.trailingSlash && replaced !== undefined && replaced.kind !== "directory") {
1367
1403
  return yield* destination.op.fail("NotDirectory");
1368
1404
  }
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)) {
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)) {
1380
1421
  if (ancestor.ino === child.ino)
1381
1422
  return yield* destination.op.fail("InvalidArgument");
1382
1423
  if (ancestor.ino === ROOT_INO)
1383
1424
  break;
1384
1425
  }
1385
1426
  if (child.ino === replaced?.ino)
1386
- return result();
1427
+ return yield* result();
1387
1428
  yield* authorize(sourceDirectory, identity, WRITE, source.op);
1388
1429
  yield* authorize(destinationDirectory, identity, WRITE, destination.op);
1389
1430
  yield* authorizeRemoval(sourceDirectory, child, source.op);
@@ -1400,9 +1441,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1400
1441
  }
1401
1442
  }
1402
1443
  const now = yield* timestamp(op);
1403
- const d = current();
1404
- publishEntry("Remove", sourceDirectory.ino, sourceName, child.ino);
1405
- d.put(withEntries({
1444
+ const d = yield* Draft;
1445
+ yield* publishEntry("Remove", sourceDirectory.ino, sourceName, child.ino);
1446
+ yield* d.put(withEntries({
1406
1447
  ...sourceDirectory,
1407
1448
  metadata: {
1408
1449
  ...sourceDirectory.metadata,
@@ -1411,8 +1452,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1411
1452
  ctimeNs: now
1412
1453
  }
1413
1454
  }, (entries) => entries.delete(sourceName)));
1414
- const destinationNow = directoryNow(destination.parent);
1415
- d.put(withEntries({
1455
+ const destinationNow = yield* directoryNow(destination.parent);
1456
+ yield* d.put(withEntries({
1416
1457
  ...destinationNow,
1417
1458
  metadata: {
1418
1459
  ...destinationNow.metadata,
@@ -1421,9 +1462,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1421
1462
  ctimeNs: now
1422
1463
  }
1423
1464
  }, (entries) => entries.set(destinationName, child.ino)));
1424
- const moved = nodeNow(child.ino);
1465
+ const moved = yield* nodeNow(child.ino);
1425
1466
  if (moved.kind === "directory") {
1426
- d.put({
1467
+ yield* d.put({
1427
1468
  ...moved,
1428
1469
  parent: destination.parent,
1429
1470
  name: destinationName,
@@ -1431,7 +1472,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1431
1472
  });
1432
1473
  }
1433
1474
  else {
1434
- d.put({
1475
+ yield* d.put({
1435
1476
  ...moved,
1436
1477
  links: [
1437
1478
  ...withoutLink(moved.links, source.parent, sourceName),
@@ -1441,11 +1482,11 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1441
1482
  });
1442
1483
  }
1443
1484
  if (replaced !== undefined) {
1444
- detach(nodeNow(replaced.ino), destination.parent, destinationName, now);
1445
- d.entries -= 1;
1485
+ yield* detach(yield* nodeNow(replaced.ino), destination.parent, destinationName, now);
1486
+ yield* d.addEntries(-1);
1446
1487
  }
1447
- publishEntry("Create", destination.parent, destinationName, child.ino);
1448
- return result();
1488
+ yield* publishEntry("Create", destination.parent, destinationName, child.ino);
1489
+ return yield* result();
1449
1490
  });
1450
1491
  const openExisting = Effect.fnUntraced(function* (file, request, at, op) {
1451
1492
  yield* authorize(file, identity, request.access === "read" ? READ : request.access === "write" ? WRITE : READ | WRITE, at);
@@ -1454,7 +1495,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1454
1495
  });
1455
1496
  const openFile = Effect.fnUntraced(function* (entry, found, request, acquired, op) {
1456
1497
  const { name } = entry;
1457
- const parent = directoryNow(entry.parent);
1498
+ const parent = yield* directoryNow(entry.parent);
1458
1499
  const before = parent.revision;
1459
1500
  let file = found;
1460
1501
  let created = false;
@@ -1481,12 +1522,13 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1481
1522
  const mode = request.exactMode
1482
1523
  ? yield* permittedMode({ kind: "file", uid: owner?.uid ?? identity.uid, gid: owner?.gid ?? parent.metadata.gid }, request.mode, op)
1483
1524
  : (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;
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);
1488
1530
  created = true;
1489
- publishEntry("Create", parent.ino, name, file.ino);
1531
+ yield* publishEntry("Create", parent.ino, name, file.ino);
1490
1532
  }
1491
1533
  else {
1492
1534
  if (file.kind === "symlink")
@@ -1495,17 +1537,18 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1495
1537
  return yield* entry.op.fail("IsDirectory");
1496
1538
  yield* openExisting(file, request, entry.op, op);
1497
1539
  }
1498
- current().retain(file.ino);
1499
- bindFile(acquired, file.ino);
1500
- 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 } };
1501
1543
  });
1502
- const releasePending = (opened) => () => {
1544
+ const releasePending = (opened) => () => Effect.gen(function* () {
1503
1545
  const ino = opened();
1504
1546
  if (ino === undefined)
1505
1547
  return;
1506
- current().release(ino);
1507
- reclaim(current(), ino);
1508
- };
1548
+ const draft = yield* Draft;
1549
+ yield* draft.release(ino);
1550
+ yield* reclaim(draft, ino);
1551
+ });
1509
1552
  const acquireOpenedFile = (ref, op, acquire) => {
1510
1553
  let opened;
1511
1554
  return acquireHandle(ref, (effect) => coordinated(op, effect), acquire((ino) => {
@@ -1521,7 +1564,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1521
1564
  return yield* op.fail("InvalidHandle");
1522
1565
  if (ref.volume !== volumeIdentity)
1523
1566
  return yield* op.fail("ForeignHandle");
1524
- const node = ref.ino === undefined ? undefined : view(ref.ino);
1567
+ const node = ref.ino === undefined ? undefined : (yield* view(ref.ino));
1525
1568
  if (node === undefined)
1526
1569
  return yield* op.fail("InvalidHandle");
1527
1570
  return node;
@@ -1561,7 +1604,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1561
1604
  if (!isAttachedBytes(bytes) || bytes.length === 0 || bytes.length > MAX_NAME_BYTES || bytes.includes(0) ||
1562
1605
  bytes.includes(SLASH_BYTE))
1563
1606
  return Result.fail(op.fail("InvalidArgument"));
1564
- return Result.succeed(Encoding.encodeHex(new Uint8Array(bytes)));
1607
+ return Result.succeed(Hex.encode(new Uint8Array(bytes)));
1565
1608
  };
1566
1609
  const prepareEntry = (input, op) => isEntry(input)
1567
1610
  ? Result.map(entryName(input.name, op), (name) => ({ kind: "entry", directory: input.directory, name }))
@@ -1594,7 +1637,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1594
1637
  }),
1595
1638
  // Nothing to undo here: the finalizer that follows an interrupted acquisition releases the hold under
1596
1639
  // every permit, where a detached directory may leave the table.
1597
- () => { }, finalizeDirectory(acquired));
1640
+ () => Effect.void, finalizeDirectory(acquired));
1598
1641
  });
1599
1642
  const fail = (op, cause, field) => op.fail("InvalidArgument", field === undefined ? { cause } : { cause, field });
1600
1643
  const preparedOp = (prepared, op) => prepared.kind === "path" ? op.at(prepared.path.input) : op;
@@ -1656,8 +1699,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1656
1699
  if (node.metadata.nlink === 0)
1657
1700
  return yield* resolved.op.fail("StaleReference");
1658
1701
  yield* openExisting(node, chosen, resolved.op, op);
1659
- current().retain(node.ino);
1660
- bindFile(acquired, node.ino);
1702
+ const draft = yield* Draft;
1703
+ yield* draft.retain(node.ino);
1704
+ yield* bindFile(acquired, node.ino);
1661
1705
  opened(node.ino);
1662
1706
  return fileHandle(acquired);
1663
1707
  }));
@@ -1684,7 +1728,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1684
1728
  return yield* op.fail("InvalidArgument");
1685
1729
  yield* authorize(parent, identity, EXECUTE, op);
1686
1730
  const directIno = parent.entries.get(name);
1687
- const direct = directIno === undefined ? undefined : view(directIno);
1731
+ const direct = directIno === undefined ? undefined : (yield* view(directIno));
1688
1732
  if (expected !== undefined) {
1689
1733
  let expectedIno;
1690
1734
  if (expected !== null) {
@@ -1739,29 +1783,31 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1739
1783
  };
1740
1784
  }));
1741
1785
  });
1742
- const walkChildren = (directory, path, depth, up) => {
1786
+ const walkChildren = Effect.fnUntraced(function* (directory, path, depth, up) {
1743
1787
  const listed = referenceFor(directory.ino);
1744
- return Arr.flatMap(Arr.sort(directory.entries, byEntryName), ([name, childIno]) => {
1745
- const child = view(childIno);
1788
+ const frames = [];
1789
+ for (const [name, childIno] of Arr.sort(directory.entries, byEntryName)) {
1790
+ const child = yield* view(childIno);
1746
1791
  if (child === undefined)
1747
- return [];
1792
+ continue;
1748
1793
  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
- };
1794
+ frames.push({
1795
+ ino: child.ino,
1796
+ kind: child.kind,
1797
+ name: bytes,
1798
+ key: name,
1799
+ path: joinPath(path, bytes),
1800
+ parent: directory.ino,
1801
+ up,
1802
+ depth: depth + 1,
1803
+ bytes: child.kind === "directory" ? 0n : child.metadata.size,
1804
+ reference: referenceFor(child.ino),
1805
+ directory: listed,
1806
+ listed: false
1807
+ });
1808
+ }
1809
+ return frames;
1810
+ });
1765
1811
  const anchorFrame = (parent, key, root) => ({
1766
1812
  ino: root.ino,
1767
1813
  kind: root.kind,
@@ -1782,13 +1828,12 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1782
1828
  const prepared = prepare(target.path, op);
1783
1829
  if (Result.isFailure(prepared))
1784
1830
  return Effect.undefined;
1785
- return ResolvedEntry.fromPath(prepared.success, target.relativeTo, op).pipe(Effect.map((entry) => {
1786
- const parent = view(entry.parent);
1831
+ return ResolvedEntry.fromPath(prepared.success, target.relativeTo, op).pipe(Effect.flatMap((entry) => Effect.map(view(entry.parent), (parent) => {
1787
1832
  return entry.name !== undefined && parent?.kind === "directory" &&
1788
1833
  parent.entries.get(entry.name) === root.ino
1789
1834
  ? anchorFrame(parent, entry.name, root)
1790
1835
  : undefined;
1791
- }), Effect.catch(() => Effect.undefined));
1836
+ })), Effect.catch(() => Effect.undefined));
1792
1837
  };
1793
1838
  const holds = (parent, frame) => parent?.kind === "directory" && parent.metadata.nlink > 0 && parent.entries.get(frame.key) === frame.ino;
1794
1839
  // The node a frame names, reached by name from the walk's root as a path lookup reaches it: every directory on
@@ -1801,13 +1846,13 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1801
1846
  const chain = [];
1802
1847
  for (let link = frame; link !== undefined; link = link.up)
1803
1848
  chain.push(link);
1804
- let node = view((chain.at(-1) ?? frame).parent);
1849
+ let node = yield* view((chain.at(-1) ?? frame).parent);
1805
1850
  for (let index = chain.length - 1; index >= 0; index--) {
1806
1851
  const link = chain[index];
1807
1852
  if (link === undefined || node?.kind !== "directory" || node.metadata.nlink === 0)
1808
1853
  return undefined;
1809
1854
  yield* authorize(node, identity, EXECUTE, at);
1810
- 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;
1811
1856
  }
1812
1857
  return node;
1813
1858
  });
@@ -1828,7 +1873,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1828
1873
  const at = op.at(locate(frame.path));
1829
1874
  return Effect.flatMap(reachFrame(frame, at), (node) => node?.kind !== "directory" || node.metadata.nlink === 0
1830
1875
  ? Effect.succeed([])
1831
- : 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)));
1832
1877
  }));
1833
1878
  const exceeded = (frame, field) => makeError({ code: "LimitExceeded", operation: op.operation, field, path: errorPath(locate(frame.path)) });
1834
1879
  const admit = (frame) => {
@@ -1900,10 +1945,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1900
1945
  let targetAt = preparedOp(prepared, op);
1901
1946
  const first = coordinatedRead(op, Effect.gen(function* () {
1902
1947
  const entry = yield* resolveEntry(prepared, op);
1903
- const parent = directoryNow(entry.parent);
1948
+ const parent = yield* directoryNow(entry.parent);
1904
1949
  yield* authorize(parent, identity, EXECUTE, entry.op);
1905
1950
  const childIno = entry.name === undefined ? undefined : parent.entries.get(entry.name);
1906
- const child = childIno === undefined ? undefined : view(childIno);
1951
+ const child = childIno === undefined ? undefined : (yield* view(childIno));
1907
1952
  if (entry.name === undefined || child === undefined)
1908
1953
  return yield* entry.op.fail("NotFound");
1909
1954
  if (child.kind !== "directory")
@@ -1911,16 +1956,16 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1911
1956
  yield* listable(child, entry.op);
1912
1957
  anchor = anchorFrame(parent, entry.name, child);
1913
1958
  targetAt = entry.op;
1914
- return walkChildren(child, new Uint8Array(0), 0, anchor);
1959
+ return yield* walkChildren(child, new Uint8Array(0), 0, anchor);
1915
1960
  })).pipe(Effect.catchIf((error) => force && error.code === "NotFound", () => Effect.succeed([])));
1916
1961
  const seams = yield* VolumeTestSeams;
1917
1962
  const removeFrame = (frame) => {
1918
1963
  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"));
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")));
1922
1967
  }
1923
- 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)
1924
1969
  ? Effect.fail(at.fail("NotFound"))
1925
1970
  : removeEntry({
1926
1971
  parent: parent.ino,
@@ -1950,7 +1995,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1950
1995
  const prepared = yield* Effect.fromResult(prepareEntry(input, op));
1951
1996
  return yield* coordinatedRead(op, Effect.gen(function* () {
1952
1997
  const entry = yield* resolveEntry(prepared, op);
1953
- const directory = directoryNow(entry.parent);
1998
+ const directory = yield* directoryNow(entry.parent);
1954
1999
  if (isDotComponent(entry.name)) {
1955
2000
  return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : "NotFound");
1956
2001
  }
@@ -1997,7 +2042,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
1997
2042
  const first = coordinatedRead(op, Effect.gen(function* () {
1998
2043
  const directory = yield* resolveDirectory(target, op);
1999
2044
  yield* authorize(directory.node, identity, READ, directory.op);
2000
- 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));
2001
2046
  }));
2002
2047
  const plan = {
2003
2048
  order: chosen.order ?? "pre",
@@ -2033,7 +2078,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2033
2078
  const pathOp = op.at(target.path);
2034
2079
  const result = yield* lookup(yield* Effect.fromResult(prepare(target.path, op)), target.relativeTo, op);
2035
2080
  const directory = result.node?.kind === "directory" ? result.node : result.parent;
2036
- const prefix = directory === undefined ? SLASH_HEX : pathOf(view, directory.ino);
2081
+ const prefix = directory === undefined ? SLASH_HEX : yield* pathOfCurrent(directory.ino);
2037
2082
  if (prefix === undefined)
2038
2083
  return yield* pathOp.fail("NotFound");
2039
2084
  if (result.node?.kind === "directory" || result.name === undefined)
@@ -2041,7 +2086,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2041
2086
  return ownedPath(nameBytes(prefix + (prefix === SLASH_HEX ? "" : SLASH_HEX) + result.name));
2042
2087
  }
2043
2088
  const resolved = yield* resolveTarget(target, op);
2044
- const path = pathOf(view, resolved.node.ino);
2089
+ const path = yield* pathOfCurrent(resolved.node.ino);
2045
2090
  if (path === undefined)
2046
2091
  return yield* resolved.op.fail("NotFound");
2047
2092
  return ownedPath(nameBytes(path));
@@ -2126,7 +2171,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2126
2171
  parent = directory.node;
2127
2172
  name = prepared.name;
2128
2173
  const childIno = parent.entries.get(name);
2129
- let child = childIno === undefined ? undefined : view(childIno);
2174
+ let child = childIno === undefined ? undefined : (yield* view(childIno));
2130
2175
  if (child?.kind === "symlink" && chosen.replaceFinalSymlink !== true &&
2131
2176
  chosen.followFinalSymlink !== false) {
2132
2177
  const resolved = yield* lookup(yield* Effect.fromResult(preparePath(ownedPath(nameBytes(name)), op.operation, undefined)), undefined, op, {
@@ -2161,7 +2206,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2161
2206
  yield* authorize(parent, identity, WRITE | EXECUTE, entryOp);
2162
2207
  if (replaced !== undefined)
2163
2208
  yield* authorizeRemoval(parent, replaced, entryOp);
2164
- yield* replaced === undefined ? reserveEntry(entryOp) : reserveInode(entryOp);
2209
+ yield* (replaced === undefined ? reserveEntry(entryOp) : reserveInode(entryOp));
2165
2210
  }
2166
2211
  else {
2167
2212
  yield* authorize(file, identity, chosen.access === "readWrite" ? READ | WRITE : WRITE, entryOp);
@@ -2186,8 +2231,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2186
2231
  data.set(captured, position);
2187
2232
  }
2188
2233
  const now = yield* timestamp(op);
2189
- const d = current();
2190
- 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));
2191
2237
  const written = {
2192
2238
  ...node,
2193
2239
  data,
@@ -2199,18 +2245,18 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2199
2245
  ctimeNs: now
2200
2246
  }
2201
2247
  };
2202
- d.usedBytes += BigInt(size - previous);
2248
+ yield* d.addUsedBytes(BigInt(size - previous));
2203
2249
  if (file === undefined) {
2204
2250
  if (replaced !== undefined)
2205
- detach(replaced, parent.ino, name, now);
2206
- 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);
2207
2253
  if (replaced === undefined)
2208
- d.entries += 1;
2209
- 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);
2210
2256
  }
2211
2257
  else {
2212
- d.put(written);
2213
- publishNode(written.ino);
2258
+ yield* d.put(written);
2259
+ yield* publishNode(written.ino);
2214
2260
  }
2215
2261
  }));
2216
2262
  }),
@@ -2371,18 +2417,17 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
2371
2417
  const statOp = OpContext.make("stat");
2372
2418
  const handle = Object.freeze({
2373
2419
  [DirectoryHandleId]: true,
2374
- stat: coordinatedRead(statOp, Effect.suspend(() => {
2375
- const node = acquired.ino === undefined ? undefined : view(acquired.ino);
2376
- 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
2377
2423
  ? Effect.fail(statOp.fail("InvalidHandle"))
2378
- : Effect.succeed(withMetadata(node));
2424
+ : Effect.succeed(withMetadata(node)));
2379
2425
  })).pipe(Effect.withSpan("DirectoryHandle.stat")),
2380
2426
  close: coordinatedCleanup(Effect.suspend(() => {
2381
2427
  if (acquired.ino === undefined) {
2382
2428
  return Effect.fail(OpContext.make("close").fail("InvalidHandle"));
2383
2429
  }
2384
- releaseDirectory(acquired);
2385
- return Effect.void;
2430
+ return releaseDirectory(acquired);
2386
2431
  })).pipe(Effect.ensuring(closeReleasedScope(acquired)), Effect.withSpan("DirectoryHandle.close"))
2387
2432
  });
2388
2433
  handles.set(handle, acquired);