Skip to content

feat(typescript): expose SSE event id and retry metadata#16677

Open
dvdaruri-art wants to merge 3 commits into
mainfrom
devin/1782307726-ts-sdk-sse-event-metadata
Open

feat(typescript): expose SSE event id and retry metadata#16677
dvdaruri-art wants to merge 3 commits into
mainfrom
devin/1782307726-ts-sdk-sse-event-metadata

Conversation

@dvdaruri-art

@dvdaruri-art dvdaruri-art commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Description

Adds lastEventId getter and events() async generator to the TypeScript SDK's Stream class, building on the ServerSentEvent<T> / withMetadata() support already on main. This gives users SSE spec-compliant access to the last event ID as an instance-level property (persists across events per the SSE spec).

Changes Made

  • Added _lastEventId instance variable to Stream class — tracks the last event ID at the instance level (persists across events per SSE spec), accessible via stream.lastEventId getter
  • Added events() async generator method — convenience alias for iterating ServerSentEvent<T> objects (equivalent to withMetadata())
  • Updated both iterDataMessages() and iterSseEvents() to use this._lastEventId instead of local variables, so the ID is accessible externally during/after iteration
  • Added unit tests for lastEventId tracking, null character rejection, and events() metadata iteration
  • Regenerated all 8 affected seed fixtures (server-sent-events, server-sent-events-resumable, server-sent-event-examples, server-sent-events-openapi, streaming/*, streaming-parameter)
  • Added changelog entry

Testing

  • Unit tests added for lastEventId, null char rejection, events() method in both discriminated and non-discriminated paths
  • All 8 seed fixtures regenerated and passing
  • All 50 CI checks passing

Link to Devin session: https://app.devin.ai/sessions/3fcaaa36e6f446809c50a8fe676b0b9e
Requested by: @dvdaruri-art


Open in Devin Review

Add ServerSentEvent<T> interface and events() method to Stream class.
Parse id: and retry: SSE fields in both discriminated and non-discriminated paths.
Expose lastEventId property for resumable streaming support.
Achieves parity with Python SDK's ServerSentEvent dataclass.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@dvdaruri-art dvdaruri-art requested a review from Swimburger as a code owner June 24, 2026 13:29
@dvdaruri-art dvdaruri-art self-assigned this Jun 24, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration devin-ai-integration Bot changed the title feat(typescript-sdk): expose SSE event id and retry metadata feat(typescript): expose SSE event id and retry metadata Jun 24, 2026

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

Open in Devin Review

Comment on lines +147 to +149
if (this.prefix != null && line.startsWith(RETRY_PREFIX)) {
continue;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 iterDataMessages recognizes retry: SSE fields but silently discards their values

In the non-discriminated SSE path (iterDataMessages), the retry: field is recognized and skipped at lines 147-149 to prevent it from being misinterpreted as data, but unlike id: (which is parsed and stored at lines 139-145), the retry value is never parsed or included in the yielded ServerSentEvent<T>. When users call events() on a non-discriminated SSE stream (i.e., no eventDiscriminator set), the yielded objects will never contain a retry field — even when retry: fields are present in the stream. This is inconsistent with the discriminated path (iterSseEvents at Stream.template.ts:217-221), which properly parses and yields retry, and contradicts the events() JSDoc ("Iterates over full SSE events including metadata (event, id, retry)").

Prompt for agents
In iterDataMessages (Stream.template.ts around line 147), the retry: field is recognized and skipped but the value is discarded. It should be parsed and tracked (similar to how id: is tracked in _lastEventId), then included in the yielded ServerSentEvent object at lines 164-167. Since iterDataMessages processes lines independently without event-block boundaries, the retry value could be tracked in a local variable and reset after each data yield, similar to how prefixSeen is reset. The yielded object should include the retry field: yield { data: message, eventId: this._lastEventId, retry: lastRetryValue }. Note that the same inconsistency exists for the event: field — event: lines are not recognized or tracked in iterDataMessages at all, despite events() promising event metadata.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in b4c3f50. iterDataMessages now parses retry: values and includes them in the yielded ServerSentEvent<T>, matching the discriminated path. The value resets after each data yield.

Comment on lines 161 to 168
return;
}
const message = await this.parse(fromJson(line));
yield message;
yield {
data: message,
eventId: this._lastEventId,
};
prefixSeen = false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 event: field is not tracked in the non-discriminated SSE path

In iterDataMessages, event: lines are not explicitly recognized — they fall through to the data-prefix check at line 151-158 and get silently skipped (since event: ... doesn't contain data:). The yielded ServerSentEvent<T> from this path never includes the event field. This is architecturally consistent with how iterDataMessages works (line-by-line without event-block semantics), but it means events() on a non-discriminated SSE stream provides less metadata than the discriminated path. The event: line association requires event-block boundaries (blank-line separation) to work correctly, so fixing this would require refactoring iterDataMessages to understand event blocks, effectively merging it with iterSseEvents. This is related to but distinct from the reported retry bug.

(Refers to lines 123-168)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — this is an inherent architectural limitation of iterDataMessages. This path processes data: lines independently (no blank-line event block accumulation), so there's no way to associate an event: line with a specific data: line without refactoring to full event-block parsing. Since non-discriminated SSE doesn't use the event: field for dispatch (that's what iterSseEvents with eventDiscriminator is for), this is by design. Users who need event: metadata should configure eventDiscriminator in their API definition, which routes to the full-featured iterSseEvents path.

@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

SDK Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-06-24T05:18:39Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
ts-sdk square 236s (n=5) 239s (n=5) 233s -3s (-1.3%)

main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via fern generate). main (E2E): full customer-observable time including build/test scripts (nightly baseline, informational). Delta is computed against generator-only baseline.
⚠️ = generation exited with a non-zero exit code (timing may not reflect a successful run).
Baseline from nightly runs on main (latest: 2026-06-24T05:18:39Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-06-24 22:24 UTC

dvdaruri-art and others added 2 commits June 24, 2026 13:47
Address Devin Review feedback: iterDataMessages now parses and yields
retry values instead of silently discarding them.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant