@symbiote-native/engine 0.5.0 → 1.0.0

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.
Files changed (91) hide show
  1. package/README.md +39 -14
  2. package/android/CMakeLists.txt +51 -0
  3. package/android/build.gradle +90 -0
  4. package/android/src/main/AndroidManifest.xml +1 -0
  5. package/android/src/main/cpp/SymbioteEngineJni.cpp +72 -0
  6. package/android/src/main/java/dev/symbiotenative/engine/SymbioteEngineModule.kt +43 -0
  7. package/android/src/main/java/dev/symbiotenative/engine/SymbioteEnginePackage.kt +35 -0
  8. package/build/accessibility-info/shared.js +1 -1
  9. package/build/accessibility-props.d.ts +1 -8
  10. package/build/accessibility-props.js +13 -16
  11. package/build/animated/animations/composition.d.ts +1 -1
  12. package/build/animated/animations/composition.js +18 -4
  13. package/build/animated/easing.d.ts +3 -2
  14. package/build/animated/easing.js +17 -88
  15. package/build/animated/event.js +6 -1
  16. package/build/animated/host-binding.d.ts +1 -1
  17. package/build/animated/host-binding.js +19 -4
  18. package/build/animated/index.d.ts +1 -1
  19. package/build/animated/mock.d.ts +1 -19
  20. package/build/animated/props.js +1 -1
  21. package/build/animated/rgba.js +16 -50
  22. package/build/events/index.js +88 -40
  23. package/build/fabric-props.d.ts +1 -1
  24. package/build/fabric-props.js +116 -184
  25. package/build/fabric.d.ts +9 -0
  26. package/build/fabric.js +32 -0
  27. package/build/host-access.d.ts +125 -0
  28. package/build/host-access.js +280 -0
  29. package/build/host-behavior.d.ts +84 -21
  30. package/build/host-behavior.js +196 -30
  31. package/build/image-source-write.d.ts +16 -0
  32. package/build/image-source-write.js +65 -0
  33. package/build/imperative.d.ts +49 -0
  34. package/build/imperative.js +258 -0
  35. package/build/index.d.ts +14 -7
  36. package/build/index.js +53 -10
  37. package/build/mutation-buffer.d.ts +222 -0
  38. package/build/mutation-buffer.js +491 -0
  39. package/build/native-engine.d.ts +182 -0
  40. package/build/native-engine.js +178 -0
  41. package/build/native-tree-host.d.ts +25 -0
  42. package/build/native-tree-host.js +66 -0
  43. package/build/node.d.ts +172 -57
  44. package/build/node.js +839 -383
  45. package/build/pan-responder/index.js +27 -52
  46. package/build/platform-color/index.d.ts +1 -1
  47. package/build/platform-color/index.js +11 -4
  48. package/build/process-background-image/index.js +30 -566
  49. package/build/process-background-longhands.d.ts +4 -0
  50. package/build/process-background-longhands.js +44 -0
  51. package/build/process-box-shadow/index.js +23 -187
  52. package/build/process-filter.js +27 -300
  53. package/build/process-transform/index.d.ts +1 -1
  54. package/build/process-transform/index.js +25 -107
  55. package/build/process-transform-origin/index.d.ts +1 -1
  56. package/build/process-transform-origin/index.js +29 -102
  57. package/build/registry.d.ts +36 -0
  58. package/build/registry.js +73 -0
  59. package/build/sound-manager/index.d.ts +3 -0
  60. package/build/sound-manager/index.js +36 -0
  61. package/build/structured-style.d.ts +10 -0
  62. package/build/structured-style.js +180 -0
  63. package/build/style-registry/index.d.ts +14 -0
  64. package/build/style-registry/index.js +60 -11
  65. package/build/surface.d.ts +31 -2
  66. package/build/surface.js +138 -56
  67. package/build/text-input-state.d.ts +1 -0
  68. package/build/text-input-state.js +17 -3
  69. package/build/tree-host.d.ts +307 -0
  70. package/build/tree-host.js +211 -0
  71. package/build/view-config.js +4 -4
  72. package/codegen-specs/NativeSymbioteEngine.ts +27 -0
  73. package/cpp/SymbioteDebug.cpp +51 -0
  74. package/cpp/SymbioteDebug.h +54 -0
  75. package/cpp/SymbioteEngineBindings.cpp +232 -0
  76. package/cpp/SymbioteEngineBindings.h +59 -0
  77. package/cpp/SymbioteFabricProps.cpp +2619 -0
  78. package/cpp/SymbioteFabricProps.h +223 -0
  79. package/cpp/SymbioteTree.cpp +2478 -0
  80. package/cpp/SymbioteTree.h +257 -0
  81. package/ios/SymbioteEngineModule.h +25 -0
  82. package/ios/SymbioteEngineModule.mm +44 -0
  83. package/package.json +31 -3
  84. package/react-native.config.cjs +23 -0
  85. package/symbiote-engine.podspec +42 -0
  86. package/build/animated/bezier.d.ts +0 -1
  87. package/build/animated/bezier.js +0 -102
  88. package/build/commit.d.ts +0 -49
  89. package/build/commit.js +0 -1058
  90. package/build/tags.d.ts +0 -2
  91. package/build/tags.js +0 -40
@@ -14,7 +14,7 @@
14
14
  // and registers the key path with the stock native module, so the event drives
15
15
  // the view with zero JS per event.
16
16
  import { dlog } from '../debug.js';
17
- import { getNativeTag, whenCommitted } from '../commit.js';
17
+ import { getNativeTag, whenCommitted } from '../imperative.js';
18
18
  import { isSymbioteNode } from '../node.js';
19
19
  import { AnimatedNode, flushValue } from './graph.js';
20
20
  import { isNativeAnimatedAvailable, nativeAnimated, } from './native/native-animated.js';
@@ -45,6 +45,11 @@ function collectMappedValues(mapping, path, out) {
45
45
  }
46
46
  }
47
47
  }
48
+ // TODO(rn-parity, low priority): vendor's `AnimatedEvent.js` has `validateMapping`, a
49
+ // `__DEV__`-only check that throws when an event's actual shape doesn't match the argMapping
50
+ // structure. We have no equivalent — `extractAtPath` below silently no-ops on a mismatch instead.
51
+ // Judged non-actionable (misuse-detection guard, no runtime-visible effect for a correct mapping,
52
+ // same class as the `numColumns` invariant) — see the audit skill for the full reasoning.
48
53
  // Pull the numeric field at `path` out of one event argument. Returns undefined
49
54
  // when the path is absent or the leaf is not a number, so a malformed event is a
50
55
  // no-op rather than a throw inside an event dispatch.
@@ -3,7 +3,7 @@ export declare function hasAnimatedBindings(): boolean;
3
3
  /**
4
4
  * Give a node a behavior-owned animated style layer, or drop it by passing `undefined`.
5
5
  *
6
- * The seam a lowered `TouchableOpacity` needs: RN runs its press fade from an `Animated.View` whose
6
+ * The seam `<touchable-opacity>` needs: RN runs its press fade from an `Animated.View` whose
7
7
  * style is `[props.style, {opacity: anim}]` (TouchableOpacity.js:302), and a tag has no such
8
8
  * wrapper. The layer is bound to the leaf, never folded into `node.props.style` — so the behavior
9
9
  * can still read the AUTHOR's resting opacity back without seeing its own fade.
@@ -31,6 +31,7 @@ import { AnimatedStyle } from './style.js';
31
31
  import { attachNativeEventHandler } from './event.js';
32
32
  import { createAnimatedLeafLifecycle, } from './leaf-lifecycle.js';
33
33
  import { setProp } from '../node.js';
34
+ import { propOf, subtreesOf } from '../host-access.js';
34
35
  const bindings = new WeakMap();
35
36
  // Nodes the commit sweep tore down, whether or not they carried a binding — the twin of
36
37
  // host-behavior.ts's `tornDown`, and marked for every node for the same reason: the node a
@@ -124,7 +125,7 @@ function withBehaviorStyle(node, style) {
124
125
  /**
125
126
  * Give a node a behavior-owned animated style layer, or drop it by passing `undefined`.
126
127
  *
127
- * The seam a lowered `TouchableOpacity` needs: RN runs its press fade from an `Animated.View` whose
128
+ * The seam `<touchable-opacity>` needs: RN runs its press fade from an `Animated.View` whose
128
129
  * style is `[props.style, {opacity: anim}]` (TouchableOpacity.js:302), and a tag has no such
129
130
  * wrapper. The layer is bound to the leaf, never folded into `node.props.style` — so the behavior
130
131
  * can still read the AUTHOR's resting opacity back without seeing its own fade.
@@ -138,7 +139,7 @@ export function setAnimatedBehaviorStyle(node, style) {
138
139
  }
139
140
  // Re-register against the style standing right now, so the pair the leaf holds is always
140
141
  // (author, layer) whichever of the two moved last.
141
- bindAnimatedValue(node, 'style', node.props.style);
142
+ bindAnimatedValue(node, 'style', propOf(node, 'style'));
142
143
  }
143
144
  /**
144
145
  * Resolve an animated value in a prop, returning what should be published for it.
@@ -251,6 +252,22 @@ export function detachAnimatedProps(node) {
251
252
  * runs ~9 000 times per benchmark create.
252
253
  */
253
254
  export function reattachAnimatedProps(node) {
255
+ if (!parked.has(node))
256
+ return;
257
+ // FLAT, one host read for the whole subtree — the twin of `reattachSubtree` in `host-behavior.ts`,
258
+ // which was converted when the teardown sweep turned out to be paying a JSI crossing per node.
259
+ // This one was missed then. It is reached far less often (the guard above is a WeakSet miss for
260
+ // every node of a freshly built tree) but costs the same per node when it does run, and a parked
261
+ // subtree is a whole screen's worth in the case it exists for — Svelte parking live nodes
262
+ // offscreen across commits.
263
+ //
264
+ // The recursion stopped at an unparked node where this skips it and carries on; the two agree
265
+ // because `parked` is filled by `detachAnimatedProps` over a whole detached subtree, so a node in
266
+ // it has its descendants in it.
267
+ for (const each of subtreesOf([node]))
268
+ reattachOneAnimated(each);
269
+ }
270
+ function reattachOneAnimated(node) {
254
271
  if (!parked.has(node))
255
272
  return;
256
273
  parked.delete(node);
@@ -258,6 +275,4 @@ export function reattachAnimatedProps(node) {
258
275
  if (binding !== undefined)
259
276
  reconcile(node, binding);
260
277
  reattachAnimatedEvents(node);
261
- for (const child of node.children)
262
- reattachAnimatedProps(child);
263
278
  }
@@ -4,7 +4,7 @@ export { AnimatedValueXY, type IValueXY } from './value-xy';
4
4
  export { AnimatedAddition, AnimatedSubtraction, AnimatedMultiplication, AnimatedDivision, AnimatedModulo, AnimatedDiffClamp, add, subtract, multiply, divide, modulo, diffClamp, } from './operators';
5
5
  export { AnimatedEvent, event, forkEvent, unforkEvent, attachNativeEvent, attachNativeEventHandler, type IEventConfig, type IEventListener, type IAnimatedEventHandler, type INativeEventAttachment, } from './event';
6
6
  export { AnimatedColor, type IRgbaValue, type IColorInput } from './color';
7
- export { Easing, type IEasingFunction } from './easing';
7
+ export { Easing, type IEasing, type IEasingFunction } from './easing';
8
8
  export { createNumericInterpolation, createInterpolation, checkValidRanges, type IInterpolationConfig, type IExtrapolateType, } from './interpolation';
9
9
  export type { IAnimation, IEndCallback, IEndResult } from './animation';
10
10
  export { TimingAnimation } from './animations/timing';
@@ -16,25 +16,7 @@ export declare const AnimatedMock: {
16
16
  Value: typeof AnimatedValue;
17
17
  ValueXY: typeof AnimatedValueXY;
18
18
  Color: typeof AnimatedColor;
19
- Easing: {
20
- step0(n: number): number;
21
- step1(n: number): number;
22
- linear(t: number): number;
23
- ease(t: number): number;
24
- quad(t: number): number;
25
- cubic(t: number): number;
26
- poly(n: number): import("./easing").IEasingFunction;
27
- sin(t: number): number;
28
- circle(t: number): number;
29
- exp(t: number): number;
30
- elastic(bounciness?: number): import("./easing").IEasingFunction;
31
- back(s?: number): import("./easing").IEasingFunction;
32
- bounce(t: number): number;
33
- bezier(x1: number, y1: number, x2: number, y2: number): import("./easing").IEasingFunction;
34
- in(easing: import("./easing").IEasingFunction): import("./easing").IEasingFunction;
35
- out(easing: import("./easing").IEasingFunction): import("./easing").IEasingFunction;
36
- inOut(easing: import("./easing").IEasingFunction): import("./easing").IEasingFunction;
37
- };
19
+ Easing: import("./easing").IEasing;
38
20
  timing: typeof timing;
39
21
  spring: typeof spring;
40
22
  decay: typeof decay;
@@ -12,7 +12,7 @@
12
12
  // a `update` function off the node, and under useDefineForClassFields a field
13
13
  // initialiser would shadow it and break subclassing. (See shared graph.ts leafUpdate.)
14
14
  import { AnimatedNode, AnimatedWithChildren } from './graph.js';
15
- import { setNativeProps, getNativeTag } from '../commit.js';
15
+ import { setNativeProps, getNativeTag } from '../imperative.js';
16
16
  import { isSymbioteNode } from '../node.js';
17
17
  import { registerPostCommit } from '../post-commit.js';
18
18
  import { nativeAnimated, } from './native/native-animated.js';
@@ -8,45 +8,25 @@
8
8
  // which surfaces at load time as `TypeError: Class extends value undefined is not a
9
9
  // constructor` — AnimatedWithChildren is still uninitialized when color.ts evaluates. Keeping
10
10
  // the pure half in its own leaf module breaks it structurally rather than by import ordering.
11
+ // No types are published for this package, and an ambient .d.ts is not picked up by another
12
+ // package's separate TS program (see symbiote-rn-import-testability) - so the suppression is
13
+ // local, at the import site.
14
+ // @ts-expect-error - untyped, @noflow plain JS that every toolchain here parses as-is.
15
+ import normalizeColorUpstream from '@react-native/normalize-colors';
11
16
  export const DEFAULT_COLOR = { r: 0, g: 0, b: 0, a: 1 };
12
- // Decompose a #hex (3/4/6/8), rgb()/rgba(), or 0xRRGGBBAA number into channels.
13
- // undefined when unparseable (a named/platform color), so the caller falls back to
14
- // the default rather than throwing inside a render. Exported so interpolation's
15
- // color path parses through the same RGBA decoder (DRY) rather than duplicating it.
17
+ // Decompose any color RN itself accepts into channels: #hex, rgb()/rgba(), hsl()/hsla(), hwb(),
18
+ // the 150 CSS names, `transparent`, and a 0xRRGGBBAA number. undefined when unparseable, so the
19
+ // caller falls back to the default rather than throwing inside a render. Exported so
20
+ // interpolation's color path decodes through this one decoder rather than duplicating it.
21
+ //
22
+ // The parsing is upstream's. A hand-rolled version lived here and knew hex and rgb() only, so
23
+ // `new AnimatedColor('red')` animated from BLACK - the fallback, not an error, and nothing was
24
+ // red anywhere. Its packed output is rrggbbaa, which is the order the shifts below assume.
16
25
  export function normalizeColor(color) {
17
- if (typeof color === 'number') {
18
- const c = color >>> 0;
19
- return {
20
- r: (c >>> 24) & 255,
21
- g: (c >>> 16) & 255,
22
- b: (c >>> 8) & 255,
23
- a: (c & 255) / 255,
24
- };
25
- }
26
- const trimmed = color.trim();
27
- if (trimmed.startsWith('#'))
28
- return parseHex(trimmed);
29
- if (/^rgba?\(/i.test(trimmed))
30
- return parseRgb(trimmed);
31
- return undefined;
32
- }
33
- function parseHex(hex) {
34
- let body = hex.slice(1);
35
- if (body.length === 3 || body.length === 4) {
36
- body = body
37
- .split('')
38
- .map(c => c + c)
39
- .join('');
40
- }
41
- if (body.length !== 6 && body.length !== 8)
26
+ const packed = normalizeColorUpstream(color);
27
+ if (typeof packed !== 'number')
42
28
  return undefined;
43
- const int = Number.parseInt(body, 16);
44
- if (Number.isNaN(int))
45
- return undefined;
46
- if (body.length === 6) {
47
- return { r: (int >>> 16) & 255, g: (int >>> 8) & 255, b: int & 255, a: 1 };
48
- }
49
- const c = int >>> 0;
29
+ const c = packed >>> 0;
50
30
  return {
51
31
  r: (c >>> 24) & 255,
52
32
  g: (c >>> 16) & 255,
@@ -54,17 +34,3 @@ function parseHex(hex) {
54
34
  a: (c & 255) / 255,
55
35
  };
56
36
  }
57
- function parseRgb(str) {
58
- const match = /^rgba?\(([^)]+)\)$/i.exec(str);
59
- if (match === null)
60
- return undefined;
61
- const parts = match[1].split(',').map(part => Number.parseFloat(part.trim()));
62
- if (parts.length < 3 || parts.some(n => Number.isNaN(n)))
63
- return undefined;
64
- return {
65
- r: parts[0],
66
- g: parts[1],
67
- b: parts[2],
68
- a: parts.length >= 4 ? parts[3] : 1,
69
- };
70
- }
@@ -5,11 +5,13 @@
5
5
  // node it started on (or a descendant). Bubbling events walk target -> root,
6
6
  // invoking each ancestor's listener until one calls stopPropagation. Layout is a
7
7
  // direct event in RN and is delivered only to its own target.
8
- import { dlog } from '../debug.js';
8
+ import { dlog, isDebug } from '../debug.js';
9
9
  import { runWrapped } from '../dispatch.js';
10
10
  import { getSlot } from '../fabric.js';
11
- import { committedOf, isAnchor, isSymbioteNode, } from '../node.js';
11
+ import { isAnchor, isSymbioteNode, } from '../node.js';
12
12
  import { registeredNativeEvent } from '../registry.js';
13
+ import { ancestorsOf, parentOf } from '../host-access.js';
14
+ import { setIsJSResponder } from '../imperative.js';
13
15
  import { attachTouchHistory, recordTouchTrack, resetTouchHistory, touchHistory, } from '../touch-history.js';
14
16
  import { isRecord } from '../type-guards.js';
15
17
  // Raw Fabric event -> listener name, split by dispatch phase. Press is synthesized
@@ -168,7 +170,7 @@ function hasRemainingTouchWithin(owner, nativeEvent) {
168
170
  // Whether any node from `target` up to the root listens for `listenerName`, used to
169
171
  // arm the long-press timer only when a handler would actually receive it.
170
172
  function hasListenerInPath(target, listenerName) {
171
- for (let node = target; node; node = node.parent) {
173
+ for (let node = target; node; node = parentOf(node)) {
172
174
  if (node.listeners?.has(listenerName) === true)
173
175
  return true;
174
176
  }
@@ -190,17 +192,20 @@ function callOwnListener(node, listenerName, nativeEvent) {
190
192
  }
191
193
  // The node chain from `from` up to the root, deepest first. The single allocation
192
194
  // the two-phase walk indexes both ways (capture reads it reversed).
195
+ //
196
+ // ONE CROSSING, not one per level. This used to climb `parentOf`, which crosses the host boundary
197
+ // each step — 18 questions per event on a depth-8 chain, and again on every frame of every drag for
198
+ // the responder's scope. The host holds the tree, so the chain is one answer it can give: the same
199
+ // argument `parentsOf` / `subtreesOf` won for the teardown sweep (F-2) and `nextSiblingOf` for Vue's
200
+ // quadratic (F-19).
193
201
  function pathToRoot(from) {
194
- const path = [];
195
- for (let node = from; node; node = node.parent)
196
- path.push(node);
197
- return path;
202
+ return [...ancestorsOf(from)];
198
203
  }
199
204
  // Depth of a node below the root (root = 0). Aligns two nodes before the lockstep
200
205
  // climb to their lowest common ancestor.
201
206
  function depthOf(node) {
202
207
  let depth = 0;
203
- for (let n = node.parent; n; n = n.parent)
208
+ for (let n = parentOf(node); n; n = parentOf(n))
204
209
  depth++;
205
210
  return depth;
206
211
  }
@@ -213,18 +218,18 @@ function lowestCommonAncestor(a, b) {
213
218
  let na = a;
214
219
  let nb = b;
215
220
  while (na && da > db) {
216
- na = na.parent;
221
+ na = parentOf(na);
217
222
  da--;
218
223
  }
219
224
  while (nb && db > da) {
220
- nb = nb.parent;
225
+ nb = parentOf(nb);
221
226
  db--;
222
227
  }
223
228
  while (na && nb) {
224
229
  if (na === nb)
225
230
  return na;
226
- na = na.parent;
227
- nb = nb.parent;
231
+ na = parentOf(na);
232
+ nb = parentOf(nb);
228
233
  }
229
234
  return undefined;
230
235
  }
@@ -261,15 +266,14 @@ function findWantsResponder(path, captureName, bubbleName, nativeEvent, skip) {
261
266
  //
262
267
  // `blockNativeResponder` is the taker's own `onResponderGrant` return, exactly as RN reads it.
263
268
  function handOverNativeResponder(from, to, blockNativeResponder) {
264
- const slot = getSlot();
265
- const fromHandle = from === undefined ? undefined : committedOf(from)?.handle;
266
- const toHandle = to === undefined ? undefined : committedOf(to)?.handle;
267
- dlog(`setIsJSResponder from=${from === undefined ? 'none' : fromHandle === undefined ? 'UNCOMMITTED' : 'yes'} ` +
268
- `to=${to === undefined ? 'none' : toHandle === undefined ? 'UNCOMMITTED' : 'yes'} block=${blockNativeResponder}`);
269
- if (fromHandle !== undefined)
270
- slot.setIsJSResponder(fromHandle, false, blockNativeResponder);
271
- if (toHandle !== undefined)
272
- slot.setIsJSResponder(toHandle, true, blockNativeResponder);
269
+ if (isDebug()) {
270
+ dlog(`setIsJSResponder from=${from === undefined ? 'none' : 'yes'} ` +
271
+ `to=${to === undefined ? 'none' : 'yes'} block=${blockNativeResponder}`);
272
+ }
273
+ if (from !== undefined)
274
+ setIsJSResponder(from, false, blockNativeResponder);
275
+ if (to !== undefined)
276
+ setIsJSResponder(to, true, blockNativeResponder);
273
277
  }
274
278
  // Whether the taker asked native to stand down. RN reads this off the grant dispatch's return;
275
279
  // an absent listener means no claim, which is RN's `blockNativeResponder || false`.
@@ -286,12 +290,33 @@ function negotiateResponder(target, phase, nativeEvent) {
286
290
  // the responder) and skips the deepest node when it IS the responder (Responder-
287
291
  // EventPlugin.setResponderAndExtractTransfer). At touch start currentResponder is
288
292
  // cleared, so this collapses to the plain target->root start walk.
293
+ //
294
+ // ONE WALK, and the common case never needs a second. `pathToRoot` crosses the host boundary per
295
+ // level, and the previous shape paid `lowestCommonAncestor` — `depthOf(a)` plus `depthOf(b)` plus
296
+ // a lockstep climb — BEFORE walking again from the answer. Measured on a depth-8 chain: 19
297
+ // crossings per move, of which 18 were the ancestor search, on a path that runs 60 times a second
298
+ // for the whole of a drag (`__tests__/touch-crossing-cost.test.ts`).
299
+ //
300
+ // The scope RN computes is the lowest common ancestor of responder and target, and while a finger
301
+ // stays inside the view that claimed it — which is what a drag IS — the responder is an ANCESTOR
302
+ // of the target, so the LCA is the responder itself and it is already in the target's own path.
303
+ // An index lookup answers it with nothing crossed, and the scoped path is that path's suffix.
304
+ // The genuine cross-subtree case (the finger left the responder's subtree) still falls back to
305
+ // the full search, and it is a rare frame rather than every frame.
306
+ const targetPath = pathToRoot(target);
307
+ const heldAt = currentResponder === undefined ? -1 : targetPath.indexOf(currentResponder);
289
308
  const from = currentResponder === undefined
290
309
  ? target
291
- : lowestCommonAncestor(currentResponder, target);
310
+ : heldAt >= 0
311
+ ? currentResponder
312
+ : lowestCommonAncestor(currentResponder, target);
292
313
  if (!from)
293
314
  return;
294
- const path = pathToRoot(from);
315
+ const path = from === target
316
+ ? targetPath
317
+ : heldAt >= 0
318
+ ? targetPath.slice(heldAt)
319
+ : pathToRoot(from);
295
320
  const skip = from === currentResponder ? from : undefined;
296
321
  const wants = phase === 'start'
297
322
  ? findWantsResponder(path, START_SHOULD_SET_CAPTURE, START_SHOULD_SET, nativeEvent, skip)
@@ -299,16 +324,20 @@ function negotiateResponder(target, phase, nativeEvent) {
299
324
  // Every exit is logged: a negotiation that declines is indistinguishable from one that never
300
325
  // ran, and the two have opposite causes.
301
326
  if (!wants) {
302
- dlog(`responder ${phase}: nobody wants it (path=${path.length}${skip === undefined ? '' : ', one skipped'})`);
327
+ if (isDebug()) {
328
+ dlog(`responder ${phase}: nobody wants it (path=${path.length}${skip === undefined ? '' : ', one skipped'})`);
329
+ }
303
330
  return;
304
331
  }
305
332
  if (wants === currentResponder) {
306
- dlog(`responder ${phase}: ${wants.component} already holds it`);
333
+ if (isDebug())
334
+ dlog(`responder ${phase}: ${wants.component} already holds it`);
307
335
  return;
308
336
  }
309
337
  if (currentResponder === undefined) {
310
338
  currentResponder = wants;
311
- dlog(`responder granted to ${wants.component}`);
339
+ if (isDebug())
340
+ dlog(`responder granted to ${wants.component}`);
312
341
  const granted = callOwnListener(wants, RESPONDER_GRANT, nativeEvent);
313
342
  handOverNativeResponder(undefined, wants, blocksNative(granted));
314
343
  return;
@@ -328,14 +357,16 @@ function negotiateResponder(target, phase, nativeEvent) {
328
357
  // visible no-op event with no behavioral counterpart. We therefore fire grant
329
358
  // before terminate on the consent path (matching RN's grant<terminate ordering) and
330
359
  // omit it on reject; the consent OUTCOME is unchanged either way.
331
- dlog(`responder transferred ${incumbent.component} -> ${wants.component}`);
360
+ if (isDebug())
361
+ dlog(`responder transferred ${incumbent.component} -> ${wants.component}`);
332
362
  const granted = callOwnListener(wants, RESPONDER_GRANT, nativeEvent);
333
363
  callOwnListener(incumbent, RESPONDER_TERMINATE, nativeEvent);
334
364
  currentResponder = wants;
335
365
  handOverNativeResponder(incumbent, wants, blocksNative(granted));
336
366
  }
337
367
  else {
338
- dlog(`responder takeover of ${incumbent.component} rejected`);
368
+ if (isDebug())
369
+ dlog(`responder takeover of ${incumbent.component} rejected`);
339
370
  callOwnListener(wants, RESPONDER_REJECT, nativeEvent);
340
371
  }
341
372
  }
@@ -347,7 +378,10 @@ export function installEventHandler() {
347
378
  if (!isSymbioteNode(instanceHandle))
348
379
  return;
349
380
  if (topLevelType === TOUCH_START) {
350
- dlog(`event ${TOUCH_START} on ${isSymbioteNode(instanceHandle) ? instanceHandle.component : 'NON-NODE'}`);
381
+ // The ternary is gone with the gate: `isSymbioteNode(instanceHandle)` was re-asked here
382
+ // three lines after the early return above already proved it.
383
+ if (isDebug())
384
+ dlog(`event ${TOUCH_START} on ${instanceHandle.component}`);
351
385
  // Update the touch bank, then attach it so responder handlers (PanResponder)
352
386
  // read each touch's own previous->current delta; RN records before dispatch.
353
387
  recordTouchTrack('start', nativeEvent);
@@ -461,7 +495,8 @@ export function installEventHandler() {
461
495
  bubble(press.owner, PRESS_OUT, nativeEvent);
462
496
  }
463
497
  if (!hadActivePress) {
464
- dlog(`event ${TOUCH_END} ignored (no matching start)`);
498
+ if (isDebug())
499
+ dlog(`event ${TOUCH_END} ignored (no matching start)`);
465
500
  }
466
501
  else if (completedPresses.length === 0) {
467
502
  dlog('press retained (another touch remains inside its owner)');
@@ -520,13 +555,17 @@ export function installEventHandler() {
520
555
  }
521
556
  const direct = DIRECT_EVENTS[topLevelType];
522
557
  if (direct !== undefined) {
523
- dlog(`event ${topLevelType} -> ${direct} (direct)`);
558
+ if (isDebug()) {
559
+ dlog(`event ${topLevelType} -> ${direct} (direct)`);
560
+ }
524
561
  runWrapped(() => deliverDirect(instanceHandle, direct, nativeEvent));
525
562
  return;
526
563
  }
527
564
  const bubbling = BUBBLING_EVENTS[topLevelType];
528
565
  if (bubbling !== undefined) {
529
- dlog(`event ${topLevelType} -> ${bubbling} (bubble)`);
566
+ if (isDebug()) {
567
+ dlog(`event ${topLevelType} -> ${bubbling} (bubble)`);
568
+ }
530
569
  runWrapped(() => bubble(instanceHandle, bubbling, nativeEvent));
531
570
  return;
532
571
  }
@@ -536,8 +575,10 @@ export function installEventHandler() {
536
575
  // bubble: same split as the built-ins.
537
576
  const registered = registeredNativeEvent(instanceHandle.component, topLevelType);
538
577
  if (registered !== undefined) {
539
- const phase = registered.direct ? 'direct' : 'bubble';
540
- dlog(`event ${topLevelType} -> ${registered.listener} (${phase}, registered)`);
578
+ if (isDebug()) {
579
+ const phase = registered.direct ? 'direct' : 'bubble';
580
+ dlog(`event ${topLevelType} -> ${registered.listener} (${phase}, registered)`);
581
+ }
541
582
  runWrapped(() => registered.direct
542
583
  ? deliverDirect(instanceHandle, registered.listener, nativeEvent)
543
584
  : bubble(instanceHandle, registered.listener, nativeEvent));
@@ -547,7 +588,9 @@ export function installEventHandler() {
547
588
  // config. A permanent diagnostic seam: if a native view fires something we drop
548
589
  // on the floor (an event the ViewConfig didn't surface, or a name mismatch),
549
590
  // this is where it shows up. Keeps "the handler silently did nothing" debuggable.
550
- dlog(`event ${topLevelType} UNMATCHED on ${instanceHandle.component} (dropped)`);
591
+ if (isDebug()) {
592
+ dlog(`event ${topLevelType} UNMATCHED on ${instanceHandle.component} (dropped)`);
593
+ }
551
594
  });
552
595
  }
553
596
  // A press is honest only if the touch ends on the node it started on, or a
@@ -559,7 +602,7 @@ function endsWithin(endTarget, start) {
559
602
  while (node) {
560
603
  if (node === start)
561
604
  return true;
562
- node = node.parent;
605
+ node = parentOf(node);
563
606
  }
564
607
  return false;
565
608
  }
@@ -591,7 +634,8 @@ function bubble(target, listenerName, nativeEvent) {
591
634
  ? undefined
592
635
  : node.listeners?.get(captureName);
593
636
  if (listener) {
594
- dlog(`event ${listenerName} capture on ${node.component}`);
637
+ if (isDebug())
638
+ dlog(`event ${listenerName} capture on ${node.component}`);
595
639
  listener({
596
640
  type: listenerName,
597
641
  target,
@@ -605,8 +649,13 @@ function bubble(target, listenerName, nativeEvent) {
605
649
  }
606
650
  // Bubble phase: target -> root, invoking each ancestor's plain listener. Anchors are
607
651
  // transparent here too, same reason (see the capture-phase comment above).
608
- let node = target;
609
- while (node) {
652
+ //
653
+ // THE SAME ARRAY, read forward. This used to re-walk `parentOf` from the target, which is a
654
+ // CROSSING of the host boundary per level — so one event asked the host for its ancestors twice,
655
+ // measured at 18 questions on a depth-8 chain where 9 is the floor. The comment above already
656
+ // says the path is built once "without a second allocation"; the second WALK is the expensive
657
+ // half, and it was the one left in. Every touch and every frame of every drag pays it.
658
+ for (const node of path) {
610
659
  const listener = isAnchor(node)
611
660
  ? undefined
612
661
  : node.listeners?.get(listenerName);
@@ -622,7 +671,6 @@ function bubble(target, listenerName, nativeEvent) {
622
671
  if (stopped)
623
672
  return;
624
673
  }
625
- node = node.parent;
626
674
  }
627
675
  }
628
676
  // Direct (non-bubbling) delivery: only the target's own listener fires.
@@ -1,3 +1,3 @@
1
1
  import type { IFabricProps } from './fabric';
2
2
  import { type ISymbioteNode } from './node';
3
- export declare function fabricProps(node: ISymbioteNode): IFabricProps;
3
+ export declare function fabricProps(node: ISymbioteNode, nodeProps: Readonly<Record<string, unknown>>): IFabricProps;