@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,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
+ }