@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.
- package/README.md +21 -235
- package/dist/Caller.d.ts +2 -0
- package/dist/Caller.d.ts.map +1 -1
- package/dist/Caller.js +3 -1
- package/dist/LiveVolume.d.ts +69 -1
- package/dist/LiveVolume.d.ts.map +1 -1
- package/dist/LiveVolume.js +89 -12
- package/dist/Search.d.ts +730 -0
- package/dist/Search.d.ts.map +1 -0
- package/dist/Search.js +576 -0
- package/dist/SnapshotDelta.d.ts +234 -0
- package/dist/SnapshotDelta.d.ts.map +1 -1
- package/dist/SnapshotDelta.js +107 -0
- package/dist/VfsError.d.ts +3 -1
- package/dist/VfsError.d.ts.map +1 -1
- package/dist/VfsError.js +3 -1
- package/dist/VirtualFileSystem.d.ts +78 -2
- package/dist/VirtualFileSystem.d.ts.map +1 -1
- package/dist/VirtualFileSystem.js +68 -4
- package/dist/Watch.d.ts +18 -0
- package/dist/Watch.d.ts.map +1 -1
- package/dist/Watch.js +12 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/internal/callerWatch.d.ts +32 -0
- package/dist/internal/callerWatch.d.ts.map +1 -0
- package/dist/internal/callerWatch.js +184 -0
- package/dist/internal/confinement.d.ts +16 -0
- package/dist/internal/confinement.d.ts.map +1 -0
- package/dist/internal/confinement.js +79 -0
- package/dist/internal/directoryEntries.d.ts +6 -0
- package/dist/internal/directoryEntries.d.ts.map +1 -0
- package/dist/internal/directoryEntries.js +79 -0
- package/dist/internal/fixture.js +4 -4
- package/dist/internal/glob.d.ts +66 -0
- package/dist/internal/glob.d.ts.map +1 -0
- package/dist/internal/glob.js +248 -0
- package/dist/internal/image.d.ts +7 -1
- package/dist/internal/image.d.ts.map +1 -1
- package/dist/internal/image.js +9 -7
- package/dist/internal/liveImage.d.ts +1 -1
- package/dist/internal/liveImage.d.ts.map +1 -1
- package/dist/internal/liveImage.js +2 -10
- package/dist/internal/path.js +2 -2
- package/dist/internal/resolution.d.ts +139 -0
- package/dist/internal/resolution.d.ts.map +1 -0
- package/dist/internal/resolution.js +332 -0
- package/dist/internal/search.d.ts +24 -0
- package/dist/internal/search.d.ts.map +1 -0
- package/dist/internal/search.js +78 -0
- package/dist/internal/searchContent.d.ts +77 -0
- package/dist/internal/searchContent.d.ts.map +1 -0
- package/dist/internal/searchContent.js +223 -0
- package/dist/internal/searchModel.d.ts +83 -0
- package/dist/internal/searchModel.d.ts.map +1 -0
- package/dist/internal/searchModel.js +23 -0
- package/dist/internal/snapshotDelta.d.ts +470 -1
- package/dist/internal/snapshotDelta.d.ts.map +1 -1
- package/dist/internal/snapshotDelta.js +59 -25
- package/dist/internal/snapshotMerge.d.ts +54 -0
- package/dist/internal/snapshotMerge.d.ts.map +1 -0
- package/dist/internal/snapshotMerge.js +959 -0
- package/dist/internal/tokenRegistry.d.ts +83 -0
- package/dist/internal/tokenRegistry.d.ts.map +1 -0
- package/dist/internal/tokenRegistry.js +146 -0
- package/dist/internal/virtualFileSystem.d.ts +32 -17
- package/dist/internal/virtualFileSystem.d.ts.map +1 -1
- package/dist/internal/virtualFileSystem.js +503 -736
- package/dist/internal/volumeLimits.d.ts +53 -0
- package/dist/internal/volumeLimits.d.ts.map +1 -0
- package/dist/internal/volumeLimits.js +63 -0
- package/dist/internal/volumeState.d.ts +2 -0
- package/dist/internal/volumeState.d.ts.map +1 -1
- package/dist/internal/volumeState.js +9 -0
- package/dist/internal/watchHub.d.ts +6 -2
- package/dist/internal/watchHub.d.ts.map +1 -1
- package/dist/internal/watchHub.js +31 -6
- 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
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
299
|
-
candidate.
|
|
97
|
+
## Limits
|
|
300
98
|
|
|
301
|
-
|
|
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
|
-
|
|
318
|
-
|
|
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`.
|
package/dist/Caller.d.ts.map
CHANGED
|
@@ -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
|
|
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`.
|
package/dist/LiveVolume.d.ts
CHANGED
|
@@ -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.
|
package/dist/LiveVolume.d.ts.map
CHANGED
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"LiveVolume.d.ts","sourceRoot":"","sources":["../src/LiveVolume.ts"],"names":[],"mappings":"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"}
|
package/dist/LiveVolume.js
CHANGED
|
@@ -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.
|
|
35
|
+
export const open = Effect.fnUntraced(function* (options) {
|
|
35
36
|
const store = yield* LiveImageStore;
|
|
36
|
-
const initial = yield*
|
|
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
|
|
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
|
|
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.
|