@lovrozagar/honey 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.
Files changed (131) hide show
  1. package/README.md +51 -0
  2. package/docs/api-ref/go.md +399 -0
  3. package/docs/api-ref/python.md +399 -0
  4. package/docs/api-ref/rust.md +399 -0
  5. package/docs/api-ref/typescript.md +399 -0
  6. package/docs/invalidate-spec.md +303 -0
  7. package/docs/sdk.md +722 -0
  8. package/package.json +120 -0
  9. package/src/accepts.ts +78 -0
  10. package/src/body-limit.ts +100 -0
  11. package/src/build/index.ts +174 -0
  12. package/src/cli-go/auth.go +15 -0
  13. package/src/cli-go/config.go +165 -0
  14. package/src/cli-go/errors.go +57 -0
  15. package/src/cli-go/multipart.go +55 -0
  16. package/src/cli-go/output.go +109 -0
  17. package/src/cli-go/root.go +30 -0
  18. package/src/cli-go/stream.go +88 -0
  19. package/src/cli-go/version.go +5 -0
  20. package/src/cli.ts +204 -0
  21. package/src/client/error.ts +27 -0
  22. package/src/client/http.ts +502 -0
  23. package/src/client/index.ts +127 -0
  24. package/src/client/sdk.ts +265 -0
  25. package/src/client/sse.ts +102 -0
  26. package/src/client/types.ts +211 -0
  27. package/src/client/ws.ts +115 -0
  28. package/src/client-go/errors.go +207 -0
  29. package/src/client-go/invalidation.go +373 -0
  30. package/src/client-go/realtime.go +317 -0
  31. package/src/client-go/result.go +20 -0
  32. package/src/client-go/runtime.go +413 -0
  33. package/src/client-go/sse.go +149 -0
  34. package/src/client-go/transport.go +452 -0
  35. package/src/client-go/ws.go +104 -0
  36. package/src/client-mcp/README.md +10 -0
  37. package/src/client-mcp/server.ts +94 -0
  38. package/src/client-mcp/types.ts +19 -0
  39. package/src/client-python/_errors.py +115 -0
  40. package/src/client-python/_invalidation.py +578 -0
  41. package/src/client-python/_realtime.py +156 -0
  42. package/src/client-python/_runtime.py +469 -0
  43. package/src/client-python/_sse.py +99 -0
  44. package/src/client-python/_transport.py +335 -0
  45. package/src/client-python/_ws.py +131 -0
  46. package/src/client-python/py.typed +0 -0
  47. package/src/client-rust/errors.rs +200 -0
  48. package/src/client-rust/invalidation.rs +441 -0
  49. package/src/client-rust/invalidation_sync.rs +306 -0
  50. package/src/client-rust/realtime.rs +695 -0
  51. package/src/client-rust/result.rs +22 -0
  52. package/src/client-rust/runtime.rs +547 -0
  53. package/src/client-rust/runtime_sync.rs +452 -0
  54. package/src/client-rust/sse.rs +167 -0
  55. package/src/client-rust/ws.rs +143 -0
  56. package/src/cloudflare/realtime.ts +236 -0
  57. package/src/codegen-go-cli.ts +1180 -0
  58. package/src/codegen-go.ts +1010 -0
  59. package/src/codegen-hash.ts +8 -0
  60. package/src/codegen-ir.ts +517 -0
  61. package/src/codegen-loaders.ts +53 -0
  62. package/src/codegen-mcp.ts +364 -0
  63. package/src/codegen-python.ts +1028 -0
  64. package/src/codegen-rust.ts +1652 -0
  65. package/src/codegen-sanitize.ts +67 -0
  66. package/src/codegen-schema-naming.ts +146 -0
  67. package/src/codegen.ts +5015 -0
  68. package/src/context.ts +210 -0
  69. package/src/cookie-sign.ts +58 -0
  70. package/src/cookie.ts +89 -0
  71. package/src/cors.ts +96 -0
  72. package/src/crypto.ts +19 -0
  73. package/src/csrf.ts +54 -0
  74. package/src/curl-logger.ts +235 -0
  75. package/src/detect-runtime.ts +19 -0
  76. package/src/error.ts +52 -0
  77. package/src/errors.ts +87 -0
  78. package/src/etag.ts +74 -0
  79. package/src/feature-load.ts +23 -0
  80. package/src/go-type-emitter.ts +404 -0
  81. package/src/honey-response.ts +354 -0
  82. package/src/i18n-slot.ts +20 -0
  83. package/src/i18n.ts +234 -0
  84. package/src/index.ts +3843 -0
  85. package/src/init.ts +149 -0
  86. package/src/input.ts +8 -0
  87. package/src/ip-restrict.ts +192 -0
  88. package/src/logger.ts +149 -0
  89. package/src/middleware.ts +199 -0
  90. package/src/node-request.ts +240 -0
  91. package/src/node.ts +212 -0
  92. package/src/openapi/register.ts +19 -0
  93. package/src/openapi/scalar.ts +33 -0
  94. package/src/openapi/spec-factory.ts +46 -0
  95. package/src/openapi/spec.ts +31 -0
  96. package/src/openapi/swagger.ts +34 -0
  97. package/src/plugin.ts +771 -0
  98. package/src/powered-by.ts +17 -0
  99. package/src/pretty-json.ts +39 -0
  100. package/src/proxy.ts +155 -0
  101. package/src/python-type-emitter.ts +144 -0
  102. package/src/realtime/buffer.ts +101 -0
  103. package/src/realtime/bus.ts +93 -0
  104. package/src/realtime/longpoll.ts +148 -0
  105. package/src/realtime/protocol.ts +128 -0
  106. package/src/realtime/route.ts +104 -0
  107. package/src/realtime/runtime.ts +430 -0
  108. package/src/request-id.ts +23 -0
  109. package/src/request-to-curl.ts +34 -0
  110. package/src/response.ts +414 -0
  111. package/src/rust-type-emitter.ts +412 -0
  112. package/src/secure-headers.ts +69 -0
  113. package/src/serve-register.ts +8 -0
  114. package/src/serve-slot.ts +20 -0
  115. package/src/serve.ts +141 -0
  116. package/src/server-timing.ts +66 -0
  117. package/src/static.ts +60 -0
  118. package/src/telemetry/otel.ts +139 -0
  119. package/src/testing.ts +114 -0
  120. package/src/timeout.ts +36 -0
  121. package/src/tree.ts +464 -0
  122. package/src/ts-type-emitter.ts +106 -0
  123. package/src/type-emitter.ts +524 -0
  124. package/src/type-extractor.ts +749 -0
  125. package/src/types.ts +638 -0
  126. package/src/validation.ts +300 -0
  127. package/src/ws/bun.ts +89 -0
  128. package/src/ws/cloudflare.ts +133 -0
  129. package/src/ws/deno.ts +109 -0
  130. package/src/ws/node.ts +161 -0
  131. package/src/yaml.ts +80 -0
@@ -0,0 +1,413 @@
1
+ // Code generated by honey. DO NOT EDIT.
2
+
3
+ package sdk
4
+
5
+ import (
6
+ "bytes"
7
+ "context"
8
+ "crypto/rand"
9
+ "encoding/json"
10
+ "errors"
11
+ "fmt"
12
+ "io"
13
+ "net/http"
14
+ "net/url"
15
+ "time"
16
+ )
17
+
18
+ // RequestContext carries mutable request metadata through OnRequest hook
19
+ // invocations. Hooks may mutate Method, URL, Headers, or Body; mutations are
20
+ // applied to the outgoing request after the full hook chain completes.
21
+ // Selector / IsStale / InvalidatedBy mirror the TS hook ctx and are populated
22
+ // only when the SDK has an active StaleTracker.
23
+ type RequestContext struct {
24
+ Method string
25
+ URL string
26
+ Path string
27
+ Headers http.Header
28
+ Body []byte
29
+ IsRetry bool
30
+ Selector string
31
+ IsStale bool
32
+ InvalidatedBy []string
33
+ }
34
+
35
+ // ResponseContext carries response metadata through OnResponse hook invocations.
36
+ // Hooks may inspect or mutate Body bytes prior to JSON unmarshal in the caller.
37
+ // Status is observational and is not mutated by hooks.
38
+ // Selector / IsStale / InvalidatedBy mirror the TS hook ctx and are populated
39
+ // only when the SDK has an active StaleTracker.
40
+ type ResponseContext struct {
41
+ Status int
42
+ Headers http.Header
43
+ Body []byte
44
+ IsRetry bool
45
+ Selector string
46
+ IsStale bool
47
+ InvalidatedBy []string
48
+ }
49
+
50
+ // Config holds all configuration for the SDK client.
51
+ type Config struct {
52
+ BaseURL string
53
+ BearerToken string
54
+ Headers map[string]string
55
+ Timeout time.Duration
56
+ ThrowOnError bool
57
+ StaleMaxEntries int
58
+ Invalidation *InvalidationConfig
59
+ OnAuthExpired func(context.Context) (string, error)
60
+ HTTPClient *http.Client
61
+ AuthHeaderName string
62
+ AuthHeaderPrefix string
63
+ OnRequest []func(*RequestContext) error
64
+ OnResponse []func(*ResponseContext) error
65
+ OnLog func(LogEntry)
66
+ }
67
+
68
+ // LogEntry is the payload passed to Config.OnLog. Consumers plug in their own
69
+ // logger; the SDK never formats, levels, or persists. Status is a pointer so
70
+ // nil encodes "omitted" (request_start) without colliding with a real 0 status.
71
+ // Err is the underlying error on the "error" event; nil otherwise. JSON tags
72
+ // match the TS/Python LogEntry shape for cross-language consumer parity.
73
+ type LogEntry struct {
74
+ Level string `json:"level"`
75
+ Event string `json:"event"`
76
+ Operation string `json:"operation"`
77
+ DurationMs int64 `json:"duration_ms"`
78
+ Status *int `json:"status,omitempty"`
79
+ Err error `json:"-"`
80
+ }
81
+
82
+ // emitLog invokes the OnLog callback if configured, swallowing any panic so a
83
+ // faulty logger cannot break the request path. Mirrors Python _emit_log /
84
+ // TS optional-chained call.
85
+ func emitLog(cfg Config, entry LogEntry) {
86
+ if cfg.OnLog == nil {
87
+ return
88
+ }
89
+ defer func() {
90
+ _ = recover()
91
+ }()
92
+ cfg.OnLog(entry)
93
+ }
94
+
95
+ func errStatus(err error) *int {
96
+ var apiErr APIError
97
+ if errors.As(err, &apiErr) {
98
+ s := apiErr.Status()
99
+ return &s
100
+ }
101
+ return nil
102
+ }
103
+
104
+ // InvalidationConfig controls cache invalidation behaviour. StaleTime is in
105
+ // milliseconds; ≤0 disables the tracker. StaleMaxEntries caps the LRU map.
106
+ // MaxSourcesPerTarget caps the `by` list per stale entry.
107
+ type InvalidationConfig struct {
108
+ StaleTime int64
109
+ StaleMaxEntries int
110
+ MaxSourcesPerTarget int
111
+ }
112
+
113
+ type requestResult struct {
114
+ body []byte
115
+ status int
116
+ resp *http.Response
117
+ }
118
+
119
+ // doRequest executes an HTTP request, injects auth, merges headers, fires
120
+ // OnRequest/OnResponse hooks in declaration order, handles OnAuthExpired retry
121
+ // on 401, and wraps context cancellation errors. Emits OnLog lifecycle entries
122
+ // (request_start once at entry, response_received once for the final response,
123
+ // error on terminal failure) — matching TS/Python behavior where retry rounds
124
+ // do not double-emit.
125
+ func doRequest(
126
+ ctx context.Context,
127
+ cfg Config,
128
+ method string,
129
+ rawURL string,
130
+ queryParams url.Values,
131
+ bodyData interface{},
132
+ bodyReader io.Reader,
133
+ contentType string,
134
+ callHeaders map[string]string,
135
+ operation string,
136
+ requestMeta *RequestMeta,
137
+ ) (result *requestResult, err error) {
138
+ start := time.Now()
139
+ defer func() {
140
+ if err != nil {
141
+ emitLog(cfg, LogEntry{
142
+ Level: "error",
143
+ Event: "error",
144
+ Operation: operation,
145
+ DurationMs: time.Since(start).Milliseconds(),
146
+ Status: errStatus(err),
147
+ Err: err,
148
+ })
149
+ }
150
+ }()
151
+
152
+ if ctx.Err() != nil {
153
+ return nil, fmt.Errorf("honey: request canceled: %w", ctx.Err())
154
+ }
155
+
156
+ emitLog(cfg, LogEntry{
157
+ Level: "debug",
158
+ Event: "request_start",
159
+ Operation: operation,
160
+ })
161
+
162
+ result, err = executeRequest(ctx, cfg, method, rawURL, queryParams, bodyData, bodyReader, contentType, callHeaders, cfg.BearerToken, false, requestMeta)
163
+ if err != nil {
164
+ return nil, err
165
+ }
166
+
167
+ if result.status == 401 && cfg.OnAuthExpired != nil {
168
+ newToken, authErr := cfg.OnAuthExpired(ctx)
169
+ if authErr != nil {
170
+ err = fmt.Errorf("honey: OnAuthExpired failed: %w", authErr)
171
+ return nil, err
172
+ }
173
+ /* streaming bodies are single-shot: once consumed by the first request,
174
+ * retry has no way to replay. Documented as v1 non-goal — consumers
175
+ * needing auto-retry on streamed uploads wrap in an outer retry loop. */
176
+ result, err = executeRequest(ctx, cfg, method, rawURL, queryParams, bodyData, bodyReader, contentType, callHeaders, newToken, true, requestMeta)
177
+ if err != nil {
178
+ return nil, err
179
+ }
180
+ }
181
+
182
+ status := result.status
183
+ level := "info"
184
+ if status >= 400 {
185
+ level = "warn"
186
+ }
187
+ emitLog(cfg, LogEntry{
188
+ Level: level,
189
+ Event: "response_received",
190
+ Operation: operation,
191
+ DurationMs: time.Since(start).Milliseconds(),
192
+ Status: &status,
193
+ })
194
+
195
+ if err2 := raiseForStatus(result.resp, result.body); err2 != nil {
196
+ err = err2
197
+ return nil, err
198
+ }
199
+
200
+ return result, nil
201
+ }
202
+
203
+ func executeRequest(
204
+ ctx context.Context,
205
+ cfg Config,
206
+ method string,
207
+ rawURL string,
208
+ queryParams url.Values,
209
+ bodyData interface{},
210
+ bodyReader io.Reader,
211
+ contentType string,
212
+ callHeaders map[string]string,
213
+ bearerToken string,
214
+ isRetry bool,
215
+ requestMeta *RequestMeta,
216
+ ) (*requestResult, error) {
217
+ fullURL, err := buildURL(cfg.BaseURL, rawURL, queryParams)
218
+ if err != nil {
219
+ return nil, fmt.Errorf("honey: build URL: %w", err)
220
+ }
221
+
222
+ /* bodyReader wins over bodyData — streaming uploads bypass JSON marshal.
223
+ * RequestContext.Body stays nil for streams so hook consumers don't try to
224
+ * read or re-hash a single-shot reader. */
225
+ var bodyBytes []byte
226
+ if bodyReader == nil && bodyData != nil {
227
+ encoded, encErr := json.Marshal(bodyData)
228
+ if encErr != nil {
229
+ return nil, fmt.Errorf("honey: marshal body: %w", encErr)
230
+ }
231
+ bodyBytes = encoded
232
+ }
233
+ hasBody := bodyReader != nil || bodyData != nil
234
+
235
+ headers := mergeHeaders(cfg, callHeaders, bearerToken, hasBody, contentType)
236
+
237
+ reqCtx := &RequestContext{
238
+ Method: method,
239
+ URL: fullURL,
240
+ Path: rawURL,
241
+ Headers: headers,
242
+ Body: bodyBytes,
243
+ IsRetry: isRetry,
244
+ }
245
+ if requestMeta != nil {
246
+ reqCtx.Selector = requestMeta.Selector
247
+ reqCtx.IsStale = requestMeta.IsStale
248
+ reqCtx.InvalidatedBy = requestMeta.InvalidatedBy
249
+ }
250
+
251
+ for _, hook := range cfg.OnRequest {
252
+ if hookErr := hook(reqCtx); hookErr != nil {
253
+ return nil, fmt.Errorf("honey: OnRequest hook: %w", hookErr)
254
+ }
255
+ }
256
+
257
+ var reader io.Reader
258
+ if bodyReader != nil {
259
+ reader = bodyReader
260
+ } else if reqCtx.Body != nil {
261
+ reader = bytes.NewReader(reqCtx.Body)
262
+ }
263
+
264
+ req, err := http.NewRequestWithContext(ctx, reqCtx.Method, reqCtx.URL, reader)
265
+ if err != nil {
266
+ return nil, fmt.Errorf("honey: build request: %w", err)
267
+ }
268
+ req.Header = reqCtx.Headers
269
+
270
+ resp, err := cfg.HTTPClient.Do(req)
271
+ if err != nil {
272
+ if ctx.Err() != nil {
273
+ if errors.Is(ctx.Err(), context.DeadlineExceeded) {
274
+ return nil, fmt.Errorf("honey: request timed out: %w", ctx.Err())
275
+ }
276
+ if errors.Is(ctx.Err(), context.Canceled) {
277
+ return nil, fmt.Errorf("honey: request canceled: %w", ctx.Err())
278
+ }
279
+ return nil, fmt.Errorf("honey: context error: %w", ctx.Err())
280
+ }
281
+ return nil, fmt.Errorf("honey: http do: %w", err)
282
+ }
283
+ defer resp.Body.Close()
284
+
285
+ body, readErr := io.ReadAll(resp.Body)
286
+ if readErr != nil {
287
+ return nil, fmt.Errorf("honey: read body: %w", readErr)
288
+ }
289
+
290
+ respCtx := &ResponseContext{
291
+ Status: resp.StatusCode,
292
+ Headers: resp.Header,
293
+ Body: body,
294
+ IsRetry: isRetry,
295
+ }
296
+ if requestMeta != nil {
297
+ respCtx.Selector = requestMeta.Selector
298
+ respCtx.IsStale = requestMeta.IsStale
299
+ respCtx.InvalidatedBy = requestMeta.InvalidatedBy
300
+ }
301
+
302
+ for _, hook := range cfg.OnResponse {
303
+ if hookErr := hook(respCtx); hookErr != nil {
304
+ return nil, fmt.Errorf("honey: OnResponse hook: %w", hookErr)
305
+ }
306
+ }
307
+
308
+ return &requestResult{body: respCtx.Body, status: resp.StatusCode, resp: resp}, nil
309
+ }
310
+
311
+ func buildURL(baseURL, path string, queryParams url.Values) (string, error) {
312
+ base, err := url.Parse(baseURL)
313
+ if err != nil {
314
+ return "", err
315
+ }
316
+ ref, err := url.Parse(path)
317
+ if err != nil {
318
+ return "", err
319
+ }
320
+ resolved := base.ResolveReference(ref)
321
+ if len(queryParams) > 0 {
322
+ existing := resolved.Query()
323
+ for k, vals := range queryParams {
324
+ for _, v := range vals {
325
+ existing.Add(k, v)
326
+ }
327
+ }
328
+ resolved.RawQuery = existing.Encode()
329
+ }
330
+ return resolved.String(), nil
331
+ }
332
+
333
+ // mergeHeaders builds the final http.Header for a request by merging in order:
334
+ // config Headers → call Headers (per-call wins per key) → Content-Type when
335
+ // hasBody → Authorization when bearerToken non-empty. Returned header is owned
336
+ // by the caller and may be mutated freely (e.g. by OnRequest hooks).
337
+ // contentType is the explicit Content-Type for the request body; when empty
338
+ // and hasBody is true, defaults to application/json. Callers pass
339
+ // application/octet-stream for streaming-upload ops.
340
+ func mergeHeaders(
341
+ cfg Config,
342
+ callHeaders map[string]string,
343
+ bearerToken string,
344
+ hasBody bool,
345
+ contentType string,
346
+ ) http.Header {
347
+ h := http.Header{}
348
+ if hasBody {
349
+ ct := contentType
350
+ if ct == "" {
351
+ ct = "application/json"
352
+ }
353
+ h.Set("Content-Type", ct)
354
+ }
355
+ for k, v := range cfg.Headers {
356
+ h.Set(k, v)
357
+ }
358
+ for k, v := range callHeaders {
359
+ h.Set(k, v)
360
+ }
361
+ if bearerToken != "" {
362
+ name := cfg.AuthHeaderName
363
+ if name == "" {
364
+ name = "Authorization"
365
+ }
366
+ prefix := cfg.AuthHeaderPrefix
367
+ if prefix == "" {
368
+ prefix = "Bearer "
369
+ }
370
+ h.Set(name, prefix+bearerToken)
371
+ }
372
+ return h
373
+ }
374
+
375
+ func unmarshalBody[T any](body []byte) (*T, error) {
376
+ if len(body) == 0 {
377
+ return nil, nil
378
+ }
379
+ var out T
380
+ if err := json.Unmarshal(body, &out); err != nil {
381
+ return nil, fmt.Errorf("honey: unmarshal response: %w", err)
382
+ }
383
+ return &out, nil
384
+ }
385
+
386
+ // newUUIDv4 returns a freshly generated RFC 4122 §4.4 random UUID string.
387
+ // Uses crypto/rand for entropy; sets version (4) and variant (10xx) bits per
388
+ // the spec, then formats canonical 8-4-4-4-12 lower-case hex. Zero external
389
+ // deps so generated SDKs add no third-party rows to consumer go.sum files.
390
+ func newUUIDv4() string {
391
+ var b [16]byte
392
+ if _, err := rand.Read(b[:]); err != nil {
393
+ /* crypto/rand.Read is documented to never return non-nil err on supported
394
+ * platforms; if it does the OS entropy source is broken — silent fallback
395
+ * would corrupt dedupe semantics, so panic mirrors google/uuid. */
396
+ panic("honey: crypto/rand failed: " + err.Error())
397
+ }
398
+ b[6] = (b[6] & 0x0f) | 0x40 /* version 4 */
399
+ b[8] = (b[8] & 0x3f) | 0x80 /* variant 10xx */
400
+ const hex = "0123456789abcdef"
401
+ out := make([]byte, 36)
402
+ j := 0
403
+ for i, by := range b {
404
+ if i == 4 || i == 6 || i == 8 || i == 10 {
405
+ out[j] = '-'
406
+ j++
407
+ }
408
+ out[j] = hex[by>>4]
409
+ out[j+1] = hex[by&0x0f]
410
+ j += 2
411
+ }
412
+ return string(out)
413
+ }
@@ -0,0 +1,149 @@
1
+ // Code generated by honey. DO NOT EDIT.
2
+
3
+ package sdk
4
+
5
+ import (
6
+ "bufio"
7
+ "context"
8
+ "io"
9
+ "iter"
10
+ "net/http"
11
+ "strconv"
12
+ "strings"
13
+ )
14
+
15
+ // SSEEvent represents a single server-sent event.
16
+ type SSEEvent struct {
17
+ Data string
18
+ Event string
19
+ ID string
20
+ Retry int
21
+ }
22
+
23
+ const sseMaxBuffer = 1024 * 1024
24
+
25
+ // parseSSEStream returns an iter.Seq2 that yields SSEEvent values read from resp.Body.
26
+ // On non-2xx response it yields a typed error immediately then terminates.
27
+ // The response body is closed when the iterator returns.
28
+ //
29
+ // Ported from public/honey/core/src/client/sse.ts:62-102 — bufio.Scanner with
30
+ // double-newline split mirrors the TextDecoder+split(/\r\n\r\n|…/) approach.
31
+ func parseSSEStream(ctx context.Context, resp *http.Response) iter.Seq2[SSEEvent, error] {
32
+ return func(yield func(SSEEvent, error) bool) {
33
+ defer resp.Body.Close()
34
+
35
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
36
+ body, _ := io.ReadAll(resp.Body)
37
+ apiErr := raiseForStatus(resp, body)
38
+ yield(SSEEvent{}, apiErr)
39
+ return
40
+ }
41
+
42
+ scanner := bufio.NewScanner(resp.Body)
43
+ scanner.Buffer(make([]byte, 0, 4096), sseMaxBuffer)
44
+ scanner.Split(splitDoubleNewline)
45
+
46
+ for scanner.Scan() {
47
+ if ctx.Err() != nil {
48
+ return
49
+ }
50
+ block := scanner.Text()
51
+ if strings.TrimSpace(block) == "" {
52
+ continue
53
+ }
54
+ ev, ok := parseSSEBlock(block)
55
+ if !ok {
56
+ continue
57
+ }
58
+ if !yield(ev, nil) {
59
+ return
60
+ }
61
+ }
62
+
63
+ if err := scanner.Err(); err != nil {
64
+ yield(SSEEvent{}, err)
65
+ }
66
+ }
67
+ }
68
+
69
+ // splitDoubleNewline is a bufio.SplitFunc that splits on \r\n\r\n, \n\n, or \r\r.
70
+ func splitDoubleNewline(data []byte, atEOF bool) (advance int, token []byte, err error) {
71
+ delimiters := []string{"\r\n\r\n", "\n\n", "\r\r"}
72
+ for _, d := range delimiters {
73
+ db := []byte(d)
74
+ for i := 0; i <= len(data)-len(db); i++ {
75
+ match := true
76
+ for j := range db {
77
+ if data[i+j] != db[j] {
78
+ match = false
79
+ break
80
+ }
81
+ }
82
+ if match {
83
+ return i + len(db), data[:i], nil
84
+ }
85
+ }
86
+ }
87
+ if atEOF && len(data) > 0 {
88
+ return len(data), data, nil
89
+ }
90
+ return 0, nil, nil
91
+ }
92
+
93
+ func parseSSEBlock(block string) (SSEEvent, bool) {
94
+ lines := strings.Split(block, "\n")
95
+ isComment := true
96
+ var data string
97
+ hasData := false
98
+ var event string
99
+ var id string
100
+ retry := 0
101
+
102
+ for _, rawLine := range lines {
103
+ line := strings.TrimRight(rawLine, "\r")
104
+ if strings.HasPrefix(line, ":") {
105
+ continue
106
+ }
107
+ isComment = false
108
+
109
+ colonIdx := strings.IndexByte(line, ':')
110
+ var field, val string
111
+ if colonIdx == -1 {
112
+ field = line
113
+ val = ""
114
+ } else {
115
+ field = line[:colonIdx]
116
+ val = line[colonIdx+1:]
117
+ if strings.HasPrefix(val, " ") {
118
+ val = val[1:]
119
+ }
120
+ }
121
+
122
+ switch field {
123
+ case "data":
124
+ if !hasData {
125
+ data = val
126
+ } else {
127
+ data = data + "\n" + val
128
+ }
129
+ hasData = true
130
+ case "event":
131
+ event = val
132
+ case "id":
133
+ /* per spec: id containing null byte must be ignored */
134
+ if !strings.ContainsRune(val, 0) {
135
+ id = val
136
+ }
137
+ case "retry":
138
+ if n, err := strconv.Atoi(val); err == nil && n >= 0 {
139
+ retry = n
140
+ }
141
+ }
142
+ }
143
+
144
+ if isComment || !hasData {
145
+ return SSEEvent{}, false
146
+ }
147
+
148
+ return SSEEvent{Data: data, Event: event, ID: id, Retry: retry}, true
149
+ }