@symbiote-native/engine 0.1.4 → 0.1.6

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-info/index.android.js +17 -29
  2. package/build/accessibility-info/index.ios.js +23 -37
  3. package/build/accessibility-info/shared.d.ts +3 -2
  4. package/build/accessibility-info/shared.js +25 -2
  5. package/build/action-sheet-ios/index.d.ts +6 -0
  6. package/build/action-sheet-ios/index.js +1 -1
  7. package/build/alert/index.android.js +1 -3
  8. package/build/animated/animations/base.js +5 -14
  9. package/build/animated/event.js +7 -9
  10. package/build/animated/graph.d.ts +6 -0
  11. package/build/animated/graph.js +14 -0
  12. package/build/animated/index.d.ts +1 -1
  13. package/build/animated/index.js +1 -1
  14. package/build/animated/interpolation-node.d.ts +0 -1
  15. package/build/animated/interpolation-node.js +5 -4
  16. package/build/animated/native/native-animated.js +1 -3
  17. package/build/animated/operators.d.ts +14 -34
  18. package/build/animated/operators.js +19 -95
  19. package/build/animated/value.d.ts +2 -4
  20. package/build/animated/value.js +17 -5
  21. package/build/app-state/index.js +17 -22
  22. package/build/appearance/index.js +17 -25
  23. package/build/back-handler/index.js +15 -18
  24. package/build/commit.d.ts +1 -3
  25. package/build/commit.js +26 -209
  26. package/build/dimensions/index.js +25 -20
  27. package/build/events/index.js +6 -151
  28. package/build/fabric-props.d.ts +3 -0
  29. package/build/fabric-props.js +186 -0
  30. package/build/i18n-manager/index.js +3 -5
  31. package/build/image-loader/index.d.ts +24 -0
  32. package/build/image-loader/index.js +175 -0
  33. package/build/image-loader.d.ts +24 -0
  34. package/build/image-loader.js +175 -0
  35. package/build/image-source-resolver/index.d.ts +9 -0
  36. package/build/image-source-resolver/index.js +16 -0
  37. package/build/image-source-resolver.d.ts +9 -0
  38. package/build/image-source-resolver.js +16 -0
  39. package/build/index.d.ts +10 -3
  40. package/build/index.js +12 -3
  41. package/build/keyboard/index.js +19 -34
  42. package/build/layout-animation/index.d.ts +1 -0
  43. package/build/layout-animation/index.js +53 -35
  44. package/build/native-modules/index.d.ts +18 -0
  45. package/build/native-modules/index.js +120 -0
  46. package/build/native-modules.d.ts +12 -0
  47. package/build/native-modules.js +64 -1
  48. package/build/node.d.ts +2 -0
  49. package/build/node.js +36 -1
  50. package/build/pan-responder/index.js +1 -3
  51. package/build/permissions-android/index.js +1 -3
  52. package/build/{platform-color.d.ts → platform-color/index.d.ts} +3 -0
  53. package/build/platform-color/index.js +49 -0
  54. package/build/process-background-image/index.js +7 -14
  55. package/build/process-box-shadow/index.js +1 -5
  56. package/build/process-filter.js +2 -8
  57. package/build/registry.js +3 -5
  58. package/build/settings/index.js +1 -3
  59. package/build/share/index.ios.js +1 -1
  60. package/build/status-bar/index.android.js +1 -1
  61. package/build/style/index.js +2 -4
  62. package/build/style-registry/index.d.ts +1 -3
  63. package/build/style-registry/index.js +15 -48
  64. package/build/style-registry/scope/index.d.ts +3 -0
  65. package/build/style-registry/scope/index.js +41 -0
  66. package/build/style-registry/scope.d.ts +3 -0
  67. package/build/style-registry/scope.js +41 -0
  68. package/build/touch-history/index.d.ts +23 -0
  69. package/build/touch-history/index.js +150 -0
  70. package/build/touch-history.d.ts +23 -0
  71. package/build/touch-history.js +150 -0
  72. package/build/type-guards/index.d.ts +4 -0
  73. package/build/type-guards/index.js +15 -0
  74. package/build/type-guards.d.ts +4 -0
  75. package/build/type-guards.js +15 -0
  76. package/package.json +1 -1
  77. package/build/platform-color.js +0 -25
  78. /package/build/animated/{animated-component-shared.d.ts → shared.d.ts} +0 -0
  79. /package/build/animated/{animated-component-shared.js → shared.js} +0 -0
package/build/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- export { createElement, createRawText, createAnchor, isAnchor, appendChild, insertBefore, removeChild, setProp, setEventListener, routeProp, getExplicitStyle, setText, isSymbioteNode, RAW_TEXT_COMPONENT, } from './node';
1
+ export { createElement, createRawText, createAnchor, isAnchor, appendChild, insertBefore, removeChild, setProp, setEventListener, routeProp, getExplicitStyle, setText, isSymbioteNode, isSymbioteEvent, RAW_TEXT_COMPONENT, debugNodeId, } from './node';
2
2
  export { isEventFor } from './view-config';
3
3
  export { registerComponent, setNativeViewConfigSource } from './registry';
4
+ export { isRecord } from './type-guards';
4
5
  export { InteractionManager, Events as InteractionManagerEvents } from './interaction-manager';
5
6
  export type { IInteractionEvent, ISimpleTask, IPromiseTask, ITask, IHandle, ICancellable, } from './interaction-manager';
6
7
  export { default as PanResponder } from './pan-responder';
@@ -27,8 +28,10 @@ export type { IParsedBackgroundImage } from './process-background-image';
27
28
  export { flattenStyle } from './style';
28
29
  export type { IViewStyle, ITextStyle, IStyleProp, IDimensionValue, IFlexAlign, IFlexJustify, ITransformProp, IBoxShadowValue, IDropShadowValue, IFilterFunction, IColorStopValue, ILinearGradientValue, IRadialGradientValue, IRadialGradientPosition, IRadialGradientShape, IRadialGradientSize, IBackgroundImageValue, IBlendMode, } from './styles';
29
30
  export { StyleSheet, computeHairlineWidth } from './style-sheet';
30
- export { registerStyles, resolveClassName, clearGlobalStyles, scopeClassName, isClassNameValue, } from './style-registry';
31
- export type { IClassNameValue, IClassToggleMap, IScopableClassValue } from './style-registry';
31
+ export { registerStyles, resolveClassName, clearGlobalStyles, isClassNameValue, } from './style-registry';
32
+ export type { IClassNameValue } from './style-registry';
33
+ export { scopeClassName } from './style-registry/scope';
34
+ export type { IClassToggleMap, IScopableClassValue } from './style-registry/scope';
32
35
  export { Platform } from './platform';
33
36
  export type { IPlatformStatic, IPlatformOSType, IPlatformSelectSpec } from './platform';
34
37
  export type { IPlatformConstantsIOS } from './platform/index.ios';
@@ -45,6 +48,10 @@ export { Alert } from './alert';
45
48
  export type { IAlertType, IAlertButtonStyle, IAlertButton, IAlertButtons, IAlertOptions, } from './alert';
46
49
  export { Share } from './share';
47
50
  export type { IShareContent, IShareOptions, IShareAction } from './share';
51
+ export { imageStatics } from './image-loader';
52
+ export type { IImageStatics, IImageSize, IImageCacheStatus } from './image-loader';
53
+ export { setImageSourceResolver, resolveImageSource } from './image-source-resolver';
54
+ export type { IImageSource, IImageSourceProp } from './image-source-resolver';
48
55
  export { ActionSheetIOS } from './action-sheet-ios';
49
56
  export type { IActionSheetIOSOptions, IShareActionSheetIOSOptions, IShareActionSheetError, } from './action-sheet-ios';
50
57
  export { Linking } from './linking';
package/build/index.js CHANGED
@@ -2,9 +2,12 @@
2
2
  // Every framework adapter drives this tiny mutation API; all Fabric-specific
3
3
  // logic (tag allocation, view-name resolution, clone-on-write, event
4
4
  // normalization) lives behind it, in one place.
5
- export { createElement, createRawText, createAnchor, isAnchor, appendChild, insertBefore, removeChild, setProp, setEventListener, routeProp, getExplicitStyle, setText, isSymbioteNode, RAW_TEXT_COMPONENT, } from './node.js';
5
+ export { createElement, createRawText, createAnchor, isAnchor, appendChild, insertBefore, removeChild, setProp, setEventListener, routeProp, getExplicitStyle, setText, isSymbioteNode, isSymbioteEvent, RAW_TEXT_COMPONENT, debugNodeId, } from './node.js';
6
6
  export { isEventFor } from './view-config.js';
7
7
  export { registerComponent, setNativeViewConfigSource } from './registry.js';
8
+ // Real cross-package consumer: core/components' KeyboardAvoidingView render narrows
9
+ // raw native payloads with this same guard, so it needs it off the package root.
10
+ export { isRecord } from './type-guards.js';
8
11
  // InteractionManager: pure JS (timers + emitter), framework-agnostic, so it lives
9
12
  // here; every adapter re-exports it.
10
13
  export { InteractionManager, Events as InteractionManagerEvents } from './interaction-manager/index.js';
@@ -18,7 +21,7 @@ export { setColorProcessor, processColor, dispatchViewCommand, sendAccessibility
18
21
  // the Vue renderer's createElement): the imperative measure/setNativeProps/focus API. Lives
19
22
  // here because it depends only on engine internals, so all adapters inherit it identically.
20
23
  export { toPublicInstance } from './host-instance/index.js';
21
- export { PlatformColor, DynamicColorIOS, isOpaqueColorValue } from './platform-color.js';
24
+ export { PlatformColor, DynamicColorIOS, isOpaqueColorValue } from './platform-color/index.js';
22
25
  // CSS-style processors (boxShadow/filter): RN parses these in JS before native because
23
26
  // enableNativeCSSParsing() defaults to false. Exported so an adapter / test can reuse them.
24
27
  export { processBoxShadow } from './process-box-shadow/index.js';
@@ -30,7 +33,8 @@ export { processFontVariant } from './process-font-variant.js';
30
33
  export { processBackgroundImage } from './process-background-image/index.js';
31
34
  export { flattenStyle } from './style/index.js';
32
35
  export { StyleSheet, computeHairlineWidth } from './style-sheet/index.js';
33
- export { registerStyles, resolveClassName, clearGlobalStyles, scopeClassName, isClassNameValue, } from './style-registry/index.js';
36
+ export { registerStyles, resolveClassName, clearGlobalStyles, isClassNameValue, } from './style-registry/index.js';
37
+ export { scopeClassName } from './style-registry/scope.js';
34
38
  export { Platform } from './platform';
35
39
  export { dlog, isDebug } from './debug.js';
36
40
  export { getNativeModule, getEnforcingNativeModule } from './native-modules.js';
@@ -43,6 +47,11 @@ export { getSlot } from './fabric.js';
43
47
  // real device or simulator, not headless (a headless fake resolves any module name).
44
48
  export { Alert } from './alert';
45
49
  export { Share } from './share';
50
+ // Image statics (getSize/prefetch/queryCache/...): a stateful, native-bridge-touching module with
51
+ // no view of its own, same shape as Alert/Share. The source-resolution seam it shares with
52
+ // @symbiote-native/components' renderImage lives alongside it in image-source-resolver.
53
+ export { imageStatics } from './image-loader.js';
54
+ export { setImageSourceResolver, resolveImageSource } from './image-source-resolver.js';
46
55
  export { ActionSheetIOS } from './action-sheet-ios/index.js';
47
56
  export { Linking } from './linking';
48
57
  export { Vibration } from './vibration';
@@ -3,22 +3,11 @@
3
3
  // subscribes through a NativeEventEmitter bound to the KeyboardObserver native
4
4
  // module, which RN keys its keyboard events off of. Mirrors RN's
5
5
  // Libraries/Components/Keyboard/Keyboard.js, slimmed to the parts we need.
6
- import { getNativeModule } from '../native-modules.js';
7
- import { installDeviceEventHub, NativeEventEmitter, } from '../native-events.js';
6
+ import { createDeviceEventModule } from '../native-modules.js';
7
+ import {} from '../native-events.js';
8
8
  import { dlog } from '../debug.js';
9
9
  import { blurTextInput, currentlyFocusedInput } from '../text-input-state.js';
10
10
  import { LayoutAnimation } from '../layout-animation/index.js';
11
- // RN's scheduleLayoutAnimation falls back to the 'keyboard' animation type when the
12
- // event's easing string isn't a known LayoutAnimation type (Keyboard.js:200).
13
- const KEYBOARD_ANIMATION_TYPE = 'keyboard';
14
- // Map a raw easing string onto a ILayoutAnimationType without a cast: RN does
15
- // `LayoutAnimation.Types[easing] || 'keyboard'`, which only yields a real type when
16
- // the string is a key of the Types table. We mirror that by checking membership in
17
- // the frozen table before trusting the value.
18
- function easingToAnimationType(easing) {
19
- const types = LayoutAnimation.Types;
20
- return types[easing] ?? KEYBOARD_ANIMATION_TYPE;
21
- }
22
11
  // The native module name RN registers the keyboard observer under, confirmed
23
12
  // from its spec (specs_DEPRECATED/modules/INativeKeyboardObserver.js:20,
24
13
  // `TurboModuleRegistry.get('KeyboardObserver')`).
@@ -39,11 +28,6 @@ function isKeyboardEvent(payload) {
39
28
  typeof payload.endCoordinates === 'object' &&
40
29
  payload.endCoordinates !== null);
41
30
  }
42
- // Lazily resolved so importing this module has no native side effect: a headless
43
- // run without a fake __turboModuleProxy still loads it; resolution happens on the
44
- // first addListener. Null when the module isn't linked.
45
- let observer;
46
- let emitter;
47
31
  // The latest keyboardDidShow event, or null when the keyboard is hidden. RN's
48
32
  // `_currentlyShowing`. Kept fresh by an internal self-subscription (see getEmitter):
49
33
  // Keyboard listens to its OWN show/hide events and caches the event so the synchronous
@@ -68,19 +52,18 @@ function trackSubscription(eventType, subscription) {
68
52
  },
69
53
  };
70
54
  }
71
- function getEmitter() {
72
- if (emitter === undefined) {
73
- if (observer === undefined) {
74
- observer = getNativeModule(KEYBOARD_OBSERVER_MODULE);
75
- dlog(`Keyboard: KeyboardObserver module ${observer ? 'resolved' : 'NOT resolved (null)'}`);
76
- }
77
- // WHY lazy: install on the first subscribe rather than at module load, so the
78
- // hub exists before native emits without a hard bootstrap-order dependency for
79
- // this first cut. Idempotent, so repeated subscribes cost one boolean check.
80
- installDeviceEventHub();
81
- emitter = new NativeEventEmitter(observer ?? undefined);
82
- // Self-subscription: cache the latest show event, clear on hide (RN's constructor).
83
- // Bypasses trackSubscription so removeAllListeners never tears down the cache feed.
55
+ // Lazily resolved so importing this module has no native side effect: a headless
56
+ // run without a fake __turboModuleProxy still loads it; resolution happens on the
57
+ // first addListener. Null when the module isn't linked.
58
+ //
59
+ // The self-subscription policy that diverges from a plain lazy-resolve+emitter:
60
+ // Keyboard caches the latest show event, clearing on hide (RN's constructor), so
61
+ // isVisible()/metrics() read synchronously with no native round-trip. Bypasses
62
+ // trackSubscription so removeAllListeners never tears down the cache feed.
63
+ const deviceEventModule = createDeviceEventModule({
64
+ moduleName: KEYBOARD_OBSERVER_MODULE,
65
+ moduleLogPrefix: 'Keyboard: KeyboardObserver module',
66
+ onEmitterCreated: emitter => {
84
67
  emitter.addListener(KEYBOARD_EVENT.didShow, payload => {
85
68
  if (isKeyboardEvent(payload))
86
69
  currentlyShowing = payload;
@@ -88,8 +71,10 @@ function getEmitter() {
88
71
  emitter.addListener(KEYBOARD_EVENT.didHide, () => {
89
72
  currentlyShowing = null;
90
73
  });
91
- }
92
- return emitter;
74
+ },
75
+ });
76
+ function getEmitter() {
77
+ return deviceEventModule.getEmitter();
93
78
  }
94
79
  export const Keyboard = {
95
80
  addListener(eventType, listener) {
@@ -128,7 +113,7 @@ export const Keyboard = {
128
113
  dlog(`Keyboard.scheduleLayoutAnimation -> duration ${duration}, easing ${easing}`);
129
114
  LayoutAnimation.configureNext({
130
115
  duration,
131
- update: { duration, type: easingToAnimationType(easing) },
116
+ update: { duration, type: LayoutAnimation.coerceType(easing) },
132
117
  });
133
118
  },
134
119
  // RN's dismissKeyboard blurs the currently-focused input (TextInputState); blurring
@@ -61,6 +61,7 @@ declare class LayoutAnimationImpl {
61
61
  spring(onAnimationDidEnd?: IOnAnimationDidEndCallback): void;
62
62
  setLayoutAnimationEnabled(enabled: boolean): void;
63
63
  checkConfig(..._args: unknown[]): void;
64
+ coerceType(easing: string): ILayoutAnimationType;
64
65
  }
65
66
  export declare const LayoutAnimation: LayoutAnimationImpl;
66
67
  export {};
@@ -9,20 +9,17 @@
9
9
  // these in the adapter, not in shared, like Keyboard / StatusBar).
10
10
  import { getNativeModule } from '../native-modules.js';
11
11
  import { dlog } from '../debug.js';
12
+ import { isRecord } from '../type-guards.js';
12
13
  // ---- native module routing ----------------------------------------------
13
- // DEVICE-VERIFY-PENDING: on bridgeless Fabric the layout-animation configure call
14
- // is exposed by RN through the UIManager surface (RN's non-Fabric path calls
15
- // `UIManager.configureNextLayoutAnimation`; the Fabric path routes the same args
16
- // onto `global.nativeFabricUIManager.configureNextLayoutAnimation`). The native
17
- // MODULE NAME is platform-specific and a headless fake answers to ANY name, so
18
- // the name below is the most plausible bridgeless candidate, NOT proven. Only the
19
- // simulator/device resolution log can confirm it; the fallback list is tried in
20
- // order so a wrong primary still resolves on a real host. Verify on-device before
21
- // trusting either name. See the `dlog` at the resolution seam below.
22
- const NATIVE_UI_MANAGER_NAME = {
23
- primary: 'UIManager',
24
- fallback: 'FabricUIManager',
25
- };
14
+ // RN's actual dual-path (LayoutAnimation.js): the Fabric global slot
15
+ // (`global.nativeFabricUIManager`, a JSI slot read directly, not a TurboModule
16
+ // lookup) is tried FIRST; only when it lacks `configureNextLayoutAnimation` does
17
+ // RN fall back to the TurboModule `UIManager.configureNextLayoutAnimation`
18
+ // (`TurboModuleRegistry.getEnforcing('UIManager')` in NativeUIManager.js; iOS's
19
+ // RCTUIManager.mm registers under the same bare "UIManager" name via
20
+ // RCT_EXPORT_MODULE()). There is only ONE correct TurboModule name; RN never
21
+ // registers a module under "FabricUIManager".
22
+ const NATIVE_UI_MANAGER_MODULE_NAME = 'UIManager';
26
23
  // ---- public type surface (ported from RN's ReactNativeTypes) -------------
27
24
  const ANIMATION_TYPE = {
28
25
  spring: 'spring',
@@ -38,28 +35,39 @@ const ANIMATION_PROPERTY = {
38
35
  scaleY: 'scaleY',
39
36
  scaleXY: 'scaleXY',
40
37
  };
41
- // Resolved FRESH on every configureNext, deliberately NOT memoized. The native module can
42
- // be absent at one moment and linked later, and a cached answer would pin the first result;
43
- // getNativeModule is a cheap proxy lookup, so per-call resolution costs nothing and stays
44
- // correct. (Memoizing here also broke the headless smoke, which flips a fake module on/off
45
- // in one process: a cached module survived the flip-off, so configureNext kept calling native
46
- // when the module was meant to be absent.)
38
+ // Narrows the Fabric global slot down to whether it ALSO carries the
39
+ // layout-animation hook. `fabric.ts`'s `IFabricSlot` deliberately omits this
40
+ // method (out of scope for the engine's own mutation API), so the slot's static
41
+ // type never has it either; this is the runtime feature-detect RN itself does
42
+ // (`FabricUIManager?.configureNextLayoutAnimation` in LayoutAnimation.js) before
43
+ // calling it, no `as` cast needed.
44
+ function hasConfigureNextLayoutAnimation(value) {
45
+ return isRecord(value) && typeof value.configureNextLayoutAnimation === 'function';
46
+ }
47
+ // Resolved FRESH on every configureNext, deliberately NOT memoized. Either mechanism can
48
+ // come and go at runtime (the Fabric global installs during bootstrap; a TurboModule can be
49
+ // absent at one moment and linked later), and a cached answer would pin the first result;
50
+ // both lookups are cheap, so per-call resolution costs nothing and stays correct.
51
+ // (Memoizing here also broke the headless smoke, which flips a fake module on/off in one
52
+ // process: a cached module survived the flip-off, so configureNext kept calling native when
53
+ // the module was meant to be absent.)
47
54
  function resolveUIManager() {
48
- // Try the most plausible bridgeless name first, then fall back. A wrong primary
49
- // resolves null on a real host (the bridgeless proxy returns null for an
50
- // unlinked name), so the fallback covers a misnamed primary.
51
- const candidates = [NATIVE_UI_MANAGER_NAME.primary, NATIVE_UI_MANAGER_NAME.fallback];
52
- for (const name of candidates) {
53
- const module = getNativeModule(name);
54
- if (module !== null) {
55
- // DEVICE-VERIFY-PENDING seam: this line on the simulator/device is the only
56
- // proof the chosen name is the real one. Keep it permanently (P0 logging gate).
57
- dlog(`LayoutAnimation: resolved native UIManager via "${name}"`);
58
- return module;
59
- }
55
+ // Mechanism 1: the Fabric global slot, read directly: a JSI global, not a
56
+ // TurboModule lookup.
57
+ const fabricUIManager = globalThis.nativeFabricUIManager;
58
+ if (hasConfigureNextLayoutAnimation(fabricUIManager)) {
59
+ dlog('LayoutAnimation: resolved native UIManager via the Fabric global slot');
60
+ return fabricUIManager;
60
61
  }
61
- dlog(`LayoutAnimation: no native UIManager resolved (tried ${candidates.join(', ')}); ` +
62
- `configureNext is a no-op (headless or module not linked)`);
62
+ // Mechanism 2: the TurboModule fallback (RN's non-Fabric path).
63
+ const module = getNativeModule(NATIVE_UI_MANAGER_MODULE_NAME);
64
+ if (module !== null) {
65
+ dlog(`LayoutAnimation: resolved native UIManager via "${NATIVE_UI_MANAGER_MODULE_NAME}"`);
66
+ return module;
67
+ }
68
+ dlog('LayoutAnimation: no native UIManager resolved (no Fabric global slot, ' +
69
+ `"${NATIVE_UI_MANAGER_MODULE_NAME}" module not linked); configureNext is a no-op ` +
70
+ '(headless or module not linked)');
63
71
  return null;
64
72
  }
65
73
  // ---- config builder ------------------------------------------------------
@@ -106,7 +114,7 @@ function setLayoutAnimationEnabled(value) {
106
114
  // ---- configureNext -------------------------------------------------------
107
115
  // Configures the next commit to be animated. NATIVE drives completion:
108
116
  // `onAnimationDidEnd` is passed straight through as the native success callback,
109
- // so it fires exactly when the native animation actually finishes, including
117
+ // so it fires exactly when the native animation finishes, including
110
118
  // when native extends it past `duration` (spring overshoot, OS slowdown,
111
119
  // reduce-motion). `onAnimationDidFail` fires only if native config parsing fails.
112
120
  // When no native module is linked (headless), this is a logged no-op; an app
@@ -140,7 +148,7 @@ function configureNext(config, onAnimationDidEnd, onAnimationDidFail) {
140
148
  onAnimationDidEnd?.();
141
149
  };
142
150
  dlog(`LayoutAnimation.configureNext: dispatching config (duration=${config.duration})`);
143
- // onError only fires if native config parsing fails; default to a no-op.
151
+ // Default to a no-op when the caller doesn't supply one.
144
152
  manager.configureNextLayoutAnimation(config, onComplete, onAnimationDidFail ?? (() => { }));
145
153
  }
146
154
  // ---- the LayoutAnimation facade ------------------------------------------
@@ -178,5 +186,15 @@ class LayoutAnimationImpl {
178
186
  checkConfig(..._args) {
179
187
  dlog('LayoutAnimation.checkConfig(...) has been disabled.');
180
188
  }
189
+ // Coerce an arbitrary string (e.g. a keyboard event's `easing` field) onto a known
190
+ // ILayoutAnimationType, falling back to 'keyboard' when it isn't a key of `Types`.
191
+ // Owned here, not by a caller: `Types` is this class's own frozen table, so by
192
+ // Information Expert the coercion belongs on the type it reads (Keyboard's
193
+ // scheduleLayoutAnimation is the first caller; RN's own
194
+ // `LayoutAnimation.Types[easing] || 'keyboard'` is the same rule).
195
+ coerceType(easing) {
196
+ const types = this.Types;
197
+ return types[easing] ?? ANIMATION_TYPE.keyboard;
198
+ }
181
199
  }
182
200
  export const LayoutAnimation = new LayoutAnimationImpl();
@@ -0,0 +1,18 @@
1
+ import { NativeEventEmitter } from '../native-events';
2
+ declare global {
3
+ var __turboModuleProxy: (<T>(name: string) => T | null) | undefined;
4
+ var nativeModuleProxy: Record<string, unknown> | undefined;
5
+ }
6
+ export declare function getNativeModule<T>(name: string): T | null;
7
+ export declare function getEnforcingNativeModule<T>(name: string): T;
8
+ export interface IDeviceEventModuleConfig<TModule> {
9
+ moduleName: string;
10
+ moduleLogPrefix: string;
11
+ bindModuleToEmitter?: boolean;
12
+ onEmitterCreated?: (emitter: NativeEventEmitter, module: TModule | null) => void;
13
+ }
14
+ export interface IDeviceEventModule<TModule> {
15
+ getModule(): TModule | null;
16
+ getEmitter(): NativeEventEmitter;
17
+ }
18
+ export declare function createDeviceEventModule<TModule>(config: IDeviceEventModuleConfig<TModule>): IDeviceEventModule<TModule>;
@@ -0,0 +1,120 @@
1
+ // JS -> native: reaching a native (Turbo)Module. The New Architecture installs
2
+ // `global.__turboModuleProxy(name)`: a JSI function that returns the native
3
+ // module registered under `name` (a HostObject whose methods call into native),
4
+ // or null. React Native's own `TurboModuleRegistry.get` is just this call; we are
5
+ // one more client of the same global, exactly as `getSlot` is for the view tree.
6
+ //
7
+ // This is first-party access, for symbiote's own modules (StatusBarManager,
8
+ // KeyboardObserver, ...). Third-party RN packages import `TurboModuleRegistry` from
9
+ // `'react-native'` and read the same global themselves; they do not go through
10
+ // here.
11
+ import { dlog } from '../debug.js';
12
+ import { installDeviceEventHub, NativeEventEmitter, } from '../native-events.js';
13
+ import { isRecord } from '../type-guards.js';
14
+ // The native module value crosses from an untyped HostObject into our types here;
15
+ // the caller vouches for its shape via T (the single trust-boundary narrowing, no
16
+ // per-call `as`). Native modules are always non-null objects.
17
+ function isNativeModule(value) {
18
+ return value !== null && value !== undefined;
19
+ }
20
+ // The native module `name`, typed as the caller's interface `T`, or null when no
21
+ // module by that name is registered in the binary (or the proxy is absent, e.g.
22
+ // running headless without a fake installed).
23
+ export function getNativeModule(name) {
24
+ // Non-bridgeless: the function proxy. Call it by name.
25
+ const turboProxy = globalThis.__turboModuleProxy;
26
+ if (typeof turboProxy === 'function') {
27
+ const module = turboProxy(name);
28
+ if (module !== null && module !== undefined)
29
+ return module;
30
+ }
31
+ // Bridgeless: the HostObject proxy, indexed by name. Guard the access: a
32
+ // HostObject may throw for an unlinked name, and a throw here would propagate
33
+ // into a render effect and blank the tree.
34
+ const bridgelessProxy = globalThis.nativeModuleProxy;
35
+ if (bridgelessProxy !== undefined) {
36
+ try {
37
+ const module = bridgelessProxy[name];
38
+ if (isNativeModule(module))
39
+ return module;
40
+ }
41
+ catch (error) {
42
+ dlog(`nativeModuleProxy["${name}"] threw: ${String(error)}`);
43
+ }
44
+ }
45
+ dlog(`native module "${name}" not found ` +
46
+ `(turbo=${typeof turboProxy}, bridgeless=${typeof globalThis.nativeModuleProxy})`);
47
+ return null;
48
+ }
49
+ // Same, but throws when the module is missing, for modules a feature hard-depends
50
+ // on (StatusBar without StatusBarManager cannot function, so failing loud beats a
51
+ // silent no-op).
52
+ export function getEnforcingNativeModule(name) {
53
+ const module = getNativeModule(name);
54
+ if (module === null) {
55
+ throw new Error(`Native module "${name}" is not registered in the binary. ` +
56
+ 'Verify it is linked (New Architecture / bridgeless host with __turboModuleProxy installed).');
57
+ }
58
+ return module;
59
+ }
60
+ // ---- device-event module factory ------------------------------------------
61
+ //
62
+ // AccessibilityInfo (iOS + Android), AppState, Appearance, BackHandler, Keyboard,
63
+ // and Dimensions each hand-rolled the identical plumbing: lazily resolve a native
64
+ // module, lazily build a NativeEventEmitter bound to it, install the device-event
65
+ // hub on first subscribe. `createLinking` (linking/shared.ts) proved this factors
66
+ // out safely for Linking's iOS/Android split; this is the general form for every
67
+ // other lazy-module + emitter pair in the runtime-module layer.
68
+ //
69
+ // Each caller's DEGRADE POLICY stays its own, via config - the factory owns only
70
+ // the plumbing:
71
+ // - `bindModuleToEmitter` (default true): whether the resolved module is wired
72
+ // into the emitter so its addListener/removeListeners observe-counters get
73
+ // pinged. Dimensions' DeviceInfo module has no observe-counters, so it opts out
74
+ // (matching its original `new NativeEventEmitter(undefined)`).
75
+ // - `onEmitterCreated`: runs exactly once, right after the emitter is built -
76
+ // the hook for a caller's own permanent self-subscription (AppState/Appearance/
77
+ // BackHandler/Keyboard each keep a cache fresh or dispatch a chain this way) or
78
+ // one-time hydration from the module's constants (AppState's initial state,
79
+ // Dimensions' initial metrics - the latter also relies on this hook running
80
+ // `addListener` BEFORE the constants read, preserving the original
81
+ // subscribe-before-resolve ordering that guards against missing an update).
82
+ // A structural check, not a cast: narrows an arbitrary resolved module down to
83
+ // IEventEmitterModule only when it actually carries both observe-counter methods.
84
+ // Needed because `TModule` is unconstrained (Dimensions' INativeDeviceInfo doesn't
85
+ // extend IEventEmitterModule at all) - NativeEventEmitter itself re-checks the same
86
+ // shape internally, so this exists to satisfy the type system, not to change
87
+ // behavior.
88
+ function hasEventEmitterShape(value) {
89
+ return (isRecord(value) &&
90
+ typeof value.addListener === 'function' &&
91
+ typeof value.removeListeners === 'function');
92
+ }
93
+ // Build one module's lazy-resolve + lazy-emitter pair. Each call owns its own
94
+ // cache, so two callers (or two platform builds loaded together in one smoke) stay
95
+ // independent - the same guarantee `createLinking` documents.
96
+ export function createDeviceEventModule(config) {
97
+ let module;
98
+ let emitter;
99
+ function getModule() {
100
+ if (module === undefined) {
101
+ module = getNativeModule(config.moduleName);
102
+ dlog(`${config.moduleLogPrefix} ${module ? 'resolved' : 'NOT resolved (null)'}`);
103
+ }
104
+ return module;
105
+ }
106
+ function getEmitter() {
107
+ if (emitter === undefined) {
108
+ // WHY lazy: install on first subscribe so the hub exists before native
109
+ // emits, without a hard bootstrap-order dependency. Idempotent.
110
+ installDeviceEventHub();
111
+ const resolved = getModule();
112
+ const bindModule = config.bindModuleToEmitter ?? true;
113
+ const boundModule = bindModule && resolved !== null && hasEventEmitterShape(resolved) ? resolved : undefined;
114
+ emitter = new NativeEventEmitter(boundModule);
115
+ config.onEmitterCreated?.(emitter, resolved);
116
+ }
117
+ return emitter;
118
+ }
119
+ return { getModule, getEmitter };
120
+ }
@@ -1,6 +1,18 @@
1
+ import { NativeEventEmitter } from './native-events';
1
2
  declare global {
2
3
  var __turboModuleProxy: (<T>(name: string) => T | null) | undefined;
3
4
  var nativeModuleProxy: Record<string, unknown> | undefined;
4
5
  }
5
6
  export declare function getNativeModule<T>(name: string): T | null;
6
7
  export declare function getEnforcingNativeModule<T>(name: string): T;
8
+ export interface IDeviceEventModuleConfig<TModule> {
9
+ moduleName: string;
10
+ moduleLogPrefix: string;
11
+ bindModuleToEmitter?: boolean;
12
+ onEmitterCreated?: (emitter: NativeEventEmitter, module: TModule | null) => void;
13
+ }
14
+ export interface IDeviceEventModule<TModule> {
15
+ getModule(): TModule | null;
16
+ getEmitter(): NativeEventEmitter;
17
+ }
18
+ export declare function createDeviceEventModule<TModule>(config: IDeviceEventModuleConfig<TModule>): IDeviceEventModule<TModule>;
@@ -5,10 +5,12 @@
5
5
  // one more client of the same global, exactly as `getSlot` is for the view tree.
6
6
  //
7
7
  // This is first-party access, for symbiote's own modules (StatusBarManager,
8
- // KeyboardObserver, …). Third-party RN packages import `TurboModuleRegistry` from
8
+ // KeyboardObserver, ...). Third-party RN packages import `TurboModuleRegistry` from
9
9
  // `'react-native'` and read the same global themselves; they do not go through
10
10
  // here.
11
11
  import { dlog } from './debug.js';
12
+ import { installDeviceEventHub, NativeEventEmitter, } from './native-events.js';
13
+ import { isRecord } from './type-guards.js';
12
14
  // The native module value crosses from an untyped HostObject into our types here;
13
15
  // the caller vouches for its shape via T (the single trust-boundary narrowing, no
14
16
  // per-call `as`). Native modules are always non-null objects.
@@ -55,3 +57,64 @@ export function getEnforcingNativeModule(name) {
55
57
  }
56
58
  return module;
57
59
  }
60
+ // ---- device-event module factory ------------------------------------------
61
+ //
62
+ // AccessibilityInfo (iOS + Android), AppState, Appearance, BackHandler, Keyboard,
63
+ // and Dimensions each hand-rolled the identical plumbing: lazily resolve a native
64
+ // module, lazily build a NativeEventEmitter bound to it, install the device-event
65
+ // hub on first subscribe. `createLinking` (linking/shared.ts) proved this factors
66
+ // out safely for Linking's iOS/Android split; this is the general form for every
67
+ // other lazy-module + emitter pair in the runtime-module layer.
68
+ //
69
+ // Each caller's DEGRADE POLICY stays its own, via config - the factory owns only
70
+ // the plumbing:
71
+ // - `bindModuleToEmitter` (default true): whether the resolved module is wired
72
+ // into the emitter so its addListener/removeListeners observe-counters get
73
+ // pinged. Dimensions' DeviceInfo module has no observe-counters, so it opts out
74
+ // (matching its original `new NativeEventEmitter(undefined)`).
75
+ // - `onEmitterCreated`: runs exactly once, right after the emitter is built -
76
+ // the hook for a caller's own permanent self-subscription (AppState/Appearance/
77
+ // BackHandler/Keyboard each keep a cache fresh or dispatch a chain this way) or
78
+ // one-time hydration from the module's constants (AppState's initial state,
79
+ // Dimensions' initial metrics - the latter also relies on this hook running
80
+ // `addListener` BEFORE the constants read, preserving the original
81
+ // subscribe-before-resolve ordering that guards against missing an update).
82
+ // A structural check, not a cast: narrows an arbitrary resolved module down to
83
+ // IEventEmitterModule only when it actually carries both observe-counter methods.
84
+ // Needed because `TModule` is unconstrained (Dimensions' INativeDeviceInfo doesn't
85
+ // extend IEventEmitterModule at all) - NativeEventEmitter itself re-checks the same
86
+ // shape internally, so this exists to satisfy the type system, not to change
87
+ // behavior.
88
+ function hasEventEmitterShape(value) {
89
+ return (isRecord(value) &&
90
+ typeof value.addListener === 'function' &&
91
+ typeof value.removeListeners === 'function');
92
+ }
93
+ // Build one module's lazy-resolve + lazy-emitter pair. Each call owns its own
94
+ // cache, so two callers (or two platform builds loaded together in one smoke) stay
95
+ // independent - the same guarantee `createLinking` documents.
96
+ export function createDeviceEventModule(config) {
97
+ let module;
98
+ let emitter;
99
+ function getModule() {
100
+ if (module === undefined) {
101
+ module = getNativeModule(config.moduleName);
102
+ dlog(`${config.moduleLogPrefix} ${module ? 'resolved' : 'NOT resolved (null)'}`);
103
+ }
104
+ return module;
105
+ }
106
+ function getEmitter() {
107
+ if (emitter === undefined) {
108
+ // WHY lazy: install on first subscribe so the hub exists before native
109
+ // emits, without a hard bootstrap-order dependency. Idempotent.
110
+ installDeviceEventHub();
111
+ const resolved = getModule();
112
+ const bindModule = config.bindModuleToEmitter ?? true;
113
+ const boundModule = bindModule && resolved !== null && hasEventEmitterShape(resolved) ? resolved : undefined;
114
+ emitter = new NativeEventEmitter(boundModule);
115
+ config.onEmitterCreated?.(emitter, resolved);
116
+ }
117
+ return emitter;
118
+ }
119
+ return { getModule, getEmitter };
120
+ }
package/build/node.d.ts CHANGED
@@ -10,6 +10,7 @@ export interface ISymbioteEvent {
10
10
  stopPropagation: () => void;
11
11
  }
12
12
  export type IListener = (event: ISymbioteEvent) => unknown;
13
+ export declare function isSymbioteEvent(value: unknown): value is ISymbioteEvent;
13
14
  export interface ISymbioteNode {
14
15
  readonly [BRAND]: true;
15
16
  readonly component: string;
@@ -22,6 +23,7 @@ export interface ISymbioteNode {
22
23
  export declare function createElement(component: string, isText?: boolean): ISymbioteNode;
23
24
  export declare function createRawText(text: string): ISymbioteNode;
24
25
  export declare function isSymbioteNode(value: unknown): value is ISymbioteNode;
26
+ export declare function debugNodeId(node: ISymbioteNode): number;
25
27
  export declare const ANCHOR_COMPONENT = "#anchor";
26
28
  export declare function createAnchor(): ISymbioteNode;
27
29
  export declare function isAnchor(node: ISymbioteNode): boolean;
package/build/node.js CHANGED
@@ -6,6 +6,7 @@
6
6
  // lives here in shared so no adapter re-implements it.
7
7
  import { isEventFor } from './view-config.js';
8
8
  import { isClassNameValue, resolveClassName } from './style-registry/index.js';
9
+ import { dlog } from './debug.js';
9
10
  const BRAND = Symbol('symbiote.node');
10
11
  // A node carries the Fabric view name directly, so adding a primitive (Image,
11
12
  // ScrollView, TextInput) is just a new string from the adapter, no core change.
@@ -15,6 +16,15 @@ const BRAND = Symbol('symbiote.node');
15
16
  export const RAW_TEXT_COMPONENT = 'RCTRawText';
16
17
  export const TEXT_COMPONENT = 'RCTText';
17
18
  export const VIRTUAL_TEXT_COMPONENT = 'RCTVirtualText';
19
+ // Runtime guard narrowing `unknown` to ISymbioteEvent (no `as` cast). Lives with the interface
20
+ // it tests, so every adapter checking for a Symbiote event shares one guard instead of writing
21
+ // its own copy.
22
+ export function isSymbioteEvent(value) {
23
+ if (typeof value !== 'object' || value === null)
24
+ return false;
25
+ const nativeEvent = Reflect.get(value, 'nativeEvent');
26
+ return typeof nativeEvent === 'object' && nativeEvent !== null;
27
+ }
18
28
  export function createElement(component, isText = false) {
19
29
  return {
20
30
  [BRAND]: true,
@@ -43,6 +53,21 @@ export function createRawText(text) {
43
53
  export function isSymbioteNode(value) {
44
54
  return typeof value === 'object' && value !== null && BRAND in value;
45
55
  }
56
+ // Investigation instrumentation (HeaderOptionsScreen search-bar-ref "node not committed" bug):
57
+ // a WeakMap can't be logged, so this gives every node a small human-readable id, assigned lazily
58
+ // on first call — lets a dlog at ref-attach time and a dlog at commit/dispatch time be compared
59
+ // directly to prove whether they're the SAME node object or two different ones. Kept behind
60
+ // DEBUG per <keep_logs_gate_behind_DEBUG>, never removed.
61
+ const debugIds = new WeakMap();
62
+ let nextDebugId = 1;
63
+ export function debugNodeId(node) {
64
+ let id = debugIds.get(node);
65
+ if (id === undefined) {
66
+ id = nextDebugId++;
67
+ debugIds.set(node, id);
68
+ }
69
+ return id;
70
+ }
46
71
  // Vue's runtime-core needs comment/anchor nodes (fragments, v-if, v-for) to track
47
72
  // sibling order; Fabric has no such concept. An anchor is a real retained node so
48
73
  // insert/nextSibling/parentNode ordering stays correct, but the commit walk SKIPS it
@@ -157,7 +182,17 @@ export function routeProp(node, key, value) {
157
182
  }
158
183
  if (ON_PREFIX.test(key)) {
159
184
  const name = listenerName(key);
160
- if (RESPONDER_EVENTS.has(name) || isEventFor(node.component, name)) {
185
+ const isRegisteredEvent = RESPONDER_EVENTS.has(name) || isEventFor(node.component, name);
186
+ // Investigation instrumentation (HeaderOptionsScreen unresponsive-buttons bug): RNS* views
187
+ // derive their events from react-native-screens' own codegen ViewConfig (registry.ts), so an
188
+ // unregistered event silently falls through to setProp below — a dead prop Fabric ignores,
189
+ // indistinguishable from "the button did nothing" at the UI. Scoped to RNS* to avoid noise
190
+ // from the rest of the app. Kept behind DEBUG per <keep_logs_gate_behind_DEBUG>, never removed.
191
+ if (node.component.startsWith('RNS')) {
192
+ dlog(`routeProp: ${node.component} "${key}" -> listener "${name}" ` +
193
+ `registered=${isRegisteredEvent} at t=${Date.now()}`);
194
+ }
195
+ if (isRegisteredEvent) {
161
196
  setEventListener(node, name, value);
162
197
  return;
163
198
  }