@superblocksteam/sdk 2.0.137 → 2.0.138-next.1
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/.turbo/turbo-build.log +1 -1
- package/dist/cli-replacement/automatic-upgrades.d.ts +1 -1
- package/dist/cli-replacement/automatic-upgrades.d.ts.map +1 -1
- package/dist/cli-replacement/automatic-upgrades.js +64 -59
- package/dist/cli-replacement/automatic-upgrades.js.map +1 -1
- package/dist/cli-replacement/automatic-upgrades.test.js +54 -33
- package/dist/cli-replacement/automatic-upgrades.test.js.map +1 -1
- package/dist/cli-replacement/dependency-install-classifier.d.mts +26 -1
- package/dist/cli-replacement/dependency-install-classifier.d.mts.map +1 -1
- package/dist/cli-replacement/dependency-install-classifier.mjs +58 -0
- package/dist/cli-replacement/dependency-install-classifier.mjs.map +1 -1
- package/dist/cli-replacement/dependency-install-classifier.test.mjs +33 -1
- package/dist/cli-replacement/dependency-install-classifier.test.mjs.map +1 -1
- package/dist/cli-replacement/dev.d.mts +4 -2
- package/dist/cli-replacement/dev.d.mts.map +1 -1
- package/dist/cli-replacement/dev.interception.test.mjs +32 -5
- package/dist/cli-replacement/dev.interception.test.mjs.map +1 -1
- package/dist/cli-replacement/dev.mjs +142 -65
- package/dist/cli-replacement/dev.mjs.map +1 -1
- package/dist/cli-replacement/home-npmrc.d.mts.map +1 -1
- package/dist/cli-replacement/home-npmrc.mjs +13 -5
- package/dist/cli-replacement/home-npmrc.mjs.map +1 -1
- package/dist/cli-replacement/home-npmrc.test.mjs +49 -17
- package/dist/cli-replacement/home-npmrc.test.mjs.map +1 -1
- package/dist/cli-replacement/package-json-snapshot.d.mts +1 -0
- package/dist/cli-replacement/package-json-snapshot.d.mts.map +1 -1
- package/dist/cli-replacement/package-json-snapshot.mjs +35 -0
- package/dist/cli-replacement/package-json-snapshot.mjs.map +1 -1
- package/dist/cli-replacement/package-json-snapshot.test.mjs +104 -0
- package/dist/cli-replacement/package-json-snapshot.test.mjs.map +1 -1
- package/dist/dev-utils/dev-server-metrics.d.mts +17 -7
- package/dist/dev-utils/dev-server-metrics.d.mts.map +1 -1
- package/dist/dev-utils/dev-server-metrics.mjs +18 -12
- package/dist/dev-utils/dev-server-metrics.mjs.map +1 -1
- package/dist/dev-utils/dev-server.d.mts.map +1 -1
- package/dist/dev-utils/dev-server.mjs +15 -6
- package/dist/dev-utils/dev-server.mjs.map +1 -1
- package/dist/dev-utils/prepare-in-place-restart.d.mts +31 -0
- package/dist/dev-utils/prepare-in-place-restart.d.mts.map +1 -0
- package/dist/dev-utils/prepare-in-place-restart.mjs +64 -0
- package/dist/dev-utils/prepare-in-place-restart.mjs.map +1 -0
- package/dist/dev-utils/prepare-in-place-restart.test.d.mts +2 -0
- package/dist/dev-utils/prepare-in-place-restart.test.d.mts.map +1 -0
- package/dist/dev-utils/prepare-in-place-restart.test.mjs +46 -0
- package/dist/dev-utils/prepare-in-place-restart.test.mjs.map +1 -0
- package/dist/telemetry/index.d.ts +12 -0
- package/dist/telemetry/index.d.ts.map +1 -1
- package/dist/telemetry/index.js +26 -12
- package/dist/telemetry/index.js.map +1 -1
- package/package.json +6 -6
- package/src/cli-replacement/automatic-upgrades.test.ts +68 -58
- package/src/cli-replacement/automatic-upgrades.ts +81 -59
- package/src/cli-replacement/dependency-install-classifier.mts +74 -1
- package/src/cli-replacement/dependency-install-classifier.test.mts +49 -0
- package/src/cli-replacement/dev.interception.test.mts +39 -7
- package/src/cli-replacement/dev.mts +154 -62
- package/src/cli-replacement/home-npmrc.mts +12 -4
- package/src/cli-replacement/home-npmrc.test.mts +57 -17
- package/src/cli-replacement/package-json-snapshot.mts +50 -0
- package/src/cli-replacement/package-json-snapshot.test.mts +121 -0
- package/src/dev-utils/dev-server-metrics.mts +48 -21
- package/src/dev-utils/dev-server.mts +22 -5
- package/src/dev-utils/prepare-in-place-restart.mts +99 -0
- package/src/dev-utils/prepare-in-place-restart.test.mts +69 -0
- package/src/telemetry/index.ts +24 -10
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -11,6 +11,21 @@ import {
|
|
|
11
11
|
restoreManagedPackageDependencies,
|
|
12
12
|
} from "./package-json-snapshot.mjs";
|
|
13
13
|
|
|
14
|
+
async function writeTempPackageJson(packageJson: unknown): Promise<string> {
|
|
15
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "pkg-json-"));
|
|
16
|
+
await fs.writeFile(
|
|
17
|
+
path.join(tmpDir, "package.json"),
|
|
18
|
+
JSON.stringify(packageJson, null, 2) + "\n",
|
|
19
|
+
);
|
|
20
|
+
return tmpDir;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function readTempPackageJson(tmpDir: string): Promise<any> {
|
|
24
|
+
return JSON.parse(
|
|
25
|
+
await fs.readFile(path.join(tmpDir, "package.json"), "utf-8"),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
14
29
|
describe("packageJsonSnapshot", () => {
|
|
15
30
|
it("is stable for object key order changes", () => {
|
|
16
31
|
const before = packageJsonSnapshot({
|
|
@@ -248,3 +263,109 @@ describe("restoreManagedPackageDependencies", () => {
|
|
|
248
263
|
}
|
|
249
264
|
});
|
|
250
265
|
});
|
|
266
|
+
|
|
267
|
+
describe("restoreManagedPackageDependencies zod override", () => {
|
|
268
|
+
it("injects the warm template zod override into an app that has none", async () => {
|
|
269
|
+
const tmpDir = await writeTempPackageJson({
|
|
270
|
+
name: "restored-app",
|
|
271
|
+
dependencies: {
|
|
272
|
+
"@superblocksteam/sdk-api": "0.0.7",
|
|
273
|
+
ai: "6.0.97",
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
const changed = await restoreManagedPackageDependencies(tmpDir, {
|
|
279
|
+
overrides: { zod: "3.25.76" },
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
expect(changed).toBe(true);
|
|
283
|
+
const restored = await readTempPackageJson(tmpDir);
|
|
284
|
+
expect(restored.overrides?.zod).toBe("3.25.76");
|
|
285
|
+
} finally {
|
|
286
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("does not inject a zod override when the warm template omits it", async () => {
|
|
291
|
+
const tmpDir = await writeTempPackageJson({
|
|
292
|
+
name: "restored-app",
|
|
293
|
+
dependencies: {
|
|
294
|
+
"@superblocksteam/sdk-api": "0.0.7",
|
|
295
|
+
ai: "6.0.97",
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
const changed = await restoreManagedPackageDependencies(tmpDir, {
|
|
301
|
+
dependencies: { "@superblocksteam/sdk-api": "0.0.7" },
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
expect(changed).toBe(false);
|
|
305
|
+
const restored = await readTempPackageJson(tmpDir);
|
|
306
|
+
expect(restored.overrides).toBeUndefined();
|
|
307
|
+
} finally {
|
|
308
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("preserves unrelated overrides while enforcing the zod override", async () => {
|
|
313
|
+
const tmpDir = await writeTempPackageJson({
|
|
314
|
+
name: "restored-app",
|
|
315
|
+
dependencies: { ai: "6.0.97" },
|
|
316
|
+
overrides: { lodash: "4.17.21" },
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
await restoreManagedPackageDependencies(tmpDir, {
|
|
321
|
+
overrides: { zod: "3.25.76" },
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const restored = await readTempPackageJson(tmpDir);
|
|
325
|
+
expect(restored.overrides).toEqual({
|
|
326
|
+
lodash: "4.17.21",
|
|
327
|
+
zod: "3.25.76",
|
|
328
|
+
});
|
|
329
|
+
} finally {
|
|
330
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("overwrites a stale app zod override with the managed version", async () => {
|
|
335
|
+
const tmpDir = await writeTempPackageJson({
|
|
336
|
+
name: "restored-app",
|
|
337
|
+
dependencies: { ai: "6.0.97" },
|
|
338
|
+
overrides: { zod: "3.23.8" },
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
const changed = await restoreManagedPackageDependencies(tmpDir, {
|
|
343
|
+
overrides: { zod: "3.25.76" },
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
expect(changed).toBe(true);
|
|
347
|
+
const restored = await readTempPackageJson(tmpDir);
|
|
348
|
+
expect(restored.overrides?.zod).toBe("3.25.76");
|
|
349
|
+
} finally {
|
|
350
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("does not rewrite when the zod override already matches", async () => {
|
|
355
|
+
const tmpDir = await writeTempPackageJson({
|
|
356
|
+
name: "restored-app",
|
|
357
|
+
dependencies: { ai: "6.0.97" },
|
|
358
|
+
overrides: { zod: "3.25.76" },
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
const changed = await restoreManagedPackageDependencies(tmpDir, {
|
|
363
|
+
overrides: { zod: "3.25.76" },
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
expect(changed).toBe(false);
|
|
367
|
+
} finally {
|
|
368
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
});
|
|
@@ -403,27 +403,32 @@ class DevServerMetrics {
|
|
|
403
403
|
}
|
|
404
404
|
|
|
405
405
|
/**
|
|
406
|
-
*
|
|
406
|
+
* Emits a dependency-failure counter with the standard low-cardinality tags.
|
|
407
|
+
* Shared by the initial-install and auto-upgrade degrade paths, which differ
|
|
408
|
+
* only in counter name + description.
|
|
407
409
|
*
|
|
408
410
|
* All tags are low-cardinality:
|
|
409
|
-
* - `category`
|
|
411
|
+
* - `category` is a closed enum from the matching error type.
|
|
410
412
|
* - `npm_error_code` is a bounded npm error code string (e.g. "E401",
|
|
411
413
|
* "ERESOLVE") or `"none"` when absent.
|
|
412
|
-
* - `has_any_registry_configured` is
|
|
413
|
-
* `"false"`) or `"unknown"` when the tri-state is undefined.
|
|
414
|
+
* - `has_any_registry_configured` is `"true"` / `"false"` / `"unknown"`.
|
|
414
415
|
*
|
|
415
|
-
* Do NOT pass raw package names, registry hostnames, or rawError — those
|
|
416
|
-
*
|
|
416
|
+
* Do NOT pass raw package names, registry hostnames, or rawError — those are
|
|
417
|
+
* high-cardinality or contain customer-infra identifiers.
|
|
417
418
|
*/
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
419
|
+
private recordDependencyFailure(
|
|
420
|
+
counterName: string,
|
|
421
|
+
description: string,
|
|
422
|
+
{
|
|
423
|
+
category,
|
|
424
|
+
npmErrorCode,
|
|
425
|
+
hasAnyRegistryConfigured,
|
|
426
|
+
}: {
|
|
427
|
+
category: string;
|
|
428
|
+
npmErrorCode?: string;
|
|
429
|
+
hasAnyRegistryConfigured?: boolean;
|
|
430
|
+
},
|
|
431
|
+
): void {
|
|
427
432
|
const labels = {
|
|
428
433
|
category,
|
|
429
434
|
npm_error_code: normalizeNpmErrorCodeTag(npmErrorCode),
|
|
@@ -433,15 +438,37 @@ class DevServerMetrics {
|
|
|
433
438
|
: String(hasAnyRegistryConfigured),
|
|
434
439
|
};
|
|
435
440
|
this.record(() => {
|
|
436
|
-
getMeter()
|
|
437
|
-
.createCounter("dev_server_initial_install_failure_total", {
|
|
438
|
-
description:
|
|
439
|
-
"Count of degraded dev-server startups caused by a failed initial dependency install.",
|
|
440
|
-
})
|
|
441
|
-
.add(1, labels);
|
|
441
|
+
getMeter().createCounter(counterName, { description }).add(1, labels);
|
|
442
442
|
});
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
+
/** Records a degraded startup caused by a failed initial dependency install. */
|
|
446
|
+
recordInitialInstallFailure(labels: {
|
|
447
|
+
category: string;
|
|
448
|
+
npmErrorCode?: string;
|
|
449
|
+
hasAnyRegistryConfigured?: boolean;
|
|
450
|
+
}): void {
|
|
451
|
+
this.recordDependencyFailure(
|
|
452
|
+
"dev_server_initial_install_failure_total",
|
|
453
|
+
"Count of degraded dev-server startups caused by a failed initial dependency install.",
|
|
454
|
+
labels,
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** Records a degraded startup where a best-effort CLI/library auto-upgrade
|
|
459
|
+
* failed and the pod kept serving the currently installed version. */
|
|
460
|
+
recordUpgradeFailure(labels: {
|
|
461
|
+
category: string;
|
|
462
|
+
npmErrorCode?: string;
|
|
463
|
+
hasAnyRegistryConfigured?: boolean;
|
|
464
|
+
}): void {
|
|
465
|
+
this.recordDependencyFailure(
|
|
466
|
+
"dev_server_auto_upgrade_failure_total",
|
|
467
|
+
"Count of degraded dev-server startups where a best-effort CLI/library auto-upgrade failed and the pod kept the current version.",
|
|
468
|
+
labels,
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
445
472
|
/**
|
|
446
473
|
* Records a recoverable shutdown error that was suppressed (logged at warn,
|
|
447
474
|
* process left alive) instead of triggering a fatal exit. This is the
|
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
isRecoverableShutdownError,
|
|
71
71
|
parseIsWarm,
|
|
72
72
|
} from "./fatal-exit.mjs";
|
|
73
|
+
import { prepareInPlaceRestart } from "./prepare-in-place-restart.mjs";
|
|
73
74
|
import {
|
|
74
75
|
formatViteDevServerStartedLog,
|
|
75
76
|
logViteBuildError,
|
|
@@ -727,6 +728,22 @@ export async function createDevServer({
|
|
|
727
728
|
}
|
|
728
729
|
}
|
|
729
730
|
|
|
731
|
+
const runPrepareInPlaceRestart = async (options?: { flipRestart?: true }) => {
|
|
732
|
+
logger.info(
|
|
733
|
+
options?.flipRestart
|
|
734
|
+
? "Pausing dev server while restarting after template flip"
|
|
735
|
+
: "Pausing dev server while restarting",
|
|
736
|
+
);
|
|
737
|
+
await prepareInPlaceRestart(
|
|
738
|
+
{
|
|
739
|
+
aiService,
|
|
740
|
+
closeViteServer,
|
|
741
|
+
lockService,
|
|
742
|
+
},
|
|
743
|
+
{ flipRestart: options?.flipRestart, logger },
|
|
744
|
+
);
|
|
745
|
+
};
|
|
746
|
+
|
|
730
747
|
async function runGracefulShutdown({
|
|
731
748
|
logger,
|
|
732
749
|
serverInitiated,
|
|
@@ -1018,11 +1035,7 @@ export async function createDevServer({
|
|
|
1018
1035
|
|
|
1019
1036
|
try {
|
|
1020
1037
|
if (isRestart) {
|
|
1021
|
-
|
|
1022
|
-
lockService?.setIsRestarting();
|
|
1023
|
-
// Invalidate chat session cache to reload fresh chat messages after checkpoint restore
|
|
1024
|
-
aiService?.chatSessionStore.invalidateCache();
|
|
1025
|
-
await closeViteServer();
|
|
1038
|
+
await runPrepareInPlaceRestart();
|
|
1026
1039
|
res.send("ok");
|
|
1027
1040
|
// RESTART_EXIT_CODE tells the supervisor to respawn, and this runs
|
|
1028
1041
|
// synchronously below (no await), so the next /_sb_connect is served by
|
|
@@ -1347,6 +1360,7 @@ export async function createDevServer({
|
|
|
1347
1360
|
logger: loggerOverride,
|
|
1348
1361
|
httpServer,
|
|
1349
1362
|
superblocksBaseUrl: explicitBaseUrl || localToken?.superblocksBaseUrl,
|
|
1363
|
+
prepareInPlaceRestart: runPrepareInPlaceRestart,
|
|
1350
1364
|
features: {
|
|
1351
1365
|
enableSessionRecording: featureFlags?.enableSessionRecording() ?? false,
|
|
1352
1366
|
// Gate the in-place restart on the LD flag OR the env escape hatch
|
|
@@ -1420,6 +1434,7 @@ async function startVite({
|
|
|
1420
1434
|
logger: loggerOverride,
|
|
1421
1435
|
superblocksBaseUrl,
|
|
1422
1436
|
features,
|
|
1437
|
+
prepareInPlaceRestart,
|
|
1423
1438
|
}: CreateDevServerOptions & {
|
|
1424
1439
|
app: express.Express;
|
|
1425
1440
|
httpServer: http.Server;
|
|
@@ -1428,6 +1443,7 @@ async function startVite({
|
|
|
1428
1443
|
enableSessionRecording: boolean;
|
|
1429
1444
|
inPlaceTemplateRestartEnabled: boolean;
|
|
1430
1445
|
};
|
|
1446
|
+
prepareInPlaceRestart: (options?: { flipRestart?: true }) => Promise<void>;
|
|
1431
1447
|
}): Promise<{
|
|
1432
1448
|
viteServer: ViteDevServer;
|
|
1433
1449
|
dispose: () => void;
|
|
@@ -1509,6 +1525,7 @@ async function startVite({
|
|
|
1509
1525
|
superblocksBaseUrl,
|
|
1510
1526
|
appRootDir: root,
|
|
1511
1527
|
features,
|
|
1528
|
+
prepareInPlaceRestart,
|
|
1512
1529
|
} as FileSyncPluginParams,
|
|
1513
1530
|
{ isCustomBuildEnabled },
|
|
1514
1531
|
);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { RESTART_REASON_FLIP_RESTART } from "@superblocksteam/library-shared/restart-contract";
|
|
2
|
+
|
|
3
|
+
/** Upper bound on the pre-exit IPC handshake to the parent supervisor. */
|
|
4
|
+
const RESTART_REASON_IPC_TIMEOUT_MS = 1000;
|
|
5
|
+
|
|
6
|
+
export interface PrepareInPlaceRestartLogger {
|
|
7
|
+
warn(message: string, meta?: Record<string, unknown>): void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PrepareInPlaceRestartServices {
|
|
11
|
+
aiService?: {
|
|
12
|
+
chatSessionStore: { invalidateCache(): void };
|
|
13
|
+
};
|
|
14
|
+
closeViteServer?: () => Promise<void>;
|
|
15
|
+
lockService?: {
|
|
16
|
+
setIsRestarting(): void | Promise<void>;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PrepareInPlaceRestartOptions {
|
|
21
|
+
flipRestart?: true;
|
|
22
|
+
logger?: PrepareInPlaceRestartLogger;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Tell the CLI parent supervisor that the imminent `RESTART_EXIT_CODE` exit is a
|
|
27
|
+
* template flip — not the editor-driven `/_sb_disconnect` restart, which shares
|
|
28
|
+
* the same exit code.
|
|
29
|
+
*/
|
|
30
|
+
export async function announceFlipRestartToParent(
|
|
31
|
+
logger?: PrepareInPlaceRestartLogger,
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
const send = process.send;
|
|
34
|
+
if (!send) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
await new Promise<void>((resolve) => {
|
|
38
|
+
let settled = false;
|
|
39
|
+
const finish = (): void => {
|
|
40
|
+
if (settled) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
settled = true;
|
|
44
|
+
resolve();
|
|
45
|
+
};
|
|
46
|
+
const timer = setTimeout(finish, RESTART_REASON_IPC_TIMEOUT_MS);
|
|
47
|
+
timer.unref?.();
|
|
48
|
+
const done = (): void => {
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
finish();
|
|
51
|
+
};
|
|
52
|
+
try {
|
|
53
|
+
send.call(
|
|
54
|
+
process,
|
|
55
|
+
{ type: "restartReason", reason: RESTART_REASON_FLIP_RESTART },
|
|
56
|
+
(error: Error | null) => {
|
|
57
|
+
if (error) {
|
|
58
|
+
logger?.warn(
|
|
59
|
+
"Failed to notify parent of flip restart reason; restart will surface as cold_start",
|
|
60
|
+
{ error: error.message },
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
done();
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
logger?.warn(
|
|
68
|
+
"Failed to notify parent of flip restart reason; restart will surface as cold_start",
|
|
69
|
+
{
|
|
70
|
+
error: error instanceof Error ? error.message : String(error),
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
done();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Shared teardown before an in-place dev-server restart (`process.exit(98)`).
|
|
80
|
+
* Used by `/_sb_disconnect` with `isRestart: true` and by the migration
|
|
81
|
+
* self-restart seam after a template flip.
|
|
82
|
+
*/
|
|
83
|
+
export async function prepareInPlaceRestart(
|
|
84
|
+
services: PrepareInPlaceRestartServices,
|
|
85
|
+
options?: PrepareInPlaceRestartOptions,
|
|
86
|
+
): Promise<void> {
|
|
87
|
+
if (services.lockService) {
|
|
88
|
+
await services.lockService.setIsRestarting();
|
|
89
|
+
}
|
|
90
|
+
if (services.aiService?.chatSessionStore) {
|
|
91
|
+
services.aiService.chatSessionStore.invalidateCache();
|
|
92
|
+
}
|
|
93
|
+
if (options?.flipRestart) {
|
|
94
|
+
await announceFlipRestartToParent(options.logger);
|
|
95
|
+
}
|
|
96
|
+
if (services.closeViteServer) {
|
|
97
|
+
await services.closeViteServer();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
announceFlipRestartToParent,
|
|
5
|
+
prepareInPlaceRestart,
|
|
6
|
+
} from "./prepare-in-place-restart.mjs";
|
|
7
|
+
|
|
8
|
+
describe("prepareInPlaceRestart", () => {
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.restoreAllMocks();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("sets restarting, invalidates AI cache, and closes Vite when services are present", async () => {
|
|
14
|
+
const setIsRestarting = vi.fn();
|
|
15
|
+
const invalidateCache = vi.fn();
|
|
16
|
+
const closeViteServer = vi.fn(async () => {});
|
|
17
|
+
|
|
18
|
+
await prepareInPlaceRestart({
|
|
19
|
+
lockService: { setIsRestarting },
|
|
20
|
+
aiService: { chatSessionStore: { invalidateCache } },
|
|
21
|
+
closeViteServer,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(setIsRestarting).toHaveBeenCalledTimes(1);
|
|
25
|
+
expect(invalidateCache).toHaveBeenCalledTimes(1);
|
|
26
|
+
expect(closeViteServer).toHaveBeenCalledTimes(1);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("announces flip restart to parent when flipRestart is set", async () => {
|
|
30
|
+
const send = vi.fn(
|
|
31
|
+
(_msg: unknown, cb: (err: Error | null) => void): boolean => {
|
|
32
|
+
cb(null);
|
|
33
|
+
return true;
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
process.send = send as unknown as typeof process.send;
|
|
37
|
+
|
|
38
|
+
await prepareInPlaceRestart(
|
|
39
|
+
{},
|
|
40
|
+
{ flipRestart: true, logger: { warn: vi.fn() } },
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
expect(send).toHaveBeenCalledWith(
|
|
44
|
+
{ type: "restartReason", reason: "flip_restart" },
|
|
45
|
+
expect.any(Function),
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("does not announce flip restart without flipRestart option", async () => {
|
|
50
|
+
const send = vi.fn(() => true);
|
|
51
|
+
process.send = send as unknown as typeof process.send;
|
|
52
|
+
|
|
53
|
+
await prepareInPlaceRestart({});
|
|
54
|
+
|
|
55
|
+
expect(send).not.toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("announceFlipRestartToParent", () => {
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
vi.restoreAllMocks();
|
|
62
|
+
process.send = undefined;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("no-ops when process.send is absent", async () => {
|
|
66
|
+
process.send = undefined;
|
|
67
|
+
await announceFlipRestartToParent();
|
|
68
|
+
});
|
|
69
|
+
});
|
package/src/telemetry/index.ts
CHANGED
|
@@ -79,16 +79,30 @@ export const deploymentType = (() => {
|
|
|
79
79
|
*/
|
|
80
80
|
export const isCloudPrem = deploymentType === DeploymentTypeEnum.CLOUD_PREM;
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Shut down telemetry, forcing a final flush of all buffered traces, metrics,
|
|
84
|
+
* and logs (via `sdk.shutdown()` → the metric reader's force-flush).
|
|
85
|
+
*
|
|
86
|
+
* Never rejects: shutdown errors are caught and logged internally. Returns
|
|
87
|
+
* `true` only when a flush actually ran to completion; `false` when telemetry
|
|
88
|
+
* was never initialized (nothing to flush) or `sdk.shutdown()` threw (the
|
|
89
|
+
* export may not have completed). Callers can use the return value to report
|
|
90
|
+
* whether buffered samples were really flushed — any `try/catch` around it is
|
|
91
|
+
* purely defensive.
|
|
92
|
+
*/
|
|
93
|
+
export async function shutdownTelemetry(): Promise<boolean> {
|
|
94
|
+
if (!isTelemetryInitialized()) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
await getTelemetryInstance().shutdown();
|
|
99
|
+
return true;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error("Error during telemetry shutdown:", error);
|
|
102
|
+
return false;
|
|
103
|
+
} finally {
|
|
104
|
+
resetTelemetry();
|
|
105
|
+
configured = false;
|
|
92
106
|
}
|
|
93
107
|
}
|
|
94
108
|
|