@effect-vfs/core 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +9 -0
  2. package/dist/BytePath.d.ts +4 -1
  3. package/dist/BytePath.d.ts.map +1 -1
  4. package/dist/BytePath.js +6 -4
  5. package/dist/Caller.d.ts +107 -66
  6. package/dist/Caller.d.ts.map +1 -1
  7. package/dist/Caller.js +16 -86
  8. package/dist/LiveVolume.d.ts +75 -2
  9. package/dist/LiveVolume.d.ts.map +1 -1
  10. package/dist/LiveVolume.js +87 -3
  11. package/dist/Search.d.ts +32 -8
  12. package/dist/Search.d.ts.map +1 -1
  13. package/dist/Search.js +19 -10
  14. package/dist/SynchronousAdapter.d.ts +58 -0
  15. package/dist/SynchronousAdapter.d.ts.map +1 -0
  16. package/dist/SynchronousAdapter.js +116 -0
  17. package/dist/Target.d.ts +7 -91
  18. package/dist/Target.d.ts.map +1 -1
  19. package/dist/Target.js +7 -47
  20. package/dist/Testing.d.ts +5 -2
  21. package/dist/Testing.d.ts.map +1 -1
  22. package/dist/Testing.js +2 -1
  23. package/dist/VfsError.d.ts +4 -2
  24. package/dist/VfsError.d.ts.map +1 -1
  25. package/dist/VfsError.js +3 -1
  26. package/dist/VirtualFileSystem.d.ts +151 -44
  27. package/dist/VirtualFileSystem.d.ts.map +1 -1
  28. package/dist/VirtualFileSystem.js +31 -21
  29. package/dist/Volume.d.ts +6 -1
  30. package/dist/Volume.d.ts.map +1 -1
  31. package/dist/Volume.js +6 -2
  32. package/dist/Watch.d.ts +18 -0
  33. package/dist/Watch.d.ts.map +1 -1
  34. package/dist/Watch.js +12 -0
  35. package/dist/internal/adapterBinding.d.ts +13 -0
  36. package/dist/internal/adapterBinding.d.ts.map +1 -0
  37. package/dist/internal/adapterBinding.js +13 -0
  38. package/dist/internal/bytePath.d.ts +6 -1
  39. package/dist/internal/bytePath.d.ts.map +1 -1
  40. package/dist/internal/callerWatch.d.ts +36 -0
  41. package/dist/internal/callerWatch.d.ts.map +1 -0
  42. package/dist/internal/callerWatch.js +185 -0
  43. package/dist/internal/engine.d.ts +1305 -0
  44. package/dist/internal/engine.d.ts.map +1 -0
  45. package/dist/internal/engine.js +3458 -0
  46. package/dist/internal/errors.d.ts +1 -33
  47. package/dist/internal/errors.d.ts.map +1 -1
  48. package/dist/internal/errors.js +1 -45
  49. package/dist/internal/fixture.d.ts +2 -1
  50. package/dist/internal/fixture.d.ts.map +1 -1
  51. package/dist/internal/fixture.js +8 -4
  52. package/dist/internal/image.d.ts +4 -1
  53. package/dist/internal/image.d.ts.map +1 -1
  54. package/dist/internal/image.js +6 -3
  55. package/dist/internal/lines.d.ts.map +1 -1
  56. package/dist/internal/lines.js +3 -2
  57. package/dist/internal/liveImage.d.ts +4 -2
  58. package/dist/internal/liveImage.d.ts.map +1 -1
  59. package/dist/internal/liveImage.js +3 -1
  60. package/dist/internal/merkle.d.ts +7 -4
  61. package/dist/internal/merkle.d.ts.map +1 -1
  62. package/dist/internal/merkle.js +6 -4
  63. package/dist/internal/search.d.ts +3 -1
  64. package/dist/internal/search.d.ts.map +1 -1
  65. package/dist/internal/search.js +4 -3
  66. package/dist/internal/snapshotDelta.d.ts +11 -7
  67. package/dist/internal/snapshotDelta.d.ts.map +1 -1
  68. package/dist/internal/snapshotDelta.js +15 -9
  69. package/dist/internal/snapshotMerge.d.ts +1 -1
  70. package/dist/internal/snapshotMerge.d.ts.map +1 -1
  71. package/dist/internal/snapshotMerge.js +5 -2
  72. package/dist/internal/tree.d.ts +4 -2
  73. package/dist/internal/tree.d.ts.map +1 -1
  74. package/dist/internal/tree.js +4 -2
  75. package/dist/internal/virtualFileSystem.d.ts +20 -49
  76. package/dist/internal/virtualFileSystem.d.ts.map +1 -1
  77. package/dist/internal/virtualFileSystem.js +85 -1927
  78. package/dist/internal/volumeLimits.d.ts +1 -0
  79. package/dist/internal/volumeLimits.d.ts.map +1 -1
  80. package/dist/internal/volumeLimits.js +3 -2
  81. package/dist/internal/volumeState.d.ts +10 -79
  82. package/dist/internal/volumeState.d.ts.map +1 -1
  83. package/dist/internal/volumeState.js +9 -79
  84. package/dist/internal/watchHub.d.ts +9 -5
  85. package/dist/internal/watchHub.d.ts.map +1 -1
  86. package/dist/internal/watchHub.js +35 -8
  87. package/package.json +16 -9
  88. package/dist/internal/inodeTable.d.ts +0 -20
  89. package/dist/internal/inodeTable.d.ts.map +0 -1
  90. package/dist/internal/inodeTable.js +0 -48
  91. package/dist/internal/path.d.ts +0 -56
  92. package/dist/internal/path.d.ts.map +0 -1
  93. package/dist/internal/path.js +0 -141
  94. package/dist/internal/resolution.d.ts +0 -135
  95. package/dist/internal/resolution.d.ts.map +0 -1
  96. package/dist/internal/resolution.js +0 -277
  97. package/dist/internal/testSeams.d.ts +0 -17
  98. package/dist/internal/testSeams.d.ts.map +0 -1
  99. package/dist/internal/testSeams.js +0 -20
  100. package/dist/internal/tokenRegistry.d.ts +0 -77
  101. package/dist/internal/tokenRegistry.d.ts.map +0 -1
  102. package/dist/internal/tokenRegistry.js +0 -128
  103. package/dist/internal/turnstile.d.ts +0 -8
  104. package/dist/internal/turnstile.d.ts.map +0 -1
  105. package/dist/internal/turnstile.js +0 -43
@@ -1,228 +1,46 @@
1
- import * as Arr from "effect/Array";
1
+ /**
2
+ * Community volume assembly over the shared private filesystem engine.
3
+ * IDs, authenticated keys, snapshots, overlays, storage encoders and watch delivery stay here.
4
+ * @internal
5
+ */
2
6
  import * as ByteSize from "effect/ByteSize";
3
- import * as Cause from "effect/Cause";
4
7
  import * as Clock from "effect/Clock";
5
- import * as Context from "effect/Context";
6
8
  import * as Crypto from "effect/Crypto";
7
9
  import * as Data from "effect/Data";
8
10
  import * as Effect from "effect/Effect";
9
11
  import * as Hex from "effect/encoding/Hex";
10
- import * as Exit from "effect/Exit";
11
- import * as Match from "effect/Match";
12
- import * as MutableRef from "effect/MutableRef";
13
- import * as Option from "effect/Option";
14
12
  import * as Predicate from "effect/Predicate";
15
13
  import * as Result from "effect/Result";
16
14
  import * as Schema from "effect/Schema";
17
- import * as Semaphore from "effect/Semaphore";
18
15
  import * as Stream from "effect/Stream";
19
16
  import { BytePath } from "../BytePath.js";
20
- import { CallerId, MkdirOptions, OpenEntryOptions, OpenOptions, RemoveOptions, RootCallerOptions, SetattrOptions, SymlinkOptions, WalkOptions, WriteFileOptions } from "../Caller.js";
21
- import { DirectoryHandleId, FileHandleId, SeekMode } from "../FileHandle.js";
22
- import { Mode, OwnerUpdate, Times } from "../Metadata.js";
23
- import { isEntry, Target } from "../Target.js";
24
- import { make as makeError } from "../VfsError.js";
17
+ import { CallerId } from "../Caller.js";
18
+ import { make as makeError, VfsError } from "../VfsError.js";
25
19
  import { OverlayChange, OverlayChangesOptions, ReferenceKey, VolumeId, VolumeIdentity, VolumeIncarnation } from "../Volume.js";
26
- import { WatchOptions } from "../Watch.js";
20
+ import { CallerWatchOptions, WatchOptions } from "../Watch.js";
21
+ import * as AdapterBinding from "./adapterBinding.js";
27
22
  import { sameBytes } from "./bytes.js";
28
- import { argumentFailure, decodeConfiguration, errorPath, fsFailure, imageFailure, OpContext, retargetFailure, VfsError } from "./errors.js";
23
+ import * as CallerWatch from "./callerWatch.js";
24
+ import * as Engine from "./engine.js";
25
+ import { imageFailure, retargetFailure } from "./errors.js";
29
26
  import { KeySecret, VolumeEpoch } from "./hex128.js";
30
27
  import { hmacSha256, sameTag } from "./hmac.js";
31
28
  import * as Image from "./image.js";
32
- import * as InodeTable from "./inodeTable.js";
33
29
  import * as LiveImage from "./liveImage.js";
34
30
  import * as MetadataDomain from "./metadata.js";
35
31
  import { compareOverlay } from "./overlayDiff.js";
36
- import { inputBytes, isAttachedBytes, isDotComponent, joinPath, nameBytes, ownedPath, ROOT_PATH, SLASH_BYTE, SLASH_HEX } from "./path.js";
37
- import * as Resolution from "./resolution.js";
38
- import { VolumeTestSeams } from "./testSeams.js";
39
- import * as TokenRegistry from "./tokenRegistry.js";
40
- import { makeTurnstile } from "./turnstile.js";
32
+ import { Errors, Paths, State, VolumeTestSeams } from "./engine.js";
41
33
  import * as Limits from "./volumeLimits.js";
42
- import { byEntryName, directoryMetadata, emptyState, getNode, Ino, reachableValue, ROOT_INO, storedMetadata, WALK_YIELD_INTERVAL } from "./volumeState.js";
34
+ import { reachableValue, storedMetadata } from "./volumeState.js";
43
35
  import * as WatchHub from "./watchHub.js";
44
- const EXECUTE = 0o1;
45
- const WRITE = 0o2;
46
- const READ = 0o4;
47
- const ANY_EXECUTE = 0o111;
48
- const SET_ID_BITS = 0o6000;
49
- const STICKY_BIT = 0o1000;
50
- const RELATIME_INTERVAL_NS = 86400000000000n; // relatime, as Linux mounts by default: a read refreshes an access time at least this old, 24 hours.
36
+ const { argumentFailure, decodeConfiguration, OpContext } = Errors;
37
+ const { joinPath, nameBytes, ownedPath, ROOT_PATH, SLASH_BYTE } = Paths;
38
+ const { emptyState, entryPath, getNode, Ino, pathOf, ROOT_INO, WALK_YIELD_INTERVAL } = State;
51
39
  /** @internal */
52
- export const memoryCommitProvider = Object.freeze({
53
- mode: "memory",
54
- commit: () => Effect.succeed("committed"),
55
- shutdown: Effect.void
56
- });
57
- // An unknown commit outcome stops the volume; definite rejection stops it only during cleanup.
58
- const offerCommit = (provider, operation, candidate, cleanup) => Effect.map(Effect.exit(Effect.suspend(() => provider.commit(candidate))), (committed) => {
59
- // TODO(#186): Preserve the cause of a provider defect or interruption.
60
- if (Exit.isFailure(committed)) {
61
- if (Cause.hasDies(committed.cause) || Cause.hasInterrupts(committed.cause)) {
62
- return { available: false, failure: fsFailure("OutcomeUnknown", operation) };
63
- }
64
- const failure = Cause.findErrorOption(committed.cause);
65
- return Option.isSome(failure)
66
- ? { available: !cleanup, failure: failure.value }
67
- : { available: false, failure: fsFailure("OutcomeUnknown", operation) };
68
- }
69
- if (committed.value === "rejected") {
70
- return { available: !cleanup, failure: fsFailure("StorageRejected", operation) };
71
- }
72
- if (committed.value === "unknown") {
73
- return { available: false, failure: fsFailure("OutcomeUnknown", operation) };
74
- }
75
- return { available: true, failure: undefined };
76
- });
77
- const WALK_CHUNK_ENTRIES = 128;
78
- const MAX_FILE_OFFSET = 0x7fffffffffffffffn;
79
- const isMode = Schema.is(Mode);
80
- const isNatural = Schema.is(Schema.Natural);
81
- const isLength = Schema.is(SetattrOptions.fields.size.schema);
82
- const inGroup = Resolution.inGroup;
40
+ export const memoryCommitProvider = Engine.memoryCommitProvider;
83
41
  const isTimestamp = Schema.is(MetadataDomain.Timestamp);
84
- const isSeekMode = Schema.is(SeekMode);
85
- const decodeOwnerUpdate = Schema.decodeEffect(OwnerUpdate, { onExcessProperty: "error" });
86
- const decodeTimes = Schema.decodeEffect(Times, { onExcessProperty: "error" });
87
- const decodeExpected = Schema.decodeEffect(SetattrOptions.fields.expected.schema, { onExcessProperty: "error" });
88
- const SETATTR_FIELDS = Object.keys(SetattrOptions.fields);
89
- 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);
90
- const decodeWriteFileOptions = Schema.decodeEffect(WriteFileOptions, { onExcessProperty: "error" });
91
- const decodeOpenOptions = Schema.decodeEffect(OpenOptions, { onExcessProperty: "error" });
92
- const decodeMkdirOptions = Schema.decodeEffect(MkdirOptions, { onExcessProperty: "error" });
93
- const decodeSymlinkOptions = Schema.decodeEffect(SymlinkOptions, { onExcessProperty: "error" });
94
- const decodeWalkOptions = Schema.decodeEffect(WalkOptions, { onExcessProperty: "error" });
95
- const decodeRemoveOptions = Schema.decodeEffect(RemoveOptions, { onExcessProperty: "error" });
96
- const decodeOpenEntryOptions = Schema.decodeEffect(OpenEntryOptions, { onExcessProperty: "error" });
97
- // Whether a read at `now` refreshes a node's access time: when it is not newer than the last modification or
98
- // status change, or when it is at least a day old. A time already equal to `now` is never refreshed, as Linux
99
- // skips it, so reads at one instant (a frozen or coarse clock) store nothing.
100
- const accessDue = (metadata, now) => now !== metadata.atimeNs && (metadata.atimeNs <= metadata.mtimeNs || metadata.atimeNs <= metadata.ctimeNs ||
101
- now - metadata.atimeNs >= RELATIME_INTERVAL_NS);
102
- const refreshDue = (read) => read.access !== undefined && accessDue(read.access.node.metadata, read.access.now);
103
- class Draft extends Context.Service()("@effect-vfs/core/internal/virtualFileSystem/Draft", {
104
- make: Effect.fnUntraced(function* (base) {
105
- const revision = base.revision + 1n;
106
- const cell = MutableRef.make({
107
- owner: Symbol(),
108
- pending: new Map(),
109
- opens: undefined,
110
- nextInode: base.nextInode,
111
- changed: false,
112
- finished: undefined,
113
- entries: base.entries,
114
- usedBytes: base.usedBytes,
115
- events: [],
116
- after: [],
117
- removed: []
118
- });
119
- const get = (ino) => Effect.sync(() => {
120
- const draft = MutableRef.get(cell);
121
- return draft.pending.has(ino) ? draft.pending.get(ino) : InodeTable.get(base.inodes, ino);
122
- });
123
- const openCount = (ino) => Effect.sync(() => MutableRef.get(cell).opens?.get(ino) ?? base.open.get(ino) ?? 0);
124
- const put = (node, changed) => Effect.sync(() => {
125
- MutableRef.update(cell, (draft) => {
126
- // This map belongs only to this transition. Copying it on every inode write makes recursive changes quadratic.
127
- draft.pending.set(node.ino, changed ? { ...node, revision } : node);
128
- return { ...draft, changed: draft.changed || changed };
129
- });
130
- });
131
- return {
132
- revision,
133
- get,
134
- put: (node) => put(node, true),
135
- putQuiet: (node) => put(node, false),
136
- unchanged: Effect.sync(() => {
137
- const draft = MutableRef.get(cell);
138
- return draft.pending.size === 0 && draft.opens === undefined && draft.nextInode === base.nextInode &&
139
- draft.entries === base.entries && draft.usedBytes === base.usedBytes;
140
- }),
141
- remove: (ino) => Effect.sync(() => {
142
- const draft = MutableRef.get(cell);
143
- draft.pending.set(ino, undefined);
144
- draft.removed.push(ino);
145
- }),
146
- allocate: Effect.sync(() => {
147
- const next = MutableRef.get(cell).nextInode;
148
- MutableRef.update(cell, (draft) => ({ ...draft, nextInode: Ino(next + 1) }));
149
- return next;
150
- }),
151
- canAllocate: Effect.sync(() => MutableRef.get(cell).nextInode < Number.MAX_SAFE_INTEGER),
152
- openCount,
153
- retain: (ino) => Effect.sync(() => {
154
- const count = MutableRef.get(cell).opens?.get(ino) ?? base.open.get(ino) ?? 0;
155
- MutableRef.update(cell, (draft) => ({ ...draft, opens: new Map(draft.opens).set(ino, count + 1) }));
156
- }),
157
- release: (ino) => Effect.sync(() => {
158
- const count = (MutableRef.get(cell).opens?.get(ino) ?? base.open.get(ino) ?? 0) - 1;
159
- MutableRef.update(cell, (draft) => ({ ...draft, opens: new Map(draft.opens).set(ino, Math.max(0, count)) }));
160
- return count;
161
- }),
162
- entries: Effect.sync(() => MutableRef.get(cell).entries),
163
- addEntries: (amount) => Effect.sync(() => {
164
- MutableRef.update(cell, (draft) => ({ ...draft, entries: draft.entries + amount }));
165
- }),
166
- usedBytes: Effect.sync(() => MutableRef.get(cell).usedBytes),
167
- addUsedBytes: (amount) => Effect.sync(() => {
168
- MutableRef.update(cell, (draft) => ({ ...draft, usedBytes: draft.usedBytes + amount }));
169
- }),
170
- publish: (event) => Effect.sync(() => {
171
- MutableRef.get(cell).events.push(event);
172
- }),
173
- afterInstall: (action) => Effect.sync(() => {
174
- MutableRef.get(cell).after.push(action);
175
- }),
176
- finish: Effect.sync(() => {
177
- const draft = MutableRef.get(cell);
178
- if (draft.finished !== undefined)
179
- return draft.finished;
180
- let inodes = base.inodes;
181
- for (const [ino, node] of draft.pending)
182
- inodes = InodeTable.set(inodes, ino, node, draft.owner);
183
- let open = base.open;
184
- if (draft.opens !== undefined) {
185
- const next = new Map(base.open);
186
- for (const [ino, count] of draft.opens) {
187
- if (count > 0)
188
- next.set(ino, count);
189
- else
190
- next.delete(ino);
191
- }
192
- open = next;
193
- }
194
- const finished = {
195
- inodes,
196
- open,
197
- nextInode: draft.nextInode,
198
- revision: draft.changed ? revision : base.revision,
199
- entries: draft.entries,
200
- usedBytes: draft.usedBytes
201
- };
202
- MutableRef.update(cell, (current) => ({ ...current, finished }));
203
- return finished;
204
- }),
205
- actions: Effect.sync(() => {
206
- const { events, after, removed } = MutableRef.get(cell);
207
- return { events, after, removed };
208
- })
209
- };
210
- })
211
- }) {
212
- }
213
- const withMetadata = (node) => ({ ...node.metadata, revision: node.revision });
214
- const withEntries = (directory, edit) => {
215
- const entries = new Map(directory.entries);
216
- edit(entries);
217
- return { ...directory, entries };
218
- };
219
- const withoutLink = (links, parent, name) => {
220
- const index = links.findIndex((link) => link.parent === parent && link.name === name);
221
- return index < 0 ? links : [...links.slice(0, index), ...links.slice(index + 1)];
222
- };
223
42
  /** @internal */
224
43
  export const VolumeSource = Data.taggedEnum();
225
- const UpdateChange = Schema.TaggedStruct("Update", { path: BytePath });
226
44
  const RescanChange = Schema.TaggedStruct("Rescan", { path: BytePath });
227
45
  const RemoveChange = Schema.TaggedStruct("Remove", { path: BytePath });
228
46
  const observeChanges = Effect.fnUntraced(function* (captured) {
@@ -253,16 +71,17 @@ const observeChanges = Effect.fnUntraced(function* (captured) {
253
71
  return observation;
254
72
  });
255
73
  const randomHex128 = Effect.gen(function* () {
74
+ // TODO: (#304) Use SecureRandom for construction once its service is released; retain 16-byte IDs.
256
75
  const crypto = yield* Crypto.Crypto;
257
76
  return Hex.encode(yield* crypto.randomBytes(16));
258
77
  }).pipe(Effect.orDie);
259
78
  /** @internal */
260
79
  export const makeVolume = Effect.fnUntraced(function* (source, commitProvider, options, durability = "memory-only") {
261
- const services = yield* Effect.context();
262
- // These accessors run inside synchronous transition callbacks. The runner is reached only when an invariant fails.
263
- const dieInvariant = (message) => Effect.runSyncWith(services)(Effect.die(message));
264
80
  const live = Predicate.isTagged("Live")(source) ? source.restored : undefined;
265
81
  const decoded = yield* Effect.fromResult(Limits.configuration(options));
82
+ if (decoded.options.execution === "sync" && commitProvider !== memoryCommitProvider) {
83
+ return yield* argumentFailure("make", "execution");
84
+ }
266
85
  const requested = decoded.limits;
267
86
  const limits = live === undefined ? requested : yield* Effect.fromResult(Limits.fromStored(live.limits, requested));
268
87
  const identity = live?.identity ?? (decoded.identity === undefined
@@ -279,32 +98,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, commitProvider, o
279
98
  if (!isTimestamp(initialTime)) {
280
99
  return yield* argumentFailure("make", "clock.currentTimeNanos");
281
100
  }
282
- const timestamp = (op) => Effect.suspend(() => {
283
- const now = clock.currentTimeNanosUnsafe();
284
- return isTimestamp(now)
285
- ? Effect.succeed(now)
286
- : Effect.fail(op.fail("InvalidArgument"));
287
- });
288
- const volumeIdentity = Symbol();
289
- const directoryHolds = new Map();
290
- const maxPendingOperations = limits.maxPendingOperations;
291
- const permits = maxPendingOperations + 1;
292
- const gate = Semaphore.makeUnsafe(permits);
293
- // The gate hands permits to whichever waiter it can satisfy, so a change waiting for every permit would be
294
- // overtaken by each later observation. A change holds a turnstile while it gathers the permits, and an
295
- // observation passes the turnstile before it takes one, so observations that arrive after a change wait
296
- // behind it. The turnstile goes to its waiters in arrival order, even to one that arrives while a finished
297
- // change is still releasing. Both waits stay interruptible, and every permit is taken and released by
298
- // `withPermits`.
299
- const turnstile = makeTurnstile();
300
- const observing = (effect) => Effect.andThen(turnstile.withTurn(Effect.void), gate.withPermits(1)(effect));
301
- const changing = (effect) => turnstile.withTurn(gate.withPermits(permits)(effect));
302
- const admission = yield* Semaphore.make(permits);
303
- const admit = (op, effect) => admission.withPermitsIfAvailable(1)(effect).pipe(Effect.flatMap(Option.match({
304
- onNone: () => op.fail("VolumeBusy"),
305
- onSome: Effect.succeed
306
- })));
307
- let state = emptyState(initialTime);
101
+ let state;
308
102
  const maxFileBytes = Number(ByteSize.toBigInt(limits.maxFileBytes));
309
103
  if (Predicate.isTagged("Restored")(source)) {
310
104
  const restored = yield* reachableValue(source.value);
@@ -319,142 +113,17 @@ export const makeVolume = Effect.fnUntraced(function* (source, commitProvider, o
319
113
  }
320
114
  if (live !== undefined)
321
115
  state = live.value;
322
- const view = (ino) => Effect.flatMap(Effect.serviceOption(Draft), (draft) => Option.isSome(draft) ? draft.value.get(ino) : Effect.sync(() => getNode(state, ino)));
323
- const registry = TokenRegistry.make(volumeIdentity, view);
324
- const referenceFor = registry.referenceFor;
325
- const makeFileReference = registry.file;
326
- const makeDirectoryReference = registry.directory;
327
- let available = true;
328
- const checkAvailable = (operation) => Effect.suspend(() => available ? Effect.void : Effect.fail(fsFailure("VolumeUnavailable", operation)));
329
- const watchCoordinate = (effect) => changing(effect);
330
- const watchHub = yield* WatchHub.make(watchCoordinate, limits.maxWatchEvents);
331
- const install = Effect.fnUntraced(function* (finished) {
332
- const before = state;
333
- state = yield* finished.finish;
334
- const actions = yield* finished.actions;
335
- for (const ino of actions.removed)
336
- registry.forget(ino);
337
- for (const apply of actions.after)
338
- apply();
339
- const after = state;
340
- watchHub.publishUnsafe(() => actions.events.flatMap((events) => Array.from(events(after))), { before, after });
341
- });
342
- const transition = Effect.fnUntraced(function* (change) {
343
- const current = yield* Draft.make(state);
344
- const value = yield* Effect.provideService(change, Draft, current);
345
- return [value, current];
346
- });
347
- // Cleanup releases local ownership even when storage refuses; that divergence disables the volume.
348
- const committed = Effect.fnUntraced(function* (op, finished, cleanup) {
349
- if ((yield* finished.unchanged) || (cleanup && !available))
350
- return yield* install(finished);
351
- const next = yield* finished.finish;
352
- const answer = yield* offerCommit(commitProvider, op.operation, next, cleanup);
353
- if (!answer.available)
354
- available = false;
355
- if (answer.failure !== undefined) {
356
- if (cleanup)
357
- yield* install(finished);
358
- return yield* answer.failure;
359
- }
360
- yield* install(finished);
361
- });
362
- const applyCleanup = Effect.fnUntraced(function* (change) {
363
- const current = yield* Draft.make(state);
364
- yield* Effect.provideService(change(current), Draft, current);
365
- yield* committed(OpContext.make("close"), current, true);
366
- });
367
- const annotateFailure = (error) => Effect.annotateCurrentSpan({ operation: error.operation, code: error.code });
368
- // Permit waits stay interruptible. Memory transitions are masked; durable transitions remain
369
- // interruptible until the candidate is ready. Every provider commit and installation is masked.
370
- const coordinated = (op, effect, cleanup = false) => admit(op, changing(Effect.uninterruptibleMask((restore) => {
371
- const staged = transition(Effect.andThen(checkAvailable(op.operation), effect));
372
- return Effect.flatMap(commitProvider.mode === "memory" ? staged : restore(staged), ([value, finished]) => Effect.as(committed(op, finished, cleanup), value));
373
- }))).pipe(Effect.tapError((error) => Schema.is(VfsError)(error) ? annotateFailure(error) : Effect.void));
374
- const coordinatedRead = (op, effect) => admit(op, observing(Effect.andThen(checkAvailable(op.operation), effect))).pipe(Effect.tapError((error) => Schema.is(VfsError)(error) ? annotateFailure(error) : Effect.void));
375
- const refreshAccess = Effect.fnUntraced(function* (read) {
376
- if (refreshDue(read)) {
377
- const { node, now } = read.access;
378
- const draft = yield* Draft;
379
- yield* draft.putQuiet({ ...node, metadata: { ...node.metadata, atimeNs: now } });
380
- }
381
- return read.value;
382
- });
383
- // A read that may refresh an access time. It observes under one permit, so reads run beside each other, and
384
- // only when the access time is due does it run again as a change. The change repeats every check and the rule,
385
- // since another read may have refreshed the time meanwhile, and its result is the one returned.
386
- const accessing = (op, read) => Effect.flatMap(coordinatedRead(op, read), (observed) => refreshDue(observed)
387
- ? coordinated(op, Effect.flatMap(read, refreshAccess))
388
- : Effect.succeed(observed.value));
389
- const coordinatedCleanup = (effect) => changing(Effect.uninterruptible(effect));
390
- const shutdown = commitProvider.mode === "memory" ? commitProvider.shutdown : changing(Effect.uninterruptible(Effect.andThen(Effect.sync(() => {
391
- available = false;
392
- }), commitProvider.shutdown)));
393
- const pathOf = (get, ino) => {
394
- const names = [];
395
- let node = get(ino);
396
- if (node?.kind !== "directory" || node.metadata.nlink === 0)
397
- return undefined;
398
- while (node.ino !== ROOT_INO) {
399
- names.push(node.name);
400
- const parent = get(node.parent);
401
- if (parent?.kind !== "directory" || parent.metadata.nlink === 0)
402
- return undefined;
403
- node = parent;
404
- }
405
- return SLASH_HEX + names.reverse().join(SLASH_HEX);
406
- };
407
- const pathOfCurrent = Effect.fnUntraced(function* (ino) {
408
- const names = [];
409
- const first = yield* view(ino);
410
- if (first?.kind !== "directory" || first.metadata.nlink === 0)
411
- return undefined;
412
- let node = first;
413
- while (node.ino !== ROOT_INO) {
414
- names.push(node.name);
415
- const parent = yield* view(node.parent);
416
- if (parent?.kind !== "directory" || parent.metadata.nlink === 0)
417
- return undefined;
418
- node = parent;
419
- }
420
- return SLASH_HEX + names.reverse().join(SLASH_HEX);
421
- });
422
- const entryPath = (prefix, name) => ownedPath(nameBytes(prefix + (prefix === SLASH_HEX ? "" : SLASH_HEX) + name));
423
- const publishEntry = (_tag, parent, name, ino) => Effect.flatMap(Draft, (draft) => draft.publish((installed) => {
424
- const prefix = pathOf((at) => getNode(installed, at), parent);
425
- return prefix === undefined ? [] : [{ change: { _tag, path: entryPath(prefix, name) }, parent, ino }];
426
- }));
427
- const publishNode = Effect.fnUntraced(function* (ino) {
428
- const draft = yield* Draft;
429
- const target = yield* draft.get(ino);
430
- if (target === undefined)
431
- return;
432
- if (target.kind === "directory") {
433
- yield* draft.publish((installed) => {
434
- const directory = getNode(installed, ino);
435
- const path = pathOf((at) => getNode(installed, at), ino);
436
- return path === undefined || directory?.kind !== "directory"
437
- ? []
438
- : [{ change: UpdateChange.make({ path: ownedPath(nameBytes(path)) }), parent: directory.parent, ino }];
439
- });
440
- return;
441
- }
442
- const links = target.links;
443
- yield* draft.publish((installed) => {
444
- const events = [];
445
- for (const link of links) {
446
- const prefix = pathOf((at) => getNode(installed, at), link.parent);
447
- if (prefix !== undefined) {
448
- events.push({
449
- change: UpdateChange.make({ path: entryPath(prefix, link.name) }),
450
- parent: link.parent,
451
- ino
452
- });
453
- }
454
- }
455
- return events;
456
- });
116
+ let watchHub;
117
+ const { maxWatchEvents, ...basicLimits } = limits;
118
+ const engine = yield* Engine.makeEngine({
119
+ execution: decoded.options.execution,
120
+ limits: basicLimits,
121
+ initialTime,
122
+ initialState: state,
123
+ commitProvider,
124
+ installed: (events, installation) => watchHub.publishUnsafe(events, installation)
457
125
  });
126
+ watchHub = yield* WatchHub.make(engine.coordinate, maxWatchEvents, engine.execution === "sync");
458
127
  const pathsOf = (installed, ino) => {
459
128
  const node = getNode(installed, ino);
460
129
  if (node === undefined)
@@ -488,10 +157,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, commitProvider, o
488
157
  includes: () => true,
489
158
  rescan: () => ({ change: RescanChange.make({ path: rootPath() }), parent: ROOT_INO, ino: ROOT_INO })
490
159
  };
491
- const scopeRoot = Effect.fnUntraced(function* (scope, op) {
492
- const node = yield* TokenRegistry.nodeOrFail(yield* registry.resolve(scope, "reference"), "reference", op);
493
- return node.metadata.nlink === 0 ? yield* op.fail("StaleReference") : node.ino;
494
- });
160
+ const scopeRoot = engine.watchRoot;
495
161
  // A watch of one object, and of its subtree when recursive. Membership is read from the installed tree when
496
162
  // each event is offered, so it follows renames of the scope and its ancestors. When the object's last name is
497
163
  // gone the watch reports its removal from the names it had and ends; that Remove is held back for the end of
@@ -517,12 +183,15 @@ export const makeVolume = Effect.fnUntraced(function* (source, commitProvider, o
517
183
  : undefined
518
184
  };
519
185
  };
520
- const captureState = Effect.fnUntraced(function* () {
521
- const captured = state;
186
+ // Cooperative capture retains its established ordering; sync summaries run after immutable capture.
187
+ const capture = (operation, read) => engine.cooperativeCapture === undefined
188
+ ? Effect.flatMap(engine.capture(operation), read)
189
+ : engine.cooperativeCapture(operation, read);
190
+ const captureState = capture("capture", Effect.fnUntraced(function* (captured) {
522
191
  const snapshot = Image.make(captured);
523
192
  yield* (yield* VolumeTestSeams).betweenSnapshotAndSummary;
524
193
  return { snapshot, observation: yield* observeChanges(captured) };
525
- });
194
+ }));
526
195
  const keyTag = (ino) => {
527
196
  const message = new Uint8Array(40);
528
197
  message.set(identityBytes);
@@ -530,1538 +199,32 @@ export const makeVolume = Effect.fnUntraced(function* (source, commitProvider, o
530
199
  new DataView(message.buffer).setBigUint64(32, ino);
531
200
  return hmacSha256(keySecretBytes, message).subarray(0, 16);
532
201
  };
533
- const holdDirectory = (ino) => {
534
- directoryHolds.set(ino, (directoryHolds.get(ino) ?? 0) + 1);
535
- };
536
- const unholdDirectory = Effect.fnUntraced(function* (ino) {
537
- const count = (directoryHolds.get(ino) ?? 1) - 1;
538
- if (count > 0) {
539
- directoryHolds.set(ino, count);
540
- return;
541
- }
542
- directoryHolds.delete(ino);
543
- const node = getNode(state, ino);
544
- if (node?.kind === "directory" && node.metadata.nlink === 0) {
545
- yield* applyCleanup((d) => d.remove(ino));
546
- }
547
- });
548
- const releaseDirectory = (reference) => TokenRegistry.release(reference, unholdDirectory, (action) => Effect.sync(action));
549
- const finalizeDirectory = (reference) => {
550
- const close = coordinatedCleanup(releaseDirectory(reference));
551
- return TokenRegistry.finalize(reference, close, Effect.ignore(close));
552
- };
553
- const closeReleasedScope = TokenRegistry.closeReleasedScope;
554
- const permitted = Resolution.permitted;
555
- const authorize = Resolution.authorize;
556
- const reclaim = Effect.fnUntraced(function* (d, ino) {
557
- const node = yield* d.get(ino);
558
- if (node === undefined)
559
- return;
560
- if (node.kind === "file") {
561
- if (node.links.length > 0 || (yield* d.openCount(ino)) > 0)
562
- return;
563
- yield* d.addUsedBytes(-BigInt(node.data.length));
564
- }
565
- else if (node.kind === "symlink") {
566
- if (node.links.length > 0)
567
- return;
568
- yield* d.addUsedBytes(-BigInt(node.target.length));
569
- }
570
- else if (node.metadata.nlink > 0 || directoryHolds.has(ino))
571
- return;
572
- yield* d.remove(ino);
573
- });
574
- const atEntryLimit = Effect.gen(function* () {
575
- const draft = yield* Draft;
576
- return limits.maxEntries !== undefined && (yield* draft.entries) >= limits.maxEntries;
577
- });
578
- const reserveEntry = Effect.fnUntraced(function* (op) {
579
- const draft = yield* Draft;
580
- if ((yield* atEntryLimit) || !(yield* draft.canAllocate))
581
- return yield* op.fail("NoSpace");
582
- });
583
- const reserveInode = (op) => Effect.flatMap(Draft, (draft) => Effect.flatMap(draft.canAllocate, (available) => available ? Effect.void : Effect.fail(op.fail("NoSpace"))));
584
- const reserveBytes = Effect.fnUntraced(function* (op, bytes) {
585
- if (limits.maxBytes === undefined)
586
- return;
587
- const draft = yield* Draft;
588
- if (bytes > ByteSize.toBigInt(limits.maxBytes) - (yield* draft.usedBytes)) {
589
- return yield* op.fail("NoSpace");
590
- }
591
- });
592
- const attach = Effect.fnUntraced(function* (parent, name, child, now) {
593
- const d = yield* Draft;
594
- yield* d.put(withEntries({
595
- ...parent,
596
- metadata: {
597
- ...parent.metadata,
598
- nlink: parent.metadata.nlink + (child.kind === "directory" ? 1 : 0),
599
- mtimeNs: now,
600
- ctimeNs: now
601
- }
602
- }, (entries) => entries.set(name, child.ino)));
603
- if (child.kind === "directory")
604
- yield* d.put({ ...child, parent: parent.ino, name });
605
- else {
606
- yield* d.put({
607
- ...child,
608
- links: [...child.links, { parent: parent.ino, name }],
609
- metadata: { ...child.metadata, nlink: child.links.length + 1, ctimeNs: now }
610
- });
611
- }
612
- });
613
- const detach = Effect.fnUntraced(function* (child, parent, name, now) {
614
- const d = yield* Draft;
615
- if (child.kind === "directory") {
616
- yield* d.put({ ...child, metadata: { ...child.metadata, nlink: 0, ctimeNs: now } });
617
- }
618
- else {
619
- const links = withoutLink(child.links, parent, name);
620
- yield* d.put({ ...child, links, metadata: { ...child.metadata, nlink: links.length, ctimeNs: now } });
621
- }
622
- yield* reclaim(d, child.ino);
623
- });
624
- const releaseFileInode = Effect.fnUntraced(function* (ino) {
625
- const d = yield* Draft;
626
- yield* d.release(ino);
627
- yield* reclaim(d, ino);
628
- });
629
- const releaseFile = (ref) => TokenRegistry.release(ref, releaseFileInode, (action) => Effect.sync(action));
630
- const releaseOpenFile = (ref) => Effect.ignore(coordinatedCleanup(Effect.suspend(() => TokenRegistry.inode(ref) === undefined ? Effect.void : applyCleanup(() => releaseFile(ref)))));
631
- // Admission refusal leaves the handle open. Every admitted close shares the registry release,
632
- // and a storage failure still completes cleanup while only explicit close reports the failure.
633
- const closeFile = (ref, op, check) => coordinated(op, Effect.andThen(check, TokenRegistry.release(ref, releaseFileInode, (action) => Effect.flatMap(Draft, (draft) => draft.afterInstall(action)))), true).pipe(Effect.tapError((error) => error.code === "VolumeBusy" ? Effect.void : releaseOpenFile(ref)));
634
- const finalizeFile = (ref) => TokenRegistry.finalize(ref, closeFile(ref, OpContext.make("close"), Effect.void), releaseOpenFile(ref));
635
- const acquireHandle = TokenRegistry.acquire;
636
- const bindFile = (ref, ino) => Effect.flatMap(Draft, (draft) => draft.afterInstall(() => TokenRegistry.publish(ref, ino)));
637
- const replaceContent = Effect.fnUntraced(function* (file, data, now, publish = true) {
638
- const d = yield* Draft;
639
- yield* d.addUsedBytes(BigInt(data.length - file.data.length));
640
- yield* d.put({
641
- ...file,
642
- data,
643
- metadata: {
644
- ...file.metadata,
645
- size: BigInt(data.length),
646
- mode: file.metadata.mode & ~SET_ID_BITS,
647
- mtimeNs: now,
648
- ctimeNs: now
649
- }
650
- });
651
- if (publish)
652
- yield* publishNode(file.ino);
653
- });
654
- const resizeAt = Effect.fnUntraced(function* (file, length, now, op) {
655
- if (length > BigInt(maxFileBytes))
656
- return yield* op.fail("FileTooLarge");
657
- const size = Number(length);
658
- yield* reserveBytes(op, BigInt(size - file.data.length));
659
- const data = new Uint8Array(size);
660
- data.set(file.data.subarray(0, size));
661
- yield* replaceContent(file, data, now, false);
662
- });
663
- const resize = Effect.fnUntraced(function* (file, length, op) {
664
- if (!isLength(length))
665
- return yield* op.fail("InvalidArgument");
666
- yield* resizeAt(file, length, yield* timestamp(op), op);
667
- yield* publishNode(file.ino);
668
- });
669
- const fileHandle = (ref) => {
670
- const get = Effect.fnUntraced(function* (op, access) {
671
- const ino = TokenRegistry.inode(ref);
672
- const node = ino === undefined ? undefined : yield* view(ino);
673
- if (node?.kind !== "file" || (access === "read" && ref.access === "write") ||
674
- (access === "write" && ref.access === "read"))
675
- return yield* op.fail("InvalidHandle");
676
- return node;
677
- });
678
- const read = (maximum, position) => {
679
- const op = OpContext.make(position === undefined ? "read" : "pread");
680
- const readOp = OpContext.make("read");
681
- const body = Effect.gen(function* () {
682
- const file = yield* get(op, "read");
683
- if (!isNatural(maximum))
684
- return yield* readOp.fail("InvalidArgument");
685
- const offset = position ?? ref.offset;
686
- if (!Predicate.isBigInt(offset) || offset < 0n || offset > MAX_FILE_OFFSET) {
687
- return yield* readOp.fail("InvalidArgument");
688
- }
689
- const start = Number(offset > file.metadata.size ? file.metadata.size : offset);
690
- const data = file.data.slice(start, start + Math.min(maximum, file.data.length - start));
691
- const eof = start + data.length >= file.data.length;
692
- const access = maximum > 0 ? { node: file, now: yield* timestamp(readOp) } : undefined;
693
- return { value: { bytes: data, eof, next: offset + BigInt(data.length) }, access };
694
- });
695
- // A positioned read runs beside other reads. A cursor read stays a change, since two reads beside each
696
- // other would start at the same offset.
697
- if (position !== undefined)
698
- return Effect.map(accessing(op, body), ({ bytes, eof }) => ({ bytes, eof }));
699
- return coordinated(op, Effect.gen(function* () {
700
- const { bytes, eof, next } = yield* refreshAccess(yield* body);
701
- const draft = yield* Draft;
702
- yield* draft.afterInstall(() => {
703
- ref.offset = next;
704
- });
705
- return { bytes, eof };
706
- }));
707
- };
708
- const write = Effect.fnUntraced(function* (input, position) {
709
- const op = OpContext.make(position === undefined ? "write" : "pwrite");
710
- const writeOp = OpContext.make("write");
711
- if (!isAttachedBytes(input))
712
- return yield* writeOp.fail("InvalidArgument");
713
- const bytes = new Uint8Array(input);
714
- return yield* coordinated(op, Effect.gen(function* () {
715
- const file = yield* get(op, "write");
716
- const offset = position ?? (ref.append ? file.metadata.size : ref.offset);
717
- if (!Predicate.isBigInt(offset) || offset < 0n || offset > MAX_FILE_OFFSET) {
718
- return yield* writeOp.fail("InvalidArgument");
719
- }
720
- if (bytes.length === 0) {
721
- return 0;
722
- }
723
- if (offset >= BigInt(maxFileBytes))
724
- return yield* writeOp.fail("FileTooLarge");
725
- const start = Number(offset);
726
- const draft = yield* Draft;
727
- const free = limits.maxBytes === undefined
728
- ? BigInt(maxFileBytes)
729
- : ByteSize.toBigInt(limits.maxBytes) - (yield* draft.usedBytes);
730
- const maximumEnd = BigInt(file.data.length) + free;
731
- const end = Number(BigInt(maxFileBytes) < maximumEnd ? BigInt(maxFileBytes) : maximumEnd);
732
- const count = Math.min(bytes.length, Math.max(0, end - start));
733
- if (count === 0)
734
- return yield* writeOp.fail("NoSpace");
735
- const size = Math.max(file.data.length, start + count);
736
- // Always detach before mutation. A same-sized write is the critical
737
- // case: the current payload may belong to the base or a prior capture.
738
- const data = new Uint8Array(size);
739
- data.set(file.data);
740
- const now = yield* timestamp(writeOp);
741
- data.set(bytes.subarray(0, count), start);
742
- yield* replaceContent(file, data, now);
743
- if (position === undefined) {
744
- const next = offset + BigInt(count);
745
- yield* draft.afterInstall(() => {
746
- ref.offset = next;
747
- });
748
- }
749
- return count;
750
- }));
751
- });
752
- const statOp = OpContext.make("stat");
753
- const syncOp = OpContext.make("sync");
754
- const closeOp = OpContext.make("close");
755
- const handle = Object.freeze({
756
- [FileHandleId]: true,
757
- read: Effect.fn("FileHandle.read")(function* (maximum) {
758
- return (yield* read(maximum)).bytes;
759
- }),
760
- pread: Effect.fn("FileHandle.pread")(function* (maximum, offset) {
761
- return yield* read(maximum, offset);
762
- }),
763
- write: Effect.fn("FileHandle.write")(function* (bytes) {
764
- return yield* write(bytes);
765
- }),
766
- pwrite: Effect.fn("FileHandle.pwrite")(function* (bytes, offset) {
767
- return yield* write(bytes, offset);
768
- }),
769
- seek: Effect.fn("FileHandle.seek")(function* (offset, mode) {
770
- const op = OpContext.make("seek");
771
- return yield* coordinatedRead(op, Effect.uninterruptible(Effect.gen(function* () {
772
- const file = yield* get(op);
773
- if (!Predicate.isBigInt(offset) || !isSeekMode(mode)) {
774
- return yield* op.fail("InvalidArgument");
775
- }
776
- let next = mode === "current"
777
- ? ref.offset + offset
778
- : mode === "end"
779
- ? file.metadata.size + offset
780
- : offset;
781
- if (next < 0n || next > MAX_FILE_OFFSET) {
782
- return yield* op.fail("InvalidArgument");
783
- }
784
- if (mode === "data" || mode === "hole") {
785
- if (offset >= file.metadata.size)
786
- return yield* op.fail("NoData");
787
- if (mode === "hole")
788
- next = file.metadata.size;
789
- }
790
- ref.offset = next;
791
- return next;
792
- })));
793
- }),
794
- truncate: Effect.fn("FileHandle.truncate")(function* (length) {
795
- const op = OpContext.make("truncate");
796
- return yield* coordinated(op, Effect.gen(function* () {
797
- yield* resize(yield* get(op, "write"), length, op);
798
- }));
799
- }),
800
- stat: coordinatedRead(statOp, Effect.map(Effect.suspend(() => get(statOp)), withMetadata)).pipe(Effect.withSpan("FileHandle.stat")),
801
- sync: coordinatedRead(syncOp, Effect.suspend(() => Effect.asVoid(get(syncOp)))).pipe(Effect.withSpan("FileHandle.sync")),
802
- close: closeFile(ref, closeOp, Effect.suspend(() => get(closeOp))).pipe(Effect.ensuring(closeReleasedScope(ref)), Effect.withSpan("FileHandle.close"))
803
- });
804
- registry.registerFile(handle, ref);
805
- return handle;
806
- };
807
- const createCaller = (reference, identity, umask) => {
808
- const resolver = Resolution.make({
809
- caller: reference,
810
- get: view,
811
- identity,
812
- maxPathBytes: limits.maxPathBytes,
813
- registry
814
- });
815
- const prepare = resolver.prepare;
816
- const prepareEntry = resolver.prepareEntry;
817
- const entryName = Resolution.entryName;
818
- const resolveTarget = (target, op, options) => resolver.resolve(target, { kind: "Node", ...options }, op);
819
- const resolveDirectory = resolver.directory;
820
- const resolveEntry = (prepared, op) => resolver.resolve(prepared, { kind: "Parent" }, op);
821
- const referencedNode = (target, op) => Effect.map(resolveTarget(Target.Reference({ reference: target }), op), (resolved) => resolved.node);
822
- const directoryNow = Effect.fnUntraced(function* (ino) {
823
- const node = yield* view(ino);
824
- if (node?.kind !== "directory") {
825
- return dieInvariant("Directory left the inode table during a transition");
826
- }
827
- return node;
828
- });
829
- const nodeNow = Effect.fnUntraced(function* (ino) {
830
- const node = yield* view(ino);
831
- if (node === undefined)
832
- return dieInvariant("Inode left the table during a transition");
833
- return node;
834
- });
835
- const creationTimes = (times, now) => ({
836
- atimeNs: times?.access.kind === "value" ? times.access.nanoseconds : now,
837
- mtimeNs: times?.modification.kind === "value" ? times.modification.nanoseconds : now
838
- });
839
- const newDirectory = Effect.fnUntraced(function* (parent, mode, now, times) {
840
- const draft = yield* Draft;
841
- const ino = yield* draft.allocate;
842
- return {
843
- kind: "directory",
844
- ino,
845
- parent: parent.ino,
846
- name: "",
847
- entries: new Map(),
848
- metadata: {
849
- ...directoryMetadata(BigInt(ino), identity.uid, parent.metadata.gid, mode, now),
850
- ...creationTimes(times, now)
851
- },
852
- revision: 0n
853
- };
854
- });
855
- const newFile = Effect.fnUntraced(function* (parent, data, mode, now, owner, times) {
856
- const draft = yield* Draft;
857
- const ino = yield* draft.allocate;
858
- return {
859
- kind: "file",
860
- ino,
861
- data,
862
- links: [],
863
- metadata: {
864
- ...directoryMetadata(BigInt(ino), owner?.uid ?? identity.uid, owner?.gid ?? parent.metadata.gid, mode, now),
865
- ...creationTimes(times, now),
866
- kind: "file",
867
- size: BigInt(data.length),
868
- nlink: 0
869
- },
870
- revision: 0n
871
- };
872
- });
873
- const newSymlink = Effect.fnUntraced(function* (parent, target, now, times) {
874
- const draft = yield* Draft;
875
- const ino = yield* draft.allocate;
876
- return {
877
- kind: "symlink",
878
- ino,
879
- target,
880
- links: [],
881
- metadata: {
882
- ...directoryMetadata(BigInt(ino), identity.uid, parent.metadata.gid, 0o777, now),
883
- ...creationTimes(times, now),
884
- kind: "symlink",
885
- nlink: 0,
886
- size: BigInt(target.length)
887
- },
888
- revision: 0n
889
- };
890
- });
891
- const grantedMode = (metadata, mode) => !identity.privileged && metadata.kind === "file" && !inGroup(identity, metadata.gid) ? mode & ~0o2000 : mode;
892
- const permittedMode = (metadata, mode, op) => !identity.privileged && identity.uid !== metadata.uid
893
- ? Effect.fail(op.fail("NotPermitted"))
894
- : Effect.succeed(grantedMode(metadata, mode));
895
- // Changes the attributes one change at a time would, as one change: one draft, one revision, one event.
896
- // Every check runs against the node before any attribute applies, ownership (NotPermitted) before
897
- // permission (AccessDenied), so the first failure leaves everything unchanged. The attributes apply as
898
- // size, owner, mode, then times, the POSIX composition of chown then chmod, so a requested mode wins over
899
- // the set-ID clearing that a resize or an owner change triggers; an adapter owns any protocol-specific
900
- // sanitising of the mode, as NFS SETATTR does after knfsd. The attributes arrive validated; an owner with
901
- // neither id still checks ownership, and times that omit both are no change and check nothing. An expected
902
- // revision is checked first, against the target as the change finds it, so a caller that decided the
903
- // attributes from an earlier observation learns the target moved instead of applying them to a newer state.
904
- const changeAttributes = (resolve, attributes, op) => {
905
- const { size, mode } = attributes;
906
- const expected = attributes.expected?.revision;
907
- const owner = attributes.owner === undefined ? undefined : { ...attributes.owner };
908
- const times = attributes.times === undefined
909
- ? undefined
910
- : { access: { ...attributes.times.access }, modification: { ...attributes.times.modification } };
911
- const timed = times !== undefined && (times.access.kind !== "omit" || times.modification.kind !== "omit");
912
- const bothNow = times?.access.kind === "now" && times.modification.kind === "now";
913
- const resolving = resolve();
914
- return coordinated(op, Effect.gen(function* () {
915
- const resolved = yield* resolving;
916
- const node = yield* nodeNow(resolved.ino);
917
- if (expected !== undefined && node.revision !== expected) {
918
- return yield* resolved.op.fail("StaleReference", { field: "expected" });
919
- }
920
- if (size !== undefined && node.kind === "symlink")
921
- return yield* resolved.op.fail("SymlinkLoop");
922
- if (size !== undefined && node.kind !== "file")
923
- return yield* resolved.op.fail("IsDirectory");
924
- const owns = identity.privileged || identity.uid === node.metadata.uid;
925
- if ((mode !== undefined && !owns) ||
926
- (owner !== undefined && !identity.privileged && (!owns ||
927
- (owner.uid !== undefined && owner.uid !== node.metadata.uid) ||
928
- (owner.gid !== undefined && !inGroup(identity, owner.gid)))) ||
929
- // POSIX grants write access only when both times are UTIME_NOW; every other combination that
930
- // changes a time, mixed ones included, needs ownership.
931
- (timed && !owns && !bothNow)) {
932
- return yield* resolved.op.fail("NotPermitted");
933
- }
934
- if (size !== undefined || (timed && !owns))
935
- yield* authorize(node, identity, WRITE, resolved.op);
936
- const chowned = owner?.uid !== undefined || owner?.gid !== undefined;
937
- if (size === undefined && !chowned && mode === undefined && !timed)
938
- return;
939
- const now = yield* timestamp(op);
940
- if (size !== undefined && node.kind === "file")
941
- yield* resizeAt(node, size, now, op);
942
- const sized = yield* nodeNow(resolved.ino);
943
- const gid = owner?.gid ?? sized.metadata.gid;
944
- const cleared = chowned && sized.kind === "file" ? sized.metadata.mode & ~SET_ID_BITS : sized.metadata.mode;
945
- if (chowned || mode !== undefined || timed) {
946
- yield* (yield* Draft).put({
947
- ...sized,
948
- metadata: {
949
- ...sized.metadata,
950
- uid: owner?.uid ?? sized.metadata.uid,
951
- gid,
952
- mode: mode === undefined ? cleared : grantedMode({ kind: sized.kind, gid }, mode),
953
- atimeNs: timeAt(times?.access, sized.metadata.atimeNs, now),
954
- mtimeNs: timeAt(times?.modification, sized.metadata.mtimeNs, now),
955
- ctimeNs: now
956
- }
957
- });
958
- }
959
- yield* publishNode(node.ino);
960
- }));
961
- };
962
- const authorizeRemoval = (parent, child, op) => (parent.metadata.mode & STICKY_BIT) !== 0 && !identity.privileged &&
963
- identity.uid !== parent.metadata.uid && identity.uid !== child.metadata.uid
964
- ? Effect.fail(op.fail("NotPermitted"))
965
- : Effect.void;
966
- const claimName = Effect.fnUntraced(function* (entry, trailingSlash = "allowed") {
967
- const parent = yield* directoryNow(entry.parent);
968
- if (isDotComponent(entry.name)) {
969
- return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : "AlreadyExists");
970
- }
971
- yield* authorize(parent, identity, EXECUTE, entry.op);
972
- if (parent.entries.has(entry.name))
973
- return yield* entry.op.fail("AlreadyExists");
974
- if (trailingSlash === "rejected" && entry.trailingSlash)
975
- return yield* entry.op.fail("NotFound");
976
- yield* authorize(parent, identity, WRITE, entry.op);
977
- return entry.name;
978
- });
979
- const makeDirectory = Effect.fnUntraced(function* (entry, request, op) {
980
- const name = yield* claimName(entry);
981
- yield* reserveEntry(entry.op);
982
- const parent = yield* directoryNow(entry.parent);
983
- const before = parent.revision;
984
- const now = yield* timestamp(op);
985
- const mode = request.exactMode
986
- ? yield* permittedMode({ kind: "directory", uid: identity.uid, gid: parent.metadata.gid }, request.mode, op)
987
- : (request.mode & 0o777 & ~umask) | (request.mode & STICKY_BIT);
988
- const child = yield* newDirectory(parent, mode, now, request.times);
989
- yield* attach(parent, name, child, now);
990
- const draft = yield* Draft;
991
- yield* draft.addEntries(1);
992
- yield* publishEntry("Create", parent.ino, name, child.ino);
993
- return { child: child.ino, directory: { before, after: draft.revision } };
994
- });
995
- // Creates every missing directory a path names in the running transition, so a failure anywhere discards
996
- // them all. The mode applies to each directory created and the times to the one in the final position. An
997
- // entry names one child, created unless it is already a directory. The result names the directory the path
998
- // ends on and its parent's revision before and after the call, so a final directory that already exists is
999
- // no change, while a path that leaves a directory it created, such as `new/..`, reports the creation.
1000
- const makeDirectories = Effect.fnUntraced(function* (prepared, request, op) {
1001
- let node;
1002
- let entryOp = op;
1003
- const revisionsBefore = new Map();
1004
- if (prepared.kind === "entry") {
1005
- const entry = yield* resolveEntry(prepared, op);
1006
- const made = yield* Effect.result(makeDirectory(entry, request, op));
1007
- if (Result.isSuccess(made))
1008
- return made.success;
1009
- const existingIno = entry.name === undefined
1010
- ? undefined
1011
- : (yield* directoryNow(entry.parent)).entries.get(entry.name);
1012
- node = existingIno === undefined ? undefined : (yield* view(existingIno));
1013
- if (made.failure.code !== "AlreadyExists" || node?.kind !== "directory")
1014
- return yield* made.failure;
1015
- }
1016
- else {
1017
- entryOp = op.at(prepared.path.input);
1018
- const resolved = yield* resolver.resolve(Target.Path(prepared.base === undefined
1019
- ? { path: prepared.path.input }
1020
- : { path: prepared.path.input, relativeTo: prepared.base }), {
1021
- kind: "Node",
1022
- createMissing: (parent, name, final) => Effect.flatMap(makeDirectory({ parent: parent.ino, name, trailingSlash: false, addressing: "path", op: entryOp }, final ? request : { mode: request.mode, exactMode: request.exactMode }, op), (made) => {
1023
- if (!revisionsBefore.has(parent.ino))
1024
- revisionsBefore.set(parent.ino, made.directory.before);
1025
- return directoryNow(made.child);
1026
- })
1027
- }, op);
1028
- node = resolved.node;
1029
- }
1030
- if (node?.kind !== "directory")
1031
- return yield* entryOp.fail("AlreadyExists");
1032
- const after = (yield* directoryNow(node.parent)).revision;
1033
- return { child: node.ino, directory: { before: revisionsBefore.get(node.parent) ?? after, after } };
1034
- });
1035
- const linkNode = Effect.fnUntraced(function* (node, entry, op) {
1036
- const name = yield* claimName(entry, "rejected");
1037
- yield* reserveEntry(entry.op);
1038
- const parent = yield* directoryNow(entry.parent);
1039
- const before = parent.revision;
1040
- const now = yield* timestamp(op);
1041
- yield* attach(parent, name, node, now);
1042
- const draft = yield* Draft;
1043
- yield* draft.addEntries(1);
1044
- yield* publishEntry("Create", parent.ino, name, node.ino);
1045
- return { before, after: draft.revision };
1046
- });
1047
- const makeSymlink = Effect.fnUntraced(function* (entry, target, times, op) {
1048
- const name = yield* claimName(entry, "rejected");
1049
- yield* reserveEntry(entry.op);
1050
- yield* reserveBytes(entry.op, BigInt(target.length));
1051
- const parent = yield* directoryNow(entry.parent);
1052
- const before = parent.revision;
1053
- const now = yield* timestamp(op);
1054
- const child = yield* newSymlink(parent, target, now, times);
1055
- yield* attach(parent, name, child, now);
1056
- const draft = yield* Draft;
1057
- yield* draft.addEntries(1);
1058
- yield* draft.addUsedBytes(BigInt(target.length));
1059
- yield* publishEntry("Create", parent.ino, name, child.ino);
1060
- return { child: child.ino, directory: { before, after: draft.revision } };
1061
- });
1062
- const removeChild = Effect.fnUntraced(function* (parent, name, child, op) {
1063
- const before = parent.revision;
1064
- const now = yield* timestamp(op);
1065
- const d = yield* Draft;
1066
- yield* d.put(withEntries({
1067
- ...parent,
1068
- metadata: {
1069
- ...parent.metadata,
1070
- nlink: parent.metadata.nlink - (child.kind === "directory" ? 1 : 0),
1071
- mtimeNs: now,
1072
- ctimeNs: now
1073
- }
1074
- }, (entries) => entries.delete(name)));
1075
- yield* publishEntry("Remove", parent.ino, name, child.ino);
1076
- yield* detach(child, parent.ino, name, now);
1077
- yield* d.addEntries(-1);
1078
- return { before, after: d.revision };
1079
- });
1080
- // Authorizes removing from the entry's directory and returns the named child. Only a path can name a dot
1081
- // entry, and each verb reports it with its own code. Search permission comes before the lookup and write
1082
- // permission after it, as on Linux; `beforeWrite` holds the verb's own checks that Linux makes in between.
1083
- const removalTarget = Effect.fnUntraced(function* (entry, dotNameCode, beforeWrite) {
1084
- const parent = yield* directoryNow(entry.parent);
1085
- if (isDotComponent(entry.name)) {
1086
- return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : dotNameCode);
1087
- }
1088
- yield* authorize(parent, identity, EXECUTE, entry.op);
1089
- const childIno = parent.entries.get(entry.name);
1090
- const child = childIno === undefined ? undefined : (yield* view(childIno));
1091
- if (child === undefined)
1092
- return yield* entry.op.fail("NotFound");
1093
- const rejected = beforeWrite?.(child);
1094
- if (rejected !== undefined)
1095
- return yield* rejected;
1096
- yield* authorize(parent, identity, WRITE, entry.op);
1097
- return { parent, name: entry.name, child };
1098
- });
1099
- const removeEntry = Effect.fnUntraced(function* (entry, op) {
1100
- const { child, name, parent } = yield* removalTarget(entry, "InvalidArgument", (found) => entry.trailingSlash && found.kind !== "directory" ? entry.op.fail("NotDirectory") : undefined);
1101
- yield* authorizeRemoval(parent, child, entry.op);
1102
- if (child.kind === "directory" && child.entries.size > 0)
1103
- return yield* entry.op.fail("NotEmpty");
1104
- return yield* removeChild(parent, name, child, op);
1105
- });
1106
- const unlinkEntry = Effect.fnUntraced(function* (entry, op) {
1107
- const { child, name, parent } = yield* removalTarget(entry, "IsDirectory", (found) => !entry.trailingSlash
1108
- ? undefined
1109
- : entry.op.fail(found.kind === "directory" ? "IsDirectory" : "NotDirectory"));
1110
- if (child.kind === "directory")
1111
- return yield* entry.op.fail("IsDirectory");
1112
- yield* authorizeRemoval(parent, child, entry.op);
1113
- return yield* removeChild(parent, name, child, op);
1114
- });
1115
- const rmdirEntry = Effect.fnUntraced(function* (entry, op) {
1116
- const { child, name, parent } = yield* removalTarget(entry, "InvalidArgument");
1117
- yield* authorizeRemoval(parent, child, entry.op);
1118
- if (child.kind !== "directory")
1119
- return yield* entry.op.fail("NotDirectory");
1120
- if (child.entries.size > 0)
1121
- return yield* entry.op.fail("NotEmpty");
1122
- return yield* removeChild(parent, name, child, op);
1123
- });
1124
- const renameEntry = Effect.fnUntraced(function* (source, destination, op) {
1125
- const sourceDirectory = yield* directoryNow(source.parent);
1126
- const destinationDirectory = yield* directoryNow(destination.parent);
1127
- const sameDirectory = source.parent === destination.parent;
1128
- const sourceName = source.name;
1129
- const destinationName = destination.name;
1130
- if (isDotComponent(sourceName) || isDotComponent(destinationName)) {
1131
- return yield* source.op.fail("InvalidArgument");
1132
- }
1133
- // Linux's order: search permission on both directories before either name is looked up; the names,
1134
- // trailing slashes, the subtree rule and the same-object no-op; then write permission and the rest.
1135
- yield* authorize(sourceDirectory, identity, EXECUTE, source.op);
1136
- yield* authorize(destinationDirectory, identity, EXECUTE, destination.op);
1137
- const sourceBefore = sourceDirectory.revision;
1138
- const destinationBefore = destinationDirectory.revision;
1139
- const childIno = sourceDirectory.entries.get(sourceName);
1140
- const child = childIno === undefined ? undefined : (yield* view(childIno));
1141
- if (child === undefined)
1142
- return yield* source.op.fail("NotFound");
1143
- const replacedIno = destinationDirectory.entries.get(destinationName);
1144
- const replaced = replacedIno === undefined ? undefined : (yield* view(replacedIno));
1145
- // A trailing slash on either side asks for a directory; a missing slashed destination is fine when the
1146
- // source is one, as Linux allows.
1147
- if ((source.trailingSlash || destination.trailingSlash) && child.kind !== "directory") {
1148
- return yield* (source.trailingSlash ? source.op : destination.op).fail("NotDirectory");
1149
- }
1150
- if (destination.trailingSlash && replaced !== undefined && replaced.kind !== "directory") {
1151
- return yield* destination.op.fail("NotDirectory");
1152
- }
1153
- const result = Effect.fnUntraced(function* () {
1154
- return sameDirectory
1155
- ? {
1156
- _tag: "SameDirectory",
1157
- directory: { before: sourceBefore, after: (yield* directoryNow(source.parent)).revision }
1158
- }
1159
- : {
1160
- _tag: "DifferentDirectories",
1161
- sourceDirectory: { before: sourceBefore, after: (yield* directoryNow(source.parent)).revision },
1162
- destinationDirectory: {
1163
- before: destinationBefore,
1164
- after: (yield* directoryNow(destination.parent)).revision
1165
- }
1166
- };
1167
- });
1168
- for (let ancestor = destinationDirectory;; ancestor = yield* directoryNow(ancestor.parent)) {
1169
- if (ancestor.ino === child.ino)
1170
- return yield* destination.op.fail("InvalidArgument");
1171
- if (ancestor.ino === ROOT_INO)
1172
- break;
1173
- }
1174
- if (child.ino === replaced?.ino)
1175
- return yield* result();
1176
- yield* authorize(sourceDirectory, identity, WRITE, source.op);
1177
- yield* authorize(destinationDirectory, identity, WRITE, destination.op);
1178
- yield* authorizeRemoval(sourceDirectory, child, source.op);
1179
- if (replaced !== undefined) {
1180
- yield* authorizeRemoval(destinationDirectory, replaced, destination.op);
1181
- if (child.kind === "directory" && replaced.kind !== "directory") {
1182
- return yield* destination.op.fail("NotDirectory");
1183
- }
1184
- if (child.kind !== "directory" && replaced.kind === "directory") {
1185
- return yield* destination.op.fail("IsDirectory");
1186
- }
1187
- if (replaced.kind === "directory" && replaced.entries.size > 0) {
1188
- return yield* destination.op.fail("NotEmpty");
1189
- }
1190
- }
1191
- const now = yield* timestamp(op);
1192
- const d = yield* Draft;
1193
- yield* publishEntry("Remove", sourceDirectory.ino, sourceName, child.ino);
1194
- yield* d.put(withEntries({
1195
- ...sourceDirectory,
1196
- metadata: {
1197
- ...sourceDirectory.metadata,
1198
- nlink: sourceDirectory.metadata.nlink - (child.kind === "directory" ? 1 : 0),
1199
- mtimeNs: now,
1200
- ctimeNs: now
1201
- }
1202
- }, (entries) => entries.delete(sourceName)));
1203
- const destinationNow = yield* directoryNow(destination.parent);
1204
- yield* d.put(withEntries({
1205
- ...destinationNow,
1206
- metadata: {
1207
- ...destinationNow.metadata,
1208
- nlink: destinationNow.metadata.nlink + (child.kind === "directory" && replaced === undefined ? 1 : 0),
1209
- mtimeNs: now,
1210
- ctimeNs: now
1211
- }
1212
- }, (entries) => entries.set(destinationName, child.ino)));
1213
- const moved = yield* nodeNow(child.ino);
1214
- if (moved.kind === "directory") {
1215
- yield* d.put({
1216
- ...moved,
1217
- parent: destination.parent,
1218
- name: destinationName,
1219
- metadata: { ...moved.metadata, ctimeNs: now }
1220
- });
1221
- }
1222
- else {
1223
- yield* d.put({
1224
- ...moved,
1225
- links: [
1226
- ...withoutLink(moved.links, source.parent, sourceName),
1227
- { parent: destination.parent, name: destinationName }
1228
- ],
1229
- metadata: { ...moved.metadata, ctimeNs: now }
1230
- });
1231
- }
1232
- if (replaced !== undefined) {
1233
- yield* detach(yield* nodeNow(replaced.ino), destination.parent, destinationName, now);
1234
- yield* d.addEntries(-1);
1235
- }
1236
- yield* publishEntry("Create", destination.parent, destinationName, child.ino);
1237
- return yield* result();
1238
- });
1239
- const openExisting = Effect.fnUntraced(function* (file, request, at, op) {
1240
- yield* authorize(file, identity, request.access === "read" ? READ : request.access === "write" ? WRITE : READ | WRITE, at);
1241
- if (request.truncate)
1242
- yield* resize(file, 0n, op);
1243
- });
1244
- const openFile = Effect.fnUntraced(function* (entry, found, request, acquired, op) {
1245
- const { name } = entry;
1246
- const parent = yield* directoryNow(entry.parent);
1247
- const before = parent.revision;
1248
- let file = found;
1249
- let created = false;
1250
- if (file === undefined) {
1251
- if (request.create === undefined || request.create === "never")
1252
- return yield* entry.op.fail("NotFound");
1253
- yield* authorize(parent, identity, WRITE | EXECUTE, entry.op);
1254
- yield* reserveEntry(entry.op);
1255
- const size = request.initialSize ?? 0n;
1256
- if (request.initialSize !== undefined) {
1257
- if (size < 0n)
1258
- return yield* entry.op.fail("InvalidArgument");
1259
- if (size > BigInt(maxFileBytes))
1260
- return yield* entry.op.fail("FileTooLarge");
1261
- yield* reserveBytes(entry.op, size);
1262
- }
1263
- const owner = request.owner;
1264
- if (!identity.privileged &&
1265
- ((owner?.uid !== undefined && owner.uid !== identity.uid) ||
1266
- (owner?.gid !== undefined && !inGroup(identity, owner.gid)))) {
1267
- return yield* entry.op.fail("NotPermitted");
1268
- }
1269
- const now = yield* timestamp(op);
1270
- const mode = request.exactMode
1271
- ? yield* permittedMode({ kind: "file", uid: owner?.uid ?? identity.uid, gid: owner?.gid ?? parent.metadata.gid }, request.mode, op)
1272
- : (request.mode ?? 0o666) & 0o777 & ~umask;
1273
- file = yield* newFile(parent, new Uint8Array(Number(size)), mode, now, owner, request.times);
1274
- yield* attach(parent, name, file, now);
1275
- const draft = yield* Draft;
1276
- yield* draft.addEntries(1);
1277
- yield* draft.addUsedBytes(size);
1278
- created = true;
1279
- yield* publishEntry("Create", parent.ino, name, file.ino);
1280
- }
1281
- else {
1282
- if (file.kind === "symlink")
1283
- return yield* entry.op.fail("SymlinkLoop");
1284
- if (file.kind !== "file")
1285
- return yield* entry.op.fail("IsDirectory");
1286
- yield* openExisting(file, request, entry.op, op);
1287
- }
1288
- yield* (yield* Draft).retain(file.ino);
1289
- yield* bindFile(acquired, file.ino);
1290
- return { ino: file.ino, created, directory: { before, after: (yield* directoryNow(entry.parent)).revision } };
1291
- });
1292
- const releasePending = (opened) => () => Effect.gen(function* () {
1293
- const ino = opened();
1294
- if (ino === undefined)
1295
- return;
1296
- yield* releaseFileInode(ino);
1297
- });
1298
- const acquireOpenedFile = (ref, op, acquire) => {
1299
- let opened;
1300
- return acquireHandle(ref, (effect) => coordinated(op, effect), acquire((ino) => {
1301
- opened = ino;
1302
- }), releasePending(() => opened), finalizeFile(ref));
1303
- };
1304
- const asResolvedNode = (target, op, options) => Effect.map(resolveTarget(target, op, options), (resolved) => ({ ino: resolved.node.ino, op: resolved.op }));
1305
- const asTarget = (input) => Target.of(input);
1306
- const acquireDirectory = Effect.fnUntraced(function* (input, op) {
1307
- const target = asTarget(input);
1308
- const acquired = makeDirectoryReference();
1309
- return yield* acquireHandle(acquired, (acquire) => coordinatedRead(op, Effect.uninterruptible(acquire)), Effect.gen(function* () {
1310
- const directory = yield* resolveDirectory(target, op);
1311
- yield* authorize(directory.node, identity, EXECUTE, directory.op);
1312
- holdDirectory(directory.node.ino);
1313
- TokenRegistry.publish(acquired, directory.node.ino);
1314
- return acquired;
1315
- }),
1316
- // Nothing to undo here: the finalizer that follows an interrupted acquisition releases the hold under
1317
- // every permit, where a detached directory may leave the table.
1318
- () => Effect.void, finalizeDirectory(acquired));
1319
- });
1320
- const fail = (op, cause, field) => op.fail("InvalidArgument", field === undefined ? { cause } : { cause, field });
1321
- const preparedOp = (prepared, op) => prepared.kind === "path" ? op.at(prepared.path.input) : op;
1322
- const openPath = Effect.fnUntraced(function* (target, options, op) {
1323
- const pathOp = op.at(target.path);
1324
- const prepared = prepare(target.path, op);
1325
- const chosen = yield* decodeOpenOptions(options).pipe(Effect.mapError((cause) => fail(pathOp, cause)));
1326
- if (chosen.access === "read" && (chosen.append || chosen.truncate)) {
1327
- return yield* pathOp.fail("InvalidArgument");
1328
- }
1329
- if (chosen.mode !== undefined && (chosen.create === undefined || chosen.create === "never")) {
1330
- return yield* pathOp.fail("InvalidArgument");
1331
- }
1332
- const acquired = makeFileReference(chosen.access, chosen.append ?? false);
1333
- const follow = target.followFinalSymlink ?? true;
1334
- return yield* acquireOpenedFile(acquired, op, (opened) => Effect.gen(function* () {
1335
- const path = yield* Effect.fromResult(prepared);
1336
- const resolved = yield* resolver.resolve({ kind: "path", path, base: target.relativeTo }, {
1337
- kind: "OrCreate",
1338
- create: chosen.create ?? "never",
1339
- finalSymlink: follow ? "follow" : "preserve"
1340
- }, op);
1341
- const { parent, name } = resolved;
1342
- if (resolved.kind === "missing" && path.trailingSlash)
1343
- return yield* pathOp.fail("IsDirectory");
1344
- yield* authorize(parent, identity, EXECUTE, pathOp);
1345
- const file = resolved.kind === "existing" ? resolved.node : undefined;
1346
- const result = yield* openFile({ parent: parent.ino, name, trailingSlash: path.trailingSlash, addressing: "path", op: pathOp }, file, chosen, acquired, op);
1347
- opened(result.ino);
1348
- return fileHandle(acquired);
1349
- }));
1350
- });
1351
- const openNode = Effect.fnUntraced(function* (target, options, op) {
1352
- const chosen = yield* decodeOpenOptions(options).pipe(Effect.mapError((cause) => fail(op, cause)));
1353
- if (chosen.access === "read" && (chosen.append || chosen.truncate))
1354
- return yield* op.fail("InvalidArgument");
1355
- if ((chosen.create !== undefined && chosen.create !== "never") || chosen.mode !== undefined) {
1356
- return yield* op.fail("InvalidArgument");
1357
- }
1358
- const acquired = makeFileReference(chosen.access, chosen.append ?? false);
1359
- return yield* acquireOpenedFile(acquired, op, (opened) => Effect.gen(function* () {
1360
- const resolved = yield* resolveTarget(target, op);
1361
- const node = resolved.node;
1362
- if (node.kind !== "file")
1363
- return yield* resolved.op.fail("IsDirectory");
1364
- if (node.metadata.nlink === 0)
1365
- return yield* resolved.op.fail("StaleReference");
1366
- yield* openExisting(node, chosen, resolved.op, op);
1367
- const draft = yield* Draft;
1368
- yield* draft.retain(node.ino);
1369
- yield* bindFile(acquired, node.ino);
1370
- opened(node.ino);
1371
- return fileHandle(acquired);
1372
- }));
1373
- });
1374
- const openEntry = Effect.fnUntraced(function* (entry, options, op) {
1375
- const name = yield* Effect.fromResult(entryName(entry.name, op));
1376
- const decoded = yield* decodeOpenEntryOptions(options).pipe(Effect.mapError((cause) => fail(op, cause)));
1377
- const chosen = { ...decoded };
1378
- if (chosen.access === "read" && (chosen.append || chosen.truncate))
1379
- return yield* op.fail("InvalidArgument");
1380
- if ((chosen.mode !== undefined || chosen.times !== undefined || chosen.initialSize !== undefined ||
1381
- chosen.exactMode !== undefined || chosen.owner !== undefined) &&
1382
- (chosen.create === undefined || chosen.create === "never")) {
1383
- return yield* op.fail("InvalidArgument");
1384
- }
1385
- if (chosen.exactMode && chosen.mode === undefined)
1386
- return yield* op.fail("InvalidArgument");
1387
- const acquired = makeFileReference(chosen.access, chosen.append ?? false);
1388
- const expected = chosen.expected;
1389
- return yield* acquireOpenedFile(acquired, op, (opened) => Effect.gen(function* () {
1390
- const resolved = yield* resolver.resolve({ kind: "entry", directory: entry.directory, name }, {
1391
- kind: "OrCreate",
1392
- create: chosen.create ?? "never",
1393
- finalSymlink: chosen.followFinalSymlink !== false ? "follow" : "preserve",
1394
- beforeFollow: Effect.fnUntraced(function* (_, direct) {
1395
- if (expected !== undefined) {
1396
- let expectedIno;
1397
- if (expected !== null) {
1398
- const observed = yield* Effect.result(referencedNode(expected, op));
1399
- if (Result.isFailure(observed)) {
1400
- return yield* op.fail("VolumeBusy", { cause: observed.failure });
1401
- }
1402
- expectedIno = observed.success.ino;
1403
- }
1404
- if (direct?.ino !== expectedIno)
1405
- return yield* op.fail("VolumeBusy");
1406
- }
1407
- if (chosen.expectedChild === null) {
1408
- if (direct !== undefined)
1409
- return yield* op.fail("StaleReference");
1410
- }
1411
- else if (chosen.expectedChild !== undefined) {
1412
- const expectedChild = chosen.expectedChild;
1413
- const observed = yield* referencedNode(expectedChild.reference, op);
1414
- if (direct?.ino !== observed.ino || observed.revision !== expectedChild.revision ||
1415
- observed.metadata.atimeNs !== expectedChild.atimeNs ||
1416
- observed.metadata.mtimeNs !== expectedChild.mtimeNs) {
1417
- return yield* op.fail("StaleReference");
1418
- }
1419
- }
1420
- })
1421
- }, op);
1422
- const file = resolved.kind === "existing" ? resolved.node : undefined;
1423
- const mutationParent = resolved.kind === "missing" ? resolved.parent.ino : resolved.origin.parent.ino;
1424
- const mutationName = resolved.kind === "missing" ? resolved.name : resolved.origin.name;
1425
- const result = yield* openFile({ parent: mutationParent, name: mutationName, trailingSlash: false, addressing: "entry", op }, file, { ...chosen, initialSize: chosen.initialSize ?? 0n }, acquired, op);
1426
- opened(result.ino);
1427
- return {
1428
- handle: fileHandle(acquired),
1429
- reference: referenceFor(result.ino),
1430
- created: result.created,
1431
- directory: result.directory
1432
- };
1433
- }));
1434
- });
1435
- const walkChildren = Effect.fnUntraced(function* (directory, path, depth, up) {
1436
- const listed = referenceFor(directory.ino);
1437
- const frames = [];
1438
- for (const [name, childIno] of Arr.sort(directory.entries, byEntryName)) {
1439
- const child = yield* view(childIno);
1440
- if (child === undefined)
1441
- continue;
1442
- const bytes = nameBytes(name);
1443
- frames.push({
1444
- ino: child.ino,
1445
- kind: child.kind,
1446
- name: bytes,
1447
- key: name,
1448
- path: joinPath(path, bytes),
1449
- parent: directory.ino,
1450
- up,
1451
- depth: depth + 1,
1452
- bytes: child.kind === "directory" ? 0n : child.metadata.size,
1453
- reference: referenceFor(child.ino),
1454
- directory: listed,
1455
- listed: false
1456
- });
1457
- }
1458
- return frames;
1459
- });
1460
- const anchorFrame = (parent, key, root) => ({
1461
- ino: root.ino,
1462
- kind: root.kind,
1463
- name: nameBytes(key),
1464
- key,
1465
- path: new Uint8Array(0),
1466
- parent: parent.ino,
1467
- up: undefined,
1468
- depth: 0,
1469
- bytes: 0n,
1470
- reference: referenceFor(root.ino),
1471
- directory: referenceFor(parent.ino),
1472
- listed: false
1473
- });
1474
- const pathAnchor = (target, root, op) => {
1475
- if (!Target.$is("Path")(target))
1476
- return Effect.undefined;
1477
- const prepared = prepare(target.path, op);
1478
- if (Result.isFailure(prepared))
1479
- return Effect.undefined;
1480
- return resolveEntry({ kind: "path", path: prepared.success, base: target.relativeTo }, op).pipe(Effect.flatMap((entry) => Effect.map(view(entry.parent), (parent) => {
1481
- return entry.name !== undefined && parent?.kind === "directory" &&
1482
- parent.entries.get(entry.name) === root.ino
1483
- ? anchorFrame(parent, entry.name, root)
1484
- : undefined;
1485
- })), Effect.catch(() => Effect.undefined));
1486
- };
1487
- const holds = (parent, frame) => parent?.kind === "directory" && parent.metadata.nlink > 0 && parent.entries.get(frame.key) === frame.ino;
1488
- // The node a frame names, reached by name from the walk's root as a path lookup reaches it: every directory on
1489
- // the way must be searchable and must still hold the name the walk listed for the object it listed. A frame
1490
- // whose chain no longer holds it, such as a directory renamed out of the tree, reaches nothing. A root the
1491
- // walk reached by name is anchored by that name, so a root moved away reaches nothing too; a root reached
1492
- // through a reference or a handle is held by the object it resolved to, as a descriptor holds it.
1493
- // It steps down the chain in a loop, so a tree of any depth reaches its frames without deepening the stack.
1494
- const reachFrame = (frame, at) => Effect.gen(function* () {
1495
- const chain = [];
1496
- for (let link = frame; link !== undefined; link = link.up)
1497
- chain.push(link);
1498
- let node = yield* view((chain.at(-1) ?? frame).parent);
1499
- for (let index = chain.length - 1; index >= 0; index--) {
1500
- const link = chain[index];
1501
- if (link === undefined || node?.kind !== "directory" || node.metadata.nlink === 0)
1502
- return undefined;
1503
- yield* authorize(node, identity, EXECUTE, at);
1504
- node = node.entries.get(link.key) === link.ino ? (yield* view(link.ino)) : undefined;
1505
- }
1506
- return node;
1507
- });
1508
- // Walks the tree below the directory `first` lists, depth first. Every later directory is read in its own
1509
- // observation, so a walk holds one permit at a time and never a handle; it writes nothing, so it refreshes no
1510
- // access time. Each directory is reached by name, so it needs search permission on the directories above it,
1511
- // and one that left the tree after it was listed has nothing to walk. A directory past `maxDepth` is never
1512
- // read. `locate` names an entry's path in a failure, and `listable` authorizes reading a directory. Entries
1513
- // gathered before a failure are handed on before the failure is.
1514
- const walkFrames = (op, first, plan, locate, listable) => Stream.suspend(() => {
1515
- let pending;
1516
- let entries = 0;
1517
- let bytes = 0n;
1518
- let failure;
1519
- const list = (frame) => coordinatedRead(op, Effect.suspend(() => {
1520
- if (TokenRegistry.inode(reference) === undefined)
1521
- return Effect.fail(op.fail("ClosedCaller"));
1522
- const at = op.at(locate(frame.path));
1523
- return Effect.flatMap(reachFrame(frame, at), (node) => node?.kind !== "directory" || node.metadata.nlink === 0
1524
- ? Effect.succeed([])
1525
- : Effect.andThen(listable(node, at), walkChildren(node, frame.path, frame.depth, frame)));
1526
- }));
1527
- const admit = (frame) => {
1528
- entries++;
1529
- bytes += frame.bytes;
1530
- return plan.limit(frame, { entries, bytes }, "entry");
1531
- };
1532
- const step = Effect.gen(function* () {
1533
- // Preserve the generic limit error E; yielding the error directly widens it to WalkFailure.
1534
- // oxlint-disable-next-line effecttsgo/unnecessary-fail-yieldable-error -- E must stay narrow for unbounded removal.
1535
- if (failure !== undefined)
1536
- return yield* Effect.fail(failure);
1537
- if (pending === undefined)
1538
- pending = (yield* first).reverse();
1539
- const out = [];
1540
- const stop = (error) => {
1541
- if (out.length === 0)
1542
- return Effect.fail(error);
1543
- failure = error;
1544
- return Effect.succeed([out, Option.some(undefined)]);
1545
- };
1546
- while (out.length < WALK_CHUNK_ENTRIES) {
1547
- const frame = pending.pop();
1548
- if (frame === undefined)
1549
- return [out, Option.none()];
1550
- const unlisted = frame.kind === "directory" && !frame.listed;
1551
- if (!unlisted || plan.order === "pre") {
1552
- const rejected = admit(frame);
1553
- if (rejected !== undefined)
1554
- return yield* stop(rejected);
1555
- out.push(frame);
1556
- }
1557
- if (unlisted) {
1558
- const rejected = plan.limit(frame, { entries, bytes }, "directory");
1559
- if (rejected !== undefined)
1560
- return yield* stop(rejected);
1561
- const listed = yield* Effect.result(list(frame));
1562
- if (Result.isFailure(listed))
1563
- return yield* stop(listed.failure);
1564
- if (plan.order === "post")
1565
- pending.push({ ...frame, listed: true });
1566
- for (let index = listed.success.length - 1; index >= 0; index--) {
1567
- const child = listed.success[index];
1568
- if (child !== undefined)
1569
- pending.push(child);
1570
- }
1571
- return [out, Option.some(undefined)];
1572
- }
1573
- }
1574
- return [out, Option.some(undefined)];
1575
- });
1576
- return Stream.paginate(undefined, () => step);
1577
- });
1578
- // Empties the directory an entry names, entries before their directories, each removal its own change. Each
1579
- // entry is removed by its name in the directory the walk reached by name from the target's name, and only
1580
- // while that name still holds the object the walk listed, so a subtree renamed out of the target, the target
1581
- // renamed away, and a replacement created under a listed name are all left alone. It stops at the first
1582
- // failure, which names the entry's path under the one the caller gave. Removing an empty directory reveals nothing, so only a directory with entries must be readable, and
1583
- // no permission is changed to make one so. With `force`, the entry itself going missing leaves nothing to
1584
- // empty.
1585
- const emptyDirectory = Effect.fnUntraced(function* (prepared, op, force) {
1586
- const prefix = prepared.kind === "path" ? prepared.path.bytes : nameBytes(prepared.name);
1587
- const locate = (path) => ownedPath(joinPath(prefix, path));
1588
- const listable = (directory, at) => directory.entries.size === 0 ? Effect.void : authorize(directory, identity, READ, at);
1589
- let anchor;
1590
- let targetAt = preparedOp(prepared, op);
1591
- const first = coordinatedRead(op, Effect.gen(function* () {
1592
- const entry = yield* resolveEntry(prepared, op);
1593
- const parent = yield* directoryNow(entry.parent);
1594
- yield* authorize(parent, identity, EXECUTE, entry.op);
1595
- const childIno = entry.name === undefined ? undefined : parent.entries.get(entry.name);
1596
- const child = childIno === undefined ? undefined : (yield* view(childIno));
1597
- if (entry.name === undefined || child === undefined)
1598
- return yield* entry.op.fail("NotFound");
1599
- if (child.kind !== "directory")
1600
- return [];
1601
- yield* listable(child, entry.op);
1602
- anchor = anchorFrame(parent, entry.name, child);
1603
- targetAt = entry.op;
1604
- return yield* walkChildren(child, new Uint8Array(0), 0, anchor);
1605
- })).pipe(Effect.catchIf((error) => force && error.code === "NotFound", () => Effect.succeed([])));
202
+ const wrapContext = (context) => Object.freeze({
203
+ ...context.operations,
204
+ [CallerId]: true,
205
+ limits,
206
+ watch: Effect.fnUntraced(function* (input, options) {
207
+ const op = OpContext.make("watch");
208
+ const decoded = yield* Effect.fromResult(decodeConfiguration(CallerWatchOptions, options ?? {}, "watch"));
1606
209
  const seams = yield* VolumeTestSeams;
1607
- const removeFrame = (frame) => {
1608
- const at = op.at(locate(frame.path));
1609
- return Effect.andThen(seams.beforeTreeRemoval, coordinated(op, Effect.gen(function* () {
1610
- if (anchor !== undefined && !holds(yield* view(anchor.parent), anchor)) {
1611
- return yield* (force ? Effect.void : Effect.fail(targetAt.fail("NotFound")));
1612
- }
1613
- return yield* Effect.flatMap(frame.up === undefined ? view(frame.parent) : reachFrame(frame.up, at), (parent) => !holds(parent, frame)
1614
- ? Effect.fail(at.fail("NotFound"))
1615
- : removeEntry({
1616
- parent: parent.ino,
1617
- name: frame.key,
1618
- trailingSlash: false,
1619
- addressing: "entry",
1620
- op: at
1621
- }, op));
1622
- })));
1623
- };
1624
- const plan = { order: "post", limit: () => undefined };
1625
- return yield* Stream.runForEach(walkFrames(op, first, plan, locate, listable), removeFrame);
1626
- });
1627
- const rootOp = OpContext.make("root");
1628
- const entryVerb = Effect.fnUntraced(function* (operation, input, body) {
1629
- const op = OpContext.make(operation);
1630
- const prepared = yield* Effect.fromResult(prepareEntry(input, op));
1631
- return yield* coordinated(op, Effect.flatMap(resolveEntry(prepared, op), (entry) => body(entry, op)));
1632
- });
1633
- const openDispatch = Effect.fn("Caller.open")(function* (input, options) {
1634
- const op = OpContext.make("open");
1635
- if (isEntry(input))
1636
- return yield* openEntry(input, options, op);
1637
- const target = asTarget(input);
1638
- if (Target.$is("Path")(target))
1639
- return yield* openPath(target, options, op);
1640
- return yield* openNode(target, options, op);
1641
- });
1642
- function open(input, options) {
1643
- return openDispatch(input, options);
1644
- }
1645
- const removeDispatch = Effect.fn("Caller.remove")(function* (input, options) {
1646
- const op = OpContext.make("remove");
1647
- const prepared = yield* Effect.fromResult(prepareEntry(input, op));
1648
- const chosen = yield* decodeRemoveOptions(options ?? {}).pipe(Effect.mapError((cause) => fail(preparedOp(prepared, op), cause)));
1649
- const once = coordinated(op, Effect.flatMap(resolveEntry(prepared, op), (entry) => removeEntry(entry, op)));
1650
- const target = (effect) => chosen.force === true
1651
- ? Effect.catchIf(effect, (error) => error.code === "NotFound", () => Effect.undefined)
1652
- : effect;
1653
- const removed = yield* Effect.result(target(once));
1654
- if (Result.isSuccess(removed))
1655
- return removed.success;
1656
- if (chosen.recursive !== true || removed.failure.code !== "NotEmpty")
1657
- return yield* removed.failure;
1658
- yield* emptyDirectory(prepared, op, chosen.force === true);
1659
- return yield* target(once);
1660
- });
1661
- function remove(input, options) {
1662
- return removeDispatch(input, options);
1663
- }
1664
- const caller = Object.freeze({
1665
- [CallerId]: true,
1666
- root: coordinatedRead(rootOp, Effect.suspend(() => TokenRegistry.inode(reference) === undefined
1667
- ? Effect.fail(rootOp.fail("ClosedCaller"))
1668
- : Effect.succeed(referenceFor(ROOT_INO)))).pipe(Effect.withSpan("Caller.root")),
1669
- lookup: Effect.fn("Caller.lookup")(function* (input) {
1670
- const op = OpContext.make("lookup");
1671
- const prepared = yield* Effect.fromResult(prepareEntry(input, op));
1672
- return yield* coordinatedRead(op, Effect.gen(function* () {
1673
- const entry = yield* resolveEntry(prepared, op);
1674
- const directory = yield* directoryNow(entry.parent);
1675
- if (isDotComponent(entry.name)) {
1676
- return yield* entry.op.fail(entry.addressing === "entry" ? "InvalidArgument" : "NotFound");
1677
- }
1678
- yield* authorize(directory, identity, EXECUTE, entry.op);
1679
- const child = directory.entries.get(entry.name);
1680
- if (child === undefined)
1681
- return yield* entry.op.fail("NotFound");
1682
- return referenceFor(child);
1683
- }));
1684
- }),
1685
- parent: Effect.fn("Caller.parent")(function* (input) {
1686
- const op = OpContext.make("parent");
1687
- const target = asTarget(input);
1688
- return yield* coordinatedRead(op, Effect.gen(function* () {
1689
- const directory = yield* resolveDirectory(target, op);
1690
- yield* authorize(directory.node, identity, EXECUTE, directory.op);
1691
- return referenceFor(directory.node.parent);
1692
- }));
1693
- }),
1694
- stat: Effect.fn("Caller.stat")(function* (input) {
1695
- const op = OpContext.make("stat");
1696
- const target = asTarget(input);
1697
- return yield* coordinatedRead(op, Effect.map(resolveTarget(target, op), (resolved) => withMetadata(resolved.node)));
1698
- }),
1699
- readDirectory: Effect.fn("Caller.readDirectory")(function* (input) {
1700
- const op = OpContext.make("readDirectory");
1701
- const target = asTarget(input);
1702
- return yield* accessing(op, Effect.gen(function* () {
1703
- const directory = yield* resolveDirectory(target, op);
1704
- yield* authorize(directory.node, identity, READ, directory.op);
1705
- const value = Object.freeze([...directory.node.entries].map(([name, child]) => Object.freeze({ name: nameBytes(name), reference: referenceFor(child) })));
1706
- const access = { node: directory.node, now: yield* timestamp(op) };
1707
- return { value: Object.freeze({ value, revision: directory.node.revision }), access };
1708
- }));
1709
- }),
1710
- walk: (input, options) => {
1711
- const op = OpContext.make("walk");
1712
- const target = asTarget(input);
1713
- const prefix = Target.$is("Path")(target) ? Result.getOrUndefined(inputBytes(target.path)) : undefined;
1714
- const locate = (path) => ownedPath(prefix === undefined ? path : joinPath(prefix, path));
1715
- const readable = (directory, at) => authorize(directory, identity, READ, at);
1716
- return Stream.unwrap(Effect.gen(function* () {
1717
- const chosen = yield* decodeWalkOptions(options ?? {}).pipe(Effect.mapError((cause) => fail(Target.$is("Path")(target) ? op.at(target.path) : op, cause)));
1718
- const first = coordinatedRead(op, Effect.gen(function* () {
1719
- const directory = yield* resolveDirectory(target, op);
1720
- yield* authorize(directory.node, identity, READ, directory.op);
1721
- return yield* walkChildren(directory.node, new Uint8Array(0), 0, yield* pathAnchor(target, directory.node, op));
1722
- }));
1723
- const exceeded = (frame, field) => makeError({ code: "LimitExceeded", operation: op.operation, field, path: errorPath(locate(frame.path)) });
1724
- const maxBytes = chosen.maxBytes === undefined ? undefined : ByteSize.toBigInt(chosen.maxBytes);
1725
- const plan = {
1726
- order: chosen.order ?? "pre",
1727
- limit: (frame, progress, phase) => {
1728
- if (chosen.maxDepth !== undefined && frame.depth > chosen.maxDepth)
1729
- return exceeded(frame, "maxDepth");
1730
- if (phase === "directory")
1731
- return undefined;
1732
- if (chosen.maxEntries !== undefined && progress.entries > chosen.maxEntries) {
1733
- return exceeded(frame, "maxEntries");
1734
- }
1735
- return maxBytes !== undefined && progress.bytes > maxBytes ? exceeded(frame, "maxBytes") : undefined;
1736
- }
1737
- };
1738
- return Stream.map(walkFrames(op, first, plan, locate, readable), (frame) => Object.freeze({
1739
- path: ownedPath(frame.path),
1740
- name: frame.name,
1741
- reference: frame.reference,
1742
- directory: frame.directory,
1743
- kind: frame.kind,
1744
- depth: frame.depth
1745
- }));
1746
- })).pipe(Stream.withSpan("Caller.walk"));
1747
- },
1748
- readLink: Effect.fn("Caller.readLink")(function* (input) {
1749
- const op = OpContext.make("readLink");
1750
- const target = asTarget(input);
1751
- return yield* coordinatedRead(op, Effect.gen(function* () {
1752
- const resolved = yield* resolveTarget(target, op, { followFinalSymlink: false, final: true });
1753
- if (resolved.node.kind !== "symlink")
1754
- return yield* resolved.op.fail("InvalidArgument");
1755
- return new Uint8Array(resolved.node.target);
1756
- }));
1757
- }),
1758
- realPath: Effect.fn("Caller.realPath")(function* (input) {
1759
- const op = OpContext.make("realPath");
1760
- const target = asTarget(input);
1761
- return yield* coordinatedRead(op, Effect.gen(function* () {
1762
- if (Target.$is("Path")(target)) {
1763
- const pathOp = op.at(target.path);
1764
- const result = yield* resolveTarget(target, op, { final: true });
1765
- const directory = result.node.kind === "directory"
1766
- ? result.node
1767
- : result.kind === "entry"
1768
- ? result.parent
1769
- : undefined;
1770
- const prefix = directory === undefined ? SLASH_HEX : yield* pathOfCurrent(directory.ino);
1771
- if (prefix === undefined)
1772
- return yield* pathOp.fail("NotFound");
1773
- if (result.node.kind === "directory" || result.kind === "node")
1774
- return ownedPath(nameBytes(prefix));
1775
- return ownedPath(nameBytes(prefix + (prefix === SLASH_HEX ? "" : SLASH_HEX) + result.name));
1776
- }
1777
- const resolved = yield* resolveTarget(target, op);
1778
- const path = yield* pathOfCurrent(resolved.node.ino);
1779
- if (path === undefined)
1780
- return yield* resolved.op.fail("NotFound");
1781
- return ownedPath(nameBytes(path));
1782
- }));
1783
- }),
1784
- access: Effect.fn("Caller.access")(function* (input, bits = 0) {
1785
- const op = OpContext.make("access");
1786
- const target = asTarget(input);
1787
- if (!Number.isInteger(bits) || bits < 0 || bits > (READ | WRITE | EXECUTE)) {
1788
- return yield* (Target.$is("Path")(target) ? op.at(target.path) : op).fail("InvalidArgument");
1789
- }
1790
- return yield* coordinatedRead(op, Effect.map(resolveTarget(target, op), (resolved) => {
1791
- const node = resolved.node;
1792
- let granted = 0;
1793
- for (const bit of [READ, WRITE, EXECUTE]) {
1794
- if ((bits & bit) === 0)
1795
- continue;
1796
- if (bit === EXECUTE && node.kind === "file" && (node.metadata.mode & ANY_EXECUTE) === 0)
1797
- continue;
1798
- if (identity.privileged || (permitted(node, identity) & bit) !== 0)
1799
- granted |= bit;
1800
- }
1801
- return granted;
1802
- }));
1803
- }),
1804
- readFile: Effect.fn("Caller.readFile")(function* (input) {
1805
- const op = OpContext.make("readFile");
1806
- const target = asTarget(input);
1807
- return yield* accessing(op, Effect.gen(function* () {
1808
- const resolved = yield* resolveTarget(target, op);
1809
- const node = resolved.node;
1810
- if (node.kind === "symlink")
1811
- return yield* resolved.op.fail("SymlinkLoop");
1812
- if (node.kind !== "file")
1813
- return yield* resolved.op.fail("IsDirectory");
1814
- yield* authorize(node, identity, READ, resolved.op);
1815
- const data = new Uint8Array(node.data);
1816
- return { value: data, access: { node, now: yield* timestamp(op) } };
1817
- }));
1818
- }),
1819
- writeFile: Effect.fn("Caller.writeFile")(function* (input, bytes, options) {
1820
- const op = OpContext.make("writeFile");
1821
- const prepared = yield* Effect.fromResult(prepareEntry(input, op));
1822
- const optionsOp = preparedOp(prepared, op);
1823
- if (!isAttachedBytes(bytes))
1824
- return yield* optionsOp.fail("InvalidArgument");
1825
- const captured = new Uint8Array(bytes);
1826
- const chosen = yield* decodeWriteFileOptions(options).pipe(Effect.mapError((cause) => fail(optionsOp, cause)));
1827
- return yield* coordinated(op, Effect.gen(function* () {
1828
- const resolved = yield* resolver.resolve(prepared, {
1829
- kind: "OrCreate",
1830
- create: chosen.create ?? "never",
1831
- finalSymlink: chosen.replaceFinalSymlink !== true && chosen.followFinalSymlink !== false
1832
- ? "follow"
1833
- : "preserve",
1834
- entryExclusive: "afterSymlink"
1835
- }, op);
1836
- const { parent, name, trailingSlash, op: entryOp } = resolved;
1837
- const found = resolved.kind === "existing" ? resolved.node : undefined;
1838
- if (found?.kind === "directory" || (found === undefined && trailingSlash)) {
1839
- return yield* entryOp.fail("IsDirectory");
1840
- }
1841
- const replaced = found?.kind === "symlink" ? found : undefined;
1842
- if (replaced !== undefined && !chosen.replaceFinalSymlink)
1843
- return yield* entryOp.fail("SymlinkLoop");
1844
- if (chosen.access === "read")
1845
- return yield* entryOp.fail("InvalidHandle");
1846
- const file = found?.kind === "file" ? found : undefined;
1847
- if (file === undefined) {
1848
- yield* authorize(parent, identity, WRITE | EXECUTE, entryOp);
1849
- if (replaced !== undefined)
1850
- yield* authorizeRemoval(parent, replaced, entryOp);
1851
- yield* (replaced === undefined ? reserveEntry(entryOp) : reserveInode(entryOp));
1852
- }
1853
- else {
1854
- yield* authorize(file, identity, chosen.access === "readWrite" ? READ | WRITE : WRITE, entryOp);
1855
- }
1856
- const finalMode = chosen.finalMode === undefined ? undefined : yield* permittedMode(file?.metadata ?? { kind: "file", uid: identity.uid, gid: parent.metadata.gid }, chosen.finalMode, entryOp);
1857
- const previous = file?.data.length ?? 0;
1858
- const initial = chosen.truncate ? 0 : previous;
1859
- const position = chosen.append ? initial : 0;
1860
- const size = Math.max(initial, position + captured.length);
1861
- if (size > maxFileBytes)
1862
- return yield* entryOp.fail("FileTooLarge");
1863
- const reclaimed = replaced !== undefined && replaced.metadata.nlink === 1 ? replaced.target.length : 0;
1864
- yield* reserveBytes(entryOp, BigInt(size - previous) - BigInt(reclaimed));
1865
- if (file !== undefined && !chosen.truncate && captured.length === 0 && chosen.finalMode === undefined) {
1866
- return;
1867
- }
1868
- let data = captured;
1869
- if (position !== 0 || size !== captured.length) {
1870
- data = new Uint8Array(size);
1871
- if (file !== undefined && !chosen.truncate)
1872
- data.set(file.data);
1873
- data.set(captured, position);
1874
- }
1875
- const now = yield* timestamp(op);
1876
- const d = yield* Draft;
1877
- const node = file ??
1878
- (yield* newFile(parent, new Uint8Array(0), (chosen.mode ?? 0o666) & 0o777 & ~umask, now));
1879
- const written = {
1880
- ...node,
1881
- data,
1882
- metadata: {
1883
- ...node.metadata,
1884
- mode: finalMode ?? node.metadata.mode & ~SET_ID_BITS,
1885
- size: BigInt(size),
1886
- mtimeNs: now,
1887
- ctimeNs: now
1888
- }
1889
- };
1890
- yield* d.addUsedBytes(BigInt(size - previous));
1891
- if (file === undefined) {
1892
- if (replaced !== undefined)
1893
- yield* detach(replaced, parent.ino, name, now);
1894
- yield* attach(yield* directoryNow(parent.ino), name, written, now);
1895
- if (replaced === undefined)
1896
- yield* d.addEntries(1);
1897
- yield* publishEntry(replaced === undefined ? "Create" : "Update", parent.ino, name, written.ino);
1898
- }
1899
- else {
1900
- yield* d.put(written);
1901
- yield* publishNode(written.ino);
1902
- }
1903
- }));
1904
- }),
1905
- open,
1906
- mkdir: Effect.fn("Caller.mkdir")(function* (input, options = {}) {
1907
- const op = OpContext.make("mkdir");
1908
- const prepared = yield* Effect.fromResult(prepareEntry(input, op));
1909
- const optionsOp = preparedOp(prepared, op);
1910
- const decoded = yield* decodeMkdirOptions(options).pipe(Effect.mapError((cause) => fail(optionsOp, cause)));
1911
- const chosen = { ...decoded };
1912
- if (chosen.exactMode && chosen.mode === undefined)
1913
- return yield* optionsOp.fail("InvalidArgument");
1914
- return yield* coordinated(op, Effect.gen(function* () {
1915
- const request = { mode: chosen.mode ?? 0o777, exactMode: chosen.exactMode, times: chosen.times };
1916
- const { child, directory } = chosen.recursive === true
1917
- ? yield* makeDirectories(prepared, request, op)
1918
- : yield* makeDirectory(yield* resolveEntry(prepared, op), request, op);
1919
- return { reference: referenceFor(child), directory };
1920
- }));
1921
- }),
1922
- symlink: Effect.fn("Caller.symlink")(function* (target, input, options = {}) {
1923
- const op = OpContext.make("symlink");
1924
- const targetOp = op.at(target);
1925
- const prepared = yield* Effect.fromResult(prepareEntry(input, op));
1926
- const optionsOp = preparedOp(prepared, op);
1927
- const decoded = yield* decodeSymlinkOptions(options).pipe(Effect.mapError((cause) => fail(optionsOp, cause)));
1928
- const rawTarget = inputBytes(target);
1929
- if (Result.isFailure(rawTarget))
1930
- return yield* targetOp.fail(rawTarget.failure);
1931
- if (rawTarget.success.includes(0))
1932
- return yield* targetOp.fail("InvalidArgument");
1933
- const targetBytes = new Uint8Array(rawTarget.success);
1934
- const chosen = { ...decoded };
1935
- return yield* coordinated(op, Effect.gen(function* () {
1936
- const entry = yield* resolveEntry(prepared, op);
1937
- const { child, directory } = yield* makeSymlink(entry, targetBytes, chosen.times, op);
1938
- return { reference: referenceFor(child), directory };
1939
- }));
1940
- }),
1941
- link: Effect.fn("Caller.link")(function* (sourceInput, input) {
1942
- const op = OpContext.make("link");
1943
- const source = asTarget(sourceInput);
1944
- const prepared = yield* Effect.fromResult(prepareEntry(input, op));
1945
- return yield* coordinated(op, Effect.gen(function* () {
1946
- const resolved = yield* resolveTarget(source, op, { followFinalSymlink: false });
1947
- const node = resolved.node;
1948
- if (node.kind === "directory")
1949
- return yield* resolved.op.fail("IsDirectory");
1950
- if (node.metadata.nlink === 0)
1951
- return yield* resolved.op.fail("StaleReference");
1952
- const entry = yield* resolveEntry(prepared, op);
1953
- const directory = yield* linkNode(node, entry, op);
1954
- return { reference: referenceFor(node.ino), directory };
1955
- }));
1956
- }),
1957
- unlink: Effect.fn("Caller.unlink")(function* (input) {
1958
- return yield* entryVerb("unlink", input, unlinkEntry);
1959
- }),
1960
- rmdir: Effect.fn("Caller.rmdir")(function* (input) {
1961
- return yield* entryVerb("rmdir", input, rmdirEntry);
1962
- }),
1963
- remove,
1964
- rename: Effect.fn("Caller.rename")(function* (fromInput, toInput) {
1965
- const op = OpContext.make("rename");
1966
- const from = yield* Effect.fromResult(prepareEntry(fromInput, op));
1967
- const to = yield* Effect.fromResult(prepareEntry(toInput, op));
1968
- return yield* coordinated(op, Effect.gen(function* () {
1969
- const source = yield* resolveEntry(from, op);
1970
- const destination = yield* resolveEntry(to, op);
1971
- return yield* renameEntry(source, destination, op);
1972
- }));
1973
- }),
1974
- chmod: Effect.fn("Caller.chmod")(function* (input, mode) {
1975
- const op = OpContext.make("chmod");
1976
- const target = asTarget(input);
1977
- if (!isMode(mode))
1978
- return yield* op.fail("InvalidArgument");
1979
- yield* changeAttributes(() => asResolvedNode(target, op), { mode }, op);
1980
- }),
1981
- chown: Effect.fn("Caller.chown")(function* (input, owner) {
1982
- const op = OpContext.make("chown");
1983
- const target = asTarget(input);
1984
- const decoded = yield* decodeOwnerUpdate(owner).pipe(Effect.mapError((cause) => fail(op, cause)));
1985
- yield* changeAttributes(() => asResolvedNode(target, op), { owner: decoded }, op);
1986
- }),
1987
- utimes: Effect.fn("Caller.utimes")(function* (input, times) {
1988
- const op = OpContext.make("utimes");
1989
- const target = asTarget(input);
1990
- const decoded = yield* decodeTimes(times).pipe(Effect.mapError((cause) => fail(op, cause)));
1991
- yield* changeAttributes(() => asResolvedNode(target, op), { times: decoded }, op);
1992
- }),
1993
- // A negative length fails before the target resolves, as truncate(2) rejects it before the lookup.
1994
- truncate: Effect.fn("Caller.truncate")(function* (input, length) {
1995
- const op = OpContext.make("truncate");
1996
- const target = asTarget(input);
1997
- if (!isLength(length))
1998
- return yield* op.fail("InvalidArgument");
1999
- yield* changeAttributes(() => asResolvedNode(target, op), { size: length }, op);
2000
- }),
2001
- setattr: Effect.fn("Caller.setattr")(function* (input, attributes) {
2002
- const op = OpContext.make("setattr");
2003
- const target = asTarget(input);
2004
- // A caller outside TypeScript can pass anything, so a non-object fails typed rather than as a defect;
2005
- // the check reads a widened copy so the attributes keep their type below.
2006
- const raw = attributes;
2007
- if (!Predicate.isObject(raw))
2008
- return yield* fail(op);
2009
- const unknown = Object.keys(attributes).find((key) => !SETATTR_FIELDS.includes(key));
2010
- if (unknown !== undefined)
2011
- return yield* fail(op, undefined, unknown);
2012
- if (attributes.size !== undefined && !isLength(attributes.size))
2013
- return yield* fail(op, undefined, "size");
2014
- if (attributes.mode !== undefined && !isMode(attributes.mode))
2015
- return yield* fail(op, undefined, "mode");
2016
- const owner = attributes.owner === undefined
2017
- ? undefined
2018
- : yield* decodeOwnerUpdate(attributes.owner).pipe(Effect.mapError((cause) => fail(op, cause, "owner")));
2019
- const times = attributes.times === undefined
2020
- ? undefined
2021
- : yield* decodeTimes(attributes.times).pipe(Effect.mapError((cause) => fail(op, cause, "times")));
2022
- const expected = attributes.expected === undefined
2023
- ? undefined
2024
- : yield* decodeExpected(attributes.expected).pipe(Effect.mapError((cause) => fail(op, cause, "expected")));
2025
- yield* changeAttributes(() => asResolvedNode(target, op), { size: attributes.size, mode: attributes.mode, owner, times, expected }, op);
2026
- }),
2027
- withDirectory: Effect.fn("Caller.withDirectory")(function* (input) {
2028
- const acquired = yield* acquireDirectory(input, OpContext.make("withDirectory"));
2029
- return createCaller(acquired, identity, umask);
2030
- }),
2031
- openDirectory: Effect.fn("Caller.openDirectory")(function* (input) {
2032
- const acquired = yield* acquireDirectory(input, OpContext.make("openDirectory"));
2033
- const statOp = OpContext.make("stat");
2034
- const handle = Object.freeze({
2035
- [DirectoryHandleId]: true,
2036
- stat: coordinatedRead(statOp, Effect.gen(function* () {
2037
- const ino = TokenRegistry.inode(acquired);
2038
- const node = ino === undefined ? undefined : yield* view(ino);
2039
- return yield* (node === undefined
2040
- ? Effect.fail(statOp.fail("InvalidHandle"))
2041
- : Effect.succeed(withMetadata(node)));
2042
- })).pipe(Effect.withSpan("DirectoryHandle.stat")),
2043
- close: coordinatedCleanup(Effect.suspend(() => {
2044
- if (TokenRegistry.inode(acquired) === undefined) {
2045
- return Effect.fail(OpContext.make("close").fail("InvalidHandle"));
2046
- }
2047
- return releaseDirectory(acquired);
2048
- })).pipe(Effect.ensuring(closeReleasedScope(acquired)), Effect.withSpan("DirectoryHandle.close"))
2049
- });
2050
- registry.registerDirectory(handle, acquired);
2051
- return handle;
2052
- })
2053
- });
2054
- return caller;
2055
- };
210
+ const select = Effect.map(context.selectWatch(input, decoded), CallerWatch.make);
211
+ return Stream.map(yield* engine.admit(op, watchHub.subscribe(select, seams.afterSubscribe)), (event) => event.change);
212
+ }),
213
+ withRoot: Effect.fnUntraced(function* (input) {
214
+ return wrapContext(yield* context.withRoot(input));
215
+ }),
216
+ withDirectory: Effect.fnUntraced(function* (input) {
217
+ return wrapContext(yield* context.withDirectory(input));
218
+ })
219
+ });
2056
220
  const volume = Object.freeze({
2057
221
  [VolumeId]: true,
2058
222
  durability,
2059
223
  identity,
2060
224
  incarnation,
2061
225
  limits,
2062
- usage: coordinatedRead(OpContext.make("usage"), Effect.sync(() => ({ usedBytes: state.usedBytes, entries: state.entries })))
2063
- .pipe(Effect.withSpan("Volume.usage")),
2064
- watch: Effect.fn("Volume.watch")(function* (options) {
226
+ usage: engine.usage,
227
+ watch: Effect.fnUntraced(function* (options) {
2065
228
  const op = OpContext.make("watch");
2066
229
  const decoded = yield* Effect.fromResult(decodeConfiguration(WatchOptions, options === undefined ? {} : options, "watch"));
2067
230
  const recursive = decoded.recursive ?? true;
@@ -2069,53 +232,44 @@ export const makeVolume = Effect.fnUntraced(function* (source, commitProvider, o
2069
232
  const seams = yield* VolumeTestSeams;
2070
233
  // The scope is resolved while the registration holds the volume, so no change lands between the check and
2071
234
  // the subscription.
2072
- const select = Effect.andThen(checkAvailable("watch"), scope === undefined
235
+ const select = Effect.andThen(engine.checkAvailable("watch"), scope === undefined
2073
236
  ? Effect.succeed(recursive ? volumeSelection : scopedSelection(ROOT_INO, false))
2074
237
  : Effect.map(scopeRoot(scope, op), (root) => scopedSelection(root, recursive)));
2075
- const stream = yield* admit(op, watchHub.subscribe(select, seams.afterSubscribe));
238
+ const stream = yield* engine.admit(op, watchHub.subscribe(select, seams.afterSubscribe));
2076
239
  return Stream.map(stream, (event) => event.change);
2077
240
  }),
2078
- snapshot: coordinatedRead(OpContext.make("snapshot"), Effect.sync(() => Image.make(state))).pipe(Effect.withSpan("Volume.snapshot")),
2079
- referenceKey: Effect.fn("Volume.referenceKey")(function* (reference) {
241
+ snapshot: capture("snapshot", (state) => Effect.succeed(Image.make(state))),
242
+ referenceKey: Effect.fnUntraced(function* (reference) {
2080
243
  const op = OpContext.make("referenceKey");
2081
- return yield* coordinatedRead(op, Effect.gen(function* () {
2082
- const node = yield* TokenRegistry.nodeOrFail(yield* registry.resolve(reference, "reference"), "reference", op);
2083
- const ino = BigInt(node.ino);
2084
- return { identity: identityBytes.slice(), epoch: epochBytes.slice(), ino, tag: keyTag(ino) };
2085
- }));
244
+ const ino = BigInt(yield* engine.referenceInode(reference, op));
245
+ return { identity: identityBytes.slice(), epoch: epochBytes.slice(), ino, tag: keyTag(ino) };
2086
246
  }),
2087
- resolveReferenceKey: Effect.fn("Volume.resolveReferenceKey")(function* (key) {
247
+ resolveReferenceKey: (key) => {
2088
248
  const op = OpContext.make("resolveReferenceKey");
2089
- return yield* coordinatedRead(op, Effect.gen(function* () {
249
+ return engine.objectFor(Effect.gen(function* () {
2090
250
  if (!Schema.is(ReferenceKey)(key))
2091
251
  return yield* op.fail("InvalidReference");
2092
- // Another epoch numbered its objects on its own, so its key is another volume's even under this identity.
2093
252
  if (!sameBytes(key.identity, identityBytes) || !sameBytes(key.epoch, epochBytes)) {
2094
253
  return yield* op.fail("ForeignReference");
2095
254
  }
2096
- // Checked before the inode is looked up, so a guessed key learns nothing about which numbers are in use.
2097
255
  if (!sameTag(key.tag, keyTag(key.ino)))
2098
256
  return yield* op.fail("InvalidReference");
2099
- const ino = Ino(Number(key.ino));
2100
- yield* TokenRegistry.nodeOrFail(yield* registry.resolveInode(ino), "reference", op);
2101
- return referenceFor(ino);
2102
- }));
2103
- }),
2104
- caller: Effect.fn("Volume.caller")(function* (options) {
2105
- const decoded = yield* Effect.fromResult(decodeConfiguration(RootCallerOptions, options === undefined ? {} : options, "caller"));
2106
- const chosen = decoded.identity ?? { uid: 0, gid: 0, groups: [], privileged: true };
2107
- const identity = Object.freeze({ ...chosen, groups: Object.freeze([...chosen.groups]) });
2108
- return yield* coordinatedRead(OpContext.make("caller"), Effect.sync(() => createCaller(makeDirectoryReference(ROOT_INO), identity, decoded.umask ?? 0o022)));
257
+ return Ino(Number(key.ino));
258
+ }), op);
259
+ },
260
+ caller: Effect.fnUntraced(function* (options) {
261
+ return wrapContext(yield* engine.context(options));
2109
262
  })
2110
263
  });
2111
264
  const observe = Object.freeze({
2112
- changes: coordinatedRead(OpContext.make("changes"), Effect.suspend(() => observeChanges(state))),
2113
- capture: coordinatedRead(OpContext.make("capture"), captureState())
265
+ changes: capture("changes", observeChanges),
266
+ capture: captureState
2114
267
  });
2115
- return Object.freeze({ volume, shutdown, observe });
268
+ AdapterBinding.register(volume, engine);
269
+ return Object.freeze({ volume, shutdown: engine.shutdown, observe });
2116
270
  });
2117
271
  /** @internal */
2118
- export const make = Effect.fn("VirtualFileSystem.make")(function* (options) {
272
+ export const make = Effect.fnUntraced(function* (options) {
2119
273
  return (yield* makeVolume(VolumeSource.Empty(), memoryCommitProvider, options).pipe(Effect.catchIf((error) => Schema.is(VfsError)(error) && error.code !== "InvalidArgument", Effect.die))).volume;
2120
274
  });
2121
275
  /** @internal */
@@ -2132,6 +286,9 @@ export const prepareEmptyLiveImage = Effect.fnUntraced(function* (options) {
2132
286
  });
2133
287
  /** @internal */
2134
288
  export const openImageVolume = Effect.fnUntraced(function* (image, maxImageBytes, commit, durability = "memory-only", options) {
289
+ const decoded = yield* Effect.fromResult(Limits.configuration(options, "openImage"));
290
+ if (decoded.options.execution === "sync")
291
+ return yield* argumentFailure("openImage", "execution");
2135
292
  const restored = yield* LiveImage.decode(image, maxImageBytes);
2136
293
  const requested = yield* Effect.fromResult(Limits.fromOptions(options, "openImage"));
2137
294
  const limits = yield* Effect.fromResult(Limits.fromStored(restored.limits, requested));
@@ -2154,22 +311,22 @@ const changeOptions = (options) => {
2154
311
  return Effect.fromResult(decodeConfiguration(OverlayChangesOptions, options === undefined ? {} : options, "changes"));
2155
312
  };
2156
313
  /** @internal */
2157
- export const fromSnapshot = Effect.fn("VirtualFileSystem.fromSnapshot")(function* (snapshot, options) {
314
+ export const fromSnapshot = Effect.fnUntraced(function* (snapshot, options) {
2158
315
  return (yield* makeVolume(VolumeSource.Restored({ value: yield* Image.valueOf(snapshot) }), memoryCommitProvider, options)).volume;
2159
316
  }, Effect.mapError((error) => retargetFailure("fromSnapshot", error)));
2160
317
  /** @internal */
2161
- export const makeOverlay = Effect.fn("VirtualFileSystem.makeOverlay")(function* (base, options) {
318
+ export const makeOverlay = Effect.fnUntraced(function* (base, options) {
2162
319
  const value = yield* Image.valueOf(base);
2163
320
  const made = yield* Effect.mapError(makeVolume(VolumeSource.Restored({ value }), memoryCommitProvider, options), (error) => retargetFailure("makeOverlay", error));
2164
321
  const baseObservation = yield* observeChanges(value);
2165
322
  const overlay = Object.freeze({
2166
323
  ...made.volume,
2167
- changes: Effect.fn("OverlayVolume.changes")(function* (options) {
324
+ changes: Effect.fnUntraced(function* (options) {
2168
325
  const selected = yield* changeOptions(options);
2169
326
  const current = yield* made.observe.changes;
2170
327
  return publicChanges(compareOverlay(baseObservation, current, selected.includeTimestamps ?? false));
2171
328
  }),
2172
- capture: Effect.fn("OverlayVolume.capture")(function* (options) {
329
+ capture: Effect.fnUntraced(function* (options) {
2173
330
  const selected = yield* changeOptions(options);
2174
331
  const current = yield* made.observe.capture;
2175
332
  return Object.freeze({
@@ -2178,5 +335,6 @@ export const makeOverlay = Effect.fn("VirtualFileSystem.makeOverlay")(function*
2178
335
  });
2179
336
  })
2180
337
  });
338
+ AdapterBinding.alias(overlay, made.volume);
2181
339
  return overlay;
2182
340
  });