@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/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
- export const describeTarget = (tag: number): TargetDescription => {
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 nameScrollContainer = (
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 const describeScrollContainer = (
569
- host: Fiber | null,
570
- axis: ScrollAxis | null,
571
- ): ScrollContainer | null => {
572
- let nearest: Fiber | null = null;
573
- let nearestDepth = 0;
574
- let depth = 0;
575
- let steps = 0;
576
- for (let f: Fiber | null = host; f !== null && steps < MAX_UP * 4; f = f.return) {
577
- steps++;
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 nearest ? nameScrollContainer(nearest, nearestDepth) : null;
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;
@@ -25,7 +25,12 @@ import {
25
25
  writeRunEvent,
26
26
  } from './core';
27
27
  import {
28
- describeScrollContainer,
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
- host: Fiber | null;
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.name,
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 target: TargetDescription =
285
- tag === null
286
- ? {
287
- name: 'unknown',
288
- source: 'tag',
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
- host: tag === null ? null : hostFiber(tag),
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 && describeScrollContainer(press.host, null) !== null;
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
- press.container = describeScrollContainer(press.host, dragAxis(press));
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) {
@@ -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
+ };
@@ -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,