cabloy 5.1.221 → 5.1.222

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +11 -0
  3. package/package.json +1 -1
  4. package/repo-agent-governance/managed-assets.json +15 -15
  5. package/repo-agent-governance/policies/repository.md +1 -0
  6. package/repo-agent-governance/skills/cabloy-backend-scaffold/SKILL.md +5 -1
  7. package/repo-agent-governance/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +7 -5
  8. package/repo-agent-governance/skills/cabloy-frontend-scaffold/SKILL.md +5 -1
  9. package/repo-agent-governance/skills/cabloy-frontend-scaffold/references/follow-up-checklist.md +6 -4
  10. package/repo-docs/backend/unit-testing.md +5 -1
  11. package/repo-docs/frontend/scripts.md +8 -0
  12. package/vona/packages-cli/cabloy-cli/package.json +1 -1
  13. package/vona/packages-cli/cli/package.json +1 -1
  14. package/vona/packages-cli/cli-set-api/package.json +1 -1
  15. package/vona/packages-utils/process-helper/package.json +1 -1
  16. package/vona/packages-utils/process-helper/src/index.test.ts +62 -0
  17. package/vona/packages-utils/process-helper/src/index.ts +9 -4
  18. package/vona/packages-vona/vona/package.json +1 -1
  19. package/vona/packages-vona/vona-core/package.json +1 -1
  20. package/vona/packages-vona/vona-core/src/lib/core/completedReadiness.test.ts +558 -0
  21. package/vona/packages-vona/vona-core/src/lib/core/completedReadiness.ts +389 -0
  22. package/vona/packages-vona/vona-core/src/lib/core/context.ts +3 -0
  23. package/vona/packages-vona/vona-core/src/lib/core/index.ts +11 -0
  24. package/vona/packages-vona/vona-core/src/lib/core/meta.ts +5 -0
  25. package/vona/packages-vona/vona-core/src/lib/framework/start.test.ts +10 -0
  26. package/vona/packages-vona/vona-core/src/lib/framework/start.ts +8 -0
  27. package/vona/packages-vona/vona-mock/package.json +1 -1
  28. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
  29. package/vona/src/suite-vendor/a-vona/modules/a-executor/package.json +1 -1
  30. package/vona/src/suite-vendor/a-vona/modules/a-executor/src/bean/bean.executor.ts +124 -4
  31. package/vona/src/suite-vendor/a-vona/modules/a-executor/src/types/executor.ts +7 -0
  32. package/vona/src/suite-vendor/a-vona/modules/a-executor/test/readyExisting.test.ts +423 -0
  33. package/vona/src/suite-vendor/a-vona/modules/a-executor/test/readyExistingCommit.test.ts +174 -0
  34. package/vona/src/suite-vendor/a-vona/modules/a-instance/package.json +1 -1
  35. package/vona/src/suite-vendor/a-vona/modules/a-instance/src/bean/bean.instance.ts +3 -1
  36. package/vona/src/suite-vendor/a-vona/modules/a-instance/src/lib/existingReadyAdmission.ts +125 -0
  37. package/vona/src/suite-vendor/a-vona/modules/a-instance/src/service/instance.ts +72 -25
  38. package/vona/src/suite-vendor/a-vona/modules/a-instance/test/existingReadyAdmission.test.ts +949 -0
  39. package/vona/src/suite-vendor/a-vona/modules/a-logger/package.json +1 -1
  40. package/vona/src/suite-vendor/a-vona/modules/a-logger/test/middlewareSystem.httpLog.test.ts +61 -59
  41. package/vona/src/suite-vendor/a-vona/modules/a-mail/package.json +1 -1
  42. package/vona/src/suite-vendor/a-vona/modules/a-mail/src/service/mail.ts +5 -0
  43. package/vona/src/suite-vendor/a-vona/modules/a-startup/package.json +1 -1
  44. package/vona/src/suite-vendor/a-vona/modules/a-startup/test/devSeed.test.ts +61 -2
  45. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  46. package/zova/package.original.json +0 -1
  47. package/zova/packages-cli/cli/package.json +4 -4
  48. package/zova/packages-cli/cli-set-front/package.json +2 -2
  49. package/zova/packages-zova/zova/package.json +2 -2
  50. package/zova/pnpm-lock.yaml +7 -78
  51. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
  52. package/zova/src/suite-vendor/a-zova/package.json +2 -2
@@ -0,0 +1,174 @@
1
+ import type { VonaApplication, VonaContext } from 'vona';
2
+ import type { ServiceDb } from 'vona-module-a-orm';
3
+
4
+ import assert from 'node:assert/strict';
5
+ import { AsyncLocalStorage } from 'node:async_hooks';
6
+ import { describe, it } from 'node:test';
7
+ import {
8
+ beginCompletedInstance,
9
+ captureCompletedInstance,
10
+ createCompletedReadiness,
11
+ createRequestBoundary,
12
+ finishCompletedInstance,
13
+ runWithRequestBoundary,
14
+ } from 'vona';
15
+ import {
16
+ ServiceDatabaseAsyncLocalStorage,
17
+ ServiceTransactionAsyncLocalStorage,
18
+ ServiceTransactionConsistency‌,
19
+ ServiceTransactionState,
20
+ } from 'vona-module-a-orm';
21
+
22
+ import { VonaAsyncLocalStorage } from '../../../../../../packages-vona/vona-core/src/lib/core/asyncLocalStorage.ts';
23
+ import {
24
+ beginCompletedApplication,
25
+ finishCompletedApplication,
26
+ getCompletedPublication,
27
+ issueExistingReadyCapability,
28
+ provisionCompletedInstanceForTest,
29
+ } from '../../../../../../packages-vona/vona-core/src/lib/core/completedReadiness.ts';
30
+ import { BeanExecutor } from '../src/bean/bean.executor.ts';
31
+
32
+ // Real executor, context disposal and ORM callback queues with inert, test-owned DB ports.
33
+ // No application singleton, persistence, environment configuration or listener is used.
34
+ function fixture() {
35
+ const events: string[] = [];
36
+ let active = 0;
37
+ const database = new ServiceDatabaseAsyncLocalStorage();
38
+ database.dbStorage = new AsyncLocalStorage();
39
+ const transaction = new ServiceTransactionAsyncLocalStorage();
40
+ transaction.transactionStorage = new AsyncLocalStorage();
41
+ Object.defineProperty(transaction, 'bean', {
42
+ value: { _newBean: () => new ServiceTransactionState() },
43
+ });
44
+ const handle = {} as ServiceDb;
45
+ let storage: VonaAsyncLocalStorage;
46
+ const local = {
47
+ meta: {
48
+ completedReadiness: undefined as ReturnType<typeof createCompletedReadiness> | undefined,
49
+ ctxCounter: {
50
+ increment() {
51
+ active++;
52
+ },
53
+ decrement() {
54
+ active--;
55
+ },
56
+ },
57
+ },
58
+ get currentContext() {
59
+ return storage.getStore();
60
+ },
61
+ createAnonymousContext() {
62
+ const consistency = new ServiceTransactionConsistency‌();
63
+ return {
64
+ commit: (cb: () => unknown) => consistency.commit(cb),
65
+ commitsDone: () => consistency.commitsDone(),
66
+ bean: {
67
+ async dispose() {
68
+ events.push('disposed');
69
+ },
70
+ },
71
+ } as unknown as VonaContext;
72
+ },
73
+ };
74
+ storage = new VonaAsyncLocalStorage(local as unknown as VonaApplication);
75
+ Object.assign(local, { ctxStorage: storage });
76
+ const executor = Object.create(BeanExecutor.prototype) as BeanExecutor;
77
+ Object.defineProperties(executor, {
78
+ app: { value: local },
79
+ ctx: { get: () => local.currentContext },
80
+ bean: {
81
+ value: {
82
+ _getBean(selector: unknown) {
83
+ assert.ok(
84
+ selector === ServiceDatabaseAsyncLocalStorage ||
85
+ selector === ServiceTransactionAsyncLocalStorage,
86
+ );
87
+ return selector === ServiceDatabaseAsyncLocalStorage ? database : transaction;
88
+ },
89
+ },
90
+ },
91
+ });
92
+ const owner = createCompletedReadiness(local);
93
+ local.meta.completedReadiness = owner;
94
+ const registry = {},
95
+ route = {};
96
+ provisionCompletedInstanceForTest(owner, '', { registry, route, handle, isolate: false });
97
+ const application = beginCompletedApplication(owner);
98
+ const attempt = beginCompletedInstance(owner, '');
99
+ const row = { id: 1, name: '', disabled: false, isolate: false, config: null };
100
+ const captured = captureCompletedInstance(owner, '', attempt, row, {});
101
+ finishCompletedInstance(owner, '', attempt, captured);
102
+ finishCompletedApplication(owner, application, true);
103
+ const publication = getCompletedPublication(owner, '', registry, route);
104
+ assert.ok(publication);
105
+ const capability = issueExistingReadyCapability(owner, publication, 1_000, () => 0);
106
+ return {
107
+ local,
108
+ executor,
109
+ database,
110
+ transaction,
111
+ handle,
112
+ capability,
113
+ events,
114
+ active: () => active,
115
+ };
116
+ }
117
+
118
+ describe('ready-existing commit lifecycle', { concurrency: false }, () => {
119
+ it('awaits async and nested commit callbacks before disposal in the pinned scope', async () => {
120
+ const f = fixture();
121
+ const result = await runWithRequestBoundary(createRequestBoundary(), () =>
122
+ f.executor.newCtxReadyExisting(f.capability, async () => {
123
+ const context = f.local.currentContext!;
124
+ const state = f.transaction.transactionState;
125
+ f.events.push('callback');
126
+ context.commit(async () => {
127
+ f.events.push('commit');
128
+ assert.equal(f.local.currentContext, context);
129
+ assert.equal(f.database.current, f.handle);
130
+ assert.equal(f.transaction.transactionState, state);
131
+ assert.equal(f.active(), 1);
132
+ await Promise.resolve();
133
+ context.commit(() => f.events.push('nested_commit'));
134
+ f.events.push('commit_done');
135
+ });
136
+ return 'result';
137
+ }),
138
+ );
139
+ assert.equal(result, 'result');
140
+ assert.deepEqual(f.events, ['callback', 'commit', 'commit_done', 'nested_commit', 'disposed']);
141
+ assert.equal(f.active(), 0);
142
+ assert.equal(f.local.currentContext, undefined);
143
+ assert.equal(f.database.current, undefined);
144
+ assert.equal(f.transaction.transactionState, undefined);
145
+ });
146
+
147
+ for (const phase of ['callback', 'commit'] as const) {
148
+ it(`propagates ${phase} failure and still disposes the context`, async () => {
149
+ const f = fixture();
150
+ const failure = new Error(`controlled_${phase}_failure`);
151
+ await assert.rejects(
152
+ runWithRequestBoundary(createRequestBoundary(), () =>
153
+ f.executor.newCtxReadyExisting(f.capability, async () => {
154
+ f.events.push('callback');
155
+ f.local.currentContext!.commit(() => {
156
+ f.events.push('commit');
157
+ throw failure;
158
+ });
159
+ if (phase === 'callback') throw failure;
160
+ }),
161
+ ),
162
+ error => error === failure,
163
+ );
164
+ assert.deepEqual(
165
+ f.events,
166
+ phase === 'callback' ? ['callback', 'disposed'] : ['callback', 'commit', 'disposed'],
167
+ );
168
+ assert.equal(f.active(), 0);
169
+ assert.equal(f.local.currentContext, undefined);
170
+ assert.equal(f.database.current, undefined);
171
+ assert.equal(f.transaction.transactionState, undefined);
172
+ });
173
+ }
174
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-instance",
3
- "version": "5.1.14",
3
+ "version": "5.1.15",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "author": "zhennann",
@@ -1,7 +1,7 @@
1
1
  import type { IInstanceRecord, VonaConfig } from 'vona';
2
2
 
3
3
  import { isNil } from '@cabloy/utils';
4
- import { BeanBase } from 'vona';
4
+ import { BeanBase, invalidateCompletedInstance } from 'vona';
5
5
  import { Bean } from 'vona-module-a-bean';
6
6
 
7
7
  import type { EntityInstance } from '../entity/instance.ts';
@@ -83,12 +83,14 @@ export class BeanInstance extends BeanBase {
83
83
  async reload(instanceName?: keyof IInstanceRecord) {
84
84
  const instanceName2 = instanceName ?? this.ctx.instanceName;
85
85
  if (isNil(instanceName2)) throw new Error('Should specify the instanceName');
86
+ invalidateCompletedInstance(this.app.meta.completedReadiness, instanceName2);
86
87
  await this.reloadWorker(instanceName2);
87
88
  // broadcast
88
89
  this.scope.broadcast.reload.emit(instanceName2);
89
90
  }
90
91
 
91
92
  async reloadWorker(instanceName: keyof IInstanceRecord) {
93
+ invalidateCompletedInstance(this.app.meta.completedReadiness, instanceName);
92
94
  await this.scope.service.instance.instanceStartupIsolate(instanceName, { force: true });
93
95
  }
94
96
  }
@@ -0,0 +1,125 @@
1
+ import type { CompletedReadiness, ExistingReadyCapability } from 'vona';
2
+
3
+ import { captureRequestBoundary, resolveExistingReadyCapability } from 'vona';
4
+
5
+ export type ExistingReadyOutcome =
6
+ | 'ready'
7
+ | 'invalid_input'
8
+ | 'boundary_required'
9
+ | 'busy'
10
+ | 'expired'
11
+ | 'unavailable';
12
+ export type ExistingReadyResult = Readonly<
13
+ | { outcome: 'ready'; capability: ExistingReadyCapability }
14
+ | { outcome: Exclude<ExistingReadyOutcome, 'ready'> }
15
+ >;
16
+
17
+ interface AdmissionDependencies {
18
+ readonly owner: CompletedReadiness;
19
+ readonly registry: object;
20
+ readonly route: object;
21
+ readonly read: (name: string) => unknown | Promise<unknown>;
22
+ readonly now: () => number;
23
+ readonly getCompletedPublication: (
24
+ owner: CompletedReadiness,
25
+ name: string,
26
+ registry: object,
27
+ route: object,
28
+ ) => object | undefined;
29
+ readonly matchesCompletedRow: (publication: object, row: unknown) => boolean;
30
+ readonly issueExistingReadyCapability: (
31
+ owner: CompletedReadiness,
32
+ publication: object,
33
+ deadline: number,
34
+ now: () => number,
35
+ ) => ExistingReadyCapability;
36
+ }
37
+
38
+ // Deliberately unregistered and implementation-file-only. Production read-port provisioning
39
+ // and driver diagnostics require their own qualification, not adoption of caller handles.
40
+ export function createExistingReadyAdmissionForTest(dependencies: AdmissionDependencies) {
41
+ if (captureRequestBoundary()) throw new Error('Provisioning requires lifecycle boundary');
42
+ const {
43
+ owner,
44
+ registry,
45
+ route,
46
+ read,
47
+ now,
48
+ getCompletedPublication,
49
+ matchesCompletedRow,
50
+ issueExistingReadyCapability,
51
+ } = dependencies;
52
+ let active = false;
53
+ const outcome = (value: Exclude<ExistingReadyOutcome, 'ready'>): ExistingReadyResult =>
54
+ Object.freeze({ outcome: value });
55
+ async function check(
56
+ name: unknown,
57
+ deadlineAt?: number,
58
+ prior?: ExistingReadyCapability,
59
+ ): Promise<ExistingReadyResult> {
60
+ const boundary = captureRequestBoundary();
61
+ if (!boundary || boundary.snapshot().portFault) return outcome('boundary_required');
62
+ if (
63
+ typeof name !== 'string' ||
64
+ !name.isWellFormed() ||
65
+ name.includes('\0') ||
66
+ Buffer.byteLength(name) > 256
67
+ ) {
68
+ return outcome('invalid_input');
69
+ }
70
+ if (active) return outcome('busy');
71
+ active = true;
72
+ try {
73
+ const started = now();
74
+ if (!Number.isFinite(started) || (deadlineAt !== undefined && !Number.isFinite(deadlineAt)))
75
+ return outcome('invalid_input');
76
+ const deadline = Math.min(deadlineAt ?? started + 1_000, started + 1_000);
77
+ if (started >= deadline) return outcome('expired');
78
+ const publication = getCompletedPublication(owner, name, registry, route);
79
+ const previous = prior && resolveExistingReadyCapability(owner, prior);
80
+ if (!publication || (prior && previous?.publication !== publication))
81
+ return outcome('unavailable');
82
+ const result = await read(name);
83
+ const ended = now();
84
+ if (!Number.isFinite(ended) || ended < started || ended >= deadline)
85
+ return outcome('expired');
86
+ if (
87
+ boundary.snapshot().portFault ||
88
+ getCompletedPublication(owner, name, registry, route) !== publication ||
89
+ (prior && resolveExistingReadyCapability(owner, prior)?.publication !== publication)
90
+ ) {
91
+ return outcome('unavailable');
92
+ }
93
+ if (!result || typeof result !== 'object') return outcome('unavailable');
94
+ const descriptor = Object.getOwnPropertyDescriptor(result, 'outcome');
95
+ const row = Object.getOwnPropertyDescriptor(result, 'row');
96
+ if (
97
+ !descriptor ||
98
+ !('value' in descriptor) ||
99
+ descriptor.value !== 'found' ||
100
+ !row ||
101
+ !('value' in row) ||
102
+ !matchesCompletedRow(publication, row.value)
103
+ ) {
104
+ return outcome('unavailable');
105
+ }
106
+ return Object.freeze({
107
+ outcome: 'ready',
108
+ capability: prior ?? issueExistingReadyCapability(owner, publication, deadline, now),
109
+ });
110
+ } catch {
111
+ return outcome('unavailable');
112
+ } finally {
113
+ active = false;
114
+ }
115
+ }
116
+ return Object.freeze({
117
+ admit: (name: string, deadlineAt?: number) => check(name, deadlineAt),
118
+ recheck: (capability: ExistingReadyCapability, deadlineAt?: number) => {
119
+ const resolved = resolveExistingReadyCapability(owner, capability);
120
+ return resolved
121
+ ? check(resolved.name, deadlineAt, capability)
122
+ : Promise.resolve(outcome('unavailable'));
123
+ },
124
+ });
125
+ }
@@ -3,7 +3,15 @@ import type { IInstanceStartupOptions } from 'vona-module-a-startup';
3
3
 
4
4
  import { isNil, sleep } from '@cabloy/utils';
5
5
  import async from 'async';
6
- import { BeanBase, deepExtend } from 'vona';
6
+ import { AsyncResource } from 'node:async_hooks';
7
+ import {
8
+ BeanBase,
9
+ beginCompletedInstance,
10
+ captureCompletedInstance,
11
+ deepExtend,
12
+ finishCompletedInstance,
13
+ invalidateCompletedInstance,
14
+ } from 'vona';
7
15
  import { Service } from 'vona-module-a-bean';
8
16
 
9
17
  import type { IInstanceStartupQueueInfo } from '../entity/instance.ts';
@@ -73,6 +81,9 @@ export class ServiceInstance extends BeanBase {
73
81
 
74
82
  async resetAllCaches() {
75
83
  const instanceNames = Object.keys(this.__cacheIntancesConfig);
84
+ for (const instanceName of instanceNames) {
85
+ invalidateCompletedInstance(this.app.meta.completedReadiness, instanceName);
86
+ }
76
87
  for (const instanceName of instanceNames) {
77
88
  await this.app.bean.executor.newCtx(
78
89
  async () => {
@@ -84,26 +95,41 @@ export class ServiceInstance extends BeanBase {
84
95
  }
85
96
 
86
97
  async resetCache(instanceName: keyof IInstanceRecord) {
98
+ invalidateCompletedInstance(this.app.meta.completedReadiness, instanceName);
87
99
  await this._cacheInstanceConfig(instanceName, true);
88
100
  }
89
101
 
90
- private async _cacheInstanceConfig(instanceName: keyof IInstanceRecord, force?: boolean) {
102
+ private async _cacheInstanceConfig(
103
+ instanceName: keyof IInstanceRecord,
104
+ force?: boolean,
105
+ attempt?: object,
106
+ preparedBase?: ConfigInstanceBase,
107
+ ) {
91
108
  if (this.__cacheIntancesConfig[instanceName] && !force) return;
92
109
  let instance = await this.bean.instance.get(instanceName);
93
110
  if (!instance) this.app.throw(403);
94
111
  instance = instance!;
95
- // config
96
- const configInstanceBase = this.getConfigInstanceBase(instanceName);
97
- const instanceConfigDb = instance.config ? JSON.parse(instance.config) : undefined;
112
+ // Capture persisted input before any other lifecycle operation can replace its cache.
113
+ const row = {
114
+ id: instance.id,
115
+ name: instance.name,
116
+ disabled: instance.disabled,
117
+ isolate: instance.isolate,
118
+ config: instance.config ?? null,
119
+ };
120
+ const configInstanceBase = preparedBase ?? this.getConfigInstanceBase(instanceName);
121
+ const instanceConfigDb = row.config ? JSON.parse(row.config) : undefined;
98
122
  // cache configs
99
- this.__cacheIntancesConfig[instanceName] = deepExtend(
100
- {},
101
- this.app.config,
102
- configInstanceBase?.config,
103
- instanceConfigDb,
104
- {
105
- instance: undefined,
106
- },
123
+ const merged = deepExtend({}, this.app.config, configInstanceBase?.config, instanceConfigDb, {
124
+ instance: undefined,
125
+ });
126
+ this.__cacheIntancesConfig[instanceName] = merged;
127
+ return captureCompletedInstance(
128
+ this.app.meta.completedReadiness,
129
+ instanceName,
130
+ attempt,
131
+ row,
132
+ merged,
107
133
  );
108
134
  }
109
135
 
@@ -111,6 +137,7 @@ export class ServiceInstance extends BeanBase {
111
137
  instanceName: keyof IInstanceRecord,
112
138
  options?: IInstanceStartupOptions,
113
139
  ) {
140
+ invalidateCompletedInstance(this.app.meta.completedReadiness, instanceName);
114
141
  await this.bean.executor.newCtx(
115
142
  async () => {
116
143
  await this.instanceStartup(instanceName, options);
@@ -121,27 +148,35 @@ export class ServiceInstance extends BeanBase {
121
148
 
122
149
  // options: force/instanceBase
123
150
  async instanceStartup(instanceName: keyof IInstanceRecord, options?: IInstanceStartupOptions) {
124
- if (!options) options = {};
151
+ const owner = this.app.meta.completedReadiness;
152
+ const attempt = beginCompletedInstance(owner, instanceName);
153
+ options = { ...options };
125
154
  if (!options.configInstanceBase) {
126
155
  const configInstanceBase = this.getConfigInstanceBase(instanceName);
127
156
  options.configInstanceBase = configInstanceBase;
128
157
  }
129
- // cache instance config
130
- await this._cacheInstanceConfig(instanceName, options?.force);
158
+ // Preparation is serialized with startup below; publication never rereads a later cache.
159
+ const startupOptions = options;
160
+ const prepare = async () =>
161
+ this._cacheInstanceConfig(
162
+ instanceName,
163
+ startupOptions.force,
164
+ attempt,
165
+ startupOptions.configInstanceBase,
166
+ );
131
167
  // queue within the same worker
132
168
  if (!this.__queueInstanceStartup[instanceName]) {
133
169
  this.__queueInstanceStartup[instanceName] = async.queue(
134
- (info: IInstanceStartupQueueInfo, cb: Function) => {
170
+ (
171
+ info: IInstanceStartupQueueInfo & { execute: (skip: boolean) => Promise<void> },
172
+ cb: Function,
173
+ ) => {
135
174
  // check again
136
175
  const force = info.options?.force;
137
- if (this.ctx.app.meta.appReadyInstances[info.instanceName] && !force) {
138
- info.resolve();
139
- cb();
140
- return;
141
- }
176
+ const skip = !!this.app.meta.appReadyInstances[info.instanceName] && !force;
142
177
  // startup
143
- this.$scope.startup.service.startup
144
- .runStartupInstance(info.instanceName, info.options)
178
+ info
179
+ .execute(skip)
145
180
  .then(() => {
146
181
  info.resolve();
147
182
  cb();
@@ -158,7 +193,19 @@ export class ServiceInstance extends BeanBase {
158
193
  // options
159
194
  if (!options) options = { force: false, configInstanceBase: undefined };
160
195
  // queue push
161
- const info: IInstanceStartupQueueInfo = { resolve, reject, instanceName, options };
196
+ const info: IInstanceStartupQueueInfo & { execute: (skip: boolean) => Promise<void> } = {
197
+ resolve,
198
+ reject,
199
+ instanceName,
200
+ options,
201
+ // The queue advances in the preceding worker's async context, not this caller's.
202
+ execute: AsyncResource.bind(async skip => {
203
+ const captured = await prepare();
204
+ if (skip) return;
205
+ await this.$scope.startup.service.startup.runStartupInstance(instanceName, options);
206
+ finishCompletedInstance(owner, instanceName, attempt, captured);
207
+ }),
208
+ };
162
209
  this.__queueInstanceStartup[instanceName].push(info);
163
210
  });
164
211
  }