@abtnode/core 1.17.7-beta-20251229-223813-e1e6c5e3 → 1.17.7-beta-20251230-120126-2836fe04

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.
@@ -980,6 +980,9 @@ class DiskBlockletManager extends BaseBlockletManager {
980
980
 
981
981
  await pAll(tasks, { concurrency: 6 });
982
982
 
983
+ // Sync parent blocklet uptime status once after all components are processed
984
+ await states.blocklet.syncUptimeStatus(parentBlockletId);
985
+
983
986
  const nextBlocklet = await this.ensureBlocklet(did, { e2eMode });
984
987
  let errorDescription = '';
985
988
  let resultBlocklet = nextBlocklet;
@@ -1271,6 +1274,9 @@ class DiskBlockletManager extends BaseBlockletManager {
1271
1274
  for (const subDid of entryComponentIds) {
1272
1275
  onError?.(subDid, err);
1273
1276
  }
1277
+ if (throwOnError) {
1278
+ throw err;
1279
+ }
1274
1280
  }
1275
1281
  }
1276
1282
 
@@ -2474,6 +2480,30 @@ class DiskBlockletManager extends BaseBlockletManager {
2474
2480
  });
2475
2481
  this.configSynchronizer.throttledSyncAppConfig(blocklet.meta.did);
2476
2482
 
2483
+ // Restart blocklet if it's running to pick up the new mount point
2484
+ // Only restart the component that was modified (did)
2485
+ if (!isRootComponent && did) {
2486
+ const updatedBlocklet = await this.getBlocklet(rootDid);
2487
+ const updatedComponent = updatedBlocklet.children.find((x) => x.meta.did === did);
2488
+
2489
+ if (
2490
+ updatedComponent &&
2491
+ (updatedComponent.status === BlockletStatus.running || updatedComponent.greenStatus === BlockletStatus.running)
2492
+ ) {
2493
+ try {
2494
+ await this.restart({ did: rootDid, componentDids: [did], operator: context?.user?.did }, context);
2495
+ logger.info('restarted blocklet after mount point update', { rootDid, componentDid: did });
2496
+ } catch (error) {
2497
+ logger.error('failed to restart blocklet after mount point update', {
2498
+ rootDid,
2499
+ componentDid: did,
2500
+ error,
2501
+ });
2502
+ // Don't throw error - mount point update succeeded, restart failure is logged but doesn't block the operation
2503
+ }
2504
+ }
2505
+ }
2506
+
2477
2507
  return this.getBlocklet(rootDid);
2478
2508
  }
2479
2509
 
@@ -3756,6 +3786,13 @@ class DiskBlockletManager extends BaseBlockletManager {
3756
3786
  }
3757
3787
 
3758
3788
  async _onRestart({ did, componentDids, context, operator }) {
3789
+ // 检查 blocklet 是否存在,如果不存在则跳过重启任务
3790
+ // 这可以防止在 blocklet 被删除后,队列中待处理的重启任务执行时出错
3791
+ if (!(await this.hasBlocklet({ did }))) {
3792
+ logger.warn('skip restart job: blocklet not found', { did });
3793
+ return;
3794
+ }
3795
+
3759
3796
  if (process.env.ABT_NODE_DISABLE_BLUE_GREEN) {
3760
3797
  await this.stop({ did, componentDids, context, operator });
3761
3798
  await this.start({ did, componentDids, checkHealthImmediately: true });
@@ -401,6 +401,9 @@ const blueGreenStartBlocklet = async (
401
401
 
402
402
  await pAll(tasks, { concurrency: 6 });
403
403
 
404
+ // Sync parent blocklet uptime status once after all components are processed
405
+ await states.blocklet.syncUptimeStatus(appId);
406
+
404
407
  const lastBlocklet = await manager.getBlocklet(did, { e2eMode });
405
408
  let errorDescription = '';
406
409
 
@@ -0,0 +1,48 @@
1
+ /* eslint-disable no-await-in-loop */
2
+
3
+ /**
4
+ * Migration script to populate startedAt/stoppedAt for existing blocklets
5
+ *
6
+ * This migration:
7
+ * 1. Queries all blocklet IDs efficiently (only fetching `id` field)
8
+ * 2. For each blocklet, calls syncUptimeStatus to populate startedAt/stoppedAt
9
+ * based on the running state of its children
10
+ *
11
+ * Uses silent: true internally to prevent updatedAt from being modified
12
+ */
13
+ module.exports = async ({ states, printInfo }) => {
14
+ printInfo('Try to populate blocklet uptime (startedAt/stoppedAt)...');
15
+
16
+ // Query only the id field for efficiency
17
+ const blocklets = await states.blocklet.model.findAll({
18
+ attributes: ['id'],
19
+ raw: true,
20
+ });
21
+
22
+ if (!blocklets || blocklets.length === 0) {
23
+ printInfo('No blocklets found, skipping migration');
24
+ return;
25
+ }
26
+
27
+ printInfo(`Found ${blocklets.length} blocklets to process`);
28
+
29
+ let updatedCount = 0;
30
+ let skippedCount = 0;
31
+
32
+ for (const blocklet of blocklets) {
33
+ try {
34
+ // syncUptimeStatus handles all the logic:
35
+ // - Checks if any child is running
36
+ // - If running and no startedAt: sets startedAt from earliest child startedAt
37
+ // - If not running and has startedAt without stoppedAt: sets stoppedAt = now
38
+ // - Uses silent: true to prevent updatedAt modification
39
+ await states.blocklet.syncUptimeStatus(blocklet.id);
40
+ updatedCount++;
41
+ } catch (err) {
42
+ printInfo(`Skipped blocklet ${blocklet.id}: ${err.message}`);
43
+ skippedCount++;
44
+ }
45
+ }
46
+
47
+ printInfo(`Blocklet uptime migration completed: ${updatedCount} updated, ${skippedCount} skipped`);
48
+ };
@@ -508,8 +508,7 @@ const expandComponentRules = (sites = [], blocklets) => {
508
508
  }
509
509
 
510
510
  const blocklet = blocklets.find((x) => x.meta.did === site.blockletDid);
511
- const components = blocklet.children.filter((x) => x.mode === BLOCKLET_MODES.PRODUCTION);
512
- const expandedRules = components
511
+ const expandedRules = blocklet.children
513
512
  .filter((x) => hasMountPoint(x.meta))
514
513
  .map((x) => ({
515
514
  id: UUID.v4(),
@@ -339,6 +339,57 @@ class BlockletChildState extends BaseState {
339
339
 
340
340
  return results.map((x) => x.toJSON());
341
341
  }
342
+
343
+ /**
344
+ * Check if any child of a parent blocklet is in running state
345
+ * Uses efficient COUNT query instead of loading all children
346
+ * Checks both status and greenStatus for blue-green deployment support
347
+ * @param {string} parentBlockletId - The parent blocklet ID
348
+ * @returns {Promise<boolean>} - True if any child is running
349
+ */
350
+ async hasAnyRunningChild(parentBlockletId) {
351
+ if (!parentBlockletId) {
352
+ return false;
353
+ }
354
+
355
+ const { Op } = Sequelize;
356
+ const count = await this.model.count({
357
+ where: {
358
+ parentBlockletId,
359
+ [Op.or]: [{ status: BlockletStatus.running }, { greenStatus: BlockletStatus.running }],
360
+ },
361
+ });
362
+
363
+ return count > 0;
364
+ }
365
+
366
+ /**
367
+ * Get the earliest startedAt timestamp from running children
368
+ * Used to determine when the app first started running
369
+ * @param {string} parentBlockletId - The parent blocklet ID
370
+ * @returns {Promise<Date|null>} - Earliest startedAt or null if no running children
371
+ */
372
+ async getEarliestRunningStartedAt(parentBlockletId) {
373
+ if (!parentBlockletId) {
374
+ return null;
375
+ }
376
+
377
+ const { Op } = Sequelize;
378
+ const result = await this.model.findOne({
379
+ attributes: [[Sequelize.fn('MIN', Sequelize.col('startedAt')), 'earliestStartedAt']],
380
+ where: {
381
+ parentBlockletId,
382
+ [Op.or]: [{ status: BlockletStatus.running }, { greenStatus: BlockletStatus.running }],
383
+ startedAt: { [Op.not]: null },
384
+ },
385
+ raw: true,
386
+ });
387
+
388
+ if (result && result.earliestStartedAt) {
389
+ return new Date(result.earliestStartedAt);
390
+ }
391
+ return null;
392
+ }
342
393
  }
343
394
 
344
395
  module.exports = BlockletChildState;
@@ -646,7 +646,7 @@ class BlockletState extends BaseState {
646
646
  */
647
647
  async findPaginated({ search, external, paging, sort } = {}) {
648
648
  // Allowed sort fields whitelist
649
- const ALLOWED_SORT_FIELDS = ['installedAt', 'updatedAt', 'status'];
649
+ const ALLOWED_SORT_FIELDS = ['installedAt', 'updatedAt', 'status', 'startedAt'];
650
650
  const ALLOWED_SORT_DIRECTIONS = ['asc', 'desc'];
651
651
 
652
652
  const conditions = { where: {} };
@@ -658,11 +658,25 @@ class BlockletState extends BaseState {
658
658
  const escapedSearch = searchTerm.replace(/[%_]/g, '\\$&');
659
659
  const searchLike = `%${escapedSearch}%`;
660
660
 
661
+ const dialect = this.model.sequelize.getDialect();
662
+
663
+ // Build search condition for meta column
664
+ // PostgreSQL: meta is JSONB, need to cast to text before using lower()
665
+ // SQLite: meta is stored as text, can use lower() directly
666
+ let metaSearchCondition;
667
+ if (dialect === 'postgres') {
668
+ // Cast JSONB to text before applying lower() using PostgreSQL-specific syntax
669
+ metaSearchCondition = Sequelize.where(Sequelize.literal('lower("meta"::text)'), { [Op.like]: searchLike });
670
+ } else {
671
+ // SQLite: meta is already text
672
+ metaSearchCondition = Sequelize.where(Sequelize.fn('lower', Sequelize.col('meta')), {
673
+ [Op.like]: searchLike,
674
+ });
675
+ }
676
+
661
677
  conditions.where[Op.or] = [
662
678
  // Search in meta column (JSON stored as text) - search the whole JSON string
663
- Sequelize.where(Sequelize.fn('lower', Sequelize.col('meta')), {
664
- [Op.like]: searchLike,
665
- }),
679
+ metaSearchCondition,
666
680
  // Search in appDid
667
681
  Sequelize.where(Sequelize.fn('lower', Sequelize.col('appDid')), {
668
682
  [Op.like]: searchLike,
@@ -1326,6 +1340,12 @@ class BlockletState extends BaseState {
1326
1340
  }
1327
1341
  }
1328
1342
 
1343
+ // Sync parent uptime for stable states (running, stopped, error)
1344
+ const stableStatuses = [BlockletStatus.running, BlockletStatus.stopped, BlockletStatus.error];
1345
+ if (stableStatuses.includes(status)) {
1346
+ await this.syncUptimeStatus(doc.id);
1347
+ }
1348
+
1329
1349
  const children = await this.loadChildren(doc.id);
1330
1350
 
1331
1351
  res.children = children;
@@ -1340,6 +1360,64 @@ class BlockletState extends BaseState {
1340
1360
  }
1341
1361
  }
1342
1362
 
1363
+ /**
1364
+ * Synchronize parent blocklet's startedAt/stoppedAt based on children's running state
1365
+ *
1366
+ * Rules:
1367
+ * - If ANY child has status === RUNNING or greenStatus === RUNNING:
1368
+ * → App is running, startedAt = earliest child startedAt, stoppedAt = null
1369
+ * - If NO child is running (all stopped/error):
1370
+ * → App is stopped, stoppedAt = now, startedAt = null
1371
+ * - Only acts on stable states, ignores in-progress statuses
1372
+ *
1373
+ * @param {string} blockletId - The parent blocklet ID (not DID)
1374
+ * @returns {Promise<void>}
1375
+ */
1376
+ async syncUptimeStatus(blockletId) {
1377
+ if (!blockletId) {
1378
+ return;
1379
+ }
1380
+
1381
+ try {
1382
+ const blocklet = await this.findOne({ id: blockletId });
1383
+ if (!blocklet) {
1384
+ return;
1385
+ }
1386
+
1387
+ const hasRunning = await this.BlockletChildState.hasAnyRunningChild(blockletId);
1388
+ if (hasRunning) {
1389
+ // App is running - update startedAt if not already set
1390
+ if (!blocklet.startedAt) {
1391
+ const earliestStartedAt = await this.BlockletChildState.getEarliestRunningStartedAt(blockletId);
1392
+ const newStartedAt = earliestStartedAt || new Date();
1393
+ // Use silent: true to prevent updatedAt from being modified
1394
+ await this.model.update(
1395
+ { startedAt: newStartedAt, stoppedAt: null },
1396
+ { where: { id: blockletId }, silent: true }
1397
+ );
1398
+ logger.info('syncUptimeStatus: app started', {
1399
+ blockletId,
1400
+ appDid: blocklet.appDid,
1401
+ startedAt: newStartedAt,
1402
+ });
1403
+ }
1404
+ } else if (blocklet.startedAt && !blocklet.stoppedAt) {
1405
+ // App is not running - update stoppedAt if was previously running
1406
+ const now = new Date();
1407
+ // Use silent: true to prevent updatedAt from being modified
1408
+ await this.model.update({ stoppedAt: now, startedAt: null }, { where: { id: blockletId }, silent: true });
1409
+ logger.info('syncUptimeStatus: app stopped', {
1410
+ blockletId,
1411
+ appDid: blocklet.appDid,
1412
+ stoppedAt: now,
1413
+ });
1414
+ }
1415
+ } catch (error) {
1416
+ logger.error('syncUptimeStatus failed', { blockletId, error: error.message });
1417
+ // Don't throw - uptime sync failure shouldn't break status updates
1418
+ }
1419
+ }
1420
+
1343
1421
  async setInstalledAt(did) {
1344
1422
  logger.info('setInstalledAt', { did });
1345
1423
  const blocklet = await this.getBlocklet(did);
package/lib/util/ready.js CHANGED
@@ -1,37 +1,18 @@
1
1
  /* eslint-disable no-console */
2
2
  const logger = require('@abtnode/logger')('@abtnode/core:ready');
3
- const chalk = require('chalk');
4
3
 
5
4
  const createStateReadyHandler =
6
5
  () =>
7
- ({ states, options }) => {
6
+ ({ states }) => {
8
7
  return states.node
9
8
  .read()
10
- .then(async (state) => {
9
+ .then((state) => {
11
10
  // Set default sender/receiver for notification center
12
11
  states.notification.setDefaultSender(state.did);
13
12
  if (state.nodeOwner) {
14
13
  states.notification.setDefaultReceiver(state.nodeOwner.did);
15
14
  }
16
-
17
- if (process.env.NODE_ENV === 'test' || process.env.CI || process.env.TRAVIS) {
18
- // Do not validate data dir sharing in test env
19
- } else {
20
- const count = await states.node.count();
21
- if (count > 1) {
22
- // eslint-disable-next-line no-underscore-dangle
23
- await states.node.remove({ did: state.did });
24
- console.error('\n\x1b[31m======================================================');
25
- console.error(`Data dir: ${options.dataDir} is used by another Blocklet Server instance, abort!`);
26
- console.error('Sharing data dir between Blocklet Server instances may break things!');
27
- console.error('======================================================\x1b[0m');
28
- console.log('\nIf you intend to use this dir:');
29
- console.log(` 1. Stop Blocklet Server by ${chalk.cyan('blocklet server stop --force')}`);
30
- console.log(` 2. Clear data dir by ${chalk.cyan(`rm -r ${options.dataDir}`)}`);
31
- console.log(' 3. Reinitialize and start Blocklet Server');
32
- process.exit(1);
33
- }
34
- }
15
+ // deleted states.node.count() check
35
16
  })
36
17
  .catch((err) => {
37
18
  console.error('Can not ready node state on Blocklet Server start:', err.message);
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.17.7-beta-20251229-223813-e1e6c5e3",
6
+ "version": "1.17.7-beta-20251230-120126-2836fe04",
7
7
  "description": "",
8
8
  "main": "lib/index.js",
9
9
  "files": [
@@ -17,19 +17,19 @@
17
17
  "author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
18
18
  "license": "Apache-2.0",
19
19
  "dependencies": {
20
- "@abtnode/analytics": "1.17.7-beta-20251229-223813-e1e6c5e3",
21
- "@abtnode/auth": "1.17.7-beta-20251229-223813-e1e6c5e3",
22
- "@abtnode/certificate-manager": "1.17.7-beta-20251229-223813-e1e6c5e3",
23
- "@abtnode/constant": "1.17.7-beta-20251229-223813-e1e6c5e3",
24
- "@abtnode/cron": "1.17.7-beta-20251229-223813-e1e6c5e3",
25
- "@abtnode/db-cache": "1.17.7-beta-20251229-223813-e1e6c5e3",
26
- "@abtnode/docker-utils": "1.17.7-beta-20251229-223813-e1e6c5e3",
27
- "@abtnode/logger": "1.17.7-beta-20251229-223813-e1e6c5e3",
28
- "@abtnode/models": "1.17.7-beta-20251229-223813-e1e6c5e3",
29
- "@abtnode/queue": "1.17.7-beta-20251229-223813-e1e6c5e3",
30
- "@abtnode/rbac": "1.17.7-beta-20251229-223813-e1e6c5e3",
31
- "@abtnode/router-provider": "1.17.7-beta-20251229-223813-e1e6c5e3",
32
- "@abtnode/util": "1.17.7-beta-20251229-223813-e1e6c5e3",
20
+ "@abtnode/analytics": "1.17.7-beta-20251230-120126-2836fe04",
21
+ "@abtnode/auth": "1.17.7-beta-20251230-120126-2836fe04",
22
+ "@abtnode/certificate-manager": "1.17.7-beta-20251230-120126-2836fe04",
23
+ "@abtnode/constant": "1.17.7-beta-20251230-120126-2836fe04",
24
+ "@abtnode/cron": "1.17.7-beta-20251230-120126-2836fe04",
25
+ "@abtnode/db-cache": "1.17.7-beta-20251230-120126-2836fe04",
26
+ "@abtnode/docker-utils": "1.17.7-beta-20251230-120126-2836fe04",
27
+ "@abtnode/logger": "1.17.7-beta-20251230-120126-2836fe04",
28
+ "@abtnode/models": "1.17.7-beta-20251230-120126-2836fe04",
29
+ "@abtnode/queue": "1.17.7-beta-20251230-120126-2836fe04",
30
+ "@abtnode/rbac": "1.17.7-beta-20251230-120126-2836fe04",
31
+ "@abtnode/router-provider": "1.17.7-beta-20251230-120126-2836fe04",
32
+ "@abtnode/util": "1.17.7-beta-20251230-120126-2836fe04",
33
33
  "@aigne/aigne-hub": "^0.10.15",
34
34
  "@arcblock/did": "^1.27.16",
35
35
  "@arcblock/did-connect-js": "^1.27.16",
@@ -41,15 +41,15 @@
41
41
  "@arcblock/pm2-events": "^0.0.5",
42
42
  "@arcblock/validator": "^1.27.16",
43
43
  "@arcblock/vc": "^1.27.16",
44
- "@blocklet/constant": "1.17.7-beta-20251229-223813-e1e6c5e3",
44
+ "@blocklet/constant": "1.17.7-beta-20251230-120126-2836fe04",
45
45
  "@blocklet/did-space-js": "^1.2.12",
46
- "@blocklet/env": "1.17.7-beta-20251229-223813-e1e6c5e3",
46
+ "@blocklet/env": "1.17.7-beta-20251230-120126-2836fe04",
47
47
  "@blocklet/error": "^0.3.5",
48
- "@blocklet/meta": "1.17.7-beta-20251229-223813-e1e6c5e3",
49
- "@blocklet/resolver": "1.17.7-beta-20251229-223813-e1e6c5e3",
50
- "@blocklet/sdk": "1.17.7-beta-20251229-223813-e1e6c5e3",
51
- "@blocklet/server-js": "1.17.7-beta-20251229-223813-e1e6c5e3",
52
- "@blocklet/store": "1.17.7-beta-20251229-223813-e1e6c5e3",
48
+ "@blocklet/meta": "1.17.7-beta-20251230-120126-2836fe04",
49
+ "@blocklet/resolver": "1.17.7-beta-20251230-120126-2836fe04",
50
+ "@blocklet/sdk": "1.17.7-beta-20251230-120126-2836fe04",
51
+ "@blocklet/server-js": "1.17.7-beta-20251230-120126-2836fe04",
52
+ "@blocklet/store": "1.17.7-beta-20251230-120126-2836fe04",
53
53
  "@blocklet/theme": "^3.3.3",
54
54
  "@fidm/x509": "^1.2.1",
55
55
  "@ocap/mcrypto": "^1.27.16",
@@ -114,5 +114,5 @@
114
114
  "express": "^4.18.2",
115
115
  "unzipper": "^0.10.11"
116
116
  },
117
- "gitHead": "5cc55a399f97ddc0822c9896615375c1d3042a72"
117
+ "gitHead": "ee9edd47a6da9d6b47799357dca96814082f8065"
118
118
  }