@ayepi/aws 0.2.0 → 0.4.0

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/README.md CHANGED
@@ -49,6 +49,11 @@ const w = createWork({ queue, pubsub, store, work: [/* ... */] as const })
49
49
  and rethrown.
50
50
  - **You own the SDK clients.** The clients are optional peer deps — you construct and configure
51
51
  them (region, credentials, endpoint) and manage their lifecycle.
52
+ - **Connection pooling.** The SDK's default handler caps outbound connections at `maxSockets: 50`
53
+ per client — a common bottleneck for a busy S3+SQS work system. `@ayepi/aws/http` exports
54
+ `pooledRequestHandler({ maxSockets })` and `sharedHttpAgents(...)` — a keep-alive handler with a
55
+ higher cap to pass as `requestHandler`, or one shared pool across clients:
56
+ `new S3Client({ requestHandler: pooledRequestHandler({ maxSockets: 256 }) })`.
52
57
 
53
58
  ## Options
54
59
 
package/ayepi-aws.md CHANGED
@@ -166,6 +166,7 @@ The returned [`Queue`](./ayepi-work-ports.md#queue--the-durable-work-log) maps o
166
166
  | `heartbeat(pulled, visibility)` | `ChangeMessageVisibility` | extends the lease to `ceil(visibility/1000)` s, **clamped to ≤ 43200 s** |
167
167
  | `ack(pulled)` | `DeleteMessage` | permanently removes the message (+ deletes the offloaded S3 body) |
168
168
  | `fail(pulled, delay?)` | `ChangeMessageVisibility` | returns the message early: `VisibilityTimeout` = `ceil((delay ?? 0)/1000)` s, **clamped to ≤ 43200 s** |
169
+ | `size()` | `GetQueueAttributes` | reads `ApproximateNumberOfMessages` and returns it as a number (`0` when absent) |
169
170
 
170
171
  **SQS range clamping (far-future scheduling).** SQS rejects out-of-range values, so the queue
171
172
  clamps each duration into SQS's allowed range: `DelaySeconds` to **0–900 s** (15 min) on `push`,
@@ -184,6 +185,11 @@ horizons or keep distant schedules sparse.
184
185
  `max`. Each `PulledWork.attempt` comes from the message's `ApproximateReceiveCount` (delivery
185
186
  count, starting at 1); `handle` is an opaque `{ receiptHandle, s3Key? }` you round-trip to
186
187
  `heartbeat`/`ack`/`fail`.
188
+ - **`size`** implements the optional `Queue.size()` — a single `GetQueueAttributes` reading
189
+ `ApproximateNumberOfMessages` (returned as a number, `0` when the attribute is absent, wrapped in
190
+ `retry` like every other call). It's what lets `@ayepi/work`'s sustained-backlog alarm
191
+ (`createWork({ onBacklog })`) report real SQS queue depth (`queued`). The count is **approximate**
192
+ — it's SQS's own `ApproximateNumberOfMessages`, not an exact tally.
187
193
  - **Dead-lettering** is the queue's own SQS **redrive policy** — configured on the queue (or its
188
194
  DLQ) in AWS, not here. After `maxReceiveCount` failed deliveries SQS moves the message to the
189
195
  DLQ natively. (`sqsQueue` does not implement the optional `Queue.deadLetter` hook.)
@@ -205,6 +211,49 @@ deleted. Notes:
205
211
  - `store` is any [`@ayepi/files`](./ayepi-files.md) `FileStore` — typically an `s3Files({...})`,
206
212
  but a filesystem store or your own works too.
207
213
 
214
+ ### `@ayepi/aws/http` — a well-pooled request handler
215
+
216
+ The SDK's default `NodeHttpHandler` caps outbound connections at **`maxSockets: 50` per client**.
217
+ Under a busy work system (S3 + SQS + more, each its own client) that cap is often the first thing to
218
+ break — calls queue behind 50 sockets and latency balloons, with no error, just slowness. This
219
+ subpath builds a keep-alive handler (and shareable agents) with a higher cap.
220
+
221
+ ```ts
222
+ import { S3Client } from '@aws-sdk/client-s3';
223
+ import { SQSClient } from '@aws-sdk/client-sqs';
224
+ import { pooledRequestHandler, sharedHttpAgents } from '@ayepi/aws/http';
225
+
226
+ // One handler per client:
227
+ const s3 = new S3Client({ region, requestHandler: pooledRequestHandler({ maxSockets: 256 }) });
228
+
229
+ // ...or share ONE pool across many clients (bounds total sockets, better reuse):
230
+ const agents = sharedHttpAgents({ maxSockets: 512 });
231
+ const s3b = new S3Client({ requestHandler: pooledRequestHandler(agents) });
232
+ const sqs = new SQSClient({ requestHandler: pooledRequestHandler(agents) });
233
+ ```
234
+
235
+ ```ts
236
+ function pooledRequestHandler(opts?: PooledHandlerOptions): NodeHttpHandler
237
+ function sharedHttpAgents(opts?: PooledAgentsOptions): { httpAgent; httpsAgent }
238
+
239
+ interface PooledAgentsOptions {
240
+ readonly maxSockets?: number; // per host (default 128; SDK default 50)
241
+ readonly maxTotalSockets?: number; // across all hosts (default: unlimited)
242
+ readonly keepAlive?: boolean; // default true — avoids TCP churn / port exhaustion
243
+ readonly keepAliveMsecs?: number; // default 1000
244
+ }
245
+ interface PooledHandlerOptions extends PooledAgentsOptions {
246
+ readonly connectionTimeout?: number; // ms
247
+ readonly requestTimeout?: number; // ms
248
+ readonly httpAgent?: HttpAgent; // bring your own (overrides the pool knobs)
249
+ readonly httpsAgent?: HttpsAgent;
250
+ }
251
+ ```
252
+
253
+ Keep-alive is on by default so connections are reused instead of churned — which also sidesteps
254
+ ephemeral-port / `TIME_WAIT` exhaustion under sustained load. Requires `@smithy/node-http-handler`
255
+ (an optional peer, already a transitive dependency of the AWS SDK clients).
256
+
208
257
  ### `@ayepi/aws` (root) — the retry seam
209
258
 
210
259
  The root subpath exports the shared resilience machinery both backends build on. You rarely call
package/dist/http.cjs ADDED
@@ -0,0 +1,70 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let node_http = require("node:http");
3
+ let node_https = require("node:https");
4
+ let _smithy_node_http_handler = require("@smithy/node-http-handler");
5
+ //#region src/http.ts
6
+ /**
7
+ * # @ayepi/aws/http — a well-pooled request handler for the AWS SDK
8
+ *
9
+ * The AWS SDK v3's default `NodeHttpHandler` caps outbound connections at **`maxSockets: 50` per
10
+ * client**. Under a busy work system (S3 + SQS + more, each its own client) that cap is often the
11
+ * first thing to break: calls queue behind 50 sockets and latency balloons — with no error, just
12
+ * mysterious slowness. This module builds a handler (and shareable agents) with keep-alive on and a
13
+ * higher cap, to pass as `requestHandler` to your clients.
14
+ *
15
+ * ```ts
16
+ * import { S3Client } from '@aws-sdk/client-s3';
17
+ * import { SQSClient } from '@aws-sdk/client-sqs';
18
+ * import { pooledRequestHandler, sharedHttpAgents } from '@ayepi/aws/http';
19
+ *
20
+ * // One handler per client:
21
+ * const s3 = new S3Client({ region, requestHandler: pooledRequestHandler({ maxSockets: 256 }) });
22
+ *
23
+ * // ...or share ONE pool across many clients (fewer total sockets, better reuse):
24
+ * const agents = sharedHttpAgents({ maxSockets: 512 });
25
+ * const s3b = new S3Client({ requestHandler: pooledRequestHandler(agents) });
26
+ * const sqs = new SQSClient({ requestHandler: pooledRequestHandler(agents) });
27
+ * ```
28
+ *
29
+ * Keep-alive is on by default so connections are reused instead of churned — which also avoids
30
+ * ephemeral-port / `TIME_WAIT` exhaustion under sustained load. Requires `@smithy/node-http-handler`
31
+ * (a transitive dependency of the AWS SDK clients).
32
+ *
33
+ * @module
34
+ */
35
+ /** Agent constructor options derived from the pool knobs. */
36
+ function agentConf(opts) {
37
+ return {
38
+ keepAlive: opts.keepAlive ?? true,
39
+ keepAliveMsecs: opts.keepAliveMsecs ?? 1e3,
40
+ maxSockets: opts.maxSockets ?? 128,
41
+ maxTotalSockets: opts.maxTotalSockets
42
+ };
43
+ }
44
+ /**
45
+ * A pair of keep-alive `node:http` / `node:https` agents you can **share across many AWS clients**
46
+ * (pass to {@link pooledRequestHandler}). Sharing one pair bounds total sockets across S3/SQS/etc.
47
+ */
48
+ function sharedHttpAgents(opts = {}) {
49
+ const conf = agentConf(opts);
50
+ return {
51
+ httpAgent: new node_http.Agent(conf),
52
+ httpsAgent: new node_https.Agent(conf)
53
+ };
54
+ }
55
+ /**
56
+ * Build a `NodeHttpHandler` with keep-alive and a higher socket cap than the SDK default — pass it
57
+ * as `requestHandler` to an `S3Client`/`SQSClient`. Reuse one handler (or one {@link sharedHttpAgents}
58
+ * pair) across clients to bound total sockets.
59
+ */
60
+ function pooledRequestHandler(opts = {}) {
61
+ return new _smithy_node_http_handler.NodeHttpHandler({
62
+ httpAgent: opts.httpAgent ?? new node_http.Agent(agentConf(opts)),
63
+ httpsAgent: opts.httpsAgent ?? new node_https.Agent(agentConf(opts)),
64
+ connectionTimeout: opts.connectionTimeout,
65
+ requestTimeout: opts.requestTimeout
66
+ });
67
+ }
68
+ //#endregion
69
+ exports.pooledRequestHandler = pooledRequestHandler;
70
+ exports.sharedHttpAgents = sharedHttpAgents;
@@ -0,0 +1,44 @@
1
+ import { Agent } from "node:http";
2
+ import { Agent as Agent$1 } from "node:https";
3
+ import { NodeHttpHandler } from "@smithy/node-http-handler";
4
+
5
+ //#region src/http.d.ts
6
+
7
+ /** Connection-pool knobs shared by the http + https agents. */
8
+ interface PooledAgentsOptions {
9
+ /** Max sockets per host (default `128`; the SDK default is `50`). */
10
+ readonly maxSockets?: number;
11
+ /** Max sockets across all hosts (default: unlimited). */
12
+ readonly maxTotalSockets?: number;
13
+ /** Reuse connections (default `true`). Keep-alive avoids TCP churn and port exhaustion. */
14
+ readonly keepAlive?: boolean;
15
+ /** Idle time before a kept-alive socket may close (ms, default `1000`). */
16
+ readonly keepAliveMsecs?: number;
17
+ }
18
+ /** Options for {@link pooledRequestHandler}: the pool knobs plus timeouts and bring-your-own agents. */
19
+ interface PooledHandlerOptions extends PooledAgentsOptions {
20
+ /** Socket connect timeout (ms). */
21
+ readonly connectionTimeout?: number;
22
+ /** Time to wait for a response (ms). */
23
+ readonly requestTimeout?: number;
24
+ /** Bring your own http agent (overrides the pool knobs for http). */
25
+ readonly httpAgent?: Agent;
26
+ /** Bring your own https agent (overrides the pool knobs for https). */
27
+ readonly httpsAgent?: Agent$1;
28
+ }
29
+ /**
30
+ * A pair of keep-alive `node:http` / `node:https` agents you can **share across many AWS clients**
31
+ * (pass to {@link pooledRequestHandler}). Sharing one pair bounds total sockets across S3/SQS/etc.
32
+ */
33
+ declare function sharedHttpAgents(opts?: PooledAgentsOptions): {
34
+ httpAgent: Agent;
35
+ httpsAgent: Agent$1;
36
+ };
37
+ /**
38
+ * Build a `NodeHttpHandler` with keep-alive and a higher socket cap than the SDK default — pass it
39
+ * as `requestHandler` to an `S3Client`/`SQSClient`. Reuse one handler (or one {@link sharedHttpAgents}
40
+ * pair) across clients to bound total sockets.
41
+ */
42
+ declare function pooledRequestHandler(opts?: PooledHandlerOptions): NodeHttpHandler;
43
+ //#endregion
44
+ export { PooledAgentsOptions, PooledHandlerOptions, pooledRequestHandler, sharedHttpAgents };
package/dist/http.d.ts ADDED
@@ -0,0 +1,44 @@
1
+ import { Agent } from "node:http";
2
+ import { Agent as Agent$1 } from "node:https";
3
+ import { NodeHttpHandler } from "@smithy/node-http-handler";
4
+
5
+ //#region src/http.d.ts
6
+
7
+ /** Connection-pool knobs shared by the http + https agents. */
8
+ interface PooledAgentsOptions {
9
+ /** Max sockets per host (default `128`; the SDK default is `50`). */
10
+ readonly maxSockets?: number;
11
+ /** Max sockets across all hosts (default: unlimited). */
12
+ readonly maxTotalSockets?: number;
13
+ /** Reuse connections (default `true`). Keep-alive avoids TCP churn and port exhaustion. */
14
+ readonly keepAlive?: boolean;
15
+ /** Idle time before a kept-alive socket may close (ms, default `1000`). */
16
+ readonly keepAliveMsecs?: number;
17
+ }
18
+ /** Options for {@link pooledRequestHandler}: the pool knobs plus timeouts and bring-your-own agents. */
19
+ interface PooledHandlerOptions extends PooledAgentsOptions {
20
+ /** Socket connect timeout (ms). */
21
+ readonly connectionTimeout?: number;
22
+ /** Time to wait for a response (ms). */
23
+ readonly requestTimeout?: number;
24
+ /** Bring your own http agent (overrides the pool knobs for http). */
25
+ readonly httpAgent?: Agent;
26
+ /** Bring your own https agent (overrides the pool knobs for https). */
27
+ readonly httpsAgent?: Agent$1;
28
+ }
29
+ /**
30
+ * A pair of keep-alive `node:http` / `node:https` agents you can **share across many AWS clients**
31
+ * (pass to {@link pooledRequestHandler}). Sharing one pair bounds total sockets across S3/SQS/etc.
32
+ */
33
+ declare function sharedHttpAgents(opts?: PooledAgentsOptions): {
34
+ httpAgent: Agent;
35
+ httpsAgent: Agent$1;
36
+ };
37
+ /**
38
+ * Build a `NodeHttpHandler` with keep-alive and a higher socket cap than the SDK default — pass it
39
+ * as `requestHandler` to an `S3Client`/`SQSClient`. Reuse one handler (or one {@link sharedHttpAgents}
40
+ * pair) across clients to bound total sockets.
41
+ */
42
+ declare function pooledRequestHandler(opts?: PooledHandlerOptions): NodeHttpHandler;
43
+ //#endregion
44
+ export { PooledAgentsOptions, PooledHandlerOptions, pooledRequestHandler, sharedHttpAgents };
package/dist/http.js ADDED
@@ -0,0 +1,68 @@
1
+ import { Agent } from "node:http";
2
+ import { Agent as Agent$1 } from "node:https";
3
+ import { NodeHttpHandler } from "@smithy/node-http-handler";
4
+ //#region src/http.ts
5
+ /**
6
+ * # @ayepi/aws/http — a well-pooled request handler for the AWS SDK
7
+ *
8
+ * The AWS SDK v3's default `NodeHttpHandler` caps outbound connections at **`maxSockets: 50` per
9
+ * client**. Under a busy work system (S3 + SQS + more, each its own client) that cap is often the
10
+ * first thing to break: calls queue behind 50 sockets and latency balloons — with no error, just
11
+ * mysterious slowness. This module builds a handler (and shareable agents) with keep-alive on and a
12
+ * higher cap, to pass as `requestHandler` to your clients.
13
+ *
14
+ * ```ts
15
+ * import { S3Client } from '@aws-sdk/client-s3';
16
+ * import { SQSClient } from '@aws-sdk/client-sqs';
17
+ * import { pooledRequestHandler, sharedHttpAgents } from '@ayepi/aws/http';
18
+ *
19
+ * // One handler per client:
20
+ * const s3 = new S3Client({ region, requestHandler: pooledRequestHandler({ maxSockets: 256 }) });
21
+ *
22
+ * // ...or share ONE pool across many clients (fewer total sockets, better reuse):
23
+ * const agents = sharedHttpAgents({ maxSockets: 512 });
24
+ * const s3b = new S3Client({ requestHandler: pooledRequestHandler(agents) });
25
+ * const sqs = new SQSClient({ requestHandler: pooledRequestHandler(agents) });
26
+ * ```
27
+ *
28
+ * Keep-alive is on by default so connections are reused instead of churned — which also avoids
29
+ * ephemeral-port / `TIME_WAIT` exhaustion under sustained load. Requires `@smithy/node-http-handler`
30
+ * (a transitive dependency of the AWS SDK clients).
31
+ *
32
+ * @module
33
+ */
34
+ /** Agent constructor options derived from the pool knobs. */
35
+ function agentConf(opts) {
36
+ return {
37
+ keepAlive: opts.keepAlive ?? true,
38
+ keepAliveMsecs: opts.keepAliveMsecs ?? 1e3,
39
+ maxSockets: opts.maxSockets ?? 128,
40
+ maxTotalSockets: opts.maxTotalSockets
41
+ };
42
+ }
43
+ /**
44
+ * A pair of keep-alive `node:http` / `node:https` agents you can **share across many AWS clients**
45
+ * (pass to {@link pooledRequestHandler}). Sharing one pair bounds total sockets across S3/SQS/etc.
46
+ */
47
+ function sharedHttpAgents(opts = {}) {
48
+ const conf = agentConf(opts);
49
+ return {
50
+ httpAgent: new Agent(conf),
51
+ httpsAgent: new Agent$1(conf)
52
+ };
53
+ }
54
+ /**
55
+ * Build a `NodeHttpHandler` with keep-alive and a higher socket cap than the SDK default — pass it
56
+ * as `requestHandler` to an `S3Client`/`SQSClient`. Reuse one handler (or one {@link sharedHttpAgents}
57
+ * pair) across clients to bound total sockets.
58
+ */
59
+ function pooledRequestHandler(opts = {}) {
60
+ return new NodeHttpHandler({
61
+ httpAgent: opts.httpAgent ?? new Agent(agentConf(opts)),
62
+ httpsAgent: opts.httpsAgent ?? new Agent$1(agentConf(opts)),
63
+ connectionTimeout: opts.connectionTimeout,
64
+ requestTimeout: opts.requestTimeout
65
+ });
66
+ }
67
+ //#endregion
68
+ export { pooledRequestHandler, sharedHttpAgents };
package/dist/sqs.cjs CHANGED
@@ -128,6 +128,13 @@ function sqsQueue(opts) {
128
128
  ReceiptHandle: handleOf(pulled).receiptHandle,
129
129
  VisibilityTimeout: visSecs(delay ?? 0)
130
130
  }));
131
+ }),
132
+ size: () => run(async () => {
133
+ const out = await client.send(new _aws_sdk_client_sqs.GetQueueAttributesCommand({
134
+ QueueUrl: queueUrl,
135
+ AttributeNames: ["ApproximateNumberOfMessages"]
136
+ }));
137
+ return Number(out.Attributes?.ApproximateNumberOfMessages ?? "0");
131
138
  })
132
139
  };
133
140
  }
package/dist/sqs.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { makeRun } from "./index.js";
2
2
  import { randomUUID } from "node:crypto";
3
- import { ChangeMessageVisibilityCommand, DeleteMessageCommand, ReceiveMessageCommand, SendMessageCommand } from "@aws-sdk/client-sqs";
3
+ import { ChangeMessageVisibilityCommand, DeleteMessageCommand, GetQueueAttributesCommand, ReceiveMessageCommand, SendMessageCommand } from "@aws-sdk/client-sqs";
4
4
  //#region src/sqs.ts
5
5
  /**
6
6
  * # @ayepi/aws/sqs — an SQS-backed `@ayepi/work` {@link Queue}
@@ -127,6 +127,13 @@ function sqsQueue(opts) {
127
127
  ReceiptHandle: handleOf(pulled).receiptHandle,
128
128
  VisibilityTimeout: visSecs(delay ?? 0)
129
129
  }));
130
+ }),
131
+ size: () => run(async () => {
132
+ const out = await client.send(new GetQueueAttributesCommand({
133
+ QueueUrl: queueUrl,
134
+ AttributeNames: ["ApproximateNumberOfMessages"]
135
+ }));
136
+ return Number(out.Attributes?.ApproximateNumberOfMessages ?? "0");
130
137
  })
131
138
  };
132
139
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ayepi/aws",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "AWS backends for ayepi — an SQS @ayepi/work queue (large payloads offloaded to S3) and an S3 @ayepi/files store, all retry-wrapped for throttle resilience",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -52,6 +52,16 @@
52
52
  "default": "./dist/sqs.cjs"
53
53
  }
54
54
  },
55
+ "./http": {
56
+ "import": {
57
+ "types": "./dist/http.d.ts",
58
+ "default": "./dist/http.js"
59
+ },
60
+ "require": {
61
+ "types": "./dist/http.d.cts",
62
+ "default": "./dist/http.cjs"
63
+ }
64
+ },
55
65
  "./package.json": "./package.json"
56
66
  },
57
67
  "engines": {
@@ -62,9 +72,10 @@
62
72
  "@aws-sdk/client-sqs": "^3",
63
73
  "@aws-sdk/lib-storage": "^3",
64
74
  "@aws-sdk/s3-request-presigner": "^3",
65
- "@ayepi/core": "^0.2.0",
66
- "@ayepi/work": "^0.2.0",
67
- "@ayepi/files": "^0.2.0"
75
+ "@smithy/node-http-handler": "^4",
76
+ "@ayepi/core": "^0.4.0",
77
+ "@ayepi/work": "^0.4.0",
78
+ "@ayepi/files": "^0.4.0"
68
79
  },
69
80
  "peerDependenciesMeta": {
70
81
  "@ayepi/work": {
@@ -84,6 +95,9 @@
84
95
  },
85
96
  "@aws-sdk/s3-request-presigner": {
86
97
  "optional": true
98
+ },
99
+ "@smithy/node-http-handler": {
100
+ "optional": true
87
101
  }
88
102
  },
89
103
  "devDependencies": {
@@ -91,15 +105,16 @@
91
105
  "@aws-sdk/client-sqs": "^3.700.0",
92
106
  "@aws-sdk/lib-storage": "^3.700.0",
93
107
  "@aws-sdk/s3-request-presigner": "^3.700.0",
108
+ "@smithy/node-http-handler": "^4.0.0",
94
109
  "@vitest/coverage-v8": "^2.1.8",
95
110
  "publint": "^0.3.0",
96
111
  "testcontainers": "^10.13.0",
97
112
  "tsdown": "^0.12.0",
98
113
  "vitest": "^2.1.8",
99
114
  "zod": "^4.4.3",
100
- "@ayepi/core": "0.2.0",
101
- "@ayepi/work": "0.2.0",
102
- "@ayepi/files": "0.2.0"
115
+ "@ayepi/core": "0.4.0",
116
+ "@ayepi/files": "0.4.0",
117
+ "@ayepi/work": "0.4.0"
103
118
  },
104
119
  "keywords": [
105
120
  "ayepi",