@symbiote-native/engine 0.1.6 → 0.1.7

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.
@@ -6,7 +6,7 @@
6
6
  // device-event NAMES also differ from iOS (e.g. screen-reader is `touchExplorationDidChange`,
7
7
  // reduce-motion is `reduceMotionDidChange`). Metro picks this on an Android host. Mirrors
8
8
  // RN's AccessibilityInfo.js Android branches.
9
- import { createDeviceEventModule } from '../native-modules.js';
9
+ import { createDeviceEventModule } from '../native-modules/index.js';
10
10
  import {} from '../native-events.js';
11
11
  import { dlog } from '../debug.js';
12
12
  import { isBoolean, routeSendAccessibilityEvent, } from './shared.js';
@@ -6,7 +6,7 @@
6
6
  // NativeEventEmitter and re-broadcasts to JS listeners. Metro picks this on an iOS host;
7
7
  // the bare accessibility-info.ts re-exports it as the default for tsc / tsx / headless.
8
8
  // Mirrors RN's AccessibilityInfo.js iOS branches.
9
- import { createDeviceEventModule } from '../native-modules.js';
9
+ import { createDeviceEventModule } from '../native-modules/index.js';
10
10
  import {} from '../native-events.js';
11
11
  import { dlog } from '../debug.js';
12
12
  import { isBoolean, routeSendAccessibilityEvent, } from './shared.js';
@@ -17,7 +17,7 @@
17
17
  // processing in @symbiote-native/engine, so we do NOT process colors here; options pass
18
18
  // through untouched. The canary needs no colors; revisit when wiring real colors.
19
19
  import { dlog } from '../debug.js';
20
- import { getNativeModule } from '../native-modules.js';
20
+ import { getNativeModule } from '../native-modules/index.js';
21
21
  export const ACTION_SHEET_MANAGER = 'ActionSheetManager';
22
22
  // The static imperative API RN exposes, mirrored as a static-method object.
23
23
  export const ActionSheetIOS = {
@@ -17,7 +17,7 @@
17
17
  //
18
18
  // Non-throwing, like StatusBar: a missing native module is a no-op, never a crash.
19
19
  import { dlog } from '../debug.js';
20
- import { getNativeModule } from '../native-modules.js';
20
+ import { getNativeModule } from '../native-modules/index.js';
21
21
  import { isRecord } from '../type-guards.js';
22
22
  import { DEFAULT_POSITIVE_TEXT, normalizeButtons, } from './shared.js';
23
23
  const DIALOG_MANAGER = 'DialogManagerAndroid';
@@ -12,7 +12,7 @@
12
12
  // Non-throwing, like StatusBar: a missing native module is a no-op, never a crash (on a
13
13
  // device the module may be absent).
14
14
  import { dlog } from '../debug.js';
15
- import { getNativeModule } from '../native-modules.js';
15
+ import { getNativeModule } from '../native-modules/index.js';
16
16
  const ALERT_MANAGER = 'AlertManager';
17
17
  // prompt builds the native args, assigns each button its array index as id, and dispatches
18
18
  // the matching button's onPress when the native callback returns that id. Non-throwing: no
@@ -10,7 +10,7 @@
10
10
  // module is resolved lazily on first use, so importing this file headless (no
11
11
  // native host) is inert until a native-driven animation actually starts.
12
12
  import { dlog } from '../../debug.js';
13
- import { getNativeModule } from '../../native-modules.js';
13
+ import { getNativeModule } from '../../native-modules/index.js';
14
14
  import { NativeEventEmitter } from '../../native-events.js';
15
15
  import { isRecord } from '../../type-guards.js';
16
16
  // iOS bridgeless registers the Turbo variant; the legacy name is the fallback.
@@ -11,7 +11,7 @@
11
11
  // registrar. Reached the same way shared reaches processColor: a dependency-injected seam
12
12
  // (setHostRegistrar), so the core stays react-native-free and the app glue wires the host once
13
13
  // at startup.
14
- import { getNativeModule } from '../native-modules.js';
14
+ import { getNativeModule } from '../native-modules/index.js';
15
15
  import { dlog } from '../debug.js';
16
16
  const HEADLESS_TASK_MODULE = 'HeadlessJsTaskSupport';
17
17
  // Builds one adapter's AppRegistry. `runnableFor` is the sole framework-specific seam: given a
@@ -5,7 +5,7 @@
5
5
  // NativeEventEmitter bound to the AppState native module and maps them onto the
6
6
  // public 'change'/'memoryWarning'/'focus'/'blur' listeners, mirroring RN's
7
7
  // Libraries/AppState/AppState.js.
8
- import { createDeviceEventModule } from '../native-modules.js';
8
+ import { createDeviceEventModule } from '../native-modules/index.js';
9
9
  import {} from '../native-events.js';
10
10
  import { dlog } from '../debug.js';
11
11
  // The native module name RN registers AppState under, confirmed from its spec
@@ -4,7 +4,7 @@
4
4
  // native module (which RN keys its appearance events off of) and re-broadcasts to
5
5
  // JS listeners as a plain `{ colorScheme }` payload. Mirrors RN's
6
6
  // Libraries/Utilities/Appearance.js, slimmed to the parts we need.
7
- import { createDeviceEventModule } from '../native-modules.js';
7
+ import { createDeviceEventModule } from '../native-modules/index.js';
8
8
  import {} from '../native-events.js';
9
9
  import { dlog } from '../debug.js';
10
10
  // The native module name RN registers the appearance module under, confirmed from
@@ -6,7 +6,7 @@
6
6
  // Android-only by intent: iOS has no hardware back button, so the whole module
7
7
  // degrades to a no-op when the native module is absent, matching RN's
8
8
  // BackHandler.ios.js stub. Mirrors RN's Libraries/Utilities/BackHandler.android.js.
9
- import { createDeviceEventModule } from '../native-modules.js';
9
+ import { createDeviceEventModule } from '../native-modules/index.js';
10
10
  import {} from '../native-events.js';
11
11
  import { dlog } from '../debug.js';
12
12
  // The native module name RN registers this under. NOTE: this is the name the
@@ -11,7 +11,7 @@
11
11
  // We resolve it through the same generic native-module bridge as Platform
12
12
  // (getNativeModule), so this module stays importable headless before a fake proxy
13
13
  // is installed.
14
- import { createDeviceEventModule } from '../native-modules.js';
14
+ import { createDeviceEventModule } from '../native-modules/index.js';
15
15
  import {} from '../native-events.js';
16
16
  import { dlog } from '../debug.js';
17
17
  import { isRecord } from '../type-guards.js';
@@ -13,7 +13,7 @@
13
13
  // forceRTL(forceRTL: boolean)
14
14
  // swapLeftAndRightInRTL(flipStyles: boolean)
15
15
  import { dlog } from '../debug.js';
16
- import { getNativeModule } from '../native-modules.js';
16
+ import { getNativeModule } from '../native-modules/index.js';
17
17
  import { isBoolean } from '../type-guards.js';
18
18
  // The iOS native module name RN registers this under (the same name on both
19
19
  // platforms). A module name is only provable on a real host (a headless fake
@@ -15,7 +15,7 @@
15
15
  // native bridge).
16
16
  import { dlog } from './debug.js';
17
17
  import { resolveImageSource } from './image-source-resolver.js';
18
- import { getNativeModule } from './native-modules.js';
18
+ import { getNativeModule } from './native-modules/index.js';
19
19
  import { Platform } from './platform';
20
20
  import { isNumber } from './type-guards.js';
21
21
  // The iOS native module name RN registers this under (NativeImageLoaderIOS.js resolves
package/build/index.js CHANGED
@@ -37,7 +37,7 @@ export { registerStyles, resolveClassName, clearGlobalStyles, isClassNameValue,
37
37
  export { scopeClassName } from './style-registry/scope.js';
38
38
  export { Platform } from './platform';
39
39
  export { dlog, isDebug } from './debug.js';
40
- export { getNativeModule, getEnforcingNativeModule } from './native-modules.js';
40
+ export { getNativeModule, getEnforcingNativeModule } from './native-modules/index.js';
41
41
  export { installDeviceEventHub, NativeEventEmitter, setDeviceEventSource } from './native-events.js';
42
42
  export { AnimatedNode, AnimatedWithChildren, AnimatedValue, AnimatedValueXY, AnimatedColor, AnimatedInterpolation, AnimatedAddition, AnimatedSubtraction, AnimatedMultiplication, AnimatedDivision, AnimatedModulo, AnimatedDiffClamp, AnimatedEvent, add, subtract, multiply, divide, modulo, diffClamp, event, forkEvent, unforkEvent, attachNativeEvent, attachNativeEventHandler, flushValue, Easing, createNumericInterpolation, checkValidRanges, TimingAnimation, SpringAnimation, DecayAnimation, AnimatedTracking, timing, spring, decay, parallel, sequence, stagger, loop, delay, nativeAnimated, isNativeAnimatedAvailable, AnimatedProps, AnimatedStyle, AnimatedTransform, AnimatedMock, reduceProps, isAnimatedNode, readPassthroughStyle, resolveHostNode, } from './animated/index.js';
43
43
  export { getSlot } from './fabric.js';
@@ -3,7 +3,7 @@
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 { createDeviceEventModule } from '../native-modules.js';
6
+ import { createDeviceEventModule } from '../native-modules/index.js';
7
7
  import {} from '../native-events.js';
8
8
  import { dlog } from '../debug.js';
9
9
  import { blurTextInput, currentlyFocusedInput } from '../text-input-state.js';
@@ -7,7 +7,7 @@
7
7
  //
8
8
  // It is a native-bridge consumer (RN's `Platform`/`StyleSheet` purity split puts
9
9
  // these in the adapter, not in shared, like Keyboard / StatusBar).
10
- import { getNativeModule } from '../native-modules.js';
10
+ import { getNativeModule } from '../native-modules/index.js';
11
11
  import { dlog } from '../debug.js';
12
12
  import { isRecord } from '../type-guards.js';
13
13
  // ---- native module routing ----------------------------------------------
@@ -8,7 +8,7 @@
8
8
  // the base linking.ts re-exports the iOS build for web/headless. There is no runtime
9
9
  // `Platform.OS` read; the filename is the selector.
10
10
  import { installDeviceEventHub, NativeEventEmitter, } from '../native-events.js';
11
- import { getNativeModule } from '../native-modules.js';
11
+ import { getNativeModule } from '../native-modules/index.js';
12
12
  import { dlog } from '../debug.js';
13
13
  // The one event symbiote observes: an incoming deep link. RN's LinkingEventDefinitions.
14
14
  const URL_EVENT = 'url';
@@ -17,7 +17,7 @@
17
17
  // requestPermission(permission): Promise<string>
18
18
  // shouldShowRequestPermissionRationale(permission): Promise<boolean>
19
19
  // requestMultiplePermissions(permissions): Promise<{[permission]: string}>
20
- import { getNativeModule } from '../native-modules.js';
20
+ import { getNativeModule } from '../native-modules/index.js';
21
21
  import { dlog } from '../debug.js';
22
22
  import { isBoolean } from '../type-guards.js';
23
23
  // The native module name RN registers this TurboModule under
@@ -5,7 +5,7 @@
5
5
  // select precedence, and the device-class getters. The filename is the selector (Metro
6
6
  // picks platform.ios / platform.android by host); no Platform.OS read lives here.
7
7
  import { dlog } from '../debug.js';
8
- import { getNativeModule } from '../native-modules.js';
8
+ import { getNativeModule } from '../native-modules/index.js';
9
9
  // The native module name RN registers PlatformConstants under, identical on both
10
10
  // platforms; only getConstants()'s SHAPE differs, which each platform file guards.
11
11
  export const PLATFORM_CONSTANTS = 'PlatformConstants';
@@ -6,7 +6,7 @@
6
6
  // device event `settingsUpdated`, which feeds the same change->fire path. Mirrors
7
7
  // RN's Libraries/Settings/Settings.ios.js, iOS surface only.
8
8
  import { installDeviceEventHub, NativeEventEmitter, } from '../native-events.js';
9
- import { getNativeModule } from '../native-modules.js';
9
+ import { getNativeModule } from '../native-modules/index.js';
10
10
  import { dlog } from '../debug.js';
11
11
  import { isRecord } from '../type-guards.js';
12
12
  // The iOS native module name RN registers this under. NOTE: this is the name the
@@ -8,7 +8,7 @@
8
8
  // TurboModuleRegistry.get('ShareModule') from RN source, but headless fakes resolve any
9
9
  // name, so it is only proven on a real Android host (a bridgeless resolution log there).
10
10
  import { dlog } from '../debug.js';
11
- import { getNativeModule } from '../native-modules.js';
11
+ import { getNativeModule } from '../native-modules/index.js';
12
12
  import { validateContent, shareActions, SHARED_ACTION, DISMISSED_ACTION } from './shared.js';
13
13
  const SHARE_MODULE = 'ShareModule';
14
14
  // ShareModule.share resolves an untyped value at the native boundary; narrow it before
@@ -5,7 +5,7 @@
5
5
  // callback. Metro picks this file on an iOS host; the base share.ts re-exports it for
6
6
  // web/headless.
7
7
  import { dlog } from '../debug.js';
8
- import { getNativeModule } from '../native-modules.js';
8
+ import { getNativeModule } from '../native-modules/index.js';
9
9
  import { ACTION_SHEET_MANAGER, } from '../action-sheet-ios/index.js';
10
10
  import { validateContent, shareActions, SHARED_ACTION, DISMISSED_ACTION } from './shared.js';
11
11
  export const Share = {
@@ -10,7 +10,7 @@
10
10
  // replace. Now that render.ts installs RN$stopSurface and tears surfaces down cleanly, the
11
11
  // relayout survives and the bar updates without blanking (verified on device: show/hide +
12
12
  // light/dark text). See render.ts's installStopSurfaceGlobal.
13
- import { getNativeModule } from '../native-modules.js';
13
+ import { getNativeModule } from '../native-modules/index.js';
14
14
  import { dlog } from '../debug.js';
15
15
  import { processColor } from '../platform-color/index.js';
16
16
  import { STATUS_BAR_MANAGER } from './shared.js';
@@ -8,7 +8,7 @@
8
8
  // boundary getNativeModule<T> resolves against. Metro picks this on an iOS host; the base
9
9
  // status-bar.ts re-exports it for tsc / tsx / headless. Each adapter wraps these with its own
10
10
  // declarative component.
11
- import { getNativeModule } from '../native-modules.js';
11
+ import { getNativeModule } from '../native-modules/index.js';
12
12
  import { dlog } from '../debug.js';
13
13
  import { STATUS_BAR_MANAGER, STATIC_HIDE_TRANSITION, hideTransition, } from './shared.js';
14
14
  // Apply a StatusBar component's props to the native module. The adapter calls this from its
@@ -5,7 +5,7 @@
5
5
  // constants. The typed ViewStyle/TextStyle live next door in styles.ts, so `create`
6
6
  // is typed against them like RN's, not left generic over a plain Record.
7
7
  import { dlog } from '../debug.js';
8
- import { getNativeModule } from '../native-modules.js';
8
+ import { getNativeModule } from '../native-modules/index.js';
9
9
  import { flattenStyle } from '../style/index.js';
10
10
  // RN's hairline factor: a "1 physical pixel" line is ~0.4 logical px, rounded to the
11
11
  // nearest device pixel. Named so the 0.4 isn't a bare magic number (RN source uses
@@ -14,7 +14,7 @@
14
14
  // showWithGravity(message: string, duration: number, gravity: number)
15
15
  // showWithGravityAndOffset(message, duration, gravity, xOffset, yOffset)
16
16
  import { dlog } from '../debug.js';
17
- import { getNativeModule } from '../native-modules.js';
17
+ import { getNativeModule } from '../native-modules/index.js';
18
18
  // The native module name RN registers this under. NOTE: this is the name the spec
19
19
  // resolves via `TurboModuleRegistry.getEnforcing<Spec>('ToastAndroid')`, NOT the
20
20
  // spec filename `INativeToastAndroid`. A module name is only provable on a real
@@ -16,7 +16,7 @@
16
16
  // the base vibration.ts re-exports the iOS build for web/headless. There is no runtime
17
17
  // `Platform.OS` read; the filename is the selector. No Fabric view, pure JS->native.
18
18
  import { dlog } from '../debug.js';
19
- import { getNativeModule } from '../native-modules.js';
19
+ import { getNativeModule } from '../native-modules/index.js';
20
20
  const VIBRATION_MODULE = 'Vibration';
21
21
  // RN's _default_vibration_length: the single-buzz duration when no pattern is given,
22
22
  // and the per-segment buzz length the iOS scheduler emits between waits.
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@symbiote-native/engine",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "SymbioteNative's retained shadow-tree engine — clone-on-write commit path + event normalization over React Native Fabric, shared by every framework adapter.",
5
+ "license": "MIT",
5
6
  "repository": {
6
7
  "type": "git",
7
8
  "url": "git+https://github.com/OneEyed1366/symbiote-native.git",
@@ -32,7 +33,7 @@
32
33
  "react-native": ">=0.86"
33
34
  },
34
35
  "devDependencies": {
35
- "@symbiote-native/test-utils": "0.1.5"
36
+ "@symbiote-native/test-utils": "0.1.6"
36
37
  },
37
38
  "scripts": {
38
39
  "typecheck": "tsc --build",
@@ -1,24 +0,0 @@
1
- import { type IImageSourceProp } from '../image-source-resolver';
2
- export type IImageSize = {
3
- width: number;
4
- height: number;
5
- };
6
- export type IImageCacheStatus = 'memory' | 'disk' | 'disk/memory';
7
- type ISizeSuccess = (width: number, height: number) => void;
8
- type ISizeFailure = (error: unknown) => void;
9
- declare function getSize(uri: string, success?: ISizeSuccess, failure?: ISizeFailure): Promise<IImageSize>;
10
- declare function getSizeWithHeaders(uri: string, headers: Record<string, string>, success?: ISizeSuccess, failure?: ISizeFailure): Promise<IImageSize>;
11
- declare function prefetch(uri: string, callback?: (requestId: number) => void): Promise<boolean>;
12
- declare function abortPrefetch(requestId: number): void;
13
- declare function queryCache(uris: string[]): Promise<Record<string, IImageCacheStatus>>;
14
- declare function resolveAssetSource(source: IImageSourceProp): unknown;
15
- export type IImageStatics = {
16
- getSize: typeof getSize;
17
- getSizeWithHeaders: typeof getSizeWithHeaders;
18
- prefetch: typeof prefetch;
19
- abortPrefetch: typeof abortPrefetch;
20
- queryCache: typeof queryCache;
21
- resolveAssetSource: typeof resolveAssetSource;
22
- };
23
- export declare const imageStatics: IImageStatics;
24
- export {};
@@ -1,175 +0,0 @@
1
- // Image static methods (RN's Image.getSize / prefetch / queryCache / etc).
2
- //
3
- // These mirror RN's iOS Image statics (Libraries/Image/Image.ios.js), which delegate to the
4
- // `ImageLoader` native (Turbo)Module declared in NativeImageLoaderIOS.js. The Android spec
5
- // (NativeImageLoaderAndroid.js) registers under the SAME module name ('ImageLoader'), so this
6
- // stays a flat, non-platform-split module - only the Android prefetch call signature differs (a
7
- // second `requestId` arg), branched on Platform.OS below, not on module name. NOTE the asymmetry
8
- // in the iOS spec: `getSize` resolves a `[width, height]` ARRAY, while `getSizeWithHeaders`
9
- // resolves a `{width, height}` OBJECT, both are guarded below before reading. The native result
10
- // crosses the I/O boundary as `unknown`; we never cast it, we narrow its shape.
11
- //
12
- // This is a stateful, native-bridge-touching imperative module (module-level ImageLoader cache +
13
- // prefetch requestId counter) with no view of its own - it belongs in @symbiote-native/engine
14
- // alongside Alert/Share, not in a view/render-*.ts file (whose contract is zero state / zero
15
- // native bridge).
16
- import { dlog } from '../debug.js';
17
- import { resolveImageSource } from '../image-source-resolver.js';
18
- import { getNativeModule } from '../native-modules.js';
19
- import { Platform } from '../platform';
20
- import { isNumber } from '../type-guards.js';
21
- // The iOS native module name RN registers this under (NativeImageLoaderIOS.js resolves
22
- // `TurboModuleRegistry.getEnforcing<Spec>('ImageLoader')`). A module name like this is only
23
- // provable on a real host - a headless fake answers to any name - so this iOS name is
24
- // device-verify-pending.
25
- const IMAGE_LOADER_MODULE = 'ImageLoader';
26
- let imageLoaderModule;
27
- function getImageLoader() {
28
- if (imageLoaderModule === undefined) {
29
- imageLoaderModule = getNativeModule(IMAGE_LOADER_MODULE);
30
- dlog(`Image: ImageLoader module ${imageLoaderModule ? 'resolved' : 'NOT resolved (null)'}`);
31
- }
32
- return imageLoaderModule;
33
- }
34
- // Narrow native's getSize result. The spec resolves a `[width, height]` array, but tolerate a
35
- // `{width, height}` object too (getSizeWithHeaders uses that shape).
36
- function toImageSize(result) {
37
- if (Array.isArray(result) && isNumber(result[0]) && isNumber(result[1])) {
38
- return { width: result[0], height: result[1] };
39
- }
40
- if (typeof result === 'object' && result !== null) {
41
- const width = Reflect.get(result, 'width');
42
- const height = Reflect.get(result, 'height');
43
- if (isNumber(width) && isNumber(height))
44
- return { width, height };
45
- }
46
- throw new Error(`Image: unexpected size result from native: ${JSON.stringify(result)}`);
47
- }
48
- function requireLoader(method) {
49
- const loader = getImageLoader();
50
- if (loader === null) {
51
- throw new Error(`Image.${method}: ImageLoader native module is not available ` +
52
- '(running headless or not linked on this host).');
53
- }
54
- return loader;
55
- }
56
- // Resolve image dimensions, optionally via success/failure callbacks. Always returns the Promise
57
- // too (RN returns void when a callback is passed, but a promise-and-callback shape is friendlier
58
- // and a strict superset).
59
- function getSize(uri, success, failure) {
60
- const promise = Promise.resolve()
61
- .then(() => requireLoader('getSize').getSize(uri))
62
- .then(toImageSize);
63
- if (typeof success === 'function') {
64
- promise
65
- .then(size => success(size.width, size.height))
66
- .catch((error) => {
67
- if (typeof failure === 'function')
68
- failure(error);
69
- else
70
- dlog(`Image.getSize failed for ${uri}: ${String(error)}`);
71
- });
72
- }
73
- return promise;
74
- }
75
- function getSizeWithHeaders(uri, headers, success, failure) {
76
- const promise = Promise.resolve()
77
- .then(() => requireLoader('getSizeWithHeaders').getSizeWithHeaders(uri, headers))
78
- .then(toImageSize);
79
- if (typeof success === 'function') {
80
- promise
81
- .then(size => success(size.width, size.height))
82
- .catch((error) => {
83
- if (typeof failure === 'function')
84
- failure(error);
85
- else
86
- dlog(`Image.getSizeWithHeaders failed for ${uri}: ${String(error)}`);
87
- });
88
- }
89
- return promise;
90
- }
91
- // Android keys an in-flight prefetch by a monotonic requestId (so abortRequest can cancel it);
92
- // RN's Image.android.js generates the same way. iOS ignores the arg.
93
- let prefetchRequestId = 0;
94
- // Download a remote image into the disk cache. Resolves to whether it succeeded. `callback`
95
- // receives the requestId (RN's Image.android.js shape) so the caller can later pass it to
96
- // abortPrefetch.
97
- async function prefetch(uri, callback) {
98
- prefetchRequestId += 1;
99
- const requestId = prefetchRequestId;
100
- if (typeof callback === 'function')
101
- callback(requestId);
102
- const loader = requireLoader('prefetch');
103
- return (Promise.resolve()
104
- // Android's prefetchImage keys an abortable request on requestId; iOS takes ONLY the uri and
105
- // throws on an extra arg (bridgeless TurboModule arg-count check). Match RN's per-platform call.
106
- .then(() => Platform.OS === 'android'
107
- ? loader.prefetchImage(uri, requestId)
108
- : loader.prefetchImage(uri))
109
- .then(result => result === true)
110
- .catch((error) => {
111
- dlog(`Image.prefetch failed for ${uri}: ${String(error)}`);
112
- throw error;
113
- }));
114
- }
115
- // Cancel an in-flight prefetch by the requestId prefetch handed back. Android only (mirrors
116
- // Image.android.js -> NativeImageLoaderAndroid.abortRequest); a missing abortRequest (iOS,
117
- // headless) is a no-op rather than a throw.
118
- function abortPrefetch(requestId) {
119
- const loader = getImageLoader();
120
- if (loader === null || typeof loader.abortRequest !== 'function') {
121
- dlog(`Image.abortPrefetch(${requestId}): no abortRequest on this host, ignoring`);
122
- return;
123
- }
124
- loader.abortRequest(requestId);
125
- }
126
- // Narrow native's queryCache result: an object mapping each known uri to its cache status.
127
- // Unknown statuses are dropped rather than trusted blindly.
128
- const CACHE_STATUS = {
129
- memory: 'memory',
130
- disk: 'disk',
131
- 'disk/memory': 'disk/memory',
132
- };
133
- function toCacheRecord(result) {
134
- const record = {};
135
- if (typeof result !== 'object' || result === null)
136
- return record;
137
- for (const key of Object.keys(result)) {
138
- const value = Reflect.get(result, key);
139
- if (typeof value === 'string' && Object.hasOwn(CACHE_STATUS, value)) {
140
- record[key] = CACHE_STATUS[value];
141
- }
142
- }
143
- return record;
144
- }
145
- async function queryCache(uris) {
146
- return Promise.resolve()
147
- .then(() => {
148
- const loader = requireLoader('queryCache');
149
- // The native queryCache never rejects (RCTImageLoader resolves getImageCacheStatus), so a
150
- // rejection here is a JS/native boundary fault: log whether the method is even callable and
151
- // the arg shape, to tell "not a function" (interop gap) from a marshalling reject.
152
- dlog(`Image.queryCache: typeof loader.queryCache=${typeof loader.queryCache} uris=${uris.length}`);
153
- return loader.queryCache(uris);
154
- })
155
- .then(toCacheRecord)
156
- .catch((error) => {
157
- dlog(`Image.queryCache failed: ${String(error)}`);
158
- throw error;
159
- });
160
- }
161
- // PURE JS: run the currently-installed source resolver (the same machinery the Image component
162
- // uses via resolveImageSource). RN's resolveAssetSource turns a require() asset id into
163
- // {uri, scale, ...}; the app injects the real one with setImageSourceResolver, and this exposes
164
- // its output to callers directly.
165
- function resolveAssetSource(source) {
166
- return resolveImageSource(source);
167
- }
168
- export const imageStatics = {
169
- getSize,
170
- getSizeWithHeaders,
171
- prefetch,
172
- abortPrefetch,
173
- queryCache,
174
- resolveAssetSource,
175
- };
@@ -1,9 +0,0 @@
1
- export declare function setImageSourceResolver(resolve: (source: unknown) => unknown): void;
2
- export declare function resolveImageSource(source: unknown): unknown;
3
- export type IImageSource = {
4
- uri?: string;
5
- scale?: number;
6
- width?: number;
7
- height?: number;
8
- };
9
- export type IImageSourceProp = IImageSource | IImageSource[] | number;
@@ -1,16 +0,0 @@
1
- // Image source resolution seam: require('./x.png') asset ids and {uri} sources are resolved by
2
- // RN's own resolveAssetSource before reaching the shared render fn. The actual resolution is
3
- // RN-platform-specific, so it is injected here rather than imported, mirroring platform-color.ts's
4
- // processColor seam - this keeps @symbiote-native/components free of a react-native dependency (and
5
- // the headless harness working). BOTH the pure renderImage view (@symbiote-native/components) and
6
- // this package's own image-loader statics (resolveAssetSource) call resolveImageSource; neither
7
- // reaches into the mutable resolver directly.
8
- let sourceResolver = source => source;
9
- export function setImageSourceResolver(resolve) {
10
- sourceResolver = resolve;
11
- }
12
- // Public mirror of RN's Image.resolveAssetSource: run a source through the injected resolver.
13
- // Headless (no resolver wired) it is the identity, so smokes see the input unchanged.
14
- export function resolveImageSource(source) {
15
- return sourceResolver(source);
16
- }
@@ -1,18 +0,0 @@
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>;
@@ -1,120 +0,0 @@
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
- }