1- import { EventSourceParserStream } from "eventsource-parser/stream" ;
1+ import { EventSourceMessage , EventSourceParserStream } from "eventsource-parser/stream" ;
22import { DeserializedJson } from "../../schemas/json.js" ;
33import { createJsonErrorObject } from "../errors.js" ;
4- import {
5- RunStatus ,
6- SubscribeRealtimeStreamChunkRawShape ,
7- SubscribeRunRawShape ,
8- } from "../schemas/api.js" ;
4+ import { RunStatus , SubscribeRunRawShape } from "../schemas/api.js" ;
95import { SerializedError } from "../schemas/common.js" ;
6+ import {
7+ AsyncIterableStream ,
8+ createAsyncIterableReadable ,
9+ } from "../streams/asyncIterableStream.js" ;
1010import { AnyRunTypes , AnyTask , InferRunTypes } from "../types/tasks.js" ;
1111import { getEnvVar } from "../utils/getEnv.js" ;
1212import {
@@ -16,11 +16,7 @@ import {
1616} from "../utils/ioSerialization.js" ;
1717import { ApiError } from "./errors.js" ;
1818import { ApiClient } from "./index.js" ;
19- import { LineTransformStream , zodShapeStream } from "./stream.js" ;
20- import {
21- AsyncIterableStream ,
22- createAsyncIterableReadable ,
23- } from "../streams/asyncIterableStream.js" ;
19+ import { zodShapeStream } from "./stream.js" ;
2420
2521export type RunShape < TRunTypes extends AnyRunTypes > = TRunTypes extends AnyRunTypes
2622 ? {
@@ -157,7 +153,7 @@ export function runShapeStream<TRunTypes extends AnyRunTypes>(
157153
158154// First, define interfaces for the stream handling
159155export interface StreamSubscription {
160- subscribe ( ) : Promise < ReadableStream < unknown > > ;
156+ subscribe ( ) : Promise < ReadableStream < SSEStreamPart < unknown > > > ;
161157}
162158
163159export type CreateStreamSubscriptionOptions = {
@@ -176,6 +172,12 @@ export interface StreamSubscriptionFactory {
176172 ) : StreamSubscription ;
177173}
178174
175+ export type SSEStreamPart < TChunk = unknown > = {
176+ id : string ;
177+ chunk : TChunk ;
178+ timestamp : number ;
179+ } ;
180+
179181// Real implementation for production
180182export class SSEStreamSubscription implements StreamSubscription {
181183 private lastEventId : string | undefined ;
@@ -197,7 +199,7 @@ export class SSEStreamSubscription implements StreamSubscription {
197199 this . lastEventId = options . lastEventId ;
198200 }
199201
200- async subscribe ( ) : Promise < ReadableStream < unknown > > {
202+ async subscribe ( ) : Promise < ReadableStream < SSEStreamPart > > {
201203 const self = this ;
202204
203205 return new ReadableStream ( {
@@ -210,7 +212,9 @@ export class SSEStreamSubscription implements StreamSubscription {
210212 } ) ;
211213 }
212214
213- private async connectStream ( controller : ReadableStreamDefaultController ) : Promise < void > {
215+ private async connectStream (
216+ controller : ReadableStreamDefaultController < SSEStreamPart >
217+ ) : Promise < void > {
214218 try {
215219 const headers : Record < string , string > = {
216220 Accept : "text/event-stream" ,
@@ -259,14 +263,21 @@ export class SSEStreamSubscription implements StreamSubscription {
259263 . pipeThrough ( new TextDecoderStream ( ) )
260264 . pipeThrough ( new EventSourceParserStream ( ) )
261265 . pipeThrough (
262- new TransformStream ( {
266+ new TransformStream < EventSourceMessage , SSEStreamPart > ( {
263267 transform : ( chunk , chunkController ) => {
264268 if ( streamVersion === "v1" ) {
265269 // Track the last event ID for resume support
266270 if ( chunk . id ) {
267271 this . lastEventId = chunk . id ;
268272 }
269- chunkController . enqueue ( safeParseJSON ( chunk . data ) ) ;
273+
274+ const timestamp = parseRedisStreamIdTimestamp ( chunk . id ) ;
275+
276+ chunkController . enqueue ( {
277+ id : chunk . id ?? "unknown" ,
278+ chunk : safeParseJSON ( chunk . data ) ,
279+ timestamp,
280+ } ) ;
270281 } else {
271282 if ( chunk . event === "batch" ) {
272283 const data = safeParseJSON ( chunk . data ) as {
@@ -276,7 +287,11 @@ export class SSEStreamSubscription implements StreamSubscription {
276287 for ( const record of data . records ) {
277288 this . lastEventId = record . seq_num . toString ( ) ;
278289
279- chunkController . enqueue ( safeParseJSON ( record . body ) ) ;
290+ chunkController . enqueue ( {
291+ id : record . seq_num . toString ( ) ,
292+ chunk : safeParseJSON ( record . body ) ,
293+ timestamp : record . timestamp ,
294+ } ) ;
280295 }
281296 }
282297 }
@@ -490,7 +505,7 @@ export class RunSubscription<TRunTypes extends AnyRunTypes> {
490505 transform ( chunk , controller ) {
491506 controller . enqueue ( {
492507 type : streamKey ,
493- chunk : chunk as TStreams [ typeof streamKey ] ,
508+ chunk : chunk . chunk as TStreams [ typeof streamKey ] ,
494509 run,
495510 } ) ;
496511 } ,
@@ -740,3 +755,17 @@ function getStreamsFromRunShape(run: AnyRunShape): string[] {
740755
741756 return run . realtimeStreams ;
742757}
758+
759+ // Redis stream IDs are in the format: <timestamp>-<sequence>
760+ function parseRedisStreamIdTimestamp ( id ?: string ) : number {
761+ if ( ! id ) {
762+ return Date . now ( ) ;
763+ }
764+
765+ const timestamp = parseInt ( id . split ( "-" ) [ 0 ] as string ) ;
766+ if ( isNaN ( timestamp ) ) {
767+ return Date . now ( ) ;
768+ }
769+
770+ return timestamp ;
771+ }
0 commit comments