@effect-vfs/core 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +34 -31
  2. package/dist/BytePath.d.ts +114 -4
  3. package/dist/BytePath.d.ts.map +1 -1
  4. package/dist/BytePath.js +155 -0
  5. package/dist/Caller.d.ts +391 -0
  6. package/dist/Caller.d.ts.map +1 -0
  7. package/dist/Caller.js +199 -0
  8. package/dist/FileHandle.d.ts +50 -0
  9. package/dist/FileHandle.d.ts.map +1 -0
  10. package/dist/FileHandle.js +28 -0
  11. package/dist/Fixture.d.ts +155 -0
  12. package/dist/Fixture.d.ts.map +1 -0
  13. package/dist/Fixture.js +65 -0
  14. package/dist/LiveVolume.d.ts +11 -21
  15. package/dist/LiveVolume.d.ts.map +1 -1
  16. package/dist/LiveVolume.js +17 -14
  17. package/dist/Metadata.d.ts +186 -0
  18. package/dist/Metadata.d.ts.map +1 -0
  19. package/dist/Metadata.js +125 -0
  20. package/dist/Snapshot.d.ts +31 -53
  21. package/dist/Snapshot.d.ts.map +1 -1
  22. package/dist/Snapshot.js +26 -49
  23. package/dist/SnapshotDelta.d.ts +24 -39
  24. package/dist/SnapshotDelta.d.ts.map +1 -1
  25. package/dist/SnapshotDelta.js +14 -35
  26. package/dist/Target.d.ts +225 -0
  27. package/dist/Target.d.ts.map +1 -0
  28. package/dist/Target.js +92 -0
  29. package/dist/Testing.d.ts +128 -0
  30. package/dist/Testing.d.ts.map +1 -0
  31. package/dist/Testing.js +111 -0
  32. package/dist/VfsError.d.ts +199 -0
  33. package/dist/VfsError.d.ts.map +1 -0
  34. package/dist/VfsError.js +164 -0
  35. package/dist/VirtualFileSystem.d.ts +628 -347
  36. package/dist/VirtualFileSystem.d.ts.map +1 -1
  37. package/dist/VirtualFileSystem.js +448 -115
  38. package/dist/Volume.d.ts +211 -0
  39. package/dist/Volume.d.ts.map +1 -0
  40. package/dist/Volume.js +172 -0
  41. package/dist/Watch.d.ts +68 -0
  42. package/dist/Watch.d.ts.map +1 -0
  43. package/dist/Watch.js +46 -0
  44. package/dist/index.d.ts +56 -1
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +56 -1
  47. package/dist/internal/budget.d.ts +90 -0
  48. package/dist/internal/budget.d.ts.map +1 -0
  49. package/dist/internal/budget.js +70 -0
  50. package/dist/internal/bytePath.d.ts +2 -0
  51. package/dist/internal/bytePath.d.ts.map +1 -1
  52. package/dist/internal/bytePath.js +3 -2
  53. package/dist/internal/bytes.d.ts +3 -0
  54. package/dist/internal/bytes.d.ts.map +1 -1
  55. package/dist/internal/bytes.js +4 -2
  56. package/dist/internal/canonicalBase64.d.ts +1 -4
  57. package/dist/internal/canonicalBase64.d.ts.map +1 -1
  58. package/dist/internal/canonicalBase64.js +9 -13
  59. package/dist/internal/errors.d.ts +47 -5
  60. package/dist/internal/errors.d.ts.map +1 -1
  61. package/dist/internal/errors.js +80 -36
  62. package/dist/internal/fixture.d.ts +2 -2
  63. package/dist/internal/fixture.d.ts.map +1 -1
  64. package/dist/internal/fixture.js +57 -74
  65. package/dist/internal/hex128.d.ts +12 -0
  66. package/dist/internal/hex128.d.ts.map +1 -0
  67. package/dist/internal/hex128.js +7 -0
  68. package/dist/internal/hmac.d.ts +5 -0
  69. package/dist/internal/hmac.d.ts.map +1 -0
  70. package/dist/internal/hmac.js +150 -0
  71. package/dist/internal/image.d.ts +14 -117
  72. package/dist/internal/image.d.ts.map +1 -1
  73. package/dist/internal/image.js +137 -167
  74. package/dist/internal/inodeTable.d.ts +20 -0
  75. package/dist/internal/inodeTable.d.ts.map +1 -0
  76. package/dist/internal/inodeTable.js +48 -0
  77. package/dist/internal/lines.d.ts +36 -0
  78. package/dist/internal/lines.d.ts.map +1 -0
  79. package/dist/internal/lines.js +115 -0
  80. package/dist/internal/liveImage.d.ts +19 -278
  81. package/dist/internal/liveImage.d.ts.map +1 -1
  82. package/dist/internal/liveImage.js +75 -166
  83. package/dist/internal/merkle.d.ts +45 -0
  84. package/dist/internal/merkle.d.ts.map +1 -0
  85. package/dist/internal/merkle.js +179 -0
  86. package/dist/internal/metadata.d.ts +2 -13
  87. package/dist/internal/metadata.d.ts.map +1 -1
  88. package/dist/internal/metadata.js +5 -15
  89. package/dist/internal/overlayDiff.d.ts +2 -2
  90. package/dist/internal/overlayDiff.d.ts.map +1 -1
  91. package/dist/internal/overlayDiff.js +0 -3
  92. package/dist/internal/path.d.ts +17 -5
  93. package/dist/internal/path.d.ts.map +1 -1
  94. package/dist/internal/path.js +38 -24
  95. package/dist/internal/snapshotDelta.d.ts +61 -60
  96. package/dist/internal/snapshotDelta.d.ts.map +1 -1
  97. package/dist/internal/snapshotDelta.js +612 -504
  98. package/dist/internal/snapshotDeltaModel.d.ts.map +1 -1
  99. package/dist/internal/snapshotDeltaModel.js +0 -1
  100. package/dist/internal/stagedState.d.ts +11 -15
  101. package/dist/internal/stagedState.d.ts.map +1 -1
  102. package/dist/internal/stagedState.js +15 -59
  103. package/dist/internal/testSeams.d.ts +17 -0
  104. package/dist/internal/testSeams.d.ts.map +1 -0
  105. package/dist/internal/testSeams.js +20 -0
  106. package/dist/internal/tree.d.ts +225 -0
  107. package/dist/internal/tree.d.ts.map +1 -0
  108. package/dist/internal/tree.js +385 -0
  109. package/dist/internal/turnstile.d.ts +8 -0
  110. package/dist/internal/turnstile.d.ts.map +1 -0
  111. package/dist/internal/turnstile.js +43 -0
  112. package/dist/internal/virtualFileSystem.d.ts +39 -489
  113. package/dist/internal/virtualFileSystem.d.ts.map +1 -1
  114. package/dist/internal/virtualFileSystem.js +2112 -2465
  115. package/dist/internal/volumeState.d.ts +110 -0
  116. package/dist/internal/volumeState.d.ts.map +1 -0
  117. package/dist/internal/volumeState.js +204 -0
  118. package/dist/internal/watchHub.d.ts +15 -6
  119. package/dist/internal/watchHub.d.ts.map +1 -1
  120. package/dist/internal/watchHub.js +56 -42
  121. package/package.json +48 -4
  122. package/dist/VirtualFileSystemError.d.ts +0 -54
  123. package/dist/VirtualFileSystemError.d.ts.map +0 -1
  124. package/dist/VirtualFileSystemError.js +0 -49
  125. package/dist/internal/overlayContent.d.ts +0 -15
  126. package/dist/internal/overlayContent.d.ts.map +0 -1
  127. package/dist/internal/overlayContent.js +0 -29
  128. package/dist/internal/testHooks.d.ts +0 -20
  129. package/dist/internal/testHooks.d.ts.map +0 -1
  130. package/dist/internal/testHooks.js +0 -23
@@ -1,24 +1,32 @@
1
1
  import * as Context from "effect/Context";
2
2
  import * as Effect from "effect/Effect";
3
+ import * as Layer from "effect/Layer";
3
4
  import * as Result from "effect/Result";
4
5
  import * as Schema from "effect/Schema";
5
6
  import * as SchemaIssue from "effect/SchemaIssue";
6
7
  import * as SchemaTransformation from "effect/SchemaTransformation";
8
+ import * as Sink from "effect/Sink";
9
+ import * as Stream from "effect/Stream";
10
+ import * as CallerModule from "./Caller.js";
11
+ import * as FileHandleModule from "./FileHandle.js";
12
+ import * as FixtureModule from "./Fixture.js";
13
+ import * as MetadataModule from "./Metadata.js";
14
+ import * as VfsErrorModule from "./VfsError.js";
15
+ import * as VolumeModule from "./Volume.js";
16
+ import * as WatchModule from "./Watch.js";
17
+ export { Entry, isEntry, isTarget, Name, Target } from "./Target.js";
7
18
  export { BytePath } from "./BytePath.js";
8
- export { DecodeLimits, ImageError, SnapshotTypeId } from "./Snapshot.js";
9
- import { decodeConfiguration } from "./internal/errors.js";
19
+ export { DecodeLimits, SnapshotTypeId } from "./Snapshot.js";
20
+ import { BudgetFromDecodeLimits, DeltaBudgetFromLimits } from "./internal/budget.js";
21
+ import { decodeConfiguration, retargetFailure } from "./internal/errors.js";
10
22
  import * as FixtureInternal from "./internal/fixture.js";
11
23
  import * as Image from "./internal/image.js";
12
24
  import * as Path from "./internal/path.js";
13
25
  import * as SnapshotDeltaInternal from "./internal/snapshotDelta.js";
14
26
  import * as VfsModel from "./internal/virtualFileSystem.js";
27
+ import { open as openLiveVolume } from "./LiveVolume.js";
15
28
  import * as SnapshotDeltaModel from "./SnapshotDelta.js";
16
- export { SnapshotChange, SnapshotChangesOptions, SnapshotDeltaError, SnapshotDeltaLimits, SnapshotDeltaTypeId, SnapshotDifference, SnapshotNodeKind } from "./SnapshotDelta.js";
17
- const VolumeId = VfsModel.VolumeId;
18
- const CallerId = VfsModel.CallerId;
19
- const FileHandleId = VfsModel.FileHandleId;
20
- const DirectoryHandleId = VfsModel.DirectoryHandleId;
21
- const ObjectReferenceId = VfsModel.ObjectReferenceId;
29
+ export { SnapshotChange, SnapshotChangesOptions, SnapshotDeltaLimits, SnapshotDeltaTypeId, SnapshotDifference, SnapshotNodeKind } from "./SnapshotDelta.js";
22
30
  /**
23
31
  * Schema for portable virtual filesystem error codes.
24
32
  *
@@ -49,7 +57,7 @@ const ObjectReferenceId = VfsModel.ObjectReferenceId;
49
57
  * @category schemas
50
58
  * @since 0.1.0
51
59
  */
52
- export const FsCode = VfsModel.FsCode;
60
+ export const FsCode = VfsErrorModule.FsCode;
53
61
  /**
54
62
  * Describes an expected filesystem operation failure.
55
63
  *
@@ -60,7 +68,6 @@ export const FsCode = VfsModel.FsCode;
60
68
  * @example
61
69
  * ```ts
62
70
  * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
63
- * import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
64
71
  * import { Effect } from "effect"
65
72
  *
66
73
  * const program = Effect.gen(function*() {
@@ -69,28 +76,26 @@ export const FsCode = VfsModel.FsCode;
69
76
  * // Every filesystem operation fails with this one tagged error; `code`
70
77
  * // distinguishes the cases, so match on it rather than on the tag alone.
71
78
  * return yield* caller.readFile("/missing").pipe(
72
- * Effect.catchTag("FsError", (error) =>
79
+ * Effect.catchTag("VfsError", (error) =>
73
80
  * error.code === "NotFound"
74
81
  * ? Effect.succeed(new Uint8Array())
75
82
  * : Effect.fail(error))
76
83
  * )
77
84
  * })
78
85
  *
79
- * Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))).then(console.log)
86
+ * Effect.runPromise(program).then(console.log)
80
87
  * // Uint8Array(0) []
81
88
  * ```
82
89
  *
83
90
  * @category errors
84
91
  * @since 0.1.0
85
92
  */
86
- export { FsError } from "./VirtualFileSystemError.js";
87
93
  /**
88
94
  * Describes an invalid volume or caller option and names the rejected field.
89
95
  *
90
96
  * @example
91
97
  * ```ts
92
98
  * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
93
- * import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
94
99
  * import { Effect } from "effect"
95
100
  *
96
101
  * // `field` names the rejected option, so the caller learns which key was wrong.
@@ -100,32 +105,31 @@ export { FsError } from "./VirtualFileSystemError.js";
100
105
  *
101
106
  * return yield* Vfs.make(options).pipe(
102
107
  * Effect.as("ok"),
103
- * Effect.catchTag("ConfigurationError", (error) => Effect.succeed(`rejected ${error.field}`))
108
+ * Effect.catchTag("VfsError", (error) => Effect.succeed(`rejected ${error.field}`))
104
109
  * )
105
110
  * })
106
111
  *
107
- * Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))).then(console.log)
112
+ * Effect.runPromise(program).then(console.log)
108
113
  * // rejected maxEntries
109
114
  * ```
110
115
  *
111
116
  * @category errors
112
117
  * @since 0.1.0
113
118
  */
114
- export { ConfigurationError } from "./VirtualFileSystemError.js";
119
+ export { DeltaCode, ImageCode, StoreCode, VfsCode, VfsError } from "./VfsError.js";
115
120
  /**
116
121
  * Schema for a caller's numeric identity, supplementary groups, and explicit privilege.
117
122
  *
118
123
  * @category schemas
119
124
  * @since 0.1.0
120
125
  */
121
- export const Identity = VfsModel.Identity;
126
+ export const Identity = CallerModule.Identity;
122
127
  /**
123
128
  * Schema for root caller credentials and creation mask.
124
129
  *
125
130
  * @example
126
131
  * ```ts
127
132
  * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
128
- * import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
129
133
  * import { Effect } from "effect"
130
134
  *
131
135
  * const program = Effect.gen(function*() {
@@ -146,56 +150,88 @@ export const Identity = VfsModel.Identity;
146
150
  * return ((yield* user.stat("/home/user")).mode & 0o777).toString(8)
147
151
  * })
148
152
  *
149
- * Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))).then(console.log)
153
+ * Effect.runPromise(program).then(console.log)
150
154
  * // 755
151
155
  * ```
152
156
  *
153
157
  * @category schemas
154
158
  * @since 0.1.0
155
159
  */
156
- export const RootCallerOptions = VfsModel.RootCallerOptions;
160
+ export const RootCallerOptions = CallerModule.RootCallerOptions;
157
161
  /**
158
162
  * Schema for the failure boundary survived by an acknowledged volume write.
159
163
  *
160
164
  * @category schemas
161
165
  * @since 0.1.0
162
166
  */
163
- export const VolumeDurability = VfsModel.VolumeDurability;
167
+ export const VolumeDurability = VolumeModule.VolumeDurability;
164
168
  /**
165
169
  * Weakest-to-strongest ordering for volume durability guarantees.
166
170
  *
167
171
  * @category ordering
168
172
  * @since 0.1.0
169
173
  */
170
- export const VolumeDurabilityOrder = VfsModel.VolumeDurabilityOrder;
174
+ export const VolumeDurabilityOrder = VolumeModule.VolumeDurabilityOrder;
171
175
  /**
172
176
  * Returns whether `actual` survives at least the failure boundary required by `required`.
173
177
  *
174
178
  * @category predicates
175
179
  * @since 0.1.0
176
180
  */
177
- export const isVolumeDurabilityAtLeast = VfsModel.isVolumeDurabilityAtLeast;
181
+ export const isVolumeDurabilityAtLeast = VolumeModule.isVolumeDurabilityAtLeast;
178
182
  /**
179
183
  * Schema for the stable logical identity of a volume.
180
184
  *
181
185
  * @category schemas
182
186
  * @since 0.1.0
183
187
  */
184
- export const VolumeIdentity = VfsModel.VolumeIdentity;
188
+ export const VolumeIdentity = VolumeModule.VolumeIdentity;
185
189
  /**
186
190
  * Schema for one uninterrupted storage lifetime of a volume.
187
191
  *
188
192
  * @category schemas
189
193
  * @since 0.1.0
190
194
  */
191
- export const VolumeIncarnation = VfsModel.VolumeIncarnation;
195
+ export const VolumeIncarnation = VolumeModule.VolumeIncarnation;
196
+ /**
197
+ * Schema for the serialisable name of one object in one volume: the volume's
198
+ * identity, the epoch of its inode numbers, the object's inode number, and a
199
+ * tag only the volume can compute, so a key cannot be guessed from another.
200
+ *
201
+ * @example
202
+ * ```ts
203
+ * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
204
+ * import { Effect, Schema } from "effect"
205
+ *
206
+ * const Json = Schema.fromJsonString(Vfs.ReferenceKey)
207
+ *
208
+ * const program = Effect.gen(function*() {
209
+ * const volume = yield* Vfs.make()
210
+ * const caller = yield* volume.caller()
211
+ * yield* caller.writeFile("/f", new Uint8Array([1]), { access: "write", create: "exclusive" })
212
+ *
213
+ * // The key survives a wire; resolving it returns the same reference.
214
+ * const reference = yield* caller.lookup("/f")
215
+ * const text = yield* Schema.encodeEffect(Json)(yield* volume.referenceKey(reference))
216
+ * const key = yield* Schema.decodeEffect(Json)(text)
217
+ *
218
+ * return (yield* volume.resolveReferenceKey(key)) === reference
219
+ * })
220
+ *
221
+ * Effect.runPromise(program).then(console.log)
222
+ * // true
223
+ * ```
224
+ *
225
+ * @category schemas
226
+ * @since 0.6.0
227
+ */
228
+ export const ReferenceKey = VolumeModule.ReferenceKey;
192
229
  /**
193
230
  * Schema for optional volume capacity and path limits.
194
231
  *
195
232
  * @example
196
233
  * ```ts
197
234
  * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
198
- * import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
199
235
  * import { ByteSize, Effect } from "effect"
200
236
  *
201
237
  * // Every limit is optional, and each one is enforced once set.
@@ -208,25 +244,24 @@ export const VolumeIncarnation = VfsModel.VolumeIncarnation;
208
244
  * create: "exclusive"
209
245
  * }).pipe(
210
246
  * Effect.as("written"),
211
- * Effect.catchTag("FsError", (error) => Effect.succeed(error.code))
247
+ * Effect.catchTag("VfsError", (error) => Effect.succeed(error.code))
212
248
  * )
213
249
  * })
214
250
  *
215
- * Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))).then(console.log)
251
+ * Effect.runPromise(program).then(console.log)
216
252
  * // FileTooLarge
217
253
  * ```
218
254
  *
219
255
  * @category schemas
220
256
  * @since 0.1.0
221
257
  */
222
- export const VolumeOptions = VfsModel.VolumeOptions;
258
+ export const VolumeOptions = VolumeModule.VolumeOptions;
223
259
  /**
224
260
  * Schema for filesystem node metadata with bigint inode, size, and nanosecond fields.
225
261
  *
226
262
  * @example
227
263
  * ```ts
228
264
  * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
229
- * import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
230
265
  * import { Effect } from "effect"
231
266
  *
232
267
  * // Sizes and inode numbers are bigint; times are nanosecond timestamps.
@@ -243,49 +278,56 @@ export const VolumeOptions = VfsModel.VolumeOptions;
243
278
  * return [metadata.kind, metadata.size, metadata.nlink]
244
279
  * })
245
280
  *
246
- * Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))).then(console.log)
281
+ * Effect.runPromise(program).then(console.log)
247
282
  * // [ 'file', 3n, 1 ]
248
283
  * ```
249
284
  *
250
285
  * @category schemas
251
286
  * @since 0.1.0
252
287
  */
253
- export const Metadata = VfsModel.Metadata;
288
+ export const Metadata = MetadataModule.Metadata;
289
+ /**
290
+ * Settings for walking a directory tree: the order and the optional depth,
291
+ * entry, and byte bounds.
292
+ *
293
+ * @category schemas
294
+ * @since 0.6.0
295
+ */
296
+ export const WalkOptions = CallerModule.WalkOptions;
254
297
  /**
255
298
  * A directory revision transition captured inside one coordinated operation.
256
299
  *
257
300
  * @category schemas
258
301
  * @since 0.4.0
259
302
  */
260
- export const DirectoryChange = VfsModel.DirectoryChange;
303
+ export const DirectoryChange = CallerModule.DirectoryChange;
261
304
  /**
262
305
  * The directory transitions produced by a same-directory or cross-directory rename.
263
306
  *
264
307
  * @category schemas
265
308
  * @since 0.4.0
266
309
  */
267
- export const RenameReferenceResult = VfsModel.RenameReferenceResult;
310
+ export const RenameReferenceResult = CallerModule.RenameReferenceResult;
268
311
  /**
269
312
  * Schema for an owner update. Omitted fields retain their existing values.
270
313
  *
271
314
  * @category schemas
272
315
  * @since 0.1.0
273
316
  */
274
- export const OwnerUpdate = VfsModel.OwnerUpdate;
317
+ export const OwnerUpdate = MetadataModule.OwnerUpdate;
275
318
  /**
276
319
  * Schema for setting a timestamp to the clock, retaining it, or supplying nanoseconds.
277
320
  *
278
321
  * @category schemas
279
322
  * @since 0.1.0
280
323
  */
281
- export const TimeUpdate = VfsModel.TimeUpdate;
324
+ export const TimeUpdate = MetadataModule.TimeUpdate;
282
325
  /**
283
326
  * Schema for independent access and modification time updates.
284
327
  *
285
328
  * @example
286
329
  * ```ts
287
330
  * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
288
- * import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
289
331
  * import { Effect } from "effect"
290
332
  *
291
333
  * const program = Effect.gen(function*() {
@@ -307,28 +349,27 @@ export const TimeUpdate = VfsModel.TimeUpdate;
307
349
  * return [metadata.atimeNs, metadata.mtimeNs === 1_000n]
308
350
  * })
309
351
  *
310
- * Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))).then(console.log)
352
+ * Effect.runPromise(program).then(console.log)
311
353
  * // [ 1000n, false ]
312
354
  * ```
313
355
  *
314
356
  * @category schemas
315
357
  * @since 0.1.0
316
358
  */
317
- export const Times = VfsModel.Times;
359
+ export const Times = MetadataModule.Times;
318
360
  /**
319
361
  * Schema for file seek origins, including dense-file data and hole queries.
320
362
  *
321
363
  * @category schemas
322
364
  * @since 0.1.0
323
365
  */
324
- export const SeekMode = VfsModel.SeekMode;
366
+ export const SeekMode = FileHandleModule.SeekMode;
325
367
  /**
326
368
  * Schema for file access, creation, append, truncate, and symlink behavior.
327
369
  *
328
370
  * @example
329
371
  * ```ts
330
372
  * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
331
- * import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
332
373
  * import { Effect } from "effect"
333
374
  *
334
375
  * // `access` is required; `create` decides whether a missing file is an error.
@@ -346,36 +387,49 @@ export const SeekMode = VfsModel.SeekMode;
346
387
  * return (yield* handle.stat).size
347
388
  * }).pipe(Effect.scoped)
348
389
  *
349
- * Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))).then(console.log)
390
+ * Effect.runPromise(program).then(console.log)
350
391
  * // 2n
351
392
  * ```
352
393
  *
353
394
  * @category schemas
354
395
  * @since 0.1.0
355
396
  */
356
- export const OpenSettings = VfsModel.OpenSettings;
397
+ export const OpenOptions = CallerModule.OpenOptions;
357
398
  /**
358
399
  * Settings for creating a directory through a parent object reference.
359
400
  * `exactMode` skips the caller's umask for an explicit creation mode; permission policy still applies.
401
+ * `recursive` creates every missing directory on a path in one change, as `mkdir -p` does.
360
402
  *
361
403
  * @category schemas
362
404
  * @since 0.4.0
363
405
  */
364
- export const MkdirReferenceSettings = VfsModel.MkdirReferenceSettings;
406
+ export const MkdirOptions = CallerModule.MkdirOptions;
407
+ /**
408
+ * Settings for removing an entry: whether a directory goes with everything
409
+ * under it, and whether a missing target is success.
410
+ *
411
+ * @category schemas
412
+ * @since 0.6.0
413
+ */
414
+ export const RemoveOptions = CallerModule.RemoveOptions;
365
415
  /**
366
416
  * Settings for creating a symbolic link through a parent object reference.
367
417
  *
368
418
  * @category schemas
369
419
  * @since 0.4.0
370
420
  */
371
- export const SymlinkReferenceSettings = VfsModel.SymlinkReferenceSettings;
421
+ export const SymlinkOptions = CallerModule.SymlinkOptions;
372
422
  /**
373
- * Settings for opening an existing regular-file reference.
423
+ * The attributes `setattr` changes together: a regular file's size, the
424
+ * permission mode, the owner, and the access and modification times. An
425
+ * omitted attribute keeps its current value. `expected` pins the revision the
426
+ * caller last observed and fails as `StaleReference` naming `expected` when
427
+ * the target has moved on.
374
428
  *
375
429
  * @category schemas
376
- * @since 0.4.0
430
+ * @since 0.6.0
377
431
  */
378
- export const OpenReferenceSettings = VfsModel.OpenReferenceSettings;
432
+ export const SetattrOptions = CallerModule.SetattrOptions;
379
433
  /**
380
434
  * Settings for atomically looking up or creating and opening one referenced child.
381
435
  * `exactMode` skips the caller's umask for an explicit creation mode; permission policy still applies.
@@ -383,21 +437,143 @@ export const OpenReferenceSettings = VfsModel.OpenReferenceSettings;
383
437
  * @category schemas
384
438
  * @since 0.4.0
385
439
  */
386
- export const OpenChildReferenceSettings = VfsModel.OpenChildReferenceSettings;
440
+ export const OpenEntryOptions = CallerModule.OpenEntryOptions;
441
+ /**
442
+ * Options for an atomic whole-file write, including replacement and final mode controls.
443
+ *
444
+ * @example
445
+ * ```ts
446
+ * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
447
+ * import { Effect } from "effect"
448
+ *
449
+ * const program = Effect.gen(function*() {
450
+ * const caller = yield* (yield* Vfs.make()).caller()
451
+ * const bytes = new TextEncoder().encode("v2")
452
+ *
453
+ * // `access` is required. `create: "exclusive"` refuses to replace an entry,
454
+ * // while `truncate` overwrites one that already exists.
455
+ * yield* caller.writeFile("/app.conf", new TextEncoder().encode("v1"), {
456
+ * access: "write",
457
+ * create: "exclusive",
458
+ * mode: 0o640
459
+ * })
460
+ *
461
+ * const refused = yield* caller.writeFile("/app.conf", bytes, {
462
+ * access: "write",
463
+ * create: "exclusive"
464
+ * }).pipe(
465
+ * Effect.as("replaced"),
466
+ * Effect.catchTag("VfsError", (error) => Effect.succeed(error.code))
467
+ * )
468
+ *
469
+ * yield* caller.writeFile("/app.conf", bytes, { access: "write", truncate: true })
470
+ *
471
+ * return [refused, new TextDecoder().decode(yield* caller.readFile("/app.conf"))]
472
+ * })
473
+ *
474
+ * Effect.runPromise(program).then(console.log)
475
+ * // [ 'AlreadyExists', 'v2' ]
476
+ * ```
477
+ *
478
+ * @category models
479
+ * @since 0.1.0
480
+ */
481
+ export const WriteFileOptions = CallerModule.WriteFileOptions;
482
+ /**
483
+ * A committed namespace or content change emitted by a volume watch stream.
484
+ *
485
+ * @example
486
+ * ```ts
487
+ * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
488
+ * import { Effect, Fiber, Stream } from "effect"
489
+ *
490
+ * const program = Effect.gen(function*() {
491
+ * const volume = yield* Vfs.make()
492
+ * const caller = yield* volume.caller()
493
+ *
494
+ * const watcher = yield* volume.watch().pipe(
495
+ * Effect.flatMap((stream) => Stream.runCollect(Stream.take(stream, 2))),
496
+ * Effect.forkChild({ startImmediately: true })
497
+ * )
498
+ *
499
+ * yield* caller.mkdir("/logs")
500
+ * yield* caller.rmdir("/logs")
501
+ *
502
+ * // Each event pairs the kind of change with the absolute path it happened to.
503
+ * const events = yield* Fiber.join(watcher)
504
+ *
505
+ * return yield* Effect.forEach(events, (change) =>
506
+ * Effect.map(
507
+ * Vfs.pathToBytes(change.path),
508
+ * (bytes) => `${change._tag} ${new TextDecoder().decode(bytes)}`
509
+ * ))
510
+ * }).pipe(Effect.scoped)
511
+ *
512
+ * Effect.runPromise(program).then(console.log)
513
+ * // [ 'Create /logs', 'Remove /logs' ]
514
+ * ```
515
+ *
516
+ * @category schemas
517
+ * @since 0.1.0
518
+ */
519
+ export const Change = WatchModule.Change;
520
+ /**
521
+ * Schema for the options of `Volume.watch`: an object reference to narrow the
522
+ * watch to, and whether its whole subtree is reported.
523
+ *
524
+ * @example
525
+ * ```ts
526
+ * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
527
+ * import { Effect, Fiber, Stream } from "effect"
528
+ *
529
+ * const program = Effect.gen(function*() {
530
+ * const volume = yield* Vfs.make()
531
+ * const caller = yield* volume.caller()
532
+ * yield* caller.mkdir("/work")
533
+ * const work = yield* caller.lookup("/work")
534
+ *
535
+ * const watcher = yield* volume.watch({ scope: work }).pipe(
536
+ * Effect.flatMap((stream) => Stream.runCollect(Stream.take(stream, 3))),
537
+ * Effect.forkChild({ startImmediately: true })
538
+ * )
539
+ *
540
+ * // Outside the scope, so not reported.
541
+ * yield* caller.mkdir("/elsewhere")
542
+ * // The scope is the directory, not its path: the rename is reported and the watch goes on.
543
+ * yield* caller.rename("/work", "/renamed")
544
+ * yield* caller.mkdir("/renamed/out")
545
+ *
546
+ * const events = yield* Fiber.join(watcher)
547
+ *
548
+ * return yield* Effect.forEach(events, (change) =>
549
+ * Effect.map(
550
+ * Vfs.pathToBytes(change.path),
551
+ * (bytes) => `${change._tag} ${new TextDecoder().decode(bytes)}`
552
+ * ))
553
+ * }).pipe(Effect.scoped)
554
+ *
555
+ * Effect.runPromise(program).then(console.log)
556
+ * // [ 'Remove /work', 'Create /renamed', 'Create /renamed/out' ]
557
+ * ```
558
+ *
559
+ * @category schemas
560
+ * @since 0.6.0
561
+ */
562
+ export const WatchOptions = WatchModule.WatchOptions;
387
563
  /**
388
564
  * Schema for the filesystem entry kinds reported by overlay summaries.
389
565
  *
390
566
  * @category schemas
391
567
  * @since 0.1.0
392
568
  */
393
- export const OverlayNodeKind = VfsModel.OverlayNodeKind;
569
+ export const OverlayNodeKind = VolumeModule.OverlayNodeKind;
394
570
  /**
395
571
  * Schema for observable fields that can differ from an overlay's immutable base.
396
572
  *
397
573
  * @category schemas
398
574
  * @since 0.1.0
399
575
  */
400
- export const OverlayDifference = VfsModel.OverlayDifference;
576
+ export const OverlayDifference = VolumeModule.OverlayDifference;
401
577
  /**
402
578
  * Schema for a final-state overlay difference.
403
579
  *
@@ -409,14 +585,13 @@ export const OverlayDifference = VfsModel.OverlayDifference;
409
585
  * @category schemas
410
586
  * @since 0.1.0
411
587
  */
412
- export const OverlayChange = VfsModel.OverlayChange;
588
+ export const OverlayChange = VolumeModule.OverlayChange;
413
589
  /**
414
590
  * Schema for overlay summary filtering.
415
591
  *
416
592
  * @example
417
593
  * ```ts
418
594
  * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
419
- * import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
420
595
  * import { Effect } from "effect"
421
596
  *
422
597
  * const program = Effect.gen(function*() {
@@ -432,58 +607,118 @@ export const OverlayChange = VfsModel.OverlayChange;
432
607
  * return [plain.length, timed.length]
433
608
  * })
434
609
  *
435
- * Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))).then(console.log)
610
+ * Effect.runPromise(program).then(console.log)
436
611
  * // [ 1, 2 ]
437
612
  * ```
438
613
  *
439
614
  * @category schemas
440
615
  * @since 0.1.0
441
616
  */
442
- export const OverlayChangesOptions = VfsModel.OverlayChangesOptions;
617
+ export const OverlayChangesOptions = VolumeModule.OverlayChangesOptions;
443
618
  /**
444
- * Optional Effect service for providing an existing filesystem caller.
619
+ * The volume an application runs against, as an Effect service. The static
620
+ * layers wrap the explicit constructors; each call builds a fresh layer, so
621
+ * share one layer value to share one volume.
445
622
  *
446
623
  * @example
447
624
  * ```ts
448
625
  * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
449
- * import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
450
626
  * import { Effect } from "effect"
451
627
  *
452
- * // Code depending on the service does not choose the volume it runs against.
453
- * const readVersion = Effect.gen(function*() {
454
- * const caller = yield* Vfs.CurrentFileSystem
628
+ * const program = Effect.gen(function*() {
629
+ * const fs = yield* Vfs.Caller
630
+ * yield* fs.mkdir("/work")
455
631
  *
456
- * return yield* caller.readFile("/version")
632
+ * return (yield* fs.readDirectory("/")).value.map((entry) => new TextDecoder().decode(entry.name))
457
633
  * })
458
634
  *
459
- * const program = Effect.gen(function*() {
460
- * const volume = yield* Vfs.make()
461
- * const caller = yield* volume.caller()
635
+ * Effect.runPromise(program.pipe(Effect.provide(Vfs.Caller.layer()), Effect.provide(Vfs.Volume.layer())))
636
+ * .then(console.log)
637
+ * // [ 'work' ]
638
+ * ```
462
639
  *
463
- * yield* caller.writeFile("/version", new Uint8Array([49]), {
464
- * access: "write",
465
- * create: "exclusive"
640
+ * @category services
641
+ * @since 0.6.0
642
+ */
643
+ export const Volume = Object.assign(Context.Service("@effect-vfs/core/Volume"), {
644
+ layer: (options) => Layer.effect(Volume, VfsModel.make(options)),
645
+ layerFromSnapshot: (snapshot, options) => Layer.effect(Volume, VfsModel.fromSnapshot(snapshot, options)),
646
+ layerFromFixture: (fixture, options) => Layer.effect(Volume, FixtureInternal.fromFixture(fixture, options)),
647
+ layerOverlay: (base, options) => Layer.effect(Volume, VfsModel.makeOverlay(base, options)),
648
+ layerLive: (options) => Layer.effect(Volume, openLiveVolume(options))
649
+ });
650
+ /**
651
+ * The caller an application operates through, as an Effect service. A caller
652
+ * supplied through the service keeps its own volume, credentials, umask and
653
+ * working directory; `layer` makes a root caller on the `Volume` in context.
654
+ *
655
+ * @category services
656
+ * @since 0.6.0
657
+ */
658
+ export const Caller = Object.assign(Context.Service("@effect-vfs/core/Caller"), {
659
+ layer: (options) => Layer.effect(Caller, Effect.flatMap(Volume, (volume) => volume.caller(options)))
660
+ });
661
+ const snapshotBudget = (operation, limits) => Effect.fromResult(decodeConfiguration(BudgetFromDecodeLimits, limits, operation));
662
+ const optionalBudget = (operation, limits) => limits === undefined ? Effect.undefined : snapshotBudget(operation, limits);
663
+ /**
664
+ * Streams a snapshot's version 1 encoding as owned UTF-8 chunks: a header line,
665
+ * then one line per node, each ending in a newline.
666
+ *
667
+ * **Details**
668
+ *
669
+ * The encoding is newline-delimited JSON. The first line names the format and
670
+ * version; each later line is one node in inode order, with names and payloads
671
+ * as canonical base64. Nodes are encoded as the stream is pulled, so writing a
672
+ * large snapshot to a file or a socket never holds its whole encoding. A chunk
673
+ * holds at most 128 lines and 64 KiB, so a pull holds at most that and the next
674
+ * line; a line longer than 64 KiB is a chunk of its own.
675
+ *
676
+ * Given `limits`, the stream fails with `LimitExceeded` at the first line that
677
+ * `decodeSnapshot` would refuse under the same limits, naming the same field,
678
+ * so bytes it completes are known to decode without decoding them.
679
+ *
680
+ * @example
681
+ * ```ts
682
+ * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
683
+ * import { Effect, Stream } from "effect"
684
+ *
685
+ * const program = Effect.gen(function*() {
686
+ * const volume = yield* Vfs.fromFixture({
687
+ * entries: [{ kind: "file", path: "/f", bytes: new Uint8Array([1]) }]
466
688
  * })
467
689
  *
468
- * return yield* readVersion.pipe(Effect.provideService(Vfs.CurrentFileSystem, caller))
690
+ * const text = yield* Vfs.encodeSnapshotStream(yield* volume.snapshot).pipe(
691
+ * Stream.decodeText,
692
+ * Stream.mkString
693
+ * )
694
+ *
695
+ * // A header line, then the root and the file, each line ending in a newline.
696
+ * return text.split("\n").map((line) => (line === "" ? "end" : JSON.parse(line)._tag ?? "header"))
469
697
  * })
470
698
  *
471
- * Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))).then(console.log)
472
- * // Uint8Array(1) [ 49 ]
699
+ * Effect.runPromise(program).then(console.log)
700
+ * // [ 'header', 'directory', 'file', 'end' ]
473
701
  * ```
474
702
  *
475
- * @see The testing guide at `/guides/testing-with-an-isolated-filesystem`.
476
- * @category services
477
- * @since 0.1.0
703
+ * @see {@link encodeSnapshot} for the same bytes collected into one array.
704
+ * @see {@link decodeSnapshotSink} for reading a stream back under limits.
705
+ * @category serialization
706
+ * @since 0.6.0
478
707
  */
479
- export const CurrentFileSystem = Context.Service("@effect-vfs/core/CurrentFileSystem");
708
+ export const encodeSnapshotStream = (snapshot, limits) => Stream.unwrap(Effect.map(optionalBudget("encodeSnapshotStream", limits), (budget) => Image.encodeSnapshotStream(snapshot, budget))).pipe(Stream.mapError((error) => retargetFailure("encodeSnapshotStream", error)));
480
709
  /**
481
- * Encodes a snapshot as owned UTF-8 JSON bytes using the version 1 snapshot format.
710
+ * Encodes a snapshot as owned UTF-8 bytes in the version 1 snapshot format.
711
+ *
712
+ * **Details**
713
+ *
714
+ * The bytes are the chunks of {@link encodeSnapshotStream} collected into one
715
+ * array: newline-delimited JSON, a header line and then one line per node.
716
+ * Given `limits`, encoding fails with `LimitExceeded` wherever `decodeSnapshot`
717
+ * would refuse the bytes under the same limits.
482
718
  *
483
719
  * @example
484
720
  * ```ts
485
721
  * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
486
- * import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
487
722
  * import { Effect } from "effect"
488
723
  *
489
724
  * // Bytes are portable: store or transmit them, then decode under explicit limits.
@@ -498,11 +733,11 @@ export const CurrentFileSystem = Context.Service("@effect-vfs/core/CurrentFileSy
498
733
  *
499
734
  * const bytes = yield* Vfs.encodeSnapshot(yield* volume.snapshot)
500
735
  *
501
- * // The bytes are UTF-8 JSON carrying the version 1 snapshot format.
502
- * return JSON.parse(new TextDecoder().decode(bytes)).version
736
+ * // The first line is the header carrying the version 1 snapshot format.
737
+ * return JSON.parse(new TextDecoder().decode(bytes).split("\n")[0]!).version
503
738
  * })
504
739
  *
505
- * Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))).then(console.log)
740
+ * Effect.runPromise(program).then(console.log)
506
741
  * // 1
507
742
  * ```
508
743
  *
@@ -510,17 +745,75 @@ export const CurrentFileSystem = Context.Service("@effect-vfs/core/CurrentFileSy
510
745
  * @category serialization
511
746
  * @since 0.1.0
512
747
  */
513
- export const encodeSnapshot = Image.encodeSnapshot;
748
+ export const encodeSnapshot = (snapshot, limits) => Effect.flatMap(optionalBudget("encodeSnapshot", limits), (budget) => Image.encodeSnapshot(snapshot, budget)).pipe(Effect.mapError((error) => retargetFailure("encodeSnapshot", error)));
749
+ /**
750
+ * A sink that decodes a stream of version 1 snapshot chunks while enforcing
751
+ * explicit input and payload limits.
752
+ *
753
+ * **Details**
754
+ *
755
+ * Chunks may split lines, and UTF-8 sequences, anywhere. Each line is checked
756
+ * as it ends, so input that breaks a limit or a rule is refused before the
757
+ * sink holds more than one line of it: `maxLineBytes` bounds that line, and
758
+ * the other limits bound what the accepted lines add up to. A line is
759
+ * refused at the byte where it first crosses `maxLineBytes` or
760
+ * `maxEncodedBytes`, so the field a refusal names does not depend on how the
761
+ * input is chunked. Every line must end in a newline, the last included; a
762
+ * carriage return, an empty line, a byte-order mark and malformed UTF-8 are
763
+ * refused as `InvalidEncoding`.
764
+ *
765
+ * @example
766
+ * ```ts
767
+ * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
768
+ * import { ByteSize, Effect, Stream } from "effect"
769
+ *
770
+ * const limits: Vfs.DecodeLimits = {
771
+ * maxEncodedBytes: ByteSize.megabytes(4),
772
+ * maxRecords: 10_000,
773
+ * maxEntries: 10_000,
774
+ * maxDecodedBytes: ByteSize.megabytes(16),
775
+ * maxLineBytes: ByteSize.megabytes(1)
776
+ * }
777
+ *
778
+ * const program = Effect.gen(function*() {
779
+ * const volume = yield* Vfs.fromFixture({
780
+ * entries: [{ kind: "file", path: "/f", bytes: new Uint8Array([1, 2, 3]) }]
781
+ * })
782
+ *
783
+ * // Rechunked to single bytes: the sink joins lines across chunks.
784
+ * const snapshot = yield* Vfs.encodeSnapshotStream(yield* volume.snapshot).pipe(
785
+ * Stream.flattenIterable,
786
+ * Stream.rechunk(1),
787
+ * Stream.map((byte) => new Uint8Array([byte])),
788
+ * Stream.run(Vfs.decodeSnapshotSink(limits))
789
+ * )
790
+ *
791
+ * return yield* (yield* (yield* Vfs.fromSnapshot(snapshot)).caller()).readFile("/f")
792
+ * })
793
+ *
794
+ * Effect.runPromise(program).then(console.log)
795
+ * // Uint8Array(3) [ 1, 2, 3 ]
796
+ * ```
797
+ *
798
+ * @see {@link DecodeLimits} for what each bound protects.
799
+ * @see {@link decodeSnapshot} for decoding bytes already in memory.
800
+ * @category serialization
801
+ * @since 0.6.0
802
+ */
803
+ export const decodeSnapshotSink = (limits) => Sink.unwrap(Effect.map(snapshotBudget("decodeSnapshot", limits), (budget) => Image.decodeSnapshotSink(budget))).pipe(Sink.mapError((error) => retargetFailure("decodeSnapshot", error)));
514
804
  /**
515
805
  * Decodes version 1 snapshot bytes while enforcing explicit input and payload limits.
516
806
  *
807
+ * **Details**
808
+ *
809
+ * The same decoder as {@link decodeSnapshotSink}, run over one chunk.
810
+ *
517
811
  * @example
518
812
  * ```ts
519
813
  * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
520
- * import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
521
814
  * import { ByteSize, Effect } from "effect"
522
815
  *
523
- * // Limits are mandatory: decoding is the boundary where untrusted input arrives.
816
+ * // Limits are mandatory: decoding is the boundary where outside input arrives.
524
817
  * const program = Effect.gen(function*() {
525
818
  * const volume = yield* Vfs.make()
526
819
  * const bytes = yield* Vfs.encodeSnapshot(yield* volume.snapshot)
@@ -532,11 +825,11 @@ export const encodeSnapshot = Image.encodeSnapshot;
532
825
  * maxDecodedBytes: ByteSize.megabytes(16)
533
826
  * }).pipe(
534
827
  * Effect.as("accepted"),
535
- * Effect.catchTag("ImageError", (error) => Effect.succeed([error.code, error.field]))
828
+ * Effect.catchTag("VfsError", (error) => Effect.succeed([error.code, error.field]))
536
829
  * )
537
830
  * })
538
831
  *
539
- * Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))).then(console.log)
832
+ * Effect.runPromise(program).then(console.log)
540
833
  * // [ 'LimitExceeded', 'encodedBytes' ]
541
834
  * ```
542
835
  *
@@ -544,11 +837,8 @@ export const encodeSnapshot = Image.encodeSnapshot;
544
837
  * @category serialization
545
838
  * @since 0.1.0
546
839
  */
547
- export const decodeSnapshot = Image.decodeSnapshot;
548
- const deltaLimits = (limits) => {
549
- const decoded = decodeConfiguration(SnapshotDeltaModel.SnapshotDeltaLimits, limits ?? SnapshotDeltaModel.SnapshotDeltaLimits.default);
550
- return Result.isFailure(decoded) ? Effect.fail(decoded.failure) : Effect.succeed(decoded.success);
551
- };
840
+ export const decodeSnapshot = (input, limits) => Stream.run(Stream.succeed(input), decodeSnapshotSink(limits));
841
+ const deltaLimits = (operation, limits) => Effect.fromResult(decodeConfiguration(DeltaBudgetFromLimits, limits ?? SnapshotDeltaModel.SnapshotDeltaLimits.default, operation));
552
842
  /**
553
843
  * Computes an exact portable delta between two immutable snapshots.
554
844
  * Requires the platform-neutral `Crypto.Crypto` service for base identity. Provide
@@ -587,7 +877,8 @@ const deltaLimits = (limits) => {
587
877
  * @since 0.1.0
588
878
  */
589
879
  export const diffSnapshots = Effect.fn("VirtualFileSystem.diffSnapshots")(function* (base, target, limits) {
590
- return yield* SnapshotDeltaInternal.diffSnapshots(base, target, yield* deltaLimits(limits));
880
+ // The delta codec fails under its own name; the public entry point names itself.
881
+ return yield* SnapshotDeltaInternal.diffSnapshots(base, target, yield* deltaLimits("diffSnapshots", limits)).pipe(Effect.mapError((error) => retargetFailure("diffSnapshots", error)));
591
882
  });
592
883
  /**
593
884
  * Verifies an exact snapshot delta against its base and derives an owned path-oriented summary.
@@ -624,10 +915,8 @@ export const diffSnapshots = Effect.fn("VirtualFileSystem.diffSnapshots")(functi
624
915
  * @since 0.1.0
625
916
  */
626
917
  export const inspectSnapshotDelta = Effect.fn("VirtualFileSystem.inspectSnapshotDelta")(function* (base, delta, options, limits) {
627
- const decoded = decodeConfiguration(SnapshotDeltaModel.SnapshotChangesOptions, options ?? {});
628
- if (Result.isFailure(decoded))
629
- return yield* decoded.failure;
630
- return yield* SnapshotDeltaInternal.inspectSnapshotDelta(base, delta, decoded.success, yield* deltaLimits(limits));
918
+ const decoded = yield* Effect.fromResult(decodeConfiguration(SnapshotDeltaModel.SnapshotChangesOptions, options ?? {}, "inspectSnapshotDelta"));
919
+ return yield* SnapshotDeltaInternal.inspectSnapshotDelta(base, delta, decoded, yield* deltaLimits("inspectSnapshotDelta", limits)).pipe(Effect.mapError((error) => retargetFailure("inspectSnapshotDelta", error)));
631
920
  });
632
921
  /**
633
922
  * Applies an exact delta to its semantically matching base and returns a new snapshot.
@@ -669,7 +958,7 @@ export const inspectSnapshotDelta = Effect.fn("VirtualFileSystem.inspectSnapshot
669
958
  * @since 0.1.0
670
959
  */
671
960
  export const applySnapshotDelta = Effect.fn("VirtualFileSystem.applySnapshotDelta")(function* (base, delta, limits) {
672
- return yield* SnapshotDeltaInternal.applySnapshotDelta(base, delta, yield* deltaLimits(limits));
961
+ return yield* SnapshotDeltaInternal.applySnapshotDelta(base, delta, yield* deltaLimits("applySnapshotDelta", limits)).pipe(Effect.mapError((error) => retargetFailure("applySnapshotDelta", error)));
673
962
  });
674
963
  const deltaSchemaIssue = (cause, input, options) => new SchemaIssue.InvalidValue({ message: `Snapshot delta ${cause.code}${cause.field === undefined ? "" : ` at ${cause.field}`}` }, input, options);
675
964
  /**
@@ -708,7 +997,7 @@ const deltaSchemaIssue = (cause, input, options) => new SchemaIssue.InvalidValue
708
997
  * @since 0.1.0
709
998
  */
710
999
  export const SnapshotDeltaFromBytes = (limits) => {
711
- const selected = Schema.decodeResult(SnapshotDeltaModel.SnapshotDeltaLimits, { onExcessProperty: "error" })(limits ?? SnapshotDeltaModel.SnapshotDeltaLimits.default);
1000
+ const selected = Schema.decodeResult(DeltaBudgetFromLimits, { onExcessProperty: "error" })(limits ?? SnapshotDeltaModel.SnapshotDeltaLimits.default);
712
1001
  return Schema.Uint8Array.pipe(Schema.decodeTo(SnapshotDeltaModel.SnapshotDelta, SchemaTransformation.transformEffect({
713
1002
  decode: (input, options) => Result.isFailure(selected)
714
1003
  ? Effect.fail(new SchemaIssue.InvalidValue({ message: "Invalid snapshot delta limits" }, limits, options))
@@ -737,7 +1026,7 @@ export const SnapshotDeltaFromBytes = (limits) => {
737
1026
  *
738
1027
  * const rejected = yield* Vfs.pathFromBytes(new Uint8Array([47, 0])).pipe(
739
1028
  * Effect.as("accepted"),
740
- * Effect.catchTag("FsError", (error) => Effect.succeed(error.code))
1029
+ * Effect.catchTag("VfsError", (error) => Effect.succeed(error.code))
741
1030
  * )
742
1031
  *
743
1032
  * return [yield* Vfs.pathToBytes(path), rejected]
@@ -768,7 +1057,6 @@ export const pathToBytes = Path.pathToBytes;
768
1057
  * @example
769
1058
  * ```ts
770
1059
  * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
771
- * import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
772
1060
  * import { Effect, Schema } from "effect"
773
1061
  *
774
1062
  * // Fixtures must list every parent directory before its children.
@@ -779,17 +1067,17 @@ export const pathToBytes = Path.pathToBytes;
779
1067
  * }).pipe(
780
1068
  * Effect.flatMap(Vfs.fromFixture),
781
1069
  * Effect.as("built"),
782
- * Effect.catchTag("ImageError", (error) => Effect.succeed(`rejected: ${error.code}`))
1070
+ * Effect.catchTag("VfsError", (error) => Effect.succeed(`rejected: ${error.code}`))
783
1071
  * )
784
1072
  *
785
- * Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))).then(console.log)
1073
+ * Effect.runPromise(program).then(console.log)
786
1074
  * // rejected: InvalidStructure
787
1075
  * ```
788
1076
  *
789
1077
  * @category schemas
790
1078
  * @since 0.1.0
791
1079
  */
792
- export const Fixture = VfsModel.Fixture;
1080
+ export const Fixture = FixtureModule.Fixture;
793
1081
  /**
794
1082
  * Creates a fresh empty volume and captures the current Effect `Clock`.
795
1083
  *
@@ -801,7 +1089,6 @@ export const Fixture = VfsModel.Fixture;
801
1089
  * @example
802
1090
  * ```ts
803
1091
  * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
804
- * import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
805
1092
  * import { Effect } from "effect"
806
1093
  *
807
1094
  * const program = Effect.gen(function*() {
@@ -814,7 +1101,7 @@ export const Fixture = VfsModel.Fixture;
814
1101
  * return yield* caller.readFile("/work/notes.txt")
815
1102
  * })
816
1103
  *
817
- * Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))).then(console.log)
1104
+ * Effect.runPromise(program).then(console.log)
818
1105
  * // Uint8Array(2) [ 104, 105 ]
819
1106
  * ```
820
1107
  *
@@ -823,13 +1110,58 @@ export const Fixture = VfsModel.Fixture;
823
1110
  * @since 0.1.0
824
1111
  */
825
1112
  export const make = VfsModel.make;
1113
+ /**
1114
+ * Streams the tree at `root` in a snapshot as fixture entries, read from the
1115
+ * snapshot itself without restoring a volume.
1116
+ *
1117
+ * **Details**
1118
+ *
1119
+ * `root` resolves as a privileged caller at `/` of a volume restored from the
1120
+ * snapshot with default options would resolve it, following intermediate
1121
+ * symbolic links but not a final one and applying no path byte limit. Entries come in sorted
1122
+ * pre-order, children in the byte order of their names, with paths rooted at
1123
+ * `root`, so the root entry's path is `/`. A path stays a string while every
1124
+ * name on it is UTF-8 and is a `BytePath` otherwise. A file or symbolic link
1125
+ * that more than one name reaches is an entry at its first path and a
1126
+ * `hardLink` to that path at the others. Each file's bytes are an owned copy,
1127
+ * made when the stream pulls its entry. Every entry after the root's is a
1128
+ * fixture entry for a volume holding the same tree.
1129
+ *
1130
+ * @example
1131
+ * ```ts
1132
+ * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
1133
+ * import { Effect, Stream } from "effect"
1134
+ *
1135
+ * const program = Effect.gen(function*() {
1136
+ * const volume = yield* Vfs.fromFixture({
1137
+ * entries: [
1138
+ * { kind: "directory", path: "/src" },
1139
+ * { kind: "file", path: "/src/b.txt", bytes: new Uint8Array([2]) },
1140
+ * { kind: "file", path: "/src/a.txt", bytes: new Uint8Array([1]) },
1141
+ * { kind: "hardLink", path: "/src/c.txt", target: "/src/a.txt" }
1142
+ * ]
1143
+ * })
1144
+ *
1145
+ * const entries = yield* Stream.runCollect(Vfs.snapshotEntries(yield* volume.snapshot, "/src"))
1146
+ *
1147
+ * return entries.map((entry) => `${entry.kind} ${String(entry.path)}`)
1148
+ * })
1149
+ *
1150
+ * Effect.runPromise(program).then(console.log)
1151
+ * // [ 'directory /', 'file /a.txt', 'file /b.txt', 'hardLink /c.txt' ]
1152
+ * ```
1153
+ *
1154
+ * @see {@link fromFixture} for building a volume from the entries.
1155
+ * @category snapshots
1156
+ * @since 0.6.0
1157
+ */
1158
+ export const snapshotEntries = Image.snapshotEntries;
826
1159
  /**
827
1160
  * Restores a fresh volume from an opaque snapshot under the supplied destination limits.
828
1161
  *
829
1162
  * @example
830
1163
  * ```ts
831
1164
  * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
832
- * import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
833
1165
  * import { Effect } from "effect"
834
1166
  *
835
1167
  * const program = Effect.gen(function*() {
@@ -856,7 +1188,7 @@ export const make = VfsModel.make;
856
1188
  * ]
857
1189
  * })
858
1190
  *
859
- * Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))).then(console.log)
1191
+ * Effect.runPromise(program).then(console.log)
860
1192
  * // [ 'one', 'two' ]
861
1193
  * ```
862
1194
  *
@@ -875,13 +1207,12 @@ export const fromSnapshot = VfsModel.fromSnapshot;
875
1207
  * storage. Metadata, namespace state, coordination, handles, and watches are
876
1208
  * always private to the new workspace.
877
1209
  *
878
- * Invalid base snapshots fail with `ImageError`; invalid volume limits fail
879
- * with `ConfigurationError`. Each execution creates a fresh workspace.
1210
+ * Invalid base snapshots and invalid volume limits fail with `VfsError`. Each
1211
+ * execution creates a fresh workspace.
880
1212
  *
881
1213
  * @example
882
1214
  * ```ts
883
1215
  * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
884
- * import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
885
1216
  * import { Effect } from "effect"
886
1217
  *
887
1218
  * const program = Effect.gen(function*() {
@@ -907,7 +1238,7 @@ export const fromSnapshot = VfsModel.fromSnapshot;
907
1238
  * ]
908
1239
  * })
909
1240
  *
910
- * Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))).then(console.log)
1241
+ * Effect.runPromise(program).then(console.log)
911
1242
  * // [ '{}', [ 'Updated' ] ]
912
1243
  * ```
913
1244
  *
@@ -922,13 +1253,12 @@ export const makeOverlay = VfsModel.makeOverlay;
922
1253
  * @example
923
1254
  * ```ts
924
1255
  * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
925
- * import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
926
1256
  * import { Effect } from "effect"
927
1257
  *
928
1258
  * const encoder = new TextEncoder()
929
1259
  *
930
1260
  * const program = Effect.gen(function*() {
931
- * // Parent directories must be listed explicitly, before their children.
1261
+ * // Parent directories must be listed explicitly, in any order.
932
1262
  * const volume = yield* Vfs.fromFixture({
933
1263
  * entries: [
934
1264
  * { kind: "directory", path: "/project" },
@@ -941,11 +1271,14 @@ export const makeOverlay = VfsModel.makeOverlay;
941
1271
  * ]
942
1272
  * })
943
1273
  *
944
- * return yield* (yield* volume.caller()).readDirectory("/project")
1274
+ * // A fixture's directories list their entries in the byte order of their names.
1275
+ * const listing = yield* (yield* volume.caller()).readDirectory("/project")
1276
+ *
1277
+ * return listing.value.map((entry) => new TextDecoder().decode(entry.name))
945
1278
  * })
946
1279
  *
947
- * Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))).then(console.log)
948
- * // [ 'package.json', 'latest' ]
1280
+ * Effect.runPromise(program).then(console.log)
1281
+ * // [ 'latest', 'package.json' ]
949
1282
  * ```
950
1283
  *
951
1284
  * @see The fixtures guide at `/guides/fixtures-and-snapshots`.