Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat: assistant-stream - ObjectStream #1912

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open

feat: assistant-stream - ObjectStream #1912

wants to merge 1 commit into from

Conversation

Yonom
Copy link
Member

@Yonom Yonom commented Apr 28, 2025

Important

Introduces object stream handling with new classes and functions for encoding, decoding, and managing streams in the assistant-stream package.

  • New Features:
    • Introduces ObjectStreamAccumulator for managing object stream state in ObjectStreamAccumulator.ts.
    • Adds ObjectStreamResponse and fromObjectStreamResponse for handling object stream responses in ObjectStreamResponse.ts.
    • Implements createObjectStream for creating object streams with a controller in createObjectStream.ts.
    • Adds SSEEncoder and SSEDecoder for handling server-sent events in SSE.ts.
  • Exports:
    • Updates index.ts to export createObjectStream, ObjectStreamResponse, fromObjectStreamResponse, and ObjectStreamChunk type.
  • Types:
    • Defines ObjectStreamOperation and ObjectStreamChunk types in types.ts.

This description was created by Ellipsis for fc002be. You can customize this summary. It will automatically update as commits are pushed.

@Copilot Copilot AI review requested due to automatic review settings April 28, 2025 10:35
Copy link
Contributor

⚠️ Trial Period Expired ⚠️

Your trial period has expired. To continue using this feature, please upgrade to a paid plan here or book a time to chat here.

Copy link

changeset-bot bot commented Apr 28, 2025

⚠️ No Changeset found

Latest commit: fc002be

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Contributor

@Copilot Copilot AI left a 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 introduces object stream functionality by adding SSE-based encoding/decoding and associated JSON utilities for handling object stream operations.

  • Adds SSEEncoder and SSEDecoder classes to process SSE event streams.
  • Implements object stream types, accumulator, and stream response handlers to support a reactive object streaming pattern.
  • Exports new APIs via the core index for external consumption.

Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/assistant-stream/src/core/utils/stream/SSE.ts Implements SSE event encoding and decoding using TransformStreams.
packages/assistant-stream/src/core/json/types.ts Defines types for object stream operations and chunks.
packages/assistant-stream/src/core/json/createObjectStream.ts Provides a function to create object streams with asynchronous execution.
packages/assistant-stream/src/core/json/ObjectStreamResponse.ts Implements response handling via object stream encoding and decoding.
packages/assistant-stream/src/core/json/ObjectStreamAccumulator.ts Provides immutable operations to accumulate and update object stream state.
packages/assistant-stream/src/core/index.ts Updates exports to include the new object stream functionality.

execute,
defaultValue = {},
}: CreateObjectStreamOptions) => {
const [stream, controller] = getStreamControllerPair({ defaultValue });
Copy link
Preview

Copilot AI Apr 28, 2025

Choose a reason for hiding this comment

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

Pass the defaultValue directly to getStreamControllerPair (i.e., getStreamControllerPair(defaultValue)) instead of wrapping it in an object, as the function expects a ReadonlyJSONValue.

Suggested change
const [stream, controller] = getStreamControllerPair({ defaultValue });
const [stream, controller] = getStreamControllerPair(defaultValue);

Copilot uses AI. Check for mistakes.

const idx = Number(key);
if (isNaN(idx))
throw new Error(`Expected array index at [${path.join(", ")}]`);
if (idx > state.length || idx < 0)
Copy link
Preview

Copilot AI Apr 28, 2025

Choose a reason for hiding this comment

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

Consider updating the array bounds check to use 'idx >= state.length' instead of 'idx > state.length' to prevent accidentally allowing an out-of-bounds index equal to the array length.

Suggested change
if (idx > state.length || idx < 0)
if (idx >= state.length || idx < 0)

Copilot uses AI. Check for mistakes.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

PR Summary

Introduces a comprehensive JSON object streaming system with SSE (Server-Sent Events) support, enabling real-time state updates and text appending operations through a structured API.

  • New ObjectStreamAccumulator in /packages/assistant-stream/src/core/json/ObjectStreamAccumulator.ts manages state tree with path-based operations and type safety
  • Added SSE encoding/decoding in /packages/assistant-stream/src/core/utils/stream/SSE.ts with proper event formatting and headers
  • Implemented ObjectStreamResponse in /packages/assistant-stream/src/core/json/ObjectStreamResponse.ts with custom Assistant-Stream-Format header versioning
  • Silent JSON parse error handling in SSE decoder could lead to lost messages without proper error propagation
  • Missing documentation for the Assistant-Stream-Format versioning scheme (marked as TODO)

6 file(s) reviewed, 6 comment(s)
Edit PR Review Bot Settings | Greptile

const idx = Number(key);
if (isNaN(idx))
throw new Error(`Expected array index at [${path.join(", ")}]`);
if (idx > state.length || idx < 0)
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: Array index equal to length should be allowed for appending to array

Suggested change
if (idx > state.length || idx < 0)
if (idx >= state.length || idx < 0)

);
case "append-text":
return ObjectStreamAccumulator.updatePath(state, op.path, (current) => {
if (current === undefined) return op.value;
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Consider validating op.value is a string when current is undefined in append-text operation

Suggested change
if (current === undefined) return op.value;
if (current === undefined) {
if (typeof op.value !== "string") throw new Error(`Expected string value in append-text operation`);
return op.value;
}

execute,
defaultValue = {},
}: CreateObjectStreamOptions) => {
const [stream, controller] = getStreamControllerPair({ defaultValue });
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: defaultValue is wrapped in an object unnecessarily - should be passed directly

Suggested change
const [stream, controller] = getStreamControllerPair({ defaultValue });
const [stream, controller] = getStreamControllerPair(defaultValue);

Comment on lines +29 to +37
enqueue(operations: readonly ObjectStreamOperation[]) {
this._accumulator.append(operations);

this._controller.enqueue({
snapshot: this._accumulator.state,
operations,
});
return this;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: enqueue returns this but return type is void in interface - could cause type errors

Comment on lines +75 to +77
case "retry":
eventBuffer.retry = Number(value);
break;
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: No validation for retry value being a positive number. Invalid values could cause reconnection issues.

Suggested change
case "retry":
eventBuffer.retry = Number(value);
break;
case "retry":
const retryNum = Number(value);
if (!isNaN(retryNum) && retryNum > 0) {
eventBuffer.retry = retryNum;
}
break;

.pipeThrough(
new TransformStream<T, string>({
transform(chunk, controller) {
controller.enqueue(`data: ${JSON.stringify(chunk)}\n\n`);
Copy link
Contributor

Choose a reason for hiding this comment

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

style: JSON.stringify may throw for circular references or BigInts. Add try/catch to handle serialization errors.

Copy link
Contributor

promptless bot commented Apr 28, 2025

📝 Documentation updates detected! You can review documentation updates here

Copy link

@mrge-io mrge-io bot left a comment

Choose a reason for hiding this comment

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

mrge found 4 issues across 6 files. View them in mrge.io

@@ -0,0 +1,18 @@
import { ReadonlyJSONValue } from "../../utils";
Copy link

Choose a reason for hiding this comment

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

The import path for ReadonlyJSONValue is incorrect. The type is actually defined in '../../utils/json/json-value.ts'.

.pipeThrough(
new TransformStream<T, string>({
transform(chunk, controller) {
controller.enqueue(`data: ${JSON.stringify(chunk)}\n\n`);
Copy link

Choose a reason for hiding this comment

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

Missing error handling for JSON.stringify which could throw on non-serializable data

execute,
defaultValue = {},
}: CreateObjectStreamOptions) => {
const [stream, controller] = getStreamControllerPair({ defaultValue });
Copy link

Choose a reason for hiding this comment

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

The function getStreamControllerPair expects defaultValue directly, but is being passed an object with a defaultValue property

if (response.headers.get("Content-Type") !== "text/event-stream") {
throw new Error("Response is not an event stream");
}
if (response.headers.get("Assistant-Stream-Format") !== "json-v0.1") {
Copy link

Choose a reason for hiding this comment

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

Magic string "json-v0.1" appears multiple times in the codebase and should be extracted as a constant

execute,
defaultValue = {},
}: CreateObjectStreamOptions) => {
const [stream, controller] = getStreamControllerPair({ defaultValue });
Copy link
Contributor

Choose a reason for hiding this comment

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

Potential bug: Passing an object { defaultValue } to getStreamControllerPair rather than the value itself. Likely use getStreamControllerPair(defaultValue).

Suggested change
const [stream, controller] = getStreamControllerPair({ defaultValue });
const [stream, controller] = getStreamControllerPair(defaultValue);

try {
controller.enqueue(JSON.parse(event.data));
} catch (err) {
console.error("Failed to parse SSE event data:", err);
Copy link
Contributor

Choose a reason for hiding this comment

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

JSON.parse errors are only logged here; consider propagating or handling the error to inform the consumer.

Copy link
Contributor

coderabbitai bot commented Apr 28, 2025

Walkthrough

This change introduces a set of new modules and exports to support a streaming protocol for JSON object operations over Server-Sent Events (SSE) within the assistant-stream package. The core index file is updated to export new streaming-related entities, including a function for creating object streams, a response wrapper, and a decoding utility. A new accumulator class is added to manage immutable JSON state through appendable operations. The protocol is defined through types for operations and chunks, supporting "set" and "append-text" operations on JSON paths. The streaming logic is implemented with utilities for encoding and decoding SSE streams, enabling transmission and reception of JSON operation streams. The createObjectStream utility provides a mechanism to emit incremental state snapshots and operations, while ObjectStreamResponse and fromObjectStreamResponse handle SSE-based response wrapping and decoding. All new entities are exported for public use, and error handling and stream cancellation are incorporated throughout the implementation.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite

📥 Commits

Reviewing files that changed from the base of the PR and between 1ec3789 and fc002be.

📒 Files selected for processing (6)
  • packages/assistant-stream/src/core/index.ts (1 hunks)
  • packages/assistant-stream/src/core/json/ObjectStreamAccumulator.ts (1 hunks)
  • packages/assistant-stream/src/core/json/ObjectStreamResponse.ts (1 hunks)
  • packages/assistant-stream/src/core/json/createObjectStream.ts (1 hunks)
  • packages/assistant-stream/src/core/json/types.ts (1 hunks)
  • packages/assistant-stream/src/core/utils/stream/SSE.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/assistant-stream/src/core/json/types.ts
  • packages/assistant-stream/src/core/index.ts
  • packages/assistant-stream/src/core/utils/stream/SSE.ts
  • packages/assistant-stream/src/core/json/createObjectStream.ts
  • packages/assistant-stream/src/core/json/ObjectStreamResponse.ts
  • packages/assistant-stream/src/core/json/ObjectStreamAccumulator.ts

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (6)
packages/assistant-stream/src/core/index.ts (1)

18-22: Consider re-exporting the companion types for a smoother DX

createObjectStream, ObjectStreamResponse, and fromObjectStreamResponse are now surfaced, but the related value/type helpers (ObjectStreamOperation, ObjectStreamChunk, ObjectStreamAccumulator, etc.) remain hidden one level down. Call-sites will frequently need both the helpers and the primitives, so bubbling the types up through this barrel can prevent deep import paths like:

import type { ObjectStreamChunk } from "@acme/assistant-stream/core/json/types";

Optional refactor:

 // index.ts
 export * from "./json/types";       // <- adds operation + chunk types
 export { ObjectStreamAccumulator } from "./json/ObjectStreamAccumulator";
packages/assistant-stream/src/core/utils/stream/SSE.ts (3)

5-12: Freeze the shared header object

static readonly headers is currently mutable because Headers exposes mutators.
Accidental runtime mutation would leak to every response using the encoder.

-static readonly headers = new Headers({
+static readonly headers: Readonly<Headers> = new Headers({
   "Content-Type": "text/event-stream",
   "Cache-Control": "no-cache",
   Connection: "keep-alive",
 });

Or freeze it after construction:

const hdr = new Headers({ /* … */ });
Object.freeze(hdr);
static readonly headers = hdr;

104-114: Decoder: reconsider error strategy for unknown events

Throwing on an unfamiliar event type terminates the entire stream, even though the client might be able to safely ignore those events (e.g., ping, heartbeat, custom extensions).

Safer pattern:

-default:
-  throw new Error(`Unknown SSE event type: ${event.event}`);
+default:
+  // Ignore or expose via side-channel; do not kill consumer stream
+  return;

This still surfaces malformed payloads while keeping the connection resilient.


106-110: Surface JSON-parse failures to the consumer

console.error hides deserialization problems and silently drops data.
Forward the error to the stream to let callers attach .catch handlers or abort processing.

-  } catch (err) {
-    console.error("Failed to parse SSE event data:", err);
+  } catch (err) {
+    controller.error(
+      new Error(`Failed to parse SSE event data: ${(err as Error).message}`),
+    );
     return;
   }
packages/assistant-stream/src/core/json/ObjectStreamAccumulator.ts (1)

53-55: Consider creating intermediate containers instead of throwing on missing parents

If an operation targets a non-existing nested path (e.g. ["foo","bar"] when state.foo is still undefined), the current implementation throws.
Depending on the use-case you may want to auto-create intermediary objects/arrays (similar to lodash’s set) so callers don’t need to pre-seed the structure.

If strictness is desired, feel free to ignore this remark.

packages/assistant-stream/src/core/json/createObjectStream.ts (1)

53-61: Guard against controller being undefined in cancel

If the stream is cancelled before start executes (very unlikely but technically possible in spec-compliant runtimes), controller would still be undefined.

-    cancel(reason: unknown) {
-      controller.__internalCancel(reason);
-    },
+    cancel(reason: unknown) {
+      if (controller) controller.__internalCancel(reason);
+    },

Not critical, but eliminates a theoretical race.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 509a0f4 and fe037fa.

📒 Files selected for processing (6)
  • packages/assistant-stream/src/core/index.ts (1 hunks)
  • packages/assistant-stream/src/core/json/ObjectStreamAccumulator.ts (1 hunks)
  • packages/assistant-stream/src/core/json/ObjectStreamResponse.ts (1 hunks)
  • packages/assistant-stream/src/core/json/createObjectStream.ts (1 hunks)
  • packages/assistant-stream/src/core/json/types.ts (1 hunks)
  • packages/assistant-stream/src/core/utils/stream/SSE.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
packages/assistant-stream/src/core/utils/stream/SSE.ts (2)
packages/assistant-stream/src/core/utils/stream/PipeableTransformStream.ts (1)
  • PipeableTransformStream (1-10)
packages/assistant-stream/src/core/utils/stream/LineDecoderStream.ts (1)
  • LineDecoderStream (1-29)
packages/assistant-stream/src/core/json/createObjectStream.ts (3)
packages/assistant-stream/src/core/json/types.ts (2)
  • ObjectStreamOperation (3-13)
  • ObjectStreamChunk (15-18)
packages/assistant-stream/src/core/json/ObjectStreamAccumulator.ts (1)
  • ObjectStreamAccumulator (4-76)
packages/assistant-stream/src/core/utils/withPromiseOrValue.ts (1)
  • withPromiseOrValue (1-20)
packages/assistant-stream/src/core/json/ObjectStreamResponse.ts (5)
packages/assistant-stream/src/core/utils/stream/PipeableTransformStream.ts (1)
  • PipeableTransformStream (1-10)
packages/assistant-stream/src/core/json/types.ts (2)
  • ObjectStreamChunk (15-18)
  • ObjectStreamOperation (3-13)
packages/assistant-stream/src/core/utils/stream/SSE.ts (2)
  • SSEEncoder (4-26)
  • SSEDecoder (94-120)
packages/assistant-stream/src/core/json/ObjectStreamAccumulator.ts (1)
  • ObjectStreamAccumulator (4-76)
packages/assistant-stream/src/core/index.ts (2)
  • ObjectStreamResponse (20-20)
  • fromObjectStreamResponse (21-21)
packages/assistant-stream/src/core/json/ObjectStreamAccumulator.ts (1)
packages/assistant-stream/src/core/json/types.ts (1)
  • ObjectStreamOperation (3-13)
🪛 Biome (1.9.4)
packages/assistant-stream/src/core/json/ObjectStreamAccumulator.ts

[error] 41-41: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Unsafe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 66-66: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 72-72: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)

⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: mrge · AI code reviewer
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (1)
packages/assistant-stream/src/core/json/types.ts (1)

3-18: Type declarations look solid

The discriminated‐union design cleanly encodes the two operations and keeps the snapshot/ops pair immutable via Readonly* types. No correctness or style concerns here. 👍

Comment on lines +46 to +61
if (line.startsWith(":")) return; // Ignore comments

if (line === "") {
if (dataLines.length > 0) {
controller.enqueue({
event: eventBuffer.event || "message",
data: dataLines.join("\n"),
id: eventBuffer.id,
retry: eventBuffer.retry,
});
}
eventBuffer = {};
dataLines = [];
return;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

SSE parsing breaks on CRLF line endings

LineDecoderStream splits on \n only, so CRLF lines arrive as
'data: {...}\r'. The trailing \r prevents the empty-line sentinel ("") from ever matching, so events never flush on Windows-style streams.

Minimal fix inside the event parser:

-  transform(line, controller) {
+  transform(rawLine, controller) {
+    const line = rawLine.replace(/\r$/, ""); // strip CR
-if (line === "") {
+if (line.length === 0) {
-const [field, ...rest] = line.split(":");
+const [field, ...rest] = line.split(":", 2);

(Using a max-split of 2 avoids unintended splits when value itself contains a colon.)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (line.startsWith(":")) return; // Ignore comments
if (line === "") {
if (dataLines.length > 0) {
controller.enqueue({
event: eventBuffer.event || "message",
data: dataLines.join("\n"),
id: eventBuffer.id,
retry: eventBuffer.retry,
});
}
eventBuffer = {};
dataLines = [];
return;
}
// --- earlier in the file, inside the TransformStream setup ----------------
transform(rawLine, controller) {
// strip trailing CR so that empty‐line checks work on CRLF inputs
const line = rawLine.replace(/\r$/, "");
if (line.startsWith(":")) return; // Ignore comments
if (line.length === 0) {
if (dataLines.length > 0) {
controller.enqueue({
event: eventBuffer.event || "message",
data: dataLines.join("\n"),
id: eventBuffer.id,
retry: eventBuffer.retry,
});
}
eventBuffer = {};
dataLines = [];
return;
}
// Only split into field and value once, even if the value contains ':'
const [field, ...rest] = line.split(":", 2);
// ...the rest of your parsing logic...
}

const nextState = [...state];
nextState[idx] = this.updatePath(nextState[idx], rest, updater);

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Replace this.updatePath with an explicit class reference in static context

Using this inside a static method is legal but usually discouraged and triggers Biome’s noThisInStatic rule.
Referencing the class directly removes the warning and clarifies intent.

-      const nextState = [...state];
-      nextState[idx] = this.updatePath(nextState[idx], rest, updater);
+      const nextState = [...state];
+      nextState[idx] = ObjectStreamAccumulator.updatePath(
+        nextState[idx],
+        rest,
+        updater,
+      );
 ...
-    const nextState = { ...(state as ReadonlyJSONObject) };
-    nextState[key] = this.updatePath(nextState[key], rest, updater);
+    const nextState = { ...(state as ReadonlyJSONObject) };
+    nextState[key] = ObjectStreamAccumulator.updatePath(
+      nextState[key],
+      rest,
+      updater,
+    );

Also applies to: 71-73

🧰 Tools
🪛 Biome (1.9.4)

[error] 66-66: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)

const _exhaustiveCheck: never = type;
throw new Error(`Invalid operation type: ${_exhaustiveCheck}`);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Wrap the _exhaustiveCheck declaration to satisfy the no-switch-declarations rule

The const _exhaustiveCheck variable is declared in the default clause and therefore becomes visible to the other clauses, triggering the Biome noSwitchDeclarations lint error.
A minimal fix is to wrap the default body in a block.

-      default:
-        const _exhaustiveCheck: never = type;
-        throw new Error(`Invalid operation type: ${_exhaustiveCheck}`);
+      default: {
+        const _exhaustiveCheck: never = type;
+        throw new Error(`Invalid operation type: ${_exhaustiveCheck}`);
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const _exhaustiveCheck: never = type;
throw new Error(`Invalid operation type: ${_exhaustiveCheck}`);
}
default: {
const _exhaustiveCheck: never = type;
throw new Error(`Invalid operation type: ${_exhaustiveCheck}`);
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 41-41: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Unsafe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

Comment on lines +75 to +76
const [stream, controller] = getStreamControllerPair({ defaultValue });

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Incorrect argument passed to getStreamControllerPair

getStreamControllerPair expects a ReadonlyJSONValue, but an object with a defaultValue property is supplied, causing the initial accumulator state to become { defaultValue: … }.

-  const [stream, controller] = getStreamControllerPair({ defaultValue });
+  const [stream, controller] = getStreamControllerPair(defaultValue);

Without this fix, every snapshot will be wrapped one level deeper than callers expect.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [stream, controller] = getStreamControllerPair({ defaultValue });
const [stream, controller] = getStreamControllerPair(defaultValue);

Comment on lines +74 to +78
if (response.headers.get("Content-Type") !== "text/event-stream") {
throw new Error("Response is not an event stream");
}
if (response.headers.get("Assistant-Stream-Format") !== "json-v0.1") {
throw new Error("Unsupported Assistant-Stream-Format header");
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Relax strict equality on Content-Type header

Some servers append a charset (text/event-stream; charset=utf-8).
A strict comparison will reject perfectly valid responses.

-  if (response.headers.get("Content-Type") !== "text/event-stream") {
+  const contentType = response.headers.get("Content-Type") ?? "";
+  if (!contentType.startsWith("text/event-stream")) {
     throw new Error("Response is not an event stream");
   }

The same logic can be applied to the custom Assistant-Stream-Format header once the TODO is resolved.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (response.headers.get("Content-Type") !== "text/event-stream") {
throw new Error("Response is not an event stream");
}
if (response.headers.get("Assistant-Stream-Format") !== "json-v0.1") {
throw new Error("Unsupported Assistant-Stream-Format header");
const contentType = response.headers.get("Content-Type") ?? "";
if (!contentType.startsWith("text/event-stream")) {
throw new Error("Response is not an event stream");
}
if (response.headers.get("Assistant-Stream-Format") !== "json-v0.1") {
throw new Error("Unsupported Assistant-Stream-Format header");
}

execute,
defaultValue = {},
}: CreateObjectStreamOptions) => {
const [stream, controller] = getStreamControllerPair({ defaultValue });
Copy link
Contributor

Choose a reason for hiding this comment

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

Potential bug: Passing { defaultValue } instead of the raw defaultValue. Likely should call getStreamControllerPair(defaultValue) to avoid nesting.

Suggested change
const [stream, controller] = getStreamControllerPair({ defaultValue });
const [stream, controller] = getStreamControllerPair(defaultValue);


default:
const _exhaustiveCheck: never = type;
throw new Error(`Invalid operation type: ${_exhaustiveCheck}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

For improved debugging, consider using op.type in the error message instead of _exhaustiveCheck in the default case.

Suggested change
throw new Error(`Invalid operation type: ${_exhaustiveCheck}`);
throw new Error(`Invalid operation type: ${type}`);

execute,
defaultValue = {},
}: CreateObjectStreamOptions) => {
const [stream, controller] = getStreamControllerPair({ defaultValue });
Copy link
Contributor

Choose a reason for hiding this comment

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

BUG: Passing { defaultValue } wraps the default value in an extra object. Pass defaultValue directly to getStreamControllerPair.

Suggested change
const [stream, controller] = getStreamControllerPair({ defaultValue });
const [stream, controller] = getStreamControllerPair(defaultValue);

if (!response.ok)
throw new Error(`Response failed, status ${response.status}`);
if (!response.body) throw new Error("Response body is null");
if (response.headers.get("Content-Type") !== "text/event-stream") {
Copy link
Contributor

Choose a reason for hiding this comment

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

The strict header check for Content-Type may fail if additional parameters (like charset) are included. Consider a more flexible check.

Suggested change
if (response.headers.get("Content-Type") !== "text/event-stream") {
if (!response.headers.get("Content-Type")?.startsWith("text/event-stream")) {

Copy link

trag-bot bot commented Apr 30, 2025

@trag-bot didn't find any issues in the code! ✅✨

Copy link

trag-bot bot commented Apr 30, 2025

Pull request summary

  • Added ObjectStreamAccumulator class to manage state and operations for object streams.
  • Implemented ObjectStreamEncoder and ObjectStreamDecoder classes for encoding and decoding object streams using Server-Sent Events (SSE).
  • Created ObjectStreamResponse class to handle responses formatted as object streams.
  • Introduced createObjectStream function to facilitate the creation of object streams with a controller for managing operations.
  • Defined types for ObjectStreamOperation and ObjectStreamChunk to standardize operations and data structure within object streams.
  • Enhanced SSE handling with SSEEncoder and SSEDecoder classes for transforming data to and from the event stream format.
  • Added error handling for invalid operations and response formats in the object stream processing.
  • Updated the main index file to export new classes and functions related to object streams and SSE handling.

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