@keepsnappy/react-native 0.1.0-alpha.14 → 0.1.0-alpha.16
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 +24 -6
- package/js/component-contract.cjs +14 -0
- package/js/component-stamps.js +192 -0
- package/js/sdk/bytes.ts +20 -0
- package/js/sdk/core.ts +55 -14
- package/js/sdk/device-model.ts +10 -0
- package/js/sdk/fiber.ts +282 -28
- package/js/sdk/interactions.ts +23 -20
- 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 +3 -19
- package/js/sdk/screens.ts +83 -4
- package/metro.js +11 -1
- package/package.json +1 -1
package/js/sdk/fiber.ts
CHANGED
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
// (to name touched views) and route key -> screen fiber (to name screens).
|
|
3
3
|
// Only reads fiber fields; never mutates the tree.
|
|
4
4
|
|
|
5
|
+
import { buildPlaceId, placeSegment, logPlace } from './place';
|
|
6
|
+
export { buildPlaceId, placeSegment } from './place';
|
|
7
|
+
|
|
5
8
|
export interface Fiber {
|
|
6
9
|
tag: number;
|
|
7
10
|
flags: number;
|
|
8
11
|
type: unknown;
|
|
12
|
+
elementType?: unknown;
|
|
9
13
|
alternate: Fiber | null;
|
|
10
14
|
child: Fiber | null;
|
|
11
15
|
sibling: Fiber | null;
|
|
12
16
|
return: Fiber | null;
|
|
13
17
|
memoizedProps: unknown;
|
|
18
|
+
memoizedState?: unknown;
|
|
14
19
|
stateNode: unknown;
|
|
15
20
|
}
|
|
16
21
|
|
|
@@ -33,6 +38,25 @@ export const isComponentTag = (tag: number) =>
|
|
|
33
38
|
tag === FORWARD_REF ||
|
|
34
39
|
tag === SIMPLE_MEMO_COMPONENT;
|
|
35
40
|
|
|
41
|
+
export const OUTSIDE_APP = '(outside app components)';
|
|
42
|
+
const hasOwn = Object.prototype.hasOwnProperty;
|
|
43
|
+
const stampOf = (type: unknown): string | null => {
|
|
44
|
+
if (type === null || (typeof type !== 'function' && typeof type !== 'object')) return null;
|
|
45
|
+
const key = (type as { __ks?: unknown }).__ks;
|
|
46
|
+
return typeof key === 'string' && hasOwn.call(type, '__ks') ? key : null;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const clientKeyOf = (fiber: Fiber): string | null => {
|
|
50
|
+
const parent = fiber.tag === SIMPLE_MEMO_COMPONENT ? null : fiber.return;
|
|
51
|
+
return (fiber.tag === SIMPLE_MEMO_COMPONENT
|
|
52
|
+
? stampOf(fiber.elementType)
|
|
53
|
+
: parent?.tag === MEMO_COMPONENT ? stampOf(parent.type) : null) ??
|
|
54
|
+
stampOf(fiber.type) ??
|
|
55
|
+
(fiber.tag === FORWARD_REF ? stampOf((fiber.type as { render?: unknown } | null)?.render) : null);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const clientFiber = (f: Fiber) => isComponentTag(f.tag) && clientKeyOf(f) !== null;
|
|
59
|
+
|
|
36
60
|
type Props = Record<string, unknown>;
|
|
37
61
|
|
|
38
62
|
export const propsOf = (fiber: Fiber): Props | null => {
|
|
@@ -158,6 +182,7 @@ export interface RouteEntry {
|
|
|
158
182
|
}
|
|
159
183
|
|
|
160
184
|
export const routes = new Map<string, RouteEntry>();
|
|
185
|
+
const scenes = new Map<string, Fiber>();
|
|
161
186
|
|
|
162
187
|
// React Navigation passes `route` and `navigation` to every screen component;
|
|
163
188
|
// Expo Router routes are React Navigation routes. Reading the props is enough.
|
|
@@ -205,6 +230,12 @@ export const noteMounted = (fiber: Fiber) => {
|
|
|
205
230
|
const props = propsOf(fiber);
|
|
206
231
|
if (props && props.route !== undefined) {
|
|
207
232
|
noteRoute(fiber, props);
|
|
233
|
+
const route = props.route as { key?: unknown } | null;
|
|
234
|
+
if (typeof route?.key === 'string' &&
|
|
235
|
+
typeof props.getState === 'function' &&
|
|
236
|
+
typeof props.clearOptions === 'function') {
|
|
237
|
+
scenes.set(route.key, fiber);
|
|
238
|
+
}
|
|
208
239
|
}
|
|
209
240
|
};
|
|
210
241
|
|
|
@@ -229,6 +260,8 @@ export const noteUnmounted = (fiber: Fiber) => {
|
|
|
229
260
|
}
|
|
230
261
|
const route = propsOf(fiber)?.route as { key?: unknown } | undefined;
|
|
231
262
|
if (route && typeof route.key === 'string') {
|
|
263
|
+
const scene = scenes.get(route.key);
|
|
264
|
+
if (scene === fiber || scene === fiber.alternate) scenes.delete(route.key);
|
|
232
265
|
const entry = routes.get(route.key);
|
|
233
266
|
if (entry && (entry.fiber === fiber || entry.fiber === fiber.alternate)) {
|
|
234
267
|
routes.delete(route.key);
|
|
@@ -239,6 +272,7 @@ export const noteUnmounted = (fiber: Fiber) => {
|
|
|
239
272
|
export const registrySizes = () => ({
|
|
240
273
|
hostTags: hostByTag.size,
|
|
241
274
|
routes: routes.size,
|
|
275
|
+
scenes: scenes.size,
|
|
242
276
|
containers: containerFibers.size,
|
|
243
277
|
});
|
|
244
278
|
|
|
@@ -248,8 +282,103 @@ export interface ScreenRoute {
|
|
|
248
282
|
routeName: string | null;
|
|
249
283
|
}
|
|
250
284
|
|
|
285
|
+
// React's slow path follows both branches: a shared child can keep a stale return pointer.
|
|
286
|
+
// Registries only supply mounted fibers; unresolved/deleted alternate pairs are not scanned.
|
|
287
|
+
export const currentFiber = (fiber: Fiber): Fiber | null => {
|
|
288
|
+
const alternate = fiber.alternate;
|
|
289
|
+
if (!alternate) return fiber;
|
|
290
|
+
let a = fiber;
|
|
291
|
+
let b = alternate;
|
|
292
|
+
let remaining = 1000;
|
|
293
|
+
while (remaining-- > 0) {
|
|
294
|
+
const parentA = a.return;
|
|
295
|
+
if (!parentA) {
|
|
296
|
+
const root = a.stateNode as { current?: Fiber } | null;
|
|
297
|
+
if (a.tag !== 3) return null;
|
|
298
|
+
if (root?.current === a) return fiber;
|
|
299
|
+
return root?.current === b ? alternate : null;
|
|
300
|
+
}
|
|
301
|
+
const parentB = parentA.alternate;
|
|
302
|
+
if (!parentB) {
|
|
303
|
+
const grandparent = parentA.return;
|
|
304
|
+
if (!grandparent) return null;
|
|
305
|
+
a = b = grandparent;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (parentA.child === parentB.child) {
|
|
309
|
+
for (let child = parentA.child; child && remaining-- > 0; child = child.sibling) {
|
|
310
|
+
if (child === a) return fiber;
|
|
311
|
+
if (child === b) return alternate;
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
if (a.return !== b.return) {
|
|
316
|
+
a = parentA;
|
|
317
|
+
b = parentB;
|
|
318
|
+
} else {
|
|
319
|
+
let found = false;
|
|
320
|
+
for (const parent of [parentA, parentB]) {
|
|
321
|
+
for (let child = parent.child; child && remaining-- > 0; child = child.sibling) {
|
|
322
|
+
if (child === a || child === b) {
|
|
323
|
+
const other = parent === parentA ? parentB : parentA;
|
|
324
|
+
if (child === a) { a = parent; b = other; }
|
|
325
|
+
else { a = other; b = parent; }
|
|
326
|
+
found = true;
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (found) break;
|
|
331
|
+
}
|
|
332
|
+
if (!found) return null;
|
|
333
|
+
}
|
|
334
|
+
if (a.alternate !== b) return null;
|
|
335
|
+
}
|
|
336
|
+
return null;
|
|
337
|
+
};
|
|
338
|
+
|
|
251
339
|
const MAX_ROUTE_SEARCH = 500;
|
|
252
340
|
|
|
341
|
+
export interface ScreenKeyResolution {
|
|
342
|
+
screen: string;
|
|
343
|
+
screenPending?: boolean;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export interface ScreenRouteResolution extends ScreenKeyResolution {
|
|
347
|
+
suspended?: Fiber;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
let screenKeyProvider: () => ScreenKeyResolution = () => ({ screen: '(no screen)' });
|
|
351
|
+
export const setScreenKeyProvider = (provider: () => ScreenKeyResolution) => {
|
|
352
|
+
screenKeyProvider = provider;
|
|
353
|
+
};
|
|
354
|
+
export const getScreenKey = () => screenKeyProvider();
|
|
355
|
+
|
|
356
|
+
export const screenKeyForRoute = (routeKey: string | null, routeName: string): ScreenRouteResolution => {
|
|
357
|
+
const scene = routeKey === null ? null : scenes.get(routeKey);
|
|
358
|
+
const root = scene ? currentFiber(scene) : null;
|
|
359
|
+
let suspended: Fiber | undefined;
|
|
360
|
+
if (root) {
|
|
361
|
+
const queue: Fiber[] = [];
|
|
362
|
+
let next: Fiber | null = root.child;
|
|
363
|
+
let visited = 0;
|
|
364
|
+
let queued = 0;
|
|
365
|
+
while ((next || queued < queue.length) && visited < MAX_ROUTE_SEARCH) {
|
|
366
|
+
const f: Fiber = next ?? queue[queued++]!;
|
|
367
|
+
visited++;
|
|
368
|
+
next = f.sibling;
|
|
369
|
+
if (clientFiber(f)) return { screen: clientKeyOf(f)! };
|
|
370
|
+
// A suspended boundary's child is hidden primary content; its sibling is the fallback Fragment.
|
|
371
|
+
if (f.tag === 13 && f.memoizedState != null) { suspended ??= f; continue; }
|
|
372
|
+
if (f.child) queue.push(f.child);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
screen: placeSegment(OUTSIDE_APP, routeName),
|
|
377
|
+
screenPending: !root || suspended !== undefined,
|
|
378
|
+
...(suspended ? { suspended } : {}),
|
|
379
|
+
};
|
|
380
|
+
};
|
|
381
|
+
|
|
253
382
|
const routeLike = (value: unknown): { key: string; name: string } | null => {
|
|
254
383
|
if (value === null || typeof value !== 'object') {
|
|
255
384
|
return null;
|
|
@@ -355,7 +484,9 @@ export type NameSource =
|
|
|
355
484
|
| 'accessibilityLabel'
|
|
356
485
|
| 'text'
|
|
357
486
|
| 'component'
|
|
358
|
-
| 'tag'
|
|
487
|
+
| 'tag'
|
|
488
|
+
| 'client'
|
|
489
|
+
| 'outside';
|
|
359
490
|
|
|
360
491
|
export interface TargetDescription {
|
|
361
492
|
name: string;
|
|
@@ -365,14 +496,17 @@ export interface TargetDescription {
|
|
|
365
496
|
accessibilityLabel: string | null;
|
|
366
497
|
component: string | null;
|
|
367
498
|
actionable: boolean;
|
|
499
|
+
legacyName: string;
|
|
500
|
+
legacySource: NameSource;
|
|
501
|
+
place: TargetPlace;
|
|
368
502
|
}
|
|
369
503
|
|
|
370
504
|
// testID -> accessibilityLabel -> text -> nearest named component -> tag.
|
|
371
505
|
// testID/label are searched from the touched view up to the first named
|
|
372
506
|
// component above the actionable (onPress/responder) fiber.
|
|
373
|
-
|
|
507
|
+
const legacyTarget = (tag: number): Omit<TargetDescription, 'legacyName' | 'legacySource' | 'place'> => {
|
|
374
508
|
const host = hostByTag.get(tag);
|
|
375
|
-
const result: TargetDescription = {
|
|
509
|
+
const result: Omit<TargetDescription, 'legacyName' | 'legacySource' | 'place'> = {
|
|
376
510
|
name: `tag:${tag}`,
|
|
377
511
|
source: 'tag',
|
|
378
512
|
tag,
|
|
@@ -438,6 +572,112 @@ export const describeTarget = (tag: number): TargetDescription => {
|
|
|
438
572
|
return result;
|
|
439
573
|
};
|
|
440
574
|
|
|
575
|
+
export interface TargetPlace {
|
|
576
|
+
screen: string;
|
|
577
|
+
parent: string | null;
|
|
578
|
+
target: string;
|
|
579
|
+
k: number | null;
|
|
580
|
+
hostType: string;
|
|
581
|
+
screenPending?: boolean;
|
|
582
|
+
manySiblings?: number;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const actionableFiber = (f: Fiber) => {
|
|
586
|
+
const props = propsOf(f);
|
|
587
|
+
return props !== null && isActionable(props);
|
|
588
|
+
};
|
|
589
|
+
const isListCell = (f: Fiber) => {
|
|
590
|
+
if (!isComponentTag(f.tag) || clientKeyOf(f) !== null) return false;
|
|
591
|
+
const p = propsOf(f);
|
|
592
|
+
return p !== null && ((typeof p.cellKey === 'string' && typeof p.index === 'number') ||
|
|
593
|
+
(typeof p.index === 'number' && 'item' in p && typeof p.renderItem === 'function') ||
|
|
594
|
+
(typeof p.getRenderedItem === 'function' && 'itemKey' in p));
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
const occurrenceOf = (
|
|
598
|
+
parent: Fiber,
|
|
599
|
+
target: Fiber,
|
|
600
|
+
region: Fiber,
|
|
601
|
+
scroll: boolean,
|
|
602
|
+
): { k: number | null; manySiblings?: number } => {
|
|
603
|
+
const none = { k: null };
|
|
604
|
+
for (let f: Fiber | null = region, n = 0; f && f !== parent && n < MAX_UP * 2; f = f.return, n++) {
|
|
605
|
+
if (isListCell(f)) return none;
|
|
606
|
+
}
|
|
607
|
+
const root = currentFiber(parent);
|
|
608
|
+
if (!root) { logPlace('place current parent unavailable'); return none; }
|
|
609
|
+
let visited = 0;
|
|
610
|
+
const key = clientKeyOf(target);
|
|
611
|
+
const stack: { f: Fiber; inside: boolean }[] = root.child ? [{ f: root.child, inside: false }] : [];
|
|
612
|
+
let count = 0;
|
|
613
|
+
let index = 0;
|
|
614
|
+
while (stack.length && visited < 1000) {
|
|
615
|
+
const { f, inside } = stack.pop()!;
|
|
616
|
+
visited++;
|
|
617
|
+
if (f.sibling) stack.push({ f: f.sibling, inside });
|
|
618
|
+
if (isListCell(f)) continue;
|
|
619
|
+
const client = clientFiber(f);
|
|
620
|
+
if (client && (inside || clientKeyOf(f) !== key)) continue;
|
|
621
|
+
const inTarget = inside || client;
|
|
622
|
+
if (inTarget && ((scroll && client) || (!scroll && actionableFiber(f)))) {
|
|
623
|
+
count++;
|
|
624
|
+
if (f === region || f === region.alternate) index = count;
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
if (f.child) stack.push({ f: f.child, inside: inTarget });
|
|
628
|
+
}
|
|
629
|
+
if (stack.length) { logPlace('place occurrence scan cap'); return none; }
|
|
630
|
+
if (index === 0) { logPlace('place occurrence region not found'); return none; }
|
|
631
|
+
if (count > 20) {
|
|
632
|
+
logPlace(`place many siblings count=${count}`);
|
|
633
|
+
return { k: index, manySiblings: count };
|
|
634
|
+
}
|
|
635
|
+
return { k: count > 1 ? index : null };
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
const placeOf = (host: Fiber | null, scroll: boolean, screen: ScreenKeyResolution): TargetPlace => {
|
|
639
|
+
const hostType = typeof host?.type === 'string' ? host.type : 'unknown';
|
|
640
|
+
let start = host;
|
|
641
|
+
if (!scroll) {
|
|
642
|
+
for (let f = host, n = 0; f && n < MAX_UP; f = f.return, n++) {
|
|
643
|
+
if (actionableFiber(f)) { start = f; break; }
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
let target: Fiber | null = null;
|
|
647
|
+
let region = start;
|
|
648
|
+
for (let f = start, n = 0; f && n < MAX_UP * 2; f = f.return, n++) {
|
|
649
|
+
if (!scroll && actionableFiber(f)) region = f;
|
|
650
|
+
if (clientFiber(f)) { target = f; break; }
|
|
651
|
+
}
|
|
652
|
+
let parent: Fiber | null = null;
|
|
653
|
+
for (let f = target?.return ?? null, n = 0; f && n < MAX_UP * 2; f = f.return, n++) {
|
|
654
|
+
if (clientFiber(f)) { parent = f; break; }
|
|
655
|
+
}
|
|
656
|
+
return {
|
|
657
|
+
screen: screen.screen,
|
|
658
|
+
parent: parent ? clientKeyOf(parent) : null,
|
|
659
|
+
target: target ? clientKeyOf(target)! : placeSegment(OUTSIDE_APP, hostType),
|
|
660
|
+
hostType,
|
|
661
|
+
...(screen.screenPending ? { screenPending: true } : {}),
|
|
662
|
+
...(parent && target && region ? occurrenceOf(parent, target, scroll ? target : region, scroll) : { k: null }),
|
|
663
|
+
};
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
const placeName = (place: TargetPlace) => buildPlaceId({ kind: 'press', ...place }).slice('press:'.length);
|
|
667
|
+
|
|
668
|
+
export const describeTarget = (tag: number, screen = getScreenKey()): TargetDescription => {
|
|
669
|
+
const legacy = legacyTarget(tag);
|
|
670
|
+
const place = placeOf(hostFiber(tag), false, screen);
|
|
671
|
+
return {
|
|
672
|
+
...legacy,
|
|
673
|
+
name: placeName(place),
|
|
674
|
+
source: place.target.startsWith(OUTSIDE_APP + '@') ? 'outside' : 'client',
|
|
675
|
+
legacyName: legacy.name,
|
|
676
|
+
legacySource: legacy.source,
|
|
677
|
+
place,
|
|
678
|
+
};
|
|
679
|
+
};
|
|
680
|
+
|
|
441
681
|
export const findInTree = (
|
|
442
682
|
root: Fiber,
|
|
443
683
|
match: (fiber: Fiber) => unknown,
|
|
@@ -474,7 +714,9 @@ export type ContainerNameSource =
|
|
|
474
714
|
| 'nativeID'
|
|
475
715
|
| 'component'
|
|
476
716
|
| 'route'
|
|
477
|
-
| 'host'
|
|
717
|
+
| 'host'
|
|
718
|
+
| 'client'
|
|
719
|
+
| 'outside';
|
|
478
720
|
|
|
479
721
|
export interface ScrollContainer {
|
|
480
722
|
name: string;
|
|
@@ -482,6 +724,9 @@ export interface ScrollContainer {
|
|
|
482
724
|
hostType: string;
|
|
483
725
|
axis: ScrollAxis;
|
|
484
726
|
depth: number;
|
|
727
|
+
legacyName: string;
|
|
728
|
+
legacySource: ContainerNameSource;
|
|
729
|
+
place: TargetPlace;
|
|
485
730
|
}
|
|
486
731
|
|
|
487
732
|
const scrollAxisOf = (fiber: Fiber): ScrollAxis =>
|
|
@@ -511,10 +756,10 @@ const routeNameAbove = (fiber: Fiber): string | null => {
|
|
|
511
756
|
// -> ScrollView -> RCTScrollView), so testID/label/nativeID are read there first,
|
|
512
757
|
// then on the components above it up to the first one with a specific name,
|
|
513
758
|
// which is the fallback name.
|
|
514
|
-
const
|
|
759
|
+
const legacyScrollContainer = (
|
|
515
760
|
host: Fiber,
|
|
516
761
|
depth: number,
|
|
517
|
-
): ScrollContainer => {
|
|
762
|
+
): Omit<ScrollContainer, 'legacyName' | 'legacySource' | 'place'> => {
|
|
518
763
|
const hostType = host.type as string;
|
|
519
764
|
let testID: string | null = null;
|
|
520
765
|
let label: string | null = null;
|
|
@@ -560,32 +805,41 @@ const nameScrollContainer = (
|
|
|
560
805
|
return { ...base, name: hostType, source: 'host' };
|
|
561
806
|
};
|
|
562
807
|
|
|
808
|
+
export const nameScrollContainer = (candidate: ScrollHost, screen: ScreenKeyResolution): ScrollContainer => {
|
|
809
|
+
const host = currentFiber(candidate.host) ?? candidate.host;
|
|
810
|
+
const depth = candidate.depth;
|
|
811
|
+
const legacy = legacyScrollContainer(host, depth);
|
|
812
|
+
const place = placeOf(host, true, screen);
|
|
813
|
+
return {
|
|
814
|
+
...legacy,
|
|
815
|
+
name: placeName(place),
|
|
816
|
+
source: place.target.startsWith(OUTSIDE_APP + '@') ? 'outside' : 'client',
|
|
817
|
+
legacyName: legacy.name,
|
|
818
|
+
legacySource: legacy.source,
|
|
819
|
+
place,
|
|
820
|
+
};
|
|
821
|
+
};
|
|
822
|
+
|
|
563
823
|
// The scroll view a drag moved: the nearest scrollable ancestor of the touched
|
|
564
824
|
// view whose axis matches the gesture's main axis, else the nearest one. A
|
|
565
825
|
// horizontal swipe on a carousel inside a vertical feed names the carousel; a
|
|
566
826
|
// vertical swipe on the same spot names the feed. Scroll events carry no target
|
|
567
827
|
// tag, so the ancestor chain is the only link from the touch to the container.
|
|
568
|
-
export
|
|
569
|
-
host: Fiber
|
|
570
|
-
axis: ScrollAxis
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
for (let f
|
|
577
|
-
|
|
578
|
-
if (!isScrollHost(f)) {
|
|
579
|
-
continue;
|
|
580
|
-
}
|
|
581
|
-
if (axis === null || scrollAxisOf(f) === axis) {
|
|
582
|
-
return nameScrollContainer(f, depth);
|
|
583
|
-
}
|
|
584
|
-
if (nearest === null) {
|
|
585
|
-
nearest = f;
|
|
586
|
-
nearestDepth = depth;
|
|
587
|
-
}
|
|
588
|
-
depth++;
|
|
828
|
+
export interface ScrollHost {
|
|
829
|
+
host: Fiber;
|
|
830
|
+
axis: ScrollAxis;
|
|
831
|
+
depth: number;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
export const scrollHostsAbove = (host: Fiber | null): ScrollHost[] => {
|
|
835
|
+
const hosts: ScrollHost[] = [];
|
|
836
|
+
for (let f = host, n = 0; f && n < MAX_UP * 4; f = f.return, n++) {
|
|
837
|
+
if (isScrollHost(f)) hosts.push({ host: f, axis: scrollAxisOf(f), depth: hosts.length });
|
|
589
838
|
}
|
|
590
|
-
return
|
|
839
|
+
return hosts;
|
|
591
840
|
};
|
|
841
|
+
|
|
842
|
+
export const selectScrollHost = (
|
|
843
|
+
hosts: readonly ScrollHost[],
|
|
844
|
+
axis: ScrollAxis | null,
|
|
845
|
+
): ScrollHost | null => hosts.find(h => axis === null || h.axis === axis) ?? hosts[0] ?? null;
|
package/js/sdk/interactions.ts
CHANGED
|
@@ -25,7 +25,12 @@ import {
|
|
|
25
25
|
writeRunEvent,
|
|
26
26
|
} from './core';
|
|
27
27
|
import {
|
|
28
|
-
|
|
28
|
+
getScreenKey,
|
|
29
|
+
nameScrollContainer,
|
|
30
|
+
scrollHostsAbove,
|
|
31
|
+
selectScrollHost,
|
|
32
|
+
type ScreenKeyResolution,
|
|
33
|
+
type ScrollHost,
|
|
29
34
|
describeTarget,
|
|
30
35
|
type Fiber,
|
|
31
36
|
findInTree,
|
|
@@ -51,8 +56,9 @@ interface ActivePress {
|
|
|
51
56
|
key: string;
|
|
52
57
|
seq: number;
|
|
53
58
|
target: TargetDescription;
|
|
54
|
-
|
|
59
|
+
screen: ScreenKeyResolution;
|
|
55
60
|
container: ScrollContainer | null;
|
|
61
|
+
scrollHosts: ScrollHost[];
|
|
56
62
|
identifier: unknown;
|
|
57
63
|
touchStartT: number;
|
|
58
64
|
touchEndT: number | null;
|
|
@@ -216,7 +222,7 @@ const details = (press: ActivePress) => () => {
|
|
|
216
222
|
target: press.target,
|
|
217
223
|
...(press.kind === 'drag'
|
|
218
224
|
? {
|
|
219
|
-
touched: press.target.
|
|
225
|
+
touched: press.target.legacyName,
|
|
220
226
|
container: press.container,
|
|
221
227
|
nameSource: press.container
|
|
222
228
|
? `container.${press.container.source}`
|
|
@@ -281,25 +287,19 @@ const onTouchStart = (ne: Record<string, unknown>) => {
|
|
|
281
287
|
touchStartT,
|
|
282
288
|
);
|
|
283
289
|
}
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
tag: -1,
|
|
290
|
-
testID: null,
|
|
291
|
-
accessibilityLabel: null,
|
|
292
|
-
component: null,
|
|
293
|
-
actionable: false,
|
|
294
|
-
}
|
|
295
|
-
: describeTarget(tag);
|
|
290
|
+
const screen = getScreenKey();
|
|
291
|
+
const target = describeTarget(tag ?? -1, screen);
|
|
292
|
+
if (tag === null) target.legacyName = 'unknown';
|
|
293
|
+
const host = tag === null ? null : hostFiber(tag);
|
|
294
|
+
const scrollHosts = scrollHostsAbove(host);
|
|
296
295
|
seq++;
|
|
297
296
|
const press: ActivePress = {
|
|
298
297
|
key: `press#${seq}`,
|
|
299
298
|
seq,
|
|
300
299
|
target,
|
|
301
|
-
|
|
300
|
+
screen,
|
|
302
301
|
container: null,
|
|
302
|
+
scrollHosts,
|
|
303
303
|
identifier: ne.identifier,
|
|
304
304
|
touchStartT,
|
|
305
305
|
touchEndT: null,
|
|
@@ -323,6 +323,7 @@ const onTouchStart = (ne: Record<string, unknown>) => {
|
|
|
323
323
|
markers.start(name, {
|
|
324
324
|
key: press.key,
|
|
325
325
|
kind: 'press',
|
|
326
|
+
legacyName: `press:${target.legacyName}`,
|
|
326
327
|
at: touchStartT,
|
|
327
328
|
details: details(press),
|
|
328
329
|
});
|
|
@@ -418,20 +419,22 @@ const onTouchEnd = (ne: Record<string, unknown>, cancelled: boolean) => {
|
|
|
418
419
|
press.touchEndT = num(ne.timestamp) ?? now();
|
|
419
420
|
// A scroll view cancels the touch when it takes the gesture. On a list still moving from a fling it does so at once,
|
|
420
421
|
// before the finger passes the slop, so the move alone would call a fling a press on the row under the finger.
|
|
421
|
-
press.takenByScroll = cancelled &&
|
|
422
|
+
press.takenByScroll = cancelled && press.scrollHosts.length > 0;
|
|
422
423
|
const drag = press.maxMovePt >= DRAG_SLOP_PT || press.scrolled || press.takenByScroll;
|
|
423
424
|
press.kind = drag ? 'drag' : 'press';
|
|
424
425
|
if (drag) {
|
|
425
|
-
|
|
426
|
+
const axis = dragAxis(press);
|
|
427
|
+
const candidate = selectScrollHost(press.scrollHosts, axis);
|
|
428
|
+
press.container = candidate ? nameScrollContainer(candidate, press.screen) : null;
|
|
426
429
|
const name = dragName(press);
|
|
427
|
-
markers.rename(press.key, name);
|
|
430
|
+
markers.rename(press.key, name, `drag:${press.container?.legacyName ?? press.target.legacyName}`);
|
|
428
431
|
return;
|
|
429
432
|
}
|
|
430
433
|
if (cancelled) {
|
|
431
434
|
markers.point('touch.cancel');
|
|
432
435
|
}
|
|
433
436
|
const name = pressName(press);
|
|
434
|
-
if (!cancelled && isFaultTarget(name)) {
|
|
437
|
+
if (!cancelled && isFaultTarget({ name, legacyName: `press:${press.target.legacyName}`, target: press.target })) {
|
|
435
438
|
press.faultsRun = true;
|
|
436
439
|
runOpenFaults(name);
|
|
437
440
|
if (settings.faults.dupnet > 0) {
|
package/js/sdk/place.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
let placeLog: (message: string) => void = () => undefined;
|
|
2
|
+
export const setPlaceLog = (log: (message: string) => void) => { placeLog = log; };
|
|
3
|
+
export const logPlace = (message: string) => placeLog(message);
|
|
4
|
+
|
|
5
|
+
export interface PlaceKeyInput {
|
|
6
|
+
kind: 'screen' | 'launch' | 'press' | 'drag';
|
|
7
|
+
screen: string;
|
|
8
|
+
parent?: string | null;
|
|
9
|
+
target?: string | null;
|
|
10
|
+
k?: number | null;
|
|
11
|
+
visit?: number | 'repeat' | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Pinned to facts/components.cjs by the shared place-keys.json vectors.
|
|
15
|
+
export const placeSegment = (name: string, file: string) =>
|
|
16
|
+
`${name}@${encodeURIComponent(file).replace(/%2F/g, '/').replace(/%40/g, '@').replace(/~/g, '%7E')}`;
|
|
17
|
+
|
|
18
|
+
export const buildPlaceId = (parts: PlaceKeyInput): string => {
|
|
19
|
+
const id = parts.kind === 'screen' ? `screen:${parts.screen}#${parts.visit}`
|
|
20
|
+
: parts.kind === 'launch' ? `launch:${parts.screen}`
|
|
21
|
+
: `${parts.kind}:${parts.screen}›${parts.parent ? parts.parent + '›' : ''}${parts.target}${parts.k ? '~' + parts.k : ''}`;
|
|
22
|
+
if ((parts.kind === 'press' || parts.kind === 'drag') && id.includes('#')) logPlace('place unexpected # in input id');
|
|
23
|
+
if (id.length > 900) logPlace(`place long id length=${id.length}`);
|
|
24
|
+
return id;
|
|
25
|
+
};
|
package/js/sdk/profile-export.ts
CHANGED
|
@@ -52,6 +52,7 @@ const elementType = (tag: number) =>
|
|
|
52
52
|
export interface ExportFiber {
|
|
53
53
|
id: number;
|
|
54
54
|
name: string;
|
|
55
|
+
owner: string;
|
|
55
56
|
type: number;
|
|
56
57
|
// Component ancestors, nearest first.
|
|
57
58
|
ancestors: number[];
|
|
@@ -147,10 +148,12 @@ export const exportFiber = (
|
|
|
147
148
|
actualMs: number | null,
|
|
148
149
|
selfMs: number | null,
|
|
149
150
|
ancestors: number[] = ancestorsOf(fiber),
|
|
151
|
+
owner: string,
|
|
150
152
|
): ExportFiber => {
|
|
151
153
|
return {
|
|
152
154
|
id: fiberId(fiber),
|
|
153
155
|
name,
|
|
156
|
+
owner,
|
|
154
157
|
type: elementType(fiber.tag),
|
|
155
158
|
ancestors,
|
|
156
159
|
actualMs: actualMs ?? 0,
|
|
@@ -215,6 +218,17 @@ export const buildReactExport = (t0: number, commits: ExportCommit[]): ReactExpo
|
|
|
215
218
|
added.add(fiber.id);
|
|
216
219
|
}
|
|
217
220
|
const table = strings.flatMap(encodeString);
|
|
221
|
+
const ownerNames: string[] = [];
|
|
222
|
+
const ownerIndices = new Map<string, number>();
|
|
223
|
+
const byFiber = commit.fibers.map(fiber => {
|
|
224
|
+
let index = ownerIndices.get(fiber.owner);
|
|
225
|
+
if (index === undefined) {
|
|
226
|
+
index = ownerNames.length;
|
|
227
|
+
ownerNames.push(fiber.owner);
|
|
228
|
+
ownerIndices.set(fiber.owner, index);
|
|
229
|
+
}
|
|
230
|
+
return [fiber.id, index];
|
|
231
|
+
});
|
|
218
232
|
operations.push([RENDERER_ID, ROOT_ID, table.length, ...table, ...ops]);
|
|
219
233
|
commitData.push({
|
|
220
234
|
changeDescriptions: commit.fibers.map(f => [f.id, changeDescription(f.cause)]),
|
|
@@ -226,6 +240,7 @@ export const buildReactExport = (t0: number, commits: ExportCommit[]): ReactExpo
|
|
|
226
240
|
priorityLevel: null,
|
|
227
241
|
timestamp: Math.max(0, commit.t - t0),
|
|
228
242
|
keepsnappyCauses: commit.causes,
|
|
243
|
+
keepsnappyOwners: { names: ownerNames, byFiber },
|
|
229
244
|
keepsnappyCarrier: commit.carrier,
|
|
230
245
|
keepsnappyStore: commit.store,
|
|
231
246
|
keepsnappyTask: commit.task,
|