@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.
- package/cli/commands/dev.js +69 -3
- package/cli/sst.js +0 -1
- package/constructs/Function.d.ts +2 -1
- package/constructs/Function.js +4 -3
- package/credentials.js +4 -3
- package/iot.d.ts +31 -0
- package/iot.js +171 -10
- package/package.json +2 -2
- package/runtime/debug-bridge-logging.d.ts +24 -0
- package/runtime/debug-bridge-logging.js +98 -0
- package/runtime/debug-file-logger.d.ts +80 -0
- package/runtime/debug-file-logger.js +148 -0
- package/runtime/event-trace-logging.d.ts +27 -0
- package/runtime/event-trace-logging.js +69 -0
- package/runtime/handlers/node.js +62 -21
- package/runtime/handlers.d.ts +6 -2
- package/runtime/handlers.js +130 -59
- package/runtime/iot.js +31 -4
- package/runtime/mono-build-config.d.ts +58 -0
- package/runtime/mono-build-config.js +104 -0
- package/runtime/request-utils.d.ts +21 -0
- package/runtime/request-utils.js +102 -0
- package/runtime/runtime.d.ts +1 -0
- package/runtime/server.d.ts +4 -1
- package/runtime/server.js +122 -29
- package/runtime/worker-pool-logging.d.ts +29 -0
- package/runtime/worker-pool-logging.js +185 -0
- package/runtime/workers.d.ts +39 -7
- package/runtime/workers.js +831 -38
- package/stacks/deploy.js +14 -10
- package/stacks/synth.js +5 -4
- package/support/bridge/live-lambda.mjs +38 -38
- package/support/nodejs-runtime/index.mjs +44 -11
- package/support/python-runtime/runtime.py +116 -45
- package/util/user-configuration.js +19 -7
- package/README.md +0 -43
- package/package.json.bak +0 -156
package/cli/commands/dev.js
CHANGED
|
@@ -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
|
|
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
package/constructs/Function.d.ts
CHANGED
|
@@ -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;
|
package/constructs/Function.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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.
|
|
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": "
|
|
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
|
+
}
|