@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.
@@ -36,6 +36,13 @@ export const dev = (program) => program.command(["dev", "start"], "Work on your
36
36
  const colors = ["#01cdfe", "#ff71ce", "#05ffa1", "#b967ff"];
37
37
  let index = 0;
38
38
  const pending = new Map();
39
+ // Track warmup request IDs to suppress their logs
40
+ const warmupRequestIDs = new Set();
41
+ // Helper to check if an event payload is a warmup request
42
+ function isWarmupEvent(event) {
43
+ return event && typeof event === 'object' &&
44
+ ('ding' in event || 'warmer' in event || event.__sst_warmup === true);
45
+ }
39
46
  function prefix(requestID) {
40
47
  const exists = pending.get(requestID);
41
48
  if (exists) {
@@ -53,14 +60,54 @@ export const dev = (program) => program.command(["dev", "start"], "Work on your
53
60
  // index--;
54
61
  // if (index < 0) index = colors.length - 1;
55
62
  pending.delete(requestID);
63
+ warmupRequestIDs.delete(requestID);
56
64
  }
65
+ // Warmup progress bar state
66
+ let warmupSpinner = null;
67
+ let warmupTotal = 0;
68
+ let warmupCompleted = 0;
69
+ bus.subscribe("warmup.start", async (evt) => {
70
+ warmupTotal = evt.properties.count;
71
+ warmupCompleted = 0;
72
+ warmupSpinner = createSpinner({
73
+ color: "cyan",
74
+ text: Colors.dim(` Warming up workers... 0/${warmupTotal}`),
75
+ }).start();
76
+ });
77
+ bus.subscribe("warmup.progress", async (evt) => {
78
+ warmupCompleted = evt.properties.completed;
79
+ if (warmupSpinner) {
80
+ warmupSpinner.text = Colors.dim(` Warming up workers... ${warmupCompleted}/${warmupTotal}`);
81
+ }
82
+ });
83
+ bus.subscribe("warmup.complete", async (evt) => {
84
+ if (warmupSpinner) {
85
+ warmupSpinner.succeed(Colors.dim(` Warmed up ${evt.properties.success} workers in ${evt.properties.elapsedMs}ms`));
86
+ warmupSpinner = null;
87
+ }
88
+ });
57
89
  bus.subscribe("function.invoked", async (evt) => {
90
+ // Check if this is a warmup request and track it
91
+ if (isWarmupEvent(evt.properties.event)) {
92
+ warmupRequestIDs.add(evt.properties.requestID);
93
+ return; // Don't log warmup invocations
94
+ }
58
95
  Colors.line(prefix(evt.properties.requestID), Colors.dim.bold("Invoked"), Colors.dim(useFunctions().fromID(evt.properties.functionID)?.handler));
59
96
  });
60
97
  bus.subscribe("worker.stdout", async (evt) => {
98
+ if (evt.properties.message.includes('[LOG]')) {
99
+ console.log(evt.properties.message);
100
+ return;
101
+ }
102
+ // Skip warmup request logs
103
+ if (warmupRequestIDs.has(evt.properties.requestID))
104
+ return;
61
105
  const info = useFunctions().fromID(evt.properties.functionID);
62
106
  prefix(evt.properties.requestID);
63
- const { started } = pending.get(evt.properties.requestID);
107
+ const pendingReq = pending.get(evt.properties.requestID);
108
+ if (!pendingReq)
109
+ return; // Safety check
110
+ const { started } = pendingReq;
64
111
  for (let line of evt.properties.message.split("\n")) {
65
112
  // Remove prefix from container logs
66
113
  if (info?.runtime === "container") {
@@ -92,9 +139,10 @@ export const dev = (program) => program.command(["dev", "start"], "Work on your
92
139
  return;
93
140
  if (info.enableLiveDev === false)
94
141
  return;
142
+ const buildType = evt.properties.monoBundle ? "(mono-bundle)" : "(individual)";
95
143
  Colors.line(info.runtime === "container"
96
- ? Colors.dim(Colors.prefix, "Built", info.handler, "container")
97
- : Colors.dim(Colors.prefix, "Built", info.handler));
144
+ ? Colors.dim(Colors.prefix, "Built", info.handler, "container", buildType)
145
+ : Colors.dim(Colors.prefix, "Built", info.handler, buildType));
98
146
  });
99
147
  bus.subscribe("function.build.failed", async (evt) => {
100
148
  const info = useFunctions().fromID(evt.properties.functionID);
@@ -110,15 +158,27 @@ export const dev = (program) => program.command(["dev", "start"], "Work on your
110
158
  Colors.gap();
111
159
  });
112
160
  bus.subscribe("function.success", async (evt) => {
161
+ // Skip warmup request logs
162
+ if (warmupRequestIDs.has(evt.properties.requestID)) {
163
+ end(evt.properties.requestID);
164
+ return;
165
+ }
113
166
  // stdout logs sometimes come in after
114
167
  const p = prefix(evt.properties.requestID);
115
168
  const req = pending.get(evt.properties.requestID);
169
+ if (!req)
170
+ return; // Safety check
116
171
  setTimeout(() => {
117
172
  Colors.line(p, Colors.dim(`Done in ${Date.now() - req.started - 100}ms`));
118
173
  end(evt.properties.requestID);
119
174
  }, 100);
120
175
  });
121
176
  bus.subscribe("function.error", async (evt) => {
177
+ // Skip warmup request logs
178
+ if (warmupRequestIDs.has(evt.properties.requestID)) {
179
+ end(evt.properties.requestID);
180
+ return;
181
+ }
122
182
  setTimeout(() => {
123
183
  Colors.line(prefix(evt.properties.requestID), Colors.danger.bold("Error:"), Colors.danger.bold(evt.properties.errorMessage));
124
184
  for (const line of evt.properties.trace || []) {
@@ -341,6 +401,12 @@ export const dev = (program) => program.command(["dev", "start"], "Work on your
341
401
  import("./plugins/warmer.js").then((mod) => mod.useRDSWarmer()),
342
402
  useFunctionLogger(),
343
403
  ]);
404
+ // Trigger warmup by invoking Lambda functions with warmup payloads
405
+ // This creates workers through the real request flow
406
+ import("../../runtime/workers.js").then(async (mod) => {
407
+ const workers = await mod.useRuntimeWorkers();
408
+ await workers.triggerWarmup(30);
409
+ });
344
410
  }
345
411
  catch (e) {
346
412
  await exitWithError(e);
package/cli/sst.js CHANGED
@@ -40,7 +40,6 @@ version(program);
40
40
  telemetry(program);
41
41
  types(program);
42
42
  if ("setSourceMapsEnabled" in process) {
43
- // @ts-expect-error
44
43
  process.setSourceMapsEnabled(true);
45
44
  }
46
45
  process.removeAllListeners("uncaughtException");
@@ -24,6 +24,7 @@ declare const supportedRuntimes: {
24
24
  "python3.10": CDKRuntime;
25
25
  "python3.11": CDKRuntime;
26
26
  "python3.12": CDKRuntime;
27
+ "python3.13": CDKRuntime;
27
28
  "dotnetcore3.1": CDKRuntime;
28
29
  dotnet6: CDKRuntime;
29
30
  dotnet8: CDKRuntime;
@@ -798,7 +799,7 @@ export declare class Function extends CDKFunction implements SSTConstruct {
798
799
  type: "Function";
799
800
  data: {
800
801
  arn: string;
801
- runtime: "container" | "rust" | "nodejs16.x" | "nodejs18.x" | "nodejs20.x" | "nodejs22.x" | "python3.7" | "python3.8" | "python3.9" | "python3.10" | "python3.11" | "python3.12" | "dotnetcore3.1" | "dotnet6" | "dotnet8" | "java8" | "java11" | "java17" | "java21" | "go1.x" | "go" | undefined;
802
+ runtime: "container" | "rust" | "nodejs16.x" | "nodejs18.x" | "nodejs20.x" | "nodejs22.x" | "python3.7" | "python3.8" | "python3.9" | "python3.10" | "python3.11" | "python3.12" | "python3.13" | "dotnetcore3.1" | "dotnet6" | "dotnet8" | "java8" | "java11" | "java17" | "java21" | "go1.x" | "go" | undefined;
802
803
  handler: string | undefined;
803
804
  missingSourcemap: boolean | undefined;
804
805
  localId: string;
@@ -43,6 +43,7 @@ const supportedRuntimes = {
43
43
  "python3.10": CDKRuntime.PYTHON_3_10,
44
44
  "python3.11": CDKRuntime.PYTHON_3_11,
45
45
  "python3.12": CDKRuntime.PYTHON_3_12,
46
+ "python3.13": CDKRuntime.PYTHON_3_13,
46
47
  "dotnetcore3.1": CDKRuntime.DOTNET_CORE_3_1,
47
48
  dotnet6: CDKRuntime.DOTNET_6,
48
49
  dotnet8: CDKRuntime.DOTNET_8,
@@ -216,18 +217,18 @@ export class Function extends CDKFunction {
216
217
  ? {
217
218
  code: Code.fromInline("export function placeholder() {}"),
218
219
  handler: "index.placeholder",
219
- runtime: CDKRuntime.NODEJS_20_X,
220
+ runtime: CDKRuntime.NODEJS_22_X,
220
221
  layers: undefined,
221
222
  }
222
223
  : props.code ? {
223
224
  code: props.code,
224
225
  handler: "index.handler",
225
- runtime: CDKRuntime.NODEJS_20_X,
226
+ runtime: CDKRuntime.NODEJS_22_X,
226
227
  layers: Function.buildLayers(scope, id, props),
227
228
  } : {
228
229
  code: Code.fromInline("export function placeholder() {}"),
229
230
  handler: "index.placeholder",
230
- runtime: CDKRuntime.NODEJS_20_X,
231
+ runtime: CDKRuntime.NODEJS_22_X,
231
232
  layers: Function.buildLayers(scope, id, props),
232
233
  }),
233
234
  architecture,
package/credentials.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import path from "path";
2
+ import { fileURLToPath, pathToFileURL } from "url";
2
3
  import { fromNodeProviderChain } from "@aws-sdk/credential-providers";
3
4
  import { GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts";
4
5
  import { Logger } from "./logger.js";
@@ -121,9 +122,9 @@ export function useAWSClient(client, force = false) {
121
122
  }
122
123
  export const useAWSProvider = lazy(async () => {
123
124
  const cdkToolkitUrl = await import.meta.resolve("@aws-cdk/toolkit-lib");
124
- const cdkToolkitPath = new URL(cdkToolkitUrl).pathname;
125
- const { SdkProvider } = await import(path.resolve(cdkToolkitPath, "..", "api", "aws-auth", "sdk-provider.js"));
126
- const { IoHelper } = await import(path.resolve(cdkToolkitPath, "..", "api", "io", "private", "io-helper.js"));
125
+ const cdkToolkitPath = fileURLToPath(cdkToolkitUrl);
126
+ const { SdkProvider } = await import(pathToFileURL(path.resolve(cdkToolkitPath, "..", "api", "aws-auth", "sdk-provider.js")).href);
127
+ const { IoHelper } = await import(pathToFileURL(path.resolve(cdkToolkitPath, "..", "api", "io", "private", "io-helper.js")).href);
127
128
  const project = useProject();
128
129
  return new SdkProvider(useAWSCredentialsProvider(), project.config.region, {
129
130
  ioHelper: IoHelper.fromActionAwareIoHost({
package/iot.d.ts CHANGED
@@ -4,4 +4,35 @@ export declare const useIOT: () => Promise<{
4
4
  prefix: string;
5
5
  publish<Type extends keyof Events>(topic: string, type: Type, properties: Events[Type]): Promise<void>;
6
6
  }>;
7
+ /**
8
+ * A second connection, carrying only small control messages.
9
+ *
10
+ * `useIOT` also carries response bodies. Those are fragmented into 50KB chunks
11
+ * and published at QoS 1, so every chunk waits on a broker round trip, and they
12
+ * queue on the same socket as everything else.
13
+ *
14
+ * `function.ack` cannot afford to wait behind them. The Lambda re-invokes if the
15
+ * ack does not arrive within two seconds, and each re-invocation re-runs the
16
+ * handler and puts another response body on the socket that is already busy —
17
+ * so a late ack makes the next ack later still. Measured on one machine before
18
+ * this change: ack publish went from 105ms to 10.2s inside a minute, publishes
19
+ * in flight from 15 to 439, and a single request executed fifteen times before
20
+ * the gateway returned 504 at 29s.
21
+ *
22
+ * Round-trip latency decides who sees it. A chunk costs a round trip to the
23
+ * region — roughly 90ms from Europe against 15ms from the US — so the same
24
+ * payload lands either side of the two second budget depending on where the
25
+ * developer sits.
26
+ *
27
+ * **The client id must differ from `useIOT`'s.** AWS IoT drops an existing
28
+ * connection when a new one presents the same id, so a shared id would make the
29
+ * two sockets disconnect each other in a loop.
30
+ *
31
+ * This connection deliberately does not subscribe. Inbound traffic still arrives
32
+ * on `useIOT`; this one only publishes.
33
+ */
34
+ export declare const useIOTControl: () => Promise<{
35
+ prefix: string;
36
+ publish<Type extends keyof Events>(topic: string, type: Type, properties: Events[Type]): Promise<void>;
37
+ }>;
7
38
  export declare const isSupported: () => boolean;
package/iot.js CHANGED
@@ -1,6 +1,11 @@
1
1
  import { IoTClient, DescribeEndpointCommand } from "@aws-sdk/client-iot";
2
2
  import { useAWSClient, useAWSCredentials } from "./credentials.js";
3
3
  import { VisibleError } from "./error.js";
4
+ import { lazy } from "./util/lazy.js";
5
+ import { Logger } from "./logger.js";
6
+ import { logIotRx } from "./runtime/debug-bridge-logging.js";
7
+ import { logEventTrace } from "./runtime/event-trace-logging.js";
8
+ import { getRequestPath, getCorrelationId, getApiGatewayRequestId, getHttpMethod } from "./runtime/request-utils.js";
4
9
  export const useIOTEndpoint = lazy(async () => {
5
10
  const iot = useAWSClient(IoTClient);
6
11
  Logger.debug("Getting IoT endpoint");
@@ -15,10 +20,61 @@ export const useIOTEndpoint = lazy(async () => {
15
20
  import iot from "aws-iot-device-sdk";
16
21
  import { useBus } from "./bus.js";
17
22
  import { useProject } from "./project.js";
18
- import { Logger } from "./logger.js";
19
23
  import { useBootstrap } from "./bootstrap.js";
20
24
  import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
21
- import { lazy } from "./util/lazy.js";
25
+ /**
26
+ * Re-sign the next reconnect with credentials that are still alive.
27
+ *
28
+ * The device is built with resolved credential *values*, because
29
+ * `aws-iot-device-sdk` takes strings rather than a provider. It keeps them in
30
+ * closure variables and re-signs the WebSocket URL from those on every
31
+ * reconnect, and nothing ever writes to them again except this call. So once
32
+ * the session expires, every retry presents a dead `sessionToken`, AWS IoT
33
+ * rejects the upgrade, and the socket closes again — forever.
34
+ *
35
+ * Measured on one machine: the connection dropped routinely five times in the
36
+ * first fifteen minutes and recovered each time; an hour later the credentials
37
+ * expired, and the next drop began **93 consecutive failed reconnects over five
38
+ * hours**, none of which emitted an `error` event. Nothing in the CLI said
39
+ * anything, and every request through the Live Lambda bridge 504'd.
40
+ *
41
+ * Every other AWS call in the process is unaffected, because `useAWSClient`
42
+ * passes the *provider* and the SDK calls it per request. This is the one path
43
+ * that holds values, so it is the one path that needs telling.
44
+ *
45
+ * **Why `close` and not `reconnect`.** The signing is synchronous and this
46
+ * refresh is not, so the two race. `emit("reconnect")` runs its handlers and
47
+ * returns — it does not await them — and `_setupStream()` signs on the next
48
+ * line, from whatever the variables hold at that instant. A refresh started
49
+ * there always loses. `close` fires one backoff interval earlier (the SDK
50
+ * manages its own, from `baseReconnectTimeMs` of 1s up to 128s, ignoring the
51
+ * `reconnectPeriod` passed in), which is ample for the resolve to land before
52
+ * the next attempt signs.
53
+ *
54
+ * So this wins the race by margin, not by construction. Two things bound it:
55
+ * `close` fires on **every** failed reconnect, not once per outage, so the
56
+ * values can never be more than one backoff interval stale; and the retry loop
57
+ * is unconditional. Making it airtight would mean keeping the stored
58
+ * credentials warm so the synchronous path can never read a stale one — a
59
+ * timer, which is machinery duplicating what the memoized provider already
60
+ * does. Measured recovery is five seconds, so that is not worth it yet.
61
+ *
62
+ * The provider only reaches the network within five minutes of expiry, so this
63
+ * is an in-memory read on most drops and one real refresh per session.
64
+ */
65
+ async function refreshWebSocketCredentials(device, label) {
66
+ try {
67
+ const fresh = await useAWSCredentials();
68
+ // The typings mark `expiration` as required, but the implementation only
69
+ // assigns the three credential strings and never reads it — so this passes
70
+ // what the provider gave rather than inventing a date to satisfy a
71
+ // parameter nothing uses.
72
+ device.updateWebSocketCredentials(fresh.accessKeyId, fresh.secretAccessKey, fresh.sessionToken, fresh.expiration);
73
+ }
74
+ catch (err) {
75
+ Logger.debug(`${label} credential refresh failed`, err);
76
+ }
77
+ }
22
78
  export const useIOT = lazy(async () => {
23
79
  const bus = useBus();
24
80
  const endpoint = await useIOTEndpoint();
@@ -113,6 +169,7 @@ export const useIOT = lazy(async () => {
113
169
  });
114
170
  device.on("close", () => {
115
171
  Logger.debug("IoT closed");
172
+ void refreshWebSocketCredentials(device, "IoT");
116
173
  });
117
174
  device.on("reconnect", () => {
118
175
  Logger.debug("IoT reconnecting...");
@@ -120,6 +177,19 @@ export const useIOT = lazy(async () => {
120
177
  device.on("message", (_topic, buffer) => {
121
178
  const fragment = JSON.parse(buffer.toString());
122
179
  if (!fragment.id) {
180
+ const requestID = fragment.properties?.requestID;
181
+ logIotRx(`Received ${fragment.type} reqId=${requestID?.slice(0, 8) || 'N/A'}`);
182
+ if (fragment.type === "function.invoked" && requestID) {
183
+ const event = fragment.properties?.event;
184
+ logEventTrace("IOT_RECEIVED", {
185
+ requestID,
186
+ functionID: fragment.properties?.functionID,
187
+ path: getRequestPath(event),
188
+ method: getHttpMethod(event),
189
+ correlationId: getCorrelationId(event),
190
+ apiGwReqId: getApiGatewayRequestId(event),
191
+ });
192
+ }
123
193
  bus.publish(fragment.type, fragment.properties);
124
194
  return;
125
195
  }
@@ -138,6 +208,20 @@ export const useIOT = lazy(async () => {
138
208
  const evt = JSON.parse(data);
139
209
  if (evt.sourceID === bus.sourceID)
140
210
  return;
211
+ const requestID = evt.properties?.requestID;
212
+ logIotRx(`Received ${evt.type} reqId=${requestID?.slice(0, 8) || 'N/A'} (${fragment.count} fragments)`);
213
+ if (evt.type === "function.invoked" && requestID) {
214
+ const event = evt.properties?.event;
215
+ logEventTrace("IOT_RECEIVED", {
216
+ requestID,
217
+ functionID: evt.properties?.functionID,
218
+ path: getRequestPath(event),
219
+ method: getHttpMethod(event),
220
+ correlationId: getCorrelationId(event),
221
+ apiGwReqId: getApiGatewayRequestId(event),
222
+ fragments: fragment.count,
223
+ });
224
+ }
141
225
  bus.publish(evt.type, evt.properties);
142
226
  }
143
227
  });
@@ -149,19 +233,96 @@ export const useIOT = lazy(async () => {
149
233
  properties,
150
234
  sourceID: bus.sourceID,
151
235
  };
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
- });
236
+ const fragments = await encode(payload);
237
+ await Promise.all(fragments.map((fragment) => new Promise((r) => {
238
+ device.publish(topic, JSON.stringify(fragment), { qos: 1 }, () => {
239
+ r();
159
240
  });
160
- }
241
+ })));
161
242
  Logger.debug("IOT Published", topic, type);
162
243
  },
163
244
  };
164
245
  });
246
+ /**
247
+ * A second connection, carrying only small control messages.
248
+ *
249
+ * `useIOT` also carries response bodies. Those are fragmented into 50KB chunks
250
+ * and published at QoS 1, so every chunk waits on a broker round trip, and they
251
+ * queue on the same socket as everything else.
252
+ *
253
+ * `function.ack` cannot afford to wait behind them. The Lambda re-invokes if the
254
+ * ack does not arrive within two seconds, and each re-invocation re-runs the
255
+ * handler and puts another response body on the socket that is already busy —
256
+ * so a late ack makes the next ack later still. Measured on one machine before
257
+ * this change: ack publish went from 105ms to 10.2s inside a minute, publishes
258
+ * in flight from 15 to 439, and a single request executed fifteen times before
259
+ * the gateway returned 504 at 29s.
260
+ *
261
+ * Round-trip latency decides who sees it. A chunk costs a round trip to the
262
+ * region — roughly 90ms from Europe against 15ms from the US — so the same
263
+ * payload lands either side of the two second budget depending on where the
264
+ * developer sits.
265
+ *
266
+ * **The client id must differ from `useIOT`'s.** AWS IoT drops an existing
267
+ * connection when a new one presents the same id, so a shared id would make the
268
+ * two sockets disconnect each other in a loop.
269
+ *
270
+ * This connection deliberately does not subscribe. Inbound traffic still arrives
271
+ * on `useIOT`; this one only publishes.
272
+ */
273
+ export const useIOTControl = lazy(async () => {
274
+ const bus = useBus();
275
+ const endpoint = await useIOTEndpoint();
276
+ const creds = await useAWSCredentials();
277
+ const project = useProject();
278
+ const device = new iot.device({
279
+ protocol: "wss",
280
+ host: endpoint,
281
+ region: project.config.region,
282
+ accessKeyId: creds.accessKeyId,
283
+ secretKey: creds.secretAccessKey,
284
+ sessionToken: creds.sessionToken,
285
+ reconnectPeriod: 1,
286
+ keepalive: 60,
287
+ clientId: `sst-control-${Math.random().toString(16).slice(2)}`,
288
+ });
289
+ const PREFIX = `/sst/${project.config.name}/${project.config.stage}`;
290
+ device.on("connect", () => {
291
+ Logger.debug("IoT control connected");
292
+ });
293
+ device.on("error", (err) => {
294
+ Logger.debug("IoT control error", err);
295
+ });
296
+ device.on("close", () => {
297
+ Logger.debug("IoT control closed");
298
+ void refreshWebSocketCredentials(device, "IoT control");
299
+ });
300
+ device.on("reconnect", () => {
301
+ Logger.debug("IoT control reconnecting...");
302
+ });
303
+ return {
304
+ prefix: PREFIX,
305
+ async publish(topic, type, properties) {
306
+ const payload = {
307
+ type,
308
+ properties,
309
+ sourceID: bus.sourceID,
310
+ };
311
+ // Control messages sit far below the fragment size, but the envelope has
312
+ // to match what the Lambda reassembles, so it is built the same way
313
+ // rather than assumed to be a single part.
314
+ const json = JSON.stringify(payload);
315
+ const parts = json.match(/.{1,50000}/g) ?? [];
316
+ const id = Math.random().toString();
317
+ await Promise.all(parts.map((data, index) => new Promise((r) => {
318
+ device.publish(topic, JSON.stringify({ id, index, count: parts.length, data }), { qos: 1 }, () => {
319
+ r();
320
+ });
321
+ })));
322
+ Logger.debug("IOT Control Published", topic, type);
323
+ },
324
+ };
325
+ });
165
326
  export const isSupported = () => [
166
327
  "eu-central-1",
167
328
  "eu-west-1",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "sideEffects": false,
3
3
  "name": "@intelligems/sst",
4
- "version": "2.49.3",
4
+ "version": "2.49.6-ig.10",
5
5
  "bin": {
6
6
  "sst": "cli/sst.js"
7
7
  },
@@ -113,7 +113,7 @@
113
113
  "@types/babel__generator": "^7.6.4",
114
114
  "@types/cross-spawn": "^6.0.2",
115
115
  "@types/express": "^4.17.14",
116
- "@types/node": "18.11.9",
116
+ "@types/node": "22.13.14",
117
117
  "@types/react": "^18.0.28",
118
118
  "@types/uuid": "^8.3.4",
119
119
  "@types/ws": "8.5.3",
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Log debug message for workers component
3
+ * Outputs to console and optionally to .sst/debug-workers.log
4
+ */
5
+ export declare function logWorkers(message: string, details?: Record<string, any>): void;
6
+ /**
7
+ * Log debug message for server component
8
+ * Outputs to console and optionally to .sst/debug-server.log
9
+ */
10
+ export declare function logServer(message: string, details?: Record<string, any>): void;
11
+ /**
12
+ * Log debug message for IoT component (publishing)
13
+ * Outputs to console and optionally to .sst/debug-iot.log
14
+ */
15
+ export declare function logIot(message: string, details?: Record<string, any>): void;
16
+ /**
17
+ * Log debug message for IoT component (receiving)
18
+ * Outputs to console and optionally to .sst/debug-iot.log
19
+ */
20
+ export declare function logIotRx(message: string, details?: Record<string, any>): void;
21
+ /**
22
+ * Check if bridge debug logging is enabled
23
+ */
24
+ export declare function isDebugBridgeEnabled(): boolean;
@@ -0,0 +1,98 @@
1
+ import { createDebugFileLogger } from "./debug-file-logger.js";
2
+ /**
3
+ * Debug logging for SST dev bridge components
4
+ *
5
+ * Enable with: SST_DEBUG_BRIDGE=true
6
+ *
7
+ * Log files:
8
+ * - .sst/debug-workers.log - Worker pool and invocation handling
9
+ * - .sst/debug-server.log - Runtime server request/response handling
10
+ * - .sst/debug-iot.log - IoT message publishing and receiving
11
+ */
12
+ const DEBUG_BRIDGE = process.env.SST_DEBUG_BRIDGE === "true";
13
+ // Lazy-initialized loggers
14
+ let workersLogger = null;
15
+ let serverLogger = null;
16
+ let iotLogger = null;
17
+ function getWorkersLogger() {
18
+ if (!DEBUG_BRIDGE)
19
+ return null;
20
+ if (!workersLogger) {
21
+ workersLogger = createDebugFileLogger({
22
+ filePath: ".sst/debug-workers.log",
23
+ sessionName: "WORKERS",
24
+ width: 120,
25
+ });
26
+ }
27
+ return workersLogger;
28
+ }
29
+ function getServerLogger() {
30
+ if (!DEBUG_BRIDGE)
31
+ return null;
32
+ if (!serverLogger) {
33
+ serverLogger = createDebugFileLogger({
34
+ filePath: ".sst/debug-server.log",
35
+ sessionName: "SERVER",
36
+ width: 120,
37
+ });
38
+ }
39
+ return serverLogger;
40
+ }
41
+ function getIotLogger() {
42
+ if (!DEBUG_BRIDGE)
43
+ return null;
44
+ if (!iotLogger) {
45
+ iotLogger = createDebugFileLogger({
46
+ filePath: ".sst/debug-iot.log",
47
+ sessionName: "IOT",
48
+ width: 120,
49
+ });
50
+ }
51
+ return iotLogger;
52
+ }
53
+ /**
54
+ * Log debug message for workers component
55
+ * Outputs to console and optionally to .sst/debug-workers.log
56
+ */
57
+ export function logWorkers(message, details) {
58
+ const logger = getWorkersLogger();
59
+ if (logger) {
60
+ logger.log("WORKERS", { msg: message, ...details });
61
+ }
62
+ }
63
+ /**
64
+ * Log debug message for server component
65
+ * Outputs to console and optionally to .sst/debug-server.log
66
+ */
67
+ export function logServer(message, details) {
68
+ const logger = getServerLogger();
69
+ if (logger) {
70
+ logger.log("SERVER", { msg: message, ...details });
71
+ }
72
+ }
73
+ /**
74
+ * Log debug message for IoT component (publishing)
75
+ * Outputs to console and optionally to .sst/debug-iot.log
76
+ */
77
+ export function logIot(message, details) {
78
+ const logger = getIotLogger();
79
+ if (logger) {
80
+ logger.log("IOT", { msg: message, ...details });
81
+ }
82
+ }
83
+ /**
84
+ * Log debug message for IoT component (receiving)
85
+ * Outputs to console and optionally to .sst/debug-iot.log
86
+ */
87
+ export function logIotRx(message, details) {
88
+ const logger = getIotLogger();
89
+ if (logger) {
90
+ logger.log("IOT-RX", { msg: message, ...details });
91
+ }
92
+ }
93
+ /**
94
+ * Check if bridge debug logging is enabled
95
+ */
96
+ export function isDebugBridgeEnabled() {
97
+ return DEBUG_BRIDGE;
98
+ }