@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 +73 -0
- package/README.md +211 -0
- package/dist/coordinator.d.ts +50 -0
- package/dist/errors.d.ts +76 -0
- package/dist/errors.js +159 -0
- package/dist/filesystem.d.ts +9 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1031 -0
- package/dist/subscriptions-internal.d.ts +11 -0
- package/dist/subscriptions.d.ts +24 -0
- package/dist/volume-BIqpe5Vt.js +357 -0
- package/dist/volume.d.ts +52 -0
- package/dist/volume.js +3 -0
- package/examples/direct.ts +8 -0
- package/examples/encrypted-session.ts +347 -0
- package/examples/encrypted-session.worker.ts +5 -0
- package/examples/filesystem-save.ts +30 -0
- package/examples/filesystem-stream.ts +20 -0
- package/examples/reconciled-view.ts +74 -0
- package/examples/subscriptions.ts +20 -0
- package/examples/worker-session.ts +32 -0
- package/package.json +64 -0
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;
|
package/dist/errors.d.ts
ADDED
|
@@ -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 {};
|
package/dist/index.d.ts
ADDED
|
@@ -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';
|