From be681217837cd24f617575b8953b3a4ead080ecf Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 5 Oct 2026 20:54:14 +0100 Subject: [PATCH] fix: peer dependency ranges state what the code needs; dev versions updated - peers: @adaptivestone/framework ^5.1.0, mongoose ^9.0.0, AWS SDK clients ^3.572.0 (was `*`) - @aws-sdk/client-sqs before 3.572 silently drops MessageSystemAttributeNames, so every delivery read as attempt 1 and a failing task was never dead-lettered; SqsTaskQueue now also warns once when SQS returns no ApproximateReceiveCount - dev: framework 5.5, mongoose 9.11, AWS SDK 3.1146, @types/node 26.6.4 - README install table and AGENTS.md list the minimum versions --- AGENTS.md | 4 +- CHANGELOG.md | 5 ++ README.md | 14 +++--- package-lock.json | 108 ++++++++++++++++++++-------------------- package.json | 22 ++++---- src/drivers/sqs.test.ts | 26 ++++++++++ src/drivers/sqs.ts | 12 ++++- 7 files changed, 117 insertions(+), 74 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3a0e3a6..a21d2af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,8 +38,8 @@ a private original. ```bash npm i @adaptivestone/framework-module-resize - # SQS task queue only: npm i @aws-sdk/client-sqs - # S3 storage only: npm i @aws-sdk/client-s3 @aws-sdk/s3-request-presigner + # SQS task queue only: npm i @aws-sdk/client-sqs (>= 3.572) + # S3 storage only: npm i @aws-sdk/client-s3 @aws-sdk/s3-request-presigner (>= 3.572) ``` 2. Scaffold the integration files (never overwrites existing files; `--force` to regenerate): diff --git a/CHANGELOG.md b/CHANGELOG.md index a53d087..741565a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -191,6 +191,11 @@ Pending changes since 0.2.1. The release version will be chosen when these chang - Host adoption documentation uses a generic checklist without internal project names or paths. - The resolved config object is validated once instead of on every `getResizeConfig()` call, so the read path no longer re-runs full validation per `resolve()`. +- Optional peer ranges state what the code needs instead of `*`: `@adaptivestone/framework` + `^5.1.0`, `mongoose` `^9.0.0`, and the AWS SDK clients `^3.572.0`. `SqsTaskQueue` reads attempt + counts from `MessageSystemAttributeNames`, which older `@aws-sdk/client-sqs` versions silently + drop: every delivery then counted as attempt 1, so a failing task was never dead-lettered. It + now also warns once when SQS returns no `ApproximateReceiveCount`. # 0.2.1 diff --git a/README.md b/README.md index 5a1b3d1..4875bdd 100644 --- a/README.md +++ b/README.md @@ -32,12 +32,14 @@ part that uses it: | You use | Also install | |---|---| -| `…/framework.js` (framework apps) | `@adaptivestone/framework` `mongoose` (already in a framework app) | -| `…/drivers/mongo.js` | `mongoose` | -| `S3Storage` (`…/drivers/s3.js`) | `@aws-sdk/client-s3` `@aws-sdk/s3-request-presigner` | -| `SqsTaskQueue` (`…/drivers/sqs.js`) | `@aws-sdk/client-sqs` | - -A missing optional peer fails at your own import line, not at the first upload. +| `…/framework.js` (framework apps) | `@adaptivestone/framework` ≥ 5.1 and `mongoose` 9 (already in a framework app) | +| `…/drivers/mongo.js` | `mongoose` 9 | +| S3: `storage: { driver: 's3' }` or `S3Storage` (`…/drivers/s3.js`) | `@aws-sdk/client-s3` `@aws-sdk/s3-request-presigner` ≥ 3.572 | +| SQS: `queue: { driver: 'sqs' }` or `SqsTaskQueue` (`…/drivers/sqs.js`) | `@aws-sdk/client-sqs` ≥ 3.572 | + +A missing optional peer fails at your own import line (or, for a driver chosen in the config, when +the Resizer first loads it), not at the first upload. An older SQS client does not return receive +counts, so failing tasks would never be dead-lettered; `SqsTaskQueue` warns if that happens. ## Quick start (framework, eager) diff --git a/package-lock.json b/package-lock.json index c925859..c58bcbb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,29 +12,29 @@ "sharp": "^0.35.3" }, "bin": { - "resize-scaffold": "dist/scaffold/command.js" + "resize-scaffold": "dist/framework/scaffold/command.js" }, "devDependencies": { - "@adaptivestone/framework": "^5.1.0", - "@aws-sdk/client-s3": "^3.0.0", - "@aws-sdk/client-sqs": "^3.1079.0", - "@aws-sdk/s3-request-presigner": "^3.0.0", + "@adaptivestone/framework": "^5.5.0", + "@aws-sdk/client-s3": "^3.1146.0", + "@aws-sdk/client-sqs": "^3.1146.0", + "@aws-sdk/s3-request-presigner": "^3.1146.0", "@biomejs/biome": "^2.5.2", "@types/express": "^5.0.6", - "@types/node": "^26.1.0", + "@types/node": "^26.6.4", "mongodb-memory-server": "^11.0.0", - "mongoose": "^9.0.0", + "mongoose": "^9.11.0", "typescript": "^7.0.2" }, "engines": { "node": ">=24.0.0" }, "peerDependencies": { - "@adaptivestone/framework": "^5.0.1", - "@aws-sdk/client-s3": "*", - "@aws-sdk/client-sqs": "*", - "@aws-sdk/s3-request-presigner": "*", - "mongoose": "*" + "@adaptivestone/framework": "^5.1.0", + "@aws-sdk/client-s3": "^3.572.0", + "@aws-sdk/client-sqs": "^3.572.0", + "@aws-sdk/s3-request-presigner": "^3.572.0", + "mongoose": "^9.0.0" }, "peerDependenciesMeta": { "@adaptivestone/framework": { @@ -55,9 +55,9 @@ } }, "node_modules/@adaptivestone/framework": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/@adaptivestone/framework/-/framework-5.4.1.tgz", - "integrity": "sha512-aA69OxRo9eA7CsVHr1xNLsCy6wpxrzJZTKjncs1AQBu6jlwThWkyi9I9GTkh5pwzXS6Za2AkF9GZweqW8J21LA==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@adaptivestone/framework/-/framework-5.5.0.tgz", + "integrity": "sha512-i6Q0Mv4l2dBMWKSD3T3he8K1RvCUNRoFunsILZtpNU10xB2Kh6MEW5UrZgotH1IvmFHaoIo4r71R95MI9vCw2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -76,13 +76,13 @@ "peerDependencies": { "@adaptivestone/framework-module-email": "^2.1.0", "@redis/client": "^6.0.0", - "@sentry/core": "^10.34.0", - "@sentry/node": "^10.34.0", + "@sentry/core": "^10.34.0 || ^11.0.0", + "@sentry/node": "^10.34.0 || ^11.0.0", "i18next": "^26.0.1", "i18next-fs-backend": "^2.0.0", "mongodb-memory-server": "^11.0.0", - "oxc-parser": "^0.149.0", - "vitest": "^4.0.0", + "oxc-parser": "^0.152.0", + "vitest": "^4.0.0 || ^5.0.0", "yup": "^1.7.0", "zod": "^3.24.0 || ^4.0.0" }, @@ -140,9 +140,9 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.1144.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1144.0.tgz", - "integrity": "sha512-CAicFlKAuCAUEU1tIptCTh6Eikr4rig9CWYwofZdEuJm9i24XNclgCspV4DKXlOY2nK2AMGHRt88oxmwDdNW7w==", + "version": "3.1146.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1146.0.tgz", + "integrity": "sha512-WY0YCBzxc4muFfY6UbGpA+oib0nGT/Px2aEPGYX7pp1la2OmDLChnK81mu8H7LdYvwGU1pLAsLIa/qgad0Wd7g==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -163,9 +163,9 @@ } }, "node_modules/@aws-sdk/client-sqs": { - "version": "3.1144.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.1144.0.tgz", - "integrity": "sha512-4JOBkMKOuwmer5r8EV9Itbk/NLwxaKRznvUf2DFagt2D811npJPQdyxnRFLFMLW0N8q+A3SRbJpbdmTCAIxagw==", + "version": "3.1146.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.1146.0.tgz", + "integrity": "sha512-JQT0qy87HLkrCJNYaBkzpQuopVFpvafwyrHTRaL55gXCrYRh8y3oJ080squ4X6gmyD2wO7uw4E65ps13KuisJw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -414,9 +414,9 @@ } }, "node_modules/@aws-sdk/s3-request-presigner": { - "version": "3.1144.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1144.0.tgz", - "integrity": "sha512-2dTx+VJx0bpCsDIhbLRI0D4DY0NvuTBpqTf/CKcwF3hXxaN4NcLC/IKasAdwvgckgZf0cjLiBc3VfgQ6vzFU/A==", + "version": "3.1146.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1146.0.tgz", + "integrity": "sha512-5BEfVr2OYX2bwB45NSWAXPoTN5M2NPLOHohgKV+XfMNcL5TF7UQN4hAOVF4S6DwrA4BrI3MsAry7vqdZ2aJEjg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1292,9 +1292,9 @@ } }, "node_modules/@smithy/core": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.35.0.tgz", - "integrity": "sha512-zRMhfkByhT2snNdr1si24vJitU6Cr9ix2MikUfWmkAgp4jrNP0GcKSP5YvwQ+TlI8AZXER5QOGJn3JsVtSD9/A==", + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.35.1.tgz", + "integrity": "sha512-i4YPS4B6ts7bjn7UwLnGjiZdprOvHvgGobFZsYK3GIY3E5hIqtj0rReU69BcTpGp+fvtraSNXeG1l+jtJvF55w==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1450,9 +1450,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", - "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", + "version": "26.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz", + "integrity": "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==", "dev": true, "license": "MIT", "dependencies": { @@ -2494,9 +2494,9 @@ "license": "MIT" }, "node_modules/follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.1.tgz", + "integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==", "dev": true, "funding": [ { @@ -2985,15 +2985,15 @@ } }, "node_modules/mongoose": { - "version": "9.10.3", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.10.3.tgz", - "integrity": "sha512-BQyjLRSJa5DzJ3PP0LHxATG4XX+6ys0J68lFmOlgoXJ+GVkuIsaZXZKxUPYd/t1Tv4qzLfaKkNWaipMIOuy5yw==", + "version": "9.11.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.11.0.tgz", + "integrity": "sha512-dclC0gx/AfasCMX0VChpv4FQ69ylEf3Crve0zKhmYM3VVBK8b0FU8VODx50Th7evM60JxVal8V/1XZQiwLHjwA==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "kareem": "3.4.0", - "mongodb": "~7.6", + "mongodb": "~7.7", "mpath": "0.9.0", "mquery": "6.0.0", "ms": "2.1.3", @@ -3008,15 +3008,15 @@ } }, "node_modules/mongoose/node_modules/mongodb": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.6.0.tgz", - "integrity": "sha512-WbZ6OCjYw2c53LOjfkQa+reXr7kIiOVpXXglnASFuiMtif0BvsMwHe3ClJHLm1/r7wJFwaasfFtD6iYIktB01g==", + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.7.0.tgz", + "integrity": "sha512-XG1xXEqyouAV8GCb3PuRaujofeXKpAJAPvXYWZgHl6Dz4QHSJ1nRLl0+GpqaLTwKvC0RFE9truOur34RNFY1nw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@mongodb-js/saslprep": "^1.4.11", - "bson": "^7.2.0", - "mongodb-connection-string-url": "^7.0.1" + "@mongodb-js/saslprep": "^1.5.0", + "bson": "^7.3.2", + "mongodb-connection-string-url": "^7.0.2" }, "engines": { "node": ">=20.19.0" @@ -3664,16 +3664,16 @@ } }, "node_modules/tar-stream": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", - "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.2.tgz", + "integrity": "sha512-+8NeqHRjQWH9nYlwo2gamAMImZCVzI4UoEgDpWorBt9OEfppiZn+uSkskzQKPWIZyji/C8fpWO7u69G0DX0tbg==", "dev": true, "license": "MIT", "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" + "b4a": "^1.9.0", + "bare-fs": "^4.8.2", + "fast-fifo": "^1.3.2", + "streamx": "^2.28.1" } }, "node_modules/teex": { diff --git a/package.json b/package.json index 71c0e5d..f83958a 100644 --- a/package.json +++ b/package.json @@ -54,11 +54,11 @@ "sharp": "^0.35.3" }, "peerDependencies": { - "@adaptivestone/framework": "^5.0.1", - "@aws-sdk/client-s3": "*", - "@aws-sdk/client-sqs": "*", - "@aws-sdk/s3-request-presigner": "*", - "mongoose": "*" + "@adaptivestone/framework": "^5.1.0", + "@aws-sdk/client-s3": "^3.572.0", + "@aws-sdk/client-sqs": "^3.572.0", + "@aws-sdk/s3-request-presigner": "^3.572.0", + "mongoose": "^9.0.0" }, "peerDependenciesMeta": { "@adaptivestone/framework": { @@ -78,15 +78,15 @@ } }, "devDependencies": { - "@adaptivestone/framework": "^5.1.0", - "@aws-sdk/client-s3": "^3.0.0", - "@aws-sdk/client-sqs": "^3.1079.0", - "@aws-sdk/s3-request-presigner": "^3.0.0", + "@adaptivestone/framework": "^5.5.0", + "@aws-sdk/client-s3": "^3.1146.0", + "@aws-sdk/client-sqs": "^3.1146.0", + "@aws-sdk/s3-request-presigner": "^3.1146.0", "@biomejs/biome": "^2.5.2", "@types/express": "^5.0.6", - "@types/node": "^26.1.0", + "@types/node": "^26.6.4", "mongodb-memory-server": "^11.0.0", - "mongoose": "^9.0.0", + "mongoose": "^9.11.0", "typescript": "^7.0.2" } } diff --git a/src/drivers/sqs.test.ts b/src/drivers/sqs.test.ts index 6ca67fa..984f4fe 100644 --- a/src/drivers/sqs.test.ts +++ b/src/drivers/sqs.test.ts @@ -117,6 +117,7 @@ const newTask = (over: Partial = {}): NewTask => ({ const message = (over: Partial = {}): FakeMessage => ({ MessageId: 'mid', ReceiptHandle: 'rh', + Attributes: { ApproximateReceiveCount: '1' }, Body: JSON.stringify({ mediaId: 'm1', pipeline: 'photo', @@ -410,6 +411,31 @@ describe('SqsTaskQueue.claim', () => { assert.equal(task.queue, 'bulk'); assert.equal(task.attempts, 1); }); + test('no ApproximateReceiveCount (a client before 3.572) reads as attempt 1 and warns once', async () => { + const bare = { ...message(), Attributes: undefined }; + const { client } = makeFakeSqsClient({ + outputs: { + ReceiveMessageCommand: [{ Messages: [bare] }, { Messages: [bare] }], + }, + }); + const warnings: unknown[][] = []; + const tasks = new SqsTaskQueue({ + queueUrl: 'q', + client, + logger: { + info() {}, + warn: (...args: unknown[]) => warnings.push(args), + error() {}, + }, + }); + assert.equal((await tasks.claim('default', 60_000))?.attempts, 1); + assert.equal((await tasks.claim('default', 60_000))?.attempts, 1); + assert.equal(warnings.length, 1); + assert.match( + String(warnings[0][0]), + /never dead-lettered.*client-sqs >= 3\.572/, + ); + }); test('long-polls 10 seconds by default, and the queue named in the body wins', async () => { const { client, receiveParams } = makeFakeSqsClient({ message: message({ diff --git a/src/drivers/sqs.ts b/src/drivers/sqs.ts index 7a526df..4ce5128 100644 --- a/src/drivers/sqs.ts +++ b/src/drivers/sqs.ts @@ -68,6 +68,7 @@ const isLostReceipt = (err: unknown): boolean => { export class SqsTaskQueue extends TaskQueue { readonly #opts: SqsTaskQueueOptions; #client: SQSClient | undefined; + #warnedNoReceiveCount = false; constructor(opts: SqsTaskQueueOptions) { super(); @@ -165,6 +166,15 @@ export class SqsTaskQueue extends TaskQueue { await this.#discard(queueUrl, message, 'malformed task body'); return null; } + const receiveCount = message.Attributes?.ApproximateReceiveCount; + if (receiveCount === undefined && !this.#warnedNoReceiveCount) { + // Clients before @aws-sdk/client-sqs 3.572 drop MessageSystemAttributeNames: every delivery + // then reads as attempt 1, so a failing task would be retried forever. + this.#warnedNoReceiveCount = true; + this.#logger.warn( + 'resize sqs: ReceiveMessage returned no ApproximateReceiveCount — attempts cannot be counted, so failing tasks are never dead-lettered; use @aws-sdk/client-sqs >= 3.572', + ); + } return { taskId: message.MessageId ?? message.ReceiptHandle, resizer: body.resizer ?? 'default', @@ -174,7 +184,7 @@ export class SqsTaskQueue extends TaskQueue { previews: body.previews ?? [], // The URL travels with the token: complete/fail/renew act on the queue it came from. token: JSON.stringify([queueUrl, message.ReceiptHandle]), - attempts: Number(message.Attributes?.ApproximateReceiveCount ?? 1), + attempts: Number(receiveCount ?? 1), }; }