@dxos/plugin-wnfs 0.7.4-staging.f7e8224 → 0.7.5-labs.5f04cf6

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 (135) hide show
  1. package/dist/lib/browser/FileContainer-4FF5D2ZF.mjs +94 -0
  2. package/dist/lib/browser/FileContainer-4FF5D2ZF.mjs.map +7 -0
  3. package/dist/lib/browser/blockstore-6U5XTSIH.mjs +192 -0
  4. package/dist/lib/browser/blockstore-6U5XTSIH.mjs.map +7 -0
  5. package/dist/lib/browser/chunk-357BWHML.mjs +16 -0
  6. package/dist/lib/browser/chunk-357BWHML.mjs.map +7 -0
  7. package/dist/lib/browser/{chunk-VL3H7P4F.mjs → chunk-6YFT42F7.mjs} +5 -8
  8. package/dist/lib/browser/chunk-6YFT42F7.mjs.map +7 -0
  9. package/dist/lib/browser/chunk-ED7HMPYN.mjs +16 -0
  10. package/dist/lib/browser/chunk-ED7HMPYN.mjs.map +7 -0
  11. package/dist/lib/browser/chunk-EMRQJXBW.mjs +60 -0
  12. package/dist/lib/browser/chunk-EMRQJXBW.mjs.map +7 -0
  13. package/dist/lib/browser/chunk-SCQWJNHD.mjs +157 -0
  14. package/dist/lib/browser/chunk-SCQWJNHD.mjs.map +7 -0
  15. package/dist/lib/browser/file-uploader-EDHMSKYS.mjs +29 -0
  16. package/dist/lib/browser/file-uploader-EDHMSKYS.mjs.map +7 -0
  17. package/dist/lib/browser/index.mjs +96 -494
  18. package/dist/lib/browser/index.mjs.map +4 -4
  19. package/dist/lib/browser/intent-resolver-WDO4EIG3.mjs +43 -0
  20. package/dist/lib/browser/intent-resolver-WDO4EIG3.mjs.map +7 -0
  21. package/dist/lib/browser/markdown-MGSH5CVQ.mjs +198 -0
  22. package/dist/lib/browser/markdown-MGSH5CVQ.mjs.map +7 -0
  23. package/dist/lib/browser/meta.json +1 -1
  24. package/dist/lib/browser/react-surface-OUVGXVOP.mjs +81 -0
  25. package/dist/lib/browser/react-surface-OUVGXVOP.mjs.map +7 -0
  26. package/dist/lib/browser/types/index.mjs +2 -2
  27. package/dist/types/src/WnfsPlugin.d.ts +1 -3
  28. package/dist/types/src/WnfsPlugin.d.ts.map +1 -1
  29. package/dist/types/src/blockstore.d.ts +4 -1
  30. package/dist/types/src/blockstore.d.ts.map +1 -1
  31. package/dist/types/src/capabilities/blockstore.d.ts +4 -0
  32. package/dist/types/src/capabilities/blockstore.d.ts.map +1 -0
  33. package/dist/types/src/capabilities/capabilities.d.ts +12 -0
  34. package/dist/types/src/capabilities/capabilities.d.ts.map +1 -0
  35. package/dist/types/src/capabilities/file-uploader.d.ts +4 -0
  36. package/dist/types/src/capabilities/file-uploader.d.ts.map +1 -0
  37. package/dist/types/src/capabilities/index.d.ts +7 -0
  38. package/dist/types/src/capabilities/index.d.ts.map +1 -0
  39. package/dist/types/src/capabilities/intent-resolver.d.ts +4 -0
  40. package/dist/types/src/capabilities/intent-resolver.d.ts.map +1 -0
  41. package/dist/types/src/capabilities/markdown.d.ts +4 -0
  42. package/dist/types/src/capabilities/markdown.d.ts.map +1 -0
  43. package/dist/types/src/capabilities/react-surface.d.ts +4 -0
  44. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
  45. package/dist/types/src/components/FileContainer.d.ts +8 -0
  46. package/dist/types/src/components/FileContainer.d.ts.map +1 -0
  47. package/dist/types/src/components/FileInput.d.ts +8 -0
  48. package/dist/types/src/components/FileInput.d.ts.map +1 -0
  49. package/dist/types/src/components/index.d.ts +4 -11
  50. package/dist/types/src/components/index.d.ts.map +1 -1
  51. package/dist/types/src/extensions/image.d.ts +5 -0
  52. package/dist/types/src/extensions/image.d.ts.map +1 -1
  53. package/dist/types/src/extensions/image.stories.d.ts.map +1 -1
  54. package/dist/types/src/helpers/common.d.ts.map +1 -0
  55. package/dist/types/src/helpers/index.d.ts +5 -0
  56. package/dist/types/src/helpers/index.d.ts.map +1 -0
  57. package/dist/types/src/helpers/load.d.ts +13 -0
  58. package/dist/types/src/helpers/load.d.ts.map +1 -0
  59. package/dist/types/src/helpers/upload.d.ts +15 -0
  60. package/dist/types/src/helpers/upload.d.ts.map +1 -0
  61. package/dist/types/src/helpers/wnfs-url.d.ts +11 -0
  62. package/dist/types/src/helpers/wnfs-url.d.ts.map +1 -0
  63. package/dist/types/src/index.d.ts +2 -2
  64. package/dist/types/src/index.d.ts.map +1 -1
  65. package/dist/types/src/meta.d.ts +1 -3
  66. package/dist/types/src/meta.d.ts.map +1 -1
  67. package/dist/types/src/translations.d.ts +14 -0
  68. package/dist/types/src/translations.d.ts.map +1 -1
  69. package/dist/types/src/types/file.d.ts +7 -11
  70. package/dist/types/src/types/file.d.ts.map +1 -1
  71. package/dist/types/src/types/types.d.ts +49 -6
  72. package/dist/types/src/types/types.d.ts.map +1 -1
  73. package/dist/types/tsconfig.tsbuildinfo +1 -0
  74. package/package.json +28 -26
  75. package/src/WnfsPlugin.tsx +77 -92
  76. package/src/blockstore.ts +151 -21
  77. package/src/capabilities/blockstore.ts +18 -0
  78. package/src/capabilities/capabilities.ts +18 -0
  79. package/src/capabilities/file-uploader.ts +24 -0
  80. package/src/capabilities/index.ts +13 -0
  81. package/src/capabilities/intent-resolver.ts +26 -0
  82. package/src/capabilities/markdown.ts +25 -0
  83. package/src/capabilities/react-surface.tsx +41 -0
  84. package/src/components/FileContainer.tsx +46 -0
  85. package/src/components/FileInput.tsx +46 -0
  86. package/src/components/index.ts +3 -5
  87. package/src/extensions/image.stories.tsx +26 -68
  88. package/src/extensions/image.tsx +203 -0
  89. package/src/{common.ts → helpers/common.ts} +1 -1
  90. package/src/helpers/index.ts +8 -0
  91. package/src/{load.ts → helpers/load.ts} +38 -9
  92. package/src/{upload.ts → helpers/upload.ts} +18 -4
  93. package/src/helpers/wnfs-url.ts +36 -0
  94. package/src/index.ts +2 -4
  95. package/src/meta.ts +1 -2
  96. package/src/translations.ts +5 -0
  97. package/src/types/file.ts +2 -3
  98. package/src/types/types.ts +30 -19
  99. package/dist/lib/browser/FileCard-HJIZDWJF.mjs +0 -39
  100. package/dist/lib/browser/FileCard-HJIZDWJF.mjs.map +0 -7
  101. package/dist/lib/browser/FileMain-WEWTX7QB.mjs +0 -27
  102. package/dist/lib/browser/FileMain-WEWTX7QB.mjs.map +0 -7
  103. package/dist/lib/browser/FileSection-BEUGJ7EI.mjs +0 -24
  104. package/dist/lib/browser/FileSection-BEUGJ7EI.mjs.map +0 -7
  105. package/dist/lib/browser/FileSlide-NYBGZEZS.mjs +0 -21
  106. package/dist/lib/browser/FileSlide-NYBGZEZS.mjs.map +0 -7
  107. package/dist/lib/browser/chunk-2I63TM7M.mjs +0 -27
  108. package/dist/lib/browser/chunk-2I63TM7M.mjs.map +0 -7
  109. package/dist/lib/browser/chunk-JDSKIDDR.mjs +0 -30
  110. package/dist/lib/browser/chunk-JDSKIDDR.mjs.map +0 -7
  111. package/dist/lib/browser/chunk-VL3H7P4F.mjs.map +0 -7
  112. package/dist/lib/browser/meta.mjs +0 -9
  113. package/dist/lib/browser/meta.mjs.map +0 -7
  114. package/dist/types/src/common.d.ts.map +0 -1
  115. package/dist/types/src/components/FileCard.d.ts +0 -9
  116. package/dist/types/src/components/FileCard.d.ts.map +0 -1
  117. package/dist/types/src/components/FileMain.d.ts +0 -7
  118. package/dist/types/src/components/FileMain.d.ts.map +0 -1
  119. package/dist/types/src/components/FileSection.d.ts +0 -8
  120. package/dist/types/src/components/FileSection.d.ts.map +0 -1
  121. package/dist/types/src/components/FileSlide.d.ts +0 -8
  122. package/dist/types/src/components/FileSlide.d.ts.map +0 -1
  123. package/dist/types/src/load.d.ts +0 -8
  124. package/dist/types/src/load.d.ts.map +0 -1
  125. package/dist/types/src/upload.d.ts +0 -10
  126. package/dist/types/src/upload.d.ts.map +0 -1
  127. package/dist/types/src/wnfs-url.d.ts +0 -2
  128. package/dist/types/src/wnfs-url.d.ts.map +0 -1
  129. package/src/components/FileCard.tsx +0 -45
  130. package/src/components/FileMain.tsx +0 -28
  131. package/src/components/FileSection.tsx +0 -20
  132. package/src/components/FileSlide.tsx +0 -20
  133. package/src/extensions/image.ts +0 -244
  134. package/src/wnfs-url.ts +0 -7
  135. /package/dist/types/src/{common.d.ts → helpers/common.d.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/plugin-wnfs",
3
- "version": "0.7.4-staging.f7e8224",
3
+ "version": "0.7.5-labs.5f04cf6",
4
4
  "description": "WNFS file management plugin",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -11,10 +11,6 @@
11
11
  "types": "./dist/types/src/index.d.ts",
12
12
  "browser": "./dist/lib/browser/index.mjs"
13
13
  },
14
- "./meta": {
15
- "types": "./dist/types/src/meta.d.ts",
16
- "browser": "./dist/lib/browser/meta.mjs"
17
- },
18
14
  "./types": {
19
15
  "types": "./dist/types/src/types/index.d.ts",
20
16
  "browser": "./dist/lib/browser/types/index.mjs"
@@ -23,9 +19,6 @@
23
19
  "types": "dist/types/src/index.d.ts",
24
20
  "typesVersions": {
25
21
  "*": {
26
- "meta": [
27
- "dist/types/src/meta.d.ts"
28
- ],
29
22
  "types": [
30
23
  "dist/types/src/types/index.d.ts"
31
24
  ]
@@ -39,24 +32,34 @@
39
32
  "@codemirror/language": "^6.10.3",
40
33
  "@codemirror/state": "^6.4.1",
41
34
  "@codemirror/view": "^6.34.1",
35
+ "@ipld/car": "^5.3.3",
42
36
  "blockstore-core": "^5.0.1",
43
37
  "blockstore-idb": "^2.0.0",
38
+ "debounce": "^2.2.0",
39
+ "effect": "^3.12.3",
40
+ "idb-keyval": "^6.2.1",
44
41
  "interface-blockstore": "^5.2.10",
42
+ "it-all": "^3.0.6",
45
43
  "multiformats": "^13.2.2",
44
+ "react-dropzone": "^14.2.3",
46
45
  "uint8arrays": "^5.1.0",
47
46
  "wnfs": "0.2.2",
48
- "@dxos/app-framework": "0.7.4-staging.f7e8224",
49
- "@dxos/crypto": "0.7.4-staging.f7e8224",
50
- "@dxos/echo-schema": "0.7.4-staging.f7e8224",
51
- "@dxos/invariant": "0.7.4-staging.f7e8224",
52
- "@dxos/local-storage": "0.7.4-staging.f7e8224",
53
- "@dxos/log": "0.7.4-staging.f7e8224",
54
- "@dxos/plugin-space": "0.7.4-staging.f7e8224",
55
- "@dxos/plugin-markdown": "0.7.4-staging.f7e8224",
56
- "@dxos/plugin-client": "0.7.4-staging.f7e8224",
57
- "@dxos/react-client": "0.7.4-staging.f7e8224",
58
- "@dxos/react-ui-mosaic": "0.7.4-staging.f7e8224",
59
- "@dxos/react-ui-card": "0.7.4-staging.f7e8224"
47
+ "@dxos/app-framework": "0.7.5-labs.5f04cf6",
48
+ "@dxos/crypto": "0.7.5-labs.5f04cf6",
49
+ "@dxos/effect": "0.7.5-labs.5f04cf6",
50
+ "@dxos/echo-schema": "0.7.5-labs.5f04cf6",
51
+ "@dxos/invariant": "0.7.5-labs.5f04cf6",
52
+ "@dxos/local-storage": "0.7.5-labs.5f04cf6",
53
+ "@dxos/log": "0.7.5-labs.5f04cf6",
54
+ "@dxos/plugin-space": "0.7.5-labs.5f04cf6",
55
+ "@dxos/plugin-client": "0.7.5-labs.5f04cf6",
56
+ "@dxos/react-client": "0.7.5-labs.5f04cf6",
57
+ "@dxos/plugin-markdown": "0.7.5-labs.5f04cf6",
58
+ "@dxos/react-ui": "0.7.5-labs.5f04cf6",
59
+ "@dxos/react-ui-editor": "0.7.5-labs.5f04cf6",
60
+ "@dxos/react-ui-form": "0.7.5-labs.5f04cf6",
61
+ "@dxos/react-ui-stack": "0.7.5-labs.5f04cf6",
62
+ "@dxos/util": "0.7.5-labs.5f04cf6"
60
63
  },
61
64
  "devDependencies": {
62
65
  "@phosphor-icons/react": "^2.1.5",
@@ -65,17 +68,16 @@
65
68
  "react": "~18.2.0",
66
69
  "react-dom": "~18.2.0",
67
70
  "vite": "5.4.7",
68
- "@dxos/react-ui": "0.7.4-staging.f7e8224",
69
- "@dxos/react-ui-editor": "0.7.4-staging.f7e8224",
70
- "@dxos/storybook-utils": "0.7.4-staging.f7e8224",
71
- "@dxos/react-ui-theme": "0.7.4-staging.f7e8224"
71
+ "@dxos/react-ui-theme": "0.7.5-labs.5f04cf6",
72
+ "@dxos/random": "0.7.5-labs.5f04cf6",
73
+ "@dxos/storybook-utils": "0.7.5-labs.5f04cf6"
72
74
  },
73
75
  "peerDependencies": {
74
76
  "@phosphor-icons/react": "^2.0.5",
75
77
  "react": "~18.2.0",
76
78
  "react-dom": "~18.2.0",
77
- "@dxos/react-ui-theme": "0.7.4-staging.f7e8224",
78
- "@dxos/react-ui": "0.7.4-staging.f7e8224"
79
+ "@dxos/react-ui": "0.7.5-labs.5f04cf6",
80
+ "@dxos/react-ui-theme": "0.7.5-labs.5f04cf6"
79
81
  },
80
82
  "publishConfig": {
81
83
  "access": "public"
@@ -2,101 +2,86 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { FileCloud, type IconProps } from '@phosphor-icons/react';
6
- import React, { type Ref } from 'react';
5
+ import { pipe } from 'effect';
7
6
 
8
- import { type PluginDefinition, isObject, resolvePlugin } from '@dxos/app-framework';
9
- import { invariant } from '@dxos/invariant';
10
- import { parseClientPlugin } from '@dxos/plugin-client';
11
- import { type DocumentType } from '@dxos/plugin-markdown/types';
12
- import { type Space, getSpace } from '@dxos/react-client/echo';
13
- import { isTileComponentProps } from '@dxos/react-ui-mosaic';
7
+ import {
8
+ Capabilities,
9
+ Events,
10
+ chain,
11
+ contributes,
12
+ createIntent,
13
+ defineModule,
14
+ definePlugin,
15
+ oneOf,
16
+ } from '@dxos/app-framework';
17
+ import { ClientCapabilities, ClientEvents } from '@dxos/plugin-client';
18
+ import { type Space } from '@dxos/react-client/echo';
14
19
 
15
- import * as Blockstore from './blockstore';
16
- import { FileCard, FileMain, FileSection, FileSlide } from './components';
17
- import { image as imageExtension } from './extensions';
18
- import meta, { WNFS_PLUGIN } from './meta';
20
+ import { Blockstore, FileUploader, IntentResolver, Markdown, ReactSurface, WnfsCapabilities } from './capabilities';
21
+ import { meta, WNFS_PLUGIN } from './meta';
19
22
  import translations from './translations';
20
- import { FileType } from './types';
21
- import { type WnfsPluginProvides } from './types';
22
- import { upload } from './upload';
23
+ import { FileType, WnfsAction } from './types';
23
24
 
24
- export const WnfsPlugin = (): PluginDefinition<WnfsPluginProvides> => {
25
- let blockstore: Blockstore.MixedBlockstore | undefined;
26
-
27
- return {
28
- meta,
29
- ready: async (plugins) => {
30
- const client = resolvePlugin(plugins, parseClientPlugin)?.provides.client;
31
- invariant(client);
32
- const apiHost = client.config.values.runtime?.services?.edge?.url || 'http://localhost:8787';
33
- blockstore = Blockstore.create(apiHost);
34
- await blockstore.open();
35
- },
36
- provides: {
37
- translations,
38
- metadata: {
39
- records: {
40
- [FileType.typename]: {
25
+ export const WnfsPlugin = () =>
26
+ definePlugin(meta, [
27
+ defineModule({
28
+ id: `${meta.id}/module/blockstore`,
29
+ activatesOn: ClientEvents.ClientReady,
30
+ activate: Blockstore,
31
+ }),
32
+ defineModule({
33
+ id: `${meta.id}/module/instances`,
34
+ activatesOn: Events.Startup,
35
+ activate: () => {
36
+ const instances: WnfsCapabilities.Instances = {};
37
+ return contributes(WnfsCapabilities.Instances, instances);
38
+ },
39
+ }),
40
+ defineModule({
41
+ id: `${meta.id}/module/translations`,
42
+ activatesOn: Events.SetupTranslations,
43
+ activate: () => contributes(Capabilities.Translations, translations),
44
+ }),
45
+ defineModule({
46
+ id: `${meta.id}/module/metadata`,
47
+ activatesOn: oneOf(Events.Startup, Events.SetupAppGraph),
48
+ activate: () =>
49
+ contributes(Capabilities.Metadata, {
50
+ id: FileType.typename,
51
+ metadata: {
52
+ creationSchema: WnfsAction.UploadFileSchema,
53
+ createObject: ({ file }: WnfsAction.UploadFileForm, { space }: { space: Space }) =>
54
+ pipe(createIntent(WnfsAction.Upload, { file, space }), chain(WnfsAction.Create, {})),
55
+ label: (object: any) => (object instanceof FileType ? object.name : undefined),
41
56
  placeholder: ['file title placeholder', { ns: WNFS_PLUGIN }],
42
- icon: (props: IconProps) => <FileCloud {...props} />,
57
+ // TODO(wittjosiah): Would be nice if icon could change based on the type of the file.
58
+ icon: 'ph--file--regular',
43
59
  },
44
- },
45
- },
46
- echo: {
47
- system: [FileType],
48
- },
49
- // TODO(burdon): Add intent to upload file.
50
- file: {
51
- upload: async (file: File, space: Space) => {
52
- if (!blockstore) {
53
- throw new Error('Blockstore is not ready yet');
54
- }
55
-
56
- return await upload({ blockstore, file, space });
57
- },
58
- },
59
- markdown: {
60
- extensions: ({ document }: { document?: DocumentType }) => {
61
- if (!blockstore) {
62
- throw new Error('Blockstore is not ready yet');
63
- }
64
-
65
- if (document) {
66
- const space = getSpace(document);
67
- return space ? [imageExtension({ blockstore, space })] : [];
68
- }
69
-
70
- return [];
71
- },
72
- },
73
- surface: {
74
- component: ({ data, role, ...props }, forwardedRef) => {
75
- switch (role) {
76
- case 'main':
77
- return data.active instanceof FileType ? <FileMain file={data.active} /> : null;
78
- case 'slide':
79
- return data.slide instanceof FileType ? <FileSlide file={data.slide} cover={false} /> : null;
80
- case 'section':
81
- return data.object instanceof FileType ? <FileSection file={data.object} /> : null;
82
- case 'card': {
83
- if (
84
- isObject(data.content) &&
85
- typeof data.content.id === 'string' &&
86
- data.content.object instanceof FileType
87
- ) {
88
- const cardProps = { ...props, item: { id: data.content.id, object: data.content.object } };
89
- return isTileComponentProps(cardProps) ? (
90
- <FileCard {...cardProps} ref={forwardedRef as Ref<HTMLDivElement>} />
91
- ) : null;
92
- }
93
- break;
94
- }
95
- }
96
-
97
- return null;
98
- },
99
- },
100
- },
101
- };
102
- };
60
+ }),
61
+ }),
62
+ defineModule({
63
+ id: `${meta.id}/module/schema`,
64
+ activatesOn: ClientEvents.SetupClient,
65
+ activate: () => contributes(ClientCapabilities.Schema, [FileType]),
66
+ }),
67
+ defineModule({
68
+ id: `${meta.id}/module/file-uploader`,
69
+ activatesOn: ClientEvents.ClientReady,
70
+ activate: FileUploader,
71
+ }),
72
+ defineModule({
73
+ id: `${meta.id}/module/markdown`,
74
+ activatesOn: Events.Startup,
75
+ activate: Markdown,
76
+ }),
77
+ defineModule({
78
+ id: `${meta.id}/module/react-surface`,
79
+ activatesOn: Events.Startup,
80
+ activate: ReactSurface,
81
+ }),
82
+ defineModule({
83
+ id: `${meta.id}/module/intent-resolver`,
84
+ activatesOn: Events.SetupIntents,
85
+ activate: IntentResolver,
86
+ }),
87
+ ]);
package/src/blockstore.ts CHANGED
@@ -2,11 +2,17 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import { CarWriter } from '@ipld/car';
6
+ import type { Block } from '@ipld/car/api';
5
7
  import { BaseBlockstore } from 'blockstore-core';
6
8
  import { IDBBlockstore } from 'blockstore-idb';
7
- import type { CID } from 'multiformats';
9
+ import debounce from 'debounce';
10
+ import * as IDB from 'idb-keyval';
11
+ import all from 'it-all';
12
+ import { CID } from 'multiformats';
13
+ import * as Uint8Arrays from 'uint8arrays';
8
14
 
9
- import { storeName } from './common';
15
+ import { storeName } from './helpers';
10
16
 
11
17
  export const create = (apiHost?: string) => {
12
18
  return new MixedBlockstore(apiHost);
@@ -18,29 +24,48 @@ export const create = (apiHost?: string) => {
18
24
  */
19
25
  export class MixedBlockstore extends BaseBlockstore {
20
26
  readonly #apiHost: string | undefined;
21
- readonly #idbStore: IDBBlockstore;
27
+ readonly #localStore: IDBBlockstore;
28
+
29
+ #isConnected: boolean;
30
+ #flushQueue: () => void;
31
+ #queue: string[];
22
32
 
23
33
  constructor(apiHost?: string) {
24
34
  super();
25
35
 
26
- this.#apiHost = apiHost;
27
- this.#idbStore = new IDBBlockstore(storeName());
36
+ this.#apiHost = apiHost ? apiHost.replace(/\/?$/, '') : undefined;
37
+ this.#localStore = new IDBBlockstore(storeName());
38
+ this.#isConnected = navigator.onLine;
39
+ this.#queue = [];
40
+
41
+ this.#flushQueue = debounce(this.#flushQueueInt, 5000);
28
42
  }
29
43
 
30
44
  async open() {
31
- await this.#idbStore.open();
45
+ await this.#localStore.open();
46
+ await this.#restoreQueue();
47
+
48
+ window.addEventListener('offline', async () => {
49
+ this.#isConnected = false;
50
+ });
51
+
52
+ window.addEventListener('online', async () => {
53
+ this.#isConnected = true;
54
+ this.#flushQueue();
55
+ });
32
56
  }
33
57
 
34
58
  url(apiHost: string, cid?: CID) {
35
59
  const path = cid ? cid.toString() : '';
36
- return `${apiHost}/api/file/${path}`;
60
+ return `${apiHost}/api/file${path.length ? '/' + path : ''}`;
37
61
  }
38
62
 
39
- // Blockstore implementation
63
+ // BLOCKSTORE IMPLEMENTATION
64
+
40
65
  override async delete(key: CID): Promise<void> {
41
- await this.#idbStore.delete(key);
66
+ await this.#localStore.delete(key);
42
67
 
43
- if (this.#apiHost) {
68
+ if (this.#isConnected && this.#apiHost) {
44
69
  await fetch(this.url(this.#apiHost, key), {
45
70
  method: 'DELETE',
46
71
  });
@@ -48,27 +73,32 @@ export class MixedBlockstore extends BaseBlockstore {
48
73
  }
49
74
 
50
75
  override async get(key: CID): Promise<Uint8Array> {
51
- if (await this.#idbStore.has(key)) {
52
- return await this.#idbStore.get(key);
76
+ if (await this.#localStore.has(key)) {
77
+ return await this.#localStore.get(key);
53
78
  }
54
79
 
55
- if (this.#apiHost) {
56
- return await fetch(this.url(this.#apiHost, key), {
80
+ if (this.#isConnected && this.#apiHost) {
81
+ const block = await fetch(this.url(this.#apiHost, key), {
57
82
  method: 'GET',
58
83
  })
59
84
  .then((r) => r.arrayBuffer())
60
85
  .then((r) => new Uint8Array(r));
86
+
87
+ // Make sure it is cached locally
88
+ await this.#localStore.put(key, block);
89
+
90
+ return block;
61
91
  }
62
92
 
63
- return await this.#idbStore.get(key);
93
+ return await this.#localStore.get(key);
64
94
  }
65
95
 
66
96
  override async has(key: CID): Promise<boolean> {
67
- if (await this.#idbStore.has(key)) {
97
+ if (await this.#localStore.has(key)) {
68
98
  return true;
69
99
  }
70
100
 
71
- if (this.#apiHost) {
101
+ if (this.#isConnected && this.#apiHost) {
72
102
  return await fetch(this.url(this.#apiHost, key), {
73
103
  method: 'HEAD',
74
104
  }).then((r) => r.ok);
@@ -78,19 +108,119 @@ export class MixedBlockstore extends BaseBlockstore {
78
108
  }
79
109
 
80
110
  override async put(key: CID, val: Uint8Array): Promise<CID> {
81
- await this.#idbStore.put(key, val);
111
+ await this.#localStore.put(key, val);
112
+
113
+ await this.#addToQueue(key);
114
+ this.#flushQueue();
82
115
 
116
+ return key;
117
+ }
118
+
119
+ async destroy(): Promise<void> {
120
+ await this.#localStore.destroy();
121
+ }
122
+
123
+ // REMOTE
124
+
125
+ async putRemote(key: CID, val: Uint8Array) {
83
126
  if (this.#apiHost) {
84
127
  await fetch(this.url(this.#apiHost, key), {
85
128
  method: 'POST',
86
129
  body: val,
87
130
  });
88
131
  }
132
+ }
89
133
 
90
- return key;
134
+ async putCarRemote(car: Uint8Array) {
135
+ if (this.#apiHost) {
136
+ await fetch(this.url(this.#apiHost), {
137
+ method: 'POST',
138
+ headers: {
139
+ 'Content-Type': 'application/vnd.ipld.car',
140
+ },
141
+ body: car,
142
+ });
143
+ }
91
144
  }
92
145
 
93
- async destroy(): Promise<void> {
94
- await this.#idbStore.destroy();
146
+ // QUEUE
147
+
148
+ readonly queueCacheName = `${storeName()}/state/queue`;
149
+
150
+ async #addToQueue(key: CID) {
151
+ await this.#saveQueue([...this.#queue, key.toString()]);
152
+ }
153
+
154
+ async #flushQueueInt() {
155
+ if (!this.#isConnected || !this.#apiHost) {
156
+ return;
157
+ }
158
+
159
+ const keys = [...this.#queue];
160
+ if (keys.length === 0) {
161
+ return;
162
+ }
163
+
164
+ // Collect blocks
165
+ const blocks = await Promise.all(
166
+ keys.map(async (key) => {
167
+ const cid = CID.parse(key);
168
+ const bytes = await this.#localStore.get(cid);
169
+ return { cid, bytes };
170
+ }),
171
+ );
172
+
173
+ // TODO: Create & submit CAR
174
+ // DEPENDS ON EDGE BLOB-SERVICE CHANGES
175
+ // const car = await this.#createCar(blocks);
176
+ // await this.putCarRemote(car);
177
+ //
178
+ // Temporary solution:
179
+ await Promise.all(
180
+ blocks.map((block: Block) => {
181
+ return this.putRemote(block.cid, block.bytes);
182
+ }),
183
+ );
184
+
185
+ // Adjust queue
186
+ const queue = [...this.#queue].filter((k) => {
187
+ if (keys.includes(k)) {
188
+ return false;
189
+ }
190
+
191
+ return true;
192
+ });
193
+
194
+ await this.#saveQueue(queue);
195
+ }
196
+
197
+ async #restoreQueue() {
198
+ const fromCache = await IDB.get(this.queueCacheName);
199
+ if (fromCache && Array.isArray(fromCache)) {
200
+ this.#queue = fromCache;
201
+ this.#flushQueue();
202
+ }
203
+ }
204
+
205
+ async #saveQueue(items: string[]) {
206
+ const arr = [...items];
207
+ this.#queue = arr;
208
+ await IDB.set(this.queueCacheName, arr);
209
+ }
210
+
211
+ // 🛠️
212
+
213
+ async #createCar(blocks: Block[]) {
214
+ const { writer, out } = CarWriter.create();
215
+ const outPromise = all(out);
216
+
217
+ await Promise.all(
218
+ blocks.map((block) => {
219
+ return writer.put(block);
220
+ }),
221
+ );
222
+
223
+ await writer.close();
224
+ return Uint8Arrays.concat(await outPromise);
95
225
  }
96
226
  }
@@ -0,0 +1,18 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { contributes, type PluginsContext } from '@dxos/app-framework';
6
+ import { ClientCapabilities } from '@dxos/plugin-client';
7
+
8
+ import { WnfsCapabilities } from './capabilities';
9
+ import * as Blockstore from '../blockstore';
10
+
11
+ export default async (context: PluginsContext) => {
12
+ const client = context.requestCapability(ClientCapabilities.Client);
13
+ const apiHost = client.config.values.runtime?.services?.edge?.url || 'http://localhost:8787';
14
+ const blockstore = Blockstore.create(apiHost);
15
+ await blockstore.open();
16
+
17
+ return contributes(WnfsCapabilities.Blockstore, blockstore);
18
+ };
@@ -0,0 +1,18 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Blockstore } from 'interface-blockstore';
6
+ import { type PrivateDirectory, type PrivateForest } from 'wnfs';
7
+
8
+ import { defineCapability } from '@dxos/app-framework';
9
+ import { type SpaceId } from '@dxos/keys';
10
+
11
+ import { WNFS_PLUGIN } from '../meta';
12
+
13
+ export namespace WnfsCapabilities {
14
+ export const Blockstore = defineCapability<Blockstore>(`${WNFS_PLUGIN}/capability/blockstore`);
15
+
16
+ export type Instances = Record<SpaceId, { directory: PrivateDirectory; forest: PrivateForest }>;
17
+ export const Instances = defineCapability<Instances>(`${WNFS_PLUGIN}/capability/instances`);
18
+ }
@@ -0,0 +1,24 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Effect, pipe } from 'effect';
6
+
7
+ import { Capabilities, chain, contributes, createIntent, type PluginsContext } from '@dxos/app-framework';
8
+ import { SpaceAction } from '@dxos/plugin-space/types';
9
+
10
+ import { WnfsAction } from '../types';
11
+
12
+ export default (context: PluginsContext) => {
13
+ return contributes(Capabilities.FileUploader, (file, space) => {
14
+ const { dispatch } = context.requestCapability(Capabilities.IntentDispatcher);
15
+
16
+ const program = Effect.gen(function* () {
17
+ const fileInfo = yield* dispatch(createIntent(WnfsAction.Upload, { file, space }));
18
+ yield* dispatch(pipe(createIntent(WnfsAction.Create, fileInfo), chain(SpaceAction.AddObject, { target: space })));
19
+ return fileInfo;
20
+ });
21
+
22
+ return Effect.runPromise(program);
23
+ });
24
+ };
@@ -0,0 +1,13 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { lazy } from '@dxos/app-framework';
6
+
7
+ export const Blockstore = lazy(() => import('./blockstore'));
8
+ export const FileUploader = lazy(() => import('./file-uploader'));
9
+ export const IntentResolver = lazy(() => import('./intent-resolver'));
10
+ export const Markdown = lazy(() => import('./markdown'));
11
+ export const ReactSurface = lazy(() => import('./react-surface'));
12
+
13
+ export * from './capabilities';
@@ -0,0 +1,26 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { contributes, Capabilities, createResolver, type PluginsContext } from '@dxos/app-framework';
6
+ import { create } from '@dxos/react-client/echo';
7
+
8
+ import { WnfsCapabilities } from './capabilities';
9
+ import { upload } from '../helpers';
10
+ import { FileType, WnfsAction } from '../types';
11
+
12
+ export default (context: PluginsContext) =>
13
+ contributes(Capabilities.IntentResolver, [
14
+ // TODO(wittjosiah): Deleting the object doesn't delete the wnfs blob.
15
+ // Consider ways to trigger blob deletion based on object deletion and/or adding file system manager.
16
+ createResolver(WnfsAction.Create, ({ name, type, cid }) => ({
17
+ data: {
18
+ object: create(FileType, { name, type, cid }),
19
+ },
20
+ })),
21
+ createResolver(WnfsAction.Upload, async ({ file, space }) => {
22
+ const blockstore = context.requestCapability(WnfsCapabilities.Blockstore);
23
+ const info = await upload({ file, blockstore, space });
24
+ return { data: info };
25
+ }),
26
+ ]);
@@ -0,0 +1,25 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { contributes, type PluginsContext } from '@dxos/app-framework';
6
+ import { MarkdownCapabilities } from '@dxos/plugin-markdown';
7
+ import { getSpace } from '@dxos/react-client/echo';
8
+
9
+ import { WnfsCapabilities } from './capabilities';
10
+ import { image } from '../extensions';
11
+
12
+ export default (context: PluginsContext) =>
13
+ contributes(MarkdownCapabilities.Extensions, [
14
+ ({ document }) => {
15
+ const blockstore = context.requestCapability(WnfsCapabilities.Blockstore);
16
+ const instances = context.requestCapability(WnfsCapabilities.Instances);
17
+
18
+ if (document) {
19
+ const space = getSpace(document);
20
+ return space ? [image({ blockstore, instances, space })] : [];
21
+ }
22
+
23
+ return [];
24
+ },
25
+ ]);