@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,153 @@
1
+ import { Includes, ReadonlyDeep } from 'type-fest'
2
+ import {
3
+ JSONSchema7 as JSONSchema7Writable,
4
+ JSONSchema7Type,
5
+ } from 'json-schema'
6
+
7
+ /** Convert optional properties to nullable */
8
+ export type OptionalToNull<T extends {}> = {
9
+ [K in keyof T]-?: undefined extends T[K] ? T[K] | null : T[K]
10
+ }
11
+ /** Convert a readonly array/object to writeable */
12
+ type Writable<T> = { -readonly [P in keyof T]: T[P] }
13
+ /** Type returned by text(columnName, { enum: [] }) */
14
+ type TextBuilder<
15
+ TName extends string,
16
+ TEnum extends readonly [string, ...string[]],
17
+ TNotNull extends boolean,
18
+ THasDefault extends boolean
19
+ > = import('drizzle-orm/sqlite-core').SQLiteTextBuilder<{
20
+ name: TName
21
+ data: Writable<TEnum>[number]
22
+ driverParam: string
23
+ columnType: 'SQLiteText'
24
+ dataType: 'string'
25
+ enumValues: Writable<TEnum>
26
+ notNull: TNotNull
27
+ hasDefault: THasDefault
28
+ }>
29
+
30
+ /** Type returned by integer(columnName, { mode: 'boolean' }) */
31
+ type BooleanBuilder<
32
+ TName extends string,
33
+ TNotNull extends boolean,
34
+ THasDefault extends boolean
35
+ > = import('drizzle-orm/sqlite-core').SQLiteBooleanBuilder<{
36
+ name: TName
37
+ data: boolean
38
+ driverParam: number
39
+ columnType: 'SQLiteBoolean'
40
+ dataType: 'boolean'
41
+ notNull: TNotNull
42
+ hasDefault: THasDefault
43
+ enumValues: undefined
44
+ }>
45
+
46
+ /** Type returned by real(columnName) */
47
+ type RealBuilder<
48
+ TName extends string,
49
+ TNotNull extends boolean,
50
+ THasDefault extends boolean
51
+ > = import('drizzle-orm/sqlite-core').SQLiteRealBuilder<{
52
+ name: TName
53
+ data: number
54
+ driverParam: number
55
+ columnType: 'SQLiteReal'
56
+ dataType: 'number'
57
+ notNull: TNotNull
58
+ hasDefault: THasDefault
59
+ enumValues: undefined
60
+ }>
61
+
62
+ /** Type returned by integer(columnName) */
63
+ type IntegerBuilder<
64
+ TName extends string,
65
+ TNotNull extends boolean,
66
+ THasDefault extends boolean
67
+ > = import('drizzle-orm/sqlite-core').SQLiteIntegerBuilder<{
68
+ name: TName
69
+ data: number
70
+ driverParam: number
71
+ columnType: 'SQLiteInteger'
72
+ dataType: 'number'
73
+ notNull: TNotNull
74
+ hasDefault: THasDefault
75
+ enumValues: undefined
76
+ }>
77
+
78
+ /** Type returned by the `customJson` custom type */
79
+ type JsonBuilder<
80
+ TName extends string,
81
+ TData extends unknown,
82
+ TNotNull extends boolean,
83
+ THasDefault extends boolean
84
+ > = import('drizzle-orm/sqlite-core').SQLiteCustomColumnBuilder<{
85
+ name: TName
86
+ data: TData
87
+ dataType: 'custom'
88
+ driverParam: string
89
+ columnType: 'SQLiteCustomColumn'
90
+ notNull: TNotNull
91
+ hasDefault: THasDefault
92
+ enumValues: undefined
93
+ }>
94
+
95
+ export type JSONSchema7 = ReadonlyDeep<JSONSchema7Writable>
96
+ type JsonSchema7Properties = { readonly [K: string]: JSONSchema7 }
97
+ export type JSONSchema7WithProps = Omit<JSONSchema7, 'properties'> & {
98
+ readonly properties: JsonSchema7Properties
99
+ }
100
+
101
+ /** Get the type of a JSONSchema string: array of constants for an enum,
102
+ otherwise string[]. Strangeness is to convert it into the format expected by
103
+ drizzle, which results in the correct type for the field from SQLite */
104
+ type Enum<
105
+ T extends JSONSchema7,
106
+ TEnum extends T['enum'] = T['enum']
107
+ > = TEnum extends readonly [string, ...string[]]
108
+ ? Writable<TEnum>
109
+ : T['const'] extends string
110
+ ? [T['const']]
111
+ : [string, ...string[]]
112
+
113
+ /** True if JSONSchema object has a default */
114
+ type HasDefault<T extends JSONSchema7> = T['default'] extends JSONSchema7Type
115
+ ? true
116
+ : false
117
+
118
+ /** True if JSONSchema value is required */
119
+ type IsRequired<
120
+ T extends JSONSchema7WithProps,
121
+ U extends string,
122
+ V extends JSONSchema7['required'] = T['required']
123
+ > = V extends readonly any[] ? Includes<V, U> : false
124
+
125
+ /**
126
+ * Convert a JSONSchema to a Drizzle Columns map (e.g. parameter for
127
+ * `sqliteTable()`). All top-level properties map to SQLite columns, with
128
+ * `required` properties marked as `NOT NULL`, and JSONSchema `default` will map
129
+ * to SQLite defaults. Any properties that are of type `object` or `array` in
130
+ * the JSONSchema will be mapped to a text field, which drizzle will parse and
131
+ * stringify. Types for parsed JSON will be derived from MapeoDoc types.
132
+ */
133
+ export type SchemaToDrizzleColumns<
134
+ T extends JSONSchema7WithProps,
135
+ TObjectType extends { [K in keyof U]?: any },
136
+ U extends JsonSchema7Properties = T['properties']
137
+ > = {
138
+ [K in keyof U]: K extends string
139
+ ? U[K]['type'] extends 'string'
140
+ ? TextBuilder<K, Enum<U[K]>, IsRequired<T, K>, HasDefault<U[K]>>
141
+ : U[K]['type'] extends 'boolean'
142
+ ? BooleanBuilder<K, IsRequired<T, K>, HasDefault<U[K]>>
143
+ : U[K]['type'] extends 'number'
144
+ ? RealBuilder<K, IsRequired<T, K>, HasDefault<U[K]>>
145
+ : U[K]['type'] extends 'integer'
146
+ ? IntegerBuilder<K, IsRequired<T, K>, HasDefault<U[K]>>
147
+ : U[K]['type'] extends 'array' | 'object'
148
+ ? JsonBuilder<K, TObjectType[K], IsRequired<T, K>, HasDefault<U[K]>>
149
+ : never
150
+ : never
151
+ } & { forks: JsonBuilder<'forks', string[], true, false> }
152
+
153
+ export type NonEmptyArray<T> = [T, ...T[]]
@@ -0,0 +1,51 @@
1
+ import {
2
+ text,
3
+ getTableConfig,
4
+ sqliteTable,
5
+ customType,
6
+ } from 'drizzle-orm/sqlite-core'
7
+ /** @import { SQLiteTableWithColumns } from 'drizzle-orm/sqlite-core' */
8
+
9
+ /**
10
+ * @template {string} [TName=string]
11
+ * @typedef {SQLiteTableWithColumns<{
12
+ * name: TName;
13
+ * dialect: 'sqlite';
14
+ * schema: string | undefined;
15
+ * columns: any
16
+ * }>} SqliteTable
17
+ */
18
+
19
+ export const BACKLINK_TABLE_POSTFIX = '_backlink'
20
+
21
+ /**
22
+ * Table for storing backlinks, used for indexing. There needs to be one for
23
+ * each indexed document type
24
+ * @param {SqliteTable} tableSchema
25
+ */
26
+ export function backlinkTable(tableSchema) {
27
+ const { name } = getTableConfig(tableSchema)
28
+ return sqliteTable(getBacklinkTableName(name), {
29
+ versionId: text('versionId').notNull().primaryKey(),
30
+ })
31
+ }
32
+
33
+ /**
34
+ * @param {string} tableName
35
+ */
36
+ export function getBacklinkTableName(tableName) {
37
+ return tableName + BACKLINK_TABLE_POSTFIX
38
+ }
39
+
40
+ export const customJson = customType({
41
+ dataType() {
42
+ return 'text'
43
+ },
44
+ fromDriver(value) {
45
+ // @ts-ignore
46
+ return JSON.parse(value)
47
+ },
48
+ toDriver(value) {
49
+ return JSON.stringify(value)
50
+ },
51
+ })
@@ -0,0 +1,440 @@
1
+ import { keyToId } from '../utils.js'
2
+ import RemoteBitfield, {
3
+ BITS_PER_PAGE,
4
+ } from '../core-manager/remote-bitfield.js'
5
+ /** @import { HypercorePeer, HypercoreRemoteBitfield, Namespace } from '../types.js' */
6
+
7
+ /**
8
+ * @typedef {RemoteBitfield} Bitfield
9
+ */
10
+ /**
11
+ * @typedef {string} PeerId
12
+ */
13
+ /**
14
+ * @typedef {Object} InternalState
15
+ * @property {number | undefined} length Core length, e.g. how many blocks in the core (including blocks that are not downloaded)
16
+ * @property {PeerState} localState
17
+ * @property {Map<PeerId, PeerState>} remoteStates
18
+ * @property {Map<string, import('./peer-sync-controller.js').PeerSyncController>} peerSyncControllers
19
+ * @property {Namespace} namespace
20
+ */
21
+ /**
22
+ * @typedef {object} LocalCoreState
23
+ * @property {number} have blocks we have
24
+ * @property {number} want unique blocks we want from any other peer
25
+ * @property {number} wanted blocks we want from this peer
26
+ */
27
+ /**
28
+ * @typedef {object} PeerNamespaceState
29
+ * @property {number} have blocks the peer has locally
30
+ * @property {number} want blocks this peer wants from us
31
+ * @property {number} wanted blocks we want from this peer
32
+ * @property {'stopped' | 'starting' | 'started'} status
33
+ */
34
+ /**
35
+ * @typedef {object} DerivedState
36
+ * @property {number} coreLength known (sparse) length of the core
37
+ * @property {LocalCoreState} localState local state
38
+ * @property {{ [peerId in PeerId]: PeerNamespaceState }} remoteStates map of state of all known peers
39
+ */
40
+
41
+ /**
42
+ * Track sync state for a core identified by `discoveryId`. Can start tracking
43
+ * state before the core instance exists locally, via the "preHave" messages
44
+ * received over the project creator core.
45
+ *
46
+ * Because deriving the state is expensive (it iterates through the bitfields of
47
+ * all peers), this is designed to be pull-based: the onUpdate event signals
48
+ * that the state is updated, but does not pass the state. The consumer can
49
+ * "pull" the state when it wants it via `coreSyncState.getState()`.
50
+ *
51
+ * Each peer (including the local peer) has a state of:
52
+ *
53
+ * 1. `have` - number of blocks the peer has locally
54
+ *
55
+ * 2. `want` - number of blocks this peer wants. For local state, this is the
56
+ * number of unique blocks we want from anyone else. For remote peers, it is
57
+ * the number of blocks this peer wants from us.
58
+ *
59
+ * 3. `wanted` - number of blocks this peer has that's wanted by others. For
60
+ * local state, this is the number of unique blocks any of our peers want.
61
+ * For remote peers, it is the number of blocks we want from them.
62
+ */
63
+ export class CoreSyncState {
64
+ /** @type {import('hypercore')<'binary', Buffer> | undefined} */
65
+ #core
66
+ /** @type {InternalState['remoteStates']} */
67
+ #remoteStates = new Map()
68
+ /** @type {InternalState['localState']} */
69
+ #localState = new PeerState()
70
+ #preHavesLength = 0
71
+ #update
72
+ #peerSyncControllers
73
+ #namespace
74
+
75
+ /**
76
+ * @param {object} opts
77
+ * @param {() => void} opts.onUpdate Called when a state update is available (via getState())
78
+ * @param {Map<string, import('./peer-sync-controller.js').PeerSyncController>} opts.peerSyncControllers
79
+ * @param {Namespace} opts.namespace
80
+ */
81
+ constructor({ onUpdate, peerSyncControllers, namespace }) {
82
+ this.#peerSyncControllers = peerSyncControllers
83
+ this.#namespace = namespace
84
+ // Called whenever the state changes, so we clear the cache because next
85
+ // call to getState() will need to re-derive the state
86
+ this.#update = () => {
87
+ process.nextTick(onUpdate)
88
+ }
89
+ }
90
+
91
+ /** @type {() => DerivedState} */
92
+ getState() {
93
+ const localCoreLength = this.#core?.length || 0
94
+ return deriveState({
95
+ length: Math.max(localCoreLength, this.#preHavesLength),
96
+ localState: this.#localState,
97
+ remoteStates: this.#remoteStates,
98
+ peerSyncControllers: this.#peerSyncControllers,
99
+ namespace: this.#namespace,
100
+ })
101
+ }
102
+
103
+ /**
104
+ * Attach a core. The sync state can be initialized without a core instance,
105
+ * because we could receive peer want and have states via extension messages
106
+ * before we have the core key that allows us to create a core instance.
107
+ *
108
+ * @param {import('hypercore')<'binary', Buffer>} core
109
+ */
110
+ attachCore(core) {
111
+ if (this.#core) return
112
+
113
+ this.#core = core
114
+
115
+ this.#core.ready().then(() => {
116
+ this.#localState.setHavesBitfield(
117
+ // @ts-ignore - internal property
118
+ core?.core?.bitfield
119
+ )
120
+ })
121
+
122
+ for (const peer of this.#core.peers) {
123
+ this.#onPeerAdd(peer)
124
+ }
125
+
126
+ this.#core.on('peer-add', this.#onPeerAdd)
127
+
128
+ this.#core.on('peer-remove', this.#onPeerRemove)
129
+
130
+ // TODO: Maybe we need to also wait on core.update() and then emit state?
131
+
132
+ // These events happen when the local bitfield changes, so we want to emit
133
+ // state because it will have changed
134
+ this.#core.on('download', () => {
135
+ this.#update()
136
+ })
137
+
138
+ this.#core.on('append', () => {
139
+ this.#update()
140
+ })
141
+ }
142
+
143
+ /**
144
+ * Add a pre-emptive "have" bitfield for a peer. This is used when we receive
145
+ * a peer "have" via extension message - it allows us to have a state for the
146
+ * peer before the peer actually starts syncing this core
147
+ *
148
+ * @param {PeerId} peerId
149
+ * @param {number} start
150
+ * @param {Uint32Array} bitfield
151
+ */
152
+ insertPreHaves(peerId, start, bitfield) {
153
+ const peerState = this.#getPeerState(peerId)
154
+ peerState.insertPreHaves(start, bitfield)
155
+ this.#preHavesLength = Math.max(
156
+ this.#preHavesLength,
157
+ peerState.preHavesBitfield.lastSet(start + bitfield.length * 32) + 1
158
+ )
159
+ this.#update()
160
+ }
161
+
162
+ /**
163
+ * Add a ranges of wanted blocks for a peer. By default a peer wants all
164
+ * blocks in a core - calling this will change the peer to only want the
165
+ * blocks/ranges that are added here
166
+ *
167
+ * @param {PeerId} peerId
168
+ * @param {Array<{ start: number, length: number }>} ranges
169
+ */
170
+ setPeerWants(peerId, ranges) {
171
+ const peerState = this.#getPeerState(peerId)
172
+ for (const { start, length } of ranges) {
173
+ peerState.setWantRange({ start, length })
174
+ }
175
+ this.#update()
176
+ }
177
+
178
+ /**
179
+ * @param {PeerId} peerId
180
+ */
181
+ addPeer(peerId) {
182
+ if (this.#remoteStates.has(peerId)) return
183
+ this.#remoteStates.set(peerId, new PeerState())
184
+ this.#update()
185
+ }
186
+
187
+ /**
188
+ * @param {PeerId} peerId
189
+ */
190
+ #getPeerState(peerId) {
191
+ let peerState = this.#remoteStates.get(peerId)
192
+ if (!peerState) {
193
+ peerState = new PeerState()
194
+ this.#remoteStates.set(peerId, peerState)
195
+ }
196
+ return peerState
197
+ }
198
+
199
+ /**
200
+ * Handle a peer being added to the core - updates state and adds listeners to
201
+ * emit state updates whenever the peer remote bitfield changes
202
+ *
203
+ * (defined as class field to bind to `this`)
204
+ * @param {HypercorePeer} peer
205
+ */
206
+ #onPeerAdd = (peer) => {
207
+ const peerId = keyToId(peer.remotePublicKey)
208
+
209
+ // Update state to ensure this peer is in the state correctly
210
+ const peerState = this.#getPeerState(peerId)
211
+ peerState.status = 'starting'
212
+
213
+ this.#core?.update({ wait: true }).then(() => {
214
+ peerState.status = 'started'
215
+ this.#update()
216
+ })
217
+
218
+ // A peer can have a pre-emptive "have" bitfield received via an extension
219
+ // message, but when the peer actually connects then we switch to the actual
220
+ // bitfield from the peer object
221
+ peerState.setHavesBitfield(peer.remoteBitfield)
222
+ this.#update()
223
+
224
+ // We want to emit state when a peer's bitfield changes, which can happen as
225
+ // a result of these two internal calls.
226
+ const originalOnBitfield = peer.onbitfield
227
+ const originalOnRange = peer.onrange
228
+ peer.onbitfield = (...args) => {
229
+ originalOnBitfield.apply(peer, args)
230
+ this.#update()
231
+ }
232
+ peer.onrange = (...args) => {
233
+ originalOnRange.apply(peer, args)
234
+ this.#update()
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Handle a peer being removed - keeps it in state, but marks it stopped
240
+ *
241
+ * (defined as class field to bind to `this`)
242
+ * @param {HypercorePeer} peer
243
+ */
244
+ #onPeerRemove = (peer) => {
245
+ const peerId = keyToId(peer.remotePublicKey)
246
+ const peerState = this.#getPeerState(peerId)
247
+ peerState.status = 'stopped'
248
+ this.#update()
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Sync state for a core for a peer. Uses an internal bitfield from Hypercore to
254
+ * track which blocks the peer has. Default is that a peer wants all blocks, but
255
+ * can set ranges of "wants". Setting a want range changes all other blocks to
256
+ * "not wanted"
257
+ *
258
+ * @private
259
+ * Only exported for testing
260
+ */
261
+ export class PeerState {
262
+ /** @type {Bitfield} */
263
+ #preHaves = new RemoteBitfield()
264
+ /** @type {HypercoreRemoteBitfield | undefined} */
265
+ #haves
266
+ /** @type {Bitfield} */
267
+ #wants = new RemoteBitfield()
268
+ /** @type {PeerNamespaceState['status']} */
269
+ status = 'stopped'
270
+ #wantAll
271
+ constructor({ wantAll = true } = {}) {
272
+ this.#wantAll = wantAll
273
+ }
274
+ get preHavesBitfield() {
275
+ return this.#preHaves
276
+ }
277
+ /**
278
+ * @param {number} start
279
+ * @param {Uint32Array} bitfield
280
+ */
281
+ insertPreHaves(start, bitfield) {
282
+ return this.#preHaves.insert(start, bitfield)
283
+ }
284
+ /**
285
+ * @param {HypercoreRemoteBitfield} bitfield
286
+ */
287
+ setHavesBitfield(bitfield) {
288
+ this.#haves = bitfield
289
+ }
290
+ /**
291
+ * Set a range of blocks that a peer wants. This is not part of the Hypercore
292
+ * protocol, so we need our own extension messages that a peer can use to
293
+ * inform us which blocks they are interested in. For most cores peers always
294
+ * want all blocks, but for blob cores often peers only want preview or
295
+ * thumbnail versions of media
296
+ *
297
+ * @param {{ start: number, length: number }} range
298
+ */
299
+ setWantRange({ start, length }) {
300
+ this.#wantAll = false
301
+ this.#wants.setRange(start, length, true)
302
+ }
303
+ /**
304
+ * Returns whether the peer has the block at `index`. If a pre-have bitfield
305
+ * has been passed, this is used if no connected peer bitfield is available.
306
+ * If neither bitfield is available then this defaults to `false`
307
+ * @param {number} index
308
+ */
309
+ have(index) {
310
+ return this.#haves?.get(index) || this.#preHaves.get(index)
311
+ }
312
+ /**
313
+ * Return the "haves" for the 32 blocks from `index`, as a 32-bit integer
314
+ *
315
+ * @param {number} index
316
+ * @returns {number} 32-bit number representing whether the peer has or not
317
+ * the 32 blocks from `index`
318
+ */
319
+ haveWord(index) {
320
+ const preHaveWord = getBitfieldWord(this.#preHaves, index)
321
+ if (!this.#haves) return preHaveWord
322
+ return preHaveWord | getBitfieldWord(this.#haves, index)
323
+ }
324
+ /**
325
+ * Returns whether this peer wants block at `index`. Defaults to `true` for
326
+ * all blocks
327
+ * @param {number} index
328
+ */
329
+ want(index) {
330
+ if (this.#wantAll) return true
331
+ return this.#wants.get(index)
332
+ }
333
+ /**
334
+ * Return the "wants" for the 32 blocks from `index`, as a 32-bit integer
335
+ *
336
+ * @param {number} index
337
+ * @returns {number} 32-bit number representing whether the peer wants or not
338
+ * the 32 blocks from `index`
339
+ */
340
+ wantWord(index) {
341
+ if (this.#wantAll) {
342
+ // This is a 32-bit number with all bits set
343
+ return 2 ** 32 - 1
344
+ }
345
+ return getBitfieldWord(this.#wants, index)
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Derive count for each peer: "want"; "have"; "wanted".
351
+ *
352
+ * @param {InternalState} coreState
353
+ *
354
+ * @private
355
+ * Only exporteed for testing
356
+ */
357
+ export function deriveState(coreState) {
358
+ const length = coreState.length || 0
359
+ /** @type {LocalCoreState} */
360
+ const localState = { have: 0, want: 0, wanted: 0 }
361
+ /** @type {Record<PeerId, PeerNamespaceState>} */
362
+ const remoteStates = {}
363
+
364
+ /** @type {Map<PeerId, PeerState>} */
365
+ const peers = new Map()
366
+ for (const [peerId, peerState] of coreState.remoteStates.entries()) {
367
+ const psc = coreState.peerSyncControllers.get(peerId)
368
+ const isBlocked = psc?.syncCapability[coreState.namespace] === 'blocked'
369
+ // Currently we do not include blocked peers in sync state - it's unclear
370
+ // how to expose this state in a meaningful way for considering sync
371
+ // completion, because blocked peers do not sync.
372
+ if (isBlocked) continue
373
+ peers.set(peerId, peerState)
374
+ remoteStates[peerId] = {
375
+ have: 0,
376
+ want: 0,
377
+ wanted: 0,
378
+ status: peerState.status,
379
+ }
380
+ }
381
+
382
+ for (let i = 0; i < length; i += 32) {
383
+ const truncate = 2 ** Math.min(32, length - i) - 1
384
+
385
+ const localHaves = coreState.localState.haveWord(i) & truncate
386
+ localState.have += bitCount32(localHaves)
387
+
388
+ let someoneElseWantsFromMe = 0
389
+ let iWantFromSomeoneElse = 0
390
+
391
+ for (const [peerId, peer] of peers.entries()) {
392
+ const peerHaves = peer.haveWord(i) & truncate
393
+ remoteStates[peerId].have += bitCount32(peerHaves)
394
+
395
+ const theyWantFromMe = peer.wantWord(i) & ~peerHaves & localHaves
396
+ remoteStates[peerId].want += bitCount32(theyWantFromMe)
397
+ someoneElseWantsFromMe |= theyWantFromMe
398
+
399
+ const iWantFromThem = peerHaves & ~localHaves
400
+ remoteStates[peerId].wanted += bitCount32(iWantFromThem)
401
+ iWantFromSomeoneElse |= iWantFromThem
402
+ }
403
+
404
+ localState.wanted += bitCount32(someoneElseWantsFromMe)
405
+ localState.want += bitCount32(iWantFromSomeoneElse)
406
+ }
407
+
408
+ return {
409
+ coreLength: length,
410
+ localState,
411
+ remoteStates,
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Apologies for the obscure code. From
417
+ * https://stackoverflow.com/a/109025/903300
418
+ * @param {number} n
419
+ */
420
+ export function bitCount32(n) {
421
+ n = n - ((n >> 1) & 0x55555555)
422
+ n = (n & 0x33333333) + ((n >> 2) & 0x33333333)
423
+ return (((n + (n >> 4)) & 0xf0f0f0f) * 0x1010101) >> 24
424
+ }
425
+
426
+ /**
427
+ * Get a 32-bit "chunk" (word) of the bitfield.
428
+ *
429
+ * @param {Bitfield | HypercoreRemoteBitfield} bitfield
430
+ * @param {number} index
431
+ */
432
+ function getBitfieldWord(bitfield, index) {
433
+ if (index % 32 !== 0) throw new Error('Index must be multiple of 32')
434
+ const j = index & (BITS_PER_PAGE - 1)
435
+ const i = (index - j) / BITS_PER_PAGE
436
+
437
+ const p = bitfield._pages.get(i)
438
+
439
+ return p ? p.bitfield[j / 32] : 0
440
+ }