@intelligems/sst 2.49.6-ig.2 → 2.49.6-ig.4

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/iot.js CHANGED
@@ -149,15 +149,12 @@ export const useIOT = lazy(async () => {
149
149
  properties,
150
150
  sourceID: bus.sourceID,
151
151
  };
152
- for (const fragment of await encode(payload)) {
153
- await new Promise((r) => {
154
- device.publish(topic, JSON.stringify(fragment), {
155
- qos: 1,
156
- }, () => {
157
- r();
158
- });
152
+ const fragments = await encode(payload);
153
+ await Promise.all(fragments.map((fragment) => new Promise((r) => {
154
+ device.publish(topic, JSON.stringify(fragment), { qos: 1 }, () => {
155
+ r();
159
156
  });
160
- }
157
+ })));
161
158
  Logger.debug("IOT Published", topic, type);
162
159
  },
163
160
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "sideEffects": false,
3
3
  "name": "@intelligems/sst",
4
- "version": "2.49.6-ig.2",
4
+ "version": "2.49.6-ig.4",
5
5
  "bin": {
6
6
  "sst": "cli/sst.js"
7
7
  },
package/package.json.bak CHANGED
@@ -5,7 +5,7 @@
5
5
  },
6
6
  "sideEffects": false,
7
7
  "name": "@intelligems/sst",
8
- "version": "2.49.6-ig.2",
8
+ "version": "2.49.6-ig.4",
9
9
  "bin": {
10
10
  "sst": "cli/sst.js"
11
11
  },
@@ -34,27 +34,25 @@ export const useNodeHandler = () => {
34
34
  canHandle: (input) => input.startsWith("nodejs"),
35
35
  startWorker: async (input) => {
36
36
  const workers = await useRuntimeWorkers();
37
- new Promise(async () => {
38
- const worker = new Worker(url.fileURLToPath(new URL("../../support/nodejs-runtime/index.mjs", import.meta.url)), {
39
- env: {
40
- ...input.environment,
41
- IS_LOCAL: "true",
42
- },
43
- execArgv: ["--enable-source-maps"],
44
- workerData: input,
45
- stderr: true,
46
- stdin: true,
47
- stdout: true,
48
- });
49
- worker.stdout.on("data", (data) => {
50
- workers.stdout(input.workerID, data.toString());
51
- });
52
- worker.stderr.on("data", (data) => {
53
- workers.stdout(input.workerID, data.toString());
54
- });
55
- worker.on("exit", () => workers.exited(input.workerID));
56
- threads.set(input.workerID, worker);
37
+ const worker = new Worker(url.fileURLToPath(new URL("../../support/nodejs-runtime/index.mjs", import.meta.url)), {
38
+ env: {
39
+ ...input.environment,
40
+ IS_LOCAL: "true",
41
+ },
42
+ execArgv: ["--enable-source-maps"],
43
+ workerData: input,
44
+ stderr: true,
45
+ stdin: true,
46
+ stdout: true,
57
47
  });
48
+ worker.stdout.on("data", (data) => {
49
+ workers.stdout(input.workerID, data.toString());
50
+ });
51
+ worker.stderr.on("data", (data) => {
52
+ workers.stdout(input.workerID, data.toString());
53
+ });
54
+ worker.on("exit", () => workers.exited(input.workerID));
55
+ threads.set(input.workerID, worker);
58
56
  },
59
57
  stopWorker: async (workerID) => {
60
58
  const worker = threads.get(workerID);
@@ -65,26 +63,37 @@ export const useNodeHandler = () => {
65
63
  const monoBundleDir = path.join(project.paths.root, ".mono-build");
66
64
  const monoBundlePath = path.join(monoBundleDir, "index.mjs");
67
65
  const monoBundleExists = fsSync.existsSync(monoBundlePath);
68
- console.log(`[MONO-BUNDLE] mode=${input.mode}, exists=${monoBundleExists}, handler=${input.props.handler}`);
69
66
  if (input.mode === "start" && monoBundleExists) {
70
- console.log(`[MONO-BUNDLE] Using mono-bundle for: ${input.props.handler}`);
71
- Logger.debug("Using mono-bundle for dev mode:", monoBundlePath);
67
+ Colors.line(Colors.prefix, Colors.dim.bold("MonoBundle"), Colors.dim(`mode=${input.mode}, exists=${monoBundleExists}, handler=${input.props.handler}`));
72
68
  // Symlink node_modules to mono-bundle dir for external dependencies
73
- // Use the same approach as individual builds: find nearest package.json above the handler
74
- // handler-functions.ts lives in web/node-backend/src/, so start from there
75
- const handlerFunctionsDir = path.join(project.paths.root, "web/node-backend/src");
69
+ // Only create if symlink doesn't exist or points to wrong location (avoid redundant I/O)
70
+ const parsed = path.parse(input.props.handler);
71
+ const handlerFunctionsDir = path.join(project.paths.root, parsed.dir);
76
72
  const root = await findAbove(handlerFunctionsDir, "package.json");
77
73
  if (root) {
78
- const sourceNodeModules = path.join(root, "node_modules");
74
+ const sourceNodeModules = path.resolve(root, "node_modules");
79
75
  const monoBundleNodeModules = path.join(monoBundleDir, "node_modules");
80
76
  try {
81
- // Remove existing node_modules in mono-build (might be stale from deploy build)
82
- await fs.rm(monoBundleNodeModules, { recursive: true, force: true });
83
- await fs.symlink(path.resolve(sourceNodeModules), monoBundleNodeModules, "dir");
84
- Logger.debug("Symlinked node_modules for mono-bundle from:", sourceNodeModules);
77
+ const existingTarget = await fs.readlink(monoBundleNodeModules);
78
+ if (existingTarget === sourceNodeModules) {
79
+ // Symlink already correct, skip
80
+ }
81
+ else {
82
+ // Symlink points to wrong location, recreate
83
+ await fs.rm(monoBundleNodeModules, { recursive: true, force: true });
84
+ await fs.symlink(sourceNodeModules, monoBundleNodeModules, "dir");
85
+ Logger.debug("Symlinked node_modules for mono-bundle from:", sourceNodeModules);
86
+ }
85
87
  }
86
- catch (err) {
87
- Logger.debug("Failed to symlink node_modules for mono-bundle:", err);
88
+ catch {
89
+ // Symlink doesn't exist, create it
90
+ try {
91
+ await fs.symlink(sourceNodeModules, monoBundleNodeModules, "dir");
92
+ Logger.debug("Symlinked node_modules for mono-bundle from:", sourceNodeModules);
93
+ }
94
+ catch (err) {
95
+ Logger.debug("Failed to symlink node_modules for mono-bundle:", err);
96
+ }
88
97
  }
89
98
  }
90
99
  return {
@@ -140,7 +149,8 @@ export const useNodeHandler = () => {
140
149
  try {
141
150
  await fs.symlink(path.resolve(dir), path.resolve(path.join(input.out, "node_modules")), "dir");
142
151
  }
143
- catch { }
152
+ catch {
153
+ }
144
154
  }
145
155
  // Rebuilt using existing esbuild context
146
156
  let ctx = rebuildCache[input.functionID]?.ctx;
@@ -57,9 +57,9 @@ export const useRuntimeHandlers = lazy(() => {
57
57
  props: func,
58
58
  });
59
59
  // If mono-bundle detected (handler returned custom out), skip all artifact work
60
+ // Don't fire build events - mono-bundle is built externally by esbuild watch
61
+ // Worker pool invalidation is handled separately when bundle file actually changes
60
62
  if (monoBundleCheck.type === "success" && monoBundleCheck.out) {
61
- bus.publish("function.build.started", { functionID });
62
- bus.publish("function.build.success", { functionID, monoBundle: true });
63
63
  return {
64
64
  type: "success",
65
65
  handler: monoBundleCheck.handler,
@@ -138,7 +138,7 @@ export const useRuntimeHandlers = lazy(() => {
138
138
  export const useFunctionBuilder = lazy(() => {
139
139
  const artifacts = new Map();
140
140
  const handlers = useRuntimeHandlers();
141
- const semaphore = new Semaphore(4);
141
+ const semaphore = new Semaphore(parseInt(process.env.SST_BUILD_CONCURRENCY || "4", 10));
142
142
  const result = {
143
143
  artifact: (functionID) => {
144
144
  if (artifacts.has(functionID))
@@ -166,6 +166,11 @@ export const useFunctionBuilder = lazy(() => {
166
166
  try {
167
167
  const functions = useFunctions();
168
168
  for (const [functionID, info] of Object.entries(functions.all)) {
169
+ // Optimization: For mono-build, the artifact path is stable and build is handled externally.
170
+ // We can skip the potentially expensive shouldBuild check and rebuild call.
171
+ const existing = artifacts.get(functionID);
172
+ if (existing?.out.includes(".mono-build"))
173
+ continue;
169
174
  const handler = handlers.for(info.runtime);
170
175
  if (!handler?.shouldBuild({
171
176
  functionID,
@@ -176,7 +181,8 @@ export const useFunctionBuilder = lazy(() => {
176
181
  Logger.debug("Rebuilt function", functionID);
177
182
  }
178
183
  }
179
- catch { }
184
+ catch {
185
+ }
180
186
  });
181
187
  return result;
182
188
  });
package/runtime/iot.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { useBus } from "../bus.js";
2
2
  import { useIOT } from "../iot.js";
3
3
  import { lazy } from "../util/lazy.js";
4
+ import { logInvokeTrace } from "./worker-pool-logging.js";
4
5
  export const useIOTBridge = lazy(async () => {
5
6
  const bus = useBus();
6
7
  const iot = await useIOT();
@@ -12,6 +13,9 @@ export const useIOTBridge = lazy(async () => {
12
13
  iot.publish(topic + "/" + evt.properties.workerID, "function.error", evt.properties);
13
14
  });
14
15
  bus.subscribe("function.ack", async (evt) => {
15
- iot.publish(topic + "/" + evt.properties.workerID, "function.ack", evt.properties);
16
+ const workerID = evt.properties.workerID;
17
+ logInvokeTrace("IOT_ACK_START", workerID, `worker=${workerID.slice(0, 8)}`);
18
+ await iot.publish(topic + "/" + workerID, "function.ack", evt.properties);
19
+ logInvokeTrace("IOT_ACK_DONE", workerID, `worker=${workerID.slice(0, 8)}`);
16
20
  });
17
21
  });
@@ -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
+ }>;
package/runtime/server.js CHANGED
@@ -31,38 +31,52 @@ export const useRuntimeServer = lazy(async () => {
31
31
  workersWaiting.set(workerID, resolve);
32
32
  });
33
33
  }
34
- workers.subscribe("worker.exited", async (evt) => {
35
- const waiting = workersWaiting.get(evt.properties.workerID);
36
- if (!waiting)
37
- return;
38
- workersWaiting.delete(evt.properties.workerID);
39
- });
40
- bus.subscribe("function.invoked", async (evt) => {
41
- const worker = workersWaiting.get(evt.properties.workerID);
42
- if (worker) {
43
- workersWaiting.delete(evt.properties.workerID);
44
- worker(evt.properties);
34
+ // Route an invocation to a specific workerID (used by workers.ts for pooled workers)
35
+ function routeInvocation(targetWorkerID, invocation) {
36
+ const waiting = workersWaiting.get(targetWorkerID);
37
+ if (waiting) {
38
+ workersWaiting.delete(targetWorkerID);
39
+ waiting(invocation);
45
40
  return;
46
41
  }
47
- let arr = invocationsQueued.get(evt.properties.workerID);
42
+ let arr = invocationsQueued.get(targetWorkerID);
48
43
  if (!arr) {
49
44
  arr = [];
50
- invocationsQueued.set(evt.properties.workerID, arr);
45
+ invocationsQueued.set(targetWorkerID, arr);
51
46
  }
52
- arr.push(evt.properties);
47
+ arr.push(invocation);
48
+ }
49
+ workers.subscribe("worker.exited", async (evt) => {
50
+ const waiting = workersWaiting.get(evt.properties.workerID);
51
+ if (!waiting)
52
+ return;
53
+ workersWaiting.delete(evt.properties.workerID);
53
54
  });
55
+ // Note: function.invoked routing is handled by workers.ts via routeInvocation()
56
+ // This ensures correct routing for both pooled and non-pooled workers
54
57
  app.post(`/:workerID/${cfg.API_VERSION}/runtime/init/error`, express.json({
55
58
  strict: false,
56
59
  type: ["application/json", "application/*+json"],
57
60
  limit: "10mb",
58
61
  }), async (req, res) => {
59
- const worker = workers.fromID(req.params.workerID);
62
+ const pooledWorkerID = req.params.workerID;
63
+ const worker = workers.fromID(pooledWorkerID);
64
+ if (!worker) {
65
+ res.status(404).send();
66
+ return;
67
+ }
68
+ // Get AWS workerID for IoT routing (if pooled)
69
+ const awsWorkerID = workers.isPooled(pooledWorkerID)
70
+ ? workers.getAwsWorkerID(pooledWorkerID) || pooledWorkerID
71
+ : pooledWorkerID;
60
72
  bus.publish("function.error", {
61
- requestID: workers.getCurrentRequestID(worker.workerID),
62
- workerID: worker.workerID,
73
+ requestID: workers.getCurrentRequestID(pooledWorkerID),
74
+ workerID: awsWorkerID,
63
75
  functionID: worker.functionID,
64
76
  ...req.body,
65
77
  });
78
+ // Return pooled worker to pool
79
+ workers.onResponse(pooledWorkerID);
66
80
  res.json("ok");
67
81
  });
68
82
  app.get(`/:workerID/${cfg.API_VERSION}/runtime/invocation/next`, async (req, res) => {
@@ -77,8 +91,15 @@ export const useRuntimeServer = lazy(async () => {
77
91
  "Lambda-Runtime-Cognito-Identity": JSON.stringify(payload.context.identity || null),
78
92
  "Lambda-Runtime-Log-Group-Name": payload.context.logGroupName,
79
93
  "Lambda-Runtime-Log-Stream-Name": payload.context.logStreamName,
94
+ // Pass function ID for mono-build shared pool: allows per-invocation dispatch
95
+ "Lambda-Runtime-Sst-Function-Id": payload.functionID,
96
+ });
97
+ // Wrap event with env for per-invocation environment variable application
98
+ // This prevents env leakage when workers are reused across different functions
99
+ res.json({
100
+ event: payload.event,
101
+ env: payload.env,
80
102
  });
81
- res.json(payload.event);
82
103
  });
83
104
  app.post(`/:workerID/${cfg.API_VERSION}/runtime/invocation/:awsRequestId/response`, express.json({
84
105
  strict: false,
@@ -87,14 +108,25 @@ export const useRuntimeServer = lazy(async () => {
87
108
  },
88
109
  limit: "10mb",
89
110
  }), (req, res) => {
90
- Logger.debug("Worker", req.params.workerID, "got response", req.body);
91
- const worker = workers.fromID(req.params.workerID);
111
+ const pooledWorkerID = req.params.workerID;
112
+ Logger.debug("Worker", pooledWorkerID, "got response", req.body);
113
+ const worker = workers.fromID(pooledWorkerID);
114
+ if (!worker) {
115
+ res.status(404).send();
116
+ return;
117
+ }
118
+ // Get AWS workerID for IoT routing (if pooled)
119
+ const awsWorkerID = workers.isPooled(pooledWorkerID)
120
+ ? workers.getAwsWorkerID(pooledWorkerID) || pooledWorkerID
121
+ : pooledWorkerID;
92
122
  bus.publish("function.success", {
93
- workerID: worker.workerID,
123
+ workerID: awsWorkerID,
94
124
  functionID: worker.functionID,
95
125
  requestID: req.params.awsRequestId,
96
126
  body: req.body,
97
127
  });
128
+ // Return pooled worker to pool
129
+ workers.onResponse(pooledWorkerID);
98
130
  res.status(202).send();
99
131
  });
100
132
  app.all(`/proxy*`, express.raw({
@@ -135,16 +167,30 @@ export const useRuntimeServer = lazy(async () => {
135
167
  type: ["application/json", "application/*+json"],
136
168
  limit: "10mb",
137
169
  }), (req, res) => {
138
- const worker = workers.fromID(req.params.workerID);
170
+ const pooledWorkerID = req.params.workerID;
171
+ const worker = workers.fromID(pooledWorkerID);
172
+ if (!worker) {
173
+ res.status(404).send();
174
+ return;
175
+ }
176
+ // Get AWS workerID for IoT routing (if pooled)
177
+ const awsWorkerID = workers.isPooled(pooledWorkerID)
178
+ ? workers.getAwsWorkerID(pooledWorkerID) || pooledWorkerID
179
+ : pooledWorkerID;
139
180
  bus.publish("function.error", {
140
- workerID: worker.workerID,
181
+ workerID: awsWorkerID,
141
182
  functionID: worker.functionID,
142
183
  errorType: req.body.errorType,
143
184
  errorMessage: req.body.errorMessage,
144
185
  requestID: req.params.awsRequestId,
145
186
  trace: req.body.trace,
146
187
  });
188
+ // Return pooled worker to pool
189
+ workers.onResponse(pooledWorkerID);
147
190
  res.status(202).send();
148
191
  });
149
192
  app.listen(cfg.port);
193
+ return {
194
+ routeInvocation,
195
+ };
150
196
  });
@@ -0,0 +1,29 @@
1
+ export declare const POOL_SIZE: number;
2
+ export declare const IDLE_TIMEOUT: number;
3
+ /**
4
+ * Set the function name resolver callback
5
+ * Called by workers.ts to provide access to useFunctions()
6
+ */
7
+ export declare function setFunctionNameResolver(resolver: (functionID: string) => string): void;
8
+ /**
9
+ * Pool debug logging helper - writes to file with bottleneck detection
10
+ */
11
+ export declare function logPool(action: string, details?: Record<string, any>): void;
12
+ /**
13
+ * Track request lifecycle for metrics - called at request start
14
+ */
15
+ export declare function trackRequestStart(functionID: string, isPoolHit: boolean): void;
16
+ /**
17
+ * Track request lifecycle for metrics - called at request end
18
+ */
19
+ export declare function trackRequestEnd(functionID: string): void;
20
+ /**
21
+ * Write session end summary and close log stream
22
+ * Called from workers.ts on process exit
23
+ */
24
+ export declare function writeSessionEndSummary(): void;
25
+ /**
26
+ * Log invocation trace events to .sst/invoke-trace.log
27
+ * Always enabled for debugging invocation flow
28
+ */
29
+ export declare function logInvokeTrace(stage: string, requestID: string, details?: string): void;
@@ -0,0 +1,190 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ // Configuration
4
+ const SST_BUILD_CONCURRENCY = parseInt(process.env.SST_BUILD_CONCURRENCY || "4", 10);
5
+ export const POOL_SIZE = parseInt(process.env.SST_WORKER_POOL_SIZE || "10", 10);
6
+ export const IDLE_TIMEOUT = parseInt(process.env.SST_WORKER_IDLE_TIMEOUT || "60000", 10);
7
+ const DEBUG_POOL = process.env.SST_DEBUG_POOL === "true";
8
+ const DEBUG_POOL_FILE = process.env.SST_DEBUG_POOL_FILE || ".sst/worker-pool.log";
9
+ const INVOKE_TRACE_FILE = ".sst/invoke-trace.log";
10
+ const metrics = {
11
+ coldStarts: new Map(),
12
+ concurrentRequests: new Map(),
13
+ peakConcurrent: new Map(),
14
+ totalRequests: new Map(),
15
+ poolHits: new Map(),
16
+ avgResponseTime: new Map(),
17
+ responseCount: new Map(),
18
+ };
19
+ let logStream = null;
20
+ // Function name resolver - set by workers.ts
21
+ let functionNameResolver = (id) => id.slice(0, 25);
22
+ /**
23
+ * Set the function name resolver callback
24
+ * Called by workers.ts to provide access to useFunctions()
25
+ */
26
+ export function setFunctionNameResolver(resolver) {
27
+ functionNameResolver = resolver;
28
+ }
29
+ function initLogFile() {
30
+ if (!DEBUG_POOL || logStream)
31
+ return;
32
+ try {
33
+ const logDir = path.dirname(DEBUG_POOL_FILE);
34
+ if (!fs.existsSync(logDir)) {
35
+ fs.mkdirSync(logDir, { recursive: true });
36
+ }
37
+ logStream = fs.createWriteStream(DEBUG_POOL_FILE, { flags: "w" });
38
+ logStream.write("\n" + "=".repeat(100) + "\n" +
39
+ "[SESSION START] " + new Date().toISOString() + " | POOL_SIZE=" + POOL_SIZE + " IDLE_TIMEOUT=" + IDLE_TIMEOUT + "ms BUILD_CONCURRENCY=" + SST_BUILD_CONCURRENCY + "\n" +
40
+ "=".repeat(100) + "\n");
41
+ }
42
+ catch {
43
+ // Fall back to no logging if file creation fails
44
+ }
45
+ }
46
+ // Extract readable function name from handler path
47
+ function getFunctionName(functionID) {
48
+ return functionNameResolver(functionID);
49
+ }
50
+ // Calculate bottleneck indicators
51
+ function getBottleneckFlags(functionID) {
52
+ const flags = [];
53
+ const concurrent = metrics.concurrentRequests.get(functionID) || 0;
54
+ const total = metrics.totalRequests.get(functionID) || 0;
55
+ const hits = metrics.poolHits.get(functionID) || 0;
56
+ const hitRate = total > 0 ? (hits / total) * 100 : 0;
57
+ const coldStarts = metrics.coldStarts.get(functionID) || 0;
58
+ // SATURATED: All pool slots in use
59
+ if (concurrent >= POOL_SIZE) {
60
+ flags.push("🚨 SATURATED");
61
+ }
62
+ // HIGH_LOAD: >70% pool utilization
63
+ else if (concurrent >= POOL_SIZE * 0.7) {
64
+ flags.push("⚠️ HIGH_LOAD");
65
+ }
66
+ // COLD_START: Low hit rate indicates frequent cold starts
67
+ if (total >= 5 && hitRate < 30) {
68
+ flags.push("❄️ LOW_REUSE");
69
+ }
70
+ // BOTTLENECK: High cold start ratio
71
+ if (total >= 3 && coldStarts / total > 0.5) {
72
+ flags.push("🔥 COLD_HEAVY");
73
+ }
74
+ return flags.length > 0 ? " " + flags.join(" ") : "";
75
+ }
76
+ /**
77
+ * Pool debug logging helper - writes to file with bottleneck detection
78
+ */
79
+ export function logPool(action, details = {}) {
80
+ if (!DEBUG_POOL)
81
+ return;
82
+ initLogFile();
83
+ const timestamp = new Date().toISOString().slice(11, 23); // HH:MM:SS.mmm
84
+ const funcName = details.functionID
85
+ ? getFunctionName(details.functionID)
86
+ : "";
87
+ const bottleneckFlags = details.functionID &&
88
+ ["CREATE", "REUSE", "POOL_MISS", "RESPONSE"].includes(action)
89
+ ? getBottleneckFlags(details.functionID)
90
+ : "";
91
+ // Build metrics string for key actions
92
+ let metricsStr = "";
93
+ if (details.functionID && ["RESPONSE", "RETURN_TO_POOL"].includes(action)) {
94
+ const concurrent = metrics.concurrentRequests.get(details.functionID) || 0;
95
+ const total = metrics.totalRequests.get(details.functionID) || 0;
96
+ const hits = metrics.poolHits.get(details.functionID) || 0;
97
+ const hitRate = total > 0 ? ((hits / total) * 100).toFixed(0) : "0";
98
+ metricsStr = " | concurrent=" + concurrent + " hitRate=" + hitRate + "%";
99
+ }
100
+ // Format details, replacing functionID with funcName
101
+ const filteredDetails = { ...details };
102
+ delete filteredDetails.functionID;
103
+ const detailStr = Object.entries(filteredDetails)
104
+ .map(([k, v]) => k + "=" + (typeof v === "object" ? JSON.stringify(v) : v))
105
+ .join(" ");
106
+ const actionPadded = action.padEnd(14);
107
+ const funcPadded = funcName.padEnd(25);
108
+ const line = "[" + timestamp + "] " + actionPadded + " " + funcPadded + " " + detailStr + metricsStr + bottleneckFlags + "\n";
109
+ if (logStream) {
110
+ logStream.write(line);
111
+ }
112
+ }
113
+ /**
114
+ * Track request lifecycle for metrics - called at request start
115
+ */
116
+ export function trackRequestStart(functionID, isPoolHit) {
117
+ const current = (metrics.concurrentRequests.get(functionID) || 0) + 1;
118
+ metrics.concurrentRequests.set(functionID, current);
119
+ metrics.totalRequests.set(functionID, (metrics.totalRequests.get(functionID) || 0) + 1);
120
+ const peak = metrics.peakConcurrent.get(functionID) || 0;
121
+ if (current > peak) {
122
+ metrics.peakConcurrent.set(functionID, current);
123
+ }
124
+ if (isPoolHit) {
125
+ metrics.poolHits.set(functionID, (metrics.poolHits.get(functionID) || 0) + 1);
126
+ }
127
+ else {
128
+ metrics.coldStarts.set(functionID, (metrics.coldStarts.get(functionID) || 0) + 1);
129
+ }
130
+ }
131
+ /**
132
+ * Track request lifecycle for metrics - called at request end
133
+ */
134
+ export function trackRequestEnd(functionID) {
135
+ const current = metrics.concurrentRequests.get(functionID) || 0;
136
+ if (current > 0) {
137
+ metrics.concurrentRequests.set(functionID, current - 1);
138
+ }
139
+ }
140
+ /**
141
+ * Write session end summary and close log stream
142
+ * Called from workers.ts on process exit
143
+ */
144
+ export function writeSessionEndSummary() {
145
+ if (!DEBUG_POOL || !logStream)
146
+ return;
147
+ let summary = "\n" + "─".repeat(100) + "\n[SESSION END] " + new Date().toISOString() + "\n";
148
+ for (const [funcID, total] of metrics.totalRequests) {
149
+ const hits = metrics.poolHits.get(funcID) || 0;
150
+ const cold = metrics.coldStarts.get(funcID) || 0;
151
+ const peak = metrics.peakConcurrent.get(funcID) || 0;
152
+ const hitRate = total > 0 ? ((hits / total) * 100).toFixed(1) : "0";
153
+ const funcName = getFunctionName(funcID);
154
+ summary += " " + funcName.padEnd(25) + " total=" + total + " poolHits=" + hits + " coldStarts=" + cold + " peakConcurrent=" + peak + " hitRate=" + hitRate + "%\n";
155
+ }
156
+ summary += "─".repeat(100) + "\n";
157
+ logStream.write(summary);
158
+ logStream.end();
159
+ }
160
+ // Invocation trace logging - always enabled, writes to separate file
161
+ let traceStream = null;
162
+ function initTraceFile() {
163
+ if (traceStream)
164
+ return;
165
+ try {
166
+ const logDir = path.dirname(INVOKE_TRACE_FILE);
167
+ if (!fs.existsSync(logDir)) {
168
+ fs.mkdirSync(logDir, { recursive: true });
169
+ }
170
+ traceStream = fs.createWriteStream(INVOKE_TRACE_FILE, { flags: "w" });
171
+ traceStream.write("\n" + "=".repeat(80) + "\n" +
172
+ "[TRACE START] " + new Date().toISOString() + "\n" +
173
+ "=".repeat(80) + "\n");
174
+ }
175
+ catch {
176
+ // Fall back to no logging if file creation fails
177
+ }
178
+ }
179
+ /**
180
+ * Log invocation trace events to .sst/invoke-trace.log
181
+ * Always enabled for debugging invocation flow
182
+ */
183
+ export function logInvokeTrace(stage, requestID, details) {
184
+ initTraceFile();
185
+ if (!traceStream)
186
+ return;
187
+ const timestamp = new Date().toISOString().slice(11, 23); // HH:MM:SS.mmm
188
+ const line = `[${timestamp}] ${stage.padEnd(20)} req=${requestID.slice(0, 8)} ${details || ""}\n`;
189
+ traceStream.write(line);
190
+ }
@@ -18,20 +18,26 @@ declare module "../bus.js" {
18
18
  requestID: string;
19
19
  message: string;
20
20
  };
21
+ "worker.reused": {
22
+ workerID: string;
23
+ functionID: string;
24
+ pooledWorkerID: string;
25
+ };
21
26
  }
22
27
  }
23
- interface Worker {
24
- workerID: string;
25
- functionID: string;
26
- }
27
28
  export declare const useRuntimeWorkers: () => Promise<{
28
- fromID(workerID: string): Worker;
29
+ fromID(workerID: string): {
30
+ workerID: string;
31
+ functionID: string;
32
+ };
29
33
  getCurrentRequestID(workerID: string): string | undefined;
30
34
  stdout(workerID: string, message: string): void;
31
35
  exited(workerID: string): void;
32
- subscribe: <Type extends "worker.started" | "worker.stopped" | "worker.exited" | "worker.stdout">(type: Type, cb: (payload: import("../bus.js").EventPayload<Type>) => void) => {
36
+ onResponse(pooledWorkerID: string): void;
37
+ getAwsWorkerID(pooledWorkerID: string): string | undefined;
38
+ isPooled(workerID: string): boolean;
39
+ subscribe: <Type extends "worker.started" | "worker.stopped" | "worker.exited" | "worker.stdout" | "worker.reused">(type: Type, cb: (payload: import("../bus.js").EventPayload<Type>) => void) => {
33
40
  type: keyof import("../bus.js").Events;
34
41
  cb: (payload: any) => void;
35
42
  };
36
43
  }>;
37
- export {};