@ellipsis-dev/sdk 0.34.1 → 0.36.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.
@@ -1,32 +1,198 @@
1
- import {
2
- SESSION_STREAM_PROTOCOL_VERSION,
3
- StreamAuthError,
4
- StreamUnavailableError,
5
- WS_CLOSE_AUTH_FAILED,
6
- WS_CLOSE_GOING_AWAY,
7
- WS_CLOSE_NORMAL,
8
- WS_CLOSE_NO_PROTOCOL,
9
- WS_CLOSE_OVER_CAPACITY,
10
- WS_CLOSE_SERVER_ERROR,
11
- WS_CLOSE_UNSUPPORTED_PROTOCOL_VERSION,
12
- classifyCloseCode,
13
- decideReconnect,
14
- nextReconnectDelayMs,
15
- sessionStatusWord,
16
- streamQuery,
17
- streamSession
18
- } from "../chunk-6V7QTLLF.js";
1
+ // src/stream/index.ts
2
+ var WS_CLOSE_NORMAL = 1e3;
3
+ var WS_CLOSE_GOING_AWAY = 1001;
4
+ var WS_CLOSE_AUTH_FAILED = 1008;
5
+ var WS_CLOSE_SERVER_ERROR = 1011;
6
+ var WS_CLOSE_OVER_CAPACITY = 1013;
7
+ function sessionStatusWord(session) {
8
+ return session.turn?.status ?? "none";
9
+ }
10
+ var StreamUnavailableError = class extends Error {
11
+ constructor(message) {
12
+ super(message);
13
+ this.name = "StreamUnavailableError";
14
+ }
15
+ };
16
+ var StreamAuthError = class extends Error {
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = "StreamAuthError";
20
+ }
21
+ };
22
+ function streamQuery(afterSeq) {
23
+ return afterSeq > 0 ? `after_seq=${afterSeq}` : "";
24
+ }
25
+ var HEARTBEAT_TIMEOUT_MS = 45e3;
26
+ var DEFAULT_MAX_RECONNECTS = 5;
27
+ function classifyCloseCode(code) {
28
+ switch (code) {
29
+ case WS_CLOSE_NORMAL:
30
+ return "normal";
31
+ case WS_CLOSE_AUTH_FAILED:
32
+ case 4401:
33
+ // dashboard ticket door: bad/expired ticket
34
+ case 4403:
35
+ return "auth";
36
+ default:
37
+ return "retry";
38
+ }
39
+ }
40
+ function nextReconnectDelayMs(attempt) {
41
+ const base = 500;
42
+ const max = 8e3;
43
+ return Math.min(max, base * 2 ** Math.max(0, attempt - 1));
44
+ }
45
+ function decideReconnect(params) {
46
+ const { closeKind, everReceivedFrame, attempt, maxReconnects } = params;
47
+ if (closeKind === "auth") return { action: "fail-auth" };
48
+ const cap = everReceivedFrame ? maxReconnects : Math.min(2, maxReconnects);
49
+ if (attempt >= cap) return { action: "give-up" };
50
+ return { action: "reconnect", delayMs: nextReconnectDelayMs(attempt) };
51
+ }
52
+ function connectOnce(sock, emit, signal) {
53
+ return new Promise((resolve) => {
54
+ let settled = false;
55
+ let heartbeat;
56
+ const finish = (result) => {
57
+ if (settled) return;
58
+ settled = true;
59
+ if (heartbeat) clearTimeout(heartbeat);
60
+ if (signal) signal.removeEventListener("abort", onAbort);
61
+ sock.close();
62
+ resolve(result);
63
+ };
64
+ const onAbort = () => finish({ kind: "aborted" });
65
+ const bumpHeartbeat = () => {
66
+ if (heartbeat) clearTimeout(heartbeat);
67
+ heartbeat = setTimeout(
68
+ () => finish({ kind: "error", err: new Error("heartbeat timeout") }),
69
+ HEARTBEAT_TIMEOUT_MS
70
+ );
71
+ };
72
+ if (signal) {
73
+ if (signal.aborted) {
74
+ finish({ kind: "aborted" });
75
+ return;
76
+ }
77
+ signal.addEventListener("abort", onAbort);
78
+ }
79
+ sock.onOpen(() => bumpHeartbeat());
80
+ sock.onMessage((data) => {
81
+ bumpHeartbeat();
82
+ let frame;
83
+ try {
84
+ frame = JSON.parse(data);
85
+ } catch {
86
+ return;
87
+ }
88
+ if (!frame || ![
89
+ "snapshot",
90
+ "records_append",
91
+ "session",
92
+ "delta",
93
+ "heartbeat",
94
+ "done",
95
+ "error"
96
+ ].includes(frame.type))
97
+ return;
98
+ emit(frame);
99
+ if (frame.type === "done") {
100
+ finish({ kind: "done" });
101
+ } else if (frame.type === "error") {
102
+ finish({
103
+ kind: "frameError",
104
+ message: frame.message ?? "stream error"
105
+ });
106
+ }
107
+ });
108
+ sock.onClose((code) => finish({ kind: "closed", code }));
109
+ sock.onError((err) => finish({ kind: "error", err }));
110
+ });
111
+ }
112
+ function sleep(ms, signal) {
113
+ return new Promise((resolve) => {
114
+ const timer = setTimeout(resolve, ms);
115
+ signal?.addEventListener(
116
+ "abort",
117
+ () => {
118
+ clearTimeout(timer);
119
+ resolve();
120
+ },
121
+ { once: true }
122
+ );
123
+ });
124
+ }
125
+ async function streamSession(opts) {
126
+ const maxReconnects = opts.maxReconnects ?? DEFAULT_MAX_RECONNECTS;
127
+ let afterSeq = opts.afterSeq ?? 0;
128
+ let everReceivedFrame = false;
129
+ let attempt = 0;
130
+ let lastStatusWord = "";
131
+ let lastReason = null;
132
+ const emit = (frame) => {
133
+ everReceivedFrame = true;
134
+ attempt = 0;
135
+ if (frame.type === "records_append") {
136
+ const records = frame.records;
137
+ for (const record of records) {
138
+ if (typeof record.feed_seq === "number") {
139
+ afterSeq = Math.max(afterSeq, record.feed_seq);
140
+ }
141
+ }
142
+ } else if (frame.type === "snapshot" || frame.type === "session") {
143
+ const session = frame.session;
144
+ lastStatusWord = sessionStatusWord(session);
145
+ lastReason = session.turn?.reason ?? null;
146
+ }
147
+ opts.onFrame(frame);
148
+ };
149
+ for (; ; ) {
150
+ if (opts.signal?.aborted) return { type: "aborted" };
151
+ let res;
152
+ try {
153
+ const sock = await opts.openSocket({
154
+ sessionId: opts.sessionId,
155
+ afterSeq,
156
+ query: streamQuery(afterSeq)
157
+ });
158
+ res = await connectOnce(sock, emit, opts.signal);
159
+ } catch (err) {
160
+ res = {
161
+ kind: "error",
162
+ err: err instanceof Error ? err : new Error(String(err))
163
+ };
164
+ }
165
+ if (res.kind === "done") {
166
+ return { type: "done", status: lastStatusWord, reason: lastReason };
167
+ }
168
+ if (res.kind === "frameError")
169
+ return { type: "error", message: res.message };
170
+ if (res.kind === "aborted") return { type: "aborted" };
171
+ attempt++;
172
+ const decision = decideReconnect({
173
+ closeKind: res.kind === "closed" ? classifyCloseCode(res.code) : void 0,
174
+ everReceivedFrame,
175
+ attempt,
176
+ maxReconnects
177
+ });
178
+ if (decision.action === "fail-auth") {
179
+ throw new StreamAuthError("not authorized to stream this session");
180
+ }
181
+ if (decision.action === "give-up") {
182
+ const why = res.kind === "error" ? res.err.message : `stream closed (code ${res.code})`;
183
+ throw new StreamUnavailableError(why);
184
+ }
185
+ await sleep(decision.delayMs ?? 0, opts.signal);
186
+ }
187
+ }
19
188
  export {
20
- SESSION_STREAM_PROTOCOL_VERSION,
21
189
  StreamAuthError,
22
190
  StreamUnavailableError,
23
191
  WS_CLOSE_AUTH_FAILED,
24
192
  WS_CLOSE_GOING_AWAY,
25
193
  WS_CLOSE_NORMAL,
26
- WS_CLOSE_NO_PROTOCOL,
27
194
  WS_CLOSE_OVER_CAPACITY,
28
195
  WS_CLOSE_SERVER_ERROR,
29
- WS_CLOSE_UNSUPPORTED_PROTOCOL_VERSION,
30
196
  classifyCloseCode,
31
197
  decideReconnect,
32
198
  nextReconnectDelayMs,