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