@effect-vfs/core 0.3.1 → 0.4.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 (37) hide show
  1. package/README.md +36 -149
  2. package/dist/BytePath.d.ts +2 -1
  3. package/dist/BytePath.d.ts.map +1 -1
  4. package/dist/LiveVolume.d.ts +101 -0
  5. package/dist/LiveVolume.d.ts.map +1 -0
  6. package/dist/LiveVolume.js +72 -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 +119 -29
  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/liveImage.d.ts +286 -0
  20. package/dist/internal/liveImage.d.ts.map +1 -0
  21. package/dist/internal/liveImage.js +176 -0
  22. package/dist/internal/virtualFileSystem/errors.d.ts +4 -23
  23. package/dist/internal/virtualFileSystem/errors.d.ts.map +1 -1
  24. package/dist/internal/virtualFileSystem/errors.js +11 -15
  25. package/dist/internal/virtualFileSystem/fixture.d.ts +4 -1
  26. package/dist/internal/virtualFileSystem/fixture.d.ts.map +1 -1
  27. package/dist/internal/virtualFileSystem/fixture.js +8 -2
  28. package/dist/internal/virtualFileSystem/stagedState.d.ts +22 -0
  29. package/dist/internal/virtualFileSystem/stagedState.d.ts.map +1 -0
  30. package/dist/internal/virtualFileSystem/stagedState.js +62 -0
  31. package/dist/internal/virtualFileSystem/watchHub.d.ts +3 -3
  32. package/dist/internal/virtualFileSystem/watchHub.d.ts.map +1 -1
  33. package/dist/internal/virtualFileSystem/watchHub.js +42 -21
  34. package/dist/internal/virtualFileSystem.d.ts +223 -15
  35. package/dist/internal/virtualFileSystem.d.ts.map +1 -1
  36. package/dist/internal/virtualFileSystem.js +1288 -153
  37. package/package.json +9 -1
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)
@@ -19,7 +16,8 @@ for the exact permission, path, timestamp, quota, and atomicity rules.
19
16
  npm install @effect-vfs/core effect@4.0.0-rc.114
20
17
  ```
21
18
 
22
- Version `0.1.0` targets the exact peer version `effect@4.0.0-rc.114`.
19
+ The package currently requires the exact peer version `effect@4.0.0-rc.114`. The examples also need a `Crypto`
20
+ service; on Node or Bun, install `@effect/platform-node-shared@4.0.0-rc.114` and provide `NodeCrypto.layer`.
23
21
 
24
22
  ## The mental model
25
23
 
@@ -40,6 +38,7 @@ lifetime are explicit values that can be composed in one Effect program.
40
38
 
41
39
  ```ts
42
40
  import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
41
+ import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
43
42
  import { ByteSize, Effect } from "effect"
44
43
 
45
44
  const utf8 = new TextEncoder()
@@ -76,7 +75,7 @@ const program = Effect.scoped(Effect.gen(function*() {
76
75
  }
77
76
  }))
78
77
 
79
- const result = await Effect.runPromise(program)
78
+ const result = await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer)))
80
79
  console.log(result) // { config: { feature: "preview" }, mode: "640" }
81
80
  ```
82
81
 
@@ -94,6 +93,7 @@ contain names which are not valid UTF-8.
94
93
 
95
94
  ```ts
96
95
  import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
96
+ import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
97
97
  import { Effect } from "effect"
98
98
 
99
99
  const program = Effect.gen(function*() {
@@ -112,7 +112,7 @@ const program = Effect.gen(function*() {
112
112
  return { names: names.map((name) => Array.from(name)), roundTrip: Array.from(roundTrip) }
113
113
  })
114
114
 
115
- console.log(await Effect.runPromise(program))
115
+ console.log(await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))))
116
116
  // { names: [[255]], roundTrip: [47, 255] }
117
117
  ```
118
118
 
@@ -128,6 +128,7 @@ resettable sandboxes.
128
128
 
129
129
  ```ts
130
130
  import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
131
+ import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
131
132
  import { Effect } from "effect"
132
133
  import * as ByteSize from "effect/ByteSize"
133
134
 
@@ -165,7 +166,7 @@ const program = Effect.gen(function*() {
165
166
  return new TextDecoder().decode(yield* b.readFile("/project/settings.json"))
166
167
  })
167
168
 
168
- console.log(await Effect.runPromise(program)) // {"theme":"dark"}
169
+ console.log(await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer)))) // {"theme":"dark"}
169
170
  ```
170
171
 
171
172
  Fixture paths must be absolute and unique, and parent directories must be listed explicitly. Fixtures can also contain
@@ -175,140 +176,15 @@ Snapshot decoding requires explicit work limits because encoded bytes may come f
175
176
  contains the reachable namespace and metadata. It excludes callers, open handles, cursor positions, watch
176
177
  subscriptions, and unlinked content. Each restored volume is independent.
177
178
 
178
- ## Branch from one immutable snapshot
179
+ ## Overlays and snapshot deltas
179
180
 
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.
181
+ An overlay creates a writable workspace from an immutable snapshot. Untouched file bytes remain shared until a
182
+ workspace edits them. See the [overlay guide](../../apps/docs/app/content/guides/overlay-filesystems.mdx) for the
183
+ workflow and conflict rules.
184
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
- ```
261
-
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.
265
-
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
- ```
185
+ Snapshot deltas encode exact changes between compatible images. They have separate decode and application limits;
186
+ check those limits before accepting an untrusted delta. See the
187
+ [snapshot delta API](../../apps/docs/app/content/api/core/snapshot-delta.mdx) for operations and defaults.
312
188
 
313
189
  ## Use scoped handles for incremental I/O
314
190
 
@@ -318,6 +194,7 @@ interruption.
318
194
 
319
195
  ```ts
320
196
  import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
197
+ import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
321
198
  import { Effect } from "effect"
322
199
 
323
200
  const program = Effect.scoped(Effect.gen(function*() {
@@ -335,7 +212,7 @@ const program = Effect.scoped(Effect.gen(function*() {
335
212
  return [first, second, preview].map((bytes) => new TextDecoder().decode(bytes))
336
213
  }))
337
214
 
338
- console.log(await Effect.runPromise(program)) // ["one\n", "two\n", "one"]
215
+ console.log(await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer)))) // ["one\n", "two\n", "one"]
339
216
  ```
340
217
 
341
218
  Handles also expose an explicit `close` effect when early release matters. Calling explicit close twice fails, while
@@ -349,6 +226,7 @@ expected failures.
349
226
 
350
227
  ```ts
351
228
  import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
229
+ import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
352
230
  import { Effect } from "effect"
353
231
 
354
232
  const program = Effect.gen(function*() {
@@ -362,7 +240,7 @@ const program = Effect.gen(function*() {
362
240
  )
363
241
  })
364
242
 
365
- const bytes = await Effect.runPromise(program)
243
+ const bytes = await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer)))
366
244
  console.log(new TextDecoder().decode(bytes)) // {}
367
245
  ```
368
246
 
@@ -374,6 +252,7 @@ current directory.
374
252
 
375
253
  ```ts
376
254
  import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
255
+ import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
377
256
  import { Effect } from "effect"
378
257
 
379
258
  const loadConfig = Effect.gen(function*() {
@@ -391,7 +270,7 @@ const program = Effect.gen(function*() {
391
270
  return yield* loadConfig.pipe(Effect.provideService(Vfs.CurrentFileSystem, fs))
392
271
  })
393
272
 
394
- const bytes = await Effect.runPromise(program)
273
+ const bytes = await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer)))
395
274
  console.log(new TextDecoder().decode(bytes)) // {}
396
275
  ```
397
276
 
@@ -410,9 +289,17 @@ candidate.
410
289
  - Components are limited to 255 bytes. Symbolic-link traversal is limited to 40 links.
411
290
  - Omitted logical quotas are unbounded by configuration, apart from fixed file and component bounds.
412
291
  - 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.
292
+ - Operations coordinate through one permit per volume. At most 65 operations are admitted at once by
293
+ default, including the active operation; set `maxPendingOperations` to change the waiting budget.
294
+ Excess work fails with retryable `FsError` code `VolumeBusy`
295
+ before mutation or storage commit. Interruption while waiting makes no change; interruption after a commit
296
+ does not roll it back.
297
+ - Each watch subscriber retains at most 256 events by default; set `maxWatchEvents` to change this bound
298
+ (minimum 2). A subscriber that loses events receives `Rescan` with path `/` after its buffered events.
299
+ Rescan the volume, then continue consuming the watch; repeat the scan if another `Rescan` arrives.
300
+ Delivered events retain their order. The memory `FileSystem.watch` stream fails with a platform error
301
+ identified by `MemoryFileSystem.isWatchOverflow`. Open a new watch before rescanning its path so changes
302
+ during the scan remain observable.
416
303
  - `sync` checks handle liveness. An in-memory volume provides no host or crash durability.
417
304
 
418
305
  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,101 @@
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` until the adapter's durability is qualified.
7
+ *
8
+ * @since 0.4.0
9
+ */
10
+ import { Context, Effect } from "effect";
11
+ import * as ByteSize from "effect/ByteSize";
12
+ import type * as Crypto from "effect/Crypto";
13
+ import type * as PlatformError from "effect/PlatformError";
14
+ import type * as Scope from "effect/Scope";
15
+ import type { ConfigurationError, ImageError, Volume, VolumeOptions } from "./VirtualFileSystem.js";
16
+ /**
17
+ * A storage commit's observed outcome.
18
+ *
19
+ * @since 0.4.0
20
+ */
21
+ export type CommitOutcome = "committed" | "rejected" | "unknown";
22
+ 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 & {
23
+ readonly _tag: "LiveVolumeError";
24
+ } & Readonly<A>;
25
+ /**
26
+ * A failed live-store startup or recovery.
27
+ *
28
+ * @category errors
29
+ * @since 0.4.0
30
+ */
31
+ export declare class LiveVolumeError extends LiveVolumeError_base<{
32
+ readonly code: "InvalidConfiguration" | "Storage" | "Ownership" | "IncompatibleStore" | "CorruptStore";
33
+ readonly cause?: unknown;
34
+ }> {
35
+ }
36
+ declare const LiveImageStore_base: Context.ServiceClass<LiveImageStore, "@effect-vfs/core/LiveImageStore", {
37
+ readonly loadOrCreate: (initial: Uint8Array) => Effect.Effect<Uint8Array, LiveVolumeError>;
38
+ readonly commit: (image: Uint8Array) => Effect.Effect<CommitOutcome>;
39
+ }>;
40
+ /**
41
+ * One exclusively owned store. Its Layer holds the storage resource until the
42
+ * volume has shut down. Providers must atomically replace each image and
43
+ * classify ambiguous commits as `unknown`.
44
+ *
45
+ * @since 0.4.0
46
+ */
47
+ export declare class LiveImageStore extends LiveImageStore_base {
48
+ }
49
+ /**
50
+ * Configuration shared by every live-image provider.
51
+ *
52
+ * @category models
53
+ * @since 0.4.0
54
+ */
55
+ export interface Options {
56
+ readonly maxImageBytes: ByteSize.ByteSize;
57
+ readonly volume: VolumeOptions;
58
+ }
59
+ /**
60
+ * Open a live volume using the supplied store Layer. The caller owns a Scope;
61
+ * the volume shuts down before that Layer releases its storage resource.
62
+ *
63
+ * @since 0.4.0
64
+ */
65
+ export declare const open: (options: Options) => Effect.Effect<Volume, LiveVolumeError, LiveImageStore | Crypto.Crypto | Scope.Scope>;
66
+ /**
67
+ * A live volume and the shutdown action its storage adapter must run before
68
+ * closing the underlying store.
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * import type { ImageSession } from "@effect-vfs/core/LiveVolume"
73
+ *
74
+ * const shutdown = (session: ImageSession) => session.shutdown
75
+ * ```
76
+ *
77
+ * @category models
78
+ * @since 0.4.0
79
+ */
80
+ export interface ImageSession {
81
+ readonly volume: Volume;
82
+ readonly shutdown: Effect.Effect<void>;
83
+ }
84
+ /**
85
+ * Construct a versioned image for a new, empty volume.
86
+ * Store this image before opening it for mutation.
87
+ *
88
+ * @since 0.4.0
89
+ */
90
+ export declare const prepareEmptyImage: (options?: VolumeOptions) => Effect.Effect<Uint8Array, ConfigurationError | ImageError | PlatformError.PlatformError, Crypto.Crypto>;
91
+ /**
92
+ * Open a validated image and stage every mutation before calling `commit`.
93
+ * The returned session contains the volume and a coordinated shutdown effect.
94
+ * Run shutdown before releasing the storage connection. An uncertain commit
95
+ * makes the volume unavailable until it is reopened.
96
+ *
97
+ * @since 0.4.0
98
+ */
99
+ export declare const openImage: (image: Uint8Array, maxImageBytes: ByteSize.ByteSize, commit: (image: Uint8Array) => Effect.Effect<CommitOutcome>) => Effect.Effect<ImageSession, ConfigurationError | ImageError | PlatformError.PlatformError, Crypto.Crypto>;
100
+ export {};
101
+ //# sourceMappingURL=LiveVolume.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LiveVolume.d.ts","sourceRoot":"","sources":["../src/LiveVolume.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;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,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAEnG;;;;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;;2BAUoB,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;;AATtE;;;;;;GAMG;AACH,qBAAa,cAAe,SAAQ,mBAGG;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,KACxD,MAAM,CAAC,MAAM,CAChB,YAAY,EACZ,kBAAkB,GAAG,UAAU,GAAG,aAAa,CAAC,aAAa,EAC7D,MAAM,CAAC,MAAM,CACU,CAAA"}
@@ -0,0 +1,72 @@
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` until the adapter's durability is qualified.
7
+ *
8
+ * @since 0.4.0
9
+ */
10
+ import { Context, Data, Effect } from "effect";
11
+ import * as ByteSize from "effect/ByteSize";
12
+ import * as Model from "./internal/virtualFileSystem.js";
13
+ /**
14
+ * A failed live-store startup or recovery.
15
+ *
16
+ * @category errors
17
+ * @since 0.4.0
18
+ */
19
+ export class LiveVolumeError extends Data.TaggedError("LiveVolumeError") {
20
+ }
21
+ /**
22
+ * One exclusively owned store. Its Layer holds the storage resource until the
23
+ * volume has shut down. Providers must atomically replace each image and
24
+ * classify ambiguous commits as `unknown`.
25
+ *
26
+ * @since 0.4.0
27
+ */
28
+ export class LiveImageStore extends Context.Service()("@effect-vfs/core/LiveImageStore") {
29
+ }
30
+ /**
31
+ * Open a live volume using the supplied store Layer. The caller owns a Scope;
32
+ * the volume shuts down before that Layer releases its storage resource.
33
+ *
34
+ * @since 0.4.0
35
+ */
36
+ export const open = Effect.fn("LiveVolume.open")(function* (options) {
37
+ const store = yield* LiveImageStore;
38
+ const initial = yield* prepareEmptyImage(options.volume).pipe(Effect.mapError((cause) => new LiveVolumeError({ code: "InvalidConfiguration", cause })));
39
+ if (BigInt(initial.length) > ByteSize.toBigInt(options.maxImageBytes)) {
40
+ return yield* new LiveVolumeError({ code: "InvalidConfiguration" });
41
+ }
42
+ const image = yield* store.loadOrCreate(initial);
43
+ const session = yield* openImage(image, options.maxImageBytes, store.commit).pipe(Effect.mapError((cause) => new LiveVolumeError({ code: "CorruptStore", cause })));
44
+ yield* Effect.addFinalizer(() => session.shutdown);
45
+ const volume = session.volume;
46
+ if (volume.limits.maxEntries !== options.volume.maxEntries || volume.limits.maxBytes === undefined ||
47
+ volume.limits.maxPathBytes === undefined || options.volume.maxBytes === undefined ||
48
+ options.volume.maxFileBytes === undefined || options.volume.maxPathBytes === undefined ||
49
+ ByteSize.toBigInt(volume.limits.maxBytes) !== ByteSize.toBigInt(options.volume.maxBytes) ||
50
+ ByteSize.toBigInt(volume.limits.maxFileBytes) !== ByteSize.toBigInt(options.volume.maxFileBytes) ||
51
+ ByteSize.toBigInt(volume.limits.maxPathBytes) !== ByteSize.toBigInt(options.volume.maxPathBytes) ||
52
+ (options.volume.identity !== undefined && options.volume.identity !== volume.identity)) {
53
+ return yield* new LiveVolumeError({ code: "IncompatibleStore" });
54
+ }
55
+ return volume;
56
+ });
57
+ /**
58
+ * Construct a versioned image for a new, empty volume.
59
+ * Store this image before opening it for mutation.
60
+ *
61
+ * @since 0.4.0
62
+ */
63
+ export const prepareEmptyImage = Model.prepareEmptyLiveImage;
64
+ /**
65
+ * Open a validated image and stage every mutation before calling `commit`.
66
+ * The returned session contains the volume and a coordinated shutdown effect.
67
+ * Run shutdown before releasing the storage connection. An uncertain commit
68
+ * makes the volume unavailable until it is reopened.
69
+ *
70
+ * @since 0.4.0
71
+ */
72
+ 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
  *