feat: ghetto durable compute

This commit is contained in:
Mythie
2024-05-16 15:44:39 +10:00
parent 61827ad729
commit 991f808890
20 changed files with 847 additions and 151 deletions

14
.vscode/settings.json vendored
View File

@ -5,11 +5,19 @@
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": "explicit" "source.fixAll": "explicit"
}, },
"eslint.validate": ["typescript", "typescriptreact", "javascript", "javascriptreact"], "eslint.validate": [
"typescript",
"typescriptreact",
"javascript",
"javascriptreact"
],
"javascript.preferences.importModuleSpecifier": "non-relative", "javascript.preferences.importModuleSpecifier": "non-relative",
"javascript.preferences.useAliasesForRenames": false, "javascript.preferences.useAliasesForRenames": false,
"typescript.enablePromptUseWorkspaceTsdk": true, "typescript.enablePromptUseWorkspaceTsdk": true,
"files.eol": "\n", "files.eol": "\n",
"editor.tabSize": 2, "editor.tabSize": 2,
"editor.insertSpaces": true "editor.insertSpaces": true,
} "[prisma]": {
"editor.defaultFormatter": "Prisma.prisma"
},
}

View File

@ -35,9 +35,9 @@
"next-plausible": "^3.10.1", "next-plausible": "^3.10.1",
"perfect-freehand": "^1.2.0", "perfect-freehand": "^1.2.0",
"posthog-js": "^1.77.3", "posthog-js": "^1.77.3",
"react": "18.3.1", "react": "18.2.0",
"react-confetti": "^6.1.0", "react-confetti": "^6.1.0",
"react-dom": "18.3.1", "react-dom": "18.2.0",
"react-hook-form": "^7.43.9", "react-hook-form": "^7.43.9",
"react-icons": "^4.11.0", "react-icons": "^4.11.0",
"recharts": "^2.7.2", "recharts": "^2.7.2",

View File

@ -41,8 +41,8 @@
"perfect-freehand": "^1.2.0", "perfect-freehand": "^1.2.0",
"posthog-js": "^1.75.3", "posthog-js": "^1.75.3",
"posthog-node": "^3.1.1", "posthog-node": "^3.1.1",
"react": "18.3.1", "react": "18.2.0",
"react-dom": "18.3.1", "react-dom": "18.2.0",
"react-dropzone": "^14.2.3", "react-dropzone": "^14.2.3",
"react-hook-form": "^7.43.9", "react-hook-form": "^7.43.9",
"react-hotkeys-hook": "^4.4.1", "react-hotkeys-hook": "^4.4.1",

515
package-lock.json generated
View File

@ -11,7 +11,8 @@
], ],
"dependencies": { "dependencies": {
"@documenso/pdf-sign": "^0.1.0", "@documenso/pdf-sign": "^0.1.0",
"next-runtime-env": "^3.2.0" "next-runtime-env": "^3.2.0",
"react": "18.2.0"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^17.7.1", "@commitlint/cli": "^17.7.1",
@ -60,9 +61,9 @@
"next-plausible": "^3.10.1", "next-plausible": "^3.10.1",
"perfect-freehand": "^1.2.0", "perfect-freehand": "^1.2.0",
"posthog-js": "^1.77.3", "posthog-js": "^1.77.3",
"react": "18.3.1", "react": "18.2.0",
"react-confetti": "^6.1.0", "react-confetti": "^6.1.0",
"react-dom": "18.3.1", "react-dom": "18.2.0",
"react-hook-form": "^7.43.9", "react-hook-form": "^7.43.9",
"react-icons": "^4.11.0", "react-icons": "^4.11.0",
"recharts": "^2.7.2", "recharts": "^2.7.2",
@ -82,37 +83,6 @@
"integrity": "sha512-O+z53uwx64xY7D6roOi4+jApDGFg0qn6WHcxe5QeqjMaTezBO/mxdfFXIVAVVyNWKx84OmPB3L8kbVYOTeN34A==", "integrity": "sha512-O+z53uwx64xY7D6roOi4+jApDGFg0qn6WHcxe5QeqjMaTezBO/mxdfFXIVAVVyNWKx84OmPB3L8kbVYOTeN34A==",
"dev": true "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": { "apps/marketing/node_modules/typescript": {
"version": "5.2.2", "version": "5.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
@ -157,8 +127,8 @@
"perfect-freehand": "^1.2.0", "perfect-freehand": "^1.2.0",
"posthog-js": "^1.75.3", "posthog-js": "^1.75.3",
"posthog-node": "^3.1.1", "posthog-node": "^3.1.1",
"react": "18.3.1", "react": "18.2.0",
"react-dom": "18.3.1", "react-dom": "18.2.0",
"react-dropzone": "^14.2.3", "react-dropzone": "^14.2.3",
"react-hook-form": "^7.43.9", "react-hook-form": "^7.43.9",
"react-hotkeys-hook": "^4.4.1", "react-hotkeys-hook": "^4.4.1",
@ -190,37 +160,6 @@
"integrity": "sha512-O+z53uwx64xY7D6roOi4+jApDGFg0qn6WHcxe5QeqjMaTezBO/mxdfFXIVAVVyNWKx84OmPB3L8kbVYOTeN34A==", "integrity": "sha512-O+z53uwx64xY7D6roOi4+jApDGFg0qn6WHcxe5QeqjMaTezBO/mxdfFXIVAVVyNWKx84OmPB3L8kbVYOTeN34A==",
"dev": true "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": { "apps/web/node_modules/typescript": {
"version": "5.2.2", "version": "5.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", "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": "^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": { "node_modules/@npmcli/move-file": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz",
@ -8728,6 +8688,15 @@
"url": "https://github.com/sponsors/tannerlinsley" "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": { "node_modules/@tootallnate/quickjs-emscripten": {
"version": "0.23.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", "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==", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
"devOptional": true "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": { "node_modules/@tybys/wasm-util": {
"version": "0.8.1", "version": "0.8.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.8.1.tgz", "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" "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": { "node_modules/configstore/node_modules/dot-prop": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz",
@ -12274,6 +12304,33 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "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": "^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": { "node_modules/make-fetch-happen/node_modules/lru-cache": {
"version": "7.18.3", "version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", "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", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" "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": { "node_modules/netmask": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", "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": "^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": { "node_modules/node-gyp/node_modules/is-fullwidth-code-point": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@ -21933,6 +22027,203 @@
"pjv": "bin/pjv" "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": { "node_modules/packet-reader": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
@ -23074,6 +23365,12 @@
"node": ">=0.4.0" "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": { "node_modules/promise-retry": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", "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": "^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": { "node_modules/read-package-json/node_modules/brace-expansion": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "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": "^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": { "node_modules/read-pkg": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
@ -27889,6 +28204,20 @@
"@esbuild/win32-x64": "0.20.2" "@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": { "node_modules/turbo": {
"version": "1.10.16", "version": "1.10.16",
"resolved": "https://registry.npmjs.org/turbo/-/turbo-1.10.16.tgz", "resolved": "https://registry.npmjs.org/turbo/-/turbo-1.10.16.tgz",
@ -28376,6 +28705,30 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/unique-string": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
@ -30128,22 +30481,11 @@
"micro": "^10.0.1", "micro": "^10.0.1",
"next": "14.0.3", "next": "14.0.3",
"next-auth": "4.24.5", "next-auth": "4.24.5",
"react": "18.3.1", "react": "18.2.0",
"ts-pattern": "^5.0.5", "ts-pattern": "^5.0.5",
"zod": "^3.22.4" "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": { "packages/email": {
"name": "@documenso/email", "name": "@documenso/email",
"version": "1.0.0", "version": "1.0.0",
@ -31339,7 +31681,7 @@
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"pg": "^8.11.3", "pg": "^8.11.3",
"playwright": "1.43.0", "playwright": "1.43.0",
"react": "18.3.1", "react": "18.2.0",
"remeda": "^1.27.1", "remeda": "^1.27.1",
"stripe": "^12.7.0", "stripe": "^12.7.0",
"ts-pattern": "^5.0.5", "ts-pattern": "^5.0.5",
@ -31387,17 +31729,6 @@
"node": "^14 || ^16 || >=18" "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": { "packages/prettier-config": {
"name": "@documenso/prettier-config", "name": "@documenso/prettier-config",
"version": "0.0.0", "version": "0.0.0",
@ -31800,7 +32131,7 @@
"@types/luxon": "^3.3.2", "@types/luxon": "^3.3.2",
"@types/react": "18.2.18", "@types/react": "18.2.18",
"@types/react-dom": "18.2.7", "@types/react-dom": "18.2.7",
"react": "18.3.1", "react": "18.2.0",
"typescript": "5.2.2" "typescript": "5.2.2"
} }
}, },

View File

@ -53,7 +53,8 @@
], ],
"dependencies": { "dependencies": {
"@documenso/pdf-sign": "^0.1.0", "@documenso/pdf-sign": "^0.1.0",
"next-runtime-env": "^3.2.0" "next-runtime-env": "^3.2.0",
"react": "18.2.0"
}, },
"overrides": { "overrides": {
"next-auth": { "next-auth": {
@ -61,7 +62,8 @@
}, },
"next-contentlayer": { "next-contentlayer": {
"next": "14.0.3" "next": "14.0.3"
} },
"react": "18.2.0"
}, },
"trigger.dev": { "trigger.dev": {
"endpointId": "documenso-app" "endpointId": "documenso-app"

View File

@ -19,7 +19,7 @@
"micro": "^10.0.1", "micro": "^10.0.1",
"next": "14.0.3", "next": "14.0.3",
"next-auth": "4.24.5", "next-auth": "4.24.5",
"react": "18.3.1", "react": "18.2.0",
"ts-pattern": "^5.0.5", "ts-pattern": "^5.0.5",
"zod": "^3.22.4" "zod": "^3.22.4"
} }

View File

@ -1,3 +1,6 @@
import { JobClient } from './client/client'; import { JobClient } from './client/client';
import { registerJobs } from './definitions';
export const jobsClient = JobClient.getInstance(); export const jobsClient = JobClient.getInstance();
registerJobs(jobsClient);

View File

@ -1,16 +1,20 @@
import { z } from 'zod'; import { z } from 'zod';
import type { Json } from './json';
export const ZTriggerJobOptionsSchema = z.object({ export const ZTriggerJobOptionsSchema = z.object({
id: z.string().optional(), id: z.string().optional(),
name: z.string(), 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(), timestamp: z.number().optional(),
}); });
// The Omit is a temporary workaround for a "bug" in the zod library // The Omit is a temporary workaround for a "bug" in the zod library
// @see: https://github.com/colinhacks/zod/issues/2966 // @see: https://github.com/colinhacks/zod/issues/2966
export type TriggerJobOptions = Omit<z.infer<typeof ZTriggerJobOptionsSchema>, 'payload'> & { export type TriggerJobOptions = Omit<z.infer<typeof ZTriggerJobOptionsSchema>, '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 // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -28,7 +32,7 @@ export type JobDefinition<T = any> = {
export interface JobRunIO { export interface JobRunIO {
// stableRun<T extends Json | void>(cacheKey: string, callback: (io: JobRunIO) => T | Promise<T>): Promise<T>; // stableRun<T extends Json | void>(cacheKey: string, callback: (io: JobRunIO) => T | Promise<T>): Promise<T>;
stableRun<T extends Json | void>(cacheKey: string, callback: () => Promise<T>): Promise<T>; runTask<T extends Json | void>(cacheKey: string, callback: () => Promise<T>): Promise<T>;
triggerJob(cacheKey: string, options: TriggerJobOptions): Promise<unknown>; triggerJob(cacheKey: string, options: TriggerJobOptions): Promise<unknown>;
wait(cacheKey: string, ms: number): Promise<void>; wait(cacheKey: string, ms: number): Promise<void>;
logger: { logger: {

View File

@ -2,13 +2,13 @@
* Below type is borrowed from Trigger.dev's SDK, it may be moved elsewhere later. * 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<T> = { export type JsonRecord<T> = {
[Property in keyof T]: Json; [Property in keyof T]: Json;
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
type Json<T = any> = JsonPrimitive | JsonArray | JsonRecord<T>; export type Json<T = any> = JsonPrimitive | JsonArray | JsonRecord<T>;

View File

@ -1,11 +1,16 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { sha256 } from '@noble/hashes/sha256';
import { json } from 'micro'; 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 { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { sign } from '../../server-only/crypto/sign'; import { sign } from '../../server-only/crypto/sign';
import { verify } from '../../server-only/crypto/verify'; import { verify } from '../../server-only/crypto/verify';
import type { JobDefinition, JobRunIO, TriggerJobOptions } from './_internal/job'; import type { JobDefinition, JobRunIO, TriggerJobOptions } from './_internal/job';
import type { Json } from './_internal/json';
import { BaseJobProvider } from './base'; import { BaseJobProvider } from './base';
export class LocalJobProvider extends BaseJobProvider { export class LocalJobProvider extends BaseJobProvider {
@ -33,33 +38,57 @@ export class LocalJobProvider extends BaseJobProvider {
} }
public async triggerJob(options: TriggerJobOptions) { public async triggerJob(options: TriggerJobOptions) {
const signature = sign(options); console.log({ jobDefinitions: this._jobDefinitions });
await Promise.race([ const eligibleJobs = Object.values(this._jobDefinitions).filter(
fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/api/jobs/trigger`, { (job) => job.trigger.name === options.name,
method: 'POST', );
body: JSON.stringify(options),
headers: { console.log({ options });
'Content-Type': 'application/json', console.log(
'X-Job-Signature': signature, '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() { public getApiHandler() {
return async (req: NextApiRequest, res: NextApiResponse) => { return async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'POST') { if (req.method === 'POST') {
const jobId = req.headers['x-job-id'];
const signature = req.headers['x-job-signature']; const signature = req.headers['x-job-signature'];
const isRetry = req.headers['x-job-retry'] !== undefined;
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const options = (await json(req)) as TriggerJobOptions; const options = (await json(req)) as TriggerJobOptions;
const definition = this._jobDefinitions[options.name]; 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'); res.status(400).send('Bad request');
return; return;
} }
@ -92,10 +121,83 @@ export class LocalJobProvider extends BaseJobProvider {
console.log(`[JOBS]: Triggering job ${options.name} with payload`, options.payload); console.log(`[JOBS]: Triggering job ${options.name} with payload`, options.payload);
await definition.handler({ let backgroundJob = await prisma.backgroundJob
payload: options.payload, .update({
io: this.createJobRunIO(options.name), 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'); res.status(200).send('OK');
} else { } 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<string, string> = {
'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 { private createJobRunIO(jobId: string): JobRunIO {
return { return {
stableRun: async (_cacheKey, callback) => await callback(), runTask: async <T extends void | Json>(cacheKey: string, callback: () => Promise<T>) => {
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), triggerJob: async (_cacheKey, payload) => await this.triggerJob(payload),
logger: { logger: {
debug: (...args) => console.debug(`[${jobId}]`, ...args), 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';
}
}

View File

@ -64,7 +64,7 @@ export class TriggerJobProvider extends BaseJobProvider {
return { return {
wait: io.wait, wait: io.wait,
logger: io.logger, logger: io.logger,
stableRun: async (cacheKey, callback) => io.runTask(cacheKey, callback), runTask: async (cacheKey, callback) => io.runTask(cacheKey, callback),
triggerJob: async (cacheKey, payload) => triggerJob: async (cacheKey, payload) =>
io.sendEvent(cacheKey, { io.sendEvent(cacheKey, {
...payload, ...payload,

View File

@ -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);
};

View File

@ -1,23 +1,47 @@
import { z } from 'zod'; import { z } from 'zod';
import { sendConfirmationToken } from '../../server-only/user/send-confirmation-token'; import { sendConfirmationToken } from '../../server-only/user/send-confirmation-token';
import { jobsClient } from '../client'; import type { JobClient } from '../client/client';
jobsClient.defineJob({ export const registerSendConfirmationEmailJob = (client: JobClient) => {
id: 'send.confirmation.email', client.defineJob({
name: 'Send Confirmation Email', id: 'send.confirmation.email',
version: '1.0.0', name: 'Send Confirmation Email',
trigger: { version: '1.0.0',
name: 'send.confirmation.email', trigger: {
schema: z.object({ name: 'send.confirmation.email',
email: z.string().email(), schema: z.object({
force: z.boolean().optional(), email: z.string().email(),
}), force: z.boolean().optional(),
}, }),
handler: async ({ payload }) => { },
await sendConfirmationToken({ handler: async ({ payload, io }) => {
email: payload.email, console.log('---- start job ----');
force: payload.force,
}); // 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,
});
},
});
};

View File

@ -45,7 +45,7 @@
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"pg": "^8.11.3", "pg": "^8.11.3",
"playwright": "1.43.0", "playwright": "1.43.0",
"react": "18.3.1", "react": "18.2.0",
"remeda": "^1.27.1", "remeda": "^1.27.1",
"stripe": "^12.7.0", "stripe": "^12.7.0",
"ts-pattern": "^5.0.5", "ts-pattern": "^5.0.5",

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "BackgroundJob" ADD COLUMN "payload" JSONB;

View File

@ -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;

View File

@ -612,3 +612,53 @@ model SiteSettings {
lastModifiedAt DateTime @default(now()) lastModifiedAt DateTime @default(now())
lastModifiedByUser User? @relation(fields: [lastModifiedByUserId], references: [id], onDelete: SetNull) 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)
}

View File

@ -22,7 +22,7 @@
"@types/luxon": "^3.3.2", "@types/luxon": "^3.3.2",
"@types/react": "18.2.18", "@types/react": "18.2.18",
"@types/react-dom": "18.2.7", "@types/react-dom": "18.2.7",
"react": "18.3.1", "react": "18.2.0",
"typescript": "5.2.2" "typescript": "5.2.2"
}, },
"dependencies": { "dependencies": {