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.
- package/.cabloy-version +1 -1
- package/CHANGELOG.md +11 -0
- package/package.json +1 -1
- package/repo-agent-governance/managed-assets.json +15 -15
- package/repo-agent-governance/policies/repository.md +1 -0
- package/repo-agent-governance/skills/cabloy-backend-scaffold/SKILL.md +5 -1
- package/repo-agent-governance/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +7 -5
- package/repo-agent-governance/skills/cabloy-frontend-scaffold/SKILL.md +5 -1
- package/repo-agent-governance/skills/cabloy-frontend-scaffold/references/follow-up-checklist.md +6 -4
- package/repo-docs/backend/unit-testing.md +5 -1
- package/repo-docs/frontend/scripts.md +8 -0
- package/vona/packages-cli/cabloy-cli/package.json +1 -1
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-utils/process-helper/package.json +1 -1
- package/vona/packages-utils/process-helper/src/index.test.ts +62 -0
- package/vona/packages-utils/process-helper/src/index.ts +9 -4
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/packages-vona/vona-core/package.json +1 -1
- package/vona/packages-vona/vona-core/src/lib/core/completedReadiness.test.ts +558 -0
- package/vona/packages-vona/vona-core/src/lib/core/completedReadiness.ts +389 -0
- package/vona/packages-vona/vona-core/src/lib/core/context.ts +3 -0
- package/vona/packages-vona/vona-core/src/lib/core/index.ts +11 -0
- package/vona/packages-vona/vona-core/src/lib/core/meta.ts +5 -0
- package/vona/packages-vona/vona-core/src/lib/framework/start.test.ts +10 -0
- package/vona/packages-vona/vona-core/src/lib/framework/start.ts +8 -0
- package/vona/packages-vona/vona-mock/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-executor/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-executor/src/bean/bean.executor.ts +124 -4
- package/vona/src/suite-vendor/a-vona/modules/a-executor/src/types/executor.ts +7 -0
- package/vona/src/suite-vendor/a-vona/modules/a-executor/test/readyExisting.test.ts +423 -0
- package/vona/src/suite-vendor/a-vona/modules/a-executor/test/readyExistingCommit.test.ts +174 -0
- package/vona/src/suite-vendor/a-vona/modules/a-instance/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-instance/src/bean/bean.instance.ts +3 -1
- package/vona/src/suite-vendor/a-vona/modules/a-instance/src/lib/existingReadyAdmission.ts +125 -0
- package/vona/src/suite-vendor/a-vona/modules/a-instance/src/service/instance.ts +72 -25
- package/vona/src/suite-vendor/a-vona/modules/a-instance/test/existingReadyAdmission.test.ts +949 -0
- package/vona/src/suite-vendor/a-vona/modules/a-logger/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-logger/test/middlewareSystem.httpLog.test.ts +61 -59
- package/vona/src/suite-vendor/a-vona/modules/a-mail/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-mail/src/service/mail.ts +5 -0
- package/vona/src/suite-vendor/a-vona/modules/a-startup/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-startup/test/devSeed.test.ts +61 -2
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/package.original.json +0 -1
- package/zova/packages-cli/cli/package.json +4 -4
- package/zova/packages-cli/cli-set-front/package.json +2 -2
- package/zova/packages-zova/zova/package.json +2 -2
- package/zova/pnpm-lock.yaml +7 -78
- package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
- 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,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 {
|
|
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(
|
|
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
|
-
//
|
|
96
|
-
const
|
|
97
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
130
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
138
|
-
info.resolve();
|
|
139
|
-
cb();
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
176
|
+
const skip = !!this.app.meta.appReadyInstances[info.instanceName] && !force;
|
|
142
177
|
// startup
|
|
143
|
-
|
|
144
|
-
.
|
|
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
|
|
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
|
}
|