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