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