@eventra_dev/eventra-sdk 1.1.3 → 1.1.5

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