@shelbyb/daywalker-sdk 0.3.0 → 0.4.1

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 (54) hide show
  1. package/README.md +64 -23
  2. package/dist/analytics/index.d.ts +16 -2
  3. package/dist/analytics/index.js +79 -14
  4. package/dist/cli.js +133 -31
  5. package/dist/client.d.ts +29 -2
  6. package/dist/client.js +314 -62
  7. package/dist/config.d.ts +8 -2
  8. package/dist/config.js +14 -0
  9. package/dist/crypto/encoding.d.ts +5 -0
  10. package/dist/crypto/encoding.js +14 -0
  11. package/dist/crypto/index.d.ts +3 -3
  12. package/dist/crypto/index.js +2 -2
  13. package/dist/crypto/keys.d.ts +3 -3
  14. package/dist/crypto/keys.js +28 -7
  15. package/dist/crypto/signatures.d.ts +42 -12
  16. package/dist/crypto/signatures.js +236 -79
  17. package/dist/express.d.ts +1 -0
  18. package/dist/express.js +8 -6
  19. package/dist/flags/client.d.ts +32 -4
  20. package/dist/flags/client.js +74 -12
  21. package/dist/flags/common/events.d.ts +9 -0
  22. package/dist/flags/common/events.js +51 -6
  23. package/dist/flags/common/index.d.ts +3 -3
  24. package/dist/flags/common/index.js +3 -3
  25. package/dist/flags/common/sse.d.ts +24 -3
  26. package/dist/flags/common/sse.js +85 -15
  27. package/dist/flags/common/version.d.ts +2 -0
  28. package/dist/flags/common/version.js +2 -0
  29. package/dist/flags/engine/index.d.ts +1 -1
  30. package/dist/flags/engine/index.js +1 -1
  31. package/dist/flags/engine/operators.d.ts +20 -3
  32. package/dist/flags/engine/operators.js +38 -32
  33. package/dist/flags/node.d.ts +26 -14
  34. package/dist/flags/node.js +51 -7
  35. package/dist/flags/react.d.ts +6 -4
  36. package/dist/flags/react.js +47 -17
  37. package/dist/index.d.ts +7 -6
  38. package/dist/index.js +4 -4
  39. package/dist/middleware.d.ts +18 -0
  40. package/dist/middleware.js +28 -7
  41. package/dist/next.d.ts +52 -8
  42. package/dist/next.js +151 -37
  43. package/dist/openfeature/server.d.ts +3 -0
  44. package/dist/openfeature/server.js +14 -2
  45. package/dist/openfeature/shared.d.ts +1 -1
  46. package/dist/openfeature/shared.js +22 -2
  47. package/dist/openfeature/web.js +9 -3
  48. package/dist/types.d.ts +56 -4
  49. package/dist/types.js +6 -0
  50. package/docs/analytics.md +1 -1
  51. package/docs/flags-node.md +1 -1
  52. package/docs/flags.md +2 -1
  53. package/package.json +32 -14
  54. package/assets/logo-512.png +0 -0
package/dist/express.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { daywalker } from "./client.js";
2
+ import { toAuthFailureResponse } from "./middleware.js";
2
3
  /**
3
4
  * Express / Connect / Node http middleware that validates incoming Daywalker tokens.
4
5
  */
5
6
  export function daywalkerAuth(options = {}) {
6
7
  const client = options.client ?? daywalker;
8
+ const serviceSlug = options.serviceSlug;
7
9
  return async (req, res, next) => {
8
10
  const getHeader = (name) => {
9
11
  if (typeof req.get === "function")
@@ -33,19 +35,19 @@ export function daywalkerAuth(options = {}) {
33
35
  res.end(JSON.stringify({ error: "INVALID_TOKEN", message: "Missing Authorization header or x-api-token" }));
34
36
  return;
35
37
  }
36
- const result = await client.validateToken(token);
38
+ const result = await client.validateToken(token, { serviceSlug });
37
39
  if (!result.ok) {
38
- const status = result.status === 429 ? 429 : 401;
39
- if (result.retryAfter) {
40
- res.setHeader("Retry-After", String(result.retryAfter));
40
+ const { status, ...body } = toAuthFailureResponse(result);
41
+ if (body.retryAfter) {
42
+ res.setHeader("Retry-After", String(body.retryAfter));
41
43
  }
42
44
  if (typeof res.status === "function" && typeof res.json === "function") {
43
- res.status(status).json({ error: result.error, message: result.message, retryAfter: result.retryAfter });
45
+ res.status(status).json(body);
44
46
  return;
45
47
  }
46
48
  res.statusCode = status;
47
49
  res.setHeader("Content-Type", "application/json");
48
- res.end(JSON.stringify({ error: result.error, message: result.message, retryAfter: result.retryAfter }));
50
+ res.end(JSON.stringify(body));
49
51
  return;
50
52
  }
51
53
  req.auth = result.data;
@@ -1,6 +1,7 @@
1
1
  import type { ContextInput, EvaluationReason, FlagValue } from "./engine/index.js";
2
- import { type Logger } from "./common/index.js";
2
+ import { SDK_VERSION, type Logger } from "./common/index.js";
3
3
  export type { ContextInput, EvaluationReason, FlagValue, Logger };
4
+ export { SDK_VERSION };
4
5
  export interface FlagState {
5
6
  value: FlagValue;
6
7
  variation: number | null;
@@ -15,7 +16,13 @@ export interface ClientOptions {
15
16
  baseUrl: string;
16
17
  /** Receive changes in real time over Server-Sent Events (default true). */
17
18
  streaming?: boolean;
18
- /** Initial flag values, e.g. from the Node SDK's allFlagsState() for SSR. */
19
+ /**
20
+ * Never touch the network: serve `bootstrap` (or defaults), send no events, open no stream.
21
+ * identify() only switches the context. Used for server-side rendering; handy in tests.
22
+ * The client is ready immediately when `bootstrap` is given.
23
+ */
24
+ offline?: boolean;
25
+ /** Initial flag values, e.g. from the Node SDK's allFlagsState().toJSON() for SSR. */
19
26
  bootstrap?: FlagSet | Record<string, FlagValue>;
20
27
  /** Secure mode hash for the context (HMAC-SHA256 computed on your server). */
21
28
  hash?: string;
@@ -58,16 +65,24 @@ export declare class DevPulseClient {
58
65
  private rejectReady;
59
66
  private initialized;
60
67
  private closed;
68
+ /** Bumped by identify(); responses for an older context are ignored. */
69
+ private contextSeq;
70
+ private stale;
71
+ private detachVisibility;
61
72
  private readonly logger;
62
73
  private readonly base;
63
74
  constructor(clientSideId: string, context: ContextInput, options: ClientOptions);
64
75
  private query;
65
76
  private start;
77
+ /** Fetch flags for the current context and apply them unless identify() moved on (or close()) meanwhile. */
78
+ private loadFlags;
66
79
  private fetchFlags;
67
80
  private onStreamEvent;
68
81
  private replaceAll;
69
82
  private flag;
70
83
  private markReady;
84
+ /** "stale" when the stream drops after loading (flags keep serving), "recovered" once it reconnects. */
85
+ private setStale;
71
86
  private fail;
72
87
  private emitChanges;
73
88
  private cacheKey;
@@ -78,22 +93,35 @@ export declare class DevPulseClient {
78
93
  timeoutMs?: number;
79
94
  }): Promise<void>;
80
95
  get isInitialized(): boolean;
96
+ /** True after close(); a closed client never updates again. */
97
+ get isClosed(): boolean;
81
98
  variation<T extends FlagValue>(key: string, defaultValue: T): T;
82
99
  variationDetail<T extends FlagValue>(key: string, defaultValue: T): EvaluationDetail<T>;
100
+ /** @internal Evaluate without recording an analytics event (React hooks record from an effect). */
101
+ peekVariationDetail<T extends FlagValue>(key: string, defaultValue: T): EvaluationDetail<T>;
102
+ /** @internal Record the analytics event for an evaluation made with peekVariationDetail(). */
103
+ recordEvaluation<T extends FlagValue>(key: string, defaultValue: T, detail: EvaluationDetail<T>): void;
83
104
  boolVariation(key: string, defaultValue: boolean): boolean;
84
105
  stringVariation(key: string, defaultValue: string): string;
85
106
  numberVariation(key: string, defaultValue: number): number;
86
107
  jsonVariation<T extends FlagValue>(key: string, defaultValue: T): T;
87
108
  /** All flag values (doesn't record evaluations). */
88
109
  allFlags(): Record<string, FlagValue>;
89
- /** Switch to a different context (e.g. after login). Resolves with the new flags. */
110
+ /**
111
+ * Switch to a different context (e.g. after login). Resolves with the new flags. When calls
112
+ * overlap, the last one wins: an earlier call's late response is ignored.
113
+ */
90
114
  identify(context: ContextInput, opts?: {
91
115
  hash?: string;
92
116
  }): Promise<Record<string, FlagValue>>;
93
117
  getContext(): ContextInput;
94
118
  /** Record a custom event for metrics and experiments. */
95
119
  track(key: string, data?: unknown, metricValue?: number): void;
96
- on(event: "ready" | "failed" | "change" | `change:${string}`, fn: Listener): () => void;
120
+ /**
121
+ * Events: "ready", "failed" (fatal, e.g. an invalid client-side ID), "change" / "change:<key>",
122
+ * "stale" (stream lost; the last flags keep serving) and "recovered".
123
+ */
124
+ on(event: "ready" | "failed" | "change" | "stale" | "recovered" | `change:${string}`, fn: Listener): () => void;
97
125
  off(event: string, fn: Listener): void;
98
126
  private emit;
99
127
  flush(): Promise<void>;
@@ -1,5 +1,6 @@
1
- import { EventProcessor, SseClient, encodeContext, noopLogger } from "./common/index.js";
1
+ import { EventProcessor, SDK_VERSION, SseClient, encodeContext, noopLogger } from "./common/index.js";
2
2
  import { assertSecureUrl } from "../config.js";
3
+ export { SDK_VERSION };
3
4
  /** A flag map without a prototype, so keys like `__proto__` or `toString` never resolve to inherited values. */
4
5
  function flagMap(entries = {}) {
5
6
  return Object.assign(Object.create(null), entries);
@@ -29,6 +30,10 @@ export class DevPulseClient {
29
30
  rejectReady;
30
31
  initialized = false;
31
32
  closed = false;
33
+ /** Bumped by identify(); responses for an older context are ignored. */
34
+ contextSeq = 0;
35
+ stale = false;
36
+ detachVisibility = null;
32
37
  logger;
33
38
  base;
34
39
  constructor(clientSideId, context, options) {
@@ -54,6 +59,8 @@ export class DevPulseClient {
54
59
  if (cached)
55
60
  this.flags = flagMap(cached);
56
61
  }
62
+ if (options.offline)
63
+ return;
57
64
  if (options.sendEvents !== false && typeof globalThis.fetch === "function") {
58
65
  this.events = new EventProcessor({
59
66
  url: `${this.base}/sdk/v1/events/client/${encodeURIComponent(clientSideId)}`,
@@ -66,10 +73,14 @@ export class DevPulseClient {
66
73
  });
67
74
  this.events.identify(context);
68
75
  const doc = globalThis.document;
69
- doc?.addEventListener("visibilitychange", () => {
70
- if (doc.visibilityState === "hidden")
71
- void this.events?.flush();
72
- });
76
+ if (doc?.addEventListener) {
77
+ const onVisibility = () => {
78
+ if (doc.visibilityState === "hidden")
79
+ void this.events?.flush();
80
+ };
81
+ doc.addEventListener("visibilitychange", onVisibility);
82
+ this.detachVisibility = () => doc.removeEventListener("visibilitychange", onVisibility);
83
+ }
73
84
  }
74
85
  void this.start();
75
86
  }
@@ -84,10 +95,11 @@ export class DevPulseClient {
84
95
  async start() {
85
96
  if (this.options.streaming === false) {
86
97
  try {
87
- this.replaceAll(await this.fetchFlags());
98
+ await this.loadFlags();
88
99
  }
89
100
  catch (err) {
90
- this.fail(err);
101
+ if (!this.closed)
102
+ this.fail(err);
91
103
  }
92
104
  return;
93
105
  }
@@ -95,7 +107,10 @@ export class DevPulseClient {
95
107
  url: () => `${this.base}/sdk/v1/client/${encodeURIComponent(this.clientSideId)}/stream?${this.query()}`,
96
108
  fetch: this.options.fetch,
97
109
  onEvent: (ev) => this.onStreamEvent(ev.type, ev.data),
98
- onOpen: () => this.logger.debug("DevPulse stream connected"),
110
+ onOpen: () => {
111
+ this.logger.debug("DevPulse stream connected");
112
+ this.setStale(false);
113
+ },
99
114
  onError: (err) => {
100
115
  this.logger.warn(`DevPulse stream error: ${err.message}`);
101
116
  if (!err.recoverable) {
@@ -103,13 +118,23 @@ export class DevPulseClient {
103
118
  return false;
104
119
  }
105
120
  // Keep serving flags while reconnecting; fetch once so a first load still succeeds.
121
+ this.setStale(true, err);
106
122
  if (!this.initialized)
107
- this.fetchFlags().then((f) => this.replaceAll(f), () => undefined);
123
+ this.loadFlags().catch(() => undefined);
108
124
  return true;
109
125
  },
110
126
  });
111
127
  this.stream.start();
112
128
  }
129
+ /** Fetch flags for the current context and apply them unless identify() moved on (or close()) meanwhile. */
130
+ async loadFlags() {
131
+ const seq = this.contextSeq;
132
+ const flags = await this.fetchFlags();
133
+ if (seq !== this.contextSeq || this.closed)
134
+ return false;
135
+ this.replaceAll(flags);
136
+ return true;
137
+ }
113
138
  async fetchFlags() {
114
139
  const res = await (this.options.fetch ?? fetch)(`${this.base}/sdk/v1/client/${encodeURIComponent(this.clientSideId)}/flags?${this.query()}`);
115
140
  if (!res.ok)
@@ -168,6 +193,16 @@ export class DevPulseClient {
168
193
  this.resolveReady();
169
194
  this.emit("ready");
170
195
  }
196
+ /** "stale" when the stream drops after loading (flags keep serving), "recovered" once it reconnects. */
197
+ setStale(stale, err) {
198
+ if (!this.initialized || this.closed || this.stale === stale)
199
+ return;
200
+ this.stale = stale;
201
+ if (stale)
202
+ this.emit("stale", err);
203
+ else
204
+ this.emit("recovered");
205
+ }
171
206
  fail(err) {
172
207
  if (!this.initialized)
173
208
  this.rejectReady(err);
@@ -214,10 +249,20 @@ export class DevPulseClient {
214
249
  get isInitialized() {
215
250
  return this.initialized;
216
251
  }
252
+ /** True after close(); a closed client never updates again. */
253
+ get isClosed() {
254
+ return this.closed;
255
+ }
217
256
  variation(key, defaultValue) {
218
257
  return this.variationDetail(key, defaultValue).value;
219
258
  }
220
259
  variationDetail(key, defaultValue) {
260
+ const detail = this.peekVariationDetail(key, defaultValue);
261
+ this.recordEvaluation(key, defaultValue, detail);
262
+ return detail;
263
+ }
264
+ /** @internal Evaluate without recording an analytics event (React hooks record from an effect). */
265
+ peekVariationDetail(key, defaultValue) {
221
266
  const f = this.flag(key);
222
267
  let detail;
223
268
  if (!f)
@@ -229,9 +274,13 @@ export class DevPulseClient {
229
274
  detail = { value: defaultValue, variationIndex: null, reason: { kind: "ERROR", errorKind: "WRONG_TYPE" } };
230
275
  else
231
276
  detail = { value: f.value, variationIndex: f.variation, reason: f.reason ?? null };
232
- this.events?.recordEvaluation({ key, context: this.context, value: detail.value, default: defaultValue, variation: detail.variationIndex, version: f?.version, reason: f?.reason, trackEvents: f?.trackEvents });
233
277
  return detail;
234
278
  }
279
+ /** @internal Record the analytics event for an evaluation made with peekVariationDetail(). */
280
+ recordEvaluation(key, defaultValue, detail) {
281
+ const f = this.flag(key);
282
+ this.events?.recordEvaluation({ key, context: this.context, value: detail.value, default: defaultValue, variation: detail.variationIndex, version: f?.version, reason: f?.reason, trackEvents: f?.trackEvents });
283
+ }
235
284
  boolVariation(key, defaultValue) {
236
285
  return this.variation(key, defaultValue);
237
286
  }
@@ -248,14 +297,21 @@ export class DevPulseClient {
248
297
  allFlags() {
249
298
  return Object.fromEntries(Object.entries(this.flags).map(([k, v]) => [k, v.value]));
250
299
  }
251
- /** Switch to a different context (e.g. after login). Resolves with the new flags. */
300
+ /**
301
+ * Switch to a different context (e.g. after login). Resolves with the new flags. When calls
302
+ * overlap, the last one wins: an earlier call's late response is ignored.
303
+ */
252
304
  async identify(context, opts = {}) {
305
+ this.contextSeq++;
253
306
  this.context = context;
254
307
  if (opts.hash !== undefined)
255
308
  this.options.hash = opts.hash;
309
+ if (this.options.offline || this.closed)
310
+ return this.allFlags();
256
311
  this.events?.identify(context);
257
- this.replaceAll(await this.fetchFlags());
312
+ // Drop the old context's stream now, so none of its events land after the switch.
258
313
  this.stream?.restart();
314
+ await this.loadFlags();
259
315
  return this.allFlags();
260
316
  }
261
317
  getContext() {
@@ -265,6 +321,10 @@ export class DevPulseClient {
265
321
  track(key, data, metricValue) {
266
322
  this.events?.track(key, this.context, data, metricValue);
267
323
  }
324
+ /**
325
+ * Events: "ready", "failed" (fatal, e.g. an invalid client-side ID), "change" / "change:<key>",
326
+ * "stale" (stream lost; the last flags keep serving) and "recovered".
327
+ */
268
328
  on(event, fn) {
269
329
  let set = this.listeners.get(event);
270
330
  if (!set)
@@ -292,6 +352,8 @@ export class DevPulseClient {
292
352
  if (this.closed)
293
353
  return;
294
354
  this.closed = true;
355
+ this.detachVisibility?.();
356
+ this.detachVisibility = null;
295
357
  this.stream?.stop();
296
358
  await this.events?.close();
297
359
  this.listeners.clear();
@@ -55,5 +55,14 @@ export declare class EventProcessor {
55
55
  private push;
56
56
  private drain;
57
57
  flush(): Promise<void>;
58
+ private send;
58
59
  close(): Promise<void>;
59
60
  }
61
+ /** DevPulse accepts at most this many events per POST (the rest are silently dropped). */
62
+ export declare const MAX_EVENTS_PER_REQUEST = 5000;
63
+ /** Browser event bodies are capped at 512 KB by DevPulse; stay under it with room for brackets. */
64
+ export declare const MAX_REQUEST_BYTES = 500000;
65
+ /** keepalive / sendBeacon bodies must fit in the browser's 64 KiB in-flight budget. */
66
+ export declare const KEEPALIVE_MAX_BYTES = 60000;
67
+ /** Split events into JSON array bodies of at most MAX_EVENTS_PER_REQUEST events and MAX_REQUEST_BYTES bytes. */
68
+ export declare function chunkEvents(events: Record<string, unknown>[], logger?: Logger, maxBytes?: number, maxEvents?: number): string[];
@@ -166,10 +166,18 @@ export class EventProcessor {
166
166
  const events = this.drain();
167
167
  if (!events.length)
168
168
  return;
169
- const body = JSON.stringify(events);
170
- const nav = globalThis.navigator;
171
- if (this.opts.beacon && nav?.sendBeacon && document?.visibilityState === "hidden") {
172
- if (nav.sendBeacon(this.opts.url, body))
169
+ for (const body of chunkEvents(events, this.logger)) {
170
+ if (this.disabled)
171
+ return;
172
+ await this.send(body);
173
+ }
174
+ }
175
+ async send(body) {
176
+ const bytes = byteLength(body);
177
+ const g = globalThis;
178
+ // Beacons and keepalive requests share a 64 KiB in-flight budget in browsers; bigger bodies fail outright.
179
+ if (this.opts.beacon && g.navigator?.sendBeacon && g.document?.visibilityState === "hidden" && bytes < KEEPALIVE_MAX_BYTES) {
180
+ if (g.navigator.sendBeacon(this.opts.url, body))
173
181
  return;
174
182
  }
175
183
  for (let attempt = 0; attempt < 2; attempt++) {
@@ -178,7 +186,7 @@ export class EventProcessor {
178
186
  method: "POST",
179
187
  headers: { "content-type": "application/json", ...this.opts.headers },
180
188
  body,
181
- keepalive: body.length < 60_000,
189
+ keepalive: bytes < KEEPALIVE_MAX_BYTES,
182
190
  });
183
191
  if (res.ok)
184
192
  return;
@@ -193,7 +201,8 @@ export class EventProcessor {
193
201
  if (attempt === 1)
194
202
  this.logger.warn("Failed to send events", err);
195
203
  }
196
- await new Promise((r) => setTimeout(r, 1000));
204
+ if (attempt === 0)
205
+ await new Promise((r) => unrefTimer(setTimeout(r, 1000)));
197
206
  }
198
207
  }
199
208
  async close() {
@@ -202,3 +211,39 @@ export class EventProcessor {
202
211
  await this.flush();
203
212
  }
204
213
  }
214
+ /** DevPulse accepts at most this many events per POST (the rest are silently dropped). */
215
+ export const MAX_EVENTS_PER_REQUEST = 5000;
216
+ /** Browser event bodies are capped at 512 KB by DevPulse; stay under it with room for brackets. */
217
+ export const MAX_REQUEST_BYTES = 500_000;
218
+ /** keepalive / sendBeacon bodies must fit in the browser's 64 KiB in-flight budget. */
219
+ export const KEEPALIVE_MAX_BYTES = 60_000;
220
+ const encoder = new TextEncoder();
221
+ const byteLength = (s) => encoder.encode(s).length;
222
+ function unrefTimer(t) {
223
+ t.unref?.();
224
+ return t;
225
+ }
226
+ /** Split events into JSON array bodies of at most MAX_EVENTS_PER_REQUEST events and MAX_REQUEST_BYTES bytes. */
227
+ export function chunkEvents(events, logger = noopLogger, maxBytes = MAX_REQUEST_BYTES, maxEvents = MAX_EVENTS_PER_REQUEST) {
228
+ const bodies = [];
229
+ let parts = [];
230
+ let size = 2; // "[" + "]"
231
+ for (const e of events) {
232
+ const json = JSON.stringify(e);
233
+ const n = byteLength(json);
234
+ if (n + 2 > maxBytes) {
235
+ logger.warn(`Dropping a ${String(e.kind)} event of ${n} bytes (over the ${maxBytes}-byte request limit)`);
236
+ continue;
237
+ }
238
+ if (parts.length && (parts.length >= maxEvents || size + n + 1 > maxBytes)) {
239
+ bodies.push(`[${parts.join(",")}]`);
240
+ parts = [];
241
+ size = 2;
242
+ }
243
+ size += n + (parts.length ? 1 : 0);
244
+ parts.push(json);
245
+ }
246
+ if (parts.length)
247
+ bodies.push(`[${parts.join(",")}]`);
248
+ return bodies;
249
+ }
@@ -1,5 +1,5 @@
1
- export { SseParser, SseClient, SseError, backoffDelay, type SseEvent, type SseClientOptions } from "./sse.js";
2
- export { EventProcessor, noopLogger, type EventProcessorOptions, type Logger } from "./events.js";
1
+ export { SseParser, SseClient, SseError, backoffDelay, DEFAULT_MAX_EVENT_SIZE, MIN_SERVER_RETRY_MS, type SseEvent, type SseClientOptions } from "./sse.js";
2
+ export { EventProcessor, noopLogger, chunkEvents, MAX_EVENTS_PER_REQUEST, MAX_REQUEST_BYTES, KEEPALIVE_MAX_BYTES, type EventProcessorOptions, type Logger } from "./events.js";
3
3
  /** Encode a context for URLs (base64url JSON), matching the DevPulse SDK endpoints. */
4
4
  export declare function encodeContext(context: unknown): string;
5
- export declare const SDK_VERSION = "0.1.0";
5
+ export { SDK_VERSION } from "./version.js";
@@ -1,5 +1,5 @@
1
- export { SseParser, SseClient, SseError, backoffDelay } from "./sse.js";
2
- export { EventProcessor, noopLogger } from "./events.js";
1
+ export { SseParser, SseClient, SseError, backoffDelay, DEFAULT_MAX_EVENT_SIZE, MIN_SERVER_RETRY_MS } from "./sse.js";
2
+ export { EventProcessor, noopLogger, chunkEvents, MAX_EVENTS_PER_REQUEST, MAX_REQUEST_BYTES, KEEPALIVE_MAX_BYTES } from "./events.js";
3
3
  /** Encode a context for URLs (base64url JSON), matching the DevPulse SDK endpoints. */
4
4
  export function encodeContext(context) {
5
5
  const json = JSON.stringify(context);
@@ -9,4 +9,4 @@ export function encodeContext(context) {
9
9
  bin += String.fromCharCode(b);
10
10
  return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
11
11
  }
12
- export const SDK_VERSION = "0.1.0";
12
+ export { SDK_VERSION } from "./version.js";
@@ -9,16 +9,25 @@ export interface SseEvent {
9
9
  data: string;
10
10
  lastEventId: string;
11
11
  }
12
- /** Incremental parser: feed it chunks of text, get dispatched events back. */
12
+ /** Default cap on one event (its pending line plus its `data` lines), in UTF-16 code units. */
13
+ export declare const DEFAULT_MAX_EVENT_SIZE: number;
14
+ /**
15
+ * Incremental parser: feed it chunks of text, get dispatched events back. Use one parser per
16
+ * connection: a dropped connection can leave a half-received event behind.
17
+ */
13
18
  export declare class SseParser {
19
+ private readonly maxEventSize;
14
20
  private buffer;
15
21
  private data;
22
+ private dataSize;
16
23
  private eventType;
17
24
  private sawFirstChunk;
18
25
  /** The previous chunk ended in CR, so an LF starting this chunk completes a CRLF. */
19
26
  private pendingCR;
20
27
  lastEventId: string;
21
28
  retry: number | null;
29
+ /** @param maxEventSize feed() throws an SseError once an event grows past this (bounded memory). */
30
+ constructor(maxEventSize?: number);
22
31
  feed(chunk: string): SseEvent[];
23
32
  private line;
24
33
  }
@@ -35,7 +44,16 @@ export interface SseClientOptions {
35
44
  maxRetryMs?: number;
36
45
  /** Reconnect if nothing (not even a heartbeat) arrives for this long. */
37
46
  readTimeoutMs?: number;
47
+ /** Largest single event accepted (default 1 MB); a bigger one drops the connection. */
48
+ maxEventSize?: number;
49
+ /**
50
+ * Send only CORS-safelisted request headers: no `cache-control` or `last-event-id`, which
51
+ * would trigger a preflight that DevPulse doesn't allow. Default: true in browsers and workers.
52
+ */
53
+ browser?: boolean;
38
54
  }
55
+ /** Lower bound for a server-sent `retry:` hint, so `retry: 0` can't cause a reconnect storm. */
56
+ export declare const MIN_SERVER_RETRY_MS = 1000;
39
57
  export declare class SseError extends Error {
40
58
  readonly status?: number | undefined;
41
59
  constructor(message: string, status?: number | undefined);
@@ -49,14 +67,17 @@ export declare class SseClient {
49
67
  private controller;
50
68
  private stopped;
51
69
  private attempt;
52
- private parser;
70
+ /** Carried across connections; the parser itself is fresh per connection. */
71
+ private lastEventId;
72
+ private retry;
53
73
  private timer;
54
74
  private watchdog;
55
75
  private reader;
56
76
  constructor(opts: SseClientOptions);
57
77
  start(): void;
58
78
  stop(): void;
59
- /** Drop the current connection and reconnect immediately (e.g. after identify). */
79
+ /** Drop the current connection (or pending reconnect) and reconnect immediately (e.g. after identify). */
60
80
  restart(): void;
81
+ private clearTimer;
61
82
  private connect;
62
83
  }