From 70140579c8f0dd0f8fbf248200efc4e6fe277a08 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Fri, 13 Dec 2024 11:03:35 +0000 Subject: [PATCH] Fixed consuming realtime runs w/streams after the run is already finished --- packages/core/src/v3/apiClient/runStream.ts | 19 +- packages/core/src/v3/apiClient/stream.ts | 91 ++++++--- packages/react-hooks/src/hooks/useRealtime.ts | 12 +- pnpm-lock.yaml | 172 +++++++++++++++++- references/nextjs-realtime/package.json | 4 +- .../src/app/realtime/[id]/page.tsx | 18 ++ .../src/components/RunRealtimeComparison.tsx | 91 +++++++++ .../src/components/ui/tabs.tsx | 55 ++++++ 8 files changed, 416 insertions(+), 46 deletions(-) create mode 100644 references/nextjs-realtime/src/app/realtime/[id]/page.tsx create mode 100644 references/nextjs-realtime/src/components/RunRealtimeComparison.tsx create mode 100644 references/nextjs-realtime/src/components/ui/tabs.tsx diff --git a/packages/core/src/v3/apiClient/runStream.ts b/packages/core/src/v3/apiClient/runStream.ts index 48494f2781..37eaa0b931 100644 --- a/packages/core/src/v3/apiClient/runStream.ts +++ b/packages/core/src/v3/apiClient/runStream.ts @@ -116,11 +116,14 @@ export function runShapeStream( { once: true } ); + const runStreamInstance = zodShapeStream(SubscribeRunRawShape, url, { + ...options, + signal: abortController.signal, + }); + const $options: RunSubscriptionOptions = { - runShapeStream: zodShapeStream(SubscribeRunRawShape, url, { - ...options, - signal: abortController.signal, - }), + runShapeStream: runStreamInstance.stream, + stopRunShapeStream: runStreamInstance.stop, streamFactory: new VersionedStreamSubscriptionFactory(version1, version2), abortController, ...options, @@ -215,7 +218,7 @@ export class ElectricStreamSubscription implements StreamSubscription { async subscribe(): Promise> { return zodShapeStream(SubscribeRealtimeStreamChunkRawShape, this.url, this.options) - .pipeThrough( + .stream.pipeThrough( new TransformStream({ transform(chunk, controller) { controller.enqueue(chunk.value); @@ -298,12 +301,12 @@ export interface RunShapeProvider { export type RunSubscriptionOptions = RunShapeStreamOptions & { runShapeStream: ReadableStream; + stopRunShapeStream: () => void; streamFactory: StreamSubscriptionFactory; abortController: AbortController; }; export class RunSubscription { - private unsubscribeShape?: () => void; private stream: AsyncIterableStream>; private packetCache = new Map(); private _closeOnComplete: boolean; @@ -330,7 +333,7 @@ export class RunSubscription { ) { console.log("Closing stream because run is complete"); - this.options.abortController.abort(); + this.options.stopRunShapeStream(); } }, }, @@ -342,7 +345,7 @@ export class RunSubscription { if (!this.options.abortController.signal.aborted) { this.options.abortController.abort(); } - this.unsubscribeShape?.(); + this.options.stopRunShapeStream(); } [Symbol.asyncIterator](): AsyncIterator> { diff --git a/packages/core/src/v3/apiClient/stream.ts b/packages/core/src/v3/apiClient/stream.ts index 266b5bc4a8..edf264d68f 100644 --- a/packages/core/src/v3/apiClient/stream.ts +++ b/packages/core/src/v3/apiClient/stream.ts @@ -16,25 +16,40 @@ export type ZodShapeStreamOptions = { signal?: AbortSignal; }; +export type ZodShapeStreamInstance = { + stream: AsyncIterableStream>; + stop: () => void; +}; + export function zodShapeStream( schema: TShapeSchema, url: string, options?: ZodShapeStreamOptions -) { - const stream = new ShapeStream>({ +): ZodShapeStreamInstance { + const abortController = new AbortController(); + + options?.signal?.addEventListener( + "abort", + () => { + abortController.abort(); + }, + { once: true } + ); + + const shapeStream = new ShapeStream({ url, headers: { ...options?.headers, "x-trigger-electric-version": "1.0.0-beta.1", }, fetchClient: options?.fetchClient, - signal: options?.signal, + signal: abortController.signal, }); - const readableShape = new ReadableShapeStream(stream); + const readableShape = new ReadableShapeStream(shapeStream); - return readableShape.stream.pipeThrough( - new TransformStream({ + const stream = readableShape.stream.pipeThrough( + new TransformStream>({ async transform(chunk, controller) { const result = schema.safeParse(chunk); @@ -46,6 +61,14 @@ export function zodShapeStream( }, }) ); + + return { + stream: stream as AsyncIterableStream>, + stop: () => { + console.log("Stopping zodShapeStream with abortController.abort()"); + abortController.abort(); + }, + }; } export type AsyncIterableStream = AsyncIterable & ReadableStream; @@ -104,6 +127,11 @@ class ReadableShapeStream = Row> { readonly #currentState: Map = new Map(); readonly #changeStream: AsyncIterableStream; #error: FetchError | false = false; + #unsubscribe?: () => void; + + stop() { + this.#unsubscribe?.(); + } constructor(stream: ShapeStreamInterface) { this.#stream = stream; @@ -111,7 +139,7 @@ class ReadableShapeStream = Row> { // Create the source stream that will receive messages const source = new ReadableStream[]>({ start: (controller) => { - this.#stream.subscribe( + this.#unsubscribe = this.#stream.subscribe( (messages) => controller.enqueue(messages), this.#handleError.bind(this) ); @@ -121,41 +149,44 @@ class ReadableShapeStream = Row> { // Create the transformed stream that processes messages and emits complete rows this.#changeStream = createAsyncIterableStream(source, { transform: (messages, controller) => { - messages.forEach((message) => { + const updatedKeys = new Set(); + + for (const message of messages) { if (isChangeMessage(message)) { + const key = message.key; switch (message.headers.operation) { case "insert": { - this.#currentState.set(message.key, message.value); - controller.enqueue(message.value); + // New row entirely + this.#currentState.set(key, message.value); + updatedKeys.add(key); break; } case "update": { - const existingRow = this.#currentState.get(message.key); - if (existingRow) { - const updatedRow = { - ...existingRow, - ...message.value, - }; - this.#currentState.set(message.key, updatedRow); - controller.enqueue(updatedRow); - } else { - this.#currentState.set(message.key, message.value); - controller.enqueue(message.value); - } + // Merge updates into existing row if any, otherwise treat as new + const existingRow = this.#currentState.get(key); + const updatedRow = existingRow + ? { ...existingRow, ...message.value } + : message.value; + this.#currentState.set(key, updatedRow); + updatedKeys.add(key); break; } } + } else if (isControlMessage(message)) { + if (message.headers.control === "must-refetch") { + this.#currentState.clear(); + this.#error = false; + } } + } - if (isControlMessage(message)) { - switch (message.headers.control) { - case "must-refetch": - this.#currentState.clear(); - this.#error = false; - break; - } + // Now enqueue only one updated row per key, after all messages have been processed. + for (const key of updatedKeys) { + const finalRow = this.#currentState.get(key); + if (finalRow) { + controller.enqueue(finalRow); } - }); + } }, }); } diff --git a/packages/react-hooks/src/hooks/useRealtime.ts b/packages/react-hooks/src/hooks/useRealtime.ts index 9481125cff..8c370666a5 100644 --- a/packages/react-hooks/src/hooks/useRealtime.ts +++ b/packages/react-hooks/src/hooks/useRealtime.ts @@ -109,10 +109,13 @@ export function useRealtimeRun( } }, [runId, mutateRun, abortControllerRef, apiClient, setError]); + const hasCalledOnCompleteRef = useRef(false); + // Effect to handle onComplete callback useEffect(() => { - if (isComplete && options?.onComplete && run) { + if (isComplete && run && options?.onComplete && !hasCalledOnCompleteRef.current) { options.onComplete(run, error); + hasCalledOnCompleteRef.current = true; } }, [isComplete, run, error, options?.onComplete]); @@ -261,10 +264,13 @@ export function useRealtimeRunWithStreams< } }, [runId, mutateRun, mutateStreams, streamsRef, abortControllerRef, apiClient, setError]); + const hasCalledOnCompleteRef = useRef(false); + // Effect to handle onComplete callback useEffect(() => { - if (isComplete && options?.onComplete && run) { + if (isComplete && run && options?.onComplete && !hasCalledOnCompleteRef.current) { options.onComplete(run, error); + hasCalledOnCompleteRef.current = true; } }, [isComplete, run, error, options?.onComplete]); @@ -593,7 +599,7 @@ async function processRealtimeRunWithStreams< nextStreamData[type] = [...(existingDataRef.current[type] || []), ...chunks]; } - await mutateStreamData(nextStreamData); + mutateStreamData(nextStreamData); }, throttleInMs); for await (const part of subscription.withStreams()) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d3c5be19f5..a35e106532 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1619,8 +1619,11 @@ importers: '@radix-ui/react-slot': specifier: ^1.1.0 version: 1.1.0(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-tabs': + specifier: ^1.0.3 + version: 1.0.3(react-dom@18.2.0)(react@18.3.1) '@trigger.dev/react-hooks': - specifier: workspace:^3 + specifier: workspace:^ version: link:../../packages/react-hooks '@trigger.dev/sdk': specifier: workspace:^3 @@ -1637,6 +1640,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + date-fns: + specifier: ^4.1.0 + version: 4.1.0 lucide-react: specifier: ^0.451.0 version: 0.451.0(react@18.3.1) @@ -8971,6 +8977,21 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-collection@1.0.2(react-dom@18.2.0)(react@18.3.1): + resolution: {integrity: sha512-s8WdQQ6wNXpaxdZ308KSr8fEWGrg4un8i4r/w7fhiS4ElRNjk5rRcl0/C6TANG2LvLOGIxtzo/jAg6Qf73TEBw==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + dependencies: + '@babel/runtime': 7.24.5 + '@radix-ui/react-compose-refs': 1.0.0(react@18.3.1) + '@radix-ui/react-context': 1.0.0(react@18.3.1) + '@radix-ui/react-primitive': 1.0.2(react-dom@18.2.0)(react@18.3.1) + '@radix-ui/react-slot': 1.0.1(react@18.3.1) + react: 18.3.1 + react-dom: 18.2.0(react@18.3.1) + dev: false + /@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==} peerDependencies: @@ -9028,6 +9049,15 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-compose-refs@1.0.0(react@18.3.1): + resolution: {integrity: sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + dependencies: + '@babel/runtime': 7.24.5 + react: 18.3.1 + dev: false + /@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.69)(react@18.2.0): resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} peerDependencies: @@ -9105,6 +9135,15 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-context@1.0.0(react@18.3.1): + resolution: {integrity: sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + dependencies: + '@babel/runtime': 7.24.5 + react: 18.3.1 + dev: false + /@radix-ui/react-context@1.0.1(@types/react@18.2.69)(react@18.2.0): resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==} peerDependencies: @@ -9250,6 +9289,15 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-direction@1.0.0(react@18.3.1): + resolution: {integrity: sha512-2HV05lGUgYcA6xgLQ4BKPDmtL+QbIZYH5fCOTAOOcJ5O0QbWS3i9lKaurLzliYUDhORI2Qr3pyjhJh44lKA3rQ==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + dependencies: + '@babel/runtime': 7.24.5 + react: 18.3.1 + dev: false + /@radix-ui/react-direction@1.0.1(@types/react@18.2.69)(react@18.2.0): resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==} peerDependencies: @@ -9520,6 +9568,16 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-id@1.0.0(react@18.3.1): + resolution: {integrity: sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + dependencies: + '@babel/runtime': 7.24.5 + '@radix-ui/react-use-layout-effect': 1.0.0(react@18.3.1) + react: 18.3.1 + dev: false + /@radix-ui/react-id@1.0.1(@types/react@18.2.69)(react@18.2.0): resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==} peerDependencies: @@ -9796,6 +9854,19 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-presence@1.0.0(react-dom@18.2.0)(react@18.3.1): + resolution: {integrity: sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + dependencies: + '@babel/runtime': 7.24.5 + '@radix-ui/react-compose-refs': 1.0.0(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.0.0(react@18.3.1) + react: 18.3.1 + react-dom: 18.2.0(react@18.3.1) + dev: false + /@radix-ui/react-presence@1.0.1(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==} peerDependencies: @@ -9873,6 +9944,18 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-primitive@1.0.2(react-dom@18.2.0)(react@18.3.1): + resolution: {integrity: sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + dependencies: + '@babel/runtime': 7.24.5 + '@radix-ui/react-slot': 1.0.1(react@18.3.1) + react: 18.3.1 + react-dom: 18.2.0(react@18.3.1) + dev: false + /@radix-ui/react-primitive@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==} peerDependencies: @@ -9985,6 +10068,26 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-roving-focus@1.0.3(react-dom@18.2.0)(react@18.3.1): + resolution: {integrity: sha512-stjCkIoMe6h+1fWtXlA6cRfikdBzCLp3SnVk7c48cv/uy3DTGoXhN76YaOYUJuy3aEDvDIKwKR5KSmvrtPvQPQ==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + dependencies: + '@babel/runtime': 7.24.5 + '@radix-ui/primitive': 1.0.0 + '@radix-ui/react-collection': 1.0.2(react-dom@18.2.0)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.0.0(react@18.3.1) + '@radix-ui/react-context': 1.0.0(react@18.3.1) + '@radix-ui/react-direction': 1.0.0(react@18.3.1) + '@radix-ui/react-id': 1.0.0(react@18.3.1) + '@radix-ui/react-primitive': 1.0.2(react-dom@18.2.0)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.0(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.0.0(react@18.3.1) + react: 18.3.1 + react-dom: 18.2.0(react@18.3.1) + dev: false + /@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==} peerDependencies: @@ -10146,6 +10249,16 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-slot@1.0.1(react@18.3.1): + resolution: {integrity: sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + dependencies: + '@babel/runtime': 7.24.5 + '@radix-ui/react-compose-refs': 1.0.0(react@18.3.1) + react: 18.3.1 + dev: false + /@radix-ui/react-slot@1.0.2(@types/react@18.2.69)(react@18.2.0): resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} peerDependencies: @@ -10251,6 +10364,25 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-tabs@1.0.3(react-dom@18.2.0)(react@18.3.1): + resolution: {integrity: sha512-4CkF/Rx1GcrusI/JZ1Rvyx4okGUs6wEenWA0RG/N+CwkRhTy7t54y7BLsWUXrAz/GRbBfHQg/Odfs/RoW0CiRA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + dependencies: + '@babel/runtime': 7.24.5 + '@radix-ui/primitive': 1.0.0 + '@radix-ui/react-context': 1.0.0(react@18.3.1) + '@radix-ui/react-direction': 1.0.0(react@18.3.1) + '@radix-ui/react-id': 1.0.0(react@18.3.1) + '@radix-ui/react-presence': 1.0.0(react-dom@18.2.0)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.2(react-dom@18.2.0)(react@18.3.1) + '@radix-ui/react-roving-focus': 1.0.3(react-dom@18.2.0)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.0.0(react@18.3.1) + react: 18.3.1 + react-dom: 18.2.0(react@18.3.1) + dev: false + /@radix-ui/react-toggle-group@1.0.4(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0)(react@18.3.1): resolution: {integrity: sha512-Uaj/M/cMyiyT9Bx6fOZO0SAG4Cls0GptBWiBmBxofmDbNVnYYoyRWj/2M/6VCi/7qcXFWnHhRUfdfZFvvkuu8A==} peerDependencies: @@ -10367,6 +10499,15 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-use-callback-ref@1.0.0(react@18.3.1): + resolution: {integrity: sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + dependencies: + '@babel/runtime': 7.24.5 + react: 18.3.1 + dev: false + /@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.2.69)(react@18.2.0): resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==} peerDependencies: @@ -10418,6 +10559,16 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-use-controllable-state@1.0.0(react@18.3.1): + resolution: {integrity: sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + dependencies: + '@babel/runtime': 7.24.5 + '@radix-ui/react-use-callback-ref': 1.0.0(react@18.3.1) + react: 18.3.1 + dev: false + /@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.2.69)(react@18.2.0): resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==} peerDependencies: @@ -10497,6 +10648,15 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-use-layout-effect@1.0.0(react@18.3.1): + resolution: {integrity: sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + dependencies: + '@babel/runtime': 7.24.5 + react: 18.3.1 + dev: false + /@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.2.69)(react@18.2.0): resolution: {integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==} peerDependencies: @@ -17367,7 +17527,7 @@ packages: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} dependencies: - '@babel/runtime': 7.22.5 + '@babel/runtime': 7.24.5 cosmiconfig: 7.1.0 resolve: 1.22.8 dev: true @@ -18889,6 +19049,10 @@ packages: resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} dev: false + /date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + dev: false + /dayjs@1.11.13: resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} dev: false @@ -20268,7 +20432,7 @@ packages: peerDependencies: eslint: ^6.8.0 || ^7.0.0 || ^8.0.0 dependencies: - '@babel/runtime': 7.22.5 + '@babel/runtime': 7.24.5 '@testing-library/dom': 8.19.1 eslint: 8.31.0 requireindex: 1.2.0 @@ -20301,7 +20465,7 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 dependencies: - '@babel/runtime': 7.22.5 + '@babel/runtime': 7.24.5 aria-query: 5.3.0 array-includes: 3.1.6 array.prototype.flatmap: 1.3.1 diff --git a/references/nextjs-realtime/package.json b/references/nextjs-realtime/package.json index 75df027772..8985fc2a32 100644 --- a/references/nextjs-realtime/package.json +++ b/references/nextjs-realtime/package.json @@ -17,12 +17,14 @@ "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-scroll-area": "^1.2.0", "@radix-ui/react-slot": "^1.1.0", - "@trigger.dev/react-hooks": "workspace:^3", + "@radix-ui/react-tabs": "^1.0.3", + "@trigger.dev/react-hooks": "workspace:^", "@trigger.dev/sdk": "workspace:^3", "@uploadthing/react": "^7.0.3", "ai": "^4.0.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "lucide-react": "^0.451.0", "next": "14.2.15", "openai": "^4.68.4", diff --git a/references/nextjs-realtime/src/app/realtime/[id]/page.tsx b/references/nextjs-realtime/src/app/realtime/[id]/page.tsx new file mode 100644 index 0000000000..cd178367d6 --- /dev/null +++ b/references/nextjs-realtime/src/app/realtime/[id]/page.tsx @@ -0,0 +1,18 @@ +import RunRealtimeComparison from "@/components/RunRealtimeComparison"; +import { auth } from "@trigger.dev/sdk/v3"; + +export default async function RunRealtimeComparisonPage({ params }: { params: { id: string } }) { + const accessToken = await auth.createPublicToken({ + scopes: { + read: { + runs: params.id, + }, + }, + }); + + return ( +
+ +
+ ); +} diff --git a/references/nextjs-realtime/src/components/RunRealtimeComparison.tsx b/references/nextjs-realtime/src/components/RunRealtimeComparison.tsx new file mode 100644 index 0000000000..d102b1af5e --- /dev/null +++ b/references/nextjs-realtime/src/components/RunRealtimeComparison.tsx @@ -0,0 +1,91 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import type { STREAMS, openaiStreaming } from "@/trigger/ai"; +import { useRealtimeRunWithStreams } from "@trigger.dev/react-hooks"; + +export default function RealtimeComparison({ + accessToken, + runId, +}: { + accessToken: string; + runId: string; +}) { + const { streams, stop, run } = useRealtimeRunWithStreams(runId, { + accessToken: accessToken, + baseURL: process.env.NEXT_PUBLIC_TRIGGER_API_URL, + onComplete: (...args) => { + console.log("Run completed!", args); + }, + }); + + console.log("run", run); + + return ( +
+
+ + + {run && ( + + )} +
+
+
+ + + + + + + + + {(streams.openai ?? []).map((part, i) => ( + + + + + ))} + +
IDData
{i + 1} +
+ {JSON.stringify(part)} +
+
+
+
+ + + + + + + + + {(streams.openaiText ?? []).map((text, i) => ( + + + + + ))} + +
IDData
{i + 1} +
{text}
+
+
+
+
+ ); +} diff --git a/references/nextjs-realtime/src/components/ui/tabs.tsx b/references/nextjs-realtime/src/components/ui/tabs.tsx new file mode 100644 index 0000000000..0f4caebb11 --- /dev/null +++ b/references/nextjs-realtime/src/components/ui/tabs.tsx @@ -0,0 +1,55 @@ +"use client" + +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent }