@effect-vfs/core 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/BytePath.d.ts +33 -2
- package/dist/BytePath.d.ts.map +1 -1
- package/dist/BytePath.js +1 -1
- package/dist/Snapshot.d.ts +58 -2
- package/dist/Snapshot.d.ts.map +1 -1
- package/dist/Snapshot.js +57 -1
- package/dist/SnapshotDelta.d.ts +113 -3
- package/dist/SnapshotDelta.d.ts.map +1 -1
- package/dist/SnapshotDelta.js +112 -2
- package/dist/VirtualFileSystem.d.ts +919 -10
- package/dist/VirtualFileSystem.d.ts.map +1 -1
- package/dist/VirtualFileSystem.js +575 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/bytePath.d.ts.map +1 -1
- package/dist/internal/bytePath.js +5 -10
- package/dist/internal/bytes.d.ts +6 -0
- package/dist/internal/bytes.d.ts.map +1 -0
- package/dist/internal/bytes.js +17 -0
- package/dist/internal/canonicalBase64.d.ts +13 -5
- package/dist/internal/canonicalBase64.d.ts.map +1 -1
- package/dist/internal/canonicalBase64.js +28 -6
- package/dist/internal/image.d.ts +76 -90
- package/dist/internal/image.d.ts.map +1 -1
- package/dist/internal/image.js +98 -89
- package/dist/internal/metadata.d.ts +28 -0
- package/dist/internal/metadata.d.ts.map +1 -0
- package/dist/internal/metadata.js +29 -0
- package/dist/internal/snapshotDelta.d.ts.map +1 -1
- package/dist/internal/snapshotDelta.js +346 -300
- package/dist/internal/snapshotDeltaModel.d.ts +2 -2
- package/dist/internal/snapshotDeltaModel.d.ts.map +1 -1
- package/dist/internal/snapshotDeltaModel.js +3 -1
- package/dist/internal/virtualFileSystem/errors.d.ts +5 -0
- package/dist/internal/virtualFileSystem/errors.d.ts.map +1 -1
- package/dist/internal/virtualFileSystem/errors.js +19 -0
- package/dist/internal/virtualFileSystem/fixture.d.ts.map +1 -1
- package/dist/internal/virtualFileSystem/fixture.js +74 -74
- package/dist/internal/virtualFileSystem/overlayContent.d.ts.map +1 -1
- package/dist/internal/virtualFileSystem/overlayContent.js +9 -2
- package/dist/internal/virtualFileSystem/overlayDiff.d.ts +2 -11
- package/dist/internal/virtualFileSystem/overlayDiff.d.ts.map +1 -1
- package/dist/internal/virtualFileSystem/overlayDiff.js +45 -71
- package/dist/internal/virtualFileSystem/path.d.ts +15 -14
- package/dist/internal/virtualFileSystem/path.d.ts.map +1 -1
- package/dist/internal/virtualFileSystem/path.js +57 -54
- package/dist/internal/virtualFileSystem/testHooks.d.ts.map +1 -1
- package/dist/internal/virtualFileSystem/watchHub.d.ts.map +1 -1
- package/dist/internal/virtualFileSystem/watchHub.js +13 -13
- package/dist/internal/virtualFileSystem.d.ts +91 -15
- package/dist/internal/virtualFileSystem.d.ts.map +1 -1
- package/dist/internal/virtualFileSystem.js +470 -392
- package/package.json +15 -5
|
@@ -13,13 +13,12 @@ import * as Result from "effect/Result";
|
|
|
13
13
|
import * as Schema from "effect/Schema";
|
|
14
14
|
import * as SchemaIssue from "effect/SchemaIssue";
|
|
15
15
|
import * as SchemaTransformation from "effect/SchemaTransformation";
|
|
16
|
-
import { BytePath } from "./BytePath.js";
|
|
17
16
|
export { BytePath } from "./BytePath.js";
|
|
18
|
-
import { DecodeLimits, ImageError } from "./Snapshot.js";
|
|
19
17
|
export { DecodeLimits, ImageError, SnapshotTypeId } from "./Snapshot.js";
|
|
20
18
|
import * as Image from "./internal/image.js";
|
|
21
19
|
import * as SnapshotDeltaInternal from "./internal/snapshotDelta.js";
|
|
22
20
|
import * as VfsModel from "./internal/virtualFileSystem.js";
|
|
21
|
+
import { decodeConfiguration } from "./internal/virtualFileSystem/errors.js";
|
|
23
22
|
import * as FixtureInternal from "./internal/virtualFileSystem/fixture.js";
|
|
24
23
|
import * as Path from "./internal/virtualFileSystem/path.js";
|
|
25
24
|
import * as SnapshotDeltaModel from "./SnapshotDelta.js";
|
|
@@ -32,6 +31,30 @@ const ObjectReferenceId = VfsModel.ObjectReferenceId;
|
|
|
32
31
|
/**
|
|
33
32
|
* Schema for portable virtual filesystem error codes.
|
|
34
33
|
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
37
|
+
* import { Effect, Schema } from "effect"
|
|
38
|
+
*
|
|
39
|
+
* const asCode = Schema.decodeUnknownEffect(Vfs.FsCode)
|
|
40
|
+
*
|
|
41
|
+
* // Useful when a code has crossed a process boundary and arrives back as data.
|
|
42
|
+
* const program = Effect.gen(function*() {
|
|
43
|
+
* const payload = JSON.parse(`{"code":"NotFound","other":"Nonsense"}`)
|
|
44
|
+
*
|
|
45
|
+
* const known = yield* asCode(payload.code)
|
|
46
|
+
*
|
|
47
|
+
* const unknown = yield* asCode(payload.other).pipe(
|
|
48
|
+
* Effect.orElseSucceed(() => "not a code")
|
|
49
|
+
* )
|
|
50
|
+
*
|
|
51
|
+
* return [known, unknown]
|
|
52
|
+
* })
|
|
53
|
+
*
|
|
54
|
+
* Effect.runPromise(program).then(console.log)
|
|
55
|
+
* // [ 'NotFound', 'not a code' ]
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
35
58
|
* @category schemas
|
|
36
59
|
* @since 0.1.0
|
|
37
60
|
*/
|
|
@@ -39,6 +62,28 @@ export const FsCode = VfsModel.FsCode;
|
|
|
39
62
|
/**
|
|
40
63
|
* Describes an expected filesystem operation failure.
|
|
41
64
|
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
68
|
+
* import { Effect } from "effect"
|
|
69
|
+
*
|
|
70
|
+
* const program = Effect.gen(function*() {
|
|
71
|
+
* const caller = yield* (yield* Vfs.make()).caller()
|
|
72
|
+
*
|
|
73
|
+
* // Every filesystem operation fails with this one tagged error; `code`
|
|
74
|
+
* // distinguishes the cases, so match on it rather than on the tag alone.
|
|
75
|
+
* return yield* caller.readFile("/missing").pipe(
|
|
76
|
+
* Effect.catchTag("FsError", (error) =>
|
|
77
|
+
* error.code === "NotFound"
|
|
78
|
+
* ? Effect.succeed(new Uint8Array())
|
|
79
|
+
* : Effect.fail(error))
|
|
80
|
+
* )
|
|
81
|
+
* })
|
|
82
|
+
*
|
|
83
|
+
* Effect.runPromise(program).then(console.log)
|
|
84
|
+
* // Uint8Array(0) []
|
|
85
|
+
* ```
|
|
86
|
+
*
|
|
42
87
|
* @category errors
|
|
43
88
|
* @since 0.1.0
|
|
44
89
|
*/
|
|
@@ -46,6 +91,26 @@ export const FsError = VfsModel.FsError;
|
|
|
46
91
|
/**
|
|
47
92
|
* Describes an invalid volume or caller option and names the rejected field.
|
|
48
93
|
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
97
|
+
* import { Effect } from "effect"
|
|
98
|
+
*
|
|
99
|
+
* // `field` names the rejected option, so the caller learns which key was wrong.
|
|
100
|
+
* const program = Effect.gen(function*() {
|
|
101
|
+
* // Options that arrived from a config file or CLI flag, not a literal.
|
|
102
|
+
* const options = JSON.parse(`{"maxEntries":-1}`) as Vfs.VolumeOptions
|
|
103
|
+
*
|
|
104
|
+
* return yield* Vfs.make(options).pipe(
|
|
105
|
+
* Effect.as("ok"),
|
|
106
|
+
* Effect.catchTag("ConfigurationError", (error) => Effect.succeed(`rejected ${error.field}`))
|
|
107
|
+
* )
|
|
108
|
+
* })
|
|
109
|
+
*
|
|
110
|
+
* Effect.runPromise(program).then(console.log)
|
|
111
|
+
* // rejected maxEntries
|
|
112
|
+
* ```
|
|
113
|
+
*
|
|
49
114
|
* @category errors
|
|
50
115
|
* @since 0.1.0
|
|
51
116
|
*/
|
|
@@ -60,6 +125,33 @@ export const Identity = VfsModel.Identity;
|
|
|
60
125
|
/**
|
|
61
126
|
* Schema for root caller credentials and creation mask.
|
|
62
127
|
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```ts
|
|
130
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
131
|
+
* import { Effect } from "effect"
|
|
132
|
+
*
|
|
133
|
+
* const program = Effect.gen(function*() {
|
|
134
|
+
* const volume = yield* Vfs.make()
|
|
135
|
+
*
|
|
136
|
+
* // Omitting `identity` yields a privileged root caller.
|
|
137
|
+
* const root = yield* volume.caller({ umask: 0 })
|
|
138
|
+
* const user = yield* volume.caller({
|
|
139
|
+
* identity: { uid: 1000, gid: 1000, groups: [], privileged: false },
|
|
140
|
+
* umask: 0o022
|
|
141
|
+
* })
|
|
142
|
+
*
|
|
143
|
+
* yield* root.mkdir("/home", { mode: 0o777 })
|
|
144
|
+
*
|
|
145
|
+
* // The umask clears its bits from the requested mode.
|
|
146
|
+
* yield* user.mkdir("/home/user", { mode: 0o777 })
|
|
147
|
+
*
|
|
148
|
+
* return ((yield* user.stat("/home/user")).mode & 0o777).toString(8)
|
|
149
|
+
* })
|
|
150
|
+
*
|
|
151
|
+
* Effect.runPromise(program).then(console.log)
|
|
152
|
+
* // 755
|
|
153
|
+
* ```
|
|
154
|
+
*
|
|
63
155
|
* @category schemas
|
|
64
156
|
* @since 0.1.0
|
|
65
157
|
*/
|
|
@@ -67,6 +159,29 @@ export const RootCallerOptions = VfsModel.RootCallerOptions;
|
|
|
67
159
|
/**
|
|
68
160
|
* Schema for optional volume capacity and path limits.
|
|
69
161
|
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```ts
|
|
164
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
165
|
+
* import { ByteSize, Effect } from "effect"
|
|
166
|
+
*
|
|
167
|
+
* // Every limit is optional, and each one is enforced once set.
|
|
168
|
+
* const program = Effect.gen(function*() {
|
|
169
|
+
* const volume = yield* Vfs.make({ maxFileBytes: ByteSize.bytes(4) })
|
|
170
|
+
* const caller = yield* volume.caller()
|
|
171
|
+
*
|
|
172
|
+
* return yield* caller.writeFile("/big.bin", new Uint8Array(16), {
|
|
173
|
+
* access: "write",
|
|
174
|
+
* create: "exclusive"
|
|
175
|
+
* }).pipe(
|
|
176
|
+
* Effect.as("written"),
|
|
177
|
+
* Effect.catchTag("FsError", (error) => Effect.succeed(error.code))
|
|
178
|
+
* )
|
|
179
|
+
* })
|
|
180
|
+
*
|
|
181
|
+
* Effect.runPromise(program).then(console.log)
|
|
182
|
+
* // FileTooLarge
|
|
183
|
+
* ```
|
|
184
|
+
*
|
|
70
185
|
* @category schemas
|
|
71
186
|
* @since 0.1.0
|
|
72
187
|
*/
|
|
@@ -74,6 +189,29 @@ export const VolumeOptions = VfsModel.VolumeOptions;
|
|
|
74
189
|
/**
|
|
75
190
|
* Schema for filesystem node metadata with bigint inode, size, and nanosecond fields.
|
|
76
191
|
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```ts
|
|
194
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
195
|
+
* import { Effect } from "effect"
|
|
196
|
+
*
|
|
197
|
+
* // Sizes and inode numbers are bigint; times are nanosecond timestamps.
|
|
198
|
+
* const program = Effect.gen(function*() {
|
|
199
|
+
* const caller = yield* (yield* Vfs.make()).caller()
|
|
200
|
+
*
|
|
201
|
+
* yield* caller.writeFile("/f", new Uint8Array([1, 2, 3]), {
|
|
202
|
+
* access: "write",
|
|
203
|
+
* create: "exclusive"
|
|
204
|
+
* })
|
|
205
|
+
*
|
|
206
|
+
* const metadata = yield* caller.stat("/f")
|
|
207
|
+
*
|
|
208
|
+
* return [metadata.kind, metadata.size, metadata.nlink]
|
|
209
|
+
* })
|
|
210
|
+
*
|
|
211
|
+
* Effect.runPromise(program).then(console.log)
|
|
212
|
+
* // [ 'file', 3n, 1 ]
|
|
213
|
+
* ```
|
|
214
|
+
*
|
|
77
215
|
* @category schemas
|
|
78
216
|
* @since 0.1.0
|
|
79
217
|
*/
|
|
@@ -95,6 +233,34 @@ export const TimeUpdate = VfsModel.TimeUpdate;
|
|
|
95
233
|
/**
|
|
96
234
|
* Schema for independent access and modification time updates.
|
|
97
235
|
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```ts
|
|
238
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
239
|
+
* import { Effect } from "effect"
|
|
240
|
+
*
|
|
241
|
+
* const program = Effect.gen(function*() {
|
|
242
|
+
* const caller = yield* (yield* Vfs.make()).caller()
|
|
243
|
+
*
|
|
244
|
+
* yield* caller.writeFile("/f", new Uint8Array([1]), {
|
|
245
|
+
* access: "write",
|
|
246
|
+
* create: "exclusive"
|
|
247
|
+
* })
|
|
248
|
+
*
|
|
249
|
+
* // Each field is set to a value, to the current clock, or left alone.
|
|
250
|
+
* yield* caller.utimes("/f", {
|
|
251
|
+
* access: { kind: "value", nanoseconds: 1_000n },
|
|
252
|
+
* modification: { kind: "omit" }
|
|
253
|
+
* })
|
|
254
|
+
*
|
|
255
|
+
* const metadata = yield* caller.stat("/f")
|
|
256
|
+
*
|
|
257
|
+
* return [metadata.atimeNs, metadata.mtimeNs === 1_000n]
|
|
258
|
+
* })
|
|
259
|
+
*
|
|
260
|
+
* Effect.runPromise(program).then(console.log)
|
|
261
|
+
* // [ 1000n, false ]
|
|
262
|
+
* ```
|
|
263
|
+
*
|
|
98
264
|
* @category schemas
|
|
99
265
|
* @since 0.1.0
|
|
100
266
|
*/
|
|
@@ -109,6 +275,30 @@ export const SeekMode = VfsModel.SeekMode;
|
|
|
109
275
|
/**
|
|
110
276
|
* Schema for file access, creation, append, truncate, and symlink behavior.
|
|
111
277
|
*
|
|
278
|
+
* @example
|
|
279
|
+
* ```ts
|
|
280
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
281
|
+
* import { Effect } from "effect"
|
|
282
|
+
*
|
|
283
|
+
* // `access` is required; `create` decides whether a missing file is an error.
|
|
284
|
+
* const program = Effect.gen(function*() {
|
|
285
|
+
* const caller = yield* (yield* Vfs.make()).caller()
|
|
286
|
+
*
|
|
287
|
+
* const handle = yield* caller.open("/log.txt", {
|
|
288
|
+
* access: "readWrite",
|
|
289
|
+
* create: "ifMissing",
|
|
290
|
+
* append: true
|
|
291
|
+
* })
|
|
292
|
+
*
|
|
293
|
+
* yield* handle.write(new Uint8Array([1, 2]))
|
|
294
|
+
*
|
|
295
|
+
* return (yield* handle.stat).size
|
|
296
|
+
* }).pipe(Effect.scoped)
|
|
297
|
+
*
|
|
298
|
+
* Effect.runPromise(program).then(console.log)
|
|
299
|
+
* // 2n
|
|
300
|
+
* ```
|
|
301
|
+
*
|
|
112
302
|
* @category schemas
|
|
113
303
|
* @since 0.1.0
|
|
114
304
|
*/
|
|
@@ -142,6 +332,28 @@ export const OverlayChange = VfsModel.OverlayChange;
|
|
|
142
332
|
/**
|
|
143
333
|
* Schema for overlay summary filtering.
|
|
144
334
|
*
|
|
335
|
+
* @example
|
|
336
|
+
* ```ts
|
|
337
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
338
|
+
* import { Effect } from "effect"
|
|
339
|
+
*
|
|
340
|
+
* const program = Effect.gen(function*() {
|
|
341
|
+
* const base = yield* Vfs.make()
|
|
342
|
+
* const workspace = yield* Vfs.makeOverlay(yield* base.snapshot)
|
|
343
|
+
*
|
|
344
|
+
* yield* (yield* workspace.caller()).mkdir("/out")
|
|
345
|
+
*
|
|
346
|
+
* // Timestamps are excluded by default, since they change on every write.
|
|
347
|
+
* const plain = yield* workspace.changes()
|
|
348
|
+
* const timed = yield* workspace.changes({ includeTimestamps: true })
|
|
349
|
+
*
|
|
350
|
+
* return [plain.length, timed.length]
|
|
351
|
+
* })
|
|
352
|
+
*
|
|
353
|
+
* Effect.runPromise(program).then(console.log)
|
|
354
|
+
* // [ 1, 2 ]
|
|
355
|
+
* ```
|
|
356
|
+
*
|
|
145
357
|
* @category schemas
|
|
146
358
|
* @since 0.1.0
|
|
147
359
|
*/
|
|
@@ -149,6 +361,35 @@ export const OverlayChangesOptions = VfsModel.OverlayChangesOptions;
|
|
|
149
361
|
/**
|
|
150
362
|
* Optional Effect service for providing an existing filesystem caller.
|
|
151
363
|
*
|
|
364
|
+
* @example
|
|
365
|
+
* ```ts
|
|
366
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
367
|
+
* import { Effect } from "effect"
|
|
368
|
+
*
|
|
369
|
+
* // Code depending on the service does not choose the volume it runs against.
|
|
370
|
+
* const readVersion = Effect.gen(function*() {
|
|
371
|
+
* const caller = yield* Vfs.CurrentFileSystem
|
|
372
|
+
*
|
|
373
|
+
* return yield* caller.readFile("/version")
|
|
374
|
+
* })
|
|
375
|
+
*
|
|
376
|
+
* const program = Effect.gen(function*() {
|
|
377
|
+
* const volume = yield* Vfs.make()
|
|
378
|
+
* const caller = yield* volume.caller()
|
|
379
|
+
*
|
|
380
|
+
* yield* caller.writeFile("/version", new Uint8Array([49]), {
|
|
381
|
+
* access: "write",
|
|
382
|
+
* create: "exclusive"
|
|
383
|
+
* })
|
|
384
|
+
*
|
|
385
|
+
* return yield* readVersion.pipe(Effect.provideService(Vfs.CurrentFileSystem, caller))
|
|
386
|
+
* })
|
|
387
|
+
*
|
|
388
|
+
* Effect.runPromise(program).then(console.log)
|
|
389
|
+
* // Uint8Array(1) [ 49 ]
|
|
390
|
+
* ```
|
|
391
|
+
*
|
|
392
|
+
* @see The testing guide at `/guides/testing-with-an-isolated-filesystem`.
|
|
152
393
|
* @category services
|
|
153
394
|
* @since 0.1.0
|
|
154
395
|
*/
|
|
@@ -156,6 +397,32 @@ export const CurrentFileSystem = VfsModel.CurrentFileSystem;
|
|
|
156
397
|
/**
|
|
157
398
|
* Encodes a snapshot as owned UTF-8 JSON bytes using the version 1 snapshot format.
|
|
158
399
|
*
|
|
400
|
+
* @example
|
|
401
|
+
* ```ts
|
|
402
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
403
|
+
* import { Effect } from "effect"
|
|
404
|
+
*
|
|
405
|
+
* // Bytes are portable: store or transmit them, then decode under explicit limits.
|
|
406
|
+
* const program = Effect.gen(function*() {
|
|
407
|
+
* const volume = yield* Vfs.make()
|
|
408
|
+
* const caller = yield* volume.caller()
|
|
409
|
+
*
|
|
410
|
+
* yield* caller.writeFile("/f", new Uint8Array([1, 2, 3]), {
|
|
411
|
+
* access: "write",
|
|
412
|
+
* create: "exclusive"
|
|
413
|
+
* })
|
|
414
|
+
*
|
|
415
|
+
* const bytes = yield* Vfs.encodeSnapshot(yield* volume.snapshot)
|
|
416
|
+
*
|
|
417
|
+
* // The bytes are UTF-8 JSON carrying the version 1 snapshot format.
|
|
418
|
+
* return JSON.parse(new TextDecoder().decode(bytes)).version
|
|
419
|
+
* })
|
|
420
|
+
*
|
|
421
|
+
* Effect.runPromise(program).then(console.log)
|
|
422
|
+
* // 1
|
|
423
|
+
* ```
|
|
424
|
+
*
|
|
425
|
+
* @see {@link decodeSnapshot} for reading the bytes back under limits.
|
|
159
426
|
* @category serialization
|
|
160
427
|
* @since 0.1.0
|
|
161
428
|
*/
|
|
@@ -163,18 +430,74 @@ export const encodeSnapshot = Image.encodeSnapshot;
|
|
|
163
430
|
/**
|
|
164
431
|
* Decodes version 1 snapshot bytes while enforcing explicit input and payload limits.
|
|
165
432
|
*
|
|
433
|
+
* @example
|
|
434
|
+
* ```ts
|
|
435
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
436
|
+
* import { ByteSize, Effect } from "effect"
|
|
437
|
+
*
|
|
438
|
+
* // Limits are mandatory: decoding is the boundary where untrusted input arrives.
|
|
439
|
+
* const program = Effect.gen(function*() {
|
|
440
|
+
* const volume = yield* Vfs.make()
|
|
441
|
+
* const bytes = yield* Vfs.encodeSnapshot(yield* volume.snapshot)
|
|
442
|
+
*
|
|
443
|
+
* return yield* Vfs.decodeSnapshot(bytes, {
|
|
444
|
+
* maxEncodedBytes: ByteSize.bytes(1),
|
|
445
|
+
* maxRecords: 10_000,
|
|
446
|
+
* maxEntries: 10_000,
|
|
447
|
+
* maxDecodedBytes: ByteSize.megabytes(16)
|
|
448
|
+
* }).pipe(
|
|
449
|
+
* Effect.as("accepted"),
|
|
450
|
+
* Effect.catchTag("ImageError", (error) => Effect.succeed([error.code, error.field]))
|
|
451
|
+
* )
|
|
452
|
+
* })
|
|
453
|
+
*
|
|
454
|
+
* Effect.runPromise(program).then(console.log)
|
|
455
|
+
* // [ 'LimitExceeded', 'encodedBytes' ]
|
|
456
|
+
* ```
|
|
457
|
+
*
|
|
458
|
+
* @see {@link DecodeLimits} for what each bound protects.
|
|
166
459
|
* @category serialization
|
|
167
460
|
* @since 0.1.0
|
|
168
461
|
*/
|
|
169
462
|
export const decodeSnapshot = Image.decodeSnapshot;
|
|
170
463
|
const deltaLimits = (limits) => {
|
|
171
|
-
const decoded =
|
|
464
|
+
const decoded = decodeConfiguration(SnapshotDeltaModel.SnapshotDeltaLimits, limits ?? SnapshotDeltaModel.SnapshotDeltaLimits.default);
|
|
172
465
|
return Result.isFailure(decoded) ? Effect.fail(decoded.failure) : Effect.succeed(decoded.success);
|
|
173
466
|
};
|
|
174
467
|
/**
|
|
175
468
|
* Computes an exact portable delta between two immutable snapshots.
|
|
176
|
-
* Requires the platform-neutral `Crypto.Crypto` service for base identity.
|
|
469
|
+
* Requires the platform-neutral `Crypto.Crypto` service for base identity. Provide
|
|
470
|
+
* `NodeCrypto.layer`, `BunCrypto.layer`, or your own via `Crypto.make`.
|
|
471
|
+
*
|
|
472
|
+
* @example
|
|
473
|
+
* ```ts
|
|
474
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
475
|
+
* import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
|
|
476
|
+
* import { Effect } from "effect"
|
|
477
|
+
*
|
|
478
|
+
* // Two snapshots in, one opaque delta out. `inspectSnapshotDelta` reads it.
|
|
479
|
+
* const program = Effect.gen(function*() {
|
|
480
|
+
* const volume = yield* Vfs.make()
|
|
481
|
+
* const caller = yield* volume.caller()
|
|
482
|
+
* const base = yield* volume.snapshot
|
|
483
|
+
*
|
|
484
|
+
* yield* caller.writeFile("/added.txt", new Uint8Array([1]), {
|
|
485
|
+
* access: "write",
|
|
486
|
+
* create: "exclusive"
|
|
487
|
+
* })
|
|
488
|
+
*
|
|
489
|
+
* const delta = yield* Vfs.diffSnapshots(base, yield* volume.snapshot)
|
|
177
490
|
*
|
|
491
|
+
* const changes = yield* Vfs.inspectSnapshotDelta(base, delta)
|
|
492
|
+
*
|
|
493
|
+
* return new TextDecoder().decode(yield* Vfs.pathToBytes(changes[0]!.path))
|
|
494
|
+
* }).pipe(Effect.provide(NodeCrypto.layer))
|
|
495
|
+
*
|
|
496
|
+
* Effect.runPromise(program).then(console.log)
|
|
497
|
+
* // /added.txt
|
|
498
|
+
* ```
|
|
499
|
+
*
|
|
500
|
+
* @see {@link inspectSnapshotDelta} to read a delta, {@link applySnapshotDelta} to apply one.
|
|
178
501
|
* @category snapshots
|
|
179
502
|
* @since 0.1.0
|
|
180
503
|
*/
|
|
@@ -183,21 +506,80 @@ export const diffSnapshots = Effect.fn("VirtualFileSystem.diffSnapshots")(functi
|
|
|
183
506
|
});
|
|
184
507
|
/**
|
|
185
508
|
* Verifies an exact snapshot delta against its base and derives an owned path-oriented summary.
|
|
186
|
-
* Requires the platform-neutral `Crypto.Crypto` service for base identity.
|
|
509
|
+
* Requires the platform-neutral `Crypto.Crypto` service for base identity. Provide
|
|
510
|
+
* `NodeCrypto.layer`, `BunCrypto.layer`, or your own via `Crypto.make`.
|
|
511
|
+
*
|
|
512
|
+
* @example
|
|
513
|
+
* ```ts
|
|
514
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
515
|
+
* import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
|
|
516
|
+
* import { Effect } from "effect"
|
|
517
|
+
*
|
|
518
|
+
* // Inspection verifies the delta against its base before describing it.
|
|
519
|
+
* const program = Effect.gen(function*() {
|
|
520
|
+
* const volume = yield* Vfs.make()
|
|
521
|
+
* const caller = yield* volume.caller()
|
|
522
|
+
* const base = yield* volume.snapshot
|
|
523
|
+
*
|
|
524
|
+
* yield* caller.mkdir("/logs")
|
|
525
|
+
*
|
|
526
|
+
* const delta = yield* Vfs.diffSnapshots(base, yield* volume.snapshot)
|
|
527
|
+
* const changes = yield* Vfs.inspectSnapshotDelta(base, delta)
|
|
187
528
|
*
|
|
529
|
+
* return changes.map((change) =>
|
|
530
|
+
* change._tag === "Updated" ? change.afterKind : change.kind)
|
|
531
|
+
* }).pipe(Effect.provide(NodeCrypto.layer))
|
|
532
|
+
*
|
|
533
|
+
* Effect.runPromise(program).then(console.log)
|
|
534
|
+
* // [ 'directory' ]
|
|
535
|
+
* ```
|
|
536
|
+
*
|
|
537
|
+
* @see {@link SnapshotChangesOptions} for including timestamp differences.
|
|
188
538
|
* @category snapshots
|
|
189
539
|
* @since 0.1.0
|
|
190
540
|
*/
|
|
191
541
|
export const inspectSnapshotDelta = Effect.fn("VirtualFileSystem.inspectSnapshotDelta")(function* (base, delta, options, limits) {
|
|
192
|
-
const decoded =
|
|
542
|
+
const decoded = decodeConfiguration(SnapshotDeltaModel.SnapshotChangesOptions, options ?? {});
|
|
193
543
|
if (Result.isFailure(decoded))
|
|
194
544
|
return yield* decoded.failure;
|
|
195
545
|
return yield* SnapshotDeltaInternal.inspectSnapshotDelta(base, delta, decoded.success, yield* deltaLimits(limits));
|
|
196
546
|
});
|
|
197
547
|
/**
|
|
198
548
|
* Applies an exact delta to its semantically matching base and returns a new snapshot.
|
|
199
|
-
* Requires the platform-neutral `Crypto.Crypto` service for base identity.
|
|
549
|
+
* Requires the platform-neutral `Crypto.Crypto` service for base identity. Provide
|
|
550
|
+
* `NodeCrypto.layer`, `BunCrypto.layer`, or your own via `Crypto.make`.
|
|
551
|
+
*
|
|
552
|
+
* @example
|
|
553
|
+
* ```ts
|
|
554
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
555
|
+
* import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
|
|
556
|
+
* import { Effect } from "effect"
|
|
557
|
+
*
|
|
558
|
+
* const program = Effect.gen(function*() {
|
|
559
|
+
* const volume = yield* Vfs.make()
|
|
560
|
+
* const caller = yield* volume.caller()
|
|
561
|
+
* const base = yield* volume.snapshot
|
|
562
|
+
*
|
|
563
|
+
* yield* caller.writeFile("/f", new Uint8Array([7]), {
|
|
564
|
+
* access: "write",
|
|
565
|
+
* create: "exclusive"
|
|
566
|
+
* })
|
|
567
|
+
*
|
|
568
|
+
* const delta = yield* Vfs.diffSnapshots(base, yield* volume.snapshot)
|
|
569
|
+
*
|
|
570
|
+
* // Applying the delta to the same base reproduces the target snapshot.
|
|
571
|
+
* const applied = yield* Vfs.applySnapshotDelta(base, delta)
|
|
572
|
+
* const restored = yield* Vfs.fromSnapshot(applied)
|
|
573
|
+
* const reader = yield* restored.caller()
|
|
200
574
|
*
|
|
575
|
+
* return yield* reader.readFile("/f")
|
|
576
|
+
* }).pipe(Effect.provide(NodeCrypto.layer))
|
|
577
|
+
*
|
|
578
|
+
* Effect.runPromise(program).then(console.log)
|
|
579
|
+
* // Uint8Array(1) [ 7 ]
|
|
580
|
+
* ```
|
|
581
|
+
*
|
|
582
|
+
* @see {@link SnapshotDeltaFromBytes} for moving a delta across a boundary.
|
|
201
583
|
* @category snapshots
|
|
202
584
|
* @since 0.1.0
|
|
203
585
|
*/
|
|
@@ -209,6 +591,34 @@ const deltaSchemaIssue = (cause, input, options) => new SchemaIssue.InvalidValue
|
|
|
209
591
|
* Creates an Effect Schema codec between owned bytes and opaque snapshot deltas.
|
|
210
592
|
* Omission uses `SnapshotDeltaLimits.default`.
|
|
211
593
|
*
|
|
594
|
+
* @example
|
|
595
|
+
* ```ts
|
|
596
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
597
|
+
* import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
|
|
598
|
+
* import { Effect, Schema } from "effect"
|
|
599
|
+
*
|
|
600
|
+
* // Omitting limits uses `SnapshotDeltaLimits.default`.
|
|
601
|
+
* const codec = Vfs.SnapshotDeltaFromBytes()
|
|
602
|
+
*
|
|
603
|
+
* const program = Effect.gen(function*() {
|
|
604
|
+
* const volume = yield* Vfs.make()
|
|
605
|
+
* const base = yield* volume.snapshot
|
|
606
|
+
*
|
|
607
|
+
* yield* (yield* volume.caller()).mkdir("/out")
|
|
608
|
+
*
|
|
609
|
+
* const delta = yield* Vfs.diffSnapshots(base, yield* volume.snapshot)
|
|
610
|
+
* const bytes = yield* Schema.encodeEffect(codec)(delta)
|
|
611
|
+
*
|
|
612
|
+
* // The delta survives the trip through bytes and still applies to its base.
|
|
613
|
+
* const decoded = yield* Schema.decodeEffect(codec)(bytes)
|
|
614
|
+
*
|
|
615
|
+
* return (yield* Vfs.inspectSnapshotDelta(base, decoded)).map((change) => change._tag)
|
|
616
|
+
* }).pipe(Effect.provide(NodeCrypto.layer))
|
|
617
|
+
*
|
|
618
|
+
* Effect.runPromise(program).then(console.log)
|
|
619
|
+
* // [ 'Added' ]
|
|
620
|
+
* ```
|
|
621
|
+
*
|
|
212
622
|
* @category schemas
|
|
213
623
|
* @since 0.1.0
|
|
214
624
|
*/
|
|
@@ -230,6 +640,28 @@ export const SnapshotDeltaFromBytes = (limits) => {
|
|
|
230
640
|
*
|
|
231
641
|
* Shared-memory-backed and detached views fail with `InvalidArgument`.
|
|
232
642
|
*
|
|
643
|
+
* @example
|
|
644
|
+
* ```ts
|
|
645
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
646
|
+
* import { Effect } from "effect"
|
|
647
|
+
*
|
|
648
|
+
* // Any byte except NUL is accepted. Empty, NUL-bearing, detached, and shared-memory
|
|
649
|
+
* // input all fail with `InvalidArgument`.
|
|
650
|
+
* const program = Effect.gen(function*() {
|
|
651
|
+
* const path = yield* Vfs.pathFromBytes(new Uint8Array([47, 116, 109, 112]))
|
|
652
|
+
*
|
|
653
|
+
* const rejected = yield* Vfs.pathFromBytes(new Uint8Array([47, 0])).pipe(
|
|
654
|
+
* Effect.as("accepted"),
|
|
655
|
+
* Effect.catchTag("FsError", (error) => Effect.succeed(error.code))
|
|
656
|
+
* )
|
|
657
|
+
*
|
|
658
|
+
* return [yield* Vfs.pathToBytes(path), rejected]
|
|
659
|
+
* })
|
|
660
|
+
*
|
|
661
|
+
* Effect.runPromise(program).then(console.log)
|
|
662
|
+
* // [ Uint8Array(4) [ 47, 116, 109, 112 ], 'InvalidArgument' ]
|
|
663
|
+
* ```
|
|
664
|
+
*
|
|
233
665
|
* @category constructors
|
|
234
666
|
* @since 0.1.0
|
|
235
667
|
*/
|
|
@@ -248,6 +680,26 @@ export const pathToBytes = Path.pathToBytes;
|
|
|
248
680
|
*
|
|
249
681
|
* Fixture paths must be absolute, unique, and explicitly include their parent directories.
|
|
250
682
|
*
|
|
683
|
+
* @example
|
|
684
|
+
* ```ts
|
|
685
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
686
|
+
* import { Effect, Schema } from "effect"
|
|
687
|
+
*
|
|
688
|
+
* // Fixtures must list every parent directory before its children.
|
|
689
|
+
* const program = Schema.decodeUnknownEffect(Vfs.Fixture)({
|
|
690
|
+
* entries: [
|
|
691
|
+
* { kind: "file", path: "/etc/hosts", bytes: new TextEncoder().encode("127.0.0.1") }
|
|
692
|
+
* ]
|
|
693
|
+
* }).pipe(
|
|
694
|
+
* Effect.flatMap(Vfs.fromFixture),
|
|
695
|
+
* Effect.as("built"),
|
|
696
|
+
* Effect.catchTag("ImageError", (error) => Effect.succeed(`rejected: ${error.code}`))
|
|
697
|
+
* )
|
|
698
|
+
*
|
|
699
|
+
* Effect.runPromise(program).then(console.log)
|
|
700
|
+
* // rejected: InvalidStructure
|
|
701
|
+
* ```
|
|
702
|
+
*
|
|
251
703
|
* @category schemas
|
|
252
704
|
* @since 0.1.0
|
|
253
705
|
*/
|
|
@@ -260,6 +712,26 @@ export const Fixture = VfsModel.Fixture;
|
|
|
260
712
|
* Each execution creates independent storage. Snapshot image failures cannot
|
|
261
713
|
* arise because this constructor does not accept persisted input.
|
|
262
714
|
*
|
|
715
|
+
* @example
|
|
716
|
+
* ```ts
|
|
717
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
718
|
+
* import { Effect } from "effect"
|
|
719
|
+
*
|
|
720
|
+
* const program = Effect.gen(function*() {
|
|
721
|
+
* const volume = yield* Vfs.make()
|
|
722
|
+
* const caller = yield* volume.caller()
|
|
723
|
+
*
|
|
724
|
+
* yield* caller.mkdir("/work")
|
|
725
|
+
* yield* caller.writeFile("/work/notes.txt", new Uint8Array([104, 105]), { access: "write", create: "exclusive" })
|
|
726
|
+
*
|
|
727
|
+
* return yield* caller.readFile("/work/notes.txt")
|
|
728
|
+
* })
|
|
729
|
+
*
|
|
730
|
+
* Effect.runPromise(program).then(console.log)
|
|
731
|
+
* // Uint8Array(2) [ 104, 105 ]
|
|
732
|
+
* ```
|
|
733
|
+
*
|
|
734
|
+
* @see The testing guide at `/guides/testing-with-an-isolated-filesystem`.
|
|
263
735
|
* @category constructors
|
|
264
736
|
* @since 0.1.0
|
|
265
737
|
*/
|
|
@@ -267,6 +739,40 @@ export const make = VfsModel.make;
|
|
|
267
739
|
/**
|
|
268
740
|
* Restores a fresh volume from an opaque snapshot under the supplied destination limits.
|
|
269
741
|
*
|
|
742
|
+
* @example
|
|
743
|
+
* ```ts
|
|
744
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
745
|
+
* import { Effect } from "effect"
|
|
746
|
+
*
|
|
747
|
+
* const program = Effect.gen(function*() {
|
|
748
|
+
* const original = yield* Vfs.make()
|
|
749
|
+
* const source = yield* original.caller()
|
|
750
|
+
*
|
|
751
|
+
* yield* source.writeFile("/seed.txt", new TextEncoder().encode("one"), {
|
|
752
|
+
* access: "write",
|
|
753
|
+
* create: "exclusive"
|
|
754
|
+
* })
|
|
755
|
+
*
|
|
756
|
+
* const restored = yield* Vfs.fromSnapshot(yield* original.snapshot)
|
|
757
|
+
* const copy = yield* restored.caller()
|
|
758
|
+
*
|
|
759
|
+
* // Writing to the restore leaves the original untouched: the two are independent.
|
|
760
|
+
* yield* copy.writeFile("/seed.txt", new TextEncoder().encode("two"), {
|
|
761
|
+
* access: "write",
|
|
762
|
+
* truncate: true
|
|
763
|
+
* })
|
|
764
|
+
*
|
|
765
|
+
* return [
|
|
766
|
+
* new TextDecoder().decode(yield* source.readFile("/seed.txt")),
|
|
767
|
+
* new TextDecoder().decode(yield* copy.readFile("/seed.txt"))
|
|
768
|
+
* ]
|
|
769
|
+
* })
|
|
770
|
+
*
|
|
771
|
+
* Effect.runPromise(program).then(console.log)
|
|
772
|
+
* // [ 'one', 'two' ]
|
|
773
|
+
* ```
|
|
774
|
+
*
|
|
775
|
+
* @see {@link makeOverlay} for a copy-on-write workspace over the same base.
|
|
270
776
|
* @category constructors
|
|
271
777
|
* @since 0.1.0
|
|
272
778
|
*/
|
|
@@ -284,6 +790,39 @@ export const fromSnapshot = VfsModel.fromSnapshot;
|
|
|
284
790
|
* Invalid base snapshots fail with `ImageError`; invalid volume limits fail
|
|
285
791
|
* with `ConfigurationError`. Each execution creates a fresh workspace.
|
|
286
792
|
*
|
|
793
|
+
* @example
|
|
794
|
+
* ```ts
|
|
795
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
796
|
+
* import { Effect } from "effect"
|
|
797
|
+
*
|
|
798
|
+
* const program = Effect.gen(function*() {
|
|
799
|
+
* const base = yield* Vfs.make()
|
|
800
|
+
* const origin = yield* base.caller()
|
|
801
|
+
*
|
|
802
|
+
* yield* origin.writeFile("/config.json", new TextEncoder().encode("{}"), {
|
|
803
|
+
* access: "write",
|
|
804
|
+
* create: "exclusive"
|
|
805
|
+
* })
|
|
806
|
+
*
|
|
807
|
+
* const workspace = yield* Vfs.makeOverlay(yield* base.snapshot)
|
|
808
|
+
*
|
|
809
|
+
* yield* (yield* workspace.caller()).writeFile("/config.json", new TextEncoder().encode("[]"), {
|
|
810
|
+
* access: "write",
|
|
811
|
+
* truncate: true
|
|
812
|
+
* })
|
|
813
|
+
*
|
|
814
|
+
* // The write landed in the workspace only; the base still reads its own value.
|
|
815
|
+
* return [
|
|
816
|
+
* new TextDecoder().decode(yield* origin.readFile("/config.json")),
|
|
817
|
+
* (yield* workspace.changes()).map((change) => change._tag)
|
|
818
|
+
* ]
|
|
819
|
+
* })
|
|
820
|
+
*
|
|
821
|
+
* Effect.runPromise(program).then(console.log)
|
|
822
|
+
* // [ '{}', [ 'Updated' ] ]
|
|
823
|
+
* ```
|
|
824
|
+
*
|
|
825
|
+
* @see The overlay guide at `/guides/overlay-filesystems`.
|
|
287
826
|
* @category constructors
|
|
288
827
|
* @since 0.1.0
|
|
289
828
|
*/
|
|
@@ -291,6 +830,35 @@ export const makeOverlay = VfsModel.makeOverlay;
|
|
|
291
830
|
/**
|
|
292
831
|
* Builds a fresh volume from a validated final-state fixture.
|
|
293
832
|
*
|
|
833
|
+
* @example
|
|
834
|
+
* ```ts
|
|
835
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
836
|
+
* import { Effect } from "effect"
|
|
837
|
+
*
|
|
838
|
+
* const encoder = new TextEncoder()
|
|
839
|
+
*
|
|
840
|
+
* const program = Effect.gen(function*() {
|
|
841
|
+
* // Parent directories must be listed explicitly, before their children.
|
|
842
|
+
* const volume = yield* Vfs.fromFixture({
|
|
843
|
+
* entries: [
|
|
844
|
+
* { kind: "directory", path: "/project" },
|
|
845
|
+
* {
|
|
846
|
+
* kind: "file",
|
|
847
|
+
* path: "/project/package.json",
|
|
848
|
+
* bytes: encoder.encode(`{"version":"1.2.3"}`)
|
|
849
|
+
* },
|
|
850
|
+
* { kind: "symlink", path: "/project/latest", target: "/project/package.json" }
|
|
851
|
+
* ]
|
|
852
|
+
* })
|
|
853
|
+
*
|
|
854
|
+
* return yield* (yield* volume.caller()).readDirectory("/project")
|
|
855
|
+
* })
|
|
856
|
+
*
|
|
857
|
+
* Effect.runPromise(program).then(console.log)
|
|
858
|
+
* // [ 'package.json', 'latest' ]
|
|
859
|
+
* ```
|
|
860
|
+
*
|
|
861
|
+
* @see The fixtures guide at `/guides/fixtures-and-snapshots`.
|
|
294
862
|
* @category constructors
|
|
295
863
|
* @since 0.1.0
|
|
296
864
|
*/
|