@opfs-vfs/effect 0.2.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/LICENSE.md ADDED
@@ -0,0 +1,73 @@
1
+ # PolyForm Noncommercial License 1.0.0
2
+
3
+ <https://polyformproject.org/licenses/noncommercial/1.0.0>
4
+
5
+ ## Acceptance
6
+
7
+ In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
8
+
9
+ ## Copyright License
10
+
11
+ The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
12
+
13
+ ## Distribution License
14
+
15
+ The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
16
+
17
+ ## Notices
18
+
19
+ You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
20
+
21
+ > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
22
+
23
+ ## Changes and New Works License
24
+
25
+ The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
26
+
27
+ ## Patent License
28
+
29
+ The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
30
+
31
+ ## Noncommercial Purposes
32
+
33
+ Any noncommercial purpose is a permitted purpose.
34
+
35
+ ## Personal Uses
36
+
37
+ Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose.
38
+
39
+ ## Noncommercial Organizations
40
+
41
+ Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding.
42
+
43
+ ## Fair Use
44
+
45
+ You may have "fair use" rights for the software under the law. These terms do not limit them.
46
+
47
+ ## No Other Rights
48
+
49
+ These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
50
+
51
+ ## Patent Defense
52
+
53
+ If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
54
+
55
+ ## Violations
56
+
57
+ The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
58
+
59
+ ## No Liability
60
+
61
+ ***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
62
+
63
+ ## Definitions
64
+
65
+ The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
66
+
67
+ **You** refers to the individual or entity agreeing to these terms.
68
+
69
+ **Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
70
+
71
+ **Your licenses** are all the licenses granted to you for the software under these terms.
72
+
73
+ **Use** means anything you do with the software requiring one of your licenses.
package/README.md ADDED
@@ -0,0 +1,211 @@
1
+ # @opfs-vfs/effect
2
+
3
+ Effect v4 adapter for worker-backed OPFS volumes and direct mounts. Requires `effect@4.0.0-rc.118`.
4
+
5
+ ```ts
6
+ import { Effect } from 'effect';
7
+ import { Volume } from '@opfs-vfs/effect';
8
+
9
+ const program = Effect.gen(function* () {
10
+ const volume = yield* Volume.make({ fileName: 'app.bin' });
11
+ yield* volume.sync;
12
+ return yield* volume.persistence;
13
+ }).pipe(Effect.scoped);
14
+ ```
15
+
16
+ `OpfsFileSystem.layer` builds the standard `FileSystem` service on a mounted
17
+ `Volume`. It supports scoped file handles, descriptor reads and writes, and
18
+ bounded whole-file helpers up to 16 MiB. Larger files and other open flags use
19
+ descriptor I/O. Use `stream` and `sink` for bounded-memory transfers. See
20
+ `examples/filesystem-stream.ts` for a chunked copy and
21
+ `examples/filesystem-save.ts` for handling persistence errors. A successful
22
+ write does not mean the data is durable; use `Volume.withSync` or await
23
+ `volume.sync` at save boundaries.
24
+
25
+ ### Save boundaries
26
+
27
+ `effect.pipe(Volume.withSync)` runs the effect, then syncs the `Volume` service
28
+ on success and preserves its value. Its filesystem must use that same volume
29
+ instance. If the wrapped operation fails, defects, or is interrupted, the helper does
30
+ not start its success sync and preserves the original failure. Existing
31
+ cancellation behavior still applies; cancellation can occur after sync dispatch
32
+ and does not guarantee that sync completes.
33
+ A sync failure fails the wrapped effect. Each wrapper syncs once after success,
34
+ including nested wrappers. Wrap a batch once to share its sync cost. There is
35
+ no isolation or rollback, and concurrent writes can also be flushed.
36
+
37
+ The default balanced mode schedules background synchronization about 150 ms
38
+ after the first dirty change, without restarting the timer for later changes.
39
+ This is not a durability deadline: scheduling, worker suspension, and I/O or
40
+ quota failures can delay or prevent completion. Background failures record a
41
+ persistence error rather than retroactively failing a completed write.
42
+
43
+ | Approach | Benefit | Cost or limit |
44
+ | ----------------------------------------- | --------------------------------------------------- | -------------------------------------------------------------------- |
45
+ | Background balanced mode | Groups nearby writes without awaiting each flush. | Write success does not acknowledge durability. |
46
+ | `Volume.withSync` or manual `volume.sync` | Returns an explicit sync result at a save boundary. | The caller waits for sync and its I/O cost. |
47
+ | One wrapper around a batch | Syncs once after all writes succeed. | Failure can leave earlier writes applied without the wrapper's sync. |
48
+
49
+ Disk mode avoids a full RAM copy of file contents, but its OPFS writes and
50
+ metadata still need flushing for confirmation. Balanced background sync applies
51
+ in disk mode too. A confirmed save needs `Volume.withSync` or manual sync in
52
+ either buffer mode; eventual balanced persistence does not require sync after
53
+ each individual write.
54
+
55
+ Background flushing and explicit boundaries are complementary. Relaxed mode
56
+ defers flushing to explicit sync or orderly close. Strict mode additionally
57
+ flushes memory-mode recovery records, not every disk-mode write. A clean
58
+ `volume.persistence` snapshot does not prove an earlier save survived an owner
59
+ change. See the [Effect guide](https://opfs.dev/docs/effect/) for session lifetime
60
+ and uncertain-save handling.
61
+
62
+ The adapter also supports path metadata, permissions, links, directory creation,
63
+ listing, removal, rename, recursive copy and volume-local temporary paths.
64
+ Unlike Node's filesystem, operands must be absolute volume paths; relative
65
+ symlink targets are preserved. Recursive copy does not follow symlinks, keeps
66
+ copied relative link targets as written (Node's default copy resolves them to
67
+ the source), is not atomic, and does not preserve hard-link topology. `chown`
68
+ and `glob` remain intentionally unsupported in v1 and return typed `ENOTSUP`
69
+ errors. Scoped temporary paths remove only their private directory
70
+ when the scope closes; the default parent is `/tmp` inside the volume. A temporary
71
+ path uses its resolved physical parent, so retargeting the caller-supplied parent
72
+ symlink does not redirect cleanup. Ownership covers the created root pathname and
73
+ its current descendants; cleanup is not an atomic inode-identity check. Keep the
74
+ resolved physical ancestors stable until cleanup completes. Shutdown, owner
75
+ replacement, or cleanup failure can leave temporary paths for explicit reclamation.
76
+
77
+ `FileSystem.watch` uses the same logical-change registration and cleanup path as
78
+ rich subscriptions. It checks the target when the stream is acquired: a missing
79
+ path fails with `NotFound`, and a symlink target fails with `BadResource`. It
80
+ watches regular files or directories and emits absolute volume paths as
81
+ `Create`, `Update`, or `Remove`. Recursive directory watching preserves the
82
+ native descendant records for directory renames; symlink entries inside a
83
+ watched directory are reported, but a symlink target is not followed.
84
+
85
+ Watch notifications are hints, not a replayable log. Preflight checks and
86
+ directory scans are not atomic snapshots; logical event counts and ordering can
87
+ differ from native operating-system watchers. Reconciliation marks its view
88
+ stale when stream consumption observes a failure, so that callback can lag the
89
+ terminal notification. A takeover ends the stream
90
+ with `Unknown` and a `SubscriptionError` whose code is
91
+ `SUBSCRIPTION_INTERRUPTED`; the standard API does not silently resubscribe.
92
+ Readiness and retirement timeouts map to `TimedOut`. Use a bounded retry only
93
+ when the application also invalidates and reconciles its current view. The
94
+ standard watch API has no registration-ready signal, so use rich subscriptions
95
+ when a view must subscribe before its initial scan. See
96
+ `examples/reconciled-view.ts` for subscribe-before-scan recovery with bounded
97
+ retries and retirement waiting. Its three-retry budget resets after a fresh scan
98
+ is successfully published.
99
+
100
+ `Subscriptions.layer` provides the Effect subscriptions service when the mounted
101
+ volume has the compatible logical-change capability. Worker mounts request it
102
+ with `subscriptionsRequest()`; direct mounts configure a logical-change plugin.
103
+ Without that capability, layer acquisition fails with a typed unsupported
104
+ `VolumeError`.
105
+
106
+ ```ts
107
+ import { Effect, Layer, Stream } from 'effect';
108
+ import { Subscriptions, Volume } from '@opfs-vfs/effect';
109
+ import { subscriptionsRequest } from '@opfs-vfs/plugin-subscriptions/config';
110
+
111
+ const liveLayer = Layer.provide(
112
+ Subscriptions.layer,
113
+ Volume.layer({ fileName: 'app.bin', plugins: () => [subscriptionsRequest()] }),
114
+ );
115
+
116
+ const observe = Effect.scoped(
117
+ Effect.gen(function* () {
118
+ const subscriptions = yield* Subscriptions.Subscriptions;
119
+ const subscription = yield* subscriptions.subscribe({ path: '/', scope: 'directory', recursive: true });
120
+ yield* Stream.runForEach(subscription.changes, (change) => Effect.sync(() => console.log(change.path)));
121
+ }),
122
+ ).pipe(Effect.provide(liveLayer));
123
+ ```
124
+
125
+ Each `changes` stream can be consumed once. Stream completion, interruption, or
126
+ scope close unsubscribes it; `retired` can be evaluated repeatedly to read the
127
+ same released or unknown cleanup result. The stream has bounded delivery and
128
+ preserves terminal subscription errors. It has no initial snapshot or replay;
129
+ subscribe before scanning and reconcile notifications received during the scan.
130
+ See `examples/subscriptions.ts` for a complete worker-backed example.
131
+
132
+ The adapter buffers at most 16 changes and permits one additional producer
133
+ offer to wait; a terminal error clears buffered changes and fails the stream.
134
+ Content capture is off by default (`content: false`). The adapter queue and its
135
+ one pending offer retain up to 17 * `maxBytes` of included payloads, in addition
136
+ to source buffers and any data a caller retains. If retirement is unknown,
137
+ new subscriptions on that same owner generation fail until the volume observes
138
+ a different ready generation. Recreating a handle in the same generation does
139
+ not clear the barrier; a new generation does, while an old handle's `retired`
140
+ result remains unchanged. Direct mounts cannot observe a new owner generation,
141
+ so an unknown retirement requires remounting.
142
+
143
+ ```ts
144
+ import { Effect, FileSystem, Layer } from 'effect';
145
+ import { OpfsFileSystem, Volume } from '@opfs-vfs/effect';
146
+
147
+ const FileSystemLive = Layer.provideMerge(OpfsFileSystem.layer, Volume.layer({ fileName: 'app.bin' }));
148
+
149
+ const app = Effect.gen(function* () {
150
+ const fs = yield* FileSystem.FileSystem;
151
+ yield* fs.writeFileString('/note.txt', 'Hello');
152
+ }).pipe(Volume.withSync, Effect.provide(FileSystemLive));
153
+ ```
154
+
155
+ `make` and `layer` own a worker client in the current `Scope`. The bundled
156
+ dedicated worker registers the subscriptions plugin; request it with
157
+ `plugins: () => [subscriptionsRequest()]`. Other plugin requests require a
158
+ custom worker that registers those plugins. A supplied SharedWorker factory is
159
+ required for `transport: 'shared-worker'`; that worker must register the same
160
+ requested plugins. `auto` retains the core worker's dedicated fallback behavior.
161
+
162
+ `makeDirect` and `layerDirect` own a direct backend in the current `Scope` and
163
+ must run where OPFS synchronous access handles are available. Configured direct
164
+ plugins must be created by a fresh `plugins` thunk for each acquisition.
165
+ `unsafeBackend` only borrows that backend: do not call `closeVfs`, and do not use
166
+ it after the owning scope closes.
167
+
168
+ `examples/worker-session.ts` is a runnable worker-backed save/read session.
169
+ `examples/direct.ts` shows the direct API.
170
+
171
+ `examples/encrypted-session.ts` shows an application-owned worker that registers
172
+ encryption and subscriptions, inspects before opening an existing encrypted
173
+ volume, and keeps one `ManagedRuntime` for the session. Its save controller
174
+ serializes work and credential replacement, preserves use and disposal exits,
175
+ and never replays the failed save. Closing stops admission immediately, abandons
176
+ a pending credential prompt, ignores late answers, and waits for runtime disposal.
177
+ The creation helper is the only path that
178
+ accepts an initial passkey. Its controller config must match the initial
179
+ runtime's file name, profile, and plugin order; a replacement supplies a new
180
+ secret only. The paired
181
+ `examples/encrypted-session.worker.ts` is application code; the base adapter
182
+ does not depend on the premium encryption package.
183
+
184
+ An existing unlocked owner can accept a follower without authenticating that
185
+ follower's secret. The secret is checked only if the follower takes ownership,
186
+ which can then fail. `Redacted` protects display while wrapped, but requests
187
+ and layers can retain revealed secrets and do not zeroize them. Mounting does
188
+ not encrypt or migrate existing plaintext storage; no format migration is
189
+ introduced here.
190
+
191
+ The encrypted packed check is explicit and requires a reviewed local candidate:
192
+
193
+ ```sh
194
+ OPFS_VFS_ENCRYPTION_TARBALL=/absolute/path/plugin-encryption.tgz \
195
+ OPFS_VFS_ENCRYPTION_SHA256=<sha256> pnpm --filter @opfs-vfs/effect test:encrypted-packed
196
+ ```
197
+
198
+ ### Conformance and release preparation
199
+
200
+ Run the shared Node, direct-worker, and worker-backed filesystem contract from
201
+ the repository root with
202
+ `OPFS_VFS_TEST_BROWSER=chromium pnpm --filter @opfs-vfs/effect test:conformance`;
203
+ set the browser variable to `webkit` for the second browser engine. Node's
204
+ `@effect/platform-node` adapter is a development-only test dependency and is
205
+ loaded only by the separate Node test configuration.
206
+
207
+ These checks validate the exact local workspace candidates. A Changesets
208
+ preview or local tarball is not a published release. Final registry peer ranges
209
+ and the private encryption package's core pin must be checked against the
210
+ versioned core release before claiming released-package compatibility; this
211
+ test command does not version or publish packages.
@@ -0,0 +1,50 @@
1
+ import { Deferred, Effect, Semaphore } from 'effect';
2
+ import type { Scope } from 'effect';
3
+ import type { OpfsVfs } from '@opfs-vfs/opfs-vfs';
4
+ import type { OpfsVfsWorkerClient } from '@opfs-vfs/opfs-vfs/worker-client';
5
+ import type { VolumeService } from './volume.js';
6
+ import type { VolumeError } from './errors.js';
7
+ export type Backend = OpfsVfs | OpfsVfsWorkerClient;
8
+ export type Continuity = {
9
+ readonly _tag: 'clean';
10
+ } | {
11
+ readonly _tag: 'pending';
12
+ readonly generation: string;
13
+ } | {
14
+ readonly _tag: 'lost';
15
+ readonly generation: string;
16
+ };
17
+ export interface SubscriptionSetupRecord {
18
+ readonly generation: string;
19
+ readonly closed: Promise<{
20
+ readonly status: 'released';
21
+ } | {
22
+ readonly status: 'unknown';
23
+ readonly error: unknown;
24
+ }>;
25
+ state: 'active' | 'retiring';
26
+ }
27
+ export interface Coordinator {
28
+ readonly backend: Backend;
29
+ readonly scope?: Scope.Scope;
30
+ readonly fileName: string;
31
+ readonly isClosed: () => boolean;
32
+ readonly currentGeneration: () => string | undefined;
33
+ readonly canRecapture: () => boolean;
34
+ readonly readinessTimeout: number;
35
+ readonly subscriptionsAvailable: boolean;
36
+ readonly terminal: () => VolumeError | undefined;
37
+ readonly awaitReady: (budget: {
38
+ remaining: number;
39
+ }, operation: string) => Effect.Effect<string, VolumeError>;
40
+ readonly gate: Semaphore.Semaphore;
41
+ readonly terminalSignal: Deferred.Deferred<VolumeError>;
42
+ readonly files: Set<() => Effect.Effect<void>>;
43
+ readonly subscriptionSetups: Map<SubscriptionSetupRecord['closed'], SubscriptionSetupRecord>;
44
+ readonly subscriptionUnknown: Map<string, unknown>;
45
+ continuity: Continuity;
46
+ }
47
+ export declare const makeCoordinator: (options: Omit<Coordinator, 'gate' | 'terminalSignal' | 'continuity' | 'files' | 'subscriptionSetups' | 'subscriptionUnknown'>) => Coordinator;
48
+ export declare const registerCoordinator: (service: VolumeService, coordinator: Coordinator) => void;
49
+ export declare const getCoordinator: (service: VolumeService) => Coordinator | undefined;
50
+ export declare const latchTerminal: (coordinator: Coordinator, error: VolumeError) => void;
@@ -0,0 +1,76 @@
1
+ import { Schema } from 'effect';
2
+ export declare const RemoteErrorDetails: Schema.Struct<{
3
+ readonly message: Schema.String;
4
+ readonly name: Schema.optionalKey<Schema.String>;
5
+ readonly code: Schema.optionalKey<Schema.String>;
6
+ readonly errno: Schema.optionalKey<Schema.Number>;
7
+ readonly category: Schema.optionalKey<Schema.Literals<readonly ["meta-snapshot", "data-wal", "bitmap", "meta-log", "format-version"]>>;
8
+ readonly offset: Schema.optionalKey<Schema.Number>;
9
+ }>;
10
+ export interface RemoteErrorDetails extends Schema.Schema.Type<typeof RemoteErrorDetails> {
11
+ }
12
+ export type ErrorOutcome = 'not-applied' | 'possibly-applied' | 'unknown';
13
+ export type VolumeErrorKind = 'configuration' | 'unsupported' | 'filesystem' | 'conflict' | 'quota' | 'corruption' | 'encryption' | 'lifecycle' | 'subscription' | 'persistence' | 'unknown';
14
+ declare const VolumeError_base: Schema.Class<VolumeError, Schema.TaggedStruct<"VolumeError", {
15
+ readonly fileName: Schema.NullOr<Schema.String>;
16
+ readonly operation: Schema.String;
17
+ readonly path: Schema.optionalKey<Schema.String>;
18
+ readonly code: Schema.optionalKey<Schema.String>;
19
+ readonly outcome: Schema.Literals<readonly ["not-applied", "possibly-applied", "unknown"]>;
20
+ readonly details: Schema.NullOr<Schema.Struct<{
21
+ readonly message: Schema.String;
22
+ readonly name: Schema.optionalKey<Schema.String>;
23
+ readonly code: Schema.optionalKey<Schema.String>;
24
+ readonly errno: Schema.optionalKey<Schema.Number>;
25
+ readonly category: Schema.optionalKey<Schema.Literals<readonly ["meta-snapshot", "data-wal", "bitmap", "meta-log", "format-version"]>>;
26
+ readonly offset: Schema.optionalKey<Schema.Number>;
27
+ }>>;
28
+ readonly cause: Schema.optionalKey<Schema.Defect>;
29
+ readonly kind: Schema.Literals<readonly ["configuration", "unsupported", "filesystem", "conflict", "quota", "corruption", "encryption", "lifecycle", "subscription", "persistence", "unknown"]>;
30
+ }>, import("effect/Cause").YieldableError>;
31
+ export declare class VolumeError extends VolumeError_base {
32
+ }
33
+ declare const EncryptionError_base: Schema.Class<EncryptionError, Schema.TaggedStruct<"EncryptionError", {
34
+ readonly fileName: Schema.NullOr<Schema.String>;
35
+ readonly operation: Schema.String;
36
+ readonly path: Schema.optionalKey<Schema.String>;
37
+ readonly code: Schema.optionalKey<Schema.String>;
38
+ readonly outcome: Schema.Literals<readonly ["not-applied", "possibly-applied", "unknown"]>;
39
+ readonly details: Schema.NullOr<Schema.Struct<{
40
+ readonly message: Schema.String;
41
+ readonly name: Schema.optionalKey<Schema.String>;
42
+ readonly code: Schema.optionalKey<Schema.String>;
43
+ readonly errno: Schema.optionalKey<Schema.Number>;
44
+ readonly category: Schema.optionalKey<Schema.Literals<readonly ["meta-snapshot", "data-wal", "bitmap", "meta-log", "format-version"]>>;
45
+ readonly offset: Schema.optionalKey<Schema.Number>;
46
+ }>>;
47
+ readonly cause: Schema.optionalKey<Schema.Defect>;
48
+ readonly reason: Schema.Literals<readonly ["CredentialsRejected", "KeyDerivationFailed", "VaultCorrupt", "UnsupportedFormat", "SidecarCorrupt", "IntegrityFailure", "PlaintextVolume"]>;
49
+ }>, import("effect/Cause").YieldableError>;
50
+ export declare class EncryptionError extends EncryptionError_base {
51
+ }
52
+ export type SubscriptionErrorCode = 'SUBSCRIPTION_OVERFLOW' | 'SUBSCRIPTION_INTERRUPTED' | 'SUBSCRIPTION_CALLBACK_FAILED' | 'SUBSCRIPTION_RESYNC_REQUIRED' | 'SUBSCRIPTION_RETIREMENT_UNKNOWN' | 'SUBSCRIPTION_SETUP_FAILED' | 'EINVAL' | 'EBADF';
53
+ declare const SubscriptionError_base: Schema.Class<SubscriptionError, Schema.TaggedStruct<"SubscriptionError", {
54
+ readonly code: Schema.Literals<readonly ["SUBSCRIPTION_OVERFLOW", "SUBSCRIPTION_INTERRUPTED", "SUBSCRIPTION_CALLBACK_FAILED", "SUBSCRIPTION_RESYNC_REQUIRED", "SUBSCRIPTION_RETIREMENT_UNKNOWN", "SUBSCRIPTION_SETUP_FAILED", "EINVAL", "EBADF"]>;
55
+ readonly fileName: Schema.String;
56
+ readonly path: Schema.String;
57
+ readonly sourceCode: Schema.optionalKey<Schema.String>;
58
+ readonly details: Schema.NullOr<Schema.Struct<{
59
+ readonly message: Schema.String;
60
+ readonly name: Schema.optionalKey<Schema.String>;
61
+ readonly code: Schema.optionalKey<Schema.String>;
62
+ readonly errno: Schema.optionalKey<Schema.Number>;
63
+ readonly category: Schema.optionalKey<Schema.Literals<readonly ["meta-snapshot", "data-wal", "bitmap", "meta-log", "format-version"]>>;
64
+ readonly offset: Schema.optionalKey<Schema.Number>;
65
+ }>>;
66
+ readonly cause: Schema.optionalKey<Schema.Defect>;
67
+ }>, import("effect/Cause").YieldableError>;
68
+ export declare class SubscriptionError extends SubscriptionError_base {
69
+ }
70
+ export declare const MountError: Schema.Union<readonly [typeof VolumeError, typeof EncryptionError]>;
71
+ export type MountError = typeof MountError.Type;
72
+ export declare const remoteDetails: (error: unknown) => RemoteErrorDetails;
73
+ export declare const classify: (details: RemoteErrorDetails) => VolumeErrorKind;
74
+ export declare const volumeError: (error: unknown, fileName: string | null, operation: string, kind?: VolumeErrorKind, outcome?: ErrorOutcome, path?: string) => VolumeError;
75
+ export declare const mountError: (error: unknown, fileName: string | null, operation: string, kind?: VolumeErrorKind, outcome?: ErrorOutcome) => MountError;
76
+ export {};
package/dist/errors.js ADDED
@@ -0,0 +1,159 @@
1
+ import { Schema as e } from "effect";
2
+ //#region src/errors.ts
3
+ var t = [
4
+ "meta-snapshot",
5
+ "data-wal",
6
+ "bitmap",
7
+ "meta-log",
8
+ "format-version"
9
+ ], n = e.Struct({
10
+ message: e.String,
11
+ name: e.optionalKey(e.String),
12
+ code: e.optionalKey(e.String),
13
+ errno: e.optionalKey(e.Number),
14
+ category: e.optionalKey(e.Literals(t)),
15
+ offset: e.optionalKey(e.Number)
16
+ }), r = {
17
+ fileName: e.NullOr(e.String),
18
+ operation: e.String,
19
+ path: e.optionalKey(e.String),
20
+ code: e.optionalKey(e.String),
21
+ outcome: e.Literals([
22
+ "not-applied",
23
+ "possibly-applied",
24
+ "unknown"
25
+ ]),
26
+ details: e.NullOr(n),
27
+ cause: e.optionalKey(e.Defect())
28
+ }, i = class extends e.TaggedError()("VolumeError", {
29
+ kind: e.Literals([
30
+ "configuration",
31
+ "unsupported",
32
+ "filesystem",
33
+ "conflict",
34
+ "quota",
35
+ "corruption",
36
+ "encryption",
37
+ "lifecycle",
38
+ "subscription",
39
+ "persistence",
40
+ "unknown"
41
+ ]),
42
+ ...r
43
+ }) {}, a = class extends e.TaggedError()("EncryptionError", {
44
+ reason: e.Literals([
45
+ "CredentialsRejected",
46
+ "KeyDerivationFailed",
47
+ "VaultCorrupt",
48
+ "UnsupportedFormat",
49
+ "SidecarCorrupt",
50
+ "IntegrityFailure",
51
+ "PlaintextVolume"
52
+ ]),
53
+ ...r
54
+ }) {}, o = class extends e.TaggedError()("SubscriptionError", {
55
+ code: e.Literals([
56
+ "SUBSCRIPTION_OVERFLOW",
57
+ "SUBSCRIPTION_INTERRUPTED",
58
+ "SUBSCRIPTION_CALLBACK_FAILED",
59
+ "SUBSCRIPTION_RESYNC_REQUIRED",
60
+ "SUBSCRIPTION_RETIREMENT_UNKNOWN",
61
+ "SUBSCRIPTION_SETUP_FAILED",
62
+ "EINVAL",
63
+ "EBADF"
64
+ ]),
65
+ fileName: e.String,
66
+ path: e.String,
67
+ sourceCode: e.optionalKey(e.String),
68
+ details: e.NullOr(n),
69
+ cause: e.optionalKey(e.Defect())
70
+ }) {}, s = e.Union([i, a]), c = (e, t) => {
71
+ try {
72
+ return Reflect.get(e, t);
73
+ } catch {
74
+ return;
75
+ }
76
+ }, l = (e) => {
77
+ let n = e !== null && (typeof e == "object" || typeof e == "function") ? e : null, r = n ? c(n, "message") : void 0, i = (e) => {
78
+ if (e.length <= 1024) return e;
79
+ let t = 1023, n = e.charCodeAt(t - 1);
80
+ return n >= 55296 && n <= 56319 && t--, `${e.slice(0, t)}…`;
81
+ }, a;
82
+ if (typeof r == "string") a = i(r);
83
+ else try {
84
+ a = i(String(e));
85
+ } catch {
86
+ a = "Unknown VFS error";
87
+ }
88
+ let o = n ? c(n, "name") : void 0, s = n ? c(n, "code") : void 0, l = n ? c(n, "errno") : void 0, u = n ? c(n, "category") : void 0, d = n ? c(n, "offset") : void 0;
89
+ return {
90
+ message: a,
91
+ ...typeof o == "string" && /^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(o) && o !== "Error" ? { name: o } : {},
92
+ ...typeof s == "string" && /^[A-Z][A-Z0-9_]{0,63}$/.test(s) ? { code: s } : {},
93
+ ...typeof l == "number" && Number.isSafeInteger(l) ? { errno: l } : {},
94
+ ...typeof u == "string" && t.includes(u) ? { category: u } : {},
95
+ ...typeof d == "number" && Number.isSafeInteger(d) && d >= 0 ? { offset: d } : {}
96
+ };
97
+ }, u = /* @__PURE__ */ new Set([
98
+ "VFS_SHUTTING_DOWN",
99
+ "VFS_ATTACHMENT_LOST",
100
+ "VFS_PROTOCOL_MISMATCH",
101
+ "VFS_PLUGIN_MISMATCH",
102
+ "VFS_NOT_LEADER",
103
+ "VFS_LEADER_NOT_READY",
104
+ "VFS_WORKER_FAILED",
105
+ "VFS_INITIALIZATION_TIMEOUT",
106
+ "VFS_OWNER_READY_TIMEOUT",
107
+ "LEADER_RESPONSE_TIMEOUT",
108
+ "VOLUME_IMPORTING"
109
+ ]), d = {
110
+ EVOLUMELOCKED: "CredentialsRejected",
111
+ EKDF: "KeyDerivationFailed",
112
+ EVAULTCORRUPT: "VaultCorrupt",
113
+ EVAULTFORMAT: "UnsupportedFormat",
114
+ ECRYPTSIDECAR: "SidecarCorrupt",
115
+ ECRYPTOINTEGRITY: "IntegrityFailure",
116
+ EPLAINTEXTVOLUME: "PlaintextVolume"
117
+ }, f = (e) => {
118
+ let { code: t, category: n, name: r } = e;
119
+ return t === "VFS_WORKER_FACTORY_FAILED" ? "configuration" : n || [
120
+ "VfsCorruptionError",
121
+ "MetaSnapshotCorruptionError",
122
+ "DataWalCorruptionError"
123
+ ].includes(r ?? "") ? "corruption" : t && d[t] ? "encryption" : t === "VFS_STORAGE_PLUGIN_REQUIRED" ? "unsupported" : t === "VFS_SYNC_OWNER_CHANGED" || t === "VFS_ACK_OWNER_CHANGED" ? "persistence" : t === "VFS_SUBSCRIPTION_RETIREMENT_TIMEOUT" ? "subscription" : t && u.has(t) ? "lifecycle" : t?.startsWith("SUBSCRIPTION_") ? "subscription" : t === "ENOSPC" || r === "QuotaExceededError" ? "quota" : t === "EEXIST" ? "conflict" : /^E[A-Z0-9]+$/.test(t ?? "") ? "filesystem" : "unknown";
124
+ }, p = (t) => {
125
+ try {
126
+ return e.is(i)(t) || e.is(a)(t) || e.is(o)(t) ? t : void 0;
127
+ } catch {
128
+ return;
129
+ }
130
+ }, m = (e, t, n, r, a = "unknown", o) => {
131
+ let s = l(e), c = p(e), u = {
132
+ fileName: typeof t == "string" ? t : null,
133
+ operation: n,
134
+ ...o === void 0 ? {} : { path: o },
135
+ ...s.code ? { code: s.code } : {},
136
+ outcome: a,
137
+ details: s,
138
+ ...c === void 0 ? {} : { cause: c }
139
+ }, d = f(s);
140
+ return new i({
141
+ kind: r === "configuration" ? r : d === "unknown" ? r ?? d : d,
142
+ ...u
143
+ });
144
+ }, h = (e, t, n, r, i = "unknown") => {
145
+ let o = l(e), s = f(o), c = r === "configuration" || s === "unknown" ? r : s, u = c === "configuration" ? void 0 : o.code ? d[o.code] : void 0;
146
+ if (!u) return m(e, t, n, c, i);
147
+ let h = p(e);
148
+ return new a({
149
+ reason: u,
150
+ fileName: t,
151
+ operation: n,
152
+ ...o.code ? { code: o.code } : {},
153
+ outcome: i,
154
+ details: o,
155
+ ...h === void 0 ? {} : { cause: h }
156
+ });
157
+ };
158
+ //#endregion
159
+ export { a as EncryptionError, s as MountError, n as RemoteErrorDetails, o as SubscriptionError, i as VolumeError, f as classify, h as mountError, l as remoteDetails, m as volumeError };
@@ -0,0 +1,9 @@
1
+ import { FileSystem, Layer } from 'effect';
2
+ import { Volume } from './volume.js';
3
+ import type { VolumeService } from './volume.js';
4
+ declare const make: (volume: VolumeService) => FileSystem.FileSystem;
5
+ export declare const OpfsFileSystem: {
6
+ readonly make: typeof make;
7
+ readonly layer: Layer.Layer<FileSystem.FileSystem, never, Volume>;
8
+ };
9
+ export {};
@@ -0,0 +1,6 @@
1
+ export * as Volume from './volume.js';
2
+ export * as Subscriptions from './subscriptions.js';
3
+ export { OpfsFileSystem } from './filesystem.js';
4
+ export { EncryptionError, MountError, RemoteErrorDetails, SubscriptionError, VolumeError } from './errors.js';
5
+ export type { ErrorOutcome, SubscriptionErrorCode, VolumeErrorKind } from './errors.js';
6
+ export type { PersistenceSnapshot, VolumeService, DirectMountOptions, WorkerMountOptions } from './volume.js';