@keepsnappy/react-native 0.1.0-alpha.12 → 0.1.0-alpha.14

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.
@@ -80,7 +80,7 @@ interface SpoolNative {
80
80
  const nativeArtifacts = (): ArtifactUploadNative | null => {
81
81
  const native = (
82
82
  globalThis as unknown as { expo?: { modules?: Record<string, unknown> } }
83
- ).expo?.modules?.PerfLab as Partial<ArtifactUploadNative> | undefined;
83
+ ).expo?.modules?.KeepSnappy as Partial<ArtifactUploadNative> | undefined;
84
84
  if (!native || typeof native.artifactUpload !== 'function') {
85
85
  return null;
86
86
  }
@@ -88,12 +88,12 @@ const nativeArtifacts = (): ArtifactUploadNative | null => {
88
88
  return { artifactUpload: (...args) => artifactUpload.apply(native, args) };
89
89
  };
90
90
 
91
- // The journal file next to the log (perf-lab/push-spool.jsonl). Without these
91
+ // The journal file next to the log (keepsnappy/push-spool.jsonl). Without these
92
92
  // native functions (an older binary) the queue is memory-only, as before.
93
93
  const nativeSpool = (): PushSpool | null => {
94
94
  const native = (
95
95
  globalThis as unknown as { expo?: { modules?: Record<string, unknown> } }
96
- ).expo?.modules?.PerfLab as SpoolNative | undefined;
96
+ ).expo?.modules?.KeepSnappy as SpoolNative | undefined;
97
97
  if (
98
98
  !native ||
99
99
  typeof native.spoolRead !== 'function' ||
package/js/sdk/push.ts CHANGED
@@ -95,9 +95,13 @@ export interface ClockSample {
95
95
 
96
96
  const MAX_CLOCK_SAMPLES = 16;
97
97
 
98
- const serverClock = async (res: { json?: () => Promise<unknown> }) => {
98
+ // `bound` rejects when the request's time is up: a native fetch resolves at the headers, and its body can stall with the
99
+ // abort signal ignored.
100
+ const serverClock = async (res: { json?: () => Promise<unknown> }, bound: Promise<never>) => {
99
101
  try {
100
- const body = (await res.json?.()) as { clock?: { recv_ms?: unknown; send_ms?: unknown } } | undefined;
102
+ const body = (await Promise.race([res.json?.(), bound])) as
103
+ | { clock?: { recv_ms?: unknown; send_ms?: unknown } }
104
+ | undefined;
101
105
  const t1 = body?.clock?.recv_ms;
102
106
  const t2 = body?.clock?.send_ms;
103
107
  return typeof t1 === 'number' && typeof t2 === 'number' ? { t1, t2 } : null;
@@ -240,6 +244,9 @@ export const createPushClient = (deps: PushDeps) => {
240
244
  const endpoint = `${deps.url.replace(/\/+$/, '')}/v1/windows`;
241
245
  // Set by flushAll: the time (deps.now) every request of that flush must be over by.
242
246
  let flushLimit: number | null = null;
247
+ // Batches of a run (with a token) the ingest refused for good: their lines are gone, so the run lacks data.
248
+ let refusedRunBatches = 0;
249
+ let lastRefusedStatus = 0;
243
250
  // Rejects after `ms`; a hard bound for a fetch that ignores its abort signal.
244
251
  const timeoutAfter = (ms: number) => {
245
252
  let handle: unknown;
@@ -603,7 +610,12 @@ export const createPushClient = (deps: PushDeps) => {
603
610
  sending = true;
604
611
  try {
605
612
  if (!device) {
606
- device = await deps.getDevice();
613
+ const bound = timeoutAfter(flushLimit !== null ? Math.max(1, Math.min(timeoutMs, flushLimit - deps.now())) : timeoutMs);
614
+ try {
615
+ device = await Promise.race([deps.getDevice(), bound.promise]);
616
+ } finally {
617
+ bound.clear();
618
+ }
607
619
  }
608
620
  if (!batch.id) {
609
621
  // Names exactly these lines (process, first and last sequence, count),
@@ -630,10 +642,10 @@ export const createPushClient = (deps: PushDeps) => {
630
642
  });
631
643
  const headers: Record<string, string> = {
632
644
  'content-type': 'application/json',
633
- 'x-perflab-key': deps.key,
645
+ 'x-keepsnappy-key': deps.key,
634
646
  };
635
647
  if (runToken) {
636
- headers['x-perflab-run-token'] = runToken;
648
+ headers['x-keepsnappy-run-token'] = runToken;
637
649
  }
638
650
  const budget = flushLimit !== null ? Math.max(1, Math.min(timeoutMs, flushLimit - deps.now())) : timeoutMs;
639
651
  const controller =
@@ -641,7 +653,8 @@ export const createPushClient = (deps: PushDeps) => {
641
653
  const abort = controller
642
654
  ? deps.setTimer(() => controller.abort(), budget)
643
655
  : null;
644
- const hard = flushLimit !== null ? timeoutAfter(budget) : null;
656
+ // Every request, idle or flush: a flush link waits for an idle send still in flight.
657
+ const hard = timeoutAfter(budget);
645
658
  let status = 0;
646
659
  try {
647
660
  const t0 = deps.now();
@@ -651,16 +664,16 @@ export const createPushClient = (deps: PushDeps) => {
651
664
  body,
652
665
  ...(controller ? { signal: controller.signal } : {}),
653
666
  });
654
- const res = hard ? await Promise.race([pending, hard.promise]) : await pending;
667
+ const res = await Promise.race([pending, hard.promise]);
655
668
  const t3 = deps.now();
656
669
  status = res.status;
670
+ // A fresh batch (202) stored the samples it carried, whether or not its body arrives; a duplicate's go again.
671
+ if (status === 202) {
672
+ dropClock(reported);
673
+ }
657
674
  if (runToken && (status === 200 || status === 202)) {
658
- const server = await serverClock(res);
675
+ const server = await serverClock(res, hard.promise);
659
676
  if (server) {
660
- // Stored only by a fresh batch; a duplicate's samples go again.
661
- if (status === 202) {
662
- dropClock(reported);
663
- }
664
677
  keepClock(runToken, { t0, ...server, t3 });
665
678
  }
666
679
  }
@@ -668,7 +681,7 @@ export const createPushClient = (deps: PushDeps) => {
668
681
  if (abort !== null) {
669
682
  deps.clearTimer(abort);
670
683
  }
671
- hard?.clear();
684
+ hard.clear();
672
685
  }
673
686
  stats.lastStatus = status;
674
687
  if (status === 200 || status === 202) {
@@ -685,6 +698,10 @@ export const createPushClient = (deps: PushDeps) => {
685
698
  } else if (isPermanent(status)) {
686
699
  stats.batchesRejected++;
687
700
  stats.lastError = `rejected ${status}, batch ${batch.id} dropped`;
701
+ if (batch.token) {
702
+ refusedRunBatches++;
703
+ lastRefusedStatus = status;
704
+ }
688
705
  retryBatch = null;
689
706
  retryAt = null;
690
707
  attempts = 0;
@@ -762,11 +779,23 @@ export const createPushClient = (deps: PushDeps) => {
762
779
  resume();
763
780
  const sentBefore = stats.batchesSent;
764
781
  const linesBefore = stats.linesSent;
782
+ const refusedBefore = refusedRunBatches;
765
783
  let failures = 0;
766
784
  flushLimit = deadline;
767
785
  try {
768
786
  if (inflight) {
769
- await inflight;
787
+ if (deadline === null) {
788
+ await inflight;
789
+ } else {
790
+ const bound = timeoutAfter(Math.max(0, deadline - deps.now()));
791
+ try {
792
+ await Promise.race([inflight, bound.promise]);
793
+ } catch {
794
+ // the deadline passed first; the loop below starts nothing more
795
+ } finally {
796
+ bound.clear();
797
+ }
798
+ }
770
799
  }
771
800
  while (
772
801
  (queue.length > 0 || retryBatch) &&
@@ -786,15 +815,21 @@ export const createPushClient = (deps: PushDeps) => {
786
815
  }
787
816
  }
788
817
  }
789
- const ok = queue.length === 0 && !retryBatch;
790
- if (ok) {
818
+ const sentAll = queue.length === 0 && !retryBatch;
819
+ if (sentAll) {
791
820
  await sendClock(deadline);
792
821
  }
822
+ const refused = refusedRunBatches - refusedBefore;
823
+ const ok = sentAll && refused === 0;
793
824
  return {
794
825
  ok,
795
826
  batches: stats.batchesSent - sentBefore,
796
827
  lines: stats.linesSent - linesBefore,
797
- error: ok ? null : (stats.lastError ?? 'flush deadline'),
828
+ error: ok
829
+ ? null
830
+ : !sentAll
831
+ ? (stats.lastError ?? 'flush deadline')
832
+ : `rejected ${lastRefusedStatus}: ${refused} ${refused === 1 ? 'batch' : 'batches'} of the run dropped`,
798
833
  };
799
834
  } finally {
800
835
  flushLimit = null;
package/js/sdk/sampler.ts CHANGED
@@ -84,7 +84,7 @@ export const SAMPLER_MS_MIN = 250;
84
84
  export const SAMPLER_MS_MAX = 60_000;
85
85
  // Background interval between native samples. 4 s: Arek 2026-10-16 (followups, RULING ~09:00); windows get their own
86
86
  // samples at open and close, so the background ones only serve peak memory in long windows and the heat trend.
87
- // The native samplers keep the same number as their unconfigured fallback (PerfLabSampler.swift / .kt).
87
+ // The native samplers keep the same number as their unconfigured fallback (KeepSnappySampler.swift / .kt).
88
88
  export const SAMPLER_MS_DEFAULT = 4000;
89
89
 
90
90
  export const parseSamplerMode = (
@@ -1,4 +1,4 @@
1
- // zustand/vanilla in a PerfLab bundle. withKeepSnappy (metro.js) resolves every
1
+ // zustand/vanilla in a KeepSnappy bundle. withKeepSnappy (metro.js) resolves every
2
2
  // `zustand/vanilla` request to this module (zustand 5's index, react and
3
3
  // traditional entries all load it by that name), and the name below to the real
4
4
  // one. Each store's setState goes through the cause tracker's store writer
@@ -10,7 +10,7 @@ var real = require('@keepsnappy/react-native/zustand/vanilla');
10
10
 
11
11
  var wrap = function (createState) {
12
12
  return function (set, get, api) {
13
- var stores = globalThis.__PERFLAB_STORES__;
13
+ var stores = globalThis.__KEEPSNAPPY_STORES__;
14
14
  if (!stores) {
15
15
  return createState(set, get, api);
16
16
  }
package/metro.js CHANGED
@@ -195,6 +195,8 @@ function withKeepSnappy(input) {
195
195
  );
196
196
  }
197
197
 
198
+ process.env.KEEPSNAPPY_SDK_VERSION = require('./js/build-version.js').sdkBuildVersion();
199
+
198
200
  // Transform workers are forked after the config loads and inherit this env.
199
201
  process.env[ENV_UPSTREAM] = upstream;
200
202
  process.env[ENV_ENTRY] = entry;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keepsnappy/react-native",
3
- "version": "0.1.0-alpha.12",
3
+ "version": "0.1.0-alpha.14",
4
4
  "description": "KeepSnappy in-app SDK for React Native / Expo: zero-code measurement windows, React commit hook, native signposts and log file",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {