Skip to content

Commit 54ae4e2

Browse files
authored
feat(js-sdk): Support CJS (langchain-ai#1742)
* feat(js-sdk): Support CJS * Bump version
1 parent c61ece2 commit 54ae4e2

File tree

14 files changed

+363
-15
lines changed

14 files changed

+363
-15
lines changed

libs/sdk-js/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@langchain/langgraph-sdk",
3-
"version": "0.0.9",
3+
"version": "0.0.10",
44
"description": "Client library for interacting with the LangGraph API",
55
"type": "module",
66
"packageManager": "[email protected]",
@@ -17,7 +17,6 @@
1717
"license": "MIT",
1818
"dependencies": {
1919
"@types/json-schema": "^7.0.15",
20-
"eventsource-parser": "^1.1.2",
2120
"p-queue": "^6.6.2",
2221
"p-retry": "4",
2322
"uuid": "^9.0.0"

libs/sdk-js/scripts/create-entrypoints.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ const generateFiles = () => {
2222
const compiledPath = `${relativePath}dist/${value}`;
2323
return [
2424
[`${key}.cjs`, `module.exports = require('${compiledPath}.cjs');`],
25-
[`${key}.js`, `export * from '${compiledPath}.mjs'`],
26-
[`${key}.d.ts`, `export * from '${compiledPath}.mjs'`],
25+
[`${key}.js`, `export * from '${compiledPath}.js'`],
26+
[`${key}.d.ts`, `export * from '${compiledPath}.js'`],
2727
[`${key}.d.cts`, `export * from '${compiledPath}.cjs'`],
2828
];
2929
},

libs/sdk-js/src/client.mts renamed to libs/sdk-js/src/client.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,20 @@ import {
1010
ThreadState,
1111
Cron,
1212
} from "./schema.js";
13-
import { AsyncCaller, AsyncCallerParams } from "./utils/async_caller.mjs";
14-
import { EventSourceParser, createParser } from "eventsource-parser";
15-
import { IterableReadableStream } from "./utils/stream.mjs";
13+
import { AsyncCaller, AsyncCallerParams } from "./utils/async_caller.js";
14+
import {
15+
EventSourceParser,
16+
createParser,
17+
} from "./utils/eventsource-parser/index.js";
18+
import { IterableReadableStream } from "./utils/stream.js";
1619
import {
1720
RunsCreatePayload,
1821
RunsStreamPayload,
1922
RunsWaitPayload,
2023
StreamEvent,
2124
CronsCreatePayload,
2225
OnConflictBehavior,
23-
} from "./types.mjs";
26+
} from "./types.js";
2427

2528
interface ClientConfig {
2629
apiUrl?: string;

libs/sdk-js/src/index.mts renamed to libs/sdk-js/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export { Client } from "./client.mjs";
1+
export { Client } from "./client.js";
22

33
export type {
44
Assistant,
File renamed without changes.
File renamed without changes.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024 Espen Hovlandsdal <[email protected]>
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// From https://github.com/rexxars/eventsource-parser
2+
// Inlined due to CJS import issues
3+
4+
export { createParser } from "./parse.js";
5+
export type {
6+
EventSourceParseCallback,
7+
EventSourceParser,
8+
ParsedEvent,
9+
ParseEvent,
10+
ReconnectInterval,
11+
} from "./types.js";
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/**
2+
* EventSource/Server-Sent Events parser
3+
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html
4+
*
5+
* Based on code from the {@link https://github.com/EventSource/eventsource | EventSource module},
6+
* which is licensed under the MIT license. And copyrighted the EventSource GitHub organisation.
7+
*/
8+
import type { EventSourceParseCallback, EventSourceParser } from "./types.js";
9+
10+
/**
11+
* Creates a new EventSource parser.
12+
*
13+
* @param onParse - Callback to invoke when a new event is parsed, or a new reconnection interval
14+
* has been sent from the server
15+
*
16+
* @returns A new EventSource parser, with `parse` and `reset` methods.
17+
* @public
18+
*/
19+
export function createParser(
20+
onParse: EventSourceParseCallback,
21+
): EventSourceParser {
22+
// Processing state
23+
let isFirstChunk: boolean;
24+
let buffer: string;
25+
let startingPosition: number;
26+
let startingFieldLength: number;
27+
28+
// Event state
29+
let eventId: string | undefined;
30+
let eventName: string | undefined;
31+
let data: string;
32+
33+
reset();
34+
return { feed, reset };
35+
36+
function reset(): void {
37+
isFirstChunk = true;
38+
buffer = "";
39+
startingPosition = 0;
40+
startingFieldLength = -1;
41+
42+
eventId = undefined;
43+
eventName = undefined;
44+
data = "";
45+
}
46+
47+
function feed(chunk: string): void {
48+
buffer = buffer ? buffer + chunk : chunk;
49+
50+
// Strip any UTF8 byte order mark (BOM) at the start of the stream.
51+
// Note that we do not strip any non - UTF8 BOM, as eventsource streams are
52+
// always decoded as UTF8 as per the specification.
53+
if (isFirstChunk && hasBom(buffer)) {
54+
buffer = buffer.slice(BOM.length);
55+
}
56+
57+
isFirstChunk = false;
58+
59+
// Set up chunk-specific processing state
60+
const length = buffer.length;
61+
let position = 0;
62+
let discardTrailingNewline = false;
63+
64+
// Read the current buffer byte by byte
65+
while (position < length) {
66+
// EventSource allows for carriage return + line feed, which means we
67+
// need to ignore a linefeed character if the previous character was a
68+
// carriage return
69+
// @todo refactor to reduce nesting, consider checking previous byte?
70+
// @todo but consider multiple chunks etc
71+
if (discardTrailingNewline) {
72+
if (buffer[position] === "\n") {
73+
++position;
74+
}
75+
discardTrailingNewline = false;
76+
}
77+
78+
let lineLength = -1;
79+
let fieldLength = startingFieldLength;
80+
let character: string;
81+
82+
for (
83+
let index = startingPosition;
84+
lineLength < 0 && index < length;
85+
++index
86+
) {
87+
character = buffer[index];
88+
if (character === ":" && fieldLength < 0) {
89+
fieldLength = index - position;
90+
} else if (character === "\r") {
91+
discardTrailingNewline = true;
92+
lineLength = index - position;
93+
} else if (character === "\n") {
94+
lineLength = index - position;
95+
}
96+
}
97+
98+
if (lineLength < 0) {
99+
startingPosition = length - position;
100+
startingFieldLength = fieldLength;
101+
break;
102+
} else {
103+
startingPosition = 0;
104+
startingFieldLength = -1;
105+
}
106+
107+
parseEventStreamLine(buffer, position, fieldLength, lineLength);
108+
109+
position += lineLength + 1;
110+
}
111+
112+
if (position === length) {
113+
// If we consumed the entire buffer to read the event, reset the buffer
114+
buffer = "";
115+
} else if (position > 0) {
116+
// If there are bytes left to process, set the buffer to the unprocessed
117+
// portion of the buffer only
118+
buffer = buffer.slice(position);
119+
}
120+
}
121+
122+
function parseEventStreamLine(
123+
lineBuffer: string,
124+
index: number,
125+
fieldLength: number,
126+
lineLength: number,
127+
) {
128+
if (lineLength === 0) {
129+
// We reached the last line of this event
130+
if (data.length > 0) {
131+
onParse({
132+
type: "event",
133+
id: eventId,
134+
event: eventName || undefined,
135+
data: data.slice(0, -1), // remove trailing newline
136+
});
137+
138+
data = "";
139+
eventId = undefined;
140+
}
141+
eventName = undefined;
142+
return;
143+
}
144+
145+
const noValue = fieldLength < 0;
146+
const field = lineBuffer.slice(
147+
index,
148+
index + (noValue ? lineLength : fieldLength),
149+
);
150+
let step = 0;
151+
152+
if (noValue) {
153+
step = lineLength;
154+
} else if (lineBuffer[index + fieldLength + 1] === " ") {
155+
step = fieldLength + 2;
156+
} else {
157+
step = fieldLength + 1;
158+
}
159+
160+
const position = index + step;
161+
const valueLength = lineLength - step;
162+
const value = lineBuffer.slice(position, position + valueLength).toString();
163+
164+
if (field === "data") {
165+
data += value ? `${value}\n` : "\n";
166+
} else if (field === "event") {
167+
eventName = value;
168+
} else if (field === "id" && !value.includes("\u0000")) {
169+
eventId = value;
170+
} else if (field === "retry") {
171+
const retry = parseInt(value, 10);
172+
if (!Number.isNaN(retry)) {
173+
onParse({ type: "reconnect-interval", value: retry });
174+
}
175+
}
176+
}
177+
}
178+
179+
const BOM = [239, 187, 191];
180+
181+
function hasBom(buffer: string) {
182+
return BOM.every(
183+
(charCode: number, index: number) => buffer.charCodeAt(index) === charCode,
184+
);
185+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { createParser } from "./parse.js";
2+
import type { EventSourceParser, ParsedEvent } from "./types.js";
3+
4+
/**
5+
* A TransformStream that ingests a stream of strings and produces a stream of ParsedEvents.
6+
*
7+
* @example
8+
* ```
9+
* const eventStream =
10+
* response.body
11+
* .pipeThrough(new TextDecoderStream())
12+
* .pipeThrough(new EventSourceParserStream())
13+
* ```
14+
* @public
15+
*/
16+
export class EventSourceParserStream extends TransformStream<
17+
string,
18+
ParsedEvent
19+
> {
20+
constructor() {
21+
let parser!: EventSourceParser;
22+
23+
super({
24+
start(controller) {
25+
parser = createParser((event: any) => {
26+
if (event.type === "event") {
27+
controller.enqueue(event);
28+
}
29+
});
30+
},
31+
transform(chunk) {
32+
parser.feed(chunk);
33+
},
34+
});
35+
}
36+
}
37+
38+
export type { ParsedEvent } from "./types.js";

0 commit comments

Comments
 (0)