@abtnode/core 1.16.25-beta-023ec101 → 1.16.25-beta-bc165d9b

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.
@@ -78,7 +78,7 @@ const resolveDownload = async (tarFile, dist, { removeTarFile = true, logger = d
78
78
 
79
79
  await fs.move(downloadDir, installDir, { overwrite: true });
80
80
 
81
- installExternalDependencies(installDir);
81
+ await installExternalDependencies({ appDir: installDir, forceInstall: true });
82
82
  } catch (error) {
83
83
  fs.removeSync(downloadDir);
84
84
  fs.removeSync(tmp);
@@ -145,7 +145,7 @@ const resolveDiffDownload = async (
145
145
  logger.info('Move downloadDir to installDir', { downloadDir, bundleDir });
146
146
  await fs.move(downloadDir, bundleDir, { overwrite: true });
147
147
 
148
- installExternalDependencies(bundleDir);
148
+ await installExternalDependencies({ appDir: bundleDir, forceInstall: true });
149
149
 
150
150
  return { meta, installDir: bundleDir };
151
151
  } catch (error) {
@@ -10,7 +10,6 @@ const merge = require('lodash/merge');
10
10
  const pick = require('lodash/pick');
11
11
  const isEmpty = require('lodash/isEmpty');
12
12
  const cloneDeep = require('lodash/cloneDeep');
13
- const { isNFTExpired, getNftExpirationDate } = require('@abtnode/util/lib/nft');
14
13
  const { sign } = require('@arcblock/jwt');
15
14
  const sleep = require('@abtnode/util/lib/sleep');
16
15
  const getBlockletInfo = require('@blocklet/meta/lib/info');
@@ -103,7 +102,7 @@ const formatContext = require('@abtnode/util/lib/format-context');
103
102
  const md5 = require('@abtnode/util/lib/md5');
104
103
  const { callFederated } = require('@abtnode/auth/lib/util/federated');
105
104
  const pAll = require('p-all');
106
- const { consumeServerlessNFT, consumeLauncherSession } = require('../../util/launcher');
105
+ const launcher = require('../../util/launcher');
107
106
  const util = require('../../util');
108
107
  const {
109
108
  refresh: refreshAccessibleExternalNodeIp,
@@ -141,7 +140,6 @@ const {
141
140
  getProcessState,
142
141
  getBlockletStatus,
143
142
  shouldSkipComponent,
144
- exceedRedemptionPeriod,
145
143
  ensureAppPortsNotOccupied,
146
144
  getComponentNamesWithVersion,
147
145
  updateDidDocument: updateBlockletDocument,
@@ -265,6 +263,7 @@ class DiskBlockletManager extends BaseBlockletManager {
265
263
  backupQueue,
266
264
  restoreQueue,
267
265
  checkUpdateQueue,
266
+ reportComponentUsageQueue,
268
267
  daemon = false,
269
268
  teamManager,
270
269
  }) {
@@ -280,6 +279,7 @@ class DiskBlockletManager extends BaseBlockletManager {
280
279
  this.backupQueue = backupQueue;
281
280
  this.restoreQueue = restoreQueue;
282
281
  this.checkUpdateQueue = checkUpdateQueue;
282
+ this.reportComponentUsageQueue = reportComponentUsageQueue;
283
283
  this.teamManager = teamManager;
284
284
 
285
285
  if (isFunction(this.backupQueue.on)) {
@@ -1197,6 +1197,27 @@ class DiskBlockletManager extends BaseBlockletManager {
1197
1197
 
1198
1198
  const newBlocklet = await this.getBlocklet(rootDid);
1199
1199
 
1200
+ if (newBlocklet.controller) {
1201
+ const componentDids = [did];
1202
+ const eventType = 'uninstall';
1203
+
1204
+ this.reportComponentUsageQueue.push({
1205
+ entity: 'blocklet',
1206
+ action: 'reportComponentUsage',
1207
+ did: newBlocklet.meta.did,
1208
+ time: new Date().toISOString(),
1209
+ componentDids,
1210
+ eventType,
1211
+ context,
1212
+ });
1213
+
1214
+ logger.info('pushed reporting uninstall components event job to queue', {
1215
+ did: newBlocklet.meta.did,
1216
+ componentDids,
1217
+ eventType,
1218
+ });
1219
+ }
1220
+
1200
1221
  await this._updateDependents(rootDid);
1201
1222
 
1202
1223
  // support edge case
@@ -2026,6 +2047,10 @@ class DiskBlockletManager extends BaseBlockletManager {
2026
2047
  if (job.action === 'autoCheckUpdate') {
2027
2048
  await this._onCheckForComponentUpdate(job);
2028
2049
  }
2050
+
2051
+ if (job.action === 'reportComponentUsage') {
2052
+ await this._reportComponentUsage(job);
2053
+ }
2029
2054
  }
2030
2055
  }
2031
2056
 
@@ -2070,7 +2095,7 @@ class DiskBlockletManager extends BaseBlockletManager {
2070
2095
  },
2071
2096
  {
2072
2097
  name: 'clean-expired-blocklet-data',
2073
- time: '0 */20 0 * * *', // 每天凌晨 0 点的每 20 分钟
2098
+ time: '0 10 0 * * *', // 每天凌晨 0:10
2074
2099
  options: { runOnInit: false },
2075
2100
  fn: () => this.cleanExpiredBlocklets(),
2076
2101
  },
@@ -2527,6 +2552,23 @@ class DiskBlockletManager extends BaseBlockletManager {
2527
2552
  context,
2528
2553
  action: postAction,
2529
2554
  });
2555
+
2556
+ const newBlocklet = await this.getBlocklet(did);
2557
+ if (newBlocklet.controller) {
2558
+ const eventType = 'install';
2559
+
2560
+ this.reportComponentUsageQueue.push({
2561
+ entity: 'blocklet',
2562
+ action: 'reportComponentUsage',
2563
+ time: new Date().toISOString(),
2564
+ did,
2565
+ componentDids,
2566
+ eventType: 'install',
2567
+ context,
2568
+ });
2569
+
2570
+ logger.info('pushed reporting install components event job to queue', { did, componentDids, eventType });
2571
+ }
2530
2572
  } catch (err) {
2531
2573
  logger.error('blocklet onUpgrade error', { error: err });
2532
2574
  }
@@ -2926,6 +2968,25 @@ class DiskBlockletManager extends BaseBlockletManager {
2926
2968
  }
2927
2969
  }
2928
2970
 
2971
+ async _reportComponentUsage({ did, componentDids, eventType, time }) {
2972
+ try {
2973
+ logger.info('start report component usage', { did, componentDids, eventType });
2974
+
2975
+ const blocklet = await this.getBlocklet(did);
2976
+ await launcher.reportComponentsEvent({
2977
+ blocklet,
2978
+ dids: componentDids,
2979
+ type: eventType,
2980
+ time: time || new Date().toISOString(),
2981
+ });
2982
+
2983
+ logger.info('report component usage success', { did, componentDids, eventType });
2984
+ } catch (error) {
2985
+ logger.error('report component usage failed', { did, componentDids, eventType, error });
2986
+ throw error; // 一定要 throw 出去,否则会导致队列任务不会重试
2987
+ }
2988
+ }
2989
+
2929
2990
  /**
2930
2991
  * @description send notification to multiple users, and send different notifications according to the user's own language
2931
2992
  * @param {{
@@ -3346,10 +3407,10 @@ class DiskBlockletManager extends BaseBlockletManager {
3346
3407
  if (blocklet.controller && process.env.NODE_ENV !== 'test') {
3347
3408
  let isNFTConsumed = false;
3348
3409
  if (blocklet.controller.launcherSessionId && blocklet.controller.launcherUrl) {
3349
- await consumeLauncherSession({ params: blocklet.controller, blocklet });
3410
+ await launcher.consumeLauncherSession({ params: blocklet.controller, blocklet });
3350
3411
  isNFTConsumed = true;
3351
3412
  } else if (blocklet.controller.nftId) {
3352
- await consumeServerlessNFT({ nftId: blocklet.controller.nftId, blocklet });
3413
+ await launcher.consumeServerlessNFT({ nftId: blocklet.controller.nftId, blocklet });
3353
3414
  isNFTConsumed = true;
3354
3415
  }
3355
3416
 
@@ -3921,8 +3982,8 @@ class DiskBlockletManager extends BaseBlockletManager {
3921
3982
  continue;
3922
3983
  }
3923
3984
 
3924
- const assetState = await util.getNFTState(data.controller.chainHost, data.controller.nftId);
3925
- const isExpired = isNFTExpired(assetState);
3985
+ const isExpired = await launcher.isBlockletExpired(data.did, data.controller);
3986
+
3926
3987
  if (isExpired === false) {
3927
3988
  logger.info('blocklet is renewed', { did: data.did });
3928
3989
  await states.blockletExtras.updateByDid(data.did, {
@@ -3956,24 +4017,21 @@ class DiskBlockletManager extends BaseBlockletManager {
3956
4017
  );
3957
4018
 
3958
4019
  if (blockletExtras.length === 0) {
3959
- logger.info('no expired blocklet');
4020
+ logger.info('no serverless blocklet');
3960
4021
  return;
3961
4022
  }
3962
4023
 
3963
- logger.info('expired blocklet count', { count: blockletExtras.length });
4024
+ logger.info('serverless blocklet count', { count: blockletExtras.length });
3964
4025
 
3965
4026
  for (const data of blockletExtras) {
3966
4027
  try {
3967
4028
  const { did } = data;
3968
- const assetState = await util.getNFTState(data.controller.chainHost, data.controller.nftId);
3969
- const isExpired = isNFTExpired(assetState);
4029
+ const isExpired = await launcher.isBlockletExpired(did, data.controller);
3970
4030
 
3971
4031
  if (isExpired) {
3972
- const expiredAt = getNftExpirationDate(assetState);
3973
4032
  logger.info('the blocklet already expired and will be stopped', {
3974
4033
  blockletDid: did,
3975
4034
  nftId: data.controller.nftId,
3976
- expiredAt,
3977
4035
  });
3978
4036
 
3979
4037
  // 如果 Blocklet 没停止, 先停止
@@ -4029,22 +4087,43 @@ class DiskBlockletManager extends BaseBlockletManager {
4029
4087
  for (const data of blockletExtras) {
4030
4088
  try {
4031
4089
  const { did } = data;
4032
- const assetState = await util.getNFTState(data.controller.chainHost, data.controller.nftId);
4033
- const expiredAt = getNftExpirationDate(assetState);
4090
+ const launcherSession = await launcher.getLauncherSession({
4091
+ launcherUrl: data.controller.launcherUrl,
4092
+ launcherSessionId: data.controller.launcherSessionId,
4093
+ });
4094
+ const isTerminated = launcher.isLaunchSessionTerminated(launcherSession);
4095
+
4096
+ if (!isTerminated) {
4097
+ logger.info('skip cleaning the non-exceed redemption blocklet', {
4098
+ blockletDid: did,
4099
+ controller: data.controller,
4100
+ launcherSession,
4101
+ });
4102
+ continue;
4103
+ }
4104
+
4105
+ if (!launcherSession.terminatedAt) {
4106
+ logger.error('the blocklet launch session does not have terminatedAt, skip', {
4107
+ blockletDid: did,
4108
+ controller: data.controller,
4109
+ launcherSession,
4110
+ });
4111
+ continue;
4112
+ }
4034
4113
 
4035
- if (!exceedRedemptionPeriod(expiredAt)) {
4036
- logger.error('skip cleaning the non-exceed redemption blocklet', {
4114
+ // 订阅终止后需要再保留一段时间数据
4115
+ if (!launcher.isDataRetentionExceeded(launcherSession.terminatedAt)) {
4116
+ logger.info('skip cleaning the non-exceed redemption blocklet', {
4037
4117
  blockletDid: did,
4038
- expiredAt,
4118
+ controller: data.controller,
4119
+ launcherSession,
4039
4120
  });
4040
- // eslint-disable-next-line no-continue
4041
4121
  continue;
4042
4122
  }
4043
4123
 
4044
4124
  logger.info('the blocklet already exceed redemption and will be deleted', {
4045
4125
  blockletDid: did,
4046
4126
  nftId: data.controller.nftId,
4047
- expiredAt,
4048
4127
  });
4049
4128
 
4050
4129
  // TODO: 如果绑定了 DID Space 备份到 DID Space
@@ -4275,9 +4354,9 @@ class DiskBlockletManager extends BaseBlockletManager {
4275
4354
  return;
4276
4355
  }
4277
4356
 
4278
- const assetState = await util.getNFTState(blocklet.controller?.chainHost, blocklet.controller?.nftId);
4357
+ const isExpired = await launcher.isBlockletExpired(blocklet.meta.did, blocklet.controller);
4279
4358
 
4280
- if (isNFTExpired(assetState)) {
4359
+ if (isExpired) {
4281
4360
  logger.error(`try to ${action} an expired blocklet`, {
4282
4361
  did: blocklet.meta.did,
4283
4362
  nftId: blocklet.controller?.nftId,
@@ -53,16 +53,6 @@ const installApplicationFromGeneral = async ({
53
53
  throw new Error(`Should not be here: unknown type ${type}`);
54
54
  }
55
55
 
56
- // check install count
57
- if (controller?.nftId) {
58
- const installedCount = await states.blockletExtras.count({ 'controller.nftId': controller.nftId });
59
- if (installedCount >= (controller.appMaxCount || 1)) {
60
- throw new Error(
61
- `You can only install ${controller.appMaxCount} blocklet with this credential: ${controller.nftId}`
62
- );
63
- }
64
- }
65
-
66
56
  let blockletWalletType;
67
57
 
68
58
  // create component
@@ -62,10 +62,6 @@ const createPackRelease = async ({
62
62
  throw new Error('project not found');
63
63
  }
64
64
 
65
- if (project0.type !== PROJECT.TYPES.pack) {
66
- throw new Error('project type is not pack');
67
- }
68
-
69
65
  const action = releaseId ? 'update' : 'create';
70
66
 
71
67
  if (action === 'update') {
@@ -99,10 +95,8 @@ const createPackRelease = async ({
99
95
  releaseId,
100
96
  status,
101
97
  });
102
- const project = await projectState.updateProject(projectId, params);
103
98
 
104
99
  const _releaseId = release.id;
105
-
106
100
  const releaseDir = path.join(projectDir, PROJECT.RELEASE_DIR, `${_releaseId}`);
107
101
  const resourceDir = path.join(releaseDir, PROJECT.RESOURCE_DIR);
108
102
  const tmpResourceDir = path.join(projectDir, PROJECT.RESOURCE_DIR);
@@ -118,10 +112,6 @@ const createPackRelease = async ({
118
112
  await fs.ensureDir(tmpResourceDir);
119
113
  }
120
114
 
121
- if (status === PROJECT.RELEASE_STATUS.draft) {
122
- return release;
123
- }
124
-
125
115
  const releaseExportDir = path.join(releaseDir, '.blocklet');
126
116
  const releaseBundleDir = path.join(releaseExportDir, 'bundle');
127
117
  const releaseReleaseDir = path.join(releaseExportDir, 'release');
@@ -129,6 +119,18 @@ const createPackRelease = async ({
129
119
  const moniker = (urlPathFriendly(slugify(release.blockletTitle)) || 'blocklet').toLowerCase();
130
120
  const releaseFileName = `${moniker}-${release.blockletVersion}.zip`;
131
121
  const releaseFile = path.join(releaseDir, releaseFileName);
122
+
123
+ if (status !== PROJECT.RELEASE_STATUS.draft) {
124
+ params.lastReleaseId = _releaseId;
125
+ params.lastReleaseFiles = [releaseFileName];
126
+ }
127
+
128
+ const project = await projectState.updateProject(projectId, params);
129
+
130
+ if (status === PROJECT.RELEASE_STATUS.draft) {
131
+ return release;
132
+ }
133
+
132
134
  if (fs.existsSync(releaseFile)) {
133
135
  logger.error('release file already exists, remove it', releaseFile);
134
136
  await fs.remove(releaseFile);
@@ -150,7 +150,9 @@ const createRelease = async ({
150
150
  throw new Error('project not found');
151
151
  }
152
152
 
153
- if (project0.type === PROJECT.TYPES.pack) {
153
+ const isPack = blockletComponents?.length > 0;
154
+
155
+ if (isPack) {
154
156
  return createPackRelease({
155
157
  did,
156
158
  projectId,
@@ -201,10 +203,8 @@ const createRelease = async ({
201
203
  releaseId,
202
204
  status,
203
205
  });
204
- const project = await projectState.updateProject(projectId, params);
205
206
 
206
207
  const _releaseId = release.id;
207
-
208
208
  const releaseDir = path.join(projectDir, PROJECT.RELEASE_DIR, `${_releaseId}`);
209
209
  const resourceDir = path.join(releaseDir, PROJECT.RESOURCE_DIR);
210
210
  const tmpResourceDir = path.join(projectDir, PROJECT.RESOURCE_DIR);
@@ -220,10 +220,6 @@ const createRelease = async ({
220
220
  await fs.ensureDir(tmpResourceDir);
221
221
  }
222
222
 
223
- if (status === PROJECT.RELEASE_STATUS.draft) {
224
- return release;
225
- }
226
-
227
223
  const releaseExportDir = path.join(releaseDir, '.blocklet');
228
224
  const releaseBundleDir = path.join(releaseExportDir, 'bundle');
229
225
  const releaseReleaseDir = path.join(releaseExportDir, 'release');
@@ -231,6 +227,18 @@ const createRelease = async ({
231
227
  const moniker = (urlPathFriendly(slugify(release.blockletTitle)) || 'blocklet').toLowerCase();
232
228
  const releaseFileName = `${moniker}-${release.blockletVersion}.zip`;
233
229
  const releaseFile = path.join(releaseDir, releaseFileName);
230
+
231
+ if (status !== PROJECT.RELEASE_STATUS.draft) {
232
+ params.lastReleaseId = _releaseId;
233
+ params.lastReleaseFiles = [releaseFileName];
234
+ }
235
+
236
+ const project = await projectState.updateProject(projectId, params);
237
+
238
+ if (status === PROJECT.RELEASE_STATUS.draft) {
239
+ return release;
240
+ }
241
+
234
242
  if (fs.existsSync(releaseFile)) {
235
243
  logger.error('release file already exists, remove it', releaseFile);
236
244
  await fs.remove(releaseFile);
package/lib/index.js CHANGED
@@ -189,6 +189,26 @@ function ABTNode(options) {
189
189
  },
190
190
  });
191
191
 
192
+ const reportComponentUsageQueue = createQueue({
193
+ daemon: options.daemon,
194
+ model: states.job,
195
+ name: 'report_component_usage_queue',
196
+ onJob: async (job) => {
197
+ /* eslint-disable no-use-before-define */
198
+
199
+ if (typeof blockletManager.onJob === 'function') {
200
+ await blockletManager.onJob(job);
201
+ }
202
+ },
203
+ options: {
204
+ concurrency,
205
+ maxRetries: 30,
206
+ retryDelay: 60 * 1000, // retry after 1 minute
207
+ maxTimeout: 60 * 1000 * 30, // throw timeout error after 15 minutes
208
+ id: (job) => (job ? md5(`${job.entity}-${job.action}-${job.id}`) : ''),
209
+ },
210
+ });
211
+
192
212
  const certManager = new Cert({
193
213
  maintainerEmail: DEFAULT_CERTIFICATE_EMAIL,
194
214
  dataDir: dataDirs.certManagerModule,
@@ -216,6 +236,7 @@ function ABTNode(options) {
216
236
  backupQueue,
217
237
  restoreQueue,
218
238
  checkUpdateQueue,
239
+ reportComponentUsageQueue,
219
240
  daemon: options.daemon,
220
241
  teamManager,
221
242
  });
@@ -42,10 +42,12 @@ class Project extends BaseState {
42
42
  'blockletDescription',
43
43
  'blockletDid',
44
44
  'blockletScreenshots',
45
+ 'lastReleaseId',
46
+ 'lastReleaseFiles',
45
47
  ]),
46
48
  (x) => !isUndefinedOrNull(x)
47
49
  );
48
-
50
+ _params.type = params.blockletComponents?.length ? 'pack' : 'resource';
49
51
  const [, [updated]] = await this.update({ id }, { $set: _params });
50
52
  return updated;
51
53
  }
@@ -2,7 +2,6 @@
2
2
 
3
3
  const fs = require('fs-extra');
4
4
  const path = require('path');
5
- const dayjs = require('@abtnode/util/lib/dayjs');
6
5
  const shelljs = require('shelljs');
7
6
  const os = require('os');
8
7
  const tar = require('tar');
@@ -38,7 +37,6 @@ const {
38
37
  BLOCKLET_MAX_MEM_LIMIT_IN_MB,
39
38
  BLOCKLET_INSTALL_TYPE,
40
39
  APP_STRUCT_VERSION,
41
- EXPIRED_BLOCKLET_DATA_RETENTION_DAYS,
42
40
  NODE_MODES,
43
41
  } = require('@abtnode/constant');
44
42
  const {
@@ -106,6 +104,7 @@ const { validate: validateEngine, get: getEngine } = require('../blocklet/manage
106
104
  const isRequirementsSatisfied = require('./requirement');
107
105
  const { getDidDomainForBlocklet } = require('./get-domain-for-blocklet');
108
106
  const { expandBundle, findInterfacePortByName, prettyURL, templateReplace, getServerDidDomain } = require('./index');
107
+ const { installExternalDependencies } = require('./install-external-dependencies');
109
108
 
110
109
  /**
111
110
  * get blocklet engine info, default is node
@@ -564,6 +563,8 @@ const startBlockletProcess = async (
564
563
  const env = getRuntimeEnvironments(b, nodeEnvironments, ancestors);
565
564
  const startedAt = Date.now();
566
565
 
566
+ await installExternalDependencies({ appDir: env?.BLOCKLET_APP_DIR });
567
+
567
568
  // run hook
568
569
  await preFlight(b, { env });
569
570
 
@@ -1801,10 +1802,6 @@ const shouldSkipComponent = (componentDid, whiteList) => {
1801
1802
  return !arr.includes(componentDid);
1802
1803
  };
1803
1804
 
1804
- const exceedRedemptionPeriod = (expirationDate) => {
1805
- return dayjs().diff(dayjs(expirationDate), 'day') > EXPIRED_BLOCKLET_DATA_RETENTION_DAYS;
1806
- };
1807
-
1808
1805
  const ensureAppPortsNotOccupied = async ({
1809
1806
  blocklet,
1810
1807
  componentDids: inputDids,
@@ -1858,7 +1855,7 @@ const getComponentNamesWithVersion = (app = {}, componentDids = []) => {
1858
1855
  };
1859
1856
 
1860
1857
  const shouldEnableSlpDomain = ({ mode, launcher }) => {
1861
- if (process.env.NODE_ENV === 'development' && process.env.ABT_NODE_ENABLE_SLP_DOMAIN === 'true') {
1858
+ if (process.env.ABT_NODE_ENABLE_SLP_DOMAIN === 'true') {
1862
1859
  return true;
1863
1860
  }
1864
1861
 
@@ -2045,7 +2042,6 @@ module.exports = {
2045
2042
  getBlockletStatus,
2046
2043
  shouldSkipComponent,
2047
2044
  getBlockletURLForLauncher,
2048
- exceedRedemptionPeriod,
2049
2045
  ensureAppPortsNotOccupied,
2050
2046
  getComponentNamesWithVersion,
2051
2047
  updateDidDocument,
@@ -1,8 +1,12 @@
1
- const { spawnSync } = require('child_process');
1
+ const { spawn } = require('child_process');
2
2
  const fs = require('fs-extra');
3
3
  const path = require('path');
4
4
 
5
- function installExternalDependencies(appDir) {
5
+ function isDependencyInstalled(appDir, dependency) {
6
+ return fs.existsSync(path.resolve(appDir, 'node_modules', dependency));
7
+ }
8
+
9
+ function installExternalDependencies({ appDir, forceInstall = false } = {}) {
6
10
  if (!appDir) {
7
11
  throw new Error('appDir is required');
8
12
  }
@@ -22,25 +26,44 @@ function installExternalDependencies(appDir) {
22
26
  // FIXME: 梁柱, 等未来所有的 blocklet 使用了正式版, 废弃 singleExternals
23
27
  const { singleExternals, blockletExternalDependencies } = packageJson;
24
28
  const externals = singleExternals || blockletExternalDependencies;
25
- if (!externals || !externals?.length) {
29
+ if (!Array.isArray(externals) || !externals.length) {
26
30
  return;
27
31
  }
28
32
 
29
- const result = spawnSync('npm', ['install'], {
30
- cwd: appDir,
31
- stdio: 'pipe',
32
- shell: true,
33
- env: {
34
- ...process.env,
35
- NODE_ENV: 'production',
36
- },
37
- });
33
+ const isNeedInstall = forceInstall || externals.some((dependency) => !isDependencyInstalled(appDir, dependency));
38
34
 
39
- if (result.status !== 0 && result.stderr && result.stderr.toString().trim()) {
40
- throw new Error(result.stderr.toString());
41
- }
42
- if (result.error) {
43
- throw result.error;
35
+ if (!isNeedInstall) {
36
+ return;
44
37
  }
38
+
39
+ // eslint-disable-next-line consistent-return
40
+ return new Promise((resolve, reject) => {
41
+ const child = spawn('npm', ['install'], {
42
+ cwd: appDir,
43
+ stdio: 'pipe',
44
+ shell: true,
45
+ env: {
46
+ ...process.env,
47
+ NODE_ENV: 'production',
48
+ },
49
+ });
50
+
51
+ let errorOutput = '';
52
+ child.stderr.on('data', (data) => {
53
+ errorOutput += data.toString();
54
+ });
55
+
56
+ child.on('close', (code) => {
57
+ if (code !== 0 && errorOutput.trim()) {
58
+ reject(new Error(errorOutput));
59
+ } else {
60
+ resolve();
61
+ }
62
+ });
63
+
64
+ child.on('error', (err) => {
65
+ reject(err);
66
+ });
67
+ });
45
68
  }
46
69
  module.exports = { installExternalDependencies };
@@ -1,7 +1,9 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs-extra');
3
3
  const joinUrl = require('url-join');
4
+ const dayjs = require('@abtnode/util/lib/dayjs');
4
5
  const pick = require('lodash/pick');
6
+ const uniq = require('lodash/uniq');
5
7
  const isEmpty = require('lodash/isEmpty');
6
8
  const {
7
9
  getUserAvatarUrl,
@@ -24,6 +26,7 @@ const {
24
26
  NODE_DATA_DIR_NAME,
25
27
  USER_AVATAR_URL_PREFIX,
26
28
  ROLES,
29
+ LAUNCH_SESSION_STATUS,
27
30
  } = require('@abtnode/constant');
28
31
 
29
32
  const logger = require('@abtnode/logger')('@abtnode/core:util:launcher');
@@ -58,16 +61,59 @@ const consumeServerlessNFT = async ({ nftId, blocklet }) => {
58
61
  }
59
62
  };
60
63
 
64
+ const reportComponentsEvent = async ({ blocklet, dids, type, time }) => {
65
+ const { controller } = blocklet;
66
+ const componentDids = uniq(dids);
67
+
68
+ const payload = {
69
+ componentDids,
70
+ type,
71
+ time,
72
+ };
73
+
74
+ try {
75
+ const info = await states.node.read();
76
+
77
+ const result = await doRequest(info.sk, {
78
+ launcherUrl: controller.launcherUrl,
79
+ pathname: `/api/launches/${controller.launcherSessionId}/events`,
80
+ payload,
81
+ method: 'post',
82
+ });
83
+
84
+ logger.info(`reported components event ${type}`, {
85
+ controller,
86
+ blockletPid: blocklet.appPid,
87
+ componentDids,
88
+ type,
89
+ result,
90
+ });
91
+ } catch (error) {
92
+ logger.error('report components event failed', { controller, blockletPid: blocklet.appPid, componentDids, type });
93
+ throw new Error(`report components event failed of blocklet ${blocklet.appPid}`);
94
+ }
95
+ };
96
+
61
97
  const consumeLauncherSession = async ({ params, blocklet }) => {
62
98
  try {
63
99
  const info = await states.node.read();
64
100
  const { name } = getBlockletInfo(blocklet, info.sk);
65
101
  const appUrl = getBlockletURLForLauncher({ blocklet, nodeInfo: info });
66
102
 
103
+ let componentDids = blocklet.children.map((x) => x.meta.did);
104
+ componentDids = uniq(componentDids);
105
+
67
106
  const result = await doRequest(info.sk, {
68
107
  launcherUrl: params.launcherUrl,
69
108
  pathname: `/api/launches/${params.launcherSessionId}/installed`,
70
- payload: { appDid: blocklet.appPid, appUrl, appName: name, ownerDid: params.ownerDid },
109
+ payload: {
110
+ appDid: blocklet.appPid,
111
+ appUrl,
112
+ appName: name,
113
+ installedAt: blocklet.installedAt,
114
+ ownerDid: params.ownerDid,
115
+ componentDids,
116
+ },
71
117
  method: 'post',
72
118
  });
73
119
 
@@ -213,7 +259,15 @@ const getLauncherSession = async ({ launcherUrl, launcherSessionId, external = t
213
259
 
214
260
  // strip sensitive data if call from external
215
261
  if (external && result.launcherSession) {
216
- result.launcherSession = pick(result.launcherSession, ['_id', 'status']);
262
+ result.launcherSession = pick(result.launcherSession, [
263
+ '_id',
264
+ 'status',
265
+ 'statusText',
266
+ 'subscription',
267
+ 'expirationDate',
268
+ 'terminatedAt',
269
+ 'reservedUntil',
270
+ ]);
217
271
  }
218
272
 
219
273
  return result;
@@ -240,10 +294,50 @@ const isLauncherSessionConsumed = async (params) => {
240
294
  return consumed;
241
295
  };
242
296
 
297
+ const getLaunchSessionStatus = async (blockletDid, controller) => {
298
+ logger.info('checking blocklet status', { blockletDid, controller });
299
+
300
+ const { launcherSessionId } = controller;
301
+ // 新版都通过 Launcher 判断过期状态
302
+ const { error, launcherSession } = await getLauncherSession({
303
+ launcherSessionId,
304
+ launcherUrl: controller.launcherUrl || 'https://launcher.arcblock.io/',
305
+ });
306
+
307
+ if (error) {
308
+ logger.error('get launcher session failed', { error, blockletDid, controller });
309
+ throw new Error(error);
310
+ }
311
+
312
+ logger.info('get launcher session success', { launcherSessionId, launcherSession });
313
+ return launcherSession.status;
314
+ };
315
+
316
+ const isBlockletExpired = async (blockletDid, controller) => {
317
+ const status = await getLaunchSessionStatus(blockletDid, controller);
318
+ return [LAUNCH_SESSION_STATUS.expired, LAUNCH_SESSION_STATUS.terminated].includes(status);
319
+ };
320
+
321
+ const isLaunchSessionTerminated = (session) => session.status === LAUNCH_SESSION_STATUS.terminated;
322
+
323
+ const isBlockletTerminated = async (blockletDid, controller) => {
324
+ const status = await getLaunchSessionStatus(blockletDid, controller);
325
+ return status === LAUNCH_SESSION_STATUS.terminated;
326
+ };
327
+
328
+ const DATA_RETENTION_DAYS = 30;
329
+ const isDataRetentionExceeded = (terminatedAt) =>
330
+ !!terminatedAt && dayjs().diff(dayjs(terminatedAt), 'days') > DATA_RETENTION_DAYS;
331
+
243
332
  module.exports = {
244
333
  consumeServerlessNFT,
245
334
  consumeLauncherSession,
335
+ reportComponentsEvent,
246
336
  setupAppOwner,
247
337
  getLauncherSession,
338
+ isDataRetentionExceeded,
339
+ isLaunchSessionTerminated,
248
340
  isLauncherSessionConsumed,
341
+ isBlockletExpired,
342
+ isBlockletTerminated,
249
343
  };
@@ -14,7 +14,6 @@ const blockletController = Joi.object({
14
14
  nftId: Joi.DID().required(),
15
15
  nftOwner: Joi.DID().required(),
16
16
  chainHost: Joi.string().uri().required(),
17
- appMaxCount: Joi.number().required().min(1),
18
17
  launcherUrl: Joi.string().uri().optional(),
19
18
  launcherSessionId: Joi.string().optional(),
20
19
  ownerDid: Joi.DID().optional(),
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.16.25-beta-023ec101",
6
+ "version": "1.16.25-beta-bc165d9b",
7
7
  "description": "",
8
8
  "main": "lib/index.js",
9
9
  "files": [
@@ -19,19 +19,19 @@
19
19
  "author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
20
20
  "license": "Apache-2.0",
21
21
  "dependencies": {
22
- "@abtnode/analytics": "1.16.25-beta-023ec101",
23
- "@abtnode/auth": "1.16.25-beta-023ec101",
24
- "@abtnode/certificate-manager": "1.16.25-beta-023ec101",
25
- "@abtnode/constant": "1.16.25-beta-023ec101",
26
- "@abtnode/cron": "1.16.25-beta-023ec101",
27
- "@abtnode/logger": "1.16.25-beta-023ec101",
28
- "@abtnode/models": "1.16.25-beta-023ec101",
29
- "@abtnode/queue": "1.16.25-beta-023ec101",
30
- "@abtnode/rbac": "1.16.25-beta-023ec101",
31
- "@abtnode/router-provider": "1.16.25-beta-023ec101",
32
- "@abtnode/static-server": "1.16.25-beta-023ec101",
33
- "@abtnode/timemachine": "1.16.25-beta-023ec101",
34
- "@abtnode/util": "1.16.25-beta-023ec101",
22
+ "@abtnode/analytics": "1.16.25-beta-bc165d9b",
23
+ "@abtnode/auth": "1.16.25-beta-bc165d9b",
24
+ "@abtnode/certificate-manager": "1.16.25-beta-bc165d9b",
25
+ "@abtnode/constant": "1.16.25-beta-bc165d9b",
26
+ "@abtnode/cron": "1.16.25-beta-bc165d9b",
27
+ "@abtnode/logger": "1.16.25-beta-bc165d9b",
28
+ "@abtnode/models": "1.16.25-beta-bc165d9b",
29
+ "@abtnode/queue": "1.16.25-beta-bc165d9b",
30
+ "@abtnode/rbac": "1.16.25-beta-bc165d9b",
31
+ "@abtnode/router-provider": "1.16.25-beta-bc165d9b",
32
+ "@abtnode/static-server": "1.16.25-beta-bc165d9b",
33
+ "@abtnode/timemachine": "1.16.25-beta-bc165d9b",
34
+ "@abtnode/util": "1.16.25-beta-bc165d9b",
35
35
  "@arcblock/did": "1.18.113",
36
36
  "@arcblock/did-auth": "1.18.113",
37
37
  "@arcblock/did-ext": "^1.18.113",
@@ -42,12 +42,12 @@
42
42
  "@arcblock/pm2-events": "^0.0.5",
43
43
  "@arcblock/validator": "^1.18.113",
44
44
  "@arcblock/vc": "1.18.113",
45
- "@blocklet/constant": "1.16.25-beta-023ec101",
46
- "@blocklet/env": "1.16.25-beta-023ec101",
47
- "@blocklet/meta": "1.16.25-beta-023ec101",
48
- "@blocklet/resolver": "1.16.25-beta-023ec101",
49
- "@blocklet/sdk": "1.16.25-beta-023ec101",
50
- "@did-space/client": "^0.3.71",
45
+ "@blocklet/constant": "1.16.25-beta-bc165d9b",
46
+ "@blocklet/env": "1.16.25-beta-bc165d9b",
47
+ "@blocklet/meta": "1.16.25-beta-bc165d9b",
48
+ "@blocklet/resolver": "1.16.25-beta-bc165d9b",
49
+ "@blocklet/sdk": "1.16.25-beta-bc165d9b",
50
+ "@did-space/client": "^0.3.73",
51
51
  "@fidm/x509": "^1.2.1",
52
52
  "@ocap/mcrypto": "1.18.113",
53
53
  "@ocap/util": "1.18.113",
@@ -102,5 +102,5 @@
102
102
  "jest": "^29.7.0",
103
103
  "unzipper": "^0.10.11"
104
104
  },
105
- "gitHead": "7e534f8dd1335f42897bf467e261f02bc7a59be0"
105
+ "gitHead": "a7895ed39e47c8c6bd10d32317804cc1904714ef"
106
106
  }