@comapeo/core 4.4.0 → 5.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/dist/blob-store/downloader.d.ts +5 -2
- package/dist/blob-store/downloader.d.ts.map +1 -1
- package/dist/constants.d.ts +0 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/datatype/index.d.ts +1 -1
- package/dist/datatype/index.d.ts.map +1 -1
- package/dist/discovery/local-discovery.d.ts.map +1 -1
- package/dist/import-categories.d.ts +19 -0
- package/dist/import-categories.d.ts.map +1 -0
- package/dist/intl/iso639.d.ts +4 -0
- package/dist/intl/iso639.d.ts.map +1 -0
- package/dist/intl/parse-bcp-47.d.ts +22 -0
- package/dist/intl/parse-bcp-47.d.ts.map +1 -0
- package/dist/invite/invite-api.d.ts.map +1 -1
- package/dist/lib/drizzle-helpers.d.ts +19 -1
- package/dist/lib/drizzle-helpers.d.ts.map +1 -1
- package/dist/mapeo-manager.d.ts +15 -9
- package/dist/mapeo-manager.d.ts.map +1 -1
- package/dist/mapeo-project.d.ts +4968 -3017
- package/dist/mapeo-project.d.ts.map +1 -1
- package/dist/schema/client.d.ts +246 -232
- package/dist/schema/client.d.ts.map +1 -1
- package/dist/schema/comapeo-to-drizzle.d.ts +65 -0
- package/dist/schema/comapeo-to-drizzle.d.ts.map +1 -0
- package/dist/schema/json-schema-to-drizzle.d.ts +18 -0
- package/dist/schema/json-schema-to-drizzle.d.ts.map +1 -0
- package/dist/schema/project.d.ts +2711 -1835
- package/dist/schema/project.d.ts.map +1 -1
- package/dist/schema/types.d.ts +73 -66
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/translation-api.d.ts +111 -189
- package/dist/translation-api.d.ts.map +1 -1
- package/dist/utils.d.ts +10 -0
- package/dist/utils.d.ts.map +1 -1
- package/drizzle/client/0004_glorious_shape.sql +1 -0
- package/drizzle/client/meta/0000_snapshot.json +13 -9
- package/drizzle/client/meta/0001_snapshot.json +13 -9
- package/drizzle/client/meta/0002_snapshot.json +13 -9
- package/drizzle/client/meta/0003_snapshot.json +13 -9
- package/drizzle/client/meta/0004_snapshot.json +239 -0
- package/drizzle/client/meta/_journal.json +7 -0
- package/drizzle/project/meta/0000_snapshot.json +43 -24
- package/drizzle/project/meta/0001_snapshot.json +47 -26
- package/drizzle/project/meta/0002_snapshot.json +47 -26
- package/package.json +16 -8
- package/src/constants.js +0 -3
- package/src/datatype/index.js +8 -5
- package/src/discovery/local-discovery.js +3 -2
- package/src/import-categories.js +364 -0
- package/src/index-writer/index.js +1 -1
- package/src/intl/iso639.js +8118 -0
- package/src/intl/parse-bcp-47.js +91 -0
- package/src/invite/invite-api.js +2 -0
- package/src/lib/drizzle-helpers.js +70 -18
- package/src/mapeo-manager.js +138 -88
- package/src/mapeo-project.js +56 -218
- package/src/roles.js +1 -1
- package/src/schema/client.js +22 -28
- package/src/schema/comapeo-to-drizzle.js +57 -0
- package/src/schema/{schema-to-drizzle.js → json-schema-to-drizzle.js} +25 -25
- package/src/schema/project.js +24 -37
- package/src/schema/types.ts +138 -99
- package/src/translation-api.js +64 -12
- package/src/utils.js +13 -0
- package/dist/config-import.d.ts +0 -74
- package/dist/config-import.d.ts.map +0 -1
- package/dist/schema/schema-to-drizzle.d.ts +0 -20
- package/dist/schema/schema-to-drizzle.d.ts.map +0 -1
- package/dist/schema/utils.d.ts +0 -55
- package/dist/schema/utils.d.ts.map +0 -1
- package/src/config-import.js +0 -603
- package/src/schema/utils.js +0 -51
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import PQueue from 'p-queue'
|
|
2
|
+
import { Reader } from 'comapeocat/reader.js'
|
|
3
|
+
import { typedEntries } from './utils.js'
|
|
4
|
+
import { parseBcp47 } from './intl/parse-bcp-47.js'
|
|
5
|
+
import ensureError from 'ensure-error'
|
|
6
|
+
|
|
7
|
+
// The main reason for the concurrency limit is to avoid run-away memory usage.
|
|
8
|
+
// The comapeocat Reader limits icon size to 2Mb (as-per the specification), and
|
|
9
|
+
// it's likely that as SVGs, most icons will be much smaller than that, but we
|
|
10
|
+
// don't want a file with 1,000 icons of 2Mb each to bring a device to its
|
|
11
|
+
// knees. The choice of 4 is somewhat arbitrary, but should keep memory usage
|
|
12
|
+
// reasonable even in the worst case (8Mb of icon data in memory at once), while
|
|
13
|
+
// still allowing some parallelism to speed up the import.
|
|
14
|
+
const ICON_QUEUE_CONCURRENCY = 4
|
|
15
|
+
// Because fields and presets are all stored in a single file in the categories
|
|
16
|
+
// archive, they all need to be loaded into memory at once anyway (each file,
|
|
17
|
+
// with all docs, is limited to 1Mb). The concurrency limit here is somewhat
|
|
18
|
+
// arbitrary also, but it's to avoid a performance bottleneck if a user tries to
|
|
19
|
+
// import a file with thousands of categories or fields.
|
|
20
|
+
const DOC_QUEUE_CONCURRENCY = 32
|
|
21
|
+
|
|
22
|
+
/** @import {MapeoProject} from './mapeo-project.js' */
|
|
23
|
+
/** @typedef {{ docId: string, versionId: string }} Ref */
|
|
24
|
+
/**
|
|
25
|
+
* @param {MapeoProject} project
|
|
26
|
+
* @param {object} options
|
|
27
|
+
* @param {string} options.filePath
|
|
28
|
+
* @param {import('./logger.js').Logger} options.logger
|
|
29
|
+
* @returns {Promise<void>}
|
|
30
|
+
*/
|
|
31
|
+
export async function importCategories(project, { filePath, logger }) {
|
|
32
|
+
// Queue promises to create icons and docs, initially with concurrency of 4
|
|
33
|
+
// for icons, to minimize memory usage.
|
|
34
|
+
const queue = new PQueue({ concurrency: ICON_QUEUE_CONCURRENCY })
|
|
35
|
+
|
|
36
|
+
// TODO: We should use something like a hash to avoid deleting and
|
|
37
|
+
// re-importing presets and fields that have not changed, and try to identify
|
|
38
|
+
// modified presets and fields so we can update them rather than delete and
|
|
39
|
+
// re-create.
|
|
40
|
+
const presetsToDelete = await existingDocIds(project.preset)
|
|
41
|
+
const fieldsToDelete = await existingDocIds(project.field)
|
|
42
|
+
// No need to delete translations or icons, since these are always fetched by
|
|
43
|
+
// their references, so unreferenced icons and translations are just ignored
|
|
44
|
+
// (because our DB is immutable, deleting docs does not save space, it instead
|
|
45
|
+
// adds a new copy of the doc with `deleted: true` to the DB)
|
|
46
|
+
|
|
47
|
+
const reader = new Reader(filePath)
|
|
48
|
+
try {
|
|
49
|
+
// This validates that all icons and fields referenced by categories exist -
|
|
50
|
+
// it should throw an error here (before any docs are created) for any of the
|
|
51
|
+
// errors that we check for below.
|
|
52
|
+
await reader.validate()
|
|
53
|
+
|
|
54
|
+
/** @type {Map<string, Ref>} */
|
|
55
|
+
const iconNameToRef = new Map()
|
|
56
|
+
/** @type {Map<string, Ref>} */
|
|
57
|
+
const fieldNameToRef = new Map()
|
|
58
|
+
/** @type {Map<string, Ref>} */
|
|
59
|
+
const presetNameToRef = new Map()
|
|
60
|
+
|
|
61
|
+
const categories = await reader.categories()
|
|
62
|
+
// We only add icons referenced by categories (the archive could contain additional icons)
|
|
63
|
+
/** @type {Set<string>} */
|
|
64
|
+
const iconsToAdd = new Set()
|
|
65
|
+
// Only import icons that are referenced by categories, rather than all icons in the file
|
|
66
|
+
for (const category of categories.values()) {
|
|
67
|
+
if (!('icon' in category && category.icon)) continue
|
|
68
|
+
iconsToAdd.add(category.icon)
|
|
69
|
+
}
|
|
70
|
+
/** @type {Error[]} */
|
|
71
|
+
const errors = []
|
|
72
|
+
for (const iconName of iconsToAdd) {
|
|
73
|
+
queue
|
|
74
|
+
.add(async () => {
|
|
75
|
+
const iconXml = await reader.getIcon(iconName)
|
|
76
|
+
if (!iconXml) {
|
|
77
|
+
// This should never happen because of the validate() call above
|
|
78
|
+
throw new Error(`Icon ${iconName} not found in import file`)
|
|
79
|
+
}
|
|
80
|
+
/** @type {Parameters<typeof project.$icons.create>[0]} */
|
|
81
|
+
const icon = {
|
|
82
|
+
name: iconName,
|
|
83
|
+
variants: [
|
|
84
|
+
{
|
|
85
|
+
mimeType: 'image/svg+xml',
|
|
86
|
+
size: 'medium',
|
|
87
|
+
blob: Buffer.from(iconXml, 'utf-8'),
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await project.$icons.create(icon).then(({ docId, versionId }) => {
|
|
93
|
+
iconNameToRef.set(iconName, { docId, versionId })
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
.catch((e) => {
|
|
97
|
+
// The queue task added above could throw (it shouldn't!), and the
|
|
98
|
+
// promise for that task is returned by `queue.add()`. We don't await
|
|
99
|
+
// this (because we want to keep adding items to the queue), but we need
|
|
100
|
+
// to avoid an uncaught error, so we catch it here and store it for later.
|
|
101
|
+
errors.push(ensureError(e))
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await queue.onIdle()
|
|
106
|
+
if (errors.length > 0) {
|
|
107
|
+
throw new AggregateError(
|
|
108
|
+
errors,
|
|
109
|
+
'Errors occurred creating icons during import'
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
queue.concurrency = DOC_QUEUE_CONCURRENCY // increase concurrency for creating fields and presets
|
|
113
|
+
|
|
114
|
+
const fields = await reader.fields()
|
|
115
|
+
/** @type {Set<string>} */
|
|
116
|
+
const fieldsToAdd = new Set()
|
|
117
|
+
// Only import fields that are referenced by categories, rather than all fields in the file
|
|
118
|
+
for (const category of categories.values()) {
|
|
119
|
+
for (const fieldName of category.fields) {
|
|
120
|
+
fieldsToAdd.add(fieldName)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
for (const fieldName of fieldsToAdd) {
|
|
124
|
+
const field = getOrThrow(fields, fieldName)
|
|
125
|
+
/** @type {import('@comapeo/schema').FieldValue} */
|
|
126
|
+
const fieldValue = { ...field, schemaName: 'field' }
|
|
127
|
+
queue
|
|
128
|
+
.add(() =>
|
|
129
|
+
project.field.create(fieldValue).then(({ docId, versionId }) => {
|
|
130
|
+
fieldNameToRef.set(fieldName, { docId, versionId })
|
|
131
|
+
})
|
|
132
|
+
)
|
|
133
|
+
.catch((e) => {
|
|
134
|
+
errors.push(ensureError(e))
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Must wait for all fields and icons to be created before creating presets,
|
|
139
|
+
// because we need the field and icon refs
|
|
140
|
+
await queue.onIdle()
|
|
141
|
+
if (errors.length > 0) {
|
|
142
|
+
throw new AggregateError(
|
|
143
|
+
errors,
|
|
144
|
+
'Errors occurred creating fields during import'
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const [categoryName, category] of categories) {
|
|
149
|
+
const {
|
|
150
|
+
fields: fieldNames,
|
|
151
|
+
icon: iconName,
|
|
152
|
+
appliesTo,
|
|
153
|
+
...rest
|
|
154
|
+
} = category
|
|
155
|
+
/** @type {Ref[]} */
|
|
156
|
+
const fieldRefs = []
|
|
157
|
+
for (const fieldName of fieldNames) {
|
|
158
|
+
const fieldRef = getOrThrow(fieldNameToRef, fieldName)
|
|
159
|
+
fieldRefs.push(fieldRef)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** @type {import('./icon-api.js').IconRef} */
|
|
163
|
+
let iconRef
|
|
164
|
+
if (iconName) {
|
|
165
|
+
iconRef = getOrThrow(iconNameToRef, iconName)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** @type {import('@comapeo/schema').PresetValue} */
|
|
169
|
+
const presetValue = {
|
|
170
|
+
...rest,
|
|
171
|
+
geometry: appliesToToGeometry(appliesTo),
|
|
172
|
+
fieldRefs,
|
|
173
|
+
iconRef,
|
|
174
|
+
schemaName: 'preset',
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
queue
|
|
178
|
+
.add(() =>
|
|
179
|
+
project.preset.create(presetValue).then(({ docId, versionId }) => {
|
|
180
|
+
presetNameToRef.set(categoryName, { docId, versionId })
|
|
181
|
+
})
|
|
182
|
+
)
|
|
183
|
+
.catch((e) => {
|
|
184
|
+
errors.push(ensureError(e))
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const { buildDateValue, ...readerMetadata } = await reader.metadata()
|
|
189
|
+
const fileVersion = await reader.fileVersion()
|
|
190
|
+
|
|
191
|
+
for await (const {
|
|
192
|
+
lang,
|
|
193
|
+
translations: translationsByDocType,
|
|
194
|
+
} of reader.translations()) {
|
|
195
|
+
const { language: languageCode, region: regionCode } = parseBcp47(lang)
|
|
196
|
+
if (!languageCode) {
|
|
197
|
+
logger.log(`ignoring invalid language tag: ${lang}`)
|
|
198
|
+
continue
|
|
199
|
+
}
|
|
200
|
+
for (const [docType, translationsByDocId] of typedEntries(
|
|
201
|
+
translationsByDocType
|
|
202
|
+
)) {
|
|
203
|
+
if (!translationsByDocId) continue
|
|
204
|
+
for (const [docId, translations] of typedEntries(translationsByDocId)) {
|
|
205
|
+
/** @type {{ docId: string, versionId: string } | undefined} */
|
|
206
|
+
let docRef
|
|
207
|
+
if (docType === 'field') {
|
|
208
|
+
docRef = fieldNameToRef.get(docId)
|
|
209
|
+
} else if (docType === 'category') {
|
|
210
|
+
docRef = presetNameToRef.get(docId)
|
|
211
|
+
}
|
|
212
|
+
if (!docRef) {
|
|
213
|
+
// ignore translations that reference unknown docs or doc types
|
|
214
|
+
continue
|
|
215
|
+
}
|
|
216
|
+
for (const [propertyRef, message] of typedEntries(translations)) {
|
|
217
|
+
/** @type {Parameters<typeof project.$translation.put>[0]} */
|
|
218
|
+
const translationValue = {
|
|
219
|
+
schemaName: 'translation',
|
|
220
|
+
languageCode: languageCode,
|
|
221
|
+
regionCode: regionCode ?? undefined,
|
|
222
|
+
docRefType: docType === 'category' ? 'preset' : 'field',
|
|
223
|
+
docRef,
|
|
224
|
+
propertyRef,
|
|
225
|
+
message,
|
|
226
|
+
}
|
|
227
|
+
queue
|
|
228
|
+
.add(() => project.$translation.put(translationValue))
|
|
229
|
+
.catch((e) => {
|
|
230
|
+
errors.push(ensureError(e))
|
|
231
|
+
})
|
|
232
|
+
// Since translations are stored in separate files in the archive,
|
|
233
|
+
// and there could potentially be hundreds of them in the future,
|
|
234
|
+
// and the async iterator in this loop will load a new file for
|
|
235
|
+
// every iteration, we pause here when there are queued tasks (up to
|
|
236
|
+
// DOCS_QUEUE_CONCURRENCY tasks could be pending however) to avoid
|
|
237
|
+
// run-away memory usage.
|
|
238
|
+
await queue.onSizeLessThan(1)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Need to wait for all presets to be created so that we can read the refs for the defaultPresets
|
|
245
|
+
await queue.onIdle()
|
|
246
|
+
if (errors.length > 0) {
|
|
247
|
+
throw new AggregateError(
|
|
248
|
+
errors,
|
|
249
|
+
'Errors occurred creating presets or translations during import'
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
/** @type {import('@comapeo/schema').ProjectSettings['defaultPresets']} */
|
|
253
|
+
const defaultPresets = {
|
|
254
|
+
point: [],
|
|
255
|
+
line: [],
|
|
256
|
+
area: [],
|
|
257
|
+
vertex: [],
|
|
258
|
+
relation: [],
|
|
259
|
+
}
|
|
260
|
+
const categorySelection = await reader.categorySelection()
|
|
261
|
+
for (const categoryName of categorySelection.observation) {
|
|
262
|
+
const ref = getOrThrow(presetNameToRef, categoryName)
|
|
263
|
+
defaultPresets.point.push(ref.docId)
|
|
264
|
+
}
|
|
265
|
+
for (const categoryName of categorySelection.track) {
|
|
266
|
+
const ref = getOrThrow(presetNameToRef, categoryName)
|
|
267
|
+
defaultPresets.line.push(ref.docId)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
await project.$setProjectSettings({
|
|
271
|
+
defaultPresets,
|
|
272
|
+
configMetadata: {
|
|
273
|
+
...readerMetadata,
|
|
274
|
+
fileVersion,
|
|
275
|
+
importDate: new Date().toISOString(),
|
|
276
|
+
buildDate: new Date(buildDateValue).toISOString(),
|
|
277
|
+
},
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
for (const docId of presetsToDelete) {
|
|
281
|
+
queue
|
|
282
|
+
.add(() => project.preset.delete(docId))
|
|
283
|
+
.catch((e) => {
|
|
284
|
+
errors.push(ensureError(e))
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
for (const docId of fieldsToDelete) {
|
|
288
|
+
queue
|
|
289
|
+
.add(() => project.field.delete(docId))
|
|
290
|
+
.catch((e) => {
|
|
291
|
+
errors.push(ensureError(e))
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
await queue.onIdle()
|
|
295
|
+
if (errors.length > 0) {
|
|
296
|
+
throw new AggregateError(
|
|
297
|
+
errors,
|
|
298
|
+
'Errors occurred deleting old presets or fields during import'
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
} finally {
|
|
302
|
+
// Don't throw errors from closing the reader, because if the import was
|
|
303
|
+
// successful we don't want to throw an error, and if there was an error
|
|
304
|
+
// with the import, the error thrown here would mask that error (Control
|
|
305
|
+
// flow statements (return, throw, break, continue) in the finally block
|
|
306
|
+
// will "mask" any completion value of the try block or catch block)
|
|
307
|
+
await reader.close().catch((e) => {
|
|
308
|
+
logger.log('error closing import file reader', e)
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* List the docIds of all existing (excluding deleted) docs in the given dataType
|
|
315
|
+
*
|
|
316
|
+
* @param {MapeoProject['field'] | MapeoProject['preset']} dataType
|
|
317
|
+
* @returns {Promise<Set<string>>}
|
|
318
|
+
*/
|
|
319
|
+
async function existingDocIds(dataType) {
|
|
320
|
+
const toDelete = new Set()
|
|
321
|
+
for (const { docId } of await dataType.getMany()) {
|
|
322
|
+
toDelete.add(docId)
|
|
323
|
+
}
|
|
324
|
+
return toDelete
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const APPLIES_TO_TO_GEOMETRY = /** @type {const} */ ({
|
|
328
|
+
observation: 'point',
|
|
329
|
+
track: 'line',
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Map the new "appliesTo" field to the old "geometry" field
|
|
334
|
+
*
|
|
335
|
+
* @param {import('comapeocat/reader.js').CategoryOutput['appliesTo']} appliesTo)}
|
|
336
|
+
* @returns {import('@comapeo/schema').Preset['geometry']}
|
|
337
|
+
*/
|
|
338
|
+
function appliesToToGeometry(appliesTo) {
|
|
339
|
+
return appliesTo.map((a) => APPLIES_TO_TO_GEOMETRY[a]).filter(Boolean)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// In the import function, the validation of the import file should ensure that
|
|
343
|
+
// all references exist, so map.get() should always succeed, however TS is
|
|
344
|
+
// unable to validate this statically. There should be no way that this will
|
|
345
|
+
// throw, but this helper will catch the case we haven't though of yet.
|
|
346
|
+
/**
|
|
347
|
+
* Get a value from a Map, or throw an error if the key is not present
|
|
348
|
+
*
|
|
349
|
+
* @template K, V
|
|
350
|
+
* @param {Map<K, V>} map
|
|
351
|
+
* @param {K} key
|
|
352
|
+
* @param {string | Error} [msgOrError]
|
|
353
|
+
* @returns {V}
|
|
354
|
+
* @throws {TypeError} if `map` is not a Map
|
|
355
|
+
* @throws {Error} if `key` is not in `map` (with `msgOrError` as message or the default message)
|
|
356
|
+
*/
|
|
357
|
+
function getOrThrow(map, key, msgOrError) {
|
|
358
|
+
if (!(map instanceof Map)) throw new TypeError('map must be a Map')
|
|
359
|
+
if (!map.has(key)) {
|
|
360
|
+
if (msgOrError instanceof Error) throw msgOrError
|
|
361
|
+
throw new Error(msgOrError ?? `key ${key} not found in map`)
|
|
362
|
+
}
|
|
363
|
+
return /** @type {V} */ (map.get(key))
|
|
364
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { decode } from '@comapeo/schema'
|
|
2
2
|
import SqliteIndexer from '@mapeo/sqlite-indexer'
|
|
3
3
|
import { getTableConfig } from 'drizzle-orm/sqlite-core'
|
|
4
|
-
import { getBacklinkTableName } from '../schema/
|
|
4
|
+
import { getBacklinkTableName } from '../schema/comapeo-to-drizzle.js'
|
|
5
5
|
import { discoveryKey } from 'hypercore-crypto'
|
|
6
6
|
import { Logger } from '../logger.js'
|
|
7
7
|
/** @import { MapeoDoc, VersionIdObject } from '@comapeo/schema' */
|