@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.
- package/LICENSE.md +9 -0
- package/README.md +31 -0
- package/dist/blob-api.d.ts +92 -0
- package/dist/blob-api.d.ts.map +1 -0
- package/dist/blob-store/index.d.ts +163 -0
- package/dist/blob-store/index.d.ts.map +1 -0
- package/dist/blob-store/live-download.d.ts +107 -0
- package/dist/blob-store/live-download.d.ts.map +1 -0
- package/dist/config-import.d.ts +74 -0
- package/dist/config-import.d.ts.map +1 -0
- package/dist/constants.d.ts +14 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/core-manager/bitfield-rle.d.ts +25 -0
- package/dist/core-manager/bitfield-rle.d.ts.map +1 -0
- package/dist/core-manager/core-index.d.ts +56 -0
- package/dist/core-manager/core-index.d.ts.map +1 -0
- package/dist/core-manager/index.d.ts +125 -0
- package/dist/core-manager/index.d.ts.map +1 -0
- package/dist/core-manager/random-access-file-pool.d.ts +17 -0
- package/dist/core-manager/random-access-file-pool.d.ts.map +1 -0
- package/dist/core-manager/remote-bitfield.d.ts +146 -0
- package/dist/core-manager/remote-bitfield.d.ts.map +1 -0
- package/dist/core-ownership.d.ts +112 -0
- package/dist/core-ownership.d.ts.map +1 -0
- package/dist/datastore/index.d.ts +91 -0
- package/dist/datastore/index.d.ts.map +1 -0
- package/dist/datatype/index.d.ts +108 -0
- package/dist/discovery/local-discovery.d.ts +64 -0
- package/dist/discovery/local-discovery.d.ts.map +1 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/fastify-controller.d.ts +27 -0
- package/dist/fastify-controller.d.ts.map +1 -0
- package/dist/fastify-plugins/blobs.d.ts +6 -0
- package/dist/fastify-plugins/blobs.d.ts.map +1 -0
- package/dist/fastify-plugins/constants.d.ts +3 -0
- package/dist/fastify-plugins/constants.d.ts.map +1 -0
- package/dist/fastify-plugins/icons.d.ts +6 -0
- package/dist/fastify-plugins/icons.d.ts.map +1 -0
- package/dist/fastify-plugins/maps/index.d.ts +11 -0
- package/dist/fastify-plugins/maps/index.d.ts.map +1 -0
- package/dist/fastify-plugins/maps/offline-fallback-map.d.ts +12 -0
- package/dist/fastify-plugins/maps/offline-fallback-map.d.ts.map +1 -0
- package/dist/fastify-plugins/maps/static-maps.d.ts +11 -0
- package/dist/fastify-plugins/maps/static-maps.d.ts.map +1 -0
- package/dist/fastify-plugins/utils.d.ts +23 -0
- package/dist/fastify-plugins/utils.d.ts.map +1 -0
- package/dist/generated/extensions.d.ts +44 -0
- package/dist/generated/extensions.d.ts.map +1 -0
- package/dist/generated/keys.d.ts +36 -0
- package/dist/generated/keys.d.ts.map +1 -0
- package/dist/generated/rpc.d.ts +87 -0
- package/dist/generated/rpc.d.ts.map +1 -0
- package/dist/icon-api.d.ts +109 -0
- package/dist/icon-api.d.ts.map +1 -0
- package/dist/index-writer/index.d.ts +51 -0
- package/dist/index-writer/index.d.ts.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/invite-api.d.ts +70 -0
- package/dist/invite-api.d.ts.map +1 -0
- package/dist/lib/hashmap.d.ts +62 -0
- package/dist/lib/hashmap.d.ts.map +1 -0
- package/dist/lib/hypercore-helpers.d.ts +6 -0
- package/dist/lib/hypercore-helpers.d.ts.map +1 -0
- package/dist/lib/noise-secret-stream-helpers.d.ts +45 -0
- package/dist/lib/noise-secret-stream-helpers.d.ts.map +1 -0
- package/dist/lib/ponyfills.d.ts +10 -0
- package/dist/lib/ponyfills.d.ts.map +1 -0
- package/dist/lib/string.d.ts +2 -0
- package/dist/lib/string.d.ts.map +1 -0
- package/dist/lib/timing-safe-equal.d.ts +15 -0
- package/dist/lib/timing-safe-equal.d.ts.map +1 -0
- package/dist/local-peers.d.ts +151 -0
- package/dist/local-peers.d.ts.map +1 -0
- package/dist/logger.d.ts +32 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/mapeo-manager.d.ts +178 -0
- package/dist/mapeo-manager.d.ts.map +1 -0
- package/dist/mapeo-project.d.ts +3233 -0
- package/dist/mapeo-project.d.ts.map +1 -0
- package/dist/member-api.d.ts +114 -0
- package/dist/member-api.d.ts.map +1 -0
- package/dist/roles.d.ts +157 -0
- package/dist/roles.d.ts.map +1 -0
- package/dist/schema/client.d.ts +284 -0
- package/dist/schema/client.d.ts.map +1 -0
- package/dist/schema/project.d.ts +1812 -0
- package/dist/schema/project.d.ts.map +1 -0
- package/dist/schema/schema-to-drizzle.d.ts +20 -0
- package/dist/schema/schema-to-drizzle.d.ts.map +1 -0
- package/dist/schema/types.d.ts +98 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/utils.d.ts +55 -0
- package/dist/schema/utils.d.ts.map +1 -0
- package/dist/sync/core-sync-state.d.ts +252 -0
- package/dist/sync/core-sync-state.d.ts.map +1 -0
- package/dist/sync/namespace-sync-state.d.ts +47 -0
- package/dist/sync/namespace-sync-state.d.ts.map +1 -0
- package/dist/sync/peer-sync-controller.d.ts +44 -0
- package/dist/sync/peer-sync-controller.d.ts.map +1 -0
- package/dist/sync/sync-api.d.ts +158 -0
- package/dist/sync/sync-api.d.ts.map +1 -0
- package/dist/sync/sync-state.d.ts +40 -0
- package/dist/sync/sync-state.d.ts.map +1 -0
- package/dist/translation-api.d.ts +288 -0
- package/dist/translation-api.d.ts.map +1 -0
- package/dist/types.d.ts +115 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils.d.ts +115 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils_types.d.ts +14 -0
- package/drizzle/client/0000_bumpy_carnage.sql +33 -0
- package/drizzle/client/meta/0000_snapshot.json +199 -0
- package/drizzle/client/meta/_journal.json +13 -0
- package/drizzle/project/0000_spooky_lady_ursula.sql +192 -0
- package/drizzle/project/meta/0000_snapshot.json +1137 -0
- package/drizzle/project/meta/_journal.json +13 -0
- package/package.json +202 -0
- package/src/blob-api.js +139 -0
- package/src/blob-store/index.js +325 -0
- package/src/blob-store/live-download.js +373 -0
- package/src/config-import.js +604 -0
- package/src/constants.js +34 -0
- package/src/core-manager/bitfield-rle.js +235 -0
- package/src/core-manager/core-index.js +87 -0
- package/src/core-manager/index.js +504 -0
- package/src/core-manager/random-access-file-pool.js +30 -0
- package/src/core-manager/remote-bitfield.js +416 -0
- package/src/core-ownership.js +235 -0
- package/src/datastore/README.md +46 -0
- package/src/datastore/index.js +234 -0
- package/src/datatype/README.md +33 -0
- package/src/datatype/index.d.ts +108 -0
- package/src/datatype/index.js +358 -0
- package/src/discovery/local-discovery.js +303 -0
- package/src/errors.js +5 -0
- package/src/fastify-controller.js +84 -0
- package/src/fastify-plugins/blobs.js +139 -0
- package/src/fastify-plugins/constants.js +5 -0
- package/src/fastify-plugins/icons.js +158 -0
- package/src/fastify-plugins/maps/index.js +173 -0
- package/src/fastify-plugins/maps/offline-fallback-map.js +114 -0
- package/src/fastify-plugins/maps/static-maps.js +271 -0
- package/src/fastify-plugins/utils.js +52 -0
- package/src/generated/README.md +3 -0
- package/src/generated/extensions.d.ts +44 -0
- package/src/generated/extensions.js +196 -0
- package/src/generated/extensions.ts +237 -0
- package/src/generated/keys.d.ts +36 -0
- package/src/generated/keys.js +148 -0
- package/src/generated/keys.ts +185 -0
- package/src/generated/rpc.d.ts +87 -0
- package/src/generated/rpc.js +389 -0
- package/src/generated/rpc.ts +463 -0
- package/src/icon-api.js +282 -0
- package/src/index-writer/README.md +38 -0
- package/src/index-writer/index.js +124 -0
- package/src/index.js +16 -0
- package/src/invite-api.js +450 -0
- package/src/lib/hashmap.js +91 -0
- package/src/lib/hypercore-helpers.js +18 -0
- package/src/lib/noise-secret-stream-helpers.js +37 -0
- package/src/lib/ponyfills.js +25 -0
- package/src/lib/string.js +7 -0
- package/src/lib/timing-safe-equal.js +34 -0
- package/src/local-peers.js +737 -0
- package/src/logger.js +99 -0
- package/src/mapeo-manager.js +914 -0
- package/src/mapeo-project.js +980 -0
- package/src/member-api.js +319 -0
- package/src/roles.js +412 -0
- package/src/schema/client.js +55 -0
- package/src/schema/project.js +44 -0
- package/src/schema/schema-to-drizzle.js +118 -0
- package/src/schema/types.ts +153 -0
- package/src/schema/utils.js +51 -0
- package/src/sync/core-sync-state.js +440 -0
- package/src/sync/namespace-sync-state.js +193 -0
- package/src/sync/peer-sync-controller.js +332 -0
- package/src/sync/sync-api.js +588 -0
- package/src/sync/sync-state.js +63 -0
- package/src/translation-api.js +141 -0
- package/src/types.ts +149 -0
- package/src/utils.js +210 -0
- package/src/utils_types.d.ts +14 -0
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
|
+
}
|
package/src/blob-api.js
ADDED
|
@@ -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
|
+
}
|