@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.
- package/lib/blocklet/manager/disk.js +74 -32
- package/lib/blocklet/manager/ensure-blocklet-running.js +1 -1
- package/lib/blocklet/manager/helper/blue-green-start-blocklet.js +1 -1
- package/lib/blocklet/manager/helper/install-application-from-general.js +2 -3
- package/lib/blocklet/manager/helper/install-component-from-url.js +7 -4
- package/lib/blocklet/migration-dist/migration.cjs +5 -4
- package/lib/blocklet/passport/index.js +10 -3
- package/lib/blocklet/project/index.js +7 -2
- package/lib/blocklet/security/index.js +2 -2
- package/lib/cert.js +6 -3
- package/lib/event/index.js +98 -87
- package/lib/event/util.js +7 -13
- package/lib/index.js +15 -26
- package/lib/migrations/1.5.0-site.js +3 -7
- package/lib/migrations/1.5.15-site.js +3 -7
- package/lib/monitor/blocklet-runtime-monitor.js +37 -5
- package/lib/monitor/node-runtime-monitor.js +4 -4
- package/lib/router/helper.js +525 -452
- package/lib/router/index.js +280 -104
- package/lib/router/manager.js +14 -28
- package/lib/states/blocklet-child.js +93 -1
- package/lib/states/blocklet-extras.js +1 -1
- package/lib/states/blocklet.js +429 -197
- package/lib/states/node.js +0 -10
- package/lib/states/site.js +87 -4
- package/lib/team/manager.js +2 -21
- package/lib/util/blocklet.js +39 -19
- package/lib/util/get-accessible-external-node-ip.js +21 -6
- package/lib/util/index.js +3 -3
- package/lib/util/ip.js +15 -1
- package/lib/util/launcher.js +11 -11
- package/lib/util/ready.js +2 -9
- package/lib/util/reset-node.js +6 -5
- package/lib/validators/router.js +0 -3
- package/lib/webhook/sender/api/index.js +5 -0
- package/package.json +23 -25
- package/lib/migrations/1.0.36-snapshot.js +0 -10
- package/lib/migrations/1.1.9-snapshot.js +0 -7
- package/lib/states/routing-snapshot.js +0 -146
package/lib/states/blocklet.js
CHANGED
|
@@ -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 {
|
|
58
|
-
* @param {Array<number>} options.portRange - 端口范围,默认 [10000,
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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 (!
|
|
103
|
+
if (!usedPortsSet.has(candidatePort)) {
|
|
91
104
|
// eslint-disable-next-line no-await-in-loop
|
|
92
|
-
const isTaken = await isPortTaken(
|
|
105
|
+
const isTaken = await isPortTaken(candidatePort);
|
|
93
106
|
if (!isTaken) {
|
|
94
|
-
assignedPort =
|
|
95
|
-
|
|
96
|
-
|
|
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 (!
|
|
104
|
-
logger.warn('
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
499
|
-
|
|
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
|
|
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
|
-
|
|
552
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
872
|
+
// DEPRECATED: App-level port allocation removed
|
|
873
|
+
// Only components (children) need ports as they are the ones that run processes
|
|
713
874
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
852
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
1053
|
+
// Fallback to ports if oldPorts is empty
|
|
878
1054
|
if (!hasOldPorts) {
|
|
879
1055
|
oldPorts = component.ports;
|
|
880
1056
|
}
|
|
881
1057
|
|
|
882
|
-
|
|
883
|
-
|
|
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
|
-
|
|
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
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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
|
-
|
|
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
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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
|
-
|
|
1163
|
+
for (const portString of redirectionPorts) {
|
|
942
1164
|
const [portA, portB] = portString.split(':');
|
|
943
|
-
if (
|
|
944
|
-
|
|
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
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1172
|
+
// Validation
|
|
1173
|
+
if (!externalPort) {
|
|
1174
|
+
logger.error('Missing service port', { iface, childDid: child.meta?.did });
|
|
1175
|
+
continue;
|
|
956
1176
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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
|
-
|
|
964
|
-
|
|
965
|
-
logger.error('Duplicate service port', {
|
|
966
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
1251
|
-
|
|
1477
|
+
// Read directly from BlockletChild table - only components bind to ports
|
|
1478
|
+
const children = await this.BlockletChildState.getAllPorts();
|
|
1252
1479
|
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
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 (
|
|
1260
|
-
|
|
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,
|