@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
package/src/icon-api.js
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/** @import { PresetValue, IconValue } from '@comapeo/schema' */
|
|
2
|
+
|
|
3
|
+
export const kGetIconBlob = Symbol('getIcon')
|
|
4
|
+
|
|
5
|
+
/** @typedef {PresetValue['iconRef']} IconRef */
|
|
6
|
+
/** @typedef {IconValue['variants']} IconVariants */
|
|
7
|
+
/** @typedef {IconVariants[number]} IconVariant */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Exclude<IconVariant['size'], 'size_unspecified'>} ValidSizes
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {Object} BitmapOpts
|
|
15
|
+
* @property {Extract<IconVariant['mimeType'], 'image/png'>} mimeType
|
|
16
|
+
* @property {Extract<IconVariant, {mimeType: 'image/png'}>['pixelDensity']} pixelDensity
|
|
17
|
+
* @property {ValidSizes} size
|
|
18
|
+
*
|
|
19
|
+
* @typedef {Object} SvgOpts
|
|
20
|
+
* @property {Extract<IconVariant['mimeType'], 'image/svg+xml'>} mimeType
|
|
21
|
+
* @property {ValidSizes} size
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** @type {{ [mime in IconVariant['mimeType']]: string }} */
|
|
25
|
+
const MIME_TO_EXTENSION = {
|
|
26
|
+
'image/png': '.png',
|
|
27
|
+
'image/svg+xml': '.svg',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class IconApi {
|
|
31
|
+
#dataType
|
|
32
|
+
#dataStore
|
|
33
|
+
#getMediaBaseUrl
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {Object} opts
|
|
37
|
+
* @param {import('./datatype/index.js').DataType<
|
|
38
|
+
* import('./datastore/index.js').DataStore<'config'>,
|
|
39
|
+
* typeof import('./schema/project.js').iconTable,
|
|
40
|
+
* 'icon',
|
|
41
|
+
* import('@comapeo/schema').Icon,
|
|
42
|
+
* import('@comapeo/schema').IconValue
|
|
43
|
+
* >} opts.iconDataType
|
|
44
|
+
* @param {import('./datastore/index.js').DataStore<'config'>} opts.iconDataStore
|
|
45
|
+
* @param {() => Promise<string>} opts.getMediaBaseUrl
|
|
46
|
+
*/
|
|
47
|
+
constructor({ iconDataType, iconDataStore, getMediaBaseUrl }) {
|
|
48
|
+
this.#dataType = iconDataType
|
|
49
|
+
this.#dataStore = iconDataStore
|
|
50
|
+
this.#getMediaBaseUrl = getMediaBaseUrl
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {object} icon
|
|
55
|
+
* @param {import('@comapeo/schema').IconValue['name']} icon.name
|
|
56
|
+
* @param {Array<(BitmapOpts | SvgOpts) & { blob: Buffer }>} icon.variants
|
|
57
|
+
*
|
|
58
|
+
* @returns {Promise<import('@comapeo/schema').Icon>}
|
|
59
|
+
*/
|
|
60
|
+
async create(icon) {
|
|
61
|
+
if (icon.variants.length < 1) {
|
|
62
|
+
throw new Error('empty variants array')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const savedVariants = await Promise.all(
|
|
66
|
+
icon.variants.map(async ({ blob, ...variant }) => {
|
|
67
|
+
const blobVersionId = await this.#dataStore.writeRaw(blob)
|
|
68
|
+
return { ...variant, blobVersionId }
|
|
69
|
+
})
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return await this.#dataType.create({
|
|
73
|
+
schemaName: 'icon',
|
|
74
|
+
name: icon.name,
|
|
75
|
+
variants: savedVariants,
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {string} iconId
|
|
81
|
+
* @param {BitmapOpts | SvgOpts} opts
|
|
82
|
+
*
|
|
83
|
+
* @returns {Promise<Buffer>}
|
|
84
|
+
*/
|
|
85
|
+
async [kGetIconBlob](iconId, opts) {
|
|
86
|
+
const iconRecord = await this.#dataType.getByDocId(iconId)
|
|
87
|
+
const iconVariant = getBestVariant(iconRecord.variants, opts)
|
|
88
|
+
const blob = await this.#dataStore.readRaw(iconVariant.blobVersionId)
|
|
89
|
+
return blob
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {string} iconId
|
|
94
|
+
* @param {BitmapOpts | SvgOpts} opts
|
|
95
|
+
*
|
|
96
|
+
* @returns {Promise<string>}
|
|
97
|
+
*/
|
|
98
|
+
async getIconUrl(iconId, opts) {
|
|
99
|
+
let base = await this.#getMediaBaseUrl()
|
|
100
|
+
|
|
101
|
+
if (!base.endsWith('/')) {
|
|
102
|
+
base += '/'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const mimeExtension = MIME_TO_EXTENSION[opts.mimeType]
|
|
106
|
+
|
|
107
|
+
const pixelDensity =
|
|
108
|
+
opts.mimeType === 'image/svg+xml' ||
|
|
109
|
+
// if the pixel density is 1, we can omit the density suffix in the resulting url
|
|
110
|
+
// and assume the pixel density is 1 for applicable mime types when using the url
|
|
111
|
+
opts.pixelDensity === 1
|
|
112
|
+
? undefined
|
|
113
|
+
: opts.pixelDensity
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
base +
|
|
117
|
+
constructIconPath({
|
|
118
|
+
pixelDensity,
|
|
119
|
+
size: opts.size,
|
|
120
|
+
extension: mimeExtension,
|
|
121
|
+
iconId,
|
|
122
|
+
})
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @type {Record<IconVariant['size'], number>}
|
|
129
|
+
*/
|
|
130
|
+
const SIZE_AS_NUMERIC = {
|
|
131
|
+
// NOTE: when size is unspecified, we fallback to medium
|
|
132
|
+
size_unspecified: 2,
|
|
133
|
+
small: 1,
|
|
134
|
+
medium: 2,
|
|
135
|
+
large: 3,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Given a list of icon variants returns the variant that most closely matches the desired parameters.
|
|
140
|
+
* Rules, in order of precedence:
|
|
141
|
+
*
|
|
142
|
+
* 1. Matching mime type (throw if no matches)
|
|
143
|
+
* 2. Matching size. If no exact match:
|
|
144
|
+
* 1. If smaller ones exist, prefer closest smaller size.
|
|
145
|
+
* 2. Otherwise prefer closest larger size.
|
|
146
|
+
* 3. Matching pixel density (when asking for PNGs). If no exact match:
|
|
147
|
+
* 1. If smaller ones exist, prefer closest smaller density.
|
|
148
|
+
* 2. Otherwise prefer closest larger density.
|
|
149
|
+
*
|
|
150
|
+
* @param {IconVariants} variants
|
|
151
|
+
* @param {BitmapOpts | SvgOpts} opts
|
|
152
|
+
*/
|
|
153
|
+
export function getBestVariant(variants, opts) {
|
|
154
|
+
const { size: wantedSize, mimeType: wantedMimeType } = opts
|
|
155
|
+
/** @type {BitmapOpts['pixelDensity']} */
|
|
156
|
+
let wantedPixelDensity
|
|
157
|
+
if (opts.mimeType === 'image/png') {
|
|
158
|
+
wantedPixelDensity = opts.pixelDensity
|
|
159
|
+
}
|
|
160
|
+
if (variants.length === 0) {
|
|
161
|
+
throw new Error('No variants exist')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const matchingMime = variants.filter((v) => v.mimeType === wantedMimeType)
|
|
165
|
+
|
|
166
|
+
if (matchingMime.length === 0) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`No variants with desired mime type ${wantedMimeType} exist`
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
const wantedSizeNum = SIZE_AS_NUMERIC[wantedSize]
|
|
172
|
+
|
|
173
|
+
// Sort the relevant variants based on the desired size and pixel density, using the rules of the preference.
|
|
174
|
+
// Sorted from closest match to furthest match.
|
|
175
|
+
matchingMime.sort((a, b) => {
|
|
176
|
+
const aSizeNum = SIZE_AS_NUMERIC[a.size]
|
|
177
|
+
const bSizeNum = SIZE_AS_NUMERIC[b.size]
|
|
178
|
+
|
|
179
|
+
const aSizeDiff = aSizeNum - wantedSizeNum
|
|
180
|
+
const bSizeDiff = bSizeNum - wantedSizeNum
|
|
181
|
+
|
|
182
|
+
// Both variants match desired size, use pixel density (when png) to determine preferred match
|
|
183
|
+
if (aSizeDiff === 0 && bSizeDiff === 0) {
|
|
184
|
+
// What to do if asking for an svg and both (a and b) are svgs and have the same size, what criteria do we use?
|
|
185
|
+
// For now, we don't change sort order
|
|
186
|
+
if (wantedMimeType === 'image/svg+xml') {
|
|
187
|
+
return 0
|
|
188
|
+
} else if (
|
|
189
|
+
wantedMimeType === 'image/png' &&
|
|
190
|
+
a.mimeType === 'image/png' &&
|
|
191
|
+
b.mimeType === 'image/png'
|
|
192
|
+
) {
|
|
193
|
+
return determineSortValue(
|
|
194
|
+
wantedPixelDensity,
|
|
195
|
+
a.pixelDensity,
|
|
196
|
+
b.pixelDensity
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return determineSortValue(wantedSizeNum, aSizeNum, bSizeNum)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// Closest match will be first element
|
|
205
|
+
return matchingMime[0]
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Determines a sort value based on the order of precedence outlined below. Winning value moves closer to front.
|
|
210
|
+
*
|
|
211
|
+
* 1. Exactly match `target`
|
|
212
|
+
* 2. Closest value smaller than `target`
|
|
213
|
+
* 3. Closest value larger than `target`
|
|
214
|
+
*
|
|
215
|
+
* @param {number} target
|
|
216
|
+
* @param {number} a
|
|
217
|
+
* @param {number} b
|
|
218
|
+
*
|
|
219
|
+
* @returns {-1 | 0 | 1}
|
|
220
|
+
*/
|
|
221
|
+
function determineSortValue(target, a, b) {
|
|
222
|
+
const aDiff = a - target
|
|
223
|
+
const bDiff = b - target
|
|
224
|
+
|
|
225
|
+
// Both match exactly, don't change sort order
|
|
226
|
+
if (aDiff === 0 && bDiff === 0) {
|
|
227
|
+
return 0
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// a matches but b doesn't, prefer a
|
|
231
|
+
if (aDiff === 0 && bDiff !== 0) {
|
|
232
|
+
return -1
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// b matches but a doesn't, prefer b
|
|
236
|
+
if (bDiff === 0 && aDiff !== 0) {
|
|
237
|
+
return 1
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Both are larger than desired, prefer smaller of the two
|
|
241
|
+
if (aDiff > 0 && bDiff > 0) {
|
|
242
|
+
return a < b ? -1 : 1
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Both are smaller than desired, prefer larger of the two
|
|
246
|
+
if (aDiff < 0 && bDiff < 0) {
|
|
247
|
+
return a < b ? 1 : -1
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Mix of smaller and larger than desired, prefer smaller of the two
|
|
251
|
+
return a < b ? -1 : 1
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* General purpose path builder for an icon
|
|
256
|
+
*
|
|
257
|
+
* @param {object} opts
|
|
258
|
+
* @param {string} opts.iconId
|
|
259
|
+
* @param {string} opts.size
|
|
260
|
+
* @param {number} [opts.pixelDensity]
|
|
261
|
+
* @param {string} opts.extension
|
|
262
|
+
*
|
|
263
|
+
* @returns {string}
|
|
264
|
+
*/
|
|
265
|
+
export function constructIconPath({ size, pixelDensity, iconId, extension }) {
|
|
266
|
+
if (iconId.length === 0 || size.length === 0 || extension.length === 0) {
|
|
267
|
+
throw new Error('iconId, size, and extension cannot be empty strings')
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let result = `${iconId}/${size}`
|
|
271
|
+
|
|
272
|
+
if (typeof pixelDensity === 'number') {
|
|
273
|
+
if (pixelDensity < 1) {
|
|
274
|
+
throw new Error('pixelDensity must be a positive number')
|
|
275
|
+
}
|
|
276
|
+
result += `@${pixelDensity}x`
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
result += extension.startsWith('.') ? extension : '.' + extension
|
|
280
|
+
|
|
281
|
+
return result
|
|
282
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# IndexWriter
|
|
2
|
+
|
|
3
|
+
> Index documents by `docId` in a SQLite database
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
The `IndexWriter` class resolves the DAG (Directed Acyclic Graph) for each `docId` to create a materialized view in SQLite of the current "head" for each `docId`. A single `IndexWriter` is responsible for decoding hypercore entries and writing them to the appropriate table (one table for each Mapeo data type). Unknown entries / data types are ignored.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
`IndexWriter` is used by `DataStore` instances for writing entries that are read from the cores in the namespace managed by each `DataStore`. It exports a single method `batch(entries)` where an entry is:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
type Entry = {
|
|
15
|
+
block: Buffer // raw data entry read from a hypercore
|
|
16
|
+
key: Buffer // public key of the hypercore where the block was read from
|
|
17
|
+
index: number // index of the block in the hypercore
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
A usage example:
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
const indexWriter = new IndexWriter({
|
|
25
|
+
tables, // Array of Drizzle table schema definitions to index
|
|
26
|
+
sqlite, // better-sqlite3 Database instance
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
indexWriter.batch(entries)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## API docs
|
|
33
|
+
|
|
34
|
+
TODO!
|
|
35
|
+
|
|
36
|
+
## Tests
|
|
37
|
+
|
|
38
|
+
There are no unit tests for the IndexWriter, tests coverage will be from end-to-end / integration tests.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { decode } from '@comapeo/schema'
|
|
2
|
+
import SqliteIndexer from '@mapeo/sqlite-indexer'
|
|
3
|
+
import { getTableConfig } from 'drizzle-orm/sqlite-core'
|
|
4
|
+
import { getBacklinkTableName } from '../schema/utils.js'
|
|
5
|
+
import { discoveryKey } from 'hypercore-crypto'
|
|
6
|
+
import { Logger } from '../logger.js'
|
|
7
|
+
/** @import { MapeoDoc, VersionIdObject } from '@comapeo/schema' */
|
|
8
|
+
/** @import { MapeoDocTables } from '../datatype/index.js' */
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {{ [K in MapeoDoc['schemaName']]?: string[] }} IndexedDocIds
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {ReturnType<typeof decode>} MapeoDocInternal
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @template {MapeoDocTables} [TTables=MapeoDocTables]
|
|
19
|
+
*/
|
|
20
|
+
export class IndexWriter {
|
|
21
|
+
/**
|
|
22
|
+
* @internal
|
|
23
|
+
* @typedef {TTables['_']['name']} SchemaName
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** @type {Map<SchemaName, SqliteIndexer>} */
|
|
27
|
+
#indexers = new Map()
|
|
28
|
+
#mapDoc
|
|
29
|
+
#l
|
|
30
|
+
/**
|
|
31
|
+
*
|
|
32
|
+
* @param {object} opts
|
|
33
|
+
* @param {import('better-sqlite3').Database} opts.sqlite
|
|
34
|
+
* @param {TTables[]} opts.tables
|
|
35
|
+
* @param {(doc: MapeoDocInternal, version: VersionIdObject) => MapeoDoc} [opts.mapDoc] optionally transform a document prior to indexing. Can also validate, if an error is thrown then the document will not be indexed
|
|
36
|
+
* @param {typeof import('@mapeo/sqlite-indexer').defaultGetWinner} [opts.getWinner] custom function to determine the "winner" of two forked documents. Defaults to choosing the document with the most recent `updatedAt`
|
|
37
|
+
* @param {Logger} [opts.logger]
|
|
38
|
+
*/
|
|
39
|
+
constructor({ tables, sqlite, mapDoc = (d) => d, getWinner, logger }) {
|
|
40
|
+
this.#l = Logger.create('indexWriter', logger)
|
|
41
|
+
this.#mapDoc = mapDoc
|
|
42
|
+
for (const table of tables) {
|
|
43
|
+
const config = getTableConfig(table)
|
|
44
|
+
const schemaName = /** @type {(typeof table)['_']['name']} */ (
|
|
45
|
+
config.name
|
|
46
|
+
)
|
|
47
|
+
const indexer = new SqliteIndexer(sqlite, {
|
|
48
|
+
docTableName: config.name,
|
|
49
|
+
backlinkTableName: getBacklinkTableName(config.name),
|
|
50
|
+
getWinner,
|
|
51
|
+
})
|
|
52
|
+
this.#indexers.set(schemaName, indexer)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @returns {Iterable<SchemaName>}
|
|
58
|
+
*/
|
|
59
|
+
get schemas() {
|
|
60
|
+
return this.#indexers.keys()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {import('multi-core-indexer').Entry[]} entries
|
|
65
|
+
* @returns {Promise<IndexedDocIds>} map of indexed docIds by schemaName
|
|
66
|
+
*/
|
|
67
|
+
async batch(entries) {
|
|
68
|
+
// sqlite-indexer is _significantly_ faster when batching even <10 at a
|
|
69
|
+
// time, so best to queue docs here before calling sliteIndexer.batch()
|
|
70
|
+
/** @type {Record<string, MapeoDoc[]>} */
|
|
71
|
+
const queued = {}
|
|
72
|
+
/** @type {IndexedDocIds} */
|
|
73
|
+
const indexed = {}
|
|
74
|
+
for (const { block, key, index } of entries) {
|
|
75
|
+
/** @type {MapeoDoc} */ let doc
|
|
76
|
+
try {
|
|
77
|
+
const version = { coreDiscoveryKey: discoveryKey(key), index }
|
|
78
|
+
doc = this.#mapDoc(decode(block, version), version)
|
|
79
|
+
} catch (e) {
|
|
80
|
+
this.#l.log('Could not decode entry %d of %h', index, key)
|
|
81
|
+
// Unknown or invalid entry - silently ignore
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
// Don't have an indexer for this type - silently ignore
|
|
85
|
+
if (!this.#indexers.has(doc.schemaName)) continue
|
|
86
|
+
if (queued[doc.schemaName]) {
|
|
87
|
+
queued[doc.schemaName].push(doc)
|
|
88
|
+
// @ts-expect-error - we know this is defined, TS doesn't
|
|
89
|
+
indexed[doc.schemaName].push(doc.docId)
|
|
90
|
+
} else {
|
|
91
|
+
queued[doc.schemaName] = [doc]
|
|
92
|
+
indexed[doc.schemaName] = [doc.docId]
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (const [schemaName, docs] of Object.entries(queued)) {
|
|
96
|
+
// @ts-expect-error
|
|
97
|
+
const indexer = this.#indexers.get(schemaName)
|
|
98
|
+
if (!indexer) continue // Won't happen, but TS doesn't know that
|
|
99
|
+
indexer.batch(docs)
|
|
100
|
+
if (this.#l.enabled) {
|
|
101
|
+
for (const doc of docs) {
|
|
102
|
+
this.#l.log(
|
|
103
|
+
'Indexed %s %S @ %S',
|
|
104
|
+
doc.schemaName,
|
|
105
|
+
doc.docId,
|
|
106
|
+
doc.versionId
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return indexed
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {SchemaName} schemaName
|
|
116
|
+
*/
|
|
117
|
+
deleteSchema(schemaName) {
|
|
118
|
+
const indexer = this.#indexers.get(schemaName)
|
|
119
|
+
if (!indexer) {
|
|
120
|
+
throw new Error(`IndexWriter doesn't know a schema named "${schemaName}"`)
|
|
121
|
+
}
|
|
122
|
+
indexer.deleteAll()
|
|
123
|
+
}
|
|
124
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CREATOR_ROLE_ID,
|
|
3
|
+
COORDINATOR_ROLE_ID,
|
|
4
|
+
MEMBER_ROLE_ID,
|
|
5
|
+
} from './roles.js'
|
|
6
|
+
export { plugin as MapeoStaticMapsFastifyPlugin } from './fastify-plugins/maps/static-maps.js'
|
|
7
|
+
export { plugin as MapeoOfflineFallbackMapFastifyPlugin } from './fastify-plugins/maps/offline-fallback-map.js'
|
|
8
|
+
export { plugin as MapeoMapsFastifyPlugin } from './fastify-plugins/maps/index.js'
|
|
9
|
+
export { FastifyController } from './fastify-controller.js'
|
|
10
|
+
export { MapeoManager } from './mapeo-manager.js'
|
|
11
|
+
|
|
12
|
+
export const roles = /** @type {const} */ ({
|
|
13
|
+
CREATOR_ROLE_ID,
|
|
14
|
+
COORDINATOR_ROLE_ID,
|
|
15
|
+
MEMBER_ROLE_ID,
|
|
16
|
+
})
|