@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.
@@ -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
- await states.blocklet.setBlockletStatus(did, BlockletStatus.error, {
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
- const startedGreenBlockletDids = startedBlockletDids.filter((x) => x.isGreen).map((x) => x.did);
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
@@ -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((err) => {
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;
@@ -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
- return doc ? formatBlocklet(doc, 'onRead', decryptSk ? this.config.dek : null) : null;
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
- return docs.filter(Boolean).map((doc) => formatBlocklet(doc, 'onRead', this.config.dek));
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
- children: blocklet.children,
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, children: 1, ports: 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, children: 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, children: 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();
@@ -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 blockletState = new BlockletState(models.Blocklet, config);
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,
@@ -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
- component.env = {
1974
- id,
1975
- name: getComponentName(component, ancestors),
1976
- processId: getComponentProcessId(component, ancestors),
1977
- ...getComponentDirs(component, {
1978
- dataDirs,
1979
- ensure: ensureIntegrity,
1980
- ancestors,
1981
- e2eMode: level === 0 ? e2eMode : false,
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
- fillBlockletConfigs(component, configs);
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
- // 如果有 createdAt updatedAt 列, 并且值是不合法的 Date, 就改成当前时间
163
- if (row.createdAt && Number.isNaN(new Date(row.createdAt).getTime())) {
164
- row.createdAt = new Date();
165
- }
166
- if (row.updatedAt && Number.isNaN(new Date(row.updatedAt).getTime())) {
167
- row.updatedAt = new Date();
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-20251209-090953-3a59e7ac",
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-20251209-090953-3a59e7ac",
21
- "@abtnode/auth": "1.17.5-beta-20251209-090953-3a59e7ac",
22
- "@abtnode/certificate-manager": "1.17.5-beta-20251209-090953-3a59e7ac",
23
- "@abtnode/constant": "1.17.5-beta-20251209-090953-3a59e7ac",
24
- "@abtnode/cron": "1.17.5-beta-20251209-090953-3a59e7ac",
25
- "@abtnode/db-cache": "1.17.5-beta-20251209-090953-3a59e7ac",
26
- "@abtnode/docker-utils": "1.17.5-beta-20251209-090953-3a59e7ac",
27
- "@abtnode/logger": "1.17.5-beta-20251209-090953-3a59e7ac",
28
- "@abtnode/models": "1.17.5-beta-20251209-090953-3a59e7ac",
29
- "@abtnode/queue": "1.17.5-beta-20251209-090953-3a59e7ac",
30
- "@abtnode/rbac": "1.17.5-beta-20251209-090953-3a59e7ac",
31
- "@abtnode/router-provider": "1.17.5-beta-20251209-090953-3a59e7ac",
32
- "@abtnode/static-server": "1.17.5-beta-20251209-090953-3a59e7ac",
33
- "@abtnode/timemachine": "1.17.5-beta-20251209-090953-3a59e7ac",
34
- "@abtnode/util": "1.17.5-beta-20251209-090953-3a59e7ac",
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-20251209-090953-3a59e7ac",
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-20251209-090953-3a59e7ac",
48
+ "@blocklet/env": "1.17.5-beta-20251211-104355-426d7eb6",
49
49
  "@blocklet/error": "^0.3.3",
50
- "@blocklet/meta": "1.17.5-beta-20251209-090953-3a59e7ac",
51
- "@blocklet/resolver": "1.17.5-beta-20251209-090953-3a59e7ac",
52
- "@blocklet/sdk": "1.17.5-beta-20251209-090953-3a59e7ac",
53
- "@blocklet/server-js": "1.17.5-beta-20251209-090953-3a59e7ac",
54
- "@blocklet/store": "1.17.5-beta-20251209-090953-3a59e7ac",
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": "514964082a28e8c6658d9a62cf283811dd6a8baf"
119
+ "gitHead": "9ea2a8077a8edbf5021281e823719758eb9fe02c"
120
120
  }