@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.
- package/README.md +35 -35
- package/android/build.gradle +2 -2
- package/android/src/main/java/expo/modules/{perflab/PerfLabModule.kt → keepsnappy/KeepSnappyModule.kt} +69 -28
- package/android/src/main/java/expo/modules/{perflab/PerfLabSampler.kt → keepsnappy/KeepSnappySampler.kt} +17 -17
- package/app.plugin.js +7 -6
- package/expo-module.config.json +2 -2
- package/ios/{PerfLab.podspec → KeepSnappy.podspec} +2 -2
- package/ios/{PerfLabHermes.h → KeepSnappyHermes.h} +1 -1
- package/ios/{PerfLabHermes.mm → KeepSnappyHermes.mm} +2 -2
- package/ios/{PerfLabModule.swift → KeepSnappyModule.swift} +65 -26
- package/ios/{PerfLabSampler.swift → KeepSnappySampler.swift} +10 -10
- package/js/babel-transformer.js +8 -4
- package/js/build-version.js +23 -0
- package/js/hook.polyfill.js +11 -11
- package/js/renderer-switch.js +1 -1
- package/js/sdk/activation.ts +15 -15
- package/js/sdk/active.ts +3 -3
- package/js/sdk/artifact-pending.ts +167 -0
- package/js/sdk/artifacts.ts +5 -4
- package/js/sdk/boot.ts +4 -4
- package/js/sdk/causes.ts +2 -2
- package/js/sdk/core.ts +143 -53
- package/js/sdk/cpuprof.ts +1 -1
- package/js/sdk/dormant.ts +7 -7
- package/js/sdk/links.ts +13 -13
- package/js/sdk/net.ts +2 -2
- package/js/sdk/profile-export.ts +7 -7
- package/js/sdk/push-install.ts +3 -3
- package/js/sdk/push.ts +52 -17
- package/js/sdk/sampler.ts +1 -1
- package/js/zustand-vanilla.js +2 -2
- package/metro.js +2 -0
- package/package.json +1 -1
package/js/sdk/push-install.ts
CHANGED
|
@@ -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?.
|
|
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 (
|
|
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?.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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-
|
|
645
|
+
'x-keepsnappy-key': deps.key,
|
|
634
646
|
};
|
|
635
647
|
if (runToken) {
|
|
636
|
-
headers['x-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
790
|
-
if (
|
|
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
|
|
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 (
|
|
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 = (
|
package/js/zustand-vanilla.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// zustand/vanilla in a
|
|
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.
|
|
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.
|
|
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": {
|