@intelligems/sst 2.49.6-ig.7 → 2.49.6-ig.9
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 +2 -14
- package/cli/sst.js +0 -0
- package/iot.d.ts +31 -0
- package/iot.js +134 -0
- package/package.json +9 -8
- package/package.json.bak +1 -1
- package/runtime/iot.js +5 -2
- package/support/dotnet31-bootstrap/release/Amazon.Lambda.Core.dll +0 -0
- package/support/dotnet31-bootstrap/release/Amazon.Lambda.RuntimeSupport.dll +0 -0
- package/support/dotnet31-bootstrap/release/System.Runtime.CompilerServices.Unsafe.dll +0 -0
- package/support/dotnet31-bootstrap/release/System.Text.Encodings.Web.dll +0 -0
- package/support/dotnet31-bootstrap/release/System.Text.Json.dll +0 -0
- package/support/dotnet31-bootstrap/release/dotnet-bootstrap +0 -0
- package/support/dotnet6-bootstrap/release/Amazon.Lambda.Core.dll +0 -0
- package/support/dotnet6-bootstrap/release/Amazon.Lambda.RuntimeSupport.dll +0 -0
- package/support/dotnet6-bootstrap/release/dotnet-bootstrap +0 -0
- package/support/dotnet8-bootstrap/release/Amazon.Lambda.Core.dll +0 -0
- package/support/dotnet8-bootstrap/release/Amazon.Lambda.RuntimeSupport.dll +0 -0
- package/support/dotnet8-bootstrap/release/dotnet-bootstrap +0 -0
- package/support/java-runtime/install.sh +0 -0
- package/LICENSE +0 -21
package/cli/commands/dev.js
CHANGED
|
@@ -151,23 +151,11 @@ export const dev = (program) => program.command(["dev", "start"], "Work on your
|
|
|
151
151
|
if (info.enableLiveDev === false)
|
|
152
152
|
return;
|
|
153
153
|
Colors.gap();
|
|
154
|
-
Colors.line(Colors.danger("
|
|
155
|
-
Colors.line(Colors.danger(" FUNCTION BUILD FAILED - SST IS EXITING"));
|
|
156
|
-
Colors.line(Colors.danger("═".repeat(60)));
|
|
157
|
-
Colors.gap();
|
|
158
|
-
Colors.line(Colors.danger("✖ "), "Handler:", info.handler);
|
|
159
|
-
Colors.line(Colors.danger("✖ "), "Function ID:", evt.properties.functionID);
|
|
160
|
-
Colors.gap();
|
|
161
|
-
Colors.line(Colors.danger(" Errors:"));
|
|
154
|
+
Colors.line(Colors.danger("✖ "), "Build failed", info.handler);
|
|
162
155
|
for (const line of evt.properties.errors) {
|
|
163
|
-
Colors.line(
|
|
156
|
+
Colors.line(" ", line);
|
|
164
157
|
}
|
|
165
158
|
Colors.gap();
|
|
166
|
-
Colors.line(Colors.danger("═".repeat(60)));
|
|
167
|
-
Colors.line(Colors.danger(" Fix the error above and restart `sst dev`"));
|
|
168
|
-
Colors.line(Colors.danger("═".repeat(60)));
|
|
169
|
-
Colors.gap();
|
|
170
|
-
process.exit(1);
|
|
171
159
|
});
|
|
172
160
|
bus.subscribe("function.success", async (evt) => {
|
|
173
161
|
// Skip warmup request logs
|
package/cli/sst.js
CHANGED
|
File without changes
|
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
|
@@ -22,6 +22,59 @@ import { useBus } from "./bus.js";
|
|
|
22
22
|
import { useProject } from "./project.js";
|
|
23
23
|
import { useBootstrap } from "./bootstrap.js";
|
|
24
24
|
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
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
|
+
}
|
|
25
78
|
export const useIOT = lazy(async () => {
|
|
26
79
|
const bus = useBus();
|
|
27
80
|
const endpoint = await useIOTEndpoint();
|
|
@@ -116,6 +169,7 @@ export const useIOT = lazy(async () => {
|
|
|
116
169
|
});
|
|
117
170
|
device.on("close", () => {
|
|
118
171
|
Logger.debug("IoT closed");
|
|
172
|
+
void refreshWebSocketCredentials(device, "IoT");
|
|
119
173
|
});
|
|
120
174
|
device.on("reconnect", () => {
|
|
121
175
|
Logger.debug("IoT reconnecting...");
|
|
@@ -189,6 +243,86 @@ export const useIOT = lazy(async () => {
|
|
|
189
243
|
},
|
|
190
244
|
};
|
|
191
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
|
+
});
|
|
192
326
|
export const isSupported = () => [
|
|
193
327
|
"eu-central-1",
|
|
194
328
|
"eu-west-1",
|
package/package.json
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"sideEffects": false,
|
|
3
3
|
"name": "@intelligems/sst",
|
|
4
|
-
"version": "2.49.6-ig.
|
|
4
|
+
"version": "2.49.6-ig.9",
|
|
5
5
|
"bin": {
|
|
6
6
|
"sst": "cli/sst.js"
|
|
7
7
|
},
|
|
8
8
|
"description": "A CLI to help deploy SST apps.",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"license": "MIT",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"prepare": "",
|
|
13
|
+
"build": "node build.mjs && tsc",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"dev": "source .env && tsc-watch --onSuccess \"rsync -av dist/* ${TO} --checksum\""
|
|
16
|
+
},
|
|
11
17
|
"repository": {
|
|
12
18
|
"type": "git",
|
|
13
19
|
"url": "git+https://github.com/sst/v2.git",
|
|
@@ -142,10 +148,5 @@
|
|
|
142
148
|
"test": "test"
|
|
143
149
|
},
|
|
144
150
|
"keywords": [],
|
|
145
|
-
"author": ""
|
|
146
|
-
|
|
147
|
-
"build": "node build.mjs && tsc",
|
|
148
|
-
"test": "vitest run",
|
|
149
|
-
"dev": "source .env && tsc-watch --onSuccess \"rsync -av dist/* ${TO} --checksum\""
|
|
150
|
-
}
|
|
151
|
-
}
|
|
151
|
+
"author": ""
|
|
152
|
+
}
|
package/package.json.bak
CHANGED
package/runtime/iot.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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
4
|
import { logInvokeTrace } from "./worker-pool-logging.js";
|
|
5
5
|
import { logIot } from "./debug-bridge-logging.js";
|
|
@@ -7,6 +7,9 @@ import { logEventTrace } from "./event-trace-logging.js";
|
|
|
7
7
|
export const useIOTBridge = lazy(async () => {
|
|
8
8
|
const bus = useBus();
|
|
9
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();
|
|
10
13
|
const topic = `${iot.prefix}/events`;
|
|
11
14
|
bus.subscribe("function.success", async (evt) => {
|
|
12
15
|
const { workerID, requestID } = evt.properties;
|
|
@@ -27,7 +30,7 @@ export const useIOTBridge = lazy(async () => {
|
|
|
27
30
|
logIot(`reqId=${requestID?.slice(0, 8)} Publishing function.ack to worker ${workerID.slice(0, 8)}`);
|
|
28
31
|
logInvokeTrace("IOT_ACK_START", workerID, `worker=${workerID.slice(0, 8)}`);
|
|
29
32
|
const startTime = Date.now();
|
|
30
|
-
await
|
|
33
|
+
await control.publish(topic + "/" + workerID, "function.ack", evt.properties);
|
|
31
34
|
const elapsed = Date.now() - startTime;
|
|
32
35
|
logIot(`reqId=${requestID?.slice(0, 8)} function.ack published in ${elapsed}ms`);
|
|
33
36
|
logInvokeTrace("IOT_ACK_DONE", workerID, `worker=${workerID.slice(0, 8)}`);
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2020 SST
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|