clintplay-react-native 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.
package/dist/index.mjs ADDED
@@ -0,0 +1,1461 @@
1
+ import { View, TextInput, Image, Dimensions, AppState, Platform, TurboModuleRegistry, NativeModules } from 'react-native';
2
+ import React5, { useCallback, useRef, useEffect } from 'react';
3
+ import { jsx, Fragment } from 'react/jsx-runtime';
4
+
5
+ // src/index.ts
6
+
7
+ // src/buffer.ts
8
+ var EventBuffer = class {
9
+ constructor(maxSize) {
10
+ this.maxSize = maxSize;
11
+ this.events = [];
12
+ }
13
+ push(event) {
14
+ this.events.push(event);
15
+ const excess = this.events.length - this.maxSize;
16
+ if (excess > 0) {
17
+ this.events.splice(0, excess);
18
+ }
19
+ }
20
+ size() {
21
+ return this.events.length;
22
+ }
23
+ drain() {
24
+ const out = this.events;
25
+ this.events = [];
26
+ return out;
27
+ }
28
+ peek() {
29
+ return this.events;
30
+ }
31
+ };
32
+
33
+ // src/session.ts
34
+ function uuid() {
35
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
36
+ return crypto.randomUUID();
37
+ }
38
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
39
+ const r = Math.random() * 16 | 0;
40
+ const v = c === "x" ? r : r & 3 | 8;
41
+ return v.toString(16);
42
+ });
43
+ }
44
+ var IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
45
+ var SessionManager = class {
46
+ constructor(now = Date.now) {
47
+ this.now = now;
48
+ this.state = this.create();
49
+ }
50
+ create() {
51
+ const ts = this.now();
52
+ return { sessionId: uuid(), startedAt: ts, lastActivity: ts, userId: null };
53
+ }
54
+ current() {
55
+ return this.state;
56
+ }
57
+ touch() {
58
+ this.state.lastActivity = this.now();
59
+ }
60
+ setUser(userId) {
61
+ const prev = this.state.userId;
62
+ if (prev !== null && (userId === null || prev !== userId)) {
63
+ const next = this.create();
64
+ next.userId = userId;
65
+ this.state = next;
66
+ return true;
67
+ }
68
+ this.state.userId = userId;
69
+ return false;
70
+ }
71
+ isStale() {
72
+ return this.now() - this.state.lastActivity > IDLE_TIMEOUT_MS;
73
+ }
74
+ // Rotates the session id if the gap since last activity exceeds the
75
+ // idle window. Caller should emit a fresh meta event under the new
76
+ // id when this returns true.
77
+ rotateIfStale() {
78
+ if (this.isStale()) {
79
+ const prevUser = this.state.userId;
80
+ this.state = this.create();
81
+ this.state.userId = prevUser;
82
+ return true;
83
+ }
84
+ return false;
85
+ }
86
+ reset() {
87
+ this.state = this.create();
88
+ }
89
+ };
90
+
91
+ // src/transport.ts
92
+ var DEFAULT_BACKOFF = (attempt) => 500 * (attempt + 1);
93
+ var MAX_RETRIES_CAP = 10;
94
+ var MAX_BACKOFF_MS = 3e4;
95
+ var Transport = class {
96
+ constructor(cfg) {
97
+ this.cfg = cfg;
98
+ }
99
+ // Returns true on a 2xx. Drops the batch on 4xx (caller-side error,
100
+ // no point retrying), retries on 5xx up to maxRetries, and returns
101
+ // false on network errors / timeout after exhausting retries.
102
+ async sendDetailed(batch) {
103
+ const maxRetries = Math.min(this.cfg.maxRetries ?? 2, MAX_RETRIES_CAP);
104
+ const timeoutMs = this.cfg.timeoutMs ?? 8e3;
105
+ const rawBackoff = this.cfg.backoffMs ?? DEFAULT_BACKOFF;
106
+ const backoff = (attempt) => Math.min(rawBackoff(attempt), MAX_BACKOFF_MS);
107
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
108
+ try {
109
+ const controller = new AbortController();
110
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
111
+ let res;
112
+ try {
113
+ res = await fetch(this.cfg.apiUrl, {
114
+ method: "POST",
115
+ headers: this.headers(),
116
+ body: JSON.stringify(batch),
117
+ signal: controller.signal
118
+ });
119
+ } finally {
120
+ clearTimeout(timer);
121
+ }
122
+ if (res.ok) return { ok: true, status: res.status };
123
+ if (res.status >= 400 && res.status < 500) {
124
+ return { ok: false, status: res.status, retryable: false };
125
+ }
126
+ } catch {
127
+ }
128
+ if (attempt < maxRetries) {
129
+ await new Promise((r) => setTimeout(() => r(), backoff(attempt)));
130
+ }
131
+ }
132
+ return { ok: false, retryable: true };
133
+ }
134
+ async send(batch) {
135
+ const res = await this.sendDetailed(batch);
136
+ return res.ok;
137
+ }
138
+ headers() {
139
+ const h = { "content-type": "application/json" };
140
+ if (this.cfg.apiKey) {
141
+ h.authorization = `Bearer ${this.cfg.apiKey}`;
142
+ h["x-api-key"] = this.cfg.apiKey;
143
+ }
144
+ if (this.cfg.projectId) {
145
+ h["x-project-id"] = this.cfg.projectId;
146
+ }
147
+ return h;
148
+ }
149
+ };
150
+ function mapAppState(state2) {
151
+ if (state2 === "active") return "active";
152
+ if (state2 === "background") return "background";
153
+ return "inactive";
154
+ }
155
+ function orientationFor(width, height) {
156
+ return width > height ? "landscape" : "portrait";
157
+ }
158
+ function createRecorder(deps) {
159
+ const cleanups = [];
160
+ let started = false;
161
+ let wireframeTimer = null;
162
+ function takeWireframeSnapshot() {
163
+ if (!deps.captureViewHierarchy) return;
164
+ try {
165
+ const raw = deps.captureViewHierarchy();
166
+ if (!raw || typeof raw !== "string") return;
167
+ const payload = JSON.parse(raw);
168
+ if (payload && Array.isArray(payload.nodes)) {
169
+ deps.onEvent({
170
+ type: "wireframe",
171
+ ts: deps.now(),
172
+ version: typeof payload.version === "number" ? payload.version : 1,
173
+ nodes: payload.nodes
174
+ });
175
+ }
176
+ } catch {
177
+ }
178
+ }
179
+ function startWireframeTimer() {
180
+ if (!deps.captureWireframe || !deps.captureViewHierarchy) return;
181
+ if (wireframeTimer) return;
182
+ takeWireframeSnapshot();
183
+ const rawInterval = deps.wireframeIntervalMs;
184
+ const intervalMs = typeof rawInterval === "number" && Number.isFinite(rawInterval) && rawInterval > 0 ? Math.max(500, rawInterval) : 1e3;
185
+ wireframeTimer = setInterval(() => {
186
+ takeWireframeSnapshot();
187
+ }, intervalMs);
188
+ }
189
+ function stopWireframeTimer() {
190
+ if (wireframeTimer) {
191
+ clearInterval(wireframeTimer);
192
+ wireframeTimer = null;
193
+ }
194
+ }
195
+ return {
196
+ takeWireframeSnapshot,
197
+ start() {
198
+ if (started) return;
199
+ started = true;
200
+ const { width, height, scale } = Dimensions.get("window");
201
+ deps.onEvent({
202
+ type: "screen",
203
+ ts: deps.now(),
204
+ width,
205
+ height,
206
+ scale,
207
+ orientation: orientationFor(width, height)
208
+ });
209
+ const appStateSub = AppState.addEventListener("change", (state2) => {
210
+ const mapped = mapAppState(state2);
211
+ deps.onEvent({
212
+ type: "lifecycle",
213
+ ts: deps.now(),
214
+ state: mapped
215
+ });
216
+ if (deps.captureWireframe && deps.captureViewHierarchy) {
217
+ if (mapped === "active") {
218
+ startWireframeTimer();
219
+ } else {
220
+ stopWireframeTimer();
221
+ }
222
+ }
223
+ });
224
+ cleanups.push(() => appStateSub.remove());
225
+ const dimSub = Dimensions.addEventListener("change", ({ window }) => {
226
+ deps.onEvent({
227
+ type: "screen",
228
+ ts: deps.now(),
229
+ width: window.width,
230
+ height: window.height,
231
+ scale: window.scale,
232
+ orientation: orientationFor(window.width, window.height)
233
+ });
234
+ });
235
+ cleanups.push(() => dimSub.remove());
236
+ if (deps.installErrorHandler) {
237
+ const g = globalThis;
238
+ const eu = g.ErrorUtils;
239
+ if (eu) {
240
+ let handlerActive = true;
241
+ const original = eu.getGlobalHandler();
242
+ const ourHandler = (error, isFatal) => {
243
+ if (handlerActive) {
244
+ try {
245
+ deps.onEvent({
246
+ type: "error",
247
+ ts: deps.now(),
248
+ message: error?.message ?? String(error),
249
+ stack: error?.stack,
250
+ name: error?.name,
251
+ fatal: !!isFatal
252
+ });
253
+ } catch {
254
+ }
255
+ }
256
+ if (original) original(error, isFatal);
257
+ };
258
+ eu.setGlobalHandler(ourHandler);
259
+ cleanups.push(() => {
260
+ handlerActive = false;
261
+ if (eu.getGlobalHandler() === ourHandler) {
262
+ eu.setGlobalHandler(original);
263
+ }
264
+ });
265
+ }
266
+ }
267
+ if (deps.captureWireframe && deps.captureViewHierarchy) {
268
+ if (AppState.currentState === "active" || !AppState.currentState) {
269
+ startWireframeTimer();
270
+ }
271
+ cleanups.push(() => stopWireframeTimer());
272
+ }
273
+ },
274
+ stop() {
275
+ if (!started) return;
276
+ started = false;
277
+ stopWireframeTimer();
278
+ for (const fn of cleanups.splice(0)) {
279
+ try {
280
+ fn();
281
+ } catch {
282
+ }
283
+ }
284
+ }
285
+ };
286
+ }
287
+
288
+ // src/modules/network-interceptor.ts
289
+ var MAX_URL_LENGTH = 2048;
290
+ var stripQuery = (raw) => {
291
+ if (typeof raw !== "string") return "";
292
+ if (raw.startsWith("data:")) {
293
+ const commaIdx = raw.indexOf(",");
294
+ if (commaIdx !== -1) {
295
+ return `${raw.slice(0, commaIdx + 1)}[data-url]`;
296
+ }
297
+ return "data:[data-url]";
298
+ }
299
+ const strippedCredentials = raw.replace(
300
+ /^((?:[a-zA-Z][a-zA-Z\d+.-]*:)?\/\/)[^/?#]+@/,
301
+ "$1"
302
+ );
303
+ const cutIdx = strippedCredentials.search(/[?#]/);
304
+ const cleaned = cutIdx === -1 ? strippedCredentials : strippedCredentials.substring(0, cutIdx);
305
+ return cleaned.length > MAX_URL_LENGTH ? cleaned.slice(0, MAX_URL_LENGTH) + "\u2026" : cleaned;
306
+ };
307
+ var xhrMetadataMap = /* @__PURE__ */ new WeakMap();
308
+ var NetworkInterceptor = class {
309
+ constructor(deps) {
310
+ this.deps = deps;
311
+ this.originalFetch = null;
312
+ this.wrappedFetch = null;
313
+ this.originalXHROpen = null;
314
+ this.wrappedXHROpen = null;
315
+ this.originalXHRSend = null;
316
+ this.wrappedXHRSend = null;
317
+ this.started = false;
318
+ this.isCallingFetchSync = false;
319
+ this.ingestOrigin = null;
320
+ this.ingestPath = null;
321
+ this.sanitize = deps.sanitizeUrl ?? stripQuery;
322
+ if (deps.apiUrl) {
323
+ try {
324
+ const parsed = typeof URL !== "undefined" ? new URL(deps.apiUrl) : null;
325
+ if (parsed) {
326
+ this.ingestOrigin = parsed.origin;
327
+ const cleanPath = parsed.pathname.replace(/\/+$/, "");
328
+ this.ingestPath = cleanPath || "/";
329
+ }
330
+ } catch {
331
+ this.ingestOrigin = null;
332
+ this.ingestPath = null;
333
+ }
334
+ }
335
+ }
336
+ isSelfIngestUrl(url) {
337
+ if (!url) return false;
338
+ try {
339
+ const parsed = typeof URL !== "undefined" ? new URL(url, "http://localhost") : null;
340
+ if (parsed) {
341
+ if (this.ingestOrigin && parsed.origin === this.ingestOrigin) {
342
+ if (this.ingestPath === "/" || !this.ingestPath) {
343
+ return true;
344
+ }
345
+ const reqPath = parsed.pathname.replace(/\/+$/, "") || "/";
346
+ if (reqPath === this.ingestPath || reqPath.startsWith(this.ingestPath + "/")) {
347
+ return true;
348
+ }
349
+ return false;
350
+ }
351
+ if (/(^|\.)clintplay\./i.test(parsed.hostname || parsed.host || "")) {
352
+ return true;
353
+ }
354
+ return false;
355
+ }
356
+ } catch {
357
+ }
358
+ return /(^|\.)clintplay\./i.test(url);
359
+ }
360
+ start() {
361
+ if (this.started) return;
362
+ this.started = true;
363
+ if (typeof globalThis.fetch === "function") {
364
+ if (globalThis.fetch !== this.wrappedFetch) {
365
+ this.originalFetch = globalThis.fetch;
366
+ }
367
+ const self = this;
368
+ const wrapped = async (input, init2) => {
369
+ if (!self.started || !self.originalFetch) {
370
+ return (self.originalFetch ?? globalThis.fetch)(input, init2);
371
+ }
372
+ const startedAt = self.deps.now();
373
+ const url = self.extractUrl(input);
374
+ const method = self.extractMethod(input, init2);
375
+ const skipTelemetry = !self.started || self.isSelfIngestUrl(url);
376
+ self.isCallingFetchSync = true;
377
+ let promise;
378
+ try {
379
+ promise = self.originalFetch(input, init2);
380
+ } finally {
381
+ self.isCallingFetchSync = false;
382
+ }
383
+ try {
384
+ const res = await promise;
385
+ if (!skipTelemetry && self.started) {
386
+ try {
387
+ self.deps.onEvent({
388
+ type: "network",
389
+ ts: startedAt,
390
+ method,
391
+ url: self.sanitize(url),
392
+ status: res.status,
393
+ durationMs: Math.max(0, self.deps.now() - startedAt)
394
+ });
395
+ } catch {
396
+ }
397
+ }
398
+ return res;
399
+ } catch (err) {
400
+ if (!skipTelemetry && self.started) {
401
+ try {
402
+ self.deps.onEvent({
403
+ type: "network",
404
+ ts: startedAt,
405
+ method,
406
+ url: self.sanitize(url),
407
+ durationMs: Math.max(0, self.deps.now() - startedAt),
408
+ error: err instanceof Error ? err.message : String(err)
409
+ });
410
+ } catch {
411
+ }
412
+ }
413
+ throw err;
414
+ }
415
+ };
416
+ this.wrappedFetch = wrapped;
417
+ globalThis.fetch = this.wrappedFetch;
418
+ }
419
+ const g = globalThis;
420
+ if (g.XMLHttpRequest && g.XMLHttpRequest.prototype) {
421
+ const xhrProto = g.XMLHttpRequest.prototype;
422
+ if (xhrProto.open !== this.wrappedXHROpen) {
423
+ this.originalXHROpen = xhrProto.open;
424
+ }
425
+ if (xhrProto.send !== this.wrappedXHRSend) {
426
+ this.originalXHRSend = xhrProto.send;
427
+ }
428
+ const self = this;
429
+ const wrappedOpen = function(method, url, ...rest) {
430
+ if (self.started) {
431
+ try {
432
+ const urlStr = self.extractUrl(url);
433
+ const normalizedMethod = typeof method === "string" ? method.toUpperCase() : "GET";
434
+ const isFromFetch = self.isCallingFetchSync;
435
+ const meta = {
436
+ method: normalizedMethod,
437
+ url: self.sanitize(urlStr),
438
+ rawUrl: urlStr,
439
+ startTime: 0,
440
+ isFromFetch
441
+ };
442
+ if (this && typeof this === "object") {
443
+ xhrMetadataMap.set(this, meta);
444
+ }
445
+ } catch {
446
+ }
447
+ }
448
+ if (self.originalXHROpen) {
449
+ return self.originalXHROpen.apply(this, [method, url, ...rest]);
450
+ }
451
+ };
452
+ const wrappedSend = function(body) {
453
+ if (self.started && this && typeof this === "object") {
454
+ try {
455
+ const meta = xhrMetadataMap.get(this);
456
+ if (meta && !meta.isFromFetch && !self.isSelfIngestUrl(meta.rawUrl || meta.url)) {
457
+ meta.startTime = self.deps.now();
458
+ let emitted = false;
459
+ const cleanupListeners = () => {
460
+ if (typeof this.removeEventListener === "function") {
461
+ this.removeEventListener("load", handleLoad);
462
+ this.removeEventListener("error", handleError);
463
+ this.removeEventListener("abort", handleAbort);
464
+ this.removeEventListener("timeout", handleTimeout);
465
+ }
466
+ };
467
+ const emitOnce = (event) => {
468
+ cleanupListeners();
469
+ if (this && typeof this === "object") {
470
+ xhrMetadataMap.delete(this);
471
+ }
472
+ if (emitted || !self.started) return;
473
+ emitted = true;
474
+ try {
475
+ self.deps.onEvent(event);
476
+ } catch {
477
+ }
478
+ };
479
+ const handleLoad = () => {
480
+ emitOnce({
481
+ type: "network",
482
+ ts: meta.startTime,
483
+ method: meta.method,
484
+ url: meta.url,
485
+ status: typeof this.status === "number" ? this.status : 200,
486
+ durationMs: Math.max(0, self.deps.now() - meta.startTime)
487
+ });
488
+ };
489
+ const handleError = (e) => {
490
+ emitOnce({
491
+ type: "network",
492
+ ts: meta.startTime,
493
+ method: meta.method,
494
+ url: meta.url,
495
+ durationMs: Math.max(0, self.deps.now() - meta.startTime),
496
+ error: e && e.message || "Network request failed"
497
+ });
498
+ };
499
+ const handleAbort = () => {
500
+ emitOnce({
501
+ type: "network",
502
+ ts: meta.startTime,
503
+ method: meta.method,
504
+ url: meta.url,
505
+ durationMs: Math.max(0, self.deps.now() - meta.startTime),
506
+ error: "Request aborted"
507
+ });
508
+ };
509
+ const handleTimeout = () => {
510
+ emitOnce({
511
+ type: "network",
512
+ ts: meta.startTime,
513
+ method: meta.method,
514
+ url: meta.url,
515
+ durationMs: Math.max(0, self.deps.now() - meta.startTime),
516
+ error: "Request timed out"
517
+ });
518
+ };
519
+ if (typeof this.addEventListener === "function") {
520
+ this.addEventListener("load", handleLoad);
521
+ this.addEventListener("error", handleError);
522
+ this.addEventListener("abort", handleAbort);
523
+ this.addEventListener("timeout", handleTimeout);
524
+ }
525
+ }
526
+ } catch {
527
+ }
528
+ }
529
+ if (self.originalXHRSend) {
530
+ return self.originalXHRSend.apply(this, [body]);
531
+ }
532
+ };
533
+ this.wrappedXHROpen = wrappedOpen;
534
+ this.wrappedXHRSend = wrappedSend;
535
+ xhrProto.open = wrappedOpen;
536
+ xhrProto.send = wrappedSend;
537
+ }
538
+ }
539
+ stop() {
540
+ if (!this.started) return;
541
+ this.started = false;
542
+ if (this.originalFetch && this.wrappedFetch) {
543
+ if (globalThis.fetch === this.wrappedFetch) {
544
+ globalThis.fetch = this.originalFetch;
545
+ }
546
+ }
547
+ const g = globalThis;
548
+ if (g.XMLHttpRequest && g.XMLHttpRequest.prototype) {
549
+ const xhrProto = g.XMLHttpRequest.prototype;
550
+ if (this.originalXHROpen && xhrProto.open === this.wrappedXHROpen) {
551
+ xhrProto.open = this.originalXHROpen;
552
+ }
553
+ if (this.originalXHRSend && xhrProto.send === this.wrappedXHRSend) {
554
+ xhrProto.send = this.originalXHRSend;
555
+ }
556
+ }
557
+ }
558
+ extractUrl(input) {
559
+ if (typeof input === "string") return input;
560
+ if (typeof URL !== "undefined" && input instanceof URL) return input.toString();
561
+ if (input && typeof input === "object" && "url" in input && typeof input.url === "string") {
562
+ return input.url;
563
+ }
564
+ return String(input ?? "");
565
+ }
566
+ extractMethod(input, init2) {
567
+ const fromInit = init2?.method;
568
+ if (fromInit) return fromInit.toUpperCase();
569
+ if (input && typeof input === "object" && "method" in input && typeof input.method === "string") {
570
+ return input.method.toUpperCase();
571
+ }
572
+ return "GET";
573
+ }
574
+ };
575
+
576
+ // src/modules/console-interceptor.ts
577
+ var DEFAULT_MAX_LENGTH = 1e3;
578
+ var ConsoleInterceptor = class {
579
+ constructor(deps) {
580
+ this.deps = deps;
581
+ this.originals = /* @__PURE__ */ new Map();
582
+ this.wrapped = /* @__PURE__ */ new Map();
583
+ this.started = false;
584
+ this.isEmitting = false;
585
+ this.maxLength = deps.maxMessageLength ?? DEFAULT_MAX_LENGTH;
586
+ }
587
+ start() {
588
+ if (this.started) return;
589
+ if (typeof console === "undefined") return;
590
+ this.started = true;
591
+ const methods = this.deps.captureConsoleLog ? ["log", "warn", "error"] : ["warn", "error"];
592
+ for (const method of methods) {
593
+ this.wrapMethod(method);
594
+ }
595
+ }
596
+ wrapMethod(method) {
597
+ const c = console;
598
+ const original = c[method];
599
+ if (typeof original !== "function") return;
600
+ this.originals.set(method, original);
601
+ const wrappedFn = (...args) => {
602
+ if (this.started && !this.isEmitting) {
603
+ this.isEmitting = true;
604
+ try {
605
+ this.deps.onEvent({
606
+ type: "log",
607
+ ts: this.deps.now(),
608
+ level: method === "log" ? "info" : method,
609
+ message: this.formatMessage(args)
610
+ });
611
+ } catch {
612
+ } finally {
613
+ this.isEmitting = false;
614
+ }
615
+ }
616
+ original.apply(console, args);
617
+ };
618
+ this.wrapped.set(method, wrappedFn);
619
+ c[method] = wrappedFn;
620
+ }
621
+ stop() {
622
+ if (!this.started) return;
623
+ this.started = false;
624
+ this.isEmitting = false;
625
+ const c = console;
626
+ for (const [method, original] of this.originals) {
627
+ if (c[method] === this.wrapped.get(method)) {
628
+ c[method] = original;
629
+ }
630
+ }
631
+ this.originals.clear();
632
+ this.wrapped.clear();
633
+ }
634
+ formatMessage(args) {
635
+ const text = args.map((a) => {
636
+ if (a === void 0) return "undefined";
637
+ if (typeof a === "string") return a;
638
+ if (typeof a === "number") return String(a);
639
+ if (typeof a === "boolean") return String(a);
640
+ if (typeof a === "symbol") return a.toString();
641
+ if (typeof a === "function") {
642
+ return a.name ? `[Function: ${a.name}]` : "[Function]";
643
+ }
644
+ if (a instanceof Error) return a.stack || `${a.name}: ${a.message}`;
645
+ try {
646
+ const json = JSON.stringify(a);
647
+ return json !== void 0 ? json : String(a);
648
+ } catch {
649
+ return String(a);
650
+ }
651
+ }).join(" ");
652
+ return text.length > this.maxLength ? text.slice(0, this.maxLength) + "\u2026" : text;
653
+ }
654
+ };
655
+
656
+ // src/modules/offline-queue.ts
657
+ var DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
658
+ var DEFAULT_PREFIX = "clintplay:batch:";
659
+ var OfflineQueue = class {
660
+ constructor(deps) {
661
+ this.deps = deps;
662
+ this.currentBytes = 0;
663
+ // Monotonic sequence number appended to the storage key. Two batches
664
+ // enqueued in the same millisecond would otherwise tie on the
665
+ // timestamp prefix and sort non-deterministically. The padded seq
666
+ // resolves ties while preserving lex order = enqueue order.
667
+ this.seq = 0;
668
+ this.hydrationPromise = null;
669
+ this.prefix = deps.keyPrefix ?? DEFAULT_PREFIX;
670
+ this.maxBytes = deps.maxBytes ?? DEFAULT_MAX_BYTES;
671
+ }
672
+ // Compute the on-disk footprint from existing keys. Cheap on first
673
+ // boot, marginal on subsequent boots — called once from `init()`.
674
+ // Per-key getItem errors are isolated so a single flaky entry can't
675
+ // wedge accounting for the entire queue. `getAllKeys` failure is
676
+ // the only hard stop (no keys → no work).
677
+ hydrate(force = false) {
678
+ if (force) {
679
+ this.hydrationPromise = null;
680
+ }
681
+ if (!this.hydrationPromise) {
682
+ this.hydrationPromise = (async () => {
683
+ let keys;
684
+ try {
685
+ keys = (await this.deps.storage.getAllKeys()).filter(
686
+ (k) => k.startsWith(this.prefix)
687
+ );
688
+ } catch (err) {
689
+ this.deps.debug?.(
690
+ `offline-queue hydrate getAllKeys failed: ${String(err)}`
691
+ );
692
+ this.hydrationPromise = null;
693
+ return;
694
+ }
695
+ let total = 0;
696
+ for (const k of keys) {
697
+ try {
698
+ const raw = await this.deps.storage.getItem(k);
699
+ if (raw) total += raw.length;
700
+ } catch (err) {
701
+ this.deps.debug?.(
702
+ `offline-queue hydrate getItem ${k} failed: ${String(err)}`
703
+ );
704
+ }
705
+ }
706
+ this.currentBytes = total;
707
+ })().catch((err) => {
708
+ this.hydrationPromise = null;
709
+ throw err;
710
+ });
711
+ }
712
+ return this.hydrationPromise;
713
+ }
714
+ // Persist a single batch. Caller invokes after a live send fails.
715
+ // Silently drops batches that exceed the per-batch ceiling — those
716
+ // are unrecoverable even with retries, and keeping them around would
717
+ // wedge the queue.
718
+ async persist(batch) {
719
+ if (this.hydrationPromise) {
720
+ await this.hydrationPromise;
721
+ }
722
+ const json = JSON.stringify(batch);
723
+ const bytes = json.length;
724
+ if (bytes > this.maxBytes) {
725
+ this.deps.debug?.(
726
+ `offline-queue batch too large (${bytes} > ${this.maxBytes})`
727
+ );
728
+ return;
729
+ }
730
+ await this.evictUntilFits(bytes);
731
+ const ts = Date.now().toString().padStart(13, "0");
732
+ const seqStr = (this.seq++).toString().padStart(6, "0");
733
+ const key = `${this.prefix}${ts}-${seqStr}`;
734
+ try {
735
+ await this.deps.storage.setItem(key, json);
736
+ this.currentBytes += bytes;
737
+ } catch (err) {
738
+ this.deps.debug?.(`offline-queue setItem failed: ${String(err)}`);
739
+ }
740
+ }
741
+ // Replay all persisted batches in enqueue order. Stops at the first
742
+ // failed send — a network outage that caused the queue to fill up is
743
+ // typically still ongoing on the next boot; pushing more batches at
744
+ // a dead endpoint just burns retries and drops events on the floor.
745
+ async drain() {
746
+ if (this.hydrationPromise) {
747
+ await this.hydrationPromise;
748
+ }
749
+ let sent = 0;
750
+ try {
751
+ const keys = (await this.deps.storage.getAllKeys()).filter((k) => k.startsWith(this.prefix)).sort();
752
+ for (const key of keys) {
753
+ const raw = await this.deps.storage.getItem(key);
754
+ if (!raw) {
755
+ await this.deps.storage.removeItem(key);
756
+ continue;
757
+ }
758
+ try {
759
+ const batch = JSON.parse(raw);
760
+ const result = await this.deps.send(batch);
761
+ const ok = typeof result === "boolean" ? result : !!result?.ok;
762
+ const isNonRetryable = typeof result === "object" && result !== null && (typeof result.status === "number" && result.status >= 400 && result.status < 500 || result.retryable === false);
763
+ if (!ok) {
764
+ if (isNonRetryable) {
765
+ this.deps.debug?.(
766
+ `offline-queue drop 4xx non-retryable batch for key: ${key}`
767
+ );
768
+ await this.deps.storage.removeItem(key);
769
+ this.currentBytes = Math.max(0, this.currentBytes - raw.length);
770
+ continue;
771
+ }
772
+ break;
773
+ }
774
+ await this.deps.storage.removeItem(key);
775
+ this.currentBytes = Math.max(0, this.currentBytes - raw.length);
776
+ sent++;
777
+ } catch (err) {
778
+ this.deps.debug?.(`offline-queue drop bad batch: ${String(err)}`);
779
+ await this.deps.storage.removeItem(key);
780
+ this.currentBytes = Math.max(0, this.currentBytes - raw.length);
781
+ }
782
+ }
783
+ } catch (err) {
784
+ this.deps.debug?.(`offline-queue drain failed: ${String(err)}`);
785
+ }
786
+ return sent;
787
+ }
788
+ async clear() {
789
+ try {
790
+ const keys = (await this.deps.storage.getAllKeys()).filter(
791
+ (k) => k.startsWith(this.prefix)
792
+ );
793
+ if (keys.length > 0) {
794
+ await this.deps.storage.multiRemove(keys);
795
+ }
796
+ this.currentBytes = 0;
797
+ this.hydrationPromise = null;
798
+ } catch (err) {
799
+ this.deps.debug?.(`offline-queue clear failed: ${String(err)}`);
800
+ }
801
+ }
802
+ async evictUntilFits(bytesNeeded) {
803
+ if (this.currentBytes + bytesNeeded <= this.maxBytes) return;
804
+ const toFree = this.currentBytes + bytesNeeded - this.maxBytes;
805
+ let keys;
806
+ try {
807
+ keys = (await this.deps.storage.getAllKeys()).filter((k) => k.startsWith(this.prefix)).sort();
808
+ } catch (err) {
809
+ this.deps.debug?.(`evict getAllKeys failed: ${String(err)}`);
810
+ return;
811
+ }
812
+ let freed = 0;
813
+ const toRemove = [];
814
+ for (const key of keys) {
815
+ if (freed >= toFree) break;
816
+ let raw;
817
+ try {
818
+ raw = await this.deps.storage.getItem(key);
819
+ } catch (err) {
820
+ this.deps.debug?.(`evict getItem ${key} failed: ${String(err)}`);
821
+ continue;
822
+ }
823
+ if (raw != null) freed += raw.length;
824
+ toRemove.push(key);
825
+ }
826
+ if (toRemove.length === 0) return;
827
+ try {
828
+ await this.deps.storage.multiRemove(toRemove);
829
+ this.currentBytes = Math.max(0, this.currentBytes - freed);
830
+ } catch (err) {
831
+ this.deps.debug?.(`evict multiRemove failed: ${String(err)}`);
832
+ await this.hydrate(true);
833
+ }
834
+ }
835
+ };
836
+
837
+ // src/runtime-bridge.ts
838
+ var sink = null;
839
+ function _setSink(next) {
840
+ sink = next;
841
+ }
842
+ function emit(event) {
843
+ if (sink) {
844
+ try {
845
+ sink(event);
846
+ } catch {
847
+ }
848
+ }
849
+ }
850
+ var cachedModule = null;
851
+ function resolveNativeModule() {
852
+ if (cachedModule) return cachedModule;
853
+ try {
854
+ const turbo = TurboModuleRegistry.get("ClintplayRN");
855
+ if (turbo) {
856
+ cachedModule = turbo;
857
+ return cachedModule;
858
+ }
859
+ } catch {
860
+ }
861
+ try {
862
+ const paper = NativeModules.ClintplayRN ?? null;
863
+ if (paper) {
864
+ cachedModule = paper;
865
+ return cachedModule;
866
+ }
867
+ } catch {
868
+ }
869
+ return null;
870
+ }
871
+ function captureViewHierarchy() {
872
+ const n = resolveNativeModule();
873
+ if (!n) return null;
874
+ try {
875
+ return n.captureViewHierarchy();
876
+ } catch {
877
+ return null;
878
+ }
879
+ }
880
+
881
+ // package.json
882
+ var version = "0.1.0";
883
+ var CLINTPLAY_MASK_ID = "clintplay-mask";
884
+ var ClintplayMaskView = React5.forwardRef(function ClintplayMaskView2({ children, testID, ...rest }, ref) {
885
+ return /* @__PURE__ */ jsx(
886
+ View,
887
+ {
888
+ ref,
889
+ testID: testID ?? CLINTPLAY_MASK_ID,
890
+ ...rest,
891
+ collapsable: false,
892
+ nativeID: CLINTPLAY_MASK_ID,
893
+ children
894
+ }
895
+ );
896
+ });
897
+ var ClintplayMaskTextInput = React5.forwardRef(function ClintplayMaskTextInput2({ testID, accessibilityLabel, ...props }, ref) {
898
+ return /* @__PURE__ */ jsx(
899
+ TextInput,
900
+ {
901
+ ref,
902
+ testID: testID ?? CLINTPLAY_MASK_ID,
903
+ accessibilityLabel,
904
+ ...props,
905
+ nativeID: CLINTPLAY_MASK_ID
906
+ }
907
+ );
908
+ });
909
+ var ClintplayMaskImage = React5.forwardRef(function ClintplayMaskImage2({ testID, accessibilityLabel, ...props }, ref) {
910
+ return /* @__PURE__ */ jsx(
911
+ Image,
912
+ {
913
+ ref,
914
+ testID: testID ?? CLINTPLAY_MASK_ID,
915
+ accessibilityLabel,
916
+ ...props,
917
+ nativeID: CLINTPLAY_MASK_ID
918
+ }
919
+ );
920
+ });
921
+ function ClintplayCaptureProvider({
922
+ children,
923
+ style,
924
+ enabled = true,
925
+ onTouchStart,
926
+ onTouchEnd,
927
+ onTouchCancel,
928
+ ...rest
929
+ }) {
930
+ const onStart = useCallback(
931
+ (e) => {
932
+ const nativeEvent = e?.nativeEvent;
933
+ if (!nativeEvent) return;
934
+ const { locationX = 0, locationY = 0, pageX, pageY } = nativeEvent;
935
+ emit({
936
+ type: "touch",
937
+ ts: Date.now(),
938
+ x: locationX,
939
+ y: locationY,
940
+ screenX: pageX,
941
+ screenY: pageY,
942
+ kind: "start"
943
+ });
944
+ onTouchStart?.(e);
945
+ },
946
+ [onTouchStart]
947
+ );
948
+ const onEnd = useCallback(
949
+ (e) => {
950
+ const nativeEvent = e?.nativeEvent;
951
+ if (!nativeEvent) return;
952
+ const { locationX = 0, locationY = 0, pageX, pageY } = nativeEvent;
953
+ emit({
954
+ type: "touch",
955
+ ts: Date.now(),
956
+ x: locationX,
957
+ y: locationY,
958
+ screenX: pageX,
959
+ screenY: pageY,
960
+ kind: "end"
961
+ });
962
+ onTouchEnd?.(e);
963
+ },
964
+ [onTouchEnd]
965
+ );
966
+ const onCancel = useCallback(
967
+ (e) => {
968
+ const nativeEvent = e?.nativeEvent;
969
+ if (!nativeEvent) return;
970
+ const { locationX = 0, locationY = 0, pageX, pageY } = nativeEvent;
971
+ emit({
972
+ type: "touch",
973
+ ts: Date.now(),
974
+ x: locationX,
975
+ y: locationY,
976
+ screenX: pageX,
977
+ screenY: pageY,
978
+ kind: "end"
979
+ });
980
+ onTouchCancel?.(e);
981
+ },
982
+ [onTouchCancel]
983
+ );
984
+ if (!enabled) return /* @__PURE__ */ jsx(Fragment, { children });
985
+ return /* @__PURE__ */ jsx(
986
+ View,
987
+ {
988
+ ...rest,
989
+ style: [{ flex: 1 }, style],
990
+ onTouchStart: onStart,
991
+ onTouchEnd: onEnd,
992
+ onTouchCancel: onCancel,
993
+ children
994
+ }
995
+ );
996
+ }
997
+ var sharedLastTrackedRoute = void 0;
998
+ function _resetLastTrackedRoute() {
999
+ sharedLastTrackedRoute = void 0;
1000
+ }
1001
+ function useClintplayNavigationTracking(routeNameOrPath, options) {
1002
+ const optionsRef = useRef(options);
1003
+ optionsRef.current = options;
1004
+ const trackScreen2 = useCallback((to, from) => {
1005
+ const cleanTo = sanitizeRoute(to);
1006
+ if (!cleanTo || cleanTo === sharedLastTrackedRoute) return;
1007
+ const cleanFrom = sanitizeRoute(from);
1008
+ const initialFrom = sanitizeRoute(optionsRef.current?.from);
1009
+ const previousRoute = cleanFrom ?? sharedLastTrackedRoute ?? (initialFrom && initialFrom !== cleanTo ? initialFrom : void 0);
1010
+ sharedLastTrackedRoute = cleanTo;
1011
+ emit({
1012
+ type: "navigation",
1013
+ ts: Date.now(),
1014
+ to: cleanTo,
1015
+ ...previousRoute ? { from: previousRoute } : {}
1016
+ });
1017
+ try {
1018
+ optionsRef.current?.onRouteChanged?.(cleanTo, previousRoute);
1019
+ } catch {
1020
+ }
1021
+ }, []);
1022
+ useEffect(() => {
1023
+ const clean = sanitizeRoute(routeNameOrPath);
1024
+ if (clean && clean !== sharedLastTrackedRoute) {
1025
+ trackScreen2(clean);
1026
+ }
1027
+ }, [routeNameOrPath, trackScreen2]);
1028
+ const handleStateChange = useCallback(
1029
+ (state2) => {
1030
+ const nextRoute = getActiveRouteName(state2);
1031
+ if (nextRoute) {
1032
+ trackScreen2(nextRoute);
1033
+ }
1034
+ },
1035
+ [trackScreen2]
1036
+ );
1037
+ const result = Object.assign(handleStateChange, {
1038
+ onStateChange: handleStateChange,
1039
+ trackScreen: trackScreen2
1040
+ });
1041
+ return result;
1042
+ }
1043
+ function sanitizeRoute(route) {
1044
+ if (!route || typeof route !== "string") return void 0;
1045
+ const clean = route.split(/[?#]/)[0];
1046
+ return clean || void 0;
1047
+ }
1048
+ function getActiveRouteName(state2, depth = 0) {
1049
+ if (depth > 20) return void 0;
1050
+ if (!state2 || typeof state2 !== "object") {
1051
+ return void 0;
1052
+ }
1053
+ const s = state2;
1054
+ if (!Array.isArray(s.routes) || s.routes.length === 0) {
1055
+ if (typeof s.name === "string") {
1056
+ return s.name;
1057
+ }
1058
+ return void 0;
1059
+ }
1060
+ const index = typeof s.index === "number" && s.index >= 0 && s.index < s.routes.length ? s.index : s.routes.length - 1;
1061
+ const currentRoute = s.routes[index];
1062
+ if (!currentRoute) return void 0;
1063
+ if (currentRoute.state) {
1064
+ if (depth >= 20) return void 0;
1065
+ const childRoute = getActiveRouteName(currentRoute.state, depth + 1);
1066
+ if (childRoute !== void 0) {
1067
+ return childRoute;
1068
+ }
1069
+ const childState = currentRoute.state;
1070
+ if (childState && typeof childState === "object" && (!Array.isArray(childState.routes) || childState.routes.length === 0)) {
1071
+ return currentRoute.name;
1072
+ }
1073
+ return void 0;
1074
+ }
1075
+ return currentRoute.name;
1076
+ }
1077
+ function createClintplayNavigationStateListener(options) {
1078
+ const opts = typeof options === "function" ? { onRouteChanged: options } : options ?? {};
1079
+ let currentRoute = void 0;
1080
+ const initialRoute = sanitizeRoute(opts.initialRouteName);
1081
+ let isFirst = true;
1082
+ return function onStateChange(state2) {
1083
+ const rawRoute = getActiveRouteName(state2);
1084
+ const nextRoute = sanitizeRoute(rawRoute);
1085
+ if (!nextRoute) return;
1086
+ if (isFirst) {
1087
+ isFirst = false;
1088
+ const from = initialRoute && initialRoute !== nextRoute ? initialRoute : void 0;
1089
+ currentRoute = nextRoute;
1090
+ emit({
1091
+ type: "navigation",
1092
+ ts: Date.now(),
1093
+ to: nextRoute,
1094
+ ...from ? { from } : {}
1095
+ });
1096
+ try {
1097
+ opts.onRouteChanged?.(nextRoute, from);
1098
+ } catch {
1099
+ }
1100
+ return;
1101
+ }
1102
+ if (nextRoute === currentRoute) return;
1103
+ const previousRoute = currentRoute;
1104
+ currentRoute = nextRoute;
1105
+ emit({
1106
+ type: "navigation",
1107
+ ts: Date.now(),
1108
+ to: nextRoute,
1109
+ ...previousRoute ? { from: previousRoute } : {}
1110
+ });
1111
+ try {
1112
+ opts.onRouteChanged?.(nextRoute, previousRoute);
1113
+ } catch {
1114
+ }
1115
+ };
1116
+ }
1117
+ function ClintplayNavigationTracker({
1118
+ children,
1119
+ pathname,
1120
+ onStateChange,
1121
+ initialRouteName,
1122
+ onRouteChanged
1123
+ }) {
1124
+ const tracking = useClintplayNavigationTracking(pathname, {
1125
+ from: initialRouteName,
1126
+ onRouteChanged
1127
+ });
1128
+ const handleStateChange = useCallback(
1129
+ (state2) => {
1130
+ tracking.onStateChange(state2);
1131
+ try {
1132
+ onStateChange?.(state2);
1133
+ } catch {
1134
+ }
1135
+ },
1136
+ [tracking, onStateChange]
1137
+ );
1138
+ const childOnStateChangeRef = useRef(void 0);
1139
+ const mergedOnStateChange = useCallback(
1140
+ (state2) => {
1141
+ handleStateChange(state2);
1142
+ try {
1143
+ childOnStateChangeRef.current?.(state2);
1144
+ } catch {
1145
+ }
1146
+ },
1147
+ [handleStateChange]
1148
+ );
1149
+ if (typeof children === "function") {
1150
+ return /* @__PURE__ */ jsx(Fragment, { children: children({ onStateChange: handleStateChange }) });
1151
+ }
1152
+ if (React5.isValidElement(children)) {
1153
+ childOnStateChangeRef.current = children.props?.onStateChange;
1154
+ return React5.cloneElement(children, {
1155
+ onStateChange: mergedOnStateChange
1156
+ });
1157
+ }
1158
+ if (Array.isArray(children) || children && React5.Children.count(children) > 1) {
1159
+ let targetIndex = -1;
1160
+ let firstValidIndex = -1;
1161
+ let scanIndex = 0;
1162
+ React5.Children.forEach(children, (child) => {
1163
+ if (React5.isValidElement(child)) {
1164
+ if (firstValidIndex === -1) {
1165
+ firstValidIndex = scanIndex;
1166
+ }
1167
+ if (typeof child.props?.onStateChange === "function" && targetIndex === -1) {
1168
+ targetIndex = scanIndex;
1169
+ }
1170
+ }
1171
+ scanIndex++;
1172
+ });
1173
+ const chosenIndex = targetIndex !== -1 ? targetIndex : firstValidIndex;
1174
+ let mapIndex = 0;
1175
+ const clonedChildren = React5.Children.map(children, (child) => {
1176
+ const currentIdx = mapIndex++;
1177
+ if (!React5.isValidElement(child)) return child;
1178
+ if (currentIdx === chosenIndex) {
1179
+ childOnStateChangeRef.current = child.props?.onStateChange;
1180
+ return React5.cloneElement(child, {
1181
+ onStateChange: mergedOnStateChange
1182
+ });
1183
+ }
1184
+ return child;
1185
+ });
1186
+ return /* @__PURE__ */ jsx(Fragment, { children: clonedChildren });
1187
+ }
1188
+ return /* @__PURE__ */ jsx(Fragment, { children: children ?? null });
1189
+ }
1190
+
1191
+ // src/index.ts
1192
+ var SDK_VERSION = version;
1193
+ var state = null;
1194
+ function deviceInfo() {
1195
+ const os = Platform.OS;
1196
+ const platform = os === "ios" ? "ios" : os === "android" ? "android" : "web";
1197
+ return { platform, osVersion: String(Platform.Version ?? "") };
1198
+ }
1199
+ function debugLog(cfg, ...args) {
1200
+ if (!cfg.debug) return;
1201
+ if (typeof console !== "undefined" && console.log) {
1202
+ console.log("[clintplay-rn]", ...args);
1203
+ }
1204
+ }
1205
+ function formatEventForBatch(event, defaultSessionId) {
1206
+ const ev = event;
1207
+ const timestamp = typeof ev.timestamp === "number" ? ev.timestamp : typeof ev.ts === "number" ? ev.ts : Date.now();
1208
+ const sessionId = ev.sessionId || defaultSessionId;
1209
+ const type = ev.type;
1210
+ if (ev.data !== void 0) {
1211
+ return {
1212
+ type,
1213
+ timestamp,
1214
+ sessionId,
1215
+ data: ev.data
1216
+ };
1217
+ }
1218
+ const { type: _t, ts: _ts, timestamp: _tms, sessionId: _s, ...rest } = ev;
1219
+ return {
1220
+ type,
1221
+ timestamp,
1222
+ sessionId,
1223
+ data: rest
1224
+ };
1225
+ }
1226
+ async function flushOnce(s) {
1227
+ try {
1228
+ if (s.buffer.size() === 0) return;
1229
+ const currentSessionId = s.session.current().sessionId;
1230
+ const rawEvents = s.buffer.drain();
1231
+ const events = rawEvents.map(
1232
+ (e) => formatEventForBatch(e, currentSessionId)
1233
+ );
1234
+ const batch = {
1235
+ projectId: s.config.projectId,
1236
+ sessionId: currentSessionId,
1237
+ sdkVersion: SDK_VERSION,
1238
+ events,
1239
+ sentAt: Date.now(),
1240
+ ...s.config.anonId ? { anonId: s.config.anonId } : {}
1241
+ };
1242
+ const ok = await s.transport.send(batch);
1243
+ if (!ok && s.offlineQueue) {
1244
+ await s.offlineQueue.persist(batch);
1245
+ }
1246
+ debugLog(s.config, ok ? "flushed" : "flush failed", events.length);
1247
+ } catch (err) {
1248
+ debugLog(s.config, "flush failed with error", String(err));
1249
+ }
1250
+ }
1251
+ function init(rawConfig) {
1252
+ if (state?.active) return;
1253
+ if (!rawConfig.apiUrl) return;
1254
+ if (rawConfig.enabled === false) return;
1255
+ const sampleRate = rawConfig.sampleRate ?? 1;
1256
+ if (sampleRate < 1 && Math.random() > sampleRate) return;
1257
+ const config = {
1258
+ apiUrl: rawConfig.apiUrl,
1259
+ apiKey: rawConfig.apiKey,
1260
+ projectId: rawConfig.projectId ?? rawConfig.apiKey ?? "",
1261
+ anonId: rawConfig.anonId,
1262
+ flushIntervalMs: rawConfig.flushIntervalMs ?? 5e3,
1263
+ flushSize: rawConfig.flushSize ?? 50,
1264
+ maxBufferSize: rawConfig.maxBufferSize ?? 1e3,
1265
+ debug: rawConfig.debug ?? false,
1266
+ captureWireframe: rawConfig.captureWireframe ?? false
1267
+ };
1268
+ const buffer = new EventBuffer(config.maxBufferSize);
1269
+ const session = new SessionManager();
1270
+ const transport = new Transport({
1271
+ apiUrl: config.apiUrl,
1272
+ apiKey: config.apiKey,
1273
+ projectId: config.projectId
1274
+ });
1275
+ const excluded = new Set(rawConfig.excludeEventTypes ?? []);
1276
+ const local = {
1277
+ config,
1278
+ buffer,
1279
+ session,
1280
+ transport,
1281
+ recorder: { start: () => {
1282
+ }, stop: () => {
1283
+ }, takeWireframeSnapshot: () => {
1284
+ } },
1285
+ networkInterceptor: null,
1286
+ consoleInterceptor: null,
1287
+ offlineQueue: rawConfig.asyncStorage ? new OfflineQueue({
1288
+ storage: rawConfig.asyncStorage,
1289
+ send: (b) => transport.sendDetailed(b),
1290
+ debug: (msg) => debugLog(config, msg)
1291
+ }) : null,
1292
+ flushTimer: null,
1293
+ excluded,
1294
+ active: true
1295
+ };
1296
+ state = local;
1297
+ const onEvent = (event) => {
1298
+ if (excluded.has(event.type)) return;
1299
+ buffer.push(event);
1300
+ session.touch();
1301
+ if (buffer.size() >= config.flushSize) {
1302
+ void flushOnce(local);
1303
+ }
1304
+ };
1305
+ const recorder = createRecorder({
1306
+ onEvent,
1307
+ now: Date.now,
1308
+ installErrorHandler: true,
1309
+ captureWireframe: rawConfig.captureWireframe,
1310
+ wireframeIntervalMs: rawConfig.wireframeIntervalMs,
1311
+ captureViewHierarchy
1312
+ });
1313
+ local.recorder = recorder;
1314
+ if (rawConfig.captureNetwork !== false) {
1315
+ local.networkInterceptor = new NetworkInterceptor({
1316
+ onEvent,
1317
+ now: Date.now,
1318
+ sanitizeUrl: rawConfig.sanitizeUrl,
1319
+ apiUrl: config.apiUrl
1320
+ });
1321
+ }
1322
+ if (rawConfig.captureConsole === true) {
1323
+ local.consoleInterceptor = new ConsoleInterceptor({
1324
+ onEvent,
1325
+ now: Date.now,
1326
+ captureConsoleLog: rawConfig.captureConsoleLog
1327
+ });
1328
+ }
1329
+ buffer.push({
1330
+ type: "meta",
1331
+ ts: Date.now(),
1332
+ sessionId: session.current().sessionId,
1333
+ sdkVersion: SDK_VERSION,
1334
+ device: deviceInfo()
1335
+ });
1336
+ recorder.start();
1337
+ local.networkInterceptor?.start();
1338
+ local.consoleInterceptor?.start();
1339
+ _setSink(onEvent);
1340
+ if (local.offlineQueue) {
1341
+ const q = local.offlineQueue;
1342
+ void q.hydrate().then(() => q.drain()).catch(
1343
+ (err) => debugLog(config, "offline queue init failed", String(err))
1344
+ );
1345
+ }
1346
+ local.flushTimer = setInterval(async () => {
1347
+ if (local.session.isStale()) {
1348
+ await flushOnce(local);
1349
+ if (local.session.rotateIfStale()) {
1350
+ local.buffer.push({
1351
+ type: "meta",
1352
+ ts: Date.now(),
1353
+ sessionId: local.session.current().sessionId,
1354
+ sdkVersion: SDK_VERSION,
1355
+ device: deviceInfo()
1356
+ });
1357
+ }
1358
+ }
1359
+ void flushOnce(local);
1360
+ }, config.flushIntervalMs);
1361
+ debugLog(config, "init", { sessionId: session.current().sessionId });
1362
+ }
1363
+ function identify(userId, traits) {
1364
+ if (!state?.active) return;
1365
+ const prevUser = state.session.current().userId;
1366
+ const isUserChange = prevUser !== null && (userId === null || prevUser !== userId);
1367
+ if (isUserChange) {
1368
+ void flushOnce(state);
1369
+ }
1370
+ const rotated = state.session.setUser(userId);
1371
+ if (rotated) {
1372
+ state.buffer.push({
1373
+ type: "meta",
1374
+ ts: Date.now(),
1375
+ sessionId: state.session.current().sessionId,
1376
+ sdkVersion: SDK_VERSION,
1377
+ device: deviceInfo()
1378
+ });
1379
+ }
1380
+ state.buffer.push({
1381
+ type: "identify",
1382
+ ts: Date.now(),
1383
+ userId,
1384
+ traits
1385
+ });
1386
+ }
1387
+ function reset(options) {
1388
+ if (!state?.active) return;
1389
+ void flushOnce(state);
1390
+ state.session.reset();
1391
+ if (options?.clearOfflineQueue && state.offlineQueue) {
1392
+ void state.offlineQueue.clear();
1393
+ }
1394
+ state.buffer.push({
1395
+ type: "meta",
1396
+ ts: Date.now(),
1397
+ sessionId: state.session.current().sessionId,
1398
+ sdkVersion: SDK_VERSION,
1399
+ device: deviceInfo()
1400
+ });
1401
+ }
1402
+ function trackScreen(path, from) {
1403
+ if (!state?.active) return;
1404
+ state.buffer.push({
1405
+ type: "navigation",
1406
+ ts: Date.now(),
1407
+ to: path,
1408
+ ...from ? { from } : {}
1409
+ });
1410
+ state.session.touch();
1411
+ if (state.config.captureWireframe) {
1412
+ state.recorder.takeWireframeSnapshot();
1413
+ }
1414
+ }
1415
+ function reportError(err) {
1416
+ if (!state?.active) return;
1417
+ const error = err instanceof Error ? err : new Error(String(err));
1418
+ state.buffer.push({
1419
+ type: "error",
1420
+ ts: Date.now(),
1421
+ message: error.message,
1422
+ stack: error.stack,
1423
+ name: error.name,
1424
+ fatal: false
1425
+ });
1426
+ }
1427
+ async function flush() {
1428
+ if (!state?.active) return;
1429
+ await flushOnce(state);
1430
+ }
1431
+ async function stop() {
1432
+ if (!state?.active) return;
1433
+ const s = state;
1434
+ s.active = false;
1435
+ state = null;
1436
+ _setSink(null);
1437
+ if (s.flushTimer) clearInterval(s.flushTimer);
1438
+ s.consoleInterceptor?.stop();
1439
+ s.networkInterceptor?.stop();
1440
+ s.recorder.stop();
1441
+ await flushOnce(s);
1442
+ }
1443
+ function current() {
1444
+ if (!state?.active) return null;
1445
+ const s = state.session.current();
1446
+ return { sessionId: s.sessionId, userId: s.userId };
1447
+ }
1448
+ var Clintplay = {
1449
+ init,
1450
+ identify,
1451
+ reset,
1452
+ trackScreen,
1453
+ reportError,
1454
+ flush,
1455
+ stop,
1456
+ current
1457
+ };
1458
+
1459
+ export { CLINTPLAY_MASK_ID, Clintplay, ClintplayCaptureProvider, ClintplayMaskImage, ClintplayMaskTextInput, ClintplayMaskView, ClintplayNavigationTracker, SDK_VERSION, _resetLastTrackedRoute, createClintplayNavigationStateListener, formatEventForBatch, getActiveRouteName, sanitizeRoute, useClintplayNavigationTracking };
1460
+ //# sourceMappingURL=index.mjs.map
1461
+ //# sourceMappingURL=index.mjs.map