@dxos/plugin-wnfs 0.10.0 → 0.11.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/PLUGIN.mdl +92 -46
- package/dist/lib/WnfsPlugin.mjs +28 -0
- package/dist/lib/WnfsPlugin.mjs.map +1 -0
- package/dist/lib/WnfsPlugin.node.mjs +8 -0
- package/dist/lib/WnfsPlugin.node.mjs.map +1 -0
- package/dist/lib/WnfsPlugin.workerd.mjs +8 -0
- package/dist/lib/WnfsPlugin.workerd.mjs.map +1 -0
- package/dist/lib/capabilities.mjs +8 -0
- package/dist/lib/capabilities.mjs.map +1 -0
- package/dist/lib/chunk-blob-backend.mjs +118 -0
- package/dist/lib/chunk-blob-backend.mjs.map +1 -0
- package/dist/lib/chunk-dependencies.mjs +146 -0
- package/dist/lib/chunk-dependencies.mjs.map +1 -0
- package/dist/lib/chunk-helpers.mjs +198 -0
- package/dist/lib/chunk-helpers.mjs.map +1 -0
- package/dist/lib/chunk-meta.mjs +38 -0
- package/dist/lib/chunk-meta.mjs.map +1 -0
- package/dist/lib/chunk-types.mjs +46 -0
- package/dist/lib/chunk-types.mjs.map +1 -0
- package/dist/lib/helpers.mjs +2 -0
- package/dist/lib/index.mjs +4 -0
- package/dist/lib/meta.mjs +2 -0
- package/dist/lib/plugin.mjs +8 -0
- package/dist/lib/plugin.mjs.map +1 -0
- package/dist/lib/translations.mjs +7 -0
- package/dist/lib/translations.mjs.map +1 -0
- package/dist/lib/types.mjs +2 -0
- package/dist/types/src/WnfsPlugin.d.ts.map +1 -1
- package/dist/types/src/capabilities/blob-backend.d.ts +22 -0
- package/dist/types/src/capabilities/blob-backend.d.ts.map +1 -0
- package/dist/types/src/capabilities/blob-backend.test.d.ts +2 -0
- package/dist/types/src/capabilities/blob-backend.test.d.ts.map +1 -0
- package/dist/types/src/capabilities/dependencies.d.ts +5 -0
- package/dist/types/src/capabilities/dependencies.d.ts.map +1 -0
- package/dist/types/src/capabilities/index.d.ts +2 -3
- package/dist/types/src/capabilities/index.d.ts.map +1 -1
- package/dist/types/src/helpers/wnfs-url.d.ts +6 -0
- package/dist/types/src/helpers/wnfs-url.d.ts.map +1 -1
- package/dist/types/src/types/WnfsCapabilities.d.ts +7 -0
- package/dist/types/src/types/WnfsCapabilities.d.ts.map +1 -1
- package/dist/types/src/types/WnfsEvents.d.ts +11 -0
- package/dist/types/src/types/WnfsEvents.d.ts.map +1 -0
- package/dist/types/src/types/index.d.ts +1 -0
- package/dist/types/src/types/index.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/dx.config.ts +4 -4
- package/package.json +34 -32
- package/src/WnfsPlugin.tsx +9 -24
- package/src/capabilities/blob-backend.test.ts +108 -0
- package/src/capabilities/blob-backend.ts +104 -0
- package/src/capabilities/{blockstore.ts → dependencies.ts} +6 -1
- package/src/capabilities/index.ts +2 -4
- package/src/helpers/wnfs-url.ts +22 -4
- package/src/types/WnfsCapabilities.ts +9 -0
- package/src/types/WnfsEvents.ts +19 -0
- package/src/types/index.ts +1 -0
- package/dist/lib/neutral/WnfsPlugin.mjs +0 -51
- package/dist/lib/neutral/WnfsPlugin.mjs.map +0 -7
- package/dist/lib/neutral/WnfsPlugin.node.mjs +0 -12
- package/dist/lib/neutral/WnfsPlugin.node.mjs.map +0 -7
- package/dist/lib/neutral/WnfsPlugin.workerd.mjs +0 -12
- package/dist/lib/neutral/WnfsPlugin.workerd.mjs.map +0 -7
- package/dist/lib/neutral/backend-4HPTHS33.mjs +0 -47
- package/dist/lib/neutral/backend-4HPTHS33.mjs.map +0 -7
- package/dist/lib/neutral/blockstore-U6SPNL2C.mjs +0 -190
- package/dist/lib/neutral/blockstore-U6SPNL2C.mjs.map +0 -7
- package/dist/lib/neutral/capabilities/index.mjs +0 -13
- package/dist/lib/neutral/capabilities/index.mjs.map +0 -7
- package/dist/lib/neutral/chunk-2WLTMUHV.mjs +0 -19
- package/dist/lib/neutral/chunk-2WLTMUHV.mjs.map +0 -7
- package/dist/lib/neutral/chunk-H2YCLSRI.mjs +0 -47
- package/dist/lib/neutral/chunk-H2YCLSRI.mjs.map +0 -7
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs +0 -10
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +0 -7
- package/dist/lib/neutral/chunk-L33MOD23.mjs +0 -219
- package/dist/lib/neutral/chunk-L33MOD23.mjs.map +0 -7
- package/dist/lib/neutral/helpers/index.mjs +0 -26
- package/dist/lib/neutral/helpers/index.mjs.map +0 -7
- package/dist/lib/neutral/index.mjs +0 -36
- package/dist/lib/neutral/index.mjs.map +0 -7
- package/dist/lib/neutral/meta.json +0 -1
- package/dist/lib/neutral/meta.mjs +0 -8
- package/dist/lib/neutral/meta.mjs.map +0 -7
- package/dist/lib/neutral/plugin.mjs +0 -12
- package/dist/lib/neutral/plugin.mjs.map +0 -7
- package/dist/lib/neutral/translations.mjs +0 -17
- package/dist/lib/neutral/translations.mjs.map +0 -7
- package/dist/lib/neutral/types/index.mjs +0 -8
- package/dist/lib/neutral/types/index.mjs.map +0 -7
- package/dist/lib/neutral/url-resolver-BIRH7WHL.mjs +0 -47
- package/dist/lib/neutral/url-resolver-BIRH7WHL.mjs.map +0 -7
- package/dist/types/src/capabilities/backend.d.ts +0 -6
- package/dist/types/src/capabilities/backend.d.ts.map +0 -1
- package/dist/types/src/capabilities/blockstore.d.ts +0 -5
- package/dist/types/src/capabilities/blockstore.d.ts.map +0 -1
- package/dist/types/src/capabilities/url-resolver.d.ts +0 -6
- package/dist/types/src/capabilities/url-resolver.d.ts.map +0 -1
- package/src/capabilities/backend.ts +0 -43
- package/src/capabilities/url-resolver.ts +0 -44
package/PLUGIN.mdl
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
id: org.dxos.plugin.wnfs
|
|
3
3
|
name: WnfsPlugin
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
A decentralized file storage plugin for `DXOS` Composer built on the Web Native File System (WNFS) protocol.
|
|
8
8
|
|
|
9
9
|
WNFS provides a content-addressed, end-to-end encrypted private file system using a cryptographic "dark forest"
|
|
10
10
|
structure. Files are stored as IPLD blocks referenced by CID, replicated to a DXOS edge blob service, and
|
|
11
|
-
cached locally in IndexedDB. The plugin
|
|
12
|
-
|
|
11
|
+
cached locally in IndexedDB. The plugin registers a `BlobBackend` (schemes: `['wnfs']`) on the ECHO Hypergraph
|
|
12
|
+
via `client.graph.registerBlobBackend`, and contributes a matching `FileCapabilities.Backend` descriptor
|
|
13
|
+
(`{ storage: 'wnfs' }`) so `plugin-file` — and any plugin built on it, e.g. `plugin-gallery` — can
|
|
14
|
+
upload files and resolve `wnfs://` URIs uniformly via the core `Blob.fromBytes`/`Blob.url`/`Blob.read` API,
|
|
13
15
|
without knowing the underlying storage layer. Uploads are scoped to a DXOS Space; the private forest root CID
|
|
14
16
|
is persisted on the space's ECHO properties and updated atomically on every write.
|
|
15
17
|
|
|
@@ -44,7 +46,9 @@ type WnfsPath
|
|
|
44
46
|
|
|
45
47
|
```mdl
|
|
46
48
|
type FileInfo
|
|
47
|
-
desc:
|
|
49
|
+
desc: |
|
|
50
|
+
Internal return value of the `upload` helper. Only `.url` is surfaced to the outside world,
|
|
51
|
+
as the `uri` field of the `BlobBackend.put` PutResponse.
|
|
48
52
|
fields:
|
|
49
53
|
name: string # Original file name
|
|
50
54
|
type: string # MIME type
|
|
@@ -75,7 +79,6 @@ type WnfsInstance
|
|
|
75
79
|
directory: PrivateDirectory
|
|
76
80
|
forest: PrivateForest
|
|
77
81
|
```
|
|
78
|
-
```
|
|
79
82
|
|
|
80
83
|
## Operations
|
|
81
84
|
|
|
@@ -97,7 +100,64 @@ op getPathFromUrl
|
|
|
97
100
|
output: string[]
|
|
98
101
|
```
|
|
99
102
|
|
|
100
|
-
|
|
103
|
+
BlobBackend implementation — registered on the ECHO Hypergraph, dispatched to by core `Blob.fromBytes` /
|
|
104
|
+
`Blob.read` / `Blob.exists` / `Blob.url` whenever a Blob's storage or URI scheme resolves to `wnfs`.
|
|
105
|
+
|
|
106
|
+
```mdl
|
|
107
|
+
op wnfsBlobPut
|
|
108
|
+
desc: |
|
|
109
|
+
Implements BlobBackend.put. Resolves the target Space from spaceId, wraps the given bytes in a
|
|
110
|
+
browser File (named `name ?? contentHash`), and delegates to the uploadFile op. Returns
|
|
111
|
+
{ uri: info.url } (a wnfs:// URL) as the PutResponse.
|
|
112
|
+
input:
|
|
113
|
+
spaceId: SpaceId
|
|
114
|
+
data: Uint8Array
|
|
115
|
+
contentType?: string
|
|
116
|
+
contentHash: string # sha256 hex digest, computed by the core BlobManager; not re-verified here
|
|
117
|
+
name?: string
|
|
118
|
+
output: { uri: string }
|
|
119
|
+
effects: [fs:read, echo:write, http:post]
|
|
120
|
+
errors:
|
|
121
|
+
SpaceNotFound: no space matching spaceId is currently open (invariant failure)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```mdl
|
|
125
|
+
op wnfsBlobGet
|
|
126
|
+
desc: |
|
|
127
|
+
Implements BlobBackend.get. Resolves the Space from spaceId, loads or reuses the cached
|
|
128
|
+
WnfsInstance, and reads the file bytes at the given wnfs:// URI (via getPathFromUrl + directory.read).
|
|
129
|
+
input:
|
|
130
|
+
spaceId: SpaceId
|
|
131
|
+
uri: string
|
|
132
|
+
output: Uint8Array | undefined
|
|
133
|
+
effects: [fs:read, http:get]
|
|
134
|
+
note: Returns undefined (never throws) if the space is not open or the read fails for any reason.
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```mdl
|
|
138
|
+
op wnfsBlobHas
|
|
139
|
+
desc: Implements BlobBackend.has by delegating to the same read path as wnfsBlobGet and checking for a defined result.
|
|
140
|
+
input:
|
|
141
|
+
spaceId: SpaceId
|
|
142
|
+
uri: string
|
|
143
|
+
output: boolean
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```mdl
|
|
147
|
+
op wnfsBlobGetUrl
|
|
148
|
+
desc: |
|
|
149
|
+
Implements BlobBackend.getUrl. Resolves the Space, loads or reuses the cached WnfsInstance,
|
|
150
|
+
reads the file bytes, wraps them in a Blob, and returns an object URL for in-browser display.
|
|
151
|
+
input:
|
|
152
|
+
spaceId: SpaceId
|
|
153
|
+
uri: string
|
|
154
|
+
contentType?: string
|
|
155
|
+
output: string | undefined
|
|
156
|
+
effects: [fs:read, http:get]
|
|
157
|
+
note: Returns undefined (never throws) if the space is not open or reading fails.
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Effectful operations underlying the BlobBackend implementation above.
|
|
101
161
|
|
|
102
162
|
```mdl
|
|
103
163
|
op uploadFile
|
|
@@ -119,22 +179,6 @@ op uploadFile
|
|
|
119
179
|
the forest CID property is only updated after the forest is stored.
|
|
120
180
|
```
|
|
121
181
|
|
|
122
|
-
```mdl
|
|
123
|
-
op resolveWnfsUrl
|
|
124
|
-
desc: |
|
|
125
|
-
Reads file bytes from the WNFS private directory for a wnfs:// URL,
|
|
126
|
-
wraps them in a Blob, and returns an object URL for in-browser display.
|
|
127
|
-
input:
|
|
128
|
-
wnfsUrl: string
|
|
129
|
-
space: Space
|
|
130
|
-
type?: string # MIME type hint for Blob construction
|
|
131
|
-
output: string # blob: object URL
|
|
132
|
-
effects: [fs:read, http:get]
|
|
133
|
-
errors:
|
|
134
|
-
UrlNotResolvable: space not available or file not found in directory
|
|
135
|
-
note: Errors are swallowed and undefined is returned to avoid breaking image renders.
|
|
136
|
-
```
|
|
137
|
-
|
|
138
182
|
```mdl
|
|
139
183
|
op flushBlockQueue
|
|
140
184
|
desc: |
|
|
@@ -156,9 +200,10 @@ op flushBlockQueue
|
|
|
156
200
|
feat F-1: Upload File
|
|
157
201
|
|
|
158
202
|
req F-1.1:
|
|
159
|
-
when:
|
|
160
|
-
|
|
161
|
-
|
|
203
|
+
when: core Blob.fromBytes routes to the 'wnfs' storage (i.e. BlobBackend.put is invoked
|
|
204
|
+
for this backend)
|
|
205
|
+
then: the file is written to the WNFS private directory and stored in the blockstore;
|
|
206
|
+
wnfsBlobPut returns { uri } to the caller
|
|
162
207
|
|
|
163
208
|
req F-1.2:
|
|
164
209
|
when: the write succeeds
|
|
@@ -166,27 +211,28 @@ feat F-1: Upload File
|
|
|
166
211
|
|
|
167
212
|
req F-1.3:
|
|
168
213
|
when: the upload completes
|
|
169
|
-
then:
|
|
214
|
+
then: the returned uri is a wnfs:// URL, embedded by core Blob.ts as the Blob object's
|
|
215
|
+
`data.uri` (an `external` BlobData variant)
|
|
170
216
|
```
|
|
171
217
|
|
|
172
218
|
```mdl
|
|
173
|
-
feat F-2: Resolve wnfs://
|
|
219
|
+
feat F-2: Resolve wnfs:// blobs
|
|
174
220
|
|
|
175
221
|
req F-2.1:
|
|
176
|
-
when:
|
|
177
|
-
then:
|
|
222
|
+
when: a Blob's URI scheme is 'wnfs' (i.e. this plugin's registered schemes include it)
|
|
223
|
+
then: core Blob.read / Blob.exists / Blob.url dispatch to this plugin's BlobBackend
|
|
178
224
|
|
|
179
225
|
req F-2.2:
|
|
180
|
-
when:
|
|
226
|
+
when: BlobBackend.get or .has is called with a wnfs:// uri
|
|
181
227
|
then: loads or reuses a cached WnfsInstance for the space and reads the file bytes
|
|
182
228
|
|
|
183
229
|
req F-2.3:
|
|
184
|
-
when:
|
|
185
|
-
then: a blob: object URL
|
|
230
|
+
when: BlobBackend.getUrl is called with a wnfs:// uri
|
|
231
|
+
then: bytes are read and wrapped in a blob: object URL for in-browser display
|
|
186
232
|
|
|
187
233
|
req F-2.4:
|
|
188
|
-
when: the space is
|
|
189
|
-
then: undefined
|
|
234
|
+
when: the space is not open, or reading/writing fails
|
|
235
|
+
then: get/has/getUrl resolve to undefined/false rather than throwing; put rejects
|
|
190
236
|
```
|
|
191
237
|
|
|
192
238
|
```mdl
|
|
@@ -241,29 +287,29 @@ feat F-4: Space-Scoped Private File System
|
|
|
241
287
|
## Acceptance
|
|
242
288
|
|
|
243
289
|
```mdl
|
|
244
|
-
test T-1: Upload creates
|
|
245
|
-
given: an active space and
|
|
246
|
-
when:
|
|
290
|
+
test T-1: Upload creates a wnfs:// blob
|
|
291
|
+
given: an active space and this plugin's BlobBackend registered on the ECHO Hypergraph
|
|
292
|
+
when: Blob.fromBytes is called with storage: 'wnfs'
|
|
247
293
|
then:
|
|
248
|
-
-
|
|
294
|
+
- the resulting Blob's data.uri starts with wnfs://
|
|
249
295
|
- space.properties.wnfs.privateForestCid is set
|
|
250
296
|
- block for the file is present in the local IndexedDB blockstore
|
|
251
297
|
```
|
|
252
298
|
|
|
253
299
|
```mdl
|
|
254
|
-
test T-2: Resolve uploaded
|
|
255
|
-
given: a
|
|
256
|
-
when:
|
|
300
|
+
test T-2: Resolve uploaded blob via getUrl
|
|
301
|
+
given: a blob was previously uploaded and its data.uri (a wnfs:// URI) is known
|
|
302
|
+
when: BlobBackend.getUrl(spaceId, uri, contentType) is called
|
|
257
303
|
then:
|
|
258
304
|
- a blob: object URL is returned
|
|
259
|
-
- URL can be used as an img src
|
|
305
|
+
- the URL can be used as an img src
|
|
260
306
|
```
|
|
261
307
|
|
|
262
308
|
```mdl
|
|
263
|
-
test T-3:
|
|
264
|
-
given: a wnfs://
|
|
265
|
-
when:
|
|
266
|
-
then: undefined is returned; no error is thrown
|
|
309
|
+
test T-3: get/has/getUrl return undefined/false when the space is not open
|
|
310
|
+
given: a wnfs:// URI whose space is not currently open
|
|
311
|
+
when: BlobBackend.get, .has, or .getUrl is called
|
|
312
|
+
then: undefined (or false for .has) is returned; no error is thrown
|
|
267
313
|
```
|
|
268
314
|
|
|
269
315
|
```mdl
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Plugin } from "@dxos/app-framework";
|
|
2
|
+
import { meta } from "#meta";
|
|
3
|
+
import { AppPlugin } from "@dxos/app-toolkit";
|
|
4
|
+
import { ClientEvents } from "@dxos/plugin-client";
|
|
5
|
+
import { BlobBackend, Dependencies } from "#capabilities";
|
|
6
|
+
import { translations } from "#translations";
|
|
7
|
+
import { WnfsEvents } from "#types";
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/WnfsPlugin.tsx
|
|
10
|
+
var WnfsPlugin = Plugin.define(meta).pipe(AppPlugin.addTranslationsModule({ translations }), Plugin.addModule({
|
|
11
|
+
id: "dependencies",
|
|
12
|
+
activatesOn: ClientEvents.ClientReady,
|
|
13
|
+
firesAfterActivation: [WnfsEvents.DependenciesReady],
|
|
14
|
+
activate: Dependencies
|
|
15
|
+
}), Plugin.addModule({
|
|
16
|
+
id: "blob-backend",
|
|
17
|
+
activatesOn: WnfsEvents.DependenciesReady,
|
|
18
|
+
activate: BlobBackend
|
|
19
|
+
}), AppPlugin.addPluginAssetModule({ asset: {
|
|
20
|
+
pluginId: meta.profile.key,
|
|
21
|
+
path: "PLUGIN.mdl",
|
|
22
|
+
content: "---\nid: org.dxos.plugin.wnfs\nname: WnfsPlugin\nversion: 0.2.0\n---\n\nA decentralized file storage plugin for `DXOS` Composer built on the Web Native File System (WNFS) protocol.\n\nWNFS provides a content-addressed, end-to-end encrypted private file system using a cryptographic \"dark forest\"\nstructure. Files are stored as IPLD blocks referenced by CID, replicated to a DXOS edge blob service, and\ncached locally in IndexedDB. The plugin registers a `BlobBackend` (schemes: `['wnfs']`) on the ECHO Hypergraph\nvia `client.graph.registerBlobBackend`, and contributes a matching `FileCapabilities.Backend` descriptor\n(`{ storage: 'wnfs' }`) so `plugin-file` — and any plugin built on it, e.g. `plugin-gallery` — can\nupload files and resolve `wnfs://` URIs uniformly via the core `Blob.fromBytes`/`Blob.url`/`Blob.read` API,\nwithout knowing the underlying storage layer. Uploads are scoped to a DXOS Space; the private forest root CID\nis persisted on the space's ECHO properties and updated atomically on every write.\n\n## Extensions\n\nThe following extension dialects are used in this document.\nEach extension is defined in the Appendix or resolved via its URI.\n\n| Term | URI |\n|-------------|--------------------------------|\n| `type` | `org.dxos.mdl.type@1.0` |\n| `feat` | `org.dxos.mdl.feat@1.0` |\n| `test` | `org.dxos.mdl.test@1.0` |\n| `component` | `org.dxos.mdl.component@1.0` |\n| `op` | `org.dxos.mdl.op@1.0` |\n\n## Types\n\n```mdl\ntype Cid\n desc: A content-addressed identifier encoded as a base-32 multibase string (CIDv1, sha2-256).\n fields:\n value: string\n```\n\n```mdl\ntype WnfsPath\n desc: A WNFS path encoded as a wnfs:// URL; segments are percent-encoded.\n fields:\n url: string # wnfs://spaces/<spaceId>/files/<cid>\n```\n\n```mdl\ntype FileInfo\n desc: |\n Internal return value of the `upload` helper. Only `.url` is surfaced to the outside world,\n as the `uri` field of the `BlobBackend.put` PutResponse.\n fields:\n name: string # Original file name\n type: string # MIME type\n url: WnfsPath # wnfs:// URL for later retrieval\n cid: Cid # Raw content CID of the file bytes\n```\n\n```mdl\ntype BlockstoreState\n desc: Runtime state of the MixedBlockstore.\n fields:\n isConnected: boolean # Online / offline status\n queueLength: number # Blocks pending remote flush\n apiHost: string # DXOS edge service base URL\n```\n\n```mdl\ntype WnfsInstances\n desc: In-memory cache mapping forest CID → loaded PrivateDirectory/PrivateForest pair.\n fields:\n entries: Record<string, WnfsInstance>\n```\n\n```mdl\ntype WnfsInstance\n desc: A loaded WNFS private file system rooted at a specific forest CID.\n fields:\n directory: PrivateDirectory\n forest: PrivateForest\n```\n\n## Operations\n\nPure operations — no external effects.\n\n```mdl\nop wnfsUrl\n desc: Encodes a WNFS path array into a wnfs:// URL with percent-encoded segments.\n input:\n filePath: string[]\n output: string\n```\n\n```mdl\nop getPathFromUrl\n desc: Decodes a wnfs:// URL back into a path segment array.\n input:\n wnfsUrl: string\n output: string[]\n```\n\nBlobBackend implementation — registered on the ECHO Hypergraph, dispatched to by core `Blob.fromBytes` /\n`Blob.read` / `Blob.exists` / `Blob.url` whenever a Blob's storage or URI scheme resolves to `wnfs`.\n\n```mdl\nop wnfsBlobPut\n desc: |\n Implements BlobBackend.put. Resolves the target Space from spaceId, wraps the given bytes in a\n browser File (named `name ?? contentHash`), and delegates to the uploadFile op. Returns\n { uri: info.url } (a wnfs:// URL) as the PutResponse.\n input:\n spaceId: SpaceId\n data: Uint8Array\n contentType?: string\n contentHash: string # sha256 hex digest, computed by the core BlobManager; not re-verified here\n name?: string\n output: { uri: string }\n effects: [fs:read, echo:write, http:post]\n errors:\n SpaceNotFound: no space matching spaceId is currently open (invariant failure)\n```\n\n```mdl\nop wnfsBlobGet\n desc: |\n Implements BlobBackend.get. Resolves the Space from spaceId, loads or reuses the cached\n WnfsInstance, and reads the file bytes at the given wnfs:// URI (via getPathFromUrl + directory.read).\n input:\n spaceId: SpaceId\n uri: string\n output: Uint8Array | undefined\n effects: [fs:read, http:get]\n note: Returns undefined (never throws) if the space is not open or the read fails for any reason.\n```\n\n```mdl\nop wnfsBlobHas\n desc: Implements BlobBackend.has by delegating to the same read path as wnfsBlobGet and checking for a defined result.\n input:\n spaceId: SpaceId\n uri: string\n output: boolean\n```\n\n```mdl\nop wnfsBlobGetUrl\n desc: |\n Implements BlobBackend.getUrl. Resolves the Space, loads or reuses the cached WnfsInstance,\n reads the file bytes, wraps them in a Blob, and returns an object URL for in-browser display.\n input:\n spaceId: SpaceId\n uri: string\n contentType?: string\n output: string | undefined\n effects: [fs:read, http:get]\n note: Returns undefined (never throws) if the space is not open or reading fails.\n```\n\nEffectful operations underlying the BlobBackend implementation above.\n\n```mdl\nop uploadFile\n desc: |\n Reads a File, writes its bytes into the WNFS private directory at a\n CID-derived path, persists the updated forest pointer to the space's\n ECHO properties, and returns FileInfo.\n input:\n file: File\n space: Space\n output: FileInfo\n effects: [fs:read, echo:write, http:post]\n errors:\n SpaceNotFound: no space matching the given spaceId\n BlockstoreError: local or remote block I/O failure\n note: |\n Uses sha256 for block hashing (not the rs-wnfs default Blake3) for\n broader runtime support. Writes are atomic from ECHO's perspective:\n the forest CID property is only updated after the forest is stored.\n```\n\n```mdl\nop flushBlockQueue\n desc: |\n Drains the pending-upload queue by assembling a CAR archive and POSTing\n it to the edge blob service. Called automatically (debounced 5 s) on\n block writes and on the browser `online` event.\n input:\n blockstore: Blockstore\n output: void\n effects: [http:post]\n errors:\n NetworkError: remote POST failed\n note: Queue is persisted to IndexedDB so pending blocks survive page reloads.\n```\n\n## Features\n\n```mdl\nfeat F-1: Upload File\n\n req F-1.1:\n when: core Blob.fromBytes routes to the 'wnfs' storage (i.e. BlobBackend.put is invoked\n for this backend)\n then: the file is written to the WNFS private directory and stored in the blockstore;\n wnfsBlobPut returns { uri } to the caller\n\n req F-1.2:\n when: the write succeeds\n then: the space's ECHO properties are updated with the new privateForestCid\n\n req F-1.3:\n when: the upload completes\n then: the returned uri is a wnfs:// URL, embedded by core Blob.ts as the Blob object's\n `data.uri` (an `external` BlobData variant)\n```\n\n```mdl\nfeat F-2: Resolve wnfs:// blobs\n\n req F-2.1:\n when: a Blob's URI scheme is 'wnfs' (i.e. this plugin's registered schemes include it)\n then: core Blob.read / Blob.exists / Blob.url dispatch to this plugin's BlobBackend\n\n req F-2.2:\n when: BlobBackend.get or .has is called with a wnfs:// uri\n then: loads or reuses a cached WnfsInstance for the space and reads the file bytes\n\n req F-2.3:\n when: BlobBackend.getUrl is called with a wnfs:// uri\n then: bytes are read and wrapped in a blob: object URL for in-browser display\n\n req F-2.4:\n when: the space is not open, or reading/writing fails\n then: get/has/getUrl resolve to undefined/false rather than throwing; put rejects\n```\n\n```mdl\nfeat F-3: Blockstore Sync\n\n req F-3.1:\n when: a block is written via blockstore.put\n then: the block is saved to IndexedDB and added to the upload queue\n\n req F-3.2:\n when: the browser is online and the queue is non-empty\n then: blocks are batched into a CAR archive and flushed to the edge service\n\n req F-3.3:\n when: the browser goes offline\n then: the queue is persisted to IndexedDB; no upload is attempted\n\n req F-3.4:\n when: the browser comes back online\n then: the flush is triggered automatically\n\n req F-3.5:\n when: a block is requested via blockstore.get and is present in IndexedDB\n then: the block is returned from the local cache without a network request\n\n req F-3.6:\n when: a block is not in IndexedDB and the browser is online\n then: the block is fetched from the edge service and cached locally\n```\n\n```mdl\nfeat F-4: Space-Scoped Private File System\n\n req F-4.1:\n when: a space has no existing WNFS properties\n then: a new PrivateForest and PrivateDirectory are created and stored\n\n req F-4.2:\n when: a space already has a privateForestCid\n then: the existing forest and directory are loaded from the blockstore\n\n req F-4.3:\n when: a WnfsInstance is already cached for the current forest CID\n then: the cached instance is reused without reloading from the blockstore\n\n req F-4.4:\n when: legacy snake_case WNFS properties are found on a space\n then: they are deleted and replaced with the current camelCase schema\n tags: [migration]\n```\n\n## Acceptance\n\n```mdl\ntest T-1: Upload creates a wnfs:// blob\n given: an active space and this plugin's BlobBackend registered on the ECHO Hypergraph\n when: Blob.fromBytes is called with storage: 'wnfs'\n then:\n - the resulting Blob's data.uri starts with wnfs://\n - space.properties.wnfs.privateForestCid is set\n - block for the file is present in the local IndexedDB blockstore\n```\n\n```mdl\ntest T-2: Resolve uploaded blob via getUrl\n given: a blob was previously uploaded and its data.uri (a wnfs:// URI) is known\n when: BlobBackend.getUrl(spaceId, uri, contentType) is called\n then:\n - a blob: object URL is returned\n - the URL can be used as an img src\n```\n\n```mdl\ntest T-3: get/has/getUrl return undefined/false when the space is not open\n given: a wnfs:// URI whose space is not currently open\n when: BlobBackend.get, .has, or .getUrl is called\n then: undefined (or false for .has) is returned; no error is thrown\n```\n\n```mdl\ntest T-4: Blockstore offline queuing\n given: the browser is offline\n when: a block is written to the blockstore\n then:\n - block is saved to IndexedDB\n - queue length increases by 1\n - no HTTP request is made\n```\n\n```mdl\ntest T-5: Blockstore queue flushes on reconnect\n given: blocks are queued while offline\n when: the browser comes back online\n then:\n - a CAR archive containing all queued blocks is POSTed to the edge service\n - the queue is cleared\n```\n\n```mdl\ntest T-6: New space initializes WNFS directory\n given: a space with no WNFS properties\n when: loadWnfs is called for that space\n then:\n - a new PrivateForest and PrivateDirectory are created\n - space.properties.wnfs.privateForestCid is populated\n```\n\n```mdl\ntest T-7: WnfsInstance cache hit\n given: a WnfsInstance is cached for the current forest CID\n when: loadWnfs is called again for the same space\n then: the cached instance is returned; no blockstore reads occur\n```\n\n---\n\n## Appendix: Extension Definitions\n\nExtension block types used in this document are defined below using\nthe core `ext` primitive — the only construct the base language provides.\n\n```mdl\next type\n uri: org.dxos.mdl.type@1.0\n desc: A named data structure with typed fields and optional literals.\n fields:\n desc?: Prose\n fields?: FieldMap # name[?]: TypeExpr (# inline comment)\n literals?: UnionList # a | b | c\n extends?: TypeRef[]\n```\n\n```mdl\next feat\n uri: org.dxos.mdl.feat@1.0\n desc: A named feature grouping one or more requirements.\n fields:\n desc?: Prose\n req: RequirementList\n nesting: self # feat blocks may contain feat blocks\n```\n\n```mdl\next test\n uri: org.dxos.mdl.test@1.0\n desc: An acceptance scenario expressed as given / when / then steps.\n fields:\n given?: Step | Step[]\n when?: Step | Step[]\n then: Step | Step[]\n tags?: TagList\n```\n\n```mdl\next component\n uri: org.dxos.mdl.component@1.0\n desc: A UI component with props, internal state, slots, actions, and events.\n fields:\n desc?: Prose\n props?: FieldMap # external inputs (immutable inside component)\n state?: FieldMap # internal reactive state\n slots?: FieldMap # named ReactNode injection points\n actions?: ActionMap # methods the component exposes or handles\n emits?: EventMap # events the component raises to its parent\n layout?: CodeBlock # ASCII sketch of visual structure (non-normative)\n```\n\n```mdl\next op\n uri: org.dxos.mdl.op@1.0\n desc: |\n A named operation with typed inputs, outputs, and declared errors.\n Pure ops have no effects or requires. Effectful ops declare both.\n fields:\n desc?: Prose\n input?: FieldMap # named input parameters\n output?: TypeExpr # return type\n errors?: ErrorMap # name: Prose (when this error occurs)\n effects?: EffectList # echo:read | echo:write | http | fs | ...\n requires?: ServiceList # injected service dependencies\n note?: Prose # implementation guidance (non-normative)\n```\n",
|
|
23
|
+
mimeType: "application/x-mdl"
|
|
24
|
+
} }), Plugin.make);
|
|
25
|
+
//#endregion
|
|
26
|
+
export { WnfsPlugin, WnfsPlugin as default };
|
|
27
|
+
|
|
28
|
+
//# sourceMappingURL=WnfsPlugin.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WnfsPlugin.mjs","names":[],"sources":["../../src/WnfsPlugin.tsx"],"sourcesContent":["//\n// Copyright 2024 DXOS.org\n//\n\nimport { Plugin } from '@dxos/app-framework';\nimport { AppPlugin } from '@dxos/app-toolkit';\nimport { ClientEvents } from '@dxos/plugin-client';\n\nimport { BlobBackend, Dependencies } from '#capabilities';\nimport { meta } from '#meta';\nimport { translations } from '#translations';\nimport { WnfsEvents } from '#types';\n\n// eslint-disable-next-line import/no-relative-packages\nimport pluginSpec from '../PLUGIN.mdl?raw';\n\nexport const WnfsPlugin = Plugin.define(meta).pipe(\n AppPlugin.addTranslationsModule({ translations }),\n Plugin.addModule({\n id: 'dependencies',\n activatesOn: ClientEvents.ClientReady,\n firesAfterActivation: [WnfsEvents.DependenciesReady],\n activate: Dependencies,\n }),\n Plugin.addModule({\n id: 'blob-backend',\n activatesOn: WnfsEvents.DependenciesReady,\n activate: BlobBackend,\n }),\n AppPlugin.addPluginAssetModule({\n asset: { pluginId: meta.profile.key, path: 'PLUGIN.mdl', content: pluginSpec, mimeType: 'application/x-mdl' },\n }),\n Plugin.make,\n);\n\nexport default WnfsPlugin;\n"],"mappings":";;;;;;;;;AAgBA,IAAa,aAAa,OAAO,OAAO,IAAI,CAAC,CAAC,KAC5C,UAAU,sBAAsB,EAAE,aAAa,CAAC,GAChD,OAAO,UAAU;CACf,IAAI;CACJ,aAAa,aAAa;CAC1B,sBAAsB,CAAC,WAAW,iBAAiB;CACnD,UAAU;AACZ,CAAC,GACD,OAAO,UAAU;CACf,IAAI;CACJ,aAAa,WAAW;CACxB,UAAU;AACZ,CAAC,GACD,UAAU,qBAAqB,EAC7B,OAAO;CAAE,UAAU,KAAK,QAAQ;CAAK,MAAM;CAAc,SAAS;CAAY,UAAU;AAAoB,EAC9G,CAAC,GACD,OAAO,IACT"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Plugin } from "@dxos/app-framework";
|
|
2
|
+
import { meta } from "#meta";
|
|
3
|
+
//#region src/WnfsPlugin.node.ts
|
|
4
|
+
var WnfsPlugin = Plugin.define(meta).pipe(Plugin.make);
|
|
5
|
+
//#endregion
|
|
6
|
+
export { WnfsPlugin, WnfsPlugin as default };
|
|
7
|
+
|
|
8
|
+
//# sourceMappingURL=WnfsPlugin.node.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WnfsPlugin.node.mjs","names":[],"sources":["../../src/WnfsPlugin.node.ts"],"sourcesContent":["//\n// Copyright 2024 DXOS.org\n//\n\nimport { Plugin } from '@dxos/app-framework';\n\nimport { meta } from '#meta';\n\nexport const WnfsPlugin = Plugin.define(meta).pipe(Plugin.make);\n\nexport default WnfsPlugin;\n"],"mappings":";;;AAQA,IAAa,aAAa,OAAO,OAAO,IAAI,CAAC,CAAC,KAAK,OAAO,IAAI"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Plugin } from "@dxos/app-framework";
|
|
2
|
+
import { meta } from "#meta";
|
|
3
|
+
//#region src/WnfsPlugin.workerd.ts
|
|
4
|
+
var WnfsPlugin = Plugin.define(meta).pipe(Plugin.make);
|
|
5
|
+
//#endregion
|
|
6
|
+
export { WnfsPlugin, WnfsPlugin as default };
|
|
7
|
+
|
|
8
|
+
//# sourceMappingURL=WnfsPlugin.workerd.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WnfsPlugin.workerd.mjs","names":[],"sources":["../../src/WnfsPlugin.workerd.ts"],"sourcesContent":["//\n// Copyright 2024 DXOS.org\n//\n\nimport { Plugin } from '@dxos/app-framework';\n\nimport { meta } from '#meta';\n\nexport const WnfsPlugin = Plugin.define(meta).pipe(Plugin.make);\n\nexport default WnfsPlugin;\n"],"mappings":";;;AAQA,IAAa,aAAa,OAAO,OAAO,IAAI,CAAC,CAAC,KAAK,OAAO,IAAI"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Capability } from "@dxos/app-framework";
|
|
2
|
+
//#region src/capabilities/index.ts
|
|
3
|
+
var BlobBackend = Capability.lazy("BlobBackend", () => import("./chunk-blob-backend.mjs"));
|
|
4
|
+
var Dependencies = Capability.lazy("Dependencies", () => import("./chunk-dependencies.mjs"));
|
|
5
|
+
//#endregion
|
|
6
|
+
export { BlobBackend, Dependencies };
|
|
7
|
+
|
|
8
|
+
//# sourceMappingURL=capabilities.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capabilities.mjs","names":[],"sources":["../../src/capabilities/index.ts"],"sourcesContent":["//\n// Copyright 2025 DXOS.org\n//\n\nimport { Capability } from '@dxos/app-framework';\n\nexport const BlobBackend = Capability.lazy('BlobBackend', () => import('./blob-backend'));\n\nexport const Dependencies = Capability.lazy('Dependencies', () => import('./dependencies'));\n"],"mappings":";;AAMA,IAAa,cAAc,WAAW,KAAK,qBAAqB,OAAO,2BAAiB;AAExF,IAAa,eAAe,WAAW,KAAK,sBAAsB,OAAO,2BAAiB"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { i as readWnfsFile, n as getBlobUrl, o as loadWnfs, t as upload } from "./chunk-helpers.mjs";
|
|
2
|
+
import { log } from "@dxos/log";
|
|
3
|
+
import { Capability } from "@dxos/app-framework";
|
|
4
|
+
import { ClientCapabilities } from "@dxos/plugin-client";
|
|
5
|
+
import { WnfsCapabilities } from "#types";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import { invariant } from "@dxos/invariant";
|
|
8
|
+
import { FileCapabilities } from "@dxos/plugin-file/types";
|
|
9
|
+
//#region src/capabilities/blob-backend.ts
|
|
10
|
+
var __dxlog_file = "/__w/dxos/dxos/packages/plugins/plugin-wnfs/src/capabilities/blob-backend.ts";
|
|
11
|
+
/**
|
|
12
|
+
* Blob backend that stores bytes in a space's WNFS private forest, addressed by `wnfs://…` URIs.
|
|
13
|
+
* The underlying WASM bindings (`directory.read`) don't expose a distinct not-found error type, so
|
|
14
|
+
* `readBytes`/`getUrl` can't tell a missing path from a genuine transport/blockstore failure —
|
|
15
|
+
* both are logged and reported as a miss (`undefined`) rather than risk rethrowing on the common
|
|
16
|
+
* missing-file path.
|
|
17
|
+
*/
|
|
18
|
+
var createWnfsBlobBackend = ({ client, blockstore, instances }) => {
|
|
19
|
+
const readBytes = async (spaceId, uri) => {
|
|
20
|
+
const space = client.spaces.get(spaceId);
|
|
21
|
+
if (!space) return;
|
|
22
|
+
try {
|
|
23
|
+
const { directory, forest } = await loadWnfs({
|
|
24
|
+
blockstore,
|
|
25
|
+
instances,
|
|
26
|
+
space
|
|
27
|
+
});
|
|
28
|
+
return await readWnfsFile({
|
|
29
|
+
wnfsUrl: uri,
|
|
30
|
+
blockstore,
|
|
31
|
+
forest,
|
|
32
|
+
directory
|
|
33
|
+
});
|
|
34
|
+
} catch (error) {
|
|
35
|
+
log.warn("failed to read wnfs blob", {
|
|
36
|
+
spaceId,
|
|
37
|
+
uri,
|
|
38
|
+
error
|
|
39
|
+
}, {
|
|
40
|
+
"~LogMeta": "~LogMeta",
|
|
41
|
+
F: __dxlog_file,
|
|
42
|
+
L: 44,
|
|
43
|
+
S: void 0
|
|
44
|
+
});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
schemes: [WnfsCapabilities.WNFS_SCHEME],
|
|
50
|
+
put: async ({ spaceId, data, contentType, contentHash, name }) => {
|
|
51
|
+
const space = client.spaces.get(spaceId);
|
|
52
|
+
invariant(space, "Space not found", {
|
|
53
|
+
"~LogMeta": "~LogMeta",
|
|
54
|
+
F: __dxlog_file,
|
|
55
|
+
L: 54,
|
|
56
|
+
S: void 0,
|
|
57
|
+
A: ["space", "'Space not found'"]
|
|
58
|
+
});
|
|
59
|
+
return { uri: (await upload({
|
|
60
|
+
file: new File([data], name ?? contentHash, { type: contentType }),
|
|
61
|
+
blockstore,
|
|
62
|
+
instances,
|
|
63
|
+
space
|
|
64
|
+
})).url };
|
|
65
|
+
},
|
|
66
|
+
get: async ({ spaceId, uri }) => readBytes(spaceId, uri),
|
|
67
|
+
has: async ({ spaceId, uri }) => await readBytes(spaceId, uri) !== void 0,
|
|
68
|
+
getUrl: async ({ spaceId, uri, contentType }) => {
|
|
69
|
+
const space = client.spaces.get(spaceId);
|
|
70
|
+
if (!space) return;
|
|
71
|
+
try {
|
|
72
|
+
const { directory, forest } = await loadWnfs({
|
|
73
|
+
blockstore,
|
|
74
|
+
instances,
|
|
75
|
+
space
|
|
76
|
+
});
|
|
77
|
+
return await getBlobUrl({
|
|
78
|
+
wnfsUrl: uri,
|
|
79
|
+
blockstore,
|
|
80
|
+
directory,
|
|
81
|
+
forest,
|
|
82
|
+
type: contentType
|
|
83
|
+
});
|
|
84
|
+
} catch (error) {
|
|
85
|
+
log.warn("failed to resolve wnfs blob url", {
|
|
86
|
+
spaceId,
|
|
87
|
+
uri,
|
|
88
|
+
error
|
|
89
|
+
}, {
|
|
90
|
+
"~LogMeta": "~LogMeta",
|
|
91
|
+
F: __dxlog_file,
|
|
92
|
+
L: 76,
|
|
93
|
+
S: void 0
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
var blob_backend_default = Capability.makeModule(Effect.fnUntraced(function* () {
|
|
101
|
+
const client = yield* Capability.get(ClientCapabilities.Client);
|
|
102
|
+
const blockstore = yield* Capability.get(WnfsCapabilities.Blockstore);
|
|
103
|
+
const instances = yield* Capability.get(WnfsCapabilities.Instances);
|
|
104
|
+
const cleanup = client.graph.registerBlobBackend(WnfsCapabilities.WNFS_BACKEND, createWnfsBlobBackend({
|
|
105
|
+
client,
|
|
106
|
+
blockstore,
|
|
107
|
+
instances
|
|
108
|
+
}));
|
|
109
|
+
return Capability.contributes(FileCapabilities.Backend, {
|
|
110
|
+
name: "WNFS",
|
|
111
|
+
description: "Decentralized, end-to-end encrypted storage via Web Native File System.",
|
|
112
|
+
storage: WnfsCapabilities.WNFS_BACKEND
|
|
113
|
+
}, () => Effect.sync(() => cleanup()));
|
|
114
|
+
}));
|
|
115
|
+
//#endregion
|
|
116
|
+
export { createWnfsBlobBackend, blob_backend_default as default };
|
|
117
|
+
|
|
118
|
+
//# sourceMappingURL=chunk-blob-backend.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-blob-backend.mjs","names":[],"sources":["../../src/capabilities/blob-backend.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\nimport type { Blockstore } from 'interface-blockstore';\n\nimport { Capability } from '@dxos/app-framework';\nimport { type Client } from '@dxos/client';\nimport { type BlobBackend } from '@dxos/echo-protocol';\nimport { invariant } from '@dxos/invariant';\nimport { type SpaceId } from '@dxos/keys';\nimport { log } from '@dxos/log';\nimport { ClientCapabilities } from '@dxos/plugin-client';\nimport { FileCapabilities } from '@dxos/plugin-file/types';\n\nimport { WnfsCapabilities } from '#types';\n\nimport { getBlobUrl, loadWnfs, readWnfsFile, upload } from '../helpers';\n\ninterface CreateWnfsBlobBackendOptions {\n client: Client;\n blockstore: Blockstore;\n instances?: WnfsCapabilities.Instances;\n}\n\n/**\n * Blob backend that stores bytes in a space's WNFS private forest, addressed by `wnfs://…` URIs.\n * The underlying WASM bindings (`directory.read`) don't expose a distinct not-found error type, so\n * `readBytes`/`getUrl` can't tell a missing path from a genuine transport/blockstore failure —\n * both are logged and reported as a miss (`undefined`) rather than risk rethrowing on the common\n * missing-file path.\n */\nexport const createWnfsBlobBackend = ({ client, blockstore, instances }: CreateWnfsBlobBackendOptions): BlobBackend => {\n const readBytes = async (spaceId: SpaceId, uri: string) => {\n const space = client.spaces.get(spaceId);\n if (!space) {\n return undefined;\n }\n try {\n const { directory, forest } = await loadWnfs({ blockstore, instances, space });\n return await readWnfsFile({ wnfsUrl: uri, blockstore, forest, directory });\n } catch (error) {\n log.warn('failed to read wnfs blob', { spaceId, uri, error });\n return undefined;\n }\n };\n\n return {\n schemes: [WnfsCapabilities.WNFS_SCHEME],\n\n put: async ({ spaceId, data, contentType, contentHash, name }) => {\n const space = client.spaces.get(spaceId);\n invariant(space, 'Space not found');\n // `Uint8Array` is generic over `ArrayBufferLike` (incl. `SharedArrayBuffer`) while DOM's\n // `BlobPart` only covers `ArrayBuffer`-backed views — a gap between the DOM lib types and\n // the TS standard lib, not fixable by typing `data` differently.\n const file = new File([data as BlobPart], name ?? contentHash, { type: contentType });\n const info = await upload({ file, blockstore, instances, space });\n return { uri: info.url };\n },\n\n get: async ({ spaceId, uri }) => readBytes(spaceId, uri),\n\n has: async ({ spaceId, uri }) => (await readBytes(spaceId, uri)) !== undefined,\n\n getUrl: async ({ spaceId, uri, contentType }) => {\n const space = client.spaces.get(spaceId);\n if (!space) {\n return undefined;\n }\n try {\n const { directory, forest } = await loadWnfs({ blockstore, instances, space });\n return await getBlobUrl({ wnfsUrl: uri, blockstore, directory, forest, type: contentType });\n } catch (error) {\n log.warn('failed to resolve wnfs blob url', { spaceId, uri, error });\n return undefined;\n }\n },\n };\n};\n\nexport default Capability.makeModule(\n Effect.fnUntraced(function* () {\n const client = yield* Capability.get(ClientCapabilities.Client);\n const blockstore = yield* Capability.get(WnfsCapabilities.Blockstore);\n const instances = yield* Capability.get(WnfsCapabilities.Instances);\n\n const cleanup = client.graph.registerBlobBackend(\n WnfsCapabilities.WNFS_BACKEND,\n createWnfsBlobBackend({ client, blockstore, instances }),\n );\n\n return Capability.contributes(\n FileCapabilities.Backend,\n {\n name: 'WNFS',\n description: 'Decentralized, end-to-end encrypted storage via Web Native File System.',\n storage: WnfsCapabilities.WNFS_BACKEND,\n },\n () => Effect.sync(() => cleanup()),\n );\n }),\n);\n"],"mappings":";;;;;;;;;;;;;;;;;AAiCA,IAAa,yBAAyB,EAAE,QAAQ,YAAY,gBAA2D;CACrH,MAAM,YAAY,OAAO,SAAkB,QAAgB;EACzD,MAAM,QAAQ,OAAO,OAAO,IAAI,OAAO;EACvC,IAAI,CAAC,OACH;EAEF,IAAI;GACF,MAAM,EAAE,WAAW,WAAW,MAAM,SAAS;IAAE;IAAY;IAAW;GAAM,CAAC;GAC7E,OAAO,MAAM,aAAa;IAAE,SAAS;IAAK;IAAY;IAAQ;GAAU,CAAC;EAC3E,SAAS,OAAO;GACd,IAAI,KAAK,4BAA4B;IAAE;IAAS;IAAK;GAAM,GAAA;IAAA,YAAA;IAAA,GAAA;IAAA,GAAA;IAAA,GAAA,KAAA;GAAA,CAAC;GAC5D;EACF;CACF;CAEA,OAAO;EACL,SAAS,CAAC,iBAAiB,WAAW;EAEtC,KAAK,OAAO,EAAE,SAAS,MAAM,aAAa,aAAa,WAAW;GAChE,MAAM,QAAQ,OAAO,OAAO,IAAI,OAAO;GACvC,UAAU,OAAO,mBAAgB;IAAA,YAAA;IAAA,GAAA;IAAA,GAAA;IAAA,GAAA,KAAA;IAAA,GAAA,CAAA,SAAA,mBAAA;GAAA,CAAC;GAMlC,OAAO,EAAE,MAAK,MADK,OAAO;IAAE,MAAA,IADX,KAAK,CAAC,IAAgB,GAAG,QAAQ,aAAa,EAAE,MAAM,YAAY,CACvD;IAAM;IAAY;IAAW;GAAM,CAAC,EAAA,CAC7C,IAAI;EACzB;EAEA,KAAK,OAAO,EAAE,SAAS,UAAU,UAAU,SAAS,GAAG;EAEvD,KAAK,OAAO,EAAE,SAAS,UAAW,MAAM,UAAU,SAAS,GAAG,MAAO,KAAA;EAErE,QAAQ,OAAO,EAAE,SAAS,KAAK,kBAAkB;GAC/C,MAAM,QAAQ,OAAO,OAAO,IAAI,OAAO;GACvC,IAAI,CAAC,OACH;GAEF,IAAI;IACF,MAAM,EAAE,WAAW,WAAW,MAAM,SAAS;KAAE;KAAY;KAAW;IAAM,CAAC;IAC7E,OAAO,MAAM,WAAW;KAAE,SAAS;KAAK;KAAY;KAAW;KAAQ,MAAM;IAAY,CAAC;GAC5F,SAAS,OAAO;IACd,IAAI,KAAK,mCAAmC;KAAE;KAAS;KAAK;IAAM,GAAA;KAAA,YAAA;KAAA,GAAA;KAAA,GAAA;KAAA,GAAA,KAAA;IAAA,CAAC;IACnE;GACF;EACF;CACF;AACF;AAEA,IAAA,uBAAe,WAAW,WACxB,OAAO,WAAW,aAAa;CAC7B,MAAM,SAAS,OAAO,WAAW,IAAI,mBAAmB,MAAM;CAC9D,MAAM,aAAa,OAAO,WAAW,IAAI,iBAAiB,UAAU;CACpE,MAAM,YAAY,OAAO,WAAW,IAAI,iBAAiB,SAAS;CAElE,MAAM,UAAU,OAAO,MAAM,oBAC3B,iBAAiB,cACjB,sBAAsB;EAAE;EAAQ;EAAY;CAAU,CAAC,CACzD;CAEA,OAAO,WAAW,YAChB,iBAAiB,SACjB;EACE,MAAM;EACN,aAAa;EACb,SAAS,iBAAiB;CAC5B,SACM,OAAO,WAAW,QAAQ,CAAC,CACnC;AACF,CAAC,CACH"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { d as storeName } from "./chunk-helpers.mjs";
|
|
2
|
+
import { CID } from "multiformats";
|
|
3
|
+
import * as Uint8Arrays from "uint8arrays";
|
|
4
|
+
import { Capability } from "@dxos/app-framework";
|
|
5
|
+
import { ClientCapabilities } from "@dxos/plugin-client";
|
|
6
|
+
import { WnfsCapabilities } from "#types";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
import { CarWriter } from "@ipld/car";
|
|
9
|
+
import { BaseBlockstore } from "blockstore-core";
|
|
10
|
+
import { IDBBlockstore } from "blockstore-idb";
|
|
11
|
+
import debounce from "debounce";
|
|
12
|
+
import * as IDB from "idb-keyval";
|
|
13
|
+
import all from "it-all";
|
|
14
|
+
//#region src/blockstore.ts
|
|
15
|
+
var create = (apiHost) => {
|
|
16
|
+
return new MixedBlockstore(apiHost);
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* A blockstore that communicates with the DXOS blob service,
|
|
20
|
+
* and which uses an indexedDB blockstore as a client cache.
|
|
21
|
+
*/
|
|
22
|
+
var MixedBlockstore = class extends BaseBlockstore {
|
|
23
|
+
#apiHost;
|
|
24
|
+
#localStore;
|
|
25
|
+
#isConnected;
|
|
26
|
+
#flushQueue;
|
|
27
|
+
#queue;
|
|
28
|
+
constructor(apiHost) {
|
|
29
|
+
super();
|
|
30
|
+
this.#apiHost = apiHost ? apiHost.replace(/\/?$/, "") : void 0;
|
|
31
|
+
this.#localStore = new IDBBlockstore(storeName());
|
|
32
|
+
this.#isConnected = navigator.onLine;
|
|
33
|
+
this.#queue = [];
|
|
34
|
+
this.#flushQueue = debounce(this.#flushQueueInt, 5e3);
|
|
35
|
+
}
|
|
36
|
+
async open() {
|
|
37
|
+
await this.#localStore.open();
|
|
38
|
+
await this.#restoreQueue();
|
|
39
|
+
window.addEventListener("offline", async () => {
|
|
40
|
+
this.#isConnected = false;
|
|
41
|
+
});
|
|
42
|
+
window.addEventListener("online", async () => {
|
|
43
|
+
this.#isConnected = true;
|
|
44
|
+
this.#flushQueue();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
url(apiHost, cid) {
|
|
48
|
+
const path = cid ? cid.toString() : "";
|
|
49
|
+
return `${apiHost}/api/file${path.length ? "/" + path : ""}`;
|
|
50
|
+
}
|
|
51
|
+
async delete(key) {
|
|
52
|
+
await this.#localStore.delete(key);
|
|
53
|
+
if (this.#isConnected && this.#apiHost) await fetch(this.url(this.#apiHost, key), { method: "DELETE" });
|
|
54
|
+
}
|
|
55
|
+
async get(key) {
|
|
56
|
+
if (await this.#localStore.has(key)) return await this.#localStore.get(key);
|
|
57
|
+
if (this.#isConnected && this.#apiHost) {
|
|
58
|
+
const block = await fetch(this.url(this.#apiHost, key), { method: "GET" }).then((r) => r.arrayBuffer()).then((r) => new Uint8Array(r));
|
|
59
|
+
await this.#localStore.put(key, block);
|
|
60
|
+
return block;
|
|
61
|
+
}
|
|
62
|
+
return await this.#localStore.get(key);
|
|
63
|
+
}
|
|
64
|
+
async has(key) {
|
|
65
|
+
if (await this.#localStore.has(key)) return true;
|
|
66
|
+
if (this.#isConnected && this.#apiHost) return await fetch(this.url(this.#apiHost, key), { method: "HEAD" }).then((r) => r.ok);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
async put(key, val) {
|
|
70
|
+
await this.#localStore.put(key, val);
|
|
71
|
+
await this.#addToQueue(key);
|
|
72
|
+
this.#flushQueue();
|
|
73
|
+
return key;
|
|
74
|
+
}
|
|
75
|
+
async destroy() {
|
|
76
|
+
await this.#localStore.destroy();
|
|
77
|
+
}
|
|
78
|
+
async putRemote(key, val) {
|
|
79
|
+
if (this.#apiHost) await fetch(this.url(this.#apiHost, key), {
|
|
80
|
+
method: "POST",
|
|
81
|
+
body: val
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async putCarRemote(car) {
|
|
85
|
+
if (this.#apiHost) await fetch(this.url(this.#apiHost), {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: { "Content-Type": "application/vnd.ipld.car" },
|
|
88
|
+
body: car
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
queueCacheName = `${storeName()}.state.queue`;
|
|
92
|
+
async #addToQueue(key) {
|
|
93
|
+
await this.#saveQueue([...this.#queue, key.toString()]);
|
|
94
|
+
}
|
|
95
|
+
async #flushQueueInt() {
|
|
96
|
+
if (!this.#isConnected || !this.#apiHost) return;
|
|
97
|
+
const keys = [...this.#queue];
|
|
98
|
+
if (keys.length === 0) return;
|
|
99
|
+
const blocks = await Promise.all(keys.map(async (key) => {
|
|
100
|
+
const cid = CID.parse(key);
|
|
101
|
+
return {
|
|
102
|
+
cid,
|
|
103
|
+
bytes: await this.#localStore.get(cid)
|
|
104
|
+
};
|
|
105
|
+
}));
|
|
106
|
+
const car = await this.#createCar(blocks);
|
|
107
|
+
await this.putCarRemote(car);
|
|
108
|
+
const queue = [...this.#queue].filter((k) => {
|
|
109
|
+
if (keys.includes(k)) return false;
|
|
110
|
+
return true;
|
|
111
|
+
});
|
|
112
|
+
await this.#saveQueue(queue);
|
|
113
|
+
}
|
|
114
|
+
async #restoreQueue() {
|
|
115
|
+
const fromCache = await IDB.get(this.queueCacheName);
|
|
116
|
+
if (fromCache && Array.isArray(fromCache)) {
|
|
117
|
+
this.#queue = fromCache;
|
|
118
|
+
this.#flushQueue();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async #saveQueue(items) {
|
|
122
|
+
const arr = [...items];
|
|
123
|
+
this.#queue = arr;
|
|
124
|
+
await IDB.set(this.queueCacheName, arr);
|
|
125
|
+
}
|
|
126
|
+
async #createCar(blocks) {
|
|
127
|
+
const { writer, out } = CarWriter.create();
|
|
128
|
+
const outPromise = all(out);
|
|
129
|
+
await Promise.all(blocks.map((block) => {
|
|
130
|
+
return writer.put(block);
|
|
131
|
+
}));
|
|
132
|
+
await writer.close();
|
|
133
|
+
return Uint8Arrays.concat(await outPromise);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/capabilities/dependencies.ts
|
|
138
|
+
var dependencies_default = Capability.makeModule(Effect.fnUntraced(function* () {
|
|
139
|
+
const blockstore = create((yield* Capability.get(ClientCapabilities.Client)).config.values.runtime?.services?.edge?.url || "http://localhost:8787");
|
|
140
|
+
yield* Effect.tryPromise(() => blockstore.open());
|
|
141
|
+
return [Capability.contributes(WnfsCapabilities.Blockstore, blockstore), Capability.contributes(WnfsCapabilities.Instances, {})];
|
|
142
|
+
}));
|
|
143
|
+
//#endregion
|
|
144
|
+
export { dependencies_default as default };
|
|
145
|
+
|
|
146
|
+
//# sourceMappingURL=chunk-dependencies.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-dependencies.mjs","names":[],"sources":["../../src/blockstore.ts","../../src/capabilities/dependencies.ts"],"sourcesContent":["//\n// Copyright 2024 DXOS.org\n//\n\nimport { CarWriter } from '@ipld/car';\nimport type { Block } from '@ipld/car/reader';\nimport { BaseBlockstore } from 'blockstore-core';\nimport { IDBBlockstore } from 'blockstore-idb';\nimport debounce from 'debounce';\nimport * as IDB from 'idb-keyval';\nimport all from 'it-all';\nimport { CID } from 'multiformats';\nimport * as Uint8Arrays from 'uint8arrays';\n\nimport { storeName } from './helpers';\n\nexport const create = (apiHost?: string) => {\n return new MixedBlockstore(apiHost);\n};\n\n/**\n * A blockstore that communicates with the DXOS blob service,\n * and which uses an indexedDB blockstore as a client cache.\n */\nexport class MixedBlockstore extends BaseBlockstore {\n readonly #apiHost: string | undefined;\n readonly #localStore: IDBBlockstore;\n\n #isConnected: boolean;\n #flushQueue: () => void;\n #queue: string[];\n\n constructor(apiHost?: string) {\n super();\n\n this.#apiHost = apiHost ? apiHost.replace(/\\/?$/, '') : undefined;\n this.#localStore = new IDBBlockstore(storeName());\n this.#isConnected = navigator.onLine;\n this.#queue = [];\n\n this.#flushQueue = debounce(this.#flushQueueInt, 5000);\n }\n\n async open(): Promise<void> {\n await this.#localStore.open();\n await this.#restoreQueue();\n\n window.addEventListener('offline', async () => {\n this.#isConnected = false;\n });\n\n window.addEventListener('online', async () => {\n this.#isConnected = true;\n this.#flushQueue();\n });\n }\n\n url(apiHost: string, cid?: CID): string {\n const path = cid ? cid.toString() : '';\n return `${apiHost}/api/file${path.length ? '/' + path : ''}`;\n }\n\n // BLOCKSTORE IMPLEMENTATION\n\n override async delete(key: CID): Promise<void> {\n await this.#localStore.delete(key);\n\n if (this.#isConnected && this.#apiHost) {\n await fetch(this.url(this.#apiHost, key), {\n method: 'DELETE',\n });\n }\n }\n\n override async get(key: CID): Promise<Uint8Array> {\n if (await this.#localStore.has(key)) {\n return await this.#localStore.get(key);\n }\n\n if (this.#isConnected && this.#apiHost) {\n const block = await fetch(this.url(this.#apiHost, key), {\n method: 'GET',\n })\n .then((r) => r.arrayBuffer())\n .then((r) => new Uint8Array(r));\n\n // Make sure it is cached locally\n await this.#localStore.put(key, block);\n\n return block;\n }\n\n return await this.#localStore.get(key);\n }\n\n override async has(key: CID): Promise<boolean> {\n if (await this.#localStore.has(key)) {\n return true;\n }\n\n if (this.#isConnected && this.#apiHost) {\n return await fetch(this.url(this.#apiHost, key), {\n method: 'HEAD',\n }).then((r) => r.ok);\n }\n\n return false;\n }\n\n override async put(key: CID, val: Uint8Array): Promise<CID> {\n await this.#localStore.put(key, val);\n\n await this.#addToQueue(key);\n this.#flushQueue();\n\n return key;\n }\n\n async destroy(): Promise<void> {\n await this.#localStore.destroy();\n }\n\n // REMOTE\n\n async putRemote(key: CID, val: Uint8Array): Promise<void> {\n if (this.#apiHost) {\n await fetch(this.url(this.#apiHost, key), {\n method: 'POST',\n body: val as Uint8Array<ArrayBuffer>,\n });\n }\n }\n\n async putCarRemote(car: Uint8Array): Promise<void> {\n if (this.#apiHost) {\n await fetch(this.url(this.#apiHost), {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/vnd.ipld.car',\n },\n body: car as Uint8Array<ArrayBuffer>,\n });\n }\n }\n\n // QUEUE\n\n readonly queueCacheName = `${storeName()}.state.queue`;\n\n async #addToQueue(key: CID): Promise<void> {\n await this.#saveQueue([...this.#queue, key.toString()]);\n }\n\n async #flushQueueInt(): Promise<void> {\n if (!this.#isConnected || !this.#apiHost) {\n return;\n }\n\n const keys = [...this.#queue];\n if (keys.length === 0) {\n return;\n }\n\n // Collect blocks\n const blocks = await Promise.all(\n keys.map(async (key) => {\n const cid = CID.parse(key);\n const bytes = await this.#localStore.get(cid);\n return { cid, bytes };\n }),\n );\n\n // Create & submit CAR\n const car = await this.#createCar(blocks);\n await this.putCarRemote(car);\n\n // Adjust queue\n const queue = [...this.#queue].filter((k) => {\n if (keys.includes(k)) {\n return false;\n }\n\n return true;\n });\n\n await this.#saveQueue(queue);\n }\n\n async #restoreQueue(): Promise<void> {\n const fromCache = await IDB.get(this.queueCacheName);\n if (fromCache && Array.isArray(fromCache)) {\n this.#queue = fromCache;\n this.#flushQueue();\n }\n }\n\n async #saveQueue(items: string[]): Promise<void> {\n const arr = [...items];\n this.#queue = arr;\n await IDB.set(this.queueCacheName, arr);\n }\n\n // 🛠️\n\n async #createCar(blocks: Block[]): Promise<Uint8Array> {\n const { writer, out } = CarWriter.create();\n const outPromise = all(out);\n\n await Promise.all(\n blocks.map((block) => {\n return writer.put(block);\n }),\n );\n\n await writer.close();\n return Uint8Arrays.concat(await outPromise);\n }\n}\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\n\nimport { Capability } from '@dxos/app-framework';\nimport { ClientCapabilities } from '@dxos/plugin-client';\n\nimport { WnfsCapabilities } from '#types';\n\nimport * as Blockstore from '../blockstore';\n\nexport default Capability.makeModule(\n Effect.fnUntraced(function* () {\n const client = yield* Capability.get(ClientCapabilities.Client);\n const apiHost = client.config.values.runtime?.services?.edge?.url || 'http://localhost:8787';\n const blockstore = Blockstore.create(apiHost);\n yield* Effect.tryPromise(() => blockstore.open());\n\n const instances: WnfsCapabilities.Instances = {};\n\n return [\n Capability.contributes(WnfsCapabilities.Blockstore, blockstore),\n Capability.contributes(WnfsCapabilities.Instances, instances),\n ];\n }),\n);\n"],"mappings":"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"}
|