@abtnode/core 1.17.7-beta-20251227-001958-ea2ba3f5 → 1.17.7-beta-20251229-223813-e1e6c5e3

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.
Files changed (39) hide show
  1. package/lib/blocklet/manager/disk.js +74 -32
  2. package/lib/blocklet/manager/ensure-blocklet-running.js +1 -1
  3. package/lib/blocklet/manager/helper/blue-green-start-blocklet.js +1 -1
  4. package/lib/blocklet/manager/helper/install-application-from-general.js +2 -3
  5. package/lib/blocklet/manager/helper/install-component-from-url.js +7 -4
  6. package/lib/blocklet/migration-dist/migration.cjs +5 -4
  7. package/lib/blocklet/passport/index.js +10 -3
  8. package/lib/blocklet/project/index.js +7 -2
  9. package/lib/blocklet/security/index.js +2 -2
  10. package/lib/cert.js +6 -3
  11. package/lib/event/index.js +98 -87
  12. package/lib/event/util.js +7 -13
  13. package/lib/index.js +15 -26
  14. package/lib/migrations/1.5.0-site.js +3 -7
  15. package/lib/migrations/1.5.15-site.js +3 -7
  16. package/lib/monitor/blocklet-runtime-monitor.js +37 -5
  17. package/lib/monitor/node-runtime-monitor.js +4 -4
  18. package/lib/router/helper.js +525 -452
  19. package/lib/router/index.js +280 -104
  20. package/lib/router/manager.js +14 -28
  21. package/lib/states/blocklet-child.js +93 -1
  22. package/lib/states/blocklet-extras.js +1 -1
  23. package/lib/states/blocklet.js +429 -197
  24. package/lib/states/node.js +0 -10
  25. package/lib/states/site.js +87 -4
  26. package/lib/team/manager.js +2 -21
  27. package/lib/util/blocklet.js +39 -19
  28. package/lib/util/get-accessible-external-node-ip.js +21 -6
  29. package/lib/util/index.js +3 -3
  30. package/lib/util/ip.js +15 -1
  31. package/lib/util/launcher.js +11 -11
  32. package/lib/util/ready.js +2 -9
  33. package/lib/util/reset-node.js +6 -5
  34. package/lib/validators/router.js +0 -3
  35. package/lib/webhook/sender/api/index.js +5 -0
  36. package/package.json +23 -25
  37. package/lib/migrations/1.0.36-snapshot.js +0 -10
  38. package/lib/migrations/1.1.9-snapshot.js +0 -7
  39. package/lib/states/routing-snapshot.js +0 -146
@@ -2,21 +2,16 @@
2
2
  /* eslint-disable no-await-in-loop */
3
3
  /* eslint-disable function-paren-newline */
4
4
  /* eslint-disable no-underscore-dangle */
5
+ const crypto = require('crypto');
5
6
  const omit = require('lodash/omit');
6
7
  const uniq = require('lodash/uniq');
8
+ const { Op, Sequelize } = require('sequelize');
7
9
  const cloneDeep = require('@abtnode/util/lib/deep-clone');
8
10
  const detectPort = require('detect-port');
9
11
  const security = require('@abtnode/util/lib/security');
10
12
  const { CustomError } = require('@blocklet/error');
11
13
  const { fixPerson, fixInterfaces } = require('@blocklet/meta/lib/fix');
12
- const {
13
- getDisplayName,
14
- forEachBlocklet,
15
- forEachBlockletSync,
16
- forEachComponentV2,
17
- getBlockletServices,
18
- hasStartEngine,
19
- } = require('@blocklet/meta/lib/util');
14
+ const { getDisplayName, forEachBlockletSync, forEachComponentV2, hasStartEngine } = require('@blocklet/meta/lib/util');
20
15
 
21
16
  const {
22
17
  BlockletStatus,
@@ -24,10 +19,12 @@ const {
24
19
  BLOCKLET_MODES,
25
20
  BLOCKLET_DEFAULT_PORT_NAME,
26
21
  BlockletGroup,
22
+ BLOCKLET_INTERFACE_TYPE_SERVICE,
27
23
  } = require('@blocklet/constant');
28
24
  const { isPortTaken } = require('@abtnode/util/lib/port');
29
25
  const { verifyVault } = require('@blocklet/meta/lib/security');
30
26
  const { APP_STRUCT_VERSION } = require('@abtnode/constant');
27
+ const { isValid: isValidDid } = require('@arcblock/did');
31
28
 
32
29
  const logger = require('@abtnode/logger')('@abtnode/core:states:blocklet');
33
30
 
@@ -49,21 +46,26 @@ const portAssignLock = new DBCache(() => ({
49
46
  ...getAbtNodeRedisAndSQLiteUrl(),
50
47
  }));
51
48
 
49
+ // Default port range: 10000-49151 (registered ports, avoiding ephemeral range 49152-65535)
50
+ const DEFAULT_PORT_RANGE = [10000, 49151];
51
+
52
52
  /**
53
- * 统一的端口分配方法
54
- * 无论是首次分配还是刷新端口,都使用这个方法
53
+ * 统一的端口分配方法(优化版本)
54
+ * 使用 Set 进行 O(1) 端口查找,hash-based 确定性分配
55
55
  * @param {Object} options
56
56
  * @param {Array<string>} options.wantedPorts - 需要分配的端口名称列表
57
- * @param {Object} options.blackListPorts - 黑名单端口(已占用的端口)
58
- * @param {Array<number>} options.portRange - 端口范围,默认 [10000, 31000]
57
+ * @param {Array<number>} options.blackListPorts - 黑名单端口(已占用的端口)
58
+ * @param {Array<number>} options.portRange - 端口范围,默认 [10000, 49151]
59
59
  * @param {number} options.defaultPort - 回退时的起始端口
60
+ * @param {string} options.blockletDid - blocklet DID,用于确定性 hash 分配
60
61
  * @returns {Promise<Object>} 分配的端口对象 { portName: portNumber }
61
62
  */
62
63
  async function assignPortsWithLock({
63
64
  wantedPorts,
64
65
  blackListPorts = [],
65
- portRange = [10000, 31000],
66
+ portRange = DEFAULT_PORT_RANGE,
66
67
  defaultPort = 5555,
68
+ blockletDid = '',
67
69
  }) {
68
70
  const lockName = 'blocklet-port-assign';
69
71
 
@@ -73,54 +75,63 @@ async function assignPortsWithLock({
73
75
 
74
76
  try {
75
77
  const assignedPorts = {};
76
- const usedPorts = [...blackListPorts];
78
+ // OPTIMIZATION: Use Set for O(1) lookup instead of Array O(n)
79
+ const usedPortsSet = new Set(blackListPorts.map(Number));
80
+ const rangeSize = portRange[1] - portRange[0];
77
81
 
78
82
  for (let i = 0; i < wantedPorts.length; i++) {
79
83
  const wantedPort = wantedPorts[i];
80
-
81
- let found = false;
82
84
  let assignedPort = null;
83
- let attempts = 0;
84
- const maxAttempts = 100;
85
85
 
86
- while (!found && attempts < maxAttempts) {
87
- // [10000, 31000] 范围内随机选择端口
88
- const randomPort = Math.floor(Math.random() * (portRange[1] - portRange[0])) + portRange[0];
86
+ // OPTIMIZATION: Hash-based starting point for deterministic allocation
87
+ // This reduces collision probability and makes port assignment reproducible
88
+ let basePort = portRange[0];
89
+ if (blockletDid) {
90
+ const hash = crypto.createHash('md5').update(`${blockletDid}:${wantedPort}:${i}`).digest('hex');
91
+ basePort = portRange[0] + (parseInt(hash.slice(0, 8), 16) % rangeSize);
92
+ } else {
93
+ // Fallback to random if no DID provided
94
+ basePort = portRange[0] + Math.floor(Math.random() * rangeSize);
95
+ }
96
+
97
+ // OPTIMIZATION: Linear probe from hash position (more efficient than random retry)
98
+ // Maximum probes limited to range size to guarantee termination
99
+ const maxProbes = Math.min(rangeSize, 1000); // Cap at 1000 to avoid excessive probing
100
+ for (let offset = 0; offset < maxProbes; offset++) {
101
+ const candidatePort = portRange[0] + ((basePort - portRange[0] + offset) % rangeSize);
89
102
 
90
- if (!usedPorts.includes(randomPort)) {
103
+ if (!usedPortsSet.has(candidatePort)) {
91
104
  // eslint-disable-next-line no-await-in-loop
92
- const isTaken = await isPortTaken(randomPort);
105
+ const isTaken = await isPortTaken(candidatePort);
93
106
  if (!isTaken) {
94
- assignedPort = randomPort;
95
- usedPorts.push(assignedPort);
96
- found = true;
107
+ assignedPort = candidatePort;
108
+ usedPortsSet.add(assignedPort);
109
+ break;
97
110
  }
111
+ // Mark as used even if taken by external process to avoid re-checking
112
+ usedPortsSet.add(candidatePort);
98
113
  }
99
- attempts++;
100
114
  }
101
115
 
102
- // 如果随机方式失败,回退到累加方式
103
- if (!found) {
104
- logger.warn('Random port assignment failed, falling back to sequential assignment', {
116
+ // Fallback to detectPort if linear probe fails
117
+ if (!assignedPort) {
118
+ logger.warn('Linear probe port assignment failed, falling back to detectPort', {
105
119
  wantedPort,
106
- attempts,
120
+ blockletDid,
121
+ rangeSize,
107
122
  });
108
- let port = defaultPort + 1;
123
+ let port = Math.max(defaultPort, portRange[0]) + 1;
109
124
  // eslint-disable-next-line no-await-in-loop
110
125
  let fallbackPort = await detectPort(Number(port));
111
- while (usedPorts.includes(fallbackPort)) {
126
+ while (usedPortsSet.has(fallbackPort) && fallbackPort < portRange[1]) {
112
127
  port = fallbackPort + 1;
113
- while (usedPorts.includes(port)) {
114
- port++;
115
- }
116
128
  // eslint-disable-next-line no-await-in-loop
117
129
  fallbackPort = await detectPort(Number(port));
118
130
  }
119
131
  assignedPort = fallbackPort;
120
- usedPorts.push(assignedPort);
132
+ usedPortsSet.add(assignedPort);
121
133
  }
122
134
 
123
- // set wantedPort to assignedPort
124
135
  assignedPorts[wantedPort] = assignedPort;
125
136
  }
126
137
 
@@ -136,8 +147,25 @@ async function assignPortsWithLock({
136
147
  const isHex = (str) => /^0x[0-9a-f]+$/i.test(str);
137
148
  const getMaxPort = (ports = {}) => Math.max(...Object.values(ports).map(Number));
138
149
 
139
- // structV1Did is just for migration purpose and should be removed in the future
140
- const getConditions = (did) => [{ 'meta.did': did }, { appDid: did }, { appPid: did }, { structV1Did: did }];
150
+ /**
151
+ * Build SQL condition for searching a DID in migratedFrom array
152
+ * @param {string} did - The DID to search for (must be validated before calling)
153
+ * @param {string} dialect - Database dialect ('postgres' or 'sqlite')
154
+ * @returns {Object} Sequelize literal condition
155
+ */
156
+ const getMigratedFromCondition = (did, dialect) => {
157
+ if (dialect === 'postgres') {
158
+ return Sequelize.literal(`EXISTS (
159
+ SELECT 1 FROM jsonb_array_elements("migratedFrom") AS m
160
+ WHERE m->>'appDid' = '${did}'
161
+ )`);
162
+ }
163
+ // SQLite
164
+ return Sequelize.literal(`EXISTS (
165
+ SELECT 1 FROM json_each("migratedFrom")
166
+ WHERE json_extract(value, '$.appDid') = '${did}'
167
+ )`);
168
+ };
141
169
 
142
170
  const getExternalPortsFromMeta = (meta) =>
143
171
  (meta.interfaces || []).map((x) => x.port && x.port.external).filter(Boolean);
@@ -254,12 +282,10 @@ class BlockletState extends BaseState {
254
282
  this.defaultPort = config.blockletPort || 5555;
255
283
  // @didMap: { [did: string]: metaDid: string }
256
284
  this.didMap = new Map();
257
- // @didToIdMap: { [did: string]: id: string } - 缓存 did 到 doc.id 的映射,避免重复 $or 查询
258
- this.didToIdMap = new Map();
259
285
  this.statusLocks = new Map();
260
286
 
261
287
  // BlockletChildState instance passed from outside
262
- this.BlockletChildState = config.BlockletChildState || null;
288
+ this.BlockletChildState = config.BlockletChildState;
263
289
  }
264
290
 
265
291
  /**
@@ -318,7 +344,7 @@ class BlockletState extends BaseState {
318
344
  }
319
345
 
320
346
  async loadChildren(blockletId) {
321
- if (!this.BlockletChildState || !blockletId) {
347
+ if (!blockletId) {
322
348
  return [];
323
349
  }
324
350
 
@@ -333,11 +359,6 @@ class BlockletState extends BaseState {
333
359
  * @param {Array} children - Array of children to save
334
360
  */
335
361
  async saveChildren(blockletId, blockletDid, children) {
336
- if (!this.BlockletChildState) {
337
- logger.warn('BlockletChildState is not initialized, cannot save children');
338
- return;
339
- }
340
-
341
362
  if (!blockletId || !blockletDid) {
342
363
  logger.warn('saveChildren called with invalid blockletId or blockletDid', { blockletId, blockletDid });
343
364
  return;
@@ -479,25 +500,24 @@ class BlockletState extends BaseState {
479
500
  * @memberof BlockletState
480
501
  */
481
502
  async getBlocklet(did, { decryptSk = true } = {}) {
482
- if (!did) {
503
+ if (!did || !isValidDid(did)) {
483
504
  return null;
484
505
  }
485
506
 
486
- // 优先使用缓存的 id 直接查询,避免 $or 查询
487
- const cachedId = process.env.NODE_ENV === 'test' ? null : this.didToIdMap.get(did);
488
- const doc = await this.findOne(cachedId ? { id: cachedId } : { $or: getConditions(did) });
507
+ const conditions = [{ appDid: did }, { appPid: did }, { 'meta.did': did }, { structV1Did: did }];
508
+ let doc = await this.findOne({ $or: conditions });
509
+
510
+ // If not found and DID is valid, try searching in migratedFrom
489
511
  if (!doc) {
490
- // 如果缓存的 id 查不到,可能是缓存失效,清除并重试
491
- if (cachedId) {
492
- this.didToIdMap.delete(did);
493
- return this.getBlocklet(did, { decryptSk });
512
+ const condition = getMigratedFromCondition(did, this.model.sequelize.getDialect());
513
+ const result = await this.model.findOne({ where: condition });
514
+ if (result) {
515
+ doc = result.toJSON();
494
516
  }
495
- return null;
496
517
  }
497
518
 
498
- // 缓存 did -> id 映射
499
- if (!cachedId && process.env.NODE_ENV !== 'test') {
500
- this.didToIdMap.set(did, doc.id);
519
+ if (!doc) {
520
+ return null;
501
521
  }
502
522
 
503
523
  // Load children from BlockletChild table
@@ -534,7 +554,15 @@ class BlockletState extends BaseState {
534
554
  return false;
535
555
  }
536
556
 
537
- const count = await this.count({ $or: getConditions(did) });
557
+ const conditions = [{ appDid: did }, { appPid: did }, { 'meta.did': did }, { structV1Did: did }];
558
+ let count = await this.count({ $or: conditions });
559
+
560
+ // If not found and DID is valid, try searching in migratedFrom
561
+ if (count === 0) {
562
+ const condition = getMigratedFromCondition(did, this.model.sequelize.getDialect());
563
+ count = await this.model.count({ where: condition });
564
+ }
565
+
538
566
  return count > 0;
539
567
  }
540
568
 
@@ -548,10 +576,8 @@ class BlockletState extends BaseState {
548
576
 
549
577
  // Batch load all children in a single query (instead of N+1 queries)
550
578
  let childrenMap = new Map();
551
- if (this.BlockletChildState) {
552
- const blockletIds = validDocs.map((doc) => doc.id);
553
- childrenMap = await this.BlockletChildState.getChildrenByParentIds(blockletIds);
554
- }
579
+ const blockletIds = validDocs.map((doc) => doc.id);
580
+ childrenMap = await this.BlockletChildState.getChildrenByParentIds(blockletIds);
555
581
 
556
582
  return validDocs.map((doc) => {
557
583
  doc.children = this._processLoadedChildren(childrenMap.get(doc.id) || [], doc.id);
@@ -559,25 +585,158 @@ class BlockletState extends BaseState {
559
585
  });
560
586
  }
561
587
 
588
+ /**
589
+ * Process all blocklets in paginated batches without loading children
590
+ * Optimized for bulk operations that only need specific fields (e.g., appPid)
591
+ * Memory-efficient: only loads one batch at a time instead of all 10K records
592
+ *
593
+ * @param {Object} options
594
+ * @param {Object} [options.query={}] - Query conditions
595
+ * @param {Object} [options.projection={}] - Fields to select (e.g., { appPid: 1 })
596
+ * @param {number} [options.batchSize=100] - Number of records per batch
597
+ * @param {Function} options.onBatch - Async callback for each batch: (batch) => Promise<void>
598
+ * @returns {Promise<void>}
599
+ *
600
+ * @example
601
+ * await blockletState.forEachBatch({
602
+ * projection: { appPid: 1 },
603
+ * batchSize: 100,
604
+ * onBatch: async (blocklets) => {
605
+ * await Promise.all(blocklets.map(b => processBlocklet(b.appPid)));
606
+ * }
607
+ * });
608
+ */
609
+ async forEachBatch({ query = {}, projection = {}, batchSize = 100, onBatch } = {}) {
610
+ if (typeof onBatch !== 'function') {
611
+ throw new Error('onBatch callback is required');
612
+ }
613
+
614
+ let page = 1;
615
+ let hasMore = true;
616
+
617
+ while (hasMore) {
618
+ // Use paginate from BaseState - does NOT load children
619
+ const result = await this.paginate(query, { createdAt: 1 }, { page, pageSize: batchSize }, projection, {
620
+ noCount: page > 1, // Only count on first page for efficiency
621
+ });
622
+
623
+ if (result.list.length > 0) {
624
+ await onBatch(result.list);
625
+ }
626
+
627
+ // Check if there are more pages
628
+ if (page === 1) {
629
+ hasMore = page < result.paging.pageCount;
630
+ } else {
631
+ // After first page, check by list length
632
+ hasMore = result.list.length === batchSize;
633
+ }
634
+ page++;
635
+ }
636
+ }
637
+
638
+ /**
639
+ * Paginated blocklet query with server-side search
640
+ * @param {Object} params - Query parameters
641
+ * @param {string} [params.search] - Search text (matches name, title, appDid, appPid)
642
+ * @param {boolean} [params.external] - Filter by external blocklets (true/false/undefined for all)
643
+ * @param {Object} [params.paging] - Pagination parameters { page, pageSize }
644
+ * @param {Object} [params.sort] - Sort parameters { field, direction }
645
+ * @returns {Promise<{ list: BlockletState[], paging: Paging }>}
646
+ */
647
+ async findPaginated({ search, external, paging, sort } = {}) {
648
+ // Allowed sort fields whitelist
649
+ const ALLOWED_SORT_FIELDS = ['installedAt', 'updatedAt', 'status'];
650
+ const ALLOWED_SORT_DIRECTIONS = ['asc', 'desc'];
651
+
652
+ const conditions = { where: {} };
653
+
654
+ // Server-side search (case-insensitive)
655
+ if (search?.trim()) {
656
+ const searchTerm = search.trim().toLowerCase();
657
+ // Escape special characters for LIKE pattern
658
+ const escapedSearch = searchTerm.replace(/[%_]/g, '\\$&');
659
+ const searchLike = `%${escapedSearch}%`;
660
+
661
+ conditions.where[Op.or] = [
662
+ // 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
+ }),
666
+ // Search in appDid
667
+ Sequelize.where(Sequelize.fn('lower', Sequelize.col('appDid')), {
668
+ [Op.like]: searchLike,
669
+ }),
670
+ // Search in appPid
671
+ Sequelize.where(Sequelize.fn('lower', Sequelize.col('appPid')), {
672
+ [Op.like]: searchLike,
673
+ }),
674
+ ];
675
+ }
676
+
677
+ // Filter by external/internal (based on controller field)
678
+ if (typeof external === 'boolean') {
679
+ // skip this for now
680
+ // conditions.where.controller = external ? { [Op.not]: null } : null;
681
+ }
682
+
683
+ // Validate and build sort order
684
+ let sortOrder = { updatedAt: -1 }; // default sort
685
+
686
+ if (sort?.field) {
687
+ // Validate sort field against whitelist, fallback to updatedAt if invalid
688
+ const isValidField = ALLOWED_SORT_FIELDS.includes(sort.field);
689
+ const sortField = isValidField ? sort.field : 'updatedAt';
690
+
691
+ if (!isValidField) {
692
+ logger.warn(
693
+ `Invalid sort field: ${sort.field}. Falling back to updatedAt. Allowed: ${ALLOWED_SORT_FIELDS.join(', ')}`
694
+ );
695
+ }
696
+
697
+ // Validate sort direction, fallback to desc if invalid
698
+ const direction = sort.direction?.toLowerCase();
699
+ const isValidDirection = direction && ALLOWED_SORT_DIRECTIONS.includes(direction);
700
+ const validDirection = isValidDirection ? direction : 'desc';
701
+
702
+ if (direction && !isValidDirection) {
703
+ logger.warn(
704
+ `Invalid sort direction: ${sort.direction}. Falling back to desc. Allowed: ${ALLOWED_SORT_DIRECTIONS.join(', ')}`
705
+ );
706
+ }
707
+
708
+ sortOrder = { [sortField]: validDirection === 'asc' ? 1 : -1 };
709
+ }
710
+
711
+ // Use inherited paginate method
712
+ const result = await this.paginate(conditions, sortOrder, { pageSize: 10, ...paging });
713
+
714
+ // Batch load children (existing pattern)
715
+ if (result.list.length > 0) {
716
+ const blockletIds = result.list.map((doc) => doc.id);
717
+ const childrenMap = await this.BlockletChildState.getChildrenByParentIds(blockletIds);
718
+ result.list = result.list.map((doc) => {
719
+ doc.children = this._processLoadedChildren(childrenMap.get(doc.id) || [], doc.id);
720
+ return formatBlocklet(doc, 'onRead', this.config.dek);
721
+ });
722
+ } else {
723
+ result.list = result.list.map((doc) => formatBlocklet(doc, 'onRead', this.config.dek));
724
+ }
725
+
726
+ return result;
727
+ }
728
+
562
729
  async deleteBlocklet(did) {
563
730
  const doc = await this.getBlocklet(did);
564
731
  if (!doc) {
565
732
  throw new CustomError(404, `Try to remove non-existing blocklet ${did}`);
566
733
  }
567
734
 
568
- // Delete children from BlockletChild table
569
- if (this.BlockletChildState) {
570
- await this.BlockletChildState.deleteByParentId(doc.id);
571
- }
572
-
735
+ await this.BlockletChildState.deleteByParentId(doc.id);
573
736
  await this.remove({ id: doc.id });
574
737
 
575
738
  this.didMap.delete(doc.meta?.did);
576
739
  this.didMap.delete(doc.appDid);
577
- this.didToIdMap.delete(did);
578
- this.didToIdMap.delete(doc.meta?.did);
579
- this.didToIdMap.delete(doc.appDid);
580
- this.didToIdMap.delete(doc.appPid);
581
740
  this.statusLocks.delete(doc.meta?.did);
582
741
  this.statusLocks.delete(doc.appDid);
583
742
 
@@ -618,9 +777,12 @@ class BlockletState extends BaseState {
618
777
  sanitized = ensureMeta(sanitized);
619
778
  sanitized = omit(sanitized, ['htmlAst']);
620
779
 
621
- // get ports
622
- const ports = await this.getBlockletPorts({ interfaces: sanitized.interfaces || [] });
623
- const children = await this.fillChildrenPorts(rawChildren, { defaultPort: getMaxPort(ports) });
780
+ // DEPRECATED: App-level port allocation removed
781
+ // Only components (children) need ports as they are the ones that run processes
782
+ // App is just a container and doesn't bind to any ports
783
+ const children = await this.fillChildrenPorts(rawChildren, {
784
+ defaultPort: this.defaultPort,
785
+ });
624
786
  fixChildren(children);
625
787
  children.forEach((x) => {
626
788
  x.installedAt = new Date();
@@ -635,7 +797,7 @@ class BlockletState extends BaseState {
635
797
  status,
636
798
  source,
637
799
  deployedFrom,
638
- ports,
800
+ ports: {}, // DEPRECATED: App-level ports no longer allocated
639
801
  environments: [],
640
802
  migratedFrom,
641
803
  externalSk,
@@ -671,9 +833,7 @@ class BlockletState extends BaseState {
671
833
  // This ensures getBlockletStatus can correctly calculate status based on children
672
834
  if (updates.children !== undefined) {
673
835
  updated.children = updates.children;
674
- if (this.BlockletChildState) {
675
- await this.saveChildren(updated.id, updated.meta.did, updates.children);
676
- }
836
+ await this.saveChildren(updated.id, updated.meta.did, updates.children);
677
837
  }
678
838
 
679
839
  updated.status = getBlockletStatus(updated);
@@ -709,22 +869,18 @@ class BlockletState extends BaseState {
709
869
 
710
870
  const sanitized = validateBlockletMeta(meta);
711
871
 
712
- // get ports
872
+ // DEPRECATED: App-level port allocation removed
873
+ // Only components (children) need ports as they are the ones that run processes
713
874
 
714
- const ports = await this.getBlockletPorts({
715
- interfaces: sanitized.interfaces || [],
716
- skipOccupiedCheckPorts: getExternalPortsFromMeta(doc.meta),
717
- });
718
- Object.keys(ports).forEach((p) => {
719
- ports[p] = doc.ports[p] || ports[p];
875
+ // fill children ports
876
+ logger.info('Fill children ports when upgrading blocklet', { name: doc.meta.name, did: doc.meta.did });
877
+ await this.fillChildrenPorts(children, {
878
+ oldChildren: doc.children,
879
+ defaultPort: this.defaultPort,
720
880
  });
721
881
 
722
- // fill
723
- logger.info('Fill children ports when when upgrading blocklet', { name: doc.meta.name, did: doc.meta.did });
724
- await this.fillChildrenPorts(children, { oldChildren: doc.children, defaultPort: getMaxPort(ports) });
725
-
726
882
  if (manualChildPorts?.length) {
727
- logger.info('Fill manual child ports when when upgrading blocklet', {
883
+ logger.info('Fill manual child ports when upgrading blocklet', {
728
884
  did: doc.meta.did,
729
885
  manualChildPorts,
730
886
  });
@@ -743,12 +899,11 @@ class BlockletState extends BaseState {
743
899
 
744
900
  fixChildren(children);
745
901
 
746
- // add to db
902
+ // add to db - no longer updating app-level ports
747
903
  const newDoc = await this.updateBlocklet(meta.did, {
748
904
  meta: omit(sanitized, ['htmlAst']),
749
905
  source,
750
906
  deployedFrom,
751
- ports,
752
907
  });
753
908
 
754
909
  // Save children to BlockletChild table
@@ -771,6 +926,7 @@ class BlockletState extends BaseState {
771
926
  skipOccupiedCheck = false,
772
927
  skipOccupiedCheckPorts = [],
773
928
  defaultPort = 0,
929
+ blockletDid = '',
774
930
  } = {}) {
775
931
  try {
776
932
  const { occupiedExternalPorts, occupiedInternalPorts } = await this._getOccupiedPorts();
@@ -820,8 +976,9 @@ class BlockletState extends BaseState {
820
976
  const newPorts = await assignPortsWithLock({
821
977
  wantedPorts: portsToAssign,
822
978
  blackListPorts,
823
- portRange: [10000, 31000],
979
+ portRange: DEFAULT_PORT_RANGE,
824
980
  defaultPort: Math.max(Number(this.defaultPort), defaultPort),
981
+ blockletDid,
825
982
  });
826
983
 
827
984
  // 合并已分配的端口和新分配的端口
@@ -834,6 +991,7 @@ class BlockletState extends BaseState {
834
991
 
835
992
  /**
836
993
  * refresh ports for blocklet if occupied during starting workflow
994
+ * OPTIMIZATION: Uses parallel port checks with concurrency limit for better performance at scale
837
995
  * @returns {Promise<{refreshed: boolean, componentDids: string[], isInitialAssignment?: boolean}>} 返回是否真正刷新了端口,isInitialAssignment 表示是否是首次分配(非冲突刷新)
838
996
  */
839
997
  async refreshBlockletPorts(did, componentDids = [], isGreen = false) {
@@ -845,78 +1003,122 @@ class BlockletState extends BaseState {
845
1003
  const { occupiedExternalPorts, occupiedInternalPorts } = await this._getOccupiedPorts();
846
1004
  const blackListPorts = [...Array.from(occupiedExternalPorts.keys()), ...Array.from(occupiedInternalPorts.keys())];
847
1005
 
1006
+ // OPTIMIZATION: Collect all components that need processing first
1007
+ const componentsToProcess = [];
1008
+ await forEachComponentV2(blocklet, (component) => {
1009
+ if (!shouldSkipComponent(component.meta.did, componentDids)) {
1010
+ componentsToProcess.push(component);
1011
+ }
1012
+ });
1013
+
1014
+ if (componentsToProcess.length === 0) {
1015
+ return { refreshed: false, componentDids: [], isInitialAssignment: false };
1016
+ }
1017
+
848
1018
  const actuallyRefreshedDids = [];
849
- let hasInitialAssignment = false; // 标记是否有首次分配(非冲突刷新)
1019
+ let hasInitialAssignment = false;
1020
+
1021
+ // OPTIMIZATION: Check port availability in parallel with concurrency limit
1022
+ const PORT_CHECK_CONCURRENCY = 10;
1023
+ const checkPortAvailability = async (ports) => {
1024
+ const portValues = Object.values(ports);
1025
+ if (portValues.length === 0) return false;
1026
+
1027
+ // Check ports in parallel batches
1028
+ for (let i = 0; i < portValues.length; i += PORT_CHECK_CONCURRENCY) {
1029
+ const batch = portValues.slice(i, i + PORT_CHECK_CONCURRENCY);
1030
+ const results = await Promise.all(batch.map((port) => isPortTaken(port)));
1031
+ if (results.some((taken) => taken)) {
1032
+ return true; // At least one port is taken
1033
+ }
1034
+ }
1035
+ return false;
1036
+ };
850
1037
 
851
- await forEachComponentV2(blocklet, async (component) => {
852
- if (!shouldSkipComponent(component.meta.did, componentDids)) {
1038
+ // Process components: first pass - check which need reassignment (parallel)
1039
+ const componentChecks = await Promise.all(
1040
+ componentsToProcess.map(async (component) => {
853
1041
  let oldPorts = component[isGreen ? 'greenPorts' : 'ports'];
854
1042
  const hasOldPorts = oldPorts && Object.keys(oldPorts).length > 0;
855
1043
 
856
- // 如果是 green 环境且 greenPorts 为空,需要分配新端口(基于 ports 的端口名)
857
- // 这是首次分配,不是冲突刷新
1044
+ // Green environment initial assignment
858
1045
  if (isGreen && !hasOldPorts) {
859
- // 从 ports 获取端口名,分配新的 green 端口
860
1046
  const basePorts = component.ports || {};
861
1047
  if (Object.keys(basePorts).length > 0) {
862
- const wantedPorts = Object.keys(basePorts);
863
- const newPorts = await assignPortsWithLock({
864
- wantedPorts,
865
- blackListPorts: [...blackListPorts, ...Object.values(basePorts)],
866
- portRange: [10000, 31000],
867
- defaultPort: this.defaultPort,
868
- });
869
-
870
- component.greenPorts = newPorts;
871
- actuallyRefreshedDids.push(component.meta.did);
872
- hasInitialAssignment = true; // 标记为首次分配
1048
+ return { component, action: 'initial_green', basePorts };
873
1049
  }
874
- return;
1050
+ return { component, action: 'skip' };
875
1051
  }
876
1052
 
877
- // 如果 oldPorts 为空,回退到 ports
1053
+ // Fallback to ports if oldPorts is empty
878
1054
  if (!hasOldPorts) {
879
1055
  oldPorts = component.ports;
880
1056
  }
881
1057
 
882
- // 检查旧端口是否真的被占用
883
- let needReassign = false;
884
- for (const port of Object.values(oldPorts)) {
885
- const isTaken = await isPortTaken(port);
886
- if (isTaken) {
887
- needReassign = true;
888
- break;
889
- }
1058
+ if (!oldPorts || Object.keys(oldPorts).length === 0) {
1059
+ return { component, action: 'skip' };
890
1060
  }
891
1061
 
892
- // 只有旧端口被占用时才分配新端口
1062
+ // Check if any port is taken (parallel within component)
1063
+ const needReassign = await checkPortAvailability(oldPorts);
893
1064
  if (needReassign) {
894
- const wantedPorts = Object.keys(oldPorts);
895
- const newPorts = await assignPortsWithLock({
896
- wantedPorts,
897
- blackListPorts: [...blackListPorts, ...Object.values(oldPorts)],
898
- portRange: [10000, 31000],
899
- defaultPort: this.defaultPort,
900
- });
901
-
902
- component[isGreen ? 'greenPorts' : 'ports'] = newPorts;
903
- actuallyRefreshedDids.push(component.meta.did);
1065
+ return { component, action: 'reassign', oldPorts };
904
1066
  }
1067
+
1068
+ return { component, action: 'skip' };
1069
+ })
1070
+ );
1071
+
1072
+ // Second pass - assign new ports for components that need it (sequential due to lock)
1073
+ // Track ports assigned in this batch to prevent duplicate assignments
1074
+ const assignedInThisBatch = [];
1075
+
1076
+ for (const { component, action, basePorts, oldPorts } of componentChecks) {
1077
+ if (action === 'skip') continue;
1078
+
1079
+ // Use combined parent/component DID for deterministic per-component port assignment
1080
+ const componentBlockletDid = [blocklet.meta.did, component.meta.did].join('/');
1081
+
1082
+ if (action === 'initial_green') {
1083
+ const wantedPorts = Object.keys(basePorts);
1084
+ const newPorts = await assignPortsWithLock({
1085
+ wantedPorts,
1086
+ blackListPorts: [...blackListPorts, ...Object.values(basePorts), ...assignedInThisBatch],
1087
+ portRange: DEFAULT_PORT_RANGE,
1088
+ defaultPort: this.defaultPort,
1089
+ blockletDid: componentBlockletDid,
1090
+ });
1091
+
1092
+ component.greenPorts = newPorts;
1093
+ actuallyRefreshedDids.push(component.meta.did);
1094
+ hasInitialAssignment = true;
1095
+ assignedInThisBatch.push(...Object.values(newPorts));
1096
+ } else if (action === 'reassign') {
1097
+ const wantedPorts = Object.keys(oldPorts);
1098
+ const newPorts = await assignPortsWithLock({
1099
+ wantedPorts,
1100
+ blackListPorts: [...blackListPorts, ...Object.values(oldPorts), ...assignedInThisBatch],
1101
+ portRange: DEFAULT_PORT_RANGE,
1102
+ defaultPort: this.defaultPort,
1103
+ blockletDid: componentBlockletDid,
1104
+ });
1105
+
1106
+ component[isGreen ? 'greenPorts' : 'ports'] = newPorts;
1107
+ actuallyRefreshedDids.push(component.meta.did);
1108
+ assignedInThisBatch.push(...Object.values(newPorts));
905
1109
  }
906
- });
1110
+ }
907
1111
 
908
1112
  if (actuallyRefreshedDids.length > 0) {
909
1113
  await this.updateBlocklet(did, {});
910
1114
 
911
1115
  // Only update ports/greenPorts to avoid overwriting status during concurrent operations
912
- if (this.BlockletChildState) {
913
- for (const component of blocklet.children) {
914
- if (actuallyRefreshedDids.includes(component.meta?.did)) {
915
- await this.BlockletChildState.updateChildPorts(blocklet.id, component.meta.did, {
916
- ports: component.ports,
917
- greenPorts: component.greenPorts,
918
- });
919
- }
1116
+ for (const component of blocklet.children) {
1117
+ if (actuallyRefreshedDids.includes(component.meta?.did)) {
1118
+ await this.BlockletChildState.updateChildPorts(blocklet.id, component.meta.did, {
1119
+ ports: component.ports,
1120
+ greenPorts: component.greenPorts,
1121
+ });
920
1122
  }
921
1123
  }
922
1124
  }
@@ -924,51 +1126,78 @@ class BlockletState extends BaseState {
924
1126
  return {
925
1127
  refreshed: actuallyRefreshedDids.length > 0,
926
1128
  componentDids: actuallyRefreshedDids,
927
- isInitialAssignment: hasInitialAssignment, // 返回是否是首次分配
1129
+ isInitialAssignment: hasInitialAssignment,
928
1130
  };
929
1131
  }
930
1132
 
1133
+ /**
1134
+ * Get all services from blocklet children with SERVICE type interfaces
1135
+ * Optimized: Single query to BlockletChildren table with DB-level JSON filtering
1136
+ * @returns {Promise<Array<{name: string, protocol: string, port: number, upstreamPort: number}>>}
1137
+ */
931
1138
  async getServices() {
932
- const blocklets = await this.getBlocklets({}, { id: 1, meta: 1, ports: 1 });
1139
+ // Single optimized query: only children with SERVICE interfaces, only needed fields
1140
+ const children = await this.BlockletChildState.getChildrenWithServiceInterfaces();
1141
+
933
1142
  const services = [];
1143
+ const portSet = new Set();
1144
+
1145
+ for (const child of children) {
1146
+ const serviceInterfaces = ((child.meta || {}).interfaces || []).filter(
1147
+ (x) => x.type === BLOCKLET_INTERFACE_TYPE_SERVICE
1148
+ );
934
1149
 
935
- blocklets.forEach((blocklet) => {
936
- const list = getBlockletServices(blocklet);
937
- list.forEach((x) => {
1150
+ for (const iface of serviceInterfaces) {
1151
+ const portCfg = iface.port || {};
1152
+ // Use greenPorts when green environment is running, otherwise use regular ports
1153
+ const isGreenRunning = child.greenStatus === BlockletStatus.running;
1154
+ const activePorts = isGreenRunning ? child.greenPorts || {} : child.ports || {};
1155
+
1156
+ let externalPort = Number(portCfg.external);
1157
+ const upstreamPort = Number(activePorts[portCfg.internal]);
1158
+
1159
+ // Port redirection support
938
1160
  // 如果本地 53 端口被系统占用,调试可以配置: export ABT_NODE_REDIRECTION_SERVICE_PORTS="53:10053,553:101553" 等多个 Service 端口重定向
939
1161
  if (process.env.ABT_NODE_REDIRECTION_SERVICE_PORTS) {
940
1162
  const redirectionPorts = process.env.ABT_NODE_REDIRECTION_SERVICE_PORTS.split(',');
941
- redirectionPorts.forEach((portString) => {
1163
+ for (const portString of redirectionPorts) {
942
1164
  const [portA, portB] = portString.split(':');
943
- if (x.port === +portA) {
944
- x.port = +portB;
1165
+ if (externalPort === +portA) {
1166
+ externalPort = +portB;
1167
+ break;
945
1168
  }
946
- });
947
- }
948
- if (!x.port) {
949
- logger.error('Missing service port', { appId: blocklet.meta.did, x });
950
- return;
1169
+ }
951
1170
  }
952
1171
 
953
- if (!x.protocol) {
954
- logger.error('Missing service protocol', { appId: blocklet.meta.did, x });
955
- return;
1172
+ // Validation
1173
+ if (!externalPort) {
1174
+ logger.error('Missing service port', { iface, childDid: child.meta?.did });
1175
+ continue;
956
1176
  }
957
-
958
- if (!x.upstreamPort) {
959
- logger.error('Missing service upstreamPort', { appId: blocklet.meta.did, x });
960
- return;
1177
+ if (!iface.protocol) {
1178
+ logger.error('Missing service protocol', { iface, childDid: child.meta?.did });
1179
+ continue;
1180
+ }
1181
+ if (!upstreamPort) {
1182
+ logger.error('Missing service upstreamPort', { iface, ports: activePorts, childDid: child.meta?.did });
1183
+ continue;
961
1184
  }
962
1185
 
963
- if (services.find((s) => s.port === x.port)) {
964
- // should not be here
965
- logger.error('Duplicate service port', { appId: blocklet.meta.did, x });
966
- return;
1186
+ // Dedup by external port
1187
+ if (portSet.has(externalPort)) {
1188
+ logger.error('Duplicate service port', { port: externalPort, childDid: child.meta?.did });
1189
+ continue;
967
1190
  }
1191
+ portSet.add(externalPort);
968
1192
 
969
- services.push(x);
970
- });
971
- });
1193
+ services.push({
1194
+ name: iface.name,
1195
+ protocol: iface.protocol,
1196
+ port: externalPort,
1197
+ upstreamPort,
1198
+ });
1199
+ }
1200
+ }
972
1201
 
973
1202
  return services;
974
1203
  }
@@ -976,8 +1205,8 @@ class BlockletState extends BaseState {
976
1205
  /**
977
1206
  * @return {Object} { <did> : { interfaceName } }
978
1207
  */
979
- async groupAllInterfaces() {
980
- const blocklets = await this.getBlocklets({}, { id: 1, meta: 1 });
1208
+ async groupAllInterfaces(blocklets) {
1209
+ const _blocklets = blocklets || (await this.getBlocklets({}, { id: 1, meta: 1 }));
981
1210
  const result = {};
982
1211
  const fillResult = (component, { id }) => {
983
1212
  const { interfaces } = component.meta;
@@ -990,7 +1219,7 @@ class BlockletState extends BaseState {
990
1219
  });
991
1220
  };
992
1221
 
993
- blocklets.forEach((blocklet) => {
1222
+ _blocklets.forEach((blocklet) => {
994
1223
  forEachBlockletSync(blocklet, fillResult);
995
1224
  });
996
1225
 
@@ -1002,14 +1231,7 @@ class BlockletState extends BaseState {
1002
1231
  * This overrides BaseState.reset() to handle foreign key constraints
1003
1232
  */
1004
1233
  async reset() {
1005
- // First, delete all children to avoid foreign key constraint errors
1006
- if (this.BlockletChildState) {
1007
- const allBlocklets = await this.getBlocklets({}, { id: 1 });
1008
- for (const blocklet of allBlocklets) {
1009
- await this.BlockletChildState.deleteByParentId(blocklet.id);
1010
- }
1011
- }
1012
- // Then call parent reset to clear the blocklets table
1234
+ await this.BlockletChildState.reset();
1013
1235
  return super.reset();
1014
1236
  }
1015
1237
 
@@ -1093,7 +1315,7 @@ class BlockletState extends BaseState {
1093
1315
  const res = await this.updateBlocklet(did, updateData);
1094
1316
 
1095
1317
  // Update each component's status individually using BlockletChildState
1096
- if (this.BlockletChildState && componentsToUpdate.length > 0) {
1318
+ if (componentsToUpdate.length > 0) {
1097
1319
  for (const componentDid of componentsToUpdate) {
1098
1320
  await this.BlockletChildState.updateChildStatus(doc.id, componentDid, {
1099
1321
  status,
@@ -1140,10 +1362,12 @@ class BlockletState extends BaseState {
1140
1362
  // get skipOccupiedCheckPorts
1141
1363
  const skipOccupiedCheckPorts = oldChild ? getExternalPortsFromMeta(oldChild.meta) : [];
1142
1364
 
1365
+ // Use child's own DID for hash-based port allocation (deterministic per component)
1143
1366
  const ports = await this.getBlockletPorts({
1144
1367
  interfaces: childMeta.interfaces || [],
1145
1368
  defaultPort: _maxPort,
1146
1369
  skipOccupiedCheckPorts,
1370
+ blockletDid: child.meta.did,
1147
1371
  });
1148
1372
  _maxPort = getMaxPort(ports);
1149
1373
 
@@ -1241,33 +1465,41 @@ class BlockletState extends BaseState {
1241
1465
  return this.updateBlocklet(did, { structV1Did: v1Did });
1242
1466
  }
1243
1467
 
1468
+ /**
1469
+ * Get all occupied ports by reading directly from BlockletChild table
1470
+ * - Internal ports: From component's ports/greenPorts fields
1471
+ * - External ports: From component's meta.interfaces
1472
+ */
1244
1473
  async _getOccupiedPorts() {
1245
- const blocklets = await this.getBlocklets({}, { id: 1, port: 1, ports: 1, meta: 1 });
1246
-
1247
1474
  const occupiedExternalPorts = new Map();
1248
1475
  const occupiedInternalPorts = new Map();
1249
1476
 
1250
- const calcPortsFromBlocklet = (blocklet) => {
1251
- occupiedInternalPorts.set(Number(blocklet.port), true);
1477
+ // Read directly from BlockletChild table - only components bind to ports
1478
+ const children = await this.BlockletChildState.getAllPorts();
1252
1479
 
1253
- if (blocklet.ports && typeof blocklet.ports === 'object') {
1254
- Object.keys(blocklet.ports).forEach((key) => {
1255
- occupiedInternalPorts.set(Number(blocklet.ports[key]), true);
1480
+ for (const child of children) {
1481
+ // Internal ports from component's ports field
1482
+ if (child.ports && typeof child.ports === 'object') {
1483
+ Object.values(child.ports).forEach((port) => {
1484
+ if (port) occupiedInternalPorts.set(Number(port), true);
1256
1485
  });
1257
1486
  }
1258
-
1259
- if (Array.isArray(blocklet.meta.interfaces)) {
1260
- blocklet.meta.interfaces.forEach((x) => {
1487
+ // Green ports as well
1488
+ if (child.greenPorts && typeof child.greenPorts === 'object') {
1489
+ Object.values(child.greenPorts).forEach((port) => {
1490
+ if (port) occupiedInternalPorts.set(Number(port), true);
1491
+ });
1492
+ }
1493
+ // External ports from component's meta.interfaces
1494
+ if (Array.isArray(child.meta?.interfaces)) {
1495
+ child.meta.interfaces.forEach((x) => {
1261
1496
  if (x.port && x.port.external) {
1262
1497
  occupiedExternalPorts.set(Number(x.port.external), true);
1263
1498
  }
1264
1499
  });
1265
1500
  }
1266
- };
1267
-
1268
- for (const blocklet of blocklets) {
1269
- await forEachBlocklet(blocklet, calcPortsFromBlocklet);
1270
1501
  }
1502
+
1271
1503
  return {
1272
1504
  occupiedExternalPorts,
1273
1505
  occupiedInternalPorts,