@lark-apaas/fullstack-cli 1.1.66-alpha.20260902192310 → 1.1.66-alpha.20260903130240
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/package.json
CHANGED
package/templates/scripts/dev.js
CHANGED
|
@@ -931,7 +931,11 @@ async function main() {
|
|
|
931
931
|
initializeActionPlugins();
|
|
932
932
|
}
|
|
933
933
|
|
|
934
|
-
//
|
|
934
|
+
// Client startup never waits for the source Nest process. With a verified
|
|
935
|
+
// handoff owner, the image runtime already serves port 3000 and owns the
|
|
936
|
+
// delayed source lifecycle; dev.js starts only Vite and watches that owner.
|
|
937
|
+
// Without that receipt this block keeps the legacy ownership model: start
|
|
938
|
+
// source Nest and Vite together.
|
|
935
939
|
const serverPromise = handoffOwnsBackend
|
|
936
940
|
? Promise.resolve()
|
|
937
941
|
: startProcess({
|
|
@@ -8,6 +8,15 @@ import { spawn } from 'node:child_process';
|
|
|
8
8
|
import { createHash, randomUUID } from 'node:crypto';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
10
|
|
|
11
|
+
// Restore-time state machine:
|
|
12
|
+
// validate immutable generation -> start cached Nest -> bind public proxy
|
|
13
|
+
// -> publish owner -> serve the first HTML response
|
|
14
|
+
// -> wait a fixed 15s -> start source Nest in the background
|
|
15
|
+
// -> switch only after source readiness, then drain/stop the cache.
|
|
16
|
+
// Source startup is therefore outside the first-screen critical path. The
|
|
17
|
+
// browser's MiaodaPreviewRendered event is observability only and is not a
|
|
18
|
+
// control-plane input. A failed source start terminates the handoff runtime so
|
|
19
|
+
// the backend immediately returns to the legacy startup-failure lifecycle.
|
|
11
20
|
const SCHEMA_VERSION = 7;
|
|
12
21
|
const BUNDLE_ENTRY = 'payload/server.bundle.cjs';
|
|
13
22
|
const MAX_GENERATION_FILES = 50_000;
|
|
@@ -16,8 +25,8 @@ const MAX_GENERATION_FILE_BYTES = 64 * 1024 * 1024;
|
|
|
16
25
|
const MAX_FILE_INDEX_BYTES = 16 * 1024 * 1024;
|
|
17
26
|
const MAX_MANIFEST_BYTES = 1024 * 1024;
|
|
18
27
|
const MAX_READINESS_RESPONSE_BYTES = 64 * 1024;
|
|
19
|
-
const
|
|
20
|
-
const
|
|
28
|
+
const DEFAULT_SOURCE_START_DELAY_MS = 15_000;
|
|
29
|
+
const TEST_SOURCE_START_DELAY_ARG = '--miaoda-test-source-start-delay-ms=';
|
|
21
30
|
const RUNTIME_CONTROL_ENV_KEYS = new Set([
|
|
22
31
|
'MIAODA_COLD_START_DECISION_FILE',
|
|
23
32
|
'MIAODA_DEPENDENCY_READY_FILE',
|
|
@@ -91,6 +100,7 @@ const shutdownTimeoutMs = positiveDuration(
|
|
|
91
100
|
5_000
|
|
92
101
|
);
|
|
93
102
|
const drainTimeoutMs = positiveDuration('MIAODA_NEST_DRAIN_TIMEOUT_MS', 5_000);
|
|
103
|
+
const sourceStartDelayMs = sourceStartDelayForRuntime();
|
|
94
104
|
const processStartTicks =
|
|
95
105
|
readLinuxProcessStartTicks(process.pid) ||
|
|
96
106
|
String(
|
|
@@ -221,12 +231,32 @@ let resolveLifetime;
|
|
|
221
231
|
const lifetime = new Promise(resolve => {
|
|
222
232
|
resolveLifetime = resolve;
|
|
223
233
|
});
|
|
234
|
+
let firstHtmlTransferredAtMs = 0;
|
|
235
|
+
let resolveFirstHtmlTransferred;
|
|
236
|
+
const firstHtmlTransferred = new Promise(resolve => {
|
|
237
|
+
resolveFirstHtmlTransferred = resolve;
|
|
238
|
+
});
|
|
224
239
|
|
|
225
240
|
function positiveDuration(name, fallback) {
|
|
226
241
|
const value = Number(process.env[name] || fallback);
|
|
227
242
|
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
228
243
|
}
|
|
229
244
|
|
|
245
|
+
function sourceStartDelayForRuntime() {
|
|
246
|
+
// Production starts this file without extra argv, so the delay is fixed.
|
|
247
|
+
// The explicit argument exists only to keep integration tests fast and
|
|
248
|
+
// deterministic; inherited workspace environment cannot shorten the delay.
|
|
249
|
+
const testArgument = process.argv
|
|
250
|
+
.slice(2)
|
|
251
|
+
.find(argument => argument.startsWith(TEST_SOURCE_START_DELAY_ARG));
|
|
252
|
+
if (!testArgument) return DEFAULT_SOURCE_START_DELAY_MS;
|
|
253
|
+
const value = Number(testArgument.slice(TEST_SOURCE_START_DELAY_ARG.length));
|
|
254
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
255
|
+
throw new Error('MIAODA_NEST_TEST_SOURCE_START_DELAY_INVALID');
|
|
256
|
+
}
|
|
257
|
+
return value;
|
|
258
|
+
}
|
|
259
|
+
|
|
230
260
|
function validateRuntimeConfiguration() {
|
|
231
261
|
if (process.env.MIAODA_NEST_BUNDLE_HANDOFF_ENABLED !== 'true') {
|
|
232
262
|
throw new Error('MIAODA_NEST_BUNDLE_HANDOFF_DISABLED');
|
|
@@ -898,7 +928,11 @@ function waitForHttp(
|
|
|
898
928
|
if (response.statusCode !== 200) {
|
|
899
929
|
attemptSettled = true;
|
|
900
930
|
response.resume();
|
|
901
|
-
|
|
931
|
+
finish(
|
|
932
|
+
new Error(
|
|
933
|
+
`${record.name} HTTP readiness rejected with status ${response.statusCode}`
|
|
934
|
+
)
|
|
935
|
+
);
|
|
902
936
|
return;
|
|
903
937
|
}
|
|
904
938
|
const chunks = [];
|
|
@@ -910,7 +944,9 @@ function waitForHttp(
|
|
|
910
944
|
if (responseBytes > MAX_READINESS_RESPONSE_BYTES) {
|
|
911
945
|
attemptSettled = true;
|
|
912
946
|
response.destroy();
|
|
913
|
-
|
|
947
|
+
finish(
|
|
948
|
+
new Error(`${record.name} HTTP readiness response too large`)
|
|
949
|
+
);
|
|
914
950
|
return;
|
|
915
951
|
}
|
|
916
952
|
chunks.push(buffer);
|
|
@@ -925,7 +961,10 @@ function waitForHttp(
|
|
|
925
961
|
)
|
|
926
962
|
)
|
|
927
963
|
finish();
|
|
928
|
-
else
|
|
964
|
+
else
|
|
965
|
+
finish(
|
|
966
|
+
new Error(`${record.name} HTTP readiness payload rejected`)
|
|
967
|
+
);
|
|
929
968
|
});
|
|
930
969
|
response.once('error', () => {
|
|
931
970
|
if (attemptSettled || settled) return;
|
|
@@ -964,6 +1003,36 @@ function retain(upstream) {
|
|
|
964
1003
|
};
|
|
965
1004
|
}
|
|
966
1005
|
|
|
1006
|
+
function observeFirstCacheHtmlTransfer(request, response, upstreamResponse) {
|
|
1007
|
+
if (
|
|
1008
|
+
firstHtmlTransferredAtMs ||
|
|
1009
|
+
request.method !== 'GET' ||
|
|
1010
|
+
activeUpstream !== cacheUpstream ||
|
|
1011
|
+
upstreamResponse.statusCode < 200 ||
|
|
1012
|
+
upstreamResponse.statusCode >= 400 ||
|
|
1013
|
+
!String(upstreamResponse.headers['content-type'] || '')
|
|
1014
|
+
.toLowerCase()
|
|
1015
|
+
.startsWith('text/html')
|
|
1016
|
+
) {
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
response.once('finish', () => {
|
|
1020
|
+
if (firstHtmlTransferredAtMs) return;
|
|
1021
|
+
firstHtmlTransferredAtMs = Date.now();
|
|
1022
|
+
resolveFirstHtmlTransferred(firstHtmlTransferredAtMs);
|
|
1023
|
+
process.stderr.write(
|
|
1024
|
+
`${JSON.stringify({
|
|
1025
|
+
event: 'miaoda_nest_html_transferred',
|
|
1026
|
+
bootId,
|
|
1027
|
+
generationId: currentGenerationId,
|
|
1028
|
+
sourceStartDelayMs,
|
|
1029
|
+
atMs: firstHtmlTransferredAtMs,
|
|
1030
|
+
elapsedMs: firstHtmlTransferredAtMs - restoreEpochMs,
|
|
1031
|
+
})}\n`
|
|
1032
|
+
);
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
|
|
967
1036
|
function createProxy() {
|
|
968
1037
|
const server = http.createServer((request, response) => {
|
|
969
1038
|
const upstream = activeUpstream;
|
|
@@ -982,6 +1051,7 @@ function createProxy() {
|
|
|
982
1051
|
headers: request.headers,
|
|
983
1052
|
},
|
|
984
1053
|
upstreamResponse => {
|
|
1054
|
+
observeFirstCacheHtmlTransfer(request, response, upstreamResponse);
|
|
985
1055
|
response.writeHead(
|
|
986
1056
|
upstreamResponse.statusCode || 502,
|
|
987
1057
|
upstreamResponse.statusMessage || '',
|
|
@@ -1590,6 +1660,21 @@ async function waitForReceipt(expectedGenerationId) {
|
|
|
1590
1660
|
if (shuttingDown) throw new Error('MIAODA_NEST_RUNTIME_STOPPING');
|
|
1591
1661
|
}
|
|
1592
1662
|
|
|
1663
|
+
async function waitForSourceStartWindow() {
|
|
1664
|
+
// `finish` on the public proxy response proves the complete cached HTML body
|
|
1665
|
+
// has been handed to Node's HTTP layer. It is intentionally independent of
|
|
1666
|
+
// frontend callbacks and of source readiness.
|
|
1667
|
+
if (sourceStartDelayMs === 0) return;
|
|
1668
|
+
const htmlTransferredAtMs = await firstHtmlTransferred;
|
|
1669
|
+
const deadline = htmlTransferredAtMs + sourceStartDelayMs;
|
|
1670
|
+
while (!shuttingDown && Date.now() < deadline) {
|
|
1671
|
+
await new Promise(resolve =>
|
|
1672
|
+
setTimeout(resolve, Math.min(50, deadline - Date.now()))
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
if (shuttingDown) throw new Error('MIAODA_NEST_RUNTIME_STOPPING');
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1593
1678
|
async function runActionPluginInit(baseEnv) {
|
|
1594
1679
|
const record = spawnOwned(
|
|
1595
1680
|
'action-plugin-init',
|
|
@@ -1619,27 +1704,6 @@ async function waitForDrain(upstream) {
|
|
|
1619
1704
|
}
|
|
1620
1705
|
}
|
|
1621
1706
|
|
|
1622
|
-
function reportSourcePromotionFailure(stage, error, retryDelayMs) {
|
|
1623
|
-
process.stderr.write(
|
|
1624
|
-
`${JSON.stringify({
|
|
1625
|
-
event: 'miaoda_nest_source_promotion_retry',
|
|
1626
|
-
stage,
|
|
1627
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1628
|
-
retryDelayMs,
|
|
1629
|
-
cacheContinuesServing: true,
|
|
1630
|
-
})}\n`
|
|
1631
|
-
);
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
async function waitForSourceRetry(delayMs) {
|
|
1635
|
-
const deadline = Date.now() + delayMs;
|
|
1636
|
-
while (!shuttingDown && Date.now() < deadline) {
|
|
1637
|
-
await new Promise(resolve =>
|
|
1638
|
-
setTimeout(resolve, Math.min(50, deadline - Date.now()))
|
|
1639
|
-
);
|
|
1640
|
-
}
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
1707
|
async function startAndPromoteSource(baseEnv, expectedCapabilityIds) {
|
|
1644
1708
|
const sourcePort = await reservePort(probeHost(publicHost));
|
|
1645
1709
|
const candidate = {
|
|
@@ -1708,30 +1772,24 @@ async function startAndPromoteSource(baseEnv, expectedCapabilityIds) {
|
|
|
1708
1772
|
|
|
1709
1773
|
async function promoteSource(expectedGenerationId, expectedCapabilityIds) {
|
|
1710
1774
|
await waitForReceipt(expectedGenerationId);
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
if (shuttingDown) break;
|
|
1730
|
-
reportSourcePromotionFailure('source-readiness', error, retryDelayMs);
|
|
1731
|
-
await waitForSourceRetry(retryDelayMs);
|
|
1732
|
-
retryDelayMs = Math.min(retryDelayMs * 2, SOURCE_RETRY_MAX_DELAY_MS);
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1775
|
+
await waitForSourceStartWindow();
|
|
1776
|
+
const sourceStartingAtMs = Date.now();
|
|
1777
|
+
process.stderr.write(
|
|
1778
|
+
`${JSON.stringify({
|
|
1779
|
+
event: 'miaoda_nest_source_starting',
|
|
1780
|
+
bootId,
|
|
1781
|
+
generationId: currentGenerationId,
|
|
1782
|
+
sourceStartDelayMs,
|
|
1783
|
+
atMs: sourceStartingAtMs,
|
|
1784
|
+
elapsedMs: sourceStartingAtMs - restoreEpochMs,
|
|
1785
|
+
htmlToSourceStartMs: firstHtmlTransferredAtMs
|
|
1786
|
+
? sourceStartingAtMs - firstHtmlTransferredAtMs
|
|
1787
|
+
: 0,
|
|
1788
|
+
})}\n`
|
|
1789
|
+
);
|
|
1790
|
+
const baseEnv = { ...process.env, ...readRuntimeEnvironment() };
|
|
1791
|
+
await runActionPluginInit(baseEnv);
|
|
1792
|
+
await startAndPromoteSource(baseEnv, expectedCapabilityIds);
|
|
1735
1793
|
}
|
|
1736
1794
|
|
|
1737
1795
|
function closeProxyBounded() {
|
|
@@ -1832,6 +1890,10 @@ async function main() {
|
|
|
1832
1890
|
[generation.entryFile],
|
|
1833
1891
|
{
|
|
1834
1892
|
env: {
|
|
1893
|
+
// The first-screen bundle is self-contained. NODE_PATH deliberately
|
|
1894
|
+
// points at the restored workspace only for latent external package
|
|
1895
|
+
// loads; the Builder probe starts from an isolated copy and rejects
|
|
1896
|
+
// workspace module resolution on the supported probe surface.
|
|
1835
1897
|
NODE_PATH: generation.workspaceNodeModulesPath,
|
|
1836
1898
|
NODE_OPTIONS: '',
|
|
1837
1899
|
NODE_ENV: 'development',
|
|
@@ -1865,13 +1927,15 @@ async function main() {
|
|
|
1865
1927
|
generation.expectedCapabilityIds
|
|
1866
1928
|
).catch(error => {
|
|
1867
1929
|
if (!shuttingDown) {
|
|
1930
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1868
1931
|
process.stderr.write(
|
|
1869
1932
|
`${JSON.stringify({
|
|
1870
|
-
event: '
|
|
1871
|
-
error:
|
|
1872
|
-
cacheContinuesServing:
|
|
1933
|
+
event: 'miaoda_nest_source_promotion_failed',
|
|
1934
|
+
error: detail,
|
|
1935
|
+
cacheContinuesServing: false,
|
|
1873
1936
|
})}\n`
|
|
1874
1937
|
);
|
|
1938
|
+
void shutdown(`source-promotion-failed:${detail}`, 1);
|
|
1875
1939
|
}
|
|
1876
1940
|
});
|
|
1877
1941
|
await lifetime;
|