-
-
Notifications
You must be signed in to change notification settings - Fork 32
feat: Added SSE Last Event Id Header #1104
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR implements SSE (Server-Sent Events) reconnection support using the Last-Event-ID mechanism to allow clients to resume receiving events from where they left off after a disconnection.
Key Changes:
- Added Last-Event-ID header support in the SSE stream endpoint
- Modified the message buffer subscribe method to support resuming from a specific event ID
- Refactored telemetry component to eliminate redundant useMemo usage
Reviewed Changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| packages/sidecar/src/routes/stream/index.ts | Extracts Last-Event-ID header and passes it to subscribe; adds event IDs to SSE messages |
| packages/sidecar/src/messageBuffer.ts | Adds lastEventId parameter to subscribe method with logic to find and resume from the last received event |
| packages/overlay/src/telemetry/index.tsx | Refactors contentTypeListeners from useMemo to inline object literal in useEffect |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| if (item[1] instanceof EventContainer) { | ||
| const envelope = item[1].getParsedEnvelope(); | ||
| if (envelope?.envelope && envelope.envelope[0].__spotlight_envelope_id === lastEventId) { |
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comparison envelope.envelope[0].__spotlight_envelope_id === lastEventId may have a type mismatch issue. The __spotlight_envelope_id is generated using uuidv7() which returns a string, but lastEventId comes from an HTTP header and is already a string. However, for consistency and safety, consider using strict equality with explicit string conversion: String(envelope.envelope[0].__spotlight_envelope_id) === lastEventId as done elsewhere in the codebase (lines 26, 51, 243).
| if (envelope?.envelope && envelope.envelope[0].__spotlight_envelope_id === lastEventId) { | |
| if (envelope?.envelope && String(envelope.envelope[0].__spotlight_envelope_id) === lastEventId) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@MathurAditya724 can you make sure __spotlight_envelope_id is always a string or undefined?
| for (let i = start; i < end; i++) { | ||
| const item = this.items[i % this.size]; | ||
| if (item == null) continue; | ||
|
|
||
| if (item[1] instanceof EventContainer) { | ||
| const envelope = item[1].getParsedEnvelope(); | ||
| if (envelope?.envelope && envelope.envelope[0].__spotlight_envelope_id === lastEventId) { | ||
| // Start from the position after the found event | ||
| startPos = i + 1; | ||
| break; | ||
| } | ||
| } | ||
| } |
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When searching for the lastEventId, this loop iterates through the entire buffer from head to writePos, potentially scanning hundreds of items (default buffer size is 500). This happens on every subscription with a Last-Event-ID header. Consider optimizing this by: 1) Searching backwards from the end since recent events are more likely, or 2) Maintaining an index mapping event IDs to positions, similar to how filenameCache works. The backwards search would typically find the event faster since SSE reconnections usually happen shortly after disconnection.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this only I was telling that we can use the timestamp, that way we can do a binary search.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are using uuidv7 which should be alpha/time-sortable: https://uuid7.com/ (see the first item in benefits)
So we should be able to do binary search here which would probably the most efficient way. Starting from the end also makes sense. Not so sure about a revers-hash-map as it's more book-keeping. I'd say binary search should be the optimal solution.
BYK
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good one!
| for (let i = start; i < end; i++) { | ||
| const item = this.items[i % this.size]; | ||
| if (item == null) continue; | ||
|
|
||
| if (item[1] instanceof EventContainer) { | ||
| const envelope = item[1].getParsedEnvelope(); | ||
| if (envelope?.envelope && envelope.envelope[0].__spotlight_envelope_id === lastEventId) { | ||
| // Start from the position after the found event | ||
| startPos = i + 1; | ||
| break; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are using uuidv7 which should be alpha/time-sortable: https://uuid7.com/ (see the first item in benefits)
So we should be able to do binary search here which would probably the most efficient way. Starting from the end also makes sense. Not so sure about a revers-hash-map as it's more book-keeping. I'd say binary search should be the optimal solution.
betegon
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add some tests to this. We're not adding them to UI or other parts of spotlight but we have to make sure the message buffer works.
So pls add some tests for this, like you did when we added the other week 🙏
Some ideas cursor gave me:
- Subscriber with lastEventId that exists in buffer
- Subscriber with lastEventId that was evicted (buffer overflow)
- Subscriber with invalid/non-existent lastEventId
- Subscriber with lastEventId at buffer boundary positions
- Integration test simulating reconnection scenario
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
|
|
||
| envelopeHeader.__spotlight_envelope_id = uuidv7(); | ||
| envelopeHeader.__spotlight_envelope_id = uuidv7obj(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
uuidv7obj() and uuidv4obj() return an object that represents a UUID as a 16-byte byte array:
Okay, why do we even need this?
| } | ||
|
|
||
| while (left <= right) { | ||
| const mid = Math.floor((left + right) / 2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be Math.floor((right - left) / 2)?
| // Skip null items or non-EventContainer items | ||
| if (item == null || !(item[1] instanceof EventContainer)) { | ||
| // Linear scan nearby to handle gaps | ||
| let boundsAdjusted = false; | ||
|
|
||
| // Check left side | ||
| for (let i = mid - 1; i >= left; i--) { | ||
| const leftItem = this.items[i % this.size]; | ||
| if (leftItem && leftItem[1] instanceof EventContainer) { | ||
| const envelope = leftItem[1].getParsedEnvelope(); | ||
| if (envelope?.envelope) { | ||
| const envelopeId = envelope.envelope[0].__spotlight_envelope_id; | ||
| const isEqual = envelopeId.compareTo(targetUuid); | ||
| if (isEqual === 0) { | ||
| return i; | ||
| } | ||
| // Adjust search bounds based on comparison | ||
| if (isEqual < 0) { | ||
| left = mid + 1; | ||
| } else { | ||
| right = mid - 1; | ||
| } | ||
| boundsAdjusted = true; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Check right side if left didn't help | ||
| if (!boundsAdjusted) { | ||
| for (let i = mid + 1; i <= right; i++) { | ||
| const rightItem = this.items[i % this.size]; | ||
| if (rightItem && rightItem[1] instanceof EventContainer) { | ||
| const envelope = rightItem[1].getParsedEnvelope(); | ||
| if (envelope?.envelope) { | ||
| const envelopeId = envelope.envelope[0].__spotlight_envelope_id; | ||
| const isEqual = envelopeId.compareTo(targetUuid); | ||
| if (isEqual === 0) { | ||
| return i; | ||
| } | ||
| // Adjust search bounds | ||
| if (isEqual < 0) { | ||
| left = mid + 1; | ||
| } else { | ||
| right = mid - 1; | ||
| } | ||
| boundsAdjusted = true; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, this entire block is very hard to follow. Vibe code?
| * Returns the position of the event, or -1 if not found. | ||
| * Takes advantage of UUIDv7's time-ordered property. | ||
| */ | ||
| private binarySearchEventId(targetId: string): number { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's just name this helper findEventIdIdx?
BYK
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You know what, all the tests pass and they make sense. Let's address my comments in a follow-up
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and publish to npm yourself or [setup this action to publish automatically](https://github.com/changesets/action#with-publishing). If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @spotlightjs/[email protected] ### Minor Changes - Docker compose support for Spotlight run command ([#1108](#1108)) - Added support for Last-Event-Id in SSE stream ([#1104](#1104)) - Change tool names: change `.` for `_` and remove `spotlight` preffix ([#1114](#1114)) - Improved human formatter readability ([#1112](#1112)) ### Patch Changes - Fix breaking error when event type is not supported ([#1111](#1111)) - Fix SDK categorization for Next.js by using User-Agent headers to distinguish browser from server events ([#1110](#1110)) ## @spotlightjs/[email protected] ### Patch Changes - Updated dependencies \[[`0942c8a`](0942c8a), [`bce012e`](bce012e), [`7624030`](7624030), [`15a7f41`](15a7f41), [`13da542`](13da542), [`b3a654a`](b3a654a)]: - @spotlightjs/[email protected] - @spotlightjs/[email protected] ## @spotlightjs/[email protected] ### Patch Changes - Fix SDK categorization for Next.js by using User-Agent headers to distinguish browser from server events ([#1110](#1110)) - Updated dependencies \[[`0942c8a`](0942c8a), [`bce012e`](bce012e), [`7624030`](7624030), [`15a7f41`](15a7f41), [`13da542`](13da542), [`b3a654a`](b3a654a)]: - @spotlightjs/[email protected] - @spotlightjs/[email protected] Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Closes #1095