@effect-vfs/core 0.7.1 → 0.9.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 (79) hide show
  1. package/README.md +21 -235
  2. package/dist/Caller.d.ts +2 -0
  3. package/dist/Caller.d.ts.map +1 -1
  4. package/dist/Caller.js +3 -1
  5. package/dist/LiveVolume.d.ts +69 -1
  6. package/dist/LiveVolume.d.ts.map +1 -1
  7. package/dist/LiveVolume.js +89 -12
  8. package/dist/Search.d.ts +730 -0
  9. package/dist/Search.d.ts.map +1 -0
  10. package/dist/Search.js +576 -0
  11. package/dist/SnapshotDelta.d.ts +234 -0
  12. package/dist/SnapshotDelta.d.ts.map +1 -1
  13. package/dist/SnapshotDelta.js +107 -0
  14. package/dist/VfsError.d.ts +3 -1
  15. package/dist/VfsError.d.ts.map +1 -1
  16. package/dist/VfsError.js +3 -1
  17. package/dist/VirtualFileSystem.d.ts +78 -2
  18. package/dist/VirtualFileSystem.d.ts.map +1 -1
  19. package/dist/VirtualFileSystem.js +68 -4
  20. package/dist/Watch.d.ts +18 -0
  21. package/dist/Watch.d.ts.map +1 -1
  22. package/dist/Watch.js +12 -0
  23. package/dist/index.d.ts +7 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +7 -0
  26. package/dist/internal/callerWatch.d.ts +32 -0
  27. package/dist/internal/callerWatch.d.ts.map +1 -0
  28. package/dist/internal/callerWatch.js +184 -0
  29. package/dist/internal/confinement.d.ts +16 -0
  30. package/dist/internal/confinement.d.ts.map +1 -0
  31. package/dist/internal/confinement.js +79 -0
  32. package/dist/internal/directoryEntries.d.ts +6 -0
  33. package/dist/internal/directoryEntries.d.ts.map +1 -0
  34. package/dist/internal/directoryEntries.js +79 -0
  35. package/dist/internal/fixture.js +4 -4
  36. package/dist/internal/glob.d.ts +66 -0
  37. package/dist/internal/glob.d.ts.map +1 -0
  38. package/dist/internal/glob.js +248 -0
  39. package/dist/internal/image.d.ts +7 -1
  40. package/dist/internal/image.d.ts.map +1 -1
  41. package/dist/internal/image.js +9 -7
  42. package/dist/internal/liveImage.d.ts +1 -1
  43. package/dist/internal/liveImage.d.ts.map +1 -1
  44. package/dist/internal/liveImage.js +2 -10
  45. package/dist/internal/path.js +2 -2
  46. package/dist/internal/resolution.d.ts +139 -0
  47. package/dist/internal/resolution.d.ts.map +1 -0
  48. package/dist/internal/resolution.js +332 -0
  49. package/dist/internal/search.d.ts +24 -0
  50. package/dist/internal/search.d.ts.map +1 -0
  51. package/dist/internal/search.js +78 -0
  52. package/dist/internal/searchContent.d.ts +77 -0
  53. package/dist/internal/searchContent.d.ts.map +1 -0
  54. package/dist/internal/searchContent.js +223 -0
  55. package/dist/internal/searchModel.d.ts +83 -0
  56. package/dist/internal/searchModel.d.ts.map +1 -0
  57. package/dist/internal/searchModel.js +23 -0
  58. package/dist/internal/snapshotDelta.d.ts +470 -1
  59. package/dist/internal/snapshotDelta.d.ts.map +1 -1
  60. package/dist/internal/snapshotDelta.js +59 -25
  61. package/dist/internal/snapshotMerge.d.ts +54 -0
  62. package/dist/internal/snapshotMerge.d.ts.map +1 -0
  63. package/dist/internal/snapshotMerge.js +959 -0
  64. package/dist/internal/tokenRegistry.d.ts +83 -0
  65. package/dist/internal/tokenRegistry.d.ts.map +1 -0
  66. package/dist/internal/tokenRegistry.js +146 -0
  67. package/dist/internal/virtualFileSystem.d.ts +32 -17
  68. package/dist/internal/virtualFileSystem.d.ts.map +1 -1
  69. package/dist/internal/virtualFileSystem.js +503 -736
  70. package/dist/internal/volumeLimits.d.ts +53 -0
  71. package/dist/internal/volumeLimits.d.ts.map +1 -0
  72. package/dist/internal/volumeLimits.js +63 -0
  73. package/dist/internal/volumeState.d.ts +2 -0
  74. package/dist/internal/volumeState.d.ts.map +1 -1
  75. package/dist/internal/volumeState.js +9 -0
  76. package/dist/internal/watchHub.d.ts +6 -2
  77. package/dist/internal/watchHub.d.ts.map +1 -1
  78. package/dist/internal/watchHub.js +31 -6
  79. package/package.json +9 -3
package/README.md CHANGED
@@ -16,29 +16,17 @@ for the exact permission, path, timestamp, quota, and atomicity rules.
16
16
  npm install @effect-vfs/core@latest
17
17
  ```
18
18
 
19
- The package declares its exact Effect version as a peer dependency. Volume constructors and snapshot delta functions require Effect’s `Crypto` service. On Node, install the matching
19
+ The package declares its exact Effect version as a peer dependency. Volume constructors and snapshot delta functions require Effect's `Crypto` service. On Node, install the matching
20
20
  provider and supply `NodeCrypto.layer` when running these effects:
21
21
 
22
22
  ```sh
23
23
  npm install "@effect/platform-node-shared@$(npm view @effect-vfs/core peerDependencies.effect)"
24
24
  ```
25
25
 
26
- ## The mental model
27
-
28
- The API has three levels:
29
-
30
- - A `Volume` owns one isolated namespace and its file contents.
31
- - A `Caller` accesses that volume with its own identity, umask, and current directory.
32
- - File and directory handles are scoped capabilities. Effect closes them when their scope ends.
33
-
34
- Callers created from the same volume see the same files. A new volume starts with independent state. This separation
35
- lets you model access by several users without reaching for process globals or the host filesystem.
36
-
37
26
  ## Create an isolated workspace
38
27
 
39
- This example creates a bounded workspace, gives an unprivileged caller access to one directory, and uses a scoped file
40
- handle to read the result. It demonstrates the main benefit of the core API: storage, credentials, limits, and resource
41
- lifetime are explicit values that can be composed in one Effect program.
28
+ A volume owns the files. A caller supplies identity, permissions, and a working directory. Scoped handles close when
29
+ their Effect scope ends. This example gives an unprivileged caller access to one directory in a bounded volume.
42
30
 
43
31
  ```ts
44
32
  import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
@@ -89,230 +77,28 @@ callers default to umask `0o022`; the developer's `0o027` mask turns the request
89
77
  The root package exports `VirtualFileSystem` as a namespace. The equivalent direct module import is
90
78
  `import * as Vfs from "@effect-vfs/core/VirtualFileSystem"`.
91
79
 
92
- ## Preserve path bytes exactly
93
-
94
- JavaScript strings cannot represent every filename allowed by a byte-oriented filesystem. `BytePath` keeps arbitrary
95
- non-NUL path bytes intact. This matters when reproducing archives, protocol fixtures, or Unix directory trees that
96
- contain names which are not valid UTF-8.
97
-
98
- ```ts
99
- import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
100
- import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
101
- import { Effect } from "effect"
102
-
103
- const program = Effect.gen(function*() {
104
- const volume = yield* Vfs.make()
105
- const fs = yield* volume.caller()
106
-
107
- // Absolute path whose final component is the single byte 0xff.
108
- const opaquePath = yield* Vfs.pathFromBytes(new Uint8Array([0x2f, 0xff]))
109
- yield* fs.writeFile(opaquePath, new Uint8Array([1, 2, 3]), {
110
- access: "write",
111
- create: "exclusive"
112
- })
113
-
114
- const listing = yield* fs.readDirectory("/")
115
- const roundTrip = yield* Vfs.pathToBytes(opaquePath)
116
- return { names: listing.value.map((entry) => Array.from(entry.name)), roundTrip: Array.from(roundTrip) }
117
- })
118
-
119
- console.log(await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))))
120
- // { names: [[255]], roundTrip: [47, 255] }
121
- ```
122
-
123
- The constructors and byte-returning operations copy their buffers, so later mutation cannot change stored paths.
124
- Directory listings, symbolic-link targets, and resolved paths come back as bytes; decoding them to text is the caller's
125
- choice, and a strict decoder reports a name that is not valid UTF-8.
126
-
127
- ## Build fixtures and restore snapshots
128
-
129
- Fixtures make tests deterministic without a setup sequence. Snapshots let you capture that prepared state, serialize
130
- it, and create independent workspaces from the same image. This is useful for test isolation, preview environments, and
131
- resettable sandboxes.
132
-
133
- ```ts
134
- import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
135
- import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
136
- import { Effect } from "effect"
137
- import * as ByteSize from "effect/ByteSize"
138
-
139
- const decodeLimits = {
140
- maxEncodedBytes: ByteSize.megabytes(1),
141
- maxRecords: 1_000,
142
- maxEntries: 1_000,
143
- maxDecodedBytes: ByteSize.megabytes(1)
144
- }
145
-
146
- const program = Effect.gen(function*() {
147
- const template = yield* Vfs.fromFixture({
148
- entries: [
149
- { kind: "directory", path: "/project" },
150
- {
151
- kind: "file",
152
- path: "/project/settings.json",
153
- bytes: new TextEncoder().encode("{\"theme\":\"dark\"}")
154
- }
155
- ]
156
- })
157
-
158
- const encoded = yield* Vfs.encodeSnapshot(yield* template.snapshot)
159
- const snapshot = yield* Vfs.decodeSnapshot(encoded, decodeLimits)
160
- const workspaceA = yield* Vfs.fromSnapshot(snapshot)
161
- const workspaceB = yield* Vfs.fromSnapshot(snapshot)
162
- const a = yield* workspaceA.caller()
163
- const b = yield* workspaceB.caller()
164
-
165
- yield* a.writeFile("/project/settings.json", new TextEncoder().encode("{\"theme\":\"light\"}"), {
166
- access: "write",
167
- truncate: true
168
- })
169
-
170
- return new TextDecoder().decode(yield* b.readFile("/project/settings.json"))
171
- })
172
-
173
- console.log(await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer)))) // {"theme":"dark"}
174
- ```
175
-
176
- Fixture paths must be absolute and unique, and parent directories must be listed explicitly. Fixtures can also contain
177
- symbolic links, metadata, and forward hard links.
178
-
179
- Snapshot decoding requires explicit work limits because encoded bytes may come from an untrusted source. A snapshot
180
- contains the reachable namespace and metadata. It excludes callers, open handles, cursor positions, watch
181
- subscriptions, and unlinked content. Each restored volume is independent.
182
-
183
- ## Overlays and snapshot deltas
184
-
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.
188
-
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.
192
-
193
- ## Use scoped handles for incremental I/O
80
+ ## Confine a caller to a directory
194
81
 
195
- Whole-file operations are convenient, but handles give each open file an independent `bigint` cursor and support
196
- incremental reads, positional I/O, seeking, and truncation. `Effect.scoped` guarantees cleanup on success, failure, or
197
- interruption.
198
-
199
- ```ts
200
- import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
201
- import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
202
- import { Effect } from "effect"
203
-
204
- const program = Effect.scoped(Effect.gen(function*() {
205
- const fs = yield* (yield* Vfs.make()).caller()
206
- yield* fs.writeFile("/events.log", new TextEncoder().encode("one\ntwo\n"), {
207
- access: "write",
208
- create: "exclusive"
209
- })
210
-
211
- const file = yield* fs.open("/events.log", { access: "read" })
212
- const first = yield* file.read(4) // Advances this handle's cursor.
213
- const second = yield* file.read(4)
214
- const preview = yield* file.pread(3, 0n) // Does not move the cursor.
215
-
216
- const decode = (bytes: Uint8Array) => new TextDecoder().decode(bytes)
217
-
218
- // A positioned read also says whether it reached the end of the file.
219
- return [decode(first), decode(second), decode(preview.bytes), preview.eof]
220
- }))
221
-
222
- console.log(await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer)))) // ["one\n", "two\n", "one", false]
223
- ```
224
-
225
- Handles also expose an explicit `close` effect when early release matters. Calling explicit close twice fails, while
226
- scope cleanup remains safe after an explicit close.
227
-
228
- ## Handle expected failures as data
229
-
230
- Every failure is a `VfsError` with a stable `code` and the `operation` that failed. A rejected option names its
231
- `field`, a path-addressed failure names its `path` as bytes, and a classified failure keeps its `cause`. Codec and
232
- store failures share the class with their own codes. Interruption and defects remain separate from these expected
233
- failures.
234
-
235
- ```ts
236
- import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
237
- import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
238
- import { Effect } from "effect"
239
-
240
- const program = Effect.gen(function*() {
241
- const fs = yield* (yield* Vfs.make()).caller()
242
-
243
- return yield* fs.readFile("/optional.json").pipe(
244
- Effect.catchTag("VfsError", (error) =>
245
- error.code === "NotFound"
246
- ? Effect.succeed(new TextEncoder().encode("{}"))
247
- : Effect.fail(error))
248
- )
249
- })
250
-
251
- const bytes = await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer)))
252
- console.log(new TextDecoder().decode(bytes)) // {}
253
- ```
254
-
255
- ## Provide a volume and a caller as Effect services
256
-
257
- `Volume` and `Caller` are services with static layers. `Volume.layer()` builds a fresh empty volume,
258
- `Volume.layerFromSnapshot`, `Volume.layerFromFixture` and `Volume.layerOverlay` build one from existing state, and
259
- `Volume.layerLive` opens a durable one through the `LiveImageStore` in context. `Caller.layer(options)` makes a root
260
- caller on the volume in context. A caller supplied through the service keeps its original volume, identity, umask and
261
- working directory.
262
-
263
- ```ts
264
- import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
265
- import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
266
- import { Effect } from "effect"
267
-
268
- const loadConfig = Effect.gen(function*() {
269
- const fs = yield* Vfs.Caller
270
- return yield* fs.readFile("/app/config.json")
271
- })
272
-
273
- const program = Effect.gen(function*() {
274
- const fs = yield* Vfs.Caller
275
- yield* fs.mkdir("/app")
276
- yield* fs.writeFile("/app/config.json", new TextEncoder().encode("{}"), {
277
- access: "write",
278
- create: "exclusive"
279
- })
280
- return yield* loadConfig
281
- })
282
-
283
- const bytes = await Effect.runPromise(
284
- program.pipe(Effect.provide(Vfs.Caller.layer()), Effect.provide(Vfs.Volume.layer()), Effect.provide(NodeCrypto.layer))
285
- )
286
- console.log(new TextDecoder().decode(bytes)) // {}
287
- ```
82
+ `caller.withRoot(path)` creates a scoped caller whose `/` is that directory. Absolute symlinks and `..` stay
83
+ inside the root. Renaming the root preserves access; deleting it permanently invalidates the caller.
288
84
 
289
- ## When to use core or memory
85
+ Keep the caller's scope open and pass only the capabilities the consumer needs. Existing hard links can share
86
+ file contents across roots. See [Confine a workspace](../../apps/docs/app/content/guides/confined-workspaces.mdx)
87
+ for a complete example with Effect's `FileSystem` service and the confinement limits.
290
88
 
291
- Choose `@effect-vfs/core` when you need direct access to volumes, callers, credentials, byte paths, quotas, watches,
292
- fixtures, or snapshots. Choose `@effect-vfs/memory` when existing code expects Effect's `FileSystem` service and you
293
- want an in-memory implementation. The memory adapter is built on this core, so you can create a core volume and bind
294
- the adapter to it when you need both interfaces.
89
+ ## Guides and reference
295
90
 
296
- ## Compatibility and behavioral limits
91
+ - [Fixtures and snapshots](../../apps/docs/app/content/guides/fixtures-and-snapshots.mdx) covers seeded trees, codecs, and portable deltas.
92
+ - [Overlay workspaces](../../apps/docs/app/content/guides/overlay-filesystems.mdx) covers independent edits and `mergeSnapshotDeltas` conflicts.
93
+ - [Snapshot search](../../apps/docs/app/content/guides/snapshot-search.mdx) covers filename globs, content matches, and bounded results.
94
+ - [Filesystem model](../../apps/docs/app/content/concepts/filesystem-model.mdx) explains callers, handles, permissions, and byte paths.
95
+ - [API reference](../../apps/docs/app/content/api/core/virtual-file-system.mdx) lists constructors, services, and operations.
297
96
 
298
- This package is experimental. Its public API may change between minor releases while Effect v4 remains a release
299
- candidate.
97
+ ## Limits
300
98
 
301
- - Components are limited to 255 bytes. Symbolic-link traversal is limited to 40 links.
302
- - Omitted logical quotas are unbounded by configuration, apart from fixed file and component bounds.
303
- - Absolute paths ignore a supplied directory base. Relative paths can use a live, same-volume directory handle.
304
- - Operations coordinate through one permit per volume. At most 65 operations are admitted at once by
305
- default, including the active operation; set `maxPendingOperations` to change the waiting budget.
306
- Excess work fails with retryable `VfsError` code `VolumeBusy`
307
- before mutation or storage commit. Interruption while waiting makes no change; interruption after a commit
308
- does not roll it back.
309
- - Each watch subscriber retains at most 256 events by default; set `maxWatchEvents` to change this bound
310
- (minimum 2). A subscriber that loses events receives `Rescan` with path `/` after its buffered events.
311
- Rescan the volume, then continue consuming the watch; repeat the scan if another `Rescan` arrives.
312
- Delivered events retain their order. The memory `FileSystem.watch` stream fails with a platform error
313
- identified by `MemoryFileSystem.isWatchOverflow`. Open a new watch before rescanning its path so changes
314
- during the scan remain observable.
315
- - `sync` checks handle liveness. An in-memory volume provides no host or crash durability.
99
+ The package is experimental. Public APIs may change between minor releases.
316
100
 
317
- For the complete contract, read the
318
- [implemented profile](https://github.com/lloydrichards/effect-virtual-fs/blob/main/.okf/profiles/implemented-filesystem.md).
101
+ Paths preserve arbitrary non-NUL bytes. Components are limited to 255 bytes, and symlink traversal to 40 links.
102
+ Volume options bound entries, file sizes, pending operations, and watch queues. In-memory volumes provide no host or
103
+ crash durability. See [compatibility and limits](../../apps/docs/app/content/reference/compatibility-and-limits.mdx)
104
+ for the complete contract.
package/dist/Caller.d.ts CHANGED
@@ -230,6 +230,7 @@ export type WalkOptions = typeof WalkOptions.Type;
230
230
  /**
231
231
  * Schema for the options of `remove`. `recursive` removes a directory and
232
232
  * everything under it; `force` succeeds when the target itself is missing.
233
+ * `expected` pins object identity at each coordinated removal stage. A replacement fails `VolumeBusy`.
233
234
  *
234
235
  * @category schemas
235
236
  * @since 0.6.0
@@ -237,6 +238,7 @@ export type WalkOptions = typeof WalkOptions.Type;
237
238
  export declare const RemoveOptions: Schema.Struct<{
238
239
  readonly recursive: Schema.optionalKey<Schema.Boolean>;
239
240
  readonly force: Schema.optionalKey<Schema.Boolean>;
241
+ readonly expected: Schema.optionalKey<Schema.suspend<Schema.declare<ObjectReference, ObjectReference>>>;
240
242
  }>;
241
243
  /**
242
244
  * Options of `remove`.
@@ -1 +1 @@
1
- {"version":3,"file":"Caller.d.ts","sourceRoot":"","sources":["../src/Caller.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAE7D;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,EAAE,OAAO,MAA8C,CAAA;AAE5E;;;;;GAKG;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAA;AAEtC;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,OAAO,MAAuD,CAAA;AAE9F;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG,OAAO,iBAAiB,CAAA;AAExD;;;;;;GAMG;AACH,eAAO,MAAM,QAAQ;;;;;EAKnB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAC,IAAI,CAAA;AAE3C;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;EAG5B,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG,OAAO,iBAAiB,CAAC,IAAI,CAAA;AAE7D;;;;;;GAMG;AACH,eAAO,MAAM,WAAW;;;;;;EAMtB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,WAAW,CAAC,IAAI,CAAA;AAEjD;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;EAK3B,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,OAAO,gBAAgB,CAAC,IAAI,CAAA;AAE3D;;;;;GAKG;AACH,eAAO,MAAM,eAAe;;;EAG1B,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,OAAO,eAAe,CAAC,IAAI,CAAA;AAEzD;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;EAMhC,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,qBAAqB,GAAG,OAAO,qBAAqB,CAAC,IAAI,CAAA;AAErE;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;EAKvB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,OAAO,YAAY,CAAC,IAAI,CAAA;AAEnD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,WAAW;;;;;EAKtB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,WAAW,CAAC,IAAI,CAAA;AAEjD;;;;;;GAMG;AACH,eAAO,MAAM,aAAa;;;EAGxB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,aAAa,CAAC,IAAI,CAAA;AAErD;;;;;GAKG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;EAEzB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,cAAc,CAAC,IAAI,CAAA;AAEvD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAMzB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,cAAc,CAAC,IAAI,CAAA;AAEvD;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,kDAEjC,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAc3B,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,OAAO,gBAAgB,CAAC,IAAI,CAAA"}
1
+ {"version":3,"file":"Caller.d.ts","sourceRoot":"","sources":["../src/Caller.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAE7D;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,EAAE,OAAO,MAA8C,CAAA;AAE5E;;;;;GAKG;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAA;AAEtC;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,OAAO,MAAuD,CAAA;AAE9F;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG,OAAO,iBAAiB,CAAA;AAExD;;;;;;GAMG;AACH,eAAO,MAAM,QAAQ;;;;;EAKnB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAC,IAAI,CAAA;AAE3C;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;EAG5B,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG,OAAO,iBAAiB,CAAC,IAAI,CAAA;AAE7D;;;;;;GAMG;AACH,eAAO,MAAM,WAAW;;;;;;EAMtB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,WAAW,CAAC,IAAI,CAAA;AAEjD;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;EAK3B,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,OAAO,gBAAgB,CAAC,IAAI,CAAA;AAE3D;;;;;GAKG;AACH,eAAO,MAAM,eAAe;;;EAG1B,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,OAAO,eAAe,CAAC,IAAI,CAAA;AAEzD;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;EAMhC,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,qBAAqB,GAAG,OAAO,qBAAqB,CAAC,IAAI,CAAA;AAErE;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;EAKvB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,OAAO,YAAY,CAAC,IAAI,CAAA;AAEnD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,WAAW;;;;;EAKtB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,WAAW,CAAC,IAAI,CAAA;AAEjD;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa;;;;EAIxB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,aAAa,CAAC,IAAI,CAAA;AAErD;;;;;GAKG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;EAEzB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,cAAc,CAAC,IAAI,CAAA;AAEvD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAMzB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,cAAc,CAAC,IAAI,CAAA;AAEvD;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,kDAEjC,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAc3B,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,OAAO,gBAAgB,CAAC,IAAI,CAAA"}
package/dist/Caller.js CHANGED
@@ -130,13 +130,15 @@ export const WalkOptions = Schema.Struct({
130
130
  /**
131
131
  * Schema for the options of `remove`. `recursive` removes a directory and
132
132
  * everything under it; `force` succeeds when the target itself is missing.
133
+ * `expected` pins object identity at each coordinated removal stage. A replacement fails `VolumeBusy`.
133
134
  *
134
135
  * @category schemas
135
136
  * @since 0.6.0
136
137
  */
137
138
  export const RemoveOptions = Schema.Struct({
138
139
  recursive: Schema.optionalKey(Schema.Boolean),
139
- force: Schema.optionalKey(Schema.Boolean)
140
+ force: Schema.optionalKey(Schema.Boolean),
141
+ expected: Schema.optionalKey(Schema.suspend(() => ObjectReferenceSchema))
140
142
  });
141
143
  /**
142
144
  * Schema for the options of `symlink`.
@@ -8,8 +8,9 @@
8
8
  *
9
9
  * @since 0.4.0
10
10
  */
11
- import { Context, Effect } from "effect";
11
+ import { Context, Duration, Effect, Layer } from "effect";
12
12
  import * as ByteSize from "effect/ByteSize";
13
+ import type * as Cause from "effect/Cause";
13
14
  import type * as Crypto from "effect/Crypto";
14
15
  import type * as Scope from "effect/Scope";
15
16
  import { type ArgumentFailure, type StoreFailure, VfsError } from "./VfsError.js";
@@ -56,6 +57,73 @@ export interface Options {
56
57
  * @since 0.4.0
57
58
  */
58
59
  export declare const open: (options: Options) => Effect.Effect<Volume, VfsError, LiveImageStore | Scope.Scope | Crypto.Crypto>;
60
+ /**
61
+ * Scoped access to live volumes shared by canonical storage identity.
62
+ * Each `get` retains its volume until the borrowing scope closes. Keep that
63
+ * scope open until its callers and handles finish using the volume.
64
+ *
65
+ * @category models
66
+ * @since 0.9.0
67
+ */
68
+ export interface Registry<K, E = never> {
69
+ readonly get: (key: K) => Effect.Effect<Volume, E | VfsError | Cause.ExceededCapacityError, Scope.Scope>;
70
+ }
71
+ /**
72
+ * Store Layers are built independently for each acquired entry. The same volume
73
+ * configuration applies to every key. Keys use Effect's equality rules and must
74
+ * identify canonical storage locations; different aliases can create competing
75
+ * owners. Coordinate ownership across processes outside this registry.
76
+ *
77
+ * @category models
78
+ * @since 0.9.0
79
+ */
80
+ export interface RegistryOptions<K, E = never, R = never> {
81
+ readonly store: (key: K) => Layer.Layer<LiveImageStore, E, R>;
82
+ readonly volume: Options;
83
+ /** Positive integer entry limit. Omission means unlimited; idle entries count. */
84
+ readonly capacity?: number;
85
+ /** Retain unused entries for this duration. Omission releases them immediately. */
86
+ readonly idleTimeToLive?: Duration.Input;
87
+ }
88
+ /**
89
+ * Lazily share one live volume and its store per key through `RcMap`. Concurrent
90
+ * borrowers share acquisition. Reusing an idle entry avoids rebuilding its store
91
+ * and decoding its image, while retaining its memory and storage locks.
92
+ *
93
+ * The registry's owning scope must outlive all borrowers. Closing it shuts down
94
+ * every volume, including borrowed ones, before releasing their store Layers.
95
+ * Capacity exhaustion fails with `Cause.ExceededCapacityError`; it does not evict
96
+ * idle entries. Acquisition failures remain shared until the entry is released.
97
+ *
98
+ * Invalidation is intentionally unavailable: acquiring a replacement while an
99
+ * existing borrower remains would violate exclusive store ownership. Canonicalize
100
+ * keys before borrowing and use one registry for each set of backing stores.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * import { LiveVolume } from "@effect-vfs/core"
105
+ * import { ByteSize, Effect, Layer } from "effect"
106
+ *
107
+ * declare const storeFor: (key: string) => Layer.Layer<LiveVolume.LiveImageStore>
108
+ *
109
+ * const program = Effect.gen(function*() {
110
+ * const registry = yield* LiveVolume.makeRegistry({
111
+ * store: storeFor,
112
+ * volume: { maxImageBytes: ByteSize.megabytes(1), volume: {} },
113
+ * capacity: 8,
114
+ * idleTimeToLive: "5 seconds"
115
+ * })
116
+ * return yield* Effect.scoped(Effect.gen(function*() {
117
+ * const volume = yield* registry.get("workspace")
118
+ * return yield* (yield* volume.caller()).readFile("/plan.md")
119
+ * }))
120
+ * })
121
+ * ```
122
+ *
123
+ * @category constructors
124
+ * @since 0.9.0
125
+ */
126
+ export declare const makeRegistry: <K, E, R>(options: RegistryOptions<K, E, R>) => Effect.Effect<Registry<K, E>, ArgumentFailure, R | Crypto.Crypto | Scope.Scope>;
59
127
  /**
60
128
  * A live volume and the shutdown action its storage adapter must run before
61
129
  * closing the underlying store.
@@ -1 +1 @@
1
- {"version":3,"file":"LiveVolume.d.ts","sourceRoot":"","sources":["../src/LiveVolume.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AACxC,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAA;AAC3C,OAAO,KAAK,KAAK,MAAM,MAAM,eAAe,CAAA;AAC5C,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAA;AAG1C,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACjF,OAAO,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAErF;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,CAAA;;IAW9D,4EAA4E;0BACtD,gBAAgB;2BACf,CAAC,OAAO,EAAE,UAAU,KAAK,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,YAAY,GAAG,eAAe,CAAC;qBACxF,CAAC,KAAK,EAAE,UAAU,KAAK,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC;;AAZtE;;;;;;;GAOG;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;;;;;;;GAOG;AACH,eAAO,MAAM,IAAI,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,CACpD,MAAM,EACN,QAAQ,EACR,cAAc,GAAG,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,CAuC5C,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;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,EAAE,CAAC,OAAO,CAAC,EAAE,aAAa,KAAK,MAAM,CAAC,MAAM,CACxE,UAAU,EACV,QAAQ,EACR,MAAM,CAAC,MAAM,CAKZ,CAAA;AAEH;;;;;;;;GAQG;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,QAAQ,EACR,MAAM,CAAC,MAAM,CACU,CAAA"}
1
+ {"version":3,"file":"LiveVolume.d.ts","sourceRoot":"","sources":["../src/LiveVolume.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,OAAO,EAAY,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAyC,MAAM,QAAQ,CAAA;AAC1G,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAA;AAC3C,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAA;AAC1C,OAAO,KAAK,KAAK,MAAM,MAAM,eAAe,CAAA;AAC5C,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAA;AAI1C,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACjF,OAAO,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAErF;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,CAAA;;IAW9D,4EAA4E;0BACtD,gBAAgB;2BACf,CAAC,OAAO,EAAE,UAAU,KAAK,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,YAAY,GAAG,eAAe,CAAC;qBACxF,CAAC,KAAK,EAAE,UAAU,KAAK,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC;;AAZtE;;;;;;;GAOG;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;;;;;;;GAOG;AACH,eAAO,MAAM,IAAI,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,CACpD,MAAM,EACN,QAAQ,EACR,cAAc,GAAG,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,CA4C5C,CAAA;AAEF;;;;;;;GAOG;AACH,MAAM,WAAW,QAAQ,CAAC,CAAC,EAAE,CAAC,GAAG,KAAK;IACpC,QAAQ,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,QAAQ,GAAG,KAAK,CAAC,qBAAqB,EAAE,KAAK,CAAC,KAAK,CAAC,CAAA;CACzG;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe,CAAC,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,KAAK;IACtD,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,KAAK,CAAC,KAAK,CAAC,cAAc,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;IAC7D,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;IACxB,kFAAkF;IAClF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,mFAAmF;IACnF,QAAQ,CAAC,cAAc,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAA;CACzC;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,eAAO,MAAM,YAAY,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CACtF,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,EACd,eAAe,EACf,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,KAAK,CAAC,KAAK,CAsD/B,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;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,EAAE,CAAC,OAAO,CAAC,EAAE,aAAa,KAAK,MAAM,CAAC,MAAM,CACxE,UAAU,EACV,QAAQ,EACR,MAAM,CAAC,MAAM,CAKZ,CAAA;AAEH;;;;;;;;GAQG;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,QAAQ,EACR,MAAM,CAAC,MAAM,CACU,CAAA"}
@@ -8,10 +8,11 @@
8
8
  *
9
9
  * @since 0.4.0
10
10
  */
11
- import { Context, Effect } from "effect";
11
+ import { Context, Deferred, Duration, Effect, Layer, MutableHashMap, Option, RcMap, Schema } from "effect";
12
12
  import * as ByteSize from "effect/ByteSize";
13
- import { argumentFailure, retargetFailure } from "./internal/errors.js";
13
+ import { argumentFailure, decodeConfiguration, retargetFailure } from "./internal/errors.js";
14
14
  import * as Model from "./internal/virtualFileSystem.js";
15
+ import * as Limits from "./internal/volumeLimits.js";
15
16
  import { VfsError } from "./VfsError.js";
16
17
  /**
17
18
  * One exclusively owned store. Its Layer holds the storage resource until the
@@ -31,9 +32,13 @@ export class LiveImageStore extends Context.Service()("@effect-vfs/core/LiveImag
31
32
  * @category constructors
32
33
  * @since 0.4.0
33
34
  */
34
- export const open = Effect.fn("LiveVolume.open")(function* (options) {
35
+ export const open = Effect.fnUntraced(function* (options) {
35
36
  const store = yield* LiveImageStore;
36
- const initial = yield* prepareEmptyImage(options.volume).pipe(
37
+ const { decoded, initial } = yield* Effect.gen(function* () {
38
+ const decoded = yield* Effect.fromResult(Limits.configuration(options.volume, "LiveVolume.open"));
39
+ const initial = yield* prepareEmptyImage(decoded.options);
40
+ return { decoded, initial };
41
+ }).pipe(
37
42
  // Names the nested option that failed, such as `volume.maxEntries`.
38
43
  Effect.mapError((cause) => {
39
44
  const field = "field" in cause ? cause.field : undefined;
@@ -43,20 +48,92 @@ export const open = Effect.fn("LiveVolume.open")(function* (options) {
43
48
  return yield* argumentFailure("LiveVolume.open", "maxImageBytes");
44
49
  }
45
50
  const image = yield* store.loadOrCreate(initial);
46
- const session = yield* openImage(image, options.maxImageBytes, store.commit, store.durability ?? "memory-only").pipe(Effect.mapError((cause) => new VfsError({ code: "CorruptStore", operation: "LiveVolume.open", cause })));
51
+ const requested = decoded.limits;
52
+ const session = yield* Model.openImageVolume(image, options.maxImageBytes, store.commit, store.durability ?? "memory-only", decoded.options).pipe(Effect.mapError((cause) => new VfsError({ code: "CorruptStore", operation: "LiveVolume.open", cause })));
47
53
  yield* Effect.addFinalizer(() => session.shutdown);
48
54
  const volume = session.volume;
49
- if (volume.limits.maxEntries !== options.volume.maxEntries || volume.limits.maxBytes === undefined ||
50
- volume.limits.maxPathBytes === undefined || options.volume.maxBytes === undefined ||
51
- options.volume.maxFileBytes === undefined || options.volume.maxPathBytes === undefined ||
52
- ByteSize.toBigInt(volume.limits.maxBytes) !== ByteSize.toBigInt(options.volume.maxBytes) ||
53
- ByteSize.toBigInt(volume.limits.maxFileBytes) !== ByteSize.toBigInt(options.volume.maxFileBytes) ||
54
- ByteSize.toBigInt(volume.limits.maxPathBytes) !== ByteSize.toBigInt(options.volume.maxPathBytes) ||
55
- (options.volume.identity !== undefined && options.volume.identity !== volume.identity)) {
55
+ if (!Limits.compatible(volume.limits, requested, volume.identity, decoded.identity)) {
56
56
  return yield* new VfsError({ code: "IncompatibleStore", operation: "LiveVolume.open" });
57
57
  }
58
58
  return volume;
59
59
  });
60
+ const RegistryCapacity = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1));
61
+ /**
62
+ * Lazily share one live volume and its store per key through `RcMap`. Concurrent
63
+ * borrowers share acquisition. Reusing an idle entry avoids rebuilding its store
64
+ * and decoding its image, while retaining its memory and storage locks.
65
+ *
66
+ * The registry's owning scope must outlive all borrowers. Closing it shuts down
67
+ * every volume, including borrowed ones, before releasing their store Layers.
68
+ * Capacity exhaustion fails with `Cause.ExceededCapacityError`; it does not evict
69
+ * idle entries. Acquisition failures remain shared until the entry is released.
70
+ *
71
+ * Invalidation is intentionally unavailable: acquiring a replacement while an
72
+ * existing borrower remains would violate exclusive store ownership. Canonicalize
73
+ * keys before borrowing and use one registry for each set of backing stores.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * import { LiveVolume } from "@effect-vfs/core"
78
+ * import { ByteSize, Effect, Layer } from "effect"
79
+ *
80
+ * declare const storeFor: (key: string) => Layer.Layer<LiveVolume.LiveImageStore>
81
+ *
82
+ * const program = Effect.gen(function*() {
83
+ * const registry = yield* LiveVolume.makeRegistry({
84
+ * store: storeFor,
85
+ * volume: { maxImageBytes: ByteSize.megabytes(1), volume: {} },
86
+ * capacity: 8,
87
+ * idleTimeToLive: "5 seconds"
88
+ * })
89
+ * return yield* Effect.scoped(Effect.gen(function*() {
90
+ * const volume = yield* registry.get("workspace")
91
+ * return yield* (yield* volume.caller()).readFile("/plan.md")
92
+ * }))
93
+ * })
94
+ * ```
95
+ *
96
+ * @category constructors
97
+ * @since 0.9.0
98
+ */
99
+ export const makeRegistry = Effect.fnUntraced(function* (options) {
100
+ const capacity = options.capacity === undefined
101
+ ? Infinity
102
+ : yield* Effect.fromResult(decodeConfiguration(RegistryCapacity, options.capacity, "LiveVolume.makeRegistry")).pipe(Effect.mapError((cause) => argumentFailure("LiveVolume.makeRegistry", "capacity", cause)));
103
+ const idleTimeToLive = Duration.fromInput(options.idleTimeToLive ?? 0);
104
+ if (Option.isNone(idleTimeToLive))
105
+ return yield* argumentFailure("LiveVolume.makeRegistry", "idleTimeToLive");
106
+ const store = options.store;
107
+ const volume = { maxImageBytes: options.volume.maxImageBytes, volume: { ...options.volume.volume } };
108
+ const retiring = MutableHashMap.empty();
109
+ const volumes = yield* RcMap.make({
110
+ lookup: Effect.fnUntraced(function* (key) {
111
+ // RcMap removes an entry before its asynchronous finalizers finish. Keep
112
+ // equal keys waiting until both the volume and its store have shut down.
113
+ yield* Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
114
+ let previous = MutableHashMap.get(retiring, key);
115
+ while (Option.isSome(previous)) {
116
+ yield* restore(Deferred.await(previous.value));
117
+ previous = MutableHashMap.get(retiring, key);
118
+ }
119
+ const released = yield* Deferred.make();
120
+ MutableHashMap.set(retiring, key, released);
121
+ // Registered first, this runs after the store and volume finalizers.
122
+ yield* Effect.addFinalizer(() => Effect.gen(function* () {
123
+ MutableHashMap.remove(retiring, key);
124
+ yield* Deferred.succeed(released, undefined);
125
+ }));
126
+ }));
127
+ // A parent Layer memo must not keep a retired store alive or share its
128
+ // mutable ownership state with a different volume entry.
129
+ const services = yield* Layer.build(Layer.fresh(store(key)));
130
+ return yield* open(volume).pipe(Effect.provideService(LiveImageStore, Context.get(services, LiveImageStore)));
131
+ }),
132
+ capacity,
133
+ idleTimeToLive: idleTimeToLive.value
134
+ });
135
+ return { get: (key) => RcMap.get(volumes, key) };
136
+ });
60
137
  /**
61
138
  * Construct a versioned image for a new, empty volume.
62
139
  * Store this image before opening it for mutation.