11const PROJECT_EVENT_TYPES = [ "created" , "updated" , "deleted" ] as const ;
22
3- export interface ProjectEvent {
3+ interface ProjectEvent {
44 type : ( typeof PROJECT_EVENT_TYPES ) [ number ] ;
55 configId : string ;
66}
@@ -122,7 +122,7 @@ class ReplaneRemoteStorage implements ReplaneStorage {
122122 this . closeController . signal ,
123123 options . signal ,
124124 ] ) ;
125- const events = fetchSse ( {
125+ const rawEvents = fetchSse ( {
126126 fetchFn : options . fetchFn ,
127127 headers : {
128128 Authorization : this . getAuthHeader ( options ) ,
@@ -133,7 +133,8 @@ class ReplaneRemoteStorage implements ReplaneStorage {
133133 url : this . getApiEndpoint ( "/v1/events" , options ) ,
134134 } ) ;
135135
136- for await ( const event of events ) {
136+ for await ( const rawEvent of rawEvents ) {
137+ const event = JSON . parse ( rawEvent ) ;
137138 if (
138139 typeof event === "object" &&
139140 event !== null &&
@@ -315,7 +316,7 @@ export interface ReplaneLogger {
315316 error ( ...args : any [ ] ) : void ;
316317}
317318
318- export interface GetConfigOptions < T > extends Partial < ReplaneClientOptions > { }
319+ export interface GetConfigOptions extends Partial < ReplaneClientOptions > { }
319320
320321export interface ConfigValueWatcher < T > {
321322 /** Current config value (or fallback if not found). */
@@ -328,12 +329,12 @@ export interface ReplaneClient {
328329 /** Fetch a config value by name. */
329330 getConfigValue < T = unknown > (
330331 configName : string ,
331- options ?: GetConfigOptions < T >
332+ options ?: GetConfigOptions
332333 ) : Promise < T | undefined > ;
333334 /** Watch a config value by name. */
334335 watchConfigValue < T = unknown > (
335336 configName : string ,
336- options ?: GetConfigOptions < T >
337+ options ?: GetConfigOptions
337338 ) : Promise < ConfigValueWatcher < T > > ;
338339 /** Close the client and clean up resources. */
339340 close ( ) : void ;
@@ -399,7 +400,7 @@ function _createReplaneClient(
399400
400401 async function getConfigValue < T = unknown > (
401402 configName : string ,
402- inputOptions : GetConfigOptions < T > = { }
403+ inputOptions : GetConfigOptions = { }
403404 ) : Promise < T > {
404405 return await storage . getConfigValue < T > ( {
405406 configName,
@@ -414,7 +415,7 @@ function _createReplaneClient(
414415
415416 async function watchConfigValue < T = unknown > (
416417 configName : string ,
417- originalOptions : GetConfigOptions < T > = { }
418+ originalOptions : GetConfigOptions = { }
418419 ) : Promise < ConfigValueWatcher < T > > {
419420 const options = combineOptions ( sdkOptions , originalOptions ) ;
420421 let currentWatcherValue : T = await storage . getConfigValue < T > ( {
@@ -518,65 +519,82 @@ function combineOptions(
518519 } ;
519520}
520521
522+ const SSE_DATA_PREFIX = "data:" ;
523+
521524async function * fetchSse ( params : {
522525 fetchFn : typeof fetch ;
523526 url : string ;
524- headers : Record < string , string > ;
525- method : string ;
526- signal : AbortSignal ;
527+ headers ? : Record < string , string > ;
528+ method ? : string ;
529+ signal ? : AbortSignal ;
527530} ) {
528531 const abortController = new AbortController ( ) ;
532+ const signal = params . signal
533+ ? combineAbortSignals ( [ params . signal , abortController . signal ] )
534+ : abortController . signal ;
529535
530- const signal = combineAbortSignals ( [ params . signal , abortController . signal ] ) ;
531-
532- const response = await fetch ( params . url , {
533- method : params . method ,
534- headers : params . headers ,
536+ const res = await params . fetchFn ( params . url , {
537+ method : params . method ?? "GET" ,
538+ headers : { Accept : "text/event-stream" , ...( params . headers ?? { } ) } ,
535539 signal,
536540 } ) ;
537541
538- if ( response . status !== 200 ) {
539- throw new Error ( "Failed to fetch SSE endpoint: " + response . statusText ) ;
540- }
542+ await ensureSuccessfulResponse ( res , `SSE ${ params . url } ` ) ;
541543
542- await ensureSuccessfulResponse ( response , `Fetch SSE ${ params . url } ` ) ;
543-
544- if ( response . body === null ) {
544+ if ( ! res . body ) {
545545 throw new ReplaneError ( {
546546 message : `Failed to fetch SSE ${ params . url } : body is empty` ,
547547 code : ReplaneErrorCode . Unknown ,
548548 } ) ;
549549 }
550550
551- const decodedResponse = new TextDecoderStream ( ) ;
552- await response . body . pipeTo ( decodedResponse . writable , {
553- signal,
554- } ) ;
551+ const decoded = res . body . pipeThrough ( new TextDecoderStream ( ) ) ;
552+ const reader = decoded . getReader ( ) ;
555553
556- let leftover : string = "" ;
554+ let buffer = "" ;
557555
558556 try {
559- for await ( const responsePart of decodedResponse . readable ) {
560- leftover += responsePart ;
561-
562- let messages = leftover . split ( "\n\n" ) ;
563- leftover = messages . at ( - 1 ) ?? "" ;
564-
565- for ( const message of messages . slice ( 0 , - 1 ) ) {
566- if ( ! message . startsWith ( SSE_DATA_MESSAGE_PREFIX ) ) continue ;
557+ while ( true ) {
558+ const { value, done } = await reader . read ( ) ;
559+ if ( done ) break ;
560+ buffer += value ! ;
561+
562+ // Split on blank line; handle both \n\n and \r\n\r\n
563+ const frames = buffer . split ( / \r ? \n \r ? \n / ) ;
564+ buffer = frames . pop ( ) ?? "" ;
565+
566+ for ( const frame of frames ) {
567+ // Parse lines inside a single SSE event frame
568+ let dataLines : string [ ] = [ ] ;
569+
570+ for ( const rawLine of frame . split ( / \r ? \n / ) ) {
571+ if ( ! rawLine ) continue ;
572+ if ( rawLine . startsWith ( ":" ) ) continue ; // comment/keepalive
573+
574+ if ( rawLine . startsWith ( SSE_DATA_PREFIX ) ) {
575+ // Keep leading space after "data:" if present per spec
576+ const line = rawLine
577+ . slice ( SSE_DATA_PREFIX . length )
578+ . replace ( / ^ \s / , "" ) ;
579+ dataLines . push ( line ) ;
580+ }
581+ // Optionally handle event:, id:, retry: here if you need them
582+ }
567583
568- yield JSON . parse (
569- message . slice ( SSE_DATA_MESSAGE_PREFIX . length )
570- ) as unknown ;
584+ if ( dataLines . length ) {
585+ const payload = dataLines . join ( "\n" ) ;
586+ yield payload ;
587+ }
571588 }
572589 }
573590 } finally {
574591 abortController . abort ( ) ;
592+ try {
593+ await reader . cancel ( ) ;
594+ } catch { }
575595 }
576596}
577597
578- const SSE_DATA_MESSAGE_PREFIX = "data: " ;
579-
580598async function ensureSuccessfulResponse ( response : Response , message : string ) {
581599 if ( response . status === 404 ) {
582600 throw new ReplaneError ( {
@@ -726,13 +744,13 @@ class Subject<T> implements Observable<T>, Observer<T> {
726744 }
727745}
728746
729- export interface DebouncerOptions {
747+ interface DebouncerOptions {
730748 name : string ;
731749 task : ( ) => Promise < void > ;
732750 onError : ( err : unknown ) => void ;
733751}
734752
735- export class Debouncer {
753+ class Debouncer {
736754 private stopped = false ;
737755 private running = false ;
738756 private rescheduleRequested = false ;
0 commit comments