@dxos/plugin-wnfs 0.9.1-staging.ee54ba693a → 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.
Files changed (99) hide show
  1. package/PLUGIN.mdl +92 -46
  2. package/dist/lib/WnfsPlugin.mjs +28 -0
  3. package/dist/lib/WnfsPlugin.mjs.map +1 -0
  4. package/dist/lib/WnfsPlugin.node.mjs +8 -0
  5. package/dist/lib/WnfsPlugin.node.mjs.map +1 -0
  6. package/dist/lib/WnfsPlugin.workerd.mjs +8 -0
  7. package/dist/lib/WnfsPlugin.workerd.mjs.map +1 -0
  8. package/dist/lib/capabilities.mjs +8 -0
  9. package/dist/lib/capabilities.mjs.map +1 -0
  10. package/dist/lib/chunk-blob-backend.mjs +118 -0
  11. package/dist/lib/chunk-blob-backend.mjs.map +1 -0
  12. package/dist/lib/chunk-dependencies.mjs +146 -0
  13. package/dist/lib/chunk-dependencies.mjs.map +1 -0
  14. package/dist/lib/chunk-helpers.mjs +198 -0
  15. package/dist/lib/chunk-helpers.mjs.map +1 -0
  16. package/dist/lib/chunk-meta.mjs +38 -0
  17. package/dist/lib/chunk-meta.mjs.map +1 -0
  18. package/dist/lib/chunk-types.mjs +46 -0
  19. package/dist/lib/chunk-types.mjs.map +1 -0
  20. package/dist/lib/helpers.mjs +2 -0
  21. package/dist/lib/index.mjs +4 -0
  22. package/dist/lib/meta.mjs +2 -0
  23. package/dist/lib/plugin.mjs +8 -0
  24. package/dist/lib/plugin.mjs.map +1 -0
  25. package/dist/lib/translations.mjs +7 -0
  26. package/dist/lib/translations.mjs.map +1 -0
  27. package/dist/lib/types.mjs +2 -0
  28. package/dist/types/src/WnfsPlugin.d.ts.map +1 -1
  29. package/dist/types/src/capabilities/blob-backend.d.ts +22 -0
  30. package/dist/types/src/capabilities/blob-backend.d.ts.map +1 -0
  31. package/dist/types/src/capabilities/blob-backend.test.d.ts +2 -0
  32. package/dist/types/src/capabilities/blob-backend.test.d.ts.map +1 -0
  33. package/dist/types/src/capabilities/dependencies.d.ts +5 -0
  34. package/dist/types/src/capabilities/dependencies.d.ts.map +1 -0
  35. package/dist/types/src/capabilities/index.d.ts +2 -3
  36. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  37. package/dist/types/src/helpers/wnfs-url.d.ts +6 -0
  38. package/dist/types/src/helpers/wnfs-url.d.ts.map +1 -1
  39. package/dist/types/src/types/WnfsCapabilities.d.ts +7 -0
  40. package/dist/types/src/types/WnfsCapabilities.d.ts.map +1 -1
  41. package/dist/types/src/types/WnfsEvents.d.ts +11 -0
  42. package/dist/types/src/types/WnfsEvents.d.ts.map +1 -0
  43. package/dist/types/src/types/index.d.ts +1 -0
  44. package/dist/types/src/types/index.d.ts.map +1 -1
  45. package/dist/types/tsconfig.tsbuildinfo +1 -1
  46. package/dx.config.ts +4 -4
  47. package/package.json +34 -32
  48. package/src/WnfsPlugin.tsx +9 -24
  49. package/src/capabilities/blob-backend.test.ts +108 -0
  50. package/src/capabilities/blob-backend.ts +104 -0
  51. package/src/capabilities/{blockstore.ts → dependencies.ts} +6 -1
  52. package/src/capabilities/index.ts +2 -4
  53. package/src/helpers/wnfs-url.ts +22 -4
  54. package/src/types/WnfsCapabilities.ts +9 -0
  55. package/src/types/WnfsEvents.ts +19 -0
  56. package/src/types/index.ts +1 -0
  57. package/dist/lib/neutral/WnfsPlugin.mjs +0 -51
  58. package/dist/lib/neutral/WnfsPlugin.mjs.map +0 -7
  59. package/dist/lib/neutral/WnfsPlugin.node.mjs +0 -12
  60. package/dist/lib/neutral/WnfsPlugin.node.mjs.map +0 -7
  61. package/dist/lib/neutral/WnfsPlugin.workerd.mjs +0 -12
  62. package/dist/lib/neutral/WnfsPlugin.workerd.mjs.map +0 -7
  63. package/dist/lib/neutral/backend-4HPTHS33.mjs +0 -47
  64. package/dist/lib/neutral/backend-4HPTHS33.mjs.map +0 -7
  65. package/dist/lib/neutral/blockstore-U6SPNL2C.mjs +0 -190
  66. package/dist/lib/neutral/blockstore-U6SPNL2C.mjs.map +0 -7
  67. package/dist/lib/neutral/capabilities/index.mjs +0 -13
  68. package/dist/lib/neutral/capabilities/index.mjs.map +0 -7
  69. package/dist/lib/neutral/chunk-2WLTMUHV.mjs +0 -19
  70. package/dist/lib/neutral/chunk-2WLTMUHV.mjs.map +0 -7
  71. package/dist/lib/neutral/chunk-H2YCLSRI.mjs +0 -47
  72. package/dist/lib/neutral/chunk-H2YCLSRI.mjs.map +0 -7
  73. package/dist/lib/neutral/chunk-J5LGTIGS.mjs +0 -10
  74. package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +0 -7
  75. package/dist/lib/neutral/chunk-L33MOD23.mjs +0 -219
  76. package/dist/lib/neutral/chunk-L33MOD23.mjs.map +0 -7
  77. package/dist/lib/neutral/helpers/index.mjs +0 -26
  78. package/dist/lib/neutral/helpers/index.mjs.map +0 -7
  79. package/dist/lib/neutral/index.mjs +0 -36
  80. package/dist/lib/neutral/index.mjs.map +0 -7
  81. package/dist/lib/neutral/meta.json +0 -1
  82. package/dist/lib/neutral/meta.mjs +0 -8
  83. package/dist/lib/neutral/meta.mjs.map +0 -7
  84. package/dist/lib/neutral/plugin.mjs +0 -12
  85. package/dist/lib/neutral/plugin.mjs.map +0 -7
  86. package/dist/lib/neutral/translations.mjs +0 -17
  87. package/dist/lib/neutral/translations.mjs.map +0 -7
  88. package/dist/lib/neutral/types/index.mjs +0 -8
  89. package/dist/lib/neutral/types/index.mjs.map +0 -7
  90. package/dist/lib/neutral/url-resolver-BIRH7WHL.mjs +0 -47
  91. package/dist/lib/neutral/url-resolver-BIRH7WHL.mjs.map +0 -7
  92. package/dist/types/src/capabilities/backend.d.ts +0 -6
  93. package/dist/types/src/capabilities/backend.d.ts.map +0 -1
  94. package/dist/types/src/capabilities/blockstore.d.ts +0 -5
  95. package/dist/types/src/capabilities/blockstore.d.ts.map +0 -1
  96. package/dist/types/src/capabilities/url-resolver.d.ts +0 -6
  97. package/dist/types/src/capabilities/url-resolver.d.ts.map +0 -1
  98. package/src/capabilities/backend.ts +0 -43
  99. 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.1.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 implements the `FileCapabilities.Backend` and `FileCapabilities.UrlResolver`
12
- interfaces from `plugin-file`, so other plugins (e.g. `plugin-gallery`) can upload and resolve `wnfs://` URLs
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: Metadata returned after a successful upload.
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
- Effectful operations — read or write persistent state.
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: a FileCapabilities.Backend consumer calls upload(file, db)
160
- then: the file is read, hashed, written to the WNFS private directory,
161
- and stored in the blockstore
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: a FileInfo object with name, type, size, and wnfs:// URL is returned to the caller
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:// URL
219
+ feat F-2: Resolve wnfs:// blobs
174
220
 
175
221
  req F-2.1:
176
- when: FileCapabilities.UrlResolver.test(url) is called with a wnfs:// URL
177
- then: returns true
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: FileCapabilities.UrlResolver.resolve(url, file, space) is called
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: bytes are successfully read
185
- then: a blob: object URL is returned for in-browser display
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 undefined or reading fails
189
- then: undefined is returned; no error is propagated
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 FileInfo
245
- given: an active space and a registered WNFS backend
246
- when: a file is uploaded via the Backend capability
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
- - FileInfo.url starts with wnfs://
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 file
255
- given: a file was previously uploaded and FileInfo.url is known
256
- when: UrlResolver.resolve(url, file, space) is called
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: Resolve without space returns undefined
264
- given: a wnfs:// URL
265
- when: UrlResolver.resolve is called with space = undefined
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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"}