@eventra_dev/eventra-sdk 1.1.3 → 1.1.4

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/dist/index.cjs CHANGED
@@ -25,23 +25,127 @@ __export(index_exports, {
25
25
  module.exports = __toCommonJS(index_exports);
26
26
 
27
27
  // src/client.ts
28
+ var SDK_NAME = "@eventra_dev/eventra-sdk";
29
+ var DEFAULT_ENDPOINT = "https://api.eventra.dev/api/v1/ingest/batch";
30
+ var SDK_VERSION = "1.1.4";
31
+ var LEADER_KEY = "__eventra_leader__";
32
+ var LEADER_TTL_MS = 4e3;
33
+ var QUEUE_KEY = "__eventra_q__";
34
+ var CHANNEL_NAME = "eventra-sdk";
35
+ var MAX_EVENT_NAME = 64;
36
+ var MAX_USER_ID = 120;
37
+ var MAX_PROPERTIES_JSON_BYTES = 32e3;
38
+ var MAX_PROPERTIES_DEPTH = 8;
39
+ var DEFAULT_MAX_PAYLOAD_BYTES = 6e4;
40
+ var DEFAULT_MAX_RETRY_DELAY_MS = 3e4;
41
+ var FETCH_TIMEOUT_MS = 5e3;
42
+ var CIRCUIT_FAILURE_THRESHOLD = 5;
43
+ var CIRCUIT_COOLDOWN_MS = 5e3;
44
+ var activeInstances = 0;
28
45
  function detectRuntime() {
29
46
  const g = globalThis;
30
- if (typeof window !== "undefined") return "browser";
47
+ if (typeof window !== "undefined" && !g.EdgeRuntime) return "browser";
31
48
  if (g.EdgeRuntime) return "edge";
32
49
  if (g.process?.env?.AWS_LAMBDA_FUNCTION_NAME) return "serverless";
33
50
  if (g.process?.versions?.node) return "node";
34
51
  return "unknown";
35
52
  }
36
- function uuid() {
37
- if (typeof crypto !== "undefined" && crypto.randomUUID) {
53
+ function uuidV4() {
54
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
38
55
  return crypto.randomUUID();
39
56
  }
40
- return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
57
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
58
+ const bytes = new Uint8Array(16);
59
+ crypto.getRandomValues(bytes);
60
+ bytes[6] = bytes[6] & 15 | 64;
61
+ bytes[8] = bytes[8] & 63 | 128;
62
+ const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
63
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
64
+ }
65
+ throw new Error("Eventra: crypto API unavailable \u2014 cannot generate idempotencyKey");
41
66
  }
42
67
  function sleep(ms) {
43
68
  return new Promise((r) => setTimeout(r, ms));
44
69
  }
70
+ function truncate(value, max) {
71
+ if (value === void 0) return void 0;
72
+ return value.length > max ? value.slice(0, max) : value;
73
+ }
74
+ function validateEventName(name) {
75
+ const trimmed = name.trim();
76
+ if (!trimmed) {
77
+ throw new Error("Eventra: event name is required");
78
+ }
79
+ if (trimmed.length > MAX_EVENT_NAME) {
80
+ throw new Error(`Eventra: event name exceeds max length (${MAX_EVENT_NAME})`);
81
+ }
82
+ return trimmed;
83
+ }
84
+ function backoffMs(attempt, base) {
85
+ const exp = Math.min(DEFAULT_MAX_RETRY_DELAY_MS, base * 2 ** attempt);
86
+ const jitter = exp * (0.5 + Math.random() * 0.5);
87
+ return jitter;
88
+ }
89
+ function utf8ByteLength(value) {
90
+ if (typeof TextEncoder !== "undefined") {
91
+ return new TextEncoder().encode(value).length;
92
+ }
93
+ const g = globalThis;
94
+ if (g.Buffer) {
95
+ return g.Buffer.byteLength(value, "utf8");
96
+ }
97
+ return value.length;
98
+ }
99
+ function mergeQueues(...queues) {
100
+ const seen = /* @__PURE__ */ new Set();
101
+ const out = [];
102
+ for (const q of queues) {
103
+ for (const e of q) {
104
+ if (seen.has(e.idempotencyKey)) continue;
105
+ seen.add(e.idempotencyKey);
106
+ out.push(e);
107
+ }
108
+ }
109
+ return out;
110
+ }
111
+ function validateProperties(properties) {
112
+ const depth = (obj, level) => {
113
+ if (level > MAX_PROPERTIES_DEPTH) {
114
+ throw new Error(`Eventra: properties exceed max depth (${MAX_PROPERTIES_DEPTH})`);
115
+ }
116
+ if (obj === null || typeof obj !== "object") return;
117
+ if (Array.isArray(obj)) {
118
+ for (const item of obj) depth(item, level + 1);
119
+ return;
120
+ }
121
+ for (const value of Object.values(obj)) {
122
+ depth(value, level + 1);
123
+ }
124
+ };
125
+ depth(properties, 0);
126
+ const json = JSON.stringify(properties);
127
+ if (utf8ByteLength(json) > MAX_PROPERTIES_JSON_BYTES) {
128
+ throw new Error(
129
+ `Eventra: properties exceed max size (${MAX_PROPERTIES_JSON_BYTES} bytes)`
130
+ );
131
+ }
132
+ return properties;
133
+ }
134
+ function estimateEnvelopeBytes(events, sdk, sentAt) {
135
+ return utf8ByteLength(
136
+ JSON.stringify({
137
+ sentAt,
138
+ sdk,
139
+ events
140
+ })
141
+ );
142
+ }
143
+ var RetryableDeliveryError = class extends Error {
144
+ constructor(message = "retryable delivery error") {
145
+ super(message);
146
+ this.name = "RetryableDeliveryError";
147
+ }
148
+ };
45
149
  var Storage = class {
46
150
  constructor() {
47
151
  this.enabled = false;
@@ -69,62 +173,167 @@ var Storage = class {
69
173
  } catch {
70
174
  }
71
175
  }
176
+ remove(key) {
177
+ if (!this.enabled) return;
178
+ try {
179
+ localStorage.removeItem(key);
180
+ } catch {
181
+ }
182
+ }
72
183
  };
73
184
  var Leader = class {
74
- constructor(enabled) {
75
- this.isLeader = true;
76
- if (!enabled) return;
77
- if (typeof BroadcastChannel !== "undefined") {
78
- this.channel = new BroadcastChannel("eventra");
79
- this.channel.onmessage = (e) => {
80
- if (e.data === "leader") this.isLeader = false;
185
+ constructor(onLeadershipChange) {
186
+ this.onLeadershipChange = onLeadershipChange;
187
+ this.tabId = uuidV4();
188
+ this.isLeader = false;
189
+ if (typeof localStorage === "undefined") {
190
+ this.isLeader = true;
191
+ return;
192
+ }
193
+ this.tick();
194
+ this.election = setInterval(() => this.tick(), LEADER_TTL_MS / 2);
195
+ if (typeof window !== "undefined") {
196
+ this.onStorage = (e) => {
197
+ if (e.key === LEADER_KEY) this.tick();
81
198
  };
82
- this.channel.postMessage("leader");
199
+ window.addEventListener("storage", this.onStorage);
200
+ }
201
+ }
202
+ tick() {
203
+ const was = this.isLeader;
204
+ if (this.isLeader) {
205
+ if (!this.verifyLease()) {
206
+ this.isLeader = false;
207
+ } else {
208
+ this.renew();
209
+ }
210
+ } else {
211
+ this.isLeader = this.tryAcquire();
212
+ if (this.isLeader) {
213
+ if (!this.heartbeat) {
214
+ this.heartbeat = setInterval(() => this.renew(), LEADER_TTL_MS / 2);
215
+ }
216
+ }
217
+ }
218
+ if (!this.isLeader && this.heartbeat) {
219
+ clearInterval(this.heartbeat);
220
+ this.heartbeat = void 0;
221
+ }
222
+ if (was !== this.isLeader) {
223
+ this.onLeadershipChange?.(this.isLeader);
224
+ }
225
+ }
226
+ tryAcquire() {
227
+ try {
228
+ const now = Date.now();
229
+ const raw = localStorage.getItem(LEADER_KEY);
230
+ if (!raw) {
231
+ this.renew();
232
+ return true;
233
+ }
234
+ const parsed = JSON.parse(raw);
235
+ if (now - parsed.ts > LEADER_TTL_MS) {
236
+ this.renew();
237
+ return true;
238
+ }
239
+ if (parsed.tabId === this.tabId) {
240
+ this.renew();
241
+ return true;
242
+ }
243
+ return false;
244
+ } catch {
245
+ return true;
246
+ }
247
+ }
248
+ verifyLease() {
249
+ try {
250
+ const raw = localStorage.getItem(LEADER_KEY);
251
+ if (!raw) return false;
252
+ const parsed = JSON.parse(raw);
253
+ return parsed.tabId === this.tabId && Date.now() - parsed.ts <= LEADER_TTL_MS;
254
+ } catch {
255
+ return false;
256
+ }
257
+ }
258
+ renew() {
259
+ try {
260
+ localStorage.setItem(
261
+ LEADER_KEY,
262
+ JSON.stringify({ ts: Date.now(), tabId: this.tabId })
263
+ );
264
+ } catch {
83
265
  }
84
266
  }
85
267
  canSend() {
86
268
  return this.isLeader;
87
269
  }
88
270
  destroy() {
89
- this.channel?.close();
271
+ if (this.election) clearInterval(this.election);
272
+ if (this.heartbeat) clearInterval(this.heartbeat);
273
+ if (this.onStorage && typeof window !== "undefined") {
274
+ window.removeEventListener("storage", this.onStorage);
275
+ }
276
+ if (this.isLeader && typeof localStorage !== "undefined") {
277
+ try {
278
+ const raw = localStorage.getItem(LEADER_KEY);
279
+ if (raw) {
280
+ const parsed = JSON.parse(raw);
281
+ if (parsed.tabId === this.tabId) {
282
+ localStorage.removeItem(LEADER_KEY);
283
+ }
284
+ }
285
+ } catch {
286
+ }
287
+ }
90
288
  }
91
289
  };
92
290
  var Eventra = class {
93
291
  constructor(options) {
94
292
  this.queue = [];
293
+ this.sending = null;
95
294
  this.inFlight = false;
96
295
  this.destroyed = false;
296
+ this.shuttingDown = false;
97
297
  this.storage = new Storage();
98
298
  this.leader = null;
99
299
  this.failureCount = 0;
100
300
  this.circuitOpenUntil = 0;
301
+ this.circuitHalfOpen = false;
101
302
  this.exitHandlers = [];
102
- if (!options.apiKey) throw new Error("apiKey required");
103
- if (!options.endpoint) throw new Error("endpoint required");
303
+ if (!options.apiKey) throw new Error("Eventra: apiKey required");
304
+ activeInstances++;
305
+ if (activeInstances > 1 && typeof console !== "undefined") {
306
+ console.warn(
307
+ "Eventra: multiple SDK instances detected \u2014 duplicate timers and sends are possible"
308
+ );
309
+ }
104
310
  this.options = options;
105
311
  this.apiKey = options.apiKey;
106
- this.endpoint = options.endpoint;
312
+ this.endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
107
313
  this.runtime = detectRuntime();
108
314
  this.fetch = options.fetchImpl ?? globalThis.fetch;
109
315
  if (!this.fetch) {
110
- throw new Error("fetch not available \u2014 provide fetchImpl");
316
+ throw new Error("Eventra: fetch not available \u2014 provide fetchImpl");
111
317
  }
112
318
  this.maxBatch = options.maxBatchSize ?? 50;
113
319
  this.maxQueue = options.maxQueueSize ?? 1e4;
320
+ this.maxPayloadBytes = options.maxPayloadBytes ?? DEFAULT_MAX_PAYLOAD_BYTES;
114
321
  this.flushInterval = options.flushInterval ?? 2e3;
115
322
  this.retries = options.maxRetries ?? 3;
116
323
  this.retryDelay = options.retryBaseDelayMs ?? 300;
117
324
  this.sdkInfo = {
118
- name: "@eventra_dev/eventra-sdk",
119
- version: "1.0.0",
325
+ name: SDK_NAME,
326
+ version: SDK_VERSION,
120
327
  runtime: this.runtime
121
328
  };
122
329
  if (this.runtime === "browser") {
123
- const saved = this.storage.get("__eventra_q__");
124
- if (Array.isArray(saved)) this.queue = saved;
330
+ this.loadAndMergeQueue();
331
+ this.setupQueueSync();
125
332
  }
126
333
  if (this.runtime === "browser" && options.multiTabMode === "leader") {
127
- this.leader = new Leader(true);
334
+ this.leader = new Leader((isLeader) => {
335
+ if (isLeader) void this.flush();
336
+ });
128
337
  }
129
338
  if (!options.disableTimer) {
130
339
  this.startTimer();
@@ -136,23 +345,31 @@ var Eventra = class {
136
345
  this.setupProcessExit();
137
346
  }
138
347
  }
139
- // ================= PUBLIC =================
140
348
  track(name, options) {
141
- if (this.destroyed) return;
349
+ if (this.destroyed || this.shuttingDown) return;
350
+ let eventName;
351
+ let properties;
352
+ try {
353
+ eventName = validateEventName(name);
354
+ properties = validateProperties(options?.properties ?? {});
355
+ } catch (err) {
356
+ const message = err instanceof Error ? err.message : "invalid event";
357
+ throw new Error(message);
358
+ }
142
359
  if (this.queue.length >= this.maxQueue) {
143
360
  this.options.onEventsDropped?.(1);
144
361
  return;
145
362
  }
146
363
  const event = {
147
- idempotencyKey: uuid(),
148
- name,
149
- userId: options?.userId,
150
- properties: options?.properties ?? {},
364
+ idempotencyKey: uuidV4(),
365
+ name: eventName,
366
+ userId: truncate(options?.userId, MAX_USER_ID),
367
+ properties,
151
368
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
152
369
  };
153
370
  this.queue.push(event);
154
371
  if (this.runtime === "browser") {
155
- this.persist();
372
+ this.syncQueue();
156
373
  }
157
374
  if (this.queue.length >= this.maxBatch) {
158
375
  void this.flush();
@@ -163,54 +380,134 @@ var Eventra = class {
163
380
  }
164
381
  async flush() {
165
382
  if (this.inFlight || this.destroyed) return;
166
- if (!this.queue.length) return;
383
+ if (!this.queue.length && !this.sending?.length) return;
167
384
  if (this.leader && !this.leader.canSend()) return;
168
- if (Date.now() < this.circuitOpenUntil) return;
169
- this.inFlight = true;
170
- const batch = this.queue.splice(0, this.maxBatch);
171
- if (this.runtime === "browser") {
172
- this.persist();
385
+ const now = Date.now();
386
+ if (now < this.circuitOpenUntil) return;
387
+ if (this.circuitOpenUntil > 0 && now >= this.circuitOpenUntil) {
388
+ this.circuitHalfOpen = true;
389
+ this.circuitOpenUntil = 0;
173
390
  }
391
+ this.inFlight = true;
174
392
  try {
175
- await this.send(batch);
176
- this.failureCount = 0;
177
- } catch {
178
- this.queue = [...batch, ...this.queue];
179
- if (this.runtime === "browser") {
180
- this.persist();
393
+ while (this.queue.length > 0) {
394
+ const batch = this.buildBatch();
395
+ if (!batch.length) break;
396
+ this.sending = batch;
397
+ try {
398
+ await this.send(batch);
399
+ this.removeDelivered(batch);
400
+ this.failureCount = 0;
401
+ this.circuitHalfOpen = false;
402
+ } catch (err) {
403
+ if (err instanceof RetryableDeliveryError) {
404
+ this.failureCount++;
405
+ if (this.circuitHalfOpen || this.failureCount >= CIRCUIT_FAILURE_THRESHOLD) {
406
+ this.circuitOpenUntil = Date.now() + CIRCUIT_COOLDOWN_MS;
407
+ this.circuitHalfOpen = false;
408
+ }
409
+ break;
410
+ }
411
+ break;
412
+ } finally {
413
+ this.sending = null;
414
+ }
415
+ if (this.circuitOpenUntil > Date.now()) break;
181
416
  }
182
- this.failureCount++;
183
- if (this.failureCount >= 5) {
184
- this.circuitOpenUntil = Date.now() + 5e3;
417
+ if (this.runtime === "browser") {
418
+ this.syncQueue();
185
419
  }
186
420
  } finally {
187
421
  this.inFlight = false;
188
422
  }
189
423
  }
424
+ /** Flush pending events, then tear down timers and listeners */
425
+ async shutdown() {
426
+ if (this.shuttingDown) return;
427
+ this.shuttingDown = true;
428
+ try {
429
+ await this.flush();
430
+ } finally {
431
+ this.destroy();
432
+ }
433
+ }
190
434
  destroy() {
435
+ if (this.destroyed) return;
191
436
  this.destroyed = true;
437
+ activeInstances = Math.max(0, activeInstances - 1);
192
438
  if (this.timer) clearInterval(this.timer);
193
439
  this.leader?.destroy();
440
+ if (this.channel) {
441
+ this.channel.close();
442
+ this.channel = void 0;
443
+ }
444
+ if (this.onStorageQueue && typeof window !== "undefined") {
445
+ window.removeEventListener("storage", this.onStorageQueue);
446
+ }
194
447
  for (const off of this.exitHandlers) {
195
448
  off();
196
449
  }
197
450
  }
198
- // ================= SEND =================
451
+ buildBatch() {
452
+ const batch = [];
453
+ const sentAt = (/* @__PURE__ */ new Date()).toISOString();
454
+ let index = 0;
455
+ while (index < this.queue.length && batch.length < this.maxBatch) {
456
+ const event = this.queue[index];
457
+ const candidate = [...batch, event];
458
+ const bytes = estimateEnvelopeBytes(candidate, this.sdkInfo, sentAt);
459
+ if (bytes > this.maxPayloadBytes) {
460
+ if (batch.length === 0) {
461
+ const poisonKey = event.idempotencyKey;
462
+ this.dropPoisonEvent(event);
463
+ if (this.queue[index]?.idempotencyKey === poisonKey) {
464
+ index++;
465
+ }
466
+ continue;
467
+ }
468
+ break;
469
+ }
470
+ batch.push(event);
471
+ index++;
472
+ }
473
+ return batch;
474
+ }
475
+ dropPoisonEvent(event) {
476
+ this.removeDelivered([event]);
477
+ this.options.onEventsDropped?.(1);
478
+ this.options.onDeliveryFailed?.({ status: 413, events: [event] });
479
+ if (this.runtime === "browser") {
480
+ this.syncQueue();
481
+ }
482
+ }
483
+ removeDelivered(batch) {
484
+ const keys = new Set(batch.map((e) => e.idempotencyKey));
485
+ this.queue = this.queue.filter((e) => !keys.has(e.idempotencyKey));
486
+ }
199
487
  async send(events) {
200
- const payload = JSON.stringify({
201
- sentAt: (/* @__PURE__ */ new Date()).toISOString(),
202
- sdk: this.sdkInfo,
203
- events
204
- });
205
- if (this.runtime === "browser" && typeof navigator !== "undefined" && navigator.sendBeacon && payload.length < 6e4 && document.visibilityState === "hidden") {
206
- navigator.sendBeacon(this.endpoint, payload);
488
+ let payload;
489
+ try {
490
+ payload = JSON.stringify({
491
+ sentAt: (/* @__PURE__ */ new Date()).toISOString(),
492
+ sdk: this.sdkInfo,
493
+ events
494
+ });
495
+ } catch {
496
+ this.options.onDeliveryFailed?.({ status: 0, events });
497
+ this.removeDelivered(events);
498
+ return;
499
+ }
500
+ const payloadBytes = utf8ByteLength(payload);
501
+ if (payloadBytes > this.maxPayloadBytes) {
502
+ this.dropPoisonEvents(events);
207
503
  return;
208
504
  }
209
- let attempt = 0;
210
- while (attempt <= this.retries) {
505
+ const useKeepalive = this.runtime === "browser" && typeof document !== "undefined" && document.visibilityState === "hidden" && payloadBytes <= this.maxPayloadBytes;
506
+ const maxAttempts = Math.max(1, this.retries);
507
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
508
+ const controller = new AbortController();
509
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
211
510
  try {
212
- const controller = new AbortController();
213
- const timeout = setTimeout(() => controller.abort(), 5e3);
214
511
  const res = await this.fetch(this.endpoint, {
215
512
  method: "POST",
216
513
  headers: {
@@ -218,20 +515,86 @@ var Eventra = class {
218
515
  "x-api-key": this.apiKey
219
516
  },
220
517
  body: payload,
221
- signal: controller.signal
518
+ signal: controller.signal,
519
+ keepalive: useKeepalive
222
520
  });
223
- clearTimeout(timeout);
224
- if (res.status >= 400 && res.status < 500) return;
225
- if (!res.ok) throw new Error();
521
+ if (res.status === 429 || res.status >= 500) {
522
+ throw new RetryableDeliveryError();
523
+ }
524
+ if (res.status >= 400 && res.status < 500) {
525
+ this.options.onDeliveryFailed?.({ status: res.status, events });
526
+ this.removeDelivered(events);
527
+ return;
528
+ }
529
+ if (!res.ok) {
530
+ throw new RetryableDeliveryError();
531
+ }
226
532
  return;
227
- } catch {
228
- attempt++;
229
- if (attempt > this.retries) throw new Error();
230
- await sleep(this.retryDelay * 2 ** attempt);
533
+ } catch (err) {
534
+ const isLast = attempt >= maxAttempts - 1;
535
+ if (isLast) {
536
+ throw err instanceof RetryableDeliveryError ? err : new RetryableDeliveryError();
537
+ }
538
+ await sleep(backoffMs(attempt + 1, this.retryDelay));
539
+ } finally {
540
+ clearTimeout(timeout);
231
541
  }
232
542
  }
233
543
  }
234
- // ================= INTERNAL =================
544
+ dropPoisonEvents(events) {
545
+ for (const event of events) {
546
+ this.dropPoisonEvent(event);
547
+ }
548
+ }
549
+ loadAndMergeQueue() {
550
+ const remote = this.storage.get(QUEUE_KEY);
551
+ if (Array.isArray(remote)) {
552
+ this.queue = mergeQueues(this.queue, remote);
553
+ }
554
+ this.trimQueue();
555
+ }
556
+ syncQueue() {
557
+ const remote = this.storage.get(QUEUE_KEY);
558
+ const merged = mergeQueues(
559
+ Array.isArray(remote) ? remote : [],
560
+ this.queue
561
+ );
562
+ this.queue = this.trimQueue(merged);
563
+ this.storage.set(QUEUE_KEY, this.queue);
564
+ this.broadcastQueue();
565
+ }
566
+ trimQueue(queue = this.queue) {
567
+ if (queue.length <= this.maxQueue) return queue;
568
+ const dropped = queue.length - this.maxQueue;
569
+ this.options.onEventsDropped?.(dropped);
570
+ return queue.slice(-this.maxQueue);
571
+ }
572
+ broadcastQueue() {
573
+ try {
574
+ this.channel?.postMessage({
575
+ type: "queue-sync",
576
+ events: this.queue
577
+ });
578
+ } catch {
579
+ }
580
+ }
581
+ setupQueueSync() {
582
+ if (typeof BroadcastChannel !== "undefined") {
583
+ this.channel = new BroadcastChannel(CHANNEL_NAME);
584
+ this.channel.onmessage = (e) => {
585
+ if (e.data?.type !== "queue-sync" || !Array.isArray(e.data.events)) return;
586
+ this.queue = mergeQueues(this.queue, e.data.events);
587
+ this.queue = this.trimQueue();
588
+ };
589
+ }
590
+ if (typeof window !== "undefined") {
591
+ this.onStorageQueue = (e) => {
592
+ if (e.key !== QUEUE_KEY) return;
593
+ this.loadAndMergeQueue();
594
+ };
595
+ window.addEventListener("storage", this.onStorageQueue);
596
+ }
597
+ }
235
598
  startTimer() {
236
599
  this.timer = setInterval(() => {
237
600
  void this.flush();
@@ -243,37 +606,29 @@ var Eventra = class {
243
606
  void this.flush();
244
607
  }
245
608
  };
609
+ const pageHide = () => {
610
+ void this.flush();
611
+ };
246
612
  window.addEventListener("visibilitychange", handler);
613
+ window.addEventListener("pagehide", pageHide);
247
614
  this.exitHandlers.push(() => {
248
615
  window.removeEventListener("visibilitychange", handler);
616
+ window.removeEventListener("pagehide", pageHide);
249
617
  });
250
618
  }
251
619
  setupProcessExit() {
252
- const g = globalThis;
253
- const flushSafe = async () => {
254
- try {
255
- await this.flush();
256
- } catch {
257
- }
620
+ const proc = globalThis.process;
621
+ if (!proc?.on) return;
622
+ const onSignal = () => {
623
+ void this.shutdown();
258
624
  };
259
- const beforeExit = () => {
260
- void flushSafe();
261
- };
262
- const sigint = async () => {
263
- await flushSafe();
264
- g.exit();
265
- };
266
- g?.on?.("beforeExit", beforeExit);
267
- g?.on?.("SIGINT", sigint);
625
+ proc.once("SIGINT", onSignal);
626
+ proc.once("SIGTERM", onSignal);
268
627
  this.exitHandlers.push(() => {
269
- g?.off?.("beforeExit", beforeExit);
270
- g?.off?.("SIGINT", sigint);
628
+ proc.off?.("SIGINT", onSignal);
629
+ proc.off?.("SIGTERM", onSignal);
271
630
  });
272
631
  }
273
- persist() {
274
- if (this.runtime !== "browser") return;
275
- this.storage.set("__eventra_q__", this.queue);
276
- }
277
632
  };
278
633
  // Annotate the CommonJS export names for ESM import in node:
279
634
  0 && (module.exports = {