Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
66a82d0
fix(javascript): remove agent-studio standalone client status and ler…
Fluf22 May 1, 2026
a337e35
revert(javascript): restore agent-studio as standalone client
Fluf22 May 1, 2026
9a00213
fix: agent-studio client build
Fluf22 Feb 25, 2026
ad8187c
feat(specs): add text/event-stream response type and x-streaming vend…
Fluf22 Mar 4, 2026
f5ef61a
feat(javascript): add SSE parser with three-layer architecture
Fluf22 Mar 4, 2026
691235d
feat(python): add SSE parser with three-layer architecture
Fluf22 Mar 4, 2026
38e3498
feat(javascript): add streaming requester support for fetch and node-…
Fluf22 Mar 4, 2026
1d78d1c
feat(python): add streaming transporter support for async and sync
Fluf22 Mar 4, 2026
7988e96
feat(javascript): generate *Stream() methods for streaming endpoints
Fluf22 Mar 4, 2026
d633cac
feat(python): generate *_stream() methods for streaming endpoints
Fluf22 Mar 4, 2026
36b3901
docs: add data type hints to streaming method documentation
Fluf22 Mar 4, 2026
9c32545
feat(tests): convert test classes to use unittest.TestCase
Fluf22 Mar 4, 2026
2b6aff9
fix: handle 204 No Content responses across language clients
Fluf22 Mar 9, 2026
de93246
feat(specs): add typed StreamEvent wrapper for streaming methods
Fluf22 Mar 9, 2026
ff8846f
chore: fix playground
Fluf22 Mar 10, 2026
db957ea
feat: update compatibility mode description for completion API
Fluf22 Mar 10, 2026
a1f8eb5
feat(types): set default type for StreamEvent and TypeVar in SSE modules
Fluf22 Mar 10, 2026
9181b3c
fix(javascript): cast sendStream request body to BodyInit for TS 5.9 …
Fluf22 May 1, 2026
1b3c269
fix(javascript): bump bundlewatch size limits to accommodate streamin…
Fluf22 May 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions clients/algoliasearch-client-javascript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,55 +27,55 @@
"files": [
{
"path": "packages/algoliasearch/dist/algoliasearch.umd.js",
"maxSize": "14.8KB"
"maxSize": "16KB"
},
{
"path": "packages/algoliasearch/dist/lite/builds/browser.umd.js",
"maxSize": "4.2KB"
"maxSize": "6KB"
},
{
"path": "packages/abtesting/dist/builds/browser.umd.js",
"maxSize": "4.5KB"
"maxSize": "6KB"
},
{
"path": "packages/client-abtesting/dist/builds/browser.umd.js",
"maxSize": "4.4KB"
"maxSize": "6KB"
},
{
"path": "packages/client-analytics/dist/builds/browser.umd.js",
"maxSize": "5.1KB"
"maxSize": "6KB"
},
{
"path": "packages/composition/dist/builds/browser.umd.js",
"maxSize": "5.0KB"
"maxSize": "6KB"
},
{
"path": "packages/client-insights/dist/builds/browser.umd.js",
"maxSize": "4.2KB"
"maxSize": "5KB"
},
{
"path": "packages/client-personalization/dist/builds/browser.umd.js",
"maxSize": "4.3KB"
"maxSize": "6KB"
},
{
"path": "packages/client-query-suggestions/dist/builds/browser.umd.js",
"maxSize": "4.3KB"
"maxSize": "6KB"
},
{
"path": "packages/client-search/dist/builds/browser.umd.js",
"maxSize": "7.7KB"
"maxSize": "9KB"
},
{
"path": "packages/ingestion/dist/builds/browser.umd.js",
"maxSize": "7.1KB"
"maxSize": "8KB"
},
{
"path": "packages/monitoring/dist/builds/browser.umd.js",
"maxSize": "4.3KB"
"maxSize": "6KB"
},
{
"path": "packages/recommend/dist/builds/browser.umd.js",
"maxSize": "4.5KB"
"maxSize": "6KB"
}
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ describe('api', () => {
set: expect.any(Function),
},
request: expect.any(Function),
requestStream: expect.any(Function),
requester: {
send: expect.any(Function),
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { describe, expect, test } from 'vitest';

import { iterSSEEvents, type ServerSentEvent } from '../sse';

// ─── Helpers ──────────────────────────────────────────────────────────────────

/** Encode each string argument as a separate Uint8Array chunk in an AsyncIterable. */
function toStream(...chunks: string[]): AsyncIterable<Uint8Array> {
const encoder = new TextEncoder();
return {
async *[Symbol.asyncIterator]() {
for (const chunk of chunks) {
yield encoder.encode(chunk);
}
},
};
}

/** Feed chunks through iterSSEEvents and collect all emitted events. */
async function collectEvents(...chunks: string[]): Promise<ServerSentEvent[]> {
const events: ServerSentEvent[] = [];
for await (const event of iterSSEEvents(toStream(...chunks))) {
events.push(event);
}
return events;
}

/** Base event with all defaults β€” spread to override specific fields. */
const BASE: ServerSentEvent = { data: '', event: '', id: null, retry: null };

// ─── Tests ────────────────────────────────────────────────────────────────────

describe('iterSSEEvents', () => {
describe('WHATWG spec cases', () => {
test('1. single data event', async () => {
const events = await collectEvents('data: hello\n\n');
expect(events).toHaveLength(1);
expect(events[0]).toEqual({ ...BASE, data: 'hello' });
});

test('2. multi-line data', async () => {
const events = await collectEvents('data: line1\ndata: line2\n\n');
expect(events).toHaveLength(1);
expect(events[0]).toEqual({ ...BASE, data: 'line1\nline2' });
});

test('3. event type', async () => {
const events = await collectEvents('event: custom\ndata: hi\n\n');
expect(events).toHaveLength(1);
expect(events[0]).toEqual({ ...BASE, event: 'custom', data: 'hi' });
});

test('4. comment ignored', async () => {
const events = await collectEvents(': comment\ndata: hi\n\n');
expect(events).toHaveLength(1);
expect(events[0]).toEqual({ ...BASE, data: 'hi' });
});

test('5. empty data suppressed (no event dispatched)', async () => {
const events = await collectEvents('event: ping\n\n');
expect(events).toHaveLength(0);
});

test('6. field with no colon', async () => {
const events = await collectEvents('data\n\n');
expect(events).toHaveLength(1);
expect(events[0]).toEqual({ ...BASE, data: '' });
});

test('7. single space strip (two spaces in β†’ one space out)', async () => {
const events = await collectEvents('data: hello\n\n');
expect(events).toHaveLength(1);
expect(events[0]).toEqual({ ...BASE, data: ' hello' });
});

test('8. unknown field ignored', async () => {
const events = await collectEvents('foo: bar\ndata: hi\n\n');
expect(events).toHaveLength(1);
expect(events[0]).toEqual({ ...BASE, data: 'hi' });
});

test('9. id persistence across dispatches', async () => {
const events = await collectEvents('id: 42\ndata: a\n\ndata: b\n\n');
expect(events).toHaveLength(2);
expect(events[0]).toEqual({ ...BASE, data: 'a', id: '42' });
expect(events[1]).toEqual({ ...BASE, data: 'b', id: '42' });
});

test('10. id with NULL character ignored', async () => {
const events = await collectEvents('id: foo\0bar\ndata: hi\n\n');
expect(events).toHaveLength(1);
expect(events[0]).toEqual({ ...BASE, data: 'hi' });
});

test('11. retry with digits only', async () => {
const events = await collectEvents('retry: 3000\ndata: hi\n\n');
expect(events).toHaveLength(1);
expect(events[0]).toEqual({ ...BASE, data: 'hi', retry: 3000 });
});

test('12. retry with non-digits ignored', async () => {
const events = await collectEvents('retry: 3s\ndata: hi\n\n');
expect(events).toHaveLength(1);
expect(events[0]).toEqual({ ...BASE, data: 'hi' });
});

test('13. CR line endings', async () => {
const events = await collectEvents('data: hello\r\r');
expect(events).toHaveLength(1);
expect(events[0]).toEqual({ ...BASE, data: 'hello' });
});

test('14. CRLF line endings', async () => {
const events = await collectEvents('data: hello\r\n\r\n');
expect(events).toHaveLength(1);
expect(events[0]).toEqual({ ...BASE, data: 'hello' });
});

test('15. mixed line endings', async () => {
const events = await collectEvents('data: a\rdata: b\n\n');
expect(events).toHaveLength(1);
expect(events[0]).toEqual({ ...BASE, data: 'a\nb' });
});

test('16. stream ends mid-event (no dispatch)', async () => {
const events = await collectEvents('data: partial');
expect(events).toHaveLength(0);
});
});

describe('chunk boundary handling', () => {
test('17. trailingCR across chunk boundaries', async () => {
const events = await collectEvents('data: hello\r', '\ndata: world\r\n\r\n');
expect(events).toHaveLength(1);
expect(events[0]).toEqual({ ...BASE, data: 'hello\nworld' });
});
});

describe('safety limits', () => {
test('18. 10MB buffer cap throws', async () => {
const chunk = 'a'.repeat(2 * 1024 * 1024); // 2MB per chunk, no newlines
const chunks = Array<string>(6).fill(chunk); // 12MB total > 10MB limit
await expect(collectEvents(...chunks)).rejects.toThrow('SSE line buffer exceeded 10MB');
});
});

describe('eventType reset', () => {
test('19. eventType resets on suppressed dispatch', async () => {
// First blank line: eventType="custom" but no data β†’ suppressed, eventType resets to ""
// Second event: data="hi" with eventType="" (not "custom")
const events = await collectEvents('event: custom\n\ndata: hi\n\n');
expect(events).toHaveLength(1);
expect(events[0]).toEqual({ ...BASE, data: 'hi', event: '' });
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export * from './createAuth';
export * from './createIterablePromise';
export * from './getAlgoliaAgent';
export * from './logger';
export * from './sse';
export * from './transporter';
export * from './types';
Loading
Loading