@keyframerdev/vitals 0.0.0-stage → 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.js ADDED
@@ -0,0 +1,1554 @@
1
+ // src/index.ts
2
+ import { createElement } from "react";
3
+ import { View } from "react-native";
4
+
5
+ // src/native.ts
6
+ import { AccessibilityInfo, AppState, Platform } from "react-native";
7
+ import { scheduleOnRN, scheduleOnUI } from "react-native-worklets";
8
+
9
+ // ../protocol/src/vitals.ts
10
+ var VITALS_FORMAT = "keyframer-vitals";
11
+ var VITALS_VERSION = 1;
12
+ var VITALS_ENDPOINT = "https://vitals.keyframer.dev";
13
+ var VITALS_BUCKETS = [0, 1, 2, 3, 5, 9, 17, 33];
14
+ var VITALS_FROZEN_MS = 700;
15
+ var VITALS_WINDOW_MS = 10 * 60 * 1000;
16
+ var VITALS_SESSION_GAP_MS = 30 * 1000;
17
+ var VITALS_LIMITS = { screens: 40, sites: 200, worst: 10, images: 64, bytes: 32 * 1024, screenName: 64 };
18
+ var VITALS_MAX_SESSIONS_PER_DAY = 3;
19
+ var VITALS_DEFAULT_SAMPLE_RATE = 0.1;
20
+
21
+ // src/collector.ts
22
+ var SLACK_MS = 32;
23
+ var LEAD_MS = 50;
24
+ var MOUNT_MS = 500;
25
+ var BURST = 6;
26
+ var TOUCH_TAIL_MS = 1500;
27
+ var SCROLL_TAIL_MS = 250;
28
+ var MAX_WINDOWS = 100;
29
+ var MAX_STARTS = 64;
30
+ var MAX_SPANS = 16;
31
+ var MAX_NAVS = 16;
32
+ function bucketOf(missed) {
33
+ let b = 0;
34
+ for (let i = 1;i < VITALS_BUCKETS.length; i++)
35
+ if (missed >= VITALS_BUCKETS[i])
36
+ b = i;
37
+ return b;
38
+ }
39
+ var emptyThread = () => ({ frames: 0, hist: VITALS_BUCKETS.map(() => 0), frozen: 0, hitchMs: 0, idleFrames: 0, idleMissed: 0 });
40
+ var siteId = (s) => s.key ?? refId(s.ref);
41
+ function refId(r) {
42
+ switch (r.kind) {
43
+ case "source":
44
+ return `${r.file}:${r.line}:${r.column ?? 0}`;
45
+ case "hashed":
46
+ return `#${r.hash}`;
47
+ case "name":
48
+ return `name:${r.name}`;
49
+ case "native":
50
+ return `native:${r.image}:${r.offset}`;
51
+ case "jvm":
52
+ return `jvm:${r.className}.${r.method}:${r.line ?? 0}`;
53
+ }
54
+ }
55
+ var hzOf = (budget) => Math.round(1000 / budget);
56
+
57
+ class Collector {
58
+ keepWorst;
59
+ screens = new Map;
60
+ sites = new Map;
61
+ windows = [];
62
+ starts = [];
63
+ touches = [];
64
+ scrolls = [];
65
+ navs = [];
66
+ shown = [];
67
+ before = "app";
68
+ worst = [];
69
+ touching = 0;
70
+ screen = "app";
71
+ fgSince = null;
72
+ fgMs = 0;
73
+ measuring = null;
74
+ measuredMs = 0;
75
+ latest = -Infinity;
76
+ hzFrames = new Map;
77
+ frameMs = { ui: 0, js: 0 };
78
+ reduceMotion = false;
79
+ reduceMotionSeen = false;
80
+ constructor(keepWorst = 5) {
81
+ this.keepWorst = keepWorst;
82
+ }
83
+ foreground(on, now) {
84
+ if (on && this.fgSince === null)
85
+ this.fgSince = now;
86
+ else if (!on && this.fgSince !== null) {
87
+ this.accrue(now);
88
+ this.fgSince = null;
89
+ this.measure(false, now);
90
+ }
91
+ }
92
+ setScreen(name, now, nav = true) {
93
+ if (name === this.screen && this.shown.length)
94
+ return;
95
+ this.accrue(now);
96
+ if (!this.shown.length)
97
+ this.before = this.screen;
98
+ this.screen = this.screenKey(name);
99
+ this.shown.push({ t: now, name: this.screen });
100
+ if (this.shown.length > MAX_NAVS)
101
+ this.before = this.shown.shift().name;
102
+ if (nav) {
103
+ this.navs.push({ t: now, name: this.screen });
104
+ if (this.navs.length > MAX_NAVS)
105
+ this.navs.shift();
106
+ }
107
+ }
108
+ setReduceMotion(on) {
109
+ this.reduceMotion = on;
110
+ if (on)
111
+ this.reduceMotionSeen = true;
112
+ }
113
+ animStart(info, now, ms, thread, eid = -1) {
114
+ const key = siteId(info);
115
+ const s = this.site(key, info);
116
+ s.runs++;
117
+ if (this.reduceMotion)
118
+ s.runsReduceMotion++;
119
+ s.screens.set(this.screen, (s.screens.get(this.screen) ?? 0) + 1);
120
+ const end = now + Math.max(0, ms);
121
+ s.activeMs += Math.max(0, end - Math.max(now, s.activeUntil));
122
+ s.activeUntil = Math.max(s.activeUntil, end);
123
+ this.windows.push({ key, info, start: now, end, eid });
124
+ if (this.windows.length > MAX_WINDOWS)
125
+ this.windows.splice(0, this.windows.length - MAX_WINDOWS);
126
+ this.starts.push({ t: now, key, thread });
127
+ if (this.starts.length > MAX_STARTS)
128
+ this.starts.shift();
129
+ }
130
+ consume(eid) {
131
+ const i = this.windows.findIndex((w2) => w2.eid === eid);
132
+ if (i < 0)
133
+ return;
134
+ const [w] = this.windows.splice(i, 1);
135
+ const s = this.sites.get(w.key);
136
+ if (s && s.runs > 0) {
137
+ s.runs--;
138
+ const n = (s.screens.get(this.screen) ?? 0) - 1;
139
+ if (n > 0)
140
+ s.screens.set(this.screen, n);
141
+ else
142
+ s.screens.delete(this.screen);
143
+ s.activeMs = Math.max(0, s.activeMs - (w.end - w.start));
144
+ if (s.runs === 0 && s.frames === 0)
145
+ this.sites.delete(w.key);
146
+ }
147
+ for (let j = this.starts.length - 1;j >= 0; j--) {
148
+ if (this.starts[j].key === w.key && this.starts[j].t === w.start) {
149
+ this.starts.splice(j, 1);
150
+ break;
151
+ }
152
+ }
153
+ }
154
+ touch(down, now) {
155
+ if (down) {
156
+ if (this.touching++ === 0)
157
+ this.pushSpan(this.touches, now, Infinity);
158
+ } else if (this.touching > 0 && --this.touching === 0) {
159
+ const last = this.touches[this.touches.length - 1];
160
+ if (last && last.end === Infinity)
161
+ last.end = now + TOUCH_TAIL_MS;
162
+ }
163
+ }
164
+ scroll(now) {
165
+ const last = this.scrolls[this.scrolls.length - 1];
166
+ if (last && now <= last.end + SCROLL_TAIL_MS)
167
+ last.end = Math.max(last.end, now);
168
+ else
169
+ this.pushSpan(this.scrolls, now, now);
170
+ }
171
+ measure(on, now) {
172
+ if (on && this.measuring === null)
173
+ this.measuring = now;
174
+ else if (!on && this.measuring !== null) {
175
+ this.measuredMs += Math.max(0, now - this.measuring);
176
+ this.measuring = null;
177
+ }
178
+ }
179
+ frame(thread, t, dt, budget) {
180
+ if (!(dt > 0) || !(budget > 0))
181
+ return;
182
+ const missed = Math.max(0, Math.round(dt / budget) - 1);
183
+ const hitch = Math.max(0, dt - budget);
184
+ const screenName = this.screenAt(t);
185
+ const screen = this.screenStats(screenName);
186
+ const st = screen.threads[thread] ??= emptyThread();
187
+ st.frames++;
188
+ st.hist[bucketOf(missed)]++;
189
+ if (dt >= VITALS_FROZEN_MS)
190
+ st.frozen++;
191
+ st.hitchMs += hitch;
192
+ const hz = hzOf(budget);
193
+ this.hzFrames.set(hz, (this.hzFrames.get(hz) ?? 0) + 1);
194
+ this.frameMs[thread] += dt;
195
+ if (t > this.latest)
196
+ this.latest = t;
197
+ const active = new Map;
198
+ for (const w of this.windows)
199
+ if (t >= w.start && t <= w.end + SLACK_MS)
200
+ active.set(w.key, w.info);
201
+ if (!active.size) {
202
+ st.idleFrames++;
203
+ st.idleMissed += missed;
204
+ }
205
+ const charged = [];
206
+ for (const [key, info] of active) {
207
+ const s = this.site(key, info);
208
+ s.frames++;
209
+ s.missed += missed;
210
+ s.hitchMs += hitch;
211
+ if (dt > s.worstMs)
212
+ s.worstMs = dt;
213
+ charged.push(s);
214
+ }
215
+ if (missed > 0)
216
+ this.considerWorst(thread, t, dt, missed, screenName, charged);
217
+ this.prune();
218
+ }
219
+ snapshot(now) {
220
+ this.accrue(now);
221
+ const gate = this.measuredMs + (this.measuring !== null ? Math.max(0, now - this.measuring) : 0);
222
+ const measuredMs = Math.min(this.fgMs, Math.max(gate, this.frameMs.ui, this.frameMs.js));
223
+ let targetHz = 60;
224
+ let best = -1;
225
+ let maxHz = 0;
226
+ for (const [hz, n] of this.hzFrames) {
227
+ if (n > best) {
228
+ best = n;
229
+ targetHz = hz;
230
+ }
231
+ if (hz > maxHz)
232
+ maxHz = hz;
233
+ }
234
+ return {
235
+ screens: [...this.screens.values()],
236
+ sites: [...this.sites.values()],
237
+ worst: [...this.worst],
238
+ foregroundMs: this.fgMs,
239
+ measuredMs,
240
+ targetHz,
241
+ maxHz: maxHz || targetHz,
242
+ reduceMotion: this.reduceMotionSeen
243
+ };
244
+ }
245
+ reset(now) {
246
+ this.accrue(now);
247
+ this.screens.clear();
248
+ this.sites.clear();
249
+ this.worst = [];
250
+ this.fgMs = 0;
251
+ this.measuredMs = 0;
252
+ if (this.measuring !== null)
253
+ this.measuring = now;
254
+ this.hzFrames.clear();
255
+ this.frameMs = { ui: 0, js: 0 };
256
+ this.reduceMotionSeen = this.reduceMotion;
257
+ }
258
+ clear() {
259
+ this.screens.clear();
260
+ this.sites.clear();
261
+ this.windows = [];
262
+ this.starts = [];
263
+ this.touches = [];
264
+ this.scrolls = [];
265
+ this.navs = [];
266
+ this.shown = [];
267
+ this.before = this.screen;
268
+ this.worst = [];
269
+ this.touching = 0;
270
+ this.fgMs = 0;
271
+ this.fgSince = null;
272
+ this.measuring = null;
273
+ this.measuredMs = 0;
274
+ this.hzFrames.clear();
275
+ this.frameMs = { ui: 0, js: 0 };
276
+ this.reduceMotionSeen = this.reduceMotion;
277
+ }
278
+ get currentScreen() {
279
+ return this.screen;
280
+ }
281
+ get foregroundMs() {
282
+ return this.fgMs;
283
+ }
284
+ foregroundAt(now) {
285
+ return this.fgMs + (this.fgSince !== null ? Math.max(0, now - this.fgSince) : 0);
286
+ }
287
+ accrue(now) {
288
+ if (this.fgSince === null)
289
+ return;
290
+ const dt = Math.max(0, now - this.fgSince);
291
+ this.fgMs += dt;
292
+ this.fgSince = now;
293
+ if (dt > 0)
294
+ this.screenStats(this.screen).ms += dt;
295
+ }
296
+ screenKey(name) {
297
+ if (this.screens.has(name) || this.screens.size < VITALS_LIMITS.screens - 1)
298
+ return name;
299
+ return "other";
300
+ }
301
+ screenStats(name) {
302
+ let s = this.screens.get(name);
303
+ if (!s)
304
+ this.screens.set(name, s = { name, ms: 0, threads: {} });
305
+ return s;
306
+ }
307
+ screenAt(t) {
308
+ for (let i = this.shown.length - 1;i >= 0; i--)
309
+ if (this.shown[i].t <= t)
310
+ return this.shown[i].name;
311
+ return this.shown.length ? this.before : this.screen;
312
+ }
313
+ site(key, info) {
314
+ let s = this.sites.get(key);
315
+ if (!s) {
316
+ s = { info, runs: 0, runsReduceMotion: 0, activeMs: 0, frames: 0, missed: 0, hitchMs: 0, worstMs: 0, screens: new Map, activeUntil: -Infinity };
317
+ this.sites.set(key, s);
318
+ }
319
+ return s;
320
+ }
321
+ pushSpan(list, start, end) {
322
+ list.push({ start, end });
323
+ if (list.length > MAX_SPANS)
324
+ list.shift();
325
+ }
326
+ open(list, t, tail) {
327
+ return list.some((s) => t >= s.start - 16 && t <= s.end + tail);
328
+ }
329
+ considerWorst(thread, t, dt, missed, screen, charged) {
330
+ if (this.worst.length >= this.keepWorst && dt <= this.worst[this.worst.length - 1].ms)
331
+ return;
332
+ const { cause, afterNavMs } = this.causeOf(thread, t, dt);
333
+ const w = { thread, ms: dt, missed, screen, sites: charged, cause, ...afterNavMs !== undefined ? { afterNavMs } : {} };
334
+ this.worst.push(w);
335
+ this.worst.sort((a, b) => b.ms - a.ms);
336
+ if (this.worst.length > this.keepWorst)
337
+ this.worst.length = this.keepWorst;
338
+ }
339
+ causeOf(thread, t, dt) {
340
+ let nav;
341
+ for (const n of this.navs)
342
+ if (n.t <= t)
343
+ nav = n.t;
344
+ if (nav !== undefined && t - nav <= MOUNT_MS)
345
+ return { cause: "screen-mount", afterNavMs: Math.round(t - nav) };
346
+ const from = t - dt - LEAD_MS;
347
+ const started = this.starts.filter((s) => s.t >= from && s.t <= t + 4);
348
+ if (new Set(started.map((s) => s.key)).size >= BURST)
349
+ return { cause: "burst" };
350
+ if (thread === "js" && started.some((s) => s.thread === "js"))
351
+ return { cause: "busy-start" };
352
+ if (!started.length && this.open(this.scrolls, t, SCROLL_TAIL_MS))
353
+ return { cause: "scroll" };
354
+ if (!started.length && this.open(this.touches, t, 0))
355
+ return { cause: "gesture" };
356
+ return { cause: "unknown" };
357
+ }
358
+ prune() {
359
+ const before = this.latest - 2000;
360
+ if (this.windows.length && this.windows[0].end + SLACK_MS < before)
361
+ this.windows = this.windows.filter((w) => w.end + SLACK_MS >= before);
362
+ while (this.touches.length > 1 && this.touches[0].end < before)
363
+ this.touches.shift();
364
+ while (this.scrolls.length > 1 && this.scrolls[0].end + SCROLL_TAIL_MS < before)
365
+ this.scrolls.shift();
366
+ }
367
+ }
368
+ var GATE_TAIL_MS = 250;
369
+
370
+ class MotionGate {
371
+ until = -Infinity;
372
+ touching = 0;
373
+ note(end, now) {
374
+ const wasOpen = this.open(now);
375
+ if (end + GATE_TAIL_MS > this.until)
376
+ this.until = end + GATE_TAIL_MS;
377
+ return !wasOpen && this.open(now);
378
+ }
379
+ touch(down, now) {
380
+ const wasOpen = this.open(now);
381
+ if (down)
382
+ this.touching++;
383
+ else if (this.touching > 0 && --this.touching === 0)
384
+ this.note(now + TOUCH_TAIL_MS, now);
385
+ return !wasOpen && this.open(now);
386
+ }
387
+ open(now) {
388
+ return this.touching > 0 || now <= this.until;
389
+ }
390
+ closesAt() {
391
+ return this.touching > 0 ? Infinity : this.until;
392
+ }
393
+ reset() {
394
+ this.until = -Infinity;
395
+ this.touching = 0;
396
+ }
397
+ }
398
+
399
+ // src/mask.ts
400
+ var MASKS = [
401
+ [/([?&#](?:access_?token|refresh_?token|id_?token|token|key|api_?key|secret|password|passwd|pwd|code|auth|session|sig|signature)=)[^&\s#"']+/gi, "$1[redacted]"],
402
+ [/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi, "[id]"],
403
+ [/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, "[email]"],
404
+ [/\beyJ[\w-]{4,}\.[\w-]{4,}\.[\w-]*/g, "[token]"],
405
+ [/\b(Bearer|Basic|Token)\s+[\w\-.~+/]{8,}=*/gi, "$1 [token]"],
406
+ [/\b(?:kf|sk|pk|rk|ghp|gho|ghs|github_pat|xox[abpr]?|AKIA)[-_][\w-]{8,}/g, "[token]"],
407
+ [/\b(?=[\w-]*\d)(?=[\w-]*[A-Za-z])[\w-]{24,}\b/g, "[token]"],
408
+ [/\+?\(?\d{1,4}\)?(?:[ .-]\(?\d{2,4}\)?){2,}/g, "[number]"],
409
+ [/\d{4,}/g, "[number]"]
410
+ ];
411
+ function maskText(text) {
412
+ let out = text;
413
+ for (const [re, by] of MASKS)
414
+ out = out.replace(re, by);
415
+ return out;
416
+ }
417
+ function screenName(raw) {
418
+ const s = typeof raw === "string" ? maskText(raw).trim() : "";
419
+ if (!s)
420
+ return "unknown";
421
+ return s.length > VITALS_LIMITS.screenName ? s.slice(0, VITALS_LIMITS.screenName) : s;
422
+ }
423
+
424
+ // src/encode.ts
425
+ var r1 = (x) => Math.round(x * 10) / 10;
426
+ var MAX_FOREGROUND = VITALS_WINDOW_MS + 60000;
427
+ function hasData(s) {
428
+ return s.sites.some((x) => x.runs > 0 || x.frames > 0) || s.screens.some((x) => Object.values(x.threads).some((t) => t && t.frames > 0));
429
+ }
430
+ function encodeReport(snap, ctx) {
431
+ const ranked = snap.sites.filter((s) => s.runs > 0 || s.frames > 0).sort((a, b) => b.frames - a.frames || b.runs - a.runs);
432
+ let keep = Math.min(ranked.length, VITALS_LIMITS.sites);
433
+ for (;; ) {
434
+ const report = build(snap, ctx, ranked.slice(0, keep));
435
+ if (keep === 0 || JSON.stringify(report).length <= VITALS_LIMITS.bytes)
436
+ return report;
437
+ keep = Math.max(0, keep - Math.max(1, Math.ceil(keep / 10)));
438
+ }
439
+ }
440
+ function build(snap, ctx, kept) {
441
+ const screens = [];
442
+ const screenIndex = new Map;
443
+ const screenOf = (name) => {
444
+ let i = screenIndex.get(name);
445
+ if (i === undefined) {
446
+ if (screens.length >= VITALS_LIMITS.screens)
447
+ return screenIndex.get("other") ?? 0;
448
+ i = screens.length;
449
+ screenIndex.set(name, i);
450
+ screens.push({ name: screenName(name), ms: 0, threads: {} });
451
+ }
452
+ return i;
453
+ };
454
+ for (const s of snap.screens.slice(0, VITALS_LIMITS.screens)) {
455
+ const i = screenOf(s.name);
456
+ const threads = {};
457
+ for (const [t, st] of Object.entries(s.threads)) {
458
+ if (st && st.frames > 0)
459
+ threads[t] = { ...st, hist: [...st.hist], hitchMs: r1(st.hitchMs) };
460
+ }
461
+ screens[i] = { name: screenName(s.name), ms: Math.round(s.ms), threads };
462
+ }
463
+ const siteIndex = new Map;
464
+ const sites = kept.map((s, i) => {
465
+ siteIndex.set(s, i);
466
+ let where = "";
467
+ let most = -1;
468
+ for (const [name, n] of s.screens) {
469
+ if (n > most) {
470
+ most = n;
471
+ where = name;
472
+ }
473
+ }
474
+ const site = {
475
+ ref: s.info.ref,
476
+ ...s.info.key !== undefined ? { key: s.info.key } : {},
477
+ fn: s.info.fn,
478
+ component: s.info.component ?? null,
479
+ prop: s.info.prop ?? null,
480
+ screen: screenOf(where || snap.screens[0]?.name || "unknown"),
481
+ runs: s.runs,
482
+ activeMs: Math.round(Math.min(s.activeMs, MAX_FOREGROUND)),
483
+ frames: s.frames,
484
+ missed: s.missed,
485
+ hitchMs: r1(s.hitchMs),
486
+ worstMs: r1(s.worstMs)
487
+ };
488
+ if (ctx.collectReduceMotion)
489
+ site.runsReduceMotion = s.runsReduceMotion;
490
+ return site;
491
+ });
492
+ const worst = snap.worst.slice(0, ctx.worst ?? 5).map((w) => ({
493
+ thread: w.thread,
494
+ ms: r1(w.ms),
495
+ missed: w.missed,
496
+ screen: screenOf(w.screen),
497
+ sites: w.sites.map((s) => siteIndex.get(s)).filter((i) => i !== undefined),
498
+ cause: w.cause,
499
+ ...w.afterNavMs !== undefined ? { afterNavMs: w.afterNavMs } : {}
500
+ }));
501
+ const foregroundMs = Math.round(Math.min(snap.foregroundMs, MAX_FOREGROUND));
502
+ const measuredMs = Math.round(Math.min(snap.measuredMs, foregroundMs));
503
+ const state = {};
504
+ if (ctx.collectReduceMotion)
505
+ state.reduceMotion = snap.reduceMotion;
506
+ if (ctx.state?.lowPower !== undefined)
507
+ state.lowPower = ctx.state.lowPower;
508
+ if (ctx.state?.thermal !== undefined)
509
+ state.thermal = ctx.state.thermal;
510
+ const hz = ctx.display?.hz ?? snap.maxHz;
511
+ return {
512
+ format: VITALS_FORMAT,
513
+ version: VITALS_VERSION,
514
+ app: ctx.app,
515
+ sdk: ctx.sdk,
516
+ device: ctx.device,
517
+ display: { hz: Math.max(hz, snap.targetHz), ...ctx.display?.adaptive !== undefined ? { adaptive: ctx.display.adaptive } : {}, targetHz: snap.targetHz },
518
+ state,
519
+ session: { ...ctx.session, foregroundMs, measuredMs },
520
+ screens,
521
+ sites,
522
+ worst
523
+ };
524
+ }
525
+
526
+ // src/estimate.ts
527
+ var isRef = (v) => !!v && typeof v === "object" && typeof v.$anim === "number";
528
+ var num = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
529
+ var cfg = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
530
+ function springSettleMs(c) {
531
+ if (typeof c.duration === "number")
532
+ return c.duration;
533
+ const k = num(c.stiffness, 100);
534
+ const d = num(c.damping, 10);
535
+ const m = num(c.mass, 1);
536
+ const w0 = Math.sqrt(k / m);
537
+ const zeta = d / (2 * Math.sqrt(k * m));
538
+ const decay = zeta < 1 ? zeta * w0 : w0 * (zeta - Math.sqrt(zeta * zeta - 1));
539
+ const s = decay > 0 ? 4 / decay : 2;
540
+ return Math.max(50, Math.min(5000, s * 1000));
541
+ }
542
+ function estimateMs(ev, byEid, depth = 0) {
543
+ if (depth > 12)
544
+ return 300;
545
+ if (typeof ev.durationMs === "number" && Number.isFinite(ev.durationMs))
546
+ return ev.durationMs;
547
+ const inner = (a) => {
548
+ if (!isRef(a))
549
+ return 300;
550
+ const e = byEid(a.$anim);
551
+ return e ? estimateMs(e, byEid, depth + 1) : 300;
552
+ };
553
+ const [a0, a1] = ev.args;
554
+ switch (ev.site.fn) {
555
+ case "withTiming":
556
+ return num(cfg(a1).duration, 300);
557
+ case "withSpring":
558
+ return springSettleMs(cfg(a1));
559
+ case "withDelay":
560
+ return num(a0, 0) + inner(a1);
561
+ case "withSequence":
562
+ return ev.args.reduce((s, a) => s + inner(a), 0);
563
+ case "withRepeat": {
564
+ const n = num(a1, 2);
565
+ return inner(a0) * (n > 0 ? n : 10);
566
+ }
567
+ case "withClamp":
568
+ return inner(a1);
569
+ case "withDecay":
570
+ return 1000;
571
+ case "CSS animation": {
572
+ const c = cfg(a0);
573
+ const n = typeof c.iterationCount === "number" ? c.iterationCount : c.iterationCount === "infinite" ? 10 : 1;
574
+ return num(c.delay, 0) + num(c.duration, 0) * n;
575
+ }
576
+ case "CSS transition":
577
+ case "entering":
578
+ case "exiting":
579
+ case "layout":
580
+ return num(cfg(a0).delay, 0) + num(cfg(a0).duration, 300);
581
+ case "withAnimation":
582
+ case "Transaction": {
583
+ const c = cfg(a1);
584
+ const speed = num(c.speed, 1) > 0 ? num(c.speed, 1) : 1;
585
+ const reps = c.repeatCount === "infinite" ? 10 : Math.max(1, num(c.repeatCount, 1));
586
+ return (num(c.delay, 0) + num(c.duration, 300) * reps) / speed;
587
+ }
588
+ default: {
589
+ const c = cfg(a1).duration !== undefined ? cfg(a1) : cfg(a0);
590
+ return typeof c.duration === "number" ? num(c.delay, 0) + c.duration : 300;
591
+ }
592
+ }
593
+ }
594
+ var KEEP = ["duration", "delay", "stiffness", "damping", "mass", "speed", "repeatCount", "iterationCount"];
595
+ function slimArg(a, idOf) {
596
+ "worklet";
597
+ if (typeof a === "number")
598
+ return a;
599
+ if (typeof a === "object" && a !== null || typeof a === "function") {
600
+ const id = idOf(a);
601
+ if (id !== undefined)
602
+ return { $anim: id };
603
+ if (typeof a === "function" || Array.isArray(a))
604
+ return null;
605
+ const o = a;
606
+ const out = {};
607
+ let any = false;
608
+ for (let i = 0;i < KEEP.length; i++) {
609
+ const k = KEEP[i];
610
+ const v = o[k];
611
+ if (typeof v === "number" || v === "infinite") {
612
+ out[k] = v;
613
+ any = true;
614
+ }
615
+ }
616
+ return any ? out : null;
617
+ }
618
+ return null;
619
+ }
620
+
621
+ // src/version.ts
622
+ var SDK_NAME = "@keyframerdev/vitals";
623
+ var SDK_VERSION = "0.1.0";
624
+
625
+ // src/controller.ts
626
+ var STORAGE_KEYS = {
627
+ config: "kfv:config",
628
+ stop: "kfv:stop",
629
+ day: "kfv:day",
630
+ checkpoint: "kfv:checkpoint",
631
+ queue: "kfv:queue"
632
+ };
633
+ var CHECKPOINT_MS = 60000;
634
+ var STOP_MS = 7 * 24 * 3600000;
635
+ var RETRY = { max: 5, bytes: 100 * 1024, ageMs: 7 * 24 * 3600000, delays: [1e4, 60000, 5 * 60000, 30 * 60000] };
636
+ var START_DELAY_MS = 1e4;
637
+ var MAX_EVENTS = 500;
638
+ var UI_EVENT_BASE = 1e9;
639
+ var isRef2 = (v) => !!v && typeof v === "object" && typeof v.$anim === "number";
640
+ function compareVersions(a, b) {
641
+ const pa = a.split(/[.+-]/).map((x) => Number.parseInt(x, 10));
642
+ const pb = b.split(/[.+-]/).map((x) => Number.parseInt(x, 10));
643
+ for (let i = 0;i < 3; i++) {
644
+ const x = Number.isFinite(pa[i]) ? pa[i] : 0;
645
+ const y = Number.isFinite(pb[i]) ? pb[i] : 0;
646
+ if (x !== y)
647
+ return x < y ? -1 : 1;
648
+ }
649
+ return 0;
650
+ }
651
+ var NONCE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
652
+ function makeNonce(random) {
653
+ let s = "";
654
+ for (let i = 0;i < 16; i++)
655
+ s += NONCE_CHARS[Math.floor(random() * NONCE_CHARS.length) % NONCE_CHARS.length];
656
+ return s;
657
+ }
658
+ function localDay(wall) {
659
+ const d = new Date(wall);
660
+ const p = (n) => String(n).padStart(2, "0");
661
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
662
+ }
663
+ async function read(storage, key) {
664
+ if (!storage)
665
+ return null;
666
+ try {
667
+ const raw = await storage.getItem(key);
668
+ return raw ? JSON.parse(raw) : null;
669
+ } catch {
670
+ return null;
671
+ }
672
+ }
673
+ async function write(storage, key, value) {
674
+ if (!storage)
675
+ return;
676
+ try {
677
+ if (value === null)
678
+ await storage.removeItem(key);
679
+ else
680
+ await storage.setItem(key, JSON.stringify(value));
681
+ } catch {}
682
+ }
683
+
684
+ class VitalsController {
685
+ env;
686
+ collector = new Collector;
687
+ gate = new MotionGate;
688
+ opts = null;
689
+ enabled = false;
690
+ run = 0;
691
+ config = null;
692
+ sampled = false;
693
+ nonce = "";
694
+ seq = 0;
695
+ sampleRate = VITALS_DEFAULT_SAMPLE_RATE;
696
+ foreground = true;
697
+ backgroundAt = null;
698
+ dayCount = null;
699
+ events = new Map;
700
+ nextEid = 1;
701
+ uiKnownUntil = -Infinity;
702
+ checkpointTimer = null;
703
+ retryTimer = null;
704
+ retryStep = 0;
705
+ queued = 0;
706
+ configTimer = null;
707
+ meters = null;
708
+ screen = "app";
709
+ reduceMotion = false;
710
+ constructor(env) {
711
+ this.env = env;
712
+ }
713
+ get isSampled() {
714
+ return this.sampled;
715
+ }
716
+ get isEnabled() {
717
+ return this.enabled;
718
+ }
719
+ get session() {
720
+ return { nonce: this.nonce, seq: this.seq, sampleRate: this.sampleRate };
721
+ }
722
+ async start(opts, meters) {
723
+ if (this.enabled)
724
+ this.stop(false);
725
+ this.opts = opts;
726
+ this.meters = meters;
727
+ this.enabled = true;
728
+ const run = ++this.run;
729
+ const storage = opts.storage;
730
+ const stopUntil = await read(storage, STORAGE_KEYS.stop);
731
+ const cached = await read(storage, STORAGE_KEYS.config);
732
+ if (run !== this.run)
733
+ return;
734
+ if (typeof stopUntil === "number" && stopUntil > this.env.wall()) {
735
+ this.log("stopped on this device by the project until " + new Date(stopUntil).toISOString());
736
+ this.enabled = false;
737
+ return;
738
+ }
739
+ this.config = cached?.config ?? null;
740
+ if (!this.allowed()) {
741
+ this.enabled = false;
742
+ return;
743
+ }
744
+ const checkpoint = await read(storage, STORAGE_KEYS.checkpoint);
745
+ if (run !== this.run)
746
+ return;
747
+ if (checkpoint) {
748
+ await write(storage, STORAGE_KEYS.checkpoint, null);
749
+ await this.enqueue(JSON.stringify(checkpoint));
750
+ }
751
+ this.queued = (await read(storage, STORAGE_KEYS.queue) ?? []).length;
752
+ if (run !== this.run)
753
+ return;
754
+ this.retryStep = 0;
755
+ this.scheduleRetry();
756
+ const fresh = cached && this.env.wall() - cached.at < Math.max(60, cached.config.ttlSec) * 1000;
757
+ if (!fresh)
758
+ this.configTimer = this.env.setTimeout(() => void this.fetchConfig(run), START_DELAY_MS);
759
+ await this.beginSession(run);
760
+ }
761
+ async setEnabled(on) {
762
+ if (!on)
763
+ return this.stop(true);
764
+ if (this.enabled || !this.opts || !this.meters)
765
+ return;
766
+ await this.start(this.opts, this.meters);
767
+ }
768
+ async stop(drop) {
769
+ this.run++;
770
+ this.enabled = false;
771
+ this.endSampling();
772
+ this.clearTimers();
773
+ if (drop)
774
+ this.queued = 0;
775
+ if (drop && this.opts?.storage) {
776
+ await write(this.opts.storage, STORAGE_KEYS.checkpoint, null);
777
+ await write(this.opts.storage, STORAGE_KEYS.queue, null);
778
+ }
779
+ }
780
+ async beginSession(run) {
781
+ const now = this.env.now();
782
+ this.nonce = makeNonce(this.env.random);
783
+ this.seq = 0;
784
+ this.sampleRate = this.rate();
785
+ const env = this.opts.environment ?? "production";
786
+ const devOk = env === "production" || !this.config || this.config.collect.development;
787
+ let sampled = devOk && this.sampleRate > 0 && this.env.random() < this.sampleRate;
788
+ if (sampled) {
789
+ const day = (this.env.day ?? localDay)(this.env.wall());
790
+ if (!this.dayCount || this.dayCount.day !== day) {
791
+ const saved = await read(this.opts.storage, STORAGE_KEYS.day);
792
+ if (run !== this.run)
793
+ return;
794
+ this.dayCount = saved && saved.day === day ? saved : { day, n: 0 };
795
+ }
796
+ if (this.dayCount.n >= VITALS_MAX_SESSIONS_PER_DAY)
797
+ sampled = false;
798
+ else {
799
+ this.dayCount.n++;
800
+ await write(this.opts.storage, STORAGE_KEYS.day, this.dayCount);
801
+ if (run !== this.run)
802
+ return;
803
+ }
804
+ }
805
+ this.sampled = sampled;
806
+ if (!sampled)
807
+ return;
808
+ this.collector.clear();
809
+ this.collector.setReduceMotion(this.reduceMotion);
810
+ this.collector.setScreen(this.screen, now, false);
811
+ if (this.foreground)
812
+ this.collector.foreground(true, now);
813
+ this.gate.reset();
814
+ this.events.clear();
815
+ this.meters.install();
816
+ }
817
+ endSampling() {
818
+ if (this.sampled) {
819
+ this.meters?.stop();
820
+ this.meters?.uninstall();
821
+ }
822
+ this.sampled = false;
823
+ this.gate.reset();
824
+ this.collector.clear();
825
+ this.events.clear();
826
+ this.uiKnownUntil = -Infinity;
827
+ if (this.checkpointTimer !== null) {
828
+ this.env.clearTimeout(this.checkpointTimer);
829
+ this.checkpointTimer = null;
830
+ }
831
+ }
832
+ appState(active) {
833
+ if (!this.enabled)
834
+ return;
835
+ const now = this.env.now();
836
+ if (!active && this.foreground) {
837
+ this.foreground = false;
838
+ this.backgroundAt = now;
839
+ if (this.retryTimer !== null) {
840
+ this.env.clearTimeout(this.retryTimer);
841
+ this.retryTimer = null;
842
+ }
843
+ if (this.sampled) {
844
+ this.gate.reset();
845
+ this.meters?.stop();
846
+ this.collector.foreground(false, now);
847
+ this.closeWindow("background");
848
+ }
849
+ } else if (active && !this.foreground) {
850
+ this.foreground = true;
851
+ const away = this.backgroundAt === null ? 0 : now - this.backgroundAt;
852
+ this.backgroundAt = null;
853
+ this.scheduleRetry();
854
+ if (away > VITALS_SESSION_GAP_MS) {
855
+ this.endSampling();
856
+ this.beginSession(this.run);
857
+ } else if (this.sampled)
858
+ this.collector.foreground(true, now);
859
+ }
860
+ }
861
+ setScreen(name) {
862
+ this.screen = name;
863
+ if (this.sampled)
864
+ this.collector.setScreen(name, this.env.now());
865
+ }
866
+ setReduceMotion(on) {
867
+ this.reduceMotion = on && !!this.config?.collect.reduceMotion;
868
+ this.collector.setReduceMotion(this.reduceMotion);
869
+ }
870
+ eid() {
871
+ return this.nextEid++;
872
+ }
873
+ anim(site, args, eid, thread, t = this.env.now()) {
874
+ if (!this.sampled || !this.foreground)
875
+ return;
876
+ const ev = { site: { fn: site.fn }, args };
877
+ this.events.set(eid, ev);
878
+ if (this.events.size > MAX_EVENTS)
879
+ this.events.delete(this.events.keys().next().value);
880
+ const ms = estimateMs(ev, (id) => this.events.get(id));
881
+ for (const a of args)
882
+ if (isRef2(a))
883
+ this.collector.consume(a.$anim);
884
+ this.collector.animStart(site, t, ms, thread, eid);
885
+ this.motion(t + ms, t);
886
+ }
887
+ layout(site, kind, builder, t = this.env.now()) {
888
+ const read2 = (k) => {
889
+ try {
890
+ const v = builder?.[k];
891
+ return typeof v === "number" && Number.isFinite(v) ? v : undefined;
892
+ } catch {
893
+ return;
894
+ }
895
+ };
896
+ this.anim({ ...site, fn: kind }, [{ duration: read2("durationV") ?? 300, delay: read2("delayV") ?? 0 }], this.eid(), "js", t);
897
+ }
898
+ scroll(t = this.env.now()) {
899
+ if (!this.sampled || !this.foreground)
900
+ return;
901
+ this.collector.scroll(t);
902
+ this.motion(t, t);
903
+ }
904
+ touch(down, t = this.env.now()) {
905
+ if (!this.sampled || !this.foreground)
906
+ return;
907
+ this.collector.touch(down, t);
908
+ if (this.gate.touch(down, t))
909
+ this.opened(t);
910
+ this.keepUi(t);
911
+ }
912
+ motion(end, now) {
913
+ if (this.gate.note(end, now))
914
+ this.opened(now);
915
+ this.keepUi(now);
916
+ this.checkWindow(now);
917
+ }
918
+ opened(now) {
919
+ this.collector.measure(true, now);
920
+ this.meters?.start();
921
+ this.scheduleCheckpoint();
922
+ }
923
+ keepUi(now) {
924
+ const closes = this.gate.closesAt();
925
+ const want = closes === Infinity ? now + 2000 : closes;
926
+ if (want > this.uiKnownUntil + 50 && want > now) {
927
+ this.uiKnownUntil = want;
928
+ this.meters?.extendUi(want - now);
929
+ }
930
+ }
931
+ jsFrame(t, dt, budget) {
932
+ if (!this.sampled || !this.foreground)
933
+ return false;
934
+ if (dt > 0)
935
+ this.collector.frame("js", t, dt, budget);
936
+ if (!this.gate.open(t)) {
937
+ this.collector.measure(false, Math.min(t, this.gate.until));
938
+ return false;
939
+ }
940
+ this.keepUi(t);
941
+ this.checkWindow(t);
942
+ return true;
943
+ }
944
+ uiFrames(buf) {
945
+ if (!this.sampled)
946
+ return;
947
+ for (let i = 0;i + 2 < buf.length; i += 3)
948
+ this.collector.frame("ui", buf[i], buf[i + 1], buf[i + 2]);
949
+ }
950
+ checkWindow(now) {
951
+ if (this.collector.foregroundAt(now) >= VITALS_WINDOW_MS)
952
+ this.closeWindow("window");
953
+ }
954
+ report(ended, now = this.env.now()) {
955
+ if (!this.opts)
956
+ return null;
957
+ const snap = this.collector.snapshot(now);
958
+ if (!hasData(snap))
959
+ return null;
960
+ return encodeReport(snap, this.context(ended));
961
+ }
962
+ async closeWindow(ended) {
963
+ if (!this.sampled)
964
+ return;
965
+ const now = this.env.now();
966
+ const report = this.report(ended, now);
967
+ this.collector.reset(now);
968
+ if (this.checkpointTimer !== null) {
969
+ this.env.clearTimeout(this.checkpointTimer);
970
+ this.checkpointTimer = null;
971
+ }
972
+ const storage = this.opts?.storage;
973
+ if (!report) {
974
+ await write(storage, STORAGE_KEYS.checkpoint, null);
975
+ return;
976
+ }
977
+ this.seq++;
978
+ await write(storage, STORAGE_KEYS.checkpoint, report);
979
+ await this.send(JSON.stringify(report));
980
+ await write(storage, STORAGE_KEYS.checkpoint, null);
981
+ }
982
+ scheduleCheckpoint() {
983
+ if (this.checkpointTimer !== null || !this.opts?.storage)
984
+ return;
985
+ this.checkpointTimer = this.env.setTimeout(() => {
986
+ this.checkpointTimer = null;
987
+ this.checkpoint();
988
+ }, CHECKPOINT_MS);
989
+ }
990
+ async checkpoint() {
991
+ if (!this.sampled || !this.opts?.storage)
992
+ return;
993
+ const report = this.report("unknown");
994
+ if (report)
995
+ await write(this.opts.storage, STORAGE_KEYS.checkpoint, report);
996
+ }
997
+ context(ended) {
998
+ const o = this.opts;
999
+ const d = o.device ?? {};
1000
+ const collect = this.config?.collect;
1001
+ const memoryGb = d.memoryGb ?? (d.memoryBytes ? Math.round(d.memoryBytes / 1024 ** 3 * 10) / 10 : undefined);
1002
+ const device = { os: this.env.platform.os, osMajor: this.env.platform.osMajor };
1003
+ if (d.model && collect?.deviceModel !== false)
1004
+ device.model = String(d.model).slice(0, 64);
1005
+ if (d.tier)
1006
+ device.tier = d.tier;
1007
+ if (memoryGb)
1008
+ device.memoryGb = memoryGb;
1009
+ const app = {
1010
+ ...o.appId ? { id: o.appId } : {},
1011
+ version: o.appVersion.slice(0, 64),
1012
+ ...o.build ? { build: String(o.build).slice(0, 64) } : {},
1013
+ platform: "react-native",
1014
+ framework: "React Native",
1015
+ environment: o.environment ?? "production"
1016
+ };
1017
+ return {
1018
+ app,
1019
+ sdk: { name: SDK_NAME, version: SDK_VERSION },
1020
+ device,
1021
+ display: d.displayHz ? { hz: d.displayHz } : undefined,
1022
+ collectReduceMotion: !!collect?.reduceMotion,
1023
+ session: { nonce: this.nonce, seq: this.seq, sampleRate: this.sampleRate, ended }
1024
+ };
1025
+ }
1026
+ endpoint() {
1027
+ return (this.opts?.endpoint ?? VITALS_ENDPOINT).replace(/\/+$/, "");
1028
+ }
1029
+ async send(body) {
1030
+ const run = this.run;
1031
+ if (await this.sendOnce(body) === "retry" && run === this.run)
1032
+ await this.enqueue(body);
1033
+ }
1034
+ async enqueue(body) {
1035
+ const storage = this.opts?.storage;
1036
+ if (!storage)
1037
+ return;
1038
+ const queue = await read(storage, STORAGE_KEYS.queue) ?? [];
1039
+ queue.push({ body, at: this.env.wall() });
1040
+ const kept = this.trim(queue);
1041
+ await write(storage, STORAGE_KEYS.queue, kept);
1042
+ this.queued = kept.length;
1043
+ this.scheduleRetry();
1044
+ }
1045
+ trim(queue) {
1046
+ const wall = this.env.wall();
1047
+ const out = [];
1048
+ let bytes = 0;
1049
+ for (let i = queue.length - 1;i >= 0 && out.length < RETRY.max; i--) {
1050
+ const q = queue[i];
1051
+ if (wall - q.at > RETRY.ageMs || typeof q.body !== "string")
1052
+ continue;
1053
+ if (bytes + q.body.length > RETRY.bytes)
1054
+ continue;
1055
+ bytes += q.body.length;
1056
+ out.unshift(q);
1057
+ }
1058
+ return out;
1059
+ }
1060
+ scheduleRetry() {
1061
+ if (this.retryTimer !== null || !this.queued || !this.foreground || !this.enabled || !this.opts?.storage)
1062
+ return;
1063
+ if (this.retryStep >= RETRY.delays.length)
1064
+ return;
1065
+ const delay = RETRY.delays[this.retryStep];
1066
+ this.retryTimer = this.env.setTimeout(() => {
1067
+ this.retryTimer = null;
1068
+ this.retryStep++;
1069
+ this.retry();
1070
+ }, delay);
1071
+ }
1072
+ async retry() {
1073
+ const storage = this.opts?.storage;
1074
+ if (!storage || !this.enabled)
1075
+ return;
1076
+ const run = this.run;
1077
+ const queue = this.trim(await read(storage, STORAGE_KEYS.queue) ?? []);
1078
+ await write(storage, STORAGE_KEYS.queue, null);
1079
+ this.queued = 0;
1080
+ if (!queue.length)
1081
+ return;
1082
+ const left = [];
1083
+ for (const q of queue) {
1084
+ if (run !== this.run)
1085
+ return;
1086
+ const r = await this.sendOnce(q.body);
1087
+ if (r === "retry")
1088
+ left.push(q);
1089
+ }
1090
+ if (left.length && run === this.run) {
1091
+ const now = await read(storage, STORAGE_KEYS.queue) ?? [];
1092
+ const kept = this.trim([...left, ...now]);
1093
+ await write(storage, STORAGE_KEYS.queue, kept);
1094
+ this.queued = kept.length;
1095
+ this.scheduleRetry();
1096
+ }
1097
+ }
1098
+ async sendOnce(body) {
1099
+ try {
1100
+ const res = await this.env.fetch(`${this.endpoint()}/v1/reports`, {
1101
+ method: "POST",
1102
+ headers: { Authorization: `Bearer ${this.opts.key}`, "Content-Type": "application/json" },
1103
+ body
1104
+ });
1105
+ if (res.status === 200) {
1106
+ const json = await res.json().catch(() => null);
1107
+ if (json?.config)
1108
+ await this.applyConfig(json.config);
1109
+ return "done";
1110
+ }
1111
+ if (res.status === 401 || res.status === 410) {
1112
+ await this.killSwitch();
1113
+ return "done";
1114
+ }
1115
+ if (res.status === 429 || res.status >= 500)
1116
+ return "retry";
1117
+ this.log(`report refused (${res.status})`);
1118
+ return "done";
1119
+ } catch {
1120
+ return "retry";
1121
+ }
1122
+ }
1123
+ async fetchConfig(run) {
1124
+ this.configTimer = null;
1125
+ if (run !== this.run || !this.opts)
1126
+ return;
1127
+ try {
1128
+ const res = await this.env.fetch(`${this.endpoint()}/v1/config/${encodeURIComponent(this.opts.key)}`, { method: "GET", headers: {} });
1129
+ if (run !== this.run)
1130
+ return;
1131
+ if (res.status === 200) {
1132
+ const config = await res.json();
1133
+ if (config && typeof config === "object" && typeof config.sampleRate === "number")
1134
+ await this.applyConfig(config);
1135
+ } else if (res.status === 401 || res.status === 410)
1136
+ await this.killSwitch();
1137
+ } catch {}
1138
+ }
1139
+ async applyConfig(config) {
1140
+ this.config = config;
1141
+ await write(this.opts?.storage, STORAGE_KEYS.config, { config, at: this.env.wall() });
1142
+ if (config.stop)
1143
+ return this.killSwitch();
1144
+ if (!this.allowed()) {
1145
+ await this.stop(false);
1146
+ return;
1147
+ }
1148
+ if (!config.collect.reduceMotion)
1149
+ this.setReduceMotion(false);
1150
+ }
1151
+ async killSwitch() {
1152
+ this.log("turned off on this device for 7 days by the project");
1153
+ const storage = this.opts?.storage;
1154
+ await this.stop(true);
1155
+ await write(storage, STORAGE_KEYS.stop, this.env.wall() + STOP_MS);
1156
+ }
1157
+ allowed() {
1158
+ const c = this.config;
1159
+ if (!c)
1160
+ return true;
1161
+ if (!c.enabled)
1162
+ return false;
1163
+ const min = c.minSdk?.["react-native"];
1164
+ if (min && compareVersions(SDK_VERSION, min) < 0) {
1165
+ this.log(`this SDK version (${SDK_VERSION}) is below the project's minimum (${min}): off`);
1166
+ return false;
1167
+ }
1168
+ return true;
1169
+ }
1170
+ rate() {
1171
+ const r = this.config ? this.config.sampleRate : this.opts?.sampleRate ?? VITALS_DEFAULT_SAMPLE_RATE;
1172
+ return Number.isFinite(r) ? Math.max(0, Math.min(1, r)) : VITALS_DEFAULT_SAMPLE_RATE;
1173
+ }
1174
+ clearTimers() {
1175
+ for (const k of ["checkpointTimer", "retryTimer", "configTimer"]) {
1176
+ if (this[k] !== null) {
1177
+ this.env.clearTimeout(this[k]);
1178
+ this[k] = null;
1179
+ }
1180
+ }
1181
+ }
1182
+ log(message) {
1183
+ this.env.log?.(`[keyframer vitals] ${message}`);
1184
+ }
1185
+ }
1186
+
1187
+ // src/hook.ts
1188
+ function createJsHook(controller, onMotion) {
1189
+ const ids = new WeakMap;
1190
+ const isObj = (r) => typeof r === "object" && r !== null || typeof r === "function";
1191
+ return {
1192
+ anim(fn, site, args, via, viaSite) {
1193
+ const r = via ? via(fn, viaSite, ...args) : fn(...args);
1194
+ try {
1195
+ const eid = controller.eid();
1196
+ const slim = args.map((a) => slimArg(a, (x) => ids.get(x)));
1197
+ if (isObj(r))
1198
+ ids.set(r, eid);
1199
+ controller.anim(site, slim, eid, "js");
1200
+ onMotion?.();
1201
+ } catch {}
1202
+ return r;
1203
+ },
1204
+ scroll() {
1205
+ controller.scroll();
1206
+ },
1207
+ layout(site, kind, builder) {
1208
+ controller.layout(site, kind, builder);
1209
+ }
1210
+ };
1211
+ }
1212
+
1213
+ // src/frameMeter.ts
1214
+ function createFrameMeter(thread, windowMs = 250) {
1215
+ "worklet";
1216
+ return { thread, windowMs, interval: 1000 / 60, last: -1, windowStart: -1, frames: 0, dropped: 0, fast: 0, mid: 0, stats: [], drops: [] };
1217
+ }
1218
+ function tickFrameMeter(m, t) {
1219
+ "worklet";
1220
+ const last = m.last;
1221
+ m.last = t;
1222
+ if (last < 0) {
1223
+ m.windowStart = t;
1224
+ return;
1225
+ }
1226
+ const dt = t - last;
1227
+ if (dt <= 0)
1228
+ return;
1229
+ if (dt < 10.5)
1230
+ m.fast += 1;
1231
+ else if (dt < 13)
1232
+ m.mid += 1;
1233
+ const lost = Math.max(0, Math.round(dt / m.interval) - 1);
1234
+ if (lost > 0) {
1235
+ m.dropped += lost;
1236
+ m.drops.push({ t, thread: m.thread, ms: Math.round(dt * 10) / 10, dropped: lost });
1237
+ }
1238
+ m.frames += 1;
1239
+ const span = t - m.windowStart;
1240
+ if (span >= m.windowMs) {
1241
+ m.stats.push({ t, thread: m.thread, fps: Math.round(m.frames * 1000 / span), dropped: m.dropped });
1242
+ const n = m.frames;
1243
+ m.interval = m.fast * 2 > n ? 1000 / 120 : (m.fast + m.mid) * 2 > n ? 1000 / 90 : 1000 / 60;
1244
+ m.frames = 0;
1245
+ m.dropped = 0;
1246
+ m.fast = 0;
1247
+ m.mid = 0;
1248
+ m.windowStart = t;
1249
+ }
1250
+ }
1251
+ function createFieldMeter(thread) {
1252
+ "worklet";
1253
+ return { m: createFrameMeter(thread), buf: [] };
1254
+ }
1255
+ function tickFieldMeter(f, t) {
1256
+ "worklet";
1257
+ const last = f.m.last;
1258
+ const interval = f.m.interval;
1259
+ tickFrameMeter(f.m, t);
1260
+ f.m.stats.length = 0;
1261
+ f.m.drops.length = 0;
1262
+ if (last >= 0 && t > last)
1263
+ f.buf.push(t, t - last, interval);
1264
+ return f.buf.length >= 90;
1265
+ }
1266
+
1267
+ // src/native.ts
1268
+ var perf = globalThis.performance;
1269
+ var now = () => perf && typeof perf.now === "function" ? perf.now() : Date.now();
1270
+ function osMajor() {
1271
+ const v = Platform.Version;
1272
+ if (Platform.OS === "android") {
1273
+ const api = typeof v === "number" ? v : Number.parseInt(String(v), 10);
1274
+ if (!Number.isFinite(api))
1275
+ return 0;
1276
+ if (api >= 29)
1277
+ return api - 20;
1278
+ return { 28: 9, 27: 8, 26: 8, 25: 7, 24: 7, 23: 6 }[api] ?? 0;
1279
+ }
1280
+ const major = Number.parseInt(String(v), 10);
1281
+ return Number.isFinite(major) ? major : 0;
1282
+ }
1283
+ var env = {
1284
+ now,
1285
+ wall: () => Date.now(),
1286
+ random: Math.random,
1287
+ fetch: (url, init) => fetch(url, init),
1288
+ setTimeout: (fn, ms) => setTimeout(fn, ms),
1289
+ clearTimeout: (h) => clearTimeout(h),
1290
+ platform: { os: Platform.OS === "ios" ? "ios" : "android", osMajor: osMajor() },
1291
+ log: (m) => console.warn(m)
1292
+ };
1293
+ var controller = new VitalsController(env);
1294
+ var jsHook = createJsHook(controller, () => checkScreen());
1295
+ var uiOffset = Infinity;
1296
+ function onUiFrames(buf, lastTs) {
1297
+ const off = now() - lastTs;
1298
+ if (off < uiOffset)
1299
+ uiOffset = off;
1300
+ for (let i = 0;i < buf.length; i += 3)
1301
+ buf[i] = buf[i] + uiOffset;
1302
+ controller.uiFrames(buf);
1303
+ }
1304
+ function uiTime(ts) {
1305
+ return ts >= 0 && Number.isFinite(uiOffset) ? ts + uiOffset : now();
1306
+ }
1307
+ function onUiAnim(site, slim, eid, ts) {
1308
+ controller.anim(site, slim, eid, "ui", uiTime(ts));
1309
+ checkScreen();
1310
+ }
1311
+ function onUiScroll(ts) {
1312
+ controller.scroll(uiTime(ts));
1313
+ }
1314
+ function uiKeepAlive(ms) {
1315
+ "worklet";
1316
+ const g = globalThis;
1317
+ const running = g.__kfvLoop;
1318
+ if (running) {
1319
+ if (running.lastTs >= 0)
1320
+ running.until = Math.max(running.until, running.lastTs + ms);
1321
+ else
1322
+ running.pending = Math.max(running.pending, ms);
1323
+ return;
1324
+ }
1325
+ const raf = g.requestAnimationFrame;
1326
+ if (!raf)
1327
+ return;
1328
+ const loop = { f: createFieldMeter("ui"), until: -1, pending: ms, lastTs: -1 };
1329
+ if (g.__kfvInterval)
1330
+ loop.f.m.interval = g.__kfvInterval;
1331
+ g.__kfvLoop = loop;
1332
+ function frame(ts) {
1333
+ const gg = globalThis;
1334
+ if (gg.__kfvLoop !== loop)
1335
+ return;
1336
+ if (loop.pending > 0) {
1337
+ loop.until = Math.max(loop.until, ts + loop.pending);
1338
+ loop.pending = 0;
1339
+ }
1340
+ loop.lastTs = ts;
1341
+ const full = tickFieldMeter(loop.f, ts);
1342
+ gg.__kfvInterval = loop.f.m.interval;
1343
+ if (ts > loop.until) {
1344
+ gg.__kfvLoop = null;
1345
+ scheduleOnRN(onUiFrames, loop.f.buf.splice(0), ts);
1346
+ return;
1347
+ }
1348
+ if (full)
1349
+ scheduleOnRN(onUiFrames, loop.f.buf.splice(0), ts);
1350
+ raf(frame);
1351
+ }
1352
+ raf(frame);
1353
+ }
1354
+ function installUi(on, base) {
1355
+ scheduleOnUI((enable, first) => {
1356
+ "worklet";
1357
+ const g = globalThis;
1358
+ if (!enable) {
1359
+ g.__KF_VITALS__ = null;
1360
+ g.__kfvLoop = null;
1361
+ return;
1362
+ }
1363
+ const ids = new WeakMap;
1364
+ let next = first;
1365
+ let lastScroll = -1e9;
1366
+ g.__KF_VITALS__ = {
1367
+ anim(fn, site, args, via, viaSite) {
1368
+ const r = via ? via(fn, viaSite, ...args) : fn(...args);
1369
+ try {
1370
+ const eid = next++;
1371
+ const slim = [];
1372
+ for (let i = 0;i < args.length; i++)
1373
+ slim.push(slimArg(args[i], (x) => ids.get(x)));
1374
+ if (typeof r === "object" && r !== null || typeof r === "function")
1375
+ ids.set(r, eid);
1376
+ uiKeepAlive(500);
1377
+ const loop = globalThis.__kfvLoop;
1378
+ scheduleOnRN(onUiAnim, site, slim, eid, loop && loop.lastTs >= 0 ? loop.lastTs : -1);
1379
+ } catch {}
1380
+ return r;
1381
+ },
1382
+ scroll() {
1383
+ uiKeepAlive(500);
1384
+ const loop = globalThis.__kfvLoop;
1385
+ const t = loop && loop.lastTs >= 0 ? loop.lastTs : -1;
1386
+ if (t < 0 || t - lastScroll > 100) {
1387
+ lastScroll = t;
1388
+ scheduleOnRN(onUiScroll, t);
1389
+ }
1390
+ }
1391
+ };
1392
+ }, on, base);
1393
+ }
1394
+ function stopUi() {
1395
+ scheduleOnUI(() => {
1396
+ "worklet";
1397
+ globalThis.__kfvLoop = null;
1398
+ });
1399
+ }
1400
+ function extendUi(ms) {
1401
+ scheduleOnUI((m) => {
1402
+ "worklet";
1403
+ uiKeepAlive(m);
1404
+ }, ms);
1405
+ }
1406
+ var jsMeter = createFieldMeter("js");
1407
+ var jsRunning = false;
1408
+ function jsTick() {
1409
+ if (!jsRunning)
1410
+ return;
1411
+ const t = now();
1412
+ const last = jsMeter.m.last;
1413
+ const interval = jsMeter.m.interval;
1414
+ tickFrameMeter(jsMeter.m, t);
1415
+ jsMeter.m.stats.length = 0;
1416
+ jsMeter.m.drops.length = 0;
1417
+ if (controller.jsFrame(t, last >= 0 ? t - last : 0, interval))
1418
+ requestAnimationFrame(jsTick);
1419
+ else
1420
+ jsRunning = false;
1421
+ }
1422
+ var uiInstalled = false;
1423
+ var meters = {
1424
+ install() {
1425
+ globalThis.__KF_VITALS__ = jsHook;
1426
+ try {
1427
+ installUi(true, UI_EVENT_BASE);
1428
+ uiInstalled = true;
1429
+ } catch {
1430
+ uiInstalled = false;
1431
+ }
1432
+ },
1433
+ uninstall() {
1434
+ globalThis.__KF_VITALS__ = null;
1435
+ if (uiInstalled)
1436
+ installUi(false, 0);
1437
+ uiInstalled = false;
1438
+ },
1439
+ start() {
1440
+ if (!jsRunning) {
1441
+ jsRunning = true;
1442
+ jsMeter.m.last = -1;
1443
+ requestAnimationFrame(jsTick);
1444
+ }
1445
+ },
1446
+ extendUi(ms) {
1447
+ if (uiInstalled)
1448
+ extendUi(ms);
1449
+ },
1450
+ stop() {
1451
+ jsRunning = false;
1452
+ if (uiInstalled)
1453
+ stopUi();
1454
+ }
1455
+ };
1456
+ var options = null;
1457
+ var subscriptions = [];
1458
+ var manualScreen = false;
1459
+ function routeName() {
1460
+ const ref = options?.navigationRef;
1461
+ if (!ref)
1462
+ return null;
1463
+ try {
1464
+ if (ref.isReady && !ref.isReady())
1465
+ return null;
1466
+ const route = ref.getCurrentRoute();
1467
+ if (!route?.name)
1468
+ return null;
1469
+ return options?.screenName ? options.screenName(route) : route.name;
1470
+ } catch {
1471
+ return null;
1472
+ }
1473
+ }
1474
+ var lastRoute = null;
1475
+ function checkScreen() {
1476
+ if (manualScreen)
1477
+ return;
1478
+ const name = routeName();
1479
+ if (name === null || name === lastRoute)
1480
+ return;
1481
+ lastRoute = name;
1482
+ controller.setScreen(screenName(name));
1483
+ }
1484
+ function unsubscribeAll() {
1485
+ for (const off of subscriptions) {
1486
+ try {
1487
+ off();
1488
+ } catch {}
1489
+ }
1490
+ subscriptions = [];
1491
+ }
1492
+ async function startVitals(opts) {
1493
+ unsubscribeAll();
1494
+ options = opts;
1495
+ lastRoute = null;
1496
+ manualScreen = false;
1497
+ const environment = opts.environment ?? (typeof __DEV__ !== "undefined" && __DEV__ ? "development" : "production");
1498
+ const appState = AppState.addEventListener("change", (s) => controller.appState(s !== "background"));
1499
+ subscriptions.push(() => appState.remove());
1500
+ if (opts.navigationRef) {
1501
+ const off = opts.navigationRef.addListener("state", checkScreen);
1502
+ subscriptions.push(() => typeof off === "function" ? off() : off && typeof off === "object" ? off.remove() : undefined);
1503
+ }
1504
+ try {
1505
+ const rm = AccessibilityInfo.addEventListener("reduceMotionChanged", (on) => controller.setReduceMotion(on));
1506
+ subscriptions.push(() => rm.remove());
1507
+ AccessibilityInfo.isReduceMotionEnabled().then((on) => controller.setReduceMotion(on), () => {
1508
+ return;
1509
+ });
1510
+ } catch {}
1511
+ checkScreen();
1512
+ await controller.start({ ...opts, environment }, meters);
1513
+ checkScreen();
1514
+ }
1515
+ async function setVitalsEnabled(enabled) {
1516
+ if (!enabled) {
1517
+ unsubscribeAll();
1518
+ touchDown = false;
1519
+ await controller.setEnabled(false);
1520
+ } else if (options && !controller.isEnabled)
1521
+ await startVitals(options);
1522
+ }
1523
+ function setVitalsScreen(name) {
1524
+ manualScreen = true;
1525
+ controller.setScreen(screenName(name));
1526
+ }
1527
+ var touchDown = false;
1528
+ function vitalsTouch(down, activeTouches) {
1529
+ if (down && !touchDown) {
1530
+ touchDown = true;
1531
+ controller.touch(true);
1532
+ } else if (!down && touchDown && activeTouches === 0) {
1533
+ touchDown = false;
1534
+ controller.touch(false);
1535
+ }
1536
+ }
1537
+
1538
+ // src/index.ts
1539
+ var touches = (e) => e.nativeEvent?.touches?.length ?? 0;
1540
+ function VitalsRoot({ children, style }) {
1541
+ return createElement(View, {
1542
+ style: style ?? { flex: 1 },
1543
+ onTouchStart: () => vitalsTouch(true, 1),
1544
+ onTouchEnd: (e) => vitalsTouch(false, touches(e)),
1545
+ onTouchCancel: (e) => vitalsTouch(false, touches(e))
1546
+ }, children);
1547
+ }
1548
+ export {
1549
+ startVitals,
1550
+ setVitalsScreen,
1551
+ setVitalsEnabled,
1552
+ VitalsRoot,
1553
+ SDK_VERSION
1554
+ };