@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,193 @@
1
+ import { CoreSyncState } from './core-sync-state.js'
2
+ import { discoveryKey } from 'hypercore-crypto'
3
+ /** @import { Namespace } from '../types.js' */
4
+
5
+ /**
6
+ * @typedef {Omit<import('./core-sync-state.js').DerivedState, 'coreLength'> & { dataToSync: boolean, coreCount: number }} SyncState
7
+ */
8
+
9
+ /**
10
+ * @template {Namespace} [TNamespace=Namespace]
11
+ */
12
+ export class NamespaceSyncState {
13
+ /** @type {Map<string, CoreSyncState>} */
14
+ #coreStates = new Map()
15
+ #coreCount = 0
16
+ #handleUpdate
17
+ #namespace
18
+ /** @type {SyncState | null} */
19
+ #cachedState = null
20
+ #peerSyncControllers
21
+
22
+ /**
23
+ * @param {object} opts
24
+ * @param {TNamespace} opts.namespace
25
+ * @param {import('../core-manager/index.js').CoreManager} opts.coreManager
26
+ * @param {() => void} opts.onUpdate Called when a state update is available (via getState())
27
+ * @param {Map<string, import('./peer-sync-controller.js').PeerSyncController>} opts.peerSyncControllers
28
+ */
29
+ constructor({ namespace, coreManager, onUpdate, peerSyncControllers }) {
30
+ this.#namespace = namespace
31
+ this.#peerSyncControllers = peerSyncControllers
32
+ // Called whenever the state changes, so we clear the cache because next
33
+ // call to getState() will need to re-derive the state
34
+ this.#handleUpdate = () => {
35
+ this.#cachedState = null
36
+ process.nextTick(onUpdate)
37
+ }
38
+
39
+ for (const { core, key } of coreManager.getCores(namespace)) {
40
+ this.#addCore(core, key)
41
+ }
42
+
43
+ coreManager.on('add-core', ({ core, namespace, key }) => {
44
+ if (namespace !== this.#namespace) return
45
+ this.#addCore(core, key)
46
+ })
47
+
48
+ coreManager.on('peer-have', (namespace, msg) => {
49
+ if (namespace !== this.#namespace) return
50
+ this.#insertPreHaves(msg)
51
+ })
52
+ }
53
+
54
+ get namespace() {
55
+ return this.#namespace
56
+ }
57
+
58
+ /** @returns {SyncState} */
59
+ getState() {
60
+ if (this.#cachedState) return this.#cachedState
61
+ /** @type {SyncState} */
62
+ const state = {
63
+ dataToSync: false,
64
+ coreCount: this.#coreCount,
65
+ localState: createState(),
66
+ remoteStates: {},
67
+ }
68
+ for (const css of this.#coreStates.values()) {
69
+ const coreState = css.getState()
70
+ mutatingAddPeerState(state.localState, coreState.localState)
71
+ for (const [peerId, peerNamespaceState] of Object.entries(
72
+ coreState.remoteStates
73
+ )) {
74
+ if (!(peerId in state.remoteStates)) {
75
+ state.remoteStates[peerId] = peerNamespaceState
76
+ } else {
77
+ mutatingAddPeerState(state.remoteStates[peerId], peerNamespaceState)
78
+ }
79
+ }
80
+ }
81
+ if (state.localState.want > 0 || state.localState.wanted > 0) {
82
+ state.dataToSync = true
83
+ }
84
+ this.#cachedState = state
85
+ return state
86
+ }
87
+
88
+ /**
89
+ * @param {string} peerId
90
+ */
91
+ addPeer(peerId) {
92
+ for (const css of this.#coreStates.values()) {
93
+ css.addPeer(peerId)
94
+ }
95
+ }
96
+
97
+ /**
98
+ * @param {import('hypercore')<"binary", Buffer>} core
99
+ * @param {Buffer} coreKey
100
+ */
101
+ #addCore(core, coreKey) {
102
+ const discoveryId = discoveryKey(coreKey).toString('hex')
103
+ this.#getCoreState(discoveryId).attachCore(core)
104
+ this.#coreCount++
105
+ }
106
+
107
+ /**
108
+ * @param {{
109
+ * peerId: string,
110
+ * start: number,
111
+ * coreDiscoveryId: string,
112
+ * bitfield: Uint32Array
113
+ * }} opts
114
+ */
115
+ #insertPreHaves({ peerId, start, coreDiscoveryId, bitfield }) {
116
+ this.#getCoreState(coreDiscoveryId).insertPreHaves(peerId, start, bitfield)
117
+ }
118
+
119
+ /**
120
+ * @param {string} discoveryId
121
+ */
122
+ #getCoreState(discoveryId) {
123
+ let coreState = this.#coreStates.get(discoveryId)
124
+ if (!coreState) {
125
+ coreState = new CoreSyncState({
126
+ onUpdate: this.#handleUpdate,
127
+ peerSyncControllers: this.#peerSyncControllers,
128
+ namespace: this.#namespace,
129
+ })
130
+ this.#coreStates.set(discoveryId, coreState)
131
+ }
132
+ return coreState
133
+ }
134
+ }
135
+
136
+ /**
137
+ * @overload
138
+ * @returns {SyncState['localState']}
139
+ */
140
+
141
+ /**
142
+ * @overload
143
+ * @param {import('./core-sync-state.js').PeerNamespaceState['status']} status
144
+ * @returns {import('./core-sync-state.js').PeerNamespaceState}
145
+ */
146
+
147
+ /**
148
+ * @param {import('./core-sync-state.js').PeerNamespaceState['status']} [status]
149
+ */
150
+ export function createState(status) {
151
+ if (status) {
152
+ return { want: 0, have: 0, wanted: 0, status }
153
+ } else {
154
+ return { want: 0, have: 0, wanted: 0 }
155
+ }
156
+ }
157
+
158
+ /**
159
+ * @overload
160
+ * @param {SyncState['localState']} accumulator
161
+ * @param {SyncState['localState']} currentValue
162
+ * @returns {SyncState['localState']}
163
+ */
164
+
165
+ /**
166
+ * @overload
167
+ * @param {import('./core-sync-state.js').PeerNamespaceState} accumulator
168
+ * @param {import('./core-sync-state.js').PeerNamespaceState} currentValue
169
+ * @returns {import('./core-sync-state.js').PeerNamespaceState}
170
+ */
171
+
172
+ /**
173
+ * Adds peer state in `currentValue` to peer state in `accumulator`
174
+ *
175
+ * @param {import('./core-sync-state.js').PeerNamespaceState} accumulator
176
+ * @param {import('./core-sync-state.js').PeerNamespaceState} currentValue
177
+ */
178
+ function mutatingAddPeerState(accumulator, currentValue) {
179
+ accumulator.have += currentValue.have
180
+ accumulator.want += currentValue.want
181
+ accumulator.wanted += currentValue.wanted
182
+ if ('status' in accumulator && accumulator.status !== currentValue.status) {
183
+ if (currentValue.status === 'stopped') {
184
+ accumulator.status === 'stopped'
185
+ } else if (
186
+ currentValue.status === 'starting' &&
187
+ accumulator.status === 'starting'
188
+ ) {
189
+ accumulator.status = 'starting'
190
+ }
191
+ }
192
+ return accumulator
193
+ }
@@ -0,0 +1,332 @@
1
+ import mapObject from 'map-obj'
2
+ import { NAMESPACES, PRESYNC_NAMESPACES } from '../constants.js'
3
+ import { Logger } from '../logger.js'
4
+ import { ExhaustivenessError, createMap } from '../utils.js'
5
+ import { unreplicate } from '../lib/hypercore-helpers.js'
6
+ /** @import { CoreRecord } from '../core-manager/index.js' */
7
+ /** @import { Role } from '../roles.js' */
8
+ /** @import { SyncEnabledState } from './sync-api.js' */
9
+ /** @import { Namespace } from '../types.js' */
10
+ /** @import { OpenedNoiseStream } from '../lib/noise-secret-stream-helpers.js' */
11
+
12
+ /**
13
+ * @typedef {Role['sync'][Namespace] | 'unknown'} SyncCapability
14
+ */
15
+
16
+ export class PeerSyncController {
17
+ #replicatingCores = new Set()
18
+ /** @type {Set<Namespace>} */
19
+ #enabledNamespaces = new Set()
20
+ #coreManager
21
+ #protomux
22
+ #roles
23
+ /** @type {Record<Namespace, SyncCapability>} */
24
+ #syncCapability = createNamespaceMap('unknown')
25
+ /** @type {SyncEnabledState} */
26
+ #syncEnabledState = 'none'
27
+ /** @type {Record<Namespace, import('./core-sync-state.js').LocalCoreState | null>} */
28
+ #prevLocalState = createNamespaceMap(null)
29
+ /** @type {SyncStatus} */
30
+ #syncStatus = createNamespaceMap('unknown')
31
+ /** @type {Map<import('hypercore')<'binary', any>, ReturnType<import('hypercore')['download']>>} */
32
+ #downloadingRanges = new Map()
33
+ /** @type {SyncStatus} */
34
+ #prevSyncStatus = createNamespaceMap('unknown')
35
+ #log
36
+
37
+ /**
38
+ * @param {object} opts
39
+ * @param {import('protomux')<OpenedNoiseStream>} opts.protomux
40
+ * @param {import("../core-manager/index.js").CoreManager} opts.coreManager
41
+ * @param {import("./sync-state.js").SyncState} opts.syncState
42
+ * @param {import('../roles.js').Roles} opts.roles
43
+ * @param {Logger} [opts.logger]
44
+ */
45
+ constructor({ protomux, coreManager, syncState, roles, logger }) {
46
+ /**
47
+ * @param {string} formatter
48
+ * @param {unknown[]} args
49
+ * @returns {void}
50
+ */
51
+ this.#log = (formatter, ...args) => {
52
+ const log = Logger.create('peer', logger).log
53
+ return log.apply(null, [
54
+ `[%h] ${formatter}`,
55
+ protomux.stream.remotePublicKey,
56
+ ...args,
57
+ ])
58
+ }
59
+ this.#coreManager = coreManager
60
+ this.#protomux = protomux
61
+ this.#roles = roles
62
+
63
+ // Always need to replicate the project creator core
64
+ this.#replicateCore(coreManager.creatorCore)
65
+
66
+ coreManager.on('add-core', this.#handleAddCore)
67
+ syncState.on('state', this.#handleStateChange)
68
+
69
+ this.#updateEnabledNamespaces()
70
+ }
71
+
72
+ get peerKey() {
73
+ return this.#protomux.stream.remotePublicKey
74
+ }
75
+
76
+ get peerId() {
77
+ return this.peerKey.toString('hex')
78
+ }
79
+
80
+ get syncCapability() {
81
+ return this.#syncCapability
82
+ }
83
+
84
+ /** @param {SyncEnabledState} syncEnabledState */
85
+ setSyncEnabledState(syncEnabledState) {
86
+ if (this.#syncEnabledState === syncEnabledState) {
87
+ return
88
+ }
89
+ this.#syncEnabledState = syncEnabledState
90
+ this.#updateEnabledNamespaces()
91
+ }
92
+
93
+ /**
94
+ * @param {Buffer} discoveryKey
95
+ */
96
+ handleDiscoveryKey(discoveryKey) {
97
+ const coreRecord = this.#coreManager.getCoreByDiscoveryKey(discoveryKey)
98
+ // If we already know about this core, then we will add it to the
99
+ // replication stream when we are ready
100
+ if (coreRecord) {
101
+ this.#log(
102
+ 'Received discovery key %h, but already have core in namespace %s',
103
+ discoveryKey,
104
+ coreRecord.namespace
105
+ )
106
+ if (this.#enabledNamespaces.has(coreRecord.namespace)) {
107
+ this.#replicateCore(coreRecord.core)
108
+ }
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Handler for 'core-add' event from CoreManager
114
+ * Bound to `this` (defined as static property)
115
+ *
116
+ * @param {CoreRecord} coreRecord
117
+ */
118
+ #handleAddCore = ({ core, namespace }) => {
119
+ if (!this.#enabledNamespaces.has(namespace)) return
120
+ this.#replicateCore(core)
121
+ }
122
+
123
+ /**
124
+ * Handler for 'state' event from SyncState
125
+ * Bound to `this` (defined as static property)
126
+ *
127
+ * @param {import("./sync-state.js").State} state
128
+ */
129
+ #handleStateChange = async (state) => {
130
+ // The remotePublicKey is only available after the noise stream has
131
+ // connected. We shouldn't get a state change before the noise stream has
132
+ // connected, but if we do we can ignore it because we won't have any useful
133
+ // information until it connects.
134
+ if (!this.peerId) return
135
+ this.#syncStatus = getSyncStatus(this.peerId, state)
136
+ const localState = mapObject(state, (ns, nsState) => {
137
+ return [ns, nsState.localState]
138
+ })
139
+ this.#log('state %X', state)
140
+
141
+ // Map of which namespaces have received new data since last sync change
142
+ const didUpdate = mapObject(state, (ns) => {
143
+ const nsDidSync =
144
+ this.#prevSyncStatus[ns] !== 'synced' &&
145
+ this.#syncStatus[ns] === 'synced'
146
+ const prevNsState = this.#prevLocalState[ns]
147
+ const nsDidUpdate =
148
+ nsDidSync &&
149
+ (prevNsState === null || prevNsState.have !== localState[ns].have)
150
+ if (nsDidUpdate) {
151
+ this.#prevLocalState[ns] = localState[ns]
152
+ }
153
+ return [ns, nsDidUpdate]
154
+ })
155
+ this.#prevSyncStatus = this.#syncStatus
156
+
157
+ if (didUpdate.auth) {
158
+ try {
159
+ const cap = await this.#roles.getRole(this.peerId)
160
+ this.#syncCapability = cap.sync
161
+ } catch (e) {
162
+ this.#log('Error reading role', e)
163
+ // Any error, consider sync unknown
164
+ this.#syncCapability = createNamespaceMap('unknown')
165
+ }
166
+ }
167
+ this.#log('capability %o', this.#syncCapability)
168
+
169
+ this.#updateEnabledNamespaces()
170
+ }
171
+
172
+ /**
173
+ * Enable and disable the appropriate namespaces.
174
+ *
175
+ * If replicating no namespace groups, all namespaces are disabled.
176
+ *
177
+ * If only replicating the initial namespace groups, only the initial
178
+ * namespaces are replicated, assuming the capability permits.
179
+ *
180
+ * If replicating all namespaces, everything is replicated. However, data
181
+ * namespaces are only enabled after the initial namespaces have synced. And
182
+ * again, capabilities are checked.
183
+ */
184
+ #updateEnabledNamespaces() {
185
+ /** @type {boolean} */ let isAnySyncEnabled
186
+ /** @type {boolean} */ let isDataSyncEnabled
187
+ switch (this.#syncEnabledState) {
188
+ case 'none':
189
+ isAnySyncEnabled = isDataSyncEnabled = false
190
+ break
191
+ case 'presync':
192
+ isAnySyncEnabled = true
193
+ isDataSyncEnabled = false
194
+ break
195
+ case 'all':
196
+ isAnySyncEnabled = isDataSyncEnabled = true
197
+ break
198
+ default:
199
+ throw new ExhaustivenessError(this.#syncEnabledState)
200
+ }
201
+
202
+ for (const ns of NAMESPACES) {
203
+ if (!isAnySyncEnabled) {
204
+ this.#disableNamespace(ns)
205
+ continue
206
+ }
207
+
208
+ const cap = this.#syncCapability[ns]
209
+ if (cap === 'blocked') {
210
+ this.#disableNamespace(ns)
211
+ } else if (cap === 'unknown') {
212
+ if (ns === 'auth') {
213
+ this.#enableNamespace(ns)
214
+ } else {
215
+ this.#disableNamespace(ns)
216
+ }
217
+ } else if (cap === 'allowed') {
218
+ if (PRESYNC_NAMESPACES.includes(ns)) {
219
+ this.#enableNamespace(ns)
220
+ } else if (isDataSyncEnabled) {
221
+ const arePresyncNamespacesSynced = PRESYNC_NAMESPACES.every(
222
+ (ns) => this.#syncStatus[ns] === 'synced'
223
+ )
224
+ // Only enable data namespaces once the pre-sync namespaces have synced
225
+ if (arePresyncNamespacesSynced) {
226
+ this.#enableNamespace(ns)
227
+ }
228
+ } else {
229
+ this.#disableNamespace(ns)
230
+ }
231
+ } else {
232
+ throw new ExhaustivenessError(cap)
233
+ }
234
+ }
235
+ }
236
+
237
+ /**
238
+ * @param {import('hypercore')<'binary', any>} core
239
+ */
240
+ #replicateCore(core) {
241
+ if (core.closed) return
242
+ if (this.#replicatingCores.has(core)) return
243
+ this.#log('replicating core %k', core.key)
244
+ core.replicate(this.#protomux)
245
+ core.on('peer-remove', (peer) => {
246
+ if (!peer.remotePublicKey.equals(this.peerKey)) return
247
+ this.#log('peer-remove %h from core %k', peer.remotePublicKey, core.key)
248
+ })
249
+ this.#replicatingCores.add(core)
250
+ }
251
+
252
+ /**
253
+ * @param {import('hypercore')<'binary', any>} core
254
+ * @returns {Promise<void>}
255
+ */
256
+ async #unreplicateCore(core) {
257
+ if (core === this.#coreManager.creatorCore) return
258
+
259
+ this.#replicatingCores.delete(core)
260
+
261
+ const isCoreReady = Boolean(core.discoveryKey)
262
+ if (!isCoreReady) {
263
+ await core.ready()
264
+ const wasReEnabledWhileWaiting = this.#replicatingCores.has(core)
265
+ if (wasReEnabledWhileWaiting) return
266
+ }
267
+
268
+ unreplicate(core, this.#protomux)
269
+ this.#log('unreplicated core %k', core.key)
270
+ }
271
+
272
+ /**
273
+ * @param {Namespace} namespace
274
+ */
275
+ #enableNamespace(namespace) {
276
+ if (this.#enabledNamespaces.has(namespace)) return
277
+ for (const { core } of this.#coreManager.getCores(namespace)) {
278
+ this.#replicateCore(core)
279
+ }
280
+ this.#enabledNamespaces.add(namespace)
281
+ this.#log('enabled namespace %s', namespace)
282
+ }
283
+
284
+ /**
285
+ * @param {Namespace} namespace
286
+ */
287
+ #disableNamespace(namespace) {
288
+ if (!this.#enabledNamespaces.has(namespace)) return
289
+ for (const { core } of this.#coreManager.getCores(namespace)) {
290
+ this.#unreplicateCore(core)
291
+ }
292
+ this.#enabledNamespaces.delete(namespace)
293
+ this.#log('disabled namespace %s', namespace)
294
+ }
295
+ }
296
+
297
+ /**
298
+ * @typedef {{ [namespace in Namespace]?: import("./core-sync-state.js").PeerNamespaceState }} PeerState
299
+ */
300
+
301
+ /** @typedef {Record<Namespace, 'unknown' | 'syncing' | 'synced'>} SyncStatus */
302
+
303
+ /**
304
+ * @param {string} peerId
305
+ * @param {import('./sync-state.js').State} state
306
+ * @returns {SyncStatus}
307
+ */
308
+ function getSyncStatus(peerId, state) {
309
+ const syncStatus = /** @type {SyncStatus} */ ({})
310
+ for (const namespace of NAMESPACES) {
311
+ const peerState = state[namespace].remoteStates[peerId]
312
+ if (!peerState) {
313
+ syncStatus[namespace] = 'unknown'
314
+ } else if (
315
+ peerState.status === 'started' &&
316
+ state[namespace].localState.want === 0
317
+ ) {
318
+ syncStatus[namespace] = 'synced'
319
+ } else {
320
+ syncStatus[namespace] = 'syncing'
321
+ }
322
+ }
323
+ return syncStatus
324
+ }
325
+
326
+ /**
327
+ * @template T
328
+ * @param {T} value
329
+ **/
330
+ function createNamespaceMap(value) {
331
+ return createMap(NAMESPACES, value)
332
+ }