diff --git a/.changeset/late-kings-smell.md b/.changeset/late-kings-smell.md new file mode 100644 index 0000000000..c6799edbd6 --- /dev/null +++ b/.changeset/late-kings-smell.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/react-hooks": patch +--- + +fix: prevent infinite useEffect when passing an array of tags to useRealtimeRunsWithTag diff --git a/packages/react-hooks/src/hooks/useRealtime.ts b/packages/react-hooks/src/hooks/useRealtime.ts index 2644bf01bb..c14a228f62 100644 --- a/packages/react-hooks/src/hooks/useRealtime.ts +++ b/packages/react-hooks/src/hooks/useRealtime.ts @@ -404,6 +404,8 @@ export function useRealtimeRunsWithTag( tag: string | string[], options?: UseRealtimeRunsWithTagOptions ): UseRealtimeRunsInstance { + const normalizedTag = (Array.isArray(tag) ? tag : [tag]).join("-"); + const hookId = useId(); const idKey = options?.id ?? hookId; @@ -466,7 +468,7 @@ export function useRealtimeRunsWithTag( abortControllerRef.current = null; } } - }, [tag, mutateRuns, runsRef, abortControllerRef, apiClient, setError]); + }, [normalizedTag, mutateRuns, runsRef, abortControllerRef, apiClient, setError]); useEffect(() => { if (typeof options?.enabled === "boolean" && !options.enabled) { @@ -478,7 +480,7 @@ export function useRealtimeRunsWithTag( return () => { stop(); }; - }, [tag, stop, options?.enabled]); + }, [normalizedTag, stop, options?.enabled]); return { runs: runs ?? [], error, stop }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4d512a2127..757dd442ab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2538,6 +2538,49 @@ importers: specifier: ^5.5.4 version: 5.5.4 + references/realtime-hooks-test: + dependencies: + '@trigger.dev/react-hooks': + specifier: workspace:* + version: link:../../packages/react-hooks + '@trigger.dev/sdk': + specifier: workspace:* + version: link:../../packages/trigger-sdk + next: + specifier: 15.5.6 + version: 15.5.6(@playwright/test@1.37.0)(react-dom@19.1.0)(react@19.1.0) + react: + specifier: 19.1.0 + version: 19.1.0 + react-dom: + specifier: 19.1.0 + version: 19.1.0(react@19.1.0) + zod: + specifier: 3.25.76 + version: 3.25.76 + devDependencies: + '@tailwindcss/postcss': + specifier: ^4 + version: 4.0.17 + '@types/node': + specifier: ^20 + version: 20.14.14 + '@types/react': + specifier: ^19 + version: 19.0.12 + '@types/react-dom': + specifier: ^19 + version: 19.0.4(@types/react@19.0.12) + tailwindcss: + specifier: ^4 + version: 4.0.17 + trigger.dev: + specifier: workspace:* + version: link:../../packages/cli-v3 + typescript: + specifier: ^5 + version: 5.5.4 + references/realtime-streams: dependencies: '@ai-sdk/openai': @@ -3463,7 +3506,7 @@ packages: '@smithy/util-endpoints': 1.0.5 '@smithy/util-retry': 2.0.7 '@smithy/util-utf8': 2.0.2 - tslib: 2.6.2 + tslib: 2.8.1 transitivePeerDependencies: - aws-crt dev: false @@ -3509,7 +3552,7 @@ packages: '@smithy/util-middleware': 3.0.11 '@smithy/util-retry': 3.0.11 '@smithy/util-utf8': 3.0.0 - tslib: 2.6.2 + tslib: 2.8.1 transitivePeerDependencies: - aws-crt dev: false @@ -3888,7 +3931,7 @@ packages: '@aws-sdk/types': 3.451.0 '@smithy/property-provider': 2.0.15 '@smithy/types': 2.6.0 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@aws-sdk/credential-provider-env@3.716.0: @@ -3899,7 +3942,7 @@ packages: '@aws-sdk/types': 3.714.0 '@smithy/property-provider': 3.1.11 '@smithy/types': 3.7.2 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@aws-sdk/credential-provider-env@3.839.0: @@ -3948,7 +3991,7 @@ packages: '@smithy/smithy-client': 3.5.1 '@smithy/types': 3.7.2 '@smithy/util-stream': 3.3.2 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@aws-sdk/credential-provider-http@3.839.0: @@ -4012,7 +4055,7 @@ packages: '@smithy/property-provider': 2.0.15 '@smithy/shared-ini-file-loader': 2.2.5 '@smithy/types': 2.6.0 - tslib: 2.6.2 + tslib: 2.8.1 transitivePeerDependencies: - aws-crt dev: false @@ -4035,7 +4078,7 @@ packages: '@smithy/property-provider': 3.1.11 '@smithy/shared-ini-file-loader': 3.1.12 '@smithy/types': 3.7.2 - tslib: 2.6.2 + tslib: 2.8.1 transitivePeerDependencies: - '@aws-sdk/client-sso-oidc' - aws-crt @@ -4213,7 +4256,7 @@ packages: '@smithy/property-provider': 2.0.15 '@smithy/shared-ini-file-loader': 2.2.5 '@smithy/types': 2.6.0 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@aws-sdk/credential-provider-process@3.716.0: @@ -4225,7 +4268,7 @@ packages: '@smithy/property-provider': 3.1.11 '@smithy/shared-ini-file-loader': 3.1.12 '@smithy/types': 3.7.2 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@aws-sdk/credential-provider-process@3.839.0: @@ -4274,7 +4317,7 @@ packages: '@smithy/property-provider': 2.0.15 '@smithy/shared-ini-file-loader': 2.2.5 '@smithy/types': 2.6.0 - tslib: 2.6.2 + tslib: 2.8.1 transitivePeerDependencies: - aws-crt dev: false @@ -4290,7 +4333,7 @@ packages: '@smithy/property-provider': 3.1.11 '@smithy/shared-ini-file-loader': 3.1.12 '@smithy/types': 3.7.2 - tslib: 2.6.2 + tslib: 2.8.1 transitivePeerDependencies: - '@aws-sdk/client-sso-oidc' - aws-crt @@ -4351,7 +4394,7 @@ packages: '@aws-sdk/types': 3.451.0 '@smithy/property-provider': 2.0.15 '@smithy/types': 2.6.0 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@aws-sdk/credential-provider-web-identity@3.716.0(@aws-sdk/client-sts@3.716.0): @@ -4365,7 +4408,7 @@ packages: '@aws-sdk/types': 3.714.0 '@smithy/property-provider': 3.1.11 '@smithy/types': 3.7.2 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@aws-sdk/credential-provider-web-identity@3.839.0: @@ -4575,7 +4618,7 @@ packages: '@aws-sdk/middleware-signing': 3.451.0 '@aws-sdk/types': 3.451.0 '@smithy/types': 2.6.0 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@aws-sdk/middleware-signing@3.451.0: @@ -4890,7 +4933,7 @@ packages: '@smithy/util-endpoints': 1.0.5 '@smithy/util-retry': 2.0.7 '@smithy/util-utf8': 2.0.2 - tslib: 2.6.2 + tslib: 2.8.1 transitivePeerDependencies: - aws-crt dev: false @@ -4906,7 +4949,7 @@ packages: '@smithy/property-provider': 3.1.11 '@smithy/shared-ini-file-loader': 3.1.12 '@smithy/types': 3.7.2 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@aws-sdk/token-providers@3.839.0: @@ -5048,7 +5091,7 @@ packages: resolution: {integrity: sha512-qo2t/vBTnoXpjKxlsC2e1gBrRm80M3bId27r0BRB2VniSSe7bL1mmzM+/HFtujm0iAxtPM+aLEflLJlJeDPg0w==} engines: {node: '>=14.0.0'} dependencies: - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@aws-sdk/util-locate-window@3.893.0: @@ -5185,7 +5228,7 @@ packages: /@aws-sdk/util-utf8-browser@3.259.0: resolution: {integrity: sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==} dependencies: - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@aws-sdk/xml-builder@3.821.0: @@ -5829,7 +5872,7 @@ packages: outdent: 0.5.0 prettier: 2.8.8 resolve-from: 5.0.0 - semver: 7.7.2 + semver: 7.7.3 dev: false /@changesets/assemble-release-plan@5.2.4(patch_hash=3wuhjtl4hjck4itk3w32z4cd5u): @@ -5840,7 +5883,7 @@ packages: '@changesets/get-dependents-graph': 1.3.6 '@changesets/types': 5.2.1 '@manypkg/get-packages': 1.1.3 - semver: 7.7.2 + semver: 7.7.3 dev: false patched: true @@ -5914,7 +5957,7 @@ packages: '@manypkg/get-packages': 1.1.3 chalk: 2.4.2 fs-extra: 7.0.1 - semver: 7.7.2 + semver: 7.7.3 dev: false /@changesets/get-github-info@0.5.2: @@ -9519,7 +9562,7 @@ packages: resolution: {integrity: sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dependencies: - semver: 7.7.2 + semver: 7.7.3 dev: true /@npmcli/git@4.1.0: @@ -9532,7 +9575,7 @@ packages: proc-log: 3.0.0 promise-inflight: 1.0.1 promise-retry: 2.0.1 - semver: 7.7.2 + semver: 7.7.3 which: 3.0.1 transitivePeerDependencies: - bluebird @@ -10269,7 +10312,7 @@ packages: '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.28.0 forwarded-parse: 2.1.2 - semver: 7.7.2 + semver: 7.7.3 transitivePeerDependencies: - supports-color dev: false @@ -17330,7 +17373,7 @@ packages: engines: {node: '>=14.0.0'} dependencies: '@smithy/types': 2.6.0 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@smithy/abort-controller@3.1.9: @@ -17338,7 +17381,7 @@ packages: engines: {node: '>=16.0.0'} dependencies: '@smithy/types': 3.7.2 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@smithy/abort-controller@4.0.4: @@ -17455,7 +17498,7 @@ packages: '@smithy/property-provider': 2.0.15 '@smithy/types': 2.6.0 '@smithy/url-parser': 2.0.14 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@smithy/credential-provider-imds@3.2.8: @@ -17466,7 +17509,7 @@ packages: '@smithy/property-provider': 3.1.11 '@smithy/types': 3.7.2 '@smithy/url-parser': 3.0.11 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@smithy/credential-provider-imds@4.0.6: @@ -17497,7 +17540,7 @@ packages: '@aws-crypto/crc32': 3.0.0 '@smithy/types': 2.6.0 '@smithy/util-hex-encoding': 2.0.0 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@smithy/fetch-http-handler@2.2.7: @@ -17630,7 +17673,7 @@ packages: resolution: {integrity: sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==} engines: {node: '>=16.0.0'} dependencies: - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@smithy/is-array-buffer@4.0.0: @@ -17960,7 +18003,7 @@ packages: engines: {node: '>=14.0.0'} dependencies: '@smithy/types': 2.6.0 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@smithy/property-provider@3.1.11: @@ -17968,7 +18011,7 @@ packages: engines: {node: '>=16.0.0'} dependencies: '@smithy/types': 3.7.2 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@smithy/property-provider@4.0.4: @@ -18025,7 +18068,7 @@ packages: dependencies: '@smithy/types': 2.6.0 '@smithy/util-uri-escape': 2.0.0 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@smithy/querystring-builder@3.0.11: @@ -18034,7 +18077,7 @@ packages: dependencies: '@smithy/types': 3.7.2 '@smithy/util-uri-escape': 3.0.0 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@smithy/querystring-builder@4.0.4: @@ -18060,7 +18103,7 @@ packages: engines: {node: '>=14.0.0'} dependencies: '@smithy/types': 2.6.0 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@smithy/querystring-parser@3.0.11: @@ -18068,7 +18111,7 @@ packages: engines: {node: '>=16.0.0'} dependencies: '@smithy/types': 3.7.2 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@smithy/querystring-parser@4.0.4: @@ -18120,7 +18163,7 @@ packages: engines: {node: '>=14.0.0'} dependencies: '@smithy/types': 2.6.0 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@smithy/shared-ini-file-loader@3.1.12: @@ -18128,7 +18171,7 @@ packages: engines: {node: '>=16.0.0'} dependencies: '@smithy/types': 3.7.2 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@smithy/shared-ini-file-loader@4.0.4: @@ -18158,7 +18201,7 @@ packages: '@smithy/util-middleware': 2.0.7 '@smithy/util-uri-escape': 2.0.0 '@smithy/util-utf8': 2.0.2 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@smithy/signature-v4@4.2.4: @@ -18172,7 +18215,7 @@ packages: '@smithy/util-middleware': 3.0.11 '@smithy/util-uri-escape': 3.0.0 '@smithy/util-utf8': 3.0.0 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@smithy/signature-v4@5.1.2: @@ -18408,7 +18451,7 @@ packages: engines: {node: '>=14.0.0'} dependencies: '@smithy/is-array-buffer': 2.0.0 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@smithy/util-buffer-from@2.2.0: @@ -18424,7 +18467,7 @@ packages: engines: {node: '>=16.0.0'} dependencies: '@smithy/is-array-buffer': 3.0.0 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@smithy/util-buffer-from@4.0.0: @@ -18447,14 +18490,14 @@ packages: resolution: {integrity: sha512-xCQ6UapcIWKxXHEU4Mcs2s7LcFQRiU3XEluM2WcCjjBtQkUN71Tb+ydGmJFPxMUrW/GWMgQEEGipLym4XG0jZg==} engines: {node: '>=14.0.0'} dependencies: - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@smithy/util-config-provider@3.0.0: resolution: {integrity: sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ==} engines: {node: '>=16.0.0'} dependencies: - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@smithy/util-config-provider@4.0.0: @@ -18606,14 +18649,14 @@ packages: resolution: {integrity: sha512-c5xY+NUnFqG6d7HFh1IFfrm3mGl29lC+vF+geHv4ToiuJCBmIfzx6IeHLg+OgRdPFKDXIw6pvi+p3CsscaMcMA==} engines: {node: '>=14.0.0'} dependencies: - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@smithy/util-hex-encoding@3.0.0: resolution: {integrity: sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==} engines: {node: '>=16.0.0'} dependencies: - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@smithy/util-hex-encoding@4.0.0: @@ -18635,7 +18678,7 @@ packages: engines: {node: '>=14.0.0'} dependencies: '@smithy/types': 2.6.0 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@smithy/util-middleware@3.0.11: @@ -18709,7 +18752,7 @@ packages: '@smithy/util-buffer-from': 2.0.0 '@smithy/util-hex-encoding': 2.0.0 '@smithy/util-utf8': 2.0.2 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@smithy/util-stream@3.3.2: @@ -18723,7 +18766,7 @@ packages: '@smithy/util-buffer-from': 3.0.0 '@smithy/util-hex-encoding': 3.0.0 '@smithy/util-utf8': 3.0.0 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@smithy/util-stream@4.2.2: @@ -18758,14 +18801,14 @@ packages: resolution: {integrity: sha512-ebkxsqinSdEooQduuk9CbKcI+wheijxEb3utGXkCoYQkJnwTnLbH1JXGimJtUkQwNQbsbuYwG2+aFVyZf5TLaw==} engines: {node: '>=14.0.0'} dependencies: - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@smithy/util-uri-escape@3.0.0: resolution: {integrity: sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==} engines: {node: '>=16.0.0'} dependencies: - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@smithy/util-uri-escape@4.0.0: @@ -19218,7 +19261,7 @@ packages: /@tailwindcss/node@4.0.17: resolution: {integrity: sha512-LIdNwcqyY7578VpofXyqjH6f+3fP4nrz7FBLki5HpzqjYfXdF2m/eW18ZfoKePtDGg90Bvvfpov9d2gy5XVCbg==} dependencies: - enhanced-resolve: 5.18.1 + enhanced-resolve: 5.18.3 jiti: 2.4.2 tailwindcss: 4.0.17 dev: true @@ -20630,7 +20673,7 @@ packages: chalk: 4.1.2 css-what: 5.1.0 cssesc: 3.0.0 - csstype: 3.1.3 + csstype: 3.2.0 deep-object-diff: 1.1.9 deepmerge: 4.3.1 media-query-parser: 2.0.2 @@ -21861,7 +21904,7 @@ packages: postcss: ^8.1.0 dependencies: browserslist: 4.24.4 - caniuse-lite: 1.0.30001707 + caniuse-lite: 1.0.30001754 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.1 @@ -21874,7 +21917,7 @@ packages: hasBin: true dependencies: browserslist: 4.24.4 - caniuse-lite: 1.0.30001720 + caniuse-lite: 1.0.30001754 normalize-range: 0.1.2 num2fraction: 1.2.2 picocolors: 0.2.1 @@ -22195,7 +22238,7 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001720 + caniuse-lite: 1.0.30001754 electron-to-chromium: 1.5.98 node-releases: 2.0.19 update-browserslist-db: 1.1.2(browserslist@4.24.4) @@ -22265,7 +22308,7 @@ packages: /builtins@5.0.1: resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} dependencies: - semver: 7.7.2 + semver: 7.7.3 dev: true /bun-types@1.1.17: @@ -22456,12 +22499,8 @@ packages: resolution: {integrity: sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==} dev: false - /caniuse-lite@1.0.30001720: - resolution: {integrity: sha512-Ec/2yV2nNPwb4DnTANEV99ZWwm3ZWfdlfkQbWSDDt+PsXEVYwlhPH8tdMaPunYTKKmz7AnHi2oNEi1GcmKCD8g==} - /caniuse-lite@1.0.30001754: resolution: {integrity: sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==} - dev: true /case-anything@2.1.13: resolution: {integrity: sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==} @@ -23302,7 +23341,6 @@ packages: /csstype@3.2.0: resolution: {integrity: sha512-si++xzRAY9iPp60roQiFta7OFbhrgvcthrhlNAGeQptSY25uJjkfUV8OArC3KLocB8JT8ohz+qgxWCmz8RhjIg==} - dev: true /csv-generate@3.4.3: resolution: {integrity: sha512-w/T+rqR0vwvHqWs/1ZyMDWtHHSJaN06klRqJXBEpDJaM/+dZkso0OKh1VcuuYvK3XM53KysVNq8Ko/epCK8wOw==} @@ -23959,17 +23997,10 @@ packages: hasBin: true dev: true - /detect-libc@2.0.3: - resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} - engines: {node: '>=8'} - requiresBuild: true - /detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} requiresBuild: true - dev: false - optional: true /detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} @@ -24079,7 +24110,7 @@ packages: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} dependencies: '@babel/runtime': 7.27.4 - csstype: 3.1.3 + csstype: 3.2.0 dev: false /dom-serializer@2.0.0: @@ -24222,7 +24253,7 @@ packages: '@one-ini/wasm': 0.1.1 commander: 10.0.1 minimatch: 9.0.1 - semver: 7.7.2 + semver: 7.7.3 dev: false /ee-first@1.1.1: @@ -24348,20 +24379,12 @@ packages: tapable: 2.2.1 dev: true - /enhanced-resolve@5.18.1: - resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} - engines: {node: '>=10.13.0'} - dependencies: - graceful-fs: 4.2.11 - tapable: 2.2.2 - /enhanced-resolve@5.18.3: resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} dependencies: graceful-fs: 4.2.11 - tapable: 2.3.0 - dev: true + tapable: 2.2.2 /enquirer@2.3.6: resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} @@ -28033,7 +28056,7 @@ packages: lodash.isstring: 4.0.1 lodash.once: 4.1.1 ms: 2.1.3 - semver: 7.6.3 + semver: 7.7.3 dev: false /jsprim@1.4.2: @@ -28371,7 +28394,7 @@ packages: resolution: {integrity: sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==} engines: {node: '>= 12.0.0'} dependencies: - detect-libc: 2.0.3 + detect-libc: 2.1.2 optionalDependencies: lightningcss-darwin-arm64: 1.29.2 lightningcss-darwin-x64: 1.29.2 @@ -28725,7 +28748,7 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} dependencies: - semver: 7.7.2 + semver: 7.7.3 dev: true /map-obj@1.0.1: @@ -30088,7 +30111,7 @@ packages: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 css-tree: 1.1.3 - csstype: 3.1.3 + csstype: 3.2.0 fastest-stable-stringify: 2.0.2 inline-style-prefixer: 7.0.1 react: 18.2.0 @@ -30183,7 +30206,7 @@ packages: '@next/env': 14.1.0 '@swc/helpers': 0.5.2 busboy: 1.6.0 - caniuse-lite: 1.0.30001707 + caniuse-lite: 1.0.30001754 graceful-fs: 4.2.11 postcss: 8.4.31 react: 18.3.1 @@ -30319,7 +30342,7 @@ packages: '@next/env': 15.5.6 '@playwright/test': 1.37.0 '@swc/helpers': 0.5.15 - caniuse-lite: 1.0.30001720 + caniuse-lite: 1.0.30001754 postcss: 8.4.31 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) @@ -30347,7 +30370,7 @@ packages: resolution: {integrity: sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==} engines: {node: '>=10'} dependencies: - semver: 7.7.2 + semver: 7.7.3 dev: true /node-abort-controller@3.1.1: @@ -30446,7 +30469,7 @@ packages: dependencies: hosted-git-info: 6.1.1 is-core-module: 2.14.0 - semver: 7.7.2 + semver: 7.7.3 validate-npm-package-license: 3.0.4 dev: true @@ -30475,7 +30498,7 @@ packages: resolution: {integrity: sha512-744wat5wAAHsxa4590mWO0tJ8PKxR8ORZsH9wGpQc3nWTzozMAgBN/XyqYw7mg3yqLM8dLwEnwSfKMmXAjF69g==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dependencies: - semver: 7.7.2 + semver: 7.7.3 dev: true /npm-normalize-package-bin@3.0.1: @@ -30489,7 +30512,7 @@ packages: dependencies: hosted-git-info: 6.1.1 proc-log: 3.0.0 - semver: 7.7.2 + semver: 7.7.3 validate-npm-package-name: 5.0.0 dev: true @@ -30500,7 +30523,7 @@ packages: npm-install-checks: 6.2.0 npm-normalize-package-bin: 3.0.1 npm-package-arg: 10.1.0 - semver: 7.7.2 + semver: 7.7.3 dev: true /npm-run-all@4.1.5: @@ -31649,6 +31672,18 @@ packages: read-cache: 1.0.0 resolve: 1.22.8 + /postcss-import@15.1.0(postcss@8.5.6): + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.8 + dev: false + /postcss-import@16.0.1(postcss@8.5.6): resolution: {integrity: sha512-i2Pci0310NaLHr/5JUFSw1j/8hf1CzwMY13g6ZDxgOavmRHQi2ba3PmUHoihO+sjaum+KmCNzskNsw7JDrg03g==} engines: {node: '>=18.0.0'} @@ -31677,6 +31712,16 @@ packages: camelcase-css: 2.0.1 postcss: 8.5.4 + /postcss-js@4.0.1(postcss@8.5.6): + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.6 + dev: false + /postcss-load-config@4.0.2(postcss@8.5.4): resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} engines: {node: '>= 14'} @@ -31693,6 +31738,23 @@ packages: postcss: 8.5.4 yaml: 2.7.1 + /postcss-load-config@4.0.2(postcss@8.5.6): + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 3.1.3 + postcss: 8.5.6 + yaml: 2.7.1 + dev: false + /postcss-load-config@6.0.1(postcss@8.5.6)(tsx@4.17.0): resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} engines: {node: '>= 18'} @@ -31852,6 +31914,16 @@ packages: postcss: 8.5.4 postcss-selector-parser: 6.1.2 + /postcss-nested@6.2.0(postcss@8.5.6): + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + dev: false + /postcss-selector-parser@6.0.10: resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} engines: {node: '>=4'} @@ -31950,7 +32022,6 @@ packages: nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 - dev: true /postgres-array@2.0.0: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} @@ -32015,7 +32086,7 @@ packages: engines: {node: '>=10'} hasBin: true dependencies: - detect-libc: 2.0.3 + detect-libc: 2.1.2 expand-template: 2.0.3 github-from-package: 0.0.0 minimist: 1.2.7 @@ -34028,8 +34099,6 @@ packages: engines: {node: '>=10'} hasBin: true requiresBuild: true - dev: false - optional: true /send@0.18.0: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} @@ -34168,7 +34237,7 @@ packages: requiresBuild: true dependencies: color: 4.2.3 - detect-libc: 2.0.3 + detect-libc: 2.1.2 semver: 7.7.2 optionalDependencies: '@img/sharp-darwin-arm64': 0.33.5 @@ -35363,11 +35432,11 @@ packages: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.4 - postcss-import: 15.1.0(postcss@8.5.4) - postcss-js: 4.0.1(postcss@8.5.4) - postcss-load-config: 4.0.2(postcss@8.5.4) - postcss-nested: 6.2.0(postcss@8.5.4) + postcss: 8.5.6 + postcss-import: 15.1.0(postcss@8.5.6) + postcss-js: 4.0.1(postcss@8.5.6) + postcss-load-config: 4.0.2(postcss@8.5.6) + postcss-nested: 6.2.0(postcss@8.5.6) postcss-selector-parser: 6.1.2 resolve: 1.22.8 sucrase: 3.35.0 @@ -35420,7 +35489,6 @@ packages: /tapable@2.3.0: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} - dev: true /tar-fs@2.1.3: resolution: {integrity: sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==} @@ -37092,7 +37160,7 @@ packages: dependencies: '@types/node': 20.14.14 esbuild: 0.18.11 - postcss: 8.5.4 + postcss: 8.5.6 rollup: 3.29.1 optionalDependencies: fsevents: 2.3.3 @@ -37128,7 +37196,7 @@ packages: dependencies: '@types/node': 20.14.14 esbuild: 0.20.2 - postcss: 8.5.4 + postcss: 8.5.6 rollup: 4.36.0 optionalDependencies: fsevents: 2.3.3 @@ -37401,7 +37469,7 @@ packages: acorn-import-assertions: 1.9.0(acorn@8.14.1) browserslist: 4.24.4 chrome-trace-event: 1.0.3 - enhanced-resolve: 5.18.1 + enhanced-resolve: 5.18.3 es-module-lexer: 1.7.0 eslint-scope: 5.1.1 events: 3.3.0 @@ -37412,7 +37480,7 @@ packages: mime-types: 2.1.35 neo-async: 2.6.2 schema-utils: 3.3.0 - tapable: 2.2.2 + tapable: 2.3.0 terser-webpack-plugin: 5.3.7(@swc/core@1.3.101)(esbuild@0.19.11)(webpack@5.88.2) watchpack: 2.4.0 webpack-sources: 3.2.3 diff --git a/references/realtime-hooks-test/.eslintrc.json b/references/realtime-hooks-test/.eslintrc.json new file mode 100644 index 0000000000..f18272b83b --- /dev/null +++ b/references/realtime-hooks-test/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": "next/core-web-vitals" +} + diff --git a/references/realtime-hooks-test/.gitignore b/references/realtime-hooks-test/.gitignore new file mode 100644 index 0000000000..db870b36d3 --- /dev/null +++ b/references/realtime-hooks-test/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# Trigger.dev +.trigger + diff --git a/references/realtime-hooks-test/README.md b/references/realtime-hooks-test/README.md new file mode 100644 index 0000000000..2f57eee6c8 --- /dev/null +++ b/references/realtime-hooks-test/README.md @@ -0,0 +1,109 @@ +# Realtime Hooks Test Reference Project + +This is a comprehensive testing reference project for the `@trigger.dev/react-hooks` package. It demonstrates all the realtime hooks available in `useRealtime.ts`. + +## Hooks Tested + +This project includes examples for: + +- ✅ `useRealtimeRun` - Subscribe to a single task run +- ✅ `useRealtimeRunWithStreams` - Subscribe to a run with stream data +- ✅ `useRealtimeRunsWithTag` - Subscribe to multiple runs by tag +- ✅ `useRealtimeBatch` - Subscribe to a batch of runs +- ✅ `useRealtimeStream` - Subscribe to a specific stream + +## Getting Started + +### 1. Install dependencies + +From the repository root: + +```bash +pnpm install +``` + +### 2. Set up environment variables + +Create a `.env.local` file: + +```bash +TRIGGER_SECRET_KEY=your_secret_key +TRIGGER_PROJECT_REF=your_project_ref +NEXT_PUBLIC_TRIGGER_PUBLIC_KEY=your_public_key +NEXT_PUBLIC_TRIGGER_API_URL=http://localhost:3030 +``` + +### 3. Run the development servers + +In one terminal, run the Trigger.dev dev server: + +```bash +pnpm run dev:trigger +``` + +In another terminal, run the Next.js dev server: + +```bash +pnpm run dev +``` + +### 4. Open the app + +Visit [http://localhost:3000](http://localhost:3000) to see the examples. + +## Project Structure + +``` +src/ +├── app/ +│ ├── page.tsx # Home page with navigation +│ ├── actions.ts # Server actions for triggering tasks +│ ├── run/[id]/page.tsx # useRealtimeRun example +│ ├── run-with-streams/[id]/page.tsx # useRealtimeRunWithStreams example +│ ├── runs-with-tag/[tag]/page.tsx # useRealtimeRunsWithTag example +│ ├── batch/[id]/page.tsx # useRealtimeBatch example +│ └── stream/[id]/page.tsx # useRealtimeStream example +├── components/ +│ ├── run-viewer.tsx # Component using useRealtimeRun +│ ├── run-with-streams-viewer.tsx # Component using useRealtimeRunWithStreams +│ ├── runs-with-tag-viewer.tsx # Component using useRealtimeRunsWithTag +│ ├── batch-viewer.tsx # Component using useRealtimeBatch +│ └── stream-viewer.tsx # Component using useRealtimeStream +└── trigger/ + ├── simple-task.ts # Simple task for useRealtimeRun + ├── stream-task.ts # Task with streams for useRealtimeRunWithStreams + ├── tagged-task.ts # Tasks that use tags + └── batch-task.ts # Tasks for batch operations +``` + +## Testing Scenarios + +Each page demonstrates different aspects of the hooks: + +### 1. Single Run (`/run/[id]`) +- Basic subscription to a single run +- onComplete callback +- stopOnCompletion option +- Error handling + +### 2. Run with Streams (`/run-with-streams/[id]`) +- Subscribe to run updates and multiple streams +- Throttling stream updates +- Type-safe stream data + +### 3. Runs with Tag (`/runs-with-tag/[tag]`) +- Subscribe to multiple runs by tag +- Real-time updates as new runs are created +- createdAt filtering + +### 4. Batch (`/batch/[id]`) +- Subscribe to all runs in a batch +- Track batch progress +- Individual run statuses + +### 5. Stream (`/stream/[id]`) +- Subscribe to a specific stream +- Start from specific index +- onData callback for each chunk +- Timeout handling + diff --git a/references/realtime-hooks-test/next.config.ts b/references/realtime-hooks-test/next.config.ts new file mode 100644 index 0000000000..7443abb674 --- /dev/null +++ b/references/realtime-hooks-test/next.config.ts @@ -0,0 +1,8 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; + diff --git a/references/realtime-hooks-test/package.json b/references/realtime-hooks-test/package.json new file mode 100644 index 0000000000..2128609f3a --- /dev/null +++ b/references/realtime-hooks-test/package.json @@ -0,0 +1,30 @@ +{ + "name": "references-realtime-hooks-test", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build --turbopack", + "start": "next start", + "dev:trigger": "trigger dev", + "deploy": "trigger deploy" + }, + "dependencies": { + "@trigger.dev/react-hooks": "workspace:*", + "@trigger.dev/sdk": "workspace:*", + "next": "15.5.6", + "react": "19.1.0", + "react-dom": "19.1.0", + "zod": "3.25.76" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "tailwindcss": "^4", + "trigger.dev": "workspace:*", + "typescript": "^5" + } +} + diff --git a/references/realtime-hooks-test/postcss.config.mjs b/references/realtime-hooks-test/postcss.config.mjs new file mode 100644 index 0000000000..fa4a1da8b5 --- /dev/null +++ b/references/realtime-hooks-test/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + diff --git a/references/realtime-hooks-test/public/.gitkeep b/references/realtime-hooks-test/public/.gitkeep new file mode 100644 index 0000000000..0b80529896 --- /dev/null +++ b/references/realtime-hooks-test/public/.gitkeep @@ -0,0 +1,2 @@ +# This file ensures the public directory is tracked by git + diff --git a/references/realtime-hooks-test/src/app/actions.ts b/references/realtime-hooks-test/src/app/actions.ts new file mode 100644 index 0000000000..9989d3d196 --- /dev/null +++ b/references/realtime-hooks-test/src/app/actions.ts @@ -0,0 +1,73 @@ +"use server"; + +import { tasks, batch, auth } from "@trigger.dev/sdk"; +import type { simpleTask } from "@/trigger/simple-task"; +import type { streamTask } from "@/trigger/stream-task"; +import type { taggedTask } from "@/trigger/tagged-task"; +import type { batchItemTask } from "@/trigger/batch-task"; +import { redirect } from "next/navigation"; + +export async function triggerSimpleTask(message: string, duration?: number) { + const handle = await tasks.trigger("simple-task", { + message, + duration, + }); + + redirect(`/run/${handle.id}?accessToken=${handle.publicAccessToken}`); +} + +export async function triggerStreamTask(scenario: "text" | "json" | "mixed", count?: number) { + const handle = await tasks.trigger("stream-task", { + scenario, + count, + }); + + redirect(`/run-with-streams/${handle.id}?accessToken=${handle.publicAccessToken}`); +} + +export async function triggerTaggedTask(userId: string, action: string, tags: string[]) { + const handle = await tasks.trigger( + "tagged-task", + { + userId, + action, + }, + { + tags, + } + ); + + const publicAccessToken = await auth.createPublicToken({ + scopes: { + read: { + tags: tags, + }, + }, + }); + + // Redirect to the tag page for the first tag + const tag = tags[0] || "test"; + redirect(`/runs-with-tag/${tag}?accessToken=${publicAccessToken}`); +} + +export async function triggerBatchTasks(itemCount: number) { + const items = Array.from({ length: itemCount }, (_, i) => ({ + payload: { + itemId: `item-${i + 1}`, + value: Math.floor(Math.random() * 100) + 1, + }, + })); + + const batchHandle = await tasks.batchTrigger("batch-item-task", items); + + redirect(`/batch/${batchHandle.batchId}?accessToken=${batchHandle.publicAccessToken}`); +} + +export async function triggerStreamOnlyTask() { + const handle = await tasks.trigger("stream-task", { + scenario: "text", + count: 30, + }); + + redirect(`/stream/${handle.id}?accessToken=${handle.publicAccessToken}`); +} diff --git a/references/realtime-hooks-test/src/app/batch/[id]/page.tsx b/references/realtime-hooks-test/src/app/batch/[id]/page.tsx new file mode 100644 index 0000000000..5a61a01cf7 --- /dev/null +++ b/references/realtime-hooks-test/src/app/batch/[id]/page.tsx @@ -0,0 +1,28 @@ +import { BatchViewer } from "@/components/batch-viewer"; + +type PageProps = { + params: Promise<{ id: string }>; + searchParams: Promise<{ accessToken: string }>; +}; + +export default async function BatchPage({ params, searchParams }: PageProps) { + const { id } = await params; + const { accessToken } = await searchParams; + + return ( +
+
+ + ← Back to Home + +

useRealtimeBatch Test

+

+ Monitoring all runs in a batch with real-time progress tracking +

+
+ + +
+ ); +} + diff --git a/references/realtime-hooks-test/src/app/globals.css b/references/realtime-hooks-test/src/app/globals.css new file mode 100644 index 0000000000..62546ef3f2 --- /dev/null +++ b/references/realtime-hooks-test/src/app/globals.css @@ -0,0 +1,80 @@ +@import "tailwindcss"; + +:root { + --background: #09090b; /* Zinc 950 */ + --foreground: #fafafa; /* Zinc 50 */ + --card: #18181b; /* Zinc 900 */ + --card-foreground: #fafafa; + --popover: #18181b; + --popover-foreground: #fafafa; + --primary: #fafafa; + --primary-foreground: #18181b; + --secondary: #27272a; /* Zinc 800 */ + --secondary-foreground: #fafafa; + --muted: #27272a; + --muted-foreground: #a1a1aa; /* Zinc 400 */ + --accent: #27272a; + --accent-foreground: #fafafa; + --destructive: #7f1d1d; + --destructive-foreground: #fafafa; + --border: #27272a; + --input: #27272a; + --ring: #d4d4d8; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + + --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; +} + +body { + background-color: var(--background); + color: var(--foreground); + font-family: var(--font-sans); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Scrollbar styling */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--background); +} + +::-webkit-scrollbar-thumb { + background: var(--secondary); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--muted-foreground); +} + +/* Syntax highlighting */ +pre { + font-family: var(--font-mono); +} diff --git a/references/realtime-hooks-test/src/app/layout.tsx b/references/realtime-hooks-test/src/app/layout.tsx new file mode 100644 index 0000000000..dd6672498e --- /dev/null +++ b/references/realtime-hooks-test/src/app/layout.tsx @@ -0,0 +1,33 @@ +import type { Metadata } from "next"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "Trigger.dev Hooks", + description: "Realtime hooks testing workbench", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + +
+
+ +
+
+ {children} +
+
+ + + ); +} diff --git a/references/realtime-hooks-test/src/app/page.tsx b/references/realtime-hooks-test/src/app/page.tsx new file mode 100644 index 0000000000..59aa721f50 --- /dev/null +++ b/references/realtime-hooks-test/src/app/page.tsx @@ -0,0 +1,140 @@ +import { + triggerSimpleTask, + triggerStreamTask, + triggerTaggedTask, + triggerBatchTasks, + triggerStreamOnlyTask, +} from "./actions"; + +function Card({ title, description, children }: { title: string; description: string; children: React.ReactNode }) { + return ( +
+
+

{title}

+

{description}

+
+
+ {children} +
+
+ ); +} + +function Button({ children, variant = "default", ...props }: React.ButtonHTMLAttributes & { variant?: "default" | "secondary" | "outline" }) { + const variants = { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", + outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground" + }; + + return ( + + ); +} + +export default function Home() { + return ( +
+
+ {/* useRealtimeRun */} + +
{ + "use server"; + await triggerSimpleTask("Quick test run", 5); + }}> + +
+
{ + "use server"; + await triggerSimpleTask("Longer test run", 15); + }}> + +
+
+ + {/* useRealtimeRunWithStreams */} + +
+
{ + "use server"; + await triggerStreamTask("text", 20); + }}> + +
+
{ + "use server"; + await triggerStreamTask("json", 20); + }}> + +
+
{ + "use server"; + await triggerStreamTask("mixed", 30); + }}> + +
+
+
+ + {/* useRealtimeRunsWithTag */} + +
{ + "use server"; + await triggerTaggedTask("user-123", "process-order", ["order-processing", "user-123"]); + }}> + +
+
{ + "use server"; + await triggerTaggedTask("user-456", "send-email", ["notifications", "user-456"]); + }}> + +
+
+ + {/* useRealtimeBatch */} + +
+
{ + "use server"; + await triggerBatchTasks(5); + }}> + +
+
{ + "use server"; + await triggerBatchTasks(10); + }}> + +
+
{ + "use server"; + await triggerBatchTasks(20); + }}> + +
+
+
+ + {/* useRealtimeStream */} + +
{ + "use server"; + await triggerStreamOnlyTask(); + }}> + +
+
+
+ +
+

Test environment ready. Click any trigger to start a session.

+
+
+ ); +} diff --git a/references/realtime-hooks-test/src/app/run-with-streams/[id]/page.tsx b/references/realtime-hooks-test/src/app/run-with-streams/[id]/page.tsx new file mode 100644 index 0000000000..b3c9d8f94b --- /dev/null +++ b/references/realtime-hooks-test/src/app/run-with-streams/[id]/page.tsx @@ -0,0 +1,28 @@ +import { RunWithStreamsViewer } from "@/components/run-with-streams-viewer"; + +type PageProps = { + params: Promise<{ id: string }>; + searchParams: Promise<{ accessToken: string }>; +}; + +export default async function RunWithStreamsPage({ params, searchParams }: PageProps) { + const { id } = await params; + const { accessToken } = await searchParams; + + return ( +
+
+ + ← Back to Home + +

useRealtimeRunWithStreams Test

+

+ Monitoring a task run with multiple real-time streams +

+
+ + +
+ ); +} + diff --git a/references/realtime-hooks-test/src/app/run/[id]/page.tsx b/references/realtime-hooks-test/src/app/run/[id]/page.tsx new file mode 100644 index 0000000000..06ec1e05cd --- /dev/null +++ b/references/realtime-hooks-test/src/app/run/[id]/page.tsx @@ -0,0 +1,28 @@ +import { RunViewer } from "@/components/run-viewer"; + +type PageProps = { + params: Promise<{ id: string }>; + searchParams: Promise<{ accessToken: string }>; +}; + +export default async function RunPage({ params, searchParams }: PageProps) { + const { id } = await params; + const { accessToken } = await searchParams; + + return ( +
+
+ + ← Back to Home + +

useRealtimeRun Test

+

+ Monitoring a single task run with real-time updates +

+
+ + +
+ ); +} + diff --git a/references/realtime-hooks-test/src/app/runs-with-tag/[tag]/page.tsx b/references/realtime-hooks-test/src/app/runs-with-tag/[tag]/page.tsx new file mode 100644 index 0000000000..41be79162b --- /dev/null +++ b/references/realtime-hooks-test/src/app/runs-with-tag/[tag]/page.tsx @@ -0,0 +1,34 @@ +import { RunsWithTagViewer } from "@/components/runs-with-tag-viewer"; + +type PageProps = { + params: Promise<{ tag: string }>; + searchParams: Promise<{ accessToken: string }>; +}; + +export default async function RunsWithTagPage({ params, searchParams }: PageProps) { + const { tag } = await params; + const { accessToken } = await searchParams; + + return ( +
+
+ + ← Back to Home + +

useRealtimeRunsWithTag Test

+

+ Monitoring all runs with tag: {tag} +

+
+ + + +
+

+ Tip: Trigger more tasks with this tag from the home page to see them appear here in real-time! +

+
+
+ ); +} + diff --git a/references/realtime-hooks-test/src/app/stream/[id]/page.tsx b/references/realtime-hooks-test/src/app/stream/[id]/page.tsx new file mode 100644 index 0000000000..bbdf929e60 --- /dev/null +++ b/references/realtime-hooks-test/src/app/stream/[id]/page.tsx @@ -0,0 +1,28 @@ +import { StreamViewer } from "@/components/stream-viewer"; + +type PageProps = { + params: Promise<{ id: string }>; + searchParams: Promise<{ accessToken: string }>; +}; + +export default async function StreamPage({ params, searchParams }: PageProps) { + const { id } = await params; + const { accessToken } = await searchParams; + + return ( +
+
+ + ← Back to Home + +

useRealtimeStream Test

+

+ Subscribing to a specific stream with real-time chunk delivery +

+
+ + +
+ ); +} + diff --git a/references/realtime-hooks-test/src/components/batch-viewer.tsx b/references/realtime-hooks-test/src/components/batch-viewer.tsx new file mode 100644 index 0000000000..3967a47b2e --- /dev/null +++ b/references/realtime-hooks-test/src/components/batch-viewer.tsx @@ -0,0 +1,125 @@ +"use client"; + +import { useRealtimeBatch } from "@trigger.dev/react-hooks"; +import type { batchItemTask } from "@/trigger/batch-task"; + +type BatchViewerProps = { + batchId: string; + accessToken: string; +}; + +function StatusDot({ status }: { status: string }) { + const colors = { + COMPLETED: "bg-emerald-500", + EXECUTING: "bg-blue-500 animate-pulse", + FAILED: "bg-red-500", + PENDING: "bg-zinc-500", + QUEUED: "bg-yellow-500", + }; + const color = colors[status as keyof typeof colors] || colors.PENDING; + return
; + } + +export function BatchViewer({ batchId, accessToken }: BatchViewerProps) { + const { runs, error, stop } = useRealtimeBatch(batchId, { + accessToken, + baseURL: process.env.NEXT_PUBLIC_TRIGGER_API_URL, + }); + + if (error) { + return ( +
+
Connection Error
+
{error.message}
+
+ ); + } + + const totalRuns = runs.length; + const completedRuns = runs.filter((r) => r.status === "COMPLETED").length; + const batchProgress = totalRuns > 0 ? completedRuns / totalRuns : 0; + + return ( +
+ {/* Header */} +
+
+
+

Batch Progress

+

{batchId}

+
+ +
+ +
+
+ Completion + {completedRuns} / {totalRuns} +
+
+
+
+
+
+ + {/* Runs Grid */} +
+
+ Items + {runs.length} Tasks +
+
+ {runs.length === 0 ? ( +
+ Waiting for batch runs... +
+ ) : ( + runs.map((run) => { + const progress = (run.metadata as any)?.progress || 0; + const itemId = (run.metadata as any)?.itemId || "Item"; + const inputValue = (run.metadata as any)?.inputValue; + const result = (run.output as any)?.result; + + return ( +
+
+ +
+
+
{itemId}
+
{run.id.slice(-8)}
+
+
+
+
+
+
+
+
+
+ {result !== undefined ? ( + Result: {result} + ) : ( + Input: {inputValue} + )} +
+
+ ) + }) + )} +
+
+
+ ); +} diff --git a/references/realtime-hooks-test/src/components/run-viewer.tsx b/references/realtime-hooks-test/src/components/run-viewer.tsx new file mode 100644 index 0000000000..3844d61542 --- /dev/null +++ b/references/realtime-hooks-test/src/components/run-viewer.tsx @@ -0,0 +1,150 @@ +"use client"; + +import { useRealtimeRun } from "@trigger.dev/react-hooks"; +import type { simpleTask } from "@/trigger/simple-task"; +import { useState } from "react"; + +type RunViewerProps = { + runId: string; + accessToken: string; +}; + +function StatusBadge({ status }: { status: string }) { + const colors = { + COMPLETED: "bg-emerald-500/15 text-emerald-500 border-emerald-500/20", + EXECUTING: "bg-blue-500/15 text-blue-500 border-blue-500/20", + FAILED: "bg-red-500/15 text-red-500 border-red-500/20", + PENDING: "bg-zinc-500/15 text-zinc-500 border-zinc-500/20", + QUEUED: "bg-yellow-500/15 text-yellow-500 border-yellow-500/20", + }; + + const colorClass = colors[status as keyof typeof colors] || colors.PENDING; + + return ( + + {status} + + ); +} + +function CodeBlock({ label, data }: { label: string, data: any }) { + if (!data) return null; + return ( +
+

{label}

+
+        {JSON.stringify(data, null, 2)}
+      
+
+ ); +} + +export function RunViewer({ runId, accessToken }: RunViewerProps) { + const [stopOnCompletion, setStopOnCompletion] = useState(true); + + const { run, error, stop } = useRealtimeRun(runId, { + accessToken, + baseURL: process.env.NEXT_PUBLIC_TRIGGER_API_URL, + stopOnCompletion, + }); + + if (error) { + return ( +
+
Connection Error
+
{error.message}
+
+ ); + } + + if (!run) { + return ( +
+
+
+
+ ); + } + + const progress = (run.metadata as any)?.progress || 0; + const currentStep = (run.metadata as any)?.currentStep || 0; + const totalSteps = (run.metadata as any)?.totalSteps || 0; + + return ( +
+ {/* Header Bar */} +
+
+ +
{run.id}
+
+
+ + +
+
+ + {/* Progress Section */} +
+
+
+

Task Progress

+

+ Step {currentStep} of {totalSteps} +

+
+
+ {Math.round(progress * 100)}% +
+
+
+
+
+
+ + {/* Data Grid */} +
+
+

Metadata

+
+ {Object.entries(run.metadata || {}).map(([key, value]) => ( +
+ {key} + + {typeof value === 'object' ? JSON.stringify(value) : String(value)} + +
+ ))} +
+
+ +
+ + + {run.error && ( +
+

Error: {run.error.name}

+

{run.error.message}

+
+ )} +
+
+
+ ); +} diff --git a/references/realtime-hooks-test/src/components/run-with-streams-viewer.tsx b/references/realtime-hooks-test/src/components/run-with-streams-viewer.tsx new file mode 100644 index 0000000000..bb4022bf99 --- /dev/null +++ b/references/realtime-hooks-test/src/components/run-with-streams-viewer.tsx @@ -0,0 +1,133 @@ +"use client"; + +import { useRealtimeRunWithStreams } from "@trigger.dev/react-hooks"; +import type { streamTask } from "@/trigger/stream-task"; +import { useState } from "react"; + +type StreamResults = { + text: string[]; + data: Array<{ step: number; data: string; timestamp: number }>; +}; + +type RunWithStreamsViewerProps = { + runId: string; + accessToken: string; +}; + +export function RunWithStreamsViewer({ runId, accessToken }: RunWithStreamsViewerProps) { + const { run, streams, error, stop } = useRealtimeRunWithStreams< + typeof streamTask, + StreamResults + >(runId, { + accessToken, + baseURL: process.env.NEXT_PUBLIC_TRIGGER_API_URL, + throttleInMs: 50, + }); + + if (error) { + return ( +
+
Connection Error
+
{error.message}
+
+ ); + } + + if (!run) { + return ( +
+
+
+
+
+
+
+ ); + } + + const textStream = streams.text || []; + const dataStream = streams.data || []; + + return ( +
+ {/* Header */} +
+
+
+ Status + + {run.status} + +
+
+
+ Run ID + {run.id} +
+
+ +
+ + {/* Streams Split View */} +
+ {/* Text Stream Panel */} +
+
+

+ + Text Stream +

+ {textStream.length} chunks +
+
+ {textStream.join("") || Waiting for data...} +
+
+ + {/* Data Stream Panel */} +
+
+

+ + Data Stream +

+ {dataStream.length} items +
+
+
+ {dataStream.length === 0 && ( +
Waiting for data...
+ )} + {dataStream.map((item, index) => ( +
+
+ Step {item.step} + {new Date(item.timestamp).toLocaleTimeString()} +
+
{item.data}
+
+ ))} +
+
+
+
+ + {run.output && ( +
+
Task Output
+
+            {JSON.stringify(run.output, null, 2)}
+          
+
+ )} +
+ ); +} diff --git a/references/realtime-hooks-test/src/components/runs-with-tag-viewer.tsx b/references/realtime-hooks-test/src/components/runs-with-tag-viewer.tsx new file mode 100644 index 0000000000..c37bae704f --- /dev/null +++ b/references/realtime-hooks-test/src/components/runs-with-tag-viewer.tsx @@ -0,0 +1,156 @@ +"use client"; + +import { useRealtimeRunsWithTag } from "@trigger.dev/react-hooks"; +import type { taggedTask } from "@/trigger/tagged-task"; + +type RunsWithTagViewerProps = { + tag: string; + accessToken: string; +}; + +function StatusDot({ status }: { status: string }) { + const colors = { + COMPLETED: "bg-emerald-500", + EXECUTING: "bg-blue-500 animate-pulse", + FAILED: "bg-red-500", + PENDING: "bg-zinc-500", + QUEUED: "bg-yellow-500", + }; + const color = colors[status as keyof typeof colors] || colors.PENDING; + return
; +} + +export function RunsWithTagViewer({ tag, accessToken }: RunsWithTagViewerProps) { + const { runs, error, stop } = useRealtimeRunsWithTag([tag], { + accessToken, + baseURL: process.env.NEXT_PUBLIC_TRIGGER_API_URL, + createdAt: "1h", + }); + + if (error) { + return ( +
+
Connection Error
+
{error.message}
+
+ ); + } + + const stats = { + total: runs.length, + executing: runs.filter((r) => r.status === "EXECUTING").length, + completed: runs.filter((r) => r.status === "COMPLETED").length, + failed: runs.filter((r) => r.status === "FAILED").length, + }; + + return ( +
+ {/* Header & Stats */} +
+
+
+ Total Runs +
+
{stats.total}
+
+
+
+ Executing +
+
{stats.executing}
+
+
+
+ Completed +
+
{stats.completed}
+
+
+
+ Failed +
+
{stats.failed}
+
+
+ + {/* Controls */} +
+
+ Watching tag: + + {tag} + +
+ +
+ + {/* Runs List */} +
+
+
Status
+
Run ID
+
User / Action
+
Progress
+
Age
+
+ +
+ {runs.length === 0 ? ( +
+ No runs found with tag "{tag}" in the last hour +
+ ) : ( + runs.map((run) => { + const progress = (run.metadata as any)?.progress || 0; + const userId = (run.metadata as any)?.userId || "-"; + const action = (run.metadata as any)?.action || "-"; + + return ( +
+
+ +
+
+ {run.id} +
+
+ {userId} + / + {action} +
+
+
+
+
+
+ + {Math.round(progress * 100)}% + +
+
+
+ {run.createdAt ? new Date(run.createdAt).toLocaleTimeString() : "-"} +
+
+ ); + }) + )} +
+
+
+ ); +} diff --git a/references/realtime-hooks-test/src/components/stream-viewer.tsx b/references/realtime-hooks-test/src/components/stream-viewer.tsx new file mode 100644 index 0000000000..cad46a9c5c --- /dev/null +++ b/references/realtime-hooks-test/src/components/stream-viewer.tsx @@ -0,0 +1,136 @@ +"use client"; + +import { useRealtimeStream } from "@trigger.dev/react-hooks"; +import { textStream } from "@/trigger/streams"; +import { useState, useEffect, useRef } from "react"; + +type StreamViewerProps = { + runId: string; + accessToken: string; +}; + +export function StreamViewer({ runId, accessToken }: StreamViewerProps) { + const [chunkLog, setChunkLog] = useState>([]); + const scrollRef = useRef(null); + + const { parts, error, stop } = useRealtimeStream(textStream, runId, { + accessToken, + baseURL: process.env.NEXT_PUBLIC_TRIGGER_API_URL, + throttleInMs: 50, + timeoutInSeconds: 120, + onData: (chunk) => { + setChunkLog((prev) => [...prev, { chunk, timestamp: Date.now() }]); + }, + }); + + // Auto-scroll to bottom + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [parts]); + + if (error) { + return ( +
+
Connection Error
+
{error.message}
+
+ ); + } + + const fullText = parts.join(""); + + return ( +
+ {/* Header */} +
+
+

Stream Monitor

+
+ + {runId} + + + {parts.length} chunks + + {fullText.length} chars +
+
+ +
+ +
+ {/* Main Output Console */} +
+
+ OUTPUT + UTF-8 +
+
+ {fullText || ( + Waiting for stream output... + )} + {/* Cursor blink effect */} + +
+
+ + {/* Chunk Log Table */} +
+
+ Packet Log +
+
+ + + + + + + + + + {chunkLog.length === 0 ? ( + + + + ) : ( + chunkLog.map((log, i) => ( + + + + + + )) + )} + +
TimeChunk PreviewSize
+ No packets received +
+ {new Date(log.timestamp).toLocaleTimeString().split(" ")[0]}. + + {String(log.timestamp % 1000).padStart(3, "0")} + + + {log.chunk.replace(/\n/g, "↵")} + + {log.chunk.length}B +
+
+
+
+
+ ); +} diff --git a/references/realtime-hooks-test/src/trigger/batch-task.ts b/references/realtime-hooks-test/src/trigger/batch-task.ts new file mode 100644 index 0000000000..fc44a6f24f --- /dev/null +++ b/references/realtime-hooks-test/src/trigger/batch-task.ts @@ -0,0 +1,48 @@ +import { task, logger, metadata } from "@trigger.dev/sdk"; +import { setTimeout } from "timers/promises"; + +export type BatchItemPayload = { + itemId: string; + value: number; +}; + +export type BatchItemOutput = { + itemId: string; + result: number; + processedAt: string; +}; + +export const batchItemTask = task({ + id: "batch-item-task", + run: async (payload: BatchItemPayload) => { + logger.info("Processing batch item", payload); + + metadata.set("status", "processing"); + metadata.set("itemId", payload.itemId); + metadata.set("inputValue", payload.value); + + // Simulate processing with varying duration based on value + const duration = Math.floor(payload.value / 10) + 2; // 2-12 seconds + + for (let i = 0; i < duration; i++) { + await setTimeout(1000); + metadata.set("progress", (i + 1) / duration); + } + + metadata.set("status", "completed"); + + // Calculate some result + const result = payload.value * 2; + + const output: BatchItemOutput = { + itemId: payload.itemId, + result, + processedAt: new Date().toISOString(), + }; + + logger.info("Batch item completed", output); + + return output; + }, +}); + diff --git a/references/realtime-hooks-test/src/trigger/simple-task.ts b/references/realtime-hooks-test/src/trigger/simple-task.ts new file mode 100644 index 0000000000..ccdaf7fe82 --- /dev/null +++ b/references/realtime-hooks-test/src/trigger/simple-task.ts @@ -0,0 +1,53 @@ +import { task, logger, metadata } from "@trigger.dev/sdk"; +import { setTimeout } from "timers/promises"; + +export type SimpleTaskPayload = { + message: string; + duration?: number; +}; + +export type SimpleTaskOutput = { + message: string; + completedAt: string; +}; + +export const simpleTask = task({ + id: "simple-task", + run: async (payload: SimpleTaskPayload) => { + const duration = payload.duration || 10; + + logger.info("Starting simple task", { message: payload.message, duration }); + + // Update metadata to track progress + metadata.set("status", "initializing"); + metadata.set("progress", 0); + + await setTimeout(1000); + metadata.set("status", "processing"); + metadata.set("progress", 0.25); + + // Simulate work + for (let i = 0; i < duration; i++) { + await setTimeout(1000); + const progress = ((i + 1) / duration) * 0.75 + 0.25; + metadata.set("progress", progress); + metadata.set("currentStep", i + 1); + metadata.set("totalSteps", duration); + + logger.info(`Processing step ${i + 1}/${duration}`); + } + + metadata.set("status", "completed"); + metadata.set("progress", 1); + + const output: SimpleTaskOutput = { + message: `Processed: ${payload.message}`, + completedAt: new Date().toISOString(), + }; + + logger.info("Simple task completed", output); + + return output; + }, +}); + diff --git a/references/realtime-hooks-test/src/trigger/stream-task.ts b/references/realtime-hooks-test/src/trigger/stream-task.ts new file mode 100644 index 0000000000..355f0e51a9 --- /dev/null +++ b/references/realtime-hooks-test/src/trigger/stream-task.ts @@ -0,0 +1,106 @@ +import { task, logger, metadata, streams } from "@trigger.dev/sdk"; +import { setTimeout } from "timers/promises"; +import { dataStream, textStream } from "./streams"; + +export type StreamTaskPayload = { + scenario: "text" | "json" | "mixed"; + count?: number; +}; + +export type StreamTaskOutput = { + scenario: string; + totalChunks: number; +}; + +export const streamTask = task({ + id: "stream-task", + run: async (payload: StreamTaskPayload) => { + const count = payload.count || 20; + + logger.info("Starting stream task", { scenario: payload.scenario, count }); + + metadata.set("status", "streaming"); + metadata.set("scenario", payload.scenario); + + switch (payload.scenario) { + case "text": { + // Stream text chunks + const words = [ + "The", + "quick", + "brown", + "fox", + "jumps", + "over", + "the", + "lazy", + "dog", + "while", + "demonstrating", + "real-time", + "streaming", + "capabilities", + ]; + + for (let i = 0; i < count; i++) { + await setTimeout(100); + await textStream.append(words[i % words.length] + " "); + metadata.set("progress", (i + 1) / count); + } + + break; + } + + case "json": { + // Stream structured data + for (let i = 0; i < count; i++) { + await setTimeout(100); + await dataStream.append({ + step: i + 1, + data: `Processing item ${i + 1}`, + timestamp: Date.now(), + }); + metadata.set("progress", (i + 1) / count); + } + + break; + } + + case "mixed": { + // Stream to both streams + const words = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon"]; + + for (let i = 0; i < count; i++) { + await setTimeout(100); + + // Alternate between streams + if (i % 2 === 0) { + await textStream.append(words[i % words.length] + " "); + } else { + await dataStream.append({ + step: i + 1, + data: `Data point ${i + 1}`, + timestamp: Date.now(), + }); + } + + metadata.set("progress", (i + 1) / count); + } + + break; + } + } + + metadata.set("status", "completed"); + metadata.set("progress", 1); + + const output: StreamTaskOutput = { + scenario: payload.scenario, + totalChunks: count, + }; + + logger.info("Stream task completed", output); + + return output; + }, +}); diff --git a/references/realtime-hooks-test/src/trigger/streams.ts b/references/realtime-hooks-test/src/trigger/streams.ts new file mode 100644 index 0000000000..a0b46cfb20 --- /dev/null +++ b/references/realtime-hooks-test/src/trigger/streams.ts @@ -0,0 +1,9 @@ +import { streams } from "@trigger.dev/sdk"; + +export const textStream = streams.define({ + id: "text", +}); + +export const dataStream = streams.define<{ step: number; data: string; timestamp: number }>({ + id: "data", +}); diff --git a/references/realtime-hooks-test/src/trigger/tagged-task.ts b/references/realtime-hooks-test/src/trigger/tagged-task.ts new file mode 100644 index 0000000000..e9f30f522f --- /dev/null +++ b/references/realtime-hooks-test/src/trigger/tagged-task.ts @@ -0,0 +1,45 @@ +import { task, logger, metadata } from "@trigger.dev/sdk"; +import { setTimeout } from "timers/promises"; + +export type TaggedTaskPayload = { + userId: string; + action: string; +}; + +export type TaggedTaskOutput = { + userId: string; + action: string; + processedAt: string; +}; + +export const taggedTask = task({ + id: "tagged-task", + run: async (payload: TaggedTaskPayload) => { + logger.info("Starting tagged task", payload); + + metadata.set("status", "processing"); + metadata.set("userId", payload.userId); + metadata.set("action", payload.action); + + // Simulate some work + const steps = 5; + for (let i = 0; i < steps; i++) { + await setTimeout(1000); + metadata.set("progress", (i + 1) / steps); + logger.info(`Processing step ${i + 1}/${steps} for user ${payload.userId}`); + } + + metadata.set("status", "completed"); + + const output: TaggedTaskOutput = { + userId: payload.userId, + action: payload.action, + processedAt: new Date().toISOString(), + }; + + logger.info("Tagged task completed", output); + + return output; + }, +}); + diff --git a/references/realtime-hooks-test/tailwind.config.ts b/references/realtime-hooks-test/tailwind.config.ts new file mode 100644 index 0000000000..91ca64310d --- /dev/null +++ b/references/realtime-hooks-test/tailwind.config.ts @@ -0,0 +1,14 @@ +import type { Config } from "tailwindcss"; + +export default { + content: [ + "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", + "./src/components/**/*.{js,ts,jsx,tsx,mdx}", + "./src/app/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: {}, + }, + plugins: [], +} satisfies Config; + diff --git a/references/realtime-hooks-test/trigger.config.ts b/references/realtime-hooks-test/trigger.config.ts new file mode 100644 index 0000000000..7346fbeec0 --- /dev/null +++ b/references/realtime-hooks-test/trigger.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "@trigger.dev/sdk"; + +export default defineConfig({ + project: process.env.TRIGGER_PROJECT_REF!, + dirs: ["./src/trigger"], + maxDuration: 3600, +}); diff --git a/references/realtime-hooks-test/tsconfig.json b/references/realtime-hooks-test/tsconfig.json new file mode 100644 index 0000000000..7f64e8882a --- /dev/null +++ b/references/realtime-hooks-test/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} +