@comapeo/core 1.0.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 (186) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +31 -0
  3. package/dist/blob-api.d.ts +92 -0
  4. package/dist/blob-api.d.ts.map +1 -0
  5. package/dist/blob-store/index.d.ts +163 -0
  6. package/dist/blob-store/index.d.ts.map +1 -0
  7. package/dist/blob-store/live-download.d.ts +107 -0
  8. package/dist/blob-store/live-download.d.ts.map +1 -0
  9. package/dist/config-import.d.ts +74 -0
  10. package/dist/config-import.d.ts.map +1 -0
  11. package/dist/constants.d.ts +14 -0
  12. package/dist/constants.d.ts.map +1 -0
  13. package/dist/core-manager/bitfield-rle.d.ts +25 -0
  14. package/dist/core-manager/bitfield-rle.d.ts.map +1 -0
  15. package/dist/core-manager/core-index.d.ts +56 -0
  16. package/dist/core-manager/core-index.d.ts.map +1 -0
  17. package/dist/core-manager/index.d.ts +125 -0
  18. package/dist/core-manager/index.d.ts.map +1 -0
  19. package/dist/core-manager/random-access-file-pool.d.ts +17 -0
  20. package/dist/core-manager/random-access-file-pool.d.ts.map +1 -0
  21. package/dist/core-manager/remote-bitfield.d.ts +146 -0
  22. package/dist/core-manager/remote-bitfield.d.ts.map +1 -0
  23. package/dist/core-ownership.d.ts +112 -0
  24. package/dist/core-ownership.d.ts.map +1 -0
  25. package/dist/datastore/index.d.ts +91 -0
  26. package/dist/datastore/index.d.ts.map +1 -0
  27. package/dist/datatype/index.d.ts +108 -0
  28. package/dist/discovery/local-discovery.d.ts +64 -0
  29. package/dist/discovery/local-discovery.d.ts.map +1 -0
  30. package/dist/errors.d.ts +4 -0
  31. package/dist/errors.d.ts.map +1 -0
  32. package/dist/fastify-controller.d.ts +27 -0
  33. package/dist/fastify-controller.d.ts.map +1 -0
  34. package/dist/fastify-plugins/blobs.d.ts +6 -0
  35. package/dist/fastify-plugins/blobs.d.ts.map +1 -0
  36. package/dist/fastify-plugins/constants.d.ts +3 -0
  37. package/dist/fastify-plugins/constants.d.ts.map +1 -0
  38. package/dist/fastify-plugins/icons.d.ts +6 -0
  39. package/dist/fastify-plugins/icons.d.ts.map +1 -0
  40. package/dist/fastify-plugins/maps/index.d.ts +11 -0
  41. package/dist/fastify-plugins/maps/index.d.ts.map +1 -0
  42. package/dist/fastify-plugins/maps/offline-fallback-map.d.ts +12 -0
  43. package/dist/fastify-plugins/maps/offline-fallback-map.d.ts.map +1 -0
  44. package/dist/fastify-plugins/maps/static-maps.d.ts +11 -0
  45. package/dist/fastify-plugins/maps/static-maps.d.ts.map +1 -0
  46. package/dist/fastify-plugins/utils.d.ts +23 -0
  47. package/dist/fastify-plugins/utils.d.ts.map +1 -0
  48. package/dist/generated/extensions.d.ts +44 -0
  49. package/dist/generated/extensions.d.ts.map +1 -0
  50. package/dist/generated/keys.d.ts +36 -0
  51. package/dist/generated/keys.d.ts.map +1 -0
  52. package/dist/generated/rpc.d.ts +87 -0
  53. package/dist/generated/rpc.d.ts.map +1 -0
  54. package/dist/icon-api.d.ts +109 -0
  55. package/dist/icon-api.d.ts.map +1 -0
  56. package/dist/index-writer/index.d.ts +51 -0
  57. package/dist/index-writer/index.d.ts.map +1 -0
  58. package/dist/index.d.ts +14 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/invite-api.d.ts +70 -0
  61. package/dist/invite-api.d.ts.map +1 -0
  62. package/dist/lib/hashmap.d.ts +62 -0
  63. package/dist/lib/hashmap.d.ts.map +1 -0
  64. package/dist/lib/hypercore-helpers.d.ts +6 -0
  65. package/dist/lib/hypercore-helpers.d.ts.map +1 -0
  66. package/dist/lib/noise-secret-stream-helpers.d.ts +45 -0
  67. package/dist/lib/noise-secret-stream-helpers.d.ts.map +1 -0
  68. package/dist/lib/ponyfills.d.ts +10 -0
  69. package/dist/lib/ponyfills.d.ts.map +1 -0
  70. package/dist/lib/string.d.ts +2 -0
  71. package/dist/lib/string.d.ts.map +1 -0
  72. package/dist/lib/timing-safe-equal.d.ts +15 -0
  73. package/dist/lib/timing-safe-equal.d.ts.map +1 -0
  74. package/dist/local-peers.d.ts +151 -0
  75. package/dist/local-peers.d.ts.map +1 -0
  76. package/dist/logger.d.ts +32 -0
  77. package/dist/logger.d.ts.map +1 -0
  78. package/dist/mapeo-manager.d.ts +178 -0
  79. package/dist/mapeo-manager.d.ts.map +1 -0
  80. package/dist/mapeo-project.d.ts +3233 -0
  81. package/dist/mapeo-project.d.ts.map +1 -0
  82. package/dist/member-api.d.ts +114 -0
  83. package/dist/member-api.d.ts.map +1 -0
  84. package/dist/roles.d.ts +157 -0
  85. package/dist/roles.d.ts.map +1 -0
  86. package/dist/schema/client.d.ts +284 -0
  87. package/dist/schema/client.d.ts.map +1 -0
  88. package/dist/schema/project.d.ts +1812 -0
  89. package/dist/schema/project.d.ts.map +1 -0
  90. package/dist/schema/schema-to-drizzle.d.ts +20 -0
  91. package/dist/schema/schema-to-drizzle.d.ts.map +1 -0
  92. package/dist/schema/types.d.ts +98 -0
  93. package/dist/schema/types.d.ts.map +1 -0
  94. package/dist/schema/utils.d.ts +55 -0
  95. package/dist/schema/utils.d.ts.map +1 -0
  96. package/dist/sync/core-sync-state.d.ts +252 -0
  97. package/dist/sync/core-sync-state.d.ts.map +1 -0
  98. package/dist/sync/namespace-sync-state.d.ts +47 -0
  99. package/dist/sync/namespace-sync-state.d.ts.map +1 -0
  100. package/dist/sync/peer-sync-controller.d.ts +44 -0
  101. package/dist/sync/peer-sync-controller.d.ts.map +1 -0
  102. package/dist/sync/sync-api.d.ts +158 -0
  103. package/dist/sync/sync-api.d.ts.map +1 -0
  104. package/dist/sync/sync-state.d.ts +40 -0
  105. package/dist/sync/sync-state.d.ts.map +1 -0
  106. package/dist/translation-api.d.ts +288 -0
  107. package/dist/translation-api.d.ts.map +1 -0
  108. package/dist/types.d.ts +115 -0
  109. package/dist/types.d.ts.map +1 -0
  110. package/dist/utils.d.ts +115 -0
  111. package/dist/utils.d.ts.map +1 -0
  112. package/dist/utils_types.d.ts +14 -0
  113. package/drizzle/client/0000_bumpy_carnage.sql +33 -0
  114. package/drizzle/client/meta/0000_snapshot.json +199 -0
  115. package/drizzle/client/meta/_journal.json +13 -0
  116. package/drizzle/project/0000_spooky_lady_ursula.sql +192 -0
  117. package/drizzle/project/meta/0000_snapshot.json +1137 -0
  118. package/drizzle/project/meta/_journal.json +13 -0
  119. package/package.json +202 -0
  120. package/src/blob-api.js +139 -0
  121. package/src/blob-store/index.js +325 -0
  122. package/src/blob-store/live-download.js +373 -0
  123. package/src/config-import.js +604 -0
  124. package/src/constants.js +34 -0
  125. package/src/core-manager/bitfield-rle.js +235 -0
  126. package/src/core-manager/core-index.js +87 -0
  127. package/src/core-manager/index.js +504 -0
  128. package/src/core-manager/random-access-file-pool.js +30 -0
  129. package/src/core-manager/remote-bitfield.js +416 -0
  130. package/src/core-ownership.js +235 -0
  131. package/src/datastore/README.md +46 -0
  132. package/src/datastore/index.js +234 -0
  133. package/src/datatype/README.md +33 -0
  134. package/src/datatype/index.d.ts +108 -0
  135. package/src/datatype/index.js +358 -0
  136. package/src/discovery/local-discovery.js +303 -0
  137. package/src/errors.js +5 -0
  138. package/src/fastify-controller.js +84 -0
  139. package/src/fastify-plugins/blobs.js +139 -0
  140. package/src/fastify-plugins/constants.js +5 -0
  141. package/src/fastify-plugins/icons.js +158 -0
  142. package/src/fastify-plugins/maps/index.js +173 -0
  143. package/src/fastify-plugins/maps/offline-fallback-map.js +114 -0
  144. package/src/fastify-plugins/maps/static-maps.js +271 -0
  145. package/src/fastify-plugins/utils.js +52 -0
  146. package/src/generated/README.md +3 -0
  147. package/src/generated/extensions.d.ts +44 -0
  148. package/src/generated/extensions.js +196 -0
  149. package/src/generated/extensions.ts +237 -0
  150. package/src/generated/keys.d.ts +36 -0
  151. package/src/generated/keys.js +148 -0
  152. package/src/generated/keys.ts +185 -0
  153. package/src/generated/rpc.d.ts +87 -0
  154. package/src/generated/rpc.js +389 -0
  155. package/src/generated/rpc.ts +463 -0
  156. package/src/icon-api.js +282 -0
  157. package/src/index-writer/README.md +38 -0
  158. package/src/index-writer/index.js +124 -0
  159. package/src/index.js +16 -0
  160. package/src/invite-api.js +450 -0
  161. package/src/lib/hashmap.js +91 -0
  162. package/src/lib/hypercore-helpers.js +18 -0
  163. package/src/lib/noise-secret-stream-helpers.js +37 -0
  164. package/src/lib/ponyfills.js +25 -0
  165. package/src/lib/string.js +7 -0
  166. package/src/lib/timing-safe-equal.js +34 -0
  167. package/src/local-peers.js +737 -0
  168. package/src/logger.js +99 -0
  169. package/src/mapeo-manager.js +914 -0
  170. package/src/mapeo-project.js +980 -0
  171. package/src/member-api.js +319 -0
  172. package/src/roles.js +412 -0
  173. package/src/schema/client.js +55 -0
  174. package/src/schema/project.js +44 -0
  175. package/src/schema/schema-to-drizzle.js +118 -0
  176. package/src/schema/types.ts +153 -0
  177. package/src/schema/utils.js +51 -0
  178. package/src/sync/core-sync-state.js +440 -0
  179. package/src/sync/namespace-sync-state.js +193 -0
  180. package/src/sync/peer-sync-controller.js +332 -0
  181. package/src/sync/sync-api.js +588 -0
  182. package/src/sync/sync-state.js +63 -0
  183. package/src/translation-api.js +141 -0
  184. package/src/types.ts +149 -0
  185. package/src/utils.js +210 -0
  186. package/src/utils_types.d.ts +14 -0
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": "5",
3
+ "dialect": "sqlite",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "5",
8
+ "when": 1726514275142,
9
+ "tag": "0000_spooky_lady_ursula",
10
+ "breakpoints": true
11
+ }
12
+ ]
13
+ }
package/package.json ADDED
@@ -0,0 +1,202 @@
1
+ {
2
+ "name": "@comapeo/core",
3
+ "version": "1.0.0",
4
+ "description": "Offline p2p mapping library",
5
+ "main": "src/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "scripts": {
9
+ "lint": "eslint --cache .",
10
+ "format": "prettier . --write",
11
+ "test": "npm-run-all lint test:prettier build:types type test:buildConfigs test:unit test:e2e test:types",
12
+ "test:prettier": "prettier --check .",
13
+ "test:buildConfigs": "node scripts/build-config-fixtures.js",
14
+ "test:unit": "node --test tests/*.js tests/**/*.js",
15
+ "test:e2e": "node --test test-e2e/*.js test-e2e/**/*.js",
16
+ "test:types": "tsc -p test-types/tsconfig.json",
17
+ "build:types": "tsc -p tsconfig.npm.json && cpy 'src/**/*.d.ts' dist",
18
+ "bench": "nanobench benchmarks/*.js",
19
+ "type": "tsc",
20
+ "doc": "rimraf docs/api/md && typedoc --plugin typedoc-plugin-markdown --plugin typedoc-plugin-missing-exports --out docs/api/md",
21
+ "doc:publish": "rimraf docs/api/html && typedoc --plugin typedoc-plugin-missing-exports --out docs/api/html",
22
+ "protobuf": "node ./scripts/build-messages.js",
23
+ "db:generate:project": "drizzle-kit generate:sqlite --schema src/schema/project.js --out drizzle/project",
24
+ "db:generate:client": "drizzle-kit generate:sqlite --schema src/schema/client.js --out drizzle/client",
25
+ "prepack": "npm run build:types",
26
+ "prepare": "husky install"
27
+ },
28
+ "files": [
29
+ "src",
30
+ "dist",
31
+ "drizzle"
32
+ ],
33
+ "prettier": {
34
+ "semi": false,
35
+ "singleQuote": true
36
+ },
37
+ "lint-staged": {
38
+ "*.js": [
39
+ "eslint --cache"
40
+ ],
41
+ "*": [
42
+ "prettier --check"
43
+ ]
44
+ },
45
+ "eslintConfig": {
46
+ "env": {
47
+ "commonjs": true,
48
+ "es2022": true,
49
+ "node": true
50
+ },
51
+ "extends": "eslint:recommended",
52
+ "parserOptions": {
53
+ "ecmaVersion": 13,
54
+ "sourceType": "module"
55
+ },
56
+ "rules": {
57
+ "curly": [
58
+ "error",
59
+ "multi-line"
60
+ ],
61
+ "eqeqeq": "error",
62
+ "default-case": "error",
63
+ "default-case-last": "error",
64
+ "prefer-const": "error",
65
+ "no-unused-vars": [
66
+ "error",
67
+ {
68
+ "varsIgnorePattern": "^_",
69
+ "argsIgnorePattern": "^_"
70
+ }
71
+ ],
72
+ "no-restricted-imports": [
73
+ "error",
74
+ {
75
+ "paths": [
76
+ {
77
+ "name": "assert",
78
+ "message": "Prefer importing node:assert/strict."
79
+ },
80
+ {
81
+ "name": "node:assert",
82
+ "message": "Prefer importing node:assert/strict."
83
+ }
84
+ ]
85
+ }
86
+ ],
87
+ "no-var": "error"
88
+ },
89
+ "ignorePatterns": [
90
+ "docs/*"
91
+ ]
92
+ },
93
+ "repository": {
94
+ "type": "git",
95
+ "url": "git+https://github.com/digidem/comapeo-core.git"
96
+ },
97
+ "authors": [
98
+ "Andrew Chou <achou@awana.digital>",
99
+ "Evan Hahn <ehahn@awana.digital>",
100
+ "Gregor MacLennan <gmaclennan@awana.digital>",
101
+ "Seth Vincent",
102
+ "Tomás Ciccola <tciccola@awana.digital>"
103
+ ],
104
+ "license": "MIT",
105
+ "bugs": {
106
+ "url": "https://github.com/digidem/comapeo-core/issues"
107
+ },
108
+ "homepage": "https://github.com/digidem/comapeo-core#readme",
109
+ "devDependencies": {
110
+ "@bufbuild/buf": "^1.26.1",
111
+ "@mapeo/default-config": "5.0.0",
112
+ "@mapeo/mock-data": "2.0.0",
113
+ "@sinonjs/fake-timers": "^10.0.2",
114
+ "@types/b4a": "^1.6.0",
115
+ "@types/bogon": "^1.0.2",
116
+ "@types/compact-encoding": "^2.15.0",
117
+ "@types/debug": "^4.1.8",
118
+ "@types/json-schema": "^7.0.11",
119
+ "@types/json-stable-stringify": "^1.0.36",
120
+ "@types/nanobench": "^3.0.0",
121
+ "@types/node": "^18.19.33",
122
+ "@types/sinonjs__fake-timers": "^8.1.2",
123
+ "@types/streamx": "^2.9.5",
124
+ "@types/sub-encoder": "^2.1.0",
125
+ "@types/throttle-debounce": "^5.0.0",
126
+ "@types/varint": "^6.0.1",
127
+ "@types/yauzl-promise": "^4.0.0",
128
+ "@types/yazl": "^2.4.5",
129
+ "bitfield": "^4.1.0",
130
+ "cpy": "^10.1.0",
131
+ "cpy-cli": "^5.0.0",
132
+ "drizzle-kit": "^0.20.14",
133
+ "eslint": "^8.57.0",
134
+ "husky": "^8.0.0",
135
+ "iterpal": "^0.4.0",
136
+ "lint-staged": "^14.0.1",
137
+ "mapeo-offline-map": "^2.0.0",
138
+ "math-random-seed": "^2.0.0",
139
+ "nanobench": "^3.0.0",
140
+ "npm-run-all": "^4.1.5",
141
+ "prettier": "^2.8.8",
142
+ "random-access-file": "^4.0.7",
143
+ "random-access-memory": "^6.2.1",
144
+ "rimraf": "^5.0.5",
145
+ "tempy": "^3.1.0",
146
+ "ts-proto": "^1.156.7",
147
+ "typedoc": "^0.26.6",
148
+ "typedoc-plugin-markdown": "^4.2.5",
149
+ "typedoc-plugin-missing-exports": "^3.0.0",
150
+ "typescript": "^5.5.4",
151
+ "yazl": "^2.5.1"
152
+ },
153
+ "dependencies": {
154
+ "@digidem/types": "^2.3.0",
155
+ "@electron/asar": "^3.2.8",
156
+ "@fastify/error": "^3.4.1",
157
+ "@fastify/static": "^7.0.3",
158
+ "@fastify/type-provider-typebox": "^4.0.0",
159
+ "@hyperswarm/secret-stream": "^6.1.2",
160
+ "@mapeo/crypto": "1.0.0-alpha.10",
161
+ "@comapeo/schema": "1.0.0",
162
+ "@mapeo/sqlite-indexer": "1.0.0-alpha.9",
163
+ "@sinclair/typebox": "^0.29.6",
164
+ "b4a": "^1.6.3",
165
+ "bcp-47": "^2.1.0",
166
+ "better-sqlite3": "^8.7.0",
167
+ "big-sparse-array": "^1.0.3",
168
+ "bogon": "^1.1.0",
169
+ "compact-encoding": "^2.12.0",
170
+ "corestore": "^6.8.4",
171
+ "debug": "^4.3.4",
172
+ "dot-prop": "^9.0.0",
173
+ "drizzle-orm": "^0.30.8",
174
+ "fastify": ">= 4",
175
+ "fastify-plugin": "^4.5.1",
176
+ "hyperblobs": "2.3.0",
177
+ "hypercore": "10.17.0",
178
+ "hypercore-crypto": "3.4.2",
179
+ "hyperdrive": "11.5.3",
180
+ "json-stable-stringify": "^1.1.1",
181
+ "magic-bytes.js": "^1.10.0",
182
+ "map-obj": "^5.0.2",
183
+ "mime": "^4.0.3",
184
+ "multi-core-indexer": "^1.0.0-alpha.10",
185
+ "p-defer": "^4.0.0",
186
+ "p-event": "^6.0.1",
187
+ "p-timeout": "^6.1.2",
188
+ "protobufjs": "^7.2.3",
189
+ "protomux": "^3.4.1",
190
+ "quickbit-universal": "^2.2.0",
191
+ "sodium-universal": "^4.0.0",
192
+ "start-stop-state-machine": "^1.2.0",
193
+ "streamx": "^2.19.0",
194
+ "sub-encoder": "^2.1.1",
195
+ "throttle-debounce": "^5.0.0",
196
+ "tiny-typed-emitter": "^2.1.0",
197
+ "type-fest": "^4.5.0",
198
+ "undici": "^6.13.0",
199
+ "varint": "^6.0.0",
200
+ "yauzl-promise": "^4.0.0"
201
+ }
202
+ }
@@ -0,0 +1,139 @@
1
+ import fs from 'node:fs'
2
+ // @ts-expect-error - pipelinePromise missing from streamx types
3
+ import { Transform, pipelinePromise as pipeline } from 'streamx'
4
+ import { createHash, randomBytes } from 'node:crypto'
5
+ /** @import { BlobId, BlobType } from './types.js' */
6
+
7
+ /**
8
+ * Location coordinate data. Based on [Expo's `LocationObjectCoords`][0].
9
+ * [0]: https://docs.expo.dev/versions/latest/sdk/location/#locationobjectcoords
10
+ *
11
+ * @typedef {object} LocationObjectCoords
12
+ * @prop {number | null} accuracy
13
+ * @prop {number | null} altitude
14
+ * @prop {number | null} altitudeAccuracy
15
+ * @prop {number | null} heading
16
+ * @prop {number} latitude
17
+ * @prop {number} longitude
18
+ * @prop {number | null} speed
19
+ */
20
+
21
+ /**
22
+ * Location metadata for a blob. Based on [Expo's `LocationObject`][0].
23
+ * [0]: https://docs.expo.dev/versions/latest/sdk/location/#locationobject
24
+ *
25
+ * @typedef {object} LocationObject
26
+ * @prop {LocationObjectCoords} coords
27
+ * @prop {boolean} [mocked]
28
+ * @prop {number} timestamp
29
+ */
30
+
31
+ /**
32
+ * @typedef {object} Metadata
33
+ * @prop {string} mimeType
34
+ * @prop {number} timestamp
35
+ * @prop {LocationObject} [location]
36
+ */
37
+
38
+ export class BlobApi {
39
+ #blobStore
40
+ #getMediaBaseUrl
41
+
42
+ /**
43
+ * @param {object} options
44
+ * @param {import('./blob-store/index.js').BlobStore} options.blobStore
45
+ * @param {() => Promise<string>} options.getMediaBaseUrl
46
+ */
47
+ constructor({ blobStore, getMediaBaseUrl }) {
48
+ this.#blobStore = blobStore
49
+ this.#getMediaBaseUrl = getMediaBaseUrl
50
+ }
51
+
52
+ /**
53
+ * Get a url for a blob based on its BlobId
54
+ * @param {BlobId} blobId
55
+ * @returns {Promise<string>}
56
+ */
57
+ async getUrl(blobId) {
58
+ const { driveId, type, variant, name } = blobId
59
+
60
+ let base = await this.#getMediaBaseUrl()
61
+
62
+ if (!base.endsWith('/')) {
63
+ base += '/'
64
+ }
65
+
66
+ return base + `${driveId}/${type}/${variant}/${name}`
67
+ }
68
+
69
+ /**
70
+ * Write blobs for provided variants of a file
71
+ * @param {{ original: string, preview?: string, thumbnail?: string }} filepaths
72
+ * @param {Metadata} metadata
73
+ * @returns {Promise<{ driveId: string, name: string, type: 'photo' | 'video' | 'audio', hash: string }>}
74
+ */
75
+ async create(filepaths, metadata) {
76
+ const { original, preview, thumbnail } = filepaths
77
+ const { mimeType } = metadata
78
+ const type = getType(mimeType)
79
+ const name = randomBytes(8).toString('hex')
80
+ const hash = createHash('sha256')
81
+
82
+ const ws = this.#blobStore.createWriteStream(
83
+ { type, variant: 'original', name },
84
+ { metadata }
85
+ )
86
+ const writePromises = [
87
+ pipeline(fs.createReadStream(original), hashTransform(hash), ws),
88
+ ]
89
+
90
+ if (preview) {
91
+ const ws = this.#blobStore.createWriteStream(
92
+ { type, variant: 'preview', name },
93
+ { metadata }
94
+ )
95
+ writePromises.push(pipeline(fs.createReadStream(preview), ws))
96
+ }
97
+
98
+ if (thumbnail) {
99
+ const ws = this.#blobStore.createWriteStream(
100
+ { type, variant: 'thumbnail', name },
101
+ { metadata }
102
+ )
103
+ writePromises.push(pipeline(fs.createReadStream(thumbnail), ws))
104
+ }
105
+
106
+ await Promise.all(writePromises)
107
+
108
+ return {
109
+ driveId: this.#blobStore.writerDriveId,
110
+ name,
111
+ type,
112
+ hash: hash.digest('hex'),
113
+ }
114
+ }
115
+ }
116
+
117
+ /**
118
+ * @param {import('node:crypto').Hash} hash
119
+ */
120
+ function hashTransform(hash) {
121
+ return new Transform({
122
+ transform: (data, cb) => {
123
+ hash.update(data)
124
+ cb(null, data)
125
+ },
126
+ })
127
+ }
128
+
129
+ /**
130
+ * @param {string} mimeType
131
+ * @returns {BlobType}
132
+ */
133
+ function getType(mimeType) {
134
+ if (mimeType.startsWith('image')) return 'photo'
135
+ if (mimeType.startsWith('video')) return 'video'
136
+ if (mimeType.startsWith('audio')) return 'audio'
137
+
138
+ throw new Error(`Unsupported mimeType: ${mimeType}`)
139
+ }
@@ -0,0 +1,325 @@
1
+ import Hyperdrive from 'hyperdrive'
2
+ import b4a from 'b4a'
3
+ import util from 'node:util'
4
+ import { discoveryKey } from 'hypercore-crypto'
5
+ import { TypedEmitter } from 'tiny-typed-emitter'
6
+ import { LiveDownload } from './live-download.js'
7
+ /** @import { BlobId } from '../types.js' */
8
+
9
+ /** @typedef {TypedEmitter<{ 'add-drive': (drive: import('hyperdrive')) => void }>} InternalDriveEmitter */
10
+
11
+ // prop = blob type name
12
+ // value = array of blob variants supported for that type
13
+ const SUPPORTED_BLOB_VARIANTS = /** @type {const} */ ({
14
+ photo: ['original', 'preview', 'thumbnail'],
15
+ audio: ['original'],
16
+ video: ['original'],
17
+ })
18
+
19
+ // Cannot directly export the const assignment above because export does not
20
+ // like the assignment being wrapped in parenthesis, which is necessary to cast
21
+ // the type with JSDoc
22
+ export { SUPPORTED_BLOB_VARIANTS }
23
+
24
+ class ErrNotFound extends Error {
25
+ constructor(message = 'NotFound') {
26
+ super(message)
27
+ this.code = 'ENOENT'
28
+ }
29
+ }
30
+
31
+ export class BlobStore {
32
+ /** @type {Map<string, Hyperdrive>} Indexed by hex-encoded discovery key */
33
+ #hyperdrives = new Map()
34
+ #writer
35
+ /**
36
+ * Used to communicate to live download instances when new drives are added
37
+ * @type {InternalDriveEmitter}
38
+ */
39
+ #driveEmitter = new TypedEmitter()
40
+
41
+ /**
42
+ * @param {object} options
43
+ * @param {import('../core-manager/index.js').CoreManager} options.coreManager
44
+ */
45
+ constructor({ coreManager }) {
46
+ /** @type {undefined | (Hyperdrive & { key: Buffer })} */
47
+ let writer
48
+ const corestore = new PretendCorestore({ coreManager })
49
+ const blobIndexCores = coreManager.getCores('blobIndex')
50
+ const { key: writerKey } = coreManager.getWriterCore('blobIndex')
51
+ for (const { key } of blobIndexCores) {
52
+ // @ts-ignore - we know pretendCorestore is not actually a Corestore
53
+ const drive = new Hyperdrive(corestore, key)
54
+ // We use the discovery key to derive the id for a drive
55
+ this.#hyperdrives.set(getDiscoveryId(key), drive)
56
+ if (key.equals(writerKey)) {
57
+ writer = proxyProps(drive, { key: writerKey })
58
+ }
59
+ }
60
+ if (!writer) {
61
+ throw new Error('Could not find a writer for the blobIndex namespace')
62
+ }
63
+ this.#writer = writer
64
+
65
+ coreManager.on('add-core', ({ key, namespace }) => {
66
+ if (namespace !== 'blobIndex') return
67
+ // We use the discovery key to derive the id for a drive
68
+ const driveId = getDiscoveryId(key)
69
+ if (this.#hyperdrives.has(driveId)) return
70
+ // @ts-ignore - we know pretendCorestore is not actually a Corestore
71
+ const drive = new Hyperdrive(corestore, key)
72
+ this.#hyperdrives.set(driveId, drive)
73
+ this.#driveEmitter.emit('add-drive', drive)
74
+ })
75
+ }
76
+
77
+ get writerDriveId() {
78
+ return getDiscoveryId(this.#writer.key)
79
+ }
80
+
81
+ /**
82
+ * @param {string} driveId hex-encoded discovery key
83
+ */
84
+ #getDrive(driveId) {
85
+ const drive = this.#hyperdrives.get(driveId)
86
+ if (!drive) throw new Error('Drive not found ' + driveId.slice(0, 7))
87
+ return drive
88
+ }
89
+
90
+ /**
91
+ * @param {BlobId} blobId
92
+ * @param {object} opts
93
+ * @param {false} [opts.wait=false] Set to `true` to wait for a blob to download, otherwise will throw if blob is not available locally
94
+ * @param {never} [opts.timeout] Optional timeout to wait for a blob to download
95
+ */
96
+ async get({ type, variant, name, driveId }, { wait = false, timeout } = {}) {
97
+ const drive = this.#getDrive(driveId)
98
+ const path = makePath({ type, variant, name })
99
+ const blob = await drive.get(path, { wait, timeout })
100
+ if (!blob) throw new ErrNotFound()
101
+ return blob
102
+ }
103
+
104
+ /**
105
+ * Download blobs from all drives, optionally filtering particular blob types
106
+ * or blob variants. Download will be 'live' and will continue downloading new
107
+ * data as it becomes available from any replicating drive.
108
+ *
109
+ * If no filter is specified, all blobs will be downloaded. If a filter is
110
+ * specified, then _only_ blobs that match the filter will be downloaded.
111
+ *
112
+ * @param {import('../types.js').BlobFilter} [filter] Filter blob types and/or variants to download. Filter is { [BlobType]: BlobVariants[] }. At least one blob variant must be specified for each blob type.
113
+ * @param {object} options
114
+ * @param {AbortSignal} [options.signal] Optional AbortSignal to cancel in-progress download
115
+ * @returns EventEmitter with `.state` propery, emits `state` with new state when it updates
116
+ */
117
+ download(filter, { signal } = {}) {
118
+ return new LiveDownload(this.#hyperdrives.values(), this.#driveEmitter, {
119
+ filter,
120
+ signal,
121
+ })
122
+ }
123
+
124
+ /**
125
+ * @param {BlobId} blobId
126
+ * @param {object} [options]
127
+ * @param {boolean} [options.wait=false] Set to `true` to wait for a blob to download, otherwise will throw if blob is not available locally
128
+ * @param {number} [options.timeout] Optional timeout to wait for a blob to download
129
+ */
130
+ createReadStream(
131
+ { type, variant, name, driveId },
132
+ options = { wait: false }
133
+ ) {
134
+ // TODO: Error thrown from this be an emit error on the returned stream?
135
+ const drive = this.#getDrive(driveId)
136
+ const path = makePath({ type, variant, name })
137
+
138
+ // @ts-ignore - TODO: update @digidem/types to include wait/timeout options
139
+ return drive.createReadStream(path, options)
140
+ }
141
+
142
+ /**
143
+ * Optimization for creating the blobs read stream when you have
144
+ * previously read the entry from Hyperdrive using `drive.entry`
145
+ * @param {BlobId['driveId']} driveId Hyperdrive drive discovery id
146
+ * @param {import('hyperdrive').HyperdriveEntry} entry Hyperdrive entry
147
+ * @param {object} [options]
148
+ * @param {boolean} [options.wait=false] Set to `true` to wait for a blob to download, otherwise will throw if blob is not available locally
149
+ */
150
+ async createEntryReadStream(driveId, entry, options = { wait: false }) {
151
+ const drive = this.#getDrive(driveId)
152
+ const blobs = await drive.getBlobs()
153
+
154
+ if (!blobs) {
155
+ throw new Error(
156
+ 'Hyperblobs instance not found for drive ' + driveId.slice(0, 7)
157
+ )
158
+ }
159
+
160
+ return blobs.createReadStream(entry.value.blob, options)
161
+ }
162
+
163
+ /**
164
+ * @param {BlobId['driveId']} driveId Hyperdrive drive id
165
+ * @param {import('hyperdrive').HyperdriveEntry} entry Hyperdrive entry
166
+ * @param {object} [opts]
167
+ * @param {number} [opts.length]
168
+ *
169
+ * @returns {Promise<Buffer | null>}
170
+ */
171
+ async getEntryBlob(driveId, entry, { length } = {}) {
172
+ const drive = this.#getDrive(driveId)
173
+ const blobs = await drive.getBlobs()
174
+
175
+ if (!blobs) {
176
+ throw new Error(
177
+ 'Hyperblobs instance not found for drive ' + driveId.slice(0, 7)
178
+ )
179
+ }
180
+
181
+ return blobs.get(entry.value.blob, { wait: false, start: 0, length })
182
+ }
183
+
184
+ /**
185
+ *
186
+ * @param {Omit<BlobId, 'driveId'>} blobId
187
+ * @param {Buffer} blob
188
+ * @param {object} [options]
189
+ * @param {{mimeType: string}} [options.metadata] Metadata to store with the blob
190
+ * @returns {Promise<string>} discovery key as hex string of hyperdrive where blob is stored
191
+ */
192
+ async put({ type, variant, name }, blob, options) {
193
+ const path = makePath({ type, variant, name })
194
+ await this.#writer.put(path, blob, options)
195
+ return this.writerDriveId
196
+ }
197
+
198
+ /**
199
+ * @param {Omit<BlobId, 'driveId'>} blobId
200
+ * @param {object} [options]
201
+ * @param {{mimeType: string}} [options.metadata] Metadata to store with the blob
202
+ */
203
+ createWriteStream({ type, variant, name }, options) {
204
+ const path = makePath({ type, variant, name })
205
+ const stream = this.#writer.createWriteStream(path, options)
206
+ return proxyProps(stream, {
207
+ driveId: this.writerDriveId,
208
+ })
209
+ }
210
+
211
+ /**
212
+ * @param {BlobId} blobId
213
+ * @param {object} [options]
214
+ * @param {boolean} [options.follow=false] Set to `true` to follow symlinks (16 max or throws an error)
215
+ * @param {false} [options.wait=false] Set to `true` to wait for a blob to download, otherwise will throw if blob is not available locally
216
+ * @param {never} [options.timeout] Optional timeout to wait for a blob to download
217
+ * @returns {Promise<import('hyperdrive').HyperdriveEntry | null>}
218
+ */
219
+ async entry(
220
+ { type, variant, name, driveId },
221
+ options = { follow: false, wait: false }
222
+ ) {
223
+ const drive = this.#hyperdrives.get(driveId)
224
+ if (!drive) throw new Error('Drive not found ' + driveId.slice(0, 7))
225
+ const path = makePath({ type, variant, name })
226
+ const entry = await drive.entry(path, options)
227
+ return entry
228
+ }
229
+
230
+ /**
231
+ * @param {BlobId} blobId
232
+ * @param {object} [options]
233
+ * @param {boolean} [options.diff=false] Enable to return an object with a `block` property with number of bytes removed
234
+ * @return {Promise<{ blocks: number } | null>}
235
+ */
236
+ async clear({ type, variant, name, driveId }, options = {}) {
237
+ const path = makePath({ type, variant, name })
238
+ const drive = this.#getDrive(driveId)
239
+
240
+ return drive.clear(path, options)
241
+ }
242
+ }
243
+
244
+ /**
245
+ * @template {object} T
246
+ * @template {object} U
247
+ * @param {T} target
248
+ * @param {U} props
249
+ * @returns {T & U}
250
+ */
251
+ function proxyProps(target, props) {
252
+ // @ts-ignore - too much time to learn how to teach this to Typescript
253
+ return new Proxy(target, {
254
+ get(target, prop, receiver) {
255
+ if (Object.hasOwn(props, prop)) {
256
+ return Reflect.get(props, prop, receiver)
257
+ } else {
258
+ return Reflect.get(target, prop, receiver)
259
+ }
260
+ },
261
+ })
262
+ }
263
+
264
+ /** @param {Pick<BlobId, 'type' | 'variant' | 'name'>} opts */
265
+ function makePath({ type, variant, name }) {
266
+ return `/${type}/${variant}/${name}`
267
+ }
268
+
269
+ /**
270
+ * Implements the `get()` method as used by hyperdrive-next. It returns the
271
+ * relevant cores from the Mapeo CoreManager.
272
+ */
273
+ class PretendCorestore {
274
+ #coreManager
275
+ /**
276
+ * @param {object} options
277
+ * @param {import('../core-manager/index.js').CoreManager} options.coreManager
278
+ */
279
+ constructor({ coreManager }) {
280
+ this.#coreManager = coreManager
281
+ }
282
+
283
+ /**
284
+ * @param {Buffer | { publicKey: Buffer } | { name: string }} opts
285
+ * @returns {import('hypercore')<"binary", Buffer> | undefined}
286
+ */
287
+ get(opts) {
288
+ if (b4a.isBuffer(opts)) {
289
+ opts = { publicKey: opts }
290
+ }
291
+ if ('key' in opts) {
292
+ // @ts-ignore
293
+ opts.publicKey = opts.key
294
+ }
295
+ if ('publicKey' in opts) {
296
+ // NB! We should always add blobIndex (Hyperbee) cores to the core manager
297
+ // before we use them here. We would only reach the addCore path if the
298
+ // blob core is read from the hyperbee header (before it is added to the
299
+ // core manager)
300
+ return (
301
+ this.#coreManager.getCoreByKey(opts.publicKey) ||
302
+ this.#coreManager.addCore(opts.publicKey, 'blob').core
303
+ )
304
+ } else if (opts.name === 'db') {
305
+ return this.#coreManager.getWriterCore('blobIndex').core
306
+ } else if (opts.name.includes('blobs')) {
307
+ return this.#coreManager.getWriterCore('blob').core
308
+ } else {
309
+ throw new Error(
310
+ 'Unsupported corestore.get() with opts ' + util.inspect(opts)
311
+ )
312
+ }
313
+ }
314
+
315
+ /** no-op */
316
+ close() {}
317
+ }
318
+
319
+ /**
320
+ * @param {Buffer} key Public key of hypercore
321
+ * @returns {string} Hex-encoded string of derived discovery key
322
+ */
323
+ function getDiscoveryId(key) {
324
+ return discoveryKey(key).toString('hex')
325
+ }