@effect-vfs/core 0.3.1 → 0.5.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 (58) hide show
  1. package/README.md +41 -150
  2. package/dist/BytePath.d.ts +2 -1
  3. package/dist/BytePath.d.ts.map +1 -1
  4. package/dist/LiveVolume.d.ts +104 -0
  5. package/dist/LiveVolume.d.ts.map +1 -0
  6. package/dist/LiveVolume.js +73 -0
  7. package/dist/Snapshot.d.ts +4 -2
  8. package/dist/Snapshot.d.ts.map +1 -1
  9. package/dist/Snapshot.js +4 -2
  10. package/dist/VirtualFileSystem.d.ts +333 -108
  11. package/dist/VirtualFileSystem.d.ts.map +1 -1
  12. package/dist/VirtualFileSystem.js +122 -32
  13. package/dist/VirtualFileSystemError.d.ts +54 -0
  14. package/dist/VirtualFileSystemError.d.ts.map +1 -0
  15. package/dist/VirtualFileSystemError.js +49 -0
  16. package/dist/index.d.ts +14 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +14 -0
  19. package/dist/internal/errors.d.ts +12 -0
  20. package/dist/internal/errors.d.ts.map +1 -0
  21. package/dist/internal/{virtualFileSystem/errors.js → errors.js} +11 -15
  22. package/dist/internal/{virtualFileSystem/fixture.d.ts → fixture.d.ts} +11 -8
  23. package/dist/internal/fixture.d.ts.map +1 -0
  24. package/dist/internal/{virtualFileSystem/fixture.js → fixture.js} +11 -5
  25. package/dist/internal/liveImage.d.ts +286 -0
  26. package/dist/internal/liveImage.d.ts.map +1 -0
  27. package/dist/internal/liveImage.js +176 -0
  28. package/dist/internal/{virtualFileSystem/overlayContent.d.ts → overlayContent.d.ts} +2 -2
  29. package/dist/internal/overlayContent.d.ts.map +1 -0
  30. package/dist/internal/{virtualFileSystem/overlayContent.js → overlayContent.js} +2 -2
  31. package/dist/internal/{virtualFileSystem/overlayDiff.d.ts → overlayDiff.d.ts} +1 -1
  32. package/dist/internal/overlayDiff.d.ts.map +1 -0
  33. package/dist/internal/{virtualFileSystem/overlayDiff.js → overlayDiff.js} +1 -1
  34. package/dist/internal/{virtualFileSystem/path.d.ts → path.d.ts} +2 -2
  35. package/dist/internal/path.d.ts.map +1 -0
  36. package/dist/internal/{virtualFileSystem/path.js → path.js} +1 -1
  37. package/dist/internal/stagedState.d.ts +22 -0
  38. package/dist/internal/stagedState.d.ts.map +1 -0
  39. package/dist/internal/stagedState.js +62 -0
  40. package/dist/internal/{virtualFileSystem/testHooks.d.ts → testHooks.d.ts} +2 -2
  41. package/dist/internal/testHooks.d.ts.map +1 -0
  42. package/dist/internal/virtualFileSystem.d.ts +224 -16
  43. package/dist/internal/virtualFileSystem.d.ts.map +1 -1
  44. package/dist/internal/virtualFileSystem.js +1294 -159
  45. package/dist/internal/{virtualFileSystem/watchHub.d.ts → watchHub.d.ts} +3 -3
  46. package/dist/internal/watchHub.d.ts.map +1 -0
  47. package/dist/internal/watchHub.js +54 -0
  48. package/package.json +14 -6
  49. package/dist/internal/virtualFileSystem/errors.d.ts +0 -31
  50. package/dist/internal/virtualFileSystem/errors.d.ts.map +0 -1
  51. package/dist/internal/virtualFileSystem/fixture.d.ts.map +0 -1
  52. package/dist/internal/virtualFileSystem/overlayContent.d.ts.map +0 -1
  53. package/dist/internal/virtualFileSystem/overlayDiff.d.ts.map +0 -1
  54. package/dist/internal/virtualFileSystem/path.d.ts.map +0 -1
  55. package/dist/internal/virtualFileSystem/testHooks.d.ts.map +0 -1
  56. package/dist/internal/virtualFileSystem/watchHub.d.ts.map +0 -1
  57. package/dist/internal/virtualFileSystem/watchHub.js +0 -33
  58. /package/dist/internal/{virtualFileSystem/testHooks.js → testHooks.js} +0 -0
package/README.md CHANGED
@@ -1,13 +1,10 @@
1
1
  # @effect-vfs/core
2
2
 
3
- `@effect-vfs/core` is a runtime-neutral virtual filesystem engine for Effect. It gives you isolated in-memory volumes,
4
- byte-preserving paths, POSIX-inspired permissions, logical quotas, change streams, deterministic fixtures, and portable
5
- encoded snapshots without reading from or writing to the host filesystem.
3
+ `@effect-vfs/core` manages in-memory volumes with byte-preserving paths, callers with explicit permissions, quotas,
4
+ watches, fixtures, and portable snapshots. It does not access the host filesystem.
6
5
 
7
- Use it when filesystem state is part of your domain: sandboxing a tool, modeling several users against one namespace,
8
- testing permission behavior, building repeatable fixtures, or saving and restoring an in-memory workspace. If you need
9
- Effect's standard `FileSystem` service, use [`@effect-vfs/memory`](https://www.npmjs.com/package/@effect-vfs/memory),
10
- which adapts this package to that interface.
6
+ Use core when you need direct control over a shared volume or byte-oriented paths. For Effect's standard
7
+ `FileSystem` service, use [`@effect-vfs/memory`](../memory/README.md).
11
8
 
12
9
  The package implements a documented subset of POSIX behavior. It does not claim full POSIX conformance. See the
13
10
  [implemented profile](https://github.com/lloydrichards/effect-virtual-fs/blob/main/.okf/profiles/implemented-filesystem.md)
@@ -16,10 +13,15 @@ for the exact permission, path, timestamp, quota, and atomicity rules.
16
13
  ## Install
17
14
 
18
15
  ```sh
19
- npm install @effect-vfs/core effect@4.0.0-rc.114
16
+ npm install @effect-vfs/core@latest
20
17
  ```
21
18
 
22
- Version `0.1.0` targets the exact peer version `effect@4.0.0-rc.114`.
19
+ The package declares its exact Effect version as a peer dependency. The examples also need a `Crypto` service. On Node
20
+ or Bun, install the matching provider and supply `NodeCrypto.layer`:
21
+
22
+ ```sh
23
+ npm install "@effect/platform-node-shared@$(npm view @effect-vfs/core peerDependencies.effect)"
24
+ ```
23
25
 
24
26
  ## The mental model
25
27
 
@@ -40,6 +42,7 @@ lifetime are explicit values that can be composed in one Effect program.
40
42
 
41
43
  ```ts
42
44
  import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
45
+ import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
43
46
  import { ByteSize, Effect } from "effect"
44
47
 
45
48
  const utf8 = new TextEncoder()
@@ -76,7 +79,7 @@ const program = Effect.scoped(Effect.gen(function*() {
76
79
  }
77
80
  }))
78
81
 
79
- const result = await Effect.runPromise(program)
82
+ const result = await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer)))
80
83
  console.log(result) // { config: { feature: "preview" }, mode: "640" }
81
84
  ```
82
85
 
@@ -94,6 +97,7 @@ contain names which are not valid UTF-8.
94
97
 
95
98
  ```ts
96
99
  import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
100
+ import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
97
101
  import { Effect } from "effect"
98
102
 
99
103
  const program = Effect.gen(function*() {
@@ -112,7 +116,7 @@ const program = Effect.gen(function*() {
112
116
  return { names: names.map((name) => Array.from(name)), roundTrip: Array.from(roundTrip) }
113
117
  })
114
118
 
115
- console.log(await Effect.runPromise(program))
119
+ console.log(await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))))
116
120
  // { names: [[255]], roundTrip: [47, 255] }
117
121
  ```
118
122
 
@@ -128,6 +132,7 @@ resettable sandboxes.
128
132
 
129
133
  ```ts
130
134
  import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
135
+ import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
131
136
  import { Effect } from "effect"
132
137
  import * as ByteSize from "effect/ByteSize"
133
138
 
@@ -165,7 +170,7 @@ const program = Effect.gen(function*() {
165
170
  return new TextDecoder().decode(yield* b.readFile("/project/settings.json"))
166
171
  })
167
172
 
168
- console.log(await Effect.runPromise(program)) // {"theme":"dark"}
173
+ console.log(await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer)))) // {"theme":"dark"}
169
174
  ```
170
175
 
171
176
  Fixture paths must be absolute and unique, and parent directories must be listed explicitly. Fixtures can also contain
@@ -175,140 +180,15 @@ Snapshot decoding requires explicit work limits because encoded bytes may come f
175
180
  contains the reachable namespace and metadata. It excludes callers, open handles, cursor positions, watch
176
181
  subscriptions, and unlinked content. Each restored volume is independent.
177
182
 
178
- ## Branch from one immutable snapshot
179
-
180
- Use `makeOverlay` when several writable workspaces start from the same snapshot. Untouched regular-file contents are
181
- shared internally. A content change copies that whole file for the editing workspace; namespace and metadata state are
182
- always private. Logical quotas still count the complete visible volume, including shared base files and unlinked-open
183
- contents.
184
-
185
- ```ts
186
- import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
187
- import { Effect } from "effect"
188
- import * as ByteSize from "effect/ByteSize"
189
-
190
- const branch = Effect.gen(function*() {
191
- const template = yield* Vfs.fromFixture({
192
- entries: [
193
- { kind: "directory", path: "/project" },
194
- { kind: "file", path: "/project/settings.json", bytes: new Uint8Array() }
195
- ]
196
- })
197
- const base = yield* template.snapshot
198
- const workspace = yield* Vfs.makeOverlay(base, { maxBytes: ByteSize.megabytes(1) })
199
- const fs = yield* workspace.caller()
200
-
201
- yield* fs.rename("/project/settings.json", "/project/preferences.json")
202
- return yield* workspace.capture()
203
- })
204
- ```
205
-
206
- `makeOverlay(base, options)` returns a reusable Effect. Each execution creates a fresh workspace from `base`; it does
207
- not memoize or reset an earlier workspace. Invalid volume options fail with `ConfigurationError`, while failure to
208
- inspect the base snapshot fails with `ImageError`.
209
-
210
- `changes()` reports final differences from the base. It hides timestamp-only differences unless called with
211
- `{ includeTimestamps: true }`. Renames are paired only from retained object identity; equal contents are never treated
212
- as rename evidence. Paths in summaries are `BytePath` values, so inspect them with `pathToBytes` when names may not be
213
- UTF-8. Although `OverlayChange` and its options have schemas, a summary is an in-process value containing opaque
214
- `BytePath` objects. The schema is not a portable summary encoding.
215
-
216
- `capture()` returns a complete version 1 snapshot and matching summary from one committed state. Later writes change
217
- neither result. The snapshot works with `encodeSnapshot`, `fromSnapshot`, and `@effect-vfs/persistence` checkpoints.
218
- Restoring it recovers the complete filesystem state, not the earlier overlay base relationship or summary. To reset,
219
- create a fresh overlay from the base and replace your application reference; existing callers, handles, and watches
220
- remain attached to the old workspace until their own lifetimes end.
221
-
222
- The Effects returned by `changes(options)` and `capture(options)` are also reusable. Each execution observes the
223
- workspace again. Invalid summary options fail with `ConfigurationError`; failure to construct the complete observed
224
- snapshot fails with `ImageError`. Reusing a previously completed `capture()` result does not observe later writes.
225
-
226
- ## Store and apply exact snapshot deltas
227
-
228
- Use `diffSnapshots` when you need a portable description of the exact difference between two immutable snapshots.
229
- The result is an opaque `SnapshotDelta`: inspect it for a path-oriented summary, encode it with Effect Schema, or apply
230
- it to a semantically identical base to reconstruct the target snapshot.
231
-
232
- ```ts
233
- import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
234
- import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
235
- import { Effect, Schema } from "effect"
236
- import * as ByteSize from "effect/ByteSize"
237
-
238
- const Delta = Vfs.SnapshotDeltaFromBytes()
239
-
240
- const program = Effect.gen(function*() {
241
- const baseVolume = yield* Vfs.fromFixture({
242
- entries: [{ kind: "file", path: "/old.txt", bytes: new TextEncoder().encode("before") }]
243
- })
244
- const targetVolume = yield* Vfs.fromFixture({
245
- entries: [{ kind: "file", path: "/new.txt", bytes: new TextEncoder().encode("after") }]
246
- })
247
- const base = yield* baseVolume.snapshot
248
- const target = yield* targetVolume.snapshot
249
-
250
- const delta = yield* Vfs.diffSnapshots(base, target)
251
- const changes = yield* Vfs.inspectSnapshotDelta(base, delta)
252
- const bytes = yield* Schema.encodeEffect(Delta)(delta)
253
- const decoded = yield* Schema.decodeEffect(Delta)(bytes)
254
- const restored = yield* Vfs.applySnapshotDelta(base, decoded)
255
-
256
- return { changes, restored }
257
- })
258
-
259
- await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer)))
260
- ```
183
+ ## Overlays and snapshot deltas
261
184
 
262
- Inspection verifies the delta against its semantic base before reporting what the two snapshots prove. It does not guess renames from equal contents or paths: a move is a
263
- `Removed` change followed by an `Added` change. `Updated` records identify changed semantic fields, including content,
264
- metadata, symbolic-link targets, and hard-link topology. Summaries do not contain replay payloads; the opaque delta does.
185
+ An overlay creates a writable workspace from an immutable snapshot. Untouched file bytes remain shared until a
186
+ workspace edits them. See the [overlay guide](../../apps/docs/app/content/guides/overlay-filesystems.mdx) for the
187
+ workflow and conflict rules.
265
188
 
266
- Each delta records the semantic identity of its required base. Inspecting or applying it against a different valid snapshot fails with a
267
- `SnapshotDeltaError` whose `code` is `BaseMismatch`. Equivalent snapshots are accepted even if their encoded bytes or
268
- internal record identifiers differ. Applying a delta returns another immutable `Snapshot`; call `fromSnapshot` when
269
- you need a writable volume.
270
-
271
- Creation, inspection, encoding, decoding, and application use finite `SnapshotDeltaLimits.default` limits when none are supplied.
272
- Delta creation, inspection, and application require Effect's platform-neutral `Crypto.Crypto` service for SHA-256 base identity.
273
- Provide the official Node, Bun, browser, or Deno crypto layer at the application edge; the core never imports a host
274
- crypto implementation. Memory-sensitive applications can use one policy throughout the workflow:
275
-
276
- ```ts
277
- const limits = Vfs.SnapshotDeltaLimits.constrained
278
- const Delta = Vfs.SnapshotDeltaFromBytes(limits)
279
-
280
- const constrained = Effect.gen(function*() {
281
- const delta = yield* Vfs.diffSnapshots(base, target, limits)
282
- const bytes = yield* Schema.encodeEffect(Delta)(delta)
283
- const decoded = yield* Schema.decodeEffect(Delta)(bytes)
284
- return yield* Vfs.applySnapshotDelta(base, decoded, limits)
285
- })
286
- ```
287
-
288
- Both presets are frozen complete policies. The shipped values are:
289
-
290
- | limit | `constrained` | `default` |
291
- | ----------------------------------- | ------------: | --------: |
292
- | encoded bytes | 2 MiB | 16 MiB |
293
- | canonical identity bytes | 4 MiB | 32 MiB |
294
- | stored records plus summary changes | 6,500 | 50,000 |
295
- | decoded delta bytes | 512 KiB | 8 MiB |
296
- | base records | 6,500 | 50,000 |
297
- | target records | 6,500 | 50,000 |
298
- | namespace entries | 6,500 | 100,000 |
299
- | output records | 6,500 | 50,000 |
300
- | output payload bytes | 256 KiB | 4 MiB |
301
- | inherited records | 6,500 | 50,000 |
302
-
303
- These are conservative finite defaults, not universal workspace-size or memory guarantees. To tune one dimension,
304
- spread a preset into a complete `SnapshotDeltaLimits` value and replace that field.
305
-
306
- ```ts
307
- const limits = {
308
- ...Vfs.SnapshotDeltaLimits.default,
309
- maxOutputBytes: ByteSize.mebibytes(8)
310
- }
311
- ```
189
+ Snapshot deltas encode exact changes between compatible images. They have separate decode and application limits;
190
+ check those limits before accepting an untrusted delta. See the
191
+ [snapshot delta API](../../apps/docs/app/content/api/core/snapshot-delta.mdx) for operations and defaults.
312
192
 
313
193
  ## Use scoped handles for incremental I/O
314
194
 
@@ -318,6 +198,7 @@ interruption.
318
198
 
319
199
  ```ts
320
200
  import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
201
+ import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
321
202
  import { Effect } from "effect"
322
203
 
323
204
  const program = Effect.scoped(Effect.gen(function*() {
@@ -335,7 +216,7 @@ const program = Effect.scoped(Effect.gen(function*() {
335
216
  return [first, second, preview].map((bytes) => new TextDecoder().decode(bytes))
336
217
  }))
337
218
 
338
- console.log(await Effect.runPromise(program)) // ["one\n", "two\n", "one"]
219
+ console.log(await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer)))) // ["one\n", "two\n", "one"]
339
220
  ```
340
221
 
341
222
  Handles also expose an explicit `close` effect when early release matters. Calling explicit close twice fails, while
@@ -349,6 +230,7 @@ expected failures.
349
230
 
350
231
  ```ts
351
232
  import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
233
+ import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
352
234
  import { Effect } from "effect"
353
235
 
354
236
  const program = Effect.gen(function*() {
@@ -362,7 +244,7 @@ const program = Effect.gen(function*() {
362
244
  )
363
245
  })
364
246
 
365
- const bytes = await Effect.runPromise(program)
247
+ const bytes = await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer)))
366
248
  console.log(new TextDecoder().decode(bytes)) // {}
367
249
  ```
368
250
 
@@ -374,6 +256,7 @@ current directory.
374
256
 
375
257
  ```ts
376
258
  import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
259
+ import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
377
260
  import { Effect } from "effect"
378
261
 
379
262
  const loadConfig = Effect.gen(function*() {
@@ -391,7 +274,7 @@ const program = Effect.gen(function*() {
391
274
  return yield* loadConfig.pipe(Effect.provideService(Vfs.CurrentFileSystem, fs))
392
275
  })
393
276
 
394
- const bytes = await Effect.runPromise(program)
277
+ const bytes = await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer)))
395
278
  console.log(new TextDecoder().decode(bytes)) // {}
396
279
  ```
397
280
 
@@ -410,9 +293,17 @@ candidate.
410
293
  - Components are limited to 255 bytes. Symbolic-link traversal is limited to 40 links.
411
294
  - Omitted logical quotas are unbounded by configuration, apart from fixed file and component bounds.
412
295
  - Absolute paths ignore a supplied directory base. Relative paths can use a live, same-volume directory handle.
413
- - Operations coordinate through one permit per volume. Interruption while waiting makes no change; interruption after
414
- a commit does not roll it back.
415
- - Watch streams contain byte paths and report future committed creates, updates, and removals without replay.
296
+ - Operations coordinate through one permit per volume. At most 65 operations are admitted at once by
297
+ default, including the active operation; set `maxPendingOperations` to change the waiting budget.
298
+ Excess work fails with retryable `FsError` code `VolumeBusy`
299
+ before mutation or storage commit. Interruption while waiting makes no change; interruption after a commit
300
+ does not roll it back.
301
+ - Each watch subscriber retains at most 256 events by default; set `maxWatchEvents` to change this bound
302
+ (minimum 2). A subscriber that loses events receives `Rescan` with path `/` after its buffered events.
303
+ Rescan the volume, then continue consuming the watch; repeat the scan if another `Rescan` arrives.
304
+ Delivered events retain their order. The memory `FileSystem.watch` stream fails with a platform error
305
+ identified by `MemoryFileSystem.isWatchOverflow`. Open a new watch before rescanning its path so changes
306
+ during the scan remain observable.
416
307
  - `sync` checks handle liveness. An in-memory volume provides no host or crash durability.
417
308
 
418
309
  For the complete contract, read the
@@ -29,6 +29,7 @@ export type BytePathId = typeof BytePathId;
29
29
  * @example
30
30
  * ```ts
31
31
  * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
32
+ * import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
32
33
  * import { Effect } from "effect"
33
34
  *
34
35
  * // A name that is not valid UTF-8 still round-trips exactly.
@@ -45,7 +46,7 @@ export type BytePathId = typeof BytePathId;
45
46
  * return yield* caller.readDirectoryBytes("/")
46
47
  * })
47
48
  *
48
- * Effect.runPromise(program).then(console.log)
49
+ * Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))).then(console.log)
49
50
  * // [ Uint8Array(2) [ 255, 254 ] ]
50
51
  * ```
51
52
  *
@@ -1 +1 @@
1
- {"version":3,"file":"BytePath.d.ts","sourceRoot":"","sources":["../src/BytePath.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAA;AAC1C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC/C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAGvC;;;;;GAKG;AACH,eAAO,MAAM,UAAU,EAAE,2BAAiD,CAAA;AAE1E;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,CAAA;AAE1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,WAAW,QAAS,SAAQ,KAAK,CAAC,KAAK,EAAE,QAAQ;IACrD,QAAQ,CAAC,CAAC,UAAU,CAAC,EAAE,UAAU,CAAA;CAClC;AAED;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,oCAAgD,CAAA"}
1
+ {"version":3,"file":"BytePath.d.ts","sourceRoot":"","sources":["../src/BytePath.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAA;AAC1C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC/C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAGvC;;;;;GAKG;AACH,eAAO,MAAM,UAAU,EAAE,2BAAiD,CAAA;AAE1E;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,CAAA;AAE1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,WAAW,QAAS,SAAQ,KAAK,CAAC,KAAK,EAAE,QAAQ;IACrD,QAAQ,CAAC,CAAC,UAAU,CAAC,EAAE,UAAU,CAAA;CAClC;AAED;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,oCAAgD,CAAA"}
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Image boundary for a storage adapter that commits each live volume mutation.
3
+ *
4
+ * An adapter must preserve the supplied bytes atomically and classify each
5
+ * commit as confirmed, definitely rejected, or uncertain. The opened volume
6
+ * stays at `memory-only` unless the adapter explicitly supplies a qualified
7
+ * durability tier.
8
+ *
9
+ * @since 0.4.0
10
+ */
11
+ import { Context, Effect } from "effect";
12
+ import * as ByteSize from "effect/ByteSize";
13
+ import type * as Crypto from "effect/Crypto";
14
+ import type * as PlatformError from "effect/PlatformError";
15
+ import type * as Scope from "effect/Scope";
16
+ import type { ConfigurationError, ImageError, Volume, VolumeDurability, VolumeOptions } from "./VirtualFileSystem.js";
17
+ /**
18
+ * A storage commit's observed outcome.
19
+ *
20
+ * @since 0.4.0
21
+ */
22
+ export type CommitOutcome = "committed" | "rejected" | "unknown";
23
+ declare const LiveVolumeError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
24
+ readonly _tag: "LiveVolumeError";
25
+ } & Readonly<A>;
26
+ /**
27
+ * A failed live-store startup or recovery.
28
+ *
29
+ * @category errors
30
+ * @since 0.4.0
31
+ */
32
+ export declare class LiveVolumeError extends LiveVolumeError_base<{
33
+ readonly code: "InvalidConfiguration" | "Storage" | "Ownership" | "IncompatibleStore" | "CorruptStore";
34
+ readonly cause?: unknown;
35
+ }> {
36
+ }
37
+ declare const LiveImageStore_base: Context.ServiceClass<LiveImageStore, "@effect-vfs/core/LiveImageStore", {
38
+ /** Storage guarantee for successful commits; omission means memory-only. */
39
+ readonly durability?: VolumeDurability;
40
+ readonly loadOrCreate: (initial: Uint8Array) => Effect.Effect<Uint8Array, LiveVolumeError>;
41
+ readonly commit: (image: Uint8Array) => Effect.Effect<CommitOutcome>;
42
+ }>;
43
+ /**
44
+ * One exclusively owned store. Its Layer holds the storage resource until the
45
+ * volume has shut down. Providers must atomically replace each image and
46
+ * classify ambiguous commits as `unknown`.
47
+ *
48
+ * @since 0.4.0
49
+ */
50
+ export declare class LiveImageStore extends LiveImageStore_base {
51
+ }
52
+ /**
53
+ * Configuration shared by every live-image provider.
54
+ *
55
+ * @category models
56
+ * @since 0.4.0
57
+ */
58
+ export interface Options {
59
+ readonly maxImageBytes: ByteSize.ByteSize;
60
+ readonly volume: VolumeOptions;
61
+ }
62
+ /**
63
+ * Open a live volume using the supplied store Layer. The caller owns a Scope;
64
+ * the volume shuts down before that Layer releases its storage resource.
65
+ *
66
+ * @since 0.4.0
67
+ */
68
+ export declare const open: (options: Options) => Effect.Effect<Volume, LiveVolumeError, LiveImageStore | Crypto.Crypto | Scope.Scope>;
69
+ /**
70
+ * A live volume and the shutdown action its storage adapter must run before
71
+ * closing the underlying store.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * import type { ImageSession } from "@effect-vfs/core/LiveVolume"
76
+ *
77
+ * const shutdown = (session: ImageSession) => session.shutdown
78
+ * ```
79
+ *
80
+ * @category models
81
+ * @since 0.4.0
82
+ */
83
+ export interface ImageSession {
84
+ readonly volume: Volume;
85
+ readonly shutdown: Effect.Effect<void>;
86
+ }
87
+ /**
88
+ * Construct a versioned image for a new, empty volume.
89
+ * Store this image before opening it for mutation.
90
+ *
91
+ * @since 0.4.0
92
+ */
93
+ export declare const prepareEmptyImage: (options?: VolumeOptions) => Effect.Effect<Uint8Array, ConfigurationError | ImageError | PlatformError.PlatformError, Crypto.Crypto>;
94
+ /**
95
+ * Open a validated image and stage every mutation before calling `commit`.
96
+ * The returned session contains the volume and a coordinated shutdown effect.
97
+ * Run shutdown before releasing the storage connection. An uncertain commit
98
+ * makes the volume unavailable until it is reopened.
99
+ *
100
+ * @since 0.4.0
101
+ */
102
+ export declare const openImage: (image: Uint8Array, maxImageBytes: ByteSize.ByteSize, commit: (image: Uint8Array) => Effect.Effect<CommitOutcome>, durability?: VolumeDurability) => Effect.Effect<ImageSession, ConfigurationError | ImageError | PlatformError.PlatformError, Crypto.Crypto>;
103
+ export {};
104
+ //# sourceMappingURL=LiveVolume.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LiveVolume.d.ts","sourceRoot":"","sources":["../src/LiveVolume.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,OAAO,EAAQ,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC9C,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAA;AAC3C,OAAO,KAAK,KAAK,MAAM,MAAM,eAAe,CAAA;AAC5C,OAAO,KAAK,KAAK,aAAa,MAAM,sBAAsB,CAAA;AAC1D,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAA;AAE1C,OAAO,KAAK,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAErH;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,CAAA;;;;AAEhE;;;;;GAKG;AACH,qBAAa,eAAgB,SAAQ,qBAAoC;IACvE,QAAQ,CAAC,IAAI,EAAE,sBAAsB,GAAG,SAAS,GAAG,WAAW,GAAG,mBAAmB,GAAG,cAAc,CAAA;IACtG,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CACzB,CAAC;CAAG;;IAUH,4EAA4E;0BACtD,gBAAgB;2BACf,CAAC,OAAO,EAAE,UAAU,KAAK,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,eAAe,CAAC;qBACzE,CAAC,KAAK,EAAE,UAAU,KAAK,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC;;AAXtE;;;;;;GAMG;AACH,qBAAa,cAAe,SAAQ,mBAKG;CAAG;AAE1C;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,aAAa,EAAE,QAAQ,CAAC,QAAQ,CAAA;IACzC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAA;CAC/B;AAED;;;;;GAKG;AACH,eAAO,MAAM,IAAI,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,CACpD,MAAM,EACN,eAAe,EACf,cAAc,GAAG,MAAM,CAAC,MAAM,GAAG,KAAK,CAAC,KAAK,CAkC5C,CAAA;AAEF;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;CACvC;AAED;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,CAAC,OAAO,CAAC,EAAE,aAAa,KAAK,MAAM,CAAC,MAAM,CACxE,UAAU,EACV,kBAAkB,GAAG,UAAU,GAAG,aAAa,CAAC,aAAa,EAC7D,MAAM,CAAC,MAAM,CACgB,CAAA;AAE/B;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,EAAE,CACtB,KAAK,EAAE,UAAU,EACjB,aAAa,EAAE,QAAQ,CAAC,QAAQ,EAChC,MAAM,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,EAC3D,UAAU,CAAC,EAAE,gBAAgB,KAC1B,MAAM,CAAC,MAAM,CAChB,YAAY,EACZ,kBAAkB,GAAG,UAAU,GAAG,aAAa,CAAC,aAAa,EAC7D,MAAM,CAAC,MAAM,CACU,CAAA"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Image boundary for a storage adapter that commits each live volume mutation.
3
+ *
4
+ * An adapter must preserve the supplied bytes atomically and classify each
5
+ * commit as confirmed, definitely rejected, or uncertain. The opened volume
6
+ * stays at `memory-only` unless the adapter explicitly supplies a qualified
7
+ * durability tier.
8
+ *
9
+ * @since 0.4.0
10
+ */
11
+ import { Context, Data, Effect } from "effect";
12
+ import * as ByteSize from "effect/ByteSize";
13
+ import * as Model from "./internal/virtualFileSystem.js";
14
+ /**
15
+ * A failed live-store startup or recovery.
16
+ *
17
+ * @category errors
18
+ * @since 0.4.0
19
+ */
20
+ export class LiveVolumeError extends Data.TaggedError("LiveVolumeError") {
21
+ }
22
+ /**
23
+ * One exclusively owned store. Its Layer holds the storage resource until the
24
+ * volume has shut down. Providers must atomically replace each image and
25
+ * classify ambiguous commits as `unknown`.
26
+ *
27
+ * @since 0.4.0
28
+ */
29
+ export class LiveImageStore extends Context.Service()("@effect-vfs/core/LiveImageStore") {
30
+ }
31
+ /**
32
+ * Open a live volume using the supplied store Layer. The caller owns a Scope;
33
+ * the volume shuts down before that Layer releases its storage resource.
34
+ *
35
+ * @since 0.4.0
36
+ */
37
+ export const open = Effect.fn("LiveVolume.open")(function* (options) {
38
+ const store = yield* LiveImageStore;
39
+ const initial = yield* prepareEmptyImage(options.volume).pipe(Effect.mapError((cause) => new LiveVolumeError({ code: "InvalidConfiguration", cause })));
40
+ if (BigInt(initial.length) > ByteSize.toBigInt(options.maxImageBytes)) {
41
+ return yield* new LiveVolumeError({ code: "InvalidConfiguration" });
42
+ }
43
+ const image = yield* store.loadOrCreate(initial);
44
+ const session = yield* openImage(image, options.maxImageBytes, store.commit, store.durability ?? "memory-only").pipe(Effect.mapError((cause) => new LiveVolumeError({ code: "CorruptStore", cause })));
45
+ yield* Effect.addFinalizer(() => session.shutdown);
46
+ const volume = session.volume;
47
+ if (volume.limits.maxEntries !== options.volume.maxEntries || volume.limits.maxBytes === undefined ||
48
+ volume.limits.maxPathBytes === undefined || options.volume.maxBytes === undefined ||
49
+ options.volume.maxFileBytes === undefined || options.volume.maxPathBytes === undefined ||
50
+ ByteSize.toBigInt(volume.limits.maxBytes) !== ByteSize.toBigInt(options.volume.maxBytes) ||
51
+ ByteSize.toBigInt(volume.limits.maxFileBytes) !== ByteSize.toBigInt(options.volume.maxFileBytes) ||
52
+ ByteSize.toBigInt(volume.limits.maxPathBytes) !== ByteSize.toBigInt(options.volume.maxPathBytes) ||
53
+ (options.volume.identity !== undefined && options.volume.identity !== volume.identity)) {
54
+ return yield* new LiveVolumeError({ code: "IncompatibleStore" });
55
+ }
56
+ return volume;
57
+ });
58
+ /**
59
+ * Construct a versioned image for a new, empty volume.
60
+ * Store this image before opening it for mutation.
61
+ *
62
+ * @since 0.4.0
63
+ */
64
+ export const prepareEmptyImage = Model.prepareEmptyLiveImage;
65
+ /**
66
+ * Open a validated image and stage every mutation before calling `commit`.
67
+ * The returned session contains the volume and a coordinated shutdown effect.
68
+ * Run shutdown before releasing the storage connection. An uncertain commit
69
+ * makes the volume unavailable until it is reopened.
70
+ *
71
+ * @since 0.4.0
72
+ */
73
+ export const openImage = Model.openImageVolume;
@@ -31,6 +31,7 @@ declare const ImageError_base: new <A extends Record<string, any> = {}>(args: im
31
31
  * @example
32
32
  * ```ts
33
33
  * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
34
+ * import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
34
35
  * import { ByteSize, Effect } from "effect"
35
36
  *
36
37
  * // Decoding is a trust boundary. `code` says what went wrong and `field` says
@@ -53,7 +54,7 @@ declare const ImageError_base: new <A extends Record<string, any> = {}>(args: im
53
54
  * )
54
55
  * })
55
56
  *
56
- * Effect.runPromise(program).then(console.log)
57
+ * Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))).then(console.log)
57
58
  * // declined at encodedBytes
58
59
  * ```
59
60
  *
@@ -73,6 +74,7 @@ export declare class ImageError extends ImageError_base<{
73
74
  * @example
74
75
  * ```ts
75
76
  * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
77
+ * import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
76
78
  * import { ByteSize, Effect } from "effect"
77
79
  *
78
80
  * // All four are required: each bounds a different resource. `maxEncodedBytes`
@@ -93,7 +95,7 @@ export declare class ImageError extends ImageError_base<{
93
95
  * return "accepted"
94
96
  * })
95
97
  *
96
- * Effect.runPromise(program).then(console.log)
98
+ * Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))).then(console.log)
97
99
  * // accepted
98
100
  * ```
99
101
  *
@@ -1 +1 @@
1
- {"version":3,"file":"Snapshot.d.ts","sourceRoot":"","sources":["../src/Snapshot.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC;;;;;GAKG;AACH,eAAO,MAAM,cAAc,EAAG,2BAAoC,CAAA;AAElE;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,cAAc,CAAA;AAElD;;;;;GAKG;AACH,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,CAAC,cAAc,CAAC,EAAE,cAAc,CAAA;CAC1C;;;;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,qBAAa,UAAW,SAAQ,gBAA+B;IAC7D,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,iBAAiB,GAAG,oBAAoB,GAAG,kBAAkB,GAAG,eAAe,CAAA;IAC9F,kFAAkF;IAClF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CACxB,CAAC;CAAG;AAEL;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,YAAY;IACvB,sDAAsD;;IAEtD,6DAA6D;;IAE7D,2CAA2C;;IAE3C,6CAA6C;;EAE7C,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,OAAO,YAAY,CAAC,IAAI,CAAA"}
1
+ {"version":3,"file":"Snapshot.d.ts","sourceRoot":"","sources":["../src/Snapshot.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC;;;;;GAKG;AACH,eAAO,MAAM,cAAc,EAAG,2BAAoC,CAAA;AAElE;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,cAAc,CAAA;AAElD;;;;;GAKG;AACH,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,CAAC,cAAc,CAAC,EAAE,cAAc,CAAA;CAC1C;;;;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,qBAAa,UAAW,SAAQ,gBAA+B;IAC7D,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,iBAAiB,GAAG,oBAAoB,GAAG,kBAAkB,GAAG,eAAe,CAAA;IAC9F,kFAAkF;IAClF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CACxB,CAAC;CAAG;AAEL;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,eAAO,MAAM,YAAY;IACvB,sDAAsD;;IAEtD,6DAA6D;;IAE7D,2CAA2C;;IAE3C,6CAA6C;;EAE7C,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,OAAO,YAAY,CAAC,IAAI,CAAA"}
package/dist/Snapshot.js CHANGED
@@ -22,6 +22,7 @@ export const SnapshotTypeId = "@effect-vfs/core/Snapshot";
22
22
  * @example
23
23
  * ```ts
24
24
  * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
25
+ * import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
25
26
  * import { ByteSize, Effect } from "effect"
26
27
  *
27
28
  * // Decoding is a trust boundary. `code` says what went wrong and `field` says
@@ -44,7 +45,7 @@ export const SnapshotTypeId = "@effect-vfs/core/Snapshot";
44
45
  * )
45
46
  * })
46
47
  *
47
- * Effect.runPromise(program).then(console.log)
48
+ * Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))).then(console.log)
48
49
  * // declined at encodedBytes
49
50
  * ```
50
51
  *
@@ -59,6 +60,7 @@ export class ImageError extends Data.TaggedError("ImageError") {
59
60
  * @example
60
61
  * ```ts
61
62
  * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
63
+ * import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
62
64
  * import { ByteSize, Effect } from "effect"
63
65
  *
64
66
  * // All four are required: each bounds a different resource. `maxEncodedBytes`
@@ -79,7 +81,7 @@ export class ImageError extends Data.TaggedError("ImageError") {
79
81
  * return "accepted"
80
82
  * })
81
83
  *
82
- * Effect.runPromise(program).then(console.log)
84
+ * Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))).then(console.log)
83
85
  * // accepted
84
86
  * ```
85
87
  *