@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
|
@@ -12,9 +12,9 @@ import * as Effect from "effect/Effect";
|
|
|
12
12
|
import * as Schema from "effect/Schema";
|
|
13
13
|
import type * as Scope from "effect/Scope";
|
|
14
14
|
import type * as Stream from "effect/Stream";
|
|
15
|
-
import { BytePath } from "./BytePath.js";
|
|
15
|
+
import type { BytePath } from "./BytePath.js";
|
|
16
16
|
export { BytePath } from "./BytePath.js";
|
|
17
|
-
import { DecodeLimits, ImageError,
|
|
17
|
+
import type { DecodeLimits, ImageError, Snapshot } from "./Snapshot.js";
|
|
18
18
|
export { DecodeLimits, ImageError, type Snapshot, SnapshotTypeId } from "./Snapshot.js";
|
|
19
19
|
import * as VfsModel from "./internal/virtualFileSystem.js";
|
|
20
20
|
import * as SnapshotDeltaModel from "./SnapshotDelta.js";
|
|
@@ -27,6 +27,12 @@ declare const ObjectReferenceId: typeof VfsModel.ObjectReferenceId;
|
|
|
27
27
|
/**
|
|
28
28
|
* A UTF-8 string path or an opaque byte-preserving path.
|
|
29
29
|
*
|
|
30
|
+
* **Details**
|
|
31
|
+
*
|
|
32
|
+
* Every operation accepts both. Pass a string unless a name is not valid UTF-8;
|
|
33
|
+
* strings are the normal case and byte paths are the escape hatch.
|
|
34
|
+
*
|
|
35
|
+
* @see {@link BytePath} for the byte-preserving form.
|
|
30
36
|
* @category models
|
|
31
37
|
* @since 0.1.0
|
|
32
38
|
*/
|
|
@@ -34,6 +40,30 @@ export type PathInput = string | BytePath;
|
|
|
34
40
|
/**
|
|
35
41
|
* Schema for portable virtual filesystem error codes.
|
|
36
42
|
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
46
|
+
* import { Effect, Schema } from "effect"
|
|
47
|
+
*
|
|
48
|
+
* const asCode = Schema.decodeUnknownEffect(Vfs.FsCode)
|
|
49
|
+
*
|
|
50
|
+
* // Useful when a code has crossed a process boundary and arrives back as data.
|
|
51
|
+
* const program = Effect.gen(function*() {
|
|
52
|
+
* const payload = JSON.parse(`{"code":"NotFound","other":"Nonsense"}`)
|
|
53
|
+
*
|
|
54
|
+
* const known = yield* asCode(payload.code)
|
|
55
|
+
*
|
|
56
|
+
* const unknown = yield* asCode(payload.other).pipe(
|
|
57
|
+
* Effect.orElseSucceed(() => "not a code")
|
|
58
|
+
* )
|
|
59
|
+
*
|
|
60
|
+
* return [known, unknown]
|
|
61
|
+
* })
|
|
62
|
+
*
|
|
63
|
+
* Effect.runPromise(program).then(console.log)
|
|
64
|
+
* // [ 'NotFound', 'not a code' ]
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
37
67
|
* @category schemas
|
|
38
68
|
* @since 0.1.0
|
|
39
69
|
*/
|
|
@@ -48,6 +78,28 @@ export type FsCode = typeof FsCode.Type;
|
|
|
48
78
|
/**
|
|
49
79
|
* Describes an expected filesystem operation failure.
|
|
50
80
|
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts
|
|
83
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
84
|
+
* import { Effect } from "effect"
|
|
85
|
+
*
|
|
86
|
+
* const program = Effect.gen(function*() {
|
|
87
|
+
* const caller = yield* (yield* Vfs.make()).caller()
|
|
88
|
+
*
|
|
89
|
+
* // Every filesystem operation fails with this one tagged error; `code`
|
|
90
|
+
* // distinguishes the cases, so match on it rather than on the tag alone.
|
|
91
|
+
* return yield* caller.readFile("/missing").pipe(
|
|
92
|
+
* Effect.catchTag("FsError", (error) =>
|
|
93
|
+
* error.code === "NotFound"
|
|
94
|
+
* ? Effect.succeed(new Uint8Array())
|
|
95
|
+
* : Effect.fail(error))
|
|
96
|
+
* )
|
|
97
|
+
* })
|
|
98
|
+
*
|
|
99
|
+
* Effect.runPromise(program).then(console.log)
|
|
100
|
+
* // Uint8Array(0) []
|
|
101
|
+
* ```
|
|
102
|
+
*
|
|
51
103
|
* @category errors
|
|
52
104
|
* @since 0.1.0
|
|
53
105
|
*/
|
|
@@ -58,6 +110,26 @@ export interface FsError extends VfsModel.FsError {
|
|
|
58
110
|
/**
|
|
59
111
|
* Describes an invalid volume or caller option and names the rejected field.
|
|
60
112
|
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
116
|
+
* import { Effect } from "effect"
|
|
117
|
+
*
|
|
118
|
+
* // `field` names the rejected option, so the caller learns which key was wrong.
|
|
119
|
+
* const program = Effect.gen(function*() {
|
|
120
|
+
* // Options that arrived from a config file or CLI flag, not a literal.
|
|
121
|
+
* const options = JSON.parse(`{"maxEntries":-1}`) as Vfs.VolumeOptions
|
|
122
|
+
*
|
|
123
|
+
* return yield* Vfs.make(options).pipe(
|
|
124
|
+
* Effect.as("ok"),
|
|
125
|
+
* Effect.catchTag("ConfigurationError", (error) => Effect.succeed(`rejected ${error.field}`))
|
|
126
|
+
* )
|
|
127
|
+
* })
|
|
128
|
+
*
|
|
129
|
+
* Effect.runPromise(program).then(console.log)
|
|
130
|
+
* // rejected maxEntries
|
|
131
|
+
* ```
|
|
132
|
+
*
|
|
61
133
|
* @category errors
|
|
62
134
|
* @since 0.1.0
|
|
63
135
|
*/
|
|
@@ -82,6 +154,33 @@ export type Identity = typeof Identity.Type;
|
|
|
82
154
|
/**
|
|
83
155
|
* Schema for root caller credentials and creation mask.
|
|
84
156
|
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```ts
|
|
159
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
160
|
+
* import { Effect } from "effect"
|
|
161
|
+
*
|
|
162
|
+
* const program = Effect.gen(function*() {
|
|
163
|
+
* const volume = yield* Vfs.make()
|
|
164
|
+
*
|
|
165
|
+
* // Omitting `identity` yields a privileged root caller.
|
|
166
|
+
* const root = yield* volume.caller({ umask: 0 })
|
|
167
|
+
* const user = yield* volume.caller({
|
|
168
|
+
* identity: { uid: 1000, gid: 1000, groups: [], privileged: false },
|
|
169
|
+
* umask: 0o022
|
|
170
|
+
* })
|
|
171
|
+
*
|
|
172
|
+
* yield* root.mkdir("/home", { mode: 0o777 })
|
|
173
|
+
*
|
|
174
|
+
* // The umask clears its bits from the requested mode.
|
|
175
|
+
* yield* user.mkdir("/home/user", { mode: 0o777 })
|
|
176
|
+
*
|
|
177
|
+
* return ((yield* user.stat("/home/user")).mode & 0o777).toString(8)
|
|
178
|
+
* })
|
|
179
|
+
*
|
|
180
|
+
* Effect.runPromise(program).then(console.log)
|
|
181
|
+
* // 755
|
|
182
|
+
* ```
|
|
183
|
+
*
|
|
85
184
|
* @category schemas
|
|
86
185
|
* @since 0.1.0
|
|
87
186
|
*/
|
|
@@ -96,6 +195,29 @@ export type RootCallerOptions = typeof RootCallerOptions.Type;
|
|
|
96
195
|
/**
|
|
97
196
|
* Schema for optional volume capacity and path limits.
|
|
98
197
|
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```ts
|
|
200
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
201
|
+
* import { ByteSize, Effect } from "effect"
|
|
202
|
+
*
|
|
203
|
+
* // Every limit is optional, and each one is enforced once set.
|
|
204
|
+
* const program = Effect.gen(function*() {
|
|
205
|
+
* const volume = yield* Vfs.make({ maxFileBytes: ByteSize.bytes(4) })
|
|
206
|
+
* const caller = yield* volume.caller()
|
|
207
|
+
*
|
|
208
|
+
* return yield* caller.writeFile("/big.bin", new Uint8Array(16), {
|
|
209
|
+
* access: "write",
|
|
210
|
+
* create: "exclusive"
|
|
211
|
+
* }).pipe(
|
|
212
|
+
* Effect.as("written"),
|
|
213
|
+
* Effect.catchTag("FsError", (error) => Effect.succeed(error.code))
|
|
214
|
+
* )
|
|
215
|
+
* })
|
|
216
|
+
*
|
|
217
|
+
* Effect.runPromise(program).then(console.log)
|
|
218
|
+
* // FileTooLarge
|
|
219
|
+
* ```
|
|
220
|
+
*
|
|
99
221
|
* @category schemas
|
|
100
222
|
* @since 0.1.0
|
|
101
223
|
*/
|
|
@@ -110,6 +232,29 @@ export type VolumeOptions = typeof VolumeOptions.Type;
|
|
|
110
232
|
/**
|
|
111
233
|
* Schema for filesystem node metadata with bigint inode, size, and nanosecond fields.
|
|
112
234
|
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```ts
|
|
237
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
238
|
+
* import { Effect } from "effect"
|
|
239
|
+
*
|
|
240
|
+
* // Sizes and inode numbers are bigint; times are nanosecond timestamps.
|
|
241
|
+
* const program = Effect.gen(function*() {
|
|
242
|
+
* const caller = yield* (yield* Vfs.make()).caller()
|
|
243
|
+
*
|
|
244
|
+
* yield* caller.writeFile("/f", new Uint8Array([1, 2, 3]), {
|
|
245
|
+
* access: "write",
|
|
246
|
+
* create: "exclusive"
|
|
247
|
+
* })
|
|
248
|
+
*
|
|
249
|
+
* const metadata = yield* caller.stat("/f")
|
|
250
|
+
*
|
|
251
|
+
* return [metadata.kind, metadata.size, metadata.nlink]
|
|
252
|
+
* })
|
|
253
|
+
*
|
|
254
|
+
* Effect.runPromise(program).then(console.log)
|
|
255
|
+
* // [ 'file', 3n, 1 ]
|
|
256
|
+
* ```
|
|
257
|
+
*
|
|
113
258
|
* @category schemas
|
|
114
259
|
* @since 0.1.0
|
|
115
260
|
*/
|
|
@@ -133,6 +278,34 @@ export interface ObjectReference {
|
|
|
133
278
|
/**
|
|
134
279
|
* A value and the revision of the object from the same coordinated observation.
|
|
135
280
|
*
|
|
281
|
+
* @example
|
|
282
|
+
* ```ts
|
|
283
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
284
|
+
* import { Effect } from "effect"
|
|
285
|
+
*
|
|
286
|
+
* // References survive renames, and each observation carries the revision it
|
|
287
|
+
* // matched, so a caller can tell whether what it read is still current.
|
|
288
|
+
* const program = Effect.gen(function*() {
|
|
289
|
+
* const caller = yield* (yield* Vfs.make()).caller()
|
|
290
|
+
*
|
|
291
|
+
* yield* caller.mkdir("/work")
|
|
292
|
+
*
|
|
293
|
+
* const root = yield* caller.rootReference
|
|
294
|
+
* const work = yield* caller.lookupReference(root, new TextEncoder().encode("work"))
|
|
295
|
+
*
|
|
296
|
+
* const before = yield* caller.observeMetadata(work)
|
|
297
|
+
*
|
|
298
|
+
* yield* caller.rename("/work", "/moved")
|
|
299
|
+
*
|
|
300
|
+
* const after = yield* caller.observeMetadata(work)
|
|
301
|
+
*
|
|
302
|
+
* return [before.value.kind, after.value.kind]
|
|
303
|
+
* })
|
|
304
|
+
*
|
|
305
|
+
* Effect.runPromise(program).then(console.log)
|
|
306
|
+
* // [ 'directory', 'directory' ]
|
|
307
|
+
* ```
|
|
308
|
+
*
|
|
136
309
|
* @category models
|
|
137
310
|
* @since 0.1.0
|
|
138
311
|
*/
|
|
@@ -163,6 +336,36 @@ export interface RelativeOptions {
|
|
|
163
336
|
/**
|
|
164
337
|
* Controls the base directory and whether metadata operations follow the final symbolic link.
|
|
165
338
|
*
|
|
339
|
+
* @example
|
|
340
|
+
* ```ts
|
|
341
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
342
|
+
* import { Effect } from "effect"
|
|
343
|
+
*
|
|
344
|
+
* const program = Effect.gen(function*() {
|
|
345
|
+
* const caller = yield* (yield* Vfs.make()).caller()
|
|
346
|
+
*
|
|
347
|
+
* yield* caller.writeFile("/target", new Uint8Array([1]), {
|
|
348
|
+
* access: "write",
|
|
349
|
+
* create: "exclusive",
|
|
350
|
+
* mode: 0o644
|
|
351
|
+
* })
|
|
352
|
+
* yield* caller.symlink("/target", "/link")
|
|
353
|
+
*
|
|
354
|
+
* // Metadata operations follow the final link by default, so this reaches the
|
|
355
|
+
* // target; opting out changes the link itself and leaves the target alone.
|
|
356
|
+
* yield* caller.chmod("/link", 0o600)
|
|
357
|
+
* const followed = (yield* caller.stat("/target")).mode & 0o777
|
|
358
|
+
*
|
|
359
|
+
* yield* caller.chmod("/link", 0o777, { followFinalSymlink: false })
|
|
360
|
+
* const unchanged = (yield* caller.stat("/target")).mode & 0o777
|
|
361
|
+
*
|
|
362
|
+
* return [followed.toString(8), unchanged.toString(8)]
|
|
363
|
+
* })
|
|
364
|
+
*
|
|
365
|
+
* Effect.runPromise(program).then(console.log)
|
|
366
|
+
* // [ '600', '600' ]
|
|
367
|
+
* ```
|
|
368
|
+
*
|
|
166
369
|
* @category models
|
|
167
370
|
* @since 0.1.0
|
|
168
371
|
*/
|
|
@@ -194,6 +397,34 @@ export declare const TimeUpdate: typeof VfsModel.TimeUpdate;
|
|
|
194
397
|
/**
|
|
195
398
|
* Schema for independent access and modification time updates.
|
|
196
399
|
*
|
|
400
|
+
* @example
|
|
401
|
+
* ```ts
|
|
402
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
403
|
+
* import { Effect } from "effect"
|
|
404
|
+
*
|
|
405
|
+
* const program = Effect.gen(function*() {
|
|
406
|
+
* const caller = yield* (yield* Vfs.make()).caller()
|
|
407
|
+
*
|
|
408
|
+
* yield* caller.writeFile("/f", new Uint8Array([1]), {
|
|
409
|
+
* access: "write",
|
|
410
|
+
* create: "exclusive"
|
|
411
|
+
* })
|
|
412
|
+
*
|
|
413
|
+
* // Each field is set to a value, to the current clock, or left alone.
|
|
414
|
+
* yield* caller.utimes("/f", {
|
|
415
|
+
* access: { kind: "value", nanoseconds: 1_000n },
|
|
416
|
+
* modification: { kind: "omit" }
|
|
417
|
+
* })
|
|
418
|
+
*
|
|
419
|
+
* const metadata = yield* caller.stat("/f")
|
|
420
|
+
*
|
|
421
|
+
* return [metadata.atimeNs, metadata.mtimeNs === 1_000n]
|
|
422
|
+
* })
|
|
423
|
+
*
|
|
424
|
+
* Effect.runPromise(program).then(console.log)
|
|
425
|
+
* // [ 1000n, false ]
|
|
426
|
+
* ```
|
|
427
|
+
*
|
|
197
428
|
* @category schemas
|
|
198
429
|
* @since 0.1.0
|
|
199
430
|
*/
|
|
@@ -208,6 +439,34 @@ export type Times = typeof Times.Type;
|
|
|
208
439
|
/**
|
|
209
440
|
* A scoped directory capability that can be used for metadata and relative lookup.
|
|
210
441
|
*
|
|
442
|
+
* @example
|
|
443
|
+
* ```ts
|
|
444
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
445
|
+
* import { Effect } from "effect"
|
|
446
|
+
*
|
|
447
|
+
* // The handle anchors relative paths to the directory itself, so a rename in
|
|
448
|
+
* // between does not send the write somewhere else.
|
|
449
|
+
* const program = Effect.gen(function*() {
|
|
450
|
+
* const caller = yield* (yield* Vfs.make()).caller()
|
|
451
|
+
*
|
|
452
|
+
* yield* caller.mkdir("/project")
|
|
453
|
+
*
|
|
454
|
+
* const directory = yield* caller.openDirectory("/project")
|
|
455
|
+
*
|
|
456
|
+
* yield* caller.rename("/project", "/renamed")
|
|
457
|
+
* yield* caller.writeFile("notes.txt", new TextEncoder().encode("kept"), {
|
|
458
|
+
* access: "write",
|
|
459
|
+
* create: "exclusive",
|
|
460
|
+
* relativeTo: directory
|
|
461
|
+
* })
|
|
462
|
+
*
|
|
463
|
+
* return yield* caller.readDirectory("/renamed")
|
|
464
|
+
* }).pipe(Effect.scoped)
|
|
465
|
+
*
|
|
466
|
+
* Effect.runPromise(program).then(console.log)
|
|
467
|
+
* // [ 'notes.txt' ]
|
|
468
|
+
* ```
|
|
469
|
+
*
|
|
211
470
|
* @category models
|
|
212
471
|
* @since 0.1.0
|
|
213
472
|
*/
|
|
@@ -233,6 +492,30 @@ export type SeekMode = typeof SeekMode.Type;
|
|
|
233
492
|
/**
|
|
234
493
|
* Schema for file access, creation, append, truncate, and symlink behavior.
|
|
235
494
|
*
|
|
495
|
+
* @example
|
|
496
|
+
* ```ts
|
|
497
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
498
|
+
* import { Effect } from "effect"
|
|
499
|
+
*
|
|
500
|
+
* // `access` is required; `create` decides whether a missing file is an error.
|
|
501
|
+
* const program = Effect.gen(function*() {
|
|
502
|
+
* const caller = yield* (yield* Vfs.make()).caller()
|
|
503
|
+
*
|
|
504
|
+
* const handle = yield* caller.open("/log.txt", {
|
|
505
|
+
* access: "readWrite",
|
|
506
|
+
* create: "ifMissing",
|
|
507
|
+
* append: true
|
|
508
|
+
* })
|
|
509
|
+
*
|
|
510
|
+
* yield* handle.write(new Uint8Array([1, 2]))
|
|
511
|
+
*
|
|
512
|
+
* return (yield* handle.stat).size
|
|
513
|
+
* }).pipe(Effect.scoped)
|
|
514
|
+
*
|
|
515
|
+
* Effect.runPromise(program).then(console.log)
|
|
516
|
+
* // 2n
|
|
517
|
+
* ```
|
|
518
|
+
*
|
|
236
519
|
* @category schemas
|
|
237
520
|
* @since 0.1.0
|
|
238
521
|
*/
|
|
@@ -247,6 +530,40 @@ export type OpenOptions = typeof OpenSettings.Type & RelativeOptions;
|
|
|
247
530
|
/**
|
|
248
531
|
* Options for an atomic whole-file write, including replacement and final mode controls.
|
|
249
532
|
*
|
|
533
|
+
* @example
|
|
534
|
+
* ```ts
|
|
535
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
536
|
+
* import { Effect } from "effect"
|
|
537
|
+
*
|
|
538
|
+
* const program = Effect.gen(function*() {
|
|
539
|
+
* const caller = yield* (yield* Vfs.make()).caller()
|
|
540
|
+
* const bytes = new TextEncoder().encode("v2")
|
|
541
|
+
*
|
|
542
|
+
* // `access` is required. `create: "exclusive"` refuses to replace an entry,
|
|
543
|
+
* // while `truncate` overwrites one that already exists.
|
|
544
|
+
* yield* caller.writeFile("/app.conf", new TextEncoder().encode("v1"), {
|
|
545
|
+
* access: "write",
|
|
546
|
+
* create: "exclusive",
|
|
547
|
+
* mode: 0o640
|
|
548
|
+
* })
|
|
549
|
+
*
|
|
550
|
+
* const refused = yield* caller.writeFile("/app.conf", bytes, {
|
|
551
|
+
* access: "write",
|
|
552
|
+
* create: "exclusive"
|
|
553
|
+
* }).pipe(
|
|
554
|
+
* Effect.as("replaced"),
|
|
555
|
+
* Effect.catchTag("FsError", (error) => Effect.succeed(error.code))
|
|
556
|
+
* )
|
|
557
|
+
*
|
|
558
|
+
* yield* caller.writeFile("/app.conf", bytes, { access: "write", truncate: true })
|
|
559
|
+
*
|
|
560
|
+
* return [refused, new TextDecoder().decode(yield* caller.readFile("/app.conf"))]
|
|
561
|
+
* })
|
|
562
|
+
*
|
|
563
|
+
* Effect.runPromise(program).then(console.log)
|
|
564
|
+
* // [ 'AlreadyExists', 'v2' ]
|
|
565
|
+
* ```
|
|
566
|
+
*
|
|
250
567
|
* @category models
|
|
251
568
|
* @since 0.1.0
|
|
252
569
|
*/
|
|
@@ -254,6 +571,31 @@ export type WriteFileOptions = VfsModel.WriteFileOptions;
|
|
|
254
571
|
/**
|
|
255
572
|
* A scoped regular-file capability with an independent bigint cursor.
|
|
256
573
|
*
|
|
574
|
+
* @example
|
|
575
|
+
* ```ts
|
|
576
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
577
|
+
* import { Effect } from "effect"
|
|
578
|
+
*
|
|
579
|
+
* const program = Effect.gen(function*() {
|
|
580
|
+
* const caller = yield* (yield* Vfs.make()).caller()
|
|
581
|
+
*
|
|
582
|
+
* const handle = yield* caller.open("/data.bin", {
|
|
583
|
+
* access: "readWrite",
|
|
584
|
+
* create: "exclusive"
|
|
585
|
+
* })
|
|
586
|
+
*
|
|
587
|
+
* yield* handle.write(new Uint8Array([1, 2, 3, 4]))
|
|
588
|
+
*
|
|
589
|
+
* // `pread` reads at an offset and leaves the cursor where writing left it.
|
|
590
|
+
* const slice = yield* handle.pread(2, 1n)
|
|
591
|
+
*
|
|
592
|
+
* return [slice, yield* handle.seek(0n, "current")]
|
|
593
|
+
* }).pipe(Effect.scoped)
|
|
594
|
+
*
|
|
595
|
+
* Effect.runPromise(program).then(console.log)
|
|
596
|
+
* // [ Uint8Array(2) [ 2, 3 ], 4n ]
|
|
597
|
+
* ```
|
|
598
|
+
*
|
|
257
599
|
* @category models
|
|
258
600
|
* @since 0.1.0
|
|
259
601
|
*/
|
|
@@ -281,6 +623,84 @@ export interface FileHandle {
|
|
|
281
623
|
/**
|
|
282
624
|
* A filesystem caller with its own identity, creation mask, and current directory.
|
|
283
625
|
*
|
|
626
|
+
* @example
|
|
627
|
+
* ```ts
|
|
628
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
629
|
+
* import { Effect } from "effect"
|
|
630
|
+
*
|
|
631
|
+
* // Whole-file reads and writes need no scope. `writeFile` always takes an
|
|
632
|
+
* // options bag; `access` is required and `create` decides what a missing file means.
|
|
633
|
+
* const program = Effect.gen(function*() {
|
|
634
|
+
* const volume = yield* Vfs.make()
|
|
635
|
+
* const caller = yield* volume.caller()
|
|
636
|
+
*
|
|
637
|
+
* yield* caller.mkdir("/work")
|
|
638
|
+
* yield* caller.writeFile("/work/notes.txt", new TextEncoder().encode("hello"), {
|
|
639
|
+
* access: "write",
|
|
640
|
+
* create: "exclusive"
|
|
641
|
+
* })
|
|
642
|
+
*
|
|
643
|
+
* return yield* caller.readDirectory("/work")
|
|
644
|
+
* })
|
|
645
|
+
*
|
|
646
|
+
* Effect.runPromise(program).then(console.log)
|
|
647
|
+
* // [ 'notes.txt' ]
|
|
648
|
+
* ```
|
|
649
|
+
*
|
|
650
|
+
* @example
|
|
651
|
+
* ```ts
|
|
652
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
653
|
+
* import { Effect } from "effect"
|
|
654
|
+
*
|
|
655
|
+
* // Reach for a handle when a whole-file write will not do. A handle carries a
|
|
656
|
+
* // cursor and lives until its scope closes.
|
|
657
|
+
* const program = Effect.gen(function*() {
|
|
658
|
+
* const caller = yield* (yield* Vfs.make()).caller()
|
|
659
|
+
*
|
|
660
|
+
* const handle = yield* caller.open("/log.txt", {
|
|
661
|
+
* access: "readWrite",
|
|
662
|
+
* create: "exclusive"
|
|
663
|
+
* })
|
|
664
|
+
*
|
|
665
|
+
* yield* handle.write(new TextEncoder().encode("first "))
|
|
666
|
+
* yield* handle.write(new TextEncoder().encode("second"))
|
|
667
|
+
* yield* handle.seek(0n, "start")
|
|
668
|
+
*
|
|
669
|
+
* return new TextDecoder().decode(yield* handle.read(64))
|
|
670
|
+
* }).pipe(Effect.scoped)
|
|
671
|
+
*
|
|
672
|
+
* Effect.runPromise(program).then(console.log)
|
|
673
|
+
* // first second
|
|
674
|
+
* ```
|
|
675
|
+
*
|
|
676
|
+
* @example
|
|
677
|
+
* ```ts
|
|
678
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
679
|
+
* import { Effect } from "effect"
|
|
680
|
+
*
|
|
681
|
+
* // A derived caller holds the directory's identity, not its path, so renaming
|
|
682
|
+
* // the directory does not redirect later relative writes.
|
|
683
|
+
* const program = Effect.gen(function*() {
|
|
684
|
+
* const caller = yield* (yield* Vfs.make()).caller()
|
|
685
|
+
*
|
|
686
|
+
* yield* caller.mkdir("/build")
|
|
687
|
+
*
|
|
688
|
+
* const work = yield* caller.withDirectory("/build")
|
|
689
|
+
*
|
|
690
|
+
* yield* caller.rename("/build", "/dist")
|
|
691
|
+
* yield* work.writeFile("out.txt", new TextEncoder().encode("done"), {
|
|
692
|
+
* access: "write",
|
|
693
|
+
* create: "exclusive"
|
|
694
|
+
* })
|
|
695
|
+
*
|
|
696
|
+
* return yield* caller.readDirectory("/dist")
|
|
697
|
+
* }).pipe(Effect.scoped)
|
|
698
|
+
*
|
|
699
|
+
* Effect.runPromise(program).then(console.log)
|
|
700
|
+
* // [ 'out.txt' ]
|
|
701
|
+
* ```
|
|
702
|
+
*
|
|
703
|
+
* @see The filesystem model at `/concepts/filesystem-model`.
|
|
284
704
|
* @category models
|
|
285
705
|
* @since 0.1.0
|
|
286
706
|
*/
|
|
@@ -292,11 +712,11 @@ export interface Caller {
|
|
|
292
712
|
readonly lookupReference: (directory: ObjectReference, name: Uint8Array) => Effect.Effect<ObjectReference, FsError>;
|
|
293
713
|
/** Returns a referenced directory's current parent. The root is its own parent. */
|
|
294
714
|
readonly parentReference: (directory: ObjectReference) => Effect.Effect<ObjectReference, FsError>;
|
|
295
|
-
/** Reads metadata and its matching live revision. */
|
|
715
|
+
/** Reads metadata and its matching live revision. Requires no permission on the object, as `stat` does not. */
|
|
296
716
|
readonly observeMetadata: (reference: ObjectReference) => Effect.Effect<ObjectObservation<Metadata>, FsError>;
|
|
297
717
|
/** Reads owned directory entries and their matching directory revision. */
|
|
298
718
|
readonly observeDirectory: (reference: ObjectReference) => Effect.Effect<ObjectObservation<ReadonlyArray<DirectoryEntry>>, FsError>;
|
|
299
|
-
/** Reads an owned symbolic-link target through a stable reference. */
|
|
719
|
+
/** Reads an owned symbolic-link target through a stable reference. Requires no permission on the link. */
|
|
300
720
|
readonly readLinkReference: (reference: ObjectReference) => Effect.Effect<Uint8Array, FsError>;
|
|
301
721
|
/** Opens a referenced regular file for reading. */
|
|
302
722
|
readonly openReference: (reference: ObjectReference) => Effect.Effect<FileHandle, FsError, Scope.Scope>;
|
|
@@ -337,15 +757,15 @@ export interface Caller {
|
|
|
337
757
|
}) => Effect.Effect<void, FsError>;
|
|
338
758
|
/** Creates a symbolic link. The target bytes are stored without resolving them. */
|
|
339
759
|
readonly symlink: (target: PathInput, path: PathInput, options?: RelativeOptions) => Effect.Effect<void, FsError>;
|
|
340
|
-
/** Reads a symbolic-link target as UTF-8, failing
|
|
760
|
+
/** Reads a symbolic-link target as UTF-8, failing `UnrepresentableName` for other bytes. See `readLinkBytes`. */
|
|
341
761
|
readonly readLink: (path: PathInput, options?: RelativeOptions) => Effect.Effect<string, FsError>;
|
|
342
762
|
/** Reads a symbolic-link target as owned bytes. */
|
|
343
763
|
readonly readLinkBytes: (path: PathInput, options?: RelativeOptions) => Effect.Effect<Uint8Array, FsError>;
|
|
344
|
-
/** Reads directory names as UTF-8, failing
|
|
764
|
+
/** Reads directory names as UTF-8, failing `UnrepresentableName` for other bytes. See `readDirectoryBytes`. */
|
|
345
765
|
readonly readDirectory: (path: PathInput, options?: RelativeOptions) => Effect.Effect<ReadonlyArray<string>, FsError>;
|
|
346
766
|
/** Reads directory names as owned byte arrays. */
|
|
347
767
|
readonly readDirectoryBytes: (path: PathInput, options?: RelativeOptions) => Effect.Effect<ReadonlyArray<Uint8Array>, FsError>;
|
|
348
|
-
/** Resolves links and normalizes a path as UTF-8. */
|
|
768
|
+
/** Resolves links and normalizes a path as UTF-8, failing `UnrepresentableName` for other bytes. See `realPathBytes`. */
|
|
349
769
|
readonly realPath: (path: PathInput, options?: RelativeOptions) => Effect.Effect<string, FsError>;
|
|
350
770
|
/** Resolves links and normalizes a path without requiring UTF-8 names. */
|
|
351
771
|
readonly realPathBytes: (path: PathInput, options?: RelativeOptions) => Effect.Effect<BytePath, FsError>;
|
|
@@ -367,6 +787,37 @@ export interface Caller {
|
|
|
367
787
|
/**
|
|
368
788
|
* A committed namespace or content change emitted by a volume watch stream.
|
|
369
789
|
*
|
|
790
|
+
* @example
|
|
791
|
+
* ```ts
|
|
792
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
793
|
+
* import { Effect, Fiber, Stream } from "effect"
|
|
794
|
+
*
|
|
795
|
+
* const program = Effect.gen(function*() {
|
|
796
|
+
* const volume = yield* Vfs.make()
|
|
797
|
+
* const caller = yield* volume.caller()
|
|
798
|
+
*
|
|
799
|
+
* const watcher = yield* volume.watch.pipe(
|
|
800
|
+
* Effect.flatMap((stream) => Stream.runCollect(Stream.take(stream, 2))),
|
|
801
|
+
* Effect.forkChild({ startImmediately: true })
|
|
802
|
+
* )
|
|
803
|
+
*
|
|
804
|
+
* yield* caller.mkdir("/logs")
|
|
805
|
+
* yield* caller.rmdir("/logs")
|
|
806
|
+
*
|
|
807
|
+
* // Each event pairs the kind of change with the absolute path it happened to.
|
|
808
|
+
* const events = yield* Fiber.join(watcher)
|
|
809
|
+
*
|
|
810
|
+
* return yield* Effect.forEach(events, (change) =>
|
|
811
|
+
* Effect.map(
|
|
812
|
+
* Vfs.pathToBytes(change.path),
|
|
813
|
+
* (bytes) => `${change._tag} ${new TextDecoder().decode(bytes)}`
|
|
814
|
+
* ))
|
|
815
|
+
* }).pipe(Effect.scoped)
|
|
816
|
+
*
|
|
817
|
+
* Effect.runPromise(program).then(console.log)
|
|
818
|
+
* // [ 'Create /logs', 'Remove /logs' ]
|
|
819
|
+
* ```
|
|
820
|
+
*
|
|
370
821
|
* @category models
|
|
371
822
|
* @since 0.1.0
|
|
372
823
|
*/
|
|
@@ -426,6 +877,28 @@ export type OverlayChange = typeof OverlayChange.Type;
|
|
|
426
877
|
/**
|
|
427
878
|
* Schema for overlay summary filtering.
|
|
428
879
|
*
|
|
880
|
+
* @example
|
|
881
|
+
* ```ts
|
|
882
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
883
|
+
* import { Effect } from "effect"
|
|
884
|
+
*
|
|
885
|
+
* const program = Effect.gen(function*() {
|
|
886
|
+
* const base = yield* Vfs.make()
|
|
887
|
+
* const workspace = yield* Vfs.makeOverlay(yield* base.snapshot)
|
|
888
|
+
*
|
|
889
|
+
* yield* (yield* workspace.caller()).mkdir("/out")
|
|
890
|
+
*
|
|
891
|
+
* // Timestamps are excluded by default, since they change on every write.
|
|
892
|
+
* const plain = yield* workspace.changes()
|
|
893
|
+
* const timed = yield* workspace.changes({ includeTimestamps: true })
|
|
894
|
+
*
|
|
895
|
+
* return [plain.length, timed.length]
|
|
896
|
+
* })
|
|
897
|
+
*
|
|
898
|
+
* Effect.runPromise(program).then(console.log)
|
|
899
|
+
* // [ 1, 2 ]
|
|
900
|
+
* ```
|
|
901
|
+
*
|
|
429
902
|
* @category schemas
|
|
430
903
|
* @since 0.1.0
|
|
431
904
|
*/
|
|
@@ -440,6 +913,36 @@ export type OverlayChangesOptions = typeof OverlayChangesOptions.Type;
|
|
|
440
913
|
/**
|
|
441
914
|
* A complete snapshot and final-difference summary captured from one committed state.
|
|
442
915
|
*
|
|
916
|
+
* @example
|
|
917
|
+
* ```ts
|
|
918
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
919
|
+
* import { Effect } from "effect"
|
|
920
|
+
*
|
|
921
|
+
* const program = Effect.gen(function*() {
|
|
922
|
+
* const base = yield* Vfs.make()
|
|
923
|
+
* const workspace = yield* Vfs.makeOverlay(yield* base.snapshot)
|
|
924
|
+
*
|
|
925
|
+
* yield* (yield* workspace.caller()).writeFile("/out.txt", new TextEncoder().encode("built"), {
|
|
926
|
+
* access: "write",
|
|
927
|
+
* create: "exclusive"
|
|
928
|
+
* })
|
|
929
|
+
*
|
|
930
|
+
* // One committed state: the summary describes exactly the snapshot beside it,
|
|
931
|
+
* // so restoring from it cannot disagree with the change list.
|
|
932
|
+
* const captured = yield* workspace.capture()
|
|
933
|
+
* const restored = yield* Vfs.fromSnapshot(captured.snapshot)
|
|
934
|
+
* const reader = yield* restored.caller()
|
|
935
|
+
*
|
|
936
|
+
* return [
|
|
937
|
+
* captured.changes.map((change) => change._tag),
|
|
938
|
+
* new TextDecoder().decode(yield* reader.readFile("/out.txt"))
|
|
939
|
+
* ]
|
|
940
|
+
* })
|
|
941
|
+
*
|
|
942
|
+
* Effect.runPromise(program).then(console.log)
|
|
943
|
+
* // [ [ 'Added' ], 'built' ]
|
|
944
|
+
* ```
|
|
945
|
+
*
|
|
443
946
|
* @category models
|
|
444
947
|
* @since 0.1.0
|
|
445
948
|
*/
|
|
@@ -452,6 +955,32 @@ export interface OverlayCapture {
|
|
|
452
955
|
/**
|
|
453
956
|
* An isolated virtual filesystem namespace that creates callers, snapshots, and watch streams.
|
|
454
957
|
*
|
|
958
|
+
* @example
|
|
959
|
+
* ```ts
|
|
960
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
961
|
+
* import { Effect, Fiber, Option, Stream } from "effect"
|
|
962
|
+
*
|
|
963
|
+
* const program = Effect.gen(function*() {
|
|
964
|
+
* const volume = yield* Vfs.make()
|
|
965
|
+
* const caller = yield* volume.caller()
|
|
966
|
+
*
|
|
967
|
+
* // `watch` reports future changes only; nothing is replayed.
|
|
968
|
+
* const watcher = yield* volume.watch.pipe(
|
|
969
|
+
* Effect.flatMap(Stream.runHead),
|
|
970
|
+
* Effect.forkChild({ startImmediately: true })
|
|
971
|
+
* )
|
|
972
|
+
*
|
|
973
|
+
* yield* caller.mkdir("/logs")
|
|
974
|
+
*
|
|
975
|
+
* const change = yield* Fiber.join(watcher)
|
|
976
|
+
*
|
|
977
|
+
* return Option.getOrElse(Option.map(change, (event) => event._tag), () => "none")
|
|
978
|
+
* }).pipe(Effect.scoped)
|
|
979
|
+
*
|
|
980
|
+
* Effect.runPromise(program).then(console.log)
|
|
981
|
+
* // Create
|
|
982
|
+
* ```
|
|
983
|
+
*
|
|
455
984
|
* @category models
|
|
456
985
|
* @since 0.1.0
|
|
457
986
|
*/
|
|
@@ -467,6 +996,30 @@ export interface Volume {
|
|
|
467
996
|
/**
|
|
468
997
|
* An ordinary volume with final-state inspection relative to one immutable snapshot base.
|
|
469
998
|
*
|
|
999
|
+
* @example
|
|
1000
|
+
* ```ts
|
|
1001
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
1002
|
+
* import { Effect } from "effect"
|
|
1003
|
+
*
|
|
1004
|
+
* const program = Effect.gen(function*() {
|
|
1005
|
+
* const base = yield* Vfs.make()
|
|
1006
|
+
* const workspace = yield* Vfs.makeOverlay(yield* base.snapshot)
|
|
1007
|
+
*
|
|
1008
|
+
* yield* (yield* workspace.caller()).writeFile("/added.txt", new Uint8Array([1]), {
|
|
1009
|
+
* access: "write",
|
|
1010
|
+
* create: "exclusive"
|
|
1011
|
+
* })
|
|
1012
|
+
*
|
|
1013
|
+
* // `capture` pairs one committed snapshot with the summary describing it.
|
|
1014
|
+
* const captured = yield* workspace.capture()
|
|
1015
|
+
*
|
|
1016
|
+
* return captured.changes.map((change) => change._tag)
|
|
1017
|
+
* })
|
|
1018
|
+
*
|
|
1019
|
+
* Effect.runPromise(program).then(console.log)
|
|
1020
|
+
* // [ 'Added' ]
|
|
1021
|
+
* ```
|
|
1022
|
+
*
|
|
470
1023
|
* @category models
|
|
471
1024
|
* @since 0.1.0
|
|
472
1025
|
*/
|
|
@@ -479,6 +1032,35 @@ export interface OverlayVolume extends Volume {
|
|
|
479
1032
|
/**
|
|
480
1033
|
* Optional Effect service for providing an existing filesystem caller.
|
|
481
1034
|
*
|
|
1035
|
+
* @example
|
|
1036
|
+
* ```ts
|
|
1037
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
1038
|
+
* import { Effect } from "effect"
|
|
1039
|
+
*
|
|
1040
|
+
* // Code depending on the service does not choose the volume it runs against.
|
|
1041
|
+
* const readVersion = Effect.gen(function*() {
|
|
1042
|
+
* const caller = yield* Vfs.CurrentFileSystem
|
|
1043
|
+
*
|
|
1044
|
+
* return yield* caller.readFile("/version")
|
|
1045
|
+
* })
|
|
1046
|
+
*
|
|
1047
|
+
* const program = Effect.gen(function*() {
|
|
1048
|
+
* const volume = yield* Vfs.make()
|
|
1049
|
+
* const caller = yield* volume.caller()
|
|
1050
|
+
*
|
|
1051
|
+
* yield* caller.writeFile("/version", new Uint8Array([49]), {
|
|
1052
|
+
* access: "write",
|
|
1053
|
+
* create: "exclusive"
|
|
1054
|
+
* })
|
|
1055
|
+
*
|
|
1056
|
+
* return yield* readVersion.pipe(Effect.provideService(Vfs.CurrentFileSystem, caller))
|
|
1057
|
+
* })
|
|
1058
|
+
*
|
|
1059
|
+
* Effect.runPromise(program).then(console.log)
|
|
1060
|
+
* // Uint8Array(1) [ 49 ]
|
|
1061
|
+
* ```
|
|
1062
|
+
*
|
|
1063
|
+
* @see The testing guide at `/guides/testing-with-an-isolated-filesystem`.
|
|
482
1064
|
* @category services
|
|
483
1065
|
* @since 0.1.0
|
|
484
1066
|
*/
|
|
@@ -488,6 +1070,32 @@ export type CurrentFileSystem = VfsModel.CurrentFileSystem;
|
|
|
488
1070
|
/**
|
|
489
1071
|
* Encodes a snapshot as owned UTF-8 JSON bytes using the version 1 snapshot format.
|
|
490
1072
|
*
|
|
1073
|
+
* @example
|
|
1074
|
+
* ```ts
|
|
1075
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
1076
|
+
* import { Effect } from "effect"
|
|
1077
|
+
*
|
|
1078
|
+
* // Bytes are portable: store or transmit them, then decode under explicit limits.
|
|
1079
|
+
* const program = Effect.gen(function*() {
|
|
1080
|
+
* const volume = yield* Vfs.make()
|
|
1081
|
+
* const caller = yield* volume.caller()
|
|
1082
|
+
*
|
|
1083
|
+
* yield* caller.writeFile("/f", new Uint8Array([1, 2, 3]), {
|
|
1084
|
+
* access: "write",
|
|
1085
|
+
* create: "exclusive"
|
|
1086
|
+
* })
|
|
1087
|
+
*
|
|
1088
|
+
* const bytes = yield* Vfs.encodeSnapshot(yield* volume.snapshot)
|
|
1089
|
+
*
|
|
1090
|
+
* // The bytes are UTF-8 JSON carrying the version 1 snapshot format.
|
|
1091
|
+
* return JSON.parse(new TextDecoder().decode(bytes)).version
|
|
1092
|
+
* })
|
|
1093
|
+
*
|
|
1094
|
+
* Effect.runPromise(program).then(console.log)
|
|
1095
|
+
* // 1
|
|
1096
|
+
* ```
|
|
1097
|
+
*
|
|
1098
|
+
* @see {@link decodeSnapshot} for reading the bytes back under limits.
|
|
491
1099
|
* @category serialization
|
|
492
1100
|
* @since 0.1.0
|
|
493
1101
|
*/
|
|
@@ -495,14 +1103,70 @@ export declare const encodeSnapshot: (snapshot: Snapshot) => Effect.Effect<Uint8
|
|
|
495
1103
|
/**
|
|
496
1104
|
* Decodes version 1 snapshot bytes while enforcing explicit input and payload limits.
|
|
497
1105
|
*
|
|
1106
|
+
* @example
|
|
1107
|
+
* ```ts
|
|
1108
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
1109
|
+
* import { ByteSize, Effect } from "effect"
|
|
1110
|
+
*
|
|
1111
|
+
* // Limits are mandatory: decoding is the boundary where untrusted input arrives.
|
|
1112
|
+
* const program = Effect.gen(function*() {
|
|
1113
|
+
* const volume = yield* Vfs.make()
|
|
1114
|
+
* const bytes = yield* Vfs.encodeSnapshot(yield* volume.snapshot)
|
|
1115
|
+
*
|
|
1116
|
+
* return yield* Vfs.decodeSnapshot(bytes, {
|
|
1117
|
+
* maxEncodedBytes: ByteSize.bytes(1),
|
|
1118
|
+
* maxRecords: 10_000,
|
|
1119
|
+
* maxEntries: 10_000,
|
|
1120
|
+
* maxDecodedBytes: ByteSize.megabytes(16)
|
|
1121
|
+
* }).pipe(
|
|
1122
|
+
* Effect.as("accepted"),
|
|
1123
|
+
* Effect.catchTag("ImageError", (error) => Effect.succeed([error.code, error.field]))
|
|
1124
|
+
* )
|
|
1125
|
+
* })
|
|
1126
|
+
*
|
|
1127
|
+
* Effect.runPromise(program).then(console.log)
|
|
1128
|
+
* // [ 'LimitExceeded', 'encodedBytes' ]
|
|
1129
|
+
* ```
|
|
1130
|
+
*
|
|
1131
|
+
* @see {@link DecodeLimits} for what each bound protects.
|
|
498
1132
|
* @category serialization
|
|
499
1133
|
* @since 0.1.0
|
|
500
1134
|
*/
|
|
501
1135
|
export declare const decodeSnapshot: (input: Uint8Array, limits: DecodeLimits) => Effect.Effect<Snapshot, ImageError>;
|
|
502
1136
|
/**
|
|
503
1137
|
* Computes an exact portable delta between two immutable snapshots.
|
|
504
|
-
* Requires the platform-neutral `Crypto.Crypto` service for base identity.
|
|
1138
|
+
* Requires the platform-neutral `Crypto.Crypto` service for base identity. Provide
|
|
1139
|
+
* `NodeCrypto.layer`, `BunCrypto.layer`, or your own via `Crypto.make`.
|
|
1140
|
+
*
|
|
1141
|
+
* @example
|
|
1142
|
+
* ```ts
|
|
1143
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
1144
|
+
* import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
|
|
1145
|
+
* import { Effect } from "effect"
|
|
1146
|
+
*
|
|
1147
|
+
* // Two snapshots in, one opaque delta out. `inspectSnapshotDelta` reads it.
|
|
1148
|
+
* const program = Effect.gen(function*() {
|
|
1149
|
+
* const volume = yield* Vfs.make()
|
|
1150
|
+
* const caller = yield* volume.caller()
|
|
1151
|
+
* const base = yield* volume.snapshot
|
|
1152
|
+
*
|
|
1153
|
+
* yield* caller.writeFile("/added.txt", new Uint8Array([1]), {
|
|
1154
|
+
* access: "write",
|
|
1155
|
+
* create: "exclusive"
|
|
1156
|
+
* })
|
|
1157
|
+
*
|
|
1158
|
+
* const delta = yield* Vfs.diffSnapshots(base, yield* volume.snapshot)
|
|
505
1159
|
*
|
|
1160
|
+
* const changes = yield* Vfs.inspectSnapshotDelta(base, delta)
|
|
1161
|
+
*
|
|
1162
|
+
* return new TextDecoder().decode(yield* Vfs.pathToBytes(changes[0]!.path))
|
|
1163
|
+
* }).pipe(Effect.provide(NodeCrypto.layer))
|
|
1164
|
+
*
|
|
1165
|
+
* Effect.runPromise(program).then(console.log)
|
|
1166
|
+
* // /added.txt
|
|
1167
|
+
* ```
|
|
1168
|
+
*
|
|
1169
|
+
* @see {@link inspectSnapshotDelta} to read a delta, {@link applySnapshotDelta} to apply one.
|
|
506
1170
|
* @category snapshots
|
|
507
1171
|
* @since 0.1.0
|
|
508
1172
|
*/
|
|
@@ -520,8 +1184,35 @@ export declare const diffSnapshots: (base: Snapshot, target: Snapshot, limits?:
|
|
|
520
1184
|
} | undefined) => Effect.Effect<SnapshotDeltaModel.SnapshotDelta, VfsModel.ConfigurationError | ImageError | import("effect/PlatformError").PlatformError, import("effect/Crypto").Crypto>;
|
|
521
1185
|
/**
|
|
522
1186
|
* Verifies an exact snapshot delta against its base and derives an owned path-oriented summary.
|
|
523
|
-
* Requires the platform-neutral `Crypto.Crypto` service for base identity.
|
|
1187
|
+
* Requires the platform-neutral `Crypto.Crypto` service for base identity. Provide
|
|
1188
|
+
* `NodeCrypto.layer`, `BunCrypto.layer`, or your own via `Crypto.make`.
|
|
1189
|
+
*
|
|
1190
|
+
* @example
|
|
1191
|
+
* ```ts
|
|
1192
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
1193
|
+
* import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
|
|
1194
|
+
* import { Effect } from "effect"
|
|
1195
|
+
*
|
|
1196
|
+
* // Inspection verifies the delta against its base before describing it.
|
|
1197
|
+
* const program = Effect.gen(function*() {
|
|
1198
|
+
* const volume = yield* Vfs.make()
|
|
1199
|
+
* const caller = yield* volume.caller()
|
|
1200
|
+
* const base = yield* volume.snapshot
|
|
1201
|
+
*
|
|
1202
|
+
* yield* caller.mkdir("/logs")
|
|
1203
|
+
*
|
|
1204
|
+
* const delta = yield* Vfs.diffSnapshots(base, yield* volume.snapshot)
|
|
1205
|
+
* const changes = yield* Vfs.inspectSnapshotDelta(base, delta)
|
|
524
1206
|
*
|
|
1207
|
+
* return changes.map((change) =>
|
|
1208
|
+
* change._tag === "Updated" ? change.afterKind : change.kind)
|
|
1209
|
+
* }).pipe(Effect.provide(NodeCrypto.layer))
|
|
1210
|
+
*
|
|
1211
|
+
* Effect.runPromise(program).then(console.log)
|
|
1212
|
+
* // [ 'directory' ]
|
|
1213
|
+
* ```
|
|
1214
|
+
*
|
|
1215
|
+
* @see {@link SnapshotChangesOptions} for including timestamp differences.
|
|
525
1216
|
* @category snapshots
|
|
526
1217
|
* @since 0.1.0
|
|
527
1218
|
*/
|
|
@@ -555,8 +1246,40 @@ export declare const inspectSnapshotDelta: (base: Snapshot, delta: SnapshotDelta
|
|
|
555
1246
|
})[], VfsModel.ConfigurationError | ImageError | import("effect/PlatformError").PlatformError | SnapshotDeltaModel.SnapshotDeltaError, import("effect/Crypto").Crypto>;
|
|
556
1247
|
/**
|
|
557
1248
|
* Applies an exact delta to its semantically matching base and returns a new snapshot.
|
|
558
|
-
* Requires the platform-neutral `Crypto.Crypto` service for base identity.
|
|
1249
|
+
* Requires the platform-neutral `Crypto.Crypto` service for base identity. Provide
|
|
1250
|
+
* `NodeCrypto.layer`, `BunCrypto.layer`, or your own via `Crypto.make`.
|
|
1251
|
+
*
|
|
1252
|
+
* @example
|
|
1253
|
+
* ```ts
|
|
1254
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
1255
|
+
* import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
|
|
1256
|
+
* import { Effect } from "effect"
|
|
1257
|
+
*
|
|
1258
|
+
* const program = Effect.gen(function*() {
|
|
1259
|
+
* const volume = yield* Vfs.make()
|
|
1260
|
+
* const caller = yield* volume.caller()
|
|
1261
|
+
* const base = yield* volume.snapshot
|
|
1262
|
+
*
|
|
1263
|
+
* yield* caller.writeFile("/f", new Uint8Array([7]), {
|
|
1264
|
+
* access: "write",
|
|
1265
|
+
* create: "exclusive"
|
|
1266
|
+
* })
|
|
1267
|
+
*
|
|
1268
|
+
* const delta = yield* Vfs.diffSnapshots(base, yield* volume.snapshot)
|
|
559
1269
|
*
|
|
1270
|
+
* // Applying the delta to the same base reproduces the target snapshot.
|
|
1271
|
+
* const applied = yield* Vfs.applySnapshotDelta(base, delta)
|
|
1272
|
+
* const restored = yield* Vfs.fromSnapshot(applied)
|
|
1273
|
+
* const reader = yield* restored.caller()
|
|
1274
|
+
*
|
|
1275
|
+
* return yield* reader.readFile("/f")
|
|
1276
|
+
* }).pipe(Effect.provide(NodeCrypto.layer))
|
|
1277
|
+
*
|
|
1278
|
+
* Effect.runPromise(program).then(console.log)
|
|
1279
|
+
* // Uint8Array(1) [ 7 ]
|
|
1280
|
+
* ```
|
|
1281
|
+
*
|
|
1282
|
+
* @see {@link SnapshotDeltaFromBytes} for moving a delta across a boundary.
|
|
560
1283
|
* @category snapshots
|
|
561
1284
|
* @since 0.1.0
|
|
562
1285
|
*/
|
|
@@ -576,6 +1299,34 @@ export declare const applySnapshotDelta: (base: Snapshot, delta: SnapshotDeltaMo
|
|
|
576
1299
|
* Creates an Effect Schema codec between owned bytes and opaque snapshot deltas.
|
|
577
1300
|
* Omission uses `SnapshotDeltaLimits.default`.
|
|
578
1301
|
*
|
|
1302
|
+
* @example
|
|
1303
|
+
* ```ts
|
|
1304
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
1305
|
+
* import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
|
|
1306
|
+
* import { Effect, Schema } from "effect"
|
|
1307
|
+
*
|
|
1308
|
+
* // Omitting limits uses `SnapshotDeltaLimits.default`.
|
|
1309
|
+
* const codec = Vfs.SnapshotDeltaFromBytes()
|
|
1310
|
+
*
|
|
1311
|
+
* const program = Effect.gen(function*() {
|
|
1312
|
+
* const volume = yield* Vfs.make()
|
|
1313
|
+
* const base = yield* volume.snapshot
|
|
1314
|
+
*
|
|
1315
|
+
* yield* (yield* volume.caller()).mkdir("/out")
|
|
1316
|
+
*
|
|
1317
|
+
* const delta = yield* Vfs.diffSnapshots(base, yield* volume.snapshot)
|
|
1318
|
+
* const bytes = yield* Schema.encodeEffect(codec)(delta)
|
|
1319
|
+
*
|
|
1320
|
+
* // The delta survives the trip through bytes and still applies to its base.
|
|
1321
|
+
* const decoded = yield* Schema.decodeEffect(codec)(bytes)
|
|
1322
|
+
*
|
|
1323
|
+
* return (yield* Vfs.inspectSnapshotDelta(base, decoded)).map((change) => change._tag)
|
|
1324
|
+
* }).pipe(Effect.provide(NodeCrypto.layer))
|
|
1325
|
+
*
|
|
1326
|
+
* Effect.runPromise(program).then(console.log)
|
|
1327
|
+
* // [ 'Added' ]
|
|
1328
|
+
* ```
|
|
1329
|
+
*
|
|
579
1330
|
* @category schemas
|
|
580
1331
|
* @since 0.1.0
|
|
581
1332
|
*/
|
|
@@ -587,6 +1338,28 @@ export declare const SnapshotDeltaFromBytes: (limits?: SnapshotDeltaModel.Snapsh
|
|
|
587
1338
|
*
|
|
588
1339
|
* Shared-memory-backed and detached views fail with `InvalidArgument`.
|
|
589
1340
|
*
|
|
1341
|
+
* @example
|
|
1342
|
+
* ```ts
|
|
1343
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
1344
|
+
* import { Effect } from "effect"
|
|
1345
|
+
*
|
|
1346
|
+
* // Any byte except NUL is accepted. Empty, NUL-bearing, detached, and shared-memory
|
|
1347
|
+
* // input all fail with `InvalidArgument`.
|
|
1348
|
+
* const program = Effect.gen(function*() {
|
|
1349
|
+
* const path = yield* Vfs.pathFromBytes(new Uint8Array([47, 116, 109, 112]))
|
|
1350
|
+
*
|
|
1351
|
+
* const rejected = yield* Vfs.pathFromBytes(new Uint8Array([47, 0])).pipe(
|
|
1352
|
+
* Effect.as("accepted"),
|
|
1353
|
+
* Effect.catchTag("FsError", (error) => Effect.succeed(error.code))
|
|
1354
|
+
* )
|
|
1355
|
+
*
|
|
1356
|
+
* return [yield* Vfs.pathToBytes(path), rejected]
|
|
1357
|
+
* })
|
|
1358
|
+
*
|
|
1359
|
+
* Effect.runPromise(program).then(console.log)
|
|
1360
|
+
* // [ Uint8Array(4) [ 47, 116, 109, 112 ], 'InvalidArgument' ]
|
|
1361
|
+
* ```
|
|
1362
|
+
*
|
|
590
1363
|
* @category constructors
|
|
591
1364
|
* @since 0.1.0
|
|
592
1365
|
*/
|
|
@@ -605,6 +1378,26 @@ export declare const pathToBytes: (path: BytePath) => Effect.Effect<Uint8Array,
|
|
|
605
1378
|
*
|
|
606
1379
|
* Fixture paths must be absolute, unique, and explicitly include their parent directories.
|
|
607
1380
|
*
|
|
1381
|
+
* @example
|
|
1382
|
+
* ```ts
|
|
1383
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
1384
|
+
* import { Effect, Schema } from "effect"
|
|
1385
|
+
*
|
|
1386
|
+
* // Fixtures must list every parent directory before its children.
|
|
1387
|
+
* const program = Schema.decodeUnknownEffect(Vfs.Fixture)({
|
|
1388
|
+
* entries: [
|
|
1389
|
+
* { kind: "file", path: "/etc/hosts", bytes: new TextEncoder().encode("127.0.0.1") }
|
|
1390
|
+
* ]
|
|
1391
|
+
* }).pipe(
|
|
1392
|
+
* Effect.flatMap(Vfs.fromFixture),
|
|
1393
|
+
* Effect.as("built"),
|
|
1394
|
+
* Effect.catchTag("ImageError", (error) => Effect.succeed(`rejected: ${error.code}`))
|
|
1395
|
+
* )
|
|
1396
|
+
*
|
|
1397
|
+
* Effect.runPromise(program).then(console.log)
|
|
1398
|
+
* // rejected: InvalidStructure
|
|
1399
|
+
* ```
|
|
1400
|
+
*
|
|
608
1401
|
* @category schemas
|
|
609
1402
|
* @since 0.1.0
|
|
610
1403
|
*/
|
|
@@ -624,6 +1417,26 @@ export type Fixture = typeof Fixture.Type;
|
|
|
624
1417
|
* Each execution creates independent storage. Snapshot image failures cannot
|
|
625
1418
|
* arise because this constructor does not accept persisted input.
|
|
626
1419
|
*
|
|
1420
|
+
* @example
|
|
1421
|
+
* ```ts
|
|
1422
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
1423
|
+
* import { Effect } from "effect"
|
|
1424
|
+
*
|
|
1425
|
+
* const program = Effect.gen(function*() {
|
|
1426
|
+
* const volume = yield* Vfs.make()
|
|
1427
|
+
* const caller = yield* volume.caller()
|
|
1428
|
+
*
|
|
1429
|
+
* yield* caller.mkdir("/work")
|
|
1430
|
+
* yield* caller.writeFile("/work/notes.txt", new Uint8Array([104, 105]), { access: "write", create: "exclusive" })
|
|
1431
|
+
*
|
|
1432
|
+
* return yield* caller.readFile("/work/notes.txt")
|
|
1433
|
+
* })
|
|
1434
|
+
*
|
|
1435
|
+
* Effect.runPromise(program).then(console.log)
|
|
1436
|
+
* // Uint8Array(2) [ 104, 105 ]
|
|
1437
|
+
* ```
|
|
1438
|
+
*
|
|
1439
|
+
* @see The testing guide at `/guides/testing-with-an-isolated-filesystem`.
|
|
627
1440
|
* @category constructors
|
|
628
1441
|
* @since 0.1.0
|
|
629
1442
|
*/
|
|
@@ -631,6 +1444,40 @@ export declare const make: (options?: VolumeOptions) => Effect.Effect<Volume, Co
|
|
|
631
1444
|
/**
|
|
632
1445
|
* Restores a fresh volume from an opaque snapshot under the supplied destination limits.
|
|
633
1446
|
*
|
|
1447
|
+
* @example
|
|
1448
|
+
* ```ts
|
|
1449
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
1450
|
+
* import { Effect } from "effect"
|
|
1451
|
+
*
|
|
1452
|
+
* const program = Effect.gen(function*() {
|
|
1453
|
+
* const original = yield* Vfs.make()
|
|
1454
|
+
* const source = yield* original.caller()
|
|
1455
|
+
*
|
|
1456
|
+
* yield* source.writeFile("/seed.txt", new TextEncoder().encode("one"), {
|
|
1457
|
+
* access: "write",
|
|
1458
|
+
* create: "exclusive"
|
|
1459
|
+
* })
|
|
1460
|
+
*
|
|
1461
|
+
* const restored = yield* Vfs.fromSnapshot(yield* original.snapshot)
|
|
1462
|
+
* const copy = yield* restored.caller()
|
|
1463
|
+
*
|
|
1464
|
+
* // Writing to the restore leaves the original untouched: the two are independent.
|
|
1465
|
+
* yield* copy.writeFile("/seed.txt", new TextEncoder().encode("two"), {
|
|
1466
|
+
* access: "write",
|
|
1467
|
+
* truncate: true
|
|
1468
|
+
* })
|
|
1469
|
+
*
|
|
1470
|
+
* return [
|
|
1471
|
+
* new TextDecoder().decode(yield* source.readFile("/seed.txt")),
|
|
1472
|
+
* new TextDecoder().decode(yield* copy.readFile("/seed.txt"))
|
|
1473
|
+
* ]
|
|
1474
|
+
* })
|
|
1475
|
+
*
|
|
1476
|
+
* Effect.runPromise(program).then(console.log)
|
|
1477
|
+
* // [ 'one', 'two' ]
|
|
1478
|
+
* ```
|
|
1479
|
+
*
|
|
1480
|
+
* @see {@link makeOverlay} for a copy-on-write workspace over the same base.
|
|
634
1481
|
* @category constructors
|
|
635
1482
|
* @since 0.1.0
|
|
636
1483
|
*/
|
|
@@ -648,6 +1495,39 @@ export declare const fromSnapshot: (snapshot: Snapshot, options?: VolumeOptions)
|
|
|
648
1495
|
* Invalid base snapshots fail with `ImageError`; invalid volume limits fail
|
|
649
1496
|
* with `ConfigurationError`. Each execution creates a fresh workspace.
|
|
650
1497
|
*
|
|
1498
|
+
* @example
|
|
1499
|
+
* ```ts
|
|
1500
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
1501
|
+
* import { Effect } from "effect"
|
|
1502
|
+
*
|
|
1503
|
+
* const program = Effect.gen(function*() {
|
|
1504
|
+
* const base = yield* Vfs.make()
|
|
1505
|
+
* const origin = yield* base.caller()
|
|
1506
|
+
*
|
|
1507
|
+
* yield* origin.writeFile("/config.json", new TextEncoder().encode("{}"), {
|
|
1508
|
+
* access: "write",
|
|
1509
|
+
* create: "exclusive"
|
|
1510
|
+
* })
|
|
1511
|
+
*
|
|
1512
|
+
* const workspace = yield* Vfs.makeOverlay(yield* base.snapshot)
|
|
1513
|
+
*
|
|
1514
|
+
* yield* (yield* workspace.caller()).writeFile("/config.json", new TextEncoder().encode("[]"), {
|
|
1515
|
+
* access: "write",
|
|
1516
|
+
* truncate: true
|
|
1517
|
+
* })
|
|
1518
|
+
*
|
|
1519
|
+
* // The write landed in the workspace only; the base still reads its own value.
|
|
1520
|
+
* return [
|
|
1521
|
+
* new TextDecoder().decode(yield* origin.readFile("/config.json")),
|
|
1522
|
+
* (yield* workspace.changes()).map((change) => change._tag)
|
|
1523
|
+
* ]
|
|
1524
|
+
* })
|
|
1525
|
+
*
|
|
1526
|
+
* Effect.runPromise(program).then(console.log)
|
|
1527
|
+
* // [ '{}', [ 'Updated' ] ]
|
|
1528
|
+
* ```
|
|
1529
|
+
*
|
|
1530
|
+
* @see The overlay guide at `/guides/overlay-filesystems`.
|
|
651
1531
|
* @category constructors
|
|
652
1532
|
* @since 0.1.0
|
|
653
1533
|
*/
|
|
@@ -655,6 +1535,35 @@ export declare const makeOverlay: (base: Snapshot, options?: VolumeOptions) => E
|
|
|
655
1535
|
/**
|
|
656
1536
|
* Builds a fresh volume from a validated final-state fixture.
|
|
657
1537
|
*
|
|
1538
|
+
* @example
|
|
1539
|
+
* ```ts
|
|
1540
|
+
* import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
|
|
1541
|
+
* import { Effect } from "effect"
|
|
1542
|
+
*
|
|
1543
|
+
* const encoder = new TextEncoder()
|
|
1544
|
+
*
|
|
1545
|
+
* const program = Effect.gen(function*() {
|
|
1546
|
+
* // Parent directories must be listed explicitly, before their children.
|
|
1547
|
+
* const volume = yield* Vfs.fromFixture({
|
|
1548
|
+
* entries: [
|
|
1549
|
+
* { kind: "directory", path: "/project" },
|
|
1550
|
+
* {
|
|
1551
|
+
* kind: "file",
|
|
1552
|
+
* path: "/project/package.json",
|
|
1553
|
+
* bytes: encoder.encode(`{"version":"1.2.3"}`)
|
|
1554
|
+
* },
|
|
1555
|
+
* { kind: "symlink", path: "/project/latest", target: "/project/package.json" }
|
|
1556
|
+
* ]
|
|
1557
|
+
* })
|
|
1558
|
+
*
|
|
1559
|
+
* return yield* (yield* volume.caller()).readDirectory("/project")
|
|
1560
|
+
* })
|
|
1561
|
+
*
|
|
1562
|
+
* Effect.runPromise(program).then(console.log)
|
|
1563
|
+
* // [ 'package.json', 'latest' ]
|
|
1564
|
+
* ```
|
|
1565
|
+
*
|
|
1566
|
+
* @see The fixtures guide at `/guides/fixtures-and-snapshots`.
|
|
658
1567
|
* @category constructors
|
|
659
1568
|
* @since 0.1.0
|
|
660
1569
|
*/
|