From c44941d698bc864b4e29ca4a53b24b72e8a89c12 Mon Sep 17 00:00:00 2001 From: Div Arora Date: Sun, 17 Apr 2022 08:53:46 +0800 Subject: [PATCH] wip: automatic retries for transient failures --- package-lock.json | 314 +++++++++++++++++++++++++- package.json | 1 + src/lib/types.ts | 140 +++++++----- test/__snapshots__/index.test.ts.snap | 56 +++++ test/index.test.ts | 1 + test/retries.ts | 24 ++ test/server.ts | 61 +++++ 7 files changed, 542 insertions(+), 55 deletions(-) create mode 100644 test/retries.ts create mode 100644 test/server.ts diff --git a/package-lock.json b/package-lock.json index c8a3e345..516efdf6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -404,6 +404,15 @@ "minimist": "^1.2.0" } }, + "@fastify/ajv-compiler": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-1.1.0.tgz", + "integrity": "sha512-gvCOUNpXsWrIQ3A4aXCLIdblL0tDq42BG/2Xw7oxbil9h11uow10ztS2GuFazNBfjbrsZ5nl+nPl5jDSjj5TSg==", + "dev": true, + "requires": { + "ajv": "^6.12.6" + } + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -803,6 +812,12 @@ "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", "dev": true }, + "abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "dev": true + }, "acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", @@ -880,6 +895,12 @@ "picomatch": "^2.0.4" } }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -958,6 +979,24 @@ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true }, + "atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "dev": true + }, + "avvio": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-7.2.5.tgz", + "integrity": "sha512-AOhBxyLVdpOad3TujtC9kL/9r3HnTkxwQ5ggOsYrvvZP1cCFvzHWJd5XxZDFuTn+IN8vkKSG5SEJrd27vCSbeA==", + "dev": true, + "requires": { + "archy": "^1.0.0", + "debug": "^4.0.0", + "fastq": "^1.6.1", + "queue-microtask": "^1.1.2" + } + }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -1349,6 +1388,12 @@ "safe-buffer": "~5.1.1" } }, + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "dev": true + }, "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", @@ -1863,6 +1908,12 @@ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", "dev": true }, + "fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "dev": true + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1875,12 +1926,85 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, + "fast-json-stringify": { + "version": "2.7.13", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-2.7.13.tgz", + "integrity": "sha512-ar+hQ4+OIurUGjSJD1anvYSDcUflywhKjfxnsW4TBTD7+u0tJufv6DKRWoQk3vI6YBOWMoz0TQtfbe7dxbQmvA==", + "dev": true, + "requires": { + "ajv": "^6.11.0", + "deepmerge": "^4.2.2", + "rfdc": "^1.2.0", + "string-similarity": "^4.0.1" + } + }, "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fast-redact": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.1.1.tgz", + "integrity": "sha512-odVmjC8x8jNeMZ3C+rPMESzXVSEU8tSWSHv9HFxP2mm89G/1WwqhrerJDQm9Zus8X6aoRgQDThKqptdNA6bt+A==", + "dev": true + }, + "fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, + "fastify": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-3.28.0.tgz", + "integrity": "sha512-LAQtGllpkRe8L6Tpf3zdbvXzXFOrgaWV3Tbvp3xMv9ngcr9zht9U2/mo5zq9qp9kplSiBJ0w43aVAMqv6PBMbw==", + "dev": true, + "requires": { + "@fastify/ajv-compiler": "^1.0.0", + "abstract-logging": "^2.0.0", + "avvio": "^7.1.2", + "fast-json-stringify": "^2.5.2", + "fastify-error": "^0.3.0", + "find-my-way": "^4.5.0", + "flatstr": "^1.0.12", + "light-my-request": "^4.2.0", + "pino": "^6.13.0", + "process-warning": "^1.0.0", + "proxy-addr": "^2.0.7", + "rfdc": "^1.1.4", + "secure-json-parse": "^2.0.0", + "semver": "^7.3.2", + "tiny-lru": "^8.0.1" + }, + "dependencies": { + "semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "fastify-error": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/fastify-error/-/fastify-error-0.3.1.tgz", + "integrity": "sha512-oCfpcsDndgnDVgiI7bwFKAun2dO+4h84vBlkWsWnz/OUK9Reff5UFoFl241xTiLeHWX/vU9zkDVXqYUxjOwHcQ==", + "dev": true + }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, "fb-watchman": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", @@ -1899,6 +2023,18 @@ "to-regex-range": "^5.0.1" } }, + "find-my-way": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-4.5.1.tgz", + "integrity": "sha512-kE0u7sGoUFbMXcOG/xpkmz4sRLCklERnBcg7Ftuu1iAxsfEt2S46RLJ3Sq7vshsEy2wJT2hZxE58XZK27qa8kg==", + "dev": true, + "requires": { + "fast-decode-uri-component": "^1.0.1", + "fast-deep-equal": "^3.1.3", + "safe-regex2": "^2.0.0", + "semver-store": "^0.3.0" + } + }, "find-package": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/find-package/-/find-package-1.0.0.tgz", @@ -1918,6 +2054,12 @@ "path-exists": "^4.0.0" } }, + "flatstr": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/flatstr/-/flatstr-1.0.12.tgz", + "integrity": "sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw==", + "dev": true + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -1941,6 +2083,12 @@ "mime-types": "^2.1.12" } }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true + }, "fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -2265,6 +2413,12 @@ "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", "dev": true }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true + }, "is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", @@ -3225,6 +3379,38 @@ "type-check": "~0.3.2" } }, + "light-my-request": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-4.9.0.tgz", + "integrity": "sha512-b1U3z4OVPoO/KanT14NRkXMr9rRtXAiq0ORqNrqhDyb5bGkZjAdEc6GRN1GWCfgaLBG+aq73qkCLDNeB3c2sLw==", + "dev": true, + "requires": { + "ajv": "^8.1.0", + "cookie": "^0.4.0", + "process-warning": "^1.0.0", + "set-cookie-parser": "^2.4.1" + }, + "dependencies": { + "ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, "lines-and-columns": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", @@ -3299,7 +3485,6 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, - "optional": true, "requires": { "yallist": "^4.0.0" } @@ -3959,6 +4144,27 @@ "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", "dev": true }, + "pino": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-6.14.0.tgz", + "integrity": "sha512-iuhEDel3Z3hF9Jfe44DPXR8l07bhjuFY3GMHIXbjnY9XcafbyDDwl2sN2vw2GjMPf5Nkoe+OFao7ffn9SXaKDg==", + "dev": true, + "requires": { + "fast-redact": "^3.0.0", + "fast-safe-stringify": "^2.0.8", + "flatstr": "^1.0.12", + "pino-std-serializers": "^3.1.0", + "process-warning": "^1.0.0", + "quick-format-unescaped": "^4.0.3", + "sonic-boom": "^1.0.2" + } + }, + "pino-std-serializers": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz", + "integrity": "sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==", + "dev": true + }, "pirates": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", @@ -4015,6 +4221,12 @@ } } }, + "process-warning": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", + "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==", + "dev": true + }, "progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -4031,6 +4243,16 @@ "sisteransi": "^1.0.4" } }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, "psl": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", @@ -4059,6 +4281,18 @@ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", "dev": true }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "dev": true + }, "quick-lru": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", @@ -4227,6 +4461,12 @@ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "dev": true }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -4269,6 +4509,18 @@ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", "dev": true }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "dev": true + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -4299,6 +4551,23 @@ "ret": "~0.1.10" } }, + "safe-regex2": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz", + "integrity": "sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==", + "dev": true, + "requires": { + "ret": "~0.2.0" + }, + "dependencies": { + "ret": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", + "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", + "dev": true + } + } + }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -4455,6 +4724,12 @@ "xmlchars": "^2.2.0" } }, + "secure-json-parse": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.4.0.tgz", + "integrity": "sha512-Q5Z/97nbON5t/L/sH6mY2EacfjVGwrCcSi5D3btRO2GZ8pf1K1UN7Z9H5J57hjVU2Qzxr1xO+FmBhOvEkzCMmg==", + "dev": true + }, "semantic-release-plugin-update-version-in-files": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/semantic-release-plugin-update-version-in-files/-/semantic-release-plugin-update-version-in-files-1.1.0.tgz", @@ -4471,12 +4746,24 @@ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true }, + "semver-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/semver-store/-/semver-store-0.3.0.tgz", + "integrity": "sha512-TcZvGMMy9vodEFSse30lWinkj+JgOBvPn8wRItpQRSayhc+4ssDs335uklkfvQQJgL/WvmHLVj4Ycv2s7QCQMg==", + "dev": true + }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true }, + "set-cookie-parser": { + "version": "2.4.8", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.4.8.tgz", + "integrity": "sha512-edRH8mBKEWNVIVMKejNnuJxleqYE/ZSdcT8/Nem9/mmosx12pctd80s2Oy00KNZzrogMZS5mauK2/ymL1bvlvg==", + "dev": true + }, "set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", @@ -4685,6 +4972,16 @@ } } }, + "sonic-boom": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.4.1.tgz", + "integrity": "sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==", + "dev": true, + "requires": { + "atomic-sleep": "^1.0.0", + "flatstr": "^1.0.12" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -4838,6 +5135,12 @@ "strip-ansi": "^6.0.0" } }, + "string-similarity": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-4.0.4.tgz", + "integrity": "sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==", + "dev": true + }, "string-width": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", @@ -4967,6 +5270,12 @@ "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", "dev": true }, + "tiny-lru": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-8.0.2.tgz", + "integrity": "sha512-ApGvZ6vVvTNdsmt676grvCkUCGwzG9IqXma5Z07xJgiC5L7akUMof5U8G2JTI9Rz/ovtVhJBlY6mNhEvtjzOIg==", + "dev": true + }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -5478,8 +5787,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "optional": true + "dev": true }, "yargs": { "version": "15.4.1", diff --git a/package.json b/package.json index c271228c..0becdb9c 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "devDependencies": { "@types/jest": "^26.0.13", "@types/node-fetch": "^2.5.7", + "fastify": "^3.28.0", "genversion": "^3.0.1", "jest": "^26.4.1", "node-abort-controller": "^3.0.0", diff --git a/src/lib/types.ts b/src/lib/types.ts index 40a2548f..ede5978c 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -50,6 +50,12 @@ export type PostgrestSingleResponse = | PostgrestResponseFailure export type PostgrestMaybeSingleResponse = PostgrestSingleResponse +const delay = (ms: number) => { + return new Promise((resolve) => setTimeout(() => resolve(), ms)) +} + +const RetryableErrorCodes = new Set([503, 504, 522, 523, 524]) + export abstract class PostgrestBuilder implements PromiseLike> { protected method!: 'GET' | 'HEAD' | 'POST' | 'PATCH' | 'DELETE' protected url!: URL @@ -88,6 +94,35 @@ export abstract class PostgrestBuilder implements PromiseLike { + // no automatic retries for non-read methods + if (!['GET', 'HEAD'].includes(this.method)) { + return Promise.resolve(this.fetch(url, opts)) + } + return new Promise((resolve, reject) => { + const retryingWrapper = (retriesRemaining: number) => { + this.fetch(url, opts) + .then((res) => { + if (!res.ok && RetryableErrorCodes.has(res.status)) { + throw new Error(res.statusText) + } + return res + }) + .then((res) => resolve(res)) + .catch(async (err) => { + if (retriesRemaining > 0) { + console.debug(`fetch failed, retrying in ${retryDelayMs}ms.`) + await delay(retryDelayMs) + retryingWrapper(--retriesRemaining) + } else { + reject(err) + } + }) + } + return retryingWrapper(retries) + }) + } + then, TResult2 = never>( onfulfilled?: | ((value: PostgrestResponse) => TResult1 | PromiseLike) @@ -107,61 +142,12 @@ export abstract class PostgrestBuilder implements PromiseLike { - let error = null - let data = null - let count = null - - if (res.ok) { - const isReturnMinimal = this.headers['Prefer']?.split(',').includes('return=minimal') - if (this.method !== 'HEAD' && !isReturnMinimal) { - const text = await res.text() - if (!text) { - // discard `text` - } else if (this.headers['Accept'] === 'text/csv') { - data = text - } else { - data = JSON.parse(text) - } - } - - const countHeader = this.headers['Prefer']?.match(/count=(exact|planned|estimated)/) - const contentRange = res.headers.get('content-range')?.split('/') - if (countHeader && contentRange && contentRange.length > 1) { - count = parseInt(contentRange[1]) - } - } else { - const body = await res.text() - - try { - error = JSON.parse(body) - } catch { - error = { - message: body, - } - } - - if (error && this.shouldThrowOnError) { - throw error - } - } - - const postgrestResponse = { - error, - data, - count, - status: res.status, - statusText: res.statusText, - body: data, - } - - return postgrestResponse - }) + }, 3, 500).then(this.processResponse) if (!this.shouldThrowOnError) { res = res.catch((fetchError) => ({ error: { @@ -177,7 +163,57 @@ export abstract class PostgrestBuilder implements PromiseLike>(res: Response): Promise> => { + let error = null + let data = null + let count = null + + if (res.ok) { + const isReturnMinimal = this.headers['Prefer']?.split(',').includes('return=minimal') + if (this.method !== 'HEAD' && !isReturnMinimal) { + const text = await res.text() + if (!text) { + // discard `text` + } else if (this.headers['Accept'] === 'text/csv') { + data = text + } else { + data = JSON.parse(text) + } + } + + const countHeader = this.headers['Prefer']?.match(/count=(exact|planned|estimated)/) + const contentRange = res.headers.get('content-range')?.split('/') + if (countHeader && contentRange && contentRange.length > 1) { + count = parseInt(contentRange[1]) + } + } else { + const body = await res.text() + + try { + error = JSON.parse(body) + } catch { + error = { + message: body, + } + } + + if (error && this.shouldThrowOnError) { + throw error + } + } + + const postgrestResponse = { + error, + data, + count, + status: res.status, + statusText: res.statusText, + body: data, + } + + return postgrestResponse + } } diff --git a/test/__snapshots__/index.test.ts.snap b/test/__snapshots__/index.test.ts.snap index b102111f..4e5072ae 100644 --- a/test/__snapshots__/index.test.ts.snap +++ b/test/__snapshots__/index.test.ts.snap @@ -1,5 +1,35 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`503 temporarily failure - retried and successful 1`] = ` +Object { + "body": Object { + "hello": "world", + }, + "count": null, + "data": Object { + "hello": "world", + }, + "error": null, + "status": 200, + "statusText": "OK", +} +`; + +exports[`524 failure - cf timeout - retried 1`] = ` +Object { + "body": Object { + "hello": "world", + }, + "count": null, + "data": Object { + "hello": "world", + }, + "error": null, + "status": 200, + "statusText": "OK", +} +`; + exports[`Prefer: return=minimal 1`] = `null`; exports[`allow ordering on JSON column 1`] = ` @@ -35,6 +65,19 @@ Array [ ] `; +exports[`bad request - not retried 1`] = ` +Object { + "body": null, + "count": null, + "data": null, + "error": Object { + "message": "", + }, + "status": 400, + "statusText": "Bad Request", +} +`; + exports[`basic insert, update, delete basic delete 1`] = ` Object { "body": Array [ @@ -1122,6 +1165,19 @@ Object { } `; +exports[`generic failure table - not retried 1`] = ` +Object { + "body": null, + "count": null, + "data": null, + "error": Object { + "message": "", + }, + "status": 500, + "statusText": "Internal Server Error", +} +`; + exports[`ignoreDuplicates upsert 1`] = ` Object { "body": Array [], diff --git a/test/index.test.ts b/test/index.test.ts index ad4e4ee7..de2c9af3 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,3 +1,4 @@ +import './retries' import './basic' import './filters' import './resource-embedding' diff --git a/test/retries.ts b/test/retries.ts new file mode 100644 index 00000000..d1bcb918 --- /dev/null +++ b/test/retries.ts @@ -0,0 +1,24 @@ +import { PostgrestClient } from '../src/index' + +const REST_URL = 'http://localhost:3001' +const postgrest = new PostgrestClient(REST_URL) + +test('503 temporarily failure - retried and successful', async () => { + const res = await postgrest.from('temporary503failure').select() + expect(res).toMatchSnapshot() +}) + +test('generic failure table - not retried', async () => { + const res = await postgrest.from('generic500failure').select() + expect(res).toMatchSnapshot() +}) + +test('bad request - not retried', async () => { + const res = await postgrest.from('badrequest').select() + expect(res).toMatchSnapshot() +}) + +test('524 failure - cf timeout - retried', async () => { + const res = await postgrest.from('timeout524failure').select() + expect(res).toMatchSnapshot() +}) diff --git a/test/server.ts b/test/server.ts new file mode 100644 index 00000000..2b5891a5 --- /dev/null +++ b/test/server.ts @@ -0,0 +1,61 @@ +import fastify from 'fastify' + +function build(opts = {}) { + const app = fastify(opts) + let count_503 = 1, count_500 = 1, count_400 = 1, count_524 = 1, count_504 = 1 + + app.get('/temporary503failure', async function(request, reply) { + if (count_503 < 2) { + count_503++; + return reply.code(503).send() + } + return { hello: 'world' } + }) + + app.get('/timeout504failure', async function(request, reply) { + if (count_504 < 2) { + count_504++; + return reply.code(504).send() + } + return { hello: 'world' } + }) + + app.get('/timeout524failure', async function(request, reply) { + if (count_524 < 2) { + count_524++; + return reply.code(524).send() + } + return { hello: 'world' } + }) + + app.get('/generic500failure', async function(request, reply) { + if (count_500 < 2) { + count_500++; + return reply.code(500).send() + } + return { hello: 'world' } + }) + + app.get('/badrequest', async function(request, reply) { + if (count_400 < 2) { + count_400++; + return reply.code(400).send() + } + return { hello: 'world' } + }) + + return app +} + +const server = build({ + logger: { + level: 'info', + } +}) + +server.listen(3001, (err, address) => { + if (err) { + server.log.error(err) + process.exit(1) + } +})