@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,416 @@
1
+ // Thanks to:
2
+ // https://github.com/holepunchto/hypercore/blob/c572826/lib/remote-bitfield.js
3
+ // With added JSDoc types
4
+
5
+ import BigSparseArray from 'big-sparse-array'
6
+ import quickbit from 'quickbit-universal'
7
+
8
+ export const BITS_PER_PAGE = 32768
9
+ const BYTES_PER_PAGE = BITS_PER_PAGE / 8
10
+ const WORDS_PER_PAGE = BYTES_PER_PAGE / 4
11
+ const BITS_PER_SEGMENT = 2097152
12
+ const BYTES_PER_SEGMENT = BITS_PER_SEGMENT / 8
13
+ const PAGES_PER_SEGMENT = BITS_PER_SEGMENT / BITS_PER_PAGE
14
+
15
+ class RemoteBitfieldPage {
16
+ /**
17
+ *
18
+ * @param {number} index
19
+ * @param {Uint32Array} bitfield
20
+ * @param {RemoteBitfieldSegment} segment
21
+ */
22
+ constructor(index, bitfield, segment) {
23
+ /** @type {typeof index} */
24
+ this.index = index
25
+ /** @type {number} */
26
+ this.offset = index * BYTES_PER_PAGE - segment.offset
27
+ /** @type {typeof bitfield} */
28
+ this.bitfield = bitfield
29
+ /** @type {typeof segment} */
30
+ this.segment = segment
31
+
32
+ segment.add(this)
33
+ }
34
+
35
+ get tree() {
36
+ return this.segment.tree
37
+ }
38
+
39
+ /**
40
+ *
41
+ * @param {number} index
42
+ * @returns {boolean}
43
+ */
44
+ get(index) {
45
+ return quickbit.get(this.bitfield, index)
46
+ }
47
+
48
+ /**
49
+ *
50
+ * @param {number} index
51
+ * @param {boolean} val
52
+ */
53
+ set(index, val) {
54
+ if (quickbit.set(this.bitfield, index, val)) {
55
+ this.tree.update(this.offset * 8 + index)
56
+ }
57
+ }
58
+
59
+ /**
60
+ *
61
+ * @param {number} start
62
+ * @param {number} length
63
+ * @param {boolean} val
64
+ */
65
+ setRange(start, length, val) {
66
+ quickbit.fill(this.bitfield, val, start, start + length)
67
+
68
+ let i = Math.floor(start / 128)
69
+ const n = i + Math.ceil(length / 128)
70
+
71
+ while (i <= n) this.tree.update(this.offset * 8 + i++ * 128)
72
+ }
73
+ /**
74
+ *
75
+ * @param {boolean} val
76
+ * @param {number} position
77
+ */
78
+ findFirst(val, position) {
79
+ return quickbit.findFirst(this.bitfield, val, position)
80
+ }
81
+ /**
82
+ * @param {boolean} val
83
+ * @param {number} position
84
+ */
85
+ findLast(val, position) {
86
+ return quickbit.findLast(this.bitfield, val, position)
87
+ }
88
+
89
+ /**
90
+ *
91
+ * @param {number} start
92
+ * @param {Uint32Array} bitfield
93
+ */
94
+ insert(start, bitfield) {
95
+ this.bitfield.set(bitfield, start / 32)
96
+ this.segment.refresh()
97
+ }
98
+ }
99
+
100
+ class RemoteBitfieldSegment {
101
+ /**
102
+ *
103
+ * @param {number} index
104
+ */
105
+ constructor(index) {
106
+ this.index = index
107
+ this.offset = index * BYTES_PER_SEGMENT
108
+ this.tree = /** @type {import('quickbit-universal').SparseIndex} */ (
109
+ quickbit.Index.from([], BYTES_PER_SEGMENT)
110
+ )
111
+ this.pages = new Array(PAGES_PER_SEGMENT)
112
+ this.pagesLength = 0
113
+ }
114
+
115
+ get chunks() {
116
+ return this.tree.chunks
117
+ }
118
+
119
+ refresh() {
120
+ this.tree = /** @type {import('quickbit-universal').SparseIndex} */ (
121
+ quickbit.Index.from(this.tree.chunks, BYTES_PER_SEGMENT)
122
+ )
123
+ }
124
+
125
+ /**
126
+ * @param {RemoteBitfieldPage} page
127
+ */
128
+ add(page) {
129
+ const pageIndex = page.index - this.index * PAGES_PER_SEGMENT
130
+ if (pageIndex >= this.pagesLength) this.pagesLength = pageIndex + 1
131
+
132
+ this.pages[pageIndex] = page
133
+
134
+ const chunk = { field: page.bitfield, offset: page.offset }
135
+
136
+ this.chunks.push(chunk)
137
+
138
+ for (let i = this.chunks.length - 2; i >= 0; i--) {
139
+ const prev = this.chunks[i]
140
+ if (prev.offset <= chunk.offset) break
141
+ this.chunks[i] = chunk
142
+ this.chunks[i + 1] = prev
143
+ }
144
+ }
145
+
146
+ /**
147
+ *
148
+ * @param {boolean} val
149
+ * @param {number} position
150
+ */
151
+ findFirst(val, position) {
152
+ position = this.tree.skipFirst(!val, position)
153
+
154
+ let j = position & (BITS_PER_PAGE - 1)
155
+ let i = (position - j) / BITS_PER_PAGE
156
+
157
+ if (i >= PAGES_PER_SEGMENT) return -1
158
+
159
+ while (i < this.pagesLength) {
160
+ const p = this.pages[i]
161
+
162
+ let index = -1
163
+
164
+ if (p) index = p.findFirst(val, j)
165
+ else if (!val) index = j
166
+
167
+ if (index !== -1) return i * BITS_PER_PAGE + index
168
+
169
+ j = 0
170
+ i++
171
+ }
172
+
173
+ return -1
174
+ }
175
+
176
+ /**
177
+ * @param {boolean} val
178
+ * @param {number} position
179
+ */
180
+ findLast(val, position) {
181
+ position = this.tree.skipLast(!val, position)
182
+
183
+ let j = position & (BITS_PER_PAGE - 1)
184
+ let i = (position - j) / BITS_PER_PAGE
185
+
186
+ if (i >= PAGES_PER_SEGMENT) return -1
187
+
188
+ while (i >= 0) {
189
+ const p = this.pages[i]
190
+
191
+ let index = -1
192
+
193
+ if (p) index = p.findLast(val, j)
194
+ else if (!val) index = j
195
+
196
+ if (index !== -1) return i * BITS_PER_PAGE + index
197
+
198
+ j = BITS_PER_PAGE - 1
199
+ i--
200
+ }
201
+
202
+ return -1
203
+ }
204
+ }
205
+
206
+ export default class RemoteBitfield {
207
+ constructor() {
208
+ /** @type {BigSparseArray<RemoteBitfieldPage>} */
209
+ this._pages = new BigSparseArray()
210
+ /** @type {BigSparseArray<RemoteBitfieldSegment>} */
211
+ this._segments = new BigSparseArray()
212
+ this._maxSegments = 0
213
+ }
214
+
215
+ /**
216
+ * @param {number} index
217
+ */
218
+ get(index) {
219
+ const j = index & (BITS_PER_PAGE - 1)
220
+ const i = (index - j) / BITS_PER_PAGE
221
+
222
+ const p = this._pages.get(i)
223
+
224
+ return p ? p.get(j) : false
225
+ }
226
+
227
+ /**
228
+ * @param {number} index
229
+ */
230
+ getBitfield(index) {
231
+ const j = index & (BITS_PER_PAGE - 1)
232
+ const i = (index - j) / BITS_PER_PAGE
233
+
234
+ const p = this._pages.get(i)
235
+ return p || null
236
+ }
237
+
238
+ /**
239
+ * @param {number} index
240
+ * @param {boolean} val
241
+ */
242
+ set(index, val) {
243
+ const j = index & (BITS_PER_PAGE - 1)
244
+ const i = (index - j) / BITS_PER_PAGE
245
+
246
+ let p = this._pages.get(i)
247
+
248
+ if (!p && val) {
249
+ const k = Math.floor(i / PAGES_PER_SEGMENT)
250
+ const s =
251
+ this._segments.get(k) ||
252
+ this._segments.set(k, new RemoteBitfieldSegment(k))
253
+ if (this._maxSegments <= k) this._maxSegments = k + 1
254
+
255
+ p = this._pages.set(
256
+ i,
257
+ new RemoteBitfieldPage(i, new Uint32Array(WORDS_PER_PAGE), s)
258
+ )
259
+ }
260
+
261
+ if (p) p.set(j, val)
262
+ }
263
+
264
+ /**
265
+ * @param {number} start
266
+ * @param {number} length
267
+ * @param {boolean} val
268
+ */
269
+ setRange(start, length, val) {
270
+ let j = start & (BITS_PER_PAGE - 1)
271
+ let i = (start - j) / BITS_PER_PAGE
272
+
273
+ while (length > 0) {
274
+ let p = this._pages.get(i)
275
+
276
+ if (!p && val) {
277
+ const k = Math.floor(i / PAGES_PER_SEGMENT)
278
+ const s =
279
+ this._segments.get(k) ||
280
+ this._segments.set(k, new RemoteBitfieldSegment(k))
281
+ if (this._maxSegments <= k) this._maxSegments = k + 1
282
+
283
+ p = this._pages.set(
284
+ i,
285
+ new RemoteBitfieldPage(i, new Uint32Array(WORDS_PER_PAGE), s)
286
+ )
287
+ }
288
+
289
+ const end = Math.min(j + length, BITS_PER_PAGE)
290
+ const range = end - j
291
+
292
+ if (p) p.setRange(j, range, val)
293
+
294
+ j = 0
295
+ i++
296
+ length -= range
297
+ }
298
+ }
299
+
300
+ /**
301
+ * @param {boolean} val
302
+ * @param {number} position
303
+ */
304
+ findFirst(val, position) {
305
+ let j = position & (BITS_PER_SEGMENT - 1)
306
+ let i = (position - j) / BITS_PER_SEGMENT
307
+
308
+ while (i < this._maxSegments) {
309
+ const s = this._segments.get(i)
310
+
311
+ let index = -1
312
+
313
+ if (s) index = s.findFirst(val, j)
314
+ else if (!val) index = j
315
+
316
+ if (index !== -1) return i * BITS_PER_SEGMENT + index
317
+
318
+ j = 0
319
+ i++
320
+ }
321
+
322
+ return val ? -1 : position
323
+ }
324
+ /**
325
+ * @param {number} position
326
+ */
327
+ firstSet(position) {
328
+ return this.findFirst(true, position)
329
+ }
330
+ /**
331
+ * @param {number} position
332
+ */
333
+ firstUnset(position) {
334
+ return this.findFirst(false, position)
335
+ }
336
+ /**
337
+ * @param {boolean} val
338
+ * @param {number} position
339
+ */
340
+ findLast(val, position) {
341
+ let j = position & (BITS_PER_SEGMENT - 1)
342
+ let i = (position - j) / BITS_PER_SEGMENT
343
+
344
+ while (i >= 0) {
345
+ const s = this._segments.get(i)
346
+
347
+ let index = -1
348
+
349
+ if (s) index = s.findLast(val, j)
350
+ else if (!val) index = j
351
+
352
+ if (index !== -1) return i * BITS_PER_SEGMENT + index
353
+
354
+ j = BITS_PER_SEGMENT - 1
355
+ i--
356
+ }
357
+
358
+ return -1
359
+ }
360
+
361
+ /**
362
+ * @param {number} position
363
+ */
364
+ lastSet(position) {
365
+ return this.findLast(true, position)
366
+ }
367
+ /**
368
+ * @param {number} position
369
+ */
370
+ lastUnset(position) {
371
+ return this.findLast(false, position)
372
+ }
373
+ /**
374
+ * @param {number} start
375
+ * @param {Uint32Array} bitfield
376
+ * @returns {boolean}
377
+ */
378
+ insert(start, bitfield) {
379
+ if (start % 32 !== 0) return false
380
+
381
+ let length = bitfield.byteLength * 8
382
+
383
+ let j = start & (BITS_PER_PAGE - 1)
384
+ let i = (start - j) / BITS_PER_PAGE
385
+
386
+ while (length > 0) {
387
+ let p = this._pages.get(i)
388
+
389
+ if (!p) {
390
+ const k = Math.floor(i / PAGES_PER_SEGMENT)
391
+ const s =
392
+ this._segments.get(k) ||
393
+ this._segments.set(k, new RemoteBitfieldSegment(k))
394
+ if (this._maxSegments <= k) this._maxSegments = k + 1
395
+
396
+ p = this._pages.set(
397
+ i,
398
+ new RemoteBitfieldPage(i, new Uint32Array(WORDS_PER_PAGE), s)
399
+ )
400
+ }
401
+
402
+ const end = Math.min(j + length, BITS_PER_PAGE)
403
+ const range = end - j
404
+
405
+ p.insert(j, bitfield.subarray(0, range / 32))
406
+
407
+ bitfield = bitfield.subarray(range / 32)
408
+
409
+ j = 0
410
+ i++
411
+ length -= range
412
+ }
413
+
414
+ return true
415
+ }
416
+ }
@@ -0,0 +1,235 @@
1
+ import { verifySignature, sign } from '@mapeo/crypto'
2
+ import { parseVersionId } from '@comapeo/schema'
3
+ import { defaultGetWinner } from '@mapeo/sqlite-indexer'
4
+ import assert from 'node:assert/strict'
5
+ import sodium from 'sodium-universal'
6
+ import {
7
+ kTable,
8
+ kSelect,
9
+ kCreateWithDocId,
10
+ kDataStore,
11
+ } from './datatype/index.js'
12
+ import { eq, or } from 'drizzle-orm'
13
+ import mapObject from 'map-obj'
14
+ import { discoveryKey } from 'hypercore-crypto'
15
+ import pDefer from 'p-defer'
16
+ import { NAMESPACES } from './constants.js'
17
+ import { TypedEmitter } from 'tiny-typed-emitter'
18
+ /**
19
+ * @import {
20
+ * CoreOwnershipWithSignatures,
21
+ * CoreOwnershipWithSignaturesValue,
22
+ * KeyPair,
23
+ * Namespace
24
+ * } from './types.js'
25
+ */
26
+
27
+ /**
28
+ * @typedef {object} CoreOwnershipEvents
29
+ * @property {(docIds: Set<string>) => void} update Emitted when new coreOwnership records are indexed
30
+ */
31
+
32
+ /**
33
+ * @extends {TypedEmitter<CoreOwnershipEvents>}
34
+ */
35
+ export class CoreOwnership extends TypedEmitter {
36
+ #dataType
37
+ #ownershipWriteDone
38
+ /**
39
+ *
40
+ * @param {object} opts
41
+ * @param {import('./datatype/index.js').DataType<
42
+ * import('./datastore/index.js').DataStore<'auth'>,
43
+ * typeof import('./schema/project.js').coreOwnershipTable,
44
+ * 'coreOwnership',
45
+ * import('@comapeo/schema').CoreOwnership,
46
+ * import('@comapeo/schema').CoreOwnershipValue
47
+ * >} opts.dataType
48
+ * @param {Record<Namespace, KeyPair>} opts.coreKeypairs
49
+ * @param {KeyPair} opts.identityKeypair
50
+ */
51
+ constructor({ dataType, coreKeypairs, identityKeypair }) {
52
+ super()
53
+ this.#dataType = dataType
54
+ const authWriterCore = dataType[kDataStore].writerCore
55
+ const deferred = pDefer()
56
+ this.#ownershipWriteDone = deferred.promise
57
+
58
+ const writeOwnership = () => {
59
+ if (authWriterCore.length > 0) {
60
+ deferred.resolve()
61
+ return
62
+ }
63
+ this.#writeOwnership(identityKeypair, coreKeypairs)
64
+ .then(deferred.resolve)
65
+ .catch(deferred.reject)
66
+ }
67
+ // @ts-ignore - opened missing from types
68
+ if (authWriterCore.opened) {
69
+ writeOwnership()
70
+ } else {
71
+ authWriterCore.once('ready', writeOwnership)
72
+ }
73
+
74
+ dataType[kDataStore].on('coreOwnership', this.emit.bind(this, 'update'))
75
+ }
76
+
77
+ /**
78
+ * @param {string} coreId
79
+ * @returns {Promise<string>} deviceId of device that owns the core
80
+ */
81
+ async getOwner(coreId) {
82
+ await this.#ownershipWriteDone
83
+ const table = this.#dataType[kTable]
84
+ const expressions = []
85
+ for (const namespace of NAMESPACES) {
86
+ expressions.push(eq(table[`${namespace}CoreId`], coreId))
87
+ }
88
+ // prettier-ignore
89
+ const result = (await this.#dataType[kSelect]())
90
+ .where(or.apply(null, expressions))
91
+ .get()
92
+ if (!result) {
93
+ throw new Error('NotFound')
94
+ }
95
+ return result.docId
96
+ }
97
+
98
+ /**
99
+ *
100
+ * @param {string} deviceId
101
+ * @param {Namespace} namespace
102
+ * @returns {Promise<string>} coreId of core belonging to `deviceId` for `namespace`
103
+ */
104
+ async getCoreId(deviceId, namespace) {
105
+ const result = await this.get(deviceId)
106
+ return result[`${namespace}CoreId`]
107
+ }
108
+
109
+ /**
110
+ * Get capabilities for a given deviceId
111
+ *
112
+ * @param {string} deviceId
113
+ */
114
+ async get(deviceId) {
115
+ await this.#ownershipWriteDone
116
+ return this.#dataType.getByDocId(deviceId)
117
+ }
118
+
119
+ async getAll() {
120
+ await this.#ownershipWriteDone
121
+ return this.#dataType.getMany()
122
+ }
123
+
124
+ /**
125
+ *
126
+ * @param {KeyPair} identityKeypair
127
+ * @param {Record<Namespace, KeyPair>} coreKeypairs
128
+ */
129
+ async #writeOwnership(identityKeypair, coreKeypairs) {
130
+ /** @type {CoreOwnershipWithSignaturesValue} */
131
+ const docValue = {
132
+ schemaName: 'coreOwnership',
133
+ ...mapObject(coreKeypairs, (key, value) => {
134
+ return [`${key}CoreId`, value.publicKey.toString('hex')]
135
+ }),
136
+ identitySignature: sign(
137
+ identityKeypair.publicKey,
138
+ identityKeypair.secretKey
139
+ ),
140
+ coreSignatures: mapObject(coreKeypairs, (key, value) => {
141
+ return [key, sign(value.publicKey, value.secretKey)]
142
+ }),
143
+ }
144
+ const docId = identityKeypair.publicKey.toString('hex')
145
+ await this.#dataType[kCreateWithDocId](docId, docValue)
146
+ }
147
+ }
148
+
149
+ /**
150
+ * - Validate that the doc is written to the core identified by doc.authCoreId
151
+ * - Verify the signatures
152
+ * - Remove the signatures (we don't add them to the indexer)
153
+ * - Set doc.links to an empty array - this forces the indexer to treat every
154
+ * document as a fork, so getWinner is called for every doc, which resolves to
155
+ * the doc with the lowest index (e.g. the first)
156
+ *
157
+ * @param {CoreOwnershipWithSignatures} doc
158
+ * @param {import('@comapeo/schema').VersionIdObject} version
159
+ * @returns {import('@comapeo/schema').CoreOwnership}
160
+ */
161
+ export function mapAndValidateCoreOwnership(doc, { coreDiscoveryKey }) {
162
+ if (
163
+ !coreDiscoveryKey.equals(discoveryKey(Buffer.from(doc.authCoreId, 'hex')))
164
+ ) {
165
+ throw new Error('Invalid coreOwnership record: mismatched authCoreId')
166
+ }
167
+ if (!verifyCoreOwnership(doc)) {
168
+ throw new Error('Invalid coreOwnership record: signatures are invalid')
169
+ }
170
+ // eslint-disable-next-line no-unused-vars
171
+ const { identitySignature, coreSignatures, ...docWithoutSignatures } = doc
172
+ docWithoutSignatures.links = []
173
+ return docWithoutSignatures
174
+ }
175
+
176
+ /**
177
+ * Verify the signatures of a coreOwnership record, which verify that the device
178
+ * with the identityKey matching the docIds does own (e.g. can write to) cores
179
+ * with the given core IDs
180
+ *
181
+ * @param {CoreOwnershipWithSignatures} doc
182
+ * @returns {boolean}
183
+ */
184
+ function verifyCoreOwnership(doc) {
185
+ const { coreSignatures, identitySignature } = doc
186
+ for (const namespace of NAMESPACES) {
187
+ const signature = coreSignatures[namespace]
188
+ const coreKey = Buffer.from(doc[`${namespace}CoreId`], 'hex')
189
+ assert.equal(
190
+ signature.length,
191
+ sodium.crypto_sign_BYTES,
192
+ 'Invalid core ownership signature'
193
+ )
194
+ assert.equal(
195
+ coreKey.length,
196
+ sodium.crypto_sign_PUBLICKEYBYTES,
197
+ 'Invalid core ownership coreId'
198
+ )
199
+ const isValidSignature = verifySignature(coreKey, signature, coreKey)
200
+ if (!isValidSignature) return false
201
+ }
202
+ const identityPublicKey = Buffer.from(doc.docId, 'hex')
203
+ assert.equal(identitySignature.length, sodium.crypto_sign_BYTES)
204
+ assert.equal(identityPublicKey.length, sodium.crypto_sign_PUBLICKEYBYTES)
205
+ const isValidIdentitySignature = verifySignature(
206
+ identityPublicKey,
207
+ identitySignature,
208
+ identityPublicKey
209
+ )
210
+ if (!isValidIdentitySignature) return false
211
+ return true
212
+ }
213
+
214
+ /**
215
+ * For coreOwnership records, we only trust the first record written to the core.
216
+ *
217
+ * @type {NonNullable<ConstructorParameters<typeof import('./index-writer/index.js').IndexWriter>[0]['getWinner']>}
218
+ */
219
+ export function getWinner(docA, docB) {
220
+ if (
221
+ 'schemaName' in docA &&
222
+ docA.schemaName === 'coreOwnership' &&
223
+ 'schemaName' in docB &&
224
+ docB.schemaName === 'coreOwnership'
225
+ ) {
226
+ // Assumes docA and docB have same coreKey, so we choose the first one
227
+ // written to the core
228
+ const docAindex = parseVersionId(docA.versionId).index
229
+ const docBindex = parseVersionId(docB.versionId).index
230
+ if (docAindex < docBindex) return docA
231
+ return docB
232
+ } else {
233
+ return defaultGetWinner(docA, docB)
234
+ }
235
+ }
@@ -0,0 +1,46 @@
1
+ # DataStore
2
+
3
+ > Manage reading cores for indexing, and reading and writing documents to cores.
4
+
5
+ ## Purpose
6
+
7
+ The `DataStore` class is an API over a CoreManager namespace, responsible for reading blocks for indexing from all cores in a namespace; writing new documents to the namespace writer core, and reading existing documents from any core in the namespace based on the `versionId`. `DataStore` does not write documents to an index, it only reads them for indexing - it will call the `batch()` constructor option with entries that are read from cores in the namespace that the datastore manages. Writes will only resolve once `batch()` resolves (e.g. once a document has been written to the SQLite index tables).
8
+
9
+ ## Usage
10
+
11
+ The `DataStore` class is used internally by the [`DataType`](../datatype/) class.
12
+
13
+ An example of `DataStore` usage taken from the [datastore tests](../../tests/datastore.js):
14
+
15
+ ```js
16
+ const datastore = new DataStore({
17
+ coreManager,
18
+ batch: async (entries) => {
19
+ // Process entries here using an indexer...
20
+ },
21
+ namespace: 'data',
22
+ })
23
+
24
+ /** @type {MapeoDoc} */
25
+ const newObservation = await datastore.write(observationValue)
26
+ /** @type {MapeoDoc} */
27
+ const existingObservation = await datastore.read(versionId)
28
+
29
+ datastore.on('index-state', ({ current, remaining, entriesPerSecond }) => {
30
+ if (current === 'idle') {
31
+ // indexing done for now
32
+ } else if (current === 'indexing') {
33
+ // show state to user that indexing is happening
34
+ }
35
+ })
36
+
37
+ const { current, remaining, entriesPerSecond } = datastore.getIndexState()
38
+ ```
39
+
40
+ ## API docs
41
+
42
+ TODO!
43
+
44
+ ## Tests
45
+
46
+ Tests for this module are in [tests/datastore.js](../../tests/datastore.js)