sse.bend
bend-kit-llm@0.2.0.0 · 293 lines · 0 definitions
1# Server-Sent Events parser (WHATWG HTML §9.2.6), fed in pieces. Source: https://github.com/paymog/bend-kit/tree/main/llm
2import Base
3# By hash, as http imports it: bend-kit-bytes@0.3.0.0.
4import bend-kit-bytes@0.3.0.0/bytes.bend as Bytes
5
6# name: the event type, "message" when the stream sets none. data: the data lines joined by LF.
7# id: the last event ID the stream set. name and id hold one Char per octet.
8type Message is Type:
9 Message{name: String, data: Bytes.Bytes, id: String}
10
11# The event so far: its type, its data lines each ended by LF, the last event ID, and the last retry in ms.
12type St is Type:
13 St{name: String, data: Bytes.Bytes, id: String, retry: Maybe<&2, U32>}
14
15# buf from pos: bytes not yet read as lines. No CR or LF is between pos and seen.
16# cr: the last line ended with a CR at the end of buf, so an LF first belongs to it.
17type Reader is Type:
18 Reader{buf: Bytes.Bytes, pos: U32, seen: U32, cr: Bool, st: St}
19
20type Next is Type:
21 Got{e: Message}
22 Want{}
23
24# ponytail: a leading U+FEFF is not dropped; the LLM APIs never send one.
25def reader() -> Reader:
26 Reader{Bytes.new(0), 0, 0, False{}, St{"", Bytes.new(0), "", None{}}}
27
28def feed.at(zero: Bool, buf: Bytes.Bytes, +pos: U32, +seen: U32, cr: Bool, st: St, chunk: Bytes.Bytes) -> Reader:
29 match zero:
30 case True{}:
31 Reader{Bytes.append(buf, chunk), 0, seen, cr, st}
32 case False{}:
33 Reader{Bytes.append(Bytes.snd(Bytes.slice(buf, pos, 4294967295)), chunk), 0, (U32.max(seen, pos) - pos : U32), cr, st}
34
35# Drops the lines already read, then adds chunk.
36def feed(r: Reader, chunk: Bytes.Bytes) -> Reader:
37 Reader{buf, +pos, +seen, cr, st} = r
38 feed.at(U32.is_zero(pos), buf, pos, seen, cr, st, chunk)
39
40# The last retry field, in ms.
41def retry(r: Reader) -> Reader & Maybe<&2, U32>:
42 Reader{buf, pos, seen, cr, St{name, data, id, +m}} = r
43 (Reader{buf, pos, seen, cr, St{name, data, id, m}}, m)
44
45# The first CR or LF at or after i, or the end. f counts the bytes from i to the end; r holds byte i.
46def eol(f: Nat, r: Array<U32> & U32, +i: U32) -> Array<U32> & U32:
47 match f:
48 case 0n:
49 (a, c) = r
50 (a, i)
51 case 1n+p:
52 (a, +c) = r
53 match c:
54 case 10:
55 (a, i)
56 case 13:
57 (a, i)
58 case _:
59 eol(p, Bytes.peek(a, (i + 1 : U32)), (i + 1 : U32))
60
61# The first colon at or after i, or the line's end. f counts the bytes from i to the line's end; r holds byte i.
62def colon(f: Nat, r: Array<U32> & U32, +i: U32) -> Array<U32> & U32:
63 match f:
64 case 0n:
65 (a, c) = r
66 (a, i)
67 case 1n+p:
68 (a, +c) = r
69 match c:
70 case 58:
71 (a, i)
72 case _:
73 colon(p, Bytes.peek(a, (i + 1 : U32)), (i + 1 : U32))
74
75# Base-ten digits, at most 9 of them; None for any other text.
76def digits(s: String, +acc: U32, +n: U32, bad: Bool) -> Maybe<&2, U32>:
77 match s:
78 case SNil{}:
79 Bool.pick(Maybe<&2, U32>, Bool.or(bad, Bool.or(U32.is_zero(n), U32.is_lt(9, n))), None{}, Some{acc})
80 case SCon{Chr{+c}, t}:
81 digits(t, (acc * 10 + (c - 48 : U32) : U32), (n + 1 : U32), Bool.or(bad, Bool.or(U32.is_lt(c, 48), U32.is_lt(57, c))))
82
83def has_nul(s: String) -> Bool:
84 match s:
85 case SNil{}:
86 False{}
87 case SCon{Chr{+c}, t}:
88 Bool.or(U32.is_zero(c), has_nul(t))
89
90def lf() -> Bytes.Bytes:
91 Bytes.from_string("\n")
92
93def kind.is(+k: U32, r: Array<U32> & Bool) -> Array<U32> & U32:
94 (a, +hit) = r
95 (a, Bool.pick(U32, hit, k, 0))
96
97def kind.five(+s: U32, r: Array<U32> & Bool) -> Array<U32> & U32:
98 (a, hit) = r
99 match hit:
100 case True{}:
101 (a, 2)
102 case False{}:
103 kind.is(4, Bytes.at(a, "retry", s))
104
105# The kind of the field whose n-byte name starts at s: 1 data, 2 event, 3 id, 4 retry, 0 any other.
106def kind(+n: U32, a: Array<U32>, +s: U32) -> Array<U32> & U32:
107 match n:
108 case 2:
109 kind.is(3, Bytes.at(a, "id", s))
110 case 4:
111 kind.is(1, Bytes.at(a, "data", s))
112 case 5:
113 kind.five(s, Bytes.at(a, "event", s))
114 case _:
115 (a, 0)
116
117def set.retry(m: Maybe<&2, U32>, old: Maybe<&2, U32>) -> Maybe<&2, U32>:
118 match m:
119 case None{}:
120 old
121 case Some{x}:
122 Some{x}
123
124# The event after a field of kind k with value v. An id with NUL and a retry that is not digits change nothing.
125def set(+k: U32, v: Bytes.Bytes, st: St) -> St:
126 match k:
127 case 1:
128 St{name, data, id, retry} = st
129 St{name, Bytes.append(Bytes.append(data, v), lf()), id, retry}
130 case 2:
131 St{old, data, id, retry} = st
132 St{Bytes.to_string(v), data, id, retry}
133 case 3:
134 St{name, data, old, retry} = st
135 +s = Bytes.to_string(v)
136 St{name, data, Bool.pick(String, has_nul(s), old, s), retry}
137 case 4:
138 St{name, data, id, old} = st
139 St{name, data, id, set.retry(digits(Bytes.to_string(v), 0, 0, False{}), old)}
140 case _:
141 st
142
143def field.put(+k: U32, st: St, r: Bytes.Bytes & Bytes.Bytes) -> Array<U32> & St:
144 (Bytes.Bytes{l, a}, v) = r
145 (a, set(k, v, st))
146
147def field.set(+len: U32, +v: U32, +e: U32, st: St, r: Array<U32> & U32) -> Array<U32> & St:
148 (a, +k) = r
149 field.put(k, st, Bytes.slice(Bytes.Bytes{len, a}, v, (e - v : U32)))
150
151# r holds the byte after the colon at c. The value starts after the colon and one space, if there is one.
152def field.at(+s: U32, +c: U32, +e: U32, +len: U32, st: St, r: Array<U32> & U32) -> Array<U32> & St:
153 (a, +b) = r
154 +v = Bool.pick(U32, U32.is_eq(c, e), e, Bool.pick(U32, Bool.and(U32.is_lt((c + 1 : U32), e), U32.is_eq(b, 32)), (c + 2 : U32), (c + 1 : U32)))
155 field.set(len, v, e, st, kind((c - s : U32), a, s))
156
157def field.colon(+s: U32, +e: U32, +len: U32, st: St, r: Array<U32> & U32) -> Array<U32> & St:
158 (a, +c) = r
159 field.at(s, c, e, len, st, Bytes.peek(a, (c + 1 : U32)))
160
161# The line from s to e as a field: the name up to the first colon, the value after it. A comment's name is empty.
162def field(a: Array<U32>, +len: U32, +s: U32, +e: U32, st: St) -> Array<U32> & St:
163 field.colon(s, e, len, st, colon(U32.to_nat((e - s : U32)), Bytes.peek(a, s), s))
164
165# Drops the last LF. Bytes past len stay 0.
166def trim(b: Bytes.Bytes) -> Bytes.Bytes:
167 Bytes.Bytes{+n, buf} = b
168 Bytes.Bytes{(n - 1 : U32), Bytes.poke(buf, (n - 1 : U32), 0)}
169
170def dispatch.of(empty: Bool, +name: String, data: Bytes.Bytes, +id: String, retry: Maybe<&2, U32>) -> St & Maybe<&1, Message>:
171 match empty:
172 case True{}:
173 (St{"", data, id, retry}, None{})
174 case False{}:
175 (St{"", Bytes.new(0), id, retry}, Some{Message{Bool.pick(String, String.is_empty(name), "message", name), trim(data), id}})
176
177# A blank line: the event, when it has data. Either way the event type starts over.
178def dispatch(st: St) -> St & Maybe<&1, Message>:
179 St{name, Bytes.Bytes{+n, buf}, id, retry} = st
180 dispatch.of(U32.is_zero(n), name, Bytes.Bytes{n, buf}, id, retry)
181
182# A line was read and more may follow; the bytes ran out first; or a line ended an event.
183type Line is Type:
184 LMore{a: Array<U32>, pos: U32, cr: Bool, st: St}
185 LWant{a: Array<U32>, pos: U32, cr: Bool, st: St}
186 LGot{a: Array<U32>, pos: U32, cr: Bool, st: St, e: Message}
187
188def line.got(a: Array<U32>, +next: U32, cr: Bool, r: St & Maybe<&1, Message>) -> Line:
189 (st, m) = r
190 match m:
191 case None{}:
192 LMore{a, next, cr, st}
193 case Some{e}:
194 LGot{a, next, cr, st, e}
195
196def line.set(+next: U32, cr: Bool, r: Array<U32> & St) -> Line:
197 (a, st) = r
198 LMore{a, next, cr, st}
199
200# The line from s to e; the next line starts at next.
201def line.body(blank: Bool, a: Array<U32>, +len: U32, +s: U32, +e: U32, +next: U32, cr: Bool, st: St) -> Line:
202 match blank:
203 case True{}:
204 line.got(a, next, cr, dispatch(st))
205 case False{}:
206 line.set(next, cr, field(a, len, s, e, st))
207
208def line.crlf(+len: U32, +s: U32, +e: U32, st: St, r: Array<U32> & U32) -> Line:
209 (a, +c) = r
210 line.body(U32.is_eq(s, e), a, len, s, e, Bool.pick(U32, U32.is_eq(c, 10), (e + 2 : U32), (e + 1 : U32)), False{}, st)
211
212# A CR at e. With nothing after it yet, an LF may still come.
213def line.cr(more: Bool, a: Array<U32>, +len: U32, +s: U32, +e: U32, st: St) -> Line:
214 match more:
215 case True{}:
216 line.crlf(len, s, e, st, Bytes.peek(a, (e + 1 : U32)))
217 case False{}:
218 line.body(U32.is_eq(s, e), a, len, s, e, (e + 1 : U32), True{}, st)
219
220def line.term(+len: U32, +s: U32, +e: U32, st: St, r: Array<U32> & U32) -> Line:
221 (a, +t) = r
222 match t:
223 case 13:
224 line.cr(U32.is_lt((e + 1 : U32), len), a, len, s, e, st)
225 case _:
226 line.body(U32.is_eq(s, e), a, len, s, e, (e + 1 : U32), False{}, st)
227
228def line.end(want: Bool, a: Array<U32>, +len: U32, +s: U32, +e: U32, cr: Bool, st: St) -> Line:
229 match want:
230 case True{}:
231 LWant{a, s, cr, st}
232 case False{}:
233 line.term(len, s, e, st, Bytes.peek(a, e))
234
235def line.eol(+len: U32, +s: U32, cr: Bool, st: St, r: Array<U32> & U32) -> Line:
236 (a, +e) = r
237 line.end(U32.is_eq(e, len), a, len, s, e, cr, st)
238
239# The line that starts at s. No CR or LF lies between s and seen.
240def line(a: Array<U32>, +len: U32, +s: U32, +seen: U32, cr: Bool, st: St) -> Line:
241 +i = U32.max(s, seen)
242 line.eol(len, s, cr, st, eol(U32.to_nat((len - i : U32)), Bytes.peek(a, i), i))
243
244def stop(l: Line, +len: U32) -> Reader & Next:
245 match l:
246 case LMore{a, +pos, cr, st}:
247 (Reader{Bytes.Bytes{len, a}, pos, pos, cr, st}, Want{})
248 case LWant{a, +pos, cr, st}:
249 (Reader{Bytes.Bytes{len, a}, pos, len, cr, st}, Want{})
250 case LGot{a, +pos, cr, st, e}:
251 (Reader{Bytes.Bytes{len, a}, pos, pos, cr, st}, Got{e})
252
253# f: at least the lines left, plus one.
254def loop(f: Nat, l: Line, +len: U32) -> Reader & Next:
255 match f:
256 case 0n:
257 stop(l, len)
258 case 1n+p:
259 match l:
260 case LMore{a, +pos, cr, st}:
261 loop(p, line(a, len, pos, pos, cr, st), len)
262 case LWant{a, +pos, cr, st}:
263 stop(LWant{a, pos, cr, st}, len)
264 case LGot{a, +pos, cr, st, e}:
265 stop(LGot{a, pos, cr, st, e}, len)
266
267def skip.byte(+pos: U32, r: Array<U32> & U32) -> Array<U32> & U32 & Bool:
268 (a, +c) = r
269 (a, Bool.pick(U32, U32.is_eq(c, 10), (pos + 1 : U32), pos), False{})
270
271def skip.at(some: Bool, a: Array<U32>, +pos: U32) -> Array<U32> & U32 & Bool:
272 match some:
273 case True{}:
274 skip.byte(pos, Bytes.peek(a, pos))
275 case False{}:
276 (a, pos, True{})
277
278# After a CR that ended the last piece, an LF first belongs to that CR.
279def skip(cr: Bool, a: Array<U32>, +len: U32, +pos: U32) -> Array<U32> & U32 & Bool:
280 match cr:
281 case True{}:
282 skip.at(U32.is_lt(pos, len), a, pos)
283 case False{}:
284 (a, pos, False{})
285
286def next.at(+len: U32, +seen: U32, st: St, r: Array<U32> & U32 & Bool) -> Reader & Next:
287 (a, +pos, cr) = r
288 loop(Nat.add(U32.to_nat((len - pos : U32)), 2n), line(a, len, pos, seen, cr, st), len)
289
290# The next whole event, or Want when the bytes run out first. A blank line with no data before it gives no event.
291def next(r: Reader) -> Reader & Next:
292 Reader{Bytes.Bytes{+len, a}, +pos, +seen, cr, st} = r
293 next.at(len, seen, st, skip(cr, a, len, pos))