@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.
@@ -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, _req: Request, params: Record<string, string>) => {
14
- const stream = subscribe(params.streamId);
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(sessionId: string): ReadableStream<Uint8Array> {
52
+ export function subscribe(
53
+ sessionId: string,
54
+ afterSeq = 0,
55
+ ): ReadableStream<Uint8Array> {
53
56
  return new ReadableStream<Uint8Array>({
54
- start(controller) {
55
- // Replay any buffered events before going live, so a client that
56
- // connects mid-stream sees the full prefix.
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
- const done = encode({ type: "done", sessionId } as any);
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);