@abtnode/core 1.17.5-beta-20251208-123021-e8c53f96 → 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
@@ -514,7 +514,7 @@ module.exports = Object.freeze({
514
514
  BLOCKLET_STORE: {
515
515
  id: 'zNKqX7D8ZAYa77HgzpoFfnV3BFbcmSRrE9aT',
516
516
  name: 'Official Store',
517
- description: 'ArcBlock official blocklet registry',
517
+ description: 'ArcBlock official store for production ready blocklets',
518
518
  url: BLOCKLET_STORE_URL,
519
519
  logoUrl: '/logo.png',
520
520
  maintainer: 'arcblock',
@@ -522,11 +522,19 @@ module.exports = Object.freeze({
522
522
  BLOCKLET_STORE_DEV: {
523
523
  id: 'zNKmfUatDhzfMVACfr3u97eqndj8f1yXXw3m',
524
524
  name: 'Dev Store',
525
- description: 'ArcBlock dev registry that contains demo and example blocklets',
525
+ description: 'ArcBlock official store for demo and example blocklets',
526
526
  url: BLOCKLET_STORE_URL_DEV,
527
527
  maintainer: 'arcblock',
528
528
  logoUrl: '/logo.png',
529
529
  },
530
+ BLOCKLET_TEST_STORE: {
531
+ id: 'zNKirQVRx4xbyTPMkvH3kguRfofTJana8WBK',
532
+ name: 'Test Store',
533
+ description: 'ArcBlock official store for non-production ready blocklets',
534
+ url: TEST_STORE_URL,
535
+ maintainer: 'arcblock',
536
+ logoUrl: '/logo.png',
537
+ },
530
538
 
531
539
  // application is a container, components have no hierarchy and are tiled in application
532
540
  APP_STRUCT_VERSION: '2',
@@ -25,6 +25,7 @@ const { encode } = require('@abtnode/util/lib/base32');
25
25
  const dayjs = require('@abtnode/util/lib/dayjs');
26
26
 
27
27
  const { isInstanceWorker } = require('@abtnode/util/lib/pm2/is-instance-worker');
28
+ const { isInServerlessMode } = require('@abtnode/util/lib/serverless');
28
29
  const { NodeMonitSender } = require('../monitor/node-monit-sender');
29
30
  const { isCLI } = require('../util');
30
31
 
@@ -44,6 +45,7 @@ const {
44
45
  routingSnapshotPrefix,
45
46
  } = require('./util');
46
47
  const { ensureBlockletHasMultipleInterfaces } = require('../router/helper');
48
+ const { sendServerlessHeartbeat } = require('../util/launcher');
47
49
 
48
50
  /**
49
51
  *
@@ -80,6 +82,10 @@ module.exports = ({
80
82
  const events = new EventEmitter();
81
83
  events.setMaxListeners(0);
82
84
 
85
+ // Throttle serverless heartbeat: only call once every 30 seconds
86
+ let lastServerlessHeartbeatTime = 0;
87
+ const SERVERLESS_HEARTBEAT_THROTTLE_MS = 30000;
88
+
83
89
  let eventHandler = null;
84
90
  events.setEventHandler = (handler) => {
85
91
  if (typeof handler === 'function') {
@@ -521,6 +527,39 @@ module.exports = ({
521
527
  } else {
522
528
  onEvent(eventName, payload);
523
529
  }
530
+
531
+ if (
532
+ [
533
+ BlockletEvents.started,
534
+ BlockletEvents.removed,
535
+ BlockletEvents.statusChange,
536
+ BlockletEvents.installed,
537
+ BlockletEvents.componentInstalled,
538
+ BlockletEvents.componentRemoved,
539
+ ].includes(eventName)
540
+ ) {
541
+ const now = Date.now();
542
+ const remainingMs = SERVERLESS_HEARTBEAT_THROTTLE_MS - (now - lastServerlessHeartbeatTime);
543
+ if (remainingMs <= 0) {
544
+ lastServerlessHeartbeatTime = now;
545
+ node
546
+ .getNodeInfo()
547
+ .then((nodeInfo) => {
548
+ if (isInServerlessMode({ mode: nodeInfo.mode })) {
549
+ logger.info('send serverless heartbeat', { eventName });
550
+ sendServerlessHeartbeat();
551
+ }
552
+ })
553
+ .catch((error) => {
554
+ logger.error('Failed to get node info to send serverless heartbeat', { error, eventName });
555
+ });
556
+ } else {
557
+ logger.debug('serverless heartbeat throttled', {
558
+ eventName,
559
+ remainingMs,
560
+ });
561
+ }
562
+ }
524
563
  };
525
564
 
526
565
  const downloadAddedBlocklet = async () => {
@@ -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;