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