@effect-vfs/core 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -238
- package/dist/LiveVolume.d.ts.map +1 -1
- package/dist/LiveVolume.js +9 -9
- package/dist/Search.d.ts +730 -0
- package/dist/Search.d.ts.map +1 -0
- package/dist/Search.js +568 -0
- package/dist/SnapshotDelta.d.ts +234 -0
- package/dist/SnapshotDelta.d.ts.map +1 -1
- package/dist/SnapshotDelta.js +107 -0
- package/dist/VirtualFileSystem.d.ts +53 -1
- package/dist/VirtualFileSystem.d.ts.map +1 -1
- package/dist/VirtualFileSystem.js +59 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/internal/fixture.js +3 -3
- package/dist/internal/glob.d.ts +66 -0
- package/dist/internal/glob.d.ts.map +1 -0
- package/dist/internal/glob.js +248 -0
- package/dist/internal/image.d.ts +7 -1
- package/dist/internal/image.d.ts.map +1 -1
- package/dist/internal/image.js +9 -7
- package/dist/internal/liveImage.d.ts +1 -1
- package/dist/internal/liveImage.d.ts.map +1 -1
- package/dist/internal/liveImage.js +2 -10
- package/dist/internal/resolution.d.ts +135 -0
- package/dist/internal/resolution.d.ts.map +1 -0
- package/dist/internal/resolution.js +277 -0
- package/dist/internal/search.d.ts +24 -0
- package/dist/internal/search.d.ts.map +1 -0
- package/dist/internal/search.js +78 -0
- package/dist/internal/searchContent.d.ts +77 -0
- package/dist/internal/searchContent.d.ts.map +1 -0
- package/dist/internal/searchContent.js +223 -0
- package/dist/internal/searchModel.d.ts +83 -0
- package/dist/internal/searchModel.d.ts.map +1 -0
- package/dist/internal/searchModel.js +23 -0
- package/dist/internal/snapshotDelta.d.ts +470 -1
- package/dist/internal/snapshotDelta.d.ts.map +1 -1
- package/dist/internal/snapshotDelta.js +59 -25
- package/dist/internal/snapshotMerge.d.ts +54 -0
- package/dist/internal/snapshotMerge.d.ts.map +1 -0
- package/dist/internal/snapshotMerge.js +959 -0
- package/dist/internal/tokenRegistry.d.ts +77 -0
- package/dist/internal/tokenRegistry.d.ts.map +1 -0
- package/dist/internal/tokenRegistry.js +128 -0
- package/dist/internal/virtualFileSystem.d.ts +32 -17
- package/dist/internal/virtualFileSystem.d.ts.map +1 -1
- package/dist/internal/virtualFileSystem.js +263 -661
- package/dist/internal/volumeLimits.d.ts +53 -0
- package/dist/internal/volumeLimits.d.ts.map +1 -0
- package/dist/internal/volumeLimits.js +63 -0
- package/dist/internal/volumeState.d.ts +2 -0
- package/dist/internal/volumeState.d.ts.map +1 -1
- package/dist/internal/volumeState.js +9 -0
- package/package.json +5 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as Arr from "effect/Array";
|
|
2
2
|
import * as ByteSize from "effect/ByteSize";
|
|
3
|
+
import * as Cause from "effect/Cause";
|
|
3
4
|
import * as Clock from "effect/Clock";
|
|
4
5
|
import * as Context from "effect/Context";
|
|
5
6
|
import * as Crypto from "effect/Crypto";
|
|
@@ -13,16 +14,15 @@ import * as Option from "effect/Option";
|
|
|
13
14
|
import * as Predicate from "effect/Predicate";
|
|
14
15
|
import * as Result from "effect/Result";
|
|
15
16
|
import * as Schema from "effect/Schema";
|
|
16
|
-
import * as Scope from "effect/Scope";
|
|
17
17
|
import * as Semaphore from "effect/Semaphore";
|
|
18
18
|
import * as Stream from "effect/Stream";
|
|
19
19
|
import { BytePath } from "../BytePath.js";
|
|
20
|
-
import { CallerId, MkdirOptions,
|
|
20
|
+
import { CallerId, MkdirOptions, OpenEntryOptions, OpenOptions, RemoveOptions, RootCallerOptions, SetattrOptions, SymlinkOptions, WalkOptions, WriteFileOptions } from "../Caller.js";
|
|
21
21
|
import { DirectoryHandleId, FileHandleId, SeekMode } from "../FileHandle.js";
|
|
22
22
|
import { Mode, OwnerUpdate, Times } from "../Metadata.js";
|
|
23
|
-
import { isEntry,
|
|
23
|
+
import { isEntry, Target } from "../Target.js";
|
|
24
24
|
import { make as makeError } from "../VfsError.js";
|
|
25
|
-
import { OverlayChange, OverlayChangesOptions, ReferenceKey, VolumeId, VolumeIdentity, VolumeIncarnation
|
|
25
|
+
import { OverlayChange, OverlayChangesOptions, ReferenceKey, VolumeId, VolumeIdentity, VolumeIncarnation } from "../Volume.js";
|
|
26
26
|
import { WatchOptions } from "../Watch.js";
|
|
27
27
|
import { sameBytes } from "./bytes.js";
|
|
28
28
|
import { argumentFailure, decodeConfiguration, errorPath, fsFailure, imageFailure, OpContext, retargetFailure, VfsError } from "./errors.js";
|
|
@@ -33,10 +33,13 @@ import * as InodeTable from "./inodeTable.js";
|
|
|
33
33
|
import * as LiveImage from "./liveImage.js";
|
|
34
34
|
import * as MetadataDomain from "./metadata.js";
|
|
35
35
|
import { compareOverlay } from "./overlayDiff.js";
|
|
36
|
-
import {
|
|
36
|
+
import { inputBytes, isAttachedBytes, isDotComponent, joinPath, nameBytes, ownedPath, ROOT_PATH, SLASH_BYTE, SLASH_HEX } from "./path.js";
|
|
37
|
+
import * as Resolution from "./resolution.js";
|
|
37
38
|
import { VolumeTestSeams } from "./testSeams.js";
|
|
39
|
+
import * as TokenRegistry from "./tokenRegistry.js";
|
|
38
40
|
import { makeTurnstile } from "./turnstile.js";
|
|
39
|
-
import
|
|
41
|
+
import * as Limits from "./volumeLimits.js";
|
|
42
|
+
import { byEntryName, directoryMetadata, emptyState, getNode, Ino, reachableValue, ROOT_INO, storedMetadata, WALK_YIELD_INTERVAL } from "./volumeState.js";
|
|
40
43
|
import * as WatchHub from "./watchHub.js";
|
|
41
44
|
const EXECUTE = 0o1;
|
|
42
45
|
const WRITE = 0o2;
|
|
@@ -45,11 +48,23 @@ const ANY_EXECUTE = 0o111;
|
|
|
45
48
|
const SET_ID_BITS = 0o6000;
|
|
46
49
|
const STICKY_BIT = 0o1000;
|
|
47
50
|
const RELATIME_INTERVAL_NS = 86400000000000n; // relatime, as Linux mounts by default: a read refreshes an access time at least this old, 24 hours.
|
|
51
|
+
/** @internal */
|
|
52
|
+
export const memoryCommitProvider = Object.freeze({
|
|
53
|
+
mode: "memory",
|
|
54
|
+
commit: () => Effect.succeed("committed"),
|
|
55
|
+
shutdown: Effect.void
|
|
56
|
+
});
|
|
48
57
|
// An unknown commit outcome stops the volume; definite rejection stops it only during cleanup.
|
|
49
58
|
const offerCommit = (provider, operation, candidate, cleanup) => Effect.map(Effect.exit(Effect.suspend(() => provider.commit(candidate))), (committed) => {
|
|
50
59
|
// TODO(#186): Preserve the cause of a provider defect or interruption.
|
|
51
60
|
if (Exit.isFailure(committed)) {
|
|
52
|
-
|
|
61
|
+
if (Cause.hasDies(committed.cause) || Cause.hasInterrupts(committed.cause)) {
|
|
62
|
+
return { available: false, failure: fsFailure("OutcomeUnknown", operation) };
|
|
63
|
+
}
|
|
64
|
+
const failure = Cause.findErrorOption(committed.cause);
|
|
65
|
+
return Option.isSome(failure)
|
|
66
|
+
? { available: !cleanup, failure: failure.value }
|
|
67
|
+
: { available: false, failure: fsFailure("OutcomeUnknown", operation) };
|
|
53
68
|
}
|
|
54
69
|
if (committed.value === "rejected") {
|
|
55
70
|
return { available: !cleanup, failure: fsFailure("StorageRejected", operation) };
|
|
@@ -64,7 +79,7 @@ const MAX_FILE_OFFSET = 0x7fffffffffffffffn;
|
|
|
64
79
|
const isMode = Schema.is(Mode);
|
|
65
80
|
const isNatural = Schema.is(Schema.Natural);
|
|
66
81
|
const isLength = Schema.is(SetattrOptions.fields.size.schema);
|
|
67
|
-
const inGroup =
|
|
82
|
+
const inGroup = Resolution.inGroup;
|
|
68
83
|
const isTimestamp = Schema.is(MetadataDomain.Timestamp);
|
|
69
84
|
const isSeekMode = Schema.is(SeekMode);
|
|
70
85
|
const decodeOwnerUpdate = Schema.decodeEffect(OwnerUpdate, { onExcessProperty: "error" });
|
|
@@ -79,7 +94,6 @@ const decodeSymlinkOptions = Schema.decodeEffect(SymlinkOptions, { onExcessPrope
|
|
|
79
94
|
const decodeWalkOptions = Schema.decodeEffect(WalkOptions, { onExcessProperty: "error" });
|
|
80
95
|
const decodeRemoveOptions = Schema.decodeEffect(RemoveOptions, { onExcessProperty: "error" });
|
|
81
96
|
const decodeOpenEntryOptions = Schema.decodeEffect(OpenEntryOptions, { onExcessProperty: "error" });
|
|
82
|
-
const encoder = new TextEncoder();
|
|
83
97
|
// Whether a read at `now` refreshes a node's access time: when it is not newer than the last modification or
|
|
84
98
|
// status change, or when it is at least a day old. A time already equal to `now` is never refreshed, as Linux
|
|
85
99
|
// skips it, so reads at one instant (a frozen or coarse clock) store nothing.
|
|
@@ -196,10 +210,6 @@ class Draft extends Context.Service()("@effect-vfs/core/internal/virtualFileSyst
|
|
|
196
210
|
})
|
|
197
211
|
}) {
|
|
198
212
|
}
|
|
199
|
-
const objectReferences = new WeakMap();
|
|
200
|
-
const isFileHandle = (value) => Predicate.hasProperty(FileHandleId)(value);
|
|
201
|
-
const files = new WeakMap();
|
|
202
|
-
const handles = new WeakMap();
|
|
203
213
|
const withMetadata = (node) => ({ ...node.metadata, revision: node.revision });
|
|
204
214
|
const withEntries = (directory, edit) => {
|
|
205
215
|
const entries = new Map(directory.entries);
|
|
@@ -247,29 +257,17 @@ const randomHex128 = Effect.gen(function* () {
|
|
|
247
257
|
return Hex.encode(yield* crypto.randomBytes(16));
|
|
248
258
|
}).pipe(Effect.orDie);
|
|
249
259
|
/** @internal */
|
|
250
|
-
export const makeVolume = Effect.fnUntraced(function* (source,
|
|
260
|
+
export const makeVolume = Effect.fnUntraced(function* (source, commitProvider, options, durability = "memory-only") {
|
|
251
261
|
const services = yield* Effect.context();
|
|
252
262
|
// These accessors run inside synchronous transition callbacks. The runner is reached only when an invariant fails.
|
|
253
263
|
const dieInvariant = (message) => Effect.runSyncWith(services)(Effect.die(message));
|
|
254
264
|
const live = Predicate.isTagged("Live")(source) ? source.restored : undefined;
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
recovered.maxEntries = live.limits.maxEntries;
|
|
260
|
-
if (live.limits.maxBytes !== undefined)
|
|
261
|
-
recovered.maxBytes = ByteSize.bytes(live.limits.maxBytes);
|
|
262
|
-
if (live.limits.maxFileBytes !== undefined)
|
|
263
|
-
recovered.maxFileBytes = ByteSize.bytes(live.limits.maxFileBytes);
|
|
264
|
-
if (live.limits.maxPathBytes !== undefined)
|
|
265
|
-
recovered.maxPathBytes = ByteSize.bytes(live.limits.maxPathBytes);
|
|
266
|
-
restoredOptions = recovered;
|
|
267
|
-
}
|
|
268
|
-
const decoded = yield* Effect.fromResult(decodeConfiguration(VolumeOptions, restoredOptions === undefined ? {} : restoredOptions, "make"));
|
|
269
|
-
const settings = { ...decoded };
|
|
270
|
-
const identity = settings.identity === undefined
|
|
265
|
+
const decoded = yield* Effect.fromResult(Limits.configuration(options));
|
|
266
|
+
const requested = decoded.limits;
|
|
267
|
+
const limits = live === undefined ? requested : yield* Effect.fromResult(Limits.fromStored(live.limits, requested));
|
|
268
|
+
const identity = live?.identity ?? (decoded.identity === undefined
|
|
271
269
|
? VolumeIdentity.make(yield* randomHex128)
|
|
272
|
-
:
|
|
270
|
+
: decoded.identity);
|
|
273
271
|
const incarnation = VolumeIncarnation.make(yield* randomHex128);
|
|
274
272
|
const epoch = live === undefined ? VolumeEpoch.make(yield* randomHex128) : live.epoch;
|
|
275
273
|
const keySecret = live === undefined ? KeySecret.make(yield* randomHex128) : live.keySecret;
|
|
@@ -288,24 +286,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
288
286
|
: Effect.fail(op.fail("InvalidArgument"));
|
|
289
287
|
});
|
|
290
288
|
const volumeIdentity = Symbol();
|
|
291
|
-
const tokens = new Map();
|
|
292
289
|
const directoryHolds = new Map();
|
|
293
|
-
const
|
|
294
|
-
volume: volumeIdentity,
|
|
295
|
-
scope: undefined,
|
|
296
|
-
closed: false,
|
|
297
|
-
ino: undefined,
|
|
298
|
-
offset: 0n,
|
|
299
|
-
access,
|
|
300
|
-
append
|
|
301
|
-
});
|
|
302
|
-
const makeDirectoryReference = (ino) => ({
|
|
303
|
-
volume: volumeIdentity,
|
|
304
|
-
scope: undefined,
|
|
305
|
-
closed: false,
|
|
306
|
-
ino
|
|
307
|
-
});
|
|
308
|
-
const maxPendingOperations = settings.maxPendingOperations ?? 64;
|
|
290
|
+
const maxPendingOperations = limits.maxPendingOperations;
|
|
309
291
|
const permits = maxPendingOperations + 1;
|
|
310
292
|
const gate = Semaphore.makeUnsafe(permits);
|
|
311
293
|
// The gate hands permits to whichever waiter it can satisfy, so a change waiting for every permit would be
|
|
@@ -322,51 +304,36 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
322
304
|
onNone: () => op.fail("VolumeBusy"),
|
|
323
305
|
onSome: Effect.succeed
|
|
324
306
|
})));
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
inodes: InodeTable.set(InodeTable.empty(), ROOT_INO, root),
|
|
328
|
-
open: new Map(),
|
|
329
|
-
nextInode: Ino(2),
|
|
330
|
-
revision: 1n,
|
|
331
|
-
entries: 0,
|
|
332
|
-
usedBytes: 0n
|
|
333
|
-
};
|
|
334
|
-
const maxFileBytes = Number(ByteSize.toBigInt(settings.maxFileBytes ?? ByteSize.bytes(MAX_FILE_BYTES)));
|
|
335
|
-
const limits = Object.freeze({
|
|
336
|
-
maxBytes: settings.maxBytes,
|
|
337
|
-
maxFileBytes: ByteSize.bytes(maxFileBytes),
|
|
338
|
-
maxEntries: settings.maxEntries,
|
|
339
|
-
maxPathBytes: settings.maxPathBytes,
|
|
340
|
-
maxPendingOperations: settings.maxPendingOperations ?? 64,
|
|
341
|
-
maxWatchEvents: settings.maxWatchEvents ?? 256
|
|
342
|
-
});
|
|
307
|
+
let state = emptyState(initialTime);
|
|
308
|
+
const maxFileBytes = Number(ByteSize.toBigInt(limits.maxFileBytes));
|
|
343
309
|
if (Predicate.isTagged("Restored")(source)) {
|
|
344
310
|
const restored = yield* reachableValue(source.value);
|
|
345
311
|
if (restored.largestFile > maxFileBytes) {
|
|
346
312
|
return yield* imageFailure("snapshot", "LimitExceeded", { field: "maxFileBytes" });
|
|
347
313
|
}
|
|
348
|
-
if ((
|
|
349
|
-
(
|
|
314
|
+
if ((limits.maxEntries !== undefined && restored.state.entries > limits.maxEntries) ||
|
|
315
|
+
(limits.maxBytes !== undefined && restored.state.usedBytes > ByteSize.toBigInt(limits.maxBytes))) {
|
|
350
316
|
return yield* imageFailure("snapshot", "LimitExceeded", { field: "volume" });
|
|
351
317
|
}
|
|
352
318
|
state = restored.state;
|
|
353
319
|
}
|
|
354
320
|
if (live !== undefined)
|
|
355
321
|
state = live.value;
|
|
356
|
-
const initialImage = captureInitial === undefined
|
|
357
|
-
? undefined
|
|
358
|
-
: yield* captureInitial(state, { identity, epoch, keySecret }, limits);
|
|
359
322
|
const view = (ino) => Effect.flatMap(Effect.serviceOption(Draft), (draft) => Option.isSome(draft) ? draft.value.get(ino) : Effect.sync(() => getNode(state, ino)));
|
|
323
|
+
const registry = TokenRegistry.make(volumeIdentity, view);
|
|
324
|
+
const referenceFor = registry.referenceFor;
|
|
325
|
+
const makeFileReference = registry.file;
|
|
326
|
+
const makeDirectoryReference = registry.directory;
|
|
360
327
|
let available = true;
|
|
361
328
|
const checkAvailable = (operation) => Effect.suspend(() => available ? Effect.void : Effect.fail(fsFailure("VolumeUnavailable", operation)));
|
|
362
329
|
const watchCoordinate = (effect) => changing(effect);
|
|
363
|
-
const watchHub = yield* WatchHub.make(watchCoordinate,
|
|
330
|
+
const watchHub = yield* WatchHub.make(watchCoordinate, limits.maxWatchEvents);
|
|
364
331
|
const install = Effect.fnUntraced(function* (finished) {
|
|
365
332
|
const before = state;
|
|
366
333
|
state = yield* finished.finish;
|
|
367
334
|
const actions = yield* finished.actions;
|
|
368
335
|
for (const ino of actions.removed)
|
|
369
|
-
|
|
336
|
+
registry.forget(ino);
|
|
370
337
|
for (const apply of actions.after)
|
|
371
338
|
apply();
|
|
372
339
|
const after = state;
|
|
@@ -377,36 +344,33 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
377
344
|
const value = yield* Effect.provideService(change, Draft, current);
|
|
378
345
|
return [value, current];
|
|
379
346
|
});
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
yield*
|
|
383
|
-
yield* install(current);
|
|
384
|
-
});
|
|
385
|
-
// Offers the finished draft's value to the store before installing it. A rejected candidate is discarded and
|
|
386
|
-
// the volume stays available; an uncertain answer stops the volume. A change that is a cleanup runs its
|
|
387
|
-
// release even when the store refuses, since the handle must not stay open.
|
|
388
|
-
const committed = Effect.fnUntraced(function* (op, provider, finished, onStorageFailure) {
|
|
389
|
-
if (yield* finished.unchanged)
|
|
347
|
+
// Cleanup releases local ownership even when storage refuses; that divergence disables the volume.
|
|
348
|
+
const committed = Effect.fnUntraced(function* (op, finished, cleanup) {
|
|
349
|
+
if ((yield* finished.unchanged) || (cleanup && !available))
|
|
390
350
|
return yield* install(finished);
|
|
391
351
|
const next = yield* finished.finish;
|
|
392
|
-
|
|
393
|
-
yield* provider.prepare(next);
|
|
394
|
-
const answer = yield* offerCommit(provider, op.operation, next, onStorageFailure !== undefined);
|
|
352
|
+
const answer = yield* offerCommit(commitProvider, op.operation, next, cleanup);
|
|
395
353
|
if (!answer.available)
|
|
396
354
|
available = false;
|
|
397
355
|
if (answer.failure !== undefined) {
|
|
398
|
-
if (
|
|
399
|
-
yield*
|
|
356
|
+
if (cleanup)
|
|
357
|
+
yield* install(finished);
|
|
400
358
|
return yield* answer.failure;
|
|
401
359
|
}
|
|
402
360
|
yield* install(finished);
|
|
403
361
|
});
|
|
362
|
+
const applyCleanup = Effect.fnUntraced(function* (change) {
|
|
363
|
+
const current = yield* Draft.make(state);
|
|
364
|
+
yield* Effect.provideService(change(current), Draft, current);
|
|
365
|
+
yield* committed(OpContext.make("close"), current, true);
|
|
366
|
+
});
|
|
404
367
|
const annotateFailure = (error) => Effect.annotateCurrentSpan({ operation: error.operation, code: error.code });
|
|
405
|
-
// Permit waits stay interruptible.
|
|
406
|
-
//
|
|
407
|
-
const coordinated = (op, effect,
|
|
408
|
-
|
|
409
|
-
|
|
368
|
+
// Permit waits stay interruptible. Memory transitions are masked; durable transitions remain
|
|
369
|
+
// interruptible until the candidate is ready. Every provider commit and installation is masked.
|
|
370
|
+
const coordinated = (op, effect, cleanup = false) => admit(op, changing(Effect.uninterruptibleMask((restore) => {
|
|
371
|
+
const staged = transition(Effect.andThen(checkAvailable(op.operation), effect));
|
|
372
|
+
return Effect.flatMap(commitProvider.mode === "memory" ? staged : restore(staged), ([value, finished]) => Effect.as(committed(op, finished, cleanup), value));
|
|
373
|
+
}))).pipe(Effect.tapError((error) => Schema.is(VfsError)(error) ? annotateFailure(error) : Effect.void));
|
|
410
374
|
const coordinatedRead = (op, effect) => admit(op, observing(Effect.andThen(checkAvailable(op.operation), effect))).pipe(Effect.tapError((error) => Schema.is(VfsError)(error) ? annotateFailure(error) : Effect.void));
|
|
411
375
|
const refreshAccess = Effect.fnUntraced(function* (read) {
|
|
412
376
|
if (refreshDue(read)) {
|
|
@@ -423,9 +387,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
423
387
|
? coordinated(op, Effect.flatMap(read, refreshAccess))
|
|
424
388
|
: Effect.succeed(observed.value));
|
|
425
389
|
const coordinatedCleanup = (effect) => changing(Effect.uninterruptible(effect));
|
|
426
|
-
const shutdown = commitProvider ===
|
|
390
|
+
const shutdown = commitProvider.mode === "memory" ? commitProvider.shutdown : changing(Effect.uninterruptible(Effect.andThen(Effect.sync(() => {
|
|
427
391
|
available = false;
|
|
428
|
-
}));
|
|
392
|
+
}), commitProvider.shutdown)));
|
|
429
393
|
const pathOf = (get, ino) => {
|
|
430
394
|
const names = [];
|
|
431
395
|
let node = get(ino);
|
|
@@ -524,16 +488,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
524
488
|
includes: () => true,
|
|
525
489
|
rescan: () => ({ change: RescanChange.make({ path: rootPath() }), parent: ROOT_INO, ino: ROOT_INO })
|
|
526
490
|
};
|
|
527
|
-
const scopeRoot = (scope, op)
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
return Effect.fail(op.fail("InvalidReference"));
|
|
531
|
-
if (known.volume !== volumeIdentity)
|
|
532
|
-
return Effect.fail(op.fail("ForeignReference"));
|
|
533
|
-
const node = getNode(state, known.ino);
|
|
534
|
-
return node === undefined || node.metadata.nlink === 0
|
|
535
|
-
? Effect.fail(op.fail("StaleReference"))
|
|
536
|
-
: Effect.succeed(known.ino);
|
|
491
|
+
const scopeRoot = Effect.fnUntraced(function* (scope, op) {
|
|
492
|
+
const node = yield* TokenRegistry.nodeOrFail(yield* registry.resolve(scope, "reference"), "reference", op);
|
|
493
|
+
return node.metadata.nlink === 0 ? yield* op.fail("StaleReference") : node.ino;
|
|
537
494
|
});
|
|
538
495
|
// A watch of one object, and of its subtree when recursive. Membership is read from the installed tree when
|
|
539
496
|
// each event is offered, so it follows renames of the scope and its ancestors. When the object's last name is
|
|
@@ -566,10 +523,6 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
566
523
|
yield* (yield* VolumeTestSeams).betweenSnapshotAndSummary;
|
|
567
524
|
return { snapshot, observation: yield* observeChanges(captured) };
|
|
568
525
|
});
|
|
569
|
-
const addressable = (ino) => {
|
|
570
|
-
const node = getNode(state, ino);
|
|
571
|
-
return node !== undefined && !(node.kind === "directory" && node.metadata.nlink === 0);
|
|
572
|
-
};
|
|
573
526
|
const keyTag = (ino) => {
|
|
574
527
|
const message = new Uint8Array(40);
|
|
575
528
|
message.set(identityBytes);
|
|
@@ -577,15 +530,6 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
577
530
|
new DataView(message.buffer).setBigUint64(32, ino);
|
|
578
531
|
return hmacSha256(keySecretBytes, message).subarray(0, 16);
|
|
579
532
|
};
|
|
580
|
-
const referenceFor = (ino) => {
|
|
581
|
-
const existing = tokens.get(ino);
|
|
582
|
-
if (existing !== undefined)
|
|
583
|
-
return existing;
|
|
584
|
-
const reference = Object.freeze({ [ObjectReferenceId]: true });
|
|
585
|
-
objectReferences.set(reference, { volume: volumeIdentity, ino });
|
|
586
|
-
tokens.set(ino, reference);
|
|
587
|
-
return reference;
|
|
588
|
-
};
|
|
589
533
|
const holdDirectory = (ino) => {
|
|
590
534
|
directoryHolds.set(ino, (directoryHolds.get(ino) ?? 0) + 1);
|
|
591
535
|
};
|
|
@@ -598,32 +542,17 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
598
542
|
directoryHolds.delete(ino);
|
|
599
543
|
const node = getNode(state, ino);
|
|
600
544
|
if (node?.kind === "directory" && node.metadata.nlink === 0) {
|
|
601
|
-
yield*
|
|
545
|
+
yield* applyCleanup((d) => d.remove(ino));
|
|
602
546
|
}
|
|
603
547
|
});
|
|
604
|
-
const releaseDirectory =
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
reference
|
|
608
|
-
if (ino !== undefined)
|
|
609
|
-
yield* unholdDirectory(ino);
|
|
610
|
-
});
|
|
611
|
-
const finalizeDirectory = (reference) => Effect.uninterruptible(Effect.suspend(() => reference.closed
|
|
612
|
-
? Effect.void
|
|
613
|
-
: coordinatedCleanup(releaseDirectory(reference))));
|
|
614
|
-
const closeReleasedScope = (reference) => Effect.suspend(() => !reference.closed || reference.scope === undefined ? Effect.void : Scope.close(reference.scope, Exit.void));
|
|
615
|
-
const permitted = (node, identity) => {
|
|
616
|
-
const metadata = node.metadata;
|
|
617
|
-
const shift = metadata.uid === identity.uid ?
|
|
618
|
-
6
|
|
619
|
-
: inGroup(identity, metadata.gid)
|
|
620
|
-
? 3
|
|
621
|
-
: 0;
|
|
622
|
-
return (metadata.mode >> shift) & (READ | WRITE | EXECUTE);
|
|
548
|
+
const releaseDirectory = (reference) => TokenRegistry.release(reference, unholdDirectory, (action) => Effect.sync(action));
|
|
549
|
+
const finalizeDirectory = (reference) => {
|
|
550
|
+
const close = coordinatedCleanup(releaseDirectory(reference));
|
|
551
|
+
return TokenRegistry.finalize(reference, close, Effect.ignore(close));
|
|
623
552
|
};
|
|
624
|
-
const
|
|
625
|
-
|
|
626
|
-
|
|
553
|
+
const closeReleasedScope = TokenRegistry.closeReleasedScope;
|
|
554
|
+
const permitted = Resolution.permitted;
|
|
555
|
+
const authorize = Resolution.authorize;
|
|
627
556
|
const reclaim = Effect.fnUntraced(function* (d, ino) {
|
|
628
557
|
const node = yield* d.get(ino);
|
|
629
558
|
if (node === undefined)
|
|
@@ -644,7 +573,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
644
573
|
});
|
|
645
574
|
const atEntryLimit = Effect.gen(function* () {
|
|
646
575
|
const draft = yield* Draft;
|
|
647
|
-
return
|
|
576
|
+
return limits.maxEntries !== undefined && (yield* draft.entries) >= limits.maxEntries;
|
|
648
577
|
});
|
|
649
578
|
const reserveEntry = Effect.fnUntraced(function* (op) {
|
|
650
579
|
const draft = yield* Draft;
|
|
@@ -653,10 +582,10 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
653
582
|
});
|
|
654
583
|
const reserveInode = (op) => Effect.flatMap(Draft, (draft) => Effect.flatMap(draft.canAllocate, (available) => available ? Effect.void : Effect.fail(op.fail("NoSpace"))));
|
|
655
584
|
const reserveBytes = Effect.fnUntraced(function* (op, bytes) {
|
|
656
|
-
if (
|
|
585
|
+
if (limits.maxBytes === undefined)
|
|
657
586
|
return;
|
|
658
587
|
const draft = yield* Draft;
|
|
659
|
-
if (bytes > ByteSize.toBigInt(
|
|
588
|
+
if (bytes > ByteSize.toBigInt(limits.maxBytes) - (yield* draft.usedBytes)) {
|
|
660
589
|
return yield* op.fail("NoSpace");
|
|
661
590
|
}
|
|
662
591
|
});
|
|
@@ -692,60 +621,19 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
692
621
|
}
|
|
693
622
|
yield* reclaim(d, child.ino);
|
|
694
623
|
});
|
|
695
|
-
const
|
|
696
|
-
const ino = ref.ino;
|
|
697
|
-
if (ino !== undefined) {
|
|
698
|
-
const d = yield* Draft;
|
|
699
|
-
yield* d.release(ino);
|
|
700
|
-
yield* reclaim(d, ino);
|
|
701
|
-
}
|
|
702
|
-
ref.ino = undefined;
|
|
703
|
-
ref.closed = true;
|
|
704
|
-
});
|
|
705
|
-
const releaseOpenFile = (ref) => coordinatedCleanup(Effect.suspend(() => ref.closed ? Effect.void : applyDirect(() => releaseFile(ref))));
|
|
706
|
-
// Explicit close and scope cleanup share one release. A release whose commit fails still completes as
|
|
707
|
-
// cleanup, so the handle never stays open; only an explicit close reports the failure. A close refused
|
|
708
|
-
// admission never entered the volume, so it leaves the handle open for a retry.
|
|
709
|
-
const closeFile = (ref, op, check) => coordinated(op, Effect.andThen(check, Effect.gen(function* () {
|
|
710
|
-
const ino = ref.ino;
|
|
711
|
-
if (ino !== undefined) {
|
|
712
|
-
const d = yield* Draft;
|
|
713
|
-
yield* d.release(ino);
|
|
714
|
-
yield* reclaim(d, ino);
|
|
715
|
-
}
|
|
624
|
+
const releaseFileInode = Effect.fnUntraced(function* (ino) {
|
|
716
625
|
const d = yield* Draft;
|
|
717
|
-
yield* d.
|
|
718
|
-
|
|
719
|
-
ref.closed = true;
|
|
720
|
-
});
|
|
721
|
-
})), () => applyDirect(() => releaseFile(ref))).pipe(Effect.tapError((error) => error.code === "VolumeBusy" ? Effect.void : releaseOpenFile(ref)));
|
|
722
|
-
// Keyed on the file rather than the closed flag, so a rerun releases an open that published after a first run.
|
|
723
|
-
// Cleanup is uninterruptible and does not need admission, so an interrupted or busy scope close still releases.
|
|
724
|
-
const finalizeFile = (ref) => Effect.uninterruptible(Effect.suspend(() => ref.ino === undefined
|
|
725
|
-
? Effect.sync(() => {
|
|
726
|
-
ref.closed = true;
|
|
727
|
-
})
|
|
728
|
-
: Effect.ignore(closeFile(ref, OpContext.make("close"), Effect.void)).pipe(Effect.andThen(Effect.suspend(() => ref.closed ? Effect.void : releaseOpenFile(ref))))));
|
|
729
|
-
// Acquires a handle into its own scope, forked from the caller's. The finalizer is registered before waiting,
|
|
730
|
-
// since a closed scope runs a new finalizer at once and the permit is not reentrant. A scope that closes
|
|
731
|
-
// before or during acquisition interrupts it, and releasing here, under the permit, keeps the acquisition
|
|
732
|
-
// from outliving a finalizer that already ran. A scope that closes while the acquisition commits interrupts it
|
|
733
|
-
// too. Every exit without a handle reruns the finalizer, which may have run before the commit published and
|
|
734
|
-
// which closing an already closed scope would not run again.
|
|
735
|
-
const acquireHandle = Effect.fnUntraced(function* (reference, coordinate, acquire, releaseAcquired, finalize) {
|
|
736
|
-
const scope = yield* Scope.fork(yield* Effect.scope);
|
|
737
|
-
reference.scope = scope;
|
|
738
|
-
yield* Scope.addFinalizer(scope, finalize);
|
|
739
|
-
const closed = () => Predicate.isTagged(scope.state, "Closed");
|
|
740
|
-
return yield* coordinate(Effect.suspend(() => closed() ? Effect.interrupt : acquire).pipe(Effect.tap(() => Effect.suspend(() => {
|
|
741
|
-
if (!closed())
|
|
742
|
-
return Effect.void;
|
|
743
|
-
return Effect.andThen(releaseAcquired(), Effect.interrupt);
|
|
744
|
-
})))).pipe(Effect.tap(() => Effect.suspend(() => closed() ? Effect.interrupt : Effect.void)), Effect.onError(() => Effect.andThen(finalize, Scope.close(scope, Exit.void))));
|
|
626
|
+
yield* d.release(ino);
|
|
627
|
+
yield* reclaim(d, ino);
|
|
745
628
|
});
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
629
|
+
const releaseFile = (ref) => TokenRegistry.release(ref, releaseFileInode, (action) => Effect.sync(action));
|
|
630
|
+
const releaseOpenFile = (ref) => Effect.ignore(coordinatedCleanup(Effect.suspend(() => TokenRegistry.inode(ref) === undefined ? Effect.void : applyCleanup(() => releaseFile(ref)))));
|
|
631
|
+
// Admission refusal leaves the handle open. Every admitted close shares the registry release,
|
|
632
|
+
// and a storage failure still completes cleanup while only explicit close reports the failure.
|
|
633
|
+
const closeFile = (ref, op, check) => coordinated(op, Effect.andThen(check, TokenRegistry.release(ref, releaseFileInode, (action) => Effect.flatMap(Draft, (draft) => draft.afterInstall(action)))), true).pipe(Effect.tapError((error) => error.code === "VolumeBusy" ? Effect.void : releaseOpenFile(ref)));
|
|
634
|
+
const finalizeFile = (ref) => TokenRegistry.finalize(ref, closeFile(ref, OpContext.make("close"), Effect.void), releaseOpenFile(ref));
|
|
635
|
+
const acquireHandle = TokenRegistry.acquire;
|
|
636
|
+
const bindFile = (ref, ino) => Effect.flatMap(Draft, (draft) => draft.afterInstall(() => TokenRegistry.publish(ref, ino)));
|
|
749
637
|
const replaceContent = Effect.fnUntraced(function* (file, data, now, publish = true) {
|
|
750
638
|
const d = yield* Draft;
|
|
751
639
|
yield* d.addUsedBytes(BigInt(data.length - file.data.length));
|
|
@@ -780,7 +668,8 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
780
668
|
});
|
|
781
669
|
const fileHandle = (ref) => {
|
|
782
670
|
const get = Effect.fnUntraced(function* (op, access) {
|
|
783
|
-
const
|
|
671
|
+
const ino = TokenRegistry.inode(ref);
|
|
672
|
+
const node = ino === undefined ? undefined : yield* view(ino);
|
|
784
673
|
if (node?.kind !== "file" || (access === "read" && ref.access === "write") ||
|
|
785
674
|
(access === "write" && ref.access === "read"))
|
|
786
675
|
return yield* op.fail("InvalidHandle");
|
|
@@ -835,9 +724,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
835
724
|
return yield* writeOp.fail("FileTooLarge");
|
|
836
725
|
const start = Number(offset);
|
|
837
726
|
const draft = yield* Draft;
|
|
838
|
-
const free =
|
|
727
|
+
const free = limits.maxBytes === undefined
|
|
839
728
|
? BigInt(maxFileBytes)
|
|
840
|
-
: ByteSize.toBigInt(
|
|
729
|
+
: ByteSize.toBigInt(limits.maxBytes) - (yield* draft.usedBytes);
|
|
841
730
|
const maximumEnd = BigInt(file.data.length) + free;
|
|
842
731
|
const end = Number(BigInt(maxFileBytes) < maximumEnd ? BigInt(maxFileBytes) : maximumEnd);
|
|
843
732
|
const count = Math.min(bytes.length, Math.max(0, end - start));
|
|
@@ -912,33 +801,24 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
912
801
|
sync: coordinatedRead(syncOp, Effect.suspend(() => Effect.asVoid(get(syncOp)))).pipe(Effect.withSpan("FileHandle.sync")),
|
|
913
802
|
close: closeFile(ref, closeOp, Effect.suspend(() => get(closeOp))).pipe(Effect.ensuring(closeReleasedScope(ref)), Effect.withSpan("FileHandle.close"))
|
|
914
803
|
});
|
|
915
|
-
|
|
804
|
+
registry.registerFile(handle, ref);
|
|
916
805
|
return handle;
|
|
917
806
|
};
|
|
918
807
|
const createCaller = (reference, identity, umask) => {
|
|
919
|
-
const
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
if (known === undefined)
|
|
926
|
-
return yield* op.fail("InvalidReference");
|
|
927
|
-
if (known.volume !== volumeIdentity) {
|
|
928
|
-
return yield* op.fail("ForeignReference");
|
|
929
|
-
}
|
|
930
|
-
const node = yield* view(known.ino);
|
|
931
|
-
if (node === undefined || (node.kind === "directory" && node.metadata.nlink === 0)) {
|
|
932
|
-
return yield* op.fail("StaleReference");
|
|
933
|
-
}
|
|
934
|
-
return node;
|
|
935
|
-
});
|
|
936
|
-
const callerDirectory = Effect.fnUntraced(function* (op) {
|
|
937
|
-
const node = reference.ino === undefined ? undefined : yield* view(reference.ino);
|
|
938
|
-
if (node?.kind !== "directory")
|
|
939
|
-
return yield* op.fail("ClosedCaller");
|
|
940
|
-
return node;
|
|
808
|
+
const resolver = Resolution.make({
|
|
809
|
+
caller: reference,
|
|
810
|
+
get: view,
|
|
811
|
+
identity,
|
|
812
|
+
maxPathBytes: limits.maxPathBytes,
|
|
813
|
+
registry
|
|
941
814
|
});
|
|
815
|
+
const prepare = resolver.prepare;
|
|
816
|
+
const prepareEntry = resolver.prepareEntry;
|
|
817
|
+
const entryName = Resolution.entryName;
|
|
818
|
+
const resolveTarget = (target, op, options) => resolver.resolve(target, { kind: "Node", ...options }, op);
|
|
819
|
+
const resolveDirectory = resolver.directory;
|
|
820
|
+
const resolveEntry = (prepared, op) => resolver.resolve(prepared, { kind: "Parent" }, op);
|
|
821
|
+
const referencedNode = (target, op) => Effect.map(resolveTarget(Target.Reference({ reference: target }), op), (resolved) => resolved.node);
|
|
942
822
|
const directoryNow = Effect.fnUntraced(function* (ino) {
|
|
943
823
|
const node = yield* view(ino);
|
|
944
824
|
if (node?.kind !== "directory") {
|
|
@@ -1008,141 +888,6 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1008
888
|
revision: 0n
|
|
1009
889
|
};
|
|
1010
890
|
});
|
|
1011
|
-
const lookup = Effect.fnUntraced(function* (path, base, op, options = {}, referencedBase) {
|
|
1012
|
-
const pathOp = op.at(path.input);
|
|
1013
|
-
const { allowMissing = false, createMissing, followFinalSymlink = true, parentOnly = false } = options;
|
|
1014
|
-
if (reference.ino === undefined) {
|
|
1015
|
-
return yield* pathOp.fail("ClosedCaller");
|
|
1016
|
-
}
|
|
1017
|
-
let current = path.absolute ? (yield* nodeNow(ROOT_INO)) : yield* callerDirectory(pathOp);
|
|
1018
|
-
if (!path.absolute && referencedBase !== undefined) {
|
|
1019
|
-
current = referencedBase;
|
|
1020
|
-
yield* authorize(current, identity, EXECUTE, op);
|
|
1021
|
-
}
|
|
1022
|
-
else if (!path.absolute && base !== undefined) {
|
|
1023
|
-
const target = handles.get(base);
|
|
1024
|
-
if (target === undefined) {
|
|
1025
|
-
return yield* pathOp.fail("InvalidHandle");
|
|
1026
|
-
}
|
|
1027
|
-
if (target.volume !== volumeIdentity) {
|
|
1028
|
-
return yield* pathOp.fail("ForeignHandle");
|
|
1029
|
-
}
|
|
1030
|
-
const directory = target.ino === undefined ? undefined : (yield* view(target.ino));
|
|
1031
|
-
if (directory?.kind !== "directory") {
|
|
1032
|
-
return yield* pathOp.fail("InvalidHandle");
|
|
1033
|
-
}
|
|
1034
|
-
current = directory;
|
|
1035
|
-
yield* authorize(current, identity, EXECUTE, pathOp);
|
|
1036
|
-
}
|
|
1037
|
-
if (current.metadata.nlink === 0) {
|
|
1038
|
-
return yield* pathOp.fail("NotFound");
|
|
1039
|
-
}
|
|
1040
|
-
let work = path;
|
|
1041
|
-
let parent;
|
|
1042
|
-
let name;
|
|
1043
|
-
let traversals = 0;
|
|
1044
|
-
// Leading components that came from a symbolic link's target. Only the components the caller wrote are
|
|
1045
|
-
// created, so a dangling link stays missing, as it does for mkdir -p.
|
|
1046
|
-
let linked = 0;
|
|
1047
|
-
for (let index = 0; index < work.components.length - (parentOnly ? 1 : 0); index++) {
|
|
1048
|
-
if (current.kind !== "directory") {
|
|
1049
|
-
return yield* pathOp.fail("NotDirectory");
|
|
1050
|
-
}
|
|
1051
|
-
yield* authorize(current, identity, EXECUTE, pathOp);
|
|
1052
|
-
const component = work.components[index];
|
|
1053
|
-
if (component === undefined)
|
|
1054
|
-
break;
|
|
1055
|
-
if (component === DOT_HEX)
|
|
1056
|
-
continue;
|
|
1057
|
-
if (component === DOT_DOT_HEX) {
|
|
1058
|
-
current = yield* directoryNow(current.parent);
|
|
1059
|
-
parent = undefined;
|
|
1060
|
-
name = undefined;
|
|
1061
|
-
continue;
|
|
1062
|
-
}
|
|
1063
|
-
parent = current;
|
|
1064
|
-
name = component;
|
|
1065
|
-
const childIno = current.entries.get(component);
|
|
1066
|
-
const child = childIno === undefined ? undefined : (yield* view(childIno));
|
|
1067
|
-
if (child === undefined) {
|
|
1068
|
-
if (allowMissing && index === work.components.length - 1) {
|
|
1069
|
-
return { node: undefined, parent, name };
|
|
1070
|
-
}
|
|
1071
|
-
if (createMissing !== undefined && index >= linked) {
|
|
1072
|
-
current = yield* createMissing(current, component, index === work.components.length - 1);
|
|
1073
|
-
continue;
|
|
1074
|
-
}
|
|
1075
|
-
return yield* pathOp.fail("NotFound");
|
|
1076
|
-
}
|
|
1077
|
-
if (child.kind === "symlink" && (followFinalSymlink || index < work.components.length - 1 || work.trailingSlash)) {
|
|
1078
|
-
if (child.target.length === 0) {
|
|
1079
|
-
return yield* pathOp.fail("NotFound");
|
|
1080
|
-
}
|
|
1081
|
-
if (++traversals > MAX_SYMLINK_TRAVERSALS) {
|
|
1082
|
-
return yield* pathOp.fail("SymlinkLoop");
|
|
1083
|
-
}
|
|
1084
|
-
const suffix = work.suffixes[index] ?? new Uint8Array(0);
|
|
1085
|
-
const remaining = work.components.length - index - 1;
|
|
1086
|
-
if (settings.maxPathBytes !== undefined &&
|
|
1087
|
-
ByteSize.isGreaterThan(ByteSize.bytes(child.target.length + suffix.length), settings.maxPathBytes)) {
|
|
1088
|
-
return yield* pathOp.fail("PathTooLong");
|
|
1089
|
-
}
|
|
1090
|
-
const expansion = new Uint8Array(child.target.length + suffix.length);
|
|
1091
|
-
expansion.set(child.target);
|
|
1092
|
-
expansion.set(suffix, child.target.length);
|
|
1093
|
-
const expanded = preparePath(ownedPath(expansion), op.operation, settings.maxPathBytes);
|
|
1094
|
-
// The expansion is synthetic: its per-component limits are the caller's to hear about,
|
|
1095
|
-
// but the path in the error has to be the one the caller passed in, so the
|
|
1096
|
-
// expansion's own failure is not kept as the cause.
|
|
1097
|
-
if (Result.isFailure(expanded)) {
|
|
1098
|
-
return yield* pathOp.fail(expanded.failure.code);
|
|
1099
|
-
}
|
|
1100
|
-
work = expanded.success;
|
|
1101
|
-
linked = work.components.length - remaining + Math.max(0, linked - index - 1);
|
|
1102
|
-
if (work.absolute)
|
|
1103
|
-
current = yield* nodeNow(ROOT_INO);
|
|
1104
|
-
index = -1;
|
|
1105
|
-
}
|
|
1106
|
-
else
|
|
1107
|
-
current = child;
|
|
1108
|
-
}
|
|
1109
|
-
if (!parentOnly && work.trailingSlash && current.kind !== "directory") {
|
|
1110
|
-
return yield* pathOp.fail("NotDirectory");
|
|
1111
|
-
}
|
|
1112
|
-
return { node: current, parent, name };
|
|
1113
|
-
});
|
|
1114
|
-
const resolveNode = Effect.fnUntraced(function* (path, base, op, options) {
|
|
1115
|
-
const pathOp = op.at(path.input);
|
|
1116
|
-
const result = yield* lookup(path, base, op, options);
|
|
1117
|
-
if (result.node === undefined) {
|
|
1118
|
-
return yield* pathOp.fail("NotFound");
|
|
1119
|
-
}
|
|
1120
|
-
return result.node;
|
|
1121
|
-
});
|
|
1122
|
-
const locate = Effect.fnUntraced(function* (path, base, op, options) {
|
|
1123
|
-
const pathOp = op.at(path.input);
|
|
1124
|
-
const result = yield* lookup(path, base, op, options);
|
|
1125
|
-
const node = result.node;
|
|
1126
|
-
if (node === undefined) {
|
|
1127
|
-
return yield* pathOp.fail("NotFound");
|
|
1128
|
-
}
|
|
1129
|
-
if (node.kind !== "directory") {
|
|
1130
|
-
return yield* pathOp.fail("NotDirectory");
|
|
1131
|
-
}
|
|
1132
|
-
return node;
|
|
1133
|
-
});
|
|
1134
|
-
const ResolvedEntry = {
|
|
1135
|
-
fromPath: Effect.fnUntraced(function* (path, base, op) {
|
|
1136
|
-
const parent = yield* locate(path, base, op, { parentOnly: true });
|
|
1137
|
-
return {
|
|
1138
|
-
parent: parent.ino,
|
|
1139
|
-
name: path.components.at(-1),
|
|
1140
|
-
trailingSlash: path.trailingSlash,
|
|
1141
|
-
addressing: "path",
|
|
1142
|
-
op: op.at(path.input)
|
|
1143
|
-
};
|
|
1144
|
-
})
|
|
1145
|
-
};
|
|
1146
891
|
const grantedMode = (metadata, mode) => !identity.privileged && metadata.kind === "file" && !inGroup(identity, metadata.gid) ? mode & ~0o2000 : mode;
|
|
1147
892
|
const permittedMode = (metadata, mode, op) => !identity.privileged && identity.uid !== metadata.uid
|
|
1148
893
|
? Effect.fail(op.fail("NotPermitted"))
|
|
@@ -1270,13 +1015,16 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1270
1015
|
}
|
|
1271
1016
|
else {
|
|
1272
1017
|
entryOp = op.at(prepared.path.input);
|
|
1273
|
-
const resolved = yield*
|
|
1018
|
+
const resolved = yield* resolver.resolve(Target.Path(prepared.base === undefined
|
|
1019
|
+
? { path: prepared.path.input }
|
|
1020
|
+
: { path: prepared.path.input, relativeTo: prepared.base }), {
|
|
1021
|
+
kind: "Node",
|
|
1274
1022
|
createMissing: (parent, name, final) => Effect.flatMap(makeDirectory({ parent: parent.ino, name, trailingSlash: false, addressing: "path", op: entryOp }, final ? request : { mode: request.mode, exactMode: request.exactMode }, op), (made) => {
|
|
1275
1023
|
if (!revisionsBefore.has(parent.ino))
|
|
1276
1024
|
revisionsBefore.set(parent.ino, made.directory.before);
|
|
1277
1025
|
return directoryNow(made.child);
|
|
1278
1026
|
})
|
|
1279
|
-
});
|
|
1027
|
+
}, op);
|
|
1280
1028
|
node = resolved.node;
|
|
1281
1029
|
}
|
|
1282
1030
|
if (node?.kind !== "directory")
|
|
@@ -1545,9 +1293,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1545
1293
|
const ino = opened();
|
|
1546
1294
|
if (ino === undefined)
|
|
1547
1295
|
return;
|
|
1548
|
-
|
|
1549
|
-
yield* draft.release(ino);
|
|
1550
|
-
yield* reclaim(draft, ino);
|
|
1296
|
+
yield* releaseFileInode(ino);
|
|
1551
1297
|
});
|
|
1552
1298
|
const acquireOpenedFile = (ref, op, acquire) => {
|
|
1553
1299
|
let opened;
|
|
@@ -1555,75 +1301,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1555
1301
|
opened = ino;
|
|
1556
1302
|
}), releasePending(() => opened), finalizeFile(ref));
|
|
1557
1303
|
};
|
|
1558
|
-
const prepare = (input, op) => preparePath(input, op.operation, settings.maxPathBytes);
|
|
1559
|
-
const handleNode = Effect.fnUntraced(function* (handle, op) {
|
|
1560
|
-
if (reference.ino === undefined)
|
|
1561
|
-
return yield* op.fail("ClosedCaller");
|
|
1562
|
-
const ref = isFileHandle(handle) ? files.get(handle) : handles.get(handle);
|
|
1563
|
-
if (ref === undefined)
|
|
1564
|
-
return yield* op.fail("InvalidHandle");
|
|
1565
|
-
if (ref.volume !== volumeIdentity)
|
|
1566
|
-
return yield* op.fail("ForeignHandle");
|
|
1567
|
-
const node = ref.ino === undefined ? undefined : (yield* view(ref.ino));
|
|
1568
|
-
if (node === undefined)
|
|
1569
|
-
return yield* op.fail("InvalidHandle");
|
|
1570
|
-
return node;
|
|
1571
|
-
});
|
|
1572
|
-
// A path prepares before the walk, so a malformed input fails before any lookup, and names the path on
|
|
1573
|
-
// every failure. A reference or handle names none. The verb's `followFinalSymlink` is the default a
|
|
1574
|
-
// path's own setting overrides; `final` makes the verb's setting win, for a verb such as `readLink` whose
|
|
1575
|
-
// meaning fixes it.
|
|
1576
|
-
const resolveTarget = (target, op, options) => Target.$match(target, {
|
|
1577
|
-
Path: ({ followFinalSymlink, path, relativeTo }) => Effect.flatMap(Effect.fromResult(prepare(path, op)), (prepared) => Effect.map(resolveNode(prepared, relativeTo, op, {
|
|
1578
|
-
followFinalSymlink: options?.final === true
|
|
1579
|
-
? options.followFinalSymlink ?? true
|
|
1580
|
-
: followFinalSymlink ?? options?.followFinalSymlink ?? true
|
|
1581
|
-
}), (node) => ({ node, op: op.at(path) }))),
|
|
1582
|
-
Reference: ({ reference }) => Effect.map(referencedNode(reference, op), (node) => ({ node, op })),
|
|
1583
|
-
Handle: ({ handle }) => Effect.map(handleNode(handle, op), (node) => ({ node, op }))
|
|
1584
|
-
});
|
|
1585
|
-
const resolveDirectory = Effect.fnUntraced(function* (target, op) {
|
|
1586
|
-
const resolved = yield* resolveTarget(target, op);
|
|
1587
|
-
if (resolved.node.kind !== "directory")
|
|
1588
|
-
return yield* resolved.op.fail("NotDirectory");
|
|
1589
|
-
return { node: resolved.node, op: resolved.op };
|
|
1590
|
-
});
|
|
1591
|
-
// The directory an entry names a child of. A removed directory that a handle still holds takes no new
|
|
1592
|
-
// children, as on Linux, where creating through a descriptor of a removed directory fails with ENOENT.
|
|
1593
|
-
const entryDirectory = Effect.fnUntraced(function* (target, op) {
|
|
1594
|
-
const directory = yield* resolveDirectory(target, op);
|
|
1595
|
-
if (directory.node.metadata.nlink === 0)
|
|
1596
|
-
return yield* directory.op.fail("NotFound");
|
|
1597
|
-
return directory;
|
|
1598
|
-
});
|
|
1599
1304
|
const asResolvedNode = (target, op, options) => Effect.map(resolveTarget(target, op, options), (resolved) => ({ ino: resolved.node.ino, op: resolved.op }));
|
|
1600
|
-
const entryName = (input, op) => {
|
|
1601
|
-
if (Predicate.isString(input) && !isWellFormed(input))
|
|
1602
|
-
return Result.fail(op.fail("InvalidPathEncoding"));
|
|
1603
|
-
const bytes = Predicate.isString(input) ? encoder.encode(input) : input;
|
|
1604
|
-
if (!isAttachedBytes(bytes) || bytes.length === 0 || bytes.length > MAX_NAME_BYTES || bytes.includes(0) ||
|
|
1605
|
-
bytes.includes(SLASH_BYTE))
|
|
1606
|
-
return Result.fail(op.fail("InvalidArgument"));
|
|
1607
|
-
return Result.succeed(Hex.encode(new Uint8Array(bytes)));
|
|
1608
|
-
};
|
|
1609
|
-
const prepareEntry = (input, op) => isEntry(input)
|
|
1610
|
-
? Result.map(entryName(input.name, op), (name) => ({ kind: "entry", directory: input.directory, name }))
|
|
1611
|
-
: isTarget(input)
|
|
1612
|
-
? Result.map(prepare(input.path, op), (path) => ({ kind: "path", path, base: input.relativeTo }))
|
|
1613
|
-
: Result.map(prepare(input, op), (path) => ({ kind: "path", path, base: undefined }));
|
|
1614
|
-
const resolveEntry = Effect.fnUntraced(function* (prepared, op) {
|
|
1615
|
-
if (prepared.kind === "path") {
|
|
1616
|
-
return yield* ResolvedEntry.fromPath(prepared.path, prepared.base, op);
|
|
1617
|
-
}
|
|
1618
|
-
const directory = yield* entryDirectory(prepared.directory, op);
|
|
1619
|
-
return {
|
|
1620
|
-
parent: directory.node.ino,
|
|
1621
|
-
name: prepared.name,
|
|
1622
|
-
trailingSlash: false,
|
|
1623
|
-
addressing: "entry",
|
|
1624
|
-
op
|
|
1625
|
-
};
|
|
1626
|
-
});
|
|
1627
1305
|
const asTarget = (input) => Target.of(input);
|
|
1628
1306
|
const acquireDirectory = Effect.fnUntraced(function* (input, op) {
|
|
1629
1307
|
const target = asTarget(input);
|
|
@@ -1632,7 +1310,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1632
1310
|
const directory = yield* resolveDirectory(target, op);
|
|
1633
1311
|
yield* authorize(directory.node, identity, EXECUTE, directory.op);
|
|
1634
1312
|
holdDirectory(directory.node.ino);
|
|
1635
|
-
acquired
|
|
1313
|
+
TokenRegistry.publish(acquired, directory.node.ino);
|
|
1636
1314
|
return acquired;
|
|
1637
1315
|
}),
|
|
1638
1316
|
// Nothing to undo here: the finalizer that follows an interrupted acquisition releases the hold under
|
|
@@ -1655,29 +1333,16 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1655
1333
|
const follow = target.followFinalSymlink ?? true;
|
|
1656
1334
|
return yield* acquireOpenedFile(acquired, op, (opened) => Effect.gen(function* () {
|
|
1657
1335
|
const path = yield* Effect.fromResult(prepared);
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
const resolved = yield* lookup(path, target.relativeTo, op, {
|
|
1666
|
-
followFinalSymlink: follow,
|
|
1667
|
-
allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
|
|
1668
|
-
});
|
|
1669
|
-
const parent = resolved.parent;
|
|
1670
|
-
if (parent === undefined)
|
|
1671
|
-
return yield* pathOp.fail("IsDirectory");
|
|
1672
|
-
const name = resolved.name;
|
|
1673
|
-
if (isDotComponent(name))
|
|
1674
|
-
return yield* pathOp.fail("IsDirectory");
|
|
1675
|
-
if (resolved.node === undefined && path.trailingSlash)
|
|
1336
|
+
const resolved = yield* resolver.resolve({ kind: "path", path, base: target.relativeTo }, {
|
|
1337
|
+
kind: "OrCreate",
|
|
1338
|
+
create: chosen.create ?? "never",
|
|
1339
|
+
finalSymlink: follow ? "follow" : "preserve"
|
|
1340
|
+
}, op);
|
|
1341
|
+
const { parent, name } = resolved;
|
|
1342
|
+
if (resolved.kind === "missing" && path.trailingSlash)
|
|
1676
1343
|
return yield* pathOp.fail("IsDirectory");
|
|
1677
1344
|
yield* authorize(parent, identity, EXECUTE, pathOp);
|
|
1678
|
-
const file = resolved.node;
|
|
1679
|
-
if (file !== undefined && chosen.create === "exclusive")
|
|
1680
|
-
return yield* pathOp.fail("AlreadyExists");
|
|
1345
|
+
const file = resolved.kind === "existing" ? resolved.node : undefined;
|
|
1681
1346
|
const result = yield* openFile({ parent: parent.ino, name, trailingSlash: path.trailingSlash, addressing: "path", op: pathOp }, file, chosen, acquired, op);
|
|
1682
1347
|
opened(result.ino);
|
|
1683
1348
|
return fileHandle(acquired);
|
|
@@ -1719,60 +1384,44 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1719
1384
|
}
|
|
1720
1385
|
if (chosen.exactMode && chosen.mode === undefined)
|
|
1721
1386
|
return yield* op.fail("InvalidArgument");
|
|
1722
|
-
const relativePath = preparePath(ownedPath(nameBytes(name)), op.operation, undefined);
|
|
1723
1387
|
const acquired = makeFileReference(chosen.access, chosen.append ?? false);
|
|
1724
1388
|
const expected = chosen.expected;
|
|
1725
1389
|
return yield* acquireOpenedFile(acquired, op, (opened) => Effect.gen(function* () {
|
|
1726
|
-
const
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1390
|
+
const resolved = yield* resolver.resolve({ kind: "entry", directory: entry.directory, name }, {
|
|
1391
|
+
kind: "OrCreate",
|
|
1392
|
+
create: chosen.create ?? "never",
|
|
1393
|
+
finalSymlink: chosen.followFinalSymlink !== false ? "follow" : "preserve",
|
|
1394
|
+
beforeFollow: Effect.fnUntraced(function* (_, direct) {
|
|
1395
|
+
if (expected !== undefined) {
|
|
1396
|
+
let expectedIno;
|
|
1397
|
+
if (expected !== null) {
|
|
1398
|
+
const observed = yield* Effect.result(referencedNode(expected, op));
|
|
1399
|
+
if (Result.isFailure(observed)) {
|
|
1400
|
+
return yield* op.fail("VolumeBusy", { cause: observed.failure });
|
|
1401
|
+
}
|
|
1402
|
+
expectedIno = observed.success.ino;
|
|
1403
|
+
}
|
|
1404
|
+
if (direct?.ino !== expectedIno)
|
|
1405
|
+
return yield* op.fail("VolumeBusy");
|
|
1738
1406
|
}
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
return yield* op.fail("VolumeBusy");
|
|
1743
|
-
}
|
|
1744
|
-
if (chosen.expectedChild === null) {
|
|
1745
|
-
if (direct !== undefined)
|
|
1746
|
-
return yield* op.fail("StaleReference");
|
|
1747
|
-
}
|
|
1748
|
-
else if (chosen.expectedChild !== undefined) {
|
|
1749
|
-
const expectedChild = chosen.expectedChild;
|
|
1750
|
-
const observed = yield* referencedNode(expectedChild.reference, op);
|
|
1751
|
-
if (direct?.ino !== observed.ino || observed.revision !== expectedChild.revision ||
|
|
1752
|
-
observed.metadata.atimeNs !== expectedChild.atimeNs ||
|
|
1753
|
-
observed.metadata.mtimeNs !== expectedChild.mtimeNs) {
|
|
1754
|
-
return yield* op.fail("StaleReference");
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
if (direct !== undefined && chosen.create === "exclusive")
|
|
1758
|
-
return yield* op.fail("AlreadyExists");
|
|
1759
|
-
let file = direct;
|
|
1760
|
-
let mutationParent = parent.ino;
|
|
1761
|
-
let mutationName = name;
|
|
1762
|
-
if (file?.kind === "symlink" && chosen.followFinalSymlink !== false) {
|
|
1763
|
-
const resolved = yield* lookup(yield* Effect.fromResult(relativePath), undefined, op, {
|
|
1764
|
-
followFinalSymlink: true,
|
|
1765
|
-
allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
|
|
1766
|
-
}, parent);
|
|
1767
|
-
file = resolved.node;
|
|
1768
|
-
if (file === undefined) {
|
|
1769
|
-
if (resolved.parent === undefined || resolved.name === undefined) {
|
|
1770
|
-
return yield* op.fail("IsDirectory");
|
|
1407
|
+
if (chosen.expectedChild === null) {
|
|
1408
|
+
if (direct !== undefined)
|
|
1409
|
+
return yield* op.fail("StaleReference");
|
|
1771
1410
|
}
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1411
|
+
else if (chosen.expectedChild !== undefined) {
|
|
1412
|
+
const expectedChild = chosen.expectedChild;
|
|
1413
|
+
const observed = yield* referencedNode(expectedChild.reference, op);
|
|
1414
|
+
if (direct?.ino !== observed.ino || observed.revision !== expectedChild.revision ||
|
|
1415
|
+
observed.metadata.atimeNs !== expectedChild.atimeNs ||
|
|
1416
|
+
observed.metadata.mtimeNs !== expectedChild.mtimeNs) {
|
|
1417
|
+
return yield* op.fail("StaleReference");
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
})
|
|
1421
|
+
}, op);
|
|
1422
|
+
const file = resolved.kind === "existing" ? resolved.node : undefined;
|
|
1423
|
+
const mutationParent = resolved.kind === "missing" ? resolved.parent.ino : resolved.origin.parent.ino;
|
|
1424
|
+
const mutationName = resolved.kind === "missing" ? resolved.name : resolved.origin.name;
|
|
1776
1425
|
const result = yield* openFile({ parent: mutationParent, name: mutationName, trailingSlash: false, addressing: "entry", op }, file, { ...chosen, initialSize: chosen.initialSize ?? 0n }, acquired, op);
|
|
1777
1426
|
opened(result.ino);
|
|
1778
1427
|
return {
|
|
@@ -1828,7 +1477,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1828
1477
|
const prepared = prepare(target.path, op);
|
|
1829
1478
|
if (Result.isFailure(prepared))
|
|
1830
1479
|
return Effect.undefined;
|
|
1831
|
-
return
|
|
1480
|
+
return resolveEntry({ kind: "path", path: prepared.success, base: target.relativeTo }, op).pipe(Effect.flatMap((entry) => Effect.map(view(entry.parent), (parent) => {
|
|
1832
1481
|
return entry.name !== undefined && parent?.kind === "directory" &&
|
|
1833
1482
|
parent.entries.get(entry.name) === root.ino
|
|
1834
1483
|
? anchorFrame(parent, entry.name, root)
|
|
@@ -1868,27 +1517,23 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1868
1517
|
let bytes = 0n;
|
|
1869
1518
|
let failure;
|
|
1870
1519
|
const list = (frame) => coordinatedRead(op, Effect.suspend(() => {
|
|
1871
|
-
if (reference
|
|
1520
|
+
if (TokenRegistry.inode(reference) === undefined)
|
|
1872
1521
|
return Effect.fail(op.fail("ClosedCaller"));
|
|
1873
1522
|
const at = op.at(locate(frame.path));
|
|
1874
1523
|
return Effect.flatMap(reachFrame(frame, at), (node) => node?.kind !== "directory" || node.metadata.nlink === 0
|
|
1875
1524
|
? Effect.succeed([])
|
|
1876
1525
|
: Effect.andThen(listable(node, at), walkChildren(node, frame.path, frame.depth, frame)));
|
|
1877
1526
|
}));
|
|
1878
|
-
const exceeded = (frame, field) => makeError({ code: "LimitExceeded", operation: op.operation, field, path: errorPath(locate(frame.path)) });
|
|
1879
1527
|
const admit = (frame) => {
|
|
1880
|
-
|
|
1881
|
-
return exceeded(frame, "maxDepth");
|
|
1882
|
-
if (plan.maxEntries !== undefined && ++entries > plan.maxEntries)
|
|
1883
|
-
return exceeded(frame, "maxEntries");
|
|
1528
|
+
entries++;
|
|
1884
1529
|
bytes += frame.bytes;
|
|
1885
|
-
|
|
1886
|
-
return exceeded(frame, "maxBytes");
|
|
1887
|
-
return undefined;
|
|
1530
|
+
return plan.limit(frame, { entries, bytes }, "entry");
|
|
1888
1531
|
};
|
|
1889
1532
|
const step = Effect.gen(function* () {
|
|
1533
|
+
// Preserve the generic limit error E; yielding the error directly widens it to WalkFailure.
|
|
1534
|
+
// oxlint-disable-next-line effecttsgo/unnecessary-fail-yieldable-error -- E must stay narrow for unbounded removal.
|
|
1890
1535
|
if (failure !== undefined)
|
|
1891
|
-
return yield* failure;
|
|
1536
|
+
return yield* Effect.fail(failure);
|
|
1892
1537
|
if (pending === undefined)
|
|
1893
1538
|
pending = (yield* first).reverse();
|
|
1894
1539
|
const out = [];
|
|
@@ -1910,9 +1555,9 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1910
1555
|
out.push(frame);
|
|
1911
1556
|
}
|
|
1912
1557
|
if (unlisted) {
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1558
|
+
const rejected = plan.limit(frame, { entries, bytes }, "directory");
|
|
1559
|
+
if (rejected !== undefined)
|
|
1560
|
+
return yield* stop(rejected);
|
|
1916
1561
|
const listed = yield* Effect.result(list(frame));
|
|
1917
1562
|
if (Result.isFailure(listed))
|
|
1918
1563
|
return yield* stop(listed.failure);
|
|
@@ -1976,7 +1621,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1976
1621
|
}, op));
|
|
1977
1622
|
})));
|
|
1978
1623
|
};
|
|
1979
|
-
const plan = { order: "post",
|
|
1624
|
+
const plan = { order: "post", limit: () => undefined };
|
|
1980
1625
|
return yield* Stream.runForEach(walkFrames(op, first, plan, locate, listable), removeFrame);
|
|
1981
1626
|
});
|
|
1982
1627
|
const rootOp = OpContext.make("root");
|
|
@@ -1985,9 +1630,40 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
1985
1630
|
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
1986
1631
|
return yield* coordinated(op, Effect.flatMap(resolveEntry(prepared, op), (entry) => body(entry, op)));
|
|
1987
1632
|
});
|
|
1633
|
+
const openDispatch = Effect.fn("Caller.open")(function* (input, options) {
|
|
1634
|
+
const op = OpContext.make("open");
|
|
1635
|
+
if (isEntry(input))
|
|
1636
|
+
return yield* openEntry(input, options, op);
|
|
1637
|
+
const target = asTarget(input);
|
|
1638
|
+
if (Target.$is("Path")(target))
|
|
1639
|
+
return yield* openPath(target, options, op);
|
|
1640
|
+
return yield* openNode(target, options, op);
|
|
1641
|
+
});
|
|
1642
|
+
function open(input, options) {
|
|
1643
|
+
return openDispatch(input, options);
|
|
1644
|
+
}
|
|
1645
|
+
const removeDispatch = Effect.fn("Caller.remove")(function* (input, options) {
|
|
1646
|
+
const op = OpContext.make("remove");
|
|
1647
|
+
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
1648
|
+
const chosen = yield* decodeRemoveOptions(options ?? {}).pipe(Effect.mapError((cause) => fail(preparedOp(prepared, op), cause)));
|
|
1649
|
+
const once = coordinated(op, Effect.flatMap(resolveEntry(prepared, op), (entry) => removeEntry(entry, op)));
|
|
1650
|
+
const target = (effect) => chosen.force === true
|
|
1651
|
+
? Effect.catchIf(effect, (error) => error.code === "NotFound", () => Effect.undefined)
|
|
1652
|
+
: effect;
|
|
1653
|
+
const removed = yield* Effect.result(target(once));
|
|
1654
|
+
if (Result.isSuccess(removed))
|
|
1655
|
+
return removed.success;
|
|
1656
|
+
if (chosen.recursive !== true || removed.failure.code !== "NotEmpty")
|
|
1657
|
+
return yield* removed.failure;
|
|
1658
|
+
yield* emptyDirectory(prepared, op, chosen.force === true);
|
|
1659
|
+
return yield* target(once);
|
|
1660
|
+
});
|
|
1661
|
+
function remove(input, options) {
|
|
1662
|
+
return removeDispatch(input, options);
|
|
1663
|
+
}
|
|
1988
1664
|
const caller = Object.freeze({
|
|
1989
1665
|
[CallerId]: true,
|
|
1990
|
-
root: coordinatedRead(rootOp, Effect.suspend(() => reference
|
|
1666
|
+
root: coordinatedRead(rootOp, Effect.suspend(() => TokenRegistry.inode(reference) === undefined
|
|
1991
1667
|
? Effect.fail(rootOp.fail("ClosedCaller"))
|
|
1992
1668
|
: Effect.succeed(referenceFor(ROOT_INO)))).pipe(Effect.withSpan("Caller.root")),
|
|
1993
1669
|
lookup: Effect.fn("Caller.lookup")(function* (input) {
|
|
@@ -2044,11 +1720,20 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2044
1720
|
yield* authorize(directory.node, identity, READ, directory.op);
|
|
2045
1721
|
return yield* walkChildren(directory.node, new Uint8Array(0), 0, yield* pathAnchor(target, directory.node, op));
|
|
2046
1722
|
}));
|
|
1723
|
+
const exceeded = (frame, field) => makeError({ code: "LimitExceeded", operation: op.operation, field, path: errorPath(locate(frame.path)) });
|
|
1724
|
+
const maxBytes = chosen.maxBytes === undefined ? undefined : ByteSize.toBigInt(chosen.maxBytes);
|
|
2047
1725
|
const plan = {
|
|
2048
1726
|
order: chosen.order ?? "pre",
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
1727
|
+
limit: (frame, progress, phase) => {
|
|
1728
|
+
if (chosen.maxDepth !== undefined && frame.depth > chosen.maxDepth)
|
|
1729
|
+
return exceeded(frame, "maxDepth");
|
|
1730
|
+
if (phase === "directory")
|
|
1731
|
+
return undefined;
|
|
1732
|
+
if (chosen.maxEntries !== undefined && progress.entries > chosen.maxEntries) {
|
|
1733
|
+
return exceeded(frame, "maxEntries");
|
|
1734
|
+
}
|
|
1735
|
+
return maxBytes !== undefined && progress.bytes > maxBytes ? exceeded(frame, "maxBytes") : undefined;
|
|
1736
|
+
}
|
|
2052
1737
|
};
|
|
2053
1738
|
return Stream.map(walkFrames(op, first, plan, locate, readable), (frame) => Object.freeze({
|
|
2054
1739
|
path: ownedPath(frame.path),
|
|
@@ -2076,12 +1761,16 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2076
1761
|
return yield* coordinatedRead(op, Effect.gen(function* () {
|
|
2077
1762
|
if (Target.$is("Path")(target)) {
|
|
2078
1763
|
const pathOp = op.at(target.path);
|
|
2079
|
-
const result = yield*
|
|
2080
|
-
const directory = result.node
|
|
1764
|
+
const result = yield* resolveTarget(target, op, { final: true });
|
|
1765
|
+
const directory = result.node.kind === "directory"
|
|
1766
|
+
? result.node
|
|
1767
|
+
: result.kind === "entry"
|
|
1768
|
+
? result.parent
|
|
1769
|
+
: undefined;
|
|
2081
1770
|
const prefix = directory === undefined ? SLASH_HEX : yield* pathOfCurrent(directory.ino);
|
|
2082
1771
|
if (prefix === undefined)
|
|
2083
1772
|
return yield* pathOp.fail("NotFound");
|
|
2084
|
-
if (result.node
|
|
1773
|
+
if (result.node.kind === "directory" || result.kind === "node")
|
|
2085
1774
|
return ownedPath(nameBytes(prefix));
|
|
2086
1775
|
return ownedPath(nameBytes(prefix + (prefix === SLASH_HEX ? "" : SLASH_HEX) + result.name));
|
|
2087
1776
|
}
|
|
@@ -2136,66 +1825,19 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2136
1825
|
const captured = new Uint8Array(bytes);
|
|
2137
1826
|
const chosen = yield* decodeWriteFileOptions(options).pipe(Effect.mapError((cause) => fail(optionsOp, cause)));
|
|
2138
1827
|
return yield* coordinated(op, Effect.gen(function* () {
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
return yield* entryOp.fail("AlreadyExists");
|
|
2151
|
-
if (exists.failure.code !== "NotFound")
|
|
2152
|
-
return yield* exists.failure;
|
|
2153
|
-
}
|
|
2154
|
-
const resolved = yield* lookup(path, prepared.base, op, {
|
|
2155
|
-
followFinalSymlink: chosen.replaceFinalSymlink !== true && chosen.followFinalSymlink !== false,
|
|
2156
|
-
allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
|
|
2157
|
-
});
|
|
2158
|
-
if (resolved.parent === undefined || resolved.name === undefined || resolved.node?.kind === "directory") {
|
|
2159
|
-
return yield* entryOp.fail("IsDirectory");
|
|
2160
|
-
}
|
|
2161
|
-
parent = resolved.parent;
|
|
2162
|
-
name = resolved.name;
|
|
2163
|
-
found = resolved.node;
|
|
2164
|
-
trailingSlash = path.trailingSlash;
|
|
2165
|
-
}
|
|
2166
|
-
else {
|
|
2167
|
-
const directory = yield* entryDirectory(prepared.directory, op);
|
|
2168
|
-
if (isDotComponent(prepared.name))
|
|
2169
|
-
return yield* op.fail("InvalidArgument");
|
|
2170
|
-
yield* authorize(directory.node, identity, EXECUTE, op);
|
|
2171
|
-
parent = directory.node;
|
|
2172
|
-
name = prepared.name;
|
|
2173
|
-
const childIno = parent.entries.get(name);
|
|
2174
|
-
let child = childIno === undefined ? undefined : (yield* view(childIno));
|
|
2175
|
-
if (child?.kind === "symlink" && chosen.replaceFinalSymlink !== true &&
|
|
2176
|
-
chosen.followFinalSymlink !== false) {
|
|
2177
|
-
const resolved = yield* lookup(yield* Effect.fromResult(preparePath(ownedPath(nameBytes(name)), op.operation, undefined)), undefined, op, {
|
|
2178
|
-
followFinalSymlink: true,
|
|
2179
|
-
allowMissing: chosen.create === "ifMissing" || chosen.create === "exclusive"
|
|
2180
|
-
}, parent);
|
|
2181
|
-
if (resolved.parent === undefined || resolved.name === undefined || resolved.node?.kind === "directory") {
|
|
2182
|
-
return yield* op.fail("IsDirectory");
|
|
2183
|
-
}
|
|
2184
|
-
parent = resolved.parent;
|
|
2185
|
-
name = resolved.name;
|
|
2186
|
-
child = resolved.node;
|
|
2187
|
-
}
|
|
2188
|
-
if (child === undefined && (chosen.create === undefined || chosen.create === "never")) {
|
|
2189
|
-
return yield* op.fail("NotFound");
|
|
2190
|
-
}
|
|
2191
|
-
if (child !== undefined && chosen.create === "exclusive")
|
|
2192
|
-
return yield* op.fail("AlreadyExists");
|
|
2193
|
-
if (child?.kind === "directory")
|
|
2194
|
-
return yield* op.fail("IsDirectory");
|
|
2195
|
-
found = child;
|
|
2196
|
-
}
|
|
2197
|
-
if (found === undefined && trailingSlash)
|
|
1828
|
+
const resolved = yield* resolver.resolve(prepared, {
|
|
1829
|
+
kind: "OrCreate",
|
|
1830
|
+
create: chosen.create ?? "never",
|
|
1831
|
+
finalSymlink: chosen.replaceFinalSymlink !== true && chosen.followFinalSymlink !== false
|
|
1832
|
+
? "follow"
|
|
1833
|
+
: "preserve",
|
|
1834
|
+
entryExclusive: "afterSymlink"
|
|
1835
|
+
}, op);
|
|
1836
|
+
const { parent, name, trailingSlash, op: entryOp } = resolved;
|
|
1837
|
+
const found = resolved.kind === "existing" ? resolved.node : undefined;
|
|
1838
|
+
if (found?.kind === "directory" || (found === undefined && trailingSlash)) {
|
|
2198
1839
|
return yield* entryOp.fail("IsDirectory");
|
|
1840
|
+
}
|
|
2199
1841
|
const replaced = found?.kind === "symlink" ? found : undefined;
|
|
2200
1842
|
if (replaced !== undefined && !chosen.replaceFinalSymlink)
|
|
2201
1843
|
return yield* entryOp.fail("SymlinkLoop");
|
|
@@ -2260,17 +1902,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2260
1902
|
}
|
|
2261
1903
|
}));
|
|
2262
1904
|
}),
|
|
2263
|
-
|
|
2264
|
-
// reject the other shape, so one implementation serves both.
|
|
2265
|
-
open: Effect.fn("Caller.open")(function* (input, options) {
|
|
2266
|
-
const op = OpContext.make("open");
|
|
2267
|
-
if (isEntry(input))
|
|
2268
|
-
return yield* openEntry(input, options, op);
|
|
2269
|
-
const target = asTarget(input);
|
|
2270
|
-
if (Target.$is("Path")(target))
|
|
2271
|
-
return yield* openPath(target, options, op);
|
|
2272
|
-
return yield* openNode(target, options, op);
|
|
2273
|
-
}),
|
|
1905
|
+
open,
|
|
2274
1906
|
mkdir: Effect.fn("Caller.mkdir")(function* (input, options = {}) {
|
|
2275
1907
|
const op = OpContext.make("mkdir");
|
|
2276
1908
|
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
@@ -2328,23 +1960,7 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2328
1960
|
rmdir: Effect.fn("Caller.rmdir")(function* (input) {
|
|
2329
1961
|
return yield* entryVerb("rmdir", input, rmdirEntry);
|
|
2330
1962
|
}),
|
|
2331
|
-
|
|
2332
|
-
remove: Effect.fn("Caller.remove")(function* (input, options) {
|
|
2333
|
-
const op = OpContext.make("remove");
|
|
2334
|
-
const prepared = yield* Effect.fromResult(prepareEntry(input, op));
|
|
2335
|
-
const chosen = yield* decodeRemoveOptions(options ?? {}).pipe(Effect.mapError((cause) => fail(preparedOp(prepared, op), cause)));
|
|
2336
|
-
const once = coordinated(op, Effect.flatMap(resolveEntry(prepared, op), (entry) => removeEntry(entry, op)));
|
|
2337
|
-
const target = (effect) => chosen.force === true
|
|
2338
|
-
? Effect.catchIf(effect, (error) => error.code === "NotFound", () => Effect.undefined)
|
|
2339
|
-
: effect;
|
|
2340
|
-
const removed = yield* Effect.result(target(once));
|
|
2341
|
-
if (Result.isSuccess(removed))
|
|
2342
|
-
return removed.success;
|
|
2343
|
-
if (chosen.recursive !== true || removed.failure.code !== "NotEmpty")
|
|
2344
|
-
return yield* removed.failure;
|
|
2345
|
-
yield* emptyDirectory(prepared, op, chosen.force === true);
|
|
2346
|
-
return yield* target(once);
|
|
2347
|
-
}),
|
|
1963
|
+
remove,
|
|
2348
1964
|
rename: Effect.fn("Caller.rename")(function* (fromInput, toInput) {
|
|
2349
1965
|
const op = OpContext.make("rename");
|
|
2350
1966
|
const from = yield* Effect.fromResult(prepareEntry(fromInput, op));
|
|
@@ -2418,19 +2034,20 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2418
2034
|
const handle = Object.freeze({
|
|
2419
2035
|
[DirectoryHandleId]: true,
|
|
2420
2036
|
stat: coordinatedRead(statOp, Effect.gen(function* () {
|
|
2421
|
-
const
|
|
2037
|
+
const ino = TokenRegistry.inode(acquired);
|
|
2038
|
+
const node = ino === undefined ? undefined : yield* view(ino);
|
|
2422
2039
|
return yield* (node === undefined
|
|
2423
2040
|
? Effect.fail(statOp.fail("InvalidHandle"))
|
|
2424
2041
|
: Effect.succeed(withMetadata(node)));
|
|
2425
2042
|
})).pipe(Effect.withSpan("DirectoryHandle.stat")),
|
|
2426
2043
|
close: coordinatedCleanup(Effect.suspend(() => {
|
|
2427
|
-
if (acquired
|
|
2044
|
+
if (TokenRegistry.inode(acquired) === undefined) {
|
|
2428
2045
|
return Effect.fail(OpContext.make("close").fail("InvalidHandle"));
|
|
2429
2046
|
}
|
|
2430
2047
|
return releaseDirectory(acquired);
|
|
2431
2048
|
})).pipe(Effect.ensuring(closeReleasedScope(acquired)), Effect.withSpan("DirectoryHandle.close"))
|
|
2432
2049
|
});
|
|
2433
|
-
|
|
2050
|
+
registry.registerDirectory(handle, acquired);
|
|
2434
2051
|
return handle;
|
|
2435
2052
|
})
|
|
2436
2053
|
});
|
|
@@ -2461,32 +2078,27 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2461
2078
|
snapshot: coordinatedRead(OpContext.make("snapshot"), Effect.sync(() => Image.make(state))).pipe(Effect.withSpan("Volume.snapshot")),
|
|
2462
2079
|
referenceKey: Effect.fn("Volume.referenceKey")(function* (reference) {
|
|
2463
2080
|
const op = OpContext.make("referenceKey");
|
|
2464
|
-
return yield* coordinatedRead(op, Effect.
|
|
2465
|
-
const
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
if (known.volume !== volumeIdentity)
|
|
2469
|
-
return Effect.fail(op.fail("ForeignReference"));
|
|
2470
|
-
if (!addressable(known.ino))
|
|
2471
|
-
return Effect.fail(op.fail("StaleReference"));
|
|
2472
|
-
const ino = BigInt(known.ino);
|
|
2473
|
-
return Effect.succeed({ identity: identityBytes.slice(), epoch: epochBytes.slice(), ino, tag: keyTag(ino) });
|
|
2081
|
+
return yield* coordinatedRead(op, Effect.gen(function* () {
|
|
2082
|
+
const node = yield* TokenRegistry.nodeOrFail(yield* registry.resolve(reference, "reference"), "reference", op);
|
|
2083
|
+
const ino = BigInt(node.ino);
|
|
2084
|
+
return { identity: identityBytes.slice(), epoch: epochBytes.slice(), ino, tag: keyTag(ino) };
|
|
2474
2085
|
}));
|
|
2475
2086
|
}),
|
|
2476
2087
|
resolveReferenceKey: Effect.fn("Volume.resolveReferenceKey")(function* (key) {
|
|
2477
2088
|
const op = OpContext.make("resolveReferenceKey");
|
|
2478
|
-
return yield* coordinatedRead(op, Effect.
|
|
2089
|
+
return yield* coordinatedRead(op, Effect.gen(function* () {
|
|
2479
2090
|
if (!Schema.is(ReferenceKey)(key))
|
|
2480
|
-
return
|
|
2091
|
+
return yield* op.fail("InvalidReference");
|
|
2481
2092
|
// Another epoch numbered its objects on its own, so its key is another volume's even under this identity.
|
|
2482
2093
|
if (!sameBytes(key.identity, identityBytes) || !sameBytes(key.epoch, epochBytes)) {
|
|
2483
|
-
return
|
|
2094
|
+
return yield* op.fail("ForeignReference");
|
|
2484
2095
|
}
|
|
2485
2096
|
// Checked before the inode is looked up, so a guessed key learns nothing about which numbers are in use.
|
|
2486
2097
|
if (!sameTag(key.tag, keyTag(key.ino)))
|
|
2487
|
-
return
|
|
2098
|
+
return yield* op.fail("InvalidReference");
|
|
2488
2099
|
const ino = Ino(Number(key.ino));
|
|
2489
|
-
|
|
2100
|
+
yield* TokenRegistry.nodeOrFail(yield* registry.resolveInode(ino), "reference", op);
|
|
2101
|
+
return referenceFor(ino);
|
|
2490
2102
|
}));
|
|
2491
2103
|
}),
|
|
2492
2104
|
caller: Effect.fn("Volume.caller")(function* (options) {
|
|
@@ -2500,47 +2112,37 @@ export const makeVolume = Effect.fnUntraced(function* (source, options, commitPr
|
|
|
2500
2112
|
changes: coordinatedRead(OpContext.make("changes"), Effect.suspend(() => observeChanges(state))),
|
|
2501
2113
|
capture: coordinatedRead(OpContext.make("capture"), captureState())
|
|
2502
2114
|
});
|
|
2503
|
-
return Object.freeze({ volume, shutdown,
|
|
2115
|
+
return Object.freeze({ volume, shutdown, observe });
|
|
2504
2116
|
});
|
|
2505
2117
|
/** @internal */
|
|
2506
2118
|
export const make = Effect.fn("VirtualFileSystem.make")(function* (options) {
|
|
2507
|
-
return (yield* makeVolume(VolumeSource.Empty(), options).pipe(Effect.catchIf((error) => Schema.is(VfsError)(error) && error.code !== "InvalidArgument", Effect.die))).volume;
|
|
2119
|
+
return (yield* makeVolume(VolumeSource.Empty(), memoryCommitProvider, options).pipe(Effect.catchIf((error) => Schema.is(VfsError)(error) && error.code !== "InvalidArgument", Effect.die))).volume;
|
|
2508
2120
|
});
|
|
2509
2121
|
/** @internal */
|
|
2510
2122
|
export const prepareEmptyLiveImage = Effect.fnUntraced(function* (options) {
|
|
2511
|
-
const
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2123
|
+
const decoded = yield* Effect.fromResult(Limits.configuration(options, "prepareEmptyImage"));
|
|
2124
|
+
const limits = decoded.limits;
|
|
2125
|
+
const identity = decoded.identity ?? VolumeIdentity.make(yield* randomHex128);
|
|
2126
|
+
const epoch = VolumeEpoch.make(yield* randomHex128);
|
|
2127
|
+
const keySecret = KeySecret.make(yield* randomHex128);
|
|
2128
|
+
const now = yield* Clock.currentTimeNanos;
|
|
2129
|
+
if (!isTimestamp(now))
|
|
2130
|
+
return yield* argumentFailure("prepareEmptyImage", "clock.currentTimeNanos");
|
|
2131
|
+
return yield* LiveImage.encode(emptyState(now), { identity, epoch, keySecret }, limits).pipe(Effect.mapError((error) => retargetFailure("prepareEmptyImage", error)));
|
|
2515
2132
|
});
|
|
2516
2133
|
/** @internal */
|
|
2517
|
-
export const openImageVolume = Effect.fnUntraced(function* (image, maxImageBytes, commit, durability = "memory-only") {
|
|
2134
|
+
export const openImageVolume = Effect.fnUntraced(function* (image, maxImageBytes, commit, durability = "memory-only", options) {
|
|
2518
2135
|
const restored = yield* LiveImage.decode(image, maxImageBytes);
|
|
2519
|
-
|
|
2520
|
-
const
|
|
2521
|
-
const
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
maxPendingOperations: 64,
|
|
2528
|
-
maxWatchEvents: 256
|
|
2136
|
+
const requested = yield* Effect.fromResult(Limits.fromOptions(options, "openImage"));
|
|
2137
|
+
const limits = yield* Effect.fromResult(Limits.fromStored(restored.limits, requested));
|
|
2138
|
+
const provider = {
|
|
2139
|
+
mode: "durable",
|
|
2140
|
+
shutdown: Effect.void,
|
|
2141
|
+
commit: (candidate) => LiveImage.encode(candidate, restored, limits).pipe(Effect.mapError((cause) => makeError({ code: "StorageRejected", operation: "commit", cause })), Effect.flatMap((bytes) => ByteSize.isGreaterThan(ByteSize.bytes(bytes.length), maxImageBytes)
|
|
2142
|
+
? Effect.fail(makeError({ code: "StorageRejected", operation: "commit" }))
|
|
2143
|
+
: commit(bytes)))
|
|
2529
2144
|
};
|
|
2530
|
-
const { volume, shutdown } = yield* makeVolume(VolumeSource.Live({ restored }),
|
|
2531
|
-
prepare: (candidate) => LiveImage.encode(candidate, restored, limits).pipe(Effect.mapError((cause) => commitOp.fail("StorageRejected", { cause })), Effect.flatMap((bytes) => ByteSize.isGreaterThan(ByteSize.bytes(bytes.length), maxImageBytes)
|
|
2532
|
-
? commitOp.fail("StorageRejected")
|
|
2533
|
-
: Effect.sync(() => {
|
|
2534
|
-
prepared = bytes;
|
|
2535
|
-
}))),
|
|
2536
|
-
commit: () => Effect.suspend(() => {
|
|
2537
|
-
const bytes = prepared;
|
|
2538
|
-
prepared = undefined;
|
|
2539
|
-
return bytes === undefined ? Effect.succeed("unknown") : commit(bytes);
|
|
2540
|
-
})
|
|
2541
|
-
}, undefined, durability);
|
|
2542
|
-
if (shutdown === undefined)
|
|
2543
|
-
return yield* imageFailure("openImage", "InvalidStructure", { field: "liveImage" });
|
|
2145
|
+
const { volume, shutdown } = yield* makeVolume(VolumeSource.Live({ restored }), provider, options, durability);
|
|
2544
2146
|
return Object.freeze({ volume, shutdown });
|
|
2545
2147
|
});
|
|
2546
2148
|
/** @internal */
|
|
@@ -2553,12 +2155,12 @@ const changeOptions = (options) => {
|
|
|
2553
2155
|
};
|
|
2554
2156
|
/** @internal */
|
|
2555
2157
|
export const fromSnapshot = Effect.fn("VirtualFileSystem.fromSnapshot")(function* (snapshot, options) {
|
|
2556
|
-
return (yield* makeVolume(VolumeSource.Restored({ value: yield* Image.valueOf(snapshot) }), options)).volume;
|
|
2158
|
+
return (yield* makeVolume(VolumeSource.Restored({ value: yield* Image.valueOf(snapshot) }), memoryCommitProvider, options)).volume;
|
|
2557
2159
|
}, Effect.mapError((error) => retargetFailure("fromSnapshot", error)));
|
|
2558
2160
|
/** @internal */
|
|
2559
2161
|
export const makeOverlay = Effect.fn("VirtualFileSystem.makeOverlay")(function* (base, options) {
|
|
2560
2162
|
const value = yield* Image.valueOf(base);
|
|
2561
|
-
const made = yield* Effect.mapError(makeVolume(VolumeSource.Restored({ value }), options), (error) => retargetFailure("makeOverlay", error));
|
|
2163
|
+
const made = yield* Effect.mapError(makeVolume(VolumeSource.Restored({ value }), memoryCommitProvider, options), (error) => retargetFailure("makeOverlay", error));
|
|
2562
2164
|
const baseObservation = yield* observeChanges(value);
|
|
2563
2165
|
const overlay = Object.freeze({
|
|
2564
2166
|
...made.volume,
|