@arcote.tech/arc-chat 0.7.7 → 0.7.9
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/package.json +7 -6
- package/src/aggregates/message.ts +131 -7
- package/src/chat-builder.ts +8 -1
- package/src/listeners/ai-generation-listener.ts +176 -36
- package/src/react/chat-component.tsx +283 -164
- package/src/routes/chat-stream-route.ts +7 -2
- package/src/streaming/stream-registry.ts +24 -5
|
@@ -10,8 +10,13 @@ export function createChatStreamRoute(config: {
|
|
|
10
10
|
.path(`/chat/${config.name}/stream/:streamId`)
|
|
11
11
|
.protectBy(config.userToken, () => true)
|
|
12
12
|
.handle({
|
|
13
|
-
GET: async (_ctx,
|
|
14
|
-
|
|
13
|
+
GET: async (_ctx, req: Request, params: Record<string, string>) => {
|
|
14
|
+
// Klient po reload przekazuje `?afterSeq=N` z `partialLastSeq`
|
|
15
|
+
// odczytanego z DB. Replay buffer pomija eventy już zaaplikowane.
|
|
16
|
+
const url = new URL(req.url);
|
|
17
|
+
const afterSeqRaw = url.searchParams.get("afterSeq");
|
|
18
|
+
const afterSeq = afterSeqRaw ? Number.parseInt(afterSeqRaw, 10) || 0 : 0;
|
|
19
|
+
const stream = subscribe(params.streamId, afterSeq);
|
|
15
20
|
|
|
16
21
|
return new Response(stream, {
|
|
17
22
|
headers: {
|
|
@@ -49,19 +49,35 @@ export function broadcast(sessionId: string, event: ChatStreamEvent): void {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export function subscribe(
|
|
52
|
+
export function subscribe(
|
|
53
|
+
sessionId: string,
|
|
54
|
+
afterSeq = 0,
|
|
55
|
+
): ReadableStream<Uint8Array> {
|
|
53
56
|
return new ReadableStream<Uint8Array>({
|
|
54
|
-
start(controller) {
|
|
55
|
-
// Replay
|
|
56
|
-
//
|
|
57
|
+
async start(controller) {
|
|
58
|
+
// Replay buffered events with seq > afterSeq. Klient po reload czyta
|
|
59
|
+
// `partialLastSeq` z DB i przekazuje go jako afterSeq — buffer pomija
|
|
60
|
+
// już-zaaplikowane chunki (eliminuje duplikację typu "Dobrze — Dobrze —").
|
|
61
|
+
//
|
|
62
|
+
// Co 10 eventów yield (`setTimeout(16)`) — bez tego cały bufor leci
|
|
63
|
+
// w jednym chunku TCP, klient nie ma szansy zrobić reader.read() +
|
|
64
|
+
// render między burstami → React batchuje setTimeline w jeden render,
|
|
65
|
+
// streaming niewidoczny. 16ms ≈ rAF tick: 100 eventów = 160ms widocznego
|
|
66
|
+
// streamu zamiast 0ms burst.
|
|
57
67
|
const buf = buffers.get(sessionId);
|
|
58
68
|
if (buf) {
|
|
69
|
+
let count = 0;
|
|
59
70
|
for (const e of buf) {
|
|
71
|
+
if (e.seq <= afterSeq) continue;
|
|
60
72
|
try {
|
|
61
73
|
controller.enqueue(encode(e));
|
|
62
74
|
} catch {
|
|
63
75
|
return;
|
|
64
76
|
}
|
|
77
|
+
count++;
|
|
78
|
+
if (count % 10 === 0) {
|
|
79
|
+
await new Promise<void>((r) => setTimeout(r, 16));
|
|
80
|
+
}
|
|
65
81
|
}
|
|
66
82
|
}
|
|
67
83
|
|
|
@@ -106,7 +122,10 @@ export function subscribe(sessionId: string): ReadableStream<Uint8Array> {
|
|
|
106
122
|
export function endStream(sessionId: string): void {
|
|
107
123
|
const controllers = streams.get(sessionId);
|
|
108
124
|
if (controllers) {
|
|
109
|
-
|
|
125
|
+
// seq dla `done` — przerzucamy ostatni z bufora lub 0 gdy pusto.
|
|
126
|
+
const buf = buffers.get(sessionId);
|
|
127
|
+
const lastSeq = buf && buf.length > 0 ? buf[buf.length - 1].seq : 0;
|
|
128
|
+
const done = encode({ type: "done", sessionId, seq: lastSeq + 1 } as ChatStreamEvent);
|
|
110
129
|
for (const controller of controllers) {
|
|
111
130
|
try {
|
|
112
131
|
controller.enqueue(done);
|