@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
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
import { TypedEmitter } from 'tiny-typed-emitter'
|
|
2
|
+
import Corestore from 'corestore'
|
|
3
|
+
import { debounce } from 'throttle-debounce'
|
|
4
|
+
import assert from 'node:assert/strict'
|
|
5
|
+
import { sql, eq } from 'drizzle-orm'
|
|
6
|
+
|
|
7
|
+
import { HaveExtension, ProjectExtension } from '../generated/extensions.js'
|
|
8
|
+
import { Logger } from '../logger.js'
|
|
9
|
+
import { NAMESPACES } from '../constants.js'
|
|
10
|
+
import { noop } from '../utils.js'
|
|
11
|
+
import { coresTable } from '../schema/project.js'
|
|
12
|
+
import * as rle from './bitfield-rle.js'
|
|
13
|
+
import { CoreIndex } from './core-index.js'
|
|
14
|
+
|
|
15
|
+
/** @import Hypercore from 'hypercore' */
|
|
16
|
+
/** @import { HypercorePeer, Namespace } from '../types.js' */
|
|
17
|
+
|
|
18
|
+
const WRITER_CORE_PREHAVES_DEBOUNCE_DELAY = 1000
|
|
19
|
+
|
|
20
|
+
export const kCoreManagerReplicate = Symbol('replicate core manager')
|
|
21
|
+
|
|
22
|
+
/** @typedef {Hypercore<'binary', Buffer>} Core */
|
|
23
|
+
/** @typedef {{ core: Core, key: Buffer, namespace: Namespace }} CoreRecord */
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {Object} Events
|
|
26
|
+
* @property {(coreRecord: CoreRecord) => void} add-core
|
|
27
|
+
* @property {(namespace: Namespace, msg: { coreDiscoveryId: string, peerId: string, start: number, bitfield: Uint32Array }) => void} peer-have
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @extends {TypedEmitter<Events>}
|
|
32
|
+
*/
|
|
33
|
+
export class CoreManager extends TypedEmitter {
|
|
34
|
+
#corestore
|
|
35
|
+
#coreIndex
|
|
36
|
+
/** @type {Core} */
|
|
37
|
+
#creatorCore
|
|
38
|
+
#projectKey
|
|
39
|
+
#queries
|
|
40
|
+
#encryptionKeys
|
|
41
|
+
#projectExtension
|
|
42
|
+
/** @type {'opened' | 'closing' | 'closed'} */
|
|
43
|
+
#state = 'opened'
|
|
44
|
+
#ready
|
|
45
|
+
#haveExtension
|
|
46
|
+
#deviceId
|
|
47
|
+
#l
|
|
48
|
+
#autoDownload
|
|
49
|
+
|
|
50
|
+
static get namespaces() {
|
|
51
|
+
return NAMESPACES
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {Object} options
|
|
56
|
+
* @param {import('drizzle-orm/better-sqlite3').BetterSQLite3Database} options.db Drizzle better-sqlite3 database instance
|
|
57
|
+
* @param {import('@mapeo/crypto').KeyManager} options.keyManager mapeo/crypto KeyManager instance
|
|
58
|
+
* @param {Buffer} options.projectKey 32-byte public key of the project creator core
|
|
59
|
+
* @param {Buffer} [options.projectSecretKey] 32-byte secret key of the project creator core
|
|
60
|
+
* @param {Partial<Record<Namespace, Buffer>>} [options.encryptionKeys] Encryption keys for each namespace
|
|
61
|
+
* @param {import('hypercore').HypercoreStorage} options.storage Folder to store all hypercore data
|
|
62
|
+
* @param {boolean} [options.autoDownload=true] Immediately start downloading cores - should only be set to false for tests
|
|
63
|
+
* @param {Logger} [options.logger]
|
|
64
|
+
*/
|
|
65
|
+
constructor({
|
|
66
|
+
db,
|
|
67
|
+
keyManager,
|
|
68
|
+
projectKey,
|
|
69
|
+
projectSecretKey,
|
|
70
|
+
encryptionKeys = {},
|
|
71
|
+
storage,
|
|
72
|
+
autoDownload = true,
|
|
73
|
+
logger,
|
|
74
|
+
}) {
|
|
75
|
+
super()
|
|
76
|
+
assert(
|
|
77
|
+
projectKey.length === 32,
|
|
78
|
+
'project owner core public key must be 32-byte buffer'
|
|
79
|
+
)
|
|
80
|
+
assert(
|
|
81
|
+
!projectSecretKey || projectSecretKey.length === 64,
|
|
82
|
+
'project owner core secret key must be 64-byte buffer'
|
|
83
|
+
)
|
|
84
|
+
// Each peer will attach a listener, so max listeners is max attached peers
|
|
85
|
+
this.setMaxListeners(0)
|
|
86
|
+
this.#l = Logger.create('coreManager', logger)
|
|
87
|
+
const primaryKey = keyManager.getDerivedKey('primaryKey', projectKey)
|
|
88
|
+
this.#deviceId = keyManager.getIdentityKeypair().publicKey.toString('hex')
|
|
89
|
+
this.#projectKey = projectKey
|
|
90
|
+
this.#encryptionKeys = encryptionKeys
|
|
91
|
+
this.#autoDownload = autoDownload
|
|
92
|
+
|
|
93
|
+
// Pre-prepare SQL statement for better performance
|
|
94
|
+
this.#queries = {
|
|
95
|
+
addCore: db
|
|
96
|
+
.insert(coresTable)
|
|
97
|
+
.values({
|
|
98
|
+
publicKey: sql.placeholder('publicKey'),
|
|
99
|
+
namespace: sql.placeholder('namespace'),
|
|
100
|
+
})
|
|
101
|
+
.onConflictDoNothing()
|
|
102
|
+
.prepare(),
|
|
103
|
+
removeCores: db
|
|
104
|
+
.delete(coresTable)
|
|
105
|
+
.where(eq(coresTable.namespace, sql.placeholder('namespace')))
|
|
106
|
+
.prepare(),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Note: the primary key here should not be used, because we do not rely on
|
|
110
|
+
// corestore for key storage (i.e. we do not get cores from corestore via a
|
|
111
|
+
// name, which would derive the keypair from the primary key), but setting
|
|
112
|
+
// this just in case a dependency does (e.g. hyperdrive) and we miss it.
|
|
113
|
+
this.#corestore = new Corestore(storage, { primaryKey })
|
|
114
|
+
// Persistent index of core keys and namespaces in the project
|
|
115
|
+
this.#coreIndex = new CoreIndex()
|
|
116
|
+
|
|
117
|
+
// Writer cores and root core, keys and namespaces are not persisted because
|
|
118
|
+
// we derive the keys here.
|
|
119
|
+
for (const namespace of NAMESPACES) {
|
|
120
|
+
let keyPair
|
|
121
|
+
if (namespace === 'auth' && projectSecretKey) {
|
|
122
|
+
// For the project creator, the creatorCore is the same as the writer
|
|
123
|
+
// core for the 'auth' namespace
|
|
124
|
+
keyPair = { publicKey: projectKey, secretKey: projectSecretKey }
|
|
125
|
+
} else {
|
|
126
|
+
// Deterministic keypair, based on rootKey, namespace & projectKey
|
|
127
|
+
keyPair = keyManager.getHypercoreKeypair(namespace, projectKey)
|
|
128
|
+
}
|
|
129
|
+
const writer = this.#addCore(keyPair, namespace)
|
|
130
|
+
if (namespace === 'auth' && projectSecretKey) {
|
|
131
|
+
this.#creatorCore = writer.core
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// For anyone other than the project creator, creatorCore is readonly
|
|
136
|
+
this.#creatorCore ??= this.#addCore({ publicKey: projectKey }, 'auth').core
|
|
137
|
+
|
|
138
|
+
// Load persisted cores
|
|
139
|
+
const rows = db.select().from(coresTable).all()
|
|
140
|
+
for (const { publicKey, namespace } of rows) {
|
|
141
|
+
this.#addCore({ publicKey }, namespace)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.#projectExtension = this.#creatorCore.registerExtension(
|
|
145
|
+
'mapeo/project',
|
|
146
|
+
{
|
|
147
|
+
encoding: ProjectExtensionCodec,
|
|
148
|
+
onmessage: (msg) => {
|
|
149
|
+
this.#handleProjectMessage(msg)
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
this.#haveExtension = this.#creatorCore.registerExtension('mapeo/have', {
|
|
155
|
+
encoding: HaveExtensionCodec,
|
|
156
|
+
onmessage: (msg, peer) => {
|
|
157
|
+
this.#handleHaveMessage(msg, peer)
|
|
158
|
+
},
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
this.#creatorCore.on('peer-add', (peer) => {
|
|
162
|
+
this.#sendHaves(peer, this.#coreIndex).catch(() => {
|
|
163
|
+
this.#l.log('Failed to send pre-haves to newly-connected peer')
|
|
164
|
+
})
|
|
165
|
+
this.#sendAuthCoreKeys(peer)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
this.#ready = Promise.all(
|
|
169
|
+
[...this.#coreIndex].map(({ core }) => core.ready())
|
|
170
|
+
)
|
|
171
|
+
.then(() => {
|
|
172
|
+
this.#l.log('ready')
|
|
173
|
+
})
|
|
174
|
+
.catch(() => {})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
get deviceId() {
|
|
178
|
+
return this.#deviceId
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
get creatorCore() {
|
|
182
|
+
return this.#creatorCore
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Resolves when all cores have finished loading
|
|
187
|
+
*
|
|
188
|
+
* @returns {Promise<void>}
|
|
189
|
+
*/
|
|
190
|
+
async ready() {
|
|
191
|
+
await this.#ready
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get the writer core for the given namespace
|
|
196
|
+
*
|
|
197
|
+
* @param {Namespace} namespace
|
|
198
|
+
*/
|
|
199
|
+
getWriterCore(namespace) {
|
|
200
|
+
return this.#coreIndex.getWriter(namespace)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get an array of all cores in the given namespace
|
|
205
|
+
*
|
|
206
|
+
* @param {Namespace} namespace
|
|
207
|
+
*/
|
|
208
|
+
getCores(namespace) {
|
|
209
|
+
return this.#coreIndex.getByNamespace(namespace)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get a core by its public key
|
|
214
|
+
*
|
|
215
|
+
* @param {Buffer} key
|
|
216
|
+
* @returns {Core | undefined}
|
|
217
|
+
*/
|
|
218
|
+
getCoreByKey(key) {
|
|
219
|
+
const coreRecord = this.#coreIndex.getByCoreKey(key)
|
|
220
|
+
return coreRecord && coreRecord.core
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Get a core by its discovery key
|
|
225
|
+
*
|
|
226
|
+
* @param {Buffer} discoveryKey
|
|
227
|
+
* @returns {CoreRecord | undefined}
|
|
228
|
+
*/
|
|
229
|
+
getCoreByDiscoveryKey(discoveryKey) {
|
|
230
|
+
const coreRecord = this.#coreIndex.getByDiscoveryId(
|
|
231
|
+
discoveryKey.toString('hex')
|
|
232
|
+
)
|
|
233
|
+
return coreRecord
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Close all open cores and end any replication streams
|
|
238
|
+
* TODO: gracefully close replication streams
|
|
239
|
+
*/
|
|
240
|
+
async close() {
|
|
241
|
+
this.#state = 'closing'
|
|
242
|
+
const promises = []
|
|
243
|
+
for (const { core } of this.#coreIndex) {
|
|
244
|
+
promises.push(core.close())
|
|
245
|
+
}
|
|
246
|
+
await Promise.all(promises)
|
|
247
|
+
this.#state = 'closed'
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Add a core to the manager (will be persisted across restarts)
|
|
252
|
+
*
|
|
253
|
+
* @param {Buffer} key 32-byte public key of core to add
|
|
254
|
+
* @param {Namespace} namespace
|
|
255
|
+
* @returns {CoreRecord}
|
|
256
|
+
*/
|
|
257
|
+
addCore(key, namespace) {
|
|
258
|
+
this.#l.log('Adding remote core %k to %s', key, namespace)
|
|
259
|
+
return this.#addCore({ publicKey: key }, namespace, true)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Add a core to the manager (writer cores and project creator core not persisted)
|
|
264
|
+
*
|
|
265
|
+
* @param {{ publicKey: Buffer, secretKey?: Buffer }} keyPair
|
|
266
|
+
* @param {Namespace} namespace
|
|
267
|
+
* @param {boolean} [persist=false]
|
|
268
|
+
* @returns {CoreRecord}
|
|
269
|
+
*/
|
|
270
|
+
#addCore(keyPair, namespace, persist = false) {
|
|
271
|
+
// No-op if core is already managed
|
|
272
|
+
const existingCore = this.#coreIndex.getByCoreKey(keyPair.publicKey)
|
|
273
|
+
if (existingCore) return existingCore
|
|
274
|
+
|
|
275
|
+
const { publicKey: key, secretKey } = keyPair
|
|
276
|
+
const writer = !!secretKey
|
|
277
|
+
const core = this.#corestore.get({
|
|
278
|
+
keyPair,
|
|
279
|
+
encryptionKey: this.#encryptionKeys[namespace],
|
|
280
|
+
})
|
|
281
|
+
if (this.#autoDownload) {
|
|
282
|
+
core.download({ start: 0, end: -1 })
|
|
283
|
+
}
|
|
284
|
+
// Every peer adds a listener, so could have many peers
|
|
285
|
+
core.setMaxListeners(0)
|
|
286
|
+
// @ts-ignore - ensure key is defined before hypercore is ready
|
|
287
|
+
core.key = key
|
|
288
|
+
this.#coreIndex.add({ core, key, namespace, writer })
|
|
289
|
+
|
|
290
|
+
// **Hack** As soon as a peer is added, eagerly send a "want" for the entire
|
|
291
|
+
// core. This ensures that the peer sends back its entire bitfield.
|
|
292
|
+
// Otherwise this would only happen once we call core.download()
|
|
293
|
+
core.on('peer-add', (peer) => {
|
|
294
|
+
if (core.length === 0) return
|
|
295
|
+
// **Warning** uses internal method, but should be covered by tests
|
|
296
|
+
peer._maybeWant(0, core.length)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
if (writer) {
|
|
300
|
+
const sendHaves = debounce(WRITER_CORE_PREHAVES_DEBOUNCE_DELAY, () => {
|
|
301
|
+
for (const peer of this.#creatorCore.peers) {
|
|
302
|
+
this.#sendHaves(peer, [{ core, namespace }]).catch(() => {
|
|
303
|
+
this.#l.log('Failed to send new pre-haves to other peers')
|
|
304
|
+
})
|
|
305
|
+
}
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
// Tell connected peers, who we aren't necessarily syncing with, about
|
|
309
|
+
// what we just added or cleared. Hypercore doesn't emit anything when
|
|
310
|
+
// clearing, so we patch it in.
|
|
311
|
+
core.on('append', sendHaves)
|
|
312
|
+
const originalClear = core.clear
|
|
313
|
+
core.clear = function clear() {
|
|
314
|
+
const result = originalClear.apply(this, /** @type {any} */ (arguments))
|
|
315
|
+
result.then(sendHaves).catch(noop)
|
|
316
|
+
return result
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
// A non-writer core will emit 'append' when its length is updated from
|
|
320
|
+
// the initial sync with a peer, and we will not have sent a "maybe want"
|
|
321
|
+
// for this range, so we need to do it now. Subsequent appends are
|
|
322
|
+
// propagated (more efficiently) via range broadcasts, so we only need to
|
|
323
|
+
// listen to the first append.
|
|
324
|
+
core.once('append', () => {
|
|
325
|
+
for (const peer of core.peers) {
|
|
326
|
+
// TODO: It would be more efficient (in terms of network traffic) to
|
|
327
|
+
// send a want with start = length of previous want. Need to track
|
|
328
|
+
// "last want length" sent by peer.
|
|
329
|
+
peer._maybeWant(0, core.length)
|
|
330
|
+
}
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (persist) {
|
|
335
|
+
this.#queries.addCore.run({ publicKey: key, namespace })
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
this.#l.log(
|
|
339
|
+
'Added %s %s core %k',
|
|
340
|
+
persist ? 'remote' : writer ? 'local' : 'creator',
|
|
341
|
+
namespace,
|
|
342
|
+
key
|
|
343
|
+
)
|
|
344
|
+
this.emit('add-core', { core, key, namespace })
|
|
345
|
+
|
|
346
|
+
return { core, key, namespace }
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* We only add auth cores from the project extension messages. Cores in other
|
|
351
|
+
* namespaces are added by the sync API from the core ownership docs
|
|
352
|
+
*
|
|
353
|
+
* @param {ProjectExtension} msg
|
|
354
|
+
*/
|
|
355
|
+
#handleProjectMessage({ authCoreKeys }) {
|
|
356
|
+
for (const authCoreKey of authCoreKeys) {
|
|
357
|
+
// Use public method - these must be persisted (private method defaults to persisted=false)
|
|
358
|
+
this.addCore(authCoreKey, 'auth')
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Sends auth core keys to the given peer, skipping any keys that we know the
|
|
364
|
+
* peer has already (depends on the peer having already replicated the auth
|
|
365
|
+
* cores it has)
|
|
366
|
+
*
|
|
367
|
+
* @param {HypercorePeer} peer
|
|
368
|
+
*/
|
|
369
|
+
#sendAuthCoreKeys(peer) {
|
|
370
|
+
const message = ProjectExtension.create()
|
|
371
|
+
for (const { key } of this.getCores('auth')) {
|
|
372
|
+
message.authCoreKeys.push(key)
|
|
373
|
+
}
|
|
374
|
+
this.#projectExtension.send(message, peer)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* @param {Omit<HaveMsg, 'namespace'> & { namespace: Namespace | 'UNRECOGNIZED' }} msg
|
|
379
|
+
* @param {HypercorePeer} peer
|
|
380
|
+
*/
|
|
381
|
+
#handleHaveMessage(msg, peer) {
|
|
382
|
+
const { start, discoveryKey, bitfield, namespace } = msg
|
|
383
|
+
if (namespace === 'UNRECOGNIZED') return
|
|
384
|
+
/** @type {string} */
|
|
385
|
+
const peerId = peer.remotePublicKey.toString('hex')
|
|
386
|
+
const coreDiscoveryId = discoveryKey.toString('hex')
|
|
387
|
+
this.emit('peer-have', namespace, {
|
|
388
|
+
coreDiscoveryId,
|
|
389
|
+
peerId,
|
|
390
|
+
start,
|
|
391
|
+
bitfield,
|
|
392
|
+
})
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
*
|
|
397
|
+
* @param {HypercorePeer} peer
|
|
398
|
+
* @param {Iterable<{ core: Hypercore<Hypercore.ValueEncoding, Buffer>, namespace: Namespace }>} cores
|
|
399
|
+
*/
|
|
400
|
+
async #sendHaves(peer, cores) {
|
|
401
|
+
if (!peer) {
|
|
402
|
+
console.error('Called #sendHaves with no peer')
|
|
403
|
+
// TODO: How to handle this and when does it happen?
|
|
404
|
+
return
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
peer.protomux.cork()
|
|
408
|
+
|
|
409
|
+
for (const { core, namespace } of cores) {
|
|
410
|
+
// We want ready() rather than update() because we are only interested in local data
|
|
411
|
+
await core.ready()
|
|
412
|
+
const { discoveryKey } = core
|
|
413
|
+
// This will always be defined after ready(), but need to let TS know
|
|
414
|
+
if (!discoveryKey) continue
|
|
415
|
+
/** @type {Iterable<{ start: number, bitfield: Uint32Array }>} */
|
|
416
|
+
// @ts-ignore - accessing internal property
|
|
417
|
+
const bitfieldIterator = core.core.bitfield.want(0, core.length)
|
|
418
|
+
for (const { start, bitfield } of bitfieldIterator) {
|
|
419
|
+
const message = { start, bitfield, discoveryKey, namespace }
|
|
420
|
+
this.#haveExtension.send(message, peer)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
peer.protomux.uncork()
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* ONLY FOR TESTING
|
|
429
|
+
* Replicate all cores in core manager
|
|
430
|
+
*
|
|
431
|
+
* NB: Remote peers need to be manually added when unit testing core manager
|
|
432
|
+
* without the Sync API
|
|
433
|
+
*
|
|
434
|
+
* @param {Parameters<Corestore['replicate']>[0]} stream
|
|
435
|
+
*/
|
|
436
|
+
[kCoreManagerReplicate](stream) {
|
|
437
|
+
const protocolStream = this.#corestore.replicate(stream)
|
|
438
|
+
return protocolStream
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* @param {Exclude<Namespace, 'auth'>} namespace
|
|
443
|
+
* @returns {Promise<void>}
|
|
444
|
+
*/
|
|
445
|
+
async deleteOthersData(namespace) {
|
|
446
|
+
const coreRecords = this.getCores(namespace)
|
|
447
|
+
const ownWriterCore = this.getWriterCore(namespace)
|
|
448
|
+
|
|
449
|
+
const deletionPromises = []
|
|
450
|
+
|
|
451
|
+
for (const { core, key } of coreRecords) {
|
|
452
|
+
if (key.equals(ownWriterCore.key)) continue
|
|
453
|
+
deletionPromises.push(core.purge())
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
await Promise.all(deletionPromises)
|
|
457
|
+
|
|
458
|
+
this.#queries.removeCores.run({ namespace })
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* @typedef {object} HaveMsg
|
|
464
|
+
* @property {Buffer} discoveryKey
|
|
465
|
+
* @property {number} start
|
|
466
|
+
* @property {Uint32Array} bitfield
|
|
467
|
+
* @property {Namespace} namespace
|
|
468
|
+
*/
|
|
469
|
+
|
|
470
|
+
const ProjectExtensionCodec = {
|
|
471
|
+
/** @param {Parameters<typeof ProjectExtension.encode>[0]} msg */
|
|
472
|
+
encode(msg) {
|
|
473
|
+
return ProjectExtension.encode(msg).finish()
|
|
474
|
+
},
|
|
475
|
+
/** @param {Buffer | Uint8Array} buf */
|
|
476
|
+
decode(buf) {
|
|
477
|
+
return ProjectExtension.decode(buf)
|
|
478
|
+
},
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const HaveExtensionCodec = {
|
|
482
|
+
/** @param {HaveMsg} msg */
|
|
483
|
+
encode({ start, discoveryKey, bitfield, namespace }) {
|
|
484
|
+
const encodedBitfield = rle.encode(bitfield)
|
|
485
|
+
const msg = { start, discoveryKey, encodedBitfield, namespace }
|
|
486
|
+
return HaveExtension.encode(msg).finish()
|
|
487
|
+
},
|
|
488
|
+
/**
|
|
489
|
+
* @param {Buffer | Uint8Array} buf
|
|
490
|
+
* @returns {Omit<HaveMsg, 'namespace'> & { namespace: HaveMsg['namespace'] | 'UNRECOGNIZED' }}
|
|
491
|
+
*/
|
|
492
|
+
decode(buf) {
|
|
493
|
+
const { start, discoveryKey, encodedBitfield, namespace } =
|
|
494
|
+
HaveExtension.decode(buf)
|
|
495
|
+
try {
|
|
496
|
+
const bitfield = rle.decode(encodedBitfield)
|
|
497
|
+
return { start, discoveryKey, bitfield, namespace }
|
|
498
|
+
} catch (e) {
|
|
499
|
+
// TODO: Log error
|
|
500
|
+
console.error(e)
|
|
501
|
+
return { start, discoveryKey, bitfield: new Uint32Array(), namespace }
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File descriptor pool for random-access-storage to limit the number of file
|
|
3
|
+
* descriptors used. Important particularly for Android where the hard limit for
|
|
4
|
+
* the app is 1024.
|
|
5
|
+
*/
|
|
6
|
+
export class RandomAccessFilePool {
|
|
7
|
+
/** @param {number} maxSize max number of file descriptors to use */
|
|
8
|
+
constructor(maxSize) {
|
|
9
|
+
this.maxSize = maxSize
|
|
10
|
+
/** @type {Set<import('random-access-file')>} */
|
|
11
|
+
this.active = new Set()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** @param {import('random-access-file')} file */
|
|
15
|
+
_onactive(file) {
|
|
16
|
+
if (this.active.size >= this.maxSize) {
|
|
17
|
+
// suspend least recently inserted this manually iterates in insertion
|
|
18
|
+
// order, but only iterates to the first one (least recently inserted)
|
|
19
|
+
const toSuspend = this.active[Symbol.iterator]().next().value
|
|
20
|
+
toSuspend.suspend()
|
|
21
|
+
this.active.delete(toSuspend)
|
|
22
|
+
}
|
|
23
|
+
this.active.add(file)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** @param {import('random-access-file')} file */
|
|
27
|
+
_oninactive(file) {
|
|
28
|
+
this.active.delete(file)
|
|
29
|
+
}
|
|
30
|
+
}
|