@effect-vfs/core 0.0.1 → 0.2.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 +145 -9
- package/dist/BytePath.d.ts +21 -0
- package/dist/BytePath.d.ts.map +1 -0
- package/dist/BytePath.js +16 -0
- package/dist/Snapshot.d.ts +2 -2
- package/dist/Snapshot.js +2 -2
- package/dist/SnapshotDelta.d.ts +126 -0
- package/dist/SnapshotDelta.d.ts.map +1 -0
- package/dist/SnapshotDelta.js +112 -0
- package/dist/VirtualFileSystem.d.ts +232 -22
- package/dist/VirtualFileSystem.d.ts.map +1 -1
- package/dist/VirtualFileSystem.js +344 -95
- package/dist/internal/canonicalBase64.d.ts +7 -0
- package/dist/internal/canonicalBase64.d.ts.map +1 -0
- package/dist/internal/canonicalBase64.js +11 -0
- package/dist/internal/content.d.ts +25 -0
- package/dist/internal/content.d.ts.map +1 -0
- package/dist/internal/content.js +26 -0
- package/dist/internal/image.d.ts +7 -5
- package/dist/internal/image.d.ts.map +1 -1
- package/dist/internal/image.js +10 -14
- package/dist/internal/overlayChanges.d.ts +50 -0
- package/dist/internal/overlayChanges.d.ts.map +1 -0
- package/dist/internal/overlayChanges.js +146 -0
- package/dist/internal/overlayTesting.d.ts +12 -0
- package/dist/internal/overlayTesting.d.ts.map +1 -0
- package/dist/internal/overlayTesting.js +12 -0
- package/dist/internal/snapshotDelta.d.ts +88 -0
- package/dist/internal/snapshotDelta.d.ts.map +1 -0
- package/dist/internal/snapshotDelta.js +589 -0
- package/package.json +6 -5
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @since 0.1.0
|
|
10
10
|
*/
|
|
11
|
+
import * as ByteSize from "effect/ByteSize";
|
|
11
12
|
import * as Clock from "effect/Clock";
|
|
12
13
|
import * as Context from "effect/Context";
|
|
13
14
|
import { DecodeLimits, ImageError } from "./Snapshot.js";
|
|
@@ -18,11 +19,20 @@ import * as Encoding from "effect/Encoding";
|
|
|
18
19
|
import * as PubSub from "effect/PubSub";
|
|
19
20
|
import * as Result from "effect/Result";
|
|
20
21
|
import * as Schema from "effect/Schema";
|
|
22
|
+
import * as SchemaIssue from "effect/SchemaIssue";
|
|
23
|
+
import * as SchemaTransformation from "effect/SchemaTransformation";
|
|
21
24
|
import * as Scope from "effect/Scope";
|
|
22
25
|
import * as Semaphore from "effect/Semaphore";
|
|
23
26
|
import * as Stream from "effect/Stream";
|
|
27
|
+
import { BytePath, getBytes as getBytePathBytes, make as makeBytePath } from "./BytePath.js";
|
|
28
|
+
export { BytePath } from "./BytePath.js";
|
|
29
|
+
import * as Content from "./internal/content.js";
|
|
24
30
|
import * as Image from "./internal/image.js";
|
|
25
|
-
|
|
31
|
+
import { compareOverlay } from "./internal/overlayChanges.js";
|
|
32
|
+
import * as OverlayTesting from "./internal/overlayTesting.js";
|
|
33
|
+
import * as SnapshotDeltaInternal from "./internal/snapshotDelta.js";
|
|
34
|
+
import * as SnapshotDeltaModel from "./SnapshotDelta.js";
|
|
35
|
+
export { SnapshotChange, SnapshotChangesOptions, SnapshotDeltaError, SnapshotDeltaLimits, SnapshotDeltaTypeId, SnapshotDifference, SnapshotNodeKind } from "./SnapshotDelta.js";
|
|
26
36
|
const VolumeId = Symbol("@effect-vfs/core/Volume");
|
|
27
37
|
const CallerId = Symbol("@effect-vfs/core/Caller");
|
|
28
38
|
const FileHandleId = Symbol("@effect-vfs/core/FileHandle");
|
|
@@ -108,11 +118,11 @@ export const VolumeOptions = Schema.Struct({
|
|
|
108
118
|
/** Maximum number of filesystem nodes. Omission leaves the count unbounded. */
|
|
109
119
|
maxEntries: Schema.optionalKey(Natural),
|
|
110
120
|
/** Maximum combined regular-file content in bytes. */
|
|
111
|
-
maxBytes: Schema.optionalKey(
|
|
121
|
+
maxBytes: Schema.optionalKey(Schema.ByteSize),
|
|
112
122
|
/** Maximum content size of one regular file in bytes. */
|
|
113
|
-
maxFileBytes: Schema.optionalKey(
|
|
123
|
+
maxFileBytes: Schema.optionalKey(Schema.ByteSize.check(Schema.makeFilter((size) => ByteSize.isLessThanOrEqualTo(size, ByteSize.bytes(0xffffffff)) ? undefined : "must be at most 4294967295 bytes"))),
|
|
114
124
|
/** Maximum encoded byte length of an absolute or relative path. */
|
|
115
|
-
maxPathBytes: Schema.optionalKey(
|
|
125
|
+
maxPathBytes: Schema.optionalKey(Schema.ByteSize.check(Schema.makeFilter((size) => ByteSize.isGreaterThanOrEqualTo(size, ByteSize.bytes(1)) ? undefined : "must be at least 1 byte")))
|
|
116
126
|
});
|
|
117
127
|
// Match snapshot v1's canonical signed decimal timestamp domain.
|
|
118
128
|
const timestampLimit = 10n ** 128n - 1n;
|
|
@@ -195,6 +205,69 @@ const WriteFileSettings = Schema.Struct({
|
|
|
195
205
|
/** Mode to apply after replacing an existing file. */
|
|
196
206
|
finalMode: Schema.optionalKey(Mode)
|
|
197
207
|
});
|
|
208
|
+
/**
|
|
209
|
+
* Schema for the filesystem entry kinds reported by overlay summaries.
|
|
210
|
+
*
|
|
211
|
+
* @category schemas
|
|
212
|
+
* @since 0.1.0
|
|
213
|
+
*/
|
|
214
|
+
export const OverlayNodeKind = Schema.Literals(["directory", "file", "symlink"]);
|
|
215
|
+
/**
|
|
216
|
+
* Schema for observable fields that can differ from an overlay's immutable base.
|
|
217
|
+
*
|
|
218
|
+
* @category schemas
|
|
219
|
+
* @since 0.1.0
|
|
220
|
+
*/
|
|
221
|
+
export const OverlayDifference = Schema.Literals([
|
|
222
|
+
"content",
|
|
223
|
+
"mode",
|
|
224
|
+
"uid",
|
|
225
|
+
"gid",
|
|
226
|
+
"atimeNs",
|
|
227
|
+
"mtimeNs",
|
|
228
|
+
"ctimeNs",
|
|
229
|
+
"birthtimeNs"
|
|
230
|
+
]);
|
|
231
|
+
const OverlayDifferences = Schema.Array(OverlayDifference);
|
|
232
|
+
const NonEmptyOverlayDifferences = OverlayDifferences.check(Schema.isMinLength(1));
|
|
233
|
+
/**
|
|
234
|
+
* Schema for a final-state overlay difference.
|
|
235
|
+
*
|
|
236
|
+
* **Details**
|
|
237
|
+
*
|
|
238
|
+
* Paths retain arbitrary non-NUL bytes. Renames are reported only when retained
|
|
239
|
+
* base identity makes the removed and added names unambiguous.
|
|
240
|
+
*
|
|
241
|
+
* @category schemas
|
|
242
|
+
* @since 0.1.0
|
|
243
|
+
*/
|
|
244
|
+
export const OverlayChange = Schema.Union([
|
|
245
|
+
Schema.TaggedStruct("Added", { path: BytePath, kind: OverlayNodeKind }),
|
|
246
|
+
Schema.TaggedStruct("Removed", { path: BytePath, kind: OverlayNodeKind }),
|
|
247
|
+
Schema.TaggedStruct("Replaced", {
|
|
248
|
+
path: BytePath,
|
|
249
|
+
beforeKind: OverlayNodeKind,
|
|
250
|
+
afterKind: OverlayNodeKind,
|
|
251
|
+
differences: OverlayDifferences
|
|
252
|
+
}),
|
|
253
|
+
Schema.TaggedStruct("Renamed", {
|
|
254
|
+
from: BytePath,
|
|
255
|
+
to: BytePath,
|
|
256
|
+
kind: OverlayNodeKind,
|
|
257
|
+
differences: OverlayDifferences
|
|
258
|
+
}),
|
|
259
|
+
Schema.TaggedStruct("Updated", { path: BytePath, kind: OverlayNodeKind, differences: NonEmptyOverlayDifferences })
|
|
260
|
+
]);
|
|
261
|
+
/**
|
|
262
|
+
* Schema for overlay summary filtering.
|
|
263
|
+
*
|
|
264
|
+
* @category schemas
|
|
265
|
+
* @since 0.1.0
|
|
266
|
+
*/
|
|
267
|
+
export const OverlayChangesOptions = Schema.Struct({
|
|
268
|
+
/** Include access, modification, change, and birth-time differences. Defaults to `false`. */
|
|
269
|
+
includeTimestamps: Schema.optionalKey(Schema.Boolean)
|
|
270
|
+
});
|
|
198
271
|
/**
|
|
199
272
|
* Optional Effect service for providing an existing filesystem caller.
|
|
200
273
|
*
|
|
@@ -217,22 +290,66 @@ export const encodeSnapshot = Image.encodeSnapshot;
|
|
|
217
290
|
* @since 0.1.0
|
|
218
291
|
*/
|
|
219
292
|
export const decodeSnapshot = Image.decodeSnapshot;
|
|
220
|
-
const
|
|
293
|
+
const deltaLimits = (limits) => {
|
|
294
|
+
const decoded = decodeConfiguration(SnapshotDeltaModel.SnapshotDeltaLimits, limits ?? SnapshotDeltaModel.SnapshotDeltaLimits.default);
|
|
295
|
+
return Result.isFailure(decoded) ? Effect.fail(decoded.failure) : Effect.succeed(decoded.success);
|
|
296
|
+
};
|
|
297
|
+
/**
|
|
298
|
+
* Computes an exact portable delta between two immutable snapshots.
|
|
299
|
+
* Requires the platform-neutral `Crypto.Crypto` service for base identity.
|
|
300
|
+
*
|
|
301
|
+
* @category snapshots
|
|
302
|
+
* @since 0.1.0
|
|
303
|
+
*/
|
|
304
|
+
export const diffSnapshots = Effect.fn("VirtualFileSystem.diffSnapshots")(function* (base, target, limits) {
|
|
305
|
+
return yield* SnapshotDeltaInternal.diffSnapshots(base, target, yield* deltaLimits(limits));
|
|
306
|
+
});
|
|
307
|
+
/**
|
|
308
|
+
* Verifies an exact snapshot delta against its base and derives an owned path-oriented summary.
|
|
309
|
+
* Requires the platform-neutral `Crypto.Crypto` service for base identity.
|
|
310
|
+
*/
|
|
311
|
+
export const inspectSnapshotDelta = Effect.fn("VirtualFileSystem.inspectSnapshotDelta")(function* (base, delta, options, limits) {
|
|
312
|
+
const decoded = decodeConfiguration(SnapshotDeltaModel.SnapshotChangesOptions, options ?? {});
|
|
313
|
+
if (Result.isFailure(decoded))
|
|
314
|
+
return yield* decoded.failure;
|
|
315
|
+
return yield* SnapshotDeltaInternal.inspectSnapshotDelta(base, delta, decoded.success, yield* deltaLimits(limits));
|
|
316
|
+
});
|
|
317
|
+
/**
|
|
318
|
+
* Applies an exact delta to its semantically matching base and returns a new snapshot.
|
|
319
|
+
* Requires the platform-neutral `Crypto.Crypto` service for base identity.
|
|
320
|
+
*
|
|
321
|
+
* @category snapshots
|
|
322
|
+
* @since 0.1.0
|
|
323
|
+
*/
|
|
324
|
+
export const applySnapshotDelta = Effect.fn("VirtualFileSystem.applySnapshotDelta")(function* (base, delta, limits) {
|
|
325
|
+
return yield* SnapshotDeltaInternal.applySnapshotDelta(base, delta, yield* deltaLimits(limits));
|
|
326
|
+
});
|
|
327
|
+
const deltaSchemaIssue = (cause, input, options) => new SchemaIssue.InvalidValue({ message: `Snapshot delta ${cause.code}${cause.field === undefined ? "" : ` at ${cause.field}`}` }, input, options);
|
|
328
|
+
/**
|
|
329
|
+
* Creates an Effect Schema codec between owned bytes and opaque snapshot deltas.
|
|
330
|
+
* Omission uses `SnapshotDeltaLimits.default`.
|
|
331
|
+
*/
|
|
332
|
+
export const SnapshotDeltaFromBytes = (limits) => {
|
|
333
|
+
const selected = Schema.decodeResult(SnapshotDeltaModel.SnapshotDeltaLimits, { onExcessProperty: "error" })(limits ?? SnapshotDeltaModel.SnapshotDeltaLimits.default);
|
|
334
|
+
return Schema.Uint8Array.pipe(Schema.decodeTo(SnapshotDeltaModel.SnapshotDelta, SchemaTransformation.transformEffect({
|
|
335
|
+
decode: (input, options) => Result.isFailure(selected)
|
|
336
|
+
? Effect.fail(new SchemaIssue.InvalidValue({ message: "Invalid snapshot delta limits" }, limits, options))
|
|
337
|
+
: SnapshotDeltaInternal.decodeSnapshotDelta(input, selected.success).pipe(Effect.mapError((cause) => deltaSchemaIssue(cause, input, options))),
|
|
338
|
+
encode: (delta, options) => Result.isFailure(selected)
|
|
339
|
+
? Effect.fail(new SchemaIssue.InvalidValue({ message: "Invalid snapshot delta limits" }, limits, options))
|
|
340
|
+
: SnapshotDeltaInternal.encodeSnapshotDelta(delta, selected.success).pipe(Effect.mapError((cause) => deltaSchemaIssue(cause, delta, options)))
|
|
341
|
+
})));
|
|
342
|
+
};
|
|
221
343
|
const failure = (code, operation, path) => new FsError({ code, operation, ...(path === undefined ? {} : { path }) });
|
|
222
344
|
const ownedPath = (bytes) => {
|
|
223
|
-
|
|
224
|
-
bytePaths.set(path, bytes);
|
|
225
|
-
return path;
|
|
345
|
+
return makeBytePath(bytes);
|
|
226
346
|
};
|
|
227
347
|
const strictString = (bytes, operation) => Effect.try({
|
|
228
348
|
try: () => new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes),
|
|
229
349
|
catch: () => failure("UnrepresentableName", operation)
|
|
230
350
|
});
|
|
231
351
|
const nameBytes = (name) => {
|
|
232
|
-
|
|
233
|
-
for (let i = 0; i < bytes.length; i++)
|
|
234
|
-
bytes[i] = Number.parseInt(name.slice(i * 2, i * 2 + 2), 16);
|
|
235
|
-
return bytes;
|
|
352
|
+
return Result.getOrThrow(Encoding.decodeHex(name));
|
|
236
353
|
};
|
|
237
354
|
// A zero-length view distinguishes a detached buffer from a valid empty buffer.
|
|
238
355
|
const attachedBuffer = (bytes) => {
|
|
@@ -265,9 +382,7 @@ export const pathFromBytes = Effect.fn("VirtualFileSystem.pathFromBytes")(functi
|
|
|
265
382
|
const owned = new Uint8Array(bytes);
|
|
266
383
|
if (owned.length === 0 || owned.includes(0))
|
|
267
384
|
return yield* failure("InvalidArgument", "pathFromBytes");
|
|
268
|
-
|
|
269
|
-
bytePaths.set(path, owned);
|
|
270
|
-
return path;
|
|
385
|
+
return makeBytePath(owned);
|
|
271
386
|
});
|
|
272
387
|
/**
|
|
273
388
|
* Copies the bytes held by an opaque byte path.
|
|
@@ -276,7 +391,7 @@ export const pathFromBytes = Effect.fn("VirtualFileSystem.pathFromBytes")(functi
|
|
|
276
391
|
* @since 0.1.0
|
|
277
392
|
*/
|
|
278
393
|
export const pathToBytes = Effect.fn("VirtualFileSystem.pathToBytes")(function* (path) {
|
|
279
|
-
const bytes =
|
|
394
|
+
const bytes = getBytePathBytes(path);
|
|
280
395
|
if (bytes === undefined)
|
|
281
396
|
return yield* failure("InvalidArgument", "pathToBytes");
|
|
282
397
|
return new Uint8Array(bytes);
|
|
@@ -304,7 +419,7 @@ const preparePath = (input, operation, maxPathBytes) => {
|
|
|
304
419
|
bytes = new TextEncoder().encode(input);
|
|
305
420
|
}
|
|
306
421
|
else if (typeof input === "object" && input !== null) {
|
|
307
|
-
bytes =
|
|
422
|
+
bytes = getBytePathBytes(input);
|
|
308
423
|
}
|
|
309
424
|
if (bytes === undefined)
|
|
310
425
|
return Result.fail(failure("InvalidArgument", operation));
|
|
@@ -312,7 +427,7 @@ const preparePath = (input, operation, maxPathBytes) => {
|
|
|
312
427
|
return Result.fail(failure("NotFound", operation, input));
|
|
313
428
|
if (bytes.includes(0))
|
|
314
429
|
return Result.fail(failure("InvalidArgument", operation, input));
|
|
315
|
-
if (maxPathBytes !== undefined && bytes.length
|
|
430
|
+
if (maxPathBytes !== undefined && ByteSize.isGreaterThan(ByteSize.bytes(bytes.length), maxPathBytes)) {
|
|
316
431
|
return Result.fail(failure("PathTooLong", operation, input));
|
|
317
432
|
}
|
|
318
433
|
const components = [];
|
|
@@ -370,7 +485,8 @@ const storedMetadata = (metadata) => ({
|
|
|
370
485
|
birthtimeNs: String(metadata.birthtimeNs)
|
|
371
486
|
});
|
|
372
487
|
/** Each execution constructs a fresh volume and captures its Clock. */
|
|
373
|
-
const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (
|
|
488
|
+
const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (source, options) {
|
|
489
|
+
const image = source._tag === "Empty" ? undefined : source.image;
|
|
374
490
|
const decoded = decodeConfiguration(VolumeOptions, options === undefined ? {} : options);
|
|
375
491
|
if (Result.isFailure(decoded))
|
|
376
492
|
return yield* decoded.failure;
|
|
@@ -388,17 +504,19 @@ const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (options,
|
|
|
388
504
|
const gate = Semaphore.makeUnsafe(1);
|
|
389
505
|
const root = {
|
|
390
506
|
kind: "directory",
|
|
507
|
+
lineage: image?.root,
|
|
391
508
|
parent: undefined,
|
|
392
509
|
entries: new Map(),
|
|
393
510
|
metadata: directoryMetadata(1n, 0, 0, 0o755, initialTime)
|
|
394
511
|
};
|
|
395
512
|
let nextInode = 2n;
|
|
396
513
|
let entries = 0;
|
|
397
|
-
let usedBytes =
|
|
398
|
-
|
|
514
|
+
let usedBytes = 0n;
|
|
515
|
+
// The schema caps this value at uint32, so this boundary conversion is exact.
|
|
516
|
+
const maxFileBytes = Number(ByteSize.toBigInt(settings.maxFileBytes ?? ByteSize.bytes(0xffffffff)));
|
|
399
517
|
if (image !== undefined) {
|
|
400
518
|
const incoming = new Map();
|
|
401
|
-
let content =
|
|
519
|
+
let content = 0n;
|
|
402
520
|
let count = 0;
|
|
403
521
|
for (const record of image.records) {
|
|
404
522
|
if (record.kind === "directory")
|
|
@@ -408,13 +526,14 @@ const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (options,
|
|
|
408
526
|
if (record.kind === "file" && length > maxFileBytes) {
|
|
409
527
|
return yield* new ImageError({ code: "LimitExceeded", field: "maxFileBytes" });
|
|
410
528
|
}
|
|
411
|
-
content += length;
|
|
529
|
+
content += BigInt(length);
|
|
412
530
|
}
|
|
413
531
|
}
|
|
414
532
|
if ((settings.maxEntries !== undefined && count > settings.maxEntries) ||
|
|
415
|
-
(settings.maxBytes !== undefined && content > settings.maxBytes)) {
|
|
533
|
+
(settings.maxBytes !== undefined && content > ByteSize.toBigInt(settings.maxBytes))) {
|
|
416
534
|
return yield* new ImageError({ code: "LimitExceeded", field: "volume" });
|
|
417
535
|
}
|
|
536
|
+
const baseContents = source._tag === "Overlay" ? Content.forOverlay(source.base, image) : undefined;
|
|
418
537
|
for (const record of image.records) {
|
|
419
538
|
const metadata = {
|
|
420
539
|
...record.metadata,
|
|
@@ -430,22 +549,28 @@ const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (options,
|
|
|
430
549
|
if (record.kind === "directory") {
|
|
431
550
|
const node = record.id === image.root
|
|
432
551
|
? root
|
|
433
|
-
: { kind: "directory", parent: undefined, entries: new Map(), metadata };
|
|
552
|
+
: { kind: "directory", lineage: record.id, parent: undefined, entries: new Map(), metadata };
|
|
434
553
|
node.metadata = metadata;
|
|
435
554
|
incoming.set(record.id, node);
|
|
436
555
|
}
|
|
437
556
|
else if (record.kind === "file") {
|
|
438
|
-
const data = Image.bytes(record.data);
|
|
557
|
+
const data = baseContents?.get(record.id) ?? Content.make(Image.bytes(record.data));
|
|
439
558
|
incoming.set(record.id, {
|
|
440
559
|
kind: "file",
|
|
560
|
+
lineage: record.id,
|
|
441
561
|
data,
|
|
442
562
|
openCount: 0,
|
|
443
|
-
metadata: { ...metadata, size: BigInt(data.length) }
|
|
563
|
+
metadata: { ...metadata, size: BigInt(data.bytes.length) }
|
|
444
564
|
});
|
|
445
565
|
}
|
|
446
566
|
else {
|
|
447
567
|
const target = Image.bytes(record.target);
|
|
448
|
-
incoming.set(record.id, {
|
|
568
|
+
incoming.set(record.id, {
|
|
569
|
+
kind: "symlink",
|
|
570
|
+
lineage: record.id,
|
|
571
|
+
target,
|
|
572
|
+
metadata: { ...metadata, size: BigInt(target.length) }
|
|
573
|
+
});
|
|
449
574
|
}
|
|
450
575
|
}
|
|
451
576
|
for (const record of image.records) {
|
|
@@ -513,6 +638,75 @@ const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (options,
|
|
|
513
638
|
}
|
|
514
639
|
}
|
|
515
640
|
};
|
|
641
|
+
const captureSnapshot = Effect.fnUntraced(function* () {
|
|
642
|
+
const ids = new Map([[root, "0"]]);
|
|
643
|
+
const pending = [root];
|
|
644
|
+
const records = [];
|
|
645
|
+
for (let index = 0; index < pending.length; index++) {
|
|
646
|
+
const node = pending[index];
|
|
647
|
+
if (node === undefined)
|
|
648
|
+
continue;
|
|
649
|
+
const id = ids.get(node);
|
|
650
|
+
if (id === undefined)
|
|
651
|
+
return yield* new ImageError({ code: "InvalidStructure" });
|
|
652
|
+
const metadata = storedMetadata(node.metadata);
|
|
653
|
+
if (node.kind === "directory") {
|
|
654
|
+
const children = [];
|
|
655
|
+
for (const [name, child] of node.entries) {
|
|
656
|
+
let target = ids.get(child);
|
|
657
|
+
if (target === undefined) {
|
|
658
|
+
target = String(ids.size);
|
|
659
|
+
ids.set(child, target);
|
|
660
|
+
pending.push(child);
|
|
661
|
+
}
|
|
662
|
+
children.push({ name: Image.base64(nameBytes(name)), target });
|
|
663
|
+
}
|
|
664
|
+
records.push({ id, kind: "directory", metadata, entries: children });
|
|
665
|
+
}
|
|
666
|
+
else if (node.kind === "file") {
|
|
667
|
+
records.push({ id, kind: "file", metadata, data: Image.base64(node.data.bytes) });
|
|
668
|
+
}
|
|
669
|
+
else
|
|
670
|
+
records.push({ id, kind: "symlink", metadata, target: Image.base64(node.target) });
|
|
671
|
+
}
|
|
672
|
+
return yield* Image.capture({ format: "effect-vfs", version: 1, root: "0", records });
|
|
673
|
+
});
|
|
674
|
+
const observeChanges = () => {
|
|
675
|
+
const observation = [];
|
|
676
|
+
const paths = [[root, new Uint8Array([47])]];
|
|
677
|
+
for (let index = 0; index < paths.length; index++) {
|
|
678
|
+
const current = paths[index];
|
|
679
|
+
if (current === undefined)
|
|
680
|
+
continue;
|
|
681
|
+
const [node, path] = current;
|
|
682
|
+
observation.push({
|
|
683
|
+
path: new Uint8Array(path),
|
|
684
|
+
lineage: node.lineage,
|
|
685
|
+
kind: node.kind,
|
|
686
|
+
content: node.kind === "file" ? node.data.bytes : node.kind === "symlink" ? node.target : undefined,
|
|
687
|
+
metadata: storedMetadata(node.metadata)
|
|
688
|
+
});
|
|
689
|
+
if (node.kind !== "directory")
|
|
690
|
+
continue;
|
|
691
|
+
for (const [name, child] of node.entries) {
|
|
692
|
+
const bytes = nameBytes(name);
|
|
693
|
+
const childPath = new Uint8Array(path.length + (path.length === 1 ? 0 : 1) + bytes.length);
|
|
694
|
+
childPath.set(path);
|
|
695
|
+
let offset = path.length;
|
|
696
|
+
if (path.length !== 1)
|
|
697
|
+
childPath[offset++] = 47;
|
|
698
|
+
childPath.set(bytes, offset);
|
|
699
|
+
paths.push([child, childPath]);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return observation;
|
|
703
|
+
};
|
|
704
|
+
const captureState = Effect.fnUntraced(function* (hook) {
|
|
705
|
+
const snapshot = yield* captureSnapshot();
|
|
706
|
+
if (hook !== undefined)
|
|
707
|
+
yield* hook.betweenSnapshotAndSummary;
|
|
708
|
+
return { snapshot, observation: observeChanges() };
|
|
709
|
+
});
|
|
516
710
|
// Permit waits stay interruptible. State transitions and resource registration do not.
|
|
517
711
|
const coordinated = (effect) => gate.withPermit(Effect.uninterruptible(effect));
|
|
518
712
|
const release = (reference) => coordinated(Effect.sync(() => {
|
|
@@ -534,8 +728,8 @@ const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (options,
|
|
|
534
728
|
};
|
|
535
729
|
const reclaim = (file) => {
|
|
536
730
|
if (file.metadata.nlink === 0 && file.openCount === 0) {
|
|
537
|
-
usedBytes -= file.data.length;
|
|
538
|
-
file.data =
|
|
731
|
+
usedBytes -= BigInt(file.data.bytes.length);
|
|
732
|
+
file.data = Content.empty();
|
|
539
733
|
}
|
|
540
734
|
};
|
|
541
735
|
const detach = (node, now) => {
|
|
@@ -548,7 +742,7 @@ const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (options,
|
|
|
548
742
|
if (node.kind === "file")
|
|
549
743
|
reclaim(node);
|
|
550
744
|
else if (node.metadata.nlink === 0)
|
|
551
|
-
usedBytes -= node.target.length;
|
|
745
|
+
usedBytes -= BigInt(node.target.length);
|
|
552
746
|
}
|
|
553
747
|
};
|
|
554
748
|
const releaseFile = (ref) => {
|
|
@@ -565,14 +759,15 @@ const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (options,
|
|
|
565
759
|
if (length > BigInt(maxFileBytes))
|
|
566
760
|
return yield* failure("FileTooLarge", operation);
|
|
567
761
|
const size = Number(length);
|
|
568
|
-
if (
|
|
762
|
+
if (settings.maxBytes !== undefined &&
|
|
763
|
+
BigInt(size - file.data.bytes.length) > ByteSize.toBigInt(settings.maxBytes) - usedBytes) {
|
|
569
764
|
return yield* failure("NoSpace", operation);
|
|
570
765
|
}
|
|
571
766
|
const data = new Uint8Array(size);
|
|
572
|
-
data.set(file.data.subarray(0, size));
|
|
767
|
+
data.set(file.data.bytes.subarray(0, size));
|
|
573
768
|
const now = yield* timestamp(operation);
|
|
574
|
-
usedBytes += size - file.data.length;
|
|
575
|
-
file.data = data;
|
|
769
|
+
usedBytes += BigInt(size - file.data.bytes.length);
|
|
770
|
+
file.data = Content.make(data);
|
|
576
771
|
file.metadata = {
|
|
577
772
|
...file.metadata,
|
|
578
773
|
size: length,
|
|
@@ -596,7 +791,7 @@ const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (options,
|
|
|
596
791
|
return yield* failure("InvalidArgument", "read");
|
|
597
792
|
}
|
|
598
793
|
const start = Number(offset > file.metadata.size ? file.metadata.size : offset);
|
|
599
|
-
const data = file.data.slice(start, start + Math.min(maximum, file.data.length - start));
|
|
794
|
+
const data = file.data.bytes.slice(start, start + Math.min(maximum, file.data.bytes.length - start));
|
|
600
795
|
if (maximum > 0) {
|
|
601
796
|
file.metadata = { ...file.metadata, atimeNs: (yield* timestamp("read")) };
|
|
602
797
|
}
|
|
@@ -621,19 +816,23 @@ const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (options,
|
|
|
621
816
|
if (offset >= BigInt(maxFileBytes))
|
|
622
817
|
return yield* failure("FileTooLarge", "write");
|
|
623
818
|
const start = Number(offset);
|
|
624
|
-
const free =
|
|
625
|
-
|
|
819
|
+
const free = settings.maxBytes === undefined
|
|
820
|
+
? BigInt(maxFileBytes)
|
|
821
|
+
: ByteSize.toBigInt(settings.maxBytes) - usedBytes;
|
|
822
|
+
const maximumEnd = BigInt(file.data.bytes.length) + free;
|
|
823
|
+
const end = Number(BigInt(maxFileBytes) < maximumEnd ? BigInt(maxFileBytes) : maximumEnd);
|
|
626
824
|
const count = Math.min(bytes.length, Math.max(0, end - start));
|
|
627
825
|
if (count === 0)
|
|
628
826
|
return yield* failure("NoSpace", "write");
|
|
629
|
-
const size = Math.max(file.data.length, start + count);
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
827
|
+
const size = Math.max(file.data.bytes.length, start + count);
|
|
828
|
+
// Always detach before mutation. A same-sized write is the critical
|
|
829
|
+
// case: the current payload may belong to the base or a prior capture.
|
|
830
|
+
const data = new Uint8Array(size);
|
|
831
|
+
data.set(file.data.bytes);
|
|
633
832
|
const now = yield* timestamp("write");
|
|
634
833
|
data.set(bytes.subarray(0, count), start);
|
|
635
|
-
usedBytes += size - file.data.length;
|
|
636
|
-
file.data = data;
|
|
834
|
+
usedBytes += BigInt(size - file.data.bytes.length);
|
|
835
|
+
file.data = Content.make(data);
|
|
637
836
|
file.metadata = {
|
|
638
837
|
...file.metadata,
|
|
639
838
|
size: BigInt(size),
|
|
@@ -750,7 +949,8 @@ const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (options,
|
|
|
750
949
|
if (++traversals > 40)
|
|
751
950
|
return yield* failure("SymlinkLoop", operation, path.input);
|
|
752
951
|
const suffix = work.suffixes[index] ?? new Uint8Array(0);
|
|
753
|
-
if (settings.maxPathBytes !== undefined &&
|
|
952
|
+
if (settings.maxPathBytes !== undefined &&
|
|
953
|
+
ByteSize.isGreaterThan(ByteSize.bytes(child.target.length + suffix.length), settings.maxPathBytes)) {
|
|
754
954
|
return yield* failure("PathTooLong", operation, path.input);
|
|
755
955
|
}
|
|
756
956
|
const expansion = new Uint8Array(child.target.length + suffix.length);
|
|
@@ -958,7 +1158,7 @@ const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (options,
|
|
|
958
1158
|
if (node.kind !== "file")
|
|
959
1159
|
return yield* failure("IsDirectory", "readFile", input);
|
|
960
1160
|
yield* authorize(node, identity, 4, "readFile", input);
|
|
961
|
-
const data = new Uint8Array(node.data);
|
|
1161
|
+
const data = new Uint8Array(node.data.bytes);
|
|
962
1162
|
node.metadata = { ...node.metadata, atimeNs: (yield* timestamp("readFile")) };
|
|
963
1163
|
return data;
|
|
964
1164
|
}));
|
|
@@ -1009,14 +1209,15 @@ const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (options,
|
|
|
1009
1209
|
else
|
|
1010
1210
|
yield* authorize(file, identity, chosen.access === "readWrite" ? 6 : 2, "writeFile", input);
|
|
1011
1211
|
const finalMode = chosen.finalMode === undefined ? undefined : yield* permittedMode(file?.metadata ?? { kind: "file", uid: identity.uid, gid: parent.metadata.gid }, chosen.finalMode, "writeFile", input);
|
|
1012
|
-
const previous = file?.data.length ?? 0;
|
|
1212
|
+
const previous = file?.data.bytes.length ?? 0;
|
|
1013
1213
|
const initial = chosen.truncate ? 0 : previous;
|
|
1014
1214
|
const position = chosen.append ? initial : 0;
|
|
1015
1215
|
const size = Math.max(initial, position + captured.length);
|
|
1016
1216
|
if (size > maxFileBytes)
|
|
1017
1217
|
return yield* failure("FileTooLarge", "writeFile", input);
|
|
1018
1218
|
const reclaimed = replaced !== undefined && replaced.metadata.nlink === 1 ? replaced.target.length : 0;
|
|
1019
|
-
if (
|
|
1219
|
+
if (settings.maxBytes !== undefined &&
|
|
1220
|
+
BigInt(size - previous) > ByteSize.toBigInt(settings.maxBytes) - usedBytes + BigInt(reclaimed)) {
|
|
1020
1221
|
return yield* failure("NoSpace", "writeFile", input);
|
|
1021
1222
|
}
|
|
1022
1223
|
if (file !== undefined && !chosen.truncate && captured.length === 0 && chosen.finalMode === undefined) {
|
|
@@ -1026,14 +1227,15 @@ const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (options,
|
|
|
1026
1227
|
if (position !== 0 || size !== captured.length) {
|
|
1027
1228
|
data = new Uint8Array(size);
|
|
1028
1229
|
if (file !== undefined && !chosen.truncate)
|
|
1029
|
-
data.set(file.data);
|
|
1230
|
+
data.set(file.data.bytes);
|
|
1030
1231
|
data.set(captured, position);
|
|
1031
1232
|
}
|
|
1032
1233
|
const now = yield* timestamp("writeFile");
|
|
1033
1234
|
const node = file ??
|
|
1034
1235
|
{
|
|
1035
1236
|
kind: "file",
|
|
1036
|
-
|
|
1237
|
+
lineage: undefined,
|
|
1238
|
+
data: Content.make(data),
|
|
1037
1239
|
openCount: 0,
|
|
1038
1240
|
metadata: {
|
|
1039
1241
|
...directoryMetadata(nextInode++, identity.uid, parent.metadata.gid, (chosen.mode ?? 0o666) & 0o777 & ~umask, now),
|
|
@@ -1041,7 +1243,7 @@ const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (options,
|
|
|
1041
1243
|
nlink: 1
|
|
1042
1244
|
}
|
|
1043
1245
|
};
|
|
1044
|
-
node.data = data;
|
|
1246
|
+
node.data = Content.make(data);
|
|
1045
1247
|
node.metadata = {
|
|
1046
1248
|
...node.metadata,
|
|
1047
1249
|
mode: finalMode ?? node.metadata.mode & ~0o6000,
|
|
@@ -1049,7 +1251,7 @@ const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (options,
|
|
|
1049
1251
|
mtimeNs: now,
|
|
1050
1252
|
ctimeNs: now
|
|
1051
1253
|
};
|
|
1052
|
-
usedBytes += size - previous;
|
|
1254
|
+
usedBytes += BigInt(size - previous);
|
|
1053
1255
|
if (file === undefined) {
|
|
1054
1256
|
if (replaced !== undefined)
|
|
1055
1257
|
detach(replaced, now);
|
|
@@ -1152,7 +1354,7 @@ const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (options,
|
|
|
1152
1354
|
if (typeof target === "string" && !wellFormed(target)) {
|
|
1153
1355
|
return yield* failure("InvalidPathEncoding", "symlink", target);
|
|
1154
1356
|
}
|
|
1155
|
-
const rawTarget = typeof target === "string" ? new TextEncoder().encode(target) :
|
|
1357
|
+
const rawTarget = typeof target === "string" ? new TextEncoder().encode(target) : getBytePathBytes(target);
|
|
1156
1358
|
if (rawTarget === undefined || rawTarget.includes(0)) {
|
|
1157
1359
|
return yield* failure("InvalidArgument", "symlink", target);
|
|
1158
1360
|
}
|
|
@@ -1170,11 +1372,13 @@ const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (options,
|
|
|
1170
1372
|
if (path.trailingSlash)
|
|
1171
1373
|
return yield* failure("NotDirectory", "symlink", input);
|
|
1172
1374
|
if ((settings.maxEntries !== undefined && entries >= settings.maxEntries) ||
|
|
1173
|
-
|
|
1375
|
+
(settings.maxBytes !== undefined &&
|
|
1376
|
+
BigInt(bytes.length) > ByteSize.toBigInt(settings.maxBytes) - usedBytes))
|
|
1174
1377
|
return yield* failure("NoSpace", "symlink", input);
|
|
1175
1378
|
const now = yield* timestamp("symlink");
|
|
1176
1379
|
const node = {
|
|
1177
1380
|
kind: "symlink",
|
|
1381
|
+
lineage: undefined,
|
|
1178
1382
|
target: new Uint8Array(bytes),
|
|
1179
1383
|
metadata: {
|
|
1180
1384
|
...directoryMetadata(nextInode, identity.uid, parent.metadata.gid, 0o777, now),
|
|
@@ -1187,7 +1391,7 @@ const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (options,
|
|
|
1187
1391
|
parent.metadata = { ...parent.metadata, mtimeNs: now, ctimeNs: now };
|
|
1188
1392
|
nextInode += 1n;
|
|
1189
1393
|
entries += 1;
|
|
1190
|
-
usedBytes += bytes.length;
|
|
1394
|
+
usedBytes += BigInt(bytes.length);
|
|
1191
1395
|
publishEntry("Create", parent, name);
|
|
1192
1396
|
}));
|
|
1193
1397
|
}),
|
|
@@ -1269,7 +1473,8 @@ const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (options,
|
|
|
1269
1473
|
const now = yield* timestamp("open");
|
|
1270
1474
|
file = {
|
|
1271
1475
|
kind: "file",
|
|
1272
|
-
|
|
1476
|
+
lineage: undefined,
|
|
1477
|
+
data: Content.empty(),
|
|
1273
1478
|
openCount: 0,
|
|
1274
1479
|
metadata: {
|
|
1275
1480
|
...directoryMetadata(nextInode, identity.uid, parent.metadata.gid, (chosen.mode ?? 0o666) & 0o777 & ~umask, now),
|
|
@@ -1464,6 +1669,7 @@ const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (options,
|
|
|
1464
1669
|
const now = yield* timestamp("mkdir");
|
|
1465
1670
|
const child = {
|
|
1466
1671
|
kind: "directory",
|
|
1672
|
+
lineage: undefined,
|
|
1467
1673
|
parent,
|
|
1468
1674
|
entries: new Map(),
|
|
1469
1675
|
metadata: directoryMetadata(nextInode, identity.uid, parent.metadata.gid, (mode & 0o777 & ~umask) | (mode & 0o1000), now)
|
|
@@ -1506,6 +1712,28 @@ const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (options,
|
|
|
1506
1712
|
})
|
|
1507
1713
|
});
|
|
1508
1714
|
};
|
|
1715
|
+
const baseObservation = source._tag === "Overlay" ? observeChanges() : undefined;
|
|
1716
|
+
const publicChange = (change) => {
|
|
1717
|
+
switch (change._tag) {
|
|
1718
|
+
case "Added":
|
|
1719
|
+
case "Removed":
|
|
1720
|
+
case "Updated":
|
|
1721
|
+
case "Replaced":
|
|
1722
|
+
return Object.freeze({ ...change, path: ownedPath(new Uint8Array(change.path)) });
|
|
1723
|
+
case "Renamed":
|
|
1724
|
+
return Object.freeze({
|
|
1725
|
+
...change,
|
|
1726
|
+
from: ownedPath(new Uint8Array(change.from)),
|
|
1727
|
+
to: ownedPath(new Uint8Array(change.to))
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
throw new Error("Unknown internal overlay change");
|
|
1731
|
+
};
|
|
1732
|
+
const publicChanges = (changes) => Object.freeze(changes.map(publicChange));
|
|
1733
|
+
const changeOptions = (options) => {
|
|
1734
|
+
const decoded = decodeConfiguration(OverlayChangesOptions, options === undefined ? {} : options);
|
|
1735
|
+
return Result.isFailure(decoded) ? Effect.fail(decoded.failure) : Effect.succeed(decoded.success);
|
|
1736
|
+
};
|
|
1509
1737
|
const volume = Object.freeze({
|
|
1510
1738
|
[VolumeId]: true,
|
|
1511
1739
|
watch: Effect.gen(function* () {
|
|
@@ -1516,39 +1744,7 @@ const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (options,
|
|
|
1516
1744
|
}));
|
|
1517
1745
|
return Stream.fromEffectRepeat(PubSub.take(subscription));
|
|
1518
1746
|
}).pipe(Effect.withSpan("Volume.watch")),
|
|
1519
|
-
snapshot: coordinated(Effect.
|
|
1520
|
-
const ids = new Map([[root, "0"]]);
|
|
1521
|
-
const pending = [root];
|
|
1522
|
-
const records = [];
|
|
1523
|
-
for (let index = 0; index < pending.length; index++) {
|
|
1524
|
-
const node = pending[index];
|
|
1525
|
-
if (node === undefined)
|
|
1526
|
-
continue;
|
|
1527
|
-
const id = ids.get(node);
|
|
1528
|
-
if (id === undefined)
|
|
1529
|
-
return yield* new ImageError({ code: "InvalidStructure" });
|
|
1530
|
-
const metadata = storedMetadata(node.metadata);
|
|
1531
|
-
if (node.kind === "directory") {
|
|
1532
|
-
const children = [];
|
|
1533
|
-
for (const [name, child] of node.entries) {
|
|
1534
|
-
let target = ids.get(child);
|
|
1535
|
-
if (target === undefined) {
|
|
1536
|
-
target = String(ids.size);
|
|
1537
|
-
ids.set(child, target);
|
|
1538
|
-
pending.push(child);
|
|
1539
|
-
}
|
|
1540
|
-
children.push({ name: Image.base64(nameBytes(name)), target });
|
|
1541
|
-
}
|
|
1542
|
-
records.push({ id, kind: "directory", metadata, entries: children });
|
|
1543
|
-
}
|
|
1544
|
-
else if (node.kind === "file") {
|
|
1545
|
-
records.push({ id, kind: "file", metadata, data: Image.base64(node.data) });
|
|
1546
|
-
}
|
|
1547
|
-
else
|
|
1548
|
-
records.push({ id, kind: "symlink", metadata, target: Image.base64(node.target) });
|
|
1549
|
-
}
|
|
1550
|
-
return yield* Image.capture({ format: "effect-vfs", version: 1, root: "0", records });
|
|
1551
|
-
})).pipe(Effect.withSpan("Volume.snapshot")),
|
|
1747
|
+
snapshot: coordinated(captureSnapshot()).pipe(Effect.withSpan("Volume.snapshot")),
|
|
1552
1748
|
caller: Effect.fn("Volume.caller")(function* (options) {
|
|
1553
1749
|
const decoded = decodeConfiguration(RootCallerOptions, options === undefined ? {} : options);
|
|
1554
1750
|
if (Result.isFailure(decoded))
|
|
@@ -1558,7 +1754,27 @@ const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (options,
|
|
|
1558
1754
|
return createCaller({ volume: volumeIdentity, directory: root, closed: false }, identity, decoded.success.umask ?? 0o022);
|
|
1559
1755
|
})
|
|
1560
1756
|
});
|
|
1561
|
-
|
|
1757
|
+
if (source._tag !== "Overlay" || baseObservation === undefined) {
|
|
1758
|
+
return { _tag: "Volume", volume };
|
|
1759
|
+
}
|
|
1760
|
+
const hook = OverlayTesting.getObservationHook(source.base);
|
|
1761
|
+
const overlay = Object.freeze({
|
|
1762
|
+
...volume,
|
|
1763
|
+
changes: Effect.fn("OverlayVolume.changes")(function* (options) {
|
|
1764
|
+
const selected = yield* changeOptions(options);
|
|
1765
|
+
const current = yield* coordinated(Effect.sync(observeChanges));
|
|
1766
|
+
return publicChanges(compareOverlay(baseObservation, current, selected.includeTimestamps ?? false));
|
|
1767
|
+
}),
|
|
1768
|
+
capture: Effect.fn("OverlayVolume.capture")(function* (options) {
|
|
1769
|
+
const selected = yield* changeOptions(options);
|
|
1770
|
+
const current = yield* coordinated(captureState(hook));
|
|
1771
|
+
return Object.freeze({
|
|
1772
|
+
snapshot: current.snapshot,
|
|
1773
|
+
changes: publicChanges(compareOverlay(baseObservation, current.observation, selected.includeTimestamps ?? false))
|
|
1774
|
+
});
|
|
1775
|
+
})
|
|
1776
|
+
});
|
|
1777
|
+
return { _tag: "Overlay", volume: overlay };
|
|
1562
1778
|
});
|
|
1563
1779
|
/**
|
|
1564
1780
|
* Creates a fresh empty volume and captures the current Effect `Clock`.
|
|
@@ -1572,7 +1788,10 @@ const makeVolume = Effect.fn("VirtualFileSystem.makeVolume")(function* (options,
|
|
|
1572
1788
|
* @since 0.1.0
|
|
1573
1789
|
*/
|
|
1574
1790
|
export const make = Effect.fn("VirtualFileSystem.make")(function* (options) {
|
|
1575
|
-
|
|
1791
|
+
const result = yield* makeVolume({ _tag: "Empty" }, options).pipe(Effect.catchTag("ImageError", Effect.die));
|
|
1792
|
+
if (result._tag === "Overlay")
|
|
1793
|
+
return yield* Effect.die(new Error("empty volume constructed as overlay"));
|
|
1794
|
+
return result.volume;
|
|
1576
1795
|
});
|
|
1577
1796
|
/**
|
|
1578
1797
|
* Restores a fresh volume from an opaque snapshot under the supplied destination limits.
|
|
@@ -1582,7 +1801,33 @@ export const make = Effect.fn("VirtualFileSystem.make")(function* (options) {
|
|
|
1582
1801
|
*/
|
|
1583
1802
|
export const fromSnapshot = Effect.fn("VirtualFileSystem.fromSnapshot")(function* (snapshot, options) {
|
|
1584
1803
|
const image = yield* Image.inspect(snapshot);
|
|
1585
|
-
|
|
1804
|
+
const result = yield* makeVolume({ _tag: "Snapshot", image }, options);
|
|
1805
|
+
if (result._tag === "Overlay")
|
|
1806
|
+
return yield* new ImageError({ code: "InvalidStructure" });
|
|
1807
|
+
return result.volume;
|
|
1808
|
+
});
|
|
1809
|
+
/**
|
|
1810
|
+
* Creates an isolated writable volume relative to one immutable snapshot base.
|
|
1811
|
+
*
|
|
1812
|
+
* **Details**
|
|
1813
|
+
*
|
|
1814
|
+
* Workspaces made from the same snapshot share unchanged regular-file payloads.
|
|
1815
|
+
* The first content mutation copies the whole file into workspace-private
|
|
1816
|
+
* storage. Metadata, namespace state, coordination, handles, and watches are
|
|
1817
|
+
* always private to the new workspace.
|
|
1818
|
+
*
|
|
1819
|
+
* Invalid base snapshots fail with `ImageError`; invalid volume limits fail
|
|
1820
|
+
* with `ConfigurationError`. Each execution creates a fresh workspace.
|
|
1821
|
+
*
|
|
1822
|
+
* @category constructors
|
|
1823
|
+
* @since 0.1.0
|
|
1824
|
+
*/
|
|
1825
|
+
export const makeOverlay = Effect.fn("VirtualFileSystem.makeOverlay")(function* (base, options) {
|
|
1826
|
+
const image = yield* Image.inspect(base);
|
|
1827
|
+
const result = yield* makeVolume({ _tag: "Overlay", base, image }, options);
|
|
1828
|
+
if (result._tag === "Volume")
|
|
1829
|
+
return yield* new ImageError({ code: "InvalidStructure" });
|
|
1830
|
+
return result.volume;
|
|
1586
1831
|
});
|
|
1587
1832
|
const FixtureMetadata = Schema.Struct({
|
|
1588
1833
|
uid: Schema.optionalKey(Natural),
|
|
@@ -1595,7 +1840,7 @@ const FixtureMetadata = Schema.Struct({
|
|
|
1595
1840
|
});
|
|
1596
1841
|
const FixturePath = Schema.Union([
|
|
1597
1842
|
Schema.String,
|
|
1598
|
-
|
|
1843
|
+
BytePath
|
|
1599
1844
|
]);
|
|
1600
1845
|
/**
|
|
1601
1846
|
* Schema for a complete fixture namespace with optional metadata and forward hard links.
|
|
@@ -1713,7 +1958,7 @@ export const fromFixture = Effect.fn("VirtualFileSystem.fromFixture")(function*
|
|
|
1713
1958
|
}
|
|
1714
1959
|
const target = typeof entry.target === "string"
|
|
1715
1960
|
? new TextEncoder().encode(entry.target)
|
|
1716
|
-
:
|
|
1961
|
+
: getBytePathBytes(entry.target);
|
|
1717
1962
|
if (target === undefined || target.includes(0)) {
|
|
1718
1963
|
return yield* new ImageError({
|
|
1719
1964
|
code: "InvalidStructure",
|
|
@@ -1764,5 +2009,9 @@ export const fromFixture = Effect.fn("VirtualFileSystem.fromFixture")(function*
|
|
|
1764
2009
|
}
|
|
1765
2010
|
const records = [...new Set(declarations.values())].map((record) => record.kind === "directory" ? { ...record, entries: children.get(record.id) ?? [] } : record);
|
|
1766
2011
|
const snapshot = yield* Image.capture({ format: "effect-vfs", version: 1, root: "root", records });
|
|
1767
|
-
|
|
2012
|
+
const image = yield* Image.inspect(snapshot);
|
|
2013
|
+
const result = yield* makeVolume({ _tag: "Snapshot", image }, config.success);
|
|
2014
|
+
if (result._tag === "Overlay")
|
|
2015
|
+
return yield* new ImageError({ code: "InvalidStructure" });
|
|
2016
|
+
return result.volume;
|
|
1768
2017
|
});
|