@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,980 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import Database from 'better-sqlite3'
|
|
3
|
+
import { decodeBlockPrefix, decode, parseVersionId } from '@comapeo/schema'
|
|
4
|
+
import { drizzle } from 'drizzle-orm/better-sqlite3'
|
|
5
|
+
import { migrate } from 'drizzle-orm/better-sqlite3/migrator'
|
|
6
|
+
import { discoveryKey } from 'hypercore-crypto'
|
|
7
|
+
import { TypedEmitter } from 'tiny-typed-emitter'
|
|
8
|
+
|
|
9
|
+
import { NAMESPACES, NAMESPACE_SCHEMAS } from './constants.js'
|
|
10
|
+
import { CoreManager } from './core-manager/index.js'
|
|
11
|
+
import { DataStore } from './datastore/index.js'
|
|
12
|
+
import { DataType, kCreateWithDocId } from './datatype/index.js'
|
|
13
|
+
import { BlobStore } from './blob-store/index.js'
|
|
14
|
+
import { BlobApi } from './blob-api.js'
|
|
15
|
+
import { IndexWriter } from './index-writer/index.js'
|
|
16
|
+
import { projectSettingsTable } from './schema/client.js'
|
|
17
|
+
import {
|
|
18
|
+
coreOwnershipTable,
|
|
19
|
+
deviceInfoTable,
|
|
20
|
+
fieldTable,
|
|
21
|
+
observationTable,
|
|
22
|
+
trackTable,
|
|
23
|
+
presetTable,
|
|
24
|
+
roleTable,
|
|
25
|
+
iconTable,
|
|
26
|
+
translationTable,
|
|
27
|
+
} from './schema/project.js'
|
|
28
|
+
import {
|
|
29
|
+
CoreOwnership,
|
|
30
|
+
getWinner,
|
|
31
|
+
mapAndValidateCoreOwnership,
|
|
32
|
+
} from './core-ownership.js'
|
|
33
|
+
import {
|
|
34
|
+
BLOCKED_ROLE_ID,
|
|
35
|
+
COORDINATOR_ROLE_ID,
|
|
36
|
+
Roles,
|
|
37
|
+
LEFT_ROLE_ID,
|
|
38
|
+
} from './roles.js'
|
|
39
|
+
import {
|
|
40
|
+
assert,
|
|
41
|
+
getDeviceId,
|
|
42
|
+
projectKeyToId,
|
|
43
|
+
projectKeyToPublicId,
|
|
44
|
+
valueOf,
|
|
45
|
+
} from './utils.js'
|
|
46
|
+
import { MemberApi } from './member-api.js'
|
|
47
|
+
import { SyncApi, kHandleDiscoveryKey } from './sync/sync-api.js'
|
|
48
|
+
import { Logger } from './logger.js'
|
|
49
|
+
import { IconApi } from './icon-api.js'
|
|
50
|
+
import { readConfig } from './config-import.js'
|
|
51
|
+
import TranslationApi from './translation-api.js'
|
|
52
|
+
/** @import { ProjectSettingsValue } from '@comapeo/schema' */
|
|
53
|
+
/** @import { CoreStorage, KeyPair, Namespace } from './types.js' */
|
|
54
|
+
|
|
55
|
+
/** @typedef {Omit<ProjectSettingsValue, 'schemaName'>} EditableProjectSettings */
|
|
56
|
+
/** @typedef {ProjectSettingsValue['configMetadata']} ConfigMetadata */
|
|
57
|
+
|
|
58
|
+
const CORESTORE_STORAGE_FOLDER_NAME = 'corestore'
|
|
59
|
+
const INDEXER_STORAGE_FOLDER_NAME = 'indexer'
|
|
60
|
+
export const kCoreManager = Symbol('coreManager')
|
|
61
|
+
export const kCoreOwnership = Symbol('coreOwnership')
|
|
62
|
+
export const kSetOwnDeviceInfo = Symbol('kSetOwnDeviceInfo')
|
|
63
|
+
export const kBlobStore = Symbol('blobStore')
|
|
64
|
+
export const kProjectReplicate = Symbol('replicate project')
|
|
65
|
+
export const kDataTypes = Symbol('dataTypes')
|
|
66
|
+
export const kProjectLeave = Symbol('leave project')
|
|
67
|
+
export const kClearDataIfLeft = Symbol('clear data if left project')
|
|
68
|
+
|
|
69
|
+
const EMPTY_PROJECT_SETTINGS = Object.freeze({})
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @extends {TypedEmitter<{ close: () => void }>}
|
|
73
|
+
*/
|
|
74
|
+
export class MapeoProject extends TypedEmitter {
|
|
75
|
+
#projectId
|
|
76
|
+
#deviceId
|
|
77
|
+
#coreManager
|
|
78
|
+
#indexWriter
|
|
79
|
+
#dataStores
|
|
80
|
+
#dataTypes
|
|
81
|
+
#blobStore
|
|
82
|
+
#coreOwnership
|
|
83
|
+
#roles
|
|
84
|
+
#sqlite
|
|
85
|
+
#memberApi
|
|
86
|
+
#iconApi
|
|
87
|
+
#syncApi
|
|
88
|
+
/** @type {TranslationApi} */
|
|
89
|
+
#translationApi
|
|
90
|
+
#l
|
|
91
|
+
/** @type {Boolean} this avoids loading multiple configs in parallel */
|
|
92
|
+
#loadingConfig
|
|
93
|
+
|
|
94
|
+
static EMPTY_PROJECT_SETTINGS = EMPTY_PROJECT_SETTINGS
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {Object} opts
|
|
98
|
+
* @param {string} opts.dbPath Path to store project sqlite db. Use `:memory:` for memory storage
|
|
99
|
+
* @param {string} opts.projectMigrationsFolder path for drizzle migration folder for project
|
|
100
|
+
* @param {import('@mapeo/crypto').KeyManager} opts.keyManager mapeo/crypto KeyManager instance
|
|
101
|
+
* @param {Buffer} opts.projectKey 32-byte public key of the project creator core
|
|
102
|
+
* @param {Buffer} [opts.projectSecretKey] 32-byte secret key of the project creator core
|
|
103
|
+
* @param {import('./generated/keys.js').EncryptionKeys} opts.encryptionKeys Encryption keys for each namespace
|
|
104
|
+
* @param {import('drizzle-orm/better-sqlite3').BetterSQLite3Database} opts.sharedDb
|
|
105
|
+
* @param {IndexWriter} opts.sharedIndexWriter
|
|
106
|
+
* @param {CoreStorage} opts.coreStorage Folder to store all hypercore data
|
|
107
|
+
* @param {(mediaType: 'blobs' | 'icons') => Promise<string>} opts.getMediaBaseUrl
|
|
108
|
+
* @param {import('./local-peers.js').LocalPeers} opts.localPeers
|
|
109
|
+
* @param {Logger} [opts.logger]
|
|
110
|
+
*
|
|
111
|
+
*/
|
|
112
|
+
constructor({
|
|
113
|
+
dbPath,
|
|
114
|
+
projectMigrationsFolder,
|
|
115
|
+
sharedDb,
|
|
116
|
+
sharedIndexWriter,
|
|
117
|
+
coreStorage,
|
|
118
|
+
keyManager,
|
|
119
|
+
projectKey,
|
|
120
|
+
projectSecretKey,
|
|
121
|
+
encryptionKeys,
|
|
122
|
+
getMediaBaseUrl,
|
|
123
|
+
localPeers,
|
|
124
|
+
logger,
|
|
125
|
+
}) {
|
|
126
|
+
super()
|
|
127
|
+
|
|
128
|
+
this.#l = Logger.create('project', logger)
|
|
129
|
+
this.#deviceId = getDeviceId(keyManager)
|
|
130
|
+
this.#projectId = projectKeyToId(projectKey)
|
|
131
|
+
this.#loadingConfig = false
|
|
132
|
+
|
|
133
|
+
///////// 1. Setup database
|
|
134
|
+
this.#sqlite = new Database(dbPath)
|
|
135
|
+
const db = drizzle(this.#sqlite)
|
|
136
|
+
migrate(db, { migrationsFolder: projectMigrationsFolder })
|
|
137
|
+
|
|
138
|
+
///////// 2. Setup random-access-storage functions
|
|
139
|
+
|
|
140
|
+
/** @type {ConstructorParameters<typeof CoreManager>[0]['storage']} */
|
|
141
|
+
const coreManagerStorage = (name) =>
|
|
142
|
+
coreStorage(path.join(CORESTORE_STORAGE_FOLDER_NAME, name))
|
|
143
|
+
|
|
144
|
+
/** @type {ConstructorParameters<typeof DataStore>[0]['storage']} */
|
|
145
|
+
const indexerStorage = (name) =>
|
|
146
|
+
coreStorage(path.join(INDEXER_STORAGE_FOLDER_NAME, name))
|
|
147
|
+
|
|
148
|
+
///////// 3. Create instances
|
|
149
|
+
|
|
150
|
+
this.#coreManager = new CoreManager({
|
|
151
|
+
projectSecretKey,
|
|
152
|
+
encryptionKeys,
|
|
153
|
+
projectKey,
|
|
154
|
+
keyManager,
|
|
155
|
+
storage: coreManagerStorage,
|
|
156
|
+
db,
|
|
157
|
+
logger: this.#l,
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
this.#indexWriter = new IndexWriter({
|
|
161
|
+
tables: [
|
|
162
|
+
observationTable,
|
|
163
|
+
trackTable,
|
|
164
|
+
presetTable,
|
|
165
|
+
fieldTable,
|
|
166
|
+
coreOwnershipTable,
|
|
167
|
+
roleTable,
|
|
168
|
+
deviceInfoTable,
|
|
169
|
+
iconTable,
|
|
170
|
+
translationTable,
|
|
171
|
+
],
|
|
172
|
+
sqlite: this.#sqlite,
|
|
173
|
+
getWinner,
|
|
174
|
+
mapDoc: (doc, version) => {
|
|
175
|
+
switch (doc.schemaName) {
|
|
176
|
+
case 'coreOwnership':
|
|
177
|
+
return mapAndValidateCoreOwnership(doc, version)
|
|
178
|
+
case 'deviceInfo':
|
|
179
|
+
return mapAndValidateDeviceInfo(doc, version)
|
|
180
|
+
default:
|
|
181
|
+
return doc
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
logger: this.#l,
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
this.#dataStores = {
|
|
188
|
+
auth: new DataStore({
|
|
189
|
+
coreManager: this.#coreManager,
|
|
190
|
+
namespace: 'auth',
|
|
191
|
+
batch: (entries) => this.#indexWriter.batch(entries),
|
|
192
|
+
storage: indexerStorage,
|
|
193
|
+
}),
|
|
194
|
+
config: new DataStore({
|
|
195
|
+
coreManager: this.#coreManager,
|
|
196
|
+
namespace: 'config',
|
|
197
|
+
batch: (entries) =>
|
|
198
|
+
this.#handleConfigEntries(entries, {
|
|
199
|
+
projectIndexWriter: this.#indexWriter,
|
|
200
|
+
sharedIndexWriter,
|
|
201
|
+
}),
|
|
202
|
+
storage: indexerStorage,
|
|
203
|
+
}),
|
|
204
|
+
data: new DataStore({
|
|
205
|
+
coreManager: this.#coreManager,
|
|
206
|
+
namespace: 'data',
|
|
207
|
+
batch: (entries) => this.#indexWriter.batch(entries),
|
|
208
|
+
storage: indexerStorage,
|
|
209
|
+
}),
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** @type {typeof TranslationApi.prototype.get} */
|
|
213
|
+
const getTranslations = (...args) => this.$translation.get(...args)
|
|
214
|
+
this.#dataTypes = {
|
|
215
|
+
observation: new DataType({
|
|
216
|
+
dataStore: this.#dataStores.data,
|
|
217
|
+
table: observationTable,
|
|
218
|
+
db,
|
|
219
|
+
getTranslations,
|
|
220
|
+
}),
|
|
221
|
+
track: new DataType({
|
|
222
|
+
dataStore: this.#dataStores.data,
|
|
223
|
+
table: trackTable,
|
|
224
|
+
db,
|
|
225
|
+
getTranslations,
|
|
226
|
+
}),
|
|
227
|
+
preset: new DataType({
|
|
228
|
+
dataStore: this.#dataStores.config,
|
|
229
|
+
table: presetTable,
|
|
230
|
+
db,
|
|
231
|
+
getTranslations,
|
|
232
|
+
}),
|
|
233
|
+
field: new DataType({
|
|
234
|
+
dataStore: this.#dataStores.config,
|
|
235
|
+
table: fieldTable,
|
|
236
|
+
db,
|
|
237
|
+
getTranslations,
|
|
238
|
+
}),
|
|
239
|
+
projectSettings: new DataType({
|
|
240
|
+
dataStore: this.#dataStores.config,
|
|
241
|
+
table: projectSettingsTable,
|
|
242
|
+
db: sharedDb,
|
|
243
|
+
getTranslations,
|
|
244
|
+
}),
|
|
245
|
+
coreOwnership: new DataType({
|
|
246
|
+
dataStore: this.#dataStores.auth,
|
|
247
|
+
table: coreOwnershipTable,
|
|
248
|
+
db,
|
|
249
|
+
getTranslations,
|
|
250
|
+
}),
|
|
251
|
+
role: new DataType({
|
|
252
|
+
dataStore: this.#dataStores.auth,
|
|
253
|
+
table: roleTable,
|
|
254
|
+
db,
|
|
255
|
+
getTranslations,
|
|
256
|
+
}),
|
|
257
|
+
deviceInfo: new DataType({
|
|
258
|
+
dataStore: this.#dataStores.config,
|
|
259
|
+
table: deviceInfoTable,
|
|
260
|
+
db,
|
|
261
|
+
getTranslations,
|
|
262
|
+
}),
|
|
263
|
+
icon: new DataType({
|
|
264
|
+
dataStore: this.#dataStores.config,
|
|
265
|
+
table: iconTable,
|
|
266
|
+
db,
|
|
267
|
+
getTranslations,
|
|
268
|
+
}),
|
|
269
|
+
translation: new DataType({
|
|
270
|
+
dataStore: this.#dataStores.config,
|
|
271
|
+
table: translationTable,
|
|
272
|
+
db,
|
|
273
|
+
getTranslations: () => {
|
|
274
|
+
throw new Error('Cannot get translation for translations')
|
|
275
|
+
},
|
|
276
|
+
}),
|
|
277
|
+
}
|
|
278
|
+
const identityKeypair = keyManager.getIdentityKeypair()
|
|
279
|
+
const coreKeypairs = getCoreKeypairs({
|
|
280
|
+
projectKey,
|
|
281
|
+
projectSecretKey,
|
|
282
|
+
keyManager,
|
|
283
|
+
})
|
|
284
|
+
this.#coreOwnership = new CoreOwnership({
|
|
285
|
+
dataType: this.#dataTypes.coreOwnership,
|
|
286
|
+
coreKeypairs,
|
|
287
|
+
identityKeypair,
|
|
288
|
+
})
|
|
289
|
+
this.#roles = new Roles({
|
|
290
|
+
dataType: this.#dataTypes.role,
|
|
291
|
+
coreOwnership: this.#coreOwnership,
|
|
292
|
+
coreManager: this.#coreManager,
|
|
293
|
+
projectKey: projectKey,
|
|
294
|
+
deviceKey: keyManager.getIdentityKeypair().publicKey,
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
this.#memberApi = new MemberApi({
|
|
298
|
+
deviceId: this.#deviceId,
|
|
299
|
+
roles: this.#roles,
|
|
300
|
+
coreOwnership: this.#coreOwnership,
|
|
301
|
+
encryptionKeys,
|
|
302
|
+
projectKey,
|
|
303
|
+
rpc: localPeers,
|
|
304
|
+
dataTypes: {
|
|
305
|
+
deviceInfo: this.#dataTypes.deviceInfo,
|
|
306
|
+
project: this.#dataTypes.projectSettings,
|
|
307
|
+
},
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
const projectPublicId = projectKeyToPublicId(projectKey)
|
|
311
|
+
|
|
312
|
+
this.#blobStore = new BlobStore({
|
|
313
|
+
coreManager: this.#coreManager,
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
this.$blobs = new BlobApi({
|
|
317
|
+
blobStore: this.#blobStore,
|
|
318
|
+
getMediaBaseUrl: async () => {
|
|
319
|
+
let base = await getMediaBaseUrl('blobs')
|
|
320
|
+
if (!base.endsWith('/')) {
|
|
321
|
+
base += '/'
|
|
322
|
+
}
|
|
323
|
+
return base + projectPublicId
|
|
324
|
+
},
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
this.#iconApi = new IconApi({
|
|
328
|
+
iconDataStore: this.#dataStores.config,
|
|
329
|
+
iconDataType: this.#dataTypes.icon,
|
|
330
|
+
getMediaBaseUrl: async () => {
|
|
331
|
+
let base = await getMediaBaseUrl('icons')
|
|
332
|
+
if (!base.endsWith('/')) {
|
|
333
|
+
base += '/'
|
|
334
|
+
}
|
|
335
|
+
return base + projectPublicId
|
|
336
|
+
},
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
this.#syncApi = new SyncApi({
|
|
340
|
+
coreManager: this.#coreManager,
|
|
341
|
+
coreOwnership: this.#coreOwnership,
|
|
342
|
+
roles: this.#roles,
|
|
343
|
+
logger: this.#l,
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
this.#translationApi = new TranslationApi({
|
|
347
|
+
dataType: this.#dataTypes.translation,
|
|
348
|
+
table: translationTable,
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
///////// 4. Replicate local peers automatically
|
|
352
|
+
|
|
353
|
+
// Replicate already connected local peers
|
|
354
|
+
for (const peer of localPeers.peers) {
|
|
355
|
+
if (peer.status !== 'connected') continue
|
|
356
|
+
this.#coreManager.creatorCore.replicate(peer.protomux)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* @type {import('./local-peers.js').LocalPeersEvents['peer-add']}
|
|
361
|
+
*/
|
|
362
|
+
const onPeerAdd = (peer) => {
|
|
363
|
+
this.#coreManager.creatorCore.replicate(peer.protomux)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* @type {import('./local-peers.js').LocalPeersEvents['discovery-key']}
|
|
368
|
+
*/
|
|
369
|
+
const onDiscoverykey = (discoveryKey, stream) => {
|
|
370
|
+
this.#syncApi[kHandleDiscoveryKey](discoveryKey, stream)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// When a new peer is found, try to replicate (if it is not a member of the
|
|
374
|
+
// project it will fail the role check and be ignored)
|
|
375
|
+
localPeers.on('peer-add', onPeerAdd)
|
|
376
|
+
|
|
377
|
+
// This happens whenever a peer replicates a core to the stream. SyncApi
|
|
378
|
+
// handles replicating this core if we also have it, or requesting the key
|
|
379
|
+
// for the core.
|
|
380
|
+
localPeers.on('discovery-key', onDiscoverykey)
|
|
381
|
+
|
|
382
|
+
this.once('close', () => {
|
|
383
|
+
localPeers.off('peer-add', onPeerAdd)
|
|
384
|
+
localPeers.off('discovery-key', onDiscoverykey)
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
this.#l.log('Created project instance %h', projectKey)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* CoreManager instance, used for tests
|
|
392
|
+
*/
|
|
393
|
+
get [kCoreManager]() {
|
|
394
|
+
return this.#coreManager
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* CoreOwnership instance, used for tests
|
|
399
|
+
*/
|
|
400
|
+
get [kCoreOwnership]() {
|
|
401
|
+
return this.#coreOwnership
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* DataTypes object mappings, used for tests
|
|
406
|
+
*/
|
|
407
|
+
get [kDataTypes]() {
|
|
408
|
+
return this.#dataTypes
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
get [kBlobStore]() {
|
|
412
|
+
return this.#blobStore
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
get deviceId() {
|
|
416
|
+
return this.#deviceId
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Resolves when hypercores have all loaded
|
|
421
|
+
*
|
|
422
|
+
* @returns {Promise<void>}
|
|
423
|
+
*/
|
|
424
|
+
ready() {
|
|
425
|
+
return this.#coreManager.ready()
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
*/
|
|
430
|
+
async close() {
|
|
431
|
+
this.#l.log('closing project %h', this.#projectId)
|
|
432
|
+
const dataStorePromises = []
|
|
433
|
+
for (const dataStore of Object.values(this.#dataStores)) {
|
|
434
|
+
dataStorePromises.push(dataStore.close())
|
|
435
|
+
}
|
|
436
|
+
await Promise.all(dataStorePromises)
|
|
437
|
+
await this.#coreManager.close()
|
|
438
|
+
|
|
439
|
+
this.#sqlite.close()
|
|
440
|
+
|
|
441
|
+
this.emit('close')
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* @param {import('multi-core-indexer').Entry[]} entries
|
|
446
|
+
* @param {{projectIndexWriter: IndexWriter, sharedIndexWriter: IndexWriter}} indexWriters
|
|
447
|
+
*/
|
|
448
|
+
async #handleConfigEntries(
|
|
449
|
+
entries,
|
|
450
|
+
{ projectIndexWriter, sharedIndexWriter }
|
|
451
|
+
) {
|
|
452
|
+
/** @type {import('multi-core-indexer').Entry[]} */
|
|
453
|
+
const projectSettingsEntries = []
|
|
454
|
+
/** @type {import('multi-core-indexer').Entry[]} */
|
|
455
|
+
const otherEntries = []
|
|
456
|
+
|
|
457
|
+
for (const entry of entries) {
|
|
458
|
+
try {
|
|
459
|
+
const { schemaName } = decodeBlockPrefix(entry.block)
|
|
460
|
+
|
|
461
|
+
if (schemaName === 'projectSettings') {
|
|
462
|
+
projectSettingsEntries.push(entry)
|
|
463
|
+
} else if (schemaName === 'translation') {
|
|
464
|
+
const doc = decode(entry.block, {
|
|
465
|
+
coreDiscoveryKey: entry.key,
|
|
466
|
+
index: entry.index,
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
assert(doc.schemaName === 'translation', 'expected a translation doc')
|
|
470
|
+
this.#translationApi.index(doc)
|
|
471
|
+
otherEntries.push(entry)
|
|
472
|
+
} else {
|
|
473
|
+
otherEntries.push(entry)
|
|
474
|
+
}
|
|
475
|
+
} catch {
|
|
476
|
+
// Ignore errors thrown by values that can't be decoded for now
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
// TODO: Note that docs indexed to the shared index writer (project
|
|
480
|
+
// settings) are not currently returned here, so it is not possible to
|
|
481
|
+
// subscribe to updates for projectSettings
|
|
482
|
+
const [indexed] = await Promise.all([
|
|
483
|
+
projectIndexWriter.batch(otherEntries),
|
|
484
|
+
sharedIndexWriter.batch(projectSettingsEntries),
|
|
485
|
+
])
|
|
486
|
+
return indexed
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
get observation() {
|
|
490
|
+
return this.#dataTypes.observation
|
|
491
|
+
}
|
|
492
|
+
get track() {
|
|
493
|
+
return this.#dataTypes.track
|
|
494
|
+
}
|
|
495
|
+
get preset() {
|
|
496
|
+
return this.#dataTypes.preset
|
|
497
|
+
}
|
|
498
|
+
get field() {
|
|
499
|
+
return this.#dataTypes.field
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
get $member() {
|
|
503
|
+
return this.#memberApi
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
get $sync() {
|
|
507
|
+
return this.#syncApi
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
get $translation() {
|
|
511
|
+
return this.#translationApi
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* @param {Partial<EditableProjectSettings>} settings
|
|
516
|
+
* @returns {Promise<EditableProjectSettings>}
|
|
517
|
+
*/
|
|
518
|
+
async $setProjectSettings(settings) {
|
|
519
|
+
const { projectSettings } = this.#dataTypes
|
|
520
|
+
|
|
521
|
+
// We only want to catch the error to the getByDocId call
|
|
522
|
+
// Using try/catch for this is a little verbose when dealing with TS types
|
|
523
|
+
const existing = await projectSettings
|
|
524
|
+
.getByDocId(this.#projectId)
|
|
525
|
+
.catch(() => {
|
|
526
|
+
// project does not exist so return null
|
|
527
|
+
return null
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
if (existing) {
|
|
531
|
+
return extractEditableProjectSettings(
|
|
532
|
+
await projectSettings.update([existing.versionId, ...existing.forks], {
|
|
533
|
+
...valueOf(existing),
|
|
534
|
+
...settings,
|
|
535
|
+
})
|
|
536
|
+
)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return extractEditableProjectSettings(
|
|
540
|
+
await projectSettings[kCreateWithDocId](this.#projectId, {
|
|
541
|
+
...settings,
|
|
542
|
+
schemaName: 'projectSettings',
|
|
543
|
+
})
|
|
544
|
+
)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* @returns {Promise<EditableProjectSettings>}
|
|
549
|
+
*/
|
|
550
|
+
async $getProjectSettings() {
|
|
551
|
+
try {
|
|
552
|
+
return extractEditableProjectSettings(
|
|
553
|
+
await this.#dataTypes.projectSettings.getByDocId(this.#projectId)
|
|
554
|
+
)
|
|
555
|
+
} catch (e) {
|
|
556
|
+
this.#l.log('No project settings')
|
|
557
|
+
return /** @type {EditableProjectSettings} */ (EMPTY_PROJECT_SETTINGS)
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async $getOwnRole() {
|
|
562
|
+
return this.#roles.getRole(this.#deviceId)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* @param {string} originalVersionId The `originalVersionId` from a document.
|
|
567
|
+
* @returns {Promise<string>} The device ID for this creator.
|
|
568
|
+
* @throws When device ID cannot be found.
|
|
569
|
+
*/
|
|
570
|
+
async $originalVersionIdToDeviceId(originalVersionId) {
|
|
571
|
+
const { coreDiscoveryKey } = parseVersionId(originalVersionId)
|
|
572
|
+
const coreId = this.#coreManager
|
|
573
|
+
.getCoreByDiscoveryKey(coreDiscoveryKey)
|
|
574
|
+
?.key.toString('hex')
|
|
575
|
+
if (!coreId) throw new Error('NotFound')
|
|
576
|
+
return this.#coreOwnership.getOwner(coreId)
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Replicate a project to a @hyperswarm/secret-stream. Invites will not
|
|
581
|
+
* function because the RPC channel is not connected for project replication,
|
|
582
|
+
* and only this project will replicate (to replicate multiple projects you
|
|
583
|
+
* need to replicate the manager instance via manager[kManagerReplicate])
|
|
584
|
+
*
|
|
585
|
+
* @param {Parameters<import('hypercore')['replicate']>[0]} stream A duplex stream, a @hyperswarm/secret-stream, or a Protomux instance
|
|
586
|
+
*/
|
|
587
|
+
[kProjectReplicate](stream) {
|
|
588
|
+
// @ts-expect-error - hypercore types need updating
|
|
589
|
+
const replicationStream = this.#coreManager.creatorCore.replicate(stream, {
|
|
590
|
+
// @ts-ignore - hypercore types do not currently include this option
|
|
591
|
+
ondiscoverykey: async (discoveryKey) => {
|
|
592
|
+
const protomux =
|
|
593
|
+
/** @type {import('protomux')<import('@hyperswarm/secret-stream')>} */ (
|
|
594
|
+
replicationStream.noiseStream.userData
|
|
595
|
+
)
|
|
596
|
+
this.#syncApi[kHandleDiscoveryKey](discoveryKey, protomux)
|
|
597
|
+
},
|
|
598
|
+
})
|
|
599
|
+
return replicationStream
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* @param {Pick<import('@comapeo/schema').DeviceInfoValue, 'name' | 'deviceType'>} value
|
|
604
|
+
* @returns {Promise<import('@comapeo/schema').DeviceInfo>}
|
|
605
|
+
*/
|
|
606
|
+
async [kSetOwnDeviceInfo](value) {
|
|
607
|
+
const { deviceInfo } = this.#dataTypes
|
|
608
|
+
|
|
609
|
+
const configCoreId = this.#coreManager
|
|
610
|
+
.getWriterCore('config')
|
|
611
|
+
.key.toString('hex')
|
|
612
|
+
|
|
613
|
+
const doc = {
|
|
614
|
+
name: value.name,
|
|
615
|
+
deviceType: value.deviceType,
|
|
616
|
+
schemaName: /** @type {const} */ ('deviceInfo'),
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
let existingDoc
|
|
620
|
+
try {
|
|
621
|
+
existingDoc = await deviceInfo.getByDocId(configCoreId)
|
|
622
|
+
} catch (err) {
|
|
623
|
+
return await deviceInfo[kCreateWithDocId](configCoreId, doc)
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return deviceInfo.update(existingDoc.versionId, doc)
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* @returns {import('./icon-api.js').IconApi}
|
|
631
|
+
*/
|
|
632
|
+
get $icons() {
|
|
633
|
+
return this.#iconApi
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* @returns {Promise<void>}
|
|
638
|
+
*/
|
|
639
|
+
async #throwIfCannotLeaveProject() {
|
|
640
|
+
const roleDocs = await this.#dataTypes.role.getMany()
|
|
641
|
+
|
|
642
|
+
const ownRole = roleDocs.find(({ docId }) => this.#deviceId === docId)
|
|
643
|
+
|
|
644
|
+
if (ownRole?.roleId === BLOCKED_ROLE_ID) {
|
|
645
|
+
throw new Error('Cannot leave a project as a blocked device')
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const allRoles = await this.#roles.getAll()
|
|
649
|
+
|
|
650
|
+
const isOnlyDevice = allRoles.size <= 1
|
|
651
|
+
if (isOnlyDevice) return
|
|
652
|
+
|
|
653
|
+
const projectCreatorDeviceId = await this.#coreOwnership.getOwner(
|
|
654
|
+
this.#projectId
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
for (const deviceId of allRoles.keys()) {
|
|
658
|
+
if (deviceId === this.#deviceId) continue
|
|
659
|
+
const isCreatorOrCoordinator =
|
|
660
|
+
deviceId === projectCreatorDeviceId ||
|
|
661
|
+
roleDocs.some(
|
|
662
|
+
(doc) => doc.docId === deviceId && doc.roleId === COORDINATOR_ROLE_ID
|
|
663
|
+
)
|
|
664
|
+
if (isCreatorOrCoordinator) return
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
throw new Error(
|
|
668
|
+
'Cannot leave a project that does not have an external creator or another coordinator'
|
|
669
|
+
)
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
async [kProjectLeave]() {
|
|
673
|
+
await this.#throwIfCannotLeaveProject()
|
|
674
|
+
|
|
675
|
+
await this.#roles.assignRole(this.#deviceId, LEFT_ROLE_ID)
|
|
676
|
+
|
|
677
|
+
await this[kClearDataIfLeft]()
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Clear data if we've left the project. No-op if you're still in the project.
|
|
682
|
+
* @returns {Promise<void>}
|
|
683
|
+
*/
|
|
684
|
+
async [kClearDataIfLeft]() {
|
|
685
|
+
const role = await this.$getOwnRole()
|
|
686
|
+
if (role.roleId !== LEFT_ROLE_ID) {
|
|
687
|
+
return
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const namespacesWithoutAuth =
|
|
691
|
+
/** @satisfies {Exclude<Namespace, 'auth'>[]} */ ([
|
|
692
|
+
'config',
|
|
693
|
+
'data',
|
|
694
|
+
'blob',
|
|
695
|
+
'blobIndex',
|
|
696
|
+
])
|
|
697
|
+
const dataStoresToUnlink = Object.values(this.#dataStores).filter(
|
|
698
|
+
(dataStore) => dataStore.namespace !== 'auth'
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
await Promise.all(dataStoresToUnlink.map((ds) => ds.close()))
|
|
702
|
+
|
|
703
|
+
await Promise.all(
|
|
704
|
+
namespacesWithoutAuth.flatMap((namespace) => [
|
|
705
|
+
this.#coreManager.getWriterCore(namespace).core.close(),
|
|
706
|
+
this.#coreManager.deleteOthersData(namespace),
|
|
707
|
+
])
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
await Promise.all(dataStoresToUnlink.map((ds) => ds.unlink()))
|
|
711
|
+
|
|
712
|
+
/** @type {Set<string>} */
|
|
713
|
+
const authSchemas = new Set(NAMESPACE_SCHEMAS.auth)
|
|
714
|
+
for (const schemaName of this.#indexWriter.schemas) {
|
|
715
|
+
const isAuthSchema = authSchemas.has(schemaName)
|
|
716
|
+
if (!isAuthSchema) this.#indexWriter.deleteSchema(schemaName)
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/** @param {Object} opts
|
|
721
|
+
* @param {string} opts.configPath
|
|
722
|
+
* @returns {Promise<Error[]>}
|
|
723
|
+
*/
|
|
724
|
+
async importConfig({ configPath }) {
|
|
725
|
+
assert(
|
|
726
|
+
!this.#loadingConfig,
|
|
727
|
+
'Cannot run multiple config imports at the same time'
|
|
728
|
+
)
|
|
729
|
+
this.#loadingConfig = true
|
|
730
|
+
|
|
731
|
+
try {
|
|
732
|
+
// check for already present fields and presets and delete them if exist
|
|
733
|
+
const presetsToDelete = await grabDocsToDelete(this.preset)
|
|
734
|
+
const fieldsToDelete = await grabDocsToDelete(this.field)
|
|
735
|
+
// delete only translations that refer to deleted fields and presets
|
|
736
|
+
const translationsToDelete = await grabTranslationsToDelete({
|
|
737
|
+
logger: this.#l,
|
|
738
|
+
translation: this.$translation.dataType,
|
|
739
|
+
preset: this.preset,
|
|
740
|
+
field: this.field,
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
const config = await readConfig(configPath)
|
|
744
|
+
/** @type {Map<string, import('./icon-api.js').IconRef>} */
|
|
745
|
+
const iconNameToRef = new Map()
|
|
746
|
+
/** @type {Map<string, import('@comapeo/schema').PresetValue['fieldRefs'][1]>} */
|
|
747
|
+
const fieldNameToRef = new Map()
|
|
748
|
+
/** @type {Map<string,import('@comapeo/schema').TranslationValue['docRef']>} */
|
|
749
|
+
const presetNameToRef = new Map()
|
|
750
|
+
|
|
751
|
+
// Do this in serial not parallel to avoid memory issues (avoid keeping all icon buffers in memory)
|
|
752
|
+
for await (const icon of config.icons()) {
|
|
753
|
+
const { docId, versionId } = await this.#iconApi.create(icon)
|
|
754
|
+
iconNameToRef.set(icon.name, { docId, versionId })
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Ok to create fields and presets in parallel
|
|
758
|
+
const fieldPromises = []
|
|
759
|
+
for (const { name, value } of config.fields()) {
|
|
760
|
+
fieldPromises.push(
|
|
761
|
+
this.#dataTypes.field.create(value).then(({ docId, versionId }) => {
|
|
762
|
+
fieldNameToRef.set(name, { docId, versionId })
|
|
763
|
+
})
|
|
764
|
+
)
|
|
765
|
+
}
|
|
766
|
+
await Promise.all(fieldPromises)
|
|
767
|
+
|
|
768
|
+
const presetsWithRefs = []
|
|
769
|
+
for (const { fieldNames, iconName, value, name } of config.presets()) {
|
|
770
|
+
const fieldRefs = fieldNames.map((fieldName) => {
|
|
771
|
+
const fieldRef = fieldNameToRef.get(fieldName)
|
|
772
|
+
if (!fieldRef) {
|
|
773
|
+
throw new Error(
|
|
774
|
+
`field ${fieldName} not found (referenced by preset ${value.name})})`
|
|
775
|
+
)
|
|
776
|
+
}
|
|
777
|
+
return fieldRef
|
|
778
|
+
})
|
|
779
|
+
|
|
780
|
+
if (!iconName) {
|
|
781
|
+
throw new Error(`preset ${value.name} is missing an icon name`)
|
|
782
|
+
}
|
|
783
|
+
const iconRef = iconNameToRef.get(iconName)
|
|
784
|
+
if (!iconRef) {
|
|
785
|
+
throw new Error(
|
|
786
|
+
`icon ${iconName} not found (referenced by preset ${value.name})`
|
|
787
|
+
)
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
presetsWithRefs.push({
|
|
791
|
+
preset: {
|
|
792
|
+
...value,
|
|
793
|
+
iconRef,
|
|
794
|
+
fieldRefs,
|
|
795
|
+
},
|
|
796
|
+
name,
|
|
797
|
+
})
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const presetPromises = []
|
|
801
|
+
for (const { preset, name } of presetsWithRefs) {
|
|
802
|
+
presetPromises.push(
|
|
803
|
+
this.preset.create(preset).then(({ docId, versionId }) => {
|
|
804
|
+
presetNameToRef.set(name, { docId, versionId })
|
|
805
|
+
})
|
|
806
|
+
)
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
await Promise.all(presetPromises)
|
|
810
|
+
|
|
811
|
+
const translationPromises = []
|
|
812
|
+
for (const { name, value } of config.translations()) {
|
|
813
|
+
let docRef
|
|
814
|
+
if (value.docRefType === 'field') {
|
|
815
|
+
docRef = { ...fieldNameToRef.get(name) }
|
|
816
|
+
} else if (value.docRefType === 'preset') {
|
|
817
|
+
docRef = { ...presetNameToRef.get(name) }
|
|
818
|
+
} else {
|
|
819
|
+
throw new Error(`invalid docRefType ${value.docRefType}`)
|
|
820
|
+
}
|
|
821
|
+
if (docRef.docId && docRef.versionId) {
|
|
822
|
+
translationPromises.push(
|
|
823
|
+
this.$translation.put({
|
|
824
|
+
...value,
|
|
825
|
+
docRef: {
|
|
826
|
+
docId: docRef.docId,
|
|
827
|
+
versionId: docRef.versionId,
|
|
828
|
+
},
|
|
829
|
+
})
|
|
830
|
+
)
|
|
831
|
+
} else {
|
|
832
|
+
throw new Error(
|
|
833
|
+
`docRef for ${value.docRefType} with name ${name} not found`
|
|
834
|
+
)
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
await Promise.all(translationPromises)
|
|
838
|
+
|
|
839
|
+
// close the zip handles after we know we won't be needing them anymore
|
|
840
|
+
await config.close()
|
|
841
|
+
const presetIds = [...presetNameToRef.values()].map((val) => val.docId)
|
|
842
|
+
|
|
843
|
+
await this.$setProjectSettings({
|
|
844
|
+
defaultPresets: {
|
|
845
|
+
point: presetIds,
|
|
846
|
+
line: [],
|
|
847
|
+
area: [],
|
|
848
|
+
vertex: [],
|
|
849
|
+
relation: [],
|
|
850
|
+
},
|
|
851
|
+
configMetadata: config.metadata,
|
|
852
|
+
})
|
|
853
|
+
|
|
854
|
+
const deletePresetsPromise = Promise.all(
|
|
855
|
+
presetsToDelete.map(async (docId) => {
|
|
856
|
+
const { deleted } = await this.preset.getByDocId(docId)
|
|
857
|
+
if (!deleted) await this.preset.delete(docId)
|
|
858
|
+
})
|
|
859
|
+
)
|
|
860
|
+
const deleteFieldsPromise = Promise.all(
|
|
861
|
+
fieldsToDelete.map(async (docId) => {
|
|
862
|
+
const { deleted } = await this.field.getByDocId(docId)
|
|
863
|
+
if (!deleted) await this.field.delete(docId)
|
|
864
|
+
})
|
|
865
|
+
)
|
|
866
|
+
const deleteTranslationsPromise = Promise.all(
|
|
867
|
+
[...translationsToDelete].map(async (docId) => {
|
|
868
|
+
const { deleted } = await this.$translation.dataType.getByDocId(docId)
|
|
869
|
+
if (!deleted) await this.$translation.dataType.delete(docId)
|
|
870
|
+
})
|
|
871
|
+
)
|
|
872
|
+
await Promise.all([
|
|
873
|
+
deletePresetsPromise,
|
|
874
|
+
deleteFieldsPromise,
|
|
875
|
+
deleteTranslationsPromise,
|
|
876
|
+
])
|
|
877
|
+
this.#loadingConfig = false
|
|
878
|
+
return config.warnings
|
|
879
|
+
} catch (e) {
|
|
880
|
+
this.#l.log('error loading config', e)
|
|
881
|
+
this.#loadingConfig = false
|
|
882
|
+
return /** @type Error[] */ []
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* @param {import("@comapeo/schema").ProjectSettings & { forks: string[] }} projectDoc
|
|
889
|
+
* @returns {EditableProjectSettings}
|
|
890
|
+
*/
|
|
891
|
+
function extractEditableProjectSettings(projectDoc) {
|
|
892
|
+
// eslint-disable-next-line no-unused-vars
|
|
893
|
+
const { schemaName, ...result } = valueOf(projectDoc)
|
|
894
|
+
return result
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
@param {MapeoProject['field'] | MapeoProject['preset']} dataType
|
|
899
|
+
@returns {Promise<String[]>}
|
|
900
|
+
*/
|
|
901
|
+
async function grabDocsToDelete(dataType) {
|
|
902
|
+
const toDelete = []
|
|
903
|
+
for (const { docId } of await dataType.getMany()) {
|
|
904
|
+
toDelete.push(docId)
|
|
905
|
+
}
|
|
906
|
+
return toDelete
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* @param {Object} opts
|
|
911
|
+
* @param {Logger} opts.logger
|
|
912
|
+
* @param {MapeoProject['$translation']['dataType']} opts.translation
|
|
913
|
+
* @param {MapeoProject['preset']} opts.preset
|
|
914
|
+
* @param {MapeoProject['field']} opts.field
|
|
915
|
+
* @returns {Promise<Set<String>>}
|
|
916
|
+
*/
|
|
917
|
+
async function grabTranslationsToDelete(opts) {
|
|
918
|
+
/** @type {Set<String>} */
|
|
919
|
+
const toDelete = new Set()
|
|
920
|
+
const translations = await opts.translation.getMany()
|
|
921
|
+
await Promise.all(
|
|
922
|
+
translations.map(async ({ docRefType, docRef, docId }) => {
|
|
923
|
+
if (docRefType === 'field' || docRefType === 'preset') {
|
|
924
|
+
let doc
|
|
925
|
+
try {
|
|
926
|
+
doc = await opts[docRefType].getByVersionId(docRef.versionId)
|
|
927
|
+
} catch (e) {
|
|
928
|
+
opts.logger.log(`referred ${docRef.versionId} is not found`)
|
|
929
|
+
}
|
|
930
|
+
if (doc) {
|
|
931
|
+
toDelete.add(docId)
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
})
|
|
935
|
+
)
|
|
936
|
+
return toDelete
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Return a map of namespace -> core keypair
|
|
941
|
+
*
|
|
942
|
+
* For the project owner the keypair for the 'auth' namespace is the projectKey
|
|
943
|
+
* and projectSecretKey. In all other cases keypairs are derived from the
|
|
944
|
+
* project key
|
|
945
|
+
*
|
|
946
|
+
* @param {object} opts
|
|
947
|
+
* @param {Buffer} opts.projectKey
|
|
948
|
+
* @param {Buffer} [opts.projectSecretKey]
|
|
949
|
+
* @param {import('@mapeo/crypto').KeyManager} opts.keyManager
|
|
950
|
+
* @returns {Record<Namespace, KeyPair>}
|
|
951
|
+
*/
|
|
952
|
+
function getCoreKeypairs({ projectKey, projectSecretKey, keyManager }) {
|
|
953
|
+
const keypairs = /** @type {Record<Namespace, KeyPair>} */ ({})
|
|
954
|
+
|
|
955
|
+
for (const namespace of NAMESPACES) {
|
|
956
|
+
keypairs[namespace] =
|
|
957
|
+
namespace === 'auth' && projectSecretKey
|
|
958
|
+
? { publicKey: projectKey, secretKey: projectSecretKey }
|
|
959
|
+
: keyManager.getHypercoreKeypair(namespace, projectKey)
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
return keypairs
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Validate that a deviceInfo record is written by the device that is it about,
|
|
967
|
+
* e.g. version.coreKey should equal docId
|
|
968
|
+
*
|
|
969
|
+
* @param {import('@comapeo/schema').DeviceInfo} doc
|
|
970
|
+
* @param {import('@comapeo/schema').VersionIdObject} version
|
|
971
|
+
* @returns {import('@comapeo/schema').DeviceInfo}
|
|
972
|
+
*/
|
|
973
|
+
function mapAndValidateDeviceInfo(doc, { coreDiscoveryKey }) {
|
|
974
|
+
if (!coreDiscoveryKey.equals(discoveryKey(Buffer.from(doc.docId, 'hex')))) {
|
|
975
|
+
throw new Error(
|
|
976
|
+
'Invalid deviceInfo record, cannot write deviceInfo for another device'
|
|
977
|
+
)
|
|
978
|
+
}
|
|
979
|
+
return doc
|
|
980
|
+
}
|