@intelligems/sst 2.49.3 → 2.49.6-ig.10

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.
@@ -14,6 +14,9 @@ import { usePythonHandler } from "./handlers/python.js";
14
14
  import { useRustHandler } from "./handlers/rust.js";
15
15
  import { lazy } from "../util/lazy.js";
16
16
  import { Semaphore } from "../util/semaphore.js";
17
+ import { isMonoBuildPath } from "./mono-build-config.js";
18
+ // Build concurrency semaphore - only used for actual builds, not mono-build lookups
19
+ const buildSemaphore = new Semaphore(parseInt(process.env.SST_BUILD_CONCURRENCY || "4", 10));
17
20
  export const useRuntimeHandlers = lazy(() => {
18
21
  const handlers = [
19
22
  useNodeHandler(),
@@ -39,64 +42,110 @@ export const useRuntimeHandlers = lazy(() => {
39
42
  return result;
40
43
  },
41
44
  async build(functionID, mode) {
42
- async function task() {
45
+ // Fast path for mono-bundle: check without semaphore since no actual build work
46
+ async function tryMonoBundleFastPath() {
43
47
  const func = useFunctions().fromID(functionID);
44
48
  if (!func)
45
- return {
46
- type: "error",
47
- errors: [`Function with ID "${functionID}" not found`],
48
- };
49
+ return null;
49
50
  const handler = result.for(func.runtime);
50
51
  const out = path.join(project.paths.artifacts, functionID);
51
- await fs.rm(out, { recursive: true, force: true });
52
- await fs.mkdir(out, { recursive: true });
53
- bus.publish("function.build.started", { functionID });
54
- if (func.hooks?.beforeBuild)
55
- await func.hooks.beforeBuild(func, out);
56
- const built = await handler.build({
52
+ // Check for mono-bundle mode by doing a preliminary build call
53
+ // In mono-bundle mode, handler returns its own out path immediately without building
54
+ const monoBundleCheck = await handler.build({
57
55
  functionID,
58
56
  out,
59
57
  mode,
60
58
  props: func,
61
59
  });
62
- if (built.type === "error") {
63
- bus.publish("function.build.failed", {
60
+ // If mono-bundle detected (handler returned custom out), skip all artifact work
61
+ // Don't fire build events - mono-bundle is built externally by esbuild watch
62
+ // Worker pool invalidation is handled separately when bundle file actually changes
63
+ if (monoBundleCheck.type === "success" && monoBundleCheck.out) {
64
+ return {
65
+ type: "success",
66
+ handler: monoBundleCheck.handler,
67
+ out: monoBundleCheck.out,
68
+ sourcemap: monoBundleCheck.sourcemap,
69
+ };
70
+ }
71
+ return null; // Not mono-bundle, need full build
72
+ }
73
+ // Full build with semaphore protection for actual compilation work
74
+ async function fullBuild() {
75
+ const func = useFunctions().fromID(functionID);
76
+ if (!func)
77
+ return {
78
+ type: "error",
79
+ errors: [`Function with ID "${functionID}" not found`],
80
+ };
81
+ const handler = result.for(func.runtime);
82
+ const out = path.join(project.paths.artifacts, functionID);
83
+ // Acquire semaphore only for actual build work
84
+ const unlock = await buildSemaphore.lock();
85
+ try {
86
+ // Non-mono-bundle: follow original flow
87
+ await fs.rm(out, { recursive: true, force: true });
88
+ await fs.mkdir(out, { recursive: true });
89
+ bus.publish("function.build.started", { functionID });
90
+ if (func.hooks?.beforeBuild)
91
+ await func.hooks.beforeBuild(func, out);
92
+ const built = await handler.build({
64
93
  functionID,
65
- errors: built.errors,
94
+ out,
95
+ mode,
96
+ props: func,
66
97
  });
67
- return built;
68
- }
69
- if (func.copyFiles) {
70
- await Promise.all(func.copyFiles.map(async (entry) => {
71
- const fromPath = path.join(project.paths.root, entry.from);
72
- const to = entry.to || entry.from;
73
- if (path.isAbsolute(to))
74
- throw new Error(`Copy destination path "${to}" must be relative`);
75
- const toPath = path.join(out, to);
76
- if (mode === "deploy")
77
- await fs.cp(fromPath, toPath, {
78
- recursive: true,
79
- });
80
- if (mode === "start") {
81
- try {
82
- const dir = path.dirname(toPath);
83
- await fs.mkdir(dir, { recursive: true });
84
- await fs.symlink(fromPath, toPath);
98
+ if (built.type === "error") {
99
+ bus.publish("function.build.failed", {
100
+ functionID,
101
+ errors: built.errors,
102
+ });
103
+ return built;
104
+ }
105
+ if (func.copyFiles) {
106
+ await Promise.all(func.copyFiles.map(async (entry) => {
107
+ const fromPath = path.join(project.paths.root, entry.from);
108
+ const to = entry.to || entry.from;
109
+ if (path.isAbsolute(to))
110
+ throw new Error(`Copy destination path "${to}" must be relative`);
111
+ const toPath = path.join(out, to);
112
+ if (mode === "deploy")
113
+ await fs.cp(fromPath, toPath, {
114
+ recursive: true,
115
+ });
116
+ if (mode === "start") {
117
+ try {
118
+ const dir = path.dirname(toPath);
119
+ await fs.mkdir(dir, { recursive: true });
120
+ await fs.symlink(fromPath, toPath);
121
+ }
122
+ catch (ex) {
123
+ Logger.debug("Failed to symlink", fromPath, toPath, ex);
124
+ }
85
125
  }
86
- catch (ex) {
87
- Logger.debug("Failed to symlink", fromPath, toPath, ex);
88
- }
89
- }
90
- }));
126
+ }));
127
+ }
128
+ if (func.hooks?.afterBuild)
129
+ await func.hooks.afterBuild(func, out);
130
+ bus.publish("function.build.success", { functionID });
131
+ return {
132
+ ...built,
133
+ out,
134
+ sourcemap: built.sourcemap,
135
+ };
136
+ }
137
+ finally {
138
+ unlock();
91
139
  }
92
- if (func.hooks?.afterBuild)
93
- await func.hooks.afterBuild(func, out);
94
- bus.publish("function.build.success", { functionID });
95
- return {
96
- ...built,
97
- out,
98
- sourcemap: built.sourcemap,
99
- };
140
+ }
141
+ async function task() {
142
+ // Try mono-bundle fast path first (no semaphore needed)
143
+ const monoBundleResult = await tryMonoBundleFastPath();
144
+ if (monoBundleResult) {
145
+ return monoBundleResult;
146
+ }
147
+ // Fall back to full build with semaphore protection
148
+ return fullBuild();
100
149
  }
101
150
  if (pendingBuilds.has(functionID)) {
102
151
  Logger.debug("Waiting on pending build", functionID);
@@ -118,27 +167,43 @@ export const useRuntimeHandlers = lazy(() => {
118
167
  export const useFunctionBuilder = lazy(() => {
119
168
  const artifacts = new Map();
120
169
  const handlers = useRuntimeHandlers();
121
- const semaphore = new Semaphore(4);
170
+ // Track pending builds to prevent duplicate concurrent builds for same function
171
+ const pendingArtifactBuilds = new Map();
122
172
  const result = {
123
173
  artifact: (functionID) => {
174
+ // Fast path: already cached - return immediately without any async work
124
175
  if (artifacts.has(functionID))
125
176
  return artifacts.get(functionID);
126
177
  return result.build(functionID);
127
178
  },
128
179
  build: async (functionID) => {
129
- const unlock = await semaphore.lock();
130
- try {
131
- const result = await handlers.build(functionID, "start");
132
- if (!result)
133
- return;
134
- if (result.type === "error")
135
- return;
136
- artifacts.set(functionID, result);
180
+ // Fast path: already cached (check again in case of concurrent calls)
181
+ if (artifacts.has(functionID))
137
182
  return artifacts.get(functionID);
138
- }
139
- finally {
140
- unlock();
141
- }
183
+ // Deduplication: if build already in progress for this function, wait for it
184
+ const pending = pendingArtifactBuilds.get(functionID);
185
+ if (pending)
186
+ return pending;
187
+ const buildTask = async () => {
188
+ try {
189
+ // handlers.build() handles semaphore internally:
190
+ // - mono-build: no semaphore (fast path)
191
+ // - non-mono-build: semaphore protected
192
+ const buildResult = await handlers.build(functionID, "start");
193
+ if (!buildResult)
194
+ return;
195
+ if (buildResult.type === "error")
196
+ return;
197
+ artifacts.set(functionID, buildResult);
198
+ return artifacts.get(functionID);
199
+ }
200
+ finally {
201
+ pendingArtifactBuilds.delete(functionID);
202
+ }
203
+ };
204
+ const promise = buildTask();
205
+ pendingArtifactBuilds.set(functionID, promise);
206
+ return promise;
142
207
  },
143
208
  };
144
209
  const watcher = useWatcher();
@@ -146,6 +211,11 @@ export const useFunctionBuilder = lazy(() => {
146
211
  try {
147
212
  const functions = useFunctions();
148
213
  for (const [functionID, info] of Object.entries(functions.all)) {
214
+ // Optimization: For mono-build, the artifact path is stable and build is handled externally.
215
+ // We can skip the potentially expensive shouldBuild check and rebuild call.
216
+ const existing = artifacts.get(functionID);
217
+ if (existing && isMonoBuildPath(existing.out))
218
+ continue;
149
219
  const handler = handlers.for(info.runtime);
150
220
  if (!handler?.shouldBuild({
151
221
  functionID,
@@ -156,7 +226,8 @@ export const useFunctionBuilder = lazy(() => {
156
226
  Logger.debug("Rebuilt function", functionID);
157
227
  }
158
228
  }
159
- catch { }
229
+ catch {
230
+ }
160
231
  });
161
232
  return result;
162
233
  });
package/runtime/iot.js CHANGED
@@ -1,17 +1,44 @@
1
1
  import { useBus } from "../bus.js";
2
- import { useIOT } from "../iot.js";
2
+ import { useIOT, useIOTControl } from "../iot.js";
3
3
  import { lazy } from "../util/lazy.js";
4
+ import { logInvokeTrace } from "./worker-pool-logging.js";
5
+ import { logIot } from "./debug-bridge-logging.js";
6
+ import { logEventTrace } from "./event-trace-logging.js";
4
7
  export const useIOTBridge = lazy(async () => {
5
8
  const bus = useBus();
6
9
  const iot = await useIOT();
10
+ // The ack goes out on its own socket so it can never queue behind a response
11
+ // body. See `useIOTControl`.
12
+ const control = await useIOTControl();
7
13
  const topic = `${iot.prefix}/events`;
8
14
  bus.subscribe("function.success", async (evt) => {
9
- iot.publish(topic + "/" + evt.properties.workerID, "function.success", evt.properties);
15
+ const { workerID, requestID } = evt.properties;
16
+ logIot(`reqId=${requestID?.slice(0, 8)} Publishing function.success to worker ${workerID.slice(0, 8)}`);
17
+ const startTime = Date.now();
18
+ await iot.publish(topic + "/" + workerID, "function.success", evt.properties);
19
+ logIot(`reqId=${requestID?.slice(0, 8)} function.success published in ${Date.now() - startTime}ms`);
10
20
  });
11
21
  bus.subscribe("function.error", async (evt) => {
12
- iot.publish(topic + "/" + evt.properties.workerID, "function.error", evt.properties);
22
+ const { workerID, requestID } = evt.properties;
23
+ logIot(`reqId=${requestID?.slice(0, 8)} Publishing function.error to worker ${workerID.slice(0, 8)}`);
24
+ const startTime = Date.now();
25
+ await iot.publish(topic + "/" + workerID, "function.error", evt.properties);
26
+ logIot(`reqId=${requestID?.slice(0, 8)} function.error published in ${Date.now() - startTime}ms`);
13
27
  });
14
28
  bus.subscribe("function.ack", async (evt) => {
15
- iot.publish(topic + "/" + evt.properties.workerID, "function.ack", evt.properties);
29
+ const { workerID, requestID, functionID } = evt.properties;
30
+ logIot(`reqId=${requestID?.slice(0, 8)} Publishing function.ack to worker ${workerID.slice(0, 8)}`);
31
+ logInvokeTrace("IOT_ACK_START", workerID, `worker=${workerID.slice(0, 8)}`);
32
+ const startTime = Date.now();
33
+ await control.publish(topic + "/" + workerID, "function.ack", evt.properties);
34
+ const elapsed = Date.now() - startTime;
35
+ logIot(`reqId=${requestID?.slice(0, 8)} function.ack published in ${elapsed}ms`);
36
+ logInvokeTrace("IOT_ACK_DONE", workerID, `worker=${workerID.slice(0, 8)}`);
37
+ logEventTrace("IOT_ACK", {
38
+ requestID,
39
+ functionID,
40
+ workerID,
41
+ elapsed,
42
+ });
16
43
  });
17
44
  });
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Global mono build configuration for SST dev mode.
3
+ *
4
+ * Mono build mode bundles all Lambda handlers into a single file (.mono-build/index.mjs)
5
+ * instead of building each handler individually. This significantly speeds up dev mode
6
+ * by sharing compilation work across all handlers.
7
+ *
8
+ * Detection latches on: the first check that finds the bundle enables mono
9
+ * build for the rest of the session. It deliberately does NOT latch off, so a
10
+ * caller arriving before the bundle has been written cannot disable it.
11
+ */
12
+ export declare const useMonoBuildConfig: () => {
13
+ /**
14
+ * Whether mono build mode is enabled. Re-checked until the bundle is
15
+ * found, so a check made before it was written does not stick.
16
+ * When true, all Node.js handlers use the shared .mono-build bundle.
17
+ */
18
+ readonly enabled: boolean;
19
+ /**
20
+ * The mono bundle directory (.mono-build)
21
+ */
22
+ dir: string;
23
+ /**
24
+ * The mono bundle entry file (.mono-build/index.mjs)
25
+ */
26
+ entryFile: string;
27
+ /**
28
+ * The handler string to use for mono build mode
29
+ */
30
+ handler: string;
31
+ /**
32
+ * Check if a build output path represents a mono build.
33
+ * This is useful when you have a build result and need to determine its type.
34
+ */
35
+ isMonoBuildPath(buildOut: string): boolean;
36
+ /**
37
+ * Get the pool key for a function based on mono build status.
38
+ * For mono build: shared key (all functions share workers)
39
+ * For non-mono build: per-function key
40
+ */
41
+ getPoolKey(functionID: string, runtime: string, buildOut: string): {
42
+ key: string;
43
+ isShared: boolean;
44
+ };
45
+ };
46
+ /**
47
+ * Quick check for mono build mode without full config initialization.
48
+ * Use this for simple boolean checks where you don't need the full config.
49
+ */
50
+ export declare function isMonoBuildEnabled(): boolean;
51
+ /**
52
+ * Get the mono build directory path.
53
+ */
54
+ export declare function getMonoBuildDir(): string;
55
+ /**
56
+ * Check if a path represents a mono build output.
57
+ */
58
+ export declare function isMonoBuildPath(buildOut: string): boolean;
@@ -0,0 +1,104 @@
1
+ import path from "path";
2
+ import fsSync from "fs";
3
+ import { useProject } from "../project.js";
4
+ import { lazy } from "../util/lazy.js";
5
+ import { Logger } from "../logger.js";
6
+ /**
7
+ * Global mono build configuration for SST dev mode.
8
+ *
9
+ * Mono build mode bundles all Lambda handlers into a single file (.mono-build/index.mjs)
10
+ * instead of building each handler individually. This significantly speeds up dev mode
11
+ * by sharing compilation work across all handlers.
12
+ *
13
+ * Detection latches on: the first check that finds the bundle enables mono
14
+ * build for the rest of the session. It deliberately does NOT latch off, so a
15
+ * caller arriving before the bundle has been written cannot disable it.
16
+ */
17
+ export const useMonoBuildConfig = lazy(() => {
18
+ const project = useProject();
19
+ const monoBundleDir = path.join(project.paths.root, ".mono-build");
20
+ const monoBundlePath = path.join(monoBundleDir, "index.mjs");
21
+ /**
22
+ * Latches on true, never on false.
23
+ *
24
+ * This was a single `existsSync` at first call, cached for the session by
25
+ * `lazy`. Dev start deletes `.mono-build/index.mjs` and then rebuilds it, so
26
+ * any caller landing in that window — typically an invocation that beat the
27
+ * bundle to disk — pinned this to false for the whole session. Every handler
28
+ * then took the per-function esbuild path, which does not carry the mono
29
+ * build's `external` list, and failed to resolve packages the mono bundle
30
+ * never opens. The bundle finishing changed nothing, because the flag had
31
+ * already been decided, so the session stayed broken until restarted.
32
+ *
33
+ * Re-checking until it is found costs one `existsSync` per call for the few
34
+ * seconds before the bundle lands, and nothing afterwards.
35
+ */
36
+ let enabled = false;
37
+ const isEnabled = () => {
38
+ if (!enabled && fsSync.existsSync(monoBundlePath)) {
39
+ enabled = true;
40
+ Logger.debug("Mono build mode enabled:", monoBundlePath);
41
+ }
42
+ return enabled;
43
+ };
44
+ isEnabled();
45
+ return {
46
+ /**
47
+ * Whether mono build mode is enabled. Re-checked until the bundle is
48
+ * found, so a check made before it was written does not stick.
49
+ * When true, all Node.js handlers use the shared .mono-build bundle.
50
+ */
51
+ get enabled() {
52
+ return isEnabled();
53
+ },
54
+ /**
55
+ * The mono bundle directory (.mono-build)
56
+ */
57
+ dir: monoBundleDir,
58
+ /**
59
+ * The mono bundle entry file (.mono-build/index.mjs)
60
+ */
61
+ entryFile: monoBundlePath,
62
+ /**
63
+ * The handler string to use for mono build mode
64
+ */
65
+ handler: "index.handler",
66
+ /**
67
+ * Check if a build output path represents a mono build.
68
+ * This is useful when you have a build result and need to determine its type.
69
+ */
70
+ isMonoBuildPath(buildOut) {
71
+ return isEnabled() && buildOut.includes(".mono-build");
72
+ },
73
+ /**
74
+ * Get the pool key for a function based on mono build status.
75
+ * For mono build: shared key (all functions share workers)
76
+ * For non-mono build: per-function key
77
+ */
78
+ getPoolKey(functionID, runtime, buildOut) {
79
+ if (isEnabled() && buildOut.includes(".mono-build")) {
80
+ return { key: `${runtime}:mono-build`, isShared: true };
81
+ }
82
+ return { key: `${runtime}:${functionID}`, isShared: false };
83
+ },
84
+ };
85
+ });
86
+ /**
87
+ * Quick check for mono build mode without full config initialization.
88
+ * Use this for simple boolean checks where you don't need the full config.
89
+ */
90
+ export function isMonoBuildEnabled() {
91
+ return useMonoBuildConfig().enabled;
92
+ }
93
+ /**
94
+ * Get the mono build directory path.
95
+ */
96
+ export function getMonoBuildDir() {
97
+ return useMonoBuildConfig().dir;
98
+ }
99
+ /**
100
+ * Check if a path represents a mono build output.
101
+ */
102
+ export function isMonoBuildPath(buildOut) {
103
+ return useMonoBuildConfig().isMonoBuildPath(buildOut);
104
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Extracts a request path from a Lambda event for logging purposes.
3
+ * Handles API Gateway v1, v2, and other event formats.
4
+ */
5
+ export declare function getRequestPath(event: unknown): string;
6
+ /**
7
+ * Extracts correlation ID from headers for request tracing.
8
+ * Checks common correlation header names.
9
+ *
10
+ * Frontend can send: X-Correlation-ID, X-Request-ID, or X-Trace-ID
11
+ */
12
+ export declare function getCorrelationId(event: unknown): string | undefined;
13
+ /**
14
+ * Extracts API Gateway Request ID from requestContext.
15
+ * This ID is visible in browser dev tools (x-amzn-requestid response header).
16
+ */
17
+ export declare function getApiGatewayRequestId(event: unknown): string | undefined;
18
+ /**
19
+ * Extracts HTTP method from event
20
+ */
21
+ export declare function getHttpMethod(event: unknown): string | undefined;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Extracts a request path from a Lambda event for logging purposes.
3
+ * Handles API Gateway v1, v2, and other event formats.
4
+ */
5
+ export function getRequestPath(event) {
6
+ if (!event || typeof event !== "object") {
7
+ return "[unknown]";
8
+ }
9
+ const evt = event;
10
+ // API Gateway v2 (HTTP API)
11
+ if (typeof evt.rawPath === "string") {
12
+ return evt.rawPath;
13
+ }
14
+ // API Gateway v1 (REST API)
15
+ if (typeof evt.path === "string") {
16
+ return evt.path;
17
+ }
18
+ // Warmup requests
19
+ if ("ding" in evt || "warmer" in evt || evt.__sst_warmup === true) {
20
+ return "[warmup]";
21
+ }
22
+ // SQS, SNS, or other event types - no path
23
+ return "[event]";
24
+ }
25
+ /**
26
+ * Extracts correlation ID from headers for request tracing.
27
+ * Checks common correlation header names.
28
+ *
29
+ * Frontend can send: X-Correlation-ID, X-Request-ID, or X-Trace-ID
30
+ */
31
+ export function getCorrelationId(event) {
32
+ if (!event || typeof event !== "object") {
33
+ return undefined;
34
+ }
35
+ const evt = event;
36
+ // Get headers - handle both v1 and v2 API Gateway formats
37
+ let headers;
38
+ // API Gateway v2 uses lowercase headers
39
+ if (evt.headers && typeof evt.headers === "object") {
40
+ headers = evt.headers;
41
+ }
42
+ if (!headers)
43
+ return undefined;
44
+ // Check common correlation header names (case-insensitive)
45
+ const correlationHeaders = [
46
+ "x-correlation-id",
47
+ "x-request-id",
48
+ "x-trace-id",
49
+ "correlation-id",
50
+ "request-id",
51
+ ];
52
+ // Normalize header keys to lowercase for comparison
53
+ const normalizedHeaders = {};
54
+ for (const [key, value] of Object.entries(headers)) {
55
+ normalizedHeaders[key.toLowerCase()] = value;
56
+ }
57
+ for (const headerName of correlationHeaders) {
58
+ const value = normalizedHeaders[headerName];
59
+ if (value) {
60
+ return value;
61
+ }
62
+ }
63
+ return undefined;
64
+ }
65
+ /**
66
+ * Extracts API Gateway Request ID from requestContext.
67
+ * This ID is visible in browser dev tools (x-amzn-requestid response header).
68
+ */
69
+ export function getApiGatewayRequestId(event) {
70
+ if (!event || typeof event !== "object") {
71
+ return undefined;
72
+ }
73
+ const evt = event;
74
+ // Check requestContext.requestId (both v1 and v2)
75
+ const requestContext = evt.requestContext;
76
+ if (requestContext?.requestId && typeof requestContext.requestId === "string") {
77
+ return requestContext.requestId;
78
+ }
79
+ return undefined;
80
+ }
81
+ /**
82
+ * Extracts HTTP method from event
83
+ */
84
+ export function getHttpMethod(event) {
85
+ if (!event || typeof event !== "object") {
86
+ return undefined;
87
+ }
88
+ const evt = event;
89
+ // API Gateway v2
90
+ const requestContext = evt.requestContext;
91
+ if (requestContext?.http && typeof requestContext.http === "object") {
92
+ const http = requestContext.http;
93
+ if (typeof http.method === "string") {
94
+ return http.method;
95
+ }
96
+ }
97
+ // API Gateway v1
98
+ if (typeof evt.httpMethod === "string") {
99
+ return evt.httpMethod;
100
+ }
101
+ return undefined;
102
+ }
@@ -4,6 +4,7 @@ declare module "../bus.js" {
4
4
  "function.ack": {
5
5
  workerID: string;
6
6
  functionID: string;
7
+ requestID: string;
7
8
  };
8
9
  "function.invoked": {
9
10
  workerID: string;
@@ -1,6 +1,9 @@
1
+ import { Events } from "../bus.js";
1
2
  export declare const useRuntimeServerConfig: () => Promise<{
2
3
  API_VERSION: string;
3
4
  port: number;
4
5
  url: string;
5
6
  }>;
6
- export declare const useRuntimeServer: () => Promise<void>;
7
+ export declare const useRuntimeServer: () => Promise<{
8
+ routeInvocation: (targetWorkerID: string, invocation: Events["function.invoked"]) => void;
9
+ }>;