From 991f808890739eafad1cd55a1f5d2e0addcef7f9 Mon Sep 17 00:00:00 2001 From: Mythie Date: Thu, 16 May 2024 15:44:39 +1000 Subject: [PATCH] feat: ghetto durable compute --- .vscode/settings.json | 14 +- apps/marketing/package.json | 4 +- apps/web/package.json | 4 +- package-lock.json | 515 ++++++++++++++---- package.json | 6 +- packages/ee/package.json | 2 +- packages/lib/jobs/client.ts | 3 + packages/lib/jobs/client/_internal/job.ts | 10 +- packages/lib/jobs/client/_internal/json.ts | 8 +- packages/lib/jobs/client/local.ts | 250 ++++++++- packages/lib/jobs/client/trigger.ts | 2 +- packages/lib/jobs/definitions/index.ts | 7 +- .../definitions/send-confirmation-email.ts | 62 ++- packages/lib/package.json | 2 +- .../migration.sql | 37 ++ .../migration.sql | 9 + .../migration.sql | 2 + .../migration.sql | 9 + packages/prisma/schema.prisma | 50 ++ packages/ui/package.json | 2 +- 20 files changed, 847 insertions(+), 151 deletions(-) create mode 100644 packages/prisma/migrations/20240516022236_add_background_job_models/migration.sql create mode 100644 packages/prisma/migrations/20240516030441_add_more_timestamp_columns_for_background_jobs/migration.sql create mode 100644 packages/prisma/migrations/20240516032317_capture_payload_for_background_job/migration.sql create mode 100644 packages/prisma/migrations/20240516034546_update_background_job_task_model/migration.sql diff --git a/.vscode/settings.json b/.vscode/settings.json index 1fc8321db..f5542fbb5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,11 +5,19 @@ "editor.codeActionsOnSave": { "source.fixAll": "explicit" }, - "eslint.validate": ["typescript", "typescriptreact", "javascript", "javascriptreact"], + "eslint.validate": [ + "typescript", + "typescriptreact", + "javascript", + "javascriptreact" + ], "javascript.preferences.importModuleSpecifier": "non-relative", "javascript.preferences.useAliasesForRenames": false, "typescript.enablePromptUseWorkspaceTsdk": true, "files.eol": "\n", "editor.tabSize": 2, - "editor.insertSpaces": true -} + "editor.insertSpaces": true, + "[prisma]": { + "editor.defaultFormatter": "Prisma.prisma" + }, +} \ No newline at end of file diff --git a/apps/marketing/package.json b/apps/marketing/package.json index da6c490b3..b56cf2f7b 100644 --- a/apps/marketing/package.json +++ b/apps/marketing/package.json @@ -35,9 +35,9 @@ "next-plausible": "^3.10.1", "perfect-freehand": "^1.2.0", "posthog-js": "^1.77.3", - "react": "18.3.1", + "react": "18.2.0", "react-confetti": "^6.1.0", - "react-dom": "18.3.1", + "react-dom": "18.2.0", "react-hook-form": "^7.43.9", "react-icons": "^4.11.0", "recharts": "^2.7.2", diff --git a/apps/web/package.json b/apps/web/package.json index de6efc856..4b5ff1dc9 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -41,8 +41,8 @@ "perfect-freehand": "^1.2.0", "posthog-js": "^1.75.3", "posthog-node": "^3.1.1", - "react": "18.3.1", - "react-dom": "18.3.1", + "react": "18.2.0", + "react-dom": "18.2.0", "react-dropzone": "^14.2.3", "react-hook-form": "^7.43.9", "react-hotkeys-hook": "^4.4.1", diff --git a/package-lock.json b/package-lock.json index 1325a32d1..70c8a8e17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ ], "dependencies": { "@documenso/pdf-sign": "^0.1.0", - "next-runtime-env": "^3.2.0" + "next-runtime-env": "^3.2.0", + "react": "18.2.0" }, "devDependencies": { "@commitlint/cli": "^17.7.1", @@ -60,9 +61,9 @@ "next-plausible": "^3.10.1", "perfect-freehand": "^1.2.0", "posthog-js": "^1.77.3", - "react": "18.3.1", + "react": "18.2.0", "react-confetti": "^6.1.0", - "react-dom": "18.3.1", + "react-dom": "18.2.0", "react-hook-form": "^7.43.9", "react-icons": "^4.11.0", "recharts": "^2.7.2", @@ -82,37 +83,6 @@ "integrity": "sha512-O+z53uwx64xY7D6roOi4+jApDGFg0qn6WHcxe5QeqjMaTezBO/mxdfFXIVAVVyNWKx84OmPB3L8kbVYOTeN34A==", "dev": true }, - "apps/marketing/node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "apps/marketing/node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "apps/marketing/node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, "apps/marketing/node_modules/typescript": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", @@ -157,8 +127,8 @@ "perfect-freehand": "^1.2.0", "posthog-js": "^1.75.3", "posthog-node": "^3.1.1", - "react": "18.3.1", - "react-dom": "18.3.1", + "react": "18.2.0", + "react-dom": "18.2.0", "react-dropzone": "^14.2.3", "react-hook-form": "^7.43.9", "react-hotkeys-hook": "^4.4.1", @@ -190,37 +160,6 @@ "integrity": "sha512-O+z53uwx64xY7D6roOi4+jApDGFg0qn6WHcxe5QeqjMaTezBO/mxdfFXIVAVVyNWKx84OmPB3L8kbVYOTeN34A==", "dev": true }, - "apps/web/node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "apps/web/node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "apps/web/node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, "apps/web/node_modules/typescript": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", @@ -4270,6 +4209,27 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/@npmcli/installed-package-contents/node_modules/npm-bundled": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.1.tgz", + "integrity": "sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==", + "dev": true, + "dependencies": { + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/installed-package-contents/node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/@npmcli/move-file": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", @@ -8728,6 +8688,15 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", @@ -9347,6 +9316,52 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "devOptional": true }, + "node_modules/@tufjs/canonical-json": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-1.0.0.tgz", + "integrity": "sha512-QTnf++uxunWvG2z3UFNzAoQPHxnSXOwtaI3iJ+AohhV+5vONuArPjJE7aPXPVXfXJsqrVbZBu9b81AJoSd09IQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-1.0.4.tgz", + "integrity": "sha512-qaGV9ltJP0EO25YfFUPhxRVK0evXFIAGicsVXuRim4Ed9cjPxYhNnNJ49SFmbeLgtxpslIkX317IgpfcHPVj/A==", + "dev": true, + "dependencies": { + "@tufjs/canonical-json": "1.0.0", + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@tufjs/models/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.8.1.tgz", @@ -12259,6 +12274,21 @@ "url": "https://github.com/yeoman/configstore?sponsor=1" } }, + "node_modules/configstore/node_modules/crypto-random-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", + "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", + "dev": true, + "dependencies": { + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/configstore/node_modules/dot-prop": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", @@ -12274,6 +12304,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/configstore/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/configstore/node_modules/unique-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", + "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", + "dev": true, + "dependencies": { + "crypto-random-string": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -18955,6 +19012,20 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/make-fetch-happen/node_modules/lru-cache": { "version": "7.18.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", @@ -20356,6 +20427,15 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/netmask": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", @@ -20734,6 +20814,20 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/node-gyp/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/node-gyp/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -21933,6 +22027,203 @@ "pjv": "bin/pjv" } }, + "node_modules/package-json/node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/package-json/node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "dev": true, + "dependencies": { + "defer-to-connect": "^2.0.1" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/package-json/node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "dev": true, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/package-json/node_modules/cacheable-request": { + "version": "10.2.14", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", + "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", + "dev": true, + "dependencies": { + "@types/http-cache-semantics": "^4.0.2", + "get-stream": "^6.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.3", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/package-json/node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json/node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json/node_modules/form-data-encoder": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "dev": true, + "engines": { + "node": ">= 14.17" + } + }, + "node_modules/package-json/node_modules/got": { + "version": "12.6.1", + "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", + "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", + "dev": true, + "dependencies": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/package-json/node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "dev": true, + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/package-json/node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json/node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json/node_modules/normalize-url": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", + "integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json/node_modules/p-cancelable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", + "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "dev": true, + "engines": { + "node": ">=12.20" + } + }, + "node_modules/package-json/node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json/node_modules/responselike": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", + "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", + "dev": true, + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/packet-reader": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", @@ -23074,6 +23365,12 @@ "node": ">=0.4.0" } }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true + }, "node_modules/promise-retry": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", @@ -24386,6 +24683,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/read-package-json-fast/node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/read-package-json/node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -24486,6 +24792,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/read-package-json/node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -27889,6 +28204,20 @@ "@esbuild/win32-x64": "0.20.2" } }, + "node_modules/tuf-js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-1.1.7.tgz", + "integrity": "sha512-i3P9Kgw3ytjELUfpuKVDNBJvk4u5bXL6gskv572mcevPbSKCV3zt3djhmlEQ65yERjIbOSncy7U4cQJaB1CBCg==", + "dev": true, + "dependencies": { + "@tufjs/models": "1.0.4", + "debug": "^4.3.4", + "make-fetch-happen": "^11.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/turbo": { "version": "1.10.16", "resolved": "https://registry.npmjs.org/turbo/-/turbo-1.10.16.tgz", @@ -28376,6 +28705,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/unique-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", @@ -30128,22 +30481,11 @@ "micro": "^10.0.1", "next": "14.0.3", "next-auth": "4.24.5", - "react": "18.3.1", + "react": "18.2.0", "ts-pattern": "^5.0.5", "zod": "^3.22.4" } }, - "packages/ee/node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "packages/email": { "name": "@documenso/email", "version": "1.0.0", @@ -31339,7 +31681,7 @@ "pdf-lib": "^1.17.1", "pg": "^8.11.3", "playwright": "1.43.0", - "react": "18.3.1", + "react": "18.2.0", "remeda": "^1.27.1", "stripe": "^12.7.0", "ts-pattern": "^5.0.5", @@ -31387,17 +31729,6 @@ "node": "^14 || ^16 || >=18" } }, - "packages/lib/node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "packages/prettier-config": { "name": "@documenso/prettier-config", "version": "0.0.0", @@ -31800,7 +32131,7 @@ "@types/luxon": "^3.3.2", "@types/react": "18.2.18", "@types/react-dom": "18.2.7", - "react": "18.3.1", + "react": "18.2.0", "typescript": "5.2.2" } }, diff --git a/package.json b/package.json index cea9d9c43..e66f4dd1b 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,8 @@ ], "dependencies": { "@documenso/pdf-sign": "^0.1.0", - "next-runtime-env": "^3.2.0" + "next-runtime-env": "^3.2.0", + "react": "18.2.0" }, "overrides": { "next-auth": { @@ -61,7 +62,8 @@ }, "next-contentlayer": { "next": "14.0.3" - } + }, + "react": "18.2.0" }, "trigger.dev": { "endpointId": "documenso-app" diff --git a/packages/ee/package.json b/packages/ee/package.json index ede8ed6de..48ff0ff24 100644 --- a/packages/ee/package.json +++ b/packages/ee/package.json @@ -19,7 +19,7 @@ "micro": "^10.0.1", "next": "14.0.3", "next-auth": "4.24.5", - "react": "18.3.1", + "react": "18.2.0", "ts-pattern": "^5.0.5", "zod": "^3.22.4" } diff --git a/packages/lib/jobs/client.ts b/packages/lib/jobs/client.ts index 04a64c900..28c712da6 100644 --- a/packages/lib/jobs/client.ts +++ b/packages/lib/jobs/client.ts @@ -1,3 +1,6 @@ import { JobClient } from './client/client'; +import { registerJobs } from './definitions'; export const jobsClient = JobClient.getInstance(); + +registerJobs(jobsClient); diff --git a/packages/lib/jobs/client/_internal/job.ts b/packages/lib/jobs/client/_internal/job.ts index f21c5ac41..c2bee58c5 100644 --- a/packages/lib/jobs/client/_internal/job.ts +++ b/packages/lib/jobs/client/_internal/job.ts @@ -1,16 +1,20 @@ import { z } from 'zod'; +import type { Json } from './json'; + export const ZTriggerJobOptionsSchema = z.object({ id: z.string().optional(), name: z.string(), - payload: z.unknown().refine((x) => x !== undefined, { message: 'payload is required' }), + payload: z.any().refine((x) => x !== undefined, { message: 'payload is required' }), timestamp: z.number().optional(), }); // The Omit is a temporary workaround for a "bug" in the zod library // @see: https://github.com/colinhacks/zod/issues/2966 export type TriggerJobOptions = Omit, 'payload'> & { - payload: unknown; + // Don't tell the feds + // eslint-disable-next-line @typescript-eslint/no-explicit-any + payload: any; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -28,7 +32,7 @@ export type JobDefinition = { export interface JobRunIO { // stableRun(cacheKey: string, callback: (io: JobRunIO) => T | Promise): Promise; - stableRun(cacheKey: string, callback: () => Promise): Promise; + runTask(cacheKey: string, callback: () => Promise): Promise; triggerJob(cacheKey: string, options: TriggerJobOptions): Promise; wait(cacheKey: string, ms: number): Promise; logger: { diff --git a/packages/lib/jobs/client/_internal/json.ts b/packages/lib/jobs/client/_internal/json.ts index e9377eab4..0aff01742 100644 --- a/packages/lib/jobs/client/_internal/json.ts +++ b/packages/lib/jobs/client/_internal/json.ts @@ -2,13 +2,13 @@ * Below type is borrowed from Trigger.dev's SDK, it may be moved elsewhere later. */ -type JsonPrimitive = string | number | boolean | null | undefined | Date | symbol; +export type JsonPrimitive = string | number | boolean | null | undefined | Date | symbol; -type JsonArray = Json[]; +export type JsonArray = Json[]; -type JsonRecord = { +export type JsonRecord = { [Property in keyof T]: Json; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any -type Json = JsonPrimitive | JsonArray | JsonRecord; +export type Json = JsonPrimitive | JsonArray | JsonRecord; diff --git a/packages/lib/jobs/client/local.ts b/packages/lib/jobs/client/local.ts index 13999f299..e48e69f5d 100644 --- a/packages/lib/jobs/client/local.ts +++ b/packages/lib/jobs/client/local.ts @@ -1,11 +1,16 @@ import type { NextApiRequest, NextApiResponse } from 'next'; +import { sha256 } from '@noble/hashes/sha256'; import { json } from 'micro'; +import { prisma } from '@documenso/prisma'; +import { BackgroundJobStatus, Prisma } from '@documenso/prisma/client'; + import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app'; import { sign } from '../../server-only/crypto/sign'; import { verify } from '../../server-only/crypto/verify'; import type { JobDefinition, JobRunIO, TriggerJobOptions } from './_internal/job'; +import type { Json } from './_internal/json'; import { BaseJobProvider } from './base'; export class LocalJobProvider extends BaseJobProvider { @@ -33,33 +38,57 @@ export class LocalJobProvider extends BaseJobProvider { } public async triggerJob(options: TriggerJobOptions) { - const signature = sign(options); + console.log({ jobDefinitions: this._jobDefinitions }); - await Promise.race([ - fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/api/jobs/trigger`, { - method: 'POST', - body: JSON.stringify(options), - headers: { - 'Content-Type': 'application/json', - 'X-Job-Signature': signature, - }, + const eligibleJobs = Object.values(this._jobDefinitions).filter( + (job) => job.trigger.name === options.name, + ); + + console.log({ options }); + console.log( + 'Eligible jobs:', + eligibleJobs.map((job) => job.name), + ); + + await Promise.all( + eligibleJobs.map(async (job) => { + // Ideally we will change this to a createMany with returning later once we upgrade Prisma + // @see: https://github.com/prisma/prisma/releases/tag/5.14.0 + const pendingJob = await prisma.backgroundJob.create({ + data: { + jobId: job.id, + name: job.name, + version: job.version, + payload: options.payload, + }, + }); + + await this.submitJobToEndpoint({ + jobId: pendingJob.id, + jobDefinitionId: pendingJob.jobId, + data: options, + }); }), - new Promise((resolve) => { - setTimeout(resolve, 150); - }), - ]); + ); } public getApiHandler() { return async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'POST') { + const jobId = req.headers['x-job-id']; const signature = req.headers['x-job-signature']; + const isRetry = req.headers['x-job-retry'] !== undefined; + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const options = (await json(req)) as TriggerJobOptions; const definition = this._jobDefinitions[options.name]; - if (typeof signature !== 'string' || typeof options !== 'object') { + if ( + typeof jobId !== 'string' || + typeof signature !== 'string' || + typeof options !== 'object' + ) { res.status(400).send('Bad request'); return; } @@ -92,10 +121,83 @@ export class LocalJobProvider extends BaseJobProvider { console.log(`[JOBS]: Triggering job ${options.name} with payload`, options.payload); - await definition.handler({ - payload: options.payload, - io: this.createJobRunIO(options.name), - }); + let backgroundJob = await prisma.backgroundJob + .update({ + where: { + id: jobId, + status: BackgroundJobStatus.PENDING, + }, + data: { + status: BackgroundJobStatus.PROCESSING, + retried: { + increment: isRetry ? 1 : 0, + }, + lastRetriedAt: isRetry ? new Date() : undefined, + }, + }) + .catch(() => null); + + if (!backgroundJob) { + res.status(404).send('Job not found'); + return; + } + + try { + await definition.handler({ + payload: options.payload, + io: this.createJobRunIO(jobId), + }); + + backgroundJob = await prisma.backgroundJob.update({ + where: { + id: jobId, + status: BackgroundJobStatus.PROCESSING, + }, + data: { + status: BackgroundJobStatus.COMPLETED, + completedAt: new Date(), + }, + }); + } catch (error) { + console.error(`[JOBS]: Job ${options.name} failed`, error); + + const taskHasExceededRetries = error instanceof BackgroundTaskExceededRetriesError; + const jobHasExceededRetries = + backgroundJob.retried >= backgroundJob.maxRetries && + !(error instanceof BackgroundTaskFailedError); + + if (taskHasExceededRetries || jobHasExceededRetries) { + backgroundJob = await prisma.backgroundJob.update({ + where: { + id: jobId, + status: BackgroundJobStatus.PROCESSING, + }, + data: { + status: BackgroundJobStatus.FAILED, + completedAt: new Date(), + }, + }); + + res.status(500).send('Task exceeded retries'); + return; + } + + backgroundJob = await prisma.backgroundJob.update({ + where: { + id: jobId, + status: BackgroundJobStatus.PROCESSING, + }, + data: { + status: BackgroundJobStatus.PENDING, + }, + }); + + await this.submitJobToEndpoint({ + jobId, + jobDefinitionId: backgroundJob.jobId, + data: options, + }); + } res.status(200).send('OK'); } else { @@ -104,9 +206,105 @@ export class LocalJobProvider extends BaseJobProvider { }; } + private async submitJobToEndpoint(options: { + jobId: string; + jobDefinitionId: string; + data: TriggerJobOptions; + isRetry?: boolean; + }) { + const { jobId, jobDefinitionId, data, isRetry } = options; + + const endpoint = `${NEXT_PUBLIC_WEBAPP_URL()}/api/jobs/${jobDefinitionId}/${jobId}`; + const signature = sign(data); + + const headers: Record = { + 'Content-Type': 'application/json', + 'X-Job-Id': jobId, + 'X-Job-Signature': signature, + }; + + if (isRetry) { + headers['X-Job-Retry'] = '1'; + } + + console.log('Submitting job to endpoint:', endpoint); + await Promise.race([ + fetch(endpoint, { + method: 'POST', + body: JSON.stringify(data), + headers, + }).catch(() => null), + new Promise((resolve) => { + setTimeout(resolve, 150); + }), + ]); + } + private createJobRunIO(jobId: string): JobRunIO { return { - stableRun: async (_cacheKey, callback) => await callback(), + runTask: async (cacheKey: string, callback: () => Promise) => { + const hashedKey = Buffer.from(sha256(cacheKey)).toString('hex'); + + let task = await prisma.backgroundJobTask.findFirst({ + where: { + id: `task-${hashedKey}--${jobId}`, + jobId, + }, + }); + + if (!task) { + task = await prisma.backgroundJobTask.create({ + data: { + id: `task-${hashedKey}--${jobId}`, + name: cacheKey, + jobId, + status: BackgroundJobStatus.PENDING, + }, + }); + } + + if (task.status === BackgroundJobStatus.COMPLETED) { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return task.result as T; + } + + if (task.retried >= 3) { + throw new BackgroundTaskExceededRetriesError('Task exceeded retries'); + } + + try { + const result = await callback(); + + task = await prisma.backgroundJobTask.update({ + where: { + id: task.id, + jobId, + }, + data: { + status: BackgroundJobStatus.COMPLETED, + result: result === null ? Prisma.JsonNull : result, + completedAt: new Date(), + }, + }); + + return result; + } catch { + task = await prisma.backgroundJobTask.update({ + where: { + id: task.id, + jobId, + }, + data: { + status: BackgroundJobStatus.PENDING, + retried: { + increment: 1, + }, + }, + }); + + throw new BackgroundTaskFailedError('Task failed'); + } + }, triggerJob: async (_cacheKey, payload) => await this.triggerJob(payload), logger: { debug: (...args) => console.debug(`[${jobId}]`, ...args), @@ -122,3 +320,17 @@ export class LocalJobProvider extends BaseJobProvider { }; } } + +class BackgroundTaskFailedError extends Error { + constructor(message: string) { + super(message); + this.name = 'BackgroundTaskFailedError'; + } +} + +class BackgroundTaskExceededRetriesError extends Error { + constructor(message: string) { + super(message); + this.name = 'BackgroundTaskExceededRetriesError'; + } +} diff --git a/packages/lib/jobs/client/trigger.ts b/packages/lib/jobs/client/trigger.ts index 7c5682273..40fbc1e83 100644 --- a/packages/lib/jobs/client/trigger.ts +++ b/packages/lib/jobs/client/trigger.ts @@ -64,7 +64,7 @@ export class TriggerJobProvider extends BaseJobProvider { return { wait: io.wait, logger: io.logger, - stableRun: async (cacheKey, callback) => io.runTask(cacheKey, callback), + runTask: async (cacheKey, callback) => io.runTask(cacheKey, callback), triggerJob: async (cacheKey, payload) => io.sendEvent(cacheKey, { ...payload, diff --git a/packages/lib/jobs/definitions/index.ts b/packages/lib/jobs/definitions/index.ts index 4a413baa0..9d1b0a78b 100644 --- a/packages/lib/jobs/definitions/index.ts +++ b/packages/lib/jobs/definitions/index.ts @@ -1 +1,6 @@ -export * from './send-confirmation-email'; +import type { JobClient } from '../client/client'; +import { registerSendConfirmationEmailJob } from './send-confirmation-email'; + +export const registerJobs = (client: JobClient) => { + registerSendConfirmationEmailJob(client); +}; diff --git a/packages/lib/jobs/definitions/send-confirmation-email.ts b/packages/lib/jobs/definitions/send-confirmation-email.ts index 38e2ea7d0..1439bdf9c 100644 --- a/packages/lib/jobs/definitions/send-confirmation-email.ts +++ b/packages/lib/jobs/definitions/send-confirmation-email.ts @@ -1,23 +1,47 @@ import { z } from 'zod'; import { sendConfirmationToken } from '../../server-only/user/send-confirmation-token'; -import { jobsClient } from '../client'; +import type { JobClient } from '../client/client'; -jobsClient.defineJob({ - id: 'send.confirmation.email', - name: 'Send Confirmation Email', - version: '1.0.0', - trigger: { - name: 'send.confirmation.email', - schema: z.object({ - email: z.string().email(), - force: z.boolean().optional(), - }), - }, - handler: async ({ payload }) => { - await sendConfirmationToken({ - email: payload.email, - force: payload.force, - }); - }, -}); +export const registerSendConfirmationEmailJob = (client: JobClient) => { + client.defineJob({ + id: 'send.confirmation.email', + name: 'Send Confirmation Email', + version: '1.0.0', + trigger: { + name: 'send.confirmation.email', + schema: z.object({ + email: z.string().email(), + force: z.boolean().optional(), + }), + }, + handler: async ({ payload, io }) => { + console.log('---- start job ----'); + + // eslint-disable-next-line @typescript-eslint/require-await + const result = await io.runTask('console-log-1', async () => { + console.log('Task 1'); + + return 5; + }); + + console.log({ result }); + + console.log('always runs'); + + // eslint-disable-next-line @typescript-eslint/require-await + await io.runTask('console-log-2', async () => { + await Promise.resolve(null); + throw new Error('dang2'); + }); + + console.log('---- end job ----'); + + // throw new Error('dang') + await sendConfirmationToken({ + email: payload.email, + force: payload.force, + }); + }, + }); +}; diff --git a/packages/lib/package.json b/packages/lib/package.json index bb78e67a5..e0cf03da6 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -45,7 +45,7 @@ "pdf-lib": "^1.17.1", "pg": "^8.11.3", "playwright": "1.43.0", - "react": "18.3.1", + "react": "18.2.0", "remeda": "^1.27.1", "stripe": "^12.7.0", "ts-pattern": "^5.0.5", diff --git a/packages/prisma/migrations/20240516022236_add_background_job_models/migration.sql b/packages/prisma/migrations/20240516022236_add_background_job_models/migration.sql new file mode 100644 index 000000000..9baeafbdd --- /dev/null +++ b/packages/prisma/migrations/20240516022236_add_background_job_models/migration.sql @@ -0,0 +1,37 @@ +-- CreateEnum +CREATE TYPE "BackgroundJobStatus" AS ENUM ('PENDING', 'PROCESSING', 'COMPLETED', 'FAILED'); + +-- CreateEnum +CREATE TYPE "BackgroundJobTaskStatus" AS ENUM ('PENDING', 'COMPLETED', 'FAILED'); + +-- CreateTable +CREATE TABLE "BackgroundJob" ( + "id" TEXT NOT NULL, + "status" "BackgroundJobStatus" NOT NULL DEFAULT 'PENDING', + "retried" INTEGER NOT NULL DEFAULT 0, + "maxRetries" INTEGER NOT NULL DEFAULT 3, + "jobId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "version" TEXT NOT NULL, + "submittedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastRetriedAt" TIMESTAMP(3), + + CONSTRAINT "BackgroundJob_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "BackgroundJobTask" ( + "id" TEXT NOT NULL, + "status" "BackgroundJobTaskStatus" NOT NULL DEFAULT 'PENDING', + "result" JSONB, + "retried" INTEGER NOT NULL DEFAULT 0, + "maxRetries" INTEGER NOT NULL DEFAULT 3, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "jobId" TEXT NOT NULL, + + CONSTRAINT "BackgroundJobTask_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "BackgroundJobTask" ADD CONSTRAINT "BackgroundJobTask_jobId_fkey" FOREIGN KEY ("jobId") REFERENCES "BackgroundJob"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/prisma/migrations/20240516030441_add_more_timestamp_columns_for_background_jobs/migration.sql b/packages/prisma/migrations/20240516030441_add_more_timestamp_columns_for_background_jobs/migration.sql new file mode 100644 index 000000000..6a326cdca --- /dev/null +++ b/packages/prisma/migrations/20240516030441_add_more_timestamp_columns_for_background_jobs/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - Added the required column `updatedAt` to the `BackgroundJob` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "BackgroundJob" ADD COLUMN "completedAt" TIMESTAMP(3), +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; diff --git a/packages/prisma/migrations/20240516032317_capture_payload_for_background_job/migration.sql b/packages/prisma/migrations/20240516032317_capture_payload_for_background_job/migration.sql new file mode 100644 index 000000000..316546da0 --- /dev/null +++ b/packages/prisma/migrations/20240516032317_capture_payload_for_background_job/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "BackgroundJob" ADD COLUMN "payload" JSONB; diff --git a/packages/prisma/migrations/20240516034546_update_background_job_task_model/migration.sql b/packages/prisma/migrations/20240516034546_update_background_job_task_model/migration.sql new file mode 100644 index 000000000..553e35273 --- /dev/null +++ b/packages/prisma/migrations/20240516034546_update_background_job_task_model/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - Added the required column `name` to the `BackgroundJobTask` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "BackgroundJobTask" ADD COLUMN "completedAt" TIMESTAMP(3), +ADD COLUMN "name" TEXT NOT NULL; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index cb1456d2b..c99acdc2a 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -612,3 +612,53 @@ model SiteSettings { lastModifiedAt DateTime @default(now()) lastModifiedByUser User? @relation(fields: [lastModifiedByUserId], references: [id], onDelete: SetNull) } + +enum BackgroundJobStatus { + PENDING + PROCESSING + COMPLETED + FAILED +} + +model BackgroundJob { + id String @id @default(cuid()) + status BackgroundJobStatus @default(PENDING) + payload Json? + retried Int @default(0) + maxRetries Int @default(3) + + // Taken from the job definition + jobId String + name String + version String + + submittedAt DateTime @default(now()) + updatedAt DateTime @updatedAt + completedAt DateTime? + lastRetriedAt DateTime? + + tasks BackgroundJobTask[] +} + +enum BackgroundJobTaskStatus { + PENDING + COMPLETED + FAILED +} + +model BackgroundJobTask { + id String @id + name String + status BackgroundJobTaskStatus @default(PENDING) + + result Json? + retried Int @default(0) + maxRetries Int @default(3) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + completedAt DateTime? + + jobId String + backgroundJob BackgroundJob @relation(fields: [jobId], references: [id], onDelete: Cascade) +} diff --git a/packages/ui/package.json b/packages/ui/package.json index 26a59d45f..964cd37d2 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -22,7 +22,7 @@ "@types/luxon": "^3.3.2", "@types/react": "18.2.18", "@types/react-dom": "18.2.7", - "react": "18.3.1", + "react": "18.2.0", "typescript": "5.2.2" }, "dependencies": {