@intelligems/sst 2.49.8-ig.2 → 2.49.8-ig.3

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/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # sst
2
+
3
+ [SST](https://sst.dev) makes it easy to build modern full-stack applications on AWS.
4
+
5
+ The `sst` package is made up of the following.
6
+
7
+ - [`sst`](https://docs.sst.dev/packages/sst) CLI
8
+ - [`sst/node`](https://docs.sst.dev/clients) Node.js client
9
+ - [`sst/constructs`](https://docs.sst.dev/constructs) CDK constructs
10
+
11
+ ## Installation
12
+
13
+ Install the `sst` package in your project root.
14
+
15
+ ```bash
16
+ npm install sst --save-exact
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ Once installed, you can run the CLI commands using.
22
+
23
+ ```bash
24
+ npx sst <command>
25
+ ```
26
+
27
+ Import the Node.js client in your functions. For example, you can import the `Bucket` client.
28
+
29
+ ```ts
30
+ import { Bucket } from "sst/node/bucket";
31
+ ```
32
+
33
+ And import the constructs you need in your stacks code. For example, you can add an API.
34
+
35
+ ```ts
36
+ import { Api } from "sst/constructs";
37
+ ```
38
+
39
+ For more details, [head over to our docs](https://docs.sst.dev).
40
+
41
+ ---
42
+
43
+ **Join our community** [Discord](https://sst.dev/discord) | [YouTube](https://www.youtube.com/c/sst-dev) | [Twitter](https://twitter.com/SST_dev)
44
+
45
+ ## Dev mode tuning (Intelligems fork)
46
+
47
+ `sst dev` runs Node functions as worker threads inside the CLI process. In mono-build mode every worker loads
48
+ the whole `.mono-build` bundle, so the number of live workers is what decides memory use. These variables
49
+ control it (defaults in `src/runtime/worker-config.ts`):
50
+
51
+ | Variable | Default | Meaning |
52
+ | --- | --- | --- |
53
+ | `SST_WORKER_POOL_SIZE` | `4` | Max live workers per pool (one shared pool for all mono-build Node functions). Requests beyond that wait for a free worker instead of spawning a new one. |
54
+ | `SST_WORKER_CONCURRENCY` | `10` | Invocations one Node worker runs at the same time. Each invocation gets its own `process.env`. Set to `1` to fall back to one request per worker. |
55
+ | `SST_WORKER_IDLE_TIMEOUT` | `300000` | Milliseconds an idle worker is kept before it is terminated. |
56
+ | `SST_WARMUP_COUNT` | pool size | Warm pings sent at startup (capped at the pool size). `0` disables warmup. |
57
+ | `SST_WORKER_MAX_HEAP_MB` | `1024` | V8 old-space cap per worker. A worker that exceeds it exits and its in-flight requests fail; the dev session keeps running. `0` removes the cap. |
58
+ | `SST_SOURCE_MAPS` | unset | `true` runs workers with `--enable-source-maps` (costs memory per worker). |
59
+ | `SST_DEBUG_MEMORY` | unset | `true` samples process and worker memory to `.sst/memory.log` every 10s and adds peaks to the pool session summary. |
60
+ | `SST_DEBUG_POOL` | unset | `true` logs pool events (`CREATE`, `REUSE`, `POOL_WAIT`, `TERMINATE`, …) to `.sst/worker-pool.log`. |
@@ -401,7 +401,7 @@ export const dev = (program) => program.command(["dev", "start"], "Work on your
401
401
  import("./plugins/warmer.js").then((mod) => mod.useRDSWarmer()),
402
402
  useFunctionLogger(),
403
403
  ]);
404
- // Warm the pool through the real request flow. SST_WARMUP_COUNT=0
404
+ // Warm SST_WARMUP_COUNT workers through the real request flow. 0
405
405
  // turns this off; the count is capped at the pool size so warmup can
406
406
  // never hold more isolates than steady state would.
407
407
  const { WARMUP_COUNT } = await import("../../runtime/worker-config.js");
@@ -103,11 +103,19 @@ export async function useLocalServer(opts) {
103
103
  const wss = new WebSocketServer({ noServer: true });
104
104
  const wss2 = new WebSocketServer({ noServer: true });
105
105
  const sockets = new Set();
106
+ // Replayed to console clients on connect. Bounded: every entry keeps its
107
+ // full event payload and every log line, so an unbounded list grows for the
108
+ // life of the dev session.
109
+ const MAX_INVOCATIONS = 200;
106
110
  let invocations = [];
107
111
  function publish(invocation) {
108
112
  const index = invocations.findLastIndex((i) => i.id === invocation.id);
109
- if (index < 0)
113
+ if (index < 0) {
110
114
  invocations.push(invocation);
115
+ if (invocations.length > MAX_INVOCATIONS) {
116
+ invocations.splice(0, invocations.length - MAX_INVOCATIONS);
117
+ }
118
+ }
111
119
  else
112
120
  invocations[index] = invocation;
113
121
  const json = JSON.stringify({
package/cli/sst.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "sideEffects": false,
3
3
  "name": "@intelligems/sst",
4
- "version": "2.49.8-ig.2",
4
+ "version": "2.49.8-ig.3",
5
5
  "bin": {
6
6
  "sst": "cli/sst.js"
7
7
  },
@@ -0,0 +1,20 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ /**
3
+ * Keys of the `sst dev` process environment that override the environment a
4
+ * deployed stub forwards with each invocation.
5
+ *
6
+ * The stub Lambda snapshots ITS OWN `process.env` and sends it along
7
+ * (`support/bridge/live-lambda.ts`), and the local worker runs with exactly
8
+ * that map. So anything a developer wants to change per session — which
9
+ * database, which frontend — either had to be on the stub's function config
10
+ * (a CloudFormation update of every stub each time it changed) or could not
11
+ * reach the handler at all.
12
+ *
13
+ * `SST_DEV_ENV_OVERRIDES=KEY1,KEY2` names the keys the dev process supplies
14
+ * instead. Only listed keys that are set in the dev process are applied; the
15
+ * rest of the stub's environment (AWS credentials, function metadata) stays
16
+ * as forwarded.
17
+ */
18
+ export declare const SST_DEV_ENV_OVERRIDES_KEY = "SST_DEV_ENV_OVERRIDES";
19
+ export declare function devEnvOverrides(env?: NodeJS.ProcessEnv): Record<string, string>;
20
+ export declare function applyDevEnvOverrides(forwarded: Record<string, string> | undefined, env?: NodeJS.ProcessEnv): Record<string, string>;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Keys of the `sst dev` process environment that override the environment a
3
+ * deployed stub forwards with each invocation.
4
+ *
5
+ * The stub Lambda snapshots ITS OWN `process.env` and sends it along
6
+ * (`support/bridge/live-lambda.ts`), and the local worker runs with exactly
7
+ * that map. So anything a developer wants to change per session — which
8
+ * database, which frontend — either had to be on the stub's function config
9
+ * (a CloudFormation update of every stub each time it changed) or could not
10
+ * reach the handler at all.
11
+ *
12
+ * `SST_DEV_ENV_OVERRIDES=KEY1,KEY2` names the keys the dev process supplies
13
+ * instead. Only listed keys that are set in the dev process are applied; the
14
+ * rest of the stub's environment (AWS credentials, function metadata) stays
15
+ * as forwarded.
16
+ */
17
+ export const SST_DEV_ENV_OVERRIDES_KEY = "SST_DEV_ENV_OVERRIDES";
18
+ export function devEnvOverrides(env = process.env) {
19
+ const list = env[SST_DEV_ENV_OVERRIDES_KEY];
20
+ if (!list)
21
+ return {};
22
+ const overrides = {};
23
+ for (const key of list.split(",").map((k) => k.trim()).filter(Boolean)) {
24
+ const value = env[key];
25
+ if (value !== undefined)
26
+ overrides[key] = value;
27
+ }
28
+ return overrides;
29
+ }
30
+ export function applyDevEnvOverrides(forwarded, env = process.env) {
31
+ return { ...(forwarded ?? {}), ...devEnvOverrides(env) };
32
+ }
@@ -14,6 +14,7 @@ import { findAbove } from "../../util/fs.js";
14
14
  import { useMonoBuildConfig } from "../mono-build-config.js";
15
15
  import { SOURCE_MAPS, WORKER_MAX_HEAP_MB } from "../worker-config.js";
16
16
  import { forgetWorkerMemory, recordWorkerMemory } from "../memory-logging.js";
17
+ import { applyDevEnvOverrides } from "../dev-env-overrides.js";
17
18
  export const useNodeHandler = () => {
18
19
  const rebuildCache = {};
19
20
  process.on("exit", () => {
@@ -39,7 +40,7 @@ export const useNodeHandler = () => {
39
40
  const workers = await useRuntimeWorkers();
40
41
  const worker = new Worker(url.fileURLToPath(new URL("../../support/nodejs-runtime/index.mjs", import.meta.url)), {
41
42
  env: {
42
- ...input.environment,
43
+ ...applyDevEnvOverrides(input.environment),
43
44
  IS_LOCAL: "true",
44
45
  },
45
46
  // Source maps cost memory in every isolate; opt in with SST_SOURCE_MAPS=true
package/runtime/server.js CHANGED
@@ -9,6 +9,7 @@ import { lazy } from "../util/lazy.js";
9
9
  import { getRequestPath } from "./request-utils.js";
10
10
  import { logServer } from "./debug-bridge-logging.js";
11
11
  import { logEventTrace } from "./event-trace-logging.js";
12
+ import { applyDevEnvOverrides } from "./dev-env-overrides.js";
12
13
  export const useRuntimeServerConfig = lazy(async () => {
13
14
  const port = await getPort({
14
15
  port: 12557,
@@ -128,7 +129,8 @@ export const useRuntimeServer = lazy(async () => {
128
129
  // This prevents env leakage when workers are reused across different functions
129
130
  res.json({
130
131
  event: payload.event,
131
- env: payload.env,
132
+ // The dev process's own axes win over what the stub forwarded.
133
+ env: applyDevEnvOverrides(payload.env),
132
134
  });
133
135
  });
134
136
  app.post(`/:workerID/${cfg.API_VERSION}/runtime/invocation/:awsRequestId/response`, express.json({
@@ -8,7 +8,7 @@ export declare const POOL_SIZE: number;
8
8
  export declare const IDLE_TIMEOUT: number;
9
9
  /** Invocations one Node worker may run at the same time. */
10
10
  export declare const WORKER_CONCURRENCY: number;
11
- /** Warm pings sent at dev start. 0 disables warmup. Capped at POOL_SIZE. */
11
+ /** Workers to warm at dev start. 0 disables warmup. Capped at POOL_SIZE. */
12
12
  export declare const WARMUP_COUNT: number;
13
13
  /** V8 old-space cap for each Node worker thread, in MB. 0 leaves it unbounded. */
14
14
  export declare const WORKER_MAX_HEAP_MB: number;
@@ -14,9 +14,9 @@ export const POOL_SIZE = int("SST_WORKER_POOL_SIZE", 4);
14
14
  /** How long an idle worker is kept before it is terminated. */
15
15
  export const IDLE_TIMEOUT = int("SST_WORKER_IDLE_TIMEOUT", 5 * 60 * 1000);
16
16
  /** Invocations one Node worker may run at the same time. */
17
- export const WORKER_CONCURRENCY = Math.max(1, int("SST_WORKER_CONCURRENCY", 5));
18
- /** Warm pings sent at dev start. 0 disables warmup. Capped at POOL_SIZE. */
19
- export const WARMUP_COUNT = Math.min(POOL_SIZE, Math.max(0, int("SST_WARMUP_COUNT", POOL_SIZE)));
17
+ export const WORKER_CONCURRENCY = Math.max(1, int("SST_WORKER_CONCURRENCY", 10));
18
+ /** Workers to warm at dev start. 0 disables warmup. Capped at POOL_SIZE. */
19
+ export const WARMUP_COUNT = Math.min(POOL_SIZE, Math.max(0, int("SST_WARMUP_COUNT", 1)));
20
20
  /** V8 old-space cap for each Node worker thread, in MB. 0 leaves it unbounded. */
21
21
  export const WORKER_MAX_HEAP_MB = int("SST_WORKER_MAX_HEAP_MB", 1024);
22
22
  /** Run Node workers with --enable-source-maps (costs memory per worker). */
@@ -52,9 +52,12 @@ export declare class WorkerPool {
52
52
  liveCount(poolKey: string): number;
53
53
  canCreate(poolKey: string): boolean;
54
54
  /**
55
- * Least-loaded live worker with spare capacity, or undefined. Workers whose
56
- * bundle is older than `currentMtime` are retired on the way: idle ones now,
57
- * busy ones once they drain.
55
+ * The busiest live worker that still has spare capacity, or undefined.
56
+ * Packing invocations into as few workers as possible is what keeps memory
57
+ * down: every worker that serves traffic grows to hold the handlers it has
58
+ * loaded, so an idle spare is a few hundred MB doing nothing. Workers whose
59
+ * bundle is older than `currentMtime` are retired on the way: idle ones
60
+ * now, busy ones once they drain.
58
61
  */
59
62
  pick(poolKey: string, currentMtime?: number): PoolWorker | undefined;
60
63
  /** Hand an invocation to a worker. */
@@ -44,9 +44,12 @@ export class WorkerPool {
44
44
  return this.liveCount(poolKey) < this.opts.maxWorkers;
45
45
  }
46
46
  /**
47
- * Least-loaded live worker with spare capacity, or undefined. Workers whose
48
- * bundle is older than `currentMtime` are retired on the way: idle ones now,
49
- * busy ones once they drain.
47
+ * The busiest live worker that still has spare capacity, or undefined.
48
+ * Packing invocations into as few workers as possible is what keeps memory
49
+ * down: every worker that serves traffic grows to hold the handlers it has
50
+ * loaded, so an idle spare is a few hundred MB doing nothing. Workers whose
51
+ * bundle is older than `currentMtime` are retired on the way: idle ones
52
+ * now, busy ones once they drain.
50
53
  */
51
54
  pick(poolKey, currentMtime) {
52
55
  const pool = this.pools.get(poolKey);
@@ -66,7 +69,7 @@ export class WorkerPool {
66
69
  continue;
67
70
  if (worker.inFlight >= worker.maxConcurrency)
68
71
  continue;
69
- if (!best || worker.inFlight < best.inFlight)
72
+ if (!best || worker.inFlight > best.inFlight)
70
73
  best = worker;
71
74
  }
72
75
  return best;
@@ -71,16 +71,20 @@ export declare const useRuntimeWorkers: () => Promise<{
71
71
  cb: (payload: any) => void;
72
72
  };
73
73
  /**
74
- * Warm the pool by invoking a Node function with warm pings.
74
+ * Warm `workersToWarm` pooled workers by invoking a Node function with
75
+ * concurrent warm pings.
75
76
  *
76
77
  * Each ping is marked as a fan-out *child* (`__WARMER_INVOCATION__ > 1`)
77
78
  * so the app's lambda-warmer preloads its handlers and returns instead of
78
79
  * fanning out to `concurrency` more Lambdas, which is what turned the old
79
- * 30 pings into ~900 worker creations. The count is capped at the pool
80
- * size, and pings go through the normal pool path, so warmup can never
81
- * hold more isolates than steady state.
80
+ * 30 pings into ~900 worker creations. Pings go through the normal pool
81
+ * path, so warmup can never hold more isolates than steady state.
82
+ *
83
+ * In mono-build mode this waits for the bundle to settle first: dev start
84
+ * kicks off the bundle's first watch build, and a worker loaded before it
85
+ * lands is retired as stale seconds later, which wasted every warm worker.
82
86
  */
83
- triggerWarmup(count: number): Promise<{
87
+ triggerWarmup(workersToWarm: number): Promise<{
84
88
  warmed: number;
85
89
  elapsed?: undefined;
86
90
  } | {
@@ -137,6 +137,44 @@ function isPoolableRuntime(runtime, env) {
137
137
  return (POOLABLE_RUNTIMES.has(runtime) ||
138
138
  [...POOLABLE_RUNTIMES].some((r) => runtime.startsWith(r)));
139
139
  }
140
+ /**
141
+ * Resolve once `.mono-build/.last-rebuild` exists and has not changed for
142
+ * `quietMs`. No-op when mono-build is off. Gives up after `maxWaitMs`.
143
+ *
144
+ * Dev start writes the timestamp twice about 7s apart (the app's initial
145
+ * rebuild, then the watcher's first pass), so the quiet window has to be
146
+ * longer than that gap or the warm worker is retired as stale right away.
147
+ */
148
+ async function waitForStableBundle(quietMs = 10_000, maxWaitMs = 120_000) {
149
+ const config = useMonoBuildConfig();
150
+ const timestampFile = path.join(config.dir, ".last-rebuild");
151
+ const started = Date.now();
152
+ let last;
153
+ let lastChange = Date.now();
154
+ while (Date.now() - started < maxWaitMs) {
155
+ if (!config.enabled) {
156
+ // Not (yet) a mono-build project; nothing to wait for unless it appears
157
+ if (Date.now() - started > quietMs)
158
+ return;
159
+ }
160
+ else {
161
+ let current;
162
+ try {
163
+ current = fs.readFileSync(timestampFile, "utf-8");
164
+ }
165
+ catch { }
166
+ if (current !== last) {
167
+ last = current;
168
+ lastChange = Date.now();
169
+ }
170
+ else if (current !== undefined && Date.now() - lastChange >= quietMs) {
171
+ return;
172
+ }
173
+ }
174
+ await new Promise((r) => setTimeout(r, 500));
175
+ }
176
+ logPool("WARMUP_BUNDLE_TIMEOUT", { waitedMs: Date.now() - started });
177
+ }
140
178
  /** Only the Node runtime shim knows how to run invocations side by side. */
141
179
  function concurrencyFor(runtime) {
142
180
  return runtime.startsWith("nodejs") ? WORKER_CONCURRENCY : 1;
@@ -689,19 +727,25 @@ export const useRuntimeWorkers = lazy(async () => {
689
727
  stats,
690
728
  subscribe: bus.forward("worker.started", "worker.stopped", "worker.exited", "worker.stdout", "worker.reused"),
691
729
  /**
692
- * Warm the pool by invoking a Node function with warm pings.
730
+ * Warm `workersToWarm` pooled workers by invoking a Node function with
731
+ * concurrent warm pings.
693
732
  *
694
733
  * Each ping is marked as a fan-out *child* (`__WARMER_INVOCATION__ > 1`)
695
734
  * so the app's lambda-warmer preloads its handlers and returns instead of
696
735
  * fanning out to `concurrency` more Lambdas, which is what turned the old
697
- * 30 pings into ~900 worker creations. The count is capped at the pool
698
- * size, and pings go through the normal pool path, so warmup can never
699
- * hold more isolates than steady state.
736
+ * 30 pings into ~900 worker creations. Pings go through the normal pool
737
+ * path, so warmup can never hold more isolates than steady state.
738
+ *
739
+ * In mono-build mode this waits for the bundle to settle first: dev start
740
+ * kicks off the bundle's first watch build, and a worker loaded before it
741
+ * lands is retired as stale seconds later, which wasted every warm worker.
700
742
  */
701
- async triggerWarmup(count) {
702
- count = Math.min(count, POOL_SIZE);
703
- if (count <= 0)
743
+ async triggerWarmup(workersToWarm) {
744
+ workersToWarm = Math.min(workersToWarm, POOL_SIZE);
745
+ if (workersToWarm <= 0)
704
746
  return { warmed: 0 };
747
+ const count = workersToWarm * WORKER_CONCURRENCY;
748
+ await waitForStableBundle();
705
749
  const functions = useFunctions();
706
750
  const allFunctions = functions.all;
707
751
  // Find a nodejs function to use as the warmup target
@@ -721,7 +765,7 @@ export const useRuntimeWorkers = lazy(async () => {
721
765
  return { warmed: 0 };
722
766
  }
723
767
  const { functionName } = targetFunction;
724
- logPool("WARMUP_START", { count, functionName });
768
+ logPool("WARMUP_START", { count, workersToWarm, functionName });
725
769
  bus.publish("warmup.start", { count });
726
770
  const startTime = Date.now();
727
771
  let success = 0;
package/stacks/build.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import esbuild from "esbuild";
2
2
  import fs from "fs/promises";
3
3
  import path from "path";
4
+ import crypto from "crypto";
4
5
  import { dynamicImport } from "../util/module.js";
5
6
  import { findAbove } from "../util/fs.js";
6
7
  import { VisibleError } from "../error.js";
@@ -9,6 +10,14 @@ const _ = await import("@babel/generator");
9
10
  const generate = _.default?.default ?? _.default;
10
11
  // @ts-expect-error
11
12
  import ts from "@babel/plugin-syntax-typescript";
13
+ /**
14
+ * The last config module we imported, keyed by the hash of its bundled
15
+ * source. An ES module can never be unloaded, so every `import()` of a fresh
16
+ * `.sst.config.<time>.mjs` stays in memory for the life of `sst dev`. Dev
17
+ * rebuilds the config on every change to a file inside its bundle (all the
18
+ * stack code), so reuse the module when the bundle did not actually change.
19
+ */
20
+ let cached;
12
21
  export async function load(input, shallow) {
13
22
  const parsed = path.parse(input);
14
23
  const root = await findAbove(input, "package.json");
@@ -81,6 +90,7 @@ export async function load(input, shallow) {
81
90
  ],
82
91
  absWorkingDir: root,
83
92
  outfile,
93
+ write: false,
84
94
  banner: {
85
95
  js: [
86
96
  `import { createRequire as topLevelCreateRequire } from 'module';`,
@@ -97,12 +107,19 @@ export async function load(input, shallow) {
97
107
  // },
98
108
  entryPoints: [input],
99
109
  });
100
- // Logger.debug("built", input);
110
+ const output = result.outputFiles?.find((f) => f.path === outfile) ?? result.outputFiles?.[0];
111
+ if (!output)
112
+ throw new VisibleError("Config build produced no output");
113
+ const hash = crypto.createHash("sha256").update(output.contents).digest("hex");
114
+ if (cached && cached.hash === hash && cached.shallow === Boolean(shallow)) {
115
+ return [cached.metafile, cached.mod];
116
+ }
117
+ await fs.writeFile(outfile, output.contents);
101
118
  const mod = await dynamicImport(outfile);
102
- // Logger.debug("imported", input);
103
119
  await fs.rm(outfile, {
104
120
  force: true,
105
121
  });
122
+ cached = { hash, metafile: result.metafile, mod: mod.default, shallow: Boolean(shallow) };
106
123
  if (!mod.default?.config)
107
124
  throw new VisibleError(`The config file is improperly formatted.`, `Example:`, `export default {`, ` config() {`, ` return {`, ` name: "my-app",`, ` region: "us-east-1"`, ` }`, ` },`, ` stacks(app) {`, ` }`, `}`);
108
125
  return [result.metafile, mod.default];