@chatu-ai/builder-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,73 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { resilientStream } from './resume';
3
+ const ev = (seq, kind = 'taskCard') => kind === 'done'
4
+ ? { kind: 'done', xid: 'x1', seq, state: 'completed' }
5
+ : { kind: 'taskCard', xid: 'x1', seq, id: `t${seq}`, label: `step ${seq}`, state: 'running' };
6
+ async function* iter(events, failAfter) {
7
+ for (let i = 0;; i++) {
8
+ if (failAfter !== undefined && i === failAfter)
9
+ throw new Error('connection dropped');
10
+ if (i >= events.length)
11
+ return;
12
+ yield events[i];
13
+ }
14
+ }
15
+ async function collect(src) {
16
+ const out = [];
17
+ for await (const e of src)
18
+ out.push(e);
19
+ return out;
20
+ }
21
+ const noSleep = { sleep: async () => { }, baseDelayMs: 0 };
22
+ describe('resilientStream', () => {
23
+ it('passes events through and stops at done', async () => {
24
+ const src = {
25
+ open: () => iter([ev(1), ev(2), ev(3, 'done')]),
26
+ reopen: () => iter([]),
27
+ };
28
+ const got = await collect(resilientStream(src, noSleep));
29
+ expect(got.map(e => e.seq)).toEqual([1, 2, 3]);
30
+ });
31
+ it('dedupes replayed events after reconnect (seq <= lastSeq dropped)', async () => {
32
+ let reopenedWith = -1;
33
+ const src = {
34
+ open: () => iter([ev(1), ev(2)], 2), // 两条后断线
35
+ reopen: lastSeq => {
36
+ reopenedWith = lastSeq;
37
+ // 服务端回放含重复(seq 1..2)+ 新事件
38
+ return iter([ev(1), ev(2), ev(3), ev(4, 'done')]);
39
+ },
40
+ };
41
+ const got = await collect(resilientStream(src, noSleep));
42
+ expect(reopenedWith).toBe(2);
43
+ expect(got.map(e => e.seq)).toEqual([1, 2, 3, 4]);
44
+ });
45
+ it('ack (seq=0) does not affect dedupe window', async () => {
46
+ const ack = {
47
+ kind: 'ack', xid: 'x1', seq: 0,
48
+ sandbox: { sandboxId: 'sbx1', state: 'ready', previewUrl: 'https://sbx1.n.example.com' },
49
+ };
50
+ const src = {
51
+ open: () => iter([ack, ev(1), ev(2, 'done')]),
52
+ reopen: () => iter([]),
53
+ };
54
+ const got = await collect(resilientStream(src, noSleep));
55
+ expect(got[0].kind).toBe('ack');
56
+ expect(got.map(e => e.seq)).toEqual([0, 1, 2]);
57
+ });
58
+ it('throws after maxRetries consecutive failures', async () => {
59
+ const src = {
60
+ open: () => iter([ev(1)], 1),
61
+ reopen: () => iter([], 0), // 每次重连立刻再断
62
+ };
63
+ await expect(collect(resilientStream(src, { ...noSleep, maxRetries: 2 }))).rejects.toThrow('dropped');
64
+ });
65
+ it('skips null (unparsed) events', async () => {
66
+ const src = {
67
+ open: () => iter([null, ev(1), null, ev(2, 'done')]),
68
+ reopen: () => iter([]),
69
+ };
70
+ const got = await collect(resilientStream(src, noSleep));
71
+ expect(got.map(e => e.seq)).toEqual([1, 2]);
72
+ });
73
+ });
package/dist/sse.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * SSE 传输(BuilderController create/connect):fetch 流式读取 → 逐条 data 行 → JSON。
3
+ * 与 EventSource 相比支持 POST + 自定义 header + AbortSignal。
4
+ */
5
+ export interface SseOptions {
6
+ fetchImpl?: typeof fetch;
7
+ signal?: AbortSignal;
8
+ }
9
+ /** 逐条产出 SSE data 载荷(已 JSON.parse;非 JSON 行原样以 string 产出;[DONE] 结束) */
10
+ export declare function readSse(url: string, init: RequestInit, opts?: SseOptions): AsyncIterable<unknown>;
package/dist/sse.js ADDED
@@ -0,0 +1,40 @@
1
+ /** 逐条产出 SSE data 载荷(已 JSON.parse;非 JSON 行原样以 string 产出;[DONE] 结束) */
2
+ export async function* readSse(url, init, opts = {}) {
3
+ const doFetch = opts.fetchImpl ?? fetch;
4
+ const res = await doFetch(url, { ...init, signal: opts.signal ?? init.signal });
5
+ if (!res.ok || !res.body) {
6
+ throw new Error(`sse ${res.status}: ${await res.text().catch(() => '')}`.slice(0, 300));
7
+ }
8
+ const reader = res.body.getReader();
9
+ const decoder = new TextDecoder();
10
+ let buffer = '';
11
+ try {
12
+ for (;;) {
13
+ const { value, done } = await reader.read();
14
+ if (done)
15
+ break;
16
+ buffer += decoder.decode(value, { stream: true });
17
+ let nl;
18
+ while ((nl = buffer.indexOf('\n')) >= 0) {
19
+ const line = buffer.slice(0, nl).replace(/\r$/, '');
20
+ buffer = buffer.slice(nl + 1);
21
+ if (!line.startsWith('data:'))
22
+ continue;
23
+ const data = line.slice(5).trim();
24
+ if (!data)
25
+ continue;
26
+ if (data === '[DONE]')
27
+ return;
28
+ try {
29
+ yield JSON.parse(data);
30
+ }
31
+ catch {
32
+ yield data;
33
+ }
34
+ }
35
+ }
36
+ }
37
+ finally {
38
+ reader.releaseLock();
39
+ }
40
+ }
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@chatu-ai/builder-sdk",
3
+ "version": "0.1.0",
4
+ "description": "ChatU Builder client SDK core: REST + streaming client, zod event schemas, seq resume",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "sideEffects": false,
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/chatu-ai/chatu-builder-sdk.git",
23
+ "directory": "packages/core"
24
+ },
25
+ "homepage": "https://github.com/chatu-ai/chatu-builder-sdk/tree/main/packages/core#readme",
26
+ "bugs": "https://github.com/chatu-ai/chatu-builder-sdk/issues",
27
+ "keywords": [
28
+ "chatu",
29
+ "builder",
30
+ "ai",
31
+ "app-builder",
32
+ "sdk",
33
+ "a2a",
34
+ "sse"
35
+ ],
36
+ "publishConfig": {
37
+ "access": "public",
38
+ "provenance": true
39
+ },
40
+ "engines": {
41
+ "node": ">=18"
42
+ },
43
+ "dependencies": {
44
+ "zod": "^3.24.0"
45
+ },
46
+ "devDependencies": {
47
+ "typescript": "^5.7.0",
48
+ "vitest": "^3.0.0"
49
+ },
50
+ "scripts": {
51
+ "build": "tsc -p tsconfig.json",
52
+ "typecheck": "tsc -p tsconfig.json --noEmit",
53
+ "test": "vitest run"
54
+ }
55
+ }