@superblocksteam/sdk 2.0.110-next.8 → 2.0.110

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.
@@ -1,5 +1,3 @@
1
- import * as child_process from "node:child_process";
2
- import { existsSync } from "node:fs";
3
1
  import type http from "node:http";
4
2
  import net from "node:net";
5
3
  import os from "node:os";
@@ -114,13 +112,6 @@ interface CreateDevServerOptions {
114
112
  sdk?: SuperblocksSdk;
115
113
  /** Explicitly provided base URL from the CLI; preferred over auth-file reads. */
116
114
  superblocksBaseUrl?: string;
117
- /**
118
- * Pre-existing HTTP server from warm standby mode. When provided, the dev
119
- * server attaches its Express app as a request handler on this server instead
120
- * of creating a new one and calling listen(). This avoids the port gap that
121
- * causes 502s from the gateway during the warm -> full server transition.
122
- */
123
- existingServer?: http.Server;
124
115
  }
125
116
 
126
117
  let httpServer: http.Server;
@@ -140,7 +131,6 @@ export async function createDevServer({
140
131
  port,
141
132
  sdk,
142
133
  superblocksBaseUrl: explicitBaseUrl,
143
- existingServer,
144
134
  }: CreateDevServerOptions) {
145
135
  const logger = getLogger(loggerOverride);
146
136
  if (httpServer) {
@@ -177,12 +167,6 @@ export async function createDevServer({
177
167
  viteResolve = resolve;
178
168
  viteReject = reject;
179
169
  });
180
- // Attach a no-op catch so that if viteReject() fires before any request
181
- // handler awaits vitePromise (e.g. Vite fails during eager init, before
182
- // /_sb_connect), the process doesn't crash with UnhandledPromiseRejection.
183
- // The actual error is still logged via the .then(_, onRejected) handler
184
- // attached at the viteStartPromise call site.
185
- vitePromise.catch(() => {});
186
170
 
187
171
  async function gracefulShutdown({
188
172
  logger,
@@ -400,21 +384,50 @@ export async function createDevServer({
400
384
  }
401
385
  }
402
386
 
403
- logger.info("Received connect request, waiting for vite server...");
404
- // Vite is started eagerly after HTTP server listen (see below).
405
- // Wait for it to be ready before responding.
406
- try {
407
- await vitePromise;
387
+ logger.info("Initializing dev server...");
388
+ if (isViteServerInitialized) {
389
+ logger.info("Dev server already initialized");
408
390
  res.send(JSON.stringify(healthResponse));
409
- } catch (e) {
410
- logger.error(
411
- "Vite server failed to initialize",
412
- getErrorMeta(e as Error),
413
- );
414
- res
415
- .status(500)
416
- .send(JSON.stringify({ error: "Dev server failed to initialize" }));
391
+ return;
417
392
  }
393
+ logger.info("Starting dev server");
394
+ isViteServerInitialized = true;
395
+
396
+ const featureFlags = await sdk?.getFeatureFlagsForCurrentUser();
397
+
398
+ // TODO(code-mode): should this include any validation checks, such as getting a token?
399
+
400
+ startVite({
401
+ port,
402
+ app,
403
+ root,
404
+ mode,
405
+ fsOperationQueue,
406
+ syncService,
407
+ lockService,
408
+ aiService,
409
+ gitService,
410
+ activateGitService,
411
+ snapshotManager,
412
+ checkAuthorization,
413
+ logger: loggerOverride,
414
+ httpServer,
415
+ superblocksBaseUrl: explicitBaseUrl || localToken?.superblocksBaseUrl,
416
+ features: {
417
+ enableSessionRecording: featureFlags?.enableSessionRecording() ?? false,
418
+ },
419
+ }).then(
420
+ (result) => {
421
+ logger.info("Dev server initialized");
422
+ viteResolve();
423
+ viteCreationResults = result;
424
+ res.send(JSON.stringify(healthResponse));
425
+ },
426
+ (e) => {
427
+ logger.error("Error initializing dev server", getErrorMeta(e));
428
+ viteReject(e);
429
+ },
430
+ );
418
431
  });
419
432
 
420
433
  app.post("/_sb_disconnect", authHandler, async (req, res) => {
@@ -444,7 +457,6 @@ export async function createDevServer({
444
457
  viteResolve = resolve;
445
458
  viteReject = reject;
446
459
  });
447
- vitePromise.catch(() => {});
448
460
  isViteServerInitialized = false;
449
461
  }
450
462
  res.send("ok");
@@ -488,143 +500,8 @@ export async function createDevServer({
488
500
  timeSinceLastActivityMs: lockService.timeSinceLastActivity,
489
501
  });
490
502
  } else {
491
- // Lock service may be undefined during warm pool activation (sync is
492
- // deferred until the first /_sb_connect). Return a safe default instead
493
- // of rejecting vitePromise which would crash the server.
494
- // Use MAX_SAFE_INTEGER so idle-detection callers don't mistake this for recent activity.
495
- res.json({
496
- isUserActive: false,
497
- connectedUsers: [],
498
- timeSinceLastActivityMs: Number.MAX_SAFE_INTEGER,
499
- });
500
- }
501
- });
502
-
503
- // /_sb_persist: SABS POSTs a presigned S3 upload URL at teardown.
504
- // This endpoint creates a tar+zstd archive of the workspace and uploads
505
- // it directly to S3 via the presigned PUT URL. Registered on the Express
506
- // app so it's available on ALL dev server instances (warm and cold-start, D-24).
507
- let persistInProgress = false;
508
- app.post("/_sb_persist", authHandler, async (req, res) => {
509
- if (persistInProgress) {
510
- res.status(409).json({ error: "persist already in progress" });
511
- return;
512
- }
513
- persistInProgress = true;
514
- const persistStart = Date.now();
515
- try {
516
- const { uploadURL } = req.body as { uploadURL: string };
517
- if (!uploadURL) {
518
- res.status(400).json({ error: "uploadURL required" });
519
- return;
520
- }
521
-
522
- logger.info("/_sb_persist: archiving and uploading workspace to S3...");
523
-
524
- // Create tar+zstd archive excluding .git/ and node_modules/.cache/
525
- // then stream directly to S3 via presigned PUT URL.
526
- const archive = await new Promise<Buffer>(
527
- (resolveArchive, rejectArchive) => {
528
- const tarProc = child_process.spawn(
529
- "tar",
530
- [
531
- "cf",
532
- "-",
533
- "--exclude",
534
- ".git",
535
- "--exclude",
536
- "node_modules/.cache",
537
- "--exclude",
538
- ".superblocks",
539
- "-C",
540
- process.cwd(),
541
- ".",
542
- ],
543
- { stdio: ["ignore", "pipe", "pipe"] },
544
- );
545
- const zstdProc = child_process.spawn(
546
- "zstd",
547
- ["-1", "--no-progress"],
548
- {
549
- stdio: ["pipe", "pipe", "pipe"],
550
- },
551
- );
552
-
553
- tarProc.stdout.pipe(zstdProc.stdin);
554
-
555
- // Attach error handlers on the piped stdin. ChildProcess-level 'error'
556
- // events don't catch stream errors like EPIPE from writing to a closed
557
- // pipe if the downstream process crashes.
558
- zstdProc.stdin.on("error", (err) => {
559
- rejectArchive(new Error(`zstd stdin stream error: ${err.message}`));
560
- });
561
-
562
- const chunks: Buffer[] = [];
563
- zstdProc.stdout.on("data", (chunk: Buffer) => chunks.push(chunk));
564
-
565
- let stderr = "";
566
- tarProc.stderr.on("data", (chunk: Buffer) => {
567
- stderr += chunk.toString();
568
- });
569
- zstdProc.stderr.on("data", (chunk: Buffer) => {
570
- stderr += chunk.toString();
571
- });
572
-
573
- let tarExitCode: number | null = null;
574
- tarProc.on("close", (code) => {
575
- tarExitCode = code;
576
- });
577
-
578
- zstdProc.on("close", (code) => {
579
- if (code !== 0) {
580
- rejectArchive(
581
- new Error(`zstd compression failed (code ${code}): ${stderr}`),
582
- );
583
- } else if (tarExitCode !== null && tarExitCode !== 0) {
584
- rejectArchive(
585
- new Error(
586
- `tar archival failed (code ${tarExitCode}): ${stderr}`,
587
- ),
588
- );
589
- } else {
590
- resolveArchive(Buffer.concat(chunks));
591
- }
592
- });
593
- tarProc.on("error", rejectArchive);
594
- zstdProc.on("error", rejectArchive);
595
- },
596
- );
597
-
598
- logger.info(
599
- `/_sb_persist: archive created (${(archive.length / 1024 / 1024).toFixed(1)}MB) in ${Date.now() - persistStart}ms, uploading...`,
600
- );
601
-
602
- const uploadStart = Date.now();
603
- const uploadResp = await fetch(uploadURL, {
604
- method: "PUT",
605
- body: archive,
606
- headers: {
607
- "Content-Type": "application/zstd",
608
- "Content-Length": String(archive.length),
609
- },
610
- signal: AbortSignal.timeout(30_000),
611
- });
612
-
613
- if (!uploadResp.ok) {
614
- throw new Error(
615
- `S3 upload failed: ${uploadResp.status} ${uploadResp.statusText}`,
616
- );
617
- }
618
-
619
- logger.info(
620
- `/_sb_persist: uploaded in ${Date.now() - uploadStart}ms (total ${Date.now() - persistStart}ms)`,
621
- );
622
- res.status(200).json({ status: "uploaded" });
623
- } catch (error) {
624
- logger.error(`/_sb_persist failed: ${error}`);
625
- res.status(500).json({ error: String(error) });
626
- } finally {
627
- persistInProgress = false;
503
+ logger.error("Lock service not found, rejecting request");
504
+ viteReject(new Error("Lock service not found, rejecting request"));
628
505
  }
629
506
  });
630
507
 
@@ -656,72 +533,9 @@ export async function createDevServer({
656
533
  );
657
534
  });
658
535
 
659
- if (existingServer) {
660
- // Warm standby mode: reuse the pre-existing HTTP server to avoid a port gap.
661
- // Replace the warm server's request handler with the full Express app.
662
- existingServer.removeAllListeners("request");
663
- existingServer.on("request", app);
664
- httpServer = existingServer;
665
- logger.info(
666
- `Attached full dev server to existing HTTP server on port ${port}`,
667
- );
668
- } else {
669
- logger.info(`Starting HTTP server on port ${port}`);
670
- httpServer = await app.listen(port);
671
- logger.info(`HTTP server started successfully on port ${port}`);
672
- }
673
-
674
- // Start vite eagerly so it's ready (or nearly ready) by the time the editor
675
- // connects via /_sb_connect. Previously vite was started lazily on first
676
- // /_sb_connect, adding ~5s to perceived connection time.
677
- if (!isViteServerInitialized) {
678
- isViteServerInitialized = true;
679
- logger.info("Eagerly starting vite server...");
680
-
681
- const featureFlags = await sdk
682
- ?.getFeatureFlagsForCurrentUser()
683
- .catch((err: unknown) => {
684
- logger.warn(`Failed to fetch feature flags, using defaults: ${err}`);
685
- return undefined;
686
- });
687
-
688
- const viteStartPromise = startVite({
689
- port,
690
- app,
691
- root,
692
- mode,
693
- fsOperationQueue,
694
- syncService,
695
- lockService,
696
- aiService,
697
- gitService,
698
- activateGitService,
699
- snapshotManager,
700
- checkAuthorization,
701
- logger: loggerOverride,
702
- httpServer,
703
- superblocksBaseUrl: explicitBaseUrl || localToken?.superblocksBaseUrl,
704
- features: {
705
- enableSessionRecording: featureFlags?.enableSessionRecording() ?? false,
706
- },
707
- });
708
- // Note: vitePromise itself gets a no-op .catch() at its construction site
709
- // (see lines ~179 / ~443) to prevent UnhandledPromiseRejection if Vite
710
- // fails before any request awaits vitePromise. The actual error is logged
711
- // below via the rejection branch of .then().
712
- viteStartPromise.then(
713
- (result) => {
714
- logger.info("Vite server initialized eagerly");
715
- viteResolve();
716
- viteCreationResults = result;
717
- },
718
- (e) => {
719
- logger.error("Error initializing vite server", getErrorMeta(e));
720
- viteReject(e);
721
- },
722
- );
723
- }
724
-
536
+ logger.info(`Starting HTTP server on port ${port}`);
537
+ httpServer = await app.listen(port);
538
+ logger.info(`HTTP server started successfully on port ${port}`);
725
539
  return httpServer;
726
540
  }
727
541
 
@@ -930,54 +744,6 @@ async function startVite({
930
744
  });
931
745
  }
932
746
 
933
- /**
934
- * Pre-warm the Vite dependency cache by creating and immediately closing a
935
- * minimal Vite server. This populates `node_modules/.vite/deps` with
936
- * pre-bundled dependencies so that `createDevServer` starts ~1-2s faster.
937
- *
938
- * Call this during warm standby phase while waiting for activation.
939
- * Non-fatal — if it fails the activation-time Vite start still works,
940
- * just without the cache benefit.
941
- */
942
- export async function preWarmViteCache(root: string): Promise<void> {
943
- const start = Date.now();
944
- console.log("[warm] Pre-warming Vite dependency cache...");
945
- try {
946
- // Create a Vite server with optimizeDeps.noDiscovery so it only processes
947
- // the deps listed in optimizeDeps.include (from vite.config) without
948
- // scanning source files. This is faster and avoids hanging on HMR setup.
949
- const server = await createServer({
950
- root,
951
- mode: "development",
952
- server: { middlewareMode: true, hmr: false },
953
- plugins: [tsconfigPaths()],
954
- logLevel: "warn",
955
- optimizeDeps: { noDiscovery: true },
956
- });
957
- // Poll for the dep cache directory instead of sleeping a fixed 500ms.
958
- // Vite renames a temp dir to node_modules/.vite/deps once optimization
959
- // completes; polling avoids both under-waiting (cache not committed) and
960
- // over-waiting (wasted time on fast disks).
961
- const depsDir = path.join(root, "node_modules", ".vite", "deps");
962
- const maxWaitMs = 5_000;
963
- const pollMs = 50;
964
- const deadline = Date.now() + maxWaitMs;
965
- while (!existsSync(depsDir) && Date.now() < deadline) {
966
- await new Promise((r) => setTimeout(r, pollMs));
967
- }
968
- await server.close();
969
- const cached = existsSync(depsDir);
970
- console.log(
971
- `[warm] Vite cache ${cached ? "warmed" : "not committed (timed out)"} in ${Date.now() - start}ms`,
972
- );
973
- } catch (error) {
974
- console.warn(
975
- `[warm] Vite cache pre-warm failed (non-fatal, ${Date.now() - start}ms):`,
976
- error,
977
- );
978
- }
979
- }
980
-
981
747
  const DEFAULT_HMR_PORT = 24678;
982
748
 
983
749
  function getFreePort() {
package/src/index.ts CHANGED
@@ -64,7 +64,7 @@ export {
64
64
  export { AUTO_UPGRADE_EXIT_CODE } from "./cli-replacement/automatic-upgrades.js";
65
65
  export { RESTART_EXIT_CODE } from "./dev-utils/dev-server.mjs";
66
66
 
67
- export { createDevServer, preWarmViteCache } from "./dev-utils/dev-server.mjs";
67
+ export { createDevServer } from "./dev-utils/dev-server.mjs";
68
68
 
69
69
  export { TokenManager } from "./dev-utils/token-manager.js";
70
70