@ellipsis-dev/sdk 0.34.0 → 0.35.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/README.md +5 -7
- package/dist/index.d.ts +97 -171
- package/dist/index.js +19 -183
- package/dist/store/index.d.ts +1 -1
- package/dist/store/index.js +246 -127
- package/dist/stream/index.d.ts +4 -7
- package/dist/stream/index.js +187 -21
- package/dist/{types-sNAOvELB.d.ts → types-BrLpRRde.d.ts} +881 -1171
- package/package.json +1 -1
- package/schema/collection_frames.schema.json +162 -17
- package/schema/frames.schema.json +770 -87
- package/schema/openapi.v1.json +8184 -8713
- package/dist/chunk-6V7QTLLF.js +0 -214
package/dist/store/index.js
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
SESSION_STREAM_PROTOCOL_VERSION
|
|
3
|
-
} from "../chunk-6V7QTLLF.js";
|
|
4
|
-
|
|
5
1
|
// src/store/claude.ts
|
|
6
2
|
function claudePayload(record) {
|
|
7
3
|
if (record.kind === "claude_sdk") return record.payload;
|
|
@@ -104,124 +100,6 @@ function codexAppServerContent(event) {
|
|
|
104
100
|
}
|
|
105
101
|
}
|
|
106
102
|
|
|
107
|
-
// src/store/progress.ts
|
|
108
|
-
function messageProgress(records) {
|
|
109
|
-
const messages = /* @__PURE__ */ new Map();
|
|
110
|
-
const pending = /* @__PURE__ */ new Map();
|
|
111
|
-
const turns = /* @__PURE__ */ new Map();
|
|
112
|
-
let ready = false;
|
|
113
|
-
const getTurn = (id) => {
|
|
114
|
-
let turn = turns.get(id);
|
|
115
|
-
if (!turn) {
|
|
116
|
-
turn = {
|
|
117
|
-
timing: { id, startedAt: null, completedAt: null },
|
|
118
|
-
phase: ready ? "working" : "starting",
|
|
119
|
-
messages: /* @__PURE__ */ new Set()
|
|
120
|
-
};
|
|
121
|
-
turns.set(id, turn);
|
|
122
|
-
}
|
|
123
|
-
return turn;
|
|
124
|
-
};
|
|
125
|
-
const attach = (messageId, turnId) => {
|
|
126
|
-
const message = messages.get(messageId);
|
|
127
|
-
if (!message) return;
|
|
128
|
-
const turn = getTurn(turnId);
|
|
129
|
-
if (!turn.messages.has(messageId)) {
|
|
130
|
-
turn.messages.add(messageId);
|
|
131
|
-
message.turns.push(turn.timing);
|
|
132
|
-
}
|
|
133
|
-
pending.delete(messageId);
|
|
134
|
-
message.phase = turn.phase;
|
|
135
|
-
};
|
|
136
|
-
const setPending = (turnId, phase) => {
|
|
137
|
-
for (const [id, turn] of pending) {
|
|
138
|
-
if (turnId == null || turn == null || turn === turnId)
|
|
139
|
-
messages.get(id).phase = phase;
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
for (const record of records) {
|
|
143
|
-
if (record.kind === "platform") {
|
|
144
|
-
const p = record.payload;
|
|
145
|
-
const messageId = typeof p.message_id === "string" ? p.message_id : null;
|
|
146
|
-
const turnId = typeof p.turn_id === "string" ? p.turn_id : record.turn_id;
|
|
147
|
-
switch (record.record_type) {
|
|
148
|
-
case "message_received":
|
|
149
|
-
if (messageId && !messages.has(messageId)) {
|
|
150
|
-
messages.set(messageId, { phase: "queued", turns: [] });
|
|
151
|
-
if (turnId && turns.has(turnId)) attach(messageId, turnId);
|
|
152
|
-
else pending.set(messageId, turnId ?? null);
|
|
153
|
-
}
|
|
154
|
-
break;
|
|
155
|
-
case "message_delivered":
|
|
156
|
-
if (messageId && turnId) attach(messageId, turnId);
|
|
157
|
-
break;
|
|
158
|
-
case "message_requeued":
|
|
159
|
-
if (messageId && messages.has(messageId)) {
|
|
160
|
-
const requeued = typeof p.requeued_turn_id === "string" ? p.requeued_turn_id : null;
|
|
161
|
-
pending.set(messageId, requeued);
|
|
162
|
-
messages.get(messageId).phase = "queued";
|
|
163
|
-
}
|
|
164
|
-
break;
|
|
165
|
-
case "environment_phase":
|
|
166
|
-
case "environment_output":
|
|
167
|
-
ready = false;
|
|
168
|
-
setPending(turnId ?? null, "preparing");
|
|
169
|
-
break;
|
|
170
|
-
case "environment_ready":
|
|
171
|
-
ready = false;
|
|
172
|
-
setPending(turnId ?? null, "starting");
|
|
173
|
-
break;
|
|
174
|
-
case "turn_started":
|
|
175
|
-
if (turnId) {
|
|
176
|
-
getTurn(turnId).timing.startedAt = record.created_at;
|
|
177
|
-
for (const [id, turn] of pending) {
|
|
178
|
-
if (turn === turnId) attach(id, turnId);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
break;
|
|
182
|
-
case "turn_ended":
|
|
183
|
-
if (turnId) {
|
|
184
|
-
const turn = getTurn(turnId);
|
|
185
|
-
turn.timing.completedAt = record.created_at;
|
|
186
|
-
turn.phase = null;
|
|
187
|
-
for (const id of turn.messages) {
|
|
188
|
-
if (!pending.has(id)) messages.get(id).phase = null;
|
|
189
|
-
}
|
|
190
|
-
for (const [id, pendingTurn] of pending) {
|
|
191
|
-
if (pendingTurn === turnId) {
|
|
192
|
-
pending.delete(id);
|
|
193
|
-
messages.get(id).phase = null;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
break;
|
|
198
|
-
case "session_closed":
|
|
199
|
-
ready = false;
|
|
200
|
-
for (const id of pending.keys()) messages.get(id).phase = null;
|
|
201
|
-
break;
|
|
202
|
-
}
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
const claude = claudePayload(record);
|
|
206
|
-
const agentStarted = record.kind === "codex_app_server" && record.payload.method === "turn/started" || record.kind === "codex" && record.payload.type === "turn.started" || claude != null && (claude.kind === "assistant" || claude.kind === "user" || claude.kind === "system" && claude.subtype === "init");
|
|
207
|
-
if (agentStarted) {
|
|
208
|
-
ready = true;
|
|
209
|
-
if (record.turn_id) {
|
|
210
|
-
const turn = getTurn(record.turn_id);
|
|
211
|
-
if (turn.timing.completedAt == null) {
|
|
212
|
-
turn.timing.startedAt ??= record.created_at;
|
|
213
|
-
turn.phase = "working";
|
|
214
|
-
for (const id of turn.messages) messages.get(id).phase = "working";
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
if (record.session_message_id && record.turn_id) {
|
|
219
|
-
attach(record.session_message_id, record.turn_id);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
return messages;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
103
|
// src/store/lifecycle.ts
|
|
226
104
|
function environmentOutputStep(payload) {
|
|
227
105
|
if (typeof payload.step === "string" && payload.step) return payload.step;
|
|
@@ -623,6 +501,55 @@ function recordToItems(record, keyBase, options = {}) {
|
|
|
623
501
|
}
|
|
624
502
|
] : [];
|
|
625
503
|
}
|
|
504
|
+
case "gemini": {
|
|
505
|
+
const content = geminiStreamContent(record.payload);
|
|
506
|
+
if (!content) return [];
|
|
507
|
+
if (content.kind === "end") {
|
|
508
|
+
return content.isError ? [
|
|
509
|
+
{
|
|
510
|
+
key: keyBase,
|
|
511
|
+
kind: "summary",
|
|
512
|
+
text: "turn ended with an error",
|
|
513
|
+
isError: true,
|
|
514
|
+
spaceBefore: true
|
|
515
|
+
}
|
|
516
|
+
] : [];
|
|
517
|
+
}
|
|
518
|
+
if (content.kind === "tool") {
|
|
519
|
+
return [
|
|
520
|
+
{
|
|
521
|
+
key: keyBase,
|
|
522
|
+
kind: "tool",
|
|
523
|
+
gutter: "\u25CF",
|
|
524
|
+
text: content.name,
|
|
525
|
+
detail: content.summary ? `(${content.summary})` : void 0,
|
|
526
|
+
spaceBefore: true,
|
|
527
|
+
tool: { name: content.name, input: content.input ?? void 0 }
|
|
528
|
+
}
|
|
529
|
+
];
|
|
530
|
+
}
|
|
531
|
+
if (content.kind === "tool_result") {
|
|
532
|
+
return [
|
|
533
|
+
{
|
|
534
|
+
key: keyBase,
|
|
535
|
+
kind: "tool_result",
|
|
536
|
+
gutter: "\u23BF",
|
|
537
|
+
text: content.text || "(completed)",
|
|
538
|
+
isError: content.isError,
|
|
539
|
+
spaceBefore: false
|
|
540
|
+
}
|
|
541
|
+
];
|
|
542
|
+
}
|
|
543
|
+
return [
|
|
544
|
+
{
|
|
545
|
+
key: keyBase,
|
|
546
|
+
kind: content.kind,
|
|
547
|
+
text: content.text.trim(),
|
|
548
|
+
isError: content.kind === "error",
|
|
549
|
+
spaceBefore: true
|
|
550
|
+
}
|
|
551
|
+
];
|
|
552
|
+
}
|
|
626
553
|
case "platform": {
|
|
627
554
|
const text2 = lifecycleText(record.record_type, record.payload);
|
|
628
555
|
return text2 ? [{ key: keyBase, kind: "notice", text: text2, spaceBefore: true }] : [];
|
|
@@ -728,6 +655,155 @@ function statusActivityText(status) {
|
|
|
728
655
|
}
|
|
729
656
|
}
|
|
730
657
|
|
|
658
|
+
// src/store/geminiStream.ts
|
|
659
|
+
function geminiStreamContent(event) {
|
|
660
|
+
switch (event.type) {
|
|
661
|
+
case "message":
|
|
662
|
+
return event.role === "assistant" && !event.delta && event.content.trim() ? { kind: "assistant", text: event.content } : null;
|
|
663
|
+
case "tool_use": {
|
|
664
|
+
const input = event.parameters ?? null;
|
|
665
|
+
return {
|
|
666
|
+
kind: "tool",
|
|
667
|
+
toolId: event.tool_id,
|
|
668
|
+
name: event.tool_name,
|
|
669
|
+
input,
|
|
670
|
+
summary: oneLine(summarizeToolInput(event.tool_name, input ?? {}), 100)
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
case "tool_result":
|
|
674
|
+
return {
|
|
675
|
+
kind: "tool_result",
|
|
676
|
+
toolId: event.tool_id,
|
|
677
|
+
text: (event.output ?? event.error?.message ?? "").trim(),
|
|
678
|
+
isError: event.status === "error"
|
|
679
|
+
};
|
|
680
|
+
case "error":
|
|
681
|
+
return { kind: "error", text: event.message };
|
|
682
|
+
case "result":
|
|
683
|
+
return { kind: "end", isError: event.status === "error" };
|
|
684
|
+
default:
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// src/store/progress.ts
|
|
690
|
+
function messageProgress(records) {
|
|
691
|
+
const messages = /* @__PURE__ */ new Map();
|
|
692
|
+
const pending = /* @__PURE__ */ new Map();
|
|
693
|
+
const turns = /* @__PURE__ */ new Map();
|
|
694
|
+
let ready = false;
|
|
695
|
+
const getTurn = (id) => {
|
|
696
|
+
let turn = turns.get(id);
|
|
697
|
+
if (!turn) {
|
|
698
|
+
turn = {
|
|
699
|
+
timing: { id, startedAt: null, completedAt: null },
|
|
700
|
+
phase: ready ? "working" : "starting",
|
|
701
|
+
messages: /* @__PURE__ */ new Set()
|
|
702
|
+
};
|
|
703
|
+
turns.set(id, turn);
|
|
704
|
+
}
|
|
705
|
+
return turn;
|
|
706
|
+
};
|
|
707
|
+
const attach = (messageId, turnId) => {
|
|
708
|
+
const message = messages.get(messageId);
|
|
709
|
+
if (!message) return;
|
|
710
|
+
const turn = getTurn(turnId);
|
|
711
|
+
if (!turn.messages.has(messageId)) {
|
|
712
|
+
turn.messages.add(messageId);
|
|
713
|
+
message.turns.push(turn.timing);
|
|
714
|
+
}
|
|
715
|
+
pending.delete(messageId);
|
|
716
|
+
message.phase = turn.phase;
|
|
717
|
+
};
|
|
718
|
+
const setPending = (turnId, phase) => {
|
|
719
|
+
for (const [id, turn] of pending) {
|
|
720
|
+
if (turnId == null || turn == null || turn === turnId)
|
|
721
|
+
messages.get(id).phase = phase;
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
for (const record of records) {
|
|
725
|
+
if (record.kind === "platform") {
|
|
726
|
+
const p = record.payload;
|
|
727
|
+
const messageId = typeof p.message_id === "string" ? p.message_id : null;
|
|
728
|
+
const turnId = typeof p.turn_id === "string" ? p.turn_id : record.turn_id;
|
|
729
|
+
switch (record.record_type) {
|
|
730
|
+
case "message_received":
|
|
731
|
+
if (messageId && !messages.has(messageId)) {
|
|
732
|
+
messages.set(messageId, { phase: "queued", turns: [] });
|
|
733
|
+
if (turnId && turns.has(turnId)) attach(messageId, turnId);
|
|
734
|
+
else pending.set(messageId, turnId ?? null);
|
|
735
|
+
}
|
|
736
|
+
break;
|
|
737
|
+
case "message_delivered":
|
|
738
|
+
if (messageId && turnId) attach(messageId, turnId);
|
|
739
|
+
break;
|
|
740
|
+
case "message_requeued":
|
|
741
|
+
if (messageId && messages.has(messageId)) {
|
|
742
|
+
const requeued = typeof p.requeued_turn_id === "string" ? p.requeued_turn_id : null;
|
|
743
|
+
pending.set(messageId, requeued);
|
|
744
|
+
messages.get(messageId).phase = "queued";
|
|
745
|
+
}
|
|
746
|
+
break;
|
|
747
|
+
case "environment_phase":
|
|
748
|
+
case "environment_output":
|
|
749
|
+
ready = false;
|
|
750
|
+
setPending(turnId ?? null, "preparing");
|
|
751
|
+
break;
|
|
752
|
+
case "environment_ready":
|
|
753
|
+
ready = false;
|
|
754
|
+
setPending(turnId ?? null, "starting");
|
|
755
|
+
break;
|
|
756
|
+
case "turn_started":
|
|
757
|
+
if (turnId) {
|
|
758
|
+
getTurn(turnId).timing.startedAt = record.created_at;
|
|
759
|
+
for (const [id, turn] of pending) {
|
|
760
|
+
if (turn === turnId) attach(id, turnId);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
break;
|
|
764
|
+
case "turn_ended":
|
|
765
|
+
if (turnId) {
|
|
766
|
+
const turn = getTurn(turnId);
|
|
767
|
+
turn.timing.completedAt = record.created_at;
|
|
768
|
+
turn.phase = null;
|
|
769
|
+
for (const id of turn.messages) {
|
|
770
|
+
if (!pending.has(id)) messages.get(id).phase = null;
|
|
771
|
+
}
|
|
772
|
+
for (const [id, pendingTurn] of pending) {
|
|
773
|
+
if (pendingTurn === turnId) {
|
|
774
|
+
pending.delete(id);
|
|
775
|
+
messages.get(id).phase = null;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
break;
|
|
780
|
+
case "session_closed":
|
|
781
|
+
ready = false;
|
|
782
|
+
for (const id of pending.keys()) messages.get(id).phase = null;
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
const claude = claudePayload(record);
|
|
788
|
+
const agentStarted = record.kind === "codex_app_server" && record.payload.method === "turn/started" || record.kind === "codex" && record.payload.type === "turn.started" || record.kind === "gemini" && record.payload.type === "init" || claude != null && (claude.kind === "assistant" || claude.kind === "user" || claude.kind === "system" && claude.subtype === "init");
|
|
789
|
+
if (agentStarted) {
|
|
790
|
+
ready = true;
|
|
791
|
+
if (record.turn_id) {
|
|
792
|
+
const turn = getTurn(record.turn_id);
|
|
793
|
+
if (turn.timing.completedAt == null) {
|
|
794
|
+
turn.timing.startedAt ??= record.created_at;
|
|
795
|
+
turn.phase = "working";
|
|
796
|
+
for (const id of turn.messages) messages.get(id).phase = "working";
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
if (record.session_message_id && record.turn_id) {
|
|
801
|
+
attach(record.session_message_id, record.turn_id);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return messages;
|
|
805
|
+
}
|
|
806
|
+
|
|
731
807
|
// src/store/chatTurns.ts
|
|
732
808
|
var isInitEvent = (data) => data.kind === "system" && data.subtype === "init";
|
|
733
809
|
var ENVIRONMENT_RECORD_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -738,6 +814,7 @@ var ENVIRONMENT_RECORD_TYPES = /* @__PURE__ */ new Set([
|
|
|
738
814
|
function groupRecordsToChatTurns(records, session) {
|
|
739
815
|
const turns = [];
|
|
740
816
|
const toolNodeByUseId = /* @__PURE__ */ new Map();
|
|
817
|
+
const openGeminiTools = /* @__PURE__ */ new Map();
|
|
741
818
|
const userTurnByMessageId = /* @__PURE__ */ new Map();
|
|
742
819
|
const turnByMessageId = /* @__PURE__ */ new Map();
|
|
743
820
|
const unansweredTurnIds = /* @__PURE__ */ new Set();
|
|
@@ -897,6 +974,49 @@ function groupRecordsToChatTurns(records, session) {
|
|
|
897
974
|
});
|
|
898
975
|
continue;
|
|
899
976
|
}
|
|
977
|
+
if (record.kind === "gemini") {
|
|
978
|
+
const content = geminiStreamContent(record.payload);
|
|
979
|
+
if (!content) continue;
|
|
980
|
+
if (content.kind === "end") {
|
|
981
|
+
if (openIdx >= 0) {
|
|
982
|
+
turns[openIdx].completedAt = record.created_at;
|
|
983
|
+
if (content.isError) turns[openIdx].isError = true;
|
|
984
|
+
openIdx = -1;
|
|
985
|
+
}
|
|
986
|
+
} else if (content.kind === "tool") {
|
|
987
|
+
const node = {
|
|
988
|
+
key: nextKey(record.id),
|
|
989
|
+
kind: "tool",
|
|
990
|
+
name: content.name,
|
|
991
|
+
input: content.input,
|
|
992
|
+
summary: content.summary,
|
|
993
|
+
result: null,
|
|
994
|
+
isError: false,
|
|
995
|
+
startedAt: record.created_at,
|
|
996
|
+
completedAt: null
|
|
997
|
+
};
|
|
998
|
+
openSessionTurn(record).nodes.push(node);
|
|
999
|
+
openGeminiTools.set(content.toolId, node);
|
|
1000
|
+
} else if (content.kind === "tool_result") {
|
|
1001
|
+
const node = openGeminiTools.get(content.toolId);
|
|
1002
|
+
if (node) {
|
|
1003
|
+
node.result = content.text;
|
|
1004
|
+
node.isError = content.isError;
|
|
1005
|
+
node.completedAt = record.created_at;
|
|
1006
|
+
openGeminiTools.delete(content.toolId);
|
|
1007
|
+
}
|
|
1008
|
+
} else {
|
|
1009
|
+
const turn2 = openSessionTurn(record);
|
|
1010
|
+
if (content.kind === "error") turn2.isError = true;
|
|
1011
|
+
turn2.nodes.push({
|
|
1012
|
+
key: nextKey(record.id),
|
|
1013
|
+
kind: "assistant",
|
|
1014
|
+
text: content.text.trim(),
|
|
1015
|
+
createdAt: record.created_at
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
900
1020
|
if (record.kind === "codex_app_server") {
|
|
901
1021
|
const content = codexAppServerContent(record.payload);
|
|
902
1022
|
if (!content) continue;
|
|
@@ -1866,7 +1986,6 @@ function isConversationOver(session) {
|
|
|
1866
1986
|
function seedTranscriptStore(store, seed) {
|
|
1867
1987
|
store.ingest({
|
|
1868
1988
|
type: "snapshot",
|
|
1869
|
-
protocol: SESSION_STREAM_PROTOCOL_VERSION,
|
|
1870
1989
|
earliest_feed_seq: seed.earliestFeedSeq ?? null,
|
|
1871
1990
|
session: seed.session,
|
|
1872
1991
|
messages: [...seed.messages ?? []]
|
|
@@ -1894,13 +2013,13 @@ var SessionTranscriptStore = class {
|
|
|
1894
2013
|
snapshot = EMPTY_SNAPSHOT;
|
|
1895
2014
|
listeners = /* @__PURE__ */ new Set();
|
|
1896
2015
|
acknowledged = /* @__PURE__ */ new Set();
|
|
1897
|
-
// The inbox projection
|
|
2016
|
+
// The inbox projection: message_received/delivered/requeued
|
|
1898
2017
|
// RECORDS drive it; the snapshot seeds the pending set at connect. Keyed by
|
|
1899
2018
|
// message id so a requeue can restore a message the delivered record
|
|
1900
2019
|
// removed from the pending view.
|
|
1901
2020
|
inbox = /* @__PURE__ */ new Map();
|
|
1902
2021
|
// The resume cursor: the highest feed_seq received via records_append. Only
|
|
1903
|
-
// records advance it
|
|
2022
|
+
// records advance it — never messages/session frames. streamSession
|
|
1904
2023
|
// tracks its own copy; this one seeds a NEW streamSession call (e.g. a
|
|
1905
2024
|
// page-level reconnect after unmount) from already-held state.
|
|
1906
2025
|
cursorSeq = 0;
|
|
@@ -1933,7 +2052,7 @@ var SessionTranscriptStore = class {
|
|
|
1933
2052
|
// The current pending set, in feed order — what queued chips render.
|
|
1934
2053
|
pendingMessages = () => [...this.inbox.values()].filter((m) => m.status === "pending").sort((a, b) => (a.feed_seq ?? 0) - (b.feed_seq ?? 0));
|
|
1935
2054
|
// Fold one lifecycle record into the inbox projection; returns whether the
|
|
1936
|
-
// pending view changed. Unknown fields/types are ignored
|
|
2055
|
+
// pending view changed. Unknown fields/types are ignored.
|
|
1937
2056
|
applyMessageRecord = (record) => {
|
|
1938
2057
|
if (record.kind !== "platform") return false;
|
|
1939
2058
|
const payload = record.payload;
|
|
@@ -1984,7 +2103,7 @@ var SessionTranscriptStore = class {
|
|
|
1984
2103
|
}
|
|
1985
2104
|
};
|
|
1986
2105
|
// Ingest one frame from the stream client. Unknown frame types stamp
|
|
1987
|
-
// liveness and change nothing else
|
|
2106
|
+
// liveness and change nothing else.
|
|
1988
2107
|
ingest = (rawFrame) => {
|
|
1989
2108
|
const prev = this.snapshot;
|
|
1990
2109
|
const next = {
|
package/dist/stream/index.d.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { d2 as StreamFrame, cm as Session } from '../types-BrLpRRde.js';
|
|
2
2
|
|
|
3
|
-
declare const SESSION_STREAM_PROTOCOL_VERSION = 7;
|
|
4
3
|
declare const WS_CLOSE_NORMAL = 1000;
|
|
5
4
|
declare const WS_CLOSE_GOING_AWAY = 1001;
|
|
6
|
-
declare const WS_CLOSE_UNSUPPORTED_PROTOCOL_VERSION = 1002;
|
|
7
|
-
declare const WS_CLOSE_NO_PROTOCOL = 1003;
|
|
8
5
|
declare const WS_CLOSE_AUTH_FAILED = 1008;
|
|
9
6
|
declare const WS_CLOSE_SERVER_ERROR = 1011;
|
|
10
7
|
declare const WS_CLOSE_OVER_CAPACITY = 1013;
|
|
@@ -38,11 +35,11 @@ type OpenSocket = (args: {
|
|
|
38
35
|
query: string;
|
|
39
36
|
}) => StreamSocket | Promise<StreamSocket>;
|
|
40
37
|
declare function streamQuery(afterSeq: number): string;
|
|
41
|
-
type CloseKind = 'normal' | 'auth' | '
|
|
38
|
+
type CloseKind = 'normal' | 'auth' | 'retry';
|
|
42
39
|
declare function classifyCloseCode(code: number): CloseKind;
|
|
43
40
|
declare function nextReconnectDelayMs(attempt: number): number;
|
|
44
41
|
interface ReconnectDecision {
|
|
45
|
-
action: 'reconnect' | '
|
|
42
|
+
action: 'reconnect' | 'give-up' | 'fail-auth';
|
|
46
43
|
delayMs?: number;
|
|
47
44
|
}
|
|
48
45
|
declare function decideReconnect(params: {
|
|
@@ -61,4 +58,4 @@ interface StreamSessionOptions {
|
|
|
61
58
|
}
|
|
62
59
|
declare function streamSession(opts: StreamSessionOptions): Promise<StreamOutcome>;
|
|
63
60
|
|
|
64
|
-
export { type CloseKind, type OpenSocket, type ReconnectDecision,
|
|
61
|
+
export { type CloseKind, type OpenSocket, type ReconnectDecision, StreamAuthError, StreamFrame, type StreamOutcome, type StreamSessionOptions, type StreamSocket, StreamUnavailableError, WS_CLOSE_AUTH_FAILED, WS_CLOSE_GOING_AWAY, WS_CLOSE_NORMAL, WS_CLOSE_OVER_CAPACITY, WS_CLOSE_SERVER_ERROR, classifyCloseCode, decideReconnect, nextReconnectDelayMs, sessionStatusWord, streamQuery, streamSession };
|
package/dist/stream/index.js
CHANGED
|
@@ -1,32 +1,198 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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,
|