@abtnode/core 1.17.7-beta-20251225-073259-cb6ecf68 → 1.17.7-beta-20251229-085620-84f09930

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 (41) hide show
  1. package/lib/blocklet/manager/disk.js +150 -36
  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 +18 -27
  14. package/lib/migrations/1.17.7-beta-2025122601-settings-authentication.js +19 -0
  15. package/lib/migrations/1.5.0-site.js +3 -7
  16. package/lib/migrations/1.5.15-site.js +3 -7
  17. package/lib/monitor/blocklet-runtime-monitor.js +37 -5
  18. package/lib/monitor/node-runtime-monitor.js +4 -4
  19. package/lib/router/helper.js +525 -452
  20. package/lib/router/index.js +280 -104
  21. package/lib/router/manager.js +14 -28
  22. package/lib/states/audit-log.js +6 -3
  23. package/lib/states/blocklet-child.js +93 -1
  24. package/lib/states/blocklet-extras.js +1 -1
  25. package/lib/states/blocklet.js +429 -197
  26. package/lib/states/node.js +0 -10
  27. package/lib/states/site.js +87 -4
  28. package/lib/team/manager.js +2 -21
  29. package/lib/util/blocklet.js +71 -37
  30. package/lib/util/get-accessible-external-node-ip.js +21 -6
  31. package/lib/util/index.js +3 -3
  32. package/lib/util/ip.js +15 -1
  33. package/lib/util/launcher.js +11 -11
  34. package/lib/util/ready.js +2 -9
  35. package/lib/util/reset-node.js +6 -5
  36. package/lib/validators/router.js +0 -3
  37. package/lib/webhook/sender/api/index.js +5 -0
  38. package/package.json +35 -37
  39. package/lib/migrations/1.0.36-snapshot.js +0 -10
  40. package/lib/migrations/1.1.9-snapshot.js +0 -7
  41. package/lib/states/routing-snapshot.js +0 -146
@@ -200,16 +200,6 @@ class NodeState extends BaseState {
200
200
  return doc;
201
201
  }
202
202
 
203
- async updateNodeRouting(entity = {}) {
204
- if (isEmpty(entity)) {
205
- throw new CustomError(400, 'empty entity');
206
- }
207
- const nodeInfo = await this.updateNodeInfo({ routing: entity });
208
- this.emit(EVENTS.ROUTING_UPDATED, nodeInfo);
209
-
210
- return nodeInfo;
211
- }
212
-
213
203
  cleanupDirtyMaintainState() {
214
204
  return this.read().then((doc) => {
215
205
  if (doc.nextVersion && semver.lte(doc.nextVersion, doc.version)) {
@@ -1,4 +1,6 @@
1
1
  const logger = require('@abtnode/logger')('@abtnode/core:states:site');
2
+ const { Op, Sequelize } = require('sequelize');
3
+ const { BLOCKLET_SITE_GROUP_SUFFIX } = require('@abtnode/constant');
2
4
 
3
5
  const BaseState = require('./base');
4
6
  const { getBlockletDomainGroupName } = require('../util/router');
@@ -46,6 +48,17 @@ class SiteState extends BaseState {
46
48
  return SiteState.renameIdFiledName(result);
47
49
  }
48
50
 
51
+ async getSystemSites() {
52
+ const result = await this.find({
53
+ where: {
54
+ domain: {
55
+ [Op.notLike]: `%${BLOCKLET_SITE_GROUP_SUFFIX}`,
56
+ },
57
+ },
58
+ });
59
+ return SiteState.renameIdFiledName(result);
60
+ }
61
+
49
62
  async getSitesByBlocklet(did) {
50
63
  const sites = await this.getSites();
51
64
  return sites.filter((x) => x.rules.some((r) => r.to?.did === did));
@@ -74,14 +87,84 @@ class SiteState extends BaseState {
74
87
  return site.rules.find((x) => x.id === id);
75
88
  }
76
89
 
90
+ /**
91
+ * Check if domain exists as primary domain or alias
92
+ * Optimized: O(1) for primary domain, efficient DB query for alias
93
+ * - PostgreSQL: Uses jsonb @> operator (can leverage GIN index)
94
+ * - SQLite: Uses json_each and json_extract for reliable array element search
95
+ */
77
96
  async domainExists(domain) {
78
- const sites = await this.getSites();
79
- return sites.some((x) => x.domain === domain || x.domainAliases.some((y) => y.value === domain));
97
+ // Fast path: check if domain is a primary domain (indexed query)
98
+ const byPrimaryDomain = await this.findOne({ domain });
99
+ if (byPrimaryDomain) {
100
+ return true;
101
+ }
102
+
103
+ const dialect = this.model.sequelize.getDialect();
104
+ if (dialect === 'postgres') {
105
+ // PostgreSQL: Use jsonb @> operator for efficient array containment check
106
+ // This can leverage GIN index on domainAliases if available
107
+ // Use JSON.stringify to safely construct JSON, then escape single quotes for SQL
108
+ const searchJson = JSON.stringify([{ value: domain }]).replace(/'/g, "''");
109
+ const count = await this.count({
110
+ where: Sequelize.literal(`"domainAliases" @> '${searchJson}'::jsonb`),
111
+ });
112
+ return count > 0;
113
+ }
114
+
115
+ // SQLite: Use json_each and json_extract for reliable array element search
116
+ // This avoids issues with JSON formatting/spacing in LIKE queries
117
+ const escapedDomain = domain.replace(/'/g, "''");
118
+ const count = await this.count({
119
+ where: Sequelize.literal(`
120
+ EXISTS (
121
+ SELECT 1 FROM json_each("domainAliases")
122
+ WHERE json_extract(value, '$.value') = '${escapedDomain}'
123
+ )
124
+ `),
125
+ });
126
+
127
+ return count > 0;
80
128
  }
81
129
 
130
+ /**
131
+ * Find site by domain alias
132
+ * Optimized: O(1) for primary domain, efficient DB query for alias
133
+ * - PostgreSQL: Uses jsonb @> operator (can leverage GIN index)
134
+ * - SQLite: Uses json_each and json_extract for reliable array element search
135
+ */
82
136
  async findByDomainAlias(domain) {
83
- const sites = await this.getSites();
84
- return sites.find((x) => x.domainAliases.some((y) => y.value === domain));
137
+ // Fast path: check if it's actually a primary domain
138
+ const byPrimaryDomain = await this.findOne({ domain });
139
+ if (byPrimaryDomain) {
140
+ return byPrimaryDomain;
141
+ }
142
+
143
+ const dialect = this.model.sequelize.getDialect();
144
+ if (dialect === 'postgres') {
145
+ // PostgreSQL: Use jsonb @> operator for efficient array containment check
146
+ // This can leverage GIN index on domainAliases if available
147
+ // Use JSON.stringify to safely construct JSON, then escape single quotes for SQL
148
+ const searchJson = JSON.stringify([{ value: domain }]).replace(/'/g, "''");
149
+ const sites = await this.find({
150
+ where: Sequelize.literal(`"domainAliases" @> '${searchJson}'::jsonb`),
151
+ });
152
+ return sites[0] || null;
153
+ }
154
+
155
+ // SQLite: Use json_each and json_extract for reliable array element search
156
+ // This avoids issues with JSON formatting/spacing in LIKE queries
157
+ const escapedDomain = domain.replace(/'/g, "''");
158
+ const sites = await this.find({
159
+ where: Sequelize.literal(`
160
+ EXISTS (
161
+ SELECT 1 FROM json_each("domainAliases")
162
+ WHERE json_extract(value, '$.value') = '${escapedDomain}'
163
+ )
164
+ `),
165
+ });
166
+
167
+ return sites[0] || null;
85
168
  }
86
169
 
87
170
  findOneByBlocklet(did) {
@@ -118,25 +118,6 @@ class TeamManager extends EventEmitter {
118
118
  }
119
119
 
120
120
  async init() {
121
- // listen blocklet state
122
- ['add', 'upgrade'].forEach((event) => {
123
- this.states.blocklet.on(event, ({ meta: { did } }) => {
124
- this.cache[did] = getDefaultTeamState();
125
- });
126
- });
127
-
128
- // init blocklet
129
- this.states.blocklet
130
- .getBlocklets()
131
- .then((blocklets) => {
132
- blocklets.forEach(({ meta: { did } }) => {
133
- this.cache[did] = getDefaultTeamState();
134
- });
135
- })
136
- .catch((error) => {
137
- logger.error('get blocklets failed', error);
138
- });
139
-
140
121
  // init server
141
122
  this.cache[this.nodeDid] = getDefaultTeamState();
142
123
  logger.info('init node team manager', { nodeDid: this.nodeDid });
@@ -347,7 +328,7 @@ class TeamManager extends EventEmitter {
347
328
  }
348
329
  }
349
330
 
350
- const domains = await getDomainsByDid(isServices ? teamDid : this.nodeDid);
331
+ const domains = await getDomainsByDid(isServices ? teamDid : this.nodeDid, this);
351
332
  const customDomains = domains.filter((d) => isCustomDomain(d));
352
333
 
353
334
  // 优先显示自定义域名
@@ -1035,7 +1016,7 @@ class TeamManager extends EventEmitter {
1035
1016
  const dbPath = await this.getDataFileByDid(did);
1036
1017
  logger.info('initDatabase', { did, dbPath });
1037
1018
  try {
1038
- await doSchemaMigration(dbPath, 'blocklet');
1019
+ await doSchemaMigration(dbPath, 'blocklet', false, `blocklet:${did}`);
1039
1020
  } catch (error) {
1040
1021
  // This error is not fatal, just log it, will happen when there are multiple service processes
1041
1022
  logger.error('initDatabase failed', { did, dbPath, error });
@@ -25,6 +25,7 @@ const isUrl = require('is-url');
25
25
  const semver = require('semver');
26
26
  const { chainInfo: chainInfoSchema } = require('@arcblock/did-connect-js/lib/schema');
27
27
 
28
+ const { types } = require('@ocap/mcrypto');
28
29
  const { urlPathFriendly } = require('@blocklet/meta/lib/url-path-friendly');
29
30
  const { fromSecretKey, fromPublicKey } = require('@ocap/wallet');
30
31
  const { toHex, isHex, toDid, toAddress, toBuffer } = require('@ocap/util');
@@ -65,7 +66,7 @@ const { getComponentApiKey } = require('@abtnode/util/lib/blocklet');
65
66
  const { toSvg: createDidLogo } = require('@arcblock/did-motif');
66
67
  const { createBlockiesSvg } = require('@blocklet/meta/lib/blockies');
67
68
  const formatName = require('@abtnode/util/lib/format-name');
68
- const { hasMountPoint } = require('@blocklet/meta/lib/engine');
69
+ const { hasMountPoint, getBlockletEngine } = require('@blocklet/meta/lib/engine');
69
70
  const { fixAvatar } = require('@blocklet/sdk/lib/util/user');
70
71
 
71
72
  const SCRIPT_ENGINES_WHITE_LIST = ['npm', 'npx', 'pnpm', 'yarn'];
@@ -87,9 +88,9 @@ const {
87
88
  BLOCKLET_TENANT_MODES,
88
89
  PROJECT,
89
90
  BLOCKLET_INTERFACE_TYPE_DOCKER,
91
+ STATIC_SERVER_ENGINE_DID,
90
92
  } = require('@blocklet/constant');
91
93
  const { validateBlockletEntry } = require('@blocklet/meta/lib/entry');
92
- const { getBlockletEngine } = require('@blocklet/meta/lib/engine');
93
94
  const { getBlockletInfo } = require('@blocklet/meta/lib/info');
94
95
  const { getApplicationWallet: getBlockletWallet } = require('@blocklet/meta/lib/wallet');
95
96
  const {
@@ -360,7 +361,7 @@ const getComponentStartEngine = (component, { e2eMode = false } = {}) => {
360
361
  const cwd = appDir;
361
362
 
362
363
  // get app dirs
363
- const { main, group } = component.meta;
364
+ const { group } = component.meta;
364
365
 
365
366
  let startFromDevEntry = '';
366
367
  if (component.mode === BLOCKLET_MODES.DEVELOPMENT && component.meta.scripts) {
@@ -386,9 +387,6 @@ const getComponentStartEngine = (component, { e2eMode = false } = {}) => {
386
387
  } else if (group === 'dapp') {
387
388
  script = blockletEngineInfo.source || BLOCKLET_ENTRY_FILE;
388
389
  args = blockletEngineInfo.args || [];
389
- } else if (group === 'static') {
390
- script = require.resolve('@abtnode/static-server');
391
- environmentObj.BLOCKLET_MAIN_DIR = path.join(appDir, main);
392
390
  }
393
391
 
394
392
  if (component.mode !== BLOCKLET_MODES.DEVELOPMENT) {
@@ -1116,6 +1114,16 @@ const deleteBlockletProcess = async (
1116
1114
  if (!hasStartEngine(b.meta)) {
1117
1115
  return;
1118
1116
  }
1117
+
1118
+ // Skip deleting static-server engine processes since they were never started
1119
+ if (b.meta?.group === 'static') {
1120
+ return;
1121
+ }
1122
+ const engine = getBlockletEngine(b.meta);
1123
+ if (engine.interpreter === 'blocklet' && engine.source?.name === STATIC_SERVER_ENGINE_DID) {
1124
+ return;
1125
+ }
1126
+
1119
1127
  await preDelete(b, { ancestors });
1120
1128
  if (isStopGreenAndBlue) {
1121
1129
  // eslint-disable-next-line no-use-before-define
@@ -1150,6 +1158,15 @@ const reloadBlockletProcess = (blocklet, { componentDids } = {}) =>
1150
1158
  return;
1151
1159
  }
1152
1160
 
1161
+ // Skip reloading static-server engine processes since they were never started
1162
+ if (b.meta?.group === 'static') {
1163
+ return;
1164
+ }
1165
+ const engine = getBlockletEngine(b.meta);
1166
+ if (engine.interpreter === 'blocklet' && engine.source?.name === STATIC_SERVER_ENGINE_DID) {
1167
+ return;
1168
+ }
1169
+
1153
1170
  // eslint-disable-next-line no-use-before-define
1154
1171
  await reloadProcess(b.env.processId);
1155
1172
  logger.info('done reload process', { processId: b.env.processId });
@@ -2244,19 +2261,20 @@ const validateAppConfig = async (config, states) => {
2244
2261
 
2245
2262
  if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK) {
2246
2263
  if (x.value) {
2264
+ let wallet;
2247
2265
  try {
2248
- fromSecretKey(x.value);
2266
+ wallet = fromSecretKey(x.value, { role: types.RoleType.ROLE_APPLICATION });
2249
2267
  } catch {
2250
2268
  try {
2251
- fromSecretKey(x.value, 'eth');
2269
+ wallet = fromSecretKey(x.value, 'eth');
2252
2270
  } catch {
2253
2271
  throw new Error('Invalid custom blocklet secret key');
2254
2272
  }
2255
2273
  }
2256
2274
 
2257
2275
  // Ensure sk is not used by existing blocklets, otherwise we may encounter appDid collision
2258
- const blocklets = await states.blocklet.getBlocklets({});
2259
- if (blocklets.some((b) => isBlockletAppSkUsed(b, x.value))) {
2276
+ const exist = await states.blocklet.hasBlocklet(wallet.address);
2277
+ if (exist) {
2260
2278
  throw new Error('Invalid custom blocklet secret key: already used by existing blocklet');
2261
2279
  }
2262
2280
  } else {
@@ -2380,14 +2398,18 @@ const checkDuplicateAppSk = async ({ sk, did, states }) => {
2380
2398
  appSk = wallet.secretKey;
2381
2399
  }
2382
2400
 
2383
- const blocklets = await states.blocklet.getBlocklets({});
2384
- const others = did ? blocklets.filter((b) => b.meta.did !== did) : blocklets;
2385
-
2386
- const exist = others.find((b) => {
2387
- const item = (b.environments || []).find((e) => e.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK);
2388
- return item?.value === toHex(appSk);
2389
- });
2401
+ let wallet;
2402
+ try {
2403
+ wallet = fromSecretKey(appSk, { role: types.RoleType.ROLE_APPLICATION });
2404
+ } catch {
2405
+ try {
2406
+ wallet = fromSecretKey(appSk, 'eth');
2407
+ } catch {
2408
+ throw new Error('Invalid custom blocklet secret key');
2409
+ }
2410
+ }
2390
2411
 
2412
+ const exist = await states.blocklet.hasBlocklet(wallet.address);
2391
2413
  if (exist) {
2392
2414
  throw new Error(`blocklet secret key already used by ${exist.meta.title || exist.meta.name}`);
2393
2415
  }
@@ -2434,6 +2456,21 @@ const validateStore = (nodeInfo, storeUrl) => {
2434
2456
  }
2435
2457
 
2436
2458
  const storeUrlObj = new URL(storeUrl);
2459
+
2460
+ // Check trusted blocklet sources from environment variable first
2461
+ const trustedSources = process.env.ABT_NODE_TRUSTED_SOURCES;
2462
+ if (trustedSources) {
2463
+ const trustedHosts = trustedSources
2464
+ .split(',')
2465
+ .map((url) => url.trim())
2466
+ .filter(Boolean)
2467
+ .map((url) => new URL(url).host);
2468
+
2469
+ if (trustedHosts.includes(storeUrlObj.host)) {
2470
+ return;
2471
+ }
2472
+ }
2473
+
2437
2474
  const registerUrlObj = new URL(nodeInfo.registerUrl);
2438
2475
 
2439
2476
  // 信任 Launcher 打包的应用
@@ -2804,7 +2841,7 @@ const publishDidDocument = async ({ blocklet, ownerInfo, nodeInfo }) => {
2804
2841
  const name = blocklet.meta?.title || blocklet.meta?.name;
2805
2842
  const state = fromBlockletStatus(blocklet.status);
2806
2843
 
2807
- let launcher = null;
2844
+ let launcher;
2808
2845
  if (!isEmpty(blocklet.controller)) {
2809
2846
  launcher = {
2810
2847
  did: toDid(blocklet.controller.did || nodeInfo.registerInfo.appPid), // 目前 controller 没有 launcher 的元信息, 默认在 nodeInfo 中存储
@@ -2847,11 +2884,14 @@ const publishDidDocument = async ({ blocklet, ownerInfo, nodeInfo }) => {
2847
2884
  })
2848
2885
  );
2849
2886
 
2850
- const owner = {
2851
- did: toDid(ownerInfo.did),
2852
- name: ownerInfo.fullName,
2853
- avatar: fixAvatar(ownerInfo.avatar),
2854
- };
2887
+ let owner;
2888
+ if (ownerInfo) {
2889
+ owner = {
2890
+ did: toDid(ownerInfo.did),
2891
+ name: ownerInfo.fullName,
2892
+ avatar: fixAvatar(ownerInfo.avatar),
2893
+ };
2894
+ }
2855
2895
 
2856
2896
  return didDocument.updateBlockletDocument({
2857
2897
  blocklet,
@@ -2878,21 +2918,15 @@ const updateDidDocument = async ({ did, nodeInfo, teamManager, states }) => {
2878
2918
 
2879
2919
  blocklet.site = await states.site.findOneByBlocklet(did);
2880
2920
  blocklet.settings = await states.blockletExtras.getSettings(did);
2881
- blocklet.controller = blockletExtra.controller;
2882
-
2883
- logger.debug('update did document', { blocklet });
2921
+ blocklet.controller = blockletExtra?.controller;
2884
2922
 
2885
- const ownerDid = blocklet.settings.owner.did;
2886
- logger.info('get owner info', { ownerDid, teamDid: blocklet.appPid });
2887
- const userState = await teamManager.getUserState(blocklet.appPid);
2888
- const ownerInfo = await userState.getUser(ownerDid);
2889
-
2890
- logger.info('got user info', {
2891
- owner: ownerInfo,
2892
- blockletDid: blocklet.meta.did,
2893
- ownerDid,
2894
- teamDid: blocklet.appPid,
2895
- });
2923
+ const ownerDid = blocklet.settings?.owner?.did;
2924
+ let ownerInfo;
2925
+ if (ownerDid) {
2926
+ logger.info('get owner info', { ownerDid, teamDid: blocklet.appPid });
2927
+ const userState = await teamManager.getUserState(blocklet.appPid);
2928
+ ownerInfo = await userState.getUser(ownerDid);
2929
+ }
2896
2930
 
2897
2931
  return publishDidDocument({ blocklet, ownerInfo, nodeInfo });
2898
2932
  };
@@ -9,7 +9,8 @@ const getNodeDomain = (ip) => (ip ? DEFAULT_IP_DOMAIN.replace(/^\*/, ip.replace(
9
9
  let cache = null;
10
10
  let cacheAt = 0;
11
11
  let cacheMissCount = 0;
12
- const cacheMissTTL = 1000 * 60 * 10; // 10 minutes
12
+ let pendingFetch = null; // Prevents concurrent fetches
13
+ const cacheMissTTL = 1000 * 60 * 30; // 30 minutes
13
14
 
14
15
  const timeout = process.env.NODE_ENV === 'test' ? 500 : 5000;
15
16
 
@@ -40,19 +41,22 @@ const checkConnected = async ({ ip, nodeInfo }) => {
40
41
  */
41
42
  const fetch = async (nodeInfo) => {
42
43
  const { external, internal } = await getIp();
43
- logger.info('refresh external ip:', external);
44
-
45
44
  if ([external, internal].includes(cache)) {
45
+ logger.info('reuse cached accessible ip:', { cache });
46
46
  return cache;
47
47
  }
48
48
 
49
+ logger.info('refresh accessible ip:', { external, internal, cache });
50
+
49
51
  // prefer external ip
50
52
  try {
51
53
  if (external) {
52
54
  await checkConnected({ ip: external, nodeInfo });
53
55
  cache = external;
56
+ logger.info('cache external ip as accessible ip', { external });
54
57
  }
55
- } catch {
58
+ } catch (err) {
59
+ logger.error('failed to check external ip', { external, error: err });
56
60
  cacheMissCount += 1;
57
61
  cache = null;
58
62
  }
@@ -63,8 +67,10 @@ const fetch = async (nodeInfo) => {
63
67
  if (internal) {
64
68
  await checkConnected({ ip: internal, nodeInfo });
65
69
  cache = internal;
70
+ logger.info('cache internal ip as accessible ip', { internal });
66
71
  }
67
- } catch {
72
+ } catch (err) {
73
+ logger.error('failed to check internal ip', { internal, error: err });
68
74
  cacheMissCount += 1;
69
75
  cache = null;
70
76
  }
@@ -86,7 +92,16 @@ module.exports.getFromCache = (nodeInfo) => {
86
92
  return cache;
87
93
  }
88
94
  if (nodeInfo) {
89
- return cache || fetch(nodeInfo);
95
+ if (cache) {
96
+ return cache;
97
+ }
98
+ // Use pending promise to prevent concurrent fetches
99
+ if (!pendingFetch) {
100
+ pendingFetch = fetch(nodeInfo).finally(() => {
101
+ pendingFetch = null;
102
+ });
103
+ }
104
+ return pendingFetch;
90
105
  }
91
106
  return cache;
92
107
  };
package/lib/util/index.js CHANGED
@@ -29,10 +29,10 @@ const {
29
29
  DEFAULT_HTTPS_PORT,
30
30
  SLOT_FOR_IP_DNS_SITE,
31
31
  BLOCKLET_SITE_GROUP_SUFFIX,
32
+ DEFAULT_WELLKNOWN_PORT,
32
33
  } = require('@abtnode/constant');
33
34
  const { BLOCKLET_CONFIGURABLE_KEY } = require('@blocklet/constant');
34
35
 
35
- const DEFAULT_WELLKNOWN_PORT = 8088;
36
36
  const APP_CONFIG_IMAGE_KEYS = [
37
37
  BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_LOGO_FAVICON,
38
38
  BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SPLASH_PORTRAIT,
@@ -219,8 +219,8 @@ const getBaseUrls = async (node, ips) => {
219
219
  return { protocol, port };
220
220
  };
221
221
 
222
- if (info.routing.provider && info.routing.snapshotHash && info.routing.adminPath) {
223
- const sites = await node.getSitesFromSnapshot();
222
+ if (info.routing.provider && info.routing.adminPath) {
223
+ const sites = await node.getSitesFromState('system');
224
224
  const { ipWildcardDomain } = info.routing;
225
225
  const adminPath = normalizePathPrefix(info.routing.adminPath);
226
226
  const tmpHttpPort = getPort(httpPort, DEFAULT_HTTP_PORT);
package/lib/util/ip.js CHANGED
@@ -8,6 +8,8 @@ const { promisify } = require('util');
8
8
  const lookup = promisify(dns.lookup);
9
9
 
10
10
  let cache = null;
11
+ let pendingFetch = null; // Prevents concurrent fetches
12
+
11
13
  const fetch = async (args = {}) => {
12
14
  try {
13
15
  const start = Date.now();
@@ -21,7 +23,19 @@ const fetch = async (args = {}) => {
21
23
  return cache;
22
24
  };
23
25
 
24
- const get = (args) => cache || fetch(args);
26
+ const get = (args) => {
27
+ if (cache) {
28
+ return cache;
29
+ }
30
+ // Use pending promise to prevent concurrent fetches
31
+ if (!pendingFetch) {
32
+ pendingFetch = fetch(args).finally(() => {
33
+ pendingFetch = null;
34
+ });
35
+ }
36
+ return pendingFetch;
37
+ };
38
+
25
39
  const cron = {
26
40
  name: 'refetch-ip',
27
41
  time: '0 */30 * * * *', // refetch every 30 minutes
@@ -166,17 +166,16 @@ const getCpuUtilization = async () => {
166
166
 
167
167
  const getComponentsAggregate = async () => {
168
168
  try {
169
- const list = await states.blocklet.getBlocklets();
169
+ // Use efficient SQL GROUP BY COUNT instead of loading all children into memory
170
+ // This is O(1) memory vs O(n) for the old implementation
171
+ const { total, counts: statusCounts } = await states.blockletChild.getStatusCounts();
172
+
173
+ // Convert numeric status keys to string keys using fromBlockletStatus
170
174
  const counts = {};
171
- let total = 0;
172
- list.forEach((b) => {
173
- const children = Array.isArray(b.children) ? b.children : [];
174
- children.forEach((child) => {
175
- const key = fromBlockletStatus(child.status) || 'unknown';
176
- counts[key] = (counts[key] || 0) + 1;
177
- total += 1;
178
- });
179
- });
175
+ for (const [status, count] of Object.entries(statusCounts)) {
176
+ const key = fromBlockletStatus(Number(status)) || 'unknown';
177
+ counts[key] = (counts[key] || 0) + count;
178
+ }
180
179
 
181
180
  return { total, counts };
182
181
  } catch (error) {
@@ -660,7 +659,7 @@ const launchBlockletByLauncher = async (node, extraParams, context) => {
660
659
  };
661
660
 
662
661
  const launchBlockletWithoutWallet = async (node, extraParams, context) => {
663
- logger.debug('launchBlockletWithoutWallet', { extraParams, context });
662
+ logger.info('launchBlockletWithoutWallet start', { extraParams });
664
663
 
665
664
  extraParams.locale = context.locale || 'en';
666
665
 
@@ -679,6 +678,7 @@ const launchBlockletWithoutWallet = async (node, extraParams, context) => {
679
678
  description,
680
679
  },
681
680
  };
681
+ logger.info('launchBlockletWithoutWallet meta fetched', { blocklet });
682
682
 
683
683
  if (!blocklet) {
684
684
  throw new Error('Blocklet not found');
package/lib/util/ready.js CHANGED
@@ -3,18 +3,11 @@ const logger = require('@abtnode/logger')('@abtnode/core:ready');
3
3
  const chalk = require('chalk');
4
4
 
5
5
  const createStateReadyHandler =
6
- (routingSnapshot) =>
7
- async ({ states, options }) => {
8
- const snapshotHash = await routingSnapshot.init();
9
-
6
+ () =>
7
+ ({ states, options }) => {
10
8
  return states.node
11
9
  .read()
12
10
  .then(async (state) => {
13
- if (snapshotHash && !state.routing.snapshotHash) {
14
- logger.info('set snapshot hash because its empty');
15
- await states.node.updateNodeRouting({ ...state.routing, snapshotHash });
16
- }
17
-
18
11
  // Set default sender/receiver for notification center
19
12
  states.notification.setDefaultSender(state.did);
20
13
  if (state.nodeOwner) {
@@ -62,7 +62,8 @@ const resetDirs = () => {
62
62
 
63
63
  /* istanbul ignore next */
64
64
  const resetBlocklets = async ({ context, blockletManager }) => {
65
- const blocklets = await blockletManager.list({ includeRuntimeInfo: false }, context);
65
+ const result = await blockletManager.list({ includeRuntimeInfo: false }, context);
66
+ const blocklets = result.blocklets || [];
66
67
  for (let i = 0; i < blocklets.length; i++) {
67
68
  const blocklet = blocklets[i];
68
69
  // eslint-disable-next-line no-await-in-loop
@@ -75,7 +76,7 @@ const resetBlocklets = async ({ context, blockletManager }) => {
75
76
  };
76
77
 
77
78
  /* istanbul ignore next */
78
- const resetSites = async ({ context, routerManager, takeRoutingSnapshot }) => {
79
+ const resetSites = async ({ context, routerManager, handleAllRouting }) => {
79
80
  const sites = await states.site.getSites();
80
81
  for (let i = 0; i < sites.length; i++) {
81
82
  const site = sites[i];
@@ -87,7 +88,7 @@ const resetSites = async ({ context, routerManager, takeRoutingSnapshot }) => {
87
88
  }
88
89
  }
89
90
 
90
- const hash = await takeRoutingSnapshot({ message: 'reset routing sites for test', dryRun: false }, context);
91
+ const hash = await handleAllRouting({ message: 'reset routing sites for test' });
91
92
  logger.info('reset routing sites', { hash });
92
93
  };
93
94
 
@@ -153,7 +154,7 @@ module.exports = async ({
153
154
  context,
154
155
  blockletManager,
155
156
  routerManager,
156
- takeRoutingSnapshot,
157
+ handleAllRouting,
157
158
  teamManager,
158
159
  certManager,
159
160
  }) => {
@@ -192,7 +193,7 @@ module.exports = async ({
192
193
  context,
193
194
  blockletManager,
194
195
  routerManager,
195
- takeRoutingSnapshot,
196
+ handleAllRouting,
196
197
  teamManager,
197
198
  certManager,
198
199
  });
@@ -103,9 +103,6 @@ const ruleSchema = {
103
103
  )
104
104
  .optional()
105
105
  .default([]),
106
-
107
- // root blocklet ruleId
108
- groupId: Joi.string(),
109
106
  };
110
107
 
111
108
  const corsSchema = Joi.array()
@@ -38,6 +38,11 @@ class APISender extends BaseSender {
38
38
  }
39
39
 
40
40
  async sendNotification(url, notification) {
41
+ if (process.env.NODE_ENV === 'test') {
42
+ return {
43
+ text: 'ok',
44
+ };
45
+ }
41
46
  try {
42
47
  const res = await axios.post(url, notification, { timeout: 10000 });
43
48
  return { text: res.statusText, code: res.status };