@superblocksteam/sdk 2.0.119-next.0 → 2.0.119
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/dev-s3-restore.test.mjs +0 -116
- package/dist/cli-replacement/dev-s3-restore.test.mjs.map +1 -1
- package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs +0 -1
- package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs.map +1 -1
- package/dist/cli-replacement/dev.d.mts +0 -6
- package/dist/cli-replacement/dev.d.mts.map +1 -1
- package/dist/cli-replacement/dev.mjs +6 -77
- package/dist/cli-replacement/dev.mjs.map +1 -1
- package/dist/client.d.ts +6 -67
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +0 -94
- package/dist/client.js.map +1 -1
- package/dist/dev-utils/dev-server-persist.test.mjs +17 -117
- package/dist/dev-utils/dev-server-persist.test.mjs.map +1 -1
- package/dist/dev-utils/dev-server.d.mts +1 -19
- package/dist/dev-utils/dev-server.d.mts.map +1 -1
- package/dist/dev-utils/dev-server.mjs +28 -296
- package/dist/dev-utils/dev-server.mjs.map +1 -1
- package/dist/flag.d.ts +1 -1
- package/dist/flag.d.ts.map +1 -1
- package/dist/flag.js +3 -3
- package/dist/flag.js.map +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/sdk.d.ts +1 -8
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +1 -24
- package/dist/sdk.js.map +1 -1
- package/dist/sdk.test.js +1 -102
- package/dist/sdk.test.js.map +1 -1
- package/dist/telemetry/index.d.ts +1 -2
- package/dist/telemetry/index.d.ts.map +1 -1
- package/dist/telemetry/index.js +1 -8
- package/dist/telemetry/index.js.map +1 -1
- package/dist/telemetry/logging.d.ts +2 -1
- package/dist/telemetry/logging.d.ts.map +1 -1
- package/dist/telemetry/logging.js +1 -1
- package/dist/telemetry/logging.js.map +1 -1
- package/dist/types/common.d.ts +1 -1
- package/dist/types/common.d.ts.map +1 -1
- package/dist/version-control.d.mts.map +1 -1
- package/dist/version-control.mjs +19 -14
- package/dist/version-control.mjs.map +1 -1
- package/eslint.config.js +0 -6
- package/package.json +8 -8
- package/src/cli-replacement/dev-s3-restore.test.mts +0 -156
- package/src/cli-replacement/dev-startup-git-before-dbfs-order.test.mts +0 -1
- package/src/cli-replacement/dev.mts +14 -104
- package/src/client.ts +6 -189
- package/src/dev-utils/dev-server-persist.test.mts +19 -170
- package/src/dev-utils/dev-server.mts +32 -366
- package/src/flag.ts +4 -4
- package/src/index.ts +1 -19
- package/src/sdk.test.ts +6 -145
- package/src/sdk.ts +0 -36
- package/src/telemetry/index.ts +1 -9
- package/src/telemetry/logging.ts +2 -2
- package/src/types/common.ts +1 -1
- package/src/version-control.mts +30 -11
- package/test/version-control.test.mts +2 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/turbo.json +0 -1
- package/dist/dev-utils/dedupe-async.d.ts +0 -16
- package/dist/dev-utils/dedupe-async.d.ts.map +0 -1
- package/dist/dev-utils/dedupe-async.js +0 -27
- package/dist/dev-utils/dedupe-async.js.map +0 -1
- package/dist/dev-utils/dedupe-async.test.d.ts +0 -2
- package/dist/dev-utils/dedupe-async.test.d.ts.map +0 -1
- package/dist/dev-utils/dedupe-async.test.js +0 -120
- package/dist/dev-utils/dedupe-async.test.js.map +0 -1
- package/dist/dev-utils/dev-server-metrics.d.mts +0 -95
- package/dist/dev-utils/dev-server-metrics.d.mts.map +0 -1
- package/dist/dev-utils/dev-server-metrics.mjs +0 -193
- package/dist/dev-utils/dev-server-metrics.mjs.map +0 -1
- package/dist/telemetry/memory-metrics.d.ts +0 -3
- package/dist/telemetry/memory-metrics.d.ts.map +0 -1
- package/dist/telemetry/memory-metrics.js +0 -59
- package/dist/telemetry/memory-metrics.js.map +0 -1
- package/src/dev-utils/dedupe-async.test.ts +0 -151
- package/src/dev-utils/dedupe-async.ts +0 -45
- package/src/dev-utils/dev-server-metrics.mts +0 -252
- package/src/telemetry/memory-metrics.ts +0 -90
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as child_process from "node:child_process";
|
|
2
|
-
import { existsSync
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
3
|
import type http from "node:http";
|
|
4
4
|
import net from "node:net";
|
|
5
5
|
import os from "node:os";
|
|
@@ -37,10 +37,6 @@ import pkg from "../../package.json" with { type: "json" };
|
|
|
37
37
|
import type { SuperblocksSdk } from "../sdk.js";
|
|
38
38
|
import { getTracer } from "../telemetry/index.js";
|
|
39
39
|
import { getErrorMeta, getLogger } from "../telemetry/logging.js";
|
|
40
|
-
import {
|
|
41
|
-
startMemoryMetrics,
|
|
42
|
-
stopMemoryMetrics,
|
|
43
|
-
} from "../telemetry/memory-metrics.js";
|
|
44
40
|
import {
|
|
45
41
|
EDIT_APPLICATION_SCOPE,
|
|
46
42
|
type SuperblocksScopedJwtPayload,
|
|
@@ -52,12 +48,6 @@ import {
|
|
|
52
48
|
customComponentsPlugin,
|
|
53
49
|
isCustomComponentsEnabled,
|
|
54
50
|
} from "./custom-build.mjs";
|
|
55
|
-
import { dedupeAsync } from "./dedupe-async.js";
|
|
56
|
-
import {
|
|
57
|
-
type DevServerEndpoint,
|
|
58
|
-
type DevServerFailureType,
|
|
59
|
-
devServerMetrics,
|
|
60
|
-
} from "./dev-server-metrics.mjs";
|
|
61
51
|
import {
|
|
62
52
|
formatViteDevServerStartedLog,
|
|
63
53
|
logViteBuildError,
|
|
@@ -66,60 +56,6 @@ import { buildManifestStubPlugin } from "./vite-plugin-build-manifest-stub.mjs";
|
|
|
66
56
|
import { ddRumPlugin } from "./vite-plugin-dd-rum.mjs";
|
|
67
57
|
|
|
68
58
|
const tracer = getTracer();
|
|
69
|
-
const logger = getLogger();
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Maps a dev-server request URL onto a low-cardinality endpoint label for
|
|
73
|
-
* metrics. Returns undefined for non-`/_sb_*` URLs (Vite-proxied traffic).
|
|
74
|
-
*/
|
|
75
|
-
function endpointForUrl(
|
|
76
|
-
url: string | undefined,
|
|
77
|
-
): DevServerEndpoint | undefined {
|
|
78
|
-
if (!url) return undefined;
|
|
79
|
-
const path = url.split("?", 1)[0];
|
|
80
|
-
switch (path) {
|
|
81
|
-
case "/_sb_health":
|
|
82
|
-
return "_sb_health";
|
|
83
|
-
case "/_sb_connect":
|
|
84
|
-
return "_sb_connect";
|
|
85
|
-
case "/_sb_activate":
|
|
86
|
-
return "_sb_activate";
|
|
87
|
-
case "/_sb_disconnect":
|
|
88
|
-
return "_sb_disconnect";
|
|
89
|
-
case "/_sb_status":
|
|
90
|
-
return "_sb_status";
|
|
91
|
-
case "/_sb_activity":
|
|
92
|
-
return "_sb_activity";
|
|
93
|
-
case "/_sb_persist":
|
|
94
|
-
return "_sb_persist";
|
|
95
|
-
case "/_sb_ready":
|
|
96
|
-
return "_sb_ready";
|
|
97
|
-
default:
|
|
98
|
-
return undefined;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Extracts a coarse failure-type label from `res.locals.sbFailureType`,
|
|
104
|
-
* which endpoints set just before they error out. Stays server-side
|
|
105
|
-
* rather than leaking through response headers.
|
|
106
|
-
*/
|
|
107
|
-
function failureTypeFromLocals(
|
|
108
|
-
locals: Record<string, unknown>,
|
|
109
|
-
): DevServerFailureType {
|
|
110
|
-
const value = locals?.sbFailureType;
|
|
111
|
-
if (typeof value !== "string") return "none";
|
|
112
|
-
switch (value) {
|
|
113
|
-
case "auth":
|
|
114
|
-
case "validation":
|
|
115
|
-
case "branch_mismatch":
|
|
116
|
-
case "vite_init":
|
|
117
|
-
case "internal":
|
|
118
|
-
return value;
|
|
119
|
-
default:
|
|
120
|
-
return "none";
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
59
|
|
|
124
60
|
function reactPlugin() {
|
|
125
61
|
return react({
|
|
@@ -187,29 +123,6 @@ function vitePlugins(
|
|
|
187
123
|
];
|
|
188
124
|
}
|
|
189
125
|
|
|
190
|
-
// .superblocks subdirectories that should survive pod recycles.
|
|
191
|
-
// Everything else under .superblocks/ is excluded automatically.
|
|
192
|
-
const PERSIST_SUPERBLOCKS_SAFELIST = new Set(["context", "drafts"]);
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Build tar --exclude flags for .superblocks/ subdirectories that are NOT
|
|
196
|
-
* in the safelist. If .superblocks/ doesn't exist we exclude it entirely.
|
|
197
|
-
*/
|
|
198
|
-
export function getSuperblocksTarExcludes(root: string): string[] {
|
|
199
|
-
const dotSuperblocks = path.join(root, ".superblocks");
|
|
200
|
-
let entries: string[];
|
|
201
|
-
try {
|
|
202
|
-
entries = readdirSync(dotSuperblocks, { withFileTypes: true })
|
|
203
|
-
.filter((d) => d.isDirectory())
|
|
204
|
-
.map((d) => d.name);
|
|
205
|
-
} catch {
|
|
206
|
-
return ["--exclude", ".superblocks"];
|
|
207
|
-
}
|
|
208
|
-
return entries
|
|
209
|
-
.filter((name) => !PERSIST_SUPERBLOCKS_SAFELIST.has(name))
|
|
210
|
-
.flatMap((name) => ["--exclude", `.superblocks/${name}`]);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
126
|
export async function createWorkspacePersistArchive(root: string) {
|
|
214
127
|
// Warm pods already have template dependencies installed, so persist only
|
|
215
128
|
// user workspace files and let startup reconcile package drift if needed.
|
|
@@ -223,7 +136,8 @@ export async function createWorkspacePersistArchive(root: string) {
|
|
|
223
136
|
".git",
|
|
224
137
|
"--exclude",
|
|
225
138
|
"node_modules",
|
|
226
|
-
|
|
139
|
+
"--exclude",
|
|
140
|
+
".superblocks",
|
|
227
141
|
"-C",
|
|
228
142
|
root,
|
|
229
143
|
".",
|
|
@@ -299,64 +213,11 @@ export async function createWorkspacePersistArchive(root: string) {
|
|
|
299
213
|
});
|
|
300
214
|
}
|
|
301
215
|
|
|
302
|
-
export function createWorkspacePersistUploadHeaders(
|
|
303
|
-
archiveLength: number,
|
|
304
|
-
uploadHeaders: Record<string, string> = {},
|
|
305
|
-
) {
|
|
306
|
-
const headers = new Headers(uploadHeaders);
|
|
307
|
-
if (!headers.has("Content-Type")) {
|
|
308
|
-
headers.set("Content-Type", "application/zstd");
|
|
309
|
-
}
|
|
310
|
-
headers.set("Content-Length", String(archiveLength));
|
|
311
|
-
return Object.fromEntries(headers.entries());
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
export function isWorkspacePersistUploadHeaders(
|
|
315
|
-
uploadHeaders: unknown,
|
|
316
|
-
): uploadHeaders is Record<string, string> | undefined {
|
|
317
|
-
if (uploadHeaders === undefined) {
|
|
318
|
-
return true;
|
|
319
|
-
}
|
|
320
|
-
if (
|
|
321
|
-
uploadHeaders === null ||
|
|
322
|
-
typeof uploadHeaders !== "object" ||
|
|
323
|
-
Array.isArray(uploadHeaders)
|
|
324
|
-
) {
|
|
325
|
-
return false;
|
|
326
|
-
}
|
|
327
|
-
const seen = new Set<string>();
|
|
328
|
-
for (const [key, value] of Object.entries(uploadHeaders)) {
|
|
329
|
-
if (typeof key !== "string" || typeof value !== "string") {
|
|
330
|
-
return false;
|
|
331
|
-
}
|
|
332
|
-
// Header names are case-insensitive. Two keys that differ only in casing
|
|
333
|
-
// (e.g. `Content-Type` + `content-type`) would be silently combined by
|
|
334
|
-
// `new Headers()` into a comma-joined value, which breaks S3 presigned
|
|
335
|
-
// PUTs that signed a single canonical value. Reject up front so the
|
|
336
|
-
// caller fixes the duplicate rather than getting a confusing S3 4xx.
|
|
337
|
-
const lower = key.toLowerCase();
|
|
338
|
-
if (seen.has(lower)) {
|
|
339
|
-
return false;
|
|
340
|
-
}
|
|
341
|
-
seen.add(lower);
|
|
342
|
-
}
|
|
343
|
-
// Validate header name/value syntax by attempting to construct a Headers
|
|
344
|
-
// instance. Doing this in the validator (rather than letting it throw
|
|
345
|
-
// later inside `createWorkspacePersistUploadHeaders`) keeps the failure
|
|
346
|
-
// mode as a 400 before any archive work runs.
|
|
347
|
-
try {
|
|
348
|
-
new Headers(uploadHeaders as Record<string, string>);
|
|
349
|
-
} catch {
|
|
350
|
-
return false;
|
|
351
|
-
}
|
|
352
|
-
return true;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
216
|
export const RESTART_EXIT_CODE = 98;
|
|
356
217
|
|
|
357
218
|
function getJwksUriWithBaseUrl(superblocksBaseUrl?: string): string {
|
|
358
219
|
if (process.env.SUPERBLOCKS_JWKS_URI) {
|
|
359
|
-
|
|
220
|
+
console.debug(
|
|
360
221
|
`[JWKS] Using override SUPERBLOCKS_JWKS_URI=${process.env.SUPERBLOCKS_JWKS_URI}`,
|
|
361
222
|
);
|
|
362
223
|
return process.env.SUPERBLOCKS_JWKS_URI;
|
|
@@ -393,7 +254,7 @@ function getJwksUriWithBaseUrl(superblocksBaseUrl?: string): string {
|
|
|
393
254
|
jwksUri = "https://staging-cdn.superblocks.com/.well-known/jwks.json";
|
|
394
255
|
}
|
|
395
256
|
|
|
396
|
-
|
|
257
|
+
console.debug(`[JWKS] Derived jwksUri=${jwksUri} from baseUrl=${baseUrl}`);
|
|
397
258
|
return jwksUri;
|
|
398
259
|
}
|
|
399
260
|
|
|
@@ -421,57 +282,10 @@ interface CreateDevServerOptions {
|
|
|
421
282
|
* causes 502s from the gateway during the warm -> full server transition.
|
|
422
283
|
*/
|
|
423
284
|
existingServer?: http.Server;
|
|
424
|
-
/**
|
|
425
|
-
* Wall-clock timestamp (Date.now()) when the warm-standby /_sb_activate POST
|
|
426
|
-
* was accepted. When provided, the dev server records the elapsed time from
|
|
427
|
-
* activation acceptance to full Express handler attachment as the
|
|
428
|
-
* `dev_server_warm_activation_handoff_duration_ms` histogram — the window
|
|
429
|
-
* during which the gateway can return 502s if the warm server stops
|
|
430
|
-
* responding before the full server takes over.
|
|
431
|
-
*/
|
|
432
|
-
warmActivationStart?: number;
|
|
433
285
|
}
|
|
434
286
|
|
|
435
287
|
let httpServer: http.Server;
|
|
436
288
|
|
|
437
|
-
/**
|
|
438
|
-
* Attaches a passive metrics-only listener for WebSocket upgrade attempts on
|
|
439
|
-
* the dev-server's HTTP socket.
|
|
440
|
-
*
|
|
441
|
-
* The outcome is a heuristic, not a direct accept/reject signal:
|
|
442
|
-
* - `accepted` = the socket closed cleanly (no `hadError`) or stayed open
|
|
443
|
-
* past the 30s defensive timer. A long-lived HMR connection
|
|
444
|
-
* is the typical success case.
|
|
445
|
-
* - `rejected` = the socket emitted `error` or closed with `hadError=true`.
|
|
446
|
-
* A clean rejection by the upstream WS layer (write 401 + end) can therefore
|
|
447
|
-
* bucket as `accepted` if the close event has no `hadError` flag — the WS
|
|
448
|
-
* handling layer does not surface a direct accept signal we can observe here.
|
|
449
|
-
*
|
|
450
|
-
* MUST be called only after Vite (or another component) has registered its own
|
|
451
|
-
* `'upgrade'` listener on the same server. Registering this listener first
|
|
452
|
-
* suppresses Node's default close-on-unhandled behavior, which would leave
|
|
453
|
-
* editor upgrade attempts hanging until client timeout in the pre-Vite window.
|
|
454
|
-
*/
|
|
455
|
-
function attachUpgradeMetricsListener(server: http.Server): void {
|
|
456
|
-
server.on("upgrade", (req, socket) => {
|
|
457
|
-
let recorded = false;
|
|
458
|
-
const recordOnce = (outcome: "accepted" | "rejected") => {
|
|
459
|
-
if (recorded) return;
|
|
460
|
-
recorded = true;
|
|
461
|
-
devServerMetrics.recordSocketUpgrade(outcome);
|
|
462
|
-
};
|
|
463
|
-
socket.once("close", (hadError: boolean) => {
|
|
464
|
-
recordOnce(hadError ? "rejected" : "accepted");
|
|
465
|
-
});
|
|
466
|
-
socket.once("error", () => recordOnce("rejected"));
|
|
467
|
-
// Defensive: if neither close nor error fires within 30s, count as accepted
|
|
468
|
-
// — a long-lived HMR connection is the success case for upgrades.
|
|
469
|
-
setTimeout(() => recordOnce("accepted"), 30_000).unref();
|
|
470
|
-
// Avoid lint warning for unused param without changing the listener shape.
|
|
471
|
-
void req;
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
|
-
|
|
475
289
|
// The "Dev Server" is a long-running HTTP server that manages the Vite server
|
|
476
290
|
export async function createDevServer({
|
|
477
291
|
root,
|
|
@@ -489,7 +303,6 @@ export async function createDevServer({
|
|
|
489
303
|
sdk,
|
|
490
304
|
superblocksBaseUrl: explicitBaseUrl,
|
|
491
305
|
existingServer,
|
|
492
|
-
warmActivationStart,
|
|
493
306
|
}: CreateDevServerOptions) {
|
|
494
307
|
const logger = getLogger(loggerOverride);
|
|
495
308
|
if (httpServer) {
|
|
@@ -533,31 +346,7 @@ export async function createDevServer({
|
|
|
533
346
|
// attached at the viteStartPromise call site.
|
|
534
347
|
vitePromise.catch(() => {});
|
|
535
348
|
|
|
536
|
-
|
|
537
|
-
// uncaughtException) can fire in rapid succession during shutdown —
|
|
538
|
-
// particularly when a parent process kills the dev-server and the OS or
|
|
539
|
-
// test harness then sends a follow-up SIGINT. Without a dedupe, every
|
|
540
|
-
// signal handler re-runs lockService shutdown / vite close / httpServer
|
|
541
|
-
// close on already-freed native state, which surfaces as
|
|
542
|
-
// `double free or corruption (fasttop)` from glibc and SIGABRT killing
|
|
543
|
-
// the process before it can exit cleanly (hq-o55a).
|
|
544
|
-
//
|
|
545
|
-
// First-caller-wins on args: the second+ caller's `switchingTo` /
|
|
546
|
-
// `initiatedByEmail` are intentionally dropped (the first caller's run is
|
|
547
|
-
// already in flight; we don't restart cleanup with new args). The
|
|
548
|
-
// `/_sb_disconnect` HTTP handler that awaits `gracefulShutdown` therefore
|
|
549
|
-
// returns "ok" without applying its `switchingTo` if a signal beat it to
|
|
550
|
-
// the punch — the process exits regardless, so the dropped arg cannot
|
|
551
|
-
// drive downstream state in this process.
|
|
552
|
-
const gracefulShutdown = dedupeAsync(runGracefulShutdown, {
|
|
553
|
-
onDuplicate: (args) => {
|
|
554
|
-
logger.info(
|
|
555
|
-
`Shutdown already in progress; ignoring duplicate signal (source=${args.source ?? "unknown"})`,
|
|
556
|
-
);
|
|
557
|
-
},
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
async function runGracefulShutdown({
|
|
349
|
+
async function gracefulShutdown({
|
|
561
350
|
logger,
|
|
562
351
|
serverInitiated,
|
|
563
352
|
switchingTo,
|
|
@@ -567,16 +356,8 @@ export async function createDevServer({
|
|
|
567
356
|
serverInitiated: boolean;
|
|
568
357
|
switchingTo?: "local" | "cloud" | "none";
|
|
569
358
|
initiatedByEmail?: string;
|
|
570
|
-
|
|
571
|
-
* Identifies what triggered the shutdown (signal name, "uncaughtException",
|
|
572
|
-
* or "/_sb_disconnect"). Threaded through so the dedupe-duplicate log can
|
|
573
|
-
* disambiguate "4 OS signals fired" from "1 signal fired and cleanup
|
|
574
|
-
* panicked 3 times".
|
|
575
|
-
*/
|
|
576
|
-
source?: string;
|
|
577
|
-
}): Promise<void> {
|
|
359
|
+
}) {
|
|
578
360
|
try {
|
|
579
|
-
stopMemoryMetrics();
|
|
580
361
|
await lockService?.shutdown({
|
|
581
362
|
serverInitiated,
|
|
582
363
|
switchingTo,
|
|
@@ -620,29 +401,6 @@ export async function createDevServer({
|
|
|
620
401
|
}),
|
|
621
402
|
);
|
|
622
403
|
|
|
623
|
-
// Per-request metrics for dev-server endpoints. Identify the endpoint by URL
|
|
624
|
-
// pattern, time the request, record duration + status + coarse failure type
|
|
625
|
-
// when the response finishes. Non-`/_sb_*` traffic (Vite proxied requests)
|
|
626
|
-
// is not instrumented here — Vite's own middleware handles those.
|
|
627
|
-
app.use((req, res, next) => {
|
|
628
|
-
const endpoint = endpointForUrl(req.url);
|
|
629
|
-
if (!endpoint) {
|
|
630
|
-
return next();
|
|
631
|
-
}
|
|
632
|
-
const startedAt = Date.now();
|
|
633
|
-
res.on("finish", () => {
|
|
634
|
-
const failureType = failureTypeFromLocals(res.locals);
|
|
635
|
-
devServerMetrics.recordEndpoint(
|
|
636
|
-
endpoint,
|
|
637
|
-
req.method,
|
|
638
|
-
res.statusCode,
|
|
639
|
-
Date.now() - startedAt,
|
|
640
|
-
failureType,
|
|
641
|
-
);
|
|
642
|
-
});
|
|
643
|
-
next();
|
|
644
|
-
});
|
|
645
|
-
|
|
646
404
|
app.use((req, res, next) => {
|
|
647
405
|
res.setHeader("Cache-Control", "no-store, max-age=0");
|
|
648
406
|
res.setHeader("x-csb-no-sw-proxy", "1");
|
|
@@ -756,7 +514,6 @@ export async function createDevServer({
|
|
|
756
514
|
if (result.success) {
|
|
757
515
|
return next();
|
|
758
516
|
} else {
|
|
759
|
-
res.locals.sbFailureType = "auth";
|
|
760
517
|
res.status(result.status).send(JSON.stringify({ error: result.error }));
|
|
761
518
|
}
|
|
762
519
|
};
|
|
@@ -780,7 +537,6 @@ export async function createDevServer({
|
|
|
780
537
|
logger.info(
|
|
781
538
|
`[dev-server-runtime] _sb_connect application mismatch: expected ${resourceConfig!.id}, got ${req.headers["x-superblocks-application-id"]}`,
|
|
782
539
|
);
|
|
783
|
-
res.locals.sbFailureType = "validation";
|
|
784
540
|
res.status(401).send(
|
|
785
541
|
JSON.stringify({
|
|
786
542
|
error: `Application ID header mismatch. Expected ${resourceConfig!.id}, received ${req.headers["x-superblocks-application-id"]}`,
|
|
@@ -803,7 +559,6 @@ export async function createDevServer({
|
|
|
803
559
|
logger.info(
|
|
804
560
|
`[dev-server-runtime] _sb_connect branch mismatch: expected ${currentBranch}, got ${incomingBranch}`,
|
|
805
561
|
);
|
|
806
|
-
res.locals.sbFailureType = "branch_mismatch";
|
|
807
562
|
res.status(401).send(
|
|
808
563
|
JSON.stringify({
|
|
809
564
|
error: `Dev server expects branch ${currentBranch}, but received ${incomingBranch}. Check your current branch with 'git branch' and try again.`,
|
|
@@ -824,7 +579,6 @@ export async function createDevServer({
|
|
|
824
579
|
"Vite server failed to initialize",
|
|
825
580
|
getErrorMeta(e as Error),
|
|
826
581
|
);
|
|
827
|
-
res.locals.sbFailureType = "vite_init";
|
|
828
582
|
res
|
|
829
583
|
.status(500)
|
|
830
584
|
.send(JSON.stringify({ error: "Dev server failed to initialize" }));
|
|
@@ -870,7 +624,6 @@ export async function createDevServer({
|
|
|
870
624
|
serverInitiated: true,
|
|
871
625
|
switchingTo,
|
|
872
626
|
initiatedByEmail: initiatedByEmail as string | undefined,
|
|
873
|
-
source: "/_sb_disconnect",
|
|
874
627
|
});
|
|
875
628
|
res.send("ok");
|
|
876
629
|
}
|
|
@@ -928,21 +681,11 @@ export async function createDevServer({
|
|
|
928
681
|
persistInProgress = true;
|
|
929
682
|
const persistStart = Date.now();
|
|
930
683
|
try {
|
|
931
|
-
const { uploadURL
|
|
932
|
-
uploadURL: string;
|
|
933
|
-
uploadHeaders?: unknown;
|
|
934
|
-
};
|
|
684
|
+
const { uploadURL } = req.body as { uploadURL: string };
|
|
935
685
|
if (!uploadURL) {
|
|
936
686
|
res.status(400).json({ error: "uploadURL required" });
|
|
937
687
|
return;
|
|
938
688
|
}
|
|
939
|
-
if (!isWorkspacePersistUploadHeaders(uploadHeaders)) {
|
|
940
|
-
res.status(400).json({
|
|
941
|
-
error:
|
|
942
|
-
"uploadHeaders must be a string map with valid HTTP header names and values and no case-insensitive duplicate keys",
|
|
943
|
-
});
|
|
944
|
-
return;
|
|
945
|
-
}
|
|
946
689
|
|
|
947
690
|
logger.info("/_sb_persist: archiving and uploading workspace to S3...");
|
|
948
691
|
|
|
@@ -956,10 +699,10 @@ export async function createDevServer({
|
|
|
956
699
|
const uploadResp = await fetch(uploadURL, {
|
|
957
700
|
method: "PUT",
|
|
958
701
|
body: archive,
|
|
959
|
-
headers:
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
702
|
+
headers: {
|
|
703
|
+
"Content-Type": "application/zstd",
|
|
704
|
+
"Content-Length": String(archive.length),
|
|
705
|
+
},
|
|
963
706
|
signal: AbortSignal.timeout(30_000),
|
|
964
707
|
});
|
|
965
708
|
|
|
@@ -981,63 +724,32 @@ export async function createDevServer({
|
|
|
981
724
|
}
|
|
982
725
|
});
|
|
983
726
|
|
|
984
|
-
|
|
985
|
-
// `runGracefulShutdown` (e.g. logger init or lockService shutdown throwing
|
|
986
|
-
// before the first `await`) is logged rather than surfacing as an
|
|
987
|
-
// unhandled rejection across N handlers (one per fired signal). Symmetric
|
|
988
|
-
// with the `uncaughtException` handler below.
|
|
989
|
-
process.on("SIGINT", () => {
|
|
727
|
+
process.on("SIGINT", async () => {
|
|
990
728
|
logger.info("SIGINT received");
|
|
991
|
-
gracefulShutdown({
|
|
992
|
-
logger,
|
|
993
|
-
serverInitiated: false,
|
|
994
|
-
source: "SIGINT",
|
|
995
|
-
}).catch((err) =>
|
|
996
|
-
logger.error("Error during SIGINT shutdown", getErrorMeta(err)),
|
|
997
|
-
);
|
|
729
|
+
await gracefulShutdown({ logger, serverInitiated: false });
|
|
998
730
|
});
|
|
999
731
|
|
|
1000
|
-
process.on("SIGTERM", () => {
|
|
732
|
+
process.on("SIGTERM", async () => {
|
|
1001
733
|
logger.info("SIGTERM received");
|
|
1002
|
-
gracefulShutdown({
|
|
1003
|
-
logger,
|
|
1004
|
-
serverInitiated: false,
|
|
1005
|
-
source: "SIGTERM",
|
|
1006
|
-
}).catch((err) =>
|
|
1007
|
-
logger.error("Error during SIGTERM shutdown", getErrorMeta(err)),
|
|
1008
|
-
);
|
|
734
|
+
await gracefulShutdown({ logger, serverInitiated: false });
|
|
1009
735
|
});
|
|
1010
736
|
|
|
1011
|
-
process.on("SIGABRT", () => {
|
|
737
|
+
process.on("SIGABRT", async () => {
|
|
1012
738
|
logger.info("SIGABRT received");
|
|
1013
|
-
gracefulShutdown({
|
|
1014
|
-
logger,
|
|
1015
|
-
serverInitiated: false,
|
|
1016
|
-
source: "SIGABRT",
|
|
1017
|
-
}).catch((err) =>
|
|
1018
|
-
logger.error("Error during SIGABRT shutdown", getErrorMeta(err)),
|
|
1019
|
-
);
|
|
739
|
+
await gracefulShutdown({ logger, serverInitiated: false });
|
|
1020
740
|
});
|
|
1021
741
|
|
|
1022
|
-
// `uncaughtException` always force-exits in `.finally`, even when the
|
|
1023
|
-
// underlying shutdown promise was already resolved by a prior signal. With
|
|
1024
|
-
// the dedupe in place, awaiting the cached resolved promise short-circuits
|
|
1025
|
-
// through `.then` without firing `.catch`; without the `.finally` the
|
|
1026
|
-
// process would linger in event-loop limbo until the OS killed it.
|
|
1027
742
|
process.on("uncaughtException", (error) => {
|
|
1028
743
|
logger.error(`Uncaught exception: ${error.message}`, getErrorMeta(error));
|
|
1029
|
-
gracefulShutdown({
|
|
1030
|
-
|
|
1031
|
-
serverInitiated: false,
|
|
1032
|
-
source: "uncaughtException",
|
|
1033
|
-
})
|
|
1034
|
-
.catch((shutdownError) => {
|
|
744
|
+
gracefulShutdown({ logger, serverInitiated: false }).catch(
|
|
745
|
+
(shutdownError) => {
|
|
1035
746
|
logger.error(
|
|
1036
747
|
"Error during shutdown after uncaught exception:",
|
|
1037
748
|
shutdownError,
|
|
1038
749
|
);
|
|
1039
|
-
|
|
1040
|
-
|
|
750
|
+
process.exit(1);
|
|
751
|
+
},
|
|
752
|
+
);
|
|
1041
753
|
});
|
|
1042
754
|
|
|
1043
755
|
if (existingServer) {
|
|
@@ -1046,19 +758,12 @@ export async function createDevServer({
|
|
|
1046
758
|
existingServer.removeAllListeners("request");
|
|
1047
759
|
existingServer.on("request", app);
|
|
1048
760
|
httpServer = existingServer;
|
|
1049
|
-
if (warmActivationStart) {
|
|
1050
|
-
devServerMetrics.recordWarmHandoff(Date.now() - warmActivationStart);
|
|
1051
|
-
}
|
|
1052
761
|
logger.info(
|
|
1053
762
|
`Attached full dev server to existing HTTP server on port ${port}`,
|
|
1054
763
|
);
|
|
1055
764
|
} else {
|
|
1056
765
|
logger.info(`Starting HTTP server on port ${port}`);
|
|
1057
|
-
|
|
1058
|
-
// `await` was a no-op on a non-Promise. The server isn't actually bound
|
|
1059
|
-
// until it emits `listening`; if downstream code ever depends on the
|
|
1060
|
-
// socket being bound, switch to `new Promise(r => httpServer.once("listening", r))`.
|
|
1061
|
-
httpServer = app.listen(port);
|
|
766
|
+
httpServer = await app.listen(port);
|
|
1062
767
|
logger.info(`HTTP server started successfully on port ${port}`);
|
|
1063
768
|
}
|
|
1064
769
|
|
|
@@ -1076,7 +781,6 @@ export async function createDevServer({
|
|
|
1076
781
|
return undefined;
|
|
1077
782
|
});
|
|
1078
783
|
|
|
1079
|
-
const viteEagerInitStart = Date.now();
|
|
1080
784
|
const viteStartPromise = startVite({
|
|
1081
785
|
port,
|
|
1082
786
|
app,
|
|
@@ -1104,34 +808,11 @@ export async function createDevServer({
|
|
|
1104
808
|
// below via the rejection branch of .then().
|
|
1105
809
|
viteStartPromise.then(
|
|
1106
810
|
(result) => {
|
|
1107
|
-
devServerMetrics.recordViteEagerInit(
|
|
1108
|
-
Date.now() - viteEagerInitStart,
|
|
1109
|
-
"success",
|
|
1110
|
-
);
|
|
1111
811
|
logger.info("Vite server initialized eagerly");
|
|
1112
812
|
viteResolve();
|
|
1113
813
|
viteCreationResults = result;
|
|
1114
|
-
// Start memory metrics only for claimed pods (after Vite init, not warm standby).
|
|
1115
|
-
// Gated by either the LD flag (cloud) or the SABS-injected env var (cloud-prem/local).
|
|
1116
|
-
if (
|
|
1117
|
-
featureFlags?.devServerMemoryMetricsEnabled() ||
|
|
1118
|
-
process.env.SUPERBLOCKS_DEV_SERVER_MEMORY_METRICS_ENABLED === "true"
|
|
1119
|
-
) {
|
|
1120
|
-
startMemoryMetrics();
|
|
1121
|
-
}
|
|
1122
|
-
// Attach the WebSocket upgrade-tracking listener AFTER Vite has
|
|
1123
|
-
// installed its own HMR upgrade handler. Registering an `upgrade`
|
|
1124
|
-
// listener pre-Vite would suppress Node's default close-on-unhandled
|
|
1125
|
-
// behavior, leaving editor upgrade attempts to hang until client
|
|
1126
|
-
// timeout. By deferring until Vite is up, our listener coexists with
|
|
1127
|
-
// the real handler instead of replacing the default close.
|
|
1128
|
-
attachUpgradeMetricsListener(httpServer);
|
|
1129
814
|
},
|
|
1130
815
|
(e) => {
|
|
1131
|
-
devServerMetrics.recordViteEagerInit(
|
|
1132
|
-
Date.now() - viteEagerInitStart,
|
|
1133
|
-
"error",
|
|
1134
|
-
);
|
|
1135
816
|
logger.error("Error initializing vite server", getErrorMeta(e));
|
|
1136
817
|
viteReject(e);
|
|
1137
818
|
},
|
|
@@ -1350,7 +1031,7 @@ const PRE_WARM_TIMEOUT_MS = 60_000;
|
|
|
1350
1031
|
|
|
1351
1032
|
export async function preWarmViteCache(root: string): Promise<void> {
|
|
1352
1033
|
const start = Date.now();
|
|
1353
|
-
|
|
1034
|
+
console.log("[warm] Pre-warming Vite dependency cache...");
|
|
1354
1035
|
try {
|
|
1355
1036
|
const server = await createServer({
|
|
1356
1037
|
root,
|
|
@@ -1372,26 +1053,11 @@ export async function preWarmViteCache(root: string): Promise<void> {
|
|
|
1372
1053
|
await new Promise((r) => setTimeout(r, pollMs));
|
|
1373
1054
|
}
|
|
1374
1055
|
await server.close();
|
|
1375
|
-
|
|
1376
|
-
logger.info(`[warm] Vite cache warmed in ${Date.now() - start}ms`);
|
|
1056
|
+
console.log(`[warm] Vite cache warmed in ${Date.now() - start}ms`);
|
|
1377
1057
|
} catch (error) {
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
// the structured log line below — telemetry isn't initialized yet in
|
|
1382
|
-
// warm-standby (no auth token until /_sb_activate), so `configureTelemetry`
|
|
1383
|
-
// can't run here, and synchronously POSTing an OTLP payload before exit
|
|
1384
|
-
// would block pod recycle on a network call that may itself be hung.
|
|
1385
|
-
devServerMetrics.recordWarmPrewarm(durationMs, "error");
|
|
1386
|
-
// Stable inline `key=value` marker so a log-based metric (Datadog
|
|
1387
|
-
// "Generate Metrics from Logs", Loki regex) can recover pre-warm failures
|
|
1388
|
-
// the OTel pipeline misses. The Logger.error signature accepts only
|
|
1389
|
-
// ErrorMeta (no arbitrary attributes) and must stay structurally
|
|
1390
|
-
// compatible with the sister Logger in vite-plugin-file-sync, so the
|
|
1391
|
-
// markers are embedded in the message body instead of passed as fields.
|
|
1392
|
-
logger.error(
|
|
1393
|
-
`[warm] Vite cache pre-warm failed — exiting for fast pod recycle metric=dev_server_warm_prewarm outcome=error duration_ms=${durationMs}`,
|
|
1394
|
-
getErrorMeta(error),
|
|
1058
|
+
console.error(
|
|
1059
|
+
`[warm] Vite cache pre-warm failed (${Date.now() - start}ms) — exiting for fast pod recycle:`,
|
|
1060
|
+
error,
|
|
1395
1061
|
);
|
|
1396
1062
|
process.exit(1);
|
|
1397
1063
|
}
|
|
@@ -1405,7 +1071,7 @@ function getFreePort() {
|
|
|
1405
1071
|
server.listen(0, () => {
|
|
1406
1072
|
const address = server.address();
|
|
1407
1073
|
if (typeof address === "string") {
|
|
1408
|
-
|
|
1074
|
+
console.debug(
|
|
1409
1075
|
`Port resolution: falling back to default HMR port ${DEFAULT_HMR_PORT}`,
|
|
1410
1076
|
);
|
|
1411
1077
|
resolve(DEFAULT_HMR_PORT);
|
|
@@ -1413,13 +1079,13 @@ function getFreePort() {
|
|
|
1413
1079
|
}
|
|
1414
1080
|
const port = address?.port;
|
|
1415
1081
|
if (!port) {
|
|
1416
|
-
|
|
1082
|
+
console.debug(
|
|
1417
1083
|
`Port resolution: no port available, using default HMR port ${DEFAULT_HMR_PORT}`,
|
|
1418
1084
|
);
|
|
1419
1085
|
resolve(DEFAULT_HMR_PORT);
|
|
1420
1086
|
return;
|
|
1421
1087
|
}
|
|
1422
|
-
|
|
1088
|
+
console.debug(`Port resolution: successfully allocated port ${port}`);
|
|
1423
1089
|
server.close(() => resolve(port));
|
|
1424
1090
|
});
|
|
1425
1091
|
});
|
package/src/flag.ts
CHANGED
|
@@ -17,6 +17,10 @@ export class FeatureFlags {
|
|
|
17
17
|
return this.flags["superblocks.git.split.large.steps.enabled"] ?? false;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
splitLargeApiStepsInNewEnabled(): boolean {
|
|
21
|
+
return this.flags["superblocks.git.split.large.steps.new.enabled"] ?? false;
|
|
22
|
+
}
|
|
23
|
+
|
|
20
24
|
linesForLargeSteps(): number {
|
|
21
25
|
return (
|
|
22
26
|
this.flags["superblocks.git.split.large.step.lines"] ??
|
|
@@ -43,8 +47,4 @@ export class FeatureFlags {
|
|
|
43
47
|
includeDotSuperblocks(): boolean {
|
|
44
48
|
return this.flags["superblocks.deploy.include-dot-superblocks"] ?? false;
|
|
45
49
|
}
|
|
46
|
-
|
|
47
|
-
devServerMemoryMetricsEnabled(): boolean {
|
|
48
|
-
return this.flags["superblocks.dev-server.memory-metrics.enabled"] ?? false;
|
|
49
|
-
}
|
|
50
50
|
}
|