@keepsnappy/react-native 0.1.0-alpha.15 → 0.1.0-alpha.17
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 +12 -0
- package/js/babel-transformer.js +31 -6
- package/js/component-contract.cjs +14 -0
- package/js/component-stamps.js +207 -0
- package/js/sdk/bytes.ts +20 -0
- package/js/sdk/core.ts +92 -22
- package/js/sdk/device-model.ts +10 -0
- package/js/sdk/fiber.ts +352 -28
- package/js/sdk/interactions.ts +38 -24
- package/js/sdk/place.ts +25 -0
- package/js/sdk/profile-export.ts +15 -0
- package/js/sdk/profile.ts +272 -18
- package/js/sdk/push-install.ts +2 -1
- package/js/sdk/push.ts +70 -46
- package/js/sdk/sampler.ts +1 -1
- package/js/sdk/screens.ts +91 -6
- package/js/sdk/stream.ts +6 -0
- package/metro.js +11 -1
- package/package.json +1 -1
package/js/sdk/push.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
// rewritten to the live queue whenever it outgrows twice the queue caps, so it
|
|
12
12
|
// stays bounded while the ingest is unreachable.
|
|
13
13
|
|
|
14
|
+
import { MAX_BATCH_BYTES, utf8Length } from './bytes';
|
|
15
|
+
|
|
14
16
|
export interface DeviceInfo {
|
|
15
17
|
model: string | null;
|
|
16
18
|
os: string | null;
|
|
@@ -113,6 +115,8 @@ const serverClock = async (res: { json?: () => Promise<unknown> }, bound: Promis
|
|
|
113
115
|
interface Item {
|
|
114
116
|
line: string;
|
|
115
117
|
bytes: number;
|
|
118
|
+
// The line's size inside the request body: a JSON string (escaped) plus its comma.
|
|
119
|
+
wire: number;
|
|
116
120
|
token: string | null;
|
|
117
121
|
// The process that recorded the line (its bootId) and its sequence there.
|
|
118
122
|
origin: string;
|
|
@@ -125,7 +129,7 @@ interface Batch {
|
|
|
125
129
|
id: string;
|
|
126
130
|
items: Item[];
|
|
127
131
|
token: string | null;
|
|
128
|
-
|
|
132
|
+
wire: number;
|
|
129
133
|
}
|
|
130
134
|
|
|
131
135
|
interface SpoolAdd {
|
|
@@ -208,31 +212,15 @@ export const readSpool = (content: string | null): { pending: PendingLine[]; dro
|
|
|
208
212
|
export const pendingFromSpool = (content: string | null): PendingLine[] =>
|
|
209
213
|
readSpool(content).pending;
|
|
210
214
|
|
|
211
|
-
const utf8Length = (text: string) => {
|
|
212
|
-
let n = 0;
|
|
213
|
-
for (let i = 0; i < text.length; i++) {
|
|
214
|
-
const c = text.charCodeAt(i);
|
|
215
|
-
if (c < 0x80) {
|
|
216
|
-
n += 1;
|
|
217
|
-
} else if (c < 0x800) {
|
|
218
|
-
n += 2;
|
|
219
|
-
} else if (c >= 0xd800 && c <= 0xdbff) {
|
|
220
|
-
n += 4;
|
|
221
|
-
i++;
|
|
222
|
-
} else {
|
|
223
|
-
n += 3;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
return n;
|
|
227
|
-
};
|
|
228
|
-
|
|
229
215
|
// Retrying cannot fix these; the batch is dropped instead of blocking the queue.
|
|
216
|
+
const wireBytes = (line: string) => utf8Length(JSON.stringify(line)) + 1;
|
|
217
|
+
|
|
230
218
|
const isPermanent = (status: number) =>
|
|
231
219
|
status >= 400 && status < 500 && status !== 408 && status !== 429;
|
|
232
220
|
|
|
233
221
|
export const createPushClient = (deps: PushDeps) => {
|
|
234
222
|
const idleFlushMs = deps.idleFlushMs ?? 10_000;
|
|
235
|
-
const maxBatchBytes = deps.maxBatchBytes ??
|
|
223
|
+
const maxBatchBytes = deps.maxBatchBytes ?? MAX_BATCH_BYTES;
|
|
236
224
|
const maxBatchItems = deps.maxBatchItems ?? 400;
|
|
237
225
|
const maxQueueLines = deps.maxQueueLines ?? 2000;
|
|
238
226
|
const maxQueueBytes = deps.maxQueueBytes ?? 8 * 1024 * 1024;
|
|
@@ -277,6 +265,11 @@ export const createPushClient = (deps: PushDeps) => {
|
|
|
277
265
|
let journalRecords = 0;
|
|
278
266
|
// Lines dropped before delivery, by this process and (from the note) earlier ones.
|
|
279
267
|
let undelivered = 0;
|
|
268
|
+
// Lines of a run dropped before delivery (queue cap, line over the batch limit), by run token, until its next flush.
|
|
269
|
+
const droppedOfRun = new Map<string, number>();
|
|
270
|
+
const noteDropped = (token: string | null) => {
|
|
271
|
+
if (token !== null) droppedOfRun.set(token, (droppedOfRun.get(token) ?? 0) + 1);
|
|
272
|
+
};
|
|
280
273
|
// Round trips not reported yet, each with the run token it was measured under.
|
|
281
274
|
let clockPending: { token: string; sample: ClockSample }[] = [];
|
|
282
275
|
const clockOf = (token: string | null) =>
|
|
@@ -440,6 +433,7 @@ export const createPushClient = (deps: PushDeps) => {
|
|
|
440
433
|
}
|
|
441
434
|
}
|
|
442
435
|
undelivered += dropped.length;
|
|
436
|
+
for (const item of dropped) noteDropped(item.token);
|
|
443
437
|
return dropped;
|
|
444
438
|
};
|
|
445
439
|
|
|
@@ -448,12 +442,14 @@ export const createPushClient = (deps: PushDeps) => {
|
|
|
448
442
|
if (bytes > maxBatchBytes) {
|
|
449
443
|
stats.dropped++;
|
|
450
444
|
undelivered++;
|
|
445
|
+
noteDropped(deps.getRunToken());
|
|
451
446
|
stats.lastError = `line of ${bytes} bytes exceeds batch limit`;
|
|
452
447
|
return;
|
|
453
448
|
}
|
|
454
449
|
const item: Item = {
|
|
455
450
|
line,
|
|
456
451
|
bytes,
|
|
452
|
+
wire: wireBytes(line),
|
|
457
453
|
token: deps.getRunToken(),
|
|
458
454
|
origin: bootId,
|
|
459
455
|
seq: nextSeq++,
|
|
@@ -502,11 +498,13 @@ export const createPushClient = (deps: PushDeps) => {
|
|
|
502
498
|
if (bytes > maxBatchBytes) {
|
|
503
499
|
stats.resumeDropped++;
|
|
504
500
|
undelivered++;
|
|
501
|
+
noteDropped(record.t);
|
|
505
502
|
continue;
|
|
506
503
|
}
|
|
507
504
|
earlier.push({
|
|
508
505
|
line: record.l,
|
|
509
506
|
bytes,
|
|
507
|
+
wire: wireBytes(record.l),
|
|
510
508
|
token: record.t,
|
|
511
509
|
origin: record.o,
|
|
512
510
|
seq: record.s,
|
|
@@ -544,16 +542,16 @@ export const createPushClient = (deps: PushDeps) => {
|
|
|
544
542
|
}
|
|
545
543
|
const first = queue[0] as Item;
|
|
546
544
|
const items: Item[] = [];
|
|
547
|
-
let
|
|
545
|
+
let wire = 0;
|
|
548
546
|
if (first.batch !== undefined) {
|
|
549
547
|
// Already sent once (by a killed process): resend exactly that batch.
|
|
550
548
|
while (queue.length > 0 && queue[0]?.batch === first.batch) {
|
|
551
549
|
const next = queue.shift() as Item;
|
|
552
550
|
queuedBytes -= next.bytes;
|
|
553
551
|
items.push(next);
|
|
554
|
-
|
|
552
|
+
wire += next.wire;
|
|
555
553
|
}
|
|
556
|
-
return { id: first.batch, items, token: first.token,
|
|
554
|
+
return { id: first.batch, items, token: first.token, wire };
|
|
557
555
|
}
|
|
558
556
|
while (queue.length > 0 && items.length < maxBatchItems) {
|
|
559
557
|
const next = queue[0];
|
|
@@ -562,16 +560,16 @@ export const createPushClient = (deps: PushDeps) => {
|
|
|
562
560
|
next.batch !== undefined ||
|
|
563
561
|
next.token !== first.token ||
|
|
564
562
|
next.origin !== first.origin ||
|
|
565
|
-
(items.length > 0 &&
|
|
563
|
+
(items.length > 0 && wire + next.wire > maxBatchBytes)
|
|
566
564
|
) {
|
|
567
565
|
break;
|
|
568
566
|
}
|
|
569
567
|
queue.shift();
|
|
570
568
|
queuedBytes -= next.bytes;
|
|
571
569
|
items.push(next);
|
|
572
|
-
|
|
570
|
+
wire += next.wire;
|
|
573
571
|
}
|
|
574
|
-
return { id: '', items, token: first.token,
|
|
572
|
+
return { id: '', items, token: first.token, wire };
|
|
575
573
|
};
|
|
576
574
|
|
|
577
575
|
let inflight: Promise<void> | null = null;
|
|
@@ -617,29 +615,48 @@ export const createPushClient = (deps: PushDeps) => {
|
|
|
617
615
|
bound.clear();
|
|
618
616
|
}
|
|
619
617
|
}
|
|
618
|
+
const runToken = batch.token;
|
|
619
|
+
const reported = clockOf(runToken);
|
|
620
|
+
const sentDevice = device;
|
|
621
|
+
const bodyOf = (id: string) => JSON.stringify({
|
|
622
|
+
batch_id: id,
|
|
623
|
+
device: sentDevice,
|
|
624
|
+
items: batch.items.map(item => item.line),
|
|
625
|
+
...(runToken ? { run_token: runToken } : {}),
|
|
626
|
+
...(reported.length ? { clock: reported } : {}),
|
|
627
|
+
});
|
|
628
|
+
// Names exactly these lines (process, first and last sequence, count),
|
|
629
|
+
// so a different set of lines is always a different batch.
|
|
630
|
+
const idOf = () => {
|
|
631
|
+
const first = batch.items[0] as Item;
|
|
632
|
+
const last = batch.items[batch.items.length - 1] as Item;
|
|
633
|
+
return `${sentDevice.install_id}-${first.origin}-${first.seq}-${last.seq}-${batch.items.length}`;
|
|
634
|
+
};
|
|
635
|
+
let body = bodyOf(batch.id || idOf());
|
|
620
636
|
if (!batch.id) {
|
|
621
|
-
//
|
|
622
|
-
//
|
|
623
|
-
|
|
637
|
+
// The cap is on the body as sent (escaped lines, device, clock samples): lines over it go back to the queue
|
|
638
|
+
// front for the next batch. A lone line still goes; a line alone over the cap was dropped at enqueue.
|
|
639
|
+
for (let over = utf8Length(body) - maxBatchBytes; over > 0 && batch.items.length > 1; over = utf8Length(body) - maxBatchBytes) {
|
|
640
|
+
const back: Item[] = [];
|
|
641
|
+
for (let freed = 0; freed < over && batch.items.length > 1;) {
|
|
642
|
+
const item = batch.items.pop() as Item;
|
|
643
|
+
freed += item.wire;
|
|
644
|
+
back.unshift(item);
|
|
645
|
+
}
|
|
646
|
+
queue.unshift(...back);
|
|
647
|
+
for (const item of back) queuedBytes += item.bytes;
|
|
648
|
+
body = bodyOf(idOf());
|
|
649
|
+
}
|
|
650
|
+
// Journalled before the first send: a later process that resends after
|
|
624
651
|
// a kill sends the same lines under the same id, and the server's
|
|
625
652
|
// replay check can only ever drop lines it already has.
|
|
653
|
+
batch.id = idOf();
|
|
626
654
|
const first = batch.items[0] as Item;
|
|
627
|
-
const last = batch.items[batch.items.length - 1] as Item;
|
|
628
|
-
batch.id = `${device.install_id}-${first.origin}-${first.seq}-${last.seq}-${batch.items.length}`;
|
|
629
655
|
for (const item of batch.items) {
|
|
630
656
|
item.batch = batch.id;
|
|
631
657
|
}
|
|
632
658
|
journal({ b: batch.id, o: first.origin, s: batch.items.map(item => item.seq) });
|
|
633
659
|
}
|
|
634
|
-
const runToken = batch.token;
|
|
635
|
-
const reported = clockOf(runToken);
|
|
636
|
-
const body = JSON.stringify({
|
|
637
|
-
batch_id: batch.id,
|
|
638
|
-
device,
|
|
639
|
-
items: batch.items.map(item => item.line),
|
|
640
|
-
...(runToken ? { run_token: runToken } : {}),
|
|
641
|
-
...(reported.length ? { clock: reported } : {}),
|
|
642
|
-
});
|
|
643
660
|
const headers: Record<string, string> = {
|
|
644
661
|
'content-type': 'application/json',
|
|
645
662
|
'x-keepsnappy-key': deps.key,
|
|
@@ -820,16 +837,23 @@ export const createPushClient = (deps: PushDeps) => {
|
|
|
820
837
|
await sendClock(deadline);
|
|
821
838
|
}
|
|
822
839
|
const refused = refusedRunBatches - refusedBefore;
|
|
823
|
-
|
|
840
|
+
// A run that lost lines before they were sent is missing windows, whatever went up after.
|
|
841
|
+
const token = deps.getRunToken();
|
|
842
|
+
const lost = token === null ? 0 : (droppedOfRun.get(token) ?? 0);
|
|
843
|
+
if (token !== null) droppedOfRun.delete(token);
|
|
844
|
+
const ok = sentAll && refused === 0 && lost === 0;
|
|
845
|
+
const error = ok
|
|
846
|
+
? null
|
|
847
|
+
: !sentAll
|
|
848
|
+
? (stats.lastError ?? 'flush deadline')
|
|
849
|
+
: refused > 0
|
|
850
|
+
? `rejected ${lastRefusedStatus}: ${refused} ${refused === 1 ? 'batch' : 'batches'} of the run dropped`
|
|
851
|
+
: 'lines of the run dropped before sending';
|
|
824
852
|
return {
|
|
825
853
|
ok,
|
|
826
854
|
batches: stats.batchesSent - sentBefore,
|
|
827
855
|
lines: stats.linesSent - linesBefore,
|
|
828
|
-
error:
|
|
829
|
-
? null
|
|
830
|
-
: !sentAll
|
|
831
|
-
? (stats.lastError ?? 'flush deadline')
|
|
832
|
-
: `rejected ${lastRefusedStatus}: ${refused} ${refused === 1 ? 'batch' : 'batches'} of the run dropped`,
|
|
856
|
+
error: error !== null && lost > 0 ? `${error} dropped=${lost}` : error,
|
|
833
857
|
};
|
|
834
858
|
} finally {
|
|
835
859
|
flushLimit = null;
|
package/js/sdk/sampler.ts
CHANGED
|
@@ -132,7 +132,7 @@ export const estimateOffset = (before: number, native: number, after: number) =>
|
|
|
132
132
|
// late after its t0 a window can start and still take one. It gates only screen windows (a screen window opened by a
|
|
133
133
|
// press is detected after the new screen rendered and takes the press's open sample, taken the touch delivery delay
|
|
134
134
|
// past the press's t0) and the reuse of a close sample for a restarted key. Press windows do not depend on it: they
|
|
135
|
-
// take the previous window's close sample by contiguity. Evidence for 50: idle lab event timing of 4.9-19.1 ms
|
|
135
|
+
// take the previous window's close sample by contiguity, else a sample of their own however late it is. Evidence for 50: idle lab event timing of 4.9-19.1 ms
|
|
136
136
|
// (packages/derive/test/fixtures/p37-*.log); no loaded-device data. Measure the touchstart delivery delay p99 per
|
|
137
137
|
// device from farm data (docs/log/followups.md).
|
|
138
138
|
export const OPEN_SAMPLE_TOLERANCE_MS = 50;
|
package/js/sdk/screens.ts
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
} from './core';
|
|
27
27
|
import { retainFloorOf } from './retain-floor';
|
|
28
28
|
import { type CauseRoot, currentCause, latestActionAt, launchedAt, launchRootId } from './causes';
|
|
29
|
-
import { routeOfScreenHost, routes } from './fiber';
|
|
29
|
+
import { buildPlaceId, routeOfScreenHost, routes, screenKeyForRoute, setScreenKeyProvider, currentFiber, type ScreenRouteResolution, type Fiber } from './fiber';
|
|
30
30
|
import {
|
|
31
31
|
lastPressStart,
|
|
32
32
|
type NativeScreenEvent,
|
|
@@ -64,6 +64,12 @@ interface Visit {
|
|
|
64
64
|
chainKey: string;
|
|
65
65
|
keyChain: string;
|
|
66
66
|
visit: number;
|
|
67
|
+
screenKey: string | null;
|
|
68
|
+
legacyName: string;
|
|
69
|
+
// The screen component is not known yet: its scene has not mounted, or it is suspended.
|
|
70
|
+
pending: boolean;
|
|
71
|
+
resolution: ScreenRouteResolution;
|
|
72
|
+
lazyBoundary: Fiber | null;
|
|
67
73
|
t0: number;
|
|
68
74
|
openedAt: number;
|
|
69
75
|
endedAt: number | null;
|
|
@@ -89,6 +95,7 @@ interface Resolution {
|
|
|
89
95
|
let current: Visit | null = null;
|
|
90
96
|
let seq = 0;
|
|
91
97
|
const visitsByChain = new Map<string, number>();
|
|
98
|
+
const visitsByScreen = new Map<string, number>();
|
|
92
99
|
let lastActionAt = -Infinity;
|
|
93
100
|
interface AppLink {
|
|
94
101
|
t: number;
|
|
@@ -248,6 +255,7 @@ const endVisit = (reason: string, at?: number) => {
|
|
|
248
255
|
if (!visit || visit.endedAt !== null) {
|
|
249
256
|
return;
|
|
250
257
|
}
|
|
258
|
+
if (visit.pending) resolveVisit(visit, true);
|
|
251
259
|
const t = Math.max(visit.t0, at ?? now());
|
|
252
260
|
visit.endedAt = t;
|
|
253
261
|
stats.ends[reason] = (stats.ends[reason] ?? 0) + 1;
|
|
@@ -262,7 +270,9 @@ const endVisit = (reason: string, at?: number) => {
|
|
|
262
270
|
// state event.
|
|
263
271
|
const chooseStart = (snapshot: NavSnapshot, floor: number) => {
|
|
264
272
|
const inSpan = (t: number) => t >= floor && t <= snapshot.stateT;
|
|
265
|
-
|
|
273
|
+
// Touches delivered since the previous visit opened, whatever their finger time: a finger that landed while the
|
|
274
|
+
// previous screen was mounting reaches JS after its state was read.
|
|
275
|
+
const presses = pressesSinceVisit.filter(p => p.t0 >= windowsEndedAt && p.t0 <= snapshot.stateT);
|
|
266
276
|
const link = pendingLink && inSpan(pendingLink.t) ? pendingLink : null;
|
|
267
277
|
const action = snapshot.action && inSpan(snapshot.action.t) ? snapshot.action : null;
|
|
268
278
|
const touchStart = (p: Press) => ({
|
|
@@ -332,6 +342,28 @@ const isStrictExtension = (prev: RoutePart[], next: RoutePart[]) =>
|
|
|
332
342
|
(part.key ?? null) === (next[i]?.key ?? null),
|
|
333
343
|
);
|
|
334
344
|
|
|
345
|
+
const screenResolution = (chain: RoutePart[]) => {
|
|
346
|
+
const leaf = chain[chain.length - 1]!;
|
|
347
|
+
return screenKeyForRoute(leaf.key ?? null, leaf.name);
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const moveScreenCounter = (visit: Visit, screen: string) => {
|
|
351
|
+
if (visit.screenKey === screen) return;
|
|
352
|
+
if (visit.screenKey !== null) visitsByScreen.set(visit.screenKey, (visitsByScreen.get(visit.screenKey) ?? 1) - 1);
|
|
353
|
+
visit.visit = (visitsByScreen.get(screen) ?? 0) + 1;
|
|
354
|
+
visitsByScreen.set(screen, visit.visit);
|
|
355
|
+
visit.screenKey = screen;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const resolveVisit = (visit: Visit, final = false) => {
|
|
359
|
+
const resolved = visit.resolution;
|
|
360
|
+
if (resolved.screenPending && !final) return;
|
|
361
|
+
moveScreenCounter(visit, resolved.screen);
|
|
362
|
+
visit.name = buildPlaceId({ kind: 'screen', screen: resolved.screen, visit: visit.visit });
|
|
363
|
+
visit.pending = false;
|
|
364
|
+
markers.rename(visit.key, visit.name, visit.legacyName);
|
|
365
|
+
};
|
|
366
|
+
|
|
335
367
|
const extend = (visit: Visit, chain: RoutePart[], chainKey: string) => {
|
|
336
368
|
const previous = visitsByChain.get(visit.chainKey) ?? 1;
|
|
337
369
|
visitsByChain.set(visit.chainKey, previous - 1);
|
|
@@ -341,9 +373,19 @@ const extend = (visit: Visit, chain: RoutePart[], chainKey: string) => {
|
|
|
341
373
|
visit.chain = chain;
|
|
342
374
|
visit.chainKey = chainKey;
|
|
343
375
|
visit.keyChain = keyChainOf(chain);
|
|
344
|
-
visit.
|
|
345
|
-
|
|
346
|
-
|
|
376
|
+
visit.legacyName = nameOf(chainKey, n);
|
|
377
|
+
const resolution = screenResolution(chain);
|
|
378
|
+
visit.resolution = resolution;
|
|
379
|
+
visit.lazyBoundary = resolution.suspended ?? null;
|
|
380
|
+
if (resolution.screenPending) {
|
|
381
|
+
if (visit.screenKey !== null) {
|
|
382
|
+
visitsByScreen.set(visit.screenKey, (visitsByScreen.get(visit.screenKey) ?? 1) - 1);
|
|
383
|
+
visit.screenKey = null;
|
|
384
|
+
}
|
|
385
|
+
visit.name = visit.legacyName;
|
|
386
|
+
visit.pending = true;
|
|
387
|
+
markers.rename(visit.key, visit.name, visit.legacyName);
|
|
388
|
+
} else resolveVisit(visit);
|
|
347
389
|
stats.extended++;
|
|
348
390
|
};
|
|
349
391
|
|
|
@@ -398,6 +440,7 @@ const onSnapshot = (snapshot: NavSnapshot, nameSource: NameSource) => {
|
|
|
398
440
|
const n = (visitsByChain.get(chainKey) ?? 0) + 1;
|
|
399
441
|
visitsByChain.set(chainKey, n);
|
|
400
442
|
seq++;
|
|
443
|
+
const resolved = screenResolution(chain);
|
|
401
444
|
const visit: Visit = {
|
|
402
445
|
key: `screen#${seq}`,
|
|
403
446
|
name: nameOf(chainKey, n),
|
|
@@ -405,6 +448,11 @@ const onSnapshot = (snapshot: NavSnapshot, nameSource: NameSource) => {
|
|
|
405
448
|
chainKey,
|
|
406
449
|
keyChain,
|
|
407
450
|
visit: n,
|
|
451
|
+
legacyName: nameOf(chainKey, n),
|
|
452
|
+
screenKey: null,
|
|
453
|
+
pending: true,
|
|
454
|
+
resolution: resolved,
|
|
455
|
+
lazyBoundary: resolved.suspended ?? null,
|
|
408
456
|
t0: start.t0,
|
|
409
457
|
openedAt: detectedAt,
|
|
410
458
|
endedAt: null,
|
|
@@ -417,6 +465,11 @@ const onSnapshot = (snapshot: NavSnapshot, nameSource: NameSource) => {
|
|
|
417
465
|
launch,
|
|
418
466
|
cause: snapshot.cause,
|
|
419
467
|
};
|
|
468
|
+
if (!resolved.screenPending) {
|
|
469
|
+
moveScreenCounter(visit, resolved.screen);
|
|
470
|
+
visit.name = buildPlaceId({ kind: 'screen', screen: resolved.screen, visit: visit.visit });
|
|
471
|
+
visit.pending = false;
|
|
472
|
+
}
|
|
420
473
|
current = visit;
|
|
421
474
|
pressesSinceVisit = [];
|
|
422
475
|
stats.visits++;
|
|
@@ -429,6 +482,12 @@ const onSnapshot = (snapshot: NavSnapshot, nameSource: NameSource) => {
|
|
|
429
482
|
markers.start(visit.name, {
|
|
430
483
|
key: visit.key,
|
|
431
484
|
kind: 'screen',
|
|
485
|
+
legacyName: visit.legacyName,
|
|
486
|
+
beforeEnd: () => {
|
|
487
|
+
if (visit.pending) resolveVisit(visit, true);
|
|
488
|
+
visit.lazyBoundary = null;
|
|
489
|
+
visit.resolution = { screen: visit.screenKey ?? visit.resolution.screen };
|
|
490
|
+
},
|
|
432
491
|
at: visit.t0,
|
|
433
492
|
details: details(visit),
|
|
434
493
|
// The launch window belongs to the launch, whatever action came before the first state (REQ-MEAS-22).
|
|
@@ -470,12 +529,14 @@ export const noteAppLink = (url: string, via: string) => {
|
|
|
470
529
|
}
|
|
471
530
|
};
|
|
472
531
|
|
|
532
|
+
// Every touch delivered while a visit is open is an action after the one that opened it, even when its finger landed
|
|
533
|
+
// before the SDK read the navigation state (the JS thread was busy with the mount): order decides, not the clocks.
|
|
473
534
|
const onPress = (t0: number) => {
|
|
474
535
|
const press = lastPressStart();
|
|
475
536
|
if (press) {
|
|
476
537
|
pressesSinceVisit.push(press);
|
|
477
538
|
}
|
|
478
|
-
if (current && current.endedAt === null
|
|
539
|
+
if (current && current.endedAt === null) {
|
|
479
540
|
endVisit('next-action', t0);
|
|
480
541
|
}
|
|
481
542
|
};
|
|
@@ -530,6 +591,7 @@ export const screenStats = () => ({
|
|
|
530
591
|
visitsByChain: Object.fromEntries(
|
|
531
592
|
[...visitsByChain].filter(([, n]) => n > 0).slice(0, MAX_LISTED),
|
|
532
593
|
),
|
|
594
|
+
visitsByScreen: Object.fromEntries([...visitsByScreen].filter(([, n]) => n > 0).slice(0, MAX_LISTED)),
|
|
533
595
|
trackedRoutes: routes.size,
|
|
534
596
|
nav: navStats(),
|
|
535
597
|
native: {
|
|
@@ -569,6 +631,10 @@ export const installScreens = () => {
|
|
|
569
631
|
return;
|
|
570
632
|
}
|
|
571
633
|
installed = true;
|
|
634
|
+
setScreenKeyProvider(() => current ? {
|
|
635
|
+
screen: current.screenKey ?? current.resolution.screen,
|
|
636
|
+
...(current.pending || current.lazyBoundary ? { screenPending: true } : {}),
|
|
637
|
+
} : { screen: '(no screen)' });
|
|
572
638
|
setRetainFloor(screenRetainFloor);
|
|
573
639
|
onWindowsEnd(onWindowsEnded);
|
|
574
640
|
onNavSnapshot(snapshot => onSnapshot(snapshot, 'state'));
|
|
@@ -578,6 +644,25 @@ export const installScreens = () => {
|
|
|
578
644
|
setTimeout(() => logNativeEvent(event), RESOLVE_DELAY_MS);
|
|
579
645
|
});
|
|
580
646
|
onAfterCommit(() => {
|
|
647
|
+
const visit = current;
|
|
648
|
+
if (visit && isWindowOpen(visit.key)) {
|
|
649
|
+
// A pending visit is resolved when its scene mounts, however many commits come first; it falls back to the route
|
|
650
|
+
// name only when it ends.
|
|
651
|
+
if (visit.pending && !visit.lazyBoundary) {
|
|
652
|
+
visit.resolution = screenResolution(visit.chain);
|
|
653
|
+
visit.lazyBoundary = visit.resolution.suspended ?? null;
|
|
654
|
+
resolveVisit(visit);
|
|
655
|
+
} else if (visit.lazyBoundary) {
|
|
656
|
+
const boundary = currentFiber(visit.lazyBoundary);
|
|
657
|
+
if (!boundary || boundary.memoizedState == null) {
|
|
658
|
+
visit.lazyBoundary = null;
|
|
659
|
+
if (boundary) {
|
|
660
|
+
visit.resolution = screenResolution(visit.chain);
|
|
661
|
+
resolveVisit(visit, true);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
581
666
|
if (!checkScheduled && containerCount() === 0) {
|
|
582
667
|
checkScheduled = true;
|
|
583
668
|
setTimeout(checkRouteProps, 0);
|
package/js/sdk/stream.ts
CHANGED
|
@@ -39,6 +39,12 @@ export const createStream = (processId: string, maxFibers = MAX_SEGMENT_FIBERS)
|
|
|
39
39
|
/** The segment the next commit goes into: windows name it so the backend can tell a stream is complete. */
|
|
40
40
|
seq: () => seq,
|
|
41
41
|
size: () => commits.length,
|
|
42
|
+
/** Sets the fiber cap of the segments from now on; returns the previous cap. */
|
|
43
|
+
setMaxFibers(n: number) {
|
|
44
|
+
const previous = maxFibers;
|
|
45
|
+
maxFibers = n;
|
|
46
|
+
return previous;
|
|
47
|
+
},
|
|
42
48
|
noteRoot(root: CauseRoot) {
|
|
43
49
|
roots.push(root);
|
|
44
50
|
},
|
package/metro.js
CHANGED
|
@@ -175,7 +175,7 @@ const resolveEntry = projectRoot => {
|
|
|
175
175
|
return null;
|
|
176
176
|
};
|
|
177
177
|
|
|
178
|
-
function withKeepSnappy(input) {
|
|
178
|
+
function withKeepSnappy(input, { clientPackages = [] } = {}) {
|
|
179
179
|
if (!isEnabled()) {
|
|
180
180
|
return isProfiling() ? withReactProfiling(input) : input;
|
|
181
181
|
}
|
|
@@ -201,13 +201,23 @@ function withKeepSnappy(input) {
|
|
|
201
201
|
process.env[ENV_UPSTREAM] = upstream;
|
|
202
202
|
process.env[ENV_ENTRY] = entry;
|
|
203
203
|
process.env[ENV_BOOT] = BOOT_PATH;
|
|
204
|
+
const packages = [...new Set(clientPackages)].sort();
|
|
205
|
+
process.env.KEEPSNAPPY_CLIENT_PACKAGES = JSON.stringify(packages);
|
|
206
|
+
process.env.KEEPSNAPPY_PROJECT_ROOT = projectRoot;
|
|
207
|
+
process.env.KEEPSNAPPY_COMPONENT_CACHE_KEY = require('./js/component-stamps.js').configure(projectRoot, packages);
|
|
204
208
|
|
|
205
209
|
const serializer = config.serializer ?? {};
|
|
206
210
|
const upstreamPolyfills = serializer.getPolyfills;
|
|
207
211
|
const upstreamPreMain = serializer.getModulesRunBeforeMainModule;
|
|
212
|
+
const resolver = config.resolver ?? {};
|
|
213
|
+
const blockList = resolver.blockList ? (Array.isArray(resolver.blockList) ? resolver.blockList : [resolver.blockList]) : [];
|
|
214
|
+
const escape = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
215
|
+
const roots = [...new Set([path.resolve(projectRoot), fs.realpathSync(projectRoot)])];
|
|
216
|
+
const inventory = new RegExp(`^(?:${roots.map(escape).join('|')})[/\\\\]\\.keepsnappy(?:[/\\\\]|$)`);
|
|
208
217
|
|
|
209
218
|
return {
|
|
210
219
|
...config,
|
|
220
|
+
resolver: { ...resolver, blockList: [...blockList, inventory] },
|
|
211
221
|
serializer: {
|
|
212
222
|
...serializer,
|
|
213
223
|
getPolyfills: options => [
|
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.17",
|
|
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": {
|