@abtnode/core 1.8.69-beta-b0bb2d67 → 1.8.69-beta-76f8a46f

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.
@@ -0,0 +1,173 @@
1
+ const capitalize = require('lodash/capitalize');
2
+ const { sign } = require('@arcblock/jwt');
3
+
4
+ const logger = require('@abtnode/logger')('@abtnode/core:install-component-url');
5
+
6
+ const { isFreeBlocklet, isComponentBlocklet } = require('@blocklet/meta/lib/util');
7
+ const { titleSchema } = require('@blocklet/meta/lib/schema');
8
+ const hasReservedKey = require('@blocklet/meta/lib/has-reserved-key');
9
+
10
+ const { BlockletStatus, BlockletEvents, BlockletGroup } = require('@blocklet/constant');
11
+ const {
12
+ getBlockletMetaFromUrl,
13
+ parseComponents,
14
+ filterDuplicateComponents,
15
+ ensureMeta,
16
+ checkStructVersion,
17
+ checkVersionCompatibility,
18
+ validateBlocklet,
19
+ } = require('../../../util/blocklet');
20
+ const StoreUtil = require('../../../util/store');
21
+ const { formatName } = require('../../../util/get-domain-for-blocklet');
22
+
23
+ const installComponentFromUrl = async ({
24
+ rootDid,
25
+ mountPoint,
26
+ url,
27
+ context,
28
+ title,
29
+ configs,
30
+ downloadTokenList,
31
+ sync,
32
+ manager,
33
+ states,
34
+ }) => {
35
+ const blocklet = await states.blocklet.getBlocklet(rootDid);
36
+ if (!blocklet) {
37
+ throw new Error('Root blocklet does not exist');
38
+ }
39
+
40
+ checkStructVersion(blocklet);
41
+
42
+ const { inStore } = await StoreUtil.parseSourceUrl(url);
43
+
44
+ const meta = await getBlockletMetaFromUrl(url);
45
+
46
+ if (meta.group === BlockletGroup.gateway) {
47
+ throw new Error('Cannot add gateway component');
48
+ }
49
+
50
+ // 如果是一个付费的blocklet,并且url来源为Store, 需要携带token才能下载成功
51
+ if (!isFreeBlocklet(meta) && inStore) {
52
+ const info = await states.node.read();
53
+
54
+ // eslint-disable-next-line no-param-reassign
55
+ context = {
56
+ ...context,
57
+ headers: {
58
+ 'x-server-did': info.did,
59
+ 'x-server-public-key': info.pk,
60
+ 'x-server-signature': sign(info.did, info.sk, {
61
+ exp: (Date.now() + 5 * 60 * 1000) / 1000,
62
+ }),
63
+ },
64
+ downloadTokenList: downloadTokenList || [],
65
+ };
66
+ }
67
+
68
+ if (!isComponentBlocklet(meta)) {
69
+ throw new Error('The blocklet cannot be a component');
70
+ }
71
+
72
+ if (title) {
73
+ meta.title = await titleSchema.validateAsync(title);
74
+ }
75
+
76
+ const newChildMeta = ensureMeta(meta);
77
+
78
+ // children
79
+ const newChild = {
80
+ meta: newChildMeta,
81
+ mountPoint: mountPoint || formatName(newChildMeta.name),
82
+ bundleSource: { url },
83
+ };
84
+
85
+ const { dynamicComponents } = await parseComponents(newChild);
86
+
87
+ const index = blocklet.children.findIndex((child) => child.meta.did === meta.did);
88
+ if (index >= 0) {
89
+ // if upgrade, do not update mountPoint and title
90
+ newChild.mountPoint = blocklet.children[index].mountPoint;
91
+ newChild.meta.title = blocklet.children[index].meta.title;
92
+ blocklet.children.splice(index, 1, newChild);
93
+ } else {
94
+ dynamicComponents.unshift(newChild);
95
+ }
96
+
97
+ const newChildren = filterDuplicateComponents(dynamicComponents, blocklet.children);
98
+
99
+ blocklet.children.push(...newChildren);
100
+
101
+ checkVersionCompatibility(blocklet.children);
102
+
103
+ const oldBlocklet = await manager._getBlockletForInstallation(rootDid);
104
+ const action = 'upgrade';
105
+ try {
106
+ // add component to db
107
+ await states.blocklet.addChildren(rootDid, newChildren);
108
+
109
+ // update configs
110
+ if (Array.isArray(configs)) {
111
+ if (hasReservedKey(configs)) {
112
+ throw new Error('Component key of environments can not start with `ABT_NODE_` or `BLOCKLET_`');
113
+ }
114
+
115
+ await states.blockletExtras.setConfigs([blocklet.meta.did, newChild.meta.did], configs);
116
+ }
117
+ } catch (err) {
118
+ logger.error('Add component failed', err);
119
+ await manager._rollback(action, rootDid, oldBlocklet);
120
+ throw err;
121
+ }
122
+
123
+ // new blocklet
124
+ const newBlocklet = await states.blocklet.setBlockletStatus(rootDid, BlockletStatus.waiting);
125
+
126
+ newBlocklet.children = blocklet.children;
127
+ await validateBlocklet(newBlocklet);
128
+
129
+ manager.emit(BlockletEvents.statusChange, newBlocklet);
130
+
131
+ const downloadParams = {
132
+ oldBlocklet: { ...oldBlocklet },
133
+ blocklet: { ...newBlocklet },
134
+ selectedComponentDids: [newChild.meta.did, ...newChildren.map((x) => x.meta.did)],
135
+ context,
136
+ postAction: action,
137
+ };
138
+
139
+ // backup rollback data
140
+ await manager._rollbackCache.backup({ did: rootDid, action, oldBlocklet });
141
+
142
+ if (sync) {
143
+ await manager._downloadAndInstall({ ...downloadParams, throwOnError: true });
144
+ return states.blocklet.getBlocklet(rootDid);
145
+ }
146
+
147
+ // add to queue
148
+ const ticket = manager.installQueue.push(
149
+ {
150
+ entity: 'blocklet',
151
+ action: 'download',
152
+ id: rootDid,
153
+ ...downloadParams,
154
+ },
155
+ rootDid
156
+ );
157
+
158
+ ticket.on('failed', async (err) => {
159
+ logger.error('queue failed', { entity: 'blocklet', action, did: rootDid, error: err });
160
+ await manager._rollback(action, rootDid, oldBlocklet);
161
+ manager.emit(`blocklet.${action}.failed`, { did: rootDid, err });
162
+ manager._createNotification(rootDid, {
163
+ title: `Blocklet ${capitalize(action)} Failed`,
164
+ description: `Blocklet ${rootDid} ${action} failed with error: ${err.message || 'queue exception'}`,
165
+ entityType: 'blocklet',
166
+ entityId: rootDid,
167
+ severity: 'error',
168
+ });
169
+ });
170
+ return newBlocklet;
171
+ };
172
+
173
+ module.exports = { installComponentFromUrl };
@@ -0,0 +1,377 @@
1
+ const path = require('path');
2
+ const fs = require('fs-extra');
3
+ const pick = require('lodash/pick');
4
+
5
+ const logger = require('@abtnode/logger')('@abtnode/core:migrate-application-to-struct-v2');
6
+
7
+ const { forEachBlockletSync, getSharedConfigObj } = require('@blocklet/meta/lib/util');
8
+ const { SLOT_FOR_IP_DNS_SITE } = require('@abtnode/constant');
9
+
10
+ const {
11
+ BlockletStatus,
12
+ BlockletSource,
13
+ BlockletEvents,
14
+ BlockletGroup,
15
+ BLOCKLET_DEFAULT_PORT_NAME,
16
+ BLOCKLET_INTERFACE_TYPE_WEB,
17
+ BLOCKLET_INTERFACE_PUBLIC,
18
+ BLOCKLET_DYNAMIC_PATH_PREFIX,
19
+ BLOCKLET_INTERFACE_PROTOCOL_HTTP,
20
+ BLOCKLET_DEFAULT_PATH_REWRITE,
21
+ BLOCKLET_DEFAULT_VERSION,
22
+ BLOCKLET_LATEST_SPEC_VERSION,
23
+ BLOCKLET_CONFIGURABLE_KEY,
24
+ BLOCKLET_META_FILE,
25
+ BLOCKLET_UPLOADS_DIR,
26
+ } = require('@blocklet/constant');
27
+ const { update: updateMetaFile } = require('@blocklet/meta/lib/file');
28
+ const { cloneDeep } = require('lodash');
29
+ const { isInProgress } = require('../../../util');
30
+ const { getBlockletDomainGroupName } = require('../../../util/router');
31
+ const { getIpDnsDomainForBlocklet } = require('../../../util/get-domain-for-blocklet');
32
+ const { getBundleDir } = require('../../../util/blocklet');
33
+
34
+ const sortMoveListBySrc = (list) => {
35
+ return list.sort((a, b) => (a.src.length > b.src.length ? -1 : 1));
36
+ };
37
+
38
+ const validateDataMoveList = (list) => {
39
+ const srcList = list.map((x) => x.src);
40
+ const distList = list.map((x) => x.dist);
41
+
42
+ // find duplicate element in src
43
+ const duplicateInSrc = srcList.filter((x, i) => srcList.indexOf(x) !== i);
44
+ if (duplicateInSrc.length) {
45
+ throw new Error(`Duplicate element in src of dataMoveList: ${duplicateInSrc}`);
46
+ }
47
+
48
+ // find duplicate element in list
49
+ const duplicateInDist = distList.filter((x, i) => distList.indexOf(x) !== i);
50
+ if (duplicateInDist.length) {
51
+ throw new Error(`Duplicate element in dist of dataMoveList: ${duplicateInDist}`);
52
+ }
53
+ };
54
+
55
+ const fillBlockletData = (data, app, id) => {
56
+ Object.assign(data, {
57
+ meta: {
58
+ name: id,
59
+ did: id,
60
+ bundleDid: id,
61
+ bundleName: id,
62
+ title: app.meta.title || '',
63
+ description: app.meta.description || '',
64
+ version: BLOCKLET_DEFAULT_VERSION,
65
+ group: BlockletGroup.gateway,
66
+ interfaces: [
67
+ {
68
+ type: BLOCKLET_INTERFACE_TYPE_WEB,
69
+ name: BLOCKLET_INTERFACE_PUBLIC,
70
+ path: BLOCKLET_DEFAULT_PATH_REWRITE,
71
+ prefix: BLOCKLET_DYNAMIC_PATH_PREFIX,
72
+ port: BLOCKLET_DEFAULT_PORT_NAME,
73
+ protocol: BLOCKLET_INTERFACE_PROTOCOL_HTTP,
74
+ },
75
+ ],
76
+ specVersion: BLOCKLET_LATEST_SPEC_VERSION,
77
+ environments: [],
78
+ },
79
+ source: BlockletSource.custom,
80
+ status: BlockletStatus.stopped,
81
+ });
82
+
83
+ if (app.meta.logo) {
84
+ data.meta.logo = app.meta.logo;
85
+ }
86
+ };
87
+
88
+ const appSystemFiles = ['logo.svg', 'rbac.db', 'session.db', 'user.db', '.assets', BLOCKLET_UPLOADS_DIR];
89
+
90
+ const migrateApplicationToStructV2 = async ({ did, appSk: newAppSk, context = {}, manager, states }) => {
91
+ logger.info('Preparing data for migration', { did });
92
+
93
+ if (!newAppSk) {
94
+ throw new Error('appSk is required');
95
+ }
96
+
97
+ const oldBlocklet = await manager.getBlocklet(did, { throwOnNotExist: true, ensureIntegrity: true });
98
+
99
+ if (oldBlocklet.structVersion || oldBlocklet.externalSk) {
100
+ throw new Error('Blocklet already migrated', pick(oldBlocklet, ['structVersion', 'externalSk']));
101
+ }
102
+
103
+ if (oldBlocklet.migratedFrom?.length) {
104
+ throw new Error('Blocklet already migrated');
105
+ }
106
+
107
+ if (isInProgress(oldBlocklet.status) || oldBlocklet.status === BlockletStatus.running) {
108
+ throw new Error('Please stop blocklet before migration');
109
+ }
110
+
111
+ const extraData = await states.blockletExtras.findOne({ did: oldBlocklet.meta.did });
112
+ const siteData = await states.site.findOneByBlocklet(oldBlocklet.meta.did);
113
+
114
+ const backupBlocklet = await states.blocklet.findOne({ 'meta.did': oldBlocklet.meta.did });
115
+ const backupExtra = cloneDeep(extraData);
116
+ const backupSite = await states.site.findOne({ _id: siteData.id });
117
+
118
+ const permanentAppDid = oldBlocklet.appDid;
119
+ const permanentAppSk = oldBlocklet.environmentObj.BLOCKLET_APP_SK;
120
+
121
+ // change index of extraData
122
+ extraData.did = permanentAppDid;
123
+ if (extraData.meta) {
124
+ extraData.meta.did = permanentAppDid;
125
+ extraData.meta.name = permanentAppDid;
126
+ }
127
+ // fork root component's configs to container
128
+ // FIXME: the configs in container should be managed in dashboard ?
129
+ // set permanent appSk first and then migrate to new appSk
130
+ extraData.configs = (extraData.configs || []).filter((x) => x.name !== BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK);
131
+ extraData.configs.push({
132
+ key: BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK,
133
+ value: permanentAppSk,
134
+ secure: true,
135
+ shared: false,
136
+ });
137
+ // clean children configs
138
+ extraData.children = [];
139
+ // clean dirty data in settings
140
+ if (extraData.settings) {
141
+ extraData.settings.children = [];
142
+ delete extraData.settings.navigation;
143
+ }
144
+
145
+ // delete system generated rules in siteData
146
+ siteData.rules = siteData.rules.filter((rule) => !rule.isProtected && rule.to.did !== oldBlocklet.meta.did);
147
+ // change index of siteData
148
+ siteData.domain = getBlockletDomainGroupName(permanentAppDid);
149
+
150
+ const blockletData = {
151
+ children: [],
152
+ };
153
+
154
+ const dataDirSrc = path.join(oldBlocklet.env.dataDir);
155
+ const dataDirDist = path.join(manager.dataDirs.data, permanentAppDid);
156
+ if (fs.existsSync(dataDirDist)) {
157
+ throw new Error(`blocklet data dir already exist: ${dataDirDist}`);
158
+ }
159
+ const dataMoveList = [];
160
+ let currentMoveIndex = -1; // for rollback
161
+ appSystemFiles.forEach((file) => {
162
+ const src = path.join(dataDirSrc, file);
163
+ const dist = path.join(dataDirDist, file);
164
+ if (fs.existsSync(src)) {
165
+ dataMoveList.push({ src, dist });
166
+ }
167
+ });
168
+
169
+ forEachBlockletSync(oldBlocklet, (component, { level, ancestors }) => {
170
+ // if have duplicate component, user should fix it manually
171
+ if (blockletData.children.some((x) => x.meta.bundleDid === component.meta.bundleDid)) {
172
+ throw new Error(
173
+ `Find duplicate component ${component.meta.title}, please delete useless components and migrate again`
174
+ );
175
+ }
176
+
177
+ // If old root component is a container, just skip it
178
+ if (level === 0 && component.meta.group === BlockletGroup.gateway) {
179
+ fillBlockletData(blockletData, component, permanentAppDid);
180
+ return;
181
+ }
182
+
183
+ // If old root component is a blocklet, make it to be level one component
184
+ if (level === 0 && component.meta.group !== BlockletGroup.gateway) {
185
+ fillBlockletData(blockletData, component, permanentAppDid);
186
+
187
+ // add root component to blockletData
188
+ const { source, deployedFrom } = component;
189
+ let bundleSource;
190
+ if (source === BlockletSource.store) {
191
+ bundleSource = {
192
+ store: component.deployedFrom,
193
+ name: component.meta.bundleName,
194
+ version: 'latest',
195
+ };
196
+ } else if (source === BlockletSource.url) {
197
+ bundleSource = {
198
+ url: component.deployedFrom,
199
+ };
200
+ }
201
+
202
+ blockletData.children.push({
203
+ ...pick(component, ['mode', 'status']),
204
+ mountPoint: component.mountPoint || '/',
205
+ meta: {
206
+ ...component.meta,
207
+ // change index of component
208
+ did: component.meta.bundleDid,
209
+ name: component.meta.bundleName,
210
+ },
211
+ bundleSource,
212
+ source,
213
+ deployedFrom,
214
+ children: [],
215
+ });
216
+
217
+ // add root component to extraData
218
+ extraData.children.push({
219
+ // change index of component
220
+ did: component.meta.bundleDid,
221
+ // filter application configs in root component
222
+ configs: (component.configs || []).filter((x) => !x.key.startsWith('BLOCKLET_')),
223
+ });
224
+
225
+ // move root component data by file
226
+ const componentFolders = (component.children || []).map((x) => x.meta.name.split('/')[0]);
227
+ const files = fs.readdirSync(dataDirSrc);
228
+ for (const file of files) {
229
+ if (!componentFolders.includes(file) && !appSystemFiles.includes(file)) {
230
+ dataMoveList.push({
231
+ src: path.join(dataDirSrc, file),
232
+ dist: path.join(dataDirDist, component.meta.bundleName, file),
233
+ });
234
+ }
235
+ }
236
+
237
+ return;
238
+ }
239
+
240
+ // add component to blockletData
241
+ blockletData.children.push({
242
+ ...pick(component, ['mode', 'status', 'bundleSource', 'source', 'deployedFrom']),
243
+ meta: {
244
+ ...component.meta,
245
+ // change index of component
246
+ did: component.meta.bundleDid,
247
+ name: component.meta.bundleName,
248
+ },
249
+ // keep pathPrefix of the component remains the same as before
250
+ mountPoint: path.join('/', ...ancestors.slice(1).map((x) => x.mountPoint || ''), component.mountPoint || '/'),
251
+ children: [],
252
+ });
253
+
254
+ // add component to extraData
255
+ extraData.children.push({
256
+ did: component.meta.bundleDid,
257
+ configs: component.configs || [],
258
+ });
259
+
260
+ const sharedConfigObj = getSharedConfigObj(component, ancestors);
261
+ if (sharedConfigObj) {
262
+ Object.entries(sharedConfigObj).forEach(([key, value]) => {
263
+ if (!extraData.configs.some((x) => x.key === key)) {
264
+ logger.info('add shared config to container configs', { did, key, value });
265
+ extraData.configs.push({ key, value });
266
+ }
267
+ });
268
+ }
269
+
270
+ // move component data dir
271
+ const src = component.env.dataDir;
272
+ const dist = path.join(dataDirSrc, component.meta.bundleName);
273
+ if (src !== dist) {
274
+ dataMoveList.push({ src: component.env.dataDir, dist: path.join(dataDirDist, component.meta.bundleName) });
275
+ }
276
+ });
277
+
278
+ // ensure the deepest component is moved first
279
+ const sortedDataMoveList = sortMoveListBySrc(dataMoveList);
280
+ validateDataMoveList(sortedDataMoveList);
281
+
282
+ // encrypt data in extras
283
+ states.blockletExtras.encryptSecurityData({ data: extraData });
284
+
285
+ // refresh ip dns domain
286
+ siteData.domainAliases = siteData.domainAliases.filter(
287
+ (x) => !x.value.includes(SLOT_FOR_IP_DNS_SITE) || !x.isProtected
288
+ );
289
+ siteData.domainAliases.push({ value: getIpDnsDomainForBlocklet(blockletData), isProtected: true });
290
+
291
+ // update state
292
+ let newBlocklet;
293
+ try {
294
+ logger.info('Start migrate application state', { did, appDid: permanentAppDid });
295
+ // delete old db in db proxy
296
+ await manager.teamManager.deleteTeam(oldBlocklet.meta.did);
297
+
298
+ // re create blocklet
299
+ await states.blocklet.remove({ appDid: permanentAppDid });
300
+ await states.blocklet.addBlocklet(blockletData);
301
+ await states.blocklet.updateStructV1Did(permanentAppDid, oldBlocklet.meta.did);
302
+
303
+ // fake install bundle
304
+ const bundleDir = getBundleDir(manager.installDir, blockletData.meta);
305
+ fs.mkdirSync(bundleDir, { recursive: true });
306
+ updateMetaFile(path.join(bundleDir, BLOCKLET_META_FILE), blockletData.meta);
307
+
308
+ if (oldBlocklet.meta.logo) {
309
+ const fileName = oldBlocklet.meta.logo;
310
+ const src = path.join(getBundleDir(manager.installDir, oldBlocklet.meta), fileName);
311
+ const dist = path.join(bundleDir, fileName);
312
+
313
+ await fs.copy(src, dist);
314
+ }
315
+
316
+ // update
317
+ await states.blockletExtras.update({ did: oldBlocklet.meta.did }, extraData);
318
+
319
+ // update environment, generate appDid and appPid
320
+ await manager._updateBlockletEnvironment(permanentAppDid);
321
+
322
+ // rotate to newAppSk
323
+ await manager.config({
324
+ did: permanentAppDid,
325
+ configs: [{ key: 'BLOCKLET_APP_SK', value: newAppSk, secure: true }],
326
+ skipHook: true,
327
+ skipDidDocument: true,
328
+ });
329
+
330
+ // update routing
331
+ await states.site.update(
332
+ { _id: siteData.id },
333
+ { $set: { domain: siteData.domain, domainAliases: siteData.domainAliases, rules: siteData.rules } }
334
+ );
335
+
336
+ newBlocklet = await manager.getBlocklet(permanentAppDid);
337
+
338
+ logger.info('Start migrate application data', { sortedDataMoveList });
339
+
340
+ // move data
341
+ fs.mkdirSync(dataDirDist, { recursive: true });
342
+ for (let i = 0; i < sortedDataMoveList.length; i++) {
343
+ const { src, dist } = sortedDataMoveList[i];
344
+ fs.moveSync(src, dist);
345
+ currentMoveIndex = i;
346
+ }
347
+ } catch (error) {
348
+ logger.error('Migrate application state failed: ', { did, error });
349
+
350
+ await states.blocklet.remove({ _id: backupBlocklet._id });
351
+ await states.blocklet.remove({ 'meta.did': blockletData.meta.did });
352
+ await states.blocklet.insert(backupBlocklet);
353
+ await states.blockletExtras.remove({ _id: backupExtra._id });
354
+ await states.blockletExtras.insert(backupExtra);
355
+ await states.site.remove({ _id: backupSite._id });
356
+ await states.site.insert(backupSite);
357
+
358
+ logger.info('Rollback application state');
359
+
360
+ // rollback data
361
+ fs.ensureDirSync(dataDirSrc);
362
+ for (let i = currentMoveIndex; i >= 0; i--) {
363
+ const { src, dist } = sortedDataMoveList[i];
364
+ fs.moveSync(dist, src);
365
+ }
366
+ fs.removeSync(dataDirDist);
367
+
368
+ logger.info('Rollback application data');
369
+
370
+ throw error;
371
+ }
372
+
373
+ // 通过 event 触发 ensureBlockletRouting
374
+ manager.emit(BlockletEvents.installed, { blocklet: newBlocklet, context });
375
+ };
376
+
377
+ module.exports = { migrateApplicationToStructV2, sortMoveListBySrc };
@@ -0,0 +1,152 @@
1
+ /* eslint-disable no-await-in-loop */
2
+ const cloneDeep = require('lodash/cloneDeep');
3
+ const capitalize = require('lodash/capitalize');
4
+
5
+ const logger = require('@abtnode/logger')('@abtnode/core:upgrade-component');
6
+
7
+ const { BlockletStatus, BlockletEvents } = require('@blocklet/constant');
8
+ const {
9
+ getUpdateMetaList,
10
+ parseComponents,
11
+ checkDuplicateComponents,
12
+ filterDuplicateComponents,
13
+ checkStructVersion,
14
+ checkVersionCompatibility,
15
+ validateBlocklet,
16
+ } = require('../../../util/blocklet');
17
+
18
+ const check = async ({ did, states }) => {
19
+ const blocklet = await states.blocklet.getBlocklet(did);
20
+ checkStructVersion(blocklet);
21
+
22
+ const newBlocklet = cloneDeep(blocklet);
23
+
24
+ const newChildren = [];
25
+
26
+ for (const child of newBlocklet.children || []) {
27
+ if (child.bundleSource) {
28
+ const {
29
+ staticComponents: [newChild],
30
+ dynamicComponents,
31
+ } = await parseComponents({
32
+ meta: {
33
+ staticComponents: [
34
+ {
35
+ source: child.bundleSource,
36
+ name: child.meta.name,
37
+ title: child.meta.title,
38
+ mountPoint: child.mountPoint,
39
+ },
40
+ ],
41
+ },
42
+ });
43
+ newChild._dynamicComponents = dynamicComponents;
44
+ newChildren.push(newChild);
45
+ } else {
46
+ const { dynamicComponents } = await parseComponents(child);
47
+ child._dynamicComponents = dynamicComponents;
48
+ newChildren.push(child);
49
+ }
50
+ }
51
+
52
+ checkDuplicateComponents(newChildren);
53
+
54
+ const updateList = getUpdateMetaList(blocklet, { ...blocklet, children: newChildren });
55
+
56
+ if (!updateList.length) {
57
+ return {};
58
+ }
59
+
60
+ // start session
61
+ const { id: updateId } = await states.session.start({
62
+ did,
63
+ children: newChildren,
64
+ });
65
+
66
+ return {
67
+ updateId,
68
+ updateList,
69
+ };
70
+ };
71
+
72
+ const upgrade = async ({ updateId, selectedComponentDids, context, states, manager }) => {
73
+ if (!selectedComponentDids?.length) {
74
+ throw new Error('At least one component needs to be selected');
75
+ }
76
+
77
+ const sessionData = await states.session.end(updateId);
78
+ const { did } = sessionData;
79
+ const oldBlocklet = await manager._getBlockletForInstallation(did);
80
+ checkStructVersion(oldBlocklet);
81
+
82
+ // parse children
83
+ let dynamicComponents = [];
84
+ const children = cloneDeep(oldBlocklet.children).map((oldComponent) => {
85
+ const newComponent = sessionData.children.find((x) => x.meta.did === oldComponent.meta.did);
86
+ if (newComponent && selectedComponentDids.includes(newComponent.meta.did)) {
87
+ dynamicComponents.push(...(newComponent._dynamicComponents || []));
88
+ delete newComponent._dynamicComponents;
89
+ return newComponent;
90
+ }
91
+ return oldComponent;
92
+ });
93
+ dynamicComponents = filterDuplicateComponents(dynamicComponents, children);
94
+ children.push(...dynamicComponents);
95
+
96
+ // selectedComponentDids
97
+ selectedComponentDids.push(...dynamicComponents.map((x) => x.meta.did));
98
+
99
+ checkVersionCompatibility(children);
100
+
101
+ logger.info('upgrade blocklet children', {
102
+ did,
103
+ children: children.map((x) => ({ name: x.meta.name, version: x.meta.version })),
104
+ });
105
+
106
+ // new blocklet
107
+ const newBlocklet = await states.blocklet.setBlockletStatus(did, BlockletStatus.waiting);
108
+
109
+ newBlocklet.children = children;
110
+ await validateBlocklet(newBlocklet);
111
+
112
+ manager.emit(BlockletEvents.statusChange, newBlocklet);
113
+
114
+ const action = 'upgrade';
115
+
116
+ // backup rollback data
117
+ await manager._rollbackCache.backup({ did, action, oldBlocklet });
118
+
119
+ // add to queue
120
+ const ticket = manager.installQueue.push(
121
+ {
122
+ entity: 'blocklet',
123
+ action: 'download',
124
+ id: did,
125
+ oldBlocklet: { ...oldBlocklet },
126
+ blocklet: { ...newBlocklet },
127
+ selectedComponentDids: selectedComponentDids || [],
128
+ context,
129
+ postAction: action,
130
+ },
131
+ did
132
+ );
133
+
134
+ ticket.on('failed', async (err) => {
135
+ logger.error('queue failed', { entity: 'blocklet', action, did, error: err });
136
+ await manager._rollback(action, did, oldBlocklet);
137
+ manager.emit(`blocklet.${action}.failed`, { did, err });
138
+ manager._createNotification(did, {
139
+ title: `Blocklet ${capitalize(action)} Failed`,
140
+ description: `Blocklet ${did} ${action} failed with error: ${err.message || 'queue exception'}`,
141
+ entityType: 'blocklet',
142
+ entityId: did,
143
+ severity: 'error',
144
+ });
145
+ });
146
+ return newBlocklet;
147
+ };
148
+
149
+ module.exports = {
150
+ check,
151
+ upgrade,
152
+ };