@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/fullstack-cli",
3
- "version": "1.1.66-alpha.20260902192310",
3
+ "version": "1.1.66-alpha.20260903130240",
4
4
  "description": "CLI tool for fullstack template management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -931,7 +931,11 @@ async function main() {
931
931
  initializeActionPlugins();
932
932
  }
933
933
 
934
- // Start server and client
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 SOURCE_RETRY_INITIAL_DELAY_MS = 250;
20
- const SOURCE_RETRY_MAX_DELAY_MS = 5_000;
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
- retry();
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
- retry();
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 retry();
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
- let retryDelayMs = SOURCE_RETRY_INITIAL_DELAY_MS;
1712
- let baseEnv;
1713
- while (!shuttingDown && !baseEnv) {
1714
- try {
1715
- baseEnv = { ...process.env, ...readRuntimeEnvironment() };
1716
- await runActionPluginInit(baseEnv);
1717
- } catch (error) {
1718
- baseEnv = undefined;
1719
- reportSourcePromotionFailure('environment-and-init', error, retryDelayMs);
1720
- await waitForSourceRetry(retryDelayMs);
1721
- retryDelayMs = Math.min(retryDelayMs * 2, SOURCE_RETRY_MAX_DELAY_MS);
1722
- }
1723
- }
1724
- retryDelayMs = SOURCE_RETRY_INITIAL_DELAY_MS;
1725
- while (!shuttingDown && !promoted) {
1726
- try {
1727
- await startAndPromoteSource(baseEnv, expectedCapabilityIds);
1728
- } catch (error) {
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: 'miaoda_nest_source_promotion_stopped',
1871
- error: error instanceof Error ? error.message : String(error),
1872
- cacheContinuesServing: true,
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;