@symbiote-native/engine 1.3.0 → 1.4.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 (79) hide show
  1. package/build/accessibility-props.d.ts +0 -11
  2. package/build/accessibility-props.js +30 -68
  3. package/build/animated/graph.js +1 -1
  4. package/build/animated/leaf-lifecycle.js +2 -2
  5. package/build/asset-source-resolver.d.ts +2 -0
  6. package/build/asset-source-resolver.js +13 -0
  7. package/build/back-handler/index.d.ts +1 -5
  8. package/build/back-handler/index.js +0 -6
  9. package/build/debug.js +8 -22
  10. package/build/dispatch.js +3 -9
  11. package/build/events/delivery.d.ts +9 -0
  12. package/build/events/delivery.js +143 -0
  13. package/build/events/index.js +199 -660
  14. package/build/events/names.d.ts +24 -0
  15. package/build/events/names.js +80 -0
  16. package/build/events/press.d.ts +20 -0
  17. package/build/events/press.js +89 -0
  18. package/build/events/responder.d.ts +6 -0
  19. package/build/events/responder.js +124 -0
  20. package/build/fabric-props.js +74 -179
  21. package/build/fabric.d.ts +0 -13
  22. package/build/fabric.js +18 -38
  23. package/build/host-access.d.ts +1 -128
  24. package/build/host-access.js +96 -205
  25. package/build/host-behavior.d.ts +0 -100
  26. package/build/host-behavior.js +125 -311
  27. package/build/image-loader.js +10 -23
  28. package/build/image-source-resolver.js +3 -7
  29. package/build/image-source-write.d.ts +0 -11
  30. package/build/image-source-write.js +14 -34
  31. package/build/imperative.d.ts +2 -28
  32. package/build/imperative.js +60 -93
  33. package/build/index.d.ts +6 -2
  34. package/build/index.js +29 -39
  35. package/build/mutation-buffer.d.ts +3 -177
  36. package/build/mutation-buffer.js +162 -316
  37. package/build/native-engine.d.ts +6 -102
  38. package/build/native-engine.js +60 -141
  39. package/build/native-events.js +9 -18
  40. package/build/native-tree-host.d.ts +0 -21
  41. package/build/native-tree-host.js +15 -31
  42. package/build/node-events.d.ts +11 -0
  43. package/build/node-events.js +145 -0
  44. package/build/node-instance.d.ts +8 -0
  45. package/build/node-instance.js +168 -0
  46. package/build/node-props.d.ts +13 -0
  47. package/build/node-props.js +131 -0
  48. package/build/node-route.d.ts +2 -0
  49. package/build/node-route.js +151 -0
  50. package/build/node-style.d.ts +15 -0
  51. package/build/node-style.js +214 -0
  52. package/build/node-tree.d.ts +6 -0
  53. package/build/node-tree.js +159 -0
  54. package/build/node-types.d.ts +70 -0
  55. package/build/node-types.js +36 -0
  56. package/build/node.d.ts +7 -309
  57. package/build/node.js +9 -1564
  58. package/build/post-commit.js +3 -8
  59. package/build/process-aspect-ratio.js +3 -7
  60. package/build/process-background-longhands.js +10 -19
  61. package/build/process-filter.js +11 -19
  62. package/build/process-font-variant.js +3 -7
  63. package/build/registry.d.ts +0 -33
  64. package/build/registry.js +22 -57
  65. package/build/report-error.js +4 -18
  66. package/build/structured-style.d.ts +0 -9
  67. package/build/structured-style.js +16 -31
  68. package/build/styles.js +3 -6
  69. package/build/surface.d.ts +0 -26
  70. package/build/surface.js +29 -76
  71. package/build/text-input-state.js +4 -8
  72. package/build/touch-history.js +5 -11
  73. package/build/tree-host.d.ts +7 -270
  74. package/build/tree-host.js +63 -153
  75. package/build/view-config.js +17 -37
  76. package/cpp/SymbioteEngineBindings.cpp +19 -18
  77. package/cpp/SymbioteTree.cpp +81 -156
  78. package/cpp/SymbioteTree.h +6 -0
  79. package/package.json +2 -2
@@ -1,13 +1,2 @@
1
1
  export declare const ARIA_ALIAS_KEYS: readonly ["role", "aria-label", "aria-labelledby", "aria-live", "aria-hidden", "aria-busy", "aria-checked", "aria-disabled", "aria-expanded", "aria-selected", "aria-modal", "aria-valuemax", "aria-valuemin", "aria-valuenow", "aria-valuetext"];
2
- /**
3
- * Fold the web-alias `aria-*` / `role` props into RN's canonical `accessibility*` props.
4
- *
5
- * Returns the input BY IDENTITY when no alias is present — the fast path that keeps this off the
6
- * hot path for the ~99% of nodes carrying none, and the property idempotence rests on: pass 1
7
- * blanks every alias, so a second pass finds nothing and returns by identity again.
8
- *
9
- * The alias keys are blanked to `undefined` rather than deleted. That is not laziness: `setProp`
10
- * treats an `undefined` write as a delete and `fabricProps` skips undefined, so a blanked alias
11
- * cannot reach Fabric, while a `delete` would deoptimise the object's shape on every folded node.
12
- */
13
2
  export declare function foldAriaProps(props: Record<string, unknown>): Record<string, unknown>;
@@ -1,27 +1,14 @@
1
- // RN's `aria-*` / `role` -> `accessibility*` fold, at the layer every path goes through.
2
- //
3
- // WHY IT IS HERE AND NOT IN A WRAPPER. It used to run inside each primitive's COMPONENT, and a tag
4
- // has none. It also cannot run per attribute — `aria-checked` has to be folded against a sibling
5
- // `accessibilityState` — so it belongs at the one point where the whole bag is known, which is the
6
- // payload build.
7
- //
8
- // IT IS A MOVE, NOT A REWRITE, AND THAT IS DELIBERATE. The function carries TWO CONTRADICTORY
9
- // PRECEDENCE RULES: for the scalars an explicit `accessibility*` WINS and the alias only fills a
10
- // hole (`if (next.X === undefined)`), while INSIDE the `accessibilityState` / `accessibilityValue`
11
- // composites the ALIAS wins per field (`ariaBusy ?? existing?.busy`). Both mirror RN's View.js.
12
- // Anyone "cleaning this up" collapses them into one rule, and every component test stays green
13
- // while real accessibility silently changes. `core/components/src/accessibility-props.test.ts`
14
- // pins both directions; read it before touching the branches below.
15
- //
16
- // Record-level rather than typed, because the engine's caller has a raw `node.props` bag and an
17
- // interface is not assignable to `Record<string, unknown>` (no index signature). The typed
18
- // `resolveAccessibilityProps<T>` in `core/components` stays where adapters already import it and
19
- // delegates here, keeping its own typed gate so the fast path allocates nothing.
1
+ // RN's aria-* / role -> accessibility* fold, at the layer every path goes through: it can't run
2
+ // per attribute (aria-checked folds against a sibling accessibilityState), so it belongs at the
3
+ // one point where the whole bag is known, the payload build.
4
+ // A move, not a rewrite: the function carries two contradictory precedence rules — scalars let an
5
+ // explicit accessibility* win, alias only fills a hole; the composites let the alias win per field
6
+ // instead. Both mirror RN's View.js. accessibility-props.test.ts pins both directions.
7
+ // Record-level rather than typed: the engine's caller has a raw node.props bag with no index
8
+ // signature. The typed resolveAccessibilityProps<T> in core/components delegates here.
20
9
  import { dlog } from './debug.js';
21
- // Copied line for line from the wrapper this replaces. The first copy silently dropped five
22
- // entries (`button`, `grid`, `link`, `list`, `listitem`) — a role that falls through simply passes
23
- // unmapped, so `role="listitem"` would have reached Fabric as `listitem` instead of `list` with
24
- // nothing red anywhere. Diff this against RN's View.js rather than reading it for plausibility.
10
+ // A role that falls through here passes unmapped, reaching Fabric under the wrong name with
11
+ // nothing red anywhere. Diff against RN's View.js, don't just read for plausibility.
25
12
  const ROLE_TO_ACCESSIBILITY_ROLE = {
26
13
  alert: 'alert',
27
14
  button: 'button',
@@ -52,14 +39,11 @@ const ROLE_TO_ACCESSIBILITY_ROLE = {
52
39
  timer: 'timer',
53
40
  toolbar: 'toolbar',
54
41
  };
55
- // Exported so a behavior that folds a DIFFERENT node's bag can name them without restating the
56
- // list. `slotDerived` (host-behavior.ts) takes prop NAMES, so a primitive whose payload derives
57
- // from an owner's aria props has to enumerate them — and a second hand-written copy is exactly what
58
- // `.claude/rules/adapter-parity-audit.md` records going stale one member at a time.
59
- // `as const` rather than `readonly string[]`, so the members are LITERALS. That is what lets a
60
- // consumer index a prop type with them — `pickAccessibilityProps` in the Svelte adapter forwards the
61
- // aria half by looping this list — and it makes the two sides check each other: a name here that is
62
- // not a key of `IAriaProps` stops compiling at the use site rather than going quietly unforwarded.
42
+ // Exported so a behavior folding a different node's bag can name these without restating the
43
+ // list — slotDerived (host-behavior.ts) takes prop names, so a derived primitive enumerates them.
44
+ // as const rather than readonly string[], so the members are literals: pickAccessibilityProps
45
+ // (Svelte adapter) indexes IAriaProps with them, and a name here not a key of IAriaProps then
46
+ // fails to compile at the use site instead of going quietly unforwarded.
63
47
  export const ARIA_ALIAS_KEYS = [
64
48
  'role',
65
49
  'aria-label',
@@ -86,28 +70,17 @@ function hasAnyAriaKey(props) {
86
70
  }
87
71
  return false;
88
72
  }
89
- // `isAriaAliasKey` WAS HERE AND IS GONE (2026-09-18). Its whole reason was to maintain
90
- // `node.hasAriaAlias` from `setProp` — the hottest write path in the engine, 32 001 writes on one
91
- // benchmark create — and that flag existed only to gate the aria fold inside the headless payload
92
- // builder. The fold left for `SymbioteFabricProps.cpp`, which recomputes presence from the bag it
93
- // holds, so the flag became write-only and this function became its only maintainer. Both went.
94
73
  function isRecord(value) {
95
74
  return typeof value === 'object' && value !== null;
96
75
  }
97
76
  function fieldOf(source, field) {
98
77
  return isRecord(source) ? source[field] : undefined;
99
78
  }
100
- /**
101
- * Fold the web-alias `aria-*` / `role` props into RN's canonical `accessibility*` props.
102
- *
103
- * Returns the input BY IDENTITY when no alias is present — the fast path that keeps this off the
104
- * hot path for the ~99% of nodes carrying none, and the property idempotence rests on: pass 1
105
- * blanks every alias, so a second pass finds nothing and returns by identity again.
106
- *
107
- * The alias keys are blanked to `undefined` rather than deleted. That is not laziness: `setProp`
108
- * treats an `undefined` write as a delete and `fabricProps` skips undefined, so a blanked alias
109
- * cannot reach Fabric, while a `delete` would deoptimise the object's shape on every folded node.
110
- */
79
+ // Fold the web-alias aria-*/role props into RN's canonical accessibility* props. Returns the input
80
+ // by identity when no alias is present, keeping this off the hot path for nodes carrying none —
81
+ // and idempotent, since pass 1 blanks every alias so a second pass finds nothing.
82
+ // Alias keys are blanked to undefined rather than deleted: setProp treats undefined as a delete
83
+ // and fabricProps skips it, while a real delete would deoptimise the object's shape.
111
84
  export function foldAriaProps(props) {
112
85
  if (!hasAnyAriaKey(props))
113
86
  return props;
@@ -157,27 +130,16 @@ export function foldAriaProps(props) {
157
130
  if (typeof role === 'string' && bag.accessibilityRole === undefined) {
158
131
  bag.accessibilityRole = ROLE_TO_ACCESSIBILITY_ROLE[role] ?? role;
159
132
  }
160
- // RULE TWO, INSIDE the composites: the polarity INVERTS and the ALIAS wins per field. Read from
161
- // the ORIGINAL props, not from `bag` — the loop above has already blanked the aliases there.
162
- //
163
- // UPSTREAM-BUG(react-native): View.js:96 is `checked: ariaChecked ?? accessibilityState?.checked`
164
- // — NO type coercion. A template writes `aria-checked="true"` as a STRING in every framework we
165
- // support, so `accessibilityState.checked` reaches native as `'true'` where the native side
166
- // declares `boolean | 'mixed'`. Ported verbatim for parity; do NOT add a cast without recording
167
- // a deliberate divergence. Found by the Svelte session 2026-08-31 and pinned as an ASSERTION in
168
- // `adapters/svelte/src/aria-fold-parity.test.ts`, so a future decision to coerce shows up as a
169
- // failing test at the point the decision was made rather than as a silent behaviour change.
170
- //
171
- // A SECOND, SMALLER DIVERGENCE, and this one is ours rather than upstream's — it predates the
172
- // move down and is kept only because changing it here would be an undeclared behaviour change:
173
- // upstream gates the composite on `!= null` (View.js:87-92) and this gates on `!== undefined`.
174
- // So an explicit `aria-busy={null}` builds an all-undefined `accessibilityState` here and builds
175
- // nothing upstream. The VALUES agree either way — `??` treats null and undefined alike — so only
176
- // the composite's existence differs.
177
- //
178
- // The composite is REPLACED by a fresh literal listing exactly the known fields, so an unknown
179
- // field riding on the incoming object is dropped. Faithful to RN and pinned by a test; it is the
180
- // shape of bug that only shows for whoever passes a field RN adds later.
133
+ // Rule two, inside the composites: the polarity inverts and the alias wins per field. Read from
134
+ // the original props, not bag — the loop above has already blanked the aliases there.
135
+ // Upstream bug, ported verbatim: RN's View.js does `checked: ariaChecked ?? accessibilityState
136
+ // ?.checked` with no coercion, so a string "true" reaches native where it declares boolean |
137
+ // 'mixed'. No cast without recording a divergence (aria-fold-parity.test.ts pins it).
138
+ // A second, smaller divergence, ours rather than upstream's: upstream gates the composite on
139
+ // `!= null`, this on `!== undefined`, so `aria-busy={null}` builds an all-undefined
140
+ // accessibilityState here but nothing upstream — the values agree either way.
141
+ // The composite is replaced by a fresh literal listing exactly the known fields, so an unknown
142
+ // field riding on the incoming object is dropped, faithful to RN.
181
143
  const existingState = fieldOf(props, 'accessibilityState');
182
144
  if (existingState !== undefined ||
183
145
  ariaBusy !== undefined ||
@@ -278,7 +278,7 @@ export function flushValue(rootNode) {
278
278
  // in RELEASE builds only (dev was fine, every headless test was fine, and the code was even
279
279
  // present in the bundle — it just never ran). Adding a bare `import './interpolation-node'` to
280
280
  // the barrel does NOT fix it either: Babel merges that import with the adjacent re-export of the
281
- // same specifier, and the merged dependency stays lazy. Both were tried on device 2026-08-14.
281
+ // same specifier, and the merged dependency stays lazy. Both were tried on device.
282
282
  //
283
283
  // Keeping the class next to its base class removes the cycle, the factory, and the entire class
284
284
  // of load-order bug with it: `interpolate()` constructs it directly and there is nothing left to
@@ -7,7 +7,7 @@
7
7
  // create-animated-component.tsx, Vue's create-animated-component.ts, Svelte's
8
8
  // animated-props-runtime.ts, Angular's animated-leaf-binder.ts). Each re-derived the same policy,
9
9
  // and they drifted: only Svelte ever grew the rebuild guard below, after a day of device
10
- // debugging in 2026-08. React's `useMemo(..., [rest])` LOOKS like the same guard but is not - its
10
+ // debugging. React's `useMemo(..., [rest])` LOOKS like the same guard but is not - its
11
11
  // dependency is a fresh rest-destructured object every render - and Vue's `pendingLeaf === null`
12
12
  // answers "has render run yet", not "did anything change". A fix landing in one copy is exactly
13
13
  // what <adapters_reach_full_feature_parity> forbids: parity has to be structural.
@@ -86,7 +86,7 @@ export function createAnimatedLeafLifecycle(label) {
86
86
  lastWantsNative = wantsNative;
87
87
  // A SNAPSHOT, never the reference. A caller can legitimately hand back the SAME object on
88
88
  // every update, mutating what its keys resolve to rather than allocating (Svelte's rest
89
- // proxy does exactly that, device-confirmed 2026-08-13); storing it would make the next
89
+ // proxy does exactly that, device-confirmed); storing it would make the next
90
90
  // call run shallowEqualProps on that object against ITSELF - reading the same current values
91
91
  // through both sides - which is unconditionally true. `propsChanged` could then never be
92
92
  // true again and reconcile would be skipped forever, so every rebuilt AnimatedInterpolation
@@ -0,0 +1,2 @@
1
+ export declare function setAssetSourceResolver(resolve: (source: unknown) => unknown): void;
2
+ export declare function resolveAssetSource(source: unknown): unknown;
@@ -0,0 +1,13 @@
1
+ // Generic asset-id resolution seam, the non-Image sibling of image-source-resolver.ts. Kept as a
2
+ // separate file so a non-image consumer (audio's `number` source, sqlite's `assetSource`) never
3
+ // has to import something named "image" to resolve its own assets.
4
+ let sourceResolver = source => source;
5
+ export function setAssetSourceResolver(resolve) {
6
+ sourceResolver = resolve;
7
+ }
8
+ // Public mirror of image-source-resolver.ts's resolveImageSource, for non-Image consumers of RN's
9
+ // require()'d-asset resolution (audio's `number` source form, sqlite's `assetSource` option).
10
+ // Headless (no resolver wired) it is the identity, so smokes see the input unchanged.
11
+ export function resolveAssetSource(source) {
12
+ return sourceResolver(source);
13
+ }
@@ -1,10 +1,6 @@
1
1
  import { type IEventSubscription } from '../native-events';
2
2
  declare const DEVICE_BACK_EVENT = "hardwareBackPress";
3
- declare const BACK_PRESS_EVENT: {
4
- readonly backPress: "backPress";
5
- readonly hardwareBackPress: "hardwareBackPress";
6
- };
7
- export type IBackPressEventName = (typeof BACK_PRESS_EVENT)[keyof typeof BACK_PRESS_EVENT];
3
+ export type IBackPressEventName = 'backPress' | 'hardwareBackPress';
8
4
  export type IHardwareBackPressEvent = {
9
5
  readonly type: typeof DEVICE_BACK_EVENT;
10
6
  readonly timeStamp: number;
@@ -17,12 +17,6 @@ import { dlog } from '../debug.js';
17
17
  const DEVICE_EVENT_MANAGER_MODULE = 'DeviceEventManager';
18
18
  // The device event native emits when the hardware back button is pressed.
19
19
  const DEVICE_BACK_EVENT = 'hardwareBackPress';
20
- // The public event names callers subscribe to. `backPress` is RN's legacy alias
21
- // for `hardwareBackPress`; both map to the same chain.
22
- const BACK_PRESS_EVENT = {
23
- backPress: 'backPress',
24
- hardwareBackPress: 'hardwareBackPress',
25
- };
26
20
  function nativeTimeStamp(nativeEvent) {
27
21
  if (typeof nativeEvent !== 'object' || nativeEvent === null)
28
22
  return undefined;
package/build/debug.js CHANGED
@@ -1,25 +1,11 @@
1
- // Opt-in diagnostic logging, off by default. Two switches, either flips it on:
2
- // - env: DEBUG=1 — read directly off process.env.DEBUG at call time (Node,
3
- // headless smokes); each example's index.js also mirrors it onto
4
- // globalThis.__SYMBIOTE_DEBUG__ once at start, so changing it needs a fresh
5
- // Metro start (--reset-cache), not a rebuild.
6
- // - runtime: globalThis.__SYMBIOTE_DEBUG__ = true, an escape hatch for hosts
7
- // where the env isn't reachable.
8
- // Production with neither set pays one property read per call and nothing else -
9
- // but ONLY if the caller does not build the message itself first. A template
10
- // literal is evaluated at the CALL SITE, before dlog can decide anything, so a
11
- // `dlog(\`… ${JSON.stringify(x)}\`)` on a per-frame path costs its full price with
12
- // logging off. On a hot path (a getter Angular re-reads every change-detection
13
- // pass, an Animated reconcile, a scroll-driven apply) pass a THUNK instead:
14
- // `dlog(() => \`…\`)` - it is only called once the switch is on.
15
- // Read ONCE, at module load. `process.env` is not a plain object in Node - each property read
16
- // crosses into the host environment - and isDebug() is called on the per-node commit path, so the
17
- // per-call read showed up as 17% of self time in a create-path CPU profile (headless; on a native
18
- // host `process` is a shim and the read is cheap, so treat that figure as a Node one).
19
- //
20
- // Safe to freeze because nothing toggles the ENV switch mid-process: every runtime toggle in the
21
- // repo goes through __SYMBIOTE_DEBUG__ below, which stays dynamic, and bootstrap mirrors the env
22
- // onto it at start anyway.
1
+ // Opt-in diagnostic logging, off by default. Two switches: env DEBUG=1 (Node, headless smokes;
2
+ // mirrored onto globalThis.__SYMBIOTE_DEBUG__ at bootstrap, so toggling it needs a Metro
3
+ // --reset-cache, not a rebuild), or setting globalThis.__SYMBIOTE_DEBUG__ directly.
4
+ // Read ONCE, at module load: `process.env` is not a plain object in Node, each property read
5
+ // crosses into the host environment, and isDebug() runs on the per-node commit path — costly in
6
+ // Node, cheap on a native host where `process` is just a shim.
7
+ // Safe to freeze: nothing toggles the env switch mid-process, every runtime toggle goes through
8
+ // the dynamic __SYMBIOTE_DEBUG__ instead.
23
9
  const envDebug = typeof process !== 'undefined' && process.env.DEBUG === '1';
24
10
  export function isDebug() {
25
11
  return envDebug || globalThis.__SYMBIOTE_DEBUG__ === true;
package/build/dispatch.js CHANGED
@@ -1,12 +1,6 @@
1
- // The one place a native event re-enters a framework's update loop. A native
2
- // event (a touch from Fabric, a keyboard event from the device hub) runs its
3
- // listener (which may call setState) OUTSIDE the framework's own loop. The
4
- // adapter injects a wrapper that runs the listener at the right priority and
5
- // flushes synchronously so the result paints (React: discrete lane +
6
- // flushSyncWork). Both event rails, Fabric events (events.ts) and device-module
7
- // events (native-events.ts), route through this single seam, so the adapter
8
- // wires it once and both are covered. Default is a pass-through for adapters (and
9
- // the headless harness) that need no wrapping.
1
+ // The one place a native event re-enters a framework's update loop: a native event runs its
2
+ // listener outside that loop, so the adapter injects a wrapper that runs it at the right priority
3
+ // and flushes synchronously. Both Fabric events and device-module events share this seam.
10
4
  let wrapDispatch = run => {
11
5
  run();
12
6
  };
@@ -0,0 +1,9 @@
1
+ import { type ISymbioteNode } from '../node';
2
+ export declare function endsWithin(endTarget: ISymbioteNode, start: ISymbioteNode): boolean;
3
+ export declare function pathToRoot(from: ISymbioteNode): ISymbioteNode[];
4
+ export declare function lowestCommonAncestor(a: ISymbioteNode, b: ISymbioteNode): ISymbioteNode | undefined;
5
+ export declare function hasListenerInPath(target: ISymbioteNode, listenerName: string): boolean;
6
+ export declare function callOwnListener(node: ISymbioteNode, listenerName: string, nativeEvent: Record<string, unknown>): unknown;
7
+ export declare function bubble(target: ISymbioteNode, listenerName: string, nativeEvent: Record<string, unknown>): void;
8
+ /** Direct (non-bubbling) delivery: only the target's own listener fires. */
9
+ export declare function deliverDirect(target: ISymbioteNode, listenerName: string, nativeEvent: Record<string, unknown>): void;
@@ -0,0 +1,143 @@
1
+ // Getting an event to the listeners that want it: the ancestor path, the two-phase bubble, direct
2
+ // delivery, and the single-node call the responder negotiation reads a boolean back from
3
+ import { dlog, isDebug } from '../debug.js';
4
+ import { ancestorsOf, parentOf } from '../host-access.js';
5
+ import { hasListenerFor, isAnchor, listenerFor, } from '../node.js';
6
+ // A press is honest only if the touch ends on the node it started on, or a descendant of it. The
7
+ // start node may have been unmounted mid-touch, so the walk runs out and answers false, no throw
8
+ export function endsWithin(endTarget, start) {
9
+ let node = endTarget;
10
+ while (node) {
11
+ if (node === start)
12
+ return true;
13
+ node = parentOf(node);
14
+ }
15
+ return false;
16
+ }
17
+ // The node chain from `from` up to the root, deepest first. The single allocation the two-phase
18
+ // walk indexes both ways (capture reads it reversed)
19
+ // ONE CROSSING, not one per level: climbing `parentOf` crossed the host boundary each step, 18
20
+ // questions per event on a depth-8 chain and again on every frame of every drag
21
+ export function pathToRoot(from) {
22
+ return [...ancestorsOf(from)];
23
+ }
24
+ // Depth below the root (root = 0), to align two nodes before the lockstep climb to their lowest
25
+ // common ancestor
26
+ function depthOf(node) {
27
+ let depth = 0;
28
+ for (let n = parentOf(node); n; n = parentOf(n))
29
+ depth++;
30
+ return depth;
31
+ }
32
+ // RN's `getLowestCommonAncestor` over our parent pointers: lift the deeper node to the shallower
33
+ // one's depth, then climb both in lockstep until they meet. Scopes the move re-negotiation
34
+ export function lowestCommonAncestor(a, b) {
35
+ let da = depthOf(a);
36
+ let db = depthOf(b);
37
+ let na = a;
38
+ let nb = b;
39
+ while (na && da > db) {
40
+ na = parentOf(na);
41
+ da--;
42
+ }
43
+ while (nb && db > da) {
44
+ nb = parentOf(nb);
45
+ db--;
46
+ }
47
+ while (na && nb) {
48
+ if (na === nb)
49
+ return na;
50
+ na = parentOf(na);
51
+ nb = parentOf(nb);
52
+ }
53
+ return undefined;
54
+ }
55
+ // Whether any node from `target` up to the root listens for `listenerName`, so the long-press
56
+ // timer is armed only when a handler would actually receive it
57
+ export function hasListenerInPath(target, listenerName) {
58
+ for (let node = target; node; node = parentOf(node)) {
59
+ if (hasListenerFor(node, listenerName))
60
+ return true;
61
+ }
62
+ return false;
63
+ }
64
+ // Invoke one node's own listener (no bubbling) and hand back its return value, so the responder
65
+ // negotiation can read the boolean from `onStartShouldSetResponder`
66
+ export function callOwnListener(node, listenerName, nativeEvent) {
67
+ const listener = listenerFor(node, listenerName);
68
+ if (!listener)
69
+ return undefined;
70
+ return listener({
71
+ type: listenerName,
72
+ target: node,
73
+ currentTarget: node,
74
+ nativeEvent,
75
+ stopPropagation: () => { },
76
+ });
77
+ }
78
+ // Anchors (Angular's `#anchor` component hosts) never paint and have no native view. A listener on
79
+ // one only exists because the framework's own output binding already delivered it directly, so
80
+ // bubbling into it would refire the same callback twice
81
+ function capturedBy(node, captureName) {
82
+ return isAnchor(node) ? undefined : listenerFor(node, captureName);
83
+ }
84
+ // Two-phase delivery, mirroring RN's `accumulateTwoPhaseDispatches`: CAPTURE root -> target
85
+ // invoking each node's `<EventName>Capture` listener, then BUBBLE target -> root invoking the
86
+ // plain one. A `stopPropagation` in capture halts before bubble ever runs
87
+ export function bubble(target, listenerName, nativeEvent) {
88
+ let stopped = false;
89
+ const stopPropagation = () => {
90
+ stopped = true;
91
+ };
92
+ // Built target -> root once, then read in reverse for capture and forward for bubble. Re-walking
93
+ // `parentOf` for the second pass crossed the host boundary per level and was the expensive half
94
+ const captureName = `${listenerName}Capture`;
95
+ const path = pathToRoot(target);
96
+ for (let i = path.length - 1; i >= 0; i--) {
97
+ const node = path[i];
98
+ const listener = capturedBy(node, captureName);
99
+ if (listener) {
100
+ if (isDebug())
101
+ dlog(`event ${listenerName} capture on ${node.component}`);
102
+ listener({
103
+ type: listenerName,
104
+ target,
105
+ currentTarget: node,
106
+ nativeEvent,
107
+ stopPropagation,
108
+ });
109
+ if (stopped)
110
+ return;
111
+ }
112
+ }
113
+ for (const node of path) {
114
+ const listener = isAnchor(node)
115
+ ? undefined
116
+ : listenerFor(node, listenerName);
117
+ if (listener) {
118
+ const event = {
119
+ type: listenerName,
120
+ target,
121
+ currentTarget: node,
122
+ nativeEvent,
123
+ stopPropagation,
124
+ };
125
+ listener(event);
126
+ if (stopped)
127
+ return;
128
+ }
129
+ }
130
+ }
131
+ /** Direct (non-bubbling) delivery: only the target's own listener fires. */
132
+ export function deliverDirect(target, listenerName, nativeEvent) {
133
+ const listener = listenerFor(target, listenerName);
134
+ if (!listener)
135
+ return;
136
+ listener({
137
+ type: listenerName,
138
+ target,
139
+ currentTarget: target,
140
+ nativeEvent,
141
+ stopPropagation: () => { },
142
+ });
143
+ }