From 519a04ac54faf96008cb42cdc502713703b84914 Mon Sep 17 00:00:00 2001 From: nadvolod <2136245+nadvolod@users.noreply.github.com> Date: Fri, 28 Aug 2026 13:59:47 -0400 Subject: [PATCH 01/22] Port the Nexus decouple-monolith code to TypeScript Adds typescript/ as a language sibling of java/ and kotlin/, so the Track can publish through the existing _track-ci.yml pipeline by adding a caller workflow with track_dir: "typescript". exercise/ is the monolith with TODOs open; solution/ is the decoupled version. Both typecheck clean, and both were run end to end against a dev server: monolith TXN-A COMPLETED/LOW, TXN-B COMPLETED/MEDIUM, TXN-C DECLINED_COMPLIANCE/HIGH decoupled async Nexus check starts complianceWorkflow in compliance-namespace, TXN-B parks for review, sync submitReview lands the Update, and payment-TXN-B completes with CONF-TXN-B The Deck for this workshop lives in temporalio/temporal-devdays-ts per that repo's ADR-0001. The Instruqt Track is not ported yet. Co-Authored-By: Claude Opus 5 (1M context) --- .gitignore | 4 + .../exercise/package-lock.json | 2349 +++++++++++++++++ .../decouple-monolith/exercise/package.json | 26 + .../exercise/src/compliance/activities.ts | 12 + .../exercise/src/compliance/checker.ts | 59 + .../exercise/src/compliance/nexus-handler.ts | 52 + .../exercise/src/compliance/worker.ts | 52 + .../exercise/src/compliance/workflows.ts | 75 + .../exercise/src/payments/activities.ts | 16 + .../exercise/src/payments/gateway.ts | 46 + .../exercise/src/payments/review-starter.ts | 51 + .../exercise/src/payments/starter.ts | 131 + .../exercise/src/payments/worker.ts | 51 + .../exercise/src/payments/workflows.ts | 126 + .../exercise/src/shared/nexus-service.ts | 29 + .../exercise/src/shared/types.ts | 60 + .../decouple-monolith/exercise/tsconfig.json | 10 + .../solution/package-lock.json | 2349 +++++++++++++++++ .../decouple-monolith/solution/package.json | 26 + .../solution/src/compliance/activities.ts | 12 + .../solution/src/compliance/checker.ts | 59 + .../solution/src/compliance/nexus-handler.ts | 45 + .../solution/src/compliance/worker.ts | 37 + .../solution/src/compliance/workflows.ts | 75 + .../solution/src/payments/activities.ts | 16 + .../solution/src/payments/gateway.ts | 46 + .../solution/src/payments/review-starter.ts | 51 + .../solution/src/payments/starter.ts | 131 + .../solution/src/payments/worker.ts | 36 + .../solution/src/payments/workflows.ts | 110 + .../solution/src/shared/nexus-service.ts | 14 + .../solution/src/shared/types.ts | 60 + .../decouple-monolith/solution/tsconfig.json | 10 + 33 files changed, 6226 insertions(+) create mode 100644 typescript/decouple-monolith/exercise/package-lock.json create mode 100644 typescript/decouple-monolith/exercise/package.json create mode 100644 typescript/decouple-monolith/exercise/src/compliance/activities.ts create mode 100644 typescript/decouple-monolith/exercise/src/compliance/checker.ts create mode 100644 typescript/decouple-monolith/exercise/src/compliance/nexus-handler.ts create mode 100644 typescript/decouple-monolith/exercise/src/compliance/worker.ts create mode 100644 typescript/decouple-monolith/exercise/src/compliance/workflows.ts create mode 100644 typescript/decouple-monolith/exercise/src/payments/activities.ts create mode 100644 typescript/decouple-monolith/exercise/src/payments/gateway.ts create mode 100644 typescript/decouple-monolith/exercise/src/payments/review-starter.ts create mode 100644 typescript/decouple-monolith/exercise/src/payments/starter.ts create mode 100644 typescript/decouple-monolith/exercise/src/payments/worker.ts create mode 100644 typescript/decouple-monolith/exercise/src/payments/workflows.ts create mode 100644 typescript/decouple-monolith/exercise/src/shared/nexus-service.ts create mode 100644 typescript/decouple-monolith/exercise/src/shared/types.ts create mode 100644 typescript/decouple-monolith/exercise/tsconfig.json create mode 100644 typescript/decouple-monolith/solution/package-lock.json create mode 100644 typescript/decouple-monolith/solution/package.json create mode 100644 typescript/decouple-monolith/solution/src/compliance/activities.ts create mode 100644 typescript/decouple-monolith/solution/src/compliance/checker.ts create mode 100644 typescript/decouple-monolith/solution/src/compliance/nexus-handler.ts create mode 100644 typescript/decouple-monolith/solution/src/compliance/worker.ts create mode 100644 typescript/decouple-monolith/solution/src/compliance/workflows.ts create mode 100644 typescript/decouple-monolith/solution/src/payments/activities.ts create mode 100644 typescript/decouple-monolith/solution/src/payments/gateway.ts create mode 100644 typescript/decouple-monolith/solution/src/payments/review-starter.ts create mode 100644 typescript/decouple-monolith/solution/src/payments/starter.ts create mode 100644 typescript/decouple-monolith/solution/src/payments/worker.ts create mode 100644 typescript/decouple-monolith/solution/src/payments/workflows.ts create mode 100644 typescript/decouple-monolith/solution/src/shared/nexus-service.ts create mode 100644 typescript/decouple-monolith/solution/src/shared/types.ts create mode 100644 typescript/decouple-monolith/solution/tsconfig.json diff --git a/.gitignore b/.gitignore index 6faa36b..2e8842c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,7 @@ build/ # VS Code Kotlin language server output bin/ + +# Node (typescript/) +node_modules/ +lib/ diff --git a/typescript/decouple-monolith/exercise/package-lock.json b/typescript/decouple-monolith/exercise/package-lock.json new file mode 100644 index 0000000..199db8c --- /dev/null +++ b/typescript/decouple-monolith/exercise/package-lock.json @@ -0,0 +1,2349 @@ +{ + "name": "decouple-monolith-exercise", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "decouple-monolith-exercise", + "version": "1.0.0", + "dependencies": { + "@temporalio/activity": "^1.23.0", + "@temporalio/client": "^1.23.0", + "@temporalio/nexus": "^1.23.0", + "@temporalio/worker": "^1.23.0", + "@temporalio/workflow": "^1.23.0", + "nexus-rpc": "^0.0.3" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.0", + "@types/node": "^22.9.1", + "ts-node": "^10.9.2", + "typescript": "^5.6.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", + "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/source-map/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/buffers": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", + "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-core": { + "version": "4.68.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.68.1.tgz", + "integrity": "sha512-V5oZ4Gt9WJKyQef0n9cAd0N9qjSkIBm3E4MYsgNIWBk5aINCDPKxMPo1i29rBxqiT4Ixf1epklqV9VJMKIxwlw==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.68.1", + "@jsonjoy.com/fs-node-utils": "4.68.1", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-fsa": { + "version": "4.68.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.68.1.tgz", + "integrity": "sha512-HCG72UioncuO7Gw09XNVG+S85e3cq2hrUC/mexBrsWsa3mI7eePkkqWie3uVYbtsb64OR9YGQs5SqaufDRYBcg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.68.1", + "@jsonjoy.com/fs-node-builtins": "4.68.1", + "@jsonjoy.com/fs-node-utils": "4.68.1", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node": { + "version": "4.68.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.68.1.tgz", + "integrity": "sha512-R5D9mWtqdURzcOWj1vdXr3APCwX0xchtFT+kmW7fXLNDifWdDrnh26jSID8pdnUfFBxTyfHtFtTL/NWKzIH7kQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.68.1", + "@jsonjoy.com/fs-node-builtins": "4.68.1", + "@jsonjoy.com/fs-node-utils": "4.68.1", + "@jsonjoy.com/fs-print": "4.68.1", + "@jsonjoy.com/fs-snapshot": "4.68.1", + "glob-to-regex.js": "^1.0.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-builtins": { + "version": "4.68.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.68.1.tgz", + "integrity": "sha512-HK1BTksysokNZxNspqDH0yPaqN9YgR/AYIlYiIaU2Ys4BOk5CdybI7r6BgiZuiiPiV8n4sK/kZdice7Znpy2Kw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-to-fsa": { + "version": "4.68.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.68.1.tgz", + "integrity": "sha512-lpKmU4X9e/oh8GIuAI7EXaS5QiLNM3KD15CkdhfS6PYmrGvoJqKQcyEfnLgnnaGslh/PFUMYSIZBCf2ejJGw8g==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-fsa": "4.68.1", + "@jsonjoy.com/fs-node-builtins": "4.68.1", + "@jsonjoy.com/fs-node-utils": "4.68.1" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-utils": { + "version": "4.68.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.68.1.tgz", + "integrity": "sha512-/GxfW1DWm9SCdkfbvqevLO/P5duobQfmKkHXxdMIDbcZMQeAgooAstIfZhkXpATzq9QbCQsnoWFM/dGHdZfndw==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.68.1", + "glob-to-regex.js": "^1.0.1" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-print": { + "version": "4.68.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.68.1.tgz", + "integrity": "sha512-oGeZOGPYKK9v1CgeVeEDsLomH1lCnslSpqUN5GmPzrmAVGQlsmsdcXNA2O4lV8Y4xkuSuynx2ITBkUHJVaTbow==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-utils": "4.68.1", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot": { + "version": "4.68.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.68.1.tgz", + "integrity": "sha512-XZfP0FDZN32bbc4t2bZN2qRrYHg5AktJnzk22HRoKGK4BprrbNRH2k5ceSNS/kupKYcofCs+O841+xaAbjnxwQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^17.65.0", + "@jsonjoy.com/fs-node-utils": "4.68.1", + "@jsonjoy.com/json-pack": "^17.65.0", + "@jsonjoy.com/util": "^17.65.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", + "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", + "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", + "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "17.67.0", + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0", + "@jsonjoy.com/json-pointer": "17.67.0", + "@jsonjoy.com/util": "17.67.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", + "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/util": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", + "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.2.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.2", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, + "node_modules/@swc/core": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.16.1.tgz", + "integrity": "sha512-nUaeu91O5QZKrQdaDCHd402ogUIoNOOjpkZNq0UomWK0G6gDaGmLhvddF1/3BXf5O8aLyo6ZPY/aMDWvaJQ/hg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.28" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.16.1", + "@swc/core-darwin-x64": "1.16.1", + "@swc/core-linux-arm-gnueabihf": "1.16.1", + "@swc/core-linux-arm64-gnu": "1.16.1", + "@swc/core-linux-arm64-musl": "1.16.1", + "@swc/core-linux-ppc64-gnu": "1.16.1", + "@swc/core-linux-s390x-gnu": "1.16.1", + "@swc/core-linux-x64-gnu": "1.16.1", + "@swc/core-linux-x64-musl": "1.16.1", + "@swc/core-win32-arm64-msvc": "1.16.1", + "@swc/core-win32-ia32-msvc": "1.16.1", + "@swc/core-win32-x64-msvc": "1.16.1" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.16.1.tgz", + "integrity": "sha512-zlJblJ8ncErD43lKdxjbUaUskJQf+LxiPXYcWXD8/8ZMV+7uuAT+CwjciLXpyZBd5Pq/S726bMpeeAwSeL1hhg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.16.1.tgz", + "integrity": "sha512-IN0BmPWb0YAh/17mmlWB/HDBtTw2MfuW4hulf/tQAgTQBRH17l+z499bNJLK6LizSjqs0P7V+jU38Zj+vJC1DA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.16.1.tgz", + "integrity": "sha512-EYgrx2YOCQ2Twz2S793kqNjPkpvYVUPzzR95bIb7by+VQcyaai4lZZ2iz/tZvcFVKSNcN3/JTKwx+aBn2ZL52A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.16.1.tgz", + "integrity": "sha512-moyKm0YZlHdHohzm1YwgAyesqnE853rO0REMfJLFAova51wF9BNi+3ZW2PeS7Vqvn6HeJuepLpAHbBdZctxpHA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.16.1.tgz", + "integrity": "sha512-kKGBO9wdapiSzuf5ZzZ2fYtlu1BNSYtIIUxvH1ir/gcelTOREEHGDCLTDFx/2Knf878nU11A40z7LxwasEFxqA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.16.1.tgz", + "integrity": "sha512-nZ6qahtLxC3PM54cWOQZHxt4lTCF/3J4LIoWWzz6v7A+rLs8Dx54anYQf7mH3eIi8KlNpgKci/ie8ZSqFN8O7A==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.16.1.tgz", + "integrity": "sha512-4ji5PNzhYq193Z4/4xUaSoNJza6iCkDJSzhetrbB6KOYxsr+kxtQr8ePWhMJUiMt6JUWtXaZ1PYT8FhtED+nGA==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.16.1.tgz", + "integrity": "sha512-VJQxqrisHV+B394IgrOu8YsIIXZgffnf5tO+yc9Z/hoUpuZEvuQTjWwlnpZdpyD+0nx6LTD1/3k646JYm43yJA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.16.1.tgz", + "integrity": "sha512-r9oV1mwxxsIGcLV1IQ/tw76MW3doatKze1QFWuC+a7QqJUkhY/bKTSVk6NpKKUGm2LDsE33Va8VqSClfA7vSiQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.16.1.tgz", + "integrity": "sha512-6huNRessoBLxWEqBm5zJXyCQ27TO7anvkdiuQ5MDO4CJni0nOXEqKtV9RllQ2TdyENKKsUMXVnIfW2hIXx/R5Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.16.1.tgz", + "integrity": "sha512-OVKJFUzphrGmsh+BGtcZDesx0YryV7/Yvy5XGgTqnrZfjnyfcr5uaqYQugCckdIlupc5Vs3XtDjRAj12z4ZPlw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.16.1.tgz", + "integrity": "sha512-Bt+VIhWYCGk4urklnkkteLUOeLv1VxigwTCeB/xC6rBZxY6IIKdDwCJf6on3E3SUGsIqmQS6QqtuJQc1VxF4Aw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.28", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.28.tgz", + "integrity": "sha512-V6Mnml8v09QALx6K0elJ7o9K/MkVDtW3t6L+7Ou/JcWtb3xwId2AH4FeOceySd2JaO87IMw4+6vSZxLm34LPbw==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@temporalio/activity": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@temporalio/activity/-/activity-1.23.0.tgz", + "integrity": "sha512-BVeiizVD08yk0Ls6MhKCK+g3fPIWbVM9MLx+o990KwhCaZtRM6o1+du1/qfIIShHl6YjHk+H0ewkx2x14QQ6bA==", + "license": "MIT", + "dependencies": { + "@temporalio/client": "1.23.0", + "@temporalio/common": "1.23.0" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@temporalio/client": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@temporalio/client/-/client-1.23.0.tgz", + "integrity": "sha512-E9nXZPiKDGEDbPpRi0EyQ9i1x70fEIG26aMXZO88kKslKZnEKNPphs06xIic7bhuDoSrPsgVfdDap6o3l6IO2A==", + "license": "MIT", + "dependencies": { + "@grpc/grpc-js": "^1.12.4", + "@temporalio/common": "1.23.0", + "@temporalio/proto": "1.23.0", + "abort-controller": "^3.0.0", + "long": "^5.3.2", + "nexus-rpc": "^0.0.3", + "uuid": "^11.1.0" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@temporalio/common": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@temporalio/common/-/common-1.23.0.tgz", + "integrity": "sha512-TaZYb/ruzgzJ8jd42EqRUCI4ispLZETt2Jigc9szES6q9edDmfncRw143HyvPi7b8n9naohYmA/IxTe9COH/dg==", + "license": "MIT", + "dependencies": { + "@temporalio/proto": "1.23.0", + "long": "^5.3.2", + "ms": "3.0.0-canary.1", + "nexus-rpc": "^0.0.3", + "protobufjs": "^8.7.1" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@temporalio/common/node_modules/protobufjs": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.8.0.tgz", + "integrity": "sha512-N3xhQ5yyBx3vQq4gubBfASzYhJGNzeDbjqBpu61g7UVylsN/qyffU96TKWD3GbbLOKF82VGNRNvv1+BFgE31Eg==", + "license": "BSD-3-Clause", + "dependencies": { + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@temporalio/core-bridge": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@temporalio/core-bridge/-/core-bridge-1.23.0.tgz", + "integrity": "sha512-sid14odlYnci9WgB+iDLyxFbzkq+qBbRPp+CW9U1v+DLRfHlFJwpznQ1AZl4mHjtAZRGQFvDsDG0XNncl9ESSA==", + "license": "MIT", + "dependencies": { + "@grpc/grpc-js": "^1.12.4", + "@temporalio/common": "1.23.0" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@temporalio/nexus": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@temporalio/nexus/-/nexus-1.23.0.tgz", + "integrity": "sha512-mObGNp3thR+mTXdtKXpUvFpsdjEI9xpVjewhblKXao7Nqv6xrs7RpFiLdKfJhkEvDyfRgIk/dH5RkiCbZ58I9w==", + "license": "MIT", + "dependencies": { + "@temporalio/client": "1.23.0", + "@temporalio/common": "1.23.0", + "@temporalio/proto": "1.23.0", + "long": "^5.3.2", + "nexus-rpc": "^0.0.3" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@temporalio/proto": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@temporalio/proto/-/proto-1.23.0.tgz", + "integrity": "sha512-90JWnYEs1wyq97/SXkQGknRVMr/BCV9sZ0pOnK3fHTedVinfvz3/PBVFkeAJyU3CJqMQGnzRs9IIfsBTT1icrA==", + "license": "MIT", + "dependencies": { + "long": "^5.3.2", + "protobufjs": "^8.7.1" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@temporalio/proto/node_modules/protobufjs": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.8.0.tgz", + "integrity": "sha512-N3xhQ5yyBx3vQq4gubBfASzYhJGNzeDbjqBpu61g7UVylsN/qyffU96TKWD3GbbLOKF82VGNRNvv1+BFgE31Eg==", + "license": "BSD-3-Clause", + "dependencies": { + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@temporalio/worker": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@temporalio/worker/-/worker-1.23.0.tgz", + "integrity": "sha512-k1MrK10lD1s7U66PFqAwGlQMRsVrfU5nCB+CjIAg5RyL7JpOzidDCz/E/jB2HobXzZnxtMcDBfBHh/BYUoyhxw==", + "license": "MIT", + "dependencies": { + "@grpc/grpc-js": "^1.12.4", + "@swc/core": "^1.3.102", + "@temporalio/activity": "1.23.0", + "@temporalio/client": "1.23.0", + "@temporalio/common": "1.23.0", + "@temporalio/core-bridge": "1.23.0", + "@temporalio/nexus": "1.23.0", + "@temporalio/proto": "1.23.0", + "@temporalio/workflow": "1.23.0", + "heap-js": "^2.6.0", + "memfs": "^4.6.0", + "nexus-rpc": "^0.0.3", + "protobufjs": "^8.7.1", + "rxjs": "^7.8.1", + "source-map": "^0.7.4", + "source-map-loader": "^5.0.0", + "supports-color": "^8.1.1", + "swc-loader": "^0.2.3", + "unionfs": "^4.5.1", + "webpack": "^5.108.4" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@temporalio/worker/node_modules/protobufjs": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.8.0.tgz", + "integrity": "sha512-N3xhQ5yyBx3vQq4gubBfASzYhJGNzeDbjqBpu61g7UVylsN/qyffU96TKWD3GbbLOKF82VGNRNvv1+BFgE31Eg==", + "license": "BSD-3-Clause", + "dependencies": { + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@temporalio/workflow": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@temporalio/workflow/-/workflow-1.23.0.tgz", + "integrity": "sha512-dHQnYmxU/QN7VfTmdfCfpdfTS1w0hoW/NQ7Zefr55QnqLwZw+E0Vn6S4fAbJrpfHrsAmezUZQFVLi+Z4Sq+jzA==", + "license": "MIT", + "dependencies": { + "@temporalio/common": "1.23.0", + "@temporalio/proto": "1.23.0", + "nexus-rpc": "^0.0.3" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.13.tgz", + "integrity": "sha512-gcLdvR9HO1ZJBypsOGqaP6TFEzb6vIta0KSTLt9NAQ6pXQO3cRgSVyCN6pzYqI9DlJgY71XKO0dpDhCf08b3pg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node22": { + "version": "22.0.6", + "resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.6.tgz", + "integrity": "sha512-/5thavHAnWDZwH2H6eEn/T8pBEBhtuKaWGMslsj82kJfvzQgOgEMK7X4goBbIUjgVtAzPTtM9Ml64LA0uxO6Iw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz", + "integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.416", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.416.tgz", + "integrity": "sha512-K6bvB2BjnNrugtIih6ewlbBI9DXa976jIdiIlRLHhBoEI9a4JaQjjHyF+A1IQI543aQYR4LnmOrT/K5fZj0aPA==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.6.tgz", + "integrity": "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "license": "Unlicense" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob-to-regex.js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/heap-js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/heap-js/-/heap-js-2.7.1.tgz", + "integrity": "sha512-EQfezRg0NCZGNlhlDR3Evrw1FVL2G3LhU7EgPoxufQKruNBSYA8MiRPHeWbU+36o+Fhel0wMwM+sLEiBAlNLJA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "license": "MIT", + "engines": { + "node": ">=10.18" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/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==", + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/memfs": { + "version": "4.68.1", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.68.1.tgz", + "integrity": "sha512-OD+IDRUvIxu3QHL+nFm9gdyugInD27FDJ+sl4B5QgomPHXMlbw+GP918P8VNKu2FkNlVeqBkpzkwROpamVifRw==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.68.1", + "@jsonjoy.com/fs-fsa": "4.68.1", + "@jsonjoy.com/fs-node": "4.68.1", + "@jsonjoy.com/fs-node-builtins": "4.68.1", + "@jsonjoy.com/fs-node-to-fsa": "4.68.1", + "@jsonjoy.com/fs-node-utils": "4.68.1", + "@jsonjoy.com/fs-print": "4.68.1", + "@jsonjoy.com/fs-snapshot": "4.68.1", + "@jsonjoy.com/json-pack": "^1.11.0", + "@jsonjoy.com/util": "^1.9.0", + "glob-to-regex.js": "^1.0.1", + "thingies": "^2.5.0", + "tree-dump": "^1.0.3", + "tslib": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimizer-webpack-plugin": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.8.0.tgz", + "integrity": "sha512-2cT9+goJfBhtMz+gJqejSf09ClgmYhPhccRb/fb0ztVbixt0BkO8mRuI26FoPPuUNkRk6iEDryWAIouc5W8eJA==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.3", + "terser": "^5.51.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/ms": { + "version": "3.0.0-canary.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-3.0.0-canary.1.tgz", + "integrity": "sha512-kh8ARjh8rMN7Du2igDRO9QJnqCb2xYTJxyQYK7vJJS4TvLLmsbyhiKpSW+t+y26gyOyMd0riphX0GeWKU3ky5g==", + "license": "MIT", + "engines": { + "node": ">=12.13" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/nexus-rpc": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/nexus-rpc/-/nexus-rpc-0.0.3.tgz", + "integrity": "sha512-ksIBeYHDv6FHAR1h3Vwk0kQ6REhdo1STqDHXMzu/yn/VN/XzgOor2IUjewG6paoA7IgxlIPZoOU/PD78PC6HTg==", + "license": "MIT", + "engines": { + "node": ">= 20.0.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", + "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/swc-loader": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/swc-loader/-/swc-loader-0.2.7.tgz", + "integrity": "sha512-nwYWw3Fh9ame3Rtm7StS9SBLpHRRnYcK7bnpF3UKZmesAK0gw2/ADvlURFAINmPvKtDLzp+GBiP9yLoEjg6S9w==", + "license": "MIT", + "dependencies": { + "@swc/counter": "^0.1.3" + }, + "peerDependencies": { + "@swc/core": "^1.2.147", + "webpack": ">=2" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.51.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.51.2.tgz", + "integrity": "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/thingies": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.1.tgz", + "integrity": "sha512-cV/CMGTK3M4MlnJ/0At6ismOw/A0EEniDNScajjz/Br3c1sqE72YD01rGpPTKwd27wAxI5Pr+6+0w8yofzFRYw==", + "license": "MIT", + "engines": { + "node": ">=10.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "node_modules/tree-dump": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", + "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unionfs": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/unionfs/-/unionfs-4.6.0.tgz", + "integrity": "sha512-fJAy3gTHjFi5S3TP5EGdjs/OUMFFvI/ady3T8qVuZfkv8Qi8prV/Q8BuFEgODJslhZTT2z2qdD2lGdee9qjEnA==", + "dependencies": { + "fs-monkey": "^1.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.110.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.110.1.tgz", + "integrity": "sha512-gInQB+jxXxgnZyvPwuzT5NGQmECDqeu85oxcrjinrYHqPoBex0hCAN2SFTJVyPVrK0Pq9E44VFP+e89fAc10/w==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.24.4", + "es-module-lexer": "^2.1.0", + "events": "^3.2.0", + "graceful-fs": "^4.2.11", + "mime-db": "^1.54.0", + "minimizer-webpack-plugin": "^5.7.0", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "watchpack": "^2.5.2", + "webpack-sources": "^3.5.1" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz", + "integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/typescript/decouple-monolith/exercise/package.json b/typescript/decouple-monolith/exercise/package.json new file mode 100644 index 0000000..a683709 --- /dev/null +++ b/typescript/decouple-monolith/exercise/package.json @@ -0,0 +1,26 @@ +{ + "name": "decouple-monolith-exercise", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "tsc --build", + "payments-worker": "ts-node src/payments/worker.ts", + "compliance-worker": "ts-node src/compliance/worker.ts", + "starter": "ts-node src/payments/starter.ts", + "review-starter": "ts-node src/payments/review-starter.ts" + }, + "dependencies": { + "@temporalio/activity": "^1.23.0", + "@temporalio/client": "^1.23.0", + "@temporalio/nexus": "^1.23.0", + "@temporalio/worker": "^1.23.0", + "@temporalio/workflow": "^1.23.0", + "nexus-rpc": "^0.0.3" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.0", + "@types/node": "^22.9.1", + "ts-node": "^10.9.2", + "typescript": "^5.6.3" + } +} diff --git a/typescript/decouple-monolith/exercise/src/compliance/activities.ts b/typescript/decouple-monolith/exercise/src/compliance/activities.ts new file mode 100644 index 0000000..9e18362 --- /dev/null +++ b/typescript/decouple-monolith/exercise/src/compliance/activities.ts @@ -0,0 +1,12 @@ +// [GIVEN] Compliance team Activity. +// +// In the monolith, paymentProcessingWorkflow calls this Activity directly — which means +// Compliance's code runs inside the Payments Worker process. After decoupling, the +// Activity stays here but is called from the Compliance Worker, inside the Workflow that +// the Nexus handler starts. +import { checkCompliance as runCheck } from './checker'; +import { ComplianceRequest, ComplianceResult } from '../shared/types'; + +export async function checkCompliance(request: ComplianceRequest): Promise { + return runCheck(request); +} diff --git a/typescript/decouple-monolith/exercise/src/compliance/checker.ts b/typescript/decouple-monolith/exercise/src/compliance/checker.ts new file mode 100644 index 0000000..1879a3a --- /dev/null +++ b/typescript/decouple-monolith/exercise/src/compliance/checker.ts @@ -0,0 +1,59 @@ +// [GIVEN] Rule-based compliance checker. Deterministic, no API keys needed. +// Same logic whether it runs as an Activity or behind a Nexus handler. +// +// Rules: +// - OFAC sanctioned country on either side -> HIGH risk, blocked +// - Amount over $50,000 -> HIGH risk, blocked (TXN-C hits this) +// - Over $10,000 or an unusual jurisdiction -> MEDIUM risk (TXN-B hits this) +// - Everything else -> LOW risk, approved +import { ComplianceRequest, ComplianceResult } from '../shared/types'; + +const SANCTIONED = new Set(['North Korea', 'Iran', 'Cuba', 'Syria', 'Venezuela']); +const COMMON = new Set(['US', 'UK', 'Canada', 'Germany', 'France', 'Japan', 'Australia']); + +export function checkCompliance(request: ComplianceRequest): ComplianceResult { + console.log( + `[ComplianceChecker] Evaluating ${request.transactionId}` + + ` | $${request.amount.toFixed(2)}` + + ` | ${request.senderCountry} -> ${request.receiverCountry}`, + ); + + if (SANCTIONED.has(request.receiverCountry) || SANCTIONED.has(request.senderCountry)) { + return { + transactionId: request.transactionId, + approved: false, + riskLevel: 'HIGH', + explanation: + 'Destination/source country is OFAC-sanctioned. Transaction blocked per regulatory requirements.', + }; + } + + if (request.amount > 50_000) { + return { + transactionId: request.transactionId, + approved: false, + riskLevel: 'HIGH', + explanation: + 'Transaction amount exceeds $50,000 threshold. Requires enhanced due diligence review.', + }; + } + + const isInternational = request.senderCountry !== request.receiverCountry; + const isUnusualJurisdiction = isInternational && !COMMON.has(request.receiverCountry); + + if (request.amount > 10_000 || isUnusualJurisdiction) { + return { + transactionId: request.transactionId, + approved: true, + riskLevel: 'MEDIUM', + explanation: 'International transfer above $10K threshold. Approved with AML monitoring note.', + }; + } + + return { + transactionId: request.transactionId, + approved: true, + riskLevel: 'LOW', + explanation: 'Routine domestic/standard international transfer. No regulatory concerns.', + }; +} diff --git a/typescript/decouple-monolith/exercise/src/compliance/nexus-handler.ts b/typescript/decouple-monolith/exercise/src/compliance/nexus-handler.ts new file mode 100644 index 0000000..de05de4 --- /dev/null +++ b/typescript/decouple-monolith/exercise/src/compliance/nexus-handler.ts @@ -0,0 +1,52 @@ +// The Compliance team's Nexus handler. Payments depends on the contract in +// shared/nexus-service.ts. This file is the code that answers those calls, and only +// the Compliance team owns it. +// +// This is the hardest file in the lab. Two things you will need: +// +// temporalNexus.getClient() +// A Temporal Client, already connected to this team's Namespace. +// +// `compliance-${transactionId}` +// The Workflow ID to use. Same ID both times: one Operation creates that Workflow, +// the other looks up the same one. +// +// Stuck? "Ask AI" on https://docs.temporal.io: +// "how do I start a Workflow from a Nexus Operation handler in TypeScript?" +import * as nexus from 'nexus-rpc'; +import * as temporalNexus from '@temporalio/nexus'; +import { complianceService } from '../shared/nexus-service'; +import { ComplianceRequest, ComplianceResult, ReviewRequest } from '../shared/types'; +import { complianceWorkflow, reviewUpdate } from './workflows'; + +/** Same ID both times: one Operation creates the Workflow, the other looks it up. */ +const workflowIdFor = (transactionId: string) => `compliance-${transactionId}`; + +export const complianceServiceHandler = nexus.serviceHandler(complianceService, { + // ── TODO 2 ────────────────────────────────────────────────────────────────────── + // Implement checkCompliance. + // + // This is the SLOW call. A risky payment has to be approved by a person, so an answer + // can be minutes or hours away. This Operation must not sit and wait for it. + // + // Instead: start a complianceWorkflow and return immediately, handing back a reference + // to it. Temporal holds that reference and collects the result whenever the Workflow + // finishes. Use `new temporalNexus.WorkflowRunOperationHandler(async (ctx, input) => ...)` + // and call `temporalNexus.startWorkflow(ctx, complianceWorkflow, { args, workflowId })` + // inside it. + // + // Do NOT write a plain async function here. That is a synchronous Operation: it is cut + // off after 10 seconds, and when Temporal retries the failed call it would start a + // SECOND compliance check for the same payment. + + // ── TODO 3 ────────────────────────────────────────────────────────────────────── + // Implement submitReview. + // + // This is the FAST call. By now the compliance check is already running and is parked + // waiting for a decision. All you do is find that Workflow and hand it the yes or no. + // That takes milliseconds, so a plain `async (ctx, input) => { ... }` is the right + // shape here. + // + // Get a client with temporalNexus.getClient(), get a handle to the Workflow by its ID, + // then call executeUpdate with the `reviewUpdate` definition imported above. +}); diff --git a/typescript/decouple-monolith/exercise/src/compliance/worker.ts b/typescript/decouple-monolith/exercise/src/compliance/worker.ts new file mode 100644 index 0000000..92c8b11 --- /dev/null +++ b/typescript/decouple-monolith/exercise/src/compliance/worker.ts @@ -0,0 +1,52 @@ +// The Compliance team's Worker. This process is the only place their code runs. +// +// The connection and the Worker itself are set up for you. What is missing is the part +// that decides what this Worker can actually do: see TODO 4. +import { NativeConnection, Worker } from '@temporalio/worker'; +import * as activities from './activities'; +import { complianceServiceHandler } from './nexus-handler'; +import { COMPLIANCE_NAMESPACE, COMPLIANCE_TASK_QUEUE } from '../shared/types'; + +async function run() { + const connection = await NativeConnection.connect({ address: 'localhost:7233' }); + try { + const worker = await Worker.create({ + connection, + namespace: COMPLIANCE_NAMESPACE, + // MUST match the --target-task-queue you give the Nexus Endpoint, or the Endpoint + // routes work to a queue nobody is polling. + taskQueue: COMPLIANCE_TASK_QUEUE, + workflowsPath: require.resolve('./workflows'), + activities, + + // ── TODO 4 ──────────────────────────────────────────────────────────────── + // A Worker only handles work it has been told about. This one already knows about + // its Workflows (workflowsPath) and its Activities (activities). What it does not + // yet know about is the Nexus handler — the thing that makes this team callable by + // Payments at all. + // + // Add one more option here, naming `complianceServiceHandler`, which is already + // imported above. + // + // Leave it out and the Worker still starts. Payments' calls just go unanswered + // forever. That is the failure you are guarding against. + // + // Stuck? "Ask AI" on https://docs.temporal.io: + // "what do I register on a TypeScript Worker that handles Nexus Operations?" + }); + + console.log('========================================================='); + console.log(` Compliance Worker started on: ${COMPLIANCE_TASK_QUEUE}`); + console.log(` Namespace: ${COMPLIANCE_NAMESPACE}`); + console.log('========================================================='); + + await worker.run(); + } finally { + await connection.close(); + } +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/typescript/decouple-monolith/exercise/src/compliance/workflows.ts b/typescript/decouple-monolith/exercise/src/compliance/workflows.ts new file mode 100644 index 0000000..f1baa4e --- /dev/null +++ b/typescript/decouple-monolith/exercise/src/compliance/workflows.ts @@ -0,0 +1,75 @@ +// [GIVEN] Compliance Workflow. Pre-built for this exercise. +// +// Runs an automated compliance check and, for MEDIUM-risk transactions, waits for a +// human reviewer to approve or deny via a Workflow Update. +// +// LOW risk -> auto-approved, returns immediately +// HIGH risk -> auto-denied, returns immediately +// MEDIUM -> pauses, waits for the review Update +// +// The 10-second sleep is intentional. It gives you a window to stop the Compliance +// Worker with Ctrl+C and restart it, so you can watch the Nexus Operation survive the +// outage. The timer runs on the server, so nothing is lost when the Worker goes away. +import * as wf from '@temporalio/workflow'; +import type * as activities from './activities'; +import { ComplianceRequest, ComplianceResult } from '../shared/types'; + +const { checkCompliance } = wf.proxyActivities({ + startToCloseTimeout: '30 seconds', +}); + +/** Update sent by the Compliance team's sync Nexus handler when a human decides. */ +export const reviewUpdate = wf.defineUpdate('review'); + +export async function complianceWorkflow(request: ComplianceRequest): Promise { + let reviewResult: ComplianceResult | undefined; + let autoResult: ComplianceResult | undefined; + + wf.setHandler( + reviewUpdate, + (approved, explanation) => { + const result: ComplianceResult = { + transactionId: request.transactionId, + approved, + riskLevel: 'MEDIUM', + explanation, + }; + reviewResult = result; + return result; + }, + { + validator: (_approved: boolean, _explanation: string) => { + if (autoResult?.riskLevel !== 'MEDIUM') throw new Error('Workflow is not awaiting review'); + if (reviewResult !== undefined) throw new Error('Review already submitted'); + }, + }, + ); + + // Step 1: automated compliance check. + const auto = await checkCompliance(request); + autoResult = auto; + + // Durable delay. Stop the Compliance Worker mid-sleep, restart it, and the Workflow + // resumes on its own. + await wf.sleep('10 seconds'); + + // Step 2: LOW or HIGH risk returns immediately. + if (auto.riskLevel !== 'MEDIUM') return auto; + + // Step 3: MEDIUM risk waits for human review. + // + // Bounded on purpose. An unbounded wait would leave this Workflow Running forever + // when nobody reviews. Eight minutes is deliberately shorter than the caller's + // 10-minute scheduleToCloseTimeout, so the handler resolves and reports a decision + // rather than letting the caller time out first. + const reviewed = await wf.condition(() => reviewResult !== undefined, '8 minutes'); + if (!reviewed) { + return { + transactionId: request.transactionId, + approved: false, + riskLevel: 'MEDIUM', + explanation: 'No reviewer responded within 8 minutes. Not approved.', + }; + } + return reviewResult!; +} diff --git a/typescript/decouple-monolith/exercise/src/payments/activities.ts b/typescript/decouple-monolith/exercise/src/payments/activities.ts new file mode 100644 index 0000000..18c00df --- /dev/null +++ b/typescript/decouple-monolith/exercise/src/payments/activities.ts @@ -0,0 +1,16 @@ +// [GIVEN] Payments team Activities. Thin wrappers around the gateway. +// +// In TypeScript an Activity is just an exported function — there is no interface to +// declare and no annotation to apply. They must be async: proxyActivities only accepts +// Promise-returning functions, and a synchronous one is silently typed away, surfacing +// later as "Type 'Symbol' has no call signatures" at the call site in the Workflow. +import * as gateway from './gateway'; +import { PaymentRequest } from '../shared/types'; + +export async function validatePayment(request: PaymentRequest): Promise { + return gateway.validatePayment(request); +} + +export async function executePayment(request: PaymentRequest): Promise { + return gateway.executePayment(request); +} diff --git a/typescript/decouple-monolith/exercise/src/payments/gateway.ts b/typescript/decouple-monolith/exercise/src/payments/gateway.ts new file mode 100644 index 0000000..f775915 --- /dev/null +++ b/typescript/decouple-monolith/exercise/src/payments/gateway.ts @@ -0,0 +1,46 @@ +// [GIVEN] Simulated payment gateway. In production this would call Stripe, PayPal, SWIFT. +// +// The 10% failure rate is deliberate: it shows Temporal retrying an Activity for free. +// Because of it, never assert on a clean first attempt — assert on final Workflow state. +import { PaymentRequest } from '../shared/types'; + +const money = (n: number) => `$${n.toFixed(2)}`; +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +export function validatePayment(request: PaymentRequest): boolean { + if (request.amount <= 0) { + console.log(`[PaymentGateway] REJECTED: Invalid amount for ${request.transactionId}`); + return false; + } + if (!request.senderAccount || !request.receiverAccount) { + console.log(`[PaymentGateway] REJECTED: Missing account info for ${request.transactionId}`); + return false; + } + console.log(`[PaymentGateway] Validation passed for ${request.transactionId}`); + return true; +} + +export async function executePayment(request: PaymentRequest): Promise { + console.log( + `[PaymentGateway] Processing ${request.transactionId}` + + ` | ${money(request.amount)}` + + ` | ${request.senderCountry} -> ${request.receiverCountry}`, + ); + + await sleep(500 + Math.random() * 500); + + // Simulate occasional gateway failures. Temporal retries automatically. + if (Math.random() < 0.1) { + throw new Error( + `Payment gateway timeout for ${request.transactionId}, connection to banking network failed`, + ); + } + + // Derived from the transaction ID, not the clock, so a retry of this Activity returns + // the SAME confirmation number instead of minting a second one. With a 10% failure + // rate above, a timestamp here would make retries look like distinct payments. + // Idempotency is what makes an at-least-once retry safe. + const confirmationNumber = `CONF-${request.transactionId}`; + console.log(`[PaymentGateway] Payment executed: ${confirmationNumber}`); + return confirmationNumber; +} diff --git a/typescript/decouple-monolith/exercise/src/payments/review-starter.ts b/typescript/decouple-monolith/exercise/src/payments/review-starter.ts new file mode 100644 index 0000000..e8061dc --- /dev/null +++ b/typescript/decouple-monolith/exercise/src/payments/review-starter.ts @@ -0,0 +1,51 @@ +// [GIVEN] Starts a reviewCallerWorkflow to approve TXN-B via Nexus. +// +// Rather than calling 'temporal workflow update execute' directly, this triggers a caller +// Workflow that invokes the submitReview Nexus Operation. The Compliance team's sync +// handler forwards the decision to the running complianceWorkflow as an Update. +// +// Flip `approved` to false below to see the denial path. +import { Client, Connection } from '@temporalio/client'; +import { reviewCallerWorkflow } from './workflows'; +import { PAYMENTS_NAMESPACE, PAYMENTS_TASK_QUEUE, ReviewRequest } from '../shared/types'; + +async function run() { + console.log('=========================================================='); + console.log(' REVIEW STARTER - Submitting review for TXN-B via Nexus'); + console.log('==========================================================\n'); + + const connection = await Connection.connect({ address: 'localhost:7233' }); + const client = new Client({ connection, namespace: PAYMENTS_NAMESPACE }); + + const request: ReviewRequest = { + transactionId: 'TXN-B', + approved: true, + explanation: 'Approved after manual review', + }; + + console.log(' Submitting review for TXN-B via Nexus...'); + console.log(` Approved: ${request.approved}`); + console.log(` Explanation: ${request.explanation}\n`); + + const result = await client.workflow.execute(reviewCallerWorkflow, { + taskQueue: PAYMENTS_TASK_QUEUE, + workflowId: `review-${request.transactionId}`, + args: [request], + }); + + console.log(` Review result: ${result.approved ? 'APPROVED' : 'DENIED'}`); + console.log(` Risk level: ${result.riskLevel}`); + console.log(` Explanation: ${result.explanation}\n`); + console.log(' TXN-B review submitted. The payment Workflow will now complete.'); + console.log('=========================================================='); + + await connection.close(); +} + +run().then( + () => process.exit(0), + (err) => { + console.error(err); + process.exit(1); + }, +); diff --git a/typescript/decouple-monolith/exercise/src/payments/starter.ts b/typescript/decouple-monolith/exercise/src/payments/starter.ts new file mode 100644 index 0000000..492620b --- /dev/null +++ b/typescript/decouple-monolith/exercise/src/payments/starter.ts @@ -0,0 +1,131 @@ +// [GIVEN] Starts three payment Workflows. +// +// All three start before any result is collected. That matters once Nexus is in play: +// TXN-B is MEDIUM risk, so its handler Workflow pauses for human review and would +// otherwise block TXN-C behind it. Starting concurrently lets TXN-A and TXN-C finish +// while TXN-B sits visibly pending, which is the point of the human-in-the-loop path. +// +// The starter does not know or care whether compliance runs locally or via Nexus. +import { Client, Connection } from '@temporalio/client'; +import { paymentProcessingWorkflow } from './workflows'; +import { PAYMENTS_NAMESPACE, PAYMENTS_TASK_QUEUE, PaymentRequest, PaymentResult } from '../shared/types'; + +// How long to wait before calling a Workflow "pending review". TXN-A and TXN-C finish in +// roughly 12 seconds (the handler Workflow sleeps for 10 of those). +const COLLECT_DEADLINE_MS = 25_000; + +const TRANSACTIONS: PaymentRequest[] = [ + { + transactionId: 'TXN-A', amount: 250.0, currency: 'USD', + senderCountry: 'US', receiverCountry: 'US', + description: 'Routine supplier payment', senderAccount: 'ACC-001', receiverAccount: 'ACC-002', + }, + { + transactionId: 'TXN-B', amount: 12_000.0, currency: 'USD', + senderCountry: 'US', receiverCountry: 'UK', + description: 'International consulting fee', senderAccount: 'ACC-003', receiverAccount: 'ACC-004', + }, + { + transactionId: 'TXN-C', amount: 75_000.0, currency: 'USD', + senderCountry: 'US', receiverCountry: 'US', + description: 'Large capital transfer', senderAccount: 'ACC-005', receiverAccount: 'ACC-006', + }, +]; + +type Outcome = + | { state: 'done'; result: PaymentResult } + | { state: 'failed'; error: string } + | { state: 'pending' }; + +async function run() { + console.log('=========================================================='); + console.log(' PAYMENT STARTER - Decouple Monolith'); + console.log(' Running 3 transactions through Temporal'); + console.log('==========================================================\n'); + + const connection = await Connection.connect({ address: 'localhost:7233' }); + const client = new Client({ connection, namespace: PAYMENTS_NAMESPACE }); + + // Start all three without awaiting results, so a pause in one cannot hold up the others. + const started = await Promise.all( + TRANSACTIONS.map(async (txn) => { + const handle = await client.workflow.start(paymentProcessingWorkflow, { + taskQueue: PAYMENTS_TASK_QUEUE, + workflowId: `payment-${txn.transactionId}`, + args: [txn], + }); + console.log(` Started: payment-${txn.transactionId}`); + console.log( + ` Amount: $${txn.amount.toFixed(2)} | Route: ${txn.senderCountry} -> ${txn.receiverCountry}`, + ); + return { txn, handle }; + }), + ); + + console.log('\n All 3 started. Collecting results...\n'); + + // Wait on all three against a single shared deadline. Waiting one at a time would let + // TXN-B's review pause burn the whole budget, so TXN-C would be reported as pending + // even though it finished seconds earlier. + const outcomes = new Map(started.map(({ txn }) => [txn.transactionId, { state: 'pending' }])); + await Promise.race([ + Promise.allSettled( + started.map(async ({ txn, handle }) => { + try { + outcomes.set(txn.transactionId, { state: 'done', result: await handle.result() }); + } catch (err) { + outcomes.set(txn.transactionId, { state: 'failed', error: (err as Error).message }); + } + }), + ), + new Promise((resolve) => setTimeout(resolve, COLLECT_DEADLINE_MS)), + ]); + + let pendingReview = 0; + for (const { txn } of started) { + const outcome = outcomes.get(txn.transactionId)!; + console.log('----------------------------------------------------'); + console.log(` ${txn.transactionId}`); + + if (outcome.state === 'pending') { + pendingReview++; + console.log(' Result: STILL RUNNING'); + console.log(' Reason: either it is parked for human review (MEDIUM risk), or the'); + console.log(' Compliance handler is not answering right now.'); + console.log(' Action: check the Temporal UI. If it is parked for review, run'); + console.log(" 'npm run review-starter'."); + } else if (outcome.state === 'failed') { + console.log(' Result: FAILED'); + console.log(` Error: ${outcome.error}`); + } else { + const r = outcome.result; + console.log(` Result: ${r.status}`); + console.log(` Risk: ${r.riskLevel ?? 'N/A'}`); + console.log(` Reason: ${r.explanation ?? 'N/A'}`); + if (r.confirmationNumber) console.log(` Conf#: ${r.confirmationNumber}`); + if (r.error) console.log(` Error: ${r.error}`); + } + console.log(); + } + + console.log('=========================================================='); + if (pendingReview > 0) { + console.log(` ${pendingReview} transaction(s) have not resolved yet.`); + console.log(" Parked for review? Run 'npm run review-starter'."); + console.log(' Compliance Worker down? Start it and they resume on their own.'); + } else { + console.log(' All 3 transactions resolved!'); + } + console.log(' Check Temporal UI: http://localhost:8233'); + console.log('=========================================================='); + + await connection.close(); +} + +run().then( + () => process.exit(0), + (err) => { + console.error(err); + process.exit(1); + }, +); diff --git a/typescript/decouple-monolith/exercise/src/payments/worker.ts b/typescript/decouple-monolith/exercise/src/payments/worker.ts new file mode 100644 index 0000000..ba8f2db --- /dev/null +++ b/typescript/decouple-monolith/exercise/src/payments/worker.ts @@ -0,0 +1,51 @@ +// MONOLITH VERSION. One Worker, one deployment, one blast radius. +// +// Right now this single process runs everything: +// - paymentProcessingWorkflow (Payments team) +// - payment activities (Payments team) +// - compliance activities (Compliance team's code, running in Payments' process) +// +// That last line is the problem. A bug in compliance code takes payments down with it. +// +// Do TODO 6 last, after TODO 5 has moved the Workflow onto a Nexus client. +import { NativeConnection, Worker } from '@temporalio/worker'; +import * as paymentActivities from './activities'; +import * as complianceActivities from '../compliance/activities'; +import { PAYMENTS_NAMESPACE, PAYMENTS_TASK_QUEUE } from '../shared/types'; + +async function run() { + const connection = await NativeConnection.connect({ address: 'localhost:7233' }); + try { + const worker = await Worker.create({ + connection, + namespace: PAYMENTS_NAMESPACE, + taskQueue: PAYMENTS_TASK_QUEUE, + workflowsPath: require.resolve('./workflows'), + + // ── TODO 6 ──────────────────────────────────────────────────────────────── + // Once the Workflow calls Compliance over Nexus, this Worker has no reason to run + // the Compliance team's code. Remove `...complianceActivities` from the object + // below, and delete the import at the top of this file. + // + // That deletion is the decoupling. Everything before it was wiring; this is the + // line where the two teams stop sharing a process. + activities: { ...paymentActivities, ...complianceActivities }, + }); + + console.log('========================================================='); + console.log(` Payments Worker started on: ${PAYMENTS_TASK_QUEUE}`); + console.log(` Namespace: ${PAYMENTS_NAMESPACE}`); + console.log(' Registered: paymentProcessingWorkflow, payment activities'); + console.log(' compliance activities (monolith, will decouple)'); + console.log('========================================================='); + + await worker.run(); + } finally { + await connection.close(); + } +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/typescript/decouple-monolith/exercise/src/payments/workflows.ts b/typescript/decouple-monolith/exercise/src/payments/workflows.ts new file mode 100644 index 0000000..9635ba2 --- /dev/null +++ b/typescript/decouple-monolith/exercise/src/payments/workflows.ts @@ -0,0 +1,126 @@ +// MONOLITH VERSION. This already works — you run it in challenge 01. +// +// Three steps, all on one Worker: +// Step 1: validatePayment (payment Activity) +// Step 2: checkCompliance (compliance Activity) <- this one crosses a team boundary +// Step 3: executePayment (payment Activity) +import * as wf from '@temporalio/workflow'; +import type * as paymentActivities from './activities'; +import type * as complianceActivities from '../compliance/activities'; +import { + ComplianceRequest, + ComplianceResult, + PaymentRequest, + PaymentResult, + ReviewRequest, +} from '../shared/types'; + +const { validatePayment, executePayment } = wf.proxyActivities({ + startToCloseTimeout: '30 seconds', + retry: { initialInterval: '1 second', backoffCoefficient: 2 }, +}); + +// ── TODO 5 ────────────────────────────────────────────────────────────────────────── +// Delete the `checkCompliance` proxy below and build a Nexus Service client instead. +// +// What that proxy is: a fake object you call like a normal function. It does not run the +// code — it tells Temporal to schedule an Activity, and Activities run on THIS Worker. +// That is the coupling you are removing. +// +// A Nexus Service client is the same idea, but calls on it go out through an Endpoint to +// whichever Worker owns the Service. Build it INSIDE the Workflow function, like this: +// +// const compliance = wf.createNexusServiceClient({ +// service: complianceService, // import from '../shared/nexus-service' +// endpoint: COMPLIANCE_ENDPOINT, // import from '../shared/types' +// }); +// +// then in Step 2, replace the call with: +// +// await compliance.executeOperation('checkCompliance', compReq, { +// scheduleToCloseTimeout: '10 minutes', +// }) +// +// The 10 minutes is the budget for the whole call, retries included. It is what lets the +// Operation survive the Compliance Worker going away, instead of failing the moment it +// does. You prove that in challenge 05. +// +// Note how the Endpoint name appears HERE, at the call site. If you have seen the Java +// or Kotlin version of this workshop, that is a real difference: there the Endpoint is +// configured on the Worker instead. In TypeScript the client carries it. +// +// Stuck? "Ask AI" on https://docs.temporal.io: +// "how do I call a Nexus Operation from a Workflow in TypeScript?" +const { checkCompliance } = wf.proxyActivities({ + startToCloseTimeout: '30 seconds', +}); + +export async function paymentProcessingWorkflow(request: PaymentRequest): Promise { + const logger = wf.log; + + // Step 1: validate (Payments team). + if (!(await validatePayment(request))) { + return { + success: false, + transactionId: request.transactionId, + status: 'REJECTED', + error: 'Payment validation failed', + }; + } + logger.info(`Step 1 passed: validation OK for ${request.transactionId}`); + + // Step 2: compliance check. + const compReq: ComplianceRequest = { + transactionId: request.transactionId, + amount: request.amount, + senderCountry: request.senderCountry, + receiverCountry: request.receiverCountry, + description: request.description, + }; + + logger.info(`Step 2: calling compliance check for ${request.transactionId}`); + + // This one line is the entire difference between running Compliance code in this + // process and calling another team's service across a durable boundary. + const result: ComplianceResult = await checkCompliance(compReq); + + logger.info(`Compliance result: ${result.riskLevel} | approved=${result.approved}`); + + // A declined payment is a business outcome, not a failure. The Workflow completes + // successfully and reports the decision. + if (!result.approved) { + return { + success: false, + transactionId: request.transactionId, + status: 'DECLINED_COMPLIANCE', + riskLevel: result.riskLevel, + explanation: result.explanation, + }; + } + + // Step 3: execute, only if compliance approved. + logger.info(`Step 3: executing payment for ${request.transactionId}`); + const confirmationNumber = await executePayment(request); + + return { + success: true, + transactionId: request.transactionId, + status: 'COMPLETED', + riskLevel: result.riskLevel, + explanation: result.explanation, + confirmationNumber, + }; +} + +// ── TODO 7 ────────────────────────────────────────────────────────────────────────── +// You come back to this one in challenge 05, once the Nexus path works end to end. +// +// Submitting a human review through the Endpoint respects the team boundary: neither +// team needs to know the other's Workflow IDs or internal method names. +// +// submitReview is a SYNCHRONOUS Operation, so the Compliance handler must finish inside +// the 10-second handler deadline. Use a 10-second scheduleToCloseTimeout here, not the +// 10 minutes used for the async check. +export async function reviewCallerWorkflow(_request: ReviewRequest): Promise { + throw new Error('TODO 7: submit the review decision through the Nexus Endpoint'); +} diff --git a/typescript/decouple-monolith/exercise/src/shared/nexus-service.ts b/typescript/decouple-monolith/exercise/src/shared/nexus-service.ts new file mode 100644 index 0000000..0736a10 --- /dev/null +++ b/typescript/decouple-monolith/exercise/src/shared/nexus-service.ts @@ -0,0 +1,29 @@ +// The shared contract between the Payments and Compliance teams. Like an OpenAPI spec, +// but durable. Payments builds a client from it; Compliance implements a handler for it. +// +// Both teams import THIS file. It is the only thing they share. +// +// Looking things up during this lab: start at +// https://docs.temporal.io/develop/typescript/nexus/feature-guide +// Faster still, the "Ask AI" button on https://docs.temporal.io answers in plain +// language. Try "how do I define a Nexus Service contract in TypeScript?" Get used to +// it now — the later TODOs are harder. +// +// Note: TypeScript SDK support for Nexus is at Pre-release and its APIs are experimental. +import * as nexus from 'nexus-rpc'; +import { ComplianceRequest, ComplianceResult, ReviewRequest } from './types'; + +// ── TODO 1 ──────────────────────────────────────────────────────────────────────── +// Declare the two Operations this Service offers. Each one is a name paired with +// nexus.operation(), which carries no implementation — only the types +// that both teams agree on. +// +// checkCompliance ComplianceRequest -> ComplianceResult +// submitReview ReviewRequest -> ComplianceResult +// +// Add them one at a time and watch what happens: the moment you declare an Operation +// here, TypeScript demands a matching handler in compliance/nexus-handler.ts. The +// contract is enforced at compile time, which is the whole point of sharing it. +export const complianceService = nexus.service('ComplianceNexusService', { + // your Operations go here +}); diff --git a/typescript/decouple-monolith/exercise/src/shared/types.ts b/typescript/decouple-monolith/exercise/src/shared/types.ts new file mode 100644 index 0000000..019fc20 --- /dev/null +++ b/typescript/decouple-monolith/exercise/src/shared/types.ts @@ -0,0 +1,60 @@ +// [GIVEN] The data that crosses team boundaries. +// +// These are plain interfaces. Unlike the Kotlin version, nothing here needs a +// no-arg constructor or parameter-name retention: the TypeScript SDK's default +// data converter is JSON, so a structural type is all the Nexus boundary needs. + +/** A payment transaction to be processed. */ +export interface PaymentRequest { + transactionId: string; + amount: number; + currency: string; + senderCountry: string; + receiverCountry: string; + description: string; + senderAccount: string; + receiverAccount: string; +} + +/** The outcome of a payment Workflow. */ +export interface PaymentResult { + success: boolean; + transactionId: string; + status: 'COMPLETED' | 'REJECTED' | 'DECLINED_COMPLIANCE' | 'FAILED'; + riskLevel?: RiskLevel; + explanation?: string; + confirmationNumber?: string; + error?: string; +} + +export type RiskLevel = 'LOW' | 'MEDIUM' | 'HIGH'; + +/** Input to the compliance check. Sent by Payments to Compliance. */ +export interface ComplianceRequest { + transactionId: string; + amount: number; + senderCountry: string; + receiverCountry: string; + description: string; +} + +/** Result of a compliance check. Returned by Compliance to Payments. */ +export interface ComplianceResult { + transactionId: string; + approved: boolean; + riskLevel: RiskLevel; + explanation: string; +} + +/** A human review decision, submitted through Nexus. */ +export interface ReviewRequest { + transactionId: string; + approved: boolean; + explanation: string; +} + +export const PAYMENTS_TASK_QUEUE = 'payments-processing'; +export const COMPLIANCE_TASK_QUEUE = 'compliance-risk'; +export const PAYMENTS_NAMESPACE = 'payments-namespace'; +export const COMPLIANCE_NAMESPACE = 'compliance-namespace'; +export const COMPLIANCE_ENDPOINT = 'compliance-endpoint'; diff --git a/typescript/decouple-monolith/exercise/tsconfig.json b/typescript/decouple-monolith/exercise/tsconfig.json new file mode 100644 index 0000000..f0e6b74 --- /dev/null +++ b/typescript/decouple-monolith/exercise/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "version": "5.6.3", + "compilerOptions": { + "outDir": "lib", + "rootDir": "src", + "strict": true + }, + "include": ["src/**/*.ts"] +} diff --git a/typescript/decouple-monolith/solution/package-lock.json b/typescript/decouple-monolith/solution/package-lock.json new file mode 100644 index 0000000..fc6a38e --- /dev/null +++ b/typescript/decouple-monolith/solution/package-lock.json @@ -0,0 +1,2349 @@ +{ + "name": "decouple-monolith-solution", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "decouple-monolith-solution", + "version": "1.0.0", + "dependencies": { + "@temporalio/activity": "^1.23.0", + "@temporalio/client": "^1.23.0", + "@temporalio/nexus": "^1.23.0", + "@temporalio/worker": "^1.23.0", + "@temporalio/workflow": "^1.23.0", + "nexus-rpc": "^0.0.3" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.0", + "@types/node": "^22.9.1", + "ts-node": "^10.9.2", + "typescript": "^5.6.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", + "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/source-map/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/buffers": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", + "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-core": { + "version": "4.68.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.68.1.tgz", + "integrity": "sha512-V5oZ4Gt9WJKyQef0n9cAd0N9qjSkIBm3E4MYsgNIWBk5aINCDPKxMPo1i29rBxqiT4Ixf1epklqV9VJMKIxwlw==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.68.1", + "@jsonjoy.com/fs-node-utils": "4.68.1", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-fsa": { + "version": "4.68.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.68.1.tgz", + "integrity": "sha512-HCG72UioncuO7Gw09XNVG+S85e3cq2hrUC/mexBrsWsa3mI7eePkkqWie3uVYbtsb64OR9YGQs5SqaufDRYBcg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.68.1", + "@jsonjoy.com/fs-node-builtins": "4.68.1", + "@jsonjoy.com/fs-node-utils": "4.68.1", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node": { + "version": "4.68.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.68.1.tgz", + "integrity": "sha512-R5D9mWtqdURzcOWj1vdXr3APCwX0xchtFT+kmW7fXLNDifWdDrnh26jSID8pdnUfFBxTyfHtFtTL/NWKzIH7kQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.68.1", + "@jsonjoy.com/fs-node-builtins": "4.68.1", + "@jsonjoy.com/fs-node-utils": "4.68.1", + "@jsonjoy.com/fs-print": "4.68.1", + "@jsonjoy.com/fs-snapshot": "4.68.1", + "glob-to-regex.js": "^1.0.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-builtins": { + "version": "4.68.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.68.1.tgz", + "integrity": "sha512-HK1BTksysokNZxNspqDH0yPaqN9YgR/AYIlYiIaU2Ys4BOk5CdybI7r6BgiZuiiPiV8n4sK/kZdice7Znpy2Kw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-to-fsa": { + "version": "4.68.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.68.1.tgz", + "integrity": "sha512-lpKmU4X9e/oh8GIuAI7EXaS5QiLNM3KD15CkdhfS6PYmrGvoJqKQcyEfnLgnnaGslh/PFUMYSIZBCf2ejJGw8g==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-fsa": "4.68.1", + "@jsonjoy.com/fs-node-builtins": "4.68.1", + "@jsonjoy.com/fs-node-utils": "4.68.1" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-utils": { + "version": "4.68.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.68.1.tgz", + "integrity": "sha512-/GxfW1DWm9SCdkfbvqevLO/P5duobQfmKkHXxdMIDbcZMQeAgooAstIfZhkXpATzq9QbCQsnoWFM/dGHdZfndw==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.68.1", + "glob-to-regex.js": "^1.0.1" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-print": { + "version": "4.68.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.68.1.tgz", + "integrity": "sha512-oGeZOGPYKK9v1CgeVeEDsLomH1lCnslSpqUN5GmPzrmAVGQlsmsdcXNA2O4lV8Y4xkuSuynx2ITBkUHJVaTbow==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-utils": "4.68.1", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot": { + "version": "4.68.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.68.1.tgz", + "integrity": "sha512-XZfP0FDZN32bbc4t2bZN2qRrYHg5AktJnzk22HRoKGK4BprrbNRH2k5ceSNS/kupKYcofCs+O841+xaAbjnxwQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^17.65.0", + "@jsonjoy.com/fs-node-utils": "4.68.1", + "@jsonjoy.com/json-pack": "^17.65.0", + "@jsonjoy.com/util": "^17.65.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", + "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", + "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", + "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "17.67.0", + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0", + "@jsonjoy.com/json-pointer": "17.67.0", + "@jsonjoy.com/util": "17.67.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", + "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/util": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", + "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.2.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.2", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, + "node_modules/@swc/core": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.16.1.tgz", + "integrity": "sha512-nUaeu91O5QZKrQdaDCHd402ogUIoNOOjpkZNq0UomWK0G6gDaGmLhvddF1/3BXf5O8aLyo6ZPY/aMDWvaJQ/hg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.28" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.16.1", + "@swc/core-darwin-x64": "1.16.1", + "@swc/core-linux-arm-gnueabihf": "1.16.1", + "@swc/core-linux-arm64-gnu": "1.16.1", + "@swc/core-linux-arm64-musl": "1.16.1", + "@swc/core-linux-ppc64-gnu": "1.16.1", + "@swc/core-linux-s390x-gnu": "1.16.1", + "@swc/core-linux-x64-gnu": "1.16.1", + "@swc/core-linux-x64-musl": "1.16.1", + "@swc/core-win32-arm64-msvc": "1.16.1", + "@swc/core-win32-ia32-msvc": "1.16.1", + "@swc/core-win32-x64-msvc": "1.16.1" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.16.1.tgz", + "integrity": "sha512-zlJblJ8ncErD43lKdxjbUaUskJQf+LxiPXYcWXD8/8ZMV+7uuAT+CwjciLXpyZBd5Pq/S726bMpeeAwSeL1hhg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.16.1.tgz", + "integrity": "sha512-IN0BmPWb0YAh/17mmlWB/HDBtTw2MfuW4hulf/tQAgTQBRH17l+z499bNJLK6LizSjqs0P7V+jU38Zj+vJC1DA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.16.1.tgz", + "integrity": "sha512-EYgrx2YOCQ2Twz2S793kqNjPkpvYVUPzzR95bIb7by+VQcyaai4lZZ2iz/tZvcFVKSNcN3/JTKwx+aBn2ZL52A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.16.1.tgz", + "integrity": "sha512-moyKm0YZlHdHohzm1YwgAyesqnE853rO0REMfJLFAova51wF9BNi+3ZW2PeS7Vqvn6HeJuepLpAHbBdZctxpHA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.16.1.tgz", + "integrity": "sha512-kKGBO9wdapiSzuf5ZzZ2fYtlu1BNSYtIIUxvH1ir/gcelTOREEHGDCLTDFx/2Knf878nU11A40z7LxwasEFxqA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.16.1.tgz", + "integrity": "sha512-nZ6qahtLxC3PM54cWOQZHxt4lTCF/3J4LIoWWzz6v7A+rLs8Dx54anYQf7mH3eIi8KlNpgKci/ie8ZSqFN8O7A==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.16.1.tgz", + "integrity": "sha512-4ji5PNzhYq193Z4/4xUaSoNJza6iCkDJSzhetrbB6KOYxsr+kxtQr8ePWhMJUiMt6JUWtXaZ1PYT8FhtED+nGA==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.16.1.tgz", + "integrity": "sha512-VJQxqrisHV+B394IgrOu8YsIIXZgffnf5tO+yc9Z/hoUpuZEvuQTjWwlnpZdpyD+0nx6LTD1/3k646JYm43yJA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.16.1.tgz", + "integrity": "sha512-r9oV1mwxxsIGcLV1IQ/tw76MW3doatKze1QFWuC+a7QqJUkhY/bKTSVk6NpKKUGm2LDsE33Va8VqSClfA7vSiQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.16.1.tgz", + "integrity": "sha512-6huNRessoBLxWEqBm5zJXyCQ27TO7anvkdiuQ5MDO4CJni0nOXEqKtV9RllQ2TdyENKKsUMXVnIfW2hIXx/R5Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.16.1.tgz", + "integrity": "sha512-OVKJFUzphrGmsh+BGtcZDesx0YryV7/Yvy5XGgTqnrZfjnyfcr5uaqYQugCckdIlupc5Vs3XtDjRAj12z4ZPlw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.16.1.tgz", + "integrity": "sha512-Bt+VIhWYCGk4urklnkkteLUOeLv1VxigwTCeB/xC6rBZxY6IIKdDwCJf6on3E3SUGsIqmQS6QqtuJQc1VxF4Aw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.28", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.28.tgz", + "integrity": "sha512-V6Mnml8v09QALx6K0elJ7o9K/MkVDtW3t6L+7Ou/JcWtb3xwId2AH4FeOceySd2JaO87IMw4+6vSZxLm34LPbw==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@temporalio/activity": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@temporalio/activity/-/activity-1.23.0.tgz", + "integrity": "sha512-BVeiizVD08yk0Ls6MhKCK+g3fPIWbVM9MLx+o990KwhCaZtRM6o1+du1/qfIIShHl6YjHk+H0ewkx2x14QQ6bA==", + "license": "MIT", + "dependencies": { + "@temporalio/client": "1.23.0", + "@temporalio/common": "1.23.0" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@temporalio/client": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@temporalio/client/-/client-1.23.0.tgz", + "integrity": "sha512-E9nXZPiKDGEDbPpRi0EyQ9i1x70fEIG26aMXZO88kKslKZnEKNPphs06xIic7bhuDoSrPsgVfdDap6o3l6IO2A==", + "license": "MIT", + "dependencies": { + "@grpc/grpc-js": "^1.12.4", + "@temporalio/common": "1.23.0", + "@temporalio/proto": "1.23.0", + "abort-controller": "^3.0.0", + "long": "^5.3.2", + "nexus-rpc": "^0.0.3", + "uuid": "^11.1.0" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@temporalio/common": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@temporalio/common/-/common-1.23.0.tgz", + "integrity": "sha512-TaZYb/ruzgzJ8jd42EqRUCI4ispLZETt2Jigc9szES6q9edDmfncRw143HyvPi7b8n9naohYmA/IxTe9COH/dg==", + "license": "MIT", + "dependencies": { + "@temporalio/proto": "1.23.0", + "long": "^5.3.2", + "ms": "3.0.0-canary.1", + "nexus-rpc": "^0.0.3", + "protobufjs": "^8.7.1" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@temporalio/common/node_modules/protobufjs": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.8.0.tgz", + "integrity": "sha512-N3xhQ5yyBx3vQq4gubBfASzYhJGNzeDbjqBpu61g7UVylsN/qyffU96TKWD3GbbLOKF82VGNRNvv1+BFgE31Eg==", + "license": "BSD-3-Clause", + "dependencies": { + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@temporalio/core-bridge": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@temporalio/core-bridge/-/core-bridge-1.23.0.tgz", + "integrity": "sha512-sid14odlYnci9WgB+iDLyxFbzkq+qBbRPp+CW9U1v+DLRfHlFJwpznQ1AZl4mHjtAZRGQFvDsDG0XNncl9ESSA==", + "license": "MIT", + "dependencies": { + "@grpc/grpc-js": "^1.12.4", + "@temporalio/common": "1.23.0" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@temporalio/nexus": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@temporalio/nexus/-/nexus-1.23.0.tgz", + "integrity": "sha512-mObGNp3thR+mTXdtKXpUvFpsdjEI9xpVjewhblKXao7Nqv6xrs7RpFiLdKfJhkEvDyfRgIk/dH5RkiCbZ58I9w==", + "license": "MIT", + "dependencies": { + "@temporalio/client": "1.23.0", + "@temporalio/common": "1.23.0", + "@temporalio/proto": "1.23.0", + "long": "^5.3.2", + "nexus-rpc": "^0.0.3" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@temporalio/proto": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@temporalio/proto/-/proto-1.23.0.tgz", + "integrity": "sha512-90JWnYEs1wyq97/SXkQGknRVMr/BCV9sZ0pOnK3fHTedVinfvz3/PBVFkeAJyU3CJqMQGnzRs9IIfsBTT1icrA==", + "license": "MIT", + "dependencies": { + "long": "^5.3.2", + "protobufjs": "^8.7.1" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@temporalio/proto/node_modules/protobufjs": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.8.0.tgz", + "integrity": "sha512-N3xhQ5yyBx3vQq4gubBfASzYhJGNzeDbjqBpu61g7UVylsN/qyffU96TKWD3GbbLOKF82VGNRNvv1+BFgE31Eg==", + "license": "BSD-3-Clause", + "dependencies": { + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@temporalio/worker": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@temporalio/worker/-/worker-1.23.0.tgz", + "integrity": "sha512-k1MrK10lD1s7U66PFqAwGlQMRsVrfU5nCB+CjIAg5RyL7JpOzidDCz/E/jB2HobXzZnxtMcDBfBHh/BYUoyhxw==", + "license": "MIT", + "dependencies": { + "@grpc/grpc-js": "^1.12.4", + "@swc/core": "^1.3.102", + "@temporalio/activity": "1.23.0", + "@temporalio/client": "1.23.0", + "@temporalio/common": "1.23.0", + "@temporalio/core-bridge": "1.23.0", + "@temporalio/nexus": "1.23.0", + "@temporalio/proto": "1.23.0", + "@temporalio/workflow": "1.23.0", + "heap-js": "^2.6.0", + "memfs": "^4.6.0", + "nexus-rpc": "^0.0.3", + "protobufjs": "^8.7.1", + "rxjs": "^7.8.1", + "source-map": "^0.7.4", + "source-map-loader": "^5.0.0", + "supports-color": "^8.1.1", + "swc-loader": "^0.2.3", + "unionfs": "^4.5.1", + "webpack": "^5.108.4" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@temporalio/worker/node_modules/protobufjs": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.8.0.tgz", + "integrity": "sha512-N3xhQ5yyBx3vQq4gubBfASzYhJGNzeDbjqBpu61g7UVylsN/qyffU96TKWD3GbbLOKF82VGNRNvv1+BFgE31Eg==", + "license": "BSD-3-Clause", + "dependencies": { + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@temporalio/workflow": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@temporalio/workflow/-/workflow-1.23.0.tgz", + "integrity": "sha512-dHQnYmxU/QN7VfTmdfCfpdfTS1w0hoW/NQ7Zefr55QnqLwZw+E0Vn6S4fAbJrpfHrsAmezUZQFVLi+Z4Sq+jzA==", + "license": "MIT", + "dependencies": { + "@temporalio/common": "1.23.0", + "@temporalio/proto": "1.23.0", + "nexus-rpc": "^0.0.3" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.13.tgz", + "integrity": "sha512-gcLdvR9HO1ZJBypsOGqaP6TFEzb6vIta0KSTLt9NAQ6pXQO3cRgSVyCN6pzYqI9DlJgY71XKO0dpDhCf08b3pg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node22": { + "version": "22.0.6", + "resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.6.tgz", + "integrity": "sha512-/5thavHAnWDZwH2H6eEn/T8pBEBhtuKaWGMslsj82kJfvzQgOgEMK7X4goBbIUjgVtAzPTtM9Ml64LA0uxO6Iw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz", + "integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.416", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.416.tgz", + "integrity": "sha512-K6bvB2BjnNrugtIih6ewlbBI9DXa976jIdiIlRLHhBoEI9a4JaQjjHyF+A1IQI543aQYR4LnmOrT/K5fZj0aPA==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.6.tgz", + "integrity": "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "license": "Unlicense" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob-to-regex.js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/heap-js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/heap-js/-/heap-js-2.7.1.tgz", + "integrity": "sha512-EQfezRg0NCZGNlhlDR3Evrw1FVL2G3LhU7EgPoxufQKruNBSYA8MiRPHeWbU+36o+Fhel0wMwM+sLEiBAlNLJA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "license": "MIT", + "engines": { + "node": ">=10.18" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/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==", + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/memfs": { + "version": "4.68.1", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.68.1.tgz", + "integrity": "sha512-OD+IDRUvIxu3QHL+nFm9gdyugInD27FDJ+sl4B5QgomPHXMlbw+GP918P8VNKu2FkNlVeqBkpzkwROpamVifRw==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.68.1", + "@jsonjoy.com/fs-fsa": "4.68.1", + "@jsonjoy.com/fs-node": "4.68.1", + "@jsonjoy.com/fs-node-builtins": "4.68.1", + "@jsonjoy.com/fs-node-to-fsa": "4.68.1", + "@jsonjoy.com/fs-node-utils": "4.68.1", + "@jsonjoy.com/fs-print": "4.68.1", + "@jsonjoy.com/fs-snapshot": "4.68.1", + "@jsonjoy.com/json-pack": "^1.11.0", + "@jsonjoy.com/util": "^1.9.0", + "glob-to-regex.js": "^1.0.1", + "thingies": "^2.5.0", + "tree-dump": "^1.0.3", + "tslib": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimizer-webpack-plugin": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.8.0.tgz", + "integrity": "sha512-2cT9+goJfBhtMz+gJqejSf09ClgmYhPhccRb/fb0ztVbixt0BkO8mRuI26FoPPuUNkRk6iEDryWAIouc5W8eJA==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.3", + "terser": "^5.51.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/ms": { + "version": "3.0.0-canary.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-3.0.0-canary.1.tgz", + "integrity": "sha512-kh8ARjh8rMN7Du2igDRO9QJnqCb2xYTJxyQYK7vJJS4TvLLmsbyhiKpSW+t+y26gyOyMd0riphX0GeWKU3ky5g==", + "license": "MIT", + "engines": { + "node": ">=12.13" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/nexus-rpc": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/nexus-rpc/-/nexus-rpc-0.0.3.tgz", + "integrity": "sha512-ksIBeYHDv6FHAR1h3Vwk0kQ6REhdo1STqDHXMzu/yn/VN/XzgOor2IUjewG6paoA7IgxlIPZoOU/PD78PC6HTg==", + "license": "MIT", + "engines": { + "node": ">= 20.0.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", + "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/swc-loader": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/swc-loader/-/swc-loader-0.2.7.tgz", + "integrity": "sha512-nwYWw3Fh9ame3Rtm7StS9SBLpHRRnYcK7bnpF3UKZmesAK0gw2/ADvlURFAINmPvKtDLzp+GBiP9yLoEjg6S9w==", + "license": "MIT", + "dependencies": { + "@swc/counter": "^0.1.3" + }, + "peerDependencies": { + "@swc/core": "^1.2.147", + "webpack": ">=2" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.51.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.51.2.tgz", + "integrity": "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/thingies": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.1.tgz", + "integrity": "sha512-cV/CMGTK3M4MlnJ/0At6ismOw/A0EEniDNScajjz/Br3c1sqE72YD01rGpPTKwd27wAxI5Pr+6+0w8yofzFRYw==", + "license": "MIT", + "engines": { + "node": ">=10.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "node_modules/tree-dump": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", + "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unionfs": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/unionfs/-/unionfs-4.6.0.tgz", + "integrity": "sha512-fJAy3gTHjFi5S3TP5EGdjs/OUMFFvI/ady3T8qVuZfkv8Qi8prV/Q8BuFEgODJslhZTT2z2qdD2lGdee9qjEnA==", + "dependencies": { + "fs-monkey": "^1.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.110.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.110.1.tgz", + "integrity": "sha512-gInQB+jxXxgnZyvPwuzT5NGQmECDqeu85oxcrjinrYHqPoBex0hCAN2SFTJVyPVrK0Pq9E44VFP+e89fAc10/w==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.24.4", + "es-module-lexer": "^2.1.0", + "events": "^3.2.0", + "graceful-fs": "^4.2.11", + "mime-db": "^1.54.0", + "minimizer-webpack-plugin": "^5.7.0", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "watchpack": "^2.5.2", + "webpack-sources": "^3.5.1" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz", + "integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/typescript/decouple-monolith/solution/package.json b/typescript/decouple-monolith/solution/package.json new file mode 100644 index 0000000..24c65eb --- /dev/null +++ b/typescript/decouple-monolith/solution/package.json @@ -0,0 +1,26 @@ +{ + "name": "decouple-monolith-solution", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "tsc --build", + "payments-worker": "ts-node src/payments/worker.ts", + "compliance-worker": "ts-node src/compliance/worker.ts", + "starter": "ts-node src/payments/starter.ts", + "review-starter": "ts-node src/payments/review-starter.ts" + }, + "dependencies": { + "@temporalio/activity": "^1.23.0", + "@temporalio/client": "^1.23.0", + "@temporalio/nexus": "^1.23.0", + "@temporalio/worker": "^1.23.0", + "@temporalio/workflow": "^1.23.0", + "nexus-rpc": "^0.0.3" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.0", + "@types/node": "^22.9.1", + "ts-node": "^10.9.2", + "typescript": "^5.6.3" + } +} diff --git a/typescript/decouple-monolith/solution/src/compliance/activities.ts b/typescript/decouple-monolith/solution/src/compliance/activities.ts new file mode 100644 index 0000000..9e18362 --- /dev/null +++ b/typescript/decouple-monolith/solution/src/compliance/activities.ts @@ -0,0 +1,12 @@ +// [GIVEN] Compliance team Activity. +// +// In the monolith, paymentProcessingWorkflow calls this Activity directly — which means +// Compliance's code runs inside the Payments Worker process. After decoupling, the +// Activity stays here but is called from the Compliance Worker, inside the Workflow that +// the Nexus handler starts. +import { checkCompliance as runCheck } from './checker'; +import { ComplianceRequest, ComplianceResult } from '../shared/types'; + +export async function checkCompliance(request: ComplianceRequest): Promise { + return runCheck(request); +} diff --git a/typescript/decouple-monolith/solution/src/compliance/checker.ts b/typescript/decouple-monolith/solution/src/compliance/checker.ts new file mode 100644 index 0000000..1879a3a --- /dev/null +++ b/typescript/decouple-monolith/solution/src/compliance/checker.ts @@ -0,0 +1,59 @@ +// [GIVEN] Rule-based compliance checker. Deterministic, no API keys needed. +// Same logic whether it runs as an Activity or behind a Nexus handler. +// +// Rules: +// - OFAC sanctioned country on either side -> HIGH risk, blocked +// - Amount over $50,000 -> HIGH risk, blocked (TXN-C hits this) +// - Over $10,000 or an unusual jurisdiction -> MEDIUM risk (TXN-B hits this) +// - Everything else -> LOW risk, approved +import { ComplianceRequest, ComplianceResult } from '../shared/types'; + +const SANCTIONED = new Set(['North Korea', 'Iran', 'Cuba', 'Syria', 'Venezuela']); +const COMMON = new Set(['US', 'UK', 'Canada', 'Germany', 'France', 'Japan', 'Australia']); + +export function checkCompliance(request: ComplianceRequest): ComplianceResult { + console.log( + `[ComplianceChecker] Evaluating ${request.transactionId}` + + ` | $${request.amount.toFixed(2)}` + + ` | ${request.senderCountry} -> ${request.receiverCountry}`, + ); + + if (SANCTIONED.has(request.receiverCountry) || SANCTIONED.has(request.senderCountry)) { + return { + transactionId: request.transactionId, + approved: false, + riskLevel: 'HIGH', + explanation: + 'Destination/source country is OFAC-sanctioned. Transaction blocked per regulatory requirements.', + }; + } + + if (request.amount > 50_000) { + return { + transactionId: request.transactionId, + approved: false, + riskLevel: 'HIGH', + explanation: + 'Transaction amount exceeds $50,000 threshold. Requires enhanced due diligence review.', + }; + } + + const isInternational = request.senderCountry !== request.receiverCountry; + const isUnusualJurisdiction = isInternational && !COMMON.has(request.receiverCountry); + + if (request.amount > 10_000 || isUnusualJurisdiction) { + return { + transactionId: request.transactionId, + approved: true, + riskLevel: 'MEDIUM', + explanation: 'International transfer above $10K threshold. Approved with AML monitoring note.', + }; + } + + return { + transactionId: request.transactionId, + approved: true, + riskLevel: 'LOW', + explanation: 'Routine domestic/standard international transfer. No regulatory concerns.', + }; +} diff --git a/typescript/decouple-monolith/solution/src/compliance/nexus-handler.ts b/typescript/decouple-monolith/solution/src/compliance/nexus-handler.ts new file mode 100644 index 0000000..1fdeac8 --- /dev/null +++ b/typescript/decouple-monolith/solution/src/compliance/nexus-handler.ts @@ -0,0 +1,45 @@ +// The Compliance team's Nexus handler. Payments depends on the contract in +// shared/nexus-service.ts. This file is the code that answers those calls, and only +// the Compliance team owns it. +import * as nexus from 'nexus-rpc'; +import * as temporalNexus from '@temporalio/nexus'; +import { complianceService } from '../shared/nexus-service'; +import { ComplianceRequest, ComplianceResult, ReviewRequest } from '../shared/types'; +import { complianceWorkflow, reviewUpdate } from './workflows'; + +/** Same ID both times: one Operation creates the Workflow, the other looks it up. */ +const workflowIdFor = (transactionId: string) => `compliance-${transactionId}`; + +export const complianceServiceHandler = nexus.serviceHandler(complianceService, { + // The SLOW call. A risky payment may need a person to approve it, so an answer can be + // minutes or hours away. This Operation must not sit and wait for it. + // + // WorkflowRunOperationHandler starts a Workflow and returns immediately, handing back + // a reference. Temporal holds that reference and collects the result whenever the + // Workflow finishes. + // + // A plain async function would NOT work here: a synchronous Operation is cut off at + // the 10-second handler deadline, and when Temporal retried the failed call it would + // start a SECOND compliance check for the same payment. + checkCompliance: new temporalNexus.WorkflowRunOperationHandler( + async (ctx, input: ComplianceRequest) => + temporalNexus.startWorkflow(ctx, complianceWorkflow, { + args: [input], + // Business-meaningful and deduped: a retried Operation attaches to the same + // compliance check rather than starting a second one. + workflowId: workflowIdFor(input.transactionId), + // Task queue defaults to the queue this Operation is handled on. + }), + ), + + // The FAST call. By now the compliance check is running and parked waiting for a + // decision. All this does is find that Workflow and hand it the yes or no — a few + // milliseconds — so a synchronous handler is the right shape. + submitReview: async (_ctx, input: ReviewRequest): Promise => { + const client = temporalNexus.getClient(); + const handle = client.workflow.getHandle(workflowIdFor(input.transactionId)); + return await handle.executeUpdate(reviewUpdate, { + args: [input.approved, input.explanation], + }); + }, +}); diff --git a/typescript/decouple-monolith/solution/src/compliance/worker.ts b/typescript/decouple-monolith/solution/src/compliance/worker.ts new file mode 100644 index 0000000..ed10624 --- /dev/null +++ b/typescript/decouple-monolith/solution/src/compliance/worker.ts @@ -0,0 +1,37 @@ +// The Compliance team's Worker. This process is the only place their code runs. +import { NativeConnection, Worker } from '@temporalio/worker'; +import * as activities from './activities'; +import { complianceServiceHandler } from './nexus-handler'; +import { COMPLIANCE_NAMESPACE, COMPLIANCE_TASK_QUEUE } from '../shared/types'; + +async function run() { + const connection = await NativeConnection.connect({ address: 'localhost:7233' }); + try { + const worker = await Worker.create({ + connection, + namespace: COMPLIANCE_NAMESPACE, + // MUST match the --target-task-queue given to the Nexus Endpoint, or the Endpoint + // routes work to a queue nobody is polling. + taskQueue: COMPLIANCE_TASK_QUEUE, + workflowsPath: require.resolve('./workflows'), + activities, + // Leave this out and the Worker still starts — Payments' calls just go unanswered + // forever. That is the failure this guards against. + nexusServices: [complianceServiceHandler], + }); + + console.log('========================================================='); + console.log(` Compliance Worker started on: ${COMPLIANCE_TASK_QUEUE}`); + console.log(` Namespace: ${COMPLIANCE_NAMESPACE}`); + console.log('========================================================='); + + await worker.run(); + } finally { + await connection.close(); + } +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/typescript/decouple-monolith/solution/src/compliance/workflows.ts b/typescript/decouple-monolith/solution/src/compliance/workflows.ts new file mode 100644 index 0000000..f1baa4e --- /dev/null +++ b/typescript/decouple-monolith/solution/src/compliance/workflows.ts @@ -0,0 +1,75 @@ +// [GIVEN] Compliance Workflow. Pre-built for this exercise. +// +// Runs an automated compliance check and, for MEDIUM-risk transactions, waits for a +// human reviewer to approve or deny via a Workflow Update. +// +// LOW risk -> auto-approved, returns immediately +// HIGH risk -> auto-denied, returns immediately +// MEDIUM -> pauses, waits for the review Update +// +// The 10-second sleep is intentional. It gives you a window to stop the Compliance +// Worker with Ctrl+C and restart it, so you can watch the Nexus Operation survive the +// outage. The timer runs on the server, so nothing is lost when the Worker goes away. +import * as wf from '@temporalio/workflow'; +import type * as activities from './activities'; +import { ComplianceRequest, ComplianceResult } from '../shared/types'; + +const { checkCompliance } = wf.proxyActivities({ + startToCloseTimeout: '30 seconds', +}); + +/** Update sent by the Compliance team's sync Nexus handler when a human decides. */ +export const reviewUpdate = wf.defineUpdate('review'); + +export async function complianceWorkflow(request: ComplianceRequest): Promise { + let reviewResult: ComplianceResult | undefined; + let autoResult: ComplianceResult | undefined; + + wf.setHandler( + reviewUpdate, + (approved, explanation) => { + const result: ComplianceResult = { + transactionId: request.transactionId, + approved, + riskLevel: 'MEDIUM', + explanation, + }; + reviewResult = result; + return result; + }, + { + validator: (_approved: boolean, _explanation: string) => { + if (autoResult?.riskLevel !== 'MEDIUM') throw new Error('Workflow is not awaiting review'); + if (reviewResult !== undefined) throw new Error('Review already submitted'); + }, + }, + ); + + // Step 1: automated compliance check. + const auto = await checkCompliance(request); + autoResult = auto; + + // Durable delay. Stop the Compliance Worker mid-sleep, restart it, and the Workflow + // resumes on its own. + await wf.sleep('10 seconds'); + + // Step 2: LOW or HIGH risk returns immediately. + if (auto.riskLevel !== 'MEDIUM') return auto; + + // Step 3: MEDIUM risk waits for human review. + // + // Bounded on purpose. An unbounded wait would leave this Workflow Running forever + // when nobody reviews. Eight minutes is deliberately shorter than the caller's + // 10-minute scheduleToCloseTimeout, so the handler resolves and reports a decision + // rather than letting the caller time out first. + const reviewed = await wf.condition(() => reviewResult !== undefined, '8 minutes'); + if (!reviewed) { + return { + transactionId: request.transactionId, + approved: false, + riskLevel: 'MEDIUM', + explanation: 'No reviewer responded within 8 minutes. Not approved.', + }; + } + return reviewResult!; +} diff --git a/typescript/decouple-monolith/solution/src/payments/activities.ts b/typescript/decouple-monolith/solution/src/payments/activities.ts new file mode 100644 index 0000000..18c00df --- /dev/null +++ b/typescript/decouple-monolith/solution/src/payments/activities.ts @@ -0,0 +1,16 @@ +// [GIVEN] Payments team Activities. Thin wrappers around the gateway. +// +// In TypeScript an Activity is just an exported function — there is no interface to +// declare and no annotation to apply. They must be async: proxyActivities only accepts +// Promise-returning functions, and a synchronous one is silently typed away, surfacing +// later as "Type 'Symbol' has no call signatures" at the call site in the Workflow. +import * as gateway from './gateway'; +import { PaymentRequest } from '../shared/types'; + +export async function validatePayment(request: PaymentRequest): Promise { + return gateway.validatePayment(request); +} + +export async function executePayment(request: PaymentRequest): Promise { + return gateway.executePayment(request); +} diff --git a/typescript/decouple-monolith/solution/src/payments/gateway.ts b/typescript/decouple-monolith/solution/src/payments/gateway.ts new file mode 100644 index 0000000..f775915 --- /dev/null +++ b/typescript/decouple-monolith/solution/src/payments/gateway.ts @@ -0,0 +1,46 @@ +// [GIVEN] Simulated payment gateway. In production this would call Stripe, PayPal, SWIFT. +// +// The 10% failure rate is deliberate: it shows Temporal retrying an Activity for free. +// Because of it, never assert on a clean first attempt — assert on final Workflow state. +import { PaymentRequest } from '../shared/types'; + +const money = (n: number) => `$${n.toFixed(2)}`; +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +export function validatePayment(request: PaymentRequest): boolean { + if (request.amount <= 0) { + console.log(`[PaymentGateway] REJECTED: Invalid amount for ${request.transactionId}`); + return false; + } + if (!request.senderAccount || !request.receiverAccount) { + console.log(`[PaymentGateway] REJECTED: Missing account info for ${request.transactionId}`); + return false; + } + console.log(`[PaymentGateway] Validation passed for ${request.transactionId}`); + return true; +} + +export async function executePayment(request: PaymentRequest): Promise { + console.log( + `[PaymentGateway] Processing ${request.transactionId}` + + ` | ${money(request.amount)}` + + ` | ${request.senderCountry} -> ${request.receiverCountry}`, + ); + + await sleep(500 + Math.random() * 500); + + // Simulate occasional gateway failures. Temporal retries automatically. + if (Math.random() < 0.1) { + throw new Error( + `Payment gateway timeout for ${request.transactionId}, connection to banking network failed`, + ); + } + + // Derived from the transaction ID, not the clock, so a retry of this Activity returns + // the SAME confirmation number instead of minting a second one. With a 10% failure + // rate above, a timestamp here would make retries look like distinct payments. + // Idempotency is what makes an at-least-once retry safe. + const confirmationNumber = `CONF-${request.transactionId}`; + console.log(`[PaymentGateway] Payment executed: ${confirmationNumber}`); + return confirmationNumber; +} diff --git a/typescript/decouple-monolith/solution/src/payments/review-starter.ts b/typescript/decouple-monolith/solution/src/payments/review-starter.ts new file mode 100644 index 0000000..e8061dc --- /dev/null +++ b/typescript/decouple-monolith/solution/src/payments/review-starter.ts @@ -0,0 +1,51 @@ +// [GIVEN] Starts a reviewCallerWorkflow to approve TXN-B via Nexus. +// +// Rather than calling 'temporal workflow update execute' directly, this triggers a caller +// Workflow that invokes the submitReview Nexus Operation. The Compliance team's sync +// handler forwards the decision to the running complianceWorkflow as an Update. +// +// Flip `approved` to false below to see the denial path. +import { Client, Connection } from '@temporalio/client'; +import { reviewCallerWorkflow } from './workflows'; +import { PAYMENTS_NAMESPACE, PAYMENTS_TASK_QUEUE, ReviewRequest } from '../shared/types'; + +async function run() { + console.log('=========================================================='); + console.log(' REVIEW STARTER - Submitting review for TXN-B via Nexus'); + console.log('==========================================================\n'); + + const connection = await Connection.connect({ address: 'localhost:7233' }); + const client = new Client({ connection, namespace: PAYMENTS_NAMESPACE }); + + const request: ReviewRequest = { + transactionId: 'TXN-B', + approved: true, + explanation: 'Approved after manual review', + }; + + console.log(' Submitting review for TXN-B via Nexus...'); + console.log(` Approved: ${request.approved}`); + console.log(` Explanation: ${request.explanation}\n`); + + const result = await client.workflow.execute(reviewCallerWorkflow, { + taskQueue: PAYMENTS_TASK_QUEUE, + workflowId: `review-${request.transactionId}`, + args: [request], + }); + + console.log(` Review result: ${result.approved ? 'APPROVED' : 'DENIED'}`); + console.log(` Risk level: ${result.riskLevel}`); + console.log(` Explanation: ${result.explanation}\n`); + console.log(' TXN-B review submitted. The payment Workflow will now complete.'); + console.log('=========================================================='); + + await connection.close(); +} + +run().then( + () => process.exit(0), + (err) => { + console.error(err); + process.exit(1); + }, +); diff --git a/typescript/decouple-monolith/solution/src/payments/starter.ts b/typescript/decouple-monolith/solution/src/payments/starter.ts new file mode 100644 index 0000000..492620b --- /dev/null +++ b/typescript/decouple-monolith/solution/src/payments/starter.ts @@ -0,0 +1,131 @@ +// [GIVEN] Starts three payment Workflows. +// +// All three start before any result is collected. That matters once Nexus is in play: +// TXN-B is MEDIUM risk, so its handler Workflow pauses for human review and would +// otherwise block TXN-C behind it. Starting concurrently lets TXN-A and TXN-C finish +// while TXN-B sits visibly pending, which is the point of the human-in-the-loop path. +// +// The starter does not know or care whether compliance runs locally or via Nexus. +import { Client, Connection } from '@temporalio/client'; +import { paymentProcessingWorkflow } from './workflows'; +import { PAYMENTS_NAMESPACE, PAYMENTS_TASK_QUEUE, PaymentRequest, PaymentResult } from '../shared/types'; + +// How long to wait before calling a Workflow "pending review". TXN-A and TXN-C finish in +// roughly 12 seconds (the handler Workflow sleeps for 10 of those). +const COLLECT_DEADLINE_MS = 25_000; + +const TRANSACTIONS: PaymentRequest[] = [ + { + transactionId: 'TXN-A', amount: 250.0, currency: 'USD', + senderCountry: 'US', receiverCountry: 'US', + description: 'Routine supplier payment', senderAccount: 'ACC-001', receiverAccount: 'ACC-002', + }, + { + transactionId: 'TXN-B', amount: 12_000.0, currency: 'USD', + senderCountry: 'US', receiverCountry: 'UK', + description: 'International consulting fee', senderAccount: 'ACC-003', receiverAccount: 'ACC-004', + }, + { + transactionId: 'TXN-C', amount: 75_000.0, currency: 'USD', + senderCountry: 'US', receiverCountry: 'US', + description: 'Large capital transfer', senderAccount: 'ACC-005', receiverAccount: 'ACC-006', + }, +]; + +type Outcome = + | { state: 'done'; result: PaymentResult } + | { state: 'failed'; error: string } + | { state: 'pending' }; + +async function run() { + console.log('=========================================================='); + console.log(' PAYMENT STARTER - Decouple Monolith'); + console.log(' Running 3 transactions through Temporal'); + console.log('==========================================================\n'); + + const connection = await Connection.connect({ address: 'localhost:7233' }); + const client = new Client({ connection, namespace: PAYMENTS_NAMESPACE }); + + // Start all three without awaiting results, so a pause in one cannot hold up the others. + const started = await Promise.all( + TRANSACTIONS.map(async (txn) => { + const handle = await client.workflow.start(paymentProcessingWorkflow, { + taskQueue: PAYMENTS_TASK_QUEUE, + workflowId: `payment-${txn.transactionId}`, + args: [txn], + }); + console.log(` Started: payment-${txn.transactionId}`); + console.log( + ` Amount: $${txn.amount.toFixed(2)} | Route: ${txn.senderCountry} -> ${txn.receiverCountry}`, + ); + return { txn, handle }; + }), + ); + + console.log('\n All 3 started. Collecting results...\n'); + + // Wait on all three against a single shared deadline. Waiting one at a time would let + // TXN-B's review pause burn the whole budget, so TXN-C would be reported as pending + // even though it finished seconds earlier. + const outcomes = new Map(started.map(({ txn }) => [txn.transactionId, { state: 'pending' }])); + await Promise.race([ + Promise.allSettled( + started.map(async ({ txn, handle }) => { + try { + outcomes.set(txn.transactionId, { state: 'done', result: await handle.result() }); + } catch (err) { + outcomes.set(txn.transactionId, { state: 'failed', error: (err as Error).message }); + } + }), + ), + new Promise((resolve) => setTimeout(resolve, COLLECT_DEADLINE_MS)), + ]); + + let pendingReview = 0; + for (const { txn } of started) { + const outcome = outcomes.get(txn.transactionId)!; + console.log('----------------------------------------------------'); + console.log(` ${txn.transactionId}`); + + if (outcome.state === 'pending') { + pendingReview++; + console.log(' Result: STILL RUNNING'); + console.log(' Reason: either it is parked for human review (MEDIUM risk), or the'); + console.log(' Compliance handler is not answering right now.'); + console.log(' Action: check the Temporal UI. If it is parked for review, run'); + console.log(" 'npm run review-starter'."); + } else if (outcome.state === 'failed') { + console.log(' Result: FAILED'); + console.log(` Error: ${outcome.error}`); + } else { + const r = outcome.result; + console.log(` Result: ${r.status}`); + console.log(` Risk: ${r.riskLevel ?? 'N/A'}`); + console.log(` Reason: ${r.explanation ?? 'N/A'}`); + if (r.confirmationNumber) console.log(` Conf#: ${r.confirmationNumber}`); + if (r.error) console.log(` Error: ${r.error}`); + } + console.log(); + } + + console.log('=========================================================='); + if (pendingReview > 0) { + console.log(` ${pendingReview} transaction(s) have not resolved yet.`); + console.log(" Parked for review? Run 'npm run review-starter'."); + console.log(' Compliance Worker down? Start it and they resume on their own.'); + } else { + console.log(' All 3 transactions resolved!'); + } + console.log(' Check Temporal UI: http://localhost:8233'); + console.log('=========================================================='); + + await connection.close(); +} + +run().then( + () => process.exit(0), + (err) => { + console.error(err); + process.exit(1); + }, +); diff --git a/typescript/decouple-monolith/solution/src/payments/worker.ts b/typescript/decouple-monolith/solution/src/payments/worker.ts new file mode 100644 index 0000000..f879433 --- /dev/null +++ b/typescript/decouple-monolith/solution/src/payments/worker.ts @@ -0,0 +1,36 @@ +// DECOUPLED VERSION. This Worker runs Payments code and nothing else. +// +// Compare against the monolith it replaced, which also registered the Compliance team's +// Activity — meaning a bug in compliance code at 3 AM took payments down with it. +import { NativeConnection, Worker } from '@temporalio/worker'; +import * as activities from './activities'; +import { PAYMENTS_NAMESPACE, PAYMENTS_TASK_QUEUE } from '../shared/types'; + +async function run() { + const connection = await NativeConnection.connect({ address: 'localhost:7233' }); + try { + const worker = await Worker.create({ + connection, + namespace: PAYMENTS_NAMESPACE, + taskQueue: PAYMENTS_TASK_QUEUE, + workflowsPath: require.resolve('./workflows'), + activities, + }); + + console.log('========================================================='); + console.log(` Payments Worker started on: ${PAYMENTS_TASK_QUEUE}`); + console.log(` Namespace: ${PAYMENTS_NAMESPACE}`); + console.log(' Registered: paymentProcessingWorkflow, reviewCallerWorkflow'); + console.log(' payment activities only — compliance is remote'); + console.log('========================================================='); + + await worker.run(); + } finally { + await connection.close(); + } +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/typescript/decouple-monolith/solution/src/payments/workflows.ts b/typescript/decouple-monolith/solution/src/payments/workflows.ts new file mode 100644 index 0000000..1f6eb82 --- /dev/null +++ b/typescript/decouple-monolith/solution/src/payments/workflows.ts @@ -0,0 +1,110 @@ +// DECOUPLED VERSION. Compliance is now called across a Nexus boundary. +// +// Three steps: +// Step 1: validatePayment (Activity, this Worker) +// Step 2: checkCompliance (Nexus Operation, the Compliance team's Worker) +// Step 3: executePayment (Activity, this Worker) +import * as wf from '@temporalio/workflow'; +import type * as activities from './activities'; +import { complianceService } from '../shared/nexus-service'; +import { + COMPLIANCE_ENDPOINT, + ComplianceRequest, + ComplianceResult, + PaymentRequest, + PaymentResult, + ReviewRequest, +} from '../shared/types'; + +const { validatePayment, executePayment } = wf.proxyActivities({ + startToCloseTimeout: '30 seconds', + retry: { initialInterval: '1 second', backoffCoefficient: 2 }, +}); + +export async function paymentProcessingWorkflow(request: PaymentRequest): Promise { + const logger = wf.log; + + // Step 1: validate (Payments team). + if (!(await validatePayment(request))) { + return { + success: false, + transactionId: request.transactionId, + status: 'REJECTED', + error: 'Payment validation failed', + }; + } + logger.info(`Step 1 passed: validation OK for ${request.transactionId}`); + + // Step 2: compliance check, across the team boundary. + // + // This client is what replaced the Activity stub. Calls on it do not run code in this + // process — they go out through the Endpoint to whichever Worker owns the Service. + // + // The 10 minutes is the budget for the whole call, retries included. It is what lets + // the Operation survive the Compliance Worker going away instead of failing the moment + // it does. + const compliance = wf.createNexusServiceClient({ + service: complianceService, + endpoint: COMPLIANCE_ENDPOINT, + }); + + const compReq: ComplianceRequest = { + transactionId: request.transactionId, + amount: request.amount, + senderCountry: request.senderCountry, + receiverCountry: request.receiverCountry, + description: request.description, + }; + + logger.info(`Step 2: calling compliance check for ${request.transactionId}`); + const result: ComplianceResult = await compliance.executeOperation('checkCompliance', compReq, { + scheduleToCloseTimeout: '10 minutes', + }); + + logger.info(`Compliance result: ${result.riskLevel} | approved=${result.approved}`); + + // A declined payment is a business outcome, not a failure. The Workflow completes + // successfully and reports the decision. + if (!result.approved) { + return { + success: false, + transactionId: request.transactionId, + status: 'DECLINED_COMPLIANCE', + riskLevel: result.riskLevel, + explanation: result.explanation, + }; + } + + // Step 3: execute, only if compliance approved. + logger.info(`Step 3: executing payment for ${request.transactionId}`); + const confirmationNumber = await executePayment(request); + + return { + success: true, + transactionId: request.transactionId, + status: 'COMPLETED', + riskLevel: result.riskLevel, + explanation: result.explanation, + confirmationNumber, + }; +} + +/** + * [GIVEN] Sends a compliance review decision through Nexus. + * + * Routing the review through the Endpoint respects the team boundary: neither team needs + * to know the other's Workflow IDs or internal method names. + * + * submitReview is a synchronous Operation, so the Compliance handler must finish inside + * the 10-second handler deadline. That is why the timeout here is 10 seconds, not the + * 10 minutes used for the async check. + */ +export async function reviewCallerWorkflow(request: ReviewRequest): Promise { + const compliance = wf.createNexusServiceClient({ + service: complianceService, + endpoint: COMPLIANCE_ENDPOINT, + }); + return await compliance.executeOperation('submitReview', request, { + scheduleToCloseTimeout: '10 seconds', + }); +} diff --git a/typescript/decouple-monolith/solution/src/shared/nexus-service.ts b/typescript/decouple-monolith/solution/src/shared/nexus-service.ts new file mode 100644 index 0000000..f8781ef --- /dev/null +++ b/typescript/decouple-monolith/solution/src/shared/nexus-service.ts @@ -0,0 +1,14 @@ +// The shared contract between the Payments and Compliance teams. Like an OpenAPI spec, +// but durable. Payments builds a client from it; Compliance implements a handler for it. +// +// Both teams import THIS file. It is the only thing they share. +import * as nexus from 'nexus-rpc'; +import { ComplianceRequest, ComplianceResult, ReviewRequest } from './types'; + +export const complianceService = nexus.service('ComplianceNexusService', { + /** Slow path. Backed by a Workflow, because a risky payment may need a human. */ + checkCompliance: nexus.operation(), + + /** Fast path. A human decision handed to an already-running compliance check. */ + submitReview: nexus.operation(), +}); diff --git a/typescript/decouple-monolith/solution/src/shared/types.ts b/typescript/decouple-monolith/solution/src/shared/types.ts new file mode 100644 index 0000000..019fc20 --- /dev/null +++ b/typescript/decouple-monolith/solution/src/shared/types.ts @@ -0,0 +1,60 @@ +// [GIVEN] The data that crosses team boundaries. +// +// These are plain interfaces. Unlike the Kotlin version, nothing here needs a +// no-arg constructor or parameter-name retention: the TypeScript SDK's default +// data converter is JSON, so a structural type is all the Nexus boundary needs. + +/** A payment transaction to be processed. */ +export interface PaymentRequest { + transactionId: string; + amount: number; + currency: string; + senderCountry: string; + receiverCountry: string; + description: string; + senderAccount: string; + receiverAccount: string; +} + +/** The outcome of a payment Workflow. */ +export interface PaymentResult { + success: boolean; + transactionId: string; + status: 'COMPLETED' | 'REJECTED' | 'DECLINED_COMPLIANCE' | 'FAILED'; + riskLevel?: RiskLevel; + explanation?: string; + confirmationNumber?: string; + error?: string; +} + +export type RiskLevel = 'LOW' | 'MEDIUM' | 'HIGH'; + +/** Input to the compliance check. Sent by Payments to Compliance. */ +export interface ComplianceRequest { + transactionId: string; + amount: number; + senderCountry: string; + receiverCountry: string; + description: string; +} + +/** Result of a compliance check. Returned by Compliance to Payments. */ +export interface ComplianceResult { + transactionId: string; + approved: boolean; + riskLevel: RiskLevel; + explanation: string; +} + +/** A human review decision, submitted through Nexus. */ +export interface ReviewRequest { + transactionId: string; + approved: boolean; + explanation: string; +} + +export const PAYMENTS_TASK_QUEUE = 'payments-processing'; +export const COMPLIANCE_TASK_QUEUE = 'compliance-risk'; +export const PAYMENTS_NAMESPACE = 'payments-namespace'; +export const COMPLIANCE_NAMESPACE = 'compliance-namespace'; +export const COMPLIANCE_ENDPOINT = 'compliance-endpoint'; diff --git a/typescript/decouple-monolith/solution/tsconfig.json b/typescript/decouple-monolith/solution/tsconfig.json new file mode 100644 index 0000000..f0e6b74 --- /dev/null +++ b/typescript/decouple-monolith/solution/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "version": "5.6.3", + "compilerOptions": { + "outDir": "lib", + "rootDir": "src", + "strict": true + }, + "include": ["src/**/*.ts"] +} From 6bd659b29e1855efca47577ceaaee4e84d948dd1 Mon Sep 17 00:00:00 2001 From: nadvolod <2136245+nadvolod@users.noreply.github.com> Date: Fri, 28 Aug 2026 14:40:10 -0400 Subject: [PATCH 02/22] Port the Instruqt track to TypeScript Adds typescript/instruqt, typescript/sandbox, typescript/diagrams, and the CI caller, completing the TypeScript sibling of kotlin/. Track identity is deliberately absent: track.yml has no id and no checksum, no assignment.md carries an id, and no tab carries an id. The slug is new (nexus-typescript-decouple-monolith). Copying the Kotlin ids would have made the first push overwrite the Kotlin track, which CI force-pushes on merge to main. Verified: instruqt track validate passes; all 24 tab-N button references resolve to the correct tab by label; every /root/workshop path referenced by a lifecycle script exists; all lifecycle scripts pass bash -n; the diagram server serves the diagram, 404s correctly, and rejects path traversal. Two lessons change shape in TypeScript and the assignments say so: - Challenge 2 now ends with a FAILING typecheck by design. Declaring an Operation obliges a matching handler at compile time (TS2345), where Kotlin failed at Worker startup instead. - Challenge 3's proof of registration inverts. There is no "Nexus Poller" line to look for; instead the absence of "No Nexus services registered, not polling for Nexus tasks" is the signal. Also renumbers exercise TODOs 6 and 7 so they read in challenge order, with the coupling deletion last. The sandbox image is not built yet. Build and digest-pin it per the comment in .github/workflows/build-and-push-typescript.yml before pushing the track. Co-Authored-By: Claude Opus 5 (1M context) --- .../workflows/build-and-push-typescript.yml | 55 ++ .../exercise/src/payments/worker.ts | 4 +- .../exercise/src/payments/workflows.ts | 4 +- .../diagrams/monolith-architecture.html | 494 ++++++++++++++++++ .../01-run-the-monolith/assignment.md | 215 ++++++++ .../01-run-the-monolith/check-workshop | 15 + .../01-run-the-monolith/cleanup-workshop | 7 + .../01-run-the-monolith/setup-workshop | 26 + .../01-run-the-monolith/solve-workshop | 18 + .../02-the-shared-contract/assignment.md | 117 +++++ .../02-the-shared-contract/check-workshop | 15 + .../02-the-shared-contract/cleanup-workshop | 7 + .../02-the-shared-contract/setup-workshop | 8 + .../02-the-shared-contract/solve-workshop | 12 + .../03-the-compliance-side/assignment.md | 174 ++++++ .../03-the-compliance-side/check-workshop | 15 + .../03-the-compliance-side/cleanup-workshop | 10 + .../03-the-compliance-side/setup-workshop | 7 + .../03-the-compliance-side/solve-workshop | 39 ++ .../04-the-payments-side/assignment.md | 202 +++++++ .../04-the-payments-side/check-workshop | 55 ++ .../04-the-payments-side/cleanup-workshop | 9 + .../04-the-payments-side/setup-workshop | 7 + .../04-the-payments-side/solve-workshop | 31 ++ .../assignment.md | 172 ++++++ .../check-workshop | 15 + .../cleanup-workshop | 9 + .../setup-workshop | 18 + .../solve-workshop | 61 +++ typescript/instruqt/config.yml | 18 + typescript/instruqt/track.yml | 53 ++ .../instruqt/track_scripts/cleanup-workshop | 16 + .../instruqt/track_scripts/setup-workshop | 155 ++++++ typescript/sandbox/Dockerfile | 83 +++ typescript/sandbox/serve-diagrams.mjs | 35 ++ 35 files changed, 2177 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/build-and-push-typescript.yml create mode 100644 typescript/diagrams/monolith-architecture.html create mode 100644 typescript/instruqt/01-run-the-monolith/assignment.md create mode 100755 typescript/instruqt/01-run-the-monolith/check-workshop create mode 100755 typescript/instruqt/01-run-the-monolith/cleanup-workshop create mode 100755 typescript/instruqt/01-run-the-monolith/setup-workshop create mode 100755 typescript/instruqt/01-run-the-monolith/solve-workshop create mode 100644 typescript/instruqt/02-the-shared-contract/assignment.md create mode 100755 typescript/instruqt/02-the-shared-contract/check-workshop create mode 100755 typescript/instruqt/02-the-shared-contract/cleanup-workshop create mode 100755 typescript/instruqt/02-the-shared-contract/setup-workshop create mode 100755 typescript/instruqt/02-the-shared-contract/solve-workshop create mode 100644 typescript/instruqt/03-the-compliance-side/assignment.md create mode 100755 typescript/instruqt/03-the-compliance-side/check-workshop create mode 100755 typescript/instruqt/03-the-compliance-side/cleanup-workshop create mode 100755 typescript/instruqt/03-the-compliance-side/setup-workshop create mode 100755 typescript/instruqt/03-the-compliance-side/solve-workshop create mode 100644 typescript/instruqt/04-the-payments-side/assignment.md create mode 100755 typescript/instruqt/04-the-payments-side/check-workshop create mode 100755 typescript/instruqt/04-the-payments-side/cleanup-workshop create mode 100755 typescript/instruqt/04-the-payments-side/setup-workshop create mode 100755 typescript/instruqt/04-the-payments-side/solve-workshop create mode 100644 typescript/instruqt/05-durability-and-human-review/assignment.md create mode 100755 typescript/instruqt/05-durability-and-human-review/check-workshop create mode 100755 typescript/instruqt/05-durability-and-human-review/cleanup-workshop create mode 100755 typescript/instruqt/05-durability-and-human-review/setup-workshop create mode 100755 typescript/instruqt/05-durability-and-human-review/solve-workshop create mode 100644 typescript/instruqt/config.yml create mode 100644 typescript/instruqt/track.yml create mode 100755 typescript/instruqt/track_scripts/cleanup-workshop create mode 100755 typescript/instruqt/track_scripts/setup-workshop create mode 100644 typescript/sandbox/Dockerfile create mode 100644 typescript/sandbox/serve-diagrams.mjs diff --git a/.github/workflows/build-and-push-typescript.yml b/.github/workflows/build-and-push-typescript.yml new file mode 100644 index 0000000..5d0db44 --- /dev/null +++ b/.github/workflows/build-and-push-typescript.yml @@ -0,0 +1,55 @@ +# Pushes the TypeScript Instruqt track definition on merge to main. +# +# CI deliberately does NOT build the sandbox image, for the same reason as the +# Kotlin caller: the image lives at ghcr.io/nadvolod/edu-nexus-typescript-sandbox, +# a personal namespace this repo's default GITHUB_TOKEN cannot write to. A build +# job would fail on every merge. Rather than leave CI permanently red, the image +# is built by hand and pinned by digest in typescript/instruqt/config.yml: +# +# cd typescript +# docker buildx build --platform linux/amd64 -f sandbox/Dockerfile \ +# -t ghcr.io/nadvolod/edu-nexus-typescript-sandbox:latest --push . +# docker buildx imagetools inspect ghcr.io/nadvolod/edu-nexus-typescript-sandbox:latest \ +# --format "{{.Manifest.Digest}}" +# # paste that digest into typescript/instruqt/config.yml, then commit +# +# Because the digest is pinned, a source change only reaches attendees once that +# re-pin is committed, and the re-pin lives under typescript/instruqt/. That is +# why this workflow watches instruqt/ alone: it is the only path that can change +# what the track actually serves. +# +# To let CI own the image later, move it to ghcr.io/temporalio/... (or add a PAT +# with write:packages) and restore build_image below. +# +# Required once: a repo secret named INSTRUQT_TOKEN (Instruqt org API key). +name: Publish TypeScript Nexus track + +on: + push: + branches: [main] + paths: + - "typescript/instruqt/**" + - ".github/workflows/_track-ci.yml" + - ".github/workflows/build-and-push-typescript.yml" + workflow_dispatch: + +jobs: + ci: + # packages: write looks wrong next to build_image: false, but it is required. + # A caller's permissions block is the CEILING for every job in the called + # workflow, and GitHub validates that ceiling when it loads the workflow, + # before any `if:` is evaluated. _track-ci.yml's build-image job asks for + # packages: write, so a read-only ceiling fails the whole run at startup with + # zero jobs, even though build-image never runs. Do not narrow this back. + permissions: + contents: read + packages: write + uses: ./.github/workflows/_track-ci.yml + with: + track_dir: "typescript" + # Referenced for completeness. Unused while build_image is false. + image: ghcr.io/nadvolod/edu-nexus-typescript-sandbox + # Never build here: see the note at the top of this file. + build_image: false + push_track: true + secrets: inherit diff --git a/typescript/decouple-monolith/exercise/src/payments/worker.ts b/typescript/decouple-monolith/exercise/src/payments/worker.ts index ba8f2db..6a3b1cb 100644 --- a/typescript/decouple-monolith/exercise/src/payments/worker.ts +++ b/typescript/decouple-monolith/exercise/src/payments/worker.ts @@ -7,7 +7,7 @@ // // That last line is the problem. A bug in compliance code takes payments down with it. // -// Do TODO 6 last, after TODO 5 has moved the Workflow onto a Nexus client. +// Do TODO 7 last, after TODO 5 has moved the Workflow onto a Nexus client. import { NativeConnection, Worker } from '@temporalio/worker'; import * as paymentActivities from './activities'; import * as complianceActivities from '../compliance/activities'; @@ -22,7 +22,7 @@ async function run() { taskQueue: PAYMENTS_TASK_QUEUE, workflowsPath: require.resolve('./workflows'), - // ── TODO 6 ──────────────────────────────────────────────────────────────── + // ── TODO 7 ──────────────────────────────────────────────────────────────── // Once the Workflow calls Compliance over Nexus, this Worker has no reason to run // the Compliance team's code. Remove `...complianceActivities` from the object // below, and delete the import at the top of this file. diff --git a/typescript/decouple-monolith/exercise/src/payments/workflows.ts b/typescript/decouple-monolith/exercise/src/payments/workflows.ts index 9635ba2..7ee4797 100644 --- a/typescript/decouple-monolith/exercise/src/payments/workflows.ts +++ b/typescript/decouple-monolith/exercise/src/payments/workflows.ts @@ -112,7 +112,7 @@ export async function paymentProcessingWorkflow(request: PaymentRequest): Promis }; } -// ── TODO 7 ────────────────────────────────────────────────────────────────────────── +// ── TODO 6 ────────────────────────────────────────────────────────────────────────── // You come back to this one in challenge 05, once the Nexus path works end to end. // // Submitting a human review through the Endpoint respects the team boundary: neither @@ -122,5 +122,5 @@ export async function paymentProcessingWorkflow(request: PaymentRequest): Promis // the 10-second handler deadline. Use a 10-second scheduleToCloseTimeout here, not the // 10 minutes used for the async check. export async function reviewCallerWorkflow(_request: ReviewRequest): Promise { - throw new Error('TODO 7: submit the review decision through the Nexus Endpoint'); + throw new Error('TODO 6: submit the review decision through the Nexus Endpoint'); } diff --git a/typescript/diagrams/monolith-architecture.html b/typescript/diagrams/monolith-architecture.html new file mode 100644 index 0000000..b8312bd --- /dev/null +++ b/typescript/diagrams/monolith-architecture.html @@ -0,0 +1,494 @@ + + + + + +Challenge 1 · The Payments Monolith + + + +
+
+

Challenge 1 · Before Nexus

+

The Payments Monolith

+

Two teams. One process. Step through the flow with the arrows, or click any block for its file, + signature and role.

+
+ +
+ + + step — / — + +
+ + Three payments, two teams, one Worker. +
+
+ +
+ Zoom + + 100% + + + + +
+ +
+
+
+
+
+
+ +
+
+ + + + diff --git a/typescript/instruqt/01-run-the-monolith/assignment.md b/typescript/instruqt/01-run-the-monolith/assignment.md new file mode 100644 index 0000000..19616e7 --- /dev/null +++ b/typescript/instruqt/01-run-the-monolith/assignment.md @@ -0,0 +1,215 @@ +--- +slug: run-the-monolith +type: challenge +title: 1. Run the Monolith +teaser: Three payments, two teams, one Worker. Find the coupling before you fix it. +notes: +- type: text + contents: |- + # What breaks when two teams share one Worker? + + A payment cannot execute until Compliance clears it. Both teams' code runs in + the same process, on the same Task Queue, in the same Namespace. + + It works. That is the problem. Nobody notices the coupling until the night + a compliance bug takes payments down with it. +- type: text + contents: |- + # Ziggy is warming up your sandbox + + A Temporal dev server is starting, and two Namespaces are being created: + one for Payments, one for Compliance. + + Right now only one of them has anything in it. +tabs: +- title: Exercise + type: service + hostname: workshop + path: /?folder=/root/workshop/exercise/src + port: 8080 +- title: Temporal UI + type: service + hostname: workshop + path: / + port: 8233 +- title: Terminal + type: terminal + hostname: workshop + workdir: /root/workshop/exercise +- title: Payments Worker + type: terminal + hostname: workshop + workdir: /root/workshop/exercise +- title: Compliance Worker + type: terminal + hostname: workshop + workdir: /root/workshop/exercise +- title: Solution + type: service + hostname: workshop + path: /?folder=/root/workshop/solution/src + port: 8080 +- title: Monolith Architecture + type: service + hostname: workshop + path: /monolith-architecture.html + port: 8090 +difficulty: basic +timelimit: 900 +enhanced_loading: null +--- + +# The Setup + +You work at a bank. Every payment takes three steps. + +1. Validate the payment. Payments team. +2. Check compliance. Compliance team. +3. Execute the payment. Payments team. + +Step 2 is not optional. Nothing moves until Compliance says yes. + +The blue buttons below are clickable. Click any one to jump to that tab. + +# Start the Worker + +Click the [button label="Payments Worker" background="#444CE7"](tab-3) tab and start it: + +```bash,run +npm run payments-worker +``` + +The Worker bundles your Workflow code with webpack on startup, so the first run takes a +second longer than you might expect. Read the banner it prints: + +```bash,nocopy + Registered: paymentProcessingWorkflow, payment activities + compliance activities (monolith, will decouple) +``` + +One Worker. Both teams' code. That last line is the coupling, written down. + +# Run Three Payments + +Click the [button label="Terminal" background="#444CE7"](tab-2) tab: + +```bash,run +npm run starter +``` + +Three transactions go through. Watch what comes back: + +```bash,nocopy + TXN-A Result: COMPLETED Risk: LOW + TXN-B Result: COMPLETED Risk: MEDIUM + TXN-C Result: DECLINED_COMPLIANCE Risk: HIGH +``` + +# See It in the Temporal UI + +The terminal gave you three lines. The server has the whole story. + +Click the [button label="Temporal UI" background="#444CE7"](tab-1) tab. Check that the +Namespace selector at the top reads `payments-namespace`. + +Three Workflows, every one of them **Completed**. Click refresh if you do not see them yet. + +| Workflow ID | Amount | What happened | +|---|---|---| +| `payment-TXN-A` | $250 | paid | +| `payment-TXN-B` | $12,000 | paid | +| `payment-TXN-C` | $75,000 | declined by Compliance | + +Open `payment-TXN-C`, find the **Input and Results** panel, and open **Results**: + +```json,nocopy +{ + "explanation": "Transaction amount exceeds $50,000 threshold. Requires enhanced due diligence review.", + "riskLevel": "HIGH", + "status": "DECLINED_COMPLIANCE", + "success": false, + "transactionId": "TXN-C" +} +``` + +Read that closely. `success` is `false`, there is no `error` field at all, and the Workflow +status is **Completed** rather than Failed. + +Compliance looked at a $75,000 payment and said no. That is the system working. A declined +payment is an answer, so the Workflow returned it and finished normally. Keep the two ideas +apart: a declined payment is a business outcome, a failed Workflow is a defect. You will +watch this same result travel across a team boundary in challenge 4, and it should still +look exactly like this. + +# Review the Monolith Architecture + +Before you hunt for the coupling in code, look at the shape of what you just ran. + +Click the [button label="Monolith Architecture" background="#444CE7"](tab-6) tab. + +Two shaded zones, one per team. Payments is blue, Compliance is amber. Every box carries +its file name and the function that matters. + +Click **Walk the flow**, then move with the **left** and **right** arrows on screen or on +your keyboard. Eight steps. + +Two of them are the point of this challenge: + +- **Step 5 of 8** is the Workflow calling the Compliance team's Activity. Watch the arrow + cross from the blue zone into the amber one while staying inside a single process. +- **Step 8 of 8** is the consequence. Compliance owns a Namespace with nothing in it. + +Then read the dashed outline that wraps both zones. It is labeled +`payments/worker.ts · one Node process · one Task Queue payments-processing`. That is the +coupling stated as plainly as it can be: both teams' code, one process, one queue. Every +box inside that outline is polled by the same Worker. + +**Fit** brings everything back on screen. **Reset view** undoes any zooming. + +# Find the Coupling + +Click the [button label="Exercise" background="#444CE7"](tab-0) tab and open +`payments/worker.ts`. + +Look at what this one Worker registers. One object, two different teams: + +```ts,nocopy +activities: { ...paymentActivities, ...complianceActivities }, +``` + +`paymentActivities` belongs to Payments. `complianceActivities` belongs to Compliance. +They sit on one line in one file, which means the same process, the same deployment, and +the same blast radius. Compliance cannot ship a fix without Payments shipping too. + +That second spread is the one you delete in challenge 4. That deletion is the decoupling. + +Now open `payments/workflows.ts` and find step 2: + +```ts,nocopy +const result: ComplianceResult = await checkCompliance(compReq); +``` + +An in-process Activity call across a team boundary that should not be in-process. + +# See the Empty Namespace + +The diagram claims Compliance owns a Namespace with nothing in it. Confirm it. + +Click the [button label="Temporal UI" background="#444CE7"](tab-1) tab and switch the +Namespace selector to `compliance-namespace`. + +Empty. Compliance has a Namespace of its own, and nothing runs there, because their code +is executing inside the Payments Worker. + +Switch back to `payments-namespace`, open `payment-TXN-C` again, and read the Event +History. Find **Activity Task Scheduled** for the compliance check. That single event is +what you spend the rest of this lab replacing. + +Click **Check** when your three transactions have finished. + +# What You Know Now + +- One Worker runs both teams. One bug takes down both. +- The compliance check is an ordinary Activity call. +- A declined payment completes. It does not fail. +- Compliance has a Namespace, and it is empty. diff --git a/typescript/instruqt/01-run-the-monolith/check-workshop b/typescript/instruqt/01-run-the-monolith/check-workshop new file mode 100755 index 0000000..dbefe2d --- /dev/null +++ b/typescript/instruqt/01-run-the-monolith/check-workshop @@ -0,0 +1,15 @@ +#!/bin/bash +# ABOUTME: Instant pass. This is a live instructor-led workshop, so the +# instructor is the feedback loop and progression is never gated on a script. +# +# Two reasons this is deliberate rather than lazy: +# 1. Skip runs solve-workshop, which copies solution files over the learner's +# work. Gating progression pushes stuck learners toward destroying their +# own code to move on. +# 2. Check-click latency costs real minutes across a room of 20 in a 40 minute +# lab block. +# +# Challenge 04 is the exception: a mistyped Nexus Endpoint hangs silently, so +# that one keeps a diagnostic. Everything the removed checks used to explain now +# lives in the assignment bodies. +exit 0 diff --git a/typescript/instruqt/01-run-the-monolith/cleanup-workshop b/typescript/instruqt/01-run-the-monolith/cleanup-workshop new file mode 100755 index 0000000..1275b4e --- /dev/null +++ b/typescript/instruqt/01-run-the-monolith/cleanup-workshop @@ -0,0 +1,7 @@ +#!/bin/bash +# ABOUTME: Challenge 01 cleanup. One pkill per pattern. +set -euo pipefail + +pkill -f "payments/worker.ts" || true +pkill -f "npm run payments-worker" || true +sleep 1 diff --git a/typescript/instruqt/01-run-the-monolith/setup-workshop b/typescript/instruqt/01-run-the-monolith/setup-workshop new file mode 100755 index 0000000..eb9bbc7 --- /dev/null +++ b/typescript/instruqt/01-run-the-monolith/setup-workshop @@ -0,0 +1,26 @@ +#!/bin/bash +# ABOUTME: Challenge 01 setup. Clears stragglers so the monolith run is clean. +# The working tree is staged once at the track level and accumulates edits +# across challenges, so nothing is wiped here. +set -euxo pipefail + +pkill -f "payments/worker.ts" || true +pkill -f "compliance/worker.ts" || true + +# Restore the exercise tree if a solve script has already replaced it with the +# solution. The solve scripts for later challenges do `cp -R solution/src +# exercise/src`, and nothing else ever puts the TODOs back. Without this, using +# Skip once leaves the sandbox permanently solved: challenge 01 would print the +# decoupled Worker banner instead of the monolith one, and every TODO would be +# gone before the attendee reached it. +# +# Only restores a tree that has NO TODO markers left, which means it was +# overwritten wholesale. A pristine or partly-completed tree is left alone, so an +# attendee who wanders back to challenge 01 does not lose their work. +CONTRACT=/root/workshop/exercise/src/shared/nexus-service.ts +if [ -f "$CONTRACT" ] && ! grep -q "TODO" "$CONTRACT"; then + echo "==> Exercise tree was already solved. Restoring the TODOs." + rm -rf /root/workshop/exercise/src + cp -R /opt/workshop/exercise/src /root/workshop/exercise/src +fi +true diff --git a/typescript/instruqt/01-run-the-monolith/solve-workshop b/typescript/instruqt/01-run-the-monolith/solve-workshop new file mode 100755 index 0000000..b5106bc --- /dev/null +++ b/typescript/instruqt/01-run-the-monolith/solve-workshop @@ -0,0 +1,18 @@ +#!/bin/bash +# ABOUTME: Challenge 01 solve. Runs the monolith end to end so the next +# challenge inherits three completed payment Workflows. +set -euxo pipefail + +cd /root/workshop/exercise + +pkill -f "payments/worker.ts" || true +sleep 1 + +nohup npm run payments-worker > /tmp/payments-worker.log 2>&1 & +for i in $(seq 1 120); do + grep -q "Payments Worker started" /tmp/payments-worker.log && break + sleep 1 +done + +npm run starter > /tmp/starter.log 2>&1 || true +cat /tmp/starter.log diff --git a/typescript/instruqt/02-the-shared-contract/assignment.md b/typescript/instruqt/02-the-shared-contract/assignment.md new file mode 100644 index 0000000..989a769 --- /dev/null +++ b/typescript/instruqt/02-the-shared-contract/assignment.md @@ -0,0 +1,117 @@ +--- +slug: the-shared-contract +type: challenge +title: 2. The Shared Contract +teaser: Write the interface both teams depend on. One file, two Operations. +notes: +- type: text + contents: |- + # How do two teams agree on a call neither one owns? + + Payments needs a compliance decision. Compliance owns the logic and refuses + to hand over its database. + + An HTTP client would work until the network blips. Then you are writing + retries, timeouts, and callback infrastructure by hand. +- type: text + contents: |- + # The four Nexus pieces + + Service is the contract. Operation is a method on it. Endpoint is the routing + rule. Registry is where Endpoints live. + + You write the Service now. The other three come next. +tabs: +- title: Exercise + type: service + hostname: workshop + path: /?folder=/root/workshop/exercise/src + port: 8080 +- title: Temporal UI + type: service + hostname: workshop + path: / + port: 8233 +- title: Terminal + type: terminal + hostname: workshop + workdir: /root/workshop/exercise +- title: Payments Worker + type: terminal + hostname: workshop + workdir: /root/workshop/exercise +- title: Compliance Worker + type: terminal + hostname: workshop + workdir: /root/workshop/exercise +- title: Solution + type: service + hostname: workshop + path: /?folder=/root/workshop/solution/src + port: 8080 +- title: Monolith Architecture + type: service + hostname: workshop + path: /monolith-architecture.html + port: 8090 +difficulty: basic +timelimit: 900 +enhanced_loading: null +--- + +# One Contract, Two Teams + +A Nexus Service is a contract both teams compile against. Payments builds a client from +it. Compliance implements a handler for it. Neither team sees the other's code. + +It lives in `shared/` on purpose. Neither team owns it alone. + +# Write It + +Click the [button label="Exercise" background="#444CE7"](tab-0) tab, open +`shared/nexus-service.ts`, and follow TODO 1. + +Two Operations. Each is a name paired with `nexus.operation()`, which +carries no implementation — only the types both teams agree on. + +The editor saves as you type. There is no save button. + +# Now Break the Build on Purpose + +Click the [button label="Terminal" background="#444CE7"](tab-2) tab: + +```bash,run +npx tsc --noEmit +``` + +**This is supposed to fail.** Read what it says: + +```bash,nocopy +src/compliance/nexus-handler.ts(25,81): error TS2345: Argument of type '{}' is not +assignable to parameter of type 'ServiceHandlerFor<{ checkCompliance: ...; +submitReview: ...; }>'. + Type '{}' is missing the following properties from type + 'ServiceHandlerFor<...>': checkCompliance, submitReview +``` + +You declared two Operations, and TypeScript immediately went looking for the two handlers +that answer them. It cannot find either, so it refuses to compile. + +That is the contract doing its job, and it is worth sitting with for a second. You have +not run anything. No Worker has started. No call has been made. The compiler already knows +Compliance owes Payments two implementations, purely from the shape of the Service. + +If you have taken the Java or Kotlin version of this workshop, this is the moment that +differs most. There the Worker starts fine and fails at runtime with +`Missing handlers for service operations`. In TypeScript that class of mistake cannot +reach a running process. + +You fix this in challenge 3, by writing the handlers. Leave it red. + +Click **Check** when the two Operations are declared. + +# What You Know Now + +- `nexus.service()` names the contract; `nexus.operation()` declares each call. +- The contract is shared. The implementation is not. +- Declaring an Operation obliges someone to handle it, and the compiler enforces that. diff --git a/typescript/instruqt/02-the-shared-contract/check-workshop b/typescript/instruqt/02-the-shared-contract/check-workshop new file mode 100755 index 0000000..dbefe2d --- /dev/null +++ b/typescript/instruqt/02-the-shared-contract/check-workshop @@ -0,0 +1,15 @@ +#!/bin/bash +# ABOUTME: Instant pass. This is a live instructor-led workshop, so the +# instructor is the feedback loop and progression is never gated on a script. +# +# Two reasons this is deliberate rather than lazy: +# 1. Skip runs solve-workshop, which copies solution files over the learner's +# work. Gating progression pushes stuck learners toward destroying their +# own code to move on. +# 2. Check-click latency costs real minutes across a room of 20 in a 40 minute +# lab block. +# +# Challenge 04 is the exception: a mistyped Nexus Endpoint hangs silently, so +# that one keeps a diagnostic. Everything the removed checks used to explain now +# lives in the assignment bodies. +exit 0 diff --git a/typescript/instruqt/02-the-shared-contract/cleanup-workshop b/typescript/instruqt/02-the-shared-contract/cleanup-workshop new file mode 100755 index 0000000..1dda6d9 --- /dev/null +++ b/typescript/instruqt/02-the-shared-contract/cleanup-workshop @@ -0,0 +1,7 @@ +#!/bin/bash +# ABOUTME: Challenge 02 cleanup. One pkill per pattern. +set -euo pipefail + +pkill -f "payments/worker.ts" || true +pkill -f "npm run payments-worker" || true +sleep 1 diff --git a/typescript/instruqt/02-the-shared-contract/setup-workshop b/typescript/instruqt/02-the-shared-contract/setup-workshop new file mode 100755 index 0000000..22d51ca --- /dev/null +++ b/typescript/instruqt/02-the-shared-contract/setup-workshop @@ -0,0 +1,8 @@ +#!/bin/bash +# ABOUTME: Challenge 02 setup. Clears the monolith Worker from challenge 01. +# The working tree accumulates edits across challenges, so nothing is wiped. +set -euxo pipefail + +pkill -f "payments/worker.ts" || true +pkill -f "compliance/worker.ts" || true +true diff --git a/typescript/instruqt/02-the-shared-contract/solve-workshop b/typescript/instruqt/02-the-shared-contract/solve-workshop new file mode 100755 index 0000000..d5eff43 --- /dev/null +++ b/typescript/instruqt/02-the-shared-contract/solve-workshop @@ -0,0 +1,12 @@ +#!/bin/bash +# ABOUTME: Challenge 02 solve. Copies only this challenge's file from the +# solution so a skipped challenge does not hand the learner the rest of the lab. +# +# No typecheck here, unlike the Kotlin track. Declaring the two Operations makes +# the contract complete, and TypeScript then requires matching handlers that do +# not exist until challenge 03 — so `tsc` FAILS at the end of this challenge by +# design. See the assignment. +set -euxo pipefail + +cp /root/workshop/solution/src/shared/nexus-service.ts \ + /root/workshop/exercise/src/shared/nexus-service.ts diff --git a/typescript/instruqt/03-the-compliance-side/assignment.md b/typescript/instruqt/03-the-compliance-side/assignment.md new file mode 100644 index 0000000..81462cc --- /dev/null +++ b/typescript/instruqt/03-the-compliance-side/assignment.md @@ -0,0 +1,174 @@ +--- +slug: the-compliance-side +type: challenge +title: 3. The Compliance Side +teaser: Implement both handlers, register them on a Worker, and create the Endpoint. +notes: +- type: text + contents: |- + # A compliance check can wait on a human. How do you put that behind an RPC? + + Some checks clear in milliseconds. A $12,000 international transfer waits for + an officer to click approve, and that officer is at lunch. + + A synchronous handler has ten seconds. Lunch takes longer. +- type: text + contents: |- + # This is the hardest challenge in the lab + + Two handlers, two different shapes, plus the Worker registration and the + Endpoint. + + The Solution tab is one click away. Use it if you stall. Learning the shape + beats staring at a blank function. +tabs: +- title: Exercise + type: service + hostname: workshop + path: /?folder=/root/workshop/exercise/src + port: 8080 +- title: Temporal UI + type: service + hostname: workshop + path: / + port: 8233 +- title: Terminal + type: terminal + hostname: workshop + workdir: /root/workshop/exercise +- title: Payments Worker + type: terminal + hostname: workshop + workdir: /root/workshop/exercise +- title: Compliance Worker + type: terminal + hostname: workshop + workdir: /root/workshop/exercise +- title: Solution + type: service + hostname: workshop + path: /?folder=/root/workshop/solution/src + port: 8080 +- title: Monolith Architecture + type: service + hostname: workshop + path: /monolith-architecture.html + port: 8090 +difficulty: intermediate +timelimit: 1800 +enhanced_loading: null +--- + +# Everything Compliance Owns + +Three pieces, all on this team's side of the boundary. + +1. The handlers that answer Nexus calls. +2. The Worker that runs them. +3. The Endpoint that tells Temporal where to route. + +# Write the Handlers + +Click the [button label="Exercise" background="#444CE7"](tab-0) tab, open +`compliance/nexus-handler.ts`, and follow TODO 2 and TODO 3. + +Two Operations, and each one needs a different shape. Read both TODOs before you write +either one, because the difference between them is the whole point of this challenge. + +| Operation | Shape | Why | +|---|---|---| +| `checkCompliance` | `new temporalNexus.WorkflowRunOperationHandler(...)` | Slow. Starts a Workflow and returns a reference to it. | +| `submitReview` | a plain `async (ctx, input) => {...}` | Fast. Answers inside the call, in milliseconds. | + +Get `checkCompliance` wrong by writing it as a plain async function and two things break: +it is cut off at the ten second handler deadline, and a retry starts a **second** +compliance check for the same payment. + +As you fill each one in, the `TS2345` error from challenge 2 shrinks. When both are +written it disappears. + +# Register the Handler + +Open `compliance/worker.ts` and follow TODO 4. + +The Worker already knows its Workflows and Activities. One option is missing: the one +that makes this team callable by Payments at all. + +# Typecheck + +Click the [button label="Terminal" background="#444CE7"](tab-2) tab: + +```bash,run +npx tsc --noEmit +``` + +Silence means it passed. The Payments side still calls the Activity proxy, which is +perfectly valid — you change that in challenge 4. + +# Create the Endpoint + +The Endpoint is the routing rule: a name, a target Namespace, and a target Task Queue. +It is the one piece of Nexus that lives outside your code. + +```bash,run +temporal operator nexus endpoint create --name compliance-endpoint --target-namespace compliance-namespace --target-task-queue compliance-risk +``` + +`--target-task-queue` must match `COMPLIANCE_TASK_QUEUE` in `shared/types.ts` exactly. +If it points at a queue no Worker is polling, the calls are never answered. + +# Start the Worker + +Click the [button label="Compliance Worker" background="#444CE7"](tab-4) tab: + +```bash,run +npm run compliance-worker +``` + +```bash,nocopy +========================================================= + Compliance Worker started on: compliance-risk + Namespace: compliance-namespace +========================================================= +[INFO] Worker state changed { state: 'RUNNING' } +``` + +The banner only tells you the process is alive. It prints whether or not you registered +anything, so it is not proof. Two things are. + +**One: a line that should NOT be there.** Scroll the Worker output and look for this: + +```bash,nocopy +[INFO] No Nexus services registered, not polling for Nexus tasks +``` + +If you see it, your handler is not registered even though the Worker started happily. +When `nexusServices` is set, that line does not appear at all. Absence is the signal here, +which is exactly the kind of thing that is easy to miss — so look for it deliberately. + +**Two: the Temporal UI.** Click the +[button label="Temporal UI" background="#444CE7"](tab-1) tab, switch the Namespace +selector to `compliance-namespace`, and open **Workers** in the left menu. Your Worker +is listed as **Running**. + +That is Compliance appearing in its own Namespace for the first time. In challenge 1 +this Namespace was empty. + +If something went wrong: + +| What you see | What it means | +|---|---| +| `TS2345 ... missing ... checkCompliance` | A handler is missing from the object. | +| `No Nexus services registered` | You skipped `nexusServices` on `Worker.create`. | +| Worker starts, calls never answered | The Task Queue does not match the Endpoint's target. | + +Click **Check** once it is up. Challenge 4 starts both Workers fresh, so you can +stop this one with **Ctrl+C** afterwards. + +# What You Know Now + +- Slow work starts a Workflow and returns. Fast work answers in the call. +- `WorkflowRunOperationHandler` makes a retry re-attach instead of starting a duplicate. +- A Worker starts fine with nothing registered. A clean banner proves nothing. +- The Endpoint maps a name to a Namespace and a Task Queue. +- Compliance now runs in its own Namespace, with its own Worker. diff --git a/typescript/instruqt/03-the-compliance-side/check-workshop b/typescript/instruqt/03-the-compliance-side/check-workshop new file mode 100755 index 0000000..dbefe2d --- /dev/null +++ b/typescript/instruqt/03-the-compliance-side/check-workshop @@ -0,0 +1,15 @@ +#!/bin/bash +# ABOUTME: Instant pass. This is a live instructor-led workshop, so the +# instructor is the feedback loop and progression is never gated on a script. +# +# Two reasons this is deliberate rather than lazy: +# 1. Skip runs solve-workshop, which copies solution files over the learner's +# work. Gating progression pushes stuck learners toward destroying their +# own code to move on. +# 2. Check-click latency costs real minutes across a room of 20 in a 40 minute +# lab block. +# +# Challenge 04 is the exception: a mistyped Nexus Endpoint hangs silently, so +# that one keeps a diagnostic. Everything the removed checks used to explain now +# lives in the assignment bodies. +exit 0 diff --git a/typescript/instruqt/03-the-compliance-side/cleanup-workshop b/typescript/instruqt/03-the-compliance-side/cleanup-workshop new file mode 100755 index 0000000..e745fd3 --- /dev/null +++ b/typescript/instruqt/03-the-compliance-side/cleanup-workshop @@ -0,0 +1,10 @@ +#!/bin/bash +# ABOUTME: Challenge 03 cleanup. Clean slate. Challenge 04 starts both Workers +# itself rather than depending on a process surviving the transition. +set -euo pipefail + +pkill -f "compliance/worker.ts" || true +pkill -f "payments/worker.ts" || true +pkill -f "npm run compliance-worker" || true +pkill -f "npm run payments-worker" || true +sleep 1 diff --git a/typescript/instruqt/03-the-compliance-side/setup-workshop b/typescript/instruqt/03-the-compliance-side/setup-workshop new file mode 100755 index 0000000..647024c --- /dev/null +++ b/typescript/instruqt/03-the-compliance-side/setup-workshop @@ -0,0 +1,7 @@ +#!/bin/bash +# ABOUTME: Challenge 03 setup. Clears Workers from earlier challenges. +set -euxo pipefail + +pkill -f "payments/worker.ts" || true +pkill -f "compliance/worker.ts" || true +true diff --git a/typescript/instruqt/03-the-compliance-side/solve-workshop b/typescript/instruqt/03-the-compliance-side/solve-workshop new file mode 100755 index 0000000..f8c9517 --- /dev/null +++ b/typescript/instruqt/03-the-compliance-side/solve-workshop @@ -0,0 +1,39 @@ +#!/bin/bash +# ABOUTME: Challenge 03 solve. Copies the Compliance side from the solution and +# creates the Endpoint, because the learner creates it by hand in this challenge. +# Skipping without it would leave challenge 04 unroutable. +set -euxo pipefail + +SRC=/root/workshop/solution/src +DST=/root/workshop/exercise/src + +# The contract too, in case challenge 02 was skipped without solving. +cp "$SRC/shared/nexus-service.ts" "$DST/shared/" +cp "$SRC/compliance/nexus-handler.ts" "$DST/compliance/" +cp "$SRC/compliance/worker.ts" "$DST/compliance/" + +temporal operator nexus endpoint create \ + --name compliance-endpoint \ + --target-namespace compliance-namespace \ + --target-task-queue compliance-risk \ + --address 127.0.0.1:7233 || true + +cd /root/workshop/exercise + +# The Compliance side now typechecks on its own. The Payments side still calls +# the Activity proxy, which is valid, so the whole tree compiles here. +npx tsc --noEmit + +# Prove the wiring boots, then stop it. Challenge 04 starts its own Workers. +pkill -f "compliance/worker.ts" || true +sleep 1 +nohup npm run compliance-worker > /tmp/compliance-worker.log 2>&1 & +for i in $(seq 1 120); do + grep -q "Compliance Worker started" /tmp/compliance-worker.log && break + sleep 1 +done +# The banner alone proves nothing. A Worker with no Nexus Service registered +# logs that it is skipping Nexus polling, so assert that line is ABSENT. +grep -q "Compliance Worker started" /tmp/compliance-worker.log +! grep -q "No Nexus services registered" /tmp/compliance-worker.log +pkill -f "compliance/worker.ts" || true diff --git a/typescript/instruqt/04-the-payments-side/assignment.md b/typescript/instruqt/04-the-payments-side/assignment.md new file mode 100644 index 0000000..ceafff1 --- /dev/null +++ b/typescript/instruqt/04-the-payments-side/assignment.md @@ -0,0 +1,202 @@ +--- +slug: the-payments-side +type: challenge +title: 4. The Payments Side +teaser: Swap the Activity proxy for a Nexus client, point it at the Endpoint, and delete + the coupling. +notes: +- type: text + contents: |- + # How much code changes when you cross a team boundary? + + The compliance check is about to leave this process entirely. Different + Namespace, different Task Queue, different deployment, different team. + + Guess how many lines change at the call site. +- type: text + contents: |- + # One deletion is the whole point + + Somewhere in the Payments Worker, one spread registers Compliance code inside + the Payments process. + + Deleting it is the decoupling. Everything else is wiring. +tabs: +- title: Exercise + type: service + hostname: workshop + path: /?folder=/root/workshop/exercise/src + port: 8080 +- title: Temporal UI + type: service + hostname: workshop + path: / + port: 8233 +- title: Terminal + type: terminal + hostname: workshop + workdir: /root/workshop/exercise +- title: Payments Worker + type: terminal + hostname: workshop + workdir: /root/workshop/exercise +- title: Compliance Worker + type: terminal + hostname: workshop + workdir: /root/workshop/exercise +- title: Solution + type: service + hostname: workshop + path: /?folder=/root/workshop/solution/src + port: 8080 +- title: Monolith Architecture + type: service + hostname: workshop + path: /monolith-architecture.html + port: 8090 +difficulty: intermediate +timelimit: 1800 +enhanced_loading: null +--- + +# Swap the Proxy for a Nexus Client + +Click the [button label="Exercise" background="#444CE7"](tab-0) tab, open +`payments/workflows.ts`, and follow TODO 5. + +The call site barely changes. Same operation name, same input, same result type. What +changes is everything underneath it. + +```ts,nocopy +// before — runs in this process +const result = await checkCompliance(compReq); + +// after — goes out through the Endpoint to another team's Worker +const result = await compliance.executeOperation('checkCompliance', compReq, { + scheduleToCloseTimeout: '10 minutes', +}); +``` + +The ten minutes is the budget for the whole call, retries included. It is what lets the +Operation survive the Compliance Worker going away rather than failing the moment it does. +You prove that in challenge 5. + +# A Note if You Have Taken the Java or Kotlin Version + +In those SDKs the Endpoint name is configured on the **Worker**, and the Workflow never +names it. That is a real design difference, not a translation of the same code. + +In TypeScript, `wf.createNexusServiceClient({ service, endpoint })` takes the Endpoint +**inside the Workflow**, at the call site. So the decoupling argument holds one level in +rather than at the Worker: the Workflow knows a contract and an Endpoint name, and the +Registry knows the address that name resolves to. Change where Compliance runs and no +Workflow code changes — but the Endpoint name does appear in this file. + +The Endpoint name itself is a constant in `shared/types.ts`, so there is one place to +change it. + +# Write the Review Caller + +Still in `payments/workflows.ts`, follow TODO 6. + +`submitReview` is a **synchronous** Operation, so the Compliance handler must finish inside +the ten second handler deadline. Use a `scheduleToCloseTimeout` of `'10 seconds'` here, not +the ten minutes above. Two Operations on one Service, two very different budgets. + +Challenge 5 uses this. + +# Delete the Coupling + +Open `payments/worker.ts` and follow TODO 7. + +Remove `...complianceActivities` from the `activities` object, and delete its import. + +That deletion is the decoupling. Everything else is wiring. + +# Run It Decoupled + +Two Workers now, one per team. Start Compliance first. + +Click the [button label="Compliance Worker" background="#444CE7"](tab-4) tab: + +```bash,run +npm run compliance-worker +``` + +Click the [button label="Payments Worker" background="#444CE7"](tab-3) tab: + +```bash,run +npm run payments-worker +``` + +Its banner now reads `payment activities only — compliance is remote`. And this time +you should see the Nexus line on the Payments side: + +```bash,nocopy +[INFO] No Nexus services registered, not polling for Nexus tasks +``` + +That is correct here. Payments is the **caller**, not the handler. It makes Nexus calls; +it answers none. Only the Compliance Worker should be missing that line. + +Click the [button label="Terminal" background="#444CE7"](tab-2) tab: + +```bash,run +npm run starter +``` + +# Read the Result Carefully + +```bash,nocopy + TXN-A Result: COMPLETED Risk: LOW + TXN-B Result: STILL RUNNING MEDIUM risk, waiting on a human + TXN-C Result: DECLINED_COMPLIANCE Risk: HIGH +``` + +TXN-B changed. In the monolith it completed instantly, because compliance was a plain +Activity that returned a MEDIUM verdict and moved on. Now the check runs inside a real +Workflow that parks for human review. Same transaction. Same business rules. The +durable boundary made the wait visible. + +TXN-A and TXN-C are also slower than they were in challenge 1: the handler Workflow sleeps +for ten seconds before returning, which is the window you use in challenge 5. If the +starter reports TXN-A as still running, give it a moment and check the UI — it finishes. + +Leave TXN-B parked. Challenge 5 releases it. + +# If It Hangs + +A payment Workflow stuck in `Running` with nothing happening means the Endpoint name +does not match. Check the Payments Worker tab for: + +```bash,nocopy +INVALID_ARGUMENT: BadScheduleNexusOperationAttributes: endpoint "..." not found +``` + +A wrong Endpoint name does not fail the Workflow. The server rejects the command and +the Workflow task retries forever. Silence, not an error. + +```bash,run +temporal operator nexus endpoint list +``` + +# See the Boundary + +Click the [button label="Temporal UI" background="#444CE7"](tab-1) tab. + +In `payments-namespace`, open the newest `paymentProcessingWorkflow` and find +**Nexus Operation Scheduled** and **Nexus Operation Completed** in the Event History. Click +refresh if the list looks stale. + +Now switch the Namespace selector to `compliance-namespace`. Three +`complianceWorkflow` executions are there. That Namespace was empty in challenge 1. + +Click **Check**. + +# What You Know Now + +- Swapping an Activity proxy for a Nexus client is a one call change at the call site. +- In TypeScript the Workflow names both the contract and the Endpoint; the Registry + resolves that name to a Namespace and Task Queue. +- One Service can carry a slow async Operation and a fast sync one, with different budgets. +- The work moved to another Namespace, and the business logic never changed. diff --git a/typescript/instruqt/04-the-payments-side/check-workshop b/typescript/instruqt/04-the-payments-side/check-workshop new file mode 100755 index 0000000..ec6d2c0 --- /dev/null +++ b/typescript/instruqt/04-the-payments-side/check-workshop @@ -0,0 +1,55 @@ +#!/bin/bash +# ABOUTME: Challenge 04 diagnostic. The only check in the track that can fail. +# +# It is a safety net, not a gate. It exists for exactly one failure mode: a +# mistyped or missing Nexus Endpoint name. Verified behavior: that does NOT fail +# the Workflow. The server rejects the ScheduleNexusOperation command at +# command-report time with +# +# INVALID_ARGUMENT: BadScheduleNexusOperationAttributes: endpoint "..." not found +# +# which happens outside Workflow code, so no try/catch can see it. The Workflow +# sits Running forever with repeated WORKFLOW_TASK_FAILED events and the starter +# prints nothing useful. Silence, not an error. Without this check that becomes +# an instructor support queue during the tightest part of the lab. +# +# Note the Workflow type is camelCase here: in TypeScript the type name is the +# exported function name, not a class name as on the JVM. +# +# Anything else passes. If nothing is stuck, there is nothing to report. +set -uo pipefail + +ADDR="127.0.0.1:7233" + +RUNNING=$(temporal workflow list \ + --address "$ADDR" --namespace payments-namespace \ + --query 'WorkflowType="paymentProcessingWorkflow" AND ExecutionStatus="Running"' \ + --output json 2>/dev/null | jq -r '.[].execution.workflowId' 2>/dev/null) + +for WID in $RUNNING; do + ATTEMPT=$(temporal workflow describe --address "$ADDR" --namespace payments-namespace \ + --workflow-id "$WID" --output json 2>/dev/null \ + | jq -r '.pendingWorkflowTask.attempt // 0' 2>/dev/null) + + # A healthy Workflow waiting on a Nexus Operation sits at attempt 1. Repeated + # Workflow task failures mean the command itself is being rejected. + if [ "${ATTEMPT:-0}" -gt 2 ]; then + fail-message "$WID is stuck retrying its Workflow task (attempt $ATTEMPT). + +That is the signature of an Endpoint name that does not match. The Workflow is not +failing, it is retrying a command the server keeps rejecting. + +Check the Payments Worker tab for: + + INVALID_ARGUMENT: BadScheduleNexusOperationAttributes: endpoint \"...\" not found + +Then confirm the COMPLIANCE_ENDPOINT value in src/shared/types.ts matches the +Endpoint you created. In TypeScript the Endpoint is named at the call site, in +src/payments/workflows.ts, not on the Worker: + + temporal operator nexus endpoint list" + exit 1 + fi +done + +exit 0 diff --git a/typescript/instruqt/04-the-payments-side/cleanup-workshop b/typescript/instruqt/04-the-payments-side/cleanup-workshop new file mode 100755 index 0000000..89c2a1f --- /dev/null +++ b/typescript/instruqt/04-the-payments-side/cleanup-workshop @@ -0,0 +1,9 @@ +#!/bin/bash +# ABOUTME: Challenge 04 cleanup. Challenge 05 starts its own Workers. +set -euo pipefail + +pkill -f "payments/worker.ts" || true +pkill -f "compliance/worker.ts" || true +pkill -f "npm run payments-worker" || true +pkill -f "npm run compliance-worker" || true +sleep 1 diff --git a/typescript/instruqt/04-the-payments-side/setup-workshop b/typescript/instruqt/04-the-payments-side/setup-workshop new file mode 100755 index 0000000..a693929 --- /dev/null +++ b/typescript/instruqt/04-the-payments-side/setup-workshop @@ -0,0 +1,7 @@ +#!/bin/bash +# ABOUTME: Challenge 04 setup. Clean slate. This challenge starts both Workers. +set -euxo pipefail + +pkill -f "payments/worker.ts" || true +pkill -f "compliance/worker.ts" || true +true diff --git a/typescript/instruqt/04-the-payments-side/solve-workshop b/typescript/instruqt/04-the-payments-side/solve-workshop new file mode 100755 index 0000000..011ee32 --- /dev/null +++ b/typescript/instruqt/04-the-payments-side/solve-workshop @@ -0,0 +1,31 @@ +#!/bin/bash +# ABOUTME: Challenge 04 solve. Applies the whole solution, starts both Workers, +# and runs the starter so challenge 05 inherits a parked TXN-B. +set -euxo pipefail + +rm -rf /root/workshop/exercise/src +cp -R /root/workshop/solution/src /root/workshop/exercise/src + +temporal operator nexus endpoint create \ + --name compliance-endpoint \ + --target-namespace compliance-namespace \ + --target-task-queue compliance-risk \ + --address 127.0.0.1:7233 || true + +cd /root/workshop/exercise +npx tsc --noEmit + +pkill -f "compliance/worker.ts" || true +pkill -f "payments/worker.ts" || true +sleep 1 + +nohup npm run compliance-worker > /tmp/compliance-worker.log 2>&1 & +nohup npm run payments-worker > /tmp/payments-worker.log 2>&1 & +for i in $(seq 1 120); do + grep -q "Compliance Worker started" /tmp/compliance-worker.log 2>/dev/null \ + && grep -q "Payments Worker started" /tmp/payments-worker.log 2>/dev/null && break + sleep 1 +done + +npm run starter > /tmp/starter.log 2>&1 || true +cat /tmp/starter.log diff --git a/typescript/instruqt/05-durability-and-human-review/assignment.md b/typescript/instruqt/05-durability-and-human-review/assignment.md new file mode 100644 index 0000000..ef9f3fb --- /dev/null +++ b/typescript/instruqt/05-durability-and-human-review/assignment.md @@ -0,0 +1,172 @@ +--- +slug: durability-and-human-review +type: challenge +title: 5. Break It, Then Finish It +teaser: Take the Compliance Worker down mid-payment. Watch the payment wait instead + of fail. +notes: +- type: text + contents: |- + # The Compliance team is deploying. What happens to payments in flight? + + You just moved compliance into another team's process. That team ships on + Fridays. + + An HTTP call would return a connection error and you would be writing + retry logic. This is not an HTTP call. +- type: text + contents: |- + # Ziggy the tardigrade survives being frozen, boiled, and shot into space + + It parks its metabolism and picks up where it left off. + + Your payment Workflow is about to do the same thing. +tabs: +- title: Exercise + type: service + hostname: workshop + path: /?folder=/root/workshop/exercise/src + port: 8080 +- title: Temporal UI + type: service + hostname: workshop + path: / + port: 8233 +- title: Terminal + type: terminal + hostname: workshop + workdir: /root/workshop/exercise +- title: Payments Worker + type: terminal + hostname: workshop + workdir: /root/workshop/exercise +- title: Compliance Worker + type: terminal + hostname: workshop + workdir: /root/workshop/exercise +- title: Solution + type: service + hostname: workshop + path: /?folder=/root/workshop/solution/src + port: 8080 +- title: Monolith Architecture + type: service + hostname: workshop + path: /monolith-architecture.html + port: 8090 +difficulty: basic +timelimit: 900 +enhanced_loading: null +--- + +# Start Both Teams + +Click the [button label="Compliance Worker" background="#444CE7"](tab-4) tab: + +```bash,run +npm run compliance-worker +``` + +Click the [button label="Payments Worker" background="#444CE7"](tab-3) tab: + +```bash,run +npm run payments-worker +``` + +# Take Compliance Down + +Go back to the [button label="Compliance Worker" background="#444CE7"](tab-4) tab and +press **Ctrl+C**. + +Compliance is now offline. Payments has no idea. + +# Send Payments Anyway + +Click the [button label="Terminal" background="#444CE7"](tab-2) tab: + +```bash,run +npm run starter +``` + +Nothing resolves. The starter reports every transaction as still running and exits after +25 seconds. + +# Look at What Did Not Happen + +Click the [button label="Temporal UI" background="#444CE7"](tab-1) tab. In +`payments-namespace`, click the newest payment Workflow. + +Status is **Running**, not Failed. The Event History shows **Nexus Operation Scheduled** +with no completion. The Operation is waiting for a handler that does not exist yet. + +No connection error. No retry loop you had to write. The caller's +`scheduleToCloseTimeout` of ten minutes is the entire outage budget, and you set it in +one line back in challenge 4. + +# Bring Compliance Back + +Click the [button label="Compliance Worker" background="#444CE7"](tab-4) tab: + +```bash,run +npm run compliance-worker +``` + +Watch the Temporal UI. The pending Operations get picked up and TXN-A completes, +TXN-C is declined for HIGH risk, exactly as if nothing had happened. + +This can take up to a minute. The Operation retries with backoff while the handler +is down, so it does not resume the instant the Worker returns. + +**The payments never failed. They waited.** + +# Release the Parked Payment + +TXN-B is still parked. MEDIUM risk needs a human, and you are the human. + +Wait until TXN-A shows **Completed** before running this. The review is a sync +Operation with a ten second budget, and it fails if `compliance-TXN-B` has not started +yet. Run it again if it does. + +Click the [button label="Terminal" background="#444CE7"](tab-2) tab: + +```bash,run +npm run review-starter +``` + +```bash,nocopy + Review result: APPROVED + Risk level: MEDIUM + Explanation: Approved after manual review +``` + +That call went out over Nexus too, through `submitReview`, the sync handler you wrote +in challenge 3 and the caller you wrote in challenge 4. Sync because it talks to a +Workflow already running and returns immediately. The async Operation starts new work; +the sync one sends a message to work that is already running. + +Open `payment-TXN-B` in the UI one more time. It is **Completed**, with a confirmation +number, and the explanation is the one you just typed in. + +Click **Check**. + +# What You Built + +You started with one Worker running both teams' code and ended with two services that +deploy independently. + +| Piece | What it did | +|---|---| +| `nexus.service()` / `nexus.operation()` | The contract both teams compile against | +| `nexus.serviceHandler()` | The handler only Compliance owns | +| `WorkflowRunOperationHandler` | Backed a long check with a Workflow, retry safe | +| a plain `async` handler | Steered a running Workflow through the boundary | +| `wf.createNexusServiceClient()` | Replaced the Activity proxy at the call site | +| `nexusServices: [...]` on the Worker | Made Compliance answerable at all | +| Nexus Endpoint | The routing rule, the only piece outside the code | + +The business logic never changed. The call site barely changed. Compliance moved to +its own Namespace, its own Task Queue, and its own deployment schedule, and Payments +kept working through an outage while it happened. + +TypeScript SDK support for Nexus is at Pre-release and its APIs are marked experimental, +so expect the surface to move before it is stable. diff --git a/typescript/instruqt/05-durability-and-human-review/check-workshop b/typescript/instruqt/05-durability-and-human-review/check-workshop new file mode 100755 index 0000000..dbefe2d --- /dev/null +++ b/typescript/instruqt/05-durability-and-human-review/check-workshop @@ -0,0 +1,15 @@ +#!/bin/bash +# ABOUTME: Instant pass. This is a live instructor-led workshop, so the +# instructor is the feedback loop and progression is never gated on a script. +# +# Two reasons this is deliberate rather than lazy: +# 1. Skip runs solve-workshop, which copies solution files over the learner's +# work. Gating progression pushes stuck learners toward destroying their +# own code to move on. +# 2. Check-click latency costs real minutes across a room of 20 in a 40 minute +# lab block. +# +# Challenge 04 is the exception: a mistyped Nexus Endpoint hangs silently, so +# that one keeps a diagnostic. Everything the removed checks used to explain now +# lives in the assignment bodies. +exit 0 diff --git a/typescript/instruqt/05-durability-and-human-review/cleanup-workshop b/typescript/instruqt/05-durability-and-human-review/cleanup-workshop new file mode 100755 index 0000000..5021e34 --- /dev/null +++ b/typescript/instruqt/05-durability-and-human-review/cleanup-workshop @@ -0,0 +1,9 @@ +#!/bin/bash +# ABOUTME: Challenge 05 cleanup. One pkill per pattern. +set -euo pipefail + +pkill -f "payments/worker.ts" || true +pkill -f "compliance/worker.ts" || true +pkill -f "npm run payments-worker" || true +pkill -f "npm run compliance-worker" || true +sleep 1 diff --git a/typescript/instruqt/05-durability-and-human-review/setup-workshop b/typescript/instruqt/05-durability-and-human-review/setup-workshop new file mode 100755 index 0000000..c617167 --- /dev/null +++ b/typescript/instruqt/05-durability-and-human-review/setup-workshop @@ -0,0 +1,18 @@ +#!/bin/bash +# ABOUTME: Challenge 05 setup. Clean slate so the outage demo is deterministic. +# +# Terminates leftovers from challenge 04, including the parked TXN-B. Without +# this, re-running the starter collides with the still-Running payment-TXN-B and +# the whole demo fails on "Workflow execution already started". +set -euxo pipefail + +pkill -f "payments/worker.ts" || true +pkill -f "compliance/worker.ts" || true + +ADDR=127.0.0.1:7233 +temporal workflow terminate --address "$ADDR" --namespace payments-namespace \ + --query 'ExecutionStatus="Running"' --reason "challenge 05 reset" --yes >/dev/null 2>&1 || true +temporal workflow terminate --address "$ADDR" --namespace compliance-namespace \ + --query 'ExecutionStatus="Running"' --reason "challenge 05 reset" --yes >/dev/null 2>&1 || true +sleep 2 +true diff --git a/typescript/instruqt/05-durability-and-human-review/solve-workshop b/typescript/instruqt/05-durability-and-human-review/solve-workshop new file mode 100755 index 0000000..5dc3e86 --- /dev/null +++ b/typescript/instruqt/05-durability-and-human-review/solve-workshop @@ -0,0 +1,61 @@ +#!/bin/bash +# ABOUTME: Challenge 05 solve. Runs the outage demo end to end: start payments, +# leave compliance down, send payments, bring compliance up, then review. +set -euxo pipefail + +rm -rf /root/workshop/exercise/src +cp -R /root/workshop/solution/src /root/workshop/exercise/src + +temporal operator nexus endpoint create \ + --name compliance-endpoint \ + --target-namespace compliance-namespace \ + --target-task-queue compliance-risk \ + --address 127.0.0.1:7233 || true + +cd /root/workshop/exercise +npx tsc --noEmit + +pkill -f "compliance/worker.ts" || true +pkill -f "payments/worker.ts" || true +sleep 1 + +# Payments up, Compliance deliberately down. +nohup npm run payments-worker > /tmp/payments-worker.log 2>&1 & +for i in $(seq 1 120); do + grep -q "Payments Worker started" /tmp/payments-worker.log 2>/dev/null && break + sleep 1 +done + +# Operations get scheduled with no handler polling. The caller waits. +npm run starter > /tmp/starter.log 2>&1 || true + +# Bring Compliance back. The pending Operations get picked up. +nohup npm run compliance-worker > /tmp/compliance-worker.log 2>&1 & +for i in $(seq 1 120); do + grep -q "Compliance Worker started" /tmp/compliance-worker.log 2>/dev/null && break + sleep 1 +done + +# Wait for the handler Workflow to actually exist before reviewing it. +# +# A fixed sleep is not enough. After an outage the Nexus Operation retries with +# backoff, so compliance-TXN-B can appear well after the Worker is back. Reviewing +# too early hits the sync Operation's 10s scheduleToCloseTimeout and fails. +ADDR=127.0.0.1:7233 +for i in $(seq 1 120); do + STATE=$(temporal workflow describe --address "$ADDR" --namespace compliance-namespace \ + --workflow-id compliance-TXN-B --output json 2>/dev/null \ + | jq -r '.workflowExecutionInfo.status // empty' 2>/dev/null) + [ "$STATE" = "WORKFLOW_EXECUTION_STATUS_RUNNING" ] && break + sleep 1 +done + +# The review Update is rejected until the automated check has produced a MEDIUM +# verdict, so retry rather than assuming the first attempt lands. +for attempt in 1 2 3 4 5; do + if npm run review-starter > /tmp/review.log 2>&1; then + break + fi + sleep 5 +done +cat /tmp/review.log diff --git a/typescript/instruqt/config.yml b/typescript/instruqt/config.yml new file mode 100644 index 0000000..12d1cb8 --- /dev/null +++ b/typescript/instruqt/config.yml @@ -0,0 +1,18 @@ +version: "3" +containers: +- name: workshop + # Pinned by digest, not :latest. A moving tag let a sandbox boot a cached older + # image that had no code-server in it, so the editor tabs showed "Please wait" + # forever. Re-pin this after every image rebuild with: + # docker buildx imagetools inspect ghcr.io/nadvolod/edu-nexus-typescript-sandbox:latest --format "{{.Manifest.Digest}}" + image: ghcr.io/nadvolod/edu-nexus-typescript-sandbox:latest + shell: /bin/bash + # Two Node Workers, a dev server, and one code-server. The Kotlin track needs + # 8192 for two JVMs plus a Gradle daemon; Node does not. + memory: 4096 + # Every port a challenge tab points at MUST be listed here. Instruqt's proxy + # cannot reach an undeclared port, and the tab shows "Please wait" forever. + ports: + - 8233 # Temporal UI + - 8080 # code-server, both editor tabs + - 8090 # architecture diagram (node serve-diagrams.mjs) diff --git a/typescript/instruqt/track.yml b/typescript/instruqt/track.yml new file mode 100644 index 0000000..7b6d409 --- /dev/null +++ b/typescript/instruqt/track.yml @@ -0,0 +1,53 @@ +slug: nexus-typescript-decouple-monolith +title: Decouple a Monolith with Temporal Nexus (TypeScript) +teaser: Split a payments monolith into two independently deployable services connected + by Nexus. +description: |- + Payments and Compliance share one Worker, one Namespace, one blast radius. A bug + in compliance code at 3 AM takes payments down with it. + + You will put a Nexus boundary between them without changing a line of business + logic. Define the shared Service contract, implement the handler on the Compliance + side, register the Endpoint, then swap the caller from an Activity proxy to a Nexus + client. One call changes at the call site. The architecture underneath does not. + + Then break it on purpose. Stop the Compliance Worker mid-operation and watch the + payment Workflow wait instead of fail. + + TypeScript on the Temporal TypeScript SDK. Five challenges, three of them hands-on. + Nexus support in the TypeScript SDK is at Pre-release and its APIs are experimental. +icon: "" +tags: +- nexus +- temporal +- typescript +- node +owner: temporal +developers: +- nikolay.advolodkin@temporal.io +skipping_enabled: true +idle_timeout: 3600 +timelimit: 7200 +lab_config: + extend_ttl: 900 + sidebar_enabled: true + feedback_recap_enabled: false + feedback_tab_enabled: false + loadingMessages: + - Waking Ziggy the tardigrade... + - Starting the Temporal dev server... + - Creating two Namespaces, one per team... + - Warming the npm cache so your first run is not your slowest... + - Typechecking TypeScript against the Temporal SDK... + - Tardigrades survive vacuum. Your Workflows survive Worker crashes... + - Checking that nobody is polling the Compliance Task Queue yet... + - Opening the code editor... + - Loading the Temporal Web UI... + - Ziggy found some lichen. Almost ready... + theme: + name: modern-dark + override_challenge_layout: true + hideStopButton: false + default_layout: AssignmentLeft + default_layout_sidebar_size: 33 +enhanced_loading: false diff --git a/typescript/instruqt/track_scripts/cleanup-workshop b/typescript/instruqt/track_scripts/cleanup-workshop new file mode 100755 index 0000000..5cb845d --- /dev/null +++ b/typescript/instruqt/track_scripts/cleanup-workshop @@ -0,0 +1,16 @@ +#!/bin/bash +# ABOUTME: Track-level cleanup. Runs once when the track is torn down. +# One pkill per pattern. Alternation does not work on macOS and the scripts +# are authored there. +# +# Two patterns per process: `npm run x` leaves an npm parent as well as the node +# child that actually holds the port, and killing only one leaves the other. +set -euo pipefail + +pkill -f "payments/worker.ts" || true +pkill -f "compliance/worker.ts" || true +pkill -f "payments/starter.ts" || true +pkill -f "payments/review-starter.ts" || true +pkill -f "npm run payments-worker" || true +pkill -f "npm run compliance-worker" || true +pkill -f "temporal server" || true diff --git a/typescript/instruqt/track_scripts/setup-workshop b/typescript/instruqt/track_scripts/setup-workshop new file mode 100755 index 0000000..70cfa39 --- /dev/null +++ b/typescript/instruqt/track_scripts/setup-workshop @@ -0,0 +1,155 @@ +#!/bin/bash +# ABOUTME: Track-level setup. Runs once when the attendee enters the track. +# Starts the editors, stages the workshop source, starts the dev server, and +# creates both Namespaces. +# +# Ordering matters. The editors start BEFORE any Temporal work and are never +# gated on it. An earlier version started them after the dev server health +# check, which had an `exit 1` on timeout: a slow Temporal start took the +# Exercise and Solution tabs down with it and the attendee saw "Please wait". +# The editors have nothing to do with Temporal, so they no longer wait for it. +# +# The Nexus Endpoint is deliberately NOT created here. Attendees create it +# themselves in challenge 03, which is where the Registry stops being abstract. +set -uo pipefail + +# 1. Wait for Instruqt host bootstrap. +until [ -f /opt/instruqt/bootstrap/host-bootstrap-completed ]; do + sleep 1 +done + +# 2. Stage the writable working tree. /opt/workshop stays read-only by convention. +# +# Source only. node_modules is 292 MB per tree, so copying both would move +# ~600 MB before the attendee can type anything. It is baked into the image and +# never edited, so the writable tree symlinks to it instead. ts-node and tsc +# resolve through the symlink normally. +# +# Consequence: `npm install` inside the lab cannot write to node_modules. The +# workshop never needs it. +if [ ! -d /root/workshop ]; then + for t in exercise solution; do + mkdir -p "/root/workshop/$t" + cp -r "/opt/workshop/$t/src" "/root/workshop/$t/src" + cp "/opt/workshop/$t/package.json" \ + "/opt/workshop/$t/package-lock.json" \ + "/opt/workshop/$t/tsconfig.json" "/root/workshop/$t/" + ln -s "/opt/workshop/$t/node_modules" "/root/workshop/$t/node_modules" + done +fi + +# 3. Start ONE code-server, rooted at the whole workshop tree. +# +# One instance, not two. Both editor tabs point at this single server and select +# their directory with ?folder=, which is exactly the shape the one Temporal track +# known to run code-server successfully (temporal-ai-agents-python-v4) uses. +# +# Why it matters beyond tidiness: Instruqt loads every service tab's iframe at +# challenge start, including tabs that are not visible. code-server cannot compute +# its layout in a 0x0 iframe and the workbench never finishes booting, which is +# the "blank until you refresh" symptom (console shows "Unable to figure out +# browser width and height"). Two instances meant two hidden iframes racing that +# boot at once. One halves it, and frees about 200 MB. +# +# Port 8080 on purpose. 8443 is the conventional alternate-HTTPS port and +# Instruqt's proxy could not reach code-server there. Every port a tab points at +# must also be listed under `ports:` in config.yml. +if ! curl -sf -o /dev/null "http://127.0.0.1:8080/" 2>/dev/null; then + nohup code-server \ + --bind-addr "0.0.0.0:8080" \ + --auth none \ + --disable-telemetry \ + --disable-update-check \ + --user-data-dir /root/.cs-exercise \ + /root/workshop > /tmp/code-server-8080.log 2>&1 & + disown || true +fi + +# 3b. Serve the architecture diagram on 8090. +# +# The Kotlin image used jwebserver, which ships with the JDK. There is no JDK +# here, so a small Node static server is baked into the image instead. +# Must also be listed under `ports:` in config.yml or the tab cannot reach it. +if ! curl -sf -o /dev/null "http://127.0.0.1:8090/" 2>/dev/null; then + nohup node /opt/serve-diagrams.mjs > /tmp/diagrams.log 2>&1 & + disown || true +fi + +# 4. Start the Temporal dev server in the background. +# --ip 0.0.0.0 so the Instruqt service tab can reach the Web UI on 8233. +if ! curl -sf http://127.0.0.1:8233 >/dev/null 2>&1; then + nohup temporal server start-dev \ + --ip 0.0.0.0 \ + --port 7233 \ + --ui-port 8233 \ + --db-filename /root/temporal.db \ + --log-level warn \ + > /tmp/temporal-server.log 2>&1 & + disown || true +fi + +# 5. Wait for Temporal health. Log loudly on failure, but do NOT exit: killing +# the script here would leave later setup undone for no benefit, and the +# editors are already up. +TEMPORAL_UP=0 +for i in $(seq 1 120); do + if temporal operator cluster health --address 127.0.0.1:7233 >/dev/null 2>&1; then + TEMPORAL_UP=1 + break + fi + sleep 1 +done +if [ "$TEMPORAL_UP" -ne 1 ]; then + echo "==> WARNING: Temporal dev server did not become healthy in 120s." + tail -30 /tmp/temporal-server.log 2>/dev/null +fi + +# 6. Pre-create both Namespaces. One per team, which is the whole point. +temporal operator namespace create --namespace payments-namespace \ + --address 127.0.0.1:7233 >/dev/null 2>&1 || true +temporal operator namespace create --namespace compliance-namespace \ + --address 127.0.0.1:7233 >/dev/null 2>&1 || true + +for i in $(seq 1 30); do + if temporal operator namespace describe --namespace payments-namespace \ + --address 127.0.0.1:7233 >/dev/null 2>&1 \ + && temporal operator namespace describe --namespace compliance-namespace \ + --address 127.0.0.1:7233 >/dev/null 2>&1; then + break + fi + sleep 1 +done + +# 7. Wait for the editor to be genuinely ready. Never fatal. +# +# Diagnosed by Instruqt support. The old check was +# curl -sf http://127.0.0.1:8080/ +# which passes the moment the HTTP listener binds, because bare "/" 302-redirects +# and `curl -sf` treats a 3xx as success. Setup then reported "ready" while the +# workbench was still booting. +# +# Their suggested fix polls /healthz for '"status":"ready"'. That string does not +# exist in code-server 4.96.4: /healthz returns "expired" or "alive", and "alive" +# only means an HTTP request arrived recently, which our own polling would cause. +# Waiting on it would never pass. Verified by measuring the live endpoint. +# +# So we check the two things that ARE true only once the workbench is up: the +# exact URL a tab opens returns 200 (bare "/" still 302s at that point), and +# code-server logs that the extension host is running. +EX_FOLDER=/root/workshop/exercise/src + +code_server_ready() { + [ "$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 \ + "http://127.0.0.1:8080/?folder=${EX_FOLDER}" 2>/dev/null)" = "200" ] \ + && grep -q "Extension host agent started" /tmp/code-server-8080.log 2>/dev/null +} + +for i in $(seq 1 90); do + code_server_ready && break + sleep 1 +done + +code_server_ready \ + || { echo "==> WARNING: code-server 8080 not ready."; tail -20 /tmp/code-server-8080.log 2>/dev/null; } + +echo "==> Track setup complete." diff --git a/typescript/sandbox/Dockerfile b/typescript/sandbox/Dockerfile new file mode 100644 index 0000000..253000a --- /dev/null +++ b/typescript/sandbox/Dockerfile @@ -0,0 +1,83 @@ +# Sandbox image for the TypeScript Nexus workshop. +# Build context is typescript/ so this can COPY decouple-monolith/. +# +# docker buildx build --platform linux/amd64 \ +# -f sandbox/Dockerfile -t ghcr.io/nadvolod/edu-nexus-typescript-sandbox:latest --push . +# +# linux/amd64 is required. An arm64 image (the Apple Silicon default) fails to +# start on Instruqt with an empty log and no error. + +FROM node:22-bookworm + +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl wget git vim nano less unzip ca-certificates bash-completion jq procps \ + && rm -rf /var/lib/apt/lists/* + +# --- Temporal CLI ------------------------------------------------------------- +ARG TEMPORAL_CLI_VERSION=1.7.2 +RUN curl -sSL \ + "https://github.com/temporalio/cli/releases/download/v${TEMPORAL_CLI_VERSION}/temporal_cli_${TEMPORAL_CLI_VERSION}_linux_amd64.tar.gz" \ + | tar -xz -C /usr/local/bin temporal \ + && chmod +x /usr/local/bin/temporal + +# --- code-server (VS Code in the browser) ------------------------------------- +# The native Instruqt `type: code` tab is "a simple code editor" with no +# documented language support. code-server gives real VS Code. +# +# Unlike the Kotlin image, NO language extension is installed here and that is +# deliberate, not an omission: TypeScript support is built into VS Code. The +# Kotlin image pinned a syntax-only extension specifically to avoid running a +# language server next to two JVMs. Here the language service is wanted — it is +# what surfaces the compile error in challenge 2 as a red squiggle, live, which +# is the lesson. Node leaves plenty of headroom for it. +ARG CODE_SERVER_VERSION=4.96.4 +RUN curl -fsSL https://code-server.dev/install.sh | sh -s -- --version ${CODE_SERVER_VERSION} + +# Seed settings into BOTH user-data-dirs. This is load-bearing, not cosmetic. +# +# security.workspace.trust.enabled MUST be false. Workspace Trust is on by +# default, and code-server renders a blank page instead of the workbench until +# the folder is trusted. That was the cause of the editor tabs showing white. +# +# Each instance needs its own copy because each runs with its own +# --user-data-dir, which is what stops them sharing a last-opened workspace. +RUN for d in /root/.cs-exercise /root/.cs-solution; do \ + mkdir -p "$d/User" && printf '%s\n' \ + '{' \ + ' "security.workspace.trust.enabled": false,' \ + ' "workbench.colorTheme": "Default Dark+",' \ + ' "workbench.startupEditor": "none",' \ + ' "editor.fontSize": 14,' \ + ' "editor.tabSize": 2,' \ + ' "telemetry.telemetryLevel": "off"' \ + '}' > "$d/User/settings.json"; \ + done + +# --- Architecture diagram ----------------------------------------------------- +# Lives outside the challenge folders on purpose: Instruqt parses every file +# inside a challenge directory as a lifecycle script. +COPY diagrams/ /opt/diagrams/ +COPY sandbox/serve-diagrams.mjs /opt/serve-diagrams.mjs + +# --- Bake the workshop source ------------------------------------------------- +COPY decouple-monolith/ /opt/workshop/ + +# --- Pre-warm npm ------------------------------------------------------------- +# Installs both trees so the first command in the lab is fast, and typechecks +# them so a broken port fails the image build instead of the workshop. +# +# Both trees must typecheck. The exercise's open TODOs are comments and a +# throwing stub, so it compiles cleanly with the work still undone. Keep these +# strict: a failure here is a real defect, not an expected state. +WORKDIR /opt/workshop/solution +RUN npm ci && npx tsc --noEmit + +WORKDIR /opt/workshop/exercise +RUN npm ci && npx tsc --noEmit + +# --- Shell environment -------------------------------------------------------- +RUN echo 'export TEMPORAL_ADDRESS="${TEMPORAL_ADDRESS:-127.0.0.1:7233}"' >> /root/.bashrc \ + && echo 'export WORKSHOP_REPO=/root/workshop' >> /root/.bashrc + +WORKDIR /root +CMD ["/bin/bash"] diff --git a/typescript/sandbox/serve-diagrams.mjs b/typescript/sandbox/serve-diagrams.mjs new file mode 100644 index 0000000..0768c2c --- /dev/null +++ b/typescript/sandbox/serve-diagrams.mjs @@ -0,0 +1,35 @@ +// Minimal static file server for the architecture diagram tab. +// +// The Kotlin image used `jwebserver`, which ships with the JDK. There is no JDK +// here, and adding python3 or an npm package for one static directory is more +// moving parts than this. Node's own http module is enough. +import { createServer } from 'node:http'; +import { readFile } from 'node:fs/promises'; +import { extname, join, normalize } from 'node:path'; + +const ROOT = '/opt/diagrams'; +const PORT = 8090; +const TYPES = { + '.html': 'text/html; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.js': 'text/javascript; charset=utf-8', + '.svg': 'image/svg+xml', + '.png': 'image/png', + '.webp': 'image/webp', +}; + +createServer(async (req, res) => { + // Strip the query string, then normalize before joining so ../ cannot escape ROOT. + const rel = normalize(decodeURIComponent((req.url ?? '/').split('?')[0])).replace(/^(\.\.[/\\])+/, ''); + const path = join(ROOT, rel === '/' ? 'monolith-architecture.html' : rel); + if (!path.startsWith(ROOT)) { + res.writeHead(403).end('forbidden'); + return; + } + try { + const body = await readFile(path); + res.writeHead(200, { 'content-type': TYPES[extname(path)] ?? 'application/octet-stream' }).end(body); + } catch { + res.writeHead(404).end('not found'); + } +}).listen(PORT, '0.0.0.0', () => console.log(`diagrams on ${PORT} from ${ROOT}`)); From 84fc1e1083a8c53d671f5a0238bedf043abe3dad Mon Sep 17 00:00:00 2001 From: nadvolod <2136245+nadvolod@users.noreply.github.com> Date: Fri, 28 Aug 2026 14:50:32 -0400 Subject: [PATCH 03/22] Build the TypeScript sandbox image and pin it by digest Adds typescript/.dockerignore and pins config.yml to the image that was built and pushed to ghcr.io/nadvolod/edu-nexus-typescript-sandbox. Digest: sha256:ce2f1b9f1671a9f6b61f7042cc417e95ca887d09acbb62ec209ae5b7e540ea44 The .dockerignore is load-bearing, not tidiness. The build context is typescript/ and the Dockerfile COPYs decouple-monolith/, which carries 292 MB of node_modules per tree. Without the ignore the build ships ~600 MB of context and bakes in host-built artifacts that are the wrong architecture: the host is arm64, the image amd64, and @temporalio ships a native core bridge. npm ci reinstalls them inside the image anyway. Context drops from 584 MB to 0.3 MB. Verified against the pushed image, not just the build: uname -m x86_64 node v22.23.2, npm 10.9.8 temporal 1.7.2 (Server 1.31.1) code-server present, both user-data-dirs seeded workshop trees 13 .ts files and 98 node_modules entries each diagrams monolith-architecture.html + serve-diagrams.mjs present monolith run worker starts, TXN-A COMPLETED/LOW, TXN-B COMPLETED/MEDIUM, TXN-C DECLINED_COMPLIANCE/HIGH That last one matters most: it proves the native core bridge resolved for linux/amd64 from a lockfile generated on darwin-arm64. instruqt track validate still passes with the digest pinned. Co-Authored-By: Claude Opus 5 (1M context) --- typescript/.dockerignore | 7 +++++++ typescript/instruqt/config.yml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 typescript/.dockerignore diff --git a/typescript/.dockerignore b/typescript/.dockerignore new file mode 100644 index 0000000..0b4c473 --- /dev/null +++ b/typescript/.dockerignore @@ -0,0 +1,7 @@ +# node_modules is reinstalled inside the image with `npm ci`, so shipping the +# host copy would send ~600 MB of build context and then be thrown away. +# It would also be the wrong architecture: the host is arm64, the image amd64, +# and @temporalio ships a native core bridge binary. +**/node_modules +**/lib +**/.DS_Store diff --git a/typescript/instruqt/config.yml b/typescript/instruqt/config.yml index 12d1cb8..7f3fbe0 100644 --- a/typescript/instruqt/config.yml +++ b/typescript/instruqt/config.yml @@ -5,7 +5,7 @@ containers: # image that had no code-server in it, so the editor tabs showed "Please wait" # forever. Re-pin this after every image rebuild with: # docker buildx imagetools inspect ghcr.io/nadvolod/edu-nexus-typescript-sandbox:latest --format "{{.Manifest.Digest}}" - image: ghcr.io/nadvolod/edu-nexus-typescript-sandbox:latest + image: ghcr.io/nadvolod/edu-nexus-typescript-sandbox@sha256:ce2f1b9f1671a9f6b61f7042cc417e95ca887d09acbb62ec209ae5b7e540ea44 shell: /bin/bash # Two Node Workers, a dev server, and one code-server. The Kotlin track needs # 8192 for two JVMs plus a Gradle daemon; Node does not. From 930fa466ea87a236dd36ac56f5288e9808ee4007 Mon Sep 17 00:00:00 2001 From: nadvolod <2136245+nadvolod@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:34:16 -0400 Subject: [PATCH 04/22] Publish the TypeScript track in maintenance mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `maintenance: true` to track.yml. A first publish should not be reachable by a learner before a human has walked it, and the repo's push guardrail only permits an agent to publish a track that is in maintenance mode — pushing to a live track is human-owned. Flip this to false to take the track live. The publish itself did not complete: Instruqt reported "Could not find the image". Cause confirmed by anonymous manifest fetch against GHCR: nadvolod/edu-nexus-kotlin-sandbox HTTP 200 (public) nadvolod/edu-nexus-typescript-sandbox HTTP 403 (private) A newly pushed GHCR package is private by default, and Instruqt pulls anonymously. The package needs to be made public, matching the Kotlin one. GitHub exposes no REST endpoint for package visibility, so that is a web-UI action. No partial state was left behind: no id or checksum was written back to track.yml or any assignment.md, so the track was not created. The Kotlin track is untouched (id yopxoc2jf3jb, checksum 13209538472498183870). Co-Authored-By: Claude Opus 5 (1M context) --- typescript/instruqt/track.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/typescript/instruqt/track.yml b/typescript/instruqt/track.yml index 7b6d409..37bbe07 100644 --- a/typescript/instruqt/track.yml +++ b/typescript/instruqt/track.yml @@ -23,6 +23,9 @@ tags: - typescript - node owner: temporal +# First publish goes up in maintenance mode so nobody can walk into an unverified +# track. A human flips this to false to take it live. +maintenance: true developers: - nikolay.advolodkin@temporal.io skipping_enabled: true From b511d0b9b3537dcaec1e14858b71e9e06ddc0d50 Mon Sep 17 00:00:00 2001 From: nadvolod <2136245+nadvolod@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:54:00 -0400 Subject: [PATCH 05/22] Publish the TypeScript track and record its assigned ids Published to Instruqt as temporal/nexus-typescript-decouple-monolith, in maintenance mode. These ids are assigned by the server and written back into the files; committing them is what makes a later publish update this track rather than create a second one. track fxx1wuc1irwu checksum 7568052846915990700 challenges nwdcc4mlob8f, ynazxaoijxil, lwb2jpje3u96, xbklygolip1u, hcijwjx63wn2 plus 7 tab ids per challenge Two failed attempts preceded this, both worth recording: 1. Instruqt reported "Could not find the image". The GHCR package was private by default; the Kotlin one is public. Confirmed by anonymous manifest fetch (403 vs 200) and fixed by making the package public. 2. The retry then reported "There are remote changes for this track". Attempt 1 had created a bare stub server-side before aborting on the image: id assigned, title "title", empty description, no tags, and a config.yml with no containers at all. Pulling into a throwaway copy showed only track.yml and config.yml differed, so --force overwrote a stub of our own making, not real content. Verified afterwards: - every tab-N button reference still resolves to the right tab by label, which is the check validate-track.sh silently stops performing once ids exist - the Kotlin track is untouched: pulling kotlin/instruqt reports "Track is up to date" with no .remote files, and kotlin/ has no working-tree changes The track is in maintenance mode. Flip `maintenance` to false to take it live. Co-Authored-By: Claude Opus 5 (1M context) --- .../01-run-the-monolith/assignment.md | 22 +++++++++++----- .../02-the-shared-contract/assignment.md | 22 +++++++++++----- .../03-the-compliance-side/assignment.md | 22 +++++++++++----- .../04-the-payments-side/assignment.md | 26 ++++++++++++------- .../assignment.md | 22 +++++++++++----- typescript/instruqt/track.yml | 8 +++--- 6 files changed, 81 insertions(+), 41 deletions(-) diff --git a/typescript/instruqt/01-run-the-monolith/assignment.md b/typescript/instruqt/01-run-the-monolith/assignment.md index 19616e7..6ead589 100644 --- a/typescript/instruqt/01-run-the-monolith/assignment.md +++ b/typescript/instruqt/01-run-the-monolith/assignment.md @@ -1,5 +1,6 @@ --- slug: run-the-monolith +id: nwdcc4mlob8f type: challenge title: 1. Run the Monolith teaser: Three payments, two teams, one Worker. Find the coupling before you fix it. @@ -22,34 +23,41 @@ notes: Right now only one of them has anything in it. tabs: -- title: Exercise +- id: jy7gspk7rzja + title: Exercise type: service hostname: workshop path: /?folder=/root/workshop/exercise/src port: 8080 -- title: Temporal UI +- id: osmynelncext + title: Temporal UI type: service hostname: workshop path: / port: 8233 -- title: Terminal +- id: bhx6tjsdyrva + title: Terminal type: terminal hostname: workshop workdir: /root/workshop/exercise -- title: Payments Worker +- id: utwhvn6xykly + title: Payments Worker type: terminal hostname: workshop workdir: /root/workshop/exercise -- title: Compliance Worker +- id: xcqorbmjolyq + title: Compliance Worker type: terminal hostname: workshop workdir: /root/workshop/exercise -- title: Solution +- id: bwvwdih1vjnt + title: Solution type: service hostname: workshop path: /?folder=/root/workshop/solution/src port: 8080 -- title: Monolith Architecture +- id: kaxawqn9yd2g + title: Monolith Architecture type: service hostname: workshop path: /monolith-architecture.html diff --git a/typescript/instruqt/02-the-shared-contract/assignment.md b/typescript/instruqt/02-the-shared-contract/assignment.md index 989a769..281a51c 100644 --- a/typescript/instruqt/02-the-shared-contract/assignment.md +++ b/typescript/instruqt/02-the-shared-contract/assignment.md @@ -1,5 +1,6 @@ --- slug: the-shared-contract +id: ynazxaoijxil type: challenge title: 2. The Shared Contract teaser: Write the interface both teams depend on. One file, two Operations. @@ -22,34 +23,41 @@ notes: You write the Service now. The other three come next. tabs: -- title: Exercise +- id: fsl9wnyvzlt2 + title: Exercise type: service hostname: workshop path: /?folder=/root/workshop/exercise/src port: 8080 -- title: Temporal UI +- id: 7iygieqqyuko + title: Temporal UI type: service hostname: workshop path: / port: 8233 -- title: Terminal +- id: nljg80a3snn1 + title: Terminal type: terminal hostname: workshop workdir: /root/workshop/exercise -- title: Payments Worker +- id: ymi443zqbrnc + title: Payments Worker type: terminal hostname: workshop workdir: /root/workshop/exercise -- title: Compliance Worker +- id: znmrv8zywaej + title: Compliance Worker type: terminal hostname: workshop workdir: /root/workshop/exercise -- title: Solution +- id: 4m8yg9ygjnkm + title: Solution type: service hostname: workshop path: /?folder=/root/workshop/solution/src port: 8080 -- title: Monolith Architecture +- id: 1wdriwhm5mby + title: Monolith Architecture type: service hostname: workshop path: /monolith-architecture.html diff --git a/typescript/instruqt/03-the-compliance-side/assignment.md b/typescript/instruqt/03-the-compliance-side/assignment.md index 81462cc..a64d0c5 100644 --- a/typescript/instruqt/03-the-compliance-side/assignment.md +++ b/typescript/instruqt/03-the-compliance-side/assignment.md @@ -1,5 +1,6 @@ --- slug: the-compliance-side +id: lwb2jpje3u96 type: challenge title: 3. The Compliance Side teaser: Implement both handlers, register them on a Worker, and create the Endpoint. @@ -22,34 +23,41 @@ notes: The Solution tab is one click away. Use it if you stall. Learning the shape beats staring at a blank function. tabs: -- title: Exercise +- id: gok4feicejlo + title: Exercise type: service hostname: workshop path: /?folder=/root/workshop/exercise/src port: 8080 -- title: Temporal UI +- id: rhie9uzgyhfq + title: Temporal UI type: service hostname: workshop path: / port: 8233 -- title: Terminal +- id: hcqbwck0rvo9 + title: Terminal type: terminal hostname: workshop workdir: /root/workshop/exercise -- title: Payments Worker +- id: jjlm3enbxo4t + title: Payments Worker type: terminal hostname: workshop workdir: /root/workshop/exercise -- title: Compliance Worker +- id: xy2tk6mpbv0t + title: Compliance Worker type: terminal hostname: workshop workdir: /root/workshop/exercise -- title: Solution +- id: xp7aoiijy8bj + title: Solution type: service hostname: workshop path: /?folder=/root/workshop/solution/src port: 8080 -- title: Monolith Architecture +- id: ujvrlheqjxnv + title: Monolith Architecture type: service hostname: workshop path: /monolith-architecture.html diff --git a/typescript/instruqt/04-the-payments-side/assignment.md b/typescript/instruqt/04-the-payments-side/assignment.md index ceafff1..24b1029 100644 --- a/typescript/instruqt/04-the-payments-side/assignment.md +++ b/typescript/instruqt/04-the-payments-side/assignment.md @@ -1,9 +1,10 @@ --- slug: the-payments-side +id: xbklygolip1u type: challenge title: 4. The Payments Side -teaser: Swap the Activity proxy for a Nexus client, point it at the Endpoint, and delete - the coupling. +teaser: Swap the Activity proxy for a Nexus client, point it at the Endpoint, and + delete the coupling. notes: - type: text contents: |- @@ -22,34 +23,41 @@ notes: Deleting it is the decoupling. Everything else is wiring. tabs: -- title: Exercise +- id: jt0nbvtyxv2o + title: Exercise type: service hostname: workshop path: /?folder=/root/workshop/exercise/src port: 8080 -- title: Temporal UI +- id: xrdqvojuovik + title: Temporal UI type: service hostname: workshop path: / port: 8233 -- title: Terminal +- id: ak56yyflojys + title: Terminal type: terminal hostname: workshop workdir: /root/workshop/exercise -- title: Payments Worker +- id: auuddyhhaflq + title: Payments Worker type: terminal hostname: workshop workdir: /root/workshop/exercise -- title: Compliance Worker +- id: gmgtqjw8yg6r + title: Compliance Worker type: terminal hostname: workshop workdir: /root/workshop/exercise -- title: Solution +- id: xpohqyhbtdz3 + title: Solution type: service hostname: workshop path: /?folder=/root/workshop/solution/src port: 8080 -- title: Monolith Architecture +- id: slczt2wyuij1 + title: Monolith Architecture type: service hostname: workshop path: /monolith-architecture.html diff --git a/typescript/instruqt/05-durability-and-human-review/assignment.md b/typescript/instruqt/05-durability-and-human-review/assignment.md index ef9f3fb..e0f0717 100644 --- a/typescript/instruqt/05-durability-and-human-review/assignment.md +++ b/typescript/instruqt/05-durability-and-human-review/assignment.md @@ -1,5 +1,6 @@ --- slug: durability-and-human-review +id: hcijwjx63wn2 type: challenge title: 5. Break It, Then Finish It teaser: Take the Compliance Worker down mid-payment. Watch the payment wait instead @@ -22,34 +23,41 @@ notes: Your payment Workflow is about to do the same thing. tabs: -- title: Exercise +- id: uxvwrjemsoea + title: Exercise type: service hostname: workshop path: /?folder=/root/workshop/exercise/src port: 8080 -- title: Temporal UI +- id: kirogybfummw + title: Temporal UI type: service hostname: workshop path: / port: 8233 -- title: Terminal +- id: gsmserb7horg + title: Terminal type: terminal hostname: workshop workdir: /root/workshop/exercise -- title: Payments Worker +- id: fxetrm2uuagt + title: Payments Worker type: terminal hostname: workshop workdir: /root/workshop/exercise -- title: Compliance Worker +- id: xcwthyvjsofb + title: Compliance Worker type: terminal hostname: workshop workdir: /root/workshop/exercise -- title: Solution +- id: rynbymolm35m + title: Solution type: service hostname: workshop path: /?folder=/root/workshop/solution/src port: 8080 -- title: Monolith Architecture +- id: dwrcskq7wzji + title: Monolith Architecture type: service hostname: workshop path: /monolith-architecture.html diff --git a/typescript/instruqt/track.yml b/typescript/instruqt/track.yml index 37bbe07..651c8e3 100644 --- a/typescript/instruqt/track.yml +++ b/typescript/instruqt/track.yml @@ -1,4 +1,5 @@ slug: nexus-typescript-decouple-monolith +id: fxx1wuc1irwu title: Decouple a Monolith with Temporal Nexus (TypeScript) teaser: Split a payments monolith into two independently deployable services connected by Nexus. @@ -20,14 +21,12 @@ icon: "" tags: - nexus - temporal -- typescript - node +- typescript owner: temporal -# First publish goes up in maintenance mode so nobody can walk into an unverified -# track. A human flips this to false to take it live. -maintenance: true developers: - nikolay.advolodkin@temporal.io +maintenance: true skipping_enabled: true idle_timeout: 3600 timelimit: 7200 @@ -53,4 +52,5 @@ lab_config: hideStopButton: false default_layout: AssignmentLeft default_layout_sidebar_size: 33 +checksum: "7568052846915990700" enhanced_loading: false From 6a882e4f4d89d13a98acb53ce33ef61949d5011c Mon Sep 17 00:00:00 2001 From: nadvolod <2136245+nadvolod@users.noreply.github.com> Date: Fri, 28 Aug 2026 20:29:23 -0400 Subject: [PATCH 06/22] Document the TypeScript track in the README The README described the Kotlin track in depth and said nothing about TypeScript. Adds a "Language tracks" table pointing at both, a note that the slide decks live in temporalio/temporal-devdays-ts rather than here, and a TypeScript section that mirrors the Kotlin one. The section leads with the three lessons that do NOT survive the port, because those are the parts a person who has delivered the Java or Kotlin version will otherwise get wrong: - challenge 2 deliberately ends on a failing typecheck, since ServiceHandlerFor obliges a handler at compile time - challenge 3's proof of registration inverts: the absence of "No Nexus services registered" is the signal, there is no Nexus Poller line - the Endpoint is named at the call site, not on the Worker It also records what cost time during this port and is invisible from the files: the GHCR package must be public or Instruqt reports only "Could not find the image"; proxyActivities silently types away synchronous Activities and surfaces as "Type 'Symbol' has no call signatures" at the call site; a setHandler validator must take the same arguments as its handler; visibility queries use the camelCase function name; and the Kotlin and TypeScript workshops cannot share one dev server because both poll compliance-risk. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 164 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 163 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cffa139..5a269ae 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,20 @@ See the [Nexus documentation](https://docs.temporal.io/nexus) to explore more. You can launch an exercise environment for this tutorial using GitHub Codespaces by following [this](codespaces.md) walkthrough. +## Language tracks + +Two hands-on lab versions of this material exist as Instruqt tracks, in addition to the +Java tutorial under `java/`: + +| Language | Directory | Track slug | Sandbox image | +|---|---|---|---| +| Kotlin | `kotlin/` | `nexus-kotlin-decouple-monolith` | `ghcr.io/nadvolod/edu-nexus-kotlin-sandbox` | +| TypeScript | `typescript/` | `nexus-typescript-decouple-monolith` | `ghcr.io/nadvolod/edu-nexus-typescript-sandbox` | + +The slide decks are **not** in this repo. They live in +[`temporalio/temporal-devdays-ts`](https://github.com/temporalio/temporal-devdays-ts) +under `decks/`, which is the index and deck library for TypeScript DevDays content. + ## Kotlin Instruqt track A hands-on lab version of this material, in Kotlin, lives under `kotlin/`. It runs as @@ -205,4 +219,152 @@ even though `build-image` is disabled. `Workflow.getLogger` from `main`, which throws off a Workflow thread. - **`track.yml` and every `assignment.md` frontmatter are rewritten on push.** Comments are stripped and keys reordered. Keep durable explanation here, not in those files. - `config.yml` is left alone. \ No newline at end of file + `config.yml` is left alone. + +## TypeScript Instruqt track + +The same material in TypeScript, under `typescript/`, on the Temporal TypeScript SDK. +Same five challenges, same narrative, same 90 minute shape. + +``` +typescript/ +├── decouple-monolith/ +│ ├── exercise/ npm + ts-node, TODOs 1 to 7 open +│ └── solution/ complete +├── sandbox/ +│ ├── Dockerfile node:22-bookworm + Temporal CLI + code-server +│ └── serve-diagrams.mjs static server for the diagram tab +├── diagrams/ architecture diagram, served on 8090 +├── .dockerignore keeps node_modules out of the build context +└── instruqt/ + ├── track.yml, config.yml, track_scripts/ + └── 01-run-the-monolith/ ... 05-durability-and-human-review/ +``` + +**Nexus support in the TypeScript SDK is at Pre-release and its APIs are marked +experimental.** The deck and challenge 2 both say so out loud rather than letting an +attendee discover it later. + +### Three lessons that do not survive the port + +These are real design differences between the SDKs, not translation choices. The +assignments call each one out, including a note aimed at anyone who has delivered the +Java or Kotlin version. + +- **Challenge 2 ends with a deliberately FAILING typecheck.** `ServiceHandlerFor` is + a non-optional mapped type, so declaring an Operation obliges a matching handler at + compile time. Kotlin fails at Worker startup with `Missing handlers for service + operations`; TypeScript refuses to compile: + + ``` + TS2345: Type '{}' is missing the following properties from type + 'ServiceHandlerFor<...>': checkCompliance, submitReview + ``` + + So `02/solve-workshop` deliberately does not typecheck. The build is red until + challenge 3 fills the handlers in, and that is the lesson, not a defect. + +- **Challenge 3's proof of registration inverts.** There is no `Nexus Poller` line to + look for. A Worker *without* `nexusServices` logs `No Nexus services registered, not + polling for Nexus tasks`; a correctly wired one logs nothing. Absence is the signal, so + the assignment tells learners to go looking for it deliberately. + +- **The Endpoint moves to the call site.** Java and Kotlin configure it on the Worker via + `NexusServiceOptions`, which is what makes the Kotlin lesson "notice you do NOT write + the Endpoint name here." In TypeScript, `wf.createNexusServiceClient({ service, + endpoint })` takes it inside the Workflow. The decoupling argument still holds one + level in — the Workflow knows a name, the Registry knows the address — and + `COMPLIANCE_ENDPOINT` is a single constant in `shared/types.ts`. + +### Challenges + +TODOs are numbered 1 to 7, not 1 to 10. Kotlin's TODOs 1 to 3 were annotations, which +have no TypeScript counterpart, and the Endpoint step is gone. + +| # | Slug | Format | Files touched | TODOs | +|---|------|--------|---------------|-------| +| 01 | run-the-monolith | observe | none | — | +| 02 | the-shared-contract | code | `shared/nexus-service.ts` | 1 | +| 03 | the-compliance-side | code | `compliance/nexus-handler.ts`, `compliance/worker.ts`, plus the Endpoint CLI | 2, 3, 4 | +| 04 | the-payments-side | code | `payments/workflows.ts`, `payments/worker.ts` | 5, 6, 7 | +| 05 | durability-and-human-review | observe | none | — | + +Tab order, the seven-tab layout, and the `tab-N`-is-a-position rule are identical to the +Kotlin track. Port 8090 is served by `node /opt/serve-diagrams.mjs` rather than +`jwebserver`, since there is no JDK in this image. + +### What the sandbox image bakes in + +`node:22-bookworm`, the Temporal CLI, code-server, both trees at `/opt/workshop` with +`node_modules` installed, and the diagram. Both trees are typechecked at build time, so +a broken port fails the image build rather than the workshop. + +Three deliberate differences from the Kotlin image: + +- **`memory: 4096`, not 8192.** That budget existed for two JVMs and a Gradle daemon. +- **No language extension is installed.** The Kotlin image pinned a syntax-only extension + to keep a language server from competing with two JVMs. Here the built-in TypeScript + service is wanted: it renders the challenge-2 compile error as a live red squiggle. +- **Track setup symlinks `node_modules` instead of copying it.** It is 292 MB per tree, + so copying both would move ~600 MB before the attendee can type. Consequence: + `npm install` inside the lab cannot write to `node_modules`. The workshop never needs + it. + +`.dockerignore` is load-bearing, not tidiness. The build context is `typescript/` and the +Dockerfile copies `decouple-monolith/`. Without the ignore the build ships ~600 MB of +host-built `node_modules` that are also the wrong architecture — the host is arm64, the +image amd64, and `@temporalio` ships a native core bridge. Context drops from 584 MB to +0.3 MB. + +### Publishing + +```bash +cd typescript/instruqt && instruqt track validate +``` + +Build for `linux/amd64`. An arm64 image fails to start on Instruqt with an empty log: + +```bash +cd typescript +docker buildx build --platform linux/amd64 \ + -f sandbox/Dockerfile -t ghcr.io/nadvolod/edu-nexus-typescript-sandbox:latest . +docker buildx imagetools inspect ghcr.io/nadvolod/edu-nexus-typescript-sandbox:latest \ + --format "{{.Manifest.Digest}}" +``` + +Same rule as Kotlin: **the image is pinned by digest, so any change under +`typescript/decouple-monolith/` requires a rebuild AND a re-pin.** Only changes under +`typescript/instruqt/` ship without one. + +**The GHCR package must be public.** A newly published package is private by default and +Instruqt pulls anonymously, which surfaces as `Could not find the image` with nothing +else to go on. Check it without credentials: + +```bash +tok=$(curl -s "https://ghcr.io/token?scope=repository:nadvolod/edu-nexus-typescript-sandbox:pull&service=ghcr.io" | jq -r .token) +curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $tok" \ + https://ghcr.io/v2/nadvolod/edu-nexus-typescript-sandbox/manifests/latest +``` + +200 is public, 403 is private. GitHub exposes no REST endpoint for package visibility, so +flipping it is a web-UI action. + +### TypeScript-specific gotchas + +- **`proxyActivities` silently types away synchronous Activities.** An Activity that is + not `async` becomes `NotAnActivityMethod`, and the failure appears at the *call site* in + the Workflow as `Type 'Symbol' has no call signatures` — which is verbatim the error the + SDK's own JSDoc quotes. Every Activity here is `async` for that reason. +- **A `setHandler` validator must take the same arguments as its handler.** A zero-arg + validator makes TypeScript infer `Args = []` and select the wrong `setHandler` overload, + producing an error that points at the definition rather than at the validator. +- **Workflow type names are camelCase in visibility queries.** Challenge 04's check queries + `WorkflowType="paymentProcessingWorkflow"`, the exported function name, not a class name. +- **Running the Kotlin and TypeScript workshops against one dev server does not work.** + Both poll `compliance-risk` in `compliance-namespace`. A stray Kotlin Worker will pick + up a TypeScript Workflow task and fail it with `Unknown workflow type + "complianceWorkflow". Known types are [ComplianceWorkflow]`, costing a Workflow-task + retry. Harmless in a sandbox, confusing locally. +- **`track.yml` and every `assignment.md` frontmatter are rewritten on publish**, exactly + as on the Kotlin track. Comments are stripped and keys reordered. Keep durable + explanation here. From 3af680d708b2fb38a62984835000da44600772f8 Mon Sep 17 00:00:00 2001 From: nadvolod <2136245+nadvolod@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:33:33 -0400 Subject: [PATCH 07/22] Make the workshop standalone: no references to other language versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An attendee of this workshop has not taken any other version of it, so telling them "this differs from the Java or Kotlin version" is noise at best and confusing at worst. Removes every such reference from attendee-facing material and from the infrastructure comments alongside it. Removed outright: - challenge 02: the paragraph contrasting the compile-time failure with a runtime "Missing handlers for service operations" - challenge 04: the whole section "A Note if You Have Taken the Java or Kotlin Version" Rewritten to stand on their own, keeping the teaching and dropping the comparison: - challenge 02 now says the contract is enforced at compile time, before anything runs - challenge 04 gains "Where the Endpoint Name Lives", which makes the boundary argument positively: the Workflow names a contract and an Endpoint and nothing else, and the Registry resolves the rest - TODO 5 in payments/workflows.ts, which a learner reads in the editor - shared/types.ts and payments/activities.ts in both trees - config.yml memory note, Dockerfile extension note, the diagram-server note in track_scripts/setup-workshop, and the comments in two lifecycle scripts The slide deck is updated in the same way in temporalio/temporal-devdays-ts. The README keeps its comparison deliberately: it documents a repo that contains java/, kotlin/ and typescript/ side by side, it is maintainer documentation no attendee reads, and the porting divergences are the reason that section exists. One sentence there was made false by this change — it claimed the assignments carry a note for people who have delivered another version — and is corrected, now stating the standalone rule so it survives the next edit. Both trees typecheck. Source changed, so the sandbox image was rebuilt and re-pinned: sha256:e86dbaffb0ad1dbfc17730cb2eafb4ac138b09d12946c94e9cce3ea2ee41c9e6 Verified in the pushed image itself: zero java/kotlin/jvm matches in payments/workflows.ts and shared/types.ts under /opt/workshop. Track republished in maintenance mode; all tab-N button references still resolve by label afterwards. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 7 ++++--- .../exercise/src/payments/activities.ts | 3 +-- .../exercise/src/payments/workflows.ts | 6 +++--- .../exercise/src/shared/types.ts | 5 ++--- .../solution/src/payments/activities.ts | 3 +-- .../solution/src/shared/types.ts | 5 ++--- .../02-the-shared-contract/assignment.md | 7 +++---- .../02-the-shared-contract/solve-workshop | 2 +- .../04-the-payments-side/assignment.md | 18 ++++++++---------- .../04-the-payments-side/check-workshop | 3 +-- typescript/instruqt/config.yml | 5 ++--- typescript/instruqt/track.yml | 2 +- .../instruqt/track_scripts/setup-workshop | 3 +-- typescript/sandbox/Dockerfile | 9 +++------ 14 files changed, 33 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 5a269ae..848d1e8 100644 --- a/README.md +++ b/README.md @@ -247,9 +247,10 @@ attendee discover it later. ### Three lessons that do not survive the port -These are real design differences between the SDKs, not translation choices. The -assignments call each one out, including a note aimed at anyone who has delivered the -Java or Kotlin version. +These are real design differences between the SDKs, not translation choices. Recorded +here for whoever maintains the port. **The workshop itself is standalone and never +mentions another language** — the assignments teach each of these on its own terms, so do +not reintroduce a comparison into attendee-facing material. - **Challenge 2 ends with a deliberately FAILING typecheck.** `ServiceHandlerFor` is a non-optional mapped type, so declaring an Operation obliges a matching handler at diff --git a/typescript/decouple-monolith/exercise/src/payments/activities.ts b/typescript/decouple-monolith/exercise/src/payments/activities.ts index 18c00df..ac67272 100644 --- a/typescript/decouple-monolith/exercise/src/payments/activities.ts +++ b/typescript/decouple-monolith/exercise/src/payments/activities.ts @@ -1,7 +1,6 @@ // [GIVEN] Payments team Activities. Thin wrappers around the gateway. // -// In TypeScript an Activity is just an exported function — there is no interface to -// declare and no annotation to apply. They must be async: proxyActivities only accepts +// An Activity is just an exported function. They must be async: proxyActivities only accepts // Promise-returning functions, and a synchronous one is silently typed away, surfacing // later as "Type 'Symbol' has no call signatures" at the call site in the Workflow. import * as gateway from './gateway'; diff --git a/typescript/decouple-monolith/exercise/src/payments/workflows.ts b/typescript/decouple-monolith/exercise/src/payments/workflows.ts index 7ee4797..f20a012 100644 --- a/typescript/decouple-monolith/exercise/src/payments/workflows.ts +++ b/typescript/decouple-monolith/exercise/src/payments/workflows.ts @@ -45,9 +45,9 @@ const { validatePayment, executePayment } = wf.proxyActivities/dev/null; then nohup node /opt/serve-diagrams.mjs > /tmp/diagrams.log 2>&1 & diff --git a/typescript/sandbox/Dockerfile b/typescript/sandbox/Dockerfile index 253000a..2be71e9 100644 --- a/typescript/sandbox/Dockerfile +++ b/typescript/sandbox/Dockerfile @@ -24,12 +24,9 @@ RUN curl -sSL \ # The native Instruqt `type: code` tab is "a simple code editor" with no # documented language support. code-server gives real VS Code. # -# Unlike the Kotlin image, NO language extension is installed here and that is -# deliberate, not an omission: TypeScript support is built into VS Code. The -# Kotlin image pinned a syntax-only extension specifically to avoid running a -# language server next to two JVMs. Here the language service is wanted — it is -# what surfaces the compile error in challenge 2 as a red squiggle, live, which -# is the lesson. Node leaves plenty of headroom for it. +# No language extension is installed, deliberately: TypeScript support is built +# into VS Code. The language service is wanted here — it is what surfaces the +# compile error in challenge 2 as a red squiggle, live, which is the lesson. ARG CODE_SERVER_VERSION=4.96.4 RUN curl -fsSL https://code-server.dev/install.sh | sh -s -- --version ${CODE_SERVER_VERSION} From ee3da0b49c22d82d61e82bde5a8734f12474f141 Mon Sep 17 00:00:00 2001 From: nadvolod <2136245+nadvolod@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:39:09 -0400 Subject: [PATCH 08/22] Take the TypeScript track out of maintenance mode Removes the `maintenance:` key entirely rather than setting it to false. The serializer omits false values, so `maintenance: false` would silently vanish on the next publish and read later as a forgotten setting. An absent key IS the off state, which is why kotlin/instruqt/track.yml has no such key either. Both tracks now have the same shape. Off is what a live workshop wants: the track is launchable by anyone with the link rather than by owners only. This is the committed intent. Applying it to the live track is a separate step that this repo's guardrail reserves for a human, and it cannot be done from an agent session: the hook refuses any command whose text contains the word it guards while a track is out of maintenance mode, which covers both the Instruqt publish and `git push`. Co-Authored-By: Claude Opus 5 (1M context) --- typescript/instruqt/track.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/typescript/instruqt/track.yml b/typescript/instruqt/track.yml index d550c36..9dda9b0 100644 --- a/typescript/instruqt/track.yml +++ b/typescript/instruqt/track.yml @@ -26,7 +26,6 @@ tags: owner: temporal developers: - nikolay.advolodkin@temporal.io -maintenance: true skipping_enabled: true idle_timeout: 3600 timelimit: 7200 From d5c9c225094e8700751143c59799d55803e830e7 Mon Sep 17 00:00:00 2001 From: nadvolod <2136245+nadvolod@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:44:36 -0400 Subject: [PATCH 09/22] Give checkCompliance as a worked example in challenge 3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit checkCompliance is the harder of the two Nexus Operations and the one whose shape carries the lesson. Asking a learner to invent WorkflowRunOperationHandler from a blank function, then invent the opposite shape immediately after, was two hard problems where one worked example plus one contrast does the teaching better. So checkCompliance is now written in the exercise tree, annotated to point at the parts submitReview will mirror: it wraps a function of (ctx, input), it calls startWorkflow, and the Workflow ID is business-meaningful so a retry re-attaches instead of starting a second check. The learner writes submitReview, which is the opposite shape on the same Service — fast work answers in the call, slow work starts a Workflow and returns a reference. TODOs renumbered 1 to 6 so there is no gap where the old TODO 2 was: 1 contract (unchanged, challenge 2) - checkCompliance handler now a worked example 2 submitReview handler was 3 3 nexusServices on the Worker was 4 4 Nexus client in the Workflow was 5 5 reviewCallerWorkflow was 6 6 delete the coupling was 7 Challenge 2's quoted compiler error is re-captured, not adjusted by hand. With one handler already present it now names exactly the missing one: Property 'submitReview' is missing in type '{ checkCompliance: WorkflowRunOperationHandler<...>; }' but required in type 'ServiceHandlerFor<...>'. which is a sharper version of the same lesson than the previous two-name error. Challenge 3's assignment is restructured to match: "Read the Worked Example First", then "Write the Other Handler". The shape table marks which Operation is given and which is the learner's. README's challenge table records the same. Both trees typecheck, instruqt track validate passes, all tab-N button references still resolve by label, and the track still contains no cross-language references. NOT YET APPLIED TO THE LIVE TRACK. Source under decouple-monolith/ changed, so the sandbox image must be rebuilt and re-pinned before this ships — the sandbox boots a pinned digest, so republishing the track alone would serve the old exercise code. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 13 ++- .../exercise/src/compliance/nexus-handler.ts | 79 +++++++++++++------ .../exercise/src/compliance/worker.ts | 4 +- .../exercise/src/payments/worker.ts | 4 +- .../exercise/src/payments/workflows.ts | 6 +- .../02-the-shared-contract/assignment.md | 16 ++-- .../03-the-compliance-side/assignment.md | 34 +++++--- .../04-the-payments-side/assignment.md | 6 +- 8 files changed, 103 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 848d1e8..b8f58be 100644 --- a/README.md +++ b/README.md @@ -279,15 +279,20 @@ not reintroduce a comparison into attendee-facing material. ### Challenges -TODOs are numbered 1 to 7, not 1 to 10. Kotlin's TODOs 1 to 3 were annotations, which -have no TypeScript counterpart, and the Endpoint step is gone. +Six TODOs, numbered 1 to 6, plus one worked example. + +`checkCompliance` in `compliance/nexus-handler.ts` is **given, not a TODO**. It is the +harder of the two Nexus Operations and the one whose shape carries the lesson, so a +learner reads it as a worked example and then writes `submitReview`, which is the +opposite shape on the same Service. That contrast is what challenge 3 is for, and it +lands better from a working example than from two blank functions. | # | Slug | Format | Files touched | TODOs | |---|------|--------|---------------|-------| | 01 | run-the-monolith | observe | none | — | | 02 | the-shared-contract | code | `shared/nexus-service.ts` | 1 | -| 03 | the-compliance-side | code | `compliance/nexus-handler.ts`, `compliance/worker.ts`, plus the Endpoint CLI | 2, 3, 4 | -| 04 | the-payments-side | code | `payments/workflows.ts`, `payments/worker.ts` | 5, 6, 7 | +| 03 | the-compliance-side | code | `compliance/nexus-handler.ts`, `compliance/worker.ts`, plus the Endpoint CLI | 2, 3 (+ worked example) | +| 04 | the-payments-side | code | `payments/workflows.ts`, `payments/worker.ts` | 4, 5, 6 | | 05 | durability-and-human-review | observe | none | — | Tab order, the seven-tab layout, and the `tab-N`-is-a-position rule are identical to the diff --git a/typescript/decouple-monolith/exercise/src/compliance/nexus-handler.ts b/typescript/decouple-monolith/exercise/src/compliance/nexus-handler.ts index de05de4..436f5d3 100644 --- a/typescript/decouple-monolith/exercise/src/compliance/nexus-handler.ts +++ b/typescript/decouple-monolith/exercise/src/compliance/nexus-handler.ts @@ -2,17 +2,21 @@ // shared/nexus-service.ts. This file is the code that answers those calls, and only // the Compliance team owns it. // -// This is the hardest file in the lab. Two things you will need: +// One Operation is written for you as a worked example. The other one is yours, and it +// is the opposite shape. Read the example first — the contrast between them is the +// whole point of this challenge. +// +// Two things you will need: // // temporalNexus.getClient() // A Temporal Client, already connected to this team's Namespace. // -// `compliance-${transactionId}` -// The Workflow ID to use. Same ID both times: one Operation creates that Workflow, -// the other looks up the same one. +// workflowIdFor(transactionId) +// The Workflow ID. The SAME id in both Operations: one creates that Workflow, the +// other looks up the one already running. // // Stuck? "Ask AI" on https://docs.temporal.io: -// "how do I start a Workflow from a Nexus Operation handler in TypeScript?" +// "how do I send a Workflow Update from a Nexus Operation handler in TypeScript?" import * as nexus from 'nexus-rpc'; import * as temporalNexus from '@temporalio/nexus'; import { complianceService } from '../shared/nexus-service'; @@ -23,30 +27,55 @@ import { complianceWorkflow, reviewUpdate } from './workflows'; const workflowIdFor = (transactionId: string) => `compliance-${transactionId}`; export const complianceServiceHandler = nexus.serviceHandler(complianceService, { - // ── TODO 2 ────────────────────────────────────────────────────────────────────── - // Implement checkCompliance. + // ── WORKED EXAMPLE — read this, then write TODO 2 below ────────────────────────── + // + // checkCompliance is the SLOW call. A risky payment has to be approved by a person, + // so an answer can be minutes or hours away. This Operation must not sit and wait. + // + // So it does not return an answer at all. It starts a Workflow and returns a + // REFERENCE to it. Temporal holds that reference and collects the result whenever the + // Workflow finishes, however long that takes. // - // This is the SLOW call. A risky payment has to be approved by a person, so an answer - // can be minutes or hours away. This Operation must not sit and wait for it. + // That is what WorkflowRunOperationHandler is for. Note the shape: // - // Instead: start a complianceWorkflow and return immediately, handing back a reference - // to it. Temporal holds that reference and collects the result whenever the Workflow - // finishes. Use `new temporalNexus.WorkflowRunOperationHandler(async (ctx, input) => ...)` - // and call `temporalNexus.startWorkflow(ctx, complianceWorkflow, { args, workflowId })` - // inside it. + // - it wraps a function of (ctx, input) + // - that function calls temporalNexus.startWorkflow(ctx, , { ... }) + // - the Workflow ID is business-meaningful, so a retried Operation re-attaches to + // the same compliance check instead of starting a second one // - // Do NOT write a plain async function here. That is a synchronous Operation: it is cut - // off after 10 seconds, and when Temporal retries the failed call it would start a - // SECOND compliance check for the same payment. + // A plain async function would NOT work here. That is a synchronous Operation: it is + // cut off at the 10-second handler deadline, and each retry would start another + // compliance check for the same payment. + checkCompliance: new temporalNexus.WorkflowRunOperationHandler( + async (ctx, input: ComplianceRequest) => + temporalNexus.startWorkflow(ctx, complianceWorkflow, { + args: [input], + workflowId: workflowIdFor(input.transactionId), + // Task queue defaults to the queue this Operation is handled on. + }), + ), - // ── TODO 3 ────────────────────────────────────────────────────────────────────── - // Implement submitReview. + // ── TODO 2 ────────────────────────────────────────────────────────────────────── + // Implement submitReview, the other half of the pattern. + // + // This is the FAST call, and it is the opposite shape to the example above. By the + // time it arrives, the compliance check is already running and parked waiting for a + // decision. All this Operation does is find that Workflow and hand it the yes or no. + // That takes milliseconds, so it can answer inside the call. + // + // Which means: NOT a WorkflowRunOperationHandler. A plain async function is right + // here. Slow work starts a Workflow and returns a reference; fast work answers in the + // call. Same Service, two Operations, two shapes. + // + // submitReview: async (_ctx, input: ReviewRequest): Promise => { + // // 1. get a Client with temporalNexus.getClient() + // // 2. get a handle to the running Workflow, using workflowIdFor(...) + // // 3. return await handle.executeUpdate(reviewUpdate, { args: [...] }) + // }, // - // This is the FAST call. By now the compliance check is already running and is parked - // waiting for a decision. All you do is find that Workflow and hand it the yes or no. - // That takes milliseconds, so a plain `async (ctx, input) => { ... }` is the right - // shape here. + // `reviewUpdate` is already imported at the top of this file. It takes two arguments, + // in this order: whether the reviewer approved, and their explanation. // - // Get a client with temporalNexus.getClient(), get a handle to the Workflow by its ID, - // then call executeUpdate with the `reviewUpdate` definition imported above. + // Until you write this, the build stays red — the contract in challenge 2 declared + // two Operations and this handler only answers one of them. }); diff --git a/typescript/decouple-monolith/exercise/src/compliance/worker.ts b/typescript/decouple-monolith/exercise/src/compliance/worker.ts index 92c8b11..49aaa56 100644 --- a/typescript/decouple-monolith/exercise/src/compliance/worker.ts +++ b/typescript/decouple-monolith/exercise/src/compliance/worker.ts @@ -1,7 +1,7 @@ // The Compliance team's Worker. This process is the only place their code runs. // // The connection and the Worker itself are set up for you. What is missing is the part -// that decides what this Worker can actually do: see TODO 4. +// that decides what this Worker can actually do: see TODO 3. import { NativeConnection, Worker } from '@temporalio/worker'; import * as activities from './activities'; import { complianceServiceHandler } from './nexus-handler'; @@ -19,7 +19,7 @@ async function run() { workflowsPath: require.resolve('./workflows'), activities, - // ── TODO 4 ──────────────────────────────────────────────────────────────── + // ── TODO 3 ──────────────────────────────────────────────────────────────── // A Worker only handles work it has been told about. This one already knows about // its Workflows (workflowsPath) and its Activities (activities). What it does not // yet know about is the Nexus handler — the thing that makes this team callable by diff --git a/typescript/decouple-monolith/exercise/src/payments/worker.ts b/typescript/decouple-monolith/exercise/src/payments/worker.ts index 6a3b1cb..5367d7e 100644 --- a/typescript/decouple-monolith/exercise/src/payments/worker.ts +++ b/typescript/decouple-monolith/exercise/src/payments/worker.ts @@ -7,7 +7,7 @@ // // That last line is the problem. A bug in compliance code takes payments down with it. // -// Do TODO 7 last, after TODO 5 has moved the Workflow onto a Nexus client. +// Do TODO 6 last, after TODO 4 has moved the Workflow onto a Nexus client. import { NativeConnection, Worker } from '@temporalio/worker'; import * as paymentActivities from './activities'; import * as complianceActivities from '../compliance/activities'; @@ -22,7 +22,7 @@ async function run() { taskQueue: PAYMENTS_TASK_QUEUE, workflowsPath: require.resolve('./workflows'), - // ── TODO 7 ──────────────────────────────────────────────────────────────── + // ── TODO 6 ──────────────────────────────────────────────────────────────── // Once the Workflow calls Compliance over Nexus, this Worker has no reason to run // the Compliance team's code. Remove `...complianceActivities` from the object // below, and delete the import at the top of this file. diff --git a/typescript/decouple-monolith/exercise/src/payments/workflows.ts b/typescript/decouple-monolith/exercise/src/payments/workflows.ts index f20a012..e74ac92 100644 --- a/typescript/decouple-monolith/exercise/src/payments/workflows.ts +++ b/typescript/decouple-monolith/exercise/src/payments/workflows.ts @@ -20,7 +20,7 @@ const { validatePayment, executePayment } = wf.proxyActivities { - throw new Error('TODO 6: submit the review decision through the Nexus Endpoint'); + throw new Error('TODO 5: submit the review decision through the Nexus Endpoint'); } diff --git a/typescript/instruqt/02-the-shared-contract/assignment.md b/typescript/instruqt/02-the-shared-contract/assignment.md index c8dc585..c06f725 100644 --- a/typescript/instruqt/02-the-shared-contract/assignment.md +++ b/typescript/instruqt/02-the-shared-contract/assignment.md @@ -95,15 +95,17 @@ npx tsc --noEmit **This is supposed to fail.** Read what it says: ```bash,nocopy -src/compliance/nexus-handler.ts(25,81): error TS2345: Argument of type '{}' is not -assignable to parameter of type 'ServiceHandlerFor<{ checkCompliance: ...; -submitReview: ...; }>'. - Type '{}' is missing the following properties from type - 'ServiceHandlerFor<...>': checkCompliance, submitReview +src/compliance/nexus-handler.ts(29,81): error TS2345: Argument of type +'{ checkCompliance: WorkflowRunOperationHandler<...>; }' is not assignable to +parameter of type 'ServiceHandlerFor<...>'. + Property 'submitReview' is missing in type + '{ checkCompliance: WorkflowRunOperationHandler<...>; }' but required in type + 'ServiceHandlerFor<...>'. ``` -You declared two Operations, and TypeScript immediately went looking for the two handlers -that answer them. It cannot find either, so it refuses to compile. +You declared two Operations, and TypeScript immediately went looking for the handlers +that answer them. One of them, `checkCompliance`, is already written for you. The other +is not, so it refuses to compile and names the one that is missing. That is the contract doing its job, and it is worth sitting with for a second. You have not run anything. No Worker has started. No call has been made. The compiler already knows diff --git a/typescript/instruqt/03-the-compliance-side/assignment.md b/typescript/instruqt/03-the-compliance-side/assignment.md index a64d0c5..afff50b 100644 --- a/typescript/instruqt/03-the-compliance-side/assignment.md +++ b/typescript/instruqt/03-the-compliance-side/assignment.md @@ -75,29 +75,37 @@ Three pieces, all on this team's side of the boundary. 2. The Worker that runs them. 3. The Endpoint that tells Temporal where to route. -# Write the Handlers +# Read the Worked Example First -Click the [button label="Exercise" background="#444CE7"](tab-0) tab, open -`compliance/nexus-handler.ts`, and follow TODO 2 and TODO 3. +Click the [button label="Exercise" background="#444CE7"](tab-0) tab and open +`compliance/nexus-handler.ts`. -Two Operations, and each one needs a different shape. Read both TODOs before you write -either one, because the difference between them is the whole point of this challenge. +`checkCompliance` is already written. Read it before you write anything, because the +Operation you are about to write is the other half of the same pattern, and the contrast +between the two is the whole point of this challenge. | Operation | Shape | Why | |---|---|---| -| `checkCompliance` | `new temporalNexus.WorkflowRunOperationHandler(...)` | Slow. Starts a Workflow and returns a reference to it. | -| `submitReview` | a plain `async (ctx, input) => {...}` | Fast. Answers inside the call, in milliseconds. | +| `checkCompliance` *(given)* | `new temporalNexus.WorkflowRunOperationHandler(...)` | Slow. Starts a Workflow and returns a reference to it. | +| `submitReview` *(yours)* | a plain `async (ctx, input) => {...}` | Fast. Answers inside the call, in milliseconds. | -Get `checkCompliance` wrong by writing it as a plain async function and two things break: -it is cut off at the ten second handler deadline, and a retry starts a **second** -compliance check for the same payment. +Notice why the given one has to be that shape. Written as a plain async function it would +be a synchronous Operation: cut off at the ten second handler deadline, and every retry +would start a **second** compliance check for the same payment. -As you fill each one in, the `TS2345` error from challenge 2 shrinks. When both are -written it disappears. +# Write the Other Handler + +Follow TODO 2 in the same file. + +`submitReview` is the fast path, so it is the plain async function. It gets a Client, +finds the Workflow the example started, and hands it the decision. Slow work starts a +Workflow and returns a reference; fast work answers in the call. + +When you have written it, the `TS2345` error from challenge 2 disappears. # Register the Handler -Open `compliance/worker.ts` and follow TODO 4. +Open `compliance/worker.ts` and follow TODO 3. The Worker already knows its Workflows and Activities. One option is missing: the one that makes this team callable by Payments at all. diff --git a/typescript/instruqt/04-the-payments-side/assignment.md b/typescript/instruqt/04-the-payments-side/assignment.md index 5ec2531..ad18189 100644 --- a/typescript/instruqt/04-the-payments-side/assignment.md +++ b/typescript/instruqt/04-the-payments-side/assignment.md @@ -70,7 +70,7 @@ enhanced_loading: null # Swap the Proxy for a Nexus Client Click the [button label="Exercise" background="#444CE7"](tab-0) tab, open -`payments/workflows.ts`, and follow TODO 5. +`payments/workflows.ts`, and follow TODO 4. The call site barely changes. Same operation name, same input, same result type. What changes is everything underneath it. @@ -103,7 +103,7 @@ is exactly one place to edit if it is ever renamed. # Write the Review Caller -Still in `payments/workflows.ts`, follow TODO 6. +Still in `payments/workflows.ts`, follow TODO 5. `submitReview` is a **synchronous** Operation, so the Compliance handler must finish inside the ten second handler deadline. Use a `scheduleToCloseTimeout` of `'10 seconds'` here, not @@ -113,7 +113,7 @@ Challenge 5 uses this. # Delete the Coupling -Open `payments/worker.ts` and follow TODO 7. +Open `payments/worker.ts` and follow TODO 6. Remove `...complianceActivities` from the `activities` object, and delete its import. From 54d39c8f3c8fd3f16cd376f6bc86c0af9429377c Mon Sep 17 00:00:00 2001 From: nadvolod <2136245+nadvolod@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:48:33 -0400 Subject: [PATCH 10/22] Rebuild and re-pin the sandbox image for the worked example The exercise tree changed when checkCompliance became a worked example, and the sandbox boots a pinned digest, so the previous image would have served attendees the old blank handler no matter what the track said. sha256:8c0d7059211849d86604b729a66eb4075e150f10d8389a738949b375964060fe Verified inside the pushed image rather than from the build log: worked example present in nexus-handler.ts yes checkCompliance implemented yes submitReview left as TODO 2 yes submitReview NOT implemented confirmed TODO numbers across the tree 1 2 3 4 5 6, contiguous npx tsc --noEmit inside the sandbox passes instruqt track validate passes with the new digest pinned. Co-Authored-By: Claude Opus 5 (1M context) --- typescript/instruqt/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/instruqt/config.yml b/typescript/instruqt/config.yml index 083d06d..8e63e7e 100644 --- a/typescript/instruqt/config.yml +++ b/typescript/instruqt/config.yml @@ -5,7 +5,7 @@ containers: # image that had no code-server in it, so the editor tabs showed "Please wait" # forever. Re-pin this after every image rebuild with: # docker buildx imagetools inspect ghcr.io/nadvolod/edu-nexus-typescript-sandbox:latest --format "{{.Manifest.Digest}}" - image: ghcr.io/nadvolod/edu-nexus-typescript-sandbox@sha256:e86dbaffb0ad1dbfc17730cb2eafb4ac138b09d12946c94e9cce3ea2ee41c9e6 + image: ghcr.io/nadvolod/edu-nexus-typescript-sandbox@sha256:8c0d7059211849d86604b729a66eb4075e150f10d8389a738949b375964060fe shell: /bin/bash # Sized for two Node Workers, a Temporal dev server, and one code-server. memory: 4096 From b5aa20c9c022b18ebf7cc7dc5d1cb1754a3f88b2 Mon Sep 17 00:00:00 2001 From: nadvolod <2136245+nadvolod@users.noreply.github.com> Date: Sun, 30 Aug 2026 14:04:15 -0400 Subject: [PATCH 11/22] Fix autosave and improve IntelliSense in the sandbox editor sha256:324b5db3f92d39d6b896dd5ff74fbcf431ba9ef7cff79225c3de89e4a9f5c732 files.autoSave was never set, while every assignment tells the learner "The editor saves as you type. There is no save button." That was simply false: VS Code defaults autoSave to off, so a learner edits a file, clicks the Terminal tab, runs the Worker and gets the OLD code back. It presents as "my fix did not work" and there is nothing on screen to explain it. Now afterDelay at 300ms, which makes the sentence true. The same gap exists in kotlin/instruqt with the same claim in its assignments. Not touched here. IntelliSense and code help: typescript.tsdk pinned to the workspace TypeScript (5.9.3) so editor diagnostics match npx tsc. This matters because challenge 2 quotes an exact TS2345 message and a version drift would make the quote wrong. disableAutomaticTypeAcquisition ATA fetches @types from npm and can stall tsserver in a sandbox. Every type needed is already in node_modules. inlay hints parameter names on literals, return types, property types. The Temporal API is unfamiliar; seeing args:/workflowId: inline is most of the value. importModuleSpecifier: relative matches how this codebase imports. Error Lens is installed from Open VSX. That is pedagogical rather than cosmetic: challenge 2 deliberately ends on a compile error and the lesson is that the contract is enforced before anything runs. An error the learner has to hover to read is a lesson half-delivered. The install is guarded with a warning fallback so a flaky registry cannot fail the image build. Ergonomics: sticky scroll on, minimap off (more code visible when projected), enablePreview off so single-click stops replacing the open tab, compactFolders off so src/payments does not collapse into one line. Deliberately NOT added: formatOnSave or Prettier. Reformatting half-written code mid-exercise is disorienting and would fight the TODO comment layout. The editor is still rooted at src/ rather than the project root. Verified rather than assumed: tsserver walks up past the workspace root and does find exercise/tsconfig.json, so strict, ES2022 and Node16 resolution are all in effect. Verified in the pushed image: autoSave present, tsserver.js present at the pinned tsdk path so the language service cannot fail to start, and errorlens listed by code-server --list-extensions. instruqt track validate passes with the new digest. Co-Authored-By: Claude Opus 5 (1M context) --- typescript/instruqt/config.yml | 2 +- typescript/sandbox/Dockerfile | 31 ++++++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/typescript/instruqt/config.yml b/typescript/instruqt/config.yml index 8e63e7e..a0ebfac 100644 --- a/typescript/instruqt/config.yml +++ b/typescript/instruqt/config.yml @@ -5,7 +5,7 @@ containers: # image that had no code-server in it, so the editor tabs showed "Please wait" # forever. Re-pin this after every image rebuild with: # docker buildx imagetools inspect ghcr.io/nadvolod/edu-nexus-typescript-sandbox:latest --format "{{.Manifest.Digest}}" - image: ghcr.io/nadvolod/edu-nexus-typescript-sandbox@sha256:8c0d7059211849d86604b729a66eb4075e150f10d8389a738949b375964060fe + image: ghcr.io/nadvolod/edu-nexus-typescript-sandbox@sha256:324b5db3f92d39d6b896dd5ff74fbcf431ba9ef7cff79225c3de89e4a9f5c732 shell: /bin/bash # Sized for two Node Workers, a Temporal dev server, and one code-server. memory: 4096 diff --git a/typescript/sandbox/Dockerfile b/typescript/sandbox/Dockerfile index 2be71e9..dcd6df4 100644 --- a/typescript/sandbox/Dockerfile +++ b/typescript/sandbox/Dockerfile @@ -46,10 +46,39 @@ RUN for d in /root/.cs-exercise /root/.cs-solution; do \ ' "workbench.startupEditor": "none",' \ ' "editor.fontSize": 14,' \ ' "editor.tabSize": 2,' \ - ' "telemetry.telemetryLevel": "off"' \ + ' "telemetry.telemetryLevel": "off",' \ + ' "files.autoSave": "afterDelay",' \ + ' "files.autoSaveDelay": 300,' \ + ' "typescript.tsdk": "/opt/workshop/exercise/node_modules/typescript/lib",' \ + ' "typescript.disableAutomaticTypeAcquisition": true,' \ + ' "typescript.inlayHints.parameterNames.enabled": "literals",' \ + ' "typescript.inlayHints.functionLikeReturnTypes.enabled": true,' \ + ' "typescript.inlayHints.propertyDeclarationTypes.enabled": true,' \ + ' "typescript.preferences.importModuleSpecifier": "relative",' \ + ' "javascript.suggestionActions.enabled": false,' \ + ' "editor.stickyScroll.enabled": true,' \ + ' "editor.minimap.enabled": false,' \ + ' "editor.inlayHints.fontSize": 11,' \ + ' "workbench.editor.enablePreview": false,' \ + ' "explorer.compactFolders": false,' \ + ' "errorLens.enabledDiagnosticLevels": ["error", "warning"],' \ + ' "errorLens.messageMaxChars": 0' \ '}' > "$d/User/settings.json"; \ done +# --- Editor extensions --------------------------------------------------------- +# Error Lens renders diagnostics inline at the end of the offending line instead of +# only underlining them. That is not decoration here: challenge 2 ends on a +# deliberate compile error, and the whole lesson is that the contract is enforced +# before anything runs. An error the learner has to hover to read is a lesson +# half-delivered. +# +# code-server resolves extensions from Open VSX, not the VS Code marketplace. +# Installed into the default extensions dir so the runtime invocation needs no +# --extensions-dir flag. +RUN code-server --install-extension usernamehw.errorlens || \ + echo "==> WARNING: Error Lens did not install; diagnostics will still underline, just not inline." + # --- Architecture diagram ----------------------------------------------------- # Lives outside the challenge folders on purpose: Instruqt parses every file # inside a challenge directory as a lifecycle script. From e210ca648cf44d1319e1000299f48e9ef3fa5fd6 Mon Sep 17 00:00:00 2001 From: nadvolod <2136245+nadvolod@users.noreply.github.com> Date: Sun, 30 Aug 2026 14:08:31 -0400 Subject: [PATCH 12/22] Sync track checksum after publishing live Checksum writeback from the publish that took the track out of maintenance mode. Committed deliberately: the checksum is how the CLI detects drift, and leaving a stale one committed makes the next publish report "There are remote changes for this track" and demand --force for no real reason. The README warns against committing checksum-only writeback FROM CI, where it would produce a commit on every merge. This is a one-off after a manual publish. Verified: pulling the track into a throwaway copy reports "Track is up to date" with no .remote files, so local and remote match exactly. Co-Authored-By: Claude Opus 5 (1M context) --- typescript/instruqt/track.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/instruqt/track.yml b/typescript/instruqt/track.yml index 9dda9b0..0e420f0 100644 --- a/typescript/instruqt/track.yml +++ b/typescript/instruqt/track.yml @@ -51,5 +51,5 @@ lab_config: hideStopButton: false default_layout: AssignmentLeft default_layout_sidebar_size: 33 -checksum: "13288812472741815799" +checksum: "14867367867617189478" enhanced_loading: false From 6d3836dc9d214de3bb1f89454d18634c9aeefbe8 Mon Sep 17 00:00:00 2001 From: nadvolod <2136245+nadvolod@users.noreply.github.com> Date: Sun, 30 Aug 2026 14:27:39 -0400 Subject: [PATCH 13/22] Explain the TS7006 a learner hits when the contract is empty sha256:079c495e5ba873fa16e8f27a9736d75ee0e0d632e2e3bcda6cfbea8030f445ed Reported from a live sandbox: writing submitReview exactly as the TODO 2 skeleton shows it fails with error TS7006: Parameter '_ctx' implicitly has an 'any' type. _ctx is a red herring. Reproduced and confirmed in both directions: contract empty ServiceHandlerFor<{}> gives no contextual type, so the parameter falls back to any and strict rejects it contract declared the identical code compiles, exit 0 So the cause is TODO 1 being unfinished, and the error names a file that is not where the fix goes. A learner can reach that state legitimately, because challenge 2's check is exit 0 and clicking Check without doing TODO 1 passes. Fixed by explaining it where it is actually seen, in both places: - above TODO 2 in nexus-handler.ts, the file open in the editor - under the typecheck step in challenge 3's assignment, quoting the exact error Written as a lesson rather than a warning, because it demonstrates the point the workshop is making: handler parameter types are inferred FROM the contract, so the contract is not documentation sitting beside the code. Delete it and the handlers stop type checking. Deliberately NOT changed: challenge 2's check is still exit 0. Giving it a diagnostic would catch this at the right moment and would match the exception already made for challenge 4, whose failure mode is also silent. It is left alone because instant-pass checks are a documented, deliberate policy in the README, and gating challenge 2 nudges a stuck learner toward Skip, which copies solution files over their work. Verified in the pushed image: the guidance is present in the file the learner opens and names shared/nexus-service.ts as the fix; autosave and Error Lens from the previous build survive; the exercise typechecks inside the sandbox. Co-Authored-By: Claude Opus 5 (1M context) --- .../exercise/src/compliance/nexus-handler.ts | 14 ++++++++++++-- .../03-the-compliance-side/assignment.md | 17 +++++++++++++++++ typescript/instruqt/config.yml | 2 +- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/typescript/decouple-monolith/exercise/src/compliance/nexus-handler.ts b/typescript/decouple-monolith/exercise/src/compliance/nexus-handler.ts index 436f5d3..3030b9c 100644 --- a/typescript/decouple-monolith/exercise/src/compliance/nexus-handler.ts +++ b/typescript/decouple-monolith/exercise/src/compliance/nexus-handler.ts @@ -76,6 +76,16 @@ export const complianceServiceHandler = nexus.serviceHandler(complianceService, // `reviewUpdate` is already imported at the top of this file. It takes two arguments, // in this order: whether the reviewer approved, and their explanation. // - // Until you write this, the build stays red — the contract in challenge 2 declared - // two Operations and this handler only answers one of them. + // Until you write this, the build stays red — the contract declared two Operations and + // this handler only answers one of them. + // + // One thing to know before you start. You do not annotate the handler's parameters: + // their types come from the contract, which is what makes the Service a contract at + // all. So if TODO 1 is still unfinished, the contract is empty, TypeScript has nothing + // to infer from, and you get: + // + // error TS7006: Parameter '_ctx' implicitly has an 'any' type. + // + // That error points at this file, but the fix is in shared/nexus-service.ts. Finish + // TODO 1 first. }); diff --git a/typescript/instruqt/03-the-compliance-side/assignment.md b/typescript/instruqt/03-the-compliance-side/assignment.md index afff50b..1c91d86 100644 --- a/typescript/instruqt/03-the-compliance-side/assignment.md +++ b/typescript/instruqt/03-the-compliance-side/assignment.md @@ -121,6 +121,23 @@ npx tsc --noEmit Silence means it passed. The Payments side still calls the Activity proxy, which is perfectly valid — you change that in challenge 4. +### If you see an error about `_ctx` + +```bash,nocopy +error TS7006: Parameter '_ctx' implicitly has an 'any' type. +``` + +That error points at `nexus-handler.ts`, but the fix is in `shared/nexus-service.ts`. + +You never annotate a handler's parameter types — they are inferred **from the contract**. +So if TODO 1 from challenge 2 is unfinished, the Service has no Operations, there is +nothing to infer from, and the parameter falls back to `any`, which `strict` rejects. + +Go back and declare the two Operations. Then this file compiles unchanged. + +It is worth noticing what just happened: the contract is not documentation sitting beside +the code. Delete it and the handlers stop type checking. + # Create the Endpoint The Endpoint is the routing rule: a name, a target Namespace, and a target Task Queue. diff --git a/typescript/instruqt/config.yml b/typescript/instruqt/config.yml index a0ebfac..e03e764 100644 --- a/typescript/instruqt/config.yml +++ b/typescript/instruqt/config.yml @@ -5,7 +5,7 @@ containers: # image that had no code-server in it, so the editor tabs showed "Please wait" # forever. Re-pin this after every image rebuild with: # docker buildx imagetools inspect ghcr.io/nadvolod/edu-nexus-typescript-sandbox:latest --format "{{.Manifest.Digest}}" - image: ghcr.io/nadvolod/edu-nexus-typescript-sandbox@sha256:324b5db3f92d39d6b896dd5ff74fbcf431ba9ef7cff79225c3de89e4a9f5c732 + image: ghcr.io/nadvolod/edu-nexus-typescript-sandbox@sha256:079c495e5ba873fa16e8f27a9736d75ee0e0d632e2e3bcda6cfbea8030f445ed shell: /bin/bash # Sized for two Node Workers, a Temporal dev server, and one code-server. memory: 4096 From 685b2b6882789de5c129e9f49b868cfedfcc828f Mon Sep 17 00:00:00 2001 From: nadvolod <2136245+nadvolod@users.noreply.github.com> Date: Sun, 30 Aug 2026 14:31:26 -0400 Subject: [PATCH 14/22] Explain the Go system Worker attendees see in the Workers list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported from a live sandbox: a Go Worker appears in compliance-namespace on task queue temporal-sys-per-ns-tq, and nothing in the workshop explains it. It is not ours. Verified in the sandbox image with NO workshop Worker started at all: temporal-sys-per-ns-tq already has activity and workflow pollers with identity temporal-system@@compliance-namespace, and the only non-shell process in the container is `temporal server start-dev`. Temporal Server is written in Go and runs an internal Worker in every Namespace for its own background work, so it is present from the moment the dev server starts and sits at Tasks Processed 0. Two assignment claims were wrong because of it: challenge 3 "Your Worker is listed as Running" — there are TWO Workers listed and no way given to tell them apart. Now a table: compliance-risk + TypeScript is theirs, temporal-sys-per-ns-tq + Go is Temporal's, with the rule that anything on temporal-sys- is the system talking to itself. challenge 3 "In challenge 1 this Namespace was empty" — it was not, in the Workers view. Now says there was no Worker of THEIRS here. challenge 1 "Empty. ... nothing runs there" — true of Workflows, false of Workers. Now says "No Workflows", and points out the Go Worker before the attendee finds it and assumes something is wrong. Assignment text only, so no image rebuild is needed. instruqt track validate passes and all tab-N button references still resolve by label. Co-Authored-By: Claude Opus 5 (1M context) --- .../01-run-the-monolith/assignment.md | 9 ++++++-- .../03-the-compliance-side/assignment.md | 21 +++++++++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/typescript/instruqt/01-run-the-monolith/assignment.md b/typescript/instruqt/01-run-the-monolith/assignment.md index 6ead589..91aace8 100644 --- a/typescript/instruqt/01-run-the-monolith/assignment.md +++ b/typescript/instruqt/01-run-the-monolith/assignment.md @@ -206,8 +206,13 @@ The diagram claims Compliance owns a Namespace with nothing in it. Confirm it. Click the [button label="Temporal UI" background="#444CE7"](tab-1) tab and switch the Namespace selector to `compliance-namespace`. -Empty. Compliance has a Namespace of its own, and nothing runs there, because their code -is executing inside the Payments Worker. +No Workflows. Compliance has a Namespace of its own and nothing of theirs runs in it, +because their code is executing inside the Payments Worker. + +If you look under **Workers** you will see one Go Worker on a `temporal-sys-` Task Queue. +Ignore it — Temporal Server is written in Go and runs an internal Worker in every +Namespace for its own background work. No Worker of the Compliance team's exists yet. +You start one in challenge 3. Switch back to `payments-namespace`, open `payment-TXN-C` again, and read the Event History. Find **Activity Task Scheduled** for the compliance check. That single event is diff --git a/typescript/instruqt/03-the-compliance-side/assignment.md b/typescript/instruqt/03-the-compliance-side/assignment.md index 1c91d86..d2af4f9 100644 --- a/typescript/instruqt/03-the-compliance-side/assignment.md +++ b/typescript/instruqt/03-the-compliance-side/assignment.md @@ -181,11 +181,24 @@ which is exactly the kind of thing that is easy to miss — so look for it delib **Two: the Temporal UI.** Click the [button label="Temporal UI" background="#444CE7"](tab-1) tab, switch the Namespace -selector to `compliance-namespace`, and open **Workers** in the left menu. Your Worker -is listed as **Running**. +selector to `compliance-namespace`, and open **Workers** in the left menu. -That is Compliance appearing in its own Namespace for the first time. In challenge 1 -this Namespace was empty. +You will see **two** Workers, and only one of them is yours: + +| Task Queue | SDK | Whose | +|---|---|---| +| `compliance-risk` | TypeScript | **yours** — the one you just started | +| `temporal-sys-per-ns-tq` | Go | Temporal's own | + +The Go one is not something you started and not something you can break. Temporal Server +is written in Go and runs an internal Worker in **every** Namespace to handle its own +background work — batch operations, schedules, deletion. It was there before you began +and it will show `Tasks Processed: 0` all day. Anything on `temporal-sys-` is the system +talking to itself. + +Yours is the one on `compliance-risk`, listed as **Running**. That is Compliance running +in its own Namespace for the first time — in challenge 1 there was no Worker of yours +here at all. If something went wrong: From 53f92fa43f2225bfaeb3161d845fd1a929c697a5 Mon Sep 17 00:00:00 2001 From: nadvolod <2136245+nadvolod@users.noreply.github.com> Date: Sun, 30 Aug 2026 14:34:52 -0400 Subject: [PATCH 15/22] Sync track checksum after publishing the assignment fixes Checksum writeback from the publish that shipped the TS7006 guidance and the Go system Worker explanation. Committed so the next publish does not report "There are remote changes for this track" against a stale value. Verified: pulling into a throwaway copy reports the track up to date with no .remote files. Co-Authored-By: Claude Opus 5 (1M context) --- typescript/instruqt/track.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/instruqt/track.yml b/typescript/instruqt/track.yml index 0e420f0..d41bb59 100644 --- a/typescript/instruqt/track.yml +++ b/typescript/instruqt/track.yml @@ -51,5 +51,5 @@ lab_config: hideStopButton: false default_layout: AssignmentLeft default_layout_sidebar_size: 33 -checksum: "14867367867617189478" +checksum: "7199110355914590725" enhanced_loading: false From 8cc66aea7fab6d82639fe02a562f203469c34a0c Mon Sep 17 00:00:00 2001 From: nadvolod <2136245+nadvolod@users.noreply.github.com> Date: Sun, 30 Aug 2026 16:10:18 -0400 Subject: [PATCH 16/22] Merge the Exercise and Solution tabs into one code tab sha256:cb82e47fb3f5874d53685f622282aac85d0214d49b8dc9a52000cfb5042cb8fd One editor tab, rooted at /root/workshop, so exercise/ and solution/ are in the same file tree. Six tabs now, not seven. This also removes a known flake rather than documenting it. Instruqt loads every service tab's iframe at challenge start including hidden ones, and code-server cannot lay itself out in a 0x0 iframe. That is why INSTRUQT.md says the Kotlin track's Solution tab "may still need one refresh". With one editor instance and one visible iframe there is no second iframe to race. Four things had to move with it, three of which would have failed silently: tab-N is a POSITION. Dropping Solution at index 5 moved Monolith Architecture from tab-6 to tab-5. No button pointed at Solution, so exactly one reference needed remapping; all 24 were re-verified by label afterwards. Source paths were ambiguous the moment solution/ became visible: "open payments/workflows.ts" now matches two files, and a learner editing the solution copy would watch their changes do nothing. All 11 references in the assignments are now full paths, exercise/src/payments/workflows.ts. track_scripts/setup-workshop probes the editor for a 200 on /?folder=$EX_FOLDER to decide it is ready. EX_FOLDER still pointed at exercise/src, which is no longer a URL any tab opens. Now /root/workshop. Challenge 3's note told learners "The Solution tab is one click away", a tab that no longer exists. Now points at solution/ in the file tree. The new root exposes package.json, tsconfig.json, lockfiles and the node_modules symlinks, so files.exclude hides node_modules and lockfiles. That is the change that required the image rebuild. Trade-off worth recording: source is two levels deeper than before, since the root is the workshop directory rather than exercise/src. Judged worth it for a single code tab plus the removed flake, but it is a real cost to a learner navigating under time pressure. Verified in the pushed image: files.exclude present, autosave and pinned tsdk and Error Lens all survive, both project roots present. instruqt track validate passes and every tab-N button still resolves by label across all five challenges. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 30 +++++++++++++++++-- .../01-run-the-monolith/assignment.md | 14 +++------ .../02-the-shared-contract/assignment.md | 10 ++----- .../03-the-compliance-side/assignment.md | 20 +++++-------- .../04-the-payments-side/assignment.md | 16 ++++------ .../assignment.md | 8 +---- typescript/instruqt/config.yml | 2 +- .../instruqt/track_scripts/setup-workshop | 2 +- typescript/sandbox/Dockerfile | 1 + 9 files changed, 49 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index b8f58be..bb965f6 100644 --- a/README.md +++ b/README.md @@ -295,9 +295,33 @@ lands better from a working example than from two blank functions. | 04 | the-payments-side | code | `payments/workflows.ts`, `payments/worker.ts` | 4, 5, 6 | | 05 | durability-and-human-review | observe | none | — | -Tab order, the seven-tab layout, and the `tab-N`-is-a-position rule are identical to the -Kotlin track. Port 8090 is served by `node /opt/serve-diagrams.mjs` rather than -`jwebserver`, since there is no JDK in this image. +### Tabs + +Six, not seven. There is **one** editor tab, rooted at `/root/workshop` so both project +folders are in the same file tree: + +| Index | Tab | Type | +|-------|-----|------| +| tab-0 | Exercise | service, port 8080 (code-server, shows `exercise/` and `solution/`) | +| tab-1 | Temporal UI | service, port 8233 | +| tab-2 | Terminal | terminal | +| tab-3 | Payments Worker | terminal | +| tab-4 | Compliance Worker | terminal | +| tab-5 | Monolith Architecture | service, port 8090 (`node /opt/serve-diagrams.mjs`) | + +A separate Solution tab was removed on purpose. Instruqt loads every service tab's iframe +at challenge start including hidden ones, and code-server cannot lay itself out in a 0x0 +iframe — which is why the Kotlin track's Solution tab "may still need one refresh". One +editor instance with one visible iframe removes that failure mode rather than documenting +it. + +The cost is that source paths are now two levels deeper, and `solution/` is visible while +a learner works. Both are handled by writing paths in full: assignments say +`exercise/src/payments/workflows.ts`, never `payments/workflows.ts`, so nobody edits the +solution copy by accident. `node_modules` and lockfiles are hidden via `files.exclude`. + +The `tab-N`-is-a-position rule still applies: these are zero-indexed positions, not ids, +so reordering means remapping every button. ### What the sandbox image bakes in diff --git a/typescript/instruqt/01-run-the-monolith/assignment.md b/typescript/instruqt/01-run-the-monolith/assignment.md index 91aace8..d1de29f 100644 --- a/typescript/instruqt/01-run-the-monolith/assignment.md +++ b/typescript/instruqt/01-run-the-monolith/assignment.md @@ -27,7 +27,7 @@ tabs: title: Exercise type: service hostname: workshop - path: /?folder=/root/workshop/exercise/src + path: /?folder=/root/workshop port: 8080 - id: osmynelncext title: Temporal UI @@ -50,12 +50,6 @@ tabs: type: terminal hostname: workshop workdir: /root/workshop/exercise -- id: bwvwdih1vjnt - title: Solution - type: service - hostname: workshop - path: /?folder=/root/workshop/solution/src - port: 8080 - id: kaxawqn9yd2g title: Monolith Architecture type: service @@ -153,7 +147,7 @@ look exactly like this. Before you hunt for the coupling in code, look at the shape of what you just ran. -Click the [button label="Monolith Architecture" background="#444CE7"](tab-6) tab. +Click the [button label="Monolith Architecture" background="#444CE7"](tab-5) tab. Two shaded zones, one per team. Payments is blue, Compliance is amber. Every box carries its file name and the function that matters. @@ -177,7 +171,7 @@ box inside that outline is polled by the same Worker. # Find the Coupling Click the [button label="Exercise" background="#444CE7"](tab-0) tab and open -`payments/worker.ts`. +`exercise/src/payments/worker.ts`. Look at what this one Worker registers. One object, two different teams: @@ -191,7 +185,7 @@ the same blast radius. Compliance cannot ship a fix without Payments shipping to That second spread is the one you delete in challenge 4. That deletion is the decoupling. -Now open `payments/workflows.ts` and find step 2: +Now open `exercise/src/payments/workflows.ts` and find step 2: ```ts,nocopy const result: ComplianceResult = await checkCompliance(compReq); diff --git a/typescript/instruqt/02-the-shared-contract/assignment.md b/typescript/instruqt/02-the-shared-contract/assignment.md index c06f725..5e11b00 100644 --- a/typescript/instruqt/02-the-shared-contract/assignment.md +++ b/typescript/instruqt/02-the-shared-contract/assignment.md @@ -27,7 +27,7 @@ tabs: title: Exercise type: service hostname: workshop - path: /?folder=/root/workshop/exercise/src + path: /?folder=/root/workshop port: 8080 - id: 7iygieqqyuko title: Temporal UI @@ -50,12 +50,6 @@ tabs: type: terminal hostname: workshop workdir: /root/workshop/exercise -- id: 4m8yg9ygjnkm - title: Solution - type: service - hostname: workshop - path: /?folder=/root/workshop/solution/src - port: 8080 - id: 1wdriwhm5mby title: Monolith Architecture type: service @@ -77,7 +71,7 @@ It lives in `shared/` on purpose. Neither team owns it alone. # Write It Click the [button label="Exercise" background="#444CE7"](tab-0) tab, open -`shared/nexus-service.ts`, and follow TODO 1. +`exercise/src/shared/nexus-service.ts`, and follow TODO 1. Two Operations. Each is a name paired with `nexus.operation()`, which carries no implementation — only the types both teams agree on. diff --git a/typescript/instruqt/03-the-compliance-side/assignment.md b/typescript/instruqt/03-the-compliance-side/assignment.md index d2af4f9..1c78f8c 100644 --- a/typescript/instruqt/03-the-compliance-side/assignment.md +++ b/typescript/instruqt/03-the-compliance-side/assignment.md @@ -20,14 +20,14 @@ notes: Two handlers, two different shapes, plus the Worker registration and the Endpoint. - The Solution tab is one click away. Use it if you stall. Learning the shape - beats staring at a blank function. + The finished code sits in solution/ in the same file tree. Use it if you + stall. Learning the shape beats staring at a blank function. tabs: - id: gok4feicejlo title: Exercise type: service hostname: workshop - path: /?folder=/root/workshop/exercise/src + path: /?folder=/root/workshop port: 8080 - id: rhie9uzgyhfq title: Temporal UI @@ -50,12 +50,6 @@ tabs: type: terminal hostname: workshop workdir: /root/workshop/exercise -- id: xp7aoiijy8bj - title: Solution - type: service - hostname: workshop - path: /?folder=/root/workshop/solution/src - port: 8080 - id: ujvrlheqjxnv title: Monolith Architecture type: service @@ -78,7 +72,7 @@ Three pieces, all on this team's side of the boundary. # Read the Worked Example First Click the [button label="Exercise" background="#444CE7"](tab-0) tab and open -`compliance/nexus-handler.ts`. +`exercise/src/compliance/nexus-handler.ts`. `checkCompliance` is already written. Read it before you write anything, because the Operation you are about to write is the other half of the same pattern, and the contrast @@ -105,7 +99,7 @@ When you have written it, the `TS2345` error from challenge 2 disappears. # Register the Handler -Open `compliance/worker.ts` and follow TODO 3. +Open `exercise/src/compliance/worker.ts` and follow TODO 3. The Worker already knows its Workflows and Activities. One option is missing: the one that makes this team callable by Payments at all. @@ -127,7 +121,7 @@ perfectly valid — you change that in challenge 4. error TS7006: Parameter '_ctx' implicitly has an 'any' type. ``` -That error points at `nexus-handler.ts`, but the fix is in `shared/nexus-service.ts`. +That error points at `nexus-handler.ts`, but the fix is in `exercise/src/shared/nexus-service.ts`. You never annotate a handler's parameter types — they are inferred **from the contract**. So if TODO 1 from challenge 2 is unfinished, the Service has no Operations, there is @@ -147,7 +141,7 @@ It is the one piece of Nexus that lives outside your code. temporal operator nexus endpoint create --name compliance-endpoint --target-namespace compliance-namespace --target-task-queue compliance-risk ``` -`--target-task-queue` must match `COMPLIANCE_TASK_QUEUE` in `shared/types.ts` exactly. +`--target-task-queue` must match `COMPLIANCE_TASK_QUEUE` in `exercise/src/shared/types.ts` exactly. If it points at a queue no Worker is polling, the calls are never answered. # Start the Worker diff --git a/typescript/instruqt/04-the-payments-side/assignment.md b/typescript/instruqt/04-the-payments-side/assignment.md index ad18189..c59597b 100644 --- a/typescript/instruqt/04-the-payments-side/assignment.md +++ b/typescript/instruqt/04-the-payments-side/assignment.md @@ -27,7 +27,7 @@ tabs: title: Exercise type: service hostname: workshop - path: /?folder=/root/workshop/exercise/src + path: /?folder=/root/workshop port: 8080 - id: xrdqvojuovik title: Temporal UI @@ -50,12 +50,6 @@ tabs: type: terminal hostname: workshop workdir: /root/workshop/exercise -- id: xpohqyhbtdz3 - title: Solution - type: service - hostname: workshop - path: /?folder=/root/workshop/solution/src - port: 8080 - id: slczt2wyuij1 title: Monolith Architecture type: service @@ -70,7 +64,7 @@ enhanced_loading: null # Swap the Proxy for a Nexus Client Click the [button label="Exercise" background="#444CE7"](tab-0) tab, open -`payments/workflows.ts`, and follow TODO 4. +`exercise/src/payments/workflows.ts`, and follow TODO 4. The call site barely changes. Same operation name, same input, same result type. What changes is everything underneath it. @@ -98,12 +92,12 @@ That is the boundary. An Endpoint is a name the Registry resolves into an addres Compliance to a different Namespace or a different Task Queue tomorrow, update the Endpoint, and not one line of this Workflow changes. -The name itself is a single constant, `COMPLIANCE_ENDPOINT` in `shared/types.ts`, so there +The name itself is a single constant, `COMPLIANCE_ENDPOINT` in `exercise/src/shared/types.ts`, so there is exactly one place to edit if it is ever renamed. # Write the Review Caller -Still in `payments/workflows.ts`, follow TODO 5. +Still in `exercise/src/payments/workflows.ts`, follow TODO 5. `submitReview` is a **synchronous** Operation, so the Compliance handler must finish inside the ten second handler deadline. Use a `scheduleToCloseTimeout` of `'10 seconds'` here, not @@ -113,7 +107,7 @@ Challenge 5 uses this. # Delete the Coupling -Open `payments/worker.ts` and follow TODO 6. +Open `exercise/src/payments/worker.ts` and follow TODO 6. Remove `...complianceActivities` from the `activities` object, and delete its import. diff --git a/typescript/instruqt/05-durability-and-human-review/assignment.md b/typescript/instruqt/05-durability-and-human-review/assignment.md index e0f0717..0876869 100644 --- a/typescript/instruqt/05-durability-and-human-review/assignment.md +++ b/typescript/instruqt/05-durability-and-human-review/assignment.md @@ -27,7 +27,7 @@ tabs: title: Exercise type: service hostname: workshop - path: /?folder=/root/workshop/exercise/src + path: /?folder=/root/workshop port: 8080 - id: kirogybfummw title: Temporal UI @@ -50,12 +50,6 @@ tabs: type: terminal hostname: workshop workdir: /root/workshop/exercise -- id: rynbymolm35m - title: Solution - type: service - hostname: workshop - path: /?folder=/root/workshop/solution/src - port: 8080 - id: dwrcskq7wzji title: Monolith Architecture type: service diff --git a/typescript/instruqt/config.yml b/typescript/instruqt/config.yml index e03e764..e7831bd 100644 --- a/typescript/instruqt/config.yml +++ b/typescript/instruqt/config.yml @@ -5,7 +5,7 @@ containers: # image that had no code-server in it, so the editor tabs showed "Please wait" # forever. Re-pin this after every image rebuild with: # docker buildx imagetools inspect ghcr.io/nadvolod/edu-nexus-typescript-sandbox:latest --format "{{.Manifest.Digest}}" - image: ghcr.io/nadvolod/edu-nexus-typescript-sandbox@sha256:079c495e5ba873fa16e8f27a9736d75ee0e0d632e2e3bcda6cfbea8030f445ed + image: ghcr.io/nadvolod/edu-nexus-typescript-sandbox@sha256:cb82e47fb3f5874d53685f622282aac85d0214d49b8dc9a52000cfb5042cb8fd shell: /bin/bash # Sized for two Node Workers, a Temporal dev server, and one code-server. memory: 4096 diff --git a/typescript/instruqt/track_scripts/setup-workshop b/typescript/instruqt/track_scripts/setup-workshop index c664b36..0e7f517 100755 --- a/typescript/instruqt/track_scripts/setup-workshop +++ b/typescript/instruqt/track_scripts/setup-workshop @@ -135,7 +135,7 @@ done # So we check the two things that ARE true only once the workbench is up: the # exact URL a tab opens returns 200 (bare "/" still 302s at that point), and # code-server logs that the extension host is running. -EX_FOLDER=/root/workshop/exercise/src +EX_FOLDER=/root/workshop code_server_ready() { [ "$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 \ diff --git a/typescript/sandbox/Dockerfile b/typescript/sandbox/Dockerfile index dcd6df4..3c6ac0a 100644 --- a/typescript/sandbox/Dockerfile +++ b/typescript/sandbox/Dockerfile @@ -61,6 +61,7 @@ RUN for d in /root/.cs-exercise /root/.cs-solution; do \ ' "editor.inlayHints.fontSize": 11,' \ ' "workbench.editor.enablePreview": false,' \ ' "explorer.compactFolders": false,' \ + ' "files.exclude": { "**/node_modules": true, "**/package-lock.json": true },' \ ' "errorLens.enabledDiagnosticLevels": ["error", "warning"],' \ ' "errorLens.messageMaxChars": 0' \ '}' > "$d/User/settings.json"; \ From 046db2dd926cc9e83f1f27102f63dbcc4a909532 Mon Sep 17 00:00:00 2001 From: nadvolod <2136245+nadvolod@users.noreply.github.com> Date: Sun, 30 Aug 2026 16:25:14 -0400 Subject: [PATCH 17/22] Merge TODOs 4 and 5, and cut the TODO comments back MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sha256:417c2f518ef4b5ecf76a8ea588b05e699e01f6d975d501458fb5d5de32a092fb Three complaints, all fair: TODO 4's instructions sat at line 23, above the proxy, while the code they described went at lines 54, 72 and 85. You read the whole block, scrolled away, and lost it. TODO 4 and TODO 5 wrote nearly identical code — createNexusServiceClient plus executeOperation — at opposite ends of the file, which reads as duplicated busywork rather than a contrast. Both were far too verbose. Thirty lines of comment for two lines of code. Now one TODO 4 with parts (a) and (b), stated once, immediately above the call it changes. The duplication becomes the point rather than an accident: same client twice, 'checkCompliance' at '10 minutes' and 'submitReview' at '10 seconds'. Async Operations budget for the whole call including retries; sync Operations must answer inside the handler deadline. One Service, two Operations, two budgets. The old proxy keeps a two-line pointer instead of the essay. workflows.ts is 114 lines, down from 127, with more of it being code. TODOs renumbered 1 to 5: the coupling deletion in payments/worker.ts was 6. reviewCallerWorkflow could NOT simply be given away, which is what prompted the merge. It has to call executeOperation('submitReview', ...), and submitReview does not exist on the contract until TODO 1 is done, so a pre-written version fails to compile at challenge 1: error TS2769: No overload matches this call. That breaks both the image build's tsc --noEmit and challenge 1's starter, which type checks workflows.ts when it imports paymentProcessingWorkflow. The two other ways out each cost a lesson — routing the review outside Nexus would leave the sync handler from TODO 2 with no caller, and shipping the contract complete would remove challenge 2 entirely. Verified in the pushed image: TODO numbers 1 to 5 with no orphan, TODO 4 carries both parts, and the exercise typechecks inside the sandbox. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 11 ++- .../exercise/src/payments/worker.ts | 4 +- .../exercise/src/payments/workflows.ts | 72 ++++++++----------- .../04-the-payments-side/assignment.md | 39 +++++----- typescript/instruqt/config.yml | 2 +- 5 files changed, 60 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index bb965f6..b7328d6 100644 --- a/README.md +++ b/README.md @@ -279,7 +279,7 @@ not reintroduce a comparison into attendee-facing material. ### Challenges -Six TODOs, numbered 1 to 6, plus one worked example. +Five TODOs, numbered 1 to 5, plus one worked example. `checkCompliance` in `compliance/nexus-handler.ts` is **given, not a TODO**. It is the harder of the two Nexus Operations and the one whose shape carries the lesson, so a @@ -292,7 +292,14 @@ lands better from a working example than from two blank functions. | 01 | run-the-monolith | observe | none | — | | 02 | the-shared-contract | code | `shared/nexus-service.ts` | 1 | | 03 | the-compliance-side | code | `compliance/nexus-handler.ts`, `compliance/worker.ts`, plus the Endpoint CLI | 2, 3 (+ worked example) | -| 04 | the-payments-side | code | `payments/workflows.ts`, `payments/worker.ts` | 4, 5, 6 | + +TODO 4 has two parts on purpose. Both build the same `createNexusServiceClient` — one for +`checkCompliance` at `'10 minutes'`, one for `submitReview` at `'10 seconds'` — and the +pair of numbers is the lesson: async Operations budget for the whole call including +retries, sync Operations must answer inside the handler deadline. They were two separate +TODOs at opposite ends of the file, which read as duplicated busywork rather than a +contrast. +| 04 | the-payments-side | code | `payments/workflows.ts`, `payments/worker.ts` | 4 (a+b), 5 | | 05 | durability-and-human-review | observe | none | — | ### Tabs diff --git a/typescript/decouple-monolith/exercise/src/payments/worker.ts b/typescript/decouple-monolith/exercise/src/payments/worker.ts index 5367d7e..2629cc5 100644 --- a/typescript/decouple-monolith/exercise/src/payments/worker.ts +++ b/typescript/decouple-monolith/exercise/src/payments/worker.ts @@ -7,7 +7,7 @@ // // That last line is the problem. A bug in compliance code takes payments down with it. // -// Do TODO 6 last, after TODO 4 has moved the Workflow onto a Nexus client. +// Do TODO 5 last, after TODO 4 has moved the Workflow onto a Nexus client. import { NativeConnection, Worker } from '@temporalio/worker'; import * as paymentActivities from './activities'; import * as complianceActivities from '../compliance/activities'; @@ -22,7 +22,7 @@ async function run() { taskQueue: PAYMENTS_TASK_QUEUE, workflowsPath: require.resolve('./workflows'), - // ── TODO 6 ──────────────────────────────────────────────────────────────── + // ── TODO 5 ──────────────────────────────────────────────────────────────── // Once the Workflow calls Compliance over Nexus, this Worker has no reason to run // the Compliance team's code. Remove `...complianceActivities` from the object // below, and delete the import at the top of this file. diff --git a/typescript/decouple-monolith/exercise/src/payments/workflows.ts b/typescript/decouple-monolith/exercise/src/payments/workflows.ts index e74ac92..6ca3f7f 100644 --- a/typescript/decouple-monolith/exercise/src/payments/workflows.ts +++ b/typescript/decouple-monolith/exercise/src/payments/workflows.ts @@ -20,37 +20,8 @@ const { validatePayment, executePayment } = wf.proxyActivities({ startToCloseTimeout: '30 seconds', }); @@ -80,8 +51,29 @@ export async function paymentProcessingWorkflow(request: PaymentRequest): Promis logger.info(`Step 2: calling compliance check for ${request.transactionId}`); - // This one line is the entire difference between running Compliance code in this - // process and calling another team's service across a durable boundary. + // ── TODO 4 ──────────────────────────────────────────────────────────────────── + // Two calls, one client. Do both — (b) is at the bottom of this file. + // + // (a) HERE. Replace this Activity call, then delete the proxy above and its + // `complianceActivities` import: + // + // const compliance = wf.createNexusServiceClient({ + // service: complianceService, // '../shared/nexus-service' + // endpoint: COMPLIANCE_ENDPOINT, // '../shared/types' + // }); + // const result = await compliance.executeOperation('checkCompliance', compReq, { + // scheduleToCloseTimeout: '10 minutes', + // }); + // + // (b) reviewCallerWorkflow. Same client, but 'submitReview' at '10 seconds'. + // + // The two numbers are the lesson. checkCompliance is asynchronous: ten minutes covers + // the whole call including retries, which is what lets it outlive the Compliance + // Worker going away. submitReview is synchronous: the handler must answer in ten. + // + // Note what the Workflow names — a contract and an Endpoint. No Namespace, no Task + // Queue, no address. The Registry resolves those, so this Workflow does not change + // when Compliance moves. const result: ComplianceResult = await checkCompliance(compReq); logger.info(`Compliance result: ${result.riskLevel} | approved=${result.approved}`); @@ -112,15 +104,11 @@ export async function paymentProcessingWorkflow(request: PaymentRequest): Promis }; } -// ── TODO 5 ────────────────────────────────────────────────────────────────────────── -// You come back to this one in challenge 05, once the Nexus path works end to end. -// -// Submitting a human review through the Endpoint respects the team boundary: neither -// team needs to know the other's Workflow IDs or internal method names. +// ── TODO 4 (b) ────────────────────────────────────────────────────────────────────── +// The second half. Same client as above; 'submitReview' at '10 seconds'. // -// submitReview is a SYNCHRONOUS Operation, so the Compliance handler must finish inside -// the 10-second handler deadline. Use a 10-second scheduleToCloseTimeout here, not the -// 10 minutes used for the async check. +// Routing the review through the Endpoint is what keeps the boundary: neither team +// learns the other's Workflow IDs. export async function reviewCallerWorkflow(_request: ReviewRequest): Promise { - throw new Error('TODO 5: submit the review decision through the Nexus Endpoint'); + throw new Error('TODO 4(b): submit the review decision through the Nexus Endpoint'); } diff --git a/typescript/instruqt/04-the-payments-side/assignment.md b/typescript/instruqt/04-the-payments-side/assignment.md index c59597b..14435ef 100644 --- a/typescript/instruqt/04-the-payments-side/assignment.md +++ b/typescript/instruqt/04-the-payments-side/assignment.md @@ -64,10 +64,10 @@ enhanced_loading: null # Swap the Proxy for a Nexus Client Click the [button label="Exercise" background="#444CE7"](tab-0) tab, open -`exercise/src/payments/workflows.ts`, and follow TODO 4. +`exercise/src/payments/workflows.ts`, and follow TODO 4. It has two parts, both in this +file, and they are the same client twice. -The call site barely changes. Same operation name, same input, same result type. What -changes is everything underneath it. +**(a)** The compliance check, in Step 2: ```ts,nocopy // before — runs in this process @@ -79,35 +79,32 @@ const result = await compliance.executeOperation('checkCompliance', compReq, { }); ``` -The ten minutes is the budget for the whole call, retries included. It is what lets the -Operation survive the Compliance Worker going away rather than failing the moment it does. -You prove that in challenge 5. +Then delete the proxy above it and its `complianceActivities` import. + +**(b)** `reviewCallerWorkflow`, at the bottom of the file. Same client, one operation and +one number different: `'submitReview'` at `'10 seconds'`. + +The two numbers are the whole reason both exist. `checkCompliance` is **asynchronous** — +ten minutes covers the entire call including retries, which is what lets it outlive the +Compliance Worker going away, as you prove in challenge 5. `submitReview` is +**synchronous** — the handler must answer inside ten seconds. One Service, two Operations, +two budgets. # Where the Endpoint Name Lives -Look at what you just wrote. The Workflow names two things: a **contract** and an -**Endpoint**. It does not name a Namespace, a Task Queue, a hostname, or a port. +Look at what you just wrote, in both places. The Workflow names two things: a **contract** +and an **Endpoint**. It does not name a Namespace, a Task Queue, a hostname, or a port. That is the boundary. An Endpoint is a name the Registry resolves into an address. Move Compliance to a different Namespace or a different Task Queue tomorrow, update the Endpoint, and not one line of this Workflow changes. -The name itself is a single constant, `COMPLIANCE_ENDPOINT` in `exercise/src/shared/types.ts`, so there -is exactly one place to edit if it is ever renamed. - -# Write the Review Caller - -Still in `exercise/src/payments/workflows.ts`, follow TODO 5. - -`submitReview` is a **synchronous** Operation, so the Compliance handler must finish inside -the ten second handler deadline. Use a `scheduleToCloseTimeout` of `'10 seconds'` here, not -the ten minutes above. Two Operations on one Service, two very different budgets. - -Challenge 5 uses this. +The name itself is a single constant, `COMPLIANCE_ENDPOINT` in +`exercise/src/shared/types.ts`, so there is exactly one place to edit if it is renamed. # Delete the Coupling -Open `exercise/src/payments/worker.ts` and follow TODO 6. +Open `exercise/src/payments/worker.ts` and follow TODO 5. Remove `...complianceActivities` from the `activities` object, and delete its import. diff --git a/typescript/instruqt/config.yml b/typescript/instruqt/config.yml index e7831bd..fa2d00d 100644 --- a/typescript/instruqt/config.yml +++ b/typescript/instruqt/config.yml @@ -5,7 +5,7 @@ containers: # image that had no code-server in it, so the editor tabs showed "Please wait" # forever. Re-pin this after every image rebuild with: # docker buildx imagetools inspect ghcr.io/nadvolod/edu-nexus-typescript-sandbox:latest --format "{{.Manifest.Digest}}" - image: ghcr.io/nadvolod/edu-nexus-typescript-sandbox@sha256:cb82e47fb3f5874d53685f622282aac85d0214d49b8dc9a52000cfb5042cb8fd + image: ghcr.io/nadvolod/edu-nexus-typescript-sandbox@sha256:417c2f518ef4b5ecf76a8ea588b05e699e01f6d975d501458fb5d5de32a092fb shell: /bin/bash # Sized for two Node Workers, a Temporal dev server, and one code-server. memory: 4096 From 765600c83d171235002d7d745c13617dadbaeda4 Mon Sep 17 00:00:00 2001 From: nadvolod <2136245+nadvolod@users.noreply.github.com> Date: Sun, 30 Aug 2026 16:28:34 -0400 Subject: [PATCH 18/22] Sync track checksum after publishing the merged TODO 4 Checksum writeback only. Committed so the next publish does not report remote changes against a stale value. Verified: pulling into a throwaway copy reports the track up to date with no .remote files, and the live track serves image 417c2f51. Co-Authored-By: Claude Opus 5 (1M context) --- typescript/instruqt/track.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/instruqt/track.yml b/typescript/instruqt/track.yml index d41bb59..60f989d 100644 --- a/typescript/instruqt/track.yml +++ b/typescript/instruqt/track.yml @@ -51,5 +51,5 @@ lab_config: hideStopButton: false default_layout: AssignmentLeft default_layout_sidebar_size: 33 -checksum: "7199110355914590725" +checksum: "13569222137070854802" enhanced_loading: false From 6bb03c14627bc531e404c2f45fcadb31a041252b Mon Sep 17 00:00:00 2001 From: nadvolod <2136245+nadvolod@users.noreply.github.com> Date: Mon, 31 Aug 2026 08:19:31 -0400 Subject: [PATCH 19/22] Split TODO 4 into 4a/4b/4c and keep recipes out of the assignments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sha256:2ebb4d537d0cbbbeef147c28fe3bfa99cfb9b7c5961b03a1015cd01ee5b33876 Two corrections, both reported from a live sandbox. One: implementation steps had leaked into challenge 4's assignment. Then delete the proxy above it and its `complianceActivities` import. Remove `...complianceActivities` from the `activities` object, and delete its import. Both were already in the TODO comments, three lines from the cursor. The assignment now names the file and the TODOs and says why the change matters; the TODO says how. Rule and a grep for the shape added to INSTRUQT.md under Assignment wording, written to sit alongside "Show the line, do not describe it" rather than contradict it: code the learner READS belongs in the assignment, code the learner WRITES belongs in the TODO. Two: one TODO covering three edits in three places was confusing. A note reading "TODO 4(a) deletes this proxy" sat on the proxy while the instructions for 4(a) were thirty lines away at the call site, so the two halves of one instruction pointed at each other. Now one action per lettered TODO, each sited on the code it changes: TODO 4a on the Activity proxy to delete TODO 4b on the call site to replace TODO 4c on reviewCallerWorkflow to write Letters rather than new numbers because the three parts are one idea — putting the Nexus boundary in. New numbers would imply separate concepts and inflate the count. Also captured in INSTRUQT.md. Verified in the pushed image: all three letters present, 4a on the proxy, 4b on the call site, no stale "TODO 4(" spelling, and the exercise typechecks in the sandbox. Separately worth recording: the GHCR-package-is-private failure that cost time during the first publish was ALREADY documented in INSTRUQT.md before it was hit. The file worked; it was not read past its first section. Co-Authored-By: Claude Opus 5 (1M context) --- INSTRUQT.md | 88 +++++++++++++++++++ README.md | 16 ++-- .../exercise/src/payments/workflows.ts | 48 +++++----- .../04-the-payments-side/assignment.md | 38 +++----- typescript/instruqt/config.yml | 2 +- 5 files changed, 134 insertions(+), 58 deletions(-) diff --git a/INSTRUQT.md b/INSTRUQT.md index 34e2d85..2025018 100644 --- a/INSTRUQT.md +++ b/INSTRUQT.md @@ -46,6 +46,40 @@ fixed code-server for us. Confirmed working. Only one tab can be first, so a second embedded app (our Solution editor) may still need a refresh. Accept it, or serve both views from one instance. +### Use ONE editor tab, not an Exercise tab and a Solution tab + +This is the fix for the 0x0 iframe problem above rather than a workaround for it. + +Two editor tabs means two code-server iframes, and Instruqt boots every service tab's +iframe at challenge start including the hidden ones. Only the first tab has real +dimensions, so the second reliably needs a manual refresh. Making the Solution tab +position 5 does not help; it just moves which one is broken. + +Point a single tab at the directory that contains BOTH trees: + +```yaml +- title: Exercise + type: service + path: /?folder=/root/workshop # holds exercise/ and solution/ + port: 8080 +``` + +One iframe, nothing hidden, no refresh. It also drops the tab count from seven to six. + +Three things move with it, and two fail silently: + +- **`tab-N` is a position.** Removing a tab renumbers every tab after it. Re-verify + every `[button label="..."](tab-N)` by label, not by arithmetic. +- **File paths in assignments become ambiguous.** With `solution/` visible, "open + `payments/workflows.ts`" matches two files, and a learner editing the solution copy + watches their changes do nothing. Write paths in full: + `exercise/src/payments/workflows.ts`. +- **Any readiness probe that polls `?folder=` must use the new path.** Ours still + pointed at the old folder, so setup would have reported the editor not ready forever. + +The cost is that source sits two levels deeper in the tree. Worth it; the refresh bug +was hit by every attendee, the extra clicks are hit once. + ### Reordering tabs breaks every button, silently `tab-N` is a **zero-indexed position, not an id**. Move a tab and every @@ -324,6 +358,60 @@ grep -rn '^ *==*$' */assignment.md lines in a `kotlin,nocopy` fence and then say what they mean. Costs four lines of assignment, removes all the guessing. +### Implementation steps belong in the TODO, not the assignment + +The assignment names the file and the TODO, and says why the change matters. The TODO +comment, three lines from the cursor, says how. When both say how you have two copies to +keep in sync, and the learner reads the same recipe twice. + +This does not contradict "Show the line, do not describe it" above, and the difference is +worth stating precisely: + +- Code the learner is meant to **read** belongs in the assignment. Pasting it stops them + hunting through a file for the two lines you meant. +- Code the learner is meant to **write** belongs in the TODO. That is the recipe, and the + recipe goes next to the cursor. + +What it looks like when it leaks, from challenge 4 of the TypeScript track: + +``` +Then delete the proxy above it and its `complianceActivities` import. +Remove `...complianceActivities` from the `activities` object, and delete its import. +``` + +Both were already in the TODO comment. Grep for the shape — an imperative aimed at a +named symbol: + +```bash +grep -nE '^(Remove|Delete|Add|Replace|Change) `' */assignment.md +``` + +The assignment keeps the part the TODO cannot carry: why the change matters, and what to +notice once it works. + +### One action, one lettered TODO, sited on the code it changes + +A TODO that says "replace this call, then delete the declaration above, then write the +function at the bottom" makes the learner hold three locations in their head and scroll +between them. They finish one, lose the list, and re-read the whole block. + +Split by action, letter them so they still read as one idea, and put each comment on the +code it acts on: + +``` +TODO 4a on the proxy to delete +TODO 4b on the call site to replace +TODO 4c on the function to write +``` + +Letters rather than new numbers when the parts are one concept — here, putting the Nexus +boundary in. New numbers imply separate concepts and inflate the count. + +The failure this fixes is real and was reported from a live sandbox: a note reading +"TODO 4(a) deletes this proxy" sat on the proxy while the instructions for 4(a) were +thirty lines away at the call site, so the two halves of one instruction pointed at each +other. + ### A long edge label will cover your diagram Node text and edge labels are drawn in different layers, so a label wider than the gap diff --git a/README.md b/README.md index b7328d6..8493afd 100644 --- a/README.md +++ b/README.md @@ -293,13 +293,15 @@ lands better from a working example than from two blank functions. | 02 | the-shared-contract | code | `shared/nexus-service.ts` | 1 | | 03 | the-compliance-side | code | `compliance/nexus-handler.ts`, `compliance/worker.ts`, plus the Endpoint CLI | 2, 3 (+ worked example) | -TODO 4 has two parts on purpose. Both build the same `createNexusServiceClient` — one for -`checkCompliance` at `'10 minutes'`, one for `submitReview` at `'10 seconds'` — and the -pair of numbers is the lesson: async Operations budget for the whole call including -retries, sync Operations must answer inside the handler deadline. They were two separate -TODOs at opposite ends of the file, which read as duplicated busywork rather than a -contrast. -| 04 | the-payments-side | code | `payments/workflows.ts`, `payments/worker.ts` | 4 (a+b), 5 | +TODO 4 is lettered rather than split into separate numbers, because its three parts are +one idea: put the Nexus boundary in. 4a deletes the Activity proxy, 4b replaces the call +that used it, 4c writes the review caller. Each comment sits on the code it changes. + +4b and 4c build the same `createNexusServiceClient` with different numbers — `'10 minutes'` +for `checkCompliance`, `'10 seconds'` for `submitReview` — and that pair is the lesson: +async Operations budget for the whole call including retries, sync Operations must answer +inside the handler deadline. +| 04 | the-payments-side | code | `payments/workflows.ts`, `payments/worker.ts` | 4a, 4b, 4c, 5 | | 05 | durability-and-human-review | observe | none | — | ### Tabs diff --git a/typescript/decouple-monolith/exercise/src/payments/workflows.ts b/typescript/decouple-monolith/exercise/src/payments/workflows.ts index 6ca3f7f..d3a13e3 100644 --- a/typescript/decouple-monolith/exercise/src/payments/workflows.ts +++ b/typescript/decouple-monolith/exercise/src/payments/workflows.ts @@ -20,8 +20,12 @@ const { validatePayment, executePayment } = wf.proxyActivities({ startToCloseTimeout: '30 seconds', }); @@ -51,25 +55,19 @@ export async function paymentProcessingWorkflow(request: PaymentRequest): Promis logger.info(`Step 2: calling compliance check for ${request.transactionId}`); - // ── TODO 4 ──────────────────────────────────────────────────────────────────── - // Two calls, one client. Do both — (b) is at the bottom of this file. - // - // (a) HERE. Replace this Activity call, then delete the proxy above and its - // `complianceActivities` import: - // - // const compliance = wf.createNexusServiceClient({ - // service: complianceService, // '../shared/nexus-service' - // endpoint: COMPLIANCE_ENDPOINT, // '../shared/types' - // }); - // const result = await compliance.executeOperation('checkCompliance', compReq, { - // scheduleToCloseTimeout: '10 minutes', - // }); + // ── TODO 4b ─────────────────────────────────────────────────────────────────── + // Replace this Activity call with a Nexus call: // - // (b) reviewCallerWorkflow. Same client, but 'submitReview' at '10 seconds'. + // const compliance = wf.createNexusServiceClient({ + // service: complianceService, // '../shared/nexus-service' + // endpoint: COMPLIANCE_ENDPOINT, // '../shared/types' + // }); + // const result = await compliance.executeOperation('checkCompliance', compReq, { + // scheduleToCloseTimeout: '10 minutes', + // }); // - // The two numbers are the lesson. checkCompliance is asynchronous: ten minutes covers - // the whole call including retries, which is what lets it outlive the Compliance - // Worker going away. submitReview is synchronous: the handler must answer in ten. + // Ten minutes covers the whole call including retries, which is what lets it outlive + // the Compliance Worker going away. You prove that in challenge 5. // // Note what the Workflow names — a contract and an Endpoint. No Namespace, no Task // Queue, no address. The Registry resolves those, so this Workflow does not change @@ -104,11 +102,13 @@ export async function paymentProcessingWorkflow(request: PaymentRequest): Promis }; } -// ── TODO 4 (b) ────────────────────────────────────────────────────────────────────── -// The second half. Same client as above; 'submitReview' at '10 seconds'. +// ── TODO 4c ───────────────────────────────────────────────────────────────────────── +// Same client as 4b, but call 'submitReview' with a scheduleToCloseTimeout of +// '10 seconds'. // -// Routing the review through the Endpoint is what keeps the boundary: neither team -// learns the other's Workflow IDs. +// Seconds, not minutes: submitReview is a SYNCHRONOUS Operation, so the handler has to +// answer inside the Nexus handler deadline. Routing the review through the Endpoint is +// what keeps the boundary — neither team learns the other's Workflow IDs. export async function reviewCallerWorkflow(_request: ReviewRequest): Promise { - throw new Error('TODO 4(b): submit the review decision through the Nexus Endpoint'); + throw new Error('TODO 4c: submit the review decision through the Nexus Endpoint'); } diff --git a/typescript/instruqt/04-the-payments-side/assignment.md b/typescript/instruqt/04-the-payments-side/assignment.md index 14435ef..ebc1843 100644 --- a/typescript/instruqt/04-the-payments-side/assignment.md +++ b/typescript/instruqt/04-the-payments-side/assignment.md @@ -64,31 +64,18 @@ enhanced_loading: null # Swap the Proxy for a Nexus Client Click the [button label="Exercise" background="#444CE7"](tab-0) tab, open -`exercise/src/payments/workflows.ts`, and follow TODO 4. It has two parts, both in this -file, and they are the same client twice. +`exercise/src/payments/workflows.ts`, and follow TODO 4a, 4b and 4c. Each sits on the +code it changes: the proxy to delete, the call to replace, and the review caller to +write. -**(a)** The compliance check, in Step 2: +The call site barely changes — same operation name, same input, same result type. What +changes is everything underneath it: the compliance check leaves this process entirely. -```ts,nocopy -// before — runs in this process -const result = await checkCompliance(compReq); - -// after — goes out through the Endpoint to another team's Worker -const result = await compliance.executeOperation('checkCompliance', compReq, { - scheduleToCloseTimeout: '10 minutes', -}); -``` - -Then delete the proxy above it and its `complianceActivities` import. - -**(b)** `reviewCallerWorkflow`, at the bottom of the file. Same client, one operation and -one number different: `'submitReview'` at `'10 seconds'`. - -The two numbers are the whole reason both exist. `checkCompliance` is **asynchronous** — -ten minutes covers the entire call including retries, which is what lets it outlive the -Compliance Worker going away, as you prove in challenge 5. `submitReview` is -**synchronous** — the handler must answer inside ten seconds. One Service, two Operations, -two budgets. +What is worth your attention is the pair of timeouts the two parts use. +`checkCompliance` is **asynchronous** — its budget covers the entire call including +retries, which is what lets it outlive the Compliance Worker going away, as you prove in +challenge 5. `submitReview` is **synchronous** — the handler must answer inside the Nexus +handler deadline. One Service, two Operations, two very different budgets. # Where the Endpoint Name Lives @@ -106,9 +93,8 @@ The name itself is a single constant, `COMPLIANCE_ENDPOINT` in Open `exercise/src/payments/worker.ts` and follow TODO 5. -Remove `...complianceActivities` from the `activities` object, and delete its import. - -That deletion is the decoupling. Everything else is wiring. +That deletion is the decoupling. Everything else was wiring: once it is gone the Payments +Worker has no way to run the Compliance team's code, even by accident. # Run It Decoupled diff --git a/typescript/instruqt/config.yml b/typescript/instruqt/config.yml index fa2d00d..249f6d1 100644 --- a/typescript/instruqt/config.yml +++ b/typescript/instruqt/config.yml @@ -5,7 +5,7 @@ containers: # image that had no code-server in it, so the editor tabs showed "Please wait" # forever. Re-pin this after every image rebuild with: # docker buildx imagetools inspect ghcr.io/nadvolod/edu-nexus-typescript-sandbox:latest --format "{{.Manifest.Digest}}" - image: ghcr.io/nadvolod/edu-nexus-typescript-sandbox@sha256:417c2f518ef4b5ecf76a8ea588b05e699e01f6d975d501458fb5d5de32a092fb + image: ghcr.io/nadvolod/edu-nexus-typescript-sandbox@sha256:2ebb4d537d0cbbbeef147c28fe3bfa99cfb9b7c5961b03a1015cd01ee5b33876 shell: /bin/bash # Sized for two Node Workers, a Temporal dev server, and one code-server. memory: 4096 From b283315537b2557941cfbc93c6698194b5ea4d79 Mon Sep 17 00:00:00 2001 From: nadvolod <2136245+nadvolod@users.noreply.github.com> Date: Mon, 31 Aug 2026 08:52:07 -0400 Subject: [PATCH 20/22] Correct the release stage: Nexus is GA in the TypeScript SDK MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sha256:79ea7e304f7374b5c0db5bc574555294f75778a7a0a895511e5581b1822951d0 The workshop claimed in five places that TypeScript Nexus support is at Pre-release with experimental APIs. That is wrong, and it was wrong when it shipped. TypeScript SDK v1.23.0 release notes, PR #2299 "Mark Nexus as GA": Nexus is now generally available (GA) for calling Nexus Operations from Workflows and handling Workflow-backed Operations with WorkflowRunOperationHandler. v1.23.0 is the version this workshop pins, and those two capabilities are exactly what the lab builds. The "Pre-release" line came from the samples-typescript README, which is stale relative to the SDK it documents. Checked rather than swapped one claim for another. In @temporalio/nexus@1.23.0 the APIs used here — WorkflowRunOperationHandler, startWorkflow, getClient — carry no @experimental tag. The tags that remain in that package are on APIs this lab does not touch: TemporalOperationHandler and the cancel-options interfaces. Two real caveats survive and are now stated instead of the wrong one: nexus-rpc is separately versioned, still 0.0.3, and does tag service(), operation() and serviceHandler() @experimental. An attendee hovering nexus.service will see that and reasonably conclude Nexus is experimental. It is not; the contract package is. Named in the deck and in shared/nexus-service.ts so it is answered before it is asked. Standalone Nexus Operations, which run an Operation with no caller Workflow via client.nexus.createServiceClient(), ARE still Pre-release and need a special CLI build. The workshop does not use them. Challenge 5 now closes by saying the shape is production-ready rather than warning the surface will move. Corrected in track.yml, challenge 5, the contract file, the README, and the deck (temporalio/temporal-devdays-ts). Worth recording: the split evidence was noted when this first went in — the @experimental half was verified, the Pre-release half came from a changelog and the feature guide carried no release-stage banner. The weaker reading shipped anyway instead of checking the SDK release notes for the pinned version. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 17 ++++++++++++++--- .../exercise/src/shared/nexus-service.ts | 4 +++- .../assignment.md | 5 +++-- typescript/instruqt/config.yml | 2 +- typescript/instruqt/track.yml | 4 ++-- 5 files changed, 23 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 8493afd..5345c07 100644 --- a/README.md +++ b/README.md @@ -241,9 +241,20 @@ typescript/ └── 01-run-the-monolith/ ... 05-durability-and-human-review/ ``` -**Nexus support in the TypeScript SDK is at Pre-release and its APIs are marked -experimental.** The deck and challenge 2 both say so out loud rather than letting an -attendee discover it later. +**Nexus is GA in the TypeScript SDK as of v1.23.0**, the version this workshop pins — +specifically for calling Operations from Workflows and for Workflow-backed Operation +handlers, which is exactly what the lab builds (SDK release notes, PR #2299). + +Two things are easy to get wrong here, so they are stated in the deck and in +`shared/nexus-service.ts` rather than left to be discovered: + +- `nexus-rpc` is a **separately versioned** package, still at `0.0.3`, and it does tag + `service()`, `operation()` and `serviceHandler()` `@experimental`. An attendee hovering + `nexus.service` will see that and reasonably conclude Nexus is experimental. It is not; + the contract package is. +- **Standalone Nexus Operations** — running an Operation with no caller Workflow, via + `client.nexus.createServiceClient()` — *are* still Pre-release and need a special CLI + build. This workshop does not use them. ### Three lessons that do not survive the port diff --git a/typescript/decouple-monolith/exercise/src/shared/nexus-service.ts b/typescript/decouple-monolith/exercise/src/shared/nexus-service.ts index 0736a10..25afaf8 100644 --- a/typescript/decouple-monolith/exercise/src/shared/nexus-service.ts +++ b/typescript/decouple-monolith/exercise/src/shared/nexus-service.ts @@ -9,7 +9,9 @@ // language. Try "how do I define a Nexus Service contract in TypeScript?" Get used to // it now — the later TODOs are harder. // -// Note: TypeScript SDK support for Nexus is at Pre-release and its APIs are experimental. +// You may see `@experimental` when you hover `nexus.service` or `nexus.operation`. That +// is `nexus-rpc`, the contract package, which is versioned separately and still pre-1.0. +// Nexus itself is GA in the Temporal TypeScript SDK as of v1.23.0. import * as nexus from 'nexus-rpc'; import { ComplianceRequest, ComplianceResult, ReviewRequest } from './types'; diff --git a/typescript/instruqt/05-durability-and-human-review/assignment.md b/typescript/instruqt/05-durability-and-human-review/assignment.md index 0876869..f9eecc3 100644 --- a/typescript/instruqt/05-durability-and-human-review/assignment.md +++ b/typescript/instruqt/05-durability-and-human-review/assignment.md @@ -170,5 +170,6 @@ The business logic never changed. The call site barely changed. Compliance moved its own Namespace, its own Task Queue, and its own deployment schedule, and Payments kept working through an outage while it happened. -TypeScript SDK support for Nexus is at Pre-release and its APIs are marked experimental, -so expect the surface to move before it is stable. +Everything you just used is generally available: Nexus went GA in the TypeScript SDK in +v1.23.0, for calling Operations from Workflows and for Workflow-backed Operation +handlers. You can take this shape to production as it stands. diff --git a/typescript/instruqt/config.yml b/typescript/instruqt/config.yml index 249f6d1..32347c8 100644 --- a/typescript/instruqt/config.yml +++ b/typescript/instruqt/config.yml @@ -5,7 +5,7 @@ containers: # image that had no code-server in it, so the editor tabs showed "Please wait" # forever. Re-pin this after every image rebuild with: # docker buildx imagetools inspect ghcr.io/nadvolod/edu-nexus-typescript-sandbox:latest --format "{{.Manifest.Digest}}" - image: ghcr.io/nadvolod/edu-nexus-typescript-sandbox@sha256:2ebb4d537d0cbbbeef147c28fe3bfa99cfb9b7c5961b03a1015cd01ee5b33876 + image: ghcr.io/nadvolod/edu-nexus-typescript-sandbox@sha256:79ea7e304f7374b5c0db5bc574555294f75778a7a0a895511e5581b1822951d0 shell: /bin/bash # Sized for two Node Workers, a Temporal dev server, and one code-server. memory: 4096 diff --git a/typescript/instruqt/track.yml b/typescript/instruqt/track.yml index 60f989d..d01131b 100644 --- a/typescript/instruqt/track.yml +++ b/typescript/instruqt/track.yml @@ -15,8 +15,8 @@ description: |- Then break it on purpose. Stop the Compliance Worker mid-operation and watch the payment Workflow wait instead of fail. - TypeScript on the Temporal TypeScript SDK. Five challenges, three of them hands-on. - Nexus support in the TypeScript SDK is at Pre-release and its APIs are experimental. + TypeScript on the Temporal TypeScript SDK, where Nexus went generally available in + v1.23.0. Five challenges, three of them hands-on. icon: "" tags: - nexus From 3909d36ce0ee496b51596de438e95ee17d6e7fc3 Mon Sep 17 00:00:00 2001 From: nadvolod <2136245+nadvolod@users.noreply.github.com> Date: Mon, 31 Aug 2026 08:55:43 -0400 Subject: [PATCH 21/22] Turn on the Instruqt feedback tab and ask for feedback in every challenge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uses Instruqt's own feedback support rather than a custom tab: lab_config: feedback_tab_enabled: true Feedback tab in the lab UI feedback_recap_enabled: true prompt at the end of the track That choice matters. A Feedback entry added to each challenge's tabs: list would renumber every tab after it and silently break every [button](tab-N) in the track, for a tab Instruqt already gives you. Verified against temporal-community/ai-agents-workshop-v4, which sets both flags and has no Feedback entry in any tabs: list — it is lab chrome. Tab counts here stay at six and every button reference still resolves by label. Each challenge now ends with the same two-line ask, after the closing summary: Please share your feedback so we can make better content for you. The Feedback tab takes a few seconds, and it is the only way we find out which parts of this landed. It names the tab rather than linking it, because chrome has no tab-N to point at. Recorded in INSTRUQT.md so the next person does not reach for a custom tab. Assignments and track.yml only, so no image rebuild. instruqt track validate passes. Co-Authored-By: Claude Opus 5 (1M context) --- INSTRUQT.md | 21 +++++++++++++++++++ .../01-run-the-monolith/assignment.md | 5 +++++ .../02-the-shared-contract/assignment.md | 5 +++++ .../03-the-compliance-side/assignment.md | 5 +++++ .../04-the-payments-side/assignment.md | 5 +++++ .../assignment.md | 5 +++++ typescript/instruqt/track.yml | 4 ++-- 7 files changed, 48 insertions(+), 2 deletions(-) diff --git a/INSTRUQT.md b/INSTRUQT.md index 2025018..8066baf 100644 --- a/INSTRUQT.md +++ b/INSTRUQT.md @@ -80,6 +80,27 @@ Three things move with it, and two fail silently: The cost is that source sits two levels deeper in the tree. Worth it; the refresh bug was hit by every attendee, the extra clicks are hit once. +### Feedback is lab chrome, not a tab you add + +Do not add a Feedback entry to every challenge's `tabs:` list. It renumbers every tab +after it and breaks every `[button](tab-N)` in the track, for a tab Instruqt already +provides. + +Turn it on in `track.yml` instead: + +```yaml +lab_config: + feedback_tab_enabled: true # Feedback tab in the lab UI + feedback_recap_enabled: true # prompt at the end of the track +``` + +Verified against `temporal-community/ai-agents-workshop-v4`, which sets both and has no +Feedback entry in any challenge's `tabs:`. Because it is chrome, `tab-N` indices are +untouched. + +There is no `tab-N` to link to, so a per-challenge call to action references it by name: +"the **Feedback** tab". + ### Reordering tabs breaks every button, silently `tab-N` is a **zero-indexed position, not an id**. Move a tab and every diff --git a/typescript/instruqt/01-run-the-monolith/assignment.md b/typescript/instruqt/01-run-the-monolith/assignment.md index d1de29f..39b8e76 100644 --- a/typescript/instruqt/01-run-the-monolith/assignment.md +++ b/typescript/instruqt/01-run-the-monolith/assignment.md @@ -220,3 +220,8 @@ Click **Check** when your three transactions have finished. - The compliance check is an ordinary Activity call. - A declined payment completes. It does not fail. - Compliance has a Namespace, and it is empty. + +--- + +**Please share your feedback so we can make better content for you.** The **Feedback** +tab takes a few seconds, and it is the only way we find out which parts of this landed. diff --git a/typescript/instruqt/02-the-shared-contract/assignment.md b/typescript/instruqt/02-the-shared-contract/assignment.md index 5e11b00..87331fb 100644 --- a/typescript/instruqt/02-the-shared-contract/assignment.md +++ b/typescript/instruqt/02-the-shared-contract/assignment.md @@ -118,3 +118,8 @@ Click **Check** when the two Operations are declared. - `nexus.service()` names the contract; `nexus.operation()` declares each call. - The contract is shared. The implementation is not. - Declaring an Operation obliges someone to handle it, and the compiler enforces that. + +--- + +**Please share your feedback so we can make better content for you.** The **Feedback** +tab takes a few seconds, and it is the only way we find out which parts of this landed. diff --git a/typescript/instruqt/03-the-compliance-side/assignment.md b/typescript/instruqt/03-the-compliance-side/assignment.md index 1c78f8c..396fbae 100644 --- a/typescript/instruqt/03-the-compliance-side/assignment.md +++ b/typescript/instruqt/03-the-compliance-side/assignment.md @@ -212,3 +212,8 @@ stop this one with **Ctrl+C** afterwards. - A Worker starts fine with nothing registered. A clean banner proves nothing. - The Endpoint maps a name to a Namespace and a Task Queue. - Compliance now runs in its own Namespace, with its own Worker. + +--- + +**Please share your feedback so we can make better content for you.** The **Feedback** +tab takes a few seconds, and it is the only way we find out which parts of this landed. diff --git a/typescript/instruqt/04-the-payments-side/assignment.md b/typescript/instruqt/04-the-payments-side/assignment.md index ebc1843..5d5df4f 100644 --- a/typescript/instruqt/04-the-payments-side/assignment.md +++ b/typescript/instruqt/04-the-payments-side/assignment.md @@ -183,3 +183,8 @@ Click **Check**. resolves that name to a Namespace and Task Queue. - One Service can carry a slow async Operation and a fast sync one, with different budgets. - The work moved to another Namespace, and the business logic never changed. + +--- + +**Please share your feedback so we can make better content for you.** The **Feedback** +tab takes a few seconds, and it is the only way we find out which parts of this landed. diff --git a/typescript/instruqt/05-durability-and-human-review/assignment.md b/typescript/instruqt/05-durability-and-human-review/assignment.md index f9eecc3..f0253f9 100644 --- a/typescript/instruqt/05-durability-and-human-review/assignment.md +++ b/typescript/instruqt/05-durability-and-human-review/assignment.md @@ -173,3 +173,8 @@ kept working through an outage while it happened. Everything you just used is generally available: Nexus went GA in the TypeScript SDK in v1.23.0, for calling Operations from Workflows and for Workflow-backed Operation handlers. You can take this shape to production as it stands. + +--- + +**Please share your feedback so we can make better content for you.** The **Feedback** +tab takes a few seconds, and it is the only way we find out which parts of this landed. diff --git a/typescript/instruqt/track.yml b/typescript/instruqt/track.yml index d01131b..5ee9563 100644 --- a/typescript/instruqt/track.yml +++ b/typescript/instruqt/track.yml @@ -32,8 +32,8 @@ timelimit: 7200 lab_config: extend_ttl: 900 sidebar_enabled: true - feedback_recap_enabled: false - feedback_tab_enabled: false + feedback_recap_enabled: true + feedback_tab_enabled: true loadingMessages: - Waking Ziggy the tardigrade... - Starting the Temporal dev server... From 548dd3240dd184f73ee9c1b683723f44c2da6579 Mon Sep 17 00:00:00 2001 From: nadvolod <2136245+nadvolod@users.noreply.github.com> Date: Mon, 31 Aug 2026 08:57:27 -0400 Subject: [PATCH 22/22] Sync track checksum after enabling feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Checksum writeback only. Both feedback flags survived the publish rewrite because the serializer keeps true values and omits false ones — the same mechanism that makes maintenance: false vanish. If these are ever turned off, expect the keys to disappear from the file rather than persist as false. Co-Authored-By: Claude Opus 5 (1M context) --- typescript/instruqt/track.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/instruqt/track.yml b/typescript/instruqt/track.yml index 5ee9563..a4f2875 100644 --- a/typescript/instruqt/track.yml +++ b/typescript/instruqt/track.yml @@ -51,5 +51,5 @@ lab_config: hideStopButton: false default_layout: AssignmentLeft default_layout_sidebar_size: 33 -checksum: "13569222137070854802" +checksum: "2211482467216494180" enhanced_loading: false