@abtnode/core 1.6.15 → 1.6.19

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.
@@ -1,67 +1,25 @@
1
1
  /* eslint-disable no-await-in-loop */
2
- const childProcess = require('child_process');
3
2
  const fs = require('fs-extra');
4
3
  const path = require('path');
5
4
  const semver = require('semver');
5
+ const runScript = require('@abtnode/util/lib/run-script');
6
6
 
7
7
  const { getMigrationScripts: getScripts } = require('../migrations');
8
8
  const { getSafeEnv } = require('../util');
9
9
  const { name } = require('../../package.json');
10
10
  const logger = require('@abtnode/logger')(`${name}:blocklet:migration`); // eslint-disable-line
11
11
 
12
- const _runScript = ({ appDir, env, migrationScript, progress = false }) => {
13
- const safeEnv = getSafeEnv(env);
14
-
15
- const child = childProcess.exec(`node ${migrationScript}`, {
16
- cwd: appDir,
17
- env: safeEnv,
18
- stdio: 'inherit',
19
- });
20
- let hasUnhandledRejection = false;
21
-
22
- if (progress) {
23
- child.stdout.pipe(process.stdout);
24
- child.stderr.pipe(process.stderr);
25
- }
26
-
27
- return new Promise((resolve, reject) => {
28
- const errorMessages = [];
29
-
30
- child.stderr.on('data', (err) => {
31
- // Check if has unhandledRejection in childProcess
32
- // https://stackoverflow.com/questions/32784649/gracefully-handle-errors-in-child-processes-in-nodejs
33
- if (err.includes('UnhandledPromiseRejectionWarning')) {
34
- hasUnhandledRejection = true;
35
- }
36
- errorMessages.push(err);
37
- });
38
-
39
- child.on('exit', (code) => {
40
- if (errorMessages.length > 0) {
41
- if (code !== 0 || hasUnhandledRejection) {
42
- return reject(new Error(errorMessages.join('\r\n')));
43
- }
44
-
45
- if (!progress) {
46
- errorMessages.forEach((message) => process.stderr.write(message));
47
- }
48
- }
49
-
50
- return resolve();
51
- });
52
- });
53
- };
54
-
55
12
  async function runScripts({
13
+ blocklet,
14
+ appDir,
15
+ env,
16
+ oldVersion,
56
17
  dbDir,
57
18
  backupDir,
58
19
  scriptsDir,
59
20
  printInfo,
60
21
  printSuccess,
61
22
  printError,
62
- appDir,
63
- env,
64
- oldVersion,
65
23
  }) {
66
24
  let scripts = [];
67
25
  try {
@@ -90,7 +48,11 @@ async function runScripts({
90
48
  const { script: scriptPath } = pendingScripts[i];
91
49
  try {
92
50
  printInfo(`Migration script started: ${scriptPath}`);
93
- await _runScript({ appDir, env, migrationScript: path.join(scriptsDir, scriptPath) });
51
+ await runScript(`node ${path.join(scriptsDir, scriptPath)}`, [blocklet.env.appId, 'migration'].join(':'), {
52
+ cwd: appDir,
53
+ env: getSafeEnv(env),
54
+ silent: false,
55
+ });
94
56
  printInfo(`Migration script executed: ${scriptPath}`);
95
57
  } catch (migrationErr) {
96
58
  printError(`Failed to execute migration script: ${scriptPath}, error: ${migrationErr.message}`);
@@ -123,6 +85,7 @@ async function doRestore({ dbDir, backupDir, printInfo, printSuccess }) {
123
85
  }
124
86
 
125
87
  module.exports = async ({
88
+ blocklet,
126
89
  appDir,
127
90
  env,
128
91
  oldVersion,
@@ -142,15 +105,16 @@ module.exports = async ({
142
105
  fs.ensureDirSync(backupDir);
143
106
 
144
107
  await runScripts({
108
+ blocklet,
109
+ appDir,
110
+ env,
111
+ oldVersion,
112
+ newVersion,
145
113
  dbDir,
146
114
  backupDir,
147
115
  scriptsDir,
148
116
  printError,
149
117
  printInfo,
150
118
  printSuccess,
151
- appDir,
152
- env,
153
- oldVersion,
154
- newVersion,
155
119
  });
156
120
  };
package/lib/event.js CHANGED
@@ -182,7 +182,7 @@ module.exports = ({
182
182
  .catch((error) => logger.error('refresh blocklets failed on initialize the registry', { error }));
183
183
 
184
184
  // We need update router on some fields change
185
- const fields = ['enableWelcomePage', 'webWalletUrl'];
185
+ const fields = ['enableWelcomePage', 'webWalletUrl', 'registerUrl'];
186
186
  const shouldUpdateRouter = fields.some((x) => nodeInfo[x] !== oldInfo[x]);
187
187
  if (shouldUpdateRouter) {
188
188
  handleRouting(nodeInfo).catch((err) => {
package/lib/index.js CHANGED
@@ -50,6 +50,7 @@ const { toStatus, fromStatus, ensureDataDirs, getQueueConcurrencyByMem } = requi
50
50
  * version
51
51
  * runtimeConfig: {}
52
52
  * autoUpgrade
53
+ * registerUrl
53
54
  * webWalletUrl
54
55
  */
55
56
  function ABTNode(options) {
@@ -196,16 +197,17 @@ function ABTNode(options) {
196
197
  // Blocklet manager
197
198
  installBlocklet: blockletManager.install.bind(blockletManager),
198
199
  installBlockletFromVc: blockletManager.installBlockletFromVc.bind(blockletManager),
200
+ installComponent: blockletManager.installComponent.bind(blockletManager),
199
201
  startBlocklet: blockletManager.start.bind(blockletManager),
200
202
  stopBlocklet: blockletManager.stop.bind(blockletManager),
201
203
  reloadBlocklet: blockletManager.reload.bind(blockletManager),
202
204
  restartBlocklet: blockletManager.restart.bind(blockletManager),
203
205
  deleteBlocklet: blockletManager.delete.bind(blockletManager),
206
+ deleteComponent: blockletManager.deleteComponent.bind(blockletManager),
204
207
  cancelDownloadBlocklet: blockletManager.cancelDownload.bind(blockletManager),
205
208
  upgradeBlocklet: blockletManager.upgrade.bind(blockletManager),
206
209
  configBlocklet: blockletManager.config.bind(blockletManager),
207
210
  devBlocklet: blockletManager.dev.bind(blockletManager),
208
- getBlockletStatus: blockletManager.getStatus.bind(blockletManager),
209
211
  checkChildBlockletsForUpdates: blockletManager.checkChildrenForUpdates.bind(blockletManager),
210
212
  updateChildBlocklets: blockletManager.updateChildren.bind(blockletManager),
211
213
  getLatestBlockletVersion: blockletManager.getLatestBlockletVersion.bind(blockletManager),
@@ -0,0 +1,48 @@
1
+ /* eslint-disable no-continue */
2
+ /* eslint-disable no-await-in-loop */
3
+ /* eslint-disable no-underscore-dangle */
4
+
5
+ module.exports = async ({ states, printInfo }) => {
6
+ printInfo('Try to update blocklet to 1.6.17...');
7
+ const blockletState = states.blocklet;
8
+
9
+ const blocklets = await blockletState.getBlocklets();
10
+ for (const blocklet of blocklets) {
11
+ if (!blocklet) {
12
+ continue;
13
+ }
14
+
15
+ if (!blocklet.children || !blocklet.children.length) {
16
+ continue;
17
+ }
18
+
19
+ const meta = blocklet.meta || {};
20
+ const { did } = meta;
21
+ if (!did) {
22
+ continue;
23
+ }
24
+
25
+ const children = (blocklet.children || []).map((child) => {
26
+ if (child.mountPoint) {
27
+ return child;
28
+ }
29
+
30
+ const config = (meta.children || []).find((x) => x.name === child.meta.name);
31
+
32
+ if (
33
+ config &&
34
+ config.mountPoints &&
35
+ config.mountPoints[0] &&
36
+ config.mountPoints[0].root &&
37
+ config.mountPoints[0].root.prefix
38
+ ) {
39
+ child.mountPoint = config.mountPoints[0].root.prefix;
40
+ }
41
+ printInfo(`Set mountPoint: ${child.mountPoint} to child ${child.meta.name} in ${blocklet.meta.name}`);
42
+
43
+ return child;
44
+ });
45
+
46
+ await blockletState.update(blocklet._id, { $set: { children } });
47
+ }
48
+ };
@@ -244,6 +244,9 @@ const ensureLatestNodeInfo = async (sites = [], { withDefaultCors = true } = {})
244
244
  site.domain = DOMAIN_FOR_IP_SITE_REGEXP;
245
245
 
246
246
  if (withDefaultCors) {
247
+ // Allow CORS from "Install on ABT Node"
248
+ addCorsToSite(site, info.registerUrl);
249
+
247
250
  // Allow CORS from "Web Wallet"
248
251
  addCorsToSite(site, info.webWalletUrl);
249
252
  }
@@ -314,7 +317,7 @@ const ensureWellknownRule = async (sites) => {
314
317
  rules.forEach((rule) => {
315
318
  if (
316
319
  rule.to.type !== ROUTING_RULE_TYPES.BLOCKLET || // is not blocklet
317
- rule.to.did !== rule.to.realDid // is a component endpoint
320
+ (rule.to.did !== rule.to.realDid && rule.from.pathPrefix !== '/') // is a component endpoint in sub path
318
321
  ) {
319
322
  return;
320
323
  }
@@ -434,7 +437,8 @@ module.exports = function getRouterHelpers({ dataDirs, routingSnapshot, routerMa
434
437
 
435
438
  const https = get(info, 'routing.https', true);
436
439
  const dashboardDomain = get(info, 'routing.dashboardDomain', '');
437
- const certDownloadAddress = get(info, 'routing.dashboardCertDownloadAddress', '');
440
+ const certDownloadAddress =
441
+ process.env.ABT_NODE_DASHBOARD_CERT_DOWN_URL || get(info, 'routing.dashboardCertDownloadAddress', '');
438
442
  if (!https || !dashboardDomain || !certDownloadAddress) {
439
443
  return;
440
444
  }
@@ -508,9 +512,9 @@ module.exports = function getRouterHelpers({ dataDirs, routingSnapshot, routerMa
508
512
  return { status: 'existed' };
509
513
  }
510
514
 
511
- logger.debug('downloading certificate', { url: downloadUrl, dashboardDomain });
515
+ logger.debug('downloading dashboard ip-domain certificate', { url: downloadUrl, dashboardDomain });
512
516
  await updateDashboardCertificate({ checkExpire: false });
513
- logger.debug('downloading certificate', { url: downloadUrl, dashboardDomain });
517
+ logger.debug('downloaded dashboard ip-domain certificate', { url: downloadUrl, dashboardDomain });
514
518
  return { status: 'downloaded' };
515
519
  };
516
520
 
@@ -656,7 +660,9 @@ module.exports = function getRouterHelpers({ dataDirs, routingSnapshot, routerMa
656
660
 
657
661
  const dashboardDomain = get(info, 'routing.dashboardDomain', '');
658
662
  const didDomain = `${info.did.toLowerCase()}.${info.didDomain}`;
659
- const dashboardAliasDomains = [dashboardDomain, didDomain]
663
+ let dashboardAliasDomains = [dashboardDomain, didDomain];
664
+
665
+ dashboardAliasDomains = dashboardAliasDomains
660
666
  .filter((item) => item && !isExistsInAlias(item))
661
667
  .map((item) => ({ value: item, isProtected: true }));
662
668
 
@@ -742,18 +748,18 @@ module.exports = function getRouterHelpers({ dataDirs, routingSnapshot, routerMa
742
748
 
743
749
  const existSite = await states.site.findOne({ domain: domainGroup });
744
750
  if (!existSite) {
745
- const ipEchoDnsDomain = getIpDnsDomainForBlocklet(blocklet, webInterface, nodeInfo.did);
746
- const appIdEnv = blocklet.environments.find((e) => e.key === 'BLOCKLET_APP_ID');
747
- const domainAliases = [{ value: ipEchoDnsDomain, isProtected: true }];
751
+ const domainAliases = [];
748
752
 
749
753
  const didDomain = getDidDomainForBlocklet({
750
- appId: appIdEnv.value,
754
+ name: blocklet.meta.name,
755
+ daemonDid: nodeInfo.did,
751
756
  didDomain: nodeInfo.didDomain,
752
757
  });
753
758
 
754
- if (blocklet.mode !== 'development') {
755
- domainAliases.push({ value: didDomain, isProtected: true });
756
- }
759
+ const ipEchoDnsDomain = getIpDnsDomainForBlocklet(blocklet, webInterface, nodeInfo.did);
760
+
761
+ // let didDomain in front of ipEchoDnsDomain
762
+ domainAliases.push({ value: didDomain, isProtected: true }, { value: ipEchoDnsDomain, isProtected: true });
757
763
 
758
764
  await routerManager.addRoutingSite(
759
765
  {
@@ -27,6 +27,7 @@ const {
27
27
  BLOCKLET_BUNDLE_FOLDER,
28
28
  BLOCKLET_DYNAMIC_PATH_PREFIX,
29
29
  BLOCKLET_INTERFACE_TYPE_WEB,
30
+ BlockletGroup,
30
31
  } = require('@blocklet/meta/lib/constants');
31
32
 
32
33
  const {
@@ -37,6 +38,7 @@ const {
37
38
  validateUpdateSite,
38
39
  } = require('../validators/router');
39
40
  const { getProviderFromNodeInfo, findInterfaceByName, isCLI, findInterfacePortByName } = require('../util');
41
+ const { findWebInterface } = require('../util/blocklet');
40
42
  const { attachInterfaceUrls, ensureLatestInfo } = require('./helper');
41
43
  const Router = require('./index');
42
44
  const states = require('../states');
@@ -219,10 +221,10 @@ class RouterManager extends EventEmitter {
219
221
  }
220
222
  }
221
223
 
222
- const updateResult = await states.site.update(
223
- { _id: id },
224
- { $push: { domainAliases: { value: domainAlias, isProtected: false } } }
225
- );
224
+ // let custom domain in front of protected domain
225
+ const domainAliases = [{ value: domainAlias, isProtected: false }, ...dbSite.domainAliases];
226
+
227
+ const updateResult = await states.site.update({ _id: id }, { $set: { domainAliases } });
226
228
  logger.debug('add domain alias update result', { id, updateResult, domainAlias });
227
229
 
228
230
  const newSite = await states.site.findOne({ _id: id });
@@ -679,49 +681,51 @@ class RouterManager extends EventEmitter {
679
681
 
680
682
  // get child rules
681
683
  const blocklet = await states.blocklet.getBlocklet(rule.to.did);
682
- for (const childMeta of blocklet.meta.children || []) {
683
- for (const mountPoint of childMeta.mountPoints) {
684
- const child = blocklet.children.find((b) => b.meta.name === childMeta.name);
685
-
686
- if (!child) {
687
- logger.error(`Child blocklet ${childMeta.name} does not exist`);
688
- // eslint-disable-next-line no-continue
689
- continue;
690
- }
684
+ for (const child of blocklet.children || []) {
685
+ const { mountPoint } = child;
686
+ if (!mountPoint) {
687
+ logger.error(`mountPoint of child ${child.meta.name} does not exist`);
688
+ // eslint-disable-next-line no-continue
689
+ continue;
690
+ }
691
691
 
692
- if (mountPoint.root.interfaceName === rule.to.interfaceName) {
693
- const pathPrefix = path.join(rule.from.pathPrefix, mountPoint.root.prefix);
694
- const isRootPath = pathPrefix === rule.from.pathPrefix;
695
- if (isRootPath) {
696
- occupied = true;
697
- }
692
+ const childWebInterface = findWebInterface(child);
693
+ if (!childWebInterface) {
694
+ logger.error(`web interface of child ${child.meta.name} does not exist`);
695
+ // eslint-disable-next-line no-continue
696
+ continue;
697
+ }
698
698
 
699
- // if is root path, child rule become root rule
700
- const childRule = {
701
- id: isRootPath ? rule.id : uuid.v4(),
702
- groupId: rule.id,
703
- from: {
704
- pathPrefix: normalizePathPrefix(pathPrefix),
705
- groupPathPrefix: rule.from.pathPrefix,
706
- },
707
- to: {
708
- type: ROUTING_RULE_TYPES.BLOCKLET,
709
- port: findInterfacePortByName(child, mountPoint.child.interfaceName),
710
- did: rule.to.did, // root blocklet did
711
- interfaceName: rule.to.interfaceName, // root blocklet interface
712
- realDid: child.meta.did, // child blocklet did
713
- realInterfaceName: mountPoint.child.interfaceName,
714
- },
715
- isProtected: isRootPath ? rule.isProtected : true,
716
- };
717
-
718
- rules.push(childRule);
719
- }
699
+ const pathPrefix = path.join(rule.from.pathPrefix, mountPoint);
700
+ const isRootPath = pathPrefix === rule.from.pathPrefix;
701
+ if (isRootPath) {
702
+ occupied = true;
720
703
  }
704
+
705
+ // if is root path, child rule become root rule
706
+ const childRule = {
707
+ id: isRootPath ? rule.id : uuid.v4(),
708
+ groupId: rule.id,
709
+ from: {
710
+ pathPrefix: normalizePathPrefix(pathPrefix),
711
+ groupPathPrefix: rule.from.pathPrefix,
712
+ },
713
+ to: {
714
+ type: ROUTING_RULE_TYPES.BLOCKLET,
715
+ port: findInterfacePortByName(child, childWebInterface.name),
716
+ did: rule.to.did, // root blocklet did
717
+ interfaceName: rule.to.interfaceName, // root blocklet interface
718
+ realDid: child.meta.did, // child blocklet did
719
+ realInterfaceName: childWebInterface.name,
720
+ },
721
+ isProtected: isRootPath ? rule.isProtected : true,
722
+ };
723
+
724
+ rules.push(childRule);
721
725
  }
722
726
 
723
727
  // get root rule
724
- if (!occupied) {
728
+ if (!occupied && blocklet.meta.group !== BlockletGroup.gateway) {
725
729
  rules.push(rule);
726
730
  }
727
731
 
@@ -3,6 +3,7 @@
3
3
  /* eslint-disable consistent-return */
4
4
  const logger = require('@abtnode/logger')('state-blocklet-extras');
5
5
  const camelCase = require('lodash/camelCase');
6
+ const get = require('lodash/get');
6
7
 
7
8
  const BaseState = require('./base');
8
9
 
@@ -36,16 +37,28 @@ class BlockletExtrasState extends BaseState {
36
37
  },
37
38
  },
38
39
  ];
40
+
41
+ this.childExtras = this.extras.filter((x) => ['configs'].includes(x.name));
42
+
39
43
  this.generateExtraFns();
40
44
  }
41
45
 
46
+ delete(did) {
47
+ return this.remove({ did });
48
+ }
49
+
42
50
  generateExtraFns() {
43
- const methods = ['get', 'set', 'del'];
51
+ const methods = ['get', 'set', 'del', 'list'];
44
52
  methods.forEach((method) => {
45
53
  this.extras.forEach((extra) => {
46
54
  const fn = camelCase(`${method} ${extra.name}`); // getConfigs, getRules
47
55
  this[fn] = this.generateExtraFn(method, extra);
56
+ });
57
+ });
48
58
 
59
+ const childMethods = ['get', 'set', 'del'];
60
+ childMethods.forEach((method) => {
61
+ this.childExtras.forEach((extra) => {
49
62
  const childFn = camelCase(`${method} child ${extra.name}`); // getChildConfigs, getChildRules
50
63
  this[childFn] = this.generateExtraChildFn(method, extra);
51
64
  });
@@ -66,14 +79,22 @@ class BlockletExtrasState extends BaseState {
66
79
  if (method === 'del') {
67
80
  return this.generateDelFn(extra);
68
81
  }
82
+
83
+ if (method === 'list') {
84
+ return this.generateListFn(extra);
85
+ }
69
86
  }
70
87
 
71
88
  generateGetFn(extra) {
72
- return async (did) => {
89
+ return async (did, path, defaultValue) => {
73
90
  const { dek } = this.options;
74
91
  const { name, afterGet = noop('data') } = extra;
75
92
  const item = await this.asyncDB.findOne({ did });
76
- return afterGet({ data: item ? item[name] : item, did, dek });
93
+ const data = afterGet({ data: item ? item[name] : item, did, dek });
94
+ if (!path) {
95
+ return data;
96
+ }
97
+ return get(data, path, defaultValue);
77
98
  };
78
99
  }
79
100
 
@@ -118,6 +139,21 @@ class BlockletExtrasState extends BaseState {
118
139
  };
119
140
  }
120
141
 
142
+ generateListFn(extra) {
143
+ return async () => {
144
+ const { dek } = this.options;
145
+ const { name, afterGet = noop('data') } = extra;
146
+ const docs = await this.asyncDB.find({});
147
+ const list = docs
148
+ .filter((x) => x[name])
149
+ .map((x) => ({
150
+ did: x.did,
151
+ [name]: afterGet({ data: x[name], did: x.did, dek }),
152
+ }));
153
+ return list;
154
+ };
155
+ }
156
+
121
157
  // generate extra child functions
122
158
 
123
159
  generateExtraChildFn(method, extra) {
@@ -4,6 +4,7 @@
4
4
  /* eslint-disable no-underscore-dangle */
5
5
  const omit = require('lodash/omit');
6
6
  const uniq = require('lodash/uniq');
7
+ const cloneDeep = require('lodash/cloneDeep');
7
8
  const detectPort = require('detect-port');
8
9
  const Lock = require('@abtnode/util/lib/lock');
9
10
  const security = require('@abtnode/util/lib/security');
@@ -19,7 +20,7 @@ const {
19
20
  const logger = require('@abtnode/logger')('state-blocklet');
20
21
 
21
22
  const BaseState = require('./base');
22
- const { forEachBlocklet } = require('../util/blocklet');
23
+ const { forEachBlocklet, checkDuplicateComponents } = require('../util/blocklet');
23
24
  const { validateBlockletMeta } = require('../util');
24
25
 
25
26
  const lock = new Lock('blocklet-port-assign-lock');
@@ -119,7 +120,7 @@ class BlockletState extends BaseState {
119
120
  }
120
121
 
121
122
  this.emit('remove', doc);
122
- return resolve(doc);
123
+ return resolve(formatBlocklet(doc, 'onRead', this.options.dek));
123
124
  });
124
125
  })
125
126
  );
@@ -130,9 +131,9 @@ class BlockletState extends BaseState {
130
131
  meta,
131
132
  source = BlockletSource.registry,
132
133
  status = BlockletStatus.added,
133
- deployedFrom,
134
+ deployedFrom = '',
134
135
  mode = BLOCKLET_MODES.PRODUCTION,
135
- childrenMeta = [],
136
+ children: rawChildren = [],
136
137
  } = {}) {
137
138
  return this.getBlocklet(did).then(
138
139
  (doc) =>
@@ -152,7 +153,7 @@ class BlockletState extends BaseState {
152
153
 
153
154
  const ports = await this.getBlockletPorts({ interfaces: sanitized.interfaces || [] });
154
155
 
155
- const children = await this.fillChildrenPorts(this.getChildrenFromMetas(childrenMeta), {
156
+ const children = await this.fillChildrenPorts(rawChildren, {
156
157
  defaultPort: getMaxPort(ports),
157
158
  });
158
159
 
@@ -196,7 +197,7 @@ class BlockletState extends BaseState {
196
197
  }
197
198
 
198
199
  try {
199
- const formatted = formatBlocklet(updates, 'onUpdate', this.options.dek);
200
+ const formatted = formatBlocklet(cloneDeep(updates), 'onUpdate', this.options.dek);
200
201
  const newDoc = await this.updateById(doc._id, { $set: formatted });
201
202
  resolve(newDoc);
202
203
  } catch (err) {
@@ -206,7 +207,7 @@ class BlockletState extends BaseState {
206
207
  );
207
208
  }
208
209
 
209
- upgradeBlocklet({ meta, source, deployedFrom, children } = {}) {
210
+ upgradeBlocklet({ meta, source, deployedFrom = '', children } = {}) {
210
211
  return this.getBlocklet(meta.did).then(
211
212
  (doc) =>
212
213
  // eslint-disable-next-line no-async-promise-executor
@@ -247,8 +248,10 @@ class BlockletState extends BaseState {
247
248
  },
248
249
  });
249
250
  lock.release();
250
- this.emit('upgrade', newDoc);
251
- resolve(newDoc);
251
+
252
+ const formatted = formatBlocklet(newDoc, 'onRead', this.options.dek);
253
+ this.emit('upgrade', formatted);
254
+ resolve(formatted);
252
255
  } catch (err) {
253
256
  lock.release();
254
257
  reject(err);
@@ -388,15 +391,23 @@ class BlockletState extends BaseState {
388
391
  return result;
389
392
  }
390
393
 
391
- async setBlockletStatus(did, status) {
394
+ /**
395
+ * @param {String} did blocklet did
396
+ * @param {BlockletStatus} status blocklet status
397
+ *
398
+ * children status only different with parent before blocklet installation
399
+ * @param {Array<{did}>} children
400
+ */
401
+ async setBlockletStatus(did, status, { children } = {}) {
392
402
  if (typeof status === 'undefined') {
393
403
  throw new Error('Unsupported blocklet status');
394
404
  }
395
- const record = await this.getBlocklet(did);
396
- // when status change update record
397
- if (record.status === status) {
398
- return record;
405
+
406
+ const doc = await this.getBlocklet(did);
407
+ if (doc.status === status && !children) {
408
+ return formatBlocklet(doc, 'onRead', this.options.dek);
399
409
  }
410
+
400
411
  const updates = { status, startedAt: undefined, stoppedAt: undefined };
401
412
  if (status === BlockletStatus.running) {
402
413
  updates.startedAt = new Date();
@@ -408,14 +419,30 @@ class BlockletState extends BaseState {
408
419
  updates.stoppedAt = new Date();
409
420
  }
410
421
 
411
- const res = await this.update(record._id, {
412
- $set: updates,
422
+ // update children status
423
+ updates.children = doc.children.map((child) => {
424
+ if (children === 'all') {
425
+ child.status = status;
426
+ return child;
427
+ }
428
+
429
+ if (!children) {
430
+ if (![BlockletStatus.waiting, BlockletStatus.upgrading, BlockletStatus.installing].includes(status)) {
431
+ child.status = status;
432
+ }
433
+
434
+ return child;
435
+ }
436
+
437
+ const inputChild = children.find((x) => x.did === child.meta.did);
438
+ if (inputChild) {
439
+ child.status = status;
440
+ }
441
+ return child;
413
442
  });
414
- return res;
415
- }
416
443
 
417
- getChildrenFromMetas(childrenMeta) {
418
- return childrenMeta.map((x) => ({ meta: x }));
444
+ await this.update(doc._id, { $set: updates });
445
+ return formatBlocklet({ ...doc, ...updates }, 'onRead', this.options.dek);
419
446
  }
420
447
 
421
448
  async fillChildrenPorts(children, { defaultPort = 0, oldChildren } = {}) {
@@ -465,6 +492,48 @@ class BlockletState extends BaseState {
465
492
 
466
493
  return children;
467
494
  }
495
+
496
+ async addChildren(did, children) {
497
+ const parent = await this.getBlocklet(did);
498
+ if (!parent) {
499
+ throw new Error('Blocklet does not exist');
500
+ }
501
+
502
+ const oldChildren = parent.children || [];
503
+
504
+ const newChildren = [...oldChildren];
505
+ for (const child of children) {
506
+ const { meta, mountPoint, sourceUrl = '', source = '', deployedFrom = '' } = child;
507
+
508
+ if (!mountPoint) {
509
+ throw new Error(`mountPoint is required when adding component ${meta.title || meta.name}`);
510
+ }
511
+
512
+ if (meta.did === parent.meta.did) {
513
+ throw new Error('Cannot add self as a component');
514
+ }
515
+
516
+ checkDuplicateComponents([child], newChildren);
517
+
518
+ newChildren.push({
519
+ mountPoint,
520
+ meta,
521
+ sourceUrl,
522
+ source,
523
+ deployedFrom,
524
+ dynamic: true,
525
+ status: BlockletStatus.added,
526
+ });
527
+ }
528
+
529
+ // use upgradeBlocklet to assign ports to children and write new data to db
530
+ return this.upgradeBlocklet({
531
+ meta: parent.meta,
532
+ source: parent.source,
533
+ deployedFrom: parent.deployedFrom,
534
+ children: newChildren,
535
+ });
536
+ }
468
537
  }
469
538
 
470
539
  BlockletState.BlockletStatus = BlockletStatus;