@abtnode/core 1.17.5-beta-20251209-090953-3a59e7ac → 1.17.5-beta-20251211-104355-426d7eb6
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 +49 -13
- package/lib/blocklet/manager/helper/blue-green-start-blocklet.js +100 -70
- package/lib/migrations/index.js +4 -4
- package/lib/monitor/blocklet-runtime-monitor.js +3 -5
- package/lib/states/blocklet-child.js +119 -0
- package/lib/states/blocklet.js +264 -11
- package/lib/states/index.js +4 -1
- package/lib/util/blocklet.js +110 -42
- package/lib/util/migration-sqlite-to-postgres.js +79 -6
- package/package.json +24 -24
|
@@ -905,6 +905,44 @@ class DiskBlockletManager extends BaseBlockletManager {
|
|
|
905
905
|
this.emit(BlockletEvents.statusChange, doc1);
|
|
906
906
|
const startedBlockletDids = [];
|
|
907
907
|
const errorBlockletDids = [];
|
|
908
|
+
const parentBlockletId = blocklet.id;
|
|
909
|
+
|
|
910
|
+
// Helper function to update child status immediately and emit events
|
|
911
|
+
const updateChildStatusImmediately = async (componentDid, status) => {
|
|
912
|
+
if (!states.blockletChild || !parentBlockletId) {
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
try {
|
|
917
|
+
await states.blockletChild.updateChildStatus(parentBlockletId, componentDid, status, false, {
|
|
918
|
+
operator,
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
// Get updated blocklet to emit events
|
|
922
|
+
const updatedBlocklet = await this.getBlocklet(did);
|
|
923
|
+
const componentsInfo = getComponentsInternalInfo(updatedBlocklet);
|
|
924
|
+
|
|
925
|
+
this.emit(BlockletInternalEvents.componentUpdated, {
|
|
926
|
+
appDid: blocklet.appDid,
|
|
927
|
+
components: componentsInfo.filter((c) => c.did === componentDid),
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
if (status === BlockletStatus.running) {
|
|
931
|
+
this.emit(BlockletInternalEvents.componentStarted, {
|
|
932
|
+
appDid: blocklet.appDid,
|
|
933
|
+
components: [{ did: componentDid }],
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
this.emit(BlockletEvents.statusChange, updatedBlocklet);
|
|
938
|
+
} catch (err) {
|
|
939
|
+
logger.error('Failed to update child status immediately', {
|
|
940
|
+
componentDid,
|
|
941
|
+
status,
|
|
942
|
+
error: err.message,
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
};
|
|
908
946
|
|
|
909
947
|
const notStartedComponentDids = await this.startRequiredComponents({
|
|
910
948
|
componentDids,
|
|
@@ -915,11 +953,13 @@ class DiskBlockletManager extends BaseBlockletManager {
|
|
|
915
953
|
e2eMode,
|
|
916
954
|
context,
|
|
917
955
|
atomic,
|
|
918
|
-
onStarted: (subDid) => {
|
|
956
|
+
onStarted: async (subDid) => {
|
|
919
957
|
startedBlockletDids.push({ did: subDid });
|
|
958
|
+
await updateChildStatusImmediately(subDid, BlockletStatus.running);
|
|
920
959
|
},
|
|
921
|
-
onError: (subDid, error) => {
|
|
960
|
+
onError: async (subDid, error) => {
|
|
922
961
|
errorBlockletDids.push({ did: subDid, error });
|
|
962
|
+
await updateChildStatusImmediately(subDid, BlockletStatus.error);
|
|
923
963
|
},
|
|
924
964
|
});
|
|
925
965
|
|
|
@@ -933,11 +973,13 @@ class DiskBlockletManager extends BaseBlockletManager {
|
|
|
933
973
|
e2eMode,
|
|
934
974
|
componentDids: [componentDid],
|
|
935
975
|
operator,
|
|
936
|
-
onStarted: (subDid) => {
|
|
976
|
+
onStarted: async (subDid) => {
|
|
937
977
|
startedBlockletDids.push({ did: subDid });
|
|
978
|
+
await updateChildStatusImmediately(subDid, BlockletStatus.running);
|
|
938
979
|
},
|
|
939
|
-
onError: (subDid, error) => {
|
|
980
|
+
onError: async (subDid, error) => {
|
|
940
981
|
errorBlockletDids.push({ did: subDid, error });
|
|
982
|
+
await updateChildStatusImmediately(subDid, BlockletStatus.error);
|
|
941
983
|
},
|
|
942
984
|
},
|
|
943
985
|
context
|
|
@@ -950,15 +992,12 @@ class DiskBlockletManager extends BaseBlockletManager {
|
|
|
950
992
|
let errorDescription = '';
|
|
951
993
|
let resultBlocklet = nextBlocklet;
|
|
952
994
|
|
|
995
|
+
// Status updates are now done immediately in callbacks, so we only need to handle final events and cleanup
|
|
953
996
|
if (startedBlockletDids.length) {
|
|
954
|
-
await states.blocklet.setBlockletStatus(did, BlockletStatus.running, {
|
|
955
|
-
componentDids: startedBlockletDids.map((x) => x.did),
|
|
956
|
-
operator,
|
|
957
|
-
});
|
|
958
|
-
|
|
959
997
|
const finalBlocklet = await this.getBlocklet(did);
|
|
960
998
|
resultBlocklet = finalBlocklet;
|
|
961
999
|
|
|
1000
|
+
// Sync app config after all components started
|
|
962
1001
|
await this.configSynchronizer.throttledSyncAppConfig(finalBlocklet, { wait: 200 });
|
|
963
1002
|
const componentsInfo = getComponentsInternalInfo(finalBlocklet);
|
|
964
1003
|
this.emit(BlockletInternalEvents.componentUpdated, {
|
|
@@ -1009,10 +1048,7 @@ class DiskBlockletManager extends BaseBlockletManager {
|
|
|
1009
1048
|
componentDids: errorBlockletDids.map((x) => x.did),
|
|
1010
1049
|
shouldUpdateBlockletStatus: false,
|
|
1011
1050
|
});
|
|
1012
|
-
|
|
1013
|
-
componentDids: errorBlockletDids.map((x) => x.did),
|
|
1014
|
-
operator,
|
|
1015
|
-
});
|
|
1051
|
+
|
|
1016
1052
|
const finalBlocklet = await this.getBlocklet(did);
|
|
1017
1053
|
resultBlocklet = finalBlocklet;
|
|
1018
1054
|
this.emit(BlockletEvents.startFailed, {
|
|
@@ -173,6 +173,97 @@ const blueGreenStartBlocklet = async (
|
|
|
173
173
|
|
|
174
174
|
const startedBlockletDids = [];
|
|
175
175
|
const errorBlockletDids = [];
|
|
176
|
+
const appId = blocklet1.id;
|
|
177
|
+
|
|
178
|
+
const notificationChange = async () => {
|
|
179
|
+
// Get latest children from blocklet_children table instead of reloading entire blocklet
|
|
180
|
+
const latestChildren = await states.blocklet.loadChildren(appId);
|
|
181
|
+
|
|
182
|
+
// Merge latest children into blocklet1 for event emission
|
|
183
|
+
const finalBlocklet = {
|
|
184
|
+
...blocklet1,
|
|
185
|
+
children: latestChildren,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
await manager.configSynchronizer.throttledSyncAppConfig(finalBlocklet);
|
|
189
|
+
const componentsInfo = getComponentsInternalInfo(finalBlocklet);
|
|
190
|
+
manager.emit(BlockletInternalEvents.componentUpdated, {
|
|
191
|
+
appDid: blocklet1.appDid,
|
|
192
|
+
components: componentsInfo,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
manager.emit(BlockletInternalEvents.componentStarted, {
|
|
196
|
+
appDid: blocklet1.appDid,
|
|
197
|
+
components: startedBlockletDids.map((x) => ({ did: x.did })),
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Emit statusChange event so UI can see the status change
|
|
201
|
+
manager.emit(BlockletEvents.statusChange, finalBlocklet);
|
|
202
|
+
|
|
203
|
+
manager.emit(BlockletEvents.started, { ...finalBlocklet, componentDids: startedBlockletDids.map((x) => x.did) });
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Helper function to update child status immediately and emit events
|
|
207
|
+
const updateChildStatusImmediately = async (componentDid, status, isGreen = false) => {
|
|
208
|
+
if (!states.blockletChild || !appId) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const updates = {
|
|
214
|
+
operator,
|
|
215
|
+
inProgressStart: Date.now(),
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
if (status === BlockletStatus.running) {
|
|
219
|
+
updates.startedAt = new Date();
|
|
220
|
+
updates.stoppedAt = null;
|
|
221
|
+
} else if (status === BlockletStatus.error || status === BlockletStatus.stopped) {
|
|
222
|
+
updates.stoppedAt = new Date();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
await states.blockletChild.updateChildStatus(appId, componentDid, status, isGreen, updates);
|
|
226
|
+
|
|
227
|
+
if (status === BlockletStatus.running) {
|
|
228
|
+
await states.blockletChild.updateChildStatus(appId, componentDid, BlockletStatus.stopped, !isGreen, updates);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Get latest children from blocklet_children table and emit events immediately
|
|
232
|
+
const latestChildren = await states.blocklet.loadChildren(appId);
|
|
233
|
+
const updatedBlocklet = {
|
|
234
|
+
...blocklet1,
|
|
235
|
+
children: latestChildren,
|
|
236
|
+
};
|
|
237
|
+
const componentsInfo = getComponentsInternalInfo(updatedBlocklet);
|
|
238
|
+
|
|
239
|
+
manager.emit(BlockletInternalEvents.componentUpdated, {
|
|
240
|
+
appDid: blocklet1.appDid,
|
|
241
|
+
components: componentsInfo.filter((c) => c.did === componentDid),
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
if (status === BlockletStatus.running) {
|
|
245
|
+
manager.emit(BlockletInternalEvents.componentStarted, {
|
|
246
|
+
appDid: blocklet1.appDid,
|
|
247
|
+
components: [{ did: componentDid }],
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
manager.emit(BlockletEvents.statusChange, updatedBlocklet);
|
|
252
|
+
|
|
253
|
+
// Emit statusChange event immediately so UI can see each component's status change
|
|
254
|
+
manager.emit(BlockletEvents.started, {
|
|
255
|
+
...updatedBlocklet,
|
|
256
|
+
componentDids: startedBlockletDids.map((x) => x.did),
|
|
257
|
+
});
|
|
258
|
+
} catch (err) {
|
|
259
|
+
logger.error('Failed to update child status immediately', {
|
|
260
|
+
componentDid,
|
|
261
|
+
status,
|
|
262
|
+
isGreen,
|
|
263
|
+
error: err.message,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
};
|
|
176
267
|
|
|
177
268
|
for (const item of blueGreenComponentIds) {
|
|
178
269
|
if (!item.componentDids.length) {
|
|
@@ -274,6 +365,11 @@ const blueGreenStartBlocklet = async (
|
|
|
274
365
|
// 收集成功的组件(排除已经在 errorBlockletDids 中的组件)
|
|
275
366
|
startedBlockletDids.push({ did: subDid, isGreen: item.changeToGreen });
|
|
276
367
|
|
|
368
|
+
// Update status immediately
|
|
369
|
+
await updateChildStatusImmediately(subDid, BlockletStatus.running, item.changeToGreen);
|
|
370
|
+
|
|
371
|
+
await manager.deleteProcess({ did, componentDids: [subDid], isGreen: !item.changeToGreen });
|
|
372
|
+
|
|
277
373
|
logger.info('Green environment started successfully', {
|
|
278
374
|
did,
|
|
279
375
|
componentDids: [subDid],
|
|
@@ -285,6 +381,9 @@ const blueGreenStartBlocklet = async (
|
|
|
285
381
|
// 收集失败的组件
|
|
286
382
|
errorBlockletDids.push({ did: subDid, error, isGreen: item.changeToGreen });
|
|
287
383
|
|
|
384
|
+
// Update status immediately
|
|
385
|
+
await updateChildStatusImmediately(subDid, BlockletStatus.error, item.changeToGreen);
|
|
386
|
+
|
|
288
387
|
try {
|
|
289
388
|
await manager.deleteProcess({ did, componentDids: [subDid], isGreen: item.changeToGreen });
|
|
290
389
|
} catch (cleanupError) {
|
|
@@ -326,37 +425,6 @@ const blueGreenStartBlocklet = async (
|
|
|
326
425
|
});
|
|
327
426
|
}
|
|
328
427
|
|
|
329
|
-
const greenBlockletDids = errorBlockletDids.filter((x) => x.isGreen).map((x) => x.did);
|
|
330
|
-
const blueBlockletDids = errorBlockletDids.filter((x) => !x.isGreen).map((x) => x.did);
|
|
331
|
-
|
|
332
|
-
if (greenBlockletDids.length) {
|
|
333
|
-
await manager.deleteProcess({
|
|
334
|
-
did,
|
|
335
|
-
componentDids: greenBlockletDids,
|
|
336
|
-
shouldUpdateBlockletStatus: false,
|
|
337
|
-
isGreen: true,
|
|
338
|
-
});
|
|
339
|
-
await states.blocklet.setBlockletStatus(did, BlockletStatus.error, {
|
|
340
|
-
componentDids: greenBlockletDids,
|
|
341
|
-
operator,
|
|
342
|
-
isGreen: true,
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (blueBlockletDids.length) {
|
|
347
|
-
await manager.deleteProcess({
|
|
348
|
-
did,
|
|
349
|
-
componentDids: blueBlockletDids,
|
|
350
|
-
shouldUpdateBlockletStatus: false,
|
|
351
|
-
isGreen: false,
|
|
352
|
-
});
|
|
353
|
-
await states.blocklet.setBlockletStatus(did, BlockletStatus.error, {
|
|
354
|
-
componentDids: blueBlockletDids,
|
|
355
|
-
operator,
|
|
356
|
-
isGreen: false,
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
|
|
360
428
|
const finalBlocklet = await manager.getBlocklet(did);
|
|
361
429
|
manager.emit(BlockletEvents.startFailed, {
|
|
362
430
|
...finalBlocklet,
|
|
@@ -368,45 +436,7 @@ const blueGreenStartBlocklet = async (
|
|
|
368
436
|
|
|
369
437
|
// 处理成功启动的组件
|
|
370
438
|
if (startedBlockletDids.length) {
|
|
371
|
-
|
|
372
|
-
const startedBlueBlockletDids = startedBlockletDids.filter((x) => !x.isGreen).map((x) => x.did);
|
|
373
|
-
|
|
374
|
-
if (startedGreenBlockletDids.length) {
|
|
375
|
-
await states.blocklet.setBlockletStatus(did, BlockletStatus.running, {
|
|
376
|
-
componentDids: startedGreenBlockletDids,
|
|
377
|
-
operator,
|
|
378
|
-
isGreen: true,
|
|
379
|
-
});
|
|
380
|
-
await manager.deleteProcess({ did, componentDids: startedGreenBlockletDids, isGreen: false });
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
if (startedBlueBlockletDids.length) {
|
|
384
|
-
await states.blocklet.setBlockletStatus(did, BlockletStatus.running, {
|
|
385
|
-
componentDids: startedBlueBlockletDids,
|
|
386
|
-
operator,
|
|
387
|
-
isGreen: false,
|
|
388
|
-
});
|
|
389
|
-
await manager.deleteProcess({ did, componentDids: startedBlueBlockletDids, isGreen: true });
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
const finalBlocklet = await manager.getBlocklet(did);
|
|
393
|
-
|
|
394
|
-
await manager.configSynchronizer.throttledSyncAppConfig(finalBlocklet);
|
|
395
|
-
const componentsInfo = getComponentsInternalInfo(finalBlocklet);
|
|
396
|
-
manager.emit(BlockletInternalEvents.componentUpdated, {
|
|
397
|
-
appDid: blocklet1.appDid,
|
|
398
|
-
components: componentsInfo,
|
|
399
|
-
});
|
|
400
|
-
manager.emit(BlockletInternalEvents.componentStarted, {
|
|
401
|
-
appDid: blocklet1.appDid,
|
|
402
|
-
components: startedBlockletDids.map((x) => ({ did: x.did })),
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
manager.emit(BlockletEvents.statusChange, finalBlocklet);
|
|
406
|
-
manager.emit(BlockletEvents.started, {
|
|
407
|
-
...finalBlocklet,
|
|
408
|
-
componentDids: startedBlockletDids.map((x) => x.did),
|
|
409
|
-
});
|
|
439
|
+
await notificationChange();
|
|
410
440
|
}
|
|
411
441
|
|
|
412
442
|
// 根据情况更新 route table, 会判断只有包含多 interfaces 的 DID 才会更新 route table
|
package/lib/migrations/index.js
CHANGED
|
@@ -169,11 +169,11 @@ const runSchemaMigrations = async ({
|
|
|
169
169
|
|
|
170
170
|
// migrate server schema
|
|
171
171
|
dbPaths.server = getDbFilePath(path.join(dataDir, 'core/server.db'));
|
|
172
|
-
await doSchemaMigration(dbPaths.server, 'server');
|
|
172
|
+
await doSchemaMigration(dbPaths.server, 'server', true);
|
|
173
173
|
printSuccess(`Server schema successfully migrated: ${dbPaths.server}`);
|
|
174
174
|
// migrate service schema
|
|
175
175
|
dbPaths.service = getDbFilePath(path.join(dataDir, 'services/service.db'));
|
|
176
|
-
await doSchemaMigration(dbPaths.service, 'service');
|
|
176
|
+
await doSchemaMigration(dbPaths.service, 'service', true);
|
|
177
177
|
printSuccess(`Service schema successfully migrated: ${dbPaths.service}`);
|
|
178
178
|
|
|
179
179
|
// migrate blocklet schema
|
|
@@ -183,7 +183,7 @@ const runSchemaMigrations = async ({
|
|
|
183
183
|
if (env) {
|
|
184
184
|
const filePath = getDbFilePath(path.join(env.value, 'blocklet.db'));
|
|
185
185
|
dbPaths.blocklets.push(filePath);
|
|
186
|
-
await doSchemaMigration(filePath, 'blocklet');
|
|
186
|
+
await doSchemaMigration(filePath, 'blocklet', true);
|
|
187
187
|
printSuccess(`Blocklet schema successfully migrated: ${blocklet.appPid}: ${filePath}`);
|
|
188
188
|
} else {
|
|
189
189
|
printInfo(`Skip migrate schema for blocklet: ${blocklet.appPid}`);
|
|
@@ -193,7 +193,7 @@ const runSchemaMigrations = async ({
|
|
|
193
193
|
// migrate certificate manager schema
|
|
194
194
|
for (let i = 0; i < MODULES.length; i++) {
|
|
195
195
|
const filePath = getDbFilePath(path.join(dataDir, `modules/${MODULES[i]}/module.db`));
|
|
196
|
-
await doSchemaMigration(filePath, MODULES[i]);
|
|
196
|
+
await doSchemaMigration(filePath, MODULES[i], true);
|
|
197
197
|
dbPaths.certificateManagers.push(filePath);
|
|
198
198
|
printSuccess(`${MODULES[i]} schema successfully migrated: ${filePath}`);
|
|
199
199
|
}
|
|
@@ -230,16 +230,14 @@ class BlockletRuntimeMonitor extends EventEmitter {
|
|
|
230
230
|
|
|
231
231
|
insertThrottleMap.set(blockletDid, now);
|
|
232
232
|
|
|
233
|
-
return this.states.runtimeInsight.insert({ did: blockletDid, ...value }).catch((
|
|
233
|
+
return this.states.runtimeInsight.insert({ did: blockletDid, ...value }).catch((error) => {
|
|
234
|
+
const err = typeof error === 'string' ? error : error.message;
|
|
234
235
|
if (err.includes('duplicate key value violates unique constraint ')) {
|
|
235
236
|
console.error('RuntimeInsight insert error duplicate key value violates unique constraint');
|
|
236
237
|
return;
|
|
237
238
|
}
|
|
238
239
|
if (err.name === 'SequelizeValidationError') {
|
|
239
|
-
console.error(
|
|
240
|
-
'RuntimeInsight validation error',
|
|
241
|
-
err.errors.map((e) => e.message)
|
|
242
|
-
);
|
|
240
|
+
console.error('RuntimeInsight validation error:', err);
|
|
243
241
|
} else {
|
|
244
242
|
console.error('RuntimeInsight insert error', err);
|
|
245
243
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/* eslint-disable no-underscore-dangle */
|
|
2
|
+
const { BlockletStatus } = require('@blocklet/constant');
|
|
3
|
+
const BaseState = require('./base');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @extends BaseState<import('@abtnode/models').BlockletChildState>
|
|
7
|
+
*/
|
|
8
|
+
class BlockletChildState extends BaseState {
|
|
9
|
+
constructor(model, config = {}) {
|
|
10
|
+
super(model, config);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get children by parent blocklet ID
|
|
15
|
+
* @param {string} parentBlockletId - The parent blocklet ID
|
|
16
|
+
* @returns {Promise<Array>} - Array of children
|
|
17
|
+
*/
|
|
18
|
+
async getChildrenByParentId(parentBlockletId) {
|
|
19
|
+
if (!parentBlockletId) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
const children = await this.find({ parentBlockletId }, {}, { createdAt: 1 });
|
|
23
|
+
return children || [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Delete children by parent blocklet ID
|
|
28
|
+
* @param {string} parentBlockletId - The parent blocklet ID
|
|
29
|
+
* @returns {Promise<number>} - Number of deleted children
|
|
30
|
+
*/
|
|
31
|
+
deleteByParentId(parentBlockletId) {
|
|
32
|
+
if (!parentBlockletId) {
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
return this.remove({ parentBlockletId });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Update child status by parent blocklet ID and child DID
|
|
40
|
+
* @param {string} parentBlockletId - The parent blocklet ID
|
|
41
|
+
* @param {string} childDid - The child DID
|
|
42
|
+
* @param {number} status - The status to set
|
|
43
|
+
* @param {Object} additionalFields - Additional fields to update
|
|
44
|
+
* @param {boolean} isGreen - Whether the child is green
|
|
45
|
+
* @returns {Promise<Object>} - Updated child
|
|
46
|
+
*/
|
|
47
|
+
async updateChildStatus(parentBlockletId, childDid, status, isGreen = false, additionalFields = {}) {
|
|
48
|
+
if (!parentBlockletId) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const children = await this.getChildrenByParentId(parentBlockletId);
|
|
52
|
+
const child = children.find((c) => c.childDid === childDid);
|
|
53
|
+
|
|
54
|
+
if (!child) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const updates = {
|
|
59
|
+
...additionalFields,
|
|
60
|
+
};
|
|
61
|
+
if (isGreen) {
|
|
62
|
+
updates.greenStatus = status;
|
|
63
|
+
} else {
|
|
64
|
+
updates.status = status;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (status === BlockletStatus.running) {
|
|
68
|
+
updates.startedAt = new Date();
|
|
69
|
+
updates.stoppedAt = null;
|
|
70
|
+
} else if (status === BlockletStatus.stopped) {
|
|
71
|
+
updates.startedAt = null;
|
|
72
|
+
updates.stoppedAt = new Date();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const [, [updated]] = await this.update({ id: child.id }, { $set: updates });
|
|
76
|
+
return updated;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Batch update children status
|
|
81
|
+
* @param {string} parentBlockletId - The parent blocklet ID
|
|
82
|
+
* @param {Array<string>} childDids - Array of child DIDs to update
|
|
83
|
+
* @param {number} status - The status to set
|
|
84
|
+
* @param {Object} additionalFields - Additional fields to update
|
|
85
|
+
* @returns {Promise<Array>} - Updated children
|
|
86
|
+
*/
|
|
87
|
+
async batchUpdateChildrenStatus(parentBlockletId, childDids, status, additionalFields = {}) {
|
|
88
|
+
if (!parentBlockletId) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
const children = await this.getChildrenByParentId(parentBlockletId);
|
|
92
|
+
const updates = [];
|
|
93
|
+
|
|
94
|
+
for (const child of children) {
|
|
95
|
+
if (childDids.includes(child.childDid)) {
|
|
96
|
+
const childUpdates = {
|
|
97
|
+
status,
|
|
98
|
+
...additionalFields,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
if (status === BlockletStatus.running) {
|
|
102
|
+
childUpdates.startedAt = new Date();
|
|
103
|
+
childUpdates.stoppedAt = null;
|
|
104
|
+
} else if (status === BlockletStatus.stopped) {
|
|
105
|
+
childUpdates.startedAt = null;
|
|
106
|
+
childUpdates.stoppedAt = new Date();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// eslint-disable-next-line no-await-in-loop
|
|
110
|
+
await this.update({ id: child.id }, { $set: childUpdates });
|
|
111
|
+
updates.push({ ...child, ...childUpdates });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return updates;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = BlockletChildState;
|
package/lib/states/blocklet.js
CHANGED
|
@@ -145,6 +145,8 @@ const getExternalPortsFromMeta = (meta) =>
|
|
|
145
145
|
const formatBlocklet = (blocklet, phase, dek) => {
|
|
146
146
|
if (phase === 'onRead') {
|
|
147
147
|
blocklet.status = getBlockletStatus(blocklet);
|
|
148
|
+
// Ensure children exists before forEachBlockletSync accesses it
|
|
149
|
+
blocklet.children = blocklet.children || [];
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
forEachBlockletSync(blocklet, (b) => {
|
|
@@ -253,6 +255,200 @@ class BlockletState extends BaseState {
|
|
|
253
255
|
// @didMap: { [did: string]: metaDid: string }
|
|
254
256
|
this.didMap = new Map();
|
|
255
257
|
this.statusLocks = new Map();
|
|
258
|
+
|
|
259
|
+
// BlockletChildState instance passed from outside
|
|
260
|
+
this.BlockletChildState = config.BlockletChildState || null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Load children for a blocklet
|
|
265
|
+
* @param {string} blockletId - The blocklet ID
|
|
266
|
+
* @returns {Promise<Array>} - Array of children
|
|
267
|
+
*/
|
|
268
|
+
async loadChildren(blockletId) {
|
|
269
|
+
if (!this.BlockletChildState || !blockletId) {
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const children = await this.BlockletChildState.getChildrenByParentId(blockletId);
|
|
274
|
+
// Ensure children is always an array
|
|
275
|
+
if (!Array.isArray(children)) {
|
|
276
|
+
return [];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return children.map((child) => {
|
|
280
|
+
// Ensure meta is an object and has required fields
|
|
281
|
+
const meta = child.meta || {};
|
|
282
|
+
if (!meta.did) {
|
|
283
|
+
logger.warn('loadChildren: child missing meta.did', { childId: child.id, parentBlockletId: blockletId });
|
|
284
|
+
}
|
|
285
|
+
if (!meta.name && !meta.bundleName) {
|
|
286
|
+
logger.warn('loadChildren: child missing meta.name and meta.bundleName', {
|
|
287
|
+
childId: child.id,
|
|
288
|
+
childDid: meta.did,
|
|
289
|
+
parentBlockletId: blockletId,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const childObj = {
|
|
294
|
+
id: child.id,
|
|
295
|
+
mountPoint: child.mountPoint,
|
|
296
|
+
meta,
|
|
297
|
+
bundleSource: child.bundleSource || {},
|
|
298
|
+
source: child.source || 0,
|
|
299
|
+
deployedFrom: child.deployedFrom || '',
|
|
300
|
+
mode: child.mode || 'production',
|
|
301
|
+
status: child.status || 0,
|
|
302
|
+
ports: child.ports || {},
|
|
303
|
+
children: child.children || [],
|
|
304
|
+
migratedFrom: child.migratedFrom || [],
|
|
305
|
+
installedAt: child.installedAt,
|
|
306
|
+
startedAt: child.startedAt,
|
|
307
|
+
stoppedAt: child.stoppedAt,
|
|
308
|
+
pausedAt: child.pausedAt,
|
|
309
|
+
operator: child.operator,
|
|
310
|
+
inProgressStart: child.inProgressStart,
|
|
311
|
+
greenStatus: child.greenStatus,
|
|
312
|
+
greenPorts: child.greenPorts,
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// Recursively load children for this child
|
|
316
|
+
if (child.children && child.children.length > 0) {
|
|
317
|
+
// Note: children array now contains IDs, need to load them
|
|
318
|
+
// This will be handled by forEachComponentV2 or similar methods
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return childObj;
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Save children to BlockletChild table
|
|
327
|
+
* @param {string} blockletId - The parent blocklet ID
|
|
328
|
+
* @param {string} blockletDid - The parent blocklet DID
|
|
329
|
+
* @param {Array} children - Array of children to save
|
|
330
|
+
*/
|
|
331
|
+
async saveChildren(blockletId, blockletDid, children) {
|
|
332
|
+
if (!this.BlockletChildState) {
|
|
333
|
+
logger.warn('BlockletChildState is not initialized, cannot save children');
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (!blockletId || !blockletDid) {
|
|
338
|
+
logger.warn('saveChildren called with invalid blockletId or blockletDid', { blockletId, blockletDid });
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (!children || children.length === 0) {
|
|
343
|
+
// If no children provided, delete all existing children
|
|
344
|
+
await this.BlockletChildState.deleteByParentId(blockletId);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Get existing children
|
|
349
|
+
const existingChildren = await this.BlockletChildState.getChildrenByParentId(blockletId);
|
|
350
|
+
const existingChildrenMap = new Map();
|
|
351
|
+
existingChildren.forEach((child) => {
|
|
352
|
+
existingChildrenMap.set(child.childDid, child);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Track which children should be kept
|
|
356
|
+
const childrenToKeep = new Set();
|
|
357
|
+
|
|
358
|
+
// Process each child: update if exists, insert if new
|
|
359
|
+
for (const child of children) {
|
|
360
|
+
const childDid = child.meta?.did;
|
|
361
|
+
if (!childDid) {
|
|
362
|
+
logger.warn('saveChildren: child missing meta.did, skipping', {
|
|
363
|
+
blockletId,
|
|
364
|
+
blockletDid,
|
|
365
|
+
child: child.meta?.name || 'unknown',
|
|
366
|
+
});
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
childrenToKeep.add(childDid);
|
|
371
|
+
|
|
372
|
+
const existingChild = existingChildrenMap.get(childDid);
|
|
373
|
+
const updates = {};
|
|
374
|
+
|
|
375
|
+
// Only update fields that have changed or are necessary
|
|
376
|
+
// Fields that should be updated: meta, bundleSource, source, deployedFrom, mode, ports, children, migratedFrom
|
|
377
|
+
// Fields that should be preserved if not provided: status, installedAt, startedAt, stoppedAt, pausedAt, operator, inProgressStart, greenStatus, greenPorts
|
|
378
|
+
|
|
379
|
+
// Always update these fields
|
|
380
|
+
if (child.mountPoint !== undefined) updates.mountPoint = child.mountPoint;
|
|
381
|
+
if (child.meta !== undefined) updates.meta = child.meta;
|
|
382
|
+
if (child.bundleSource !== undefined) updates.bundleSource = child.bundleSource;
|
|
383
|
+
if (child.source !== undefined) updates.source = child.source;
|
|
384
|
+
if (child.deployedFrom !== undefined) updates.deployedFrom = child.deployedFrom;
|
|
385
|
+
if (child.mode !== undefined) updates.mode = child.mode;
|
|
386
|
+
if (child.ports !== undefined) updates.ports = child.ports;
|
|
387
|
+
if (child.children !== undefined) updates.children = child.children || [];
|
|
388
|
+
if (child.migratedFrom !== undefined) updates.migratedFrom = child.migratedFrom;
|
|
389
|
+
|
|
390
|
+
// Only update status-related fields if explicitly provided
|
|
391
|
+
if (child.status !== undefined) updates.status = child.status;
|
|
392
|
+
if (child.installedAt !== undefined) updates.installedAt = child.installedAt;
|
|
393
|
+
if (child.startedAt !== undefined) updates.startedAt = child.startedAt;
|
|
394
|
+
if (child.stoppedAt !== undefined) updates.stoppedAt = child.stoppedAt;
|
|
395
|
+
if (child.pausedAt !== undefined) updates.pausedAt = child.pausedAt;
|
|
396
|
+
if (child.operator !== undefined) updates.operator = child.operator;
|
|
397
|
+
if (child.inProgressStart !== undefined) updates.inProgressStart = child.inProgressStart;
|
|
398
|
+
if (child.greenStatus !== undefined) updates.greenStatus = child.greenStatus;
|
|
399
|
+
if (child.greenPorts !== undefined) updates.greenPorts = child.greenPorts;
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
if (existingChild) {
|
|
403
|
+
// Update existing child only if there are changes
|
|
404
|
+
if (Object.keys(updates).length > 0) {
|
|
405
|
+
await this.BlockletChildState.update({ id: existingChild.id }, { $set: updates });
|
|
406
|
+
}
|
|
407
|
+
} else {
|
|
408
|
+
// Insert new child
|
|
409
|
+
await this.BlockletChildState.insert({
|
|
410
|
+
parentBlockletId: blockletId,
|
|
411
|
+
parentBlockletDid: blockletDid,
|
|
412
|
+
childDid,
|
|
413
|
+
mountPoint: child.mountPoint,
|
|
414
|
+
meta: child.meta,
|
|
415
|
+
bundleSource: child.bundleSource,
|
|
416
|
+
source: child.source || 0,
|
|
417
|
+
deployedFrom: child.deployedFrom || '',
|
|
418
|
+
mode: child.mode || 'production',
|
|
419
|
+
status: child.status || 0,
|
|
420
|
+
ports: child.ports || {},
|
|
421
|
+
children: child.children || [],
|
|
422
|
+
migratedFrom: child.migratedFrom || [],
|
|
423
|
+
installedAt: child.installedAt,
|
|
424
|
+
startedAt: child.startedAt,
|
|
425
|
+
stoppedAt: child.stoppedAt,
|
|
426
|
+
pausedAt: child.pausedAt,
|
|
427
|
+
operator: child.operator,
|
|
428
|
+
inProgressStart: child.inProgressStart,
|
|
429
|
+
greenStatus: child.greenStatus,
|
|
430
|
+
greenPorts: child.greenPorts,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
} catch (error) {
|
|
434
|
+
logger.error('saveChildren: failed to save child', {
|
|
435
|
+
blockletId,
|
|
436
|
+
blockletDid,
|
|
437
|
+
childDid,
|
|
438
|
+
isUpdate: !!existingChild,
|
|
439
|
+
error: error.message,
|
|
440
|
+
});
|
|
441
|
+
throw error;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Delete children that are no longer in the list
|
|
446
|
+
const childrenToDelete = existingChildren.filter((child) => !childrenToKeep.has(child.childDid));
|
|
447
|
+
if (childrenToDelete.length > 0) {
|
|
448
|
+
for (const childToDelete of childrenToDelete) {
|
|
449
|
+
await this.BlockletChildState.remove({ id: childToDelete.id });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
256
452
|
}
|
|
257
453
|
|
|
258
454
|
/**
|
|
@@ -268,7 +464,15 @@ class BlockletState extends BaseState {
|
|
|
268
464
|
}
|
|
269
465
|
|
|
270
466
|
const doc = await this.findOne({ $or: getConditions(did) });
|
|
271
|
-
|
|
467
|
+
if (!doc) {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Load children from BlockletChild table
|
|
472
|
+
const children = await this.loadChildren(doc.id);
|
|
473
|
+
doc.children = children;
|
|
474
|
+
|
|
475
|
+
return formatBlocklet(doc, 'onRead', decryptSk ? this.config.dek : null);
|
|
272
476
|
}
|
|
273
477
|
|
|
274
478
|
async getBlockletMetaDid(did) {
|
|
@@ -304,7 +508,16 @@ class BlockletState extends BaseState {
|
|
|
304
508
|
|
|
305
509
|
async getBlocklets(query = {}, projection = {}, sort = { createdAt: -1 }) {
|
|
306
510
|
const docs = await this.find(query, projection, sort);
|
|
307
|
-
|
|
511
|
+
const result = [];
|
|
512
|
+
|
|
513
|
+
for (const doc of docs.filter(Boolean)) {
|
|
514
|
+
// Load children for each blocklet
|
|
515
|
+
const children = await this.loadChildren(doc.id);
|
|
516
|
+
doc.children = children;
|
|
517
|
+
result.push(formatBlocklet(doc, 'onRead', this.config.dek));
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return result;
|
|
308
521
|
}
|
|
309
522
|
|
|
310
523
|
async deleteBlocklet(did) {
|
|
@@ -313,6 +526,11 @@ class BlockletState extends BaseState {
|
|
|
313
526
|
throw new CustomError(404, `Try to remove non-existing blocklet ${did}`);
|
|
314
527
|
}
|
|
315
528
|
|
|
529
|
+
// Delete children from BlockletChild table
|
|
530
|
+
if (this.BlockletChildState) {
|
|
531
|
+
await this.BlockletChildState.deleteByParentId(doc.id);
|
|
532
|
+
}
|
|
533
|
+
|
|
316
534
|
await this.remove({ id: doc.id });
|
|
317
535
|
|
|
318
536
|
this.didMap.delete(doc.meta?.did);
|
|
@@ -376,7 +594,6 @@ class BlockletState extends BaseState {
|
|
|
376
594
|
deployedFrom,
|
|
377
595
|
ports,
|
|
378
596
|
environments: [],
|
|
379
|
-
children,
|
|
380
597
|
migratedFrom,
|
|
381
598
|
externalSk,
|
|
382
599
|
externalSkSource,
|
|
@@ -384,6 +601,10 @@ class BlockletState extends BaseState {
|
|
|
384
601
|
});
|
|
385
602
|
|
|
386
603
|
doc = await this.findOne({ id: doc.id });
|
|
604
|
+
doc.children = children;
|
|
605
|
+
|
|
606
|
+
// Save children to BlockletChild table
|
|
607
|
+
await this.saveChildren(doc.id, doc.meta.did, children);
|
|
387
608
|
|
|
388
609
|
this.emit('add', doc);
|
|
389
610
|
|
|
@@ -403,6 +624,15 @@ class BlockletState extends BaseState {
|
|
|
403
624
|
const formatted = formatBlocklet(omit(cloneDeep(updates), ['vaults']), 'onUpdate', this.config.dek);
|
|
404
625
|
const [, [updated]] = await this.update({ id: doc.id }, { $set: formatted });
|
|
405
626
|
|
|
627
|
+
// If children are being updated, set them on the updated object before calculating status
|
|
628
|
+
// This ensures getBlockletStatus can correctly calculate status based on children
|
|
629
|
+
if (updates.children !== undefined) {
|
|
630
|
+
updated.children = updates.children;
|
|
631
|
+
if (this.BlockletChildState) {
|
|
632
|
+
await this.saveChildren(updated.id, updated.meta.did, updates.children);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
406
636
|
updated.status = getBlockletStatus(updated);
|
|
407
637
|
|
|
408
638
|
return updated;
|
|
@@ -475,10 +705,13 @@ class BlockletState extends BaseState {
|
|
|
475
705
|
meta: omit(sanitized, ['htmlAst']),
|
|
476
706
|
source,
|
|
477
707
|
deployedFrom,
|
|
478
|
-
children,
|
|
479
708
|
ports,
|
|
480
709
|
});
|
|
481
710
|
|
|
711
|
+
// Save children to BlockletChild table
|
|
712
|
+
await this.saveChildren(newDoc.id, newDoc.meta.did, children);
|
|
713
|
+
newDoc.children = children;
|
|
714
|
+
|
|
482
715
|
this.emit('upgrade', newDoc);
|
|
483
716
|
return newDoc;
|
|
484
717
|
} finally {
|
|
@@ -630,9 +863,10 @@ class BlockletState extends BaseState {
|
|
|
630
863
|
});
|
|
631
864
|
|
|
632
865
|
if (actuallyRefreshedDids.length > 0) {
|
|
633
|
-
await this.updateBlocklet(did, {
|
|
634
|
-
|
|
635
|
-
|
|
866
|
+
await this.updateBlocklet(did, {});
|
|
867
|
+
|
|
868
|
+
// Save updated children to BlockletChild table
|
|
869
|
+
await this.saveChildren(blocklet.id, blocklet.meta.did, blocklet.children);
|
|
636
870
|
}
|
|
637
871
|
|
|
638
872
|
return {
|
|
@@ -643,7 +877,7 @@ class BlockletState extends BaseState {
|
|
|
643
877
|
}
|
|
644
878
|
|
|
645
879
|
async getServices() {
|
|
646
|
-
const blocklets = await this.getBlocklets({}, { meta: 1,
|
|
880
|
+
const blocklets = await this.getBlocklets({}, { meta: 1, ports: 1 });
|
|
647
881
|
const services = [];
|
|
648
882
|
|
|
649
883
|
blocklets.forEach((blocklet) => {
|
|
@@ -681,7 +915,7 @@ class BlockletState extends BaseState {
|
|
|
681
915
|
* @return {Object} { <did> : { interfaceName } }
|
|
682
916
|
*/
|
|
683
917
|
async groupAllInterfaces() {
|
|
684
|
-
const blocklets = await this.getBlocklets({}, { meta: 1
|
|
918
|
+
const blocklets = await this.getBlocklets({}, { meta: 1 });
|
|
685
919
|
const result = {};
|
|
686
920
|
const fillResult = (component, { id }) => {
|
|
687
921
|
const { interfaces } = component.meta;
|
|
@@ -701,6 +935,22 @@ class BlockletState extends BaseState {
|
|
|
701
935
|
return result;
|
|
702
936
|
}
|
|
703
937
|
|
|
938
|
+
/**
|
|
939
|
+
* Reset the blocklet state by clearing all blocklets and their children
|
|
940
|
+
* This overrides BaseState.reset() to handle foreign key constraints
|
|
941
|
+
*/
|
|
942
|
+
async reset() {
|
|
943
|
+
// First, delete all children to avoid foreign key constraint errors
|
|
944
|
+
if (this.BlockletChildState) {
|
|
945
|
+
const allBlocklets = await this.getBlocklets({}, { id: 1 });
|
|
946
|
+
for (const blocklet of allBlocklets) {
|
|
947
|
+
await this.BlockletChildState.deleteByParentId(blocklet.id);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
// Then call parent reset to clear the blocklets table
|
|
951
|
+
return super.reset();
|
|
952
|
+
}
|
|
953
|
+
|
|
704
954
|
/**
|
|
705
955
|
* @param {Did} did blocklet did
|
|
706
956
|
* @param {BlockletStatus} status blocklet status
|
|
@@ -764,14 +1014,17 @@ class BlockletState extends BaseState {
|
|
|
764
1014
|
|
|
765
1015
|
const shouldSetStatus = status === BlockletStatus.downloading || status === BlockletStatus.waiting;
|
|
766
1016
|
const updateData = {
|
|
767
|
-
children: doc.children,
|
|
768
1017
|
operator,
|
|
769
1018
|
};
|
|
770
1019
|
if (shouldSetStatus) {
|
|
771
1020
|
updateData.status = status;
|
|
772
1021
|
}
|
|
773
1022
|
|
|
1023
|
+
updateData.children = doc.children;
|
|
1024
|
+
|
|
774
1025
|
const res = await this.updateBlocklet(did, updateData);
|
|
1026
|
+
|
|
1027
|
+
res.children = doc.children;
|
|
775
1028
|
return res;
|
|
776
1029
|
} finally {
|
|
777
1030
|
await lock.releaseLock(lockName);
|
|
@@ -902,7 +1155,7 @@ class BlockletState extends BaseState {
|
|
|
902
1155
|
}
|
|
903
1156
|
|
|
904
1157
|
async _getOccupiedPorts() {
|
|
905
|
-
const blocklets = await this.getBlocklets({}, { port: 1, ports: 1, meta: 1
|
|
1158
|
+
const blocklets = await this.getBlocklets({}, { port: 1, ports: 1, meta: 1 });
|
|
906
1159
|
|
|
907
1160
|
const occupiedExternalPorts = new Map();
|
|
908
1161
|
const occupiedInternalPorts = new Map();
|
package/lib/states/index.js
CHANGED
|
@@ -4,6 +4,7 @@ const logger = require('@abtnode/logger')('@abtnode/core:states');
|
|
|
4
4
|
|
|
5
5
|
const NodeState = require('./node');
|
|
6
6
|
const BlockletState = require('./blocklet');
|
|
7
|
+
const BlockletChildState = require('./blocklet-child');
|
|
7
8
|
const NotificationState = require('./notification');
|
|
8
9
|
const SiteState = require('./site');
|
|
9
10
|
const AccessKeyState = require('./access-key');
|
|
@@ -33,7 +34,8 @@ const init = (dataDirs, config = {}) => {
|
|
|
33
34
|
const notificationState = new NotificationState(models.Notification, config, models);
|
|
34
35
|
|
|
35
36
|
const nodeState = new NodeState(models.Server, config, dataDirs, notificationState);
|
|
36
|
-
const
|
|
37
|
+
const blockletChildState = new BlockletChildState(models.BlockletChild, config);
|
|
38
|
+
const blockletState = new BlockletState(models.Blocklet, { ...config, BlockletChildState: blockletChildState });
|
|
37
39
|
const siteState = new SiteState(models.Site, config);
|
|
38
40
|
const accessKeyState = new AccessKeyState(models.AccessKey, config);
|
|
39
41
|
const webhookState = new WebhookState(models.WebHook, config);
|
|
@@ -52,6 +54,7 @@ const init = (dataDirs, config = {}) => {
|
|
|
52
54
|
return {
|
|
53
55
|
node: nodeState,
|
|
54
56
|
blocklet: blockletState,
|
|
57
|
+
blockletChild: blockletChildState,
|
|
55
58
|
notification: notificationState,
|
|
56
59
|
site: siteState,
|
|
57
60
|
accessKey: accessKeyState,
|
package/lib/util/blocklet.js
CHANGED
|
@@ -416,36 +416,6 @@ const getBlockletConfigObj = (blocklet, { excludeSecure } = {}) => {
|
|
|
416
416
|
|
|
417
417
|
return obj;
|
|
418
418
|
};
|
|
419
|
-
/**
|
|
420
|
-
* set 'configs', configObj', 'environmentObj' to blocklet TODO
|
|
421
|
-
* @param {*} blocklet
|
|
422
|
-
* @param {*} configs
|
|
423
|
-
*/
|
|
424
|
-
const fillBlockletConfigs = (blocklet, configs) => {
|
|
425
|
-
blocklet.configs = configs || [];
|
|
426
|
-
blocklet.configObj = getBlockletConfigObj(blocklet);
|
|
427
|
-
blocklet.environments = blocklet.environments || [];
|
|
428
|
-
blocklet.environmentObj = blocklet.environments.reduce((acc, x) => {
|
|
429
|
-
acc[x.key] = templateReplace(x.value, blocklet);
|
|
430
|
-
return acc;
|
|
431
|
-
}, {});
|
|
432
|
-
};
|
|
433
|
-
|
|
434
|
-
const ensureBlockletExpanded = async (_meta, appDir) => {
|
|
435
|
-
const bundlePath = path.join(appDir, BLOCKLET_BUNDLE_FILE);
|
|
436
|
-
if (fs.existsSync(bundlePath)) {
|
|
437
|
-
try {
|
|
438
|
-
const nodeModulesPath = path.join(appDir, 'node_modules');
|
|
439
|
-
if (fs.existsSync(nodeModulesPath)) {
|
|
440
|
-
fs.removeSync(nodeModulesPath);
|
|
441
|
-
}
|
|
442
|
-
await expandBundle(bundlePath, appDir);
|
|
443
|
-
fs.removeSync(bundlePath);
|
|
444
|
-
} catch (err) {
|
|
445
|
-
throw new Error(`Failed to expand blocklet bundle: ${err.message}`);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
};
|
|
449
419
|
|
|
450
420
|
const getAppSystemEnvironments = (blocklet, nodeInfo, dataDirs) => {
|
|
451
421
|
const { did, name, title, description } = blocklet.meta;
|
|
@@ -562,6 +532,81 @@ const getComponentSystemEnvironments = (blocklet) => {
|
|
|
562
532
|
};
|
|
563
533
|
};
|
|
564
534
|
|
|
535
|
+
/**
|
|
536
|
+
* set 'configs', configObj', 'environmentObj' to blocklet TODO
|
|
537
|
+
* @param {*} blocklet
|
|
538
|
+
* @param {*} configs
|
|
539
|
+
* @param {*} options - Optional: { rootBlocklet, nodeInfo, dataDirs }
|
|
540
|
+
*/
|
|
541
|
+
const fillBlockletConfigs = (blocklet, configs, options = {}) => {
|
|
542
|
+
blocklet.configs = configs || [];
|
|
543
|
+
blocklet.configObj = getBlockletConfigObj(blocklet);
|
|
544
|
+
blocklet.environments = blocklet.environments || [];
|
|
545
|
+
blocklet.environmentObj = blocklet.environments.reduce((acc, x) => {
|
|
546
|
+
acc[x.key] = templateReplace(x.value, blocklet);
|
|
547
|
+
return acc;
|
|
548
|
+
}, {});
|
|
549
|
+
|
|
550
|
+
// After migration: ensure all component system environments are set from blocklet.env if available
|
|
551
|
+
// This ensures children loaded from blocklet_children table have all required env vars in environmentObj
|
|
552
|
+
if (blocklet.env) {
|
|
553
|
+
try {
|
|
554
|
+
const componentSystemEnvs = getComponentSystemEnvironments(blocklet);
|
|
555
|
+
// Only set env vars that are not already set
|
|
556
|
+
Object.entries(componentSystemEnvs).forEach(([key, value]) => {
|
|
557
|
+
if (value !== undefined && value !== null && !blocklet.environmentObj[key]) {
|
|
558
|
+
blocklet.environmentObj[key] = value;
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
} catch (error) {
|
|
562
|
+
// If getting component system environments fails, log warning but continue
|
|
563
|
+
logger.warn('fillBlockletConfigs: failed to get component system environments', {
|
|
564
|
+
blockletDid: blocklet.meta?.did,
|
|
565
|
+
error: error.message,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// For children: also set app-level environment variables from root blocklet
|
|
571
|
+
// This ensures children have app-level env vars like BLOCKLET_APP_ID
|
|
572
|
+
const { rootBlocklet, nodeInfo, dataDirs } = options;
|
|
573
|
+
if (rootBlocklet && nodeInfo && dataDirs && blocklet !== rootBlocklet) {
|
|
574
|
+
try {
|
|
575
|
+
const appSystemEnvs = getAppSystemEnvironments(rootBlocklet, nodeInfo, dataDirs);
|
|
576
|
+
const appOverwrittenEnvs = getAppOverwrittenEnvironments(rootBlocklet, nodeInfo);
|
|
577
|
+
// Only set env vars that are not already set
|
|
578
|
+
Object.entries({ ...appSystemEnvs, ...appOverwrittenEnvs }).forEach(([key, value]) => {
|
|
579
|
+
if (value !== undefined && value !== null && !blocklet.environmentObj[key]) {
|
|
580
|
+
blocklet.environmentObj[key] = value;
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
} catch (error) {
|
|
584
|
+
// If getting app system environments fails, log warning but continue
|
|
585
|
+
logger.warn('fillBlockletConfigs: failed to get app system environments', {
|
|
586
|
+
blockletDid: blocklet.meta?.did,
|
|
587
|
+
rootDid: rootBlocklet.meta?.did,
|
|
588
|
+
error: error.message,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
const ensureBlockletExpanded = async (_meta, appDir) => {
|
|
595
|
+
const bundlePath = path.join(appDir, BLOCKLET_BUNDLE_FILE);
|
|
596
|
+
if (fs.existsSync(bundlePath)) {
|
|
597
|
+
try {
|
|
598
|
+
const nodeModulesPath = path.join(appDir, 'node_modules');
|
|
599
|
+
if (fs.existsSync(nodeModulesPath)) {
|
|
600
|
+
fs.removeSync(nodeModulesPath);
|
|
601
|
+
}
|
|
602
|
+
await expandBundle(bundlePath, appDir);
|
|
603
|
+
fs.removeSync(bundlePath);
|
|
604
|
+
} catch (err) {
|
|
605
|
+
throw new Error(`Failed to expand blocklet bundle: ${err.message}`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
|
|
565
610
|
const getRuntimeEnvironments = (blocklet, nodeEnvironments, ancestors, isGreen = false) => {
|
|
566
611
|
const root = (ancestors || [])[0] || blocklet;
|
|
567
612
|
|
|
@@ -1970,21 +2015,44 @@ const _getBlocklet = async ({
|
|
|
1970
2015
|
|
|
1971
2016
|
await forEachBlocklet(blocklet, async (component, { id, level, ancestors }) => {
|
|
1972
2017
|
// component env
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
2018
|
+
try {
|
|
2019
|
+
// Validate component has required meta fields for getComponentDirs
|
|
2020
|
+
if (!component.meta) {
|
|
2021
|
+
throw new Error(`Component missing meta field: ${component.meta?.did || id}`);
|
|
2022
|
+
}
|
|
2023
|
+
if (!component.meta.name && !component.meta.bundleName) {
|
|
2024
|
+
throw new Error(
|
|
2025
|
+
`Component missing meta.name and meta.bundleName: ${component.meta.did || id}. ` +
|
|
2026
|
+
'This may indicate a migration issue with blocklet_children table.'
|
|
2027
|
+
);
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
component.env = {
|
|
2031
|
+
id,
|
|
2032
|
+
name: getComponentName(component, ancestors),
|
|
2033
|
+
processId: getComponentProcessId(component, ancestors),
|
|
2034
|
+
...getComponentDirs(component, {
|
|
2035
|
+
dataDirs,
|
|
2036
|
+
ensure: ensureIntegrity,
|
|
2037
|
+
ancestors,
|
|
2038
|
+
e2eMode: level === 0 ? e2eMode : false,
|
|
2039
|
+
}),
|
|
2040
|
+
};
|
|
2041
|
+
} catch (error) {
|
|
2042
|
+
logger.error('Failed to set component env in _getBlocklet', {
|
|
2043
|
+
componentDid: component.meta?.did,
|
|
2044
|
+
componentName: component.meta?.name,
|
|
2045
|
+
componentBundleName: component.meta?.bundleName,
|
|
2046
|
+
error: error.message,
|
|
2047
|
+
stack: error.stack,
|
|
2048
|
+
});
|
|
2049
|
+
throw error;
|
|
2050
|
+
}
|
|
1984
2051
|
|
|
1985
2052
|
// component config
|
|
1986
2053
|
const configs = await states.blockletExtras.getConfigs([...ancestors.map((x) => x.meta.did), component.meta.did]);
|
|
1987
|
-
|
|
2054
|
+
const rootBlocklet = ancestors.length > 0 ? ancestors[0] : blocklet;
|
|
2055
|
+
fillBlockletConfigs(component, configs, { rootBlocklet, nodeInfo, dataDirs });
|
|
1988
2056
|
});
|
|
1989
2057
|
|
|
1990
2058
|
if (getOptionalComponents) {
|
|
@@ -94,6 +94,14 @@ async function migrateAllTablesNoModels(dbPath) {
|
|
|
94
94
|
.filter(([, def]) => def.type && ['JSON', 'JSONB'].includes(def.type.toUpperCase()))
|
|
95
95
|
.map(([col, def]) => ({ name: col, type: def.type.toUpperCase() }));
|
|
96
96
|
|
|
97
|
+
// find DATE/TIMESTAMP columns (need to validate and fix invalid dates)
|
|
98
|
+
const dateCols = Object.entries(pgSchema)
|
|
99
|
+
.filter(([, def]) => {
|
|
100
|
+
const type = def.type?.toUpperCase() || '';
|
|
101
|
+
return type.includes('DATE') || type.includes('TIMESTAMP') || type === 'DATE' || type.startsWith('TIMESTAMP');
|
|
102
|
+
})
|
|
103
|
+
.map(([col]) => col);
|
|
104
|
+
|
|
97
105
|
// find auto-increment columns (nextval default)
|
|
98
106
|
const autoIncCols = Object.entries(pgSchema)
|
|
99
107
|
.filter(([, def]) => typeof def.defaultValue === 'string' && def.defaultValue.startsWith('nextval('))
|
|
@@ -159,12 +167,37 @@ async function migrateAllTablesNoModels(dbPath) {
|
|
|
159
167
|
console.log(` Migrating rows ${offset + 1}-${offset + rows.length}`);
|
|
160
168
|
|
|
161
169
|
for (const row of rows) {
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
row
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
170
|
+
// Fix invalid date values for all DATE/TIMESTAMP columns
|
|
171
|
+
for (const dateCol of dateCols) {
|
|
172
|
+
if (row[dateCol] != null) {
|
|
173
|
+
const dateVal = row[dateCol];
|
|
174
|
+
// Check if it's an invalid date (NaN, "Invalid date" string, or invalid Date object)
|
|
175
|
+
let isValid = false;
|
|
176
|
+
if (dateVal instanceof Date) {
|
|
177
|
+
isValid = !Number.isNaN(dateVal.getTime());
|
|
178
|
+
} else if (typeof dateVal === 'string') {
|
|
179
|
+
// Check for "Invalid date" string or empty string
|
|
180
|
+
if (dateVal === 'Invalid date' || dateVal === '' || dateVal === 'null') {
|
|
181
|
+
isValid = false;
|
|
182
|
+
} else {
|
|
183
|
+
const parsed = new Date(dateVal);
|
|
184
|
+
isValid = !Number.isNaN(parsed.getTime());
|
|
185
|
+
}
|
|
186
|
+
} else if (typeof dateVal === 'number') {
|
|
187
|
+
// Check if it's a valid timestamp
|
|
188
|
+
const parsed = new Date(dateVal);
|
|
189
|
+
isValid = !Number.isNaN(parsed.getTime());
|
|
190
|
+
} else {
|
|
191
|
+
// null or undefined are valid (will be handled by allowNull)
|
|
192
|
+
isValid = true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!isValid) {
|
|
196
|
+
console.warn(` ⚠️ ${tableName}: Invalid date in column "${dateCol}", fixing to current time`);
|
|
197
|
+
console.log(` Old value: ${dateVal} (type: ${typeof dateVal})`);
|
|
198
|
+
row[dateCol] = new Date();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
168
201
|
}
|
|
169
202
|
|
|
170
203
|
// 修复不合格的旧数据
|
|
@@ -266,6 +299,46 @@ async function migrateAllTablesNoModels(dbPath) {
|
|
|
266
299
|
console.error(` ❌ ${tableName}: string too long for VARCHAR columns:`, badCols);
|
|
267
300
|
continue;
|
|
268
301
|
}
|
|
302
|
+
// Handle invalid timestamp/date errors - should have been fixed above, but log if still occurs
|
|
303
|
+
const timestampErr = err.message.match(/invalid input syntax for type timestamp/i);
|
|
304
|
+
if (timestampErr) {
|
|
305
|
+
console.error(` ❌ ${tableName}: Invalid timestamp error (should have been fixed):`, err.message);
|
|
306
|
+
console.log(' Row data:', JSON.stringify(row, null, 2));
|
|
307
|
+
// Try to fix and retry once
|
|
308
|
+
let fixed = false;
|
|
309
|
+
for (const dateCol of dateCols) {
|
|
310
|
+
if (row[dateCol] != null) {
|
|
311
|
+
const dateVal = row[dateCol];
|
|
312
|
+
if (
|
|
313
|
+
dateVal === 'Invalid date' ||
|
|
314
|
+
dateVal === '' ||
|
|
315
|
+
(typeof dateVal === 'string' && dateVal.toLowerCase() === 'null')
|
|
316
|
+
) {
|
|
317
|
+
row[dateCol] = new Date();
|
|
318
|
+
fixed = true;
|
|
319
|
+
} else {
|
|
320
|
+
const parsed = new Date(dateVal);
|
|
321
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
322
|
+
row[dateCol] = new Date();
|
|
323
|
+
fixed = true;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (fixed) {
|
|
329
|
+
console.log(' 🔧 Fixed invalid dates, retrying insert...');
|
|
330
|
+
const retryBindVals = insertCols.map((c) => row[c]);
|
|
331
|
+
try {
|
|
332
|
+
await pgDb.query(upsertSQL, { bind: retryBindVals });
|
|
333
|
+
continue;
|
|
334
|
+
} catch (retryErr) {
|
|
335
|
+
console.error(' ❌ Retry failed:', retryErr.message);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// If still failing, skip this row
|
|
339
|
+
console.warn(' ⚠️ Skipping row due to timestamp error');
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
269
342
|
console.error(` ❌ Upsert failed for ${tableName} : ${err.message}, SQL:${upsertSQL} value: ${bindVals}`);
|
|
270
343
|
if (ignoreErrorTableNames.has(tableName)) {
|
|
271
344
|
console.log(` ❌ Ignore error for ${tableName}`);
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.17.5-beta-
|
|
6
|
+
"version": "1.17.5-beta-20251211-104355-426d7eb6",
|
|
7
7
|
"description": "",
|
|
8
8
|
"main": "lib/index.js",
|
|
9
9
|
"files": [
|
|
@@ -17,21 +17,21 @@
|
|
|
17
17
|
"author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
|
|
18
18
|
"license": "Apache-2.0",
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@abtnode/analytics": "1.17.5-beta-
|
|
21
|
-
"@abtnode/auth": "1.17.5-beta-
|
|
22
|
-
"@abtnode/certificate-manager": "1.17.5-beta-
|
|
23
|
-
"@abtnode/constant": "1.17.5-beta-
|
|
24
|
-
"@abtnode/cron": "1.17.5-beta-
|
|
25
|
-
"@abtnode/db-cache": "1.17.5-beta-
|
|
26
|
-
"@abtnode/docker-utils": "1.17.5-beta-
|
|
27
|
-
"@abtnode/logger": "1.17.5-beta-
|
|
28
|
-
"@abtnode/models": "1.17.5-beta-
|
|
29
|
-
"@abtnode/queue": "1.17.5-beta-
|
|
30
|
-
"@abtnode/rbac": "1.17.5-beta-
|
|
31
|
-
"@abtnode/router-provider": "1.17.5-beta-
|
|
32
|
-
"@abtnode/static-server": "1.17.5-beta-
|
|
33
|
-
"@abtnode/timemachine": "1.17.5-beta-
|
|
34
|
-
"@abtnode/util": "1.17.5-beta-
|
|
20
|
+
"@abtnode/analytics": "1.17.5-beta-20251211-104355-426d7eb6",
|
|
21
|
+
"@abtnode/auth": "1.17.5-beta-20251211-104355-426d7eb6",
|
|
22
|
+
"@abtnode/certificate-manager": "1.17.5-beta-20251211-104355-426d7eb6",
|
|
23
|
+
"@abtnode/constant": "1.17.5-beta-20251211-104355-426d7eb6",
|
|
24
|
+
"@abtnode/cron": "1.17.5-beta-20251211-104355-426d7eb6",
|
|
25
|
+
"@abtnode/db-cache": "1.17.5-beta-20251211-104355-426d7eb6",
|
|
26
|
+
"@abtnode/docker-utils": "1.17.5-beta-20251211-104355-426d7eb6",
|
|
27
|
+
"@abtnode/logger": "1.17.5-beta-20251211-104355-426d7eb6",
|
|
28
|
+
"@abtnode/models": "1.17.5-beta-20251211-104355-426d7eb6",
|
|
29
|
+
"@abtnode/queue": "1.17.5-beta-20251211-104355-426d7eb6",
|
|
30
|
+
"@abtnode/rbac": "1.17.5-beta-20251211-104355-426d7eb6",
|
|
31
|
+
"@abtnode/router-provider": "1.17.5-beta-20251211-104355-426d7eb6",
|
|
32
|
+
"@abtnode/static-server": "1.17.5-beta-20251211-104355-426d7eb6",
|
|
33
|
+
"@abtnode/timemachine": "1.17.5-beta-20251211-104355-426d7eb6",
|
|
34
|
+
"@abtnode/util": "1.17.5-beta-20251211-104355-426d7eb6",
|
|
35
35
|
"@aigne/aigne-hub": "^0.10.10",
|
|
36
36
|
"@arcblock/did": "^1.27.12",
|
|
37
37
|
"@arcblock/did-connect-js": "^1.27.12",
|
|
@@ -43,15 +43,15 @@
|
|
|
43
43
|
"@arcblock/pm2-events": "^0.0.5",
|
|
44
44
|
"@arcblock/validator": "^1.27.12",
|
|
45
45
|
"@arcblock/vc": "^1.27.12",
|
|
46
|
-
"@blocklet/constant": "1.17.5-beta-
|
|
46
|
+
"@blocklet/constant": "1.17.5-beta-20251211-104355-426d7eb6",
|
|
47
47
|
"@blocklet/did-space-js": "^1.2.6",
|
|
48
|
-
"@blocklet/env": "1.17.5-beta-
|
|
48
|
+
"@blocklet/env": "1.17.5-beta-20251211-104355-426d7eb6",
|
|
49
49
|
"@blocklet/error": "^0.3.3",
|
|
50
|
-
"@blocklet/meta": "1.17.5-beta-
|
|
51
|
-
"@blocklet/resolver": "1.17.5-beta-
|
|
52
|
-
"@blocklet/sdk": "1.17.5-beta-
|
|
53
|
-
"@blocklet/server-js": "1.17.5-beta-
|
|
54
|
-
"@blocklet/store": "1.17.5-beta-
|
|
50
|
+
"@blocklet/meta": "1.17.5-beta-20251211-104355-426d7eb6",
|
|
51
|
+
"@blocklet/resolver": "1.17.5-beta-20251211-104355-426d7eb6",
|
|
52
|
+
"@blocklet/sdk": "1.17.5-beta-20251211-104355-426d7eb6",
|
|
53
|
+
"@blocklet/server-js": "1.17.5-beta-20251211-104355-426d7eb6",
|
|
54
|
+
"@blocklet/store": "1.17.5-beta-20251211-104355-426d7eb6",
|
|
55
55
|
"@blocklet/theme": "^3.2.11",
|
|
56
56
|
"@fidm/x509": "^1.2.1",
|
|
57
57
|
"@ocap/mcrypto": "^1.27.12",
|
|
@@ -116,5 +116,5 @@
|
|
|
116
116
|
"express": "^4.18.2",
|
|
117
117
|
"unzipper": "^0.10.11"
|
|
118
118
|
},
|
|
119
|
-
"gitHead": "
|
|
119
|
+
"gitHead": "9ea2a8077a8edbf5021281e823719758eb9fe02c"
|
|
120
120
|
}
|