@symbiote-native/engine 1.3.0 → 1.3.1

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 (54) 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/index.js +2 -2
  12. package/build/fabric-props.js +74 -179
  13. package/build/fabric.d.ts +0 -13
  14. package/build/fabric.js +18 -38
  15. package/build/host-access.d.ts +1 -128
  16. package/build/host-access.js +96 -205
  17. package/build/host-behavior.d.ts +0 -100
  18. package/build/host-behavior.js +125 -311
  19. package/build/image-loader.js +10 -23
  20. package/build/image-source-resolver.js +3 -7
  21. package/build/image-source-write.d.ts +0 -11
  22. package/build/image-source-write.js +14 -34
  23. package/build/imperative.d.ts +0 -28
  24. package/build/imperative.js +42 -92
  25. package/build/index.d.ts +1 -0
  26. package/build/index.js +24 -37
  27. package/build/mutation-buffer.d.ts +0 -177
  28. package/build/mutation-buffer.js +137 -308
  29. package/build/native-engine.d.ts +0 -102
  30. package/build/native-engine.js +38 -98
  31. package/build/native-events.js +9 -18
  32. package/build/native-tree-host.d.ts +0 -21
  33. package/build/native-tree-host.js +14 -31
  34. package/build/node.d.ts +0 -212
  35. package/build/node.js +338 -774
  36. package/build/post-commit.js +3 -8
  37. package/build/process-aspect-ratio.js +3 -7
  38. package/build/process-background-longhands.js +10 -19
  39. package/build/process-filter.js +11 -19
  40. package/build/process-font-variant.js +3 -7
  41. package/build/registry.d.ts +0 -33
  42. package/build/registry.js +22 -57
  43. package/build/report-error.js +4 -18
  44. package/build/structured-style.d.ts +0 -9
  45. package/build/structured-style.js +16 -31
  46. package/build/styles.js +3 -6
  47. package/build/surface.d.ts +0 -26
  48. package/build/surface.js +29 -76
  49. package/build/text-input-state.js +4 -8
  50. package/build/touch-history.js +5 -11
  51. package/build/tree-host.d.ts +0 -270
  52. package/build/tree-host.js +63 -153
  53. package/build/view-config.js +17 -37
  54. package/package.json +2 -2
@@ -1,27 +1,16 @@
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).
1
+ // Image static methods (RN's Image.getSize/prefetch/queryCache/etc). Both iOS and Android specs
2
+ // register under the same module name ('ImageLoader'), so this stays flat, non-platform-split —
3
+ // only the Android prefetch call signature differs (a second requestId arg), branched below.
4
+ // The native result crosses the I/O boundary as unknown; we never cast it, we narrow its shape.
5
+ // This is a stateful, native-bridge-touching imperative module with no view of its own — it
6
+ // belongs here alongside Alert/Share, not in a view/render-*.ts file.
16
7
  import { dlog } from './debug.js';
17
8
  import { resolveImageSource, } from './image-source-resolver.js';
18
9
  import { getNativeModule } from './native-modules/index.js';
19
10
  import { Platform } from './platform';
20
11
  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.
12
+ // The native module name RN registers this under. A module name like this is only provable on a
13
+ // real host — a headless fake answers to any name.
25
14
  const IMAGE_LOADER_MODULE = 'ImageLoader';
26
15
  let imageLoaderModule;
27
16
  function getImageLoader() {
@@ -148,10 +137,8 @@ async function queryCache(uris) {
148
137
  throw error;
149
138
  });
150
139
  }
151
- // PURE JS: run the currently-installed source resolver (the same machinery the Image component
152
- // uses via resolveImageSource). RN's resolveAssetSource turns a require() asset id into
153
- // {uri, scale, ...}; the app injects the real one with setImageSourceResolver, and this exposes
154
- // its output to callers directly.
140
+ // Pure JS: run the currently-installed source resolver (the same machinery the Image component
141
+ // uses via resolveImageSource). The app injects the real one with setImageSourceResolver.
155
142
  function resolveAssetSource(source) {
156
143
  return resolveImageSource(source);
157
144
  }
@@ -1,10 +1,6 @@
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.
1
+ // require('./x.png') asset ids and {uri} sources resolve via RN's own resolveAssetSource, which is
2
+ // platform-specific and injected here rather than imported (mirrors platform-color.ts's
3
+ // processColor seam) so @symbiote-native/components stays free of a react-native dependency.
8
4
  let sourceResolver = source => source;
9
5
  export function setImageSourceResolver(resolve) {
10
6
  sourceResolver = resolve;
@@ -1,15 +1,4 @@
1
1
  export declare const IMAGE_SOURCE_PROPS: ReadonlySet<string>;
2
- /**
3
- * Resolve a source prop and normalise it to the ARRAY shape native expects.
4
- *
5
- * Always an array, including for the single-object and asset-id cases: a bare object reaching
6
- * Fabric paints nothing and reports nothing, which is worse than an image that is simply absent.
7
- * The rule downstream then has one shape to reason about instead of three.
8
- *
9
- * A value this cannot make sense of comes back UNTOUCHED rather than wrapped. `routeProp` writes
10
- * whatever it is handed, and a tag with no image behavior must keep its props verbatim — the
11
- * control case in `image-payload.itest.ts` is what holds that line.
12
- */
13
2
  export declare function resolveImageSourceProp(value: unknown, dropsSingleSourceHeaders?: boolean): unknown;
14
3
  export declare const IMAGE_LOAD_EVENT_NAMES: ReadonlySet<string>;
15
4
  /** Whether at least one of the four still has a listener installed on the node. */
@@ -1,17 +1,7 @@
1
- // Image sources, resolved on the way IN rather than on the way out.
2
- //
3
- // WHY IT IS HERE AND NOT IN THE PAYLOAD BUILDER, which is where every other part of Image's rule
4
- // now lives. `resolveImageSource` asks METRO'S ASSET REGISTRY — the table `require('./logo.png')`
5
- // indexes into, populated at bundle time, in JavaScript. There is no such table in C++ and there
6
- // should not be: it is the bundler's, not the platform's.
7
- //
8
- // This is the same seam and the same argument as `structured-style.ts`, which resolves
9
- // `boxShadow`/`filter`/`transform` at write time for the identical reason — a value resolved at
10
- // PAYLOAD-BUILD time is resolved HEADLESS ONLY, because the C++ builder has no JS to call, and the
11
- // device then commits the raw input and Fabric drops it in silence. Moving the lookup one step
12
- // earlier costs nothing and leaves the rest of the rule pure, which is what let it move at all.
13
- //
14
- // The three names are Image's: `source` is the real one, `defaultSource` the placeholder, and
1
+ // Image sources, resolved on the way IN rather than in the C++ payload builder like the rest of
2
+ // Image's rule: resolveImageSource asks Metro's asset registry, a JS-only, bundle-time table with
3
+ // no C++ side. Same seam as structured-style.ts's write-time boxShadow/filter/transform fold.
4
+ // The three names are Image's: `source` the real one, `defaultSource` the placeholder,
15
5
  // `loadingIndicatorSource` Android's spinner.
16
6
  import { resolveImageSource } from './image-source-resolver.js';
17
7
  export const IMAGE_SOURCE_PROPS = new Set([
@@ -19,17 +9,11 @@ export const IMAGE_SOURCE_PROPS = new Set([
19
9
  'defaultSource',
20
10
  'loadingIndicatorSource',
21
11
  ]);
22
- /**
23
- * Resolve a source prop and normalise it to the ARRAY shape native expects.
24
- *
25
- * Always an array, including for the single-object and asset-id cases: a bare object reaching
26
- * Fabric paints nothing and reports nothing, which is worse than an image that is simply absent.
27
- * The rule downstream then has one shape to reason about instead of three.
28
- *
29
- * A value this cannot make sense of comes back UNTOUCHED rather than wrapped. `routeProp` writes
30
- * whatever it is handed, and a tag with no image behavior must keep its props verbatim — the
31
- * control case in `image-payload.itest.ts` is what holds that line.
32
- */
12
+ // Resolve a source prop and normalize it to the array shape native expects — always an array,
13
+ // even for the single-object and asset-id cases, since a bare object reaching Fabric paints and
14
+ // reports nothing at all.
15
+ // A value this can't make sense of comes back untouched rather than wrapped: a tag with no image
16
+ // behavior must keep its props verbatim (pinned by image-payload.itest.ts).
33
17
  export function resolveImageSourceProp(value, dropsSingleSourceHeaders = false) {
34
18
  if (value === undefined || value === null)
35
19
  return value;
@@ -50,15 +34,11 @@ export function resolveImageSourceProp(value, dropsSingleSourceHeaders = false)
50
34
  }
51
35
  return [resolved];
52
36
  }
53
- // `ReactImageView.setShouldNotifyLoadEvents` (Android) — `downloadListener` stays `null`, and
54
- // none of these four ever fires, until this prop is `true`. `Image.android.js` sets it whenever
55
- // ANY one of them is authored; iOS's native side has no such gate and never sets it.
56
- //
57
- // These four are real Fabric events (`view-config.ts`'s `COMPONENT_EVENTS.RCTImageView`), so
58
- // `routeProp` diverts them through `setEventListener`/`node.listeners`, never through `writeProp`
59
- // — unlike an ordinary function prop, they never reach the `functionProps` stash. Named here in
60
- // LISTENER form (post `listenerName()`: `onLoad` -> `load`), which is what `node.listeners` keys
61
- // on. Same shape `GATED_EVENT_PROPS` uses for `onLayout`, applied to a name no host behavior owns.
37
+ // Android's ReactImageView.setShouldNotifyLoadEvents gates on this prop: none of these four ever
38
+ // fire until Image.android.js sets it, because any one of them is authored; iOS has no such gate.
39
+ // Real Fabric events (view-config.ts's COMPONENT_EVENTS.RCTImageView), so routeProp diverts them
40
+ // through setEventListener/node.listeners, never writeProp — named in listener form (onLoad ->
41
+ // load), what node.listeners keys on.
62
42
  export const IMAGE_LOAD_EVENT_NAMES = new Set([
63
43
  'loadStart',
64
44
  'load',
@@ -2,47 +2,19 @@ import type { IMeasureInWindowOnSuccess, IMeasureLayoutOnSuccess, IMeasureOnSucc
2
2
  import { type ISymbioteNode } from './node';
3
3
  export declare function registerSurfaceCommit(commit: (rootTag: IRootTag) => void, forget: (rootTag: IRootTag) => void): void;
4
4
  export declare function flushNativeProps(): void;
5
- /**
6
- * Publish a node whose props changed OUTSIDE any renderer mutation.
7
- *
8
- * Recording is not publishing. Every other write reaches Fabric because the framework's own commit
9
- * follows it; a change driven by a NATIVE EVENT has no such follow-up — the press path's
10
- * `setNodePressed` is the first caller that is not `setNativeProps`.
11
- *
12
- * Queued rather than committed on the spot: several writes in one task publish together at the
13
- * microtask boundary, one commit per surface.
14
- */
15
5
  export declare function requestCommitFor(node: ISymbioteNode): void;
16
6
  export declare function disposeRoot(rootTag: IRootTag): void;
17
7
  /** The committed reactTag, stable across clone-on-write — what the native Animated driver binds. */
18
8
  export declare function getNativeTag(node: ISymbioteNode): number | undefined;
19
- /**
20
- * The node's current native handle, in kind identical to React's `stateNode.node`.
21
- *
22
- * OPAQUE. Under the native host it is a `ShadowNode`, under a headless one whatever that host
23
- * committed — see `ICommittedRecord.handle`. It used to be typed `IFabricNode`, a brand with no
24
- * members, so a caller can do exactly as much with it as before.
25
- */
26
9
  export declare function getNativeNode(node: ISymbioteNode): object | undefined;
27
10
  /** Called by the commit path once a batch has published. */
28
11
  export declare function notifyCommitted(): void;
29
- /**
30
- * Run `action` once `node` has a committed Fabric handle — immediately if it already does, else
31
- * after the commit that assigns one. Returns a cancel fn (drop the retry, e.g. on unmount).
32
- */
33
12
  export declare function whenCommitted(node: ISymbioteNode, action: () => void): () => void;
34
13
  export declare function dispatchViewCommand(node: ISymbioteNode, commandName: string, args: readonly unknown[]): void;
35
14
  export declare function sendAccessibilityEvent(node: ISymbioteNode, eventType: string): void;
36
15
  export declare function measure(node: ISymbioteNode, callback: IMeasureOnSuccess): void;
37
16
  export declare function measureInWindow(node: ISymbioteNode, callback: IMeasureInWindowOnSuccess): void;
38
17
  export declare function measureLayout(node: ISymbioteNode, relativeTo: ISymbioteNode, onSuccess: IMeasureLayoutOnSuccess, onFail?: () => void): void;
39
- /**
40
- * Tell native that JS has taken the gesture, or given it up.
41
- *
42
- * Through the HOST like the five above it, never through the Fabric slot: under the native tree
43
- * host the committed handle is our placeholder, and `nativeFabricUIManager.setIsJSResponder` unwraps
44
- * only a `ShadowNode` reference it minted itself.
45
- */
46
18
  export declare function setIsJSResponder(node: ISymbioteNode, isResponder: boolean, blockNativeResponder: boolean): void;
47
19
  /** The node's CURRENT prop value, as the host holds it. The behaviors' one read. */
48
20
  export declare function propOf(node: ISymbioteNode, key: string): unknown;
@@ -1,29 +1,18 @@
1
- // The imperative half of what `commit.ts` used to export — everything an app or an adapter reaches
2
- // for OUTSIDE a render: measure, view commands, accessibility events, the Animated fast path.
3
- //
4
- // It survived the tree's removal unchanged in SHAPE, because every one of these was already written
5
- // against three fields — a Fabric handle, a tag, a root tag — and never against the walk. What
6
- // changed is where those come from: `committedOf(node)` read an `IMirror` on the node, and the tree
7
- // HOST answers now (`tree-host.ts`) — native on device, the TypeScript applier headlessly.
8
- //
9
- // `undefined` is the ORDINARY state here, not an error. An adapter that wires an imperative call at
10
- // lifecycle time runs before `completeRoot` under an async-batched commit (Vue and Svelte schedule
11
- // it on a microtask), so the node has no handle yet. Every function either defers (`whenCommitted`)
12
- // or logs and returns — never throws.
1
+ // Everything an app or adapter reaches for outside a render: measure, view commands, accessibility
2
+ // events, the Animated fast path. Each is written against a Fabric handle/tag/root tag answered by
3
+ // the tree host (tree-host.ts): native on device, a TypeScript applier headlessly.
4
+ // undefined is the ordinary state here, not an error. An adapter wiring an imperative call at
5
+ // lifecycle time can run before completeRoot under an async-batched commit, so the node has no
6
+ // handle yet. Every function either defers (whenCommitted) or logs and returns — never throws.
13
7
  import { dlog, isDebug } from './debug.js';
14
8
  import { clearPublishedStyle, writeProp } from './node.js';
15
9
  import { flattenStyle } from './style/index.js';
16
10
  import { flushOps, treeHost } from './tree-host.js';
17
- /**
18
- * What Fabric currently holds for `node`, or `undefined` before its first commit.
19
- *
20
- * Flushes first: an imperative call can land between a mutation and its commit, and the host cannot
21
- * answer about ops it has not been handed.
22
- *
23
- * `undefined` is the ORDINARY state, not an error — and it is also what a WRAPPED node gets. The host
24
- * keys on the handle OBJECT, so a Vue `reactive()` / deep-`ref()` Proxy around a host element misses
25
- * and every function below degrades to its log. Hold host nodes with `shallowRef`.
26
- */
11
+ // What Fabric currently holds for `node`, or undefined before its first commit. Flushes first: an
12
+ // imperative call can land between a mutation and its commit, and the host can't answer about ops
13
+ // it hasn't been handed.
14
+ // undefined is also what a wrapped node gets: the host keys on the handle object, so a Vue
15
+ // reactive()/deep-ref() Proxy around a host element misses. Hold host nodes with shallowRef.
27
16
  function committedRecordOf(node) {
28
17
  flushOps();
29
18
  return treeHost()?.committedRecordOf(node);
@@ -48,16 +37,11 @@ export function flushNativeProps() {
48
37
  for (const rootTag of roots)
49
38
  commitSurface?.(rootTag);
50
39
  }
51
- /**
52
- * Publish a node whose props changed OUTSIDE any renderer mutation.
53
- *
54
- * Recording is not publishing. Every other write reaches Fabric because the framework's own commit
55
- * follows it; a change driven by a NATIVE EVENT has no such follow-up — the press path's
56
- * `setNodePressed` is the first caller that is not `setNativeProps`.
57
- *
58
- * Queued rather than committed on the spot: several writes in one task publish together at the
59
- * microtask boundary, one commit per surface.
60
- */
40
+ // Publish a node whose props changed outside any renderer mutation. Recording is not publishing:
41
+ // every other write reaches Fabric because the framework's own commit follows it, but a change
42
+ // driven by a native event has no such follow-up (setNodePressed is the first such caller).
43
+ // Queued rather than committed on the spot: several writes in one task publish together at the
44
+ // microtask boundary, one commit per surface.
61
45
  export function requestCommitFor(node) {
62
46
  const record = committedRecordOf(node);
63
47
  if (record === undefined) {
@@ -66,14 +50,9 @@ export function requestCommitFor(node) {
66
50
  }
67
51
  requestCommitForRoot(record.rootTag);
68
52
  }
69
- /**
70
- * The same request, for a caller that already holds the node's record.
71
- *
72
- * `committedRecordOf` crosses the host boundary, and `setNativeProps` fetches the record two lines
73
- * before it calls this — so asking again was two crossings for one fact, on the path an
74
- * `AnimatedProps` leaf runs once per frame per animated node. Measured at 2 of the 3 crossings a
75
- * frame made (`__tests__/animated-frame-cost.test.ts`).
76
- */
53
+ // The same request, for a caller that already holds the node's record. committedRecordOf crosses
54
+ // the host boundary, and setNativeProps fetches the record two lines before it calls this — asking
55
+ // again would be a second crossing for one fact, on a path an AnimatedProps leaf runs per frame.
77
56
  function requestCommitForRoot(rootTag) {
78
57
  pendingRoots.add(rootTag);
79
58
  if (!flushScheduled) {
@@ -92,20 +71,15 @@ export function disposeRoot(rootTag) {
92
71
  export function getNativeTag(node) {
93
72
  return committedRecordOf(node)?.tag;
94
73
  }
95
- /**
96
- * The node's current native handle, in kind identical to React's `stateNode.node`.
97
- *
98
- * OPAQUE. Under the native host it is a `ShadowNode`, under a headless one whatever that host
99
- * committed — see `ICommittedRecord.handle`. It used to be typed `IFabricNode`, a brand with no
100
- * members, so a caller can do exactly as much with it as before.
101
- */
74
+ // The node's current native handle, in kind identical to React's stateNode.node. Opaque: under the
75
+ // native host it's a ShadowNode, under a headless one whatever that host committed (see
76
+ // ICommittedRecord.handle).
102
77
  export function getNativeNode(node) {
103
78
  return committedRecordOf(node)?.handle;
104
79
  }
105
- // Actions waiting for their node's first commit. An adapter that wires an imperative call at
106
- // lifecycle time can run BEFORE completeRoot under an async-batched commit, so the node has no
107
- // handle yet and the call would silently no-op. Each waiter retries after a commit and is dropped
108
- // once it runs. React commits synchronously, so its actions run inline and never land here.
80
+ // Actions waiting for their node's first commit — an adapter wiring a call at lifecycle time can
81
+ // run before completeRoot under an async-batched commit, so the node has no handle yet. Each
82
+ // waiter retries after a commit and is dropped once it runs; React runs its actions inline.
109
83
  const pendingCommitWaiters = new Set();
110
84
  /** Called by the commit path once a batch has published. */
111
85
  export function notifyCommitted() {
@@ -114,10 +88,8 @@ export function notifyCommitted() {
114
88
  pendingCommitWaiters.delete(waiter);
115
89
  }
116
90
  }
117
- /**
118
- * Run `action` once `node` has a committed Fabric handle — immediately if it already does, else
119
- * after the commit that assigns one. Returns a cancel fn (drop the retry, e.g. on unmount).
120
- */
91
+ // Run `action` once `node` has a committed Fabric handle — immediately if it already does, else
92
+ // after the commit that assigns one. Returns a cancel fn to drop the retry (e.g. on unmount).
121
93
  export function whenCommitted(node, action) {
122
94
  const attempt = () => {
123
95
  if (committedRecordOf(node) === undefined)
@@ -175,13 +147,9 @@ export function measureLayout(node, relativeTo, onSuccess, onFail = () => { }) {
175
147
  }
176
148
  treeHost()?.measureLayout(node, relativeTo, onFail, onSuccess);
177
149
  }
178
- /**
179
- * Tell native that JS has taken the gesture, or given it up.
180
- *
181
- * Through the HOST like the five above it, never through the Fabric slot: under the native tree
182
- * host the committed handle is our placeholder, and `nativeFabricUIManager.setIsJSResponder` unwraps
183
- * only a `ShadowNode` reference it minted itself.
184
- */
150
+ // Tell native that JS has taken the gesture, or given it up. Through the host like the five above
151
+ // it, never through the Fabric slot: under the native tree host the committed handle is our
152
+ // placeholder, and nativeFabricUIManager's own version unwraps only a ShadowNode it minted itself.
185
153
  export function setIsJSResponder(node, isResponder, blockNativeResponder) {
186
154
  if (committedRecordOf(node) === undefined) {
187
155
  dlog('setIsJSResponder skipped: node not committed');
@@ -194,22 +162,12 @@ export function propOf(node, key) {
194
162
  flushOps();
195
163
  return treeHost()?.propOf(node, key);
196
164
  }
197
- // Per-frame prop write for the JS-driven Animated path, and the one write that bypasses the
198
- // declarative style. RN flushes an animation frame with an in-place `instance.setNativeProps(...)`;
199
- // Fabric is persistent, so a frame is one scoped commit instead.
200
- //
201
- // IT NO LONGER BYPASSES ANYTHING, and the correction it used to owe the host is gone with the
202
- // bypass. This wrote to Fabric directly once, so the host had to be told what had been sent behind
203
- // its back or its next diff would be computed against a stale base. The write is an ordinary op now
204
- // — the commit that carries it updates that record itself. Telling the host EARLY was strictly
205
- // worse than not telling it: `noteNativePropsBypass` folded the values into `committedProps` before
206
- // the commit ran, so the diff found nothing to send and the write never reached Fabric at all.
207
- //
208
- // One correction survives, because it is about the ENGINE's own state rather than the host's:
209
- // `parts.published` is cleared, so `pushClassStyle`'s guard cannot turn away the re-push that
210
- // RESTORES the declarative style this frame just overwrote. The guard is exact, and an app handing
211
- // over a hoisted style constant (StyleSheet.create, a module-level object) would otherwise never
212
- // get it back.
165
+ // Per-frame prop write for the JS-driven Animated path — RN flushes an animation frame with an
166
+ // in-place instance.setNativeProps(...); Fabric is persistent, so a frame is one scoped commit
167
+ // instead, an ordinary op the commit that carries it accounts for on its own.
168
+ // One correction survives, about the engine's own state: parts.published is cleared, so
169
+ // pushClassStyle's guard can't turn away the re-push that restores the declarative style this
170
+ // frame just overwrote — otherwise a hoisted style constant would never come back.
213
171
  export function setNativeProps(node, partial) {
214
172
  const record = committedRecordOf(node);
215
173
  if (record === undefined) {
@@ -229,30 +187,22 @@ export function setNativeProps(node, partial) {
229
187
  }
230
188
  else {
231
189
  merged[key] = value;
232
- // This path used to owe the aria gate that `setProp` carried, since it bypasses it. There is
233
- // no gate to owe any more: the fold is the device's rule and it recomputes presence from the
234
- // bag. See `node.ts`'s note at the old choke point.
235
190
  }
236
191
  }
237
192
  for (const [key, value] of Object.entries(merged)) {
238
- // `writeProp`, not `recordSetProp`: this is the path with no `routeProp` in front of it, so a
239
- // function reaches the wire from here and nowhere else. `AnimatedProps.__getValue()` copies
240
- // every key its leaf holds, `panHandlers` included.
193
+ // writeProp, not recordSetProp: this is the path with no routeProp in front of it, so a
194
+ // function reaches the wire from here and nowhere else (AnimatedProps.__getValue() copies
195
+ // every key its leaf holds, panHandlers included).
241
196
  writeProp(node, key, value);
242
197
  }
243
198
  if (merged.style !== undefined)
244
199
  clearPublishedStyle(node);
245
- // GATED, and this is the one place in the engine where "per frame" is the right unit to worry
246
- // about. An `AnimatedProps` leaf calls this once per frame per animated node, so the template
247
- // string and the `Object.keys` array it builds are 60 allocations a second per node — times the
248
- // list, for a list that animates its rows. F-61 dismissed 347 ungated arguments because none of
249
- // them was on a path that repeats; this one is, and F-61 had it in its own in-loop list and
250
- // priced it as "runs once".
200
+ // Gated: an AnimatedProps leaf calls this once per frame per animated node, so the template
201
+ // string and Object.keys array below would otherwise allocate 60 times a second per node.
251
202
  if (isDebug()) {
252
203
  dlog(`setNativeProps root=${record.rootTag} tag=${record.tag} keys=${Object.keys(partial)}`);
253
204
  }
254
205
  // Queued, not committed: every write made in this task publishes together at the microtask
255
- // boundary. With the record already in hand — asking for it again is a second crossing for a
256
- // fact this function fetched four lines up.
206
+ // boundary. The record is already in hand, so this avoids a second crossing for the same fact.
257
207
  requestCommitForRoot(record.rootTag);
258
208
  }
package/build/index.d.ts CHANGED
@@ -65,6 +65,7 @@ export { imageStatics } from './image-loader';
65
65
  export type { IImageStatics, IImageSize, IImageCacheStatus, } from './image-loader';
66
66
  export { setImageSourceResolver, resolveImageSource, } from './image-source-resolver';
67
67
  export type { IImageSource, IImageSourceProp } from './image-source-resolver';
68
+ export { setAssetSourceResolver, resolveAssetSource, } from './asset-source-resolver';
68
69
  export { ActionSheetIOS } from './action-sheet-ios';
69
70
  export type { IActionSheetIOSOptions, IShareActionSheetIOSOptions, IShareActionSheetError, } from './action-sheet-ios';
70
71
  export { Linking } from './linking';
package/build/index.js CHANGED
@@ -1,23 +1,18 @@
1
- // @symbiote-native/engine: the retained shadow-tree + clone-on-write commit engine.
2
- // Every framework adapter drives this tiny mutation API; all Fabric-specific
3
- // logic (tag allocation, view-name resolution, clone-on-write, event
4
- // normalization) lives behind it, in one place.
1
+ // @symbiote-native/engine: the retained shadow-tree + clone-on-write commit engine. Every framework
2
+ // adapter drives this tiny mutation API; all Fabric-specific logic (tag allocation, view-name
3
+ // resolution, clone-on-write, event normalization) lives behind it, in one place.
5
4
  export { createElement, createRawText, createAnchor,
6
- // The component name of a node the commit walk skips and whose children flatten into its parent.
7
- // Exported so a PRIMITIVE that renders no view of its own can be born with it — RN's
8
- // TouchableNativeFeedback clones onto its single child and commits nothing
9
- // (TouchableNativeFeedback.js:339) — rather than being converted after the fact.
5
+ // The component name of a node the commit walk skips and whose children flatten into its
6
+ // parent. Exported so a primitive rendering no view of its own (TouchableNativeFeedback) can be
7
+ // born with it directly, rather than converted after the fact.
10
8
  ANCHOR_COMPONENT, isAnchor, createVoid,
11
- // The component name of a node whose ENTIRE subtree the commit walk drops — unlike
12
- // `ANCHOR_COMPONENT`, which hoists its children up in its place, a void node contributes neither
13
- // itself nor them. For a primitive whose whole component renders nothing on this platform —
14
- // `input-accessory-view` on Android, `InputAccessoryView.js`'s `return null`.
9
+ // The component name of a node whose entire subtree the commit walk drops — unlike
10
+ // ANCHOR_COMPONENT, which hoists children in its place, a void node contributes neither itself
11
+ // nor them. For a primitive whose whole component renders nothing on this platform.
15
12
  VOID_COMPONENT, appendChild, insertBefore, removeChild, setProp, setEventListener, routeProp, censusRetainedTree, getExplicitStyle,
16
- // Exported for the ONE adapter that has to build style objects rather than receive them: Angular's
17
- // `ɵɵstyleMap` hands over keys, so its renderer allocates a fresh object per node and needs to
18
- // recognise one it has already published. A second copy of this comparator in the adapter is the
19
- // mirror shape this codebase deletes on sight — and its deliberate conservatism (a nested value
20
- // reports "not the same") is exactly right for that use too.
13
+ // Exported for the one adapter that has to build style objects rather than receive them:
14
+ // Angular's ɵɵstyleMap hands over keys, so its renderer allocates a fresh object per node and
15
+ // needs to recognise one it has already published.
21
16
  isSameShallowStyle, getPublishedStyle, setNodeHidden, setNodeComponent, setNodePressed, setNodeUnderlayShown, setText, isSymbioteNode, isSymbioteEvent, RAW_TEXT_COMPONENT, debugNodeId, takePropKeyTally, } from './node.js';
22
17
  // Host access — the DOM read half Fabric does not ship. Adapters route their seam's
23
18
  // parentNode/nextSibling/firstChild through these instead of reading a node's fields, which is
@@ -53,18 +48,12 @@ export { setTreeHost, treeHost, registerBeforeFlush, readCommitProfile, readSurf
53
48
  // `installNativeTreeHost` is not on any app path: `getSlot()` already calls it, and it is here so a
54
49
  // bring-up probe can install one explicitly against a hand-built set of bindings.
55
50
  export { nativeTreeHost, installNativeTreeHost } from './native-tree-host.js';
56
- // The OPCODES and the recorder are deliberately NOT here. They are the wire contract between this
57
- // package and whatever implements the tree, so their audience is a HOST author — the C++ and the
58
- // TypeScript applier — not an app. They live on the `@symbiote-native/engine/mutation-buffer`
59
- // subpath, for the same reason `state-style` does: a name on this barrel is public API on all five
60
- // adapters at once, with no edit to any of them
61
- // (`.claude/rules/adapter-parity-audit.md`, "A build-tool-facing symbol belongs on a SUBPATH").
62
- // "A commit just reached completeRoot." The one seam that means the same thing under every
63
- // adapter: React commits synchronously inside its own commit phase, while Vue / Svelte / Angular
64
- // schedule completeRoot on a microtask, so each framework's own after-render hook fires at a
65
- // DIFFERENT point relative to the native commit. Anything timing or comparing the commit path
66
- // across adapters has to hang off this, not off a per-framework lifecycle hook, or it measures a
67
- // different quantity in each one under the same name.
51
+ // The opcodes and the recorder are deliberately NOT here: they're the wire contract between this
52
+ // package and whatever implements the tree, audience a host author, not an app. They live on the
53
+ // mutation-buffer subpath instead, since a name on this barrel is public API on all five adapters.
54
+ // "A commit just reached completeRoot" — the one seam that means the same thing under every
55
+ // adapter: React commits synchronously, Vue/Svelte/Angular schedule it on a microtask, so each
56
+ // framework's after-render hook fires at a different point relative to the native commit.
68
57
  export { registerPostCommit, unregisterPostCommit } from './post-commit.js';
69
58
  // The aria/role -> accessibility* fold. Lives here rather than in a component wrapper because a tag
70
59
  // has none: `fabricProps` runs it on the way to the payload, so every path gets it.
@@ -104,9 +93,8 @@ export { getSlot } from './fabric.js';
104
93
  // another package (`@symbiote-native/test-utils`) and can no longer reach `./fabric` directly.
105
94
  export { resetSlot } from './fabric.js';
106
95
  // Imperative runtime modules: framework-agnostic native-bridge consumers (no visual, no
107
- // lifecycle), moved here from @symbiote-native/react so every adapter re-exports the SAME module.
108
- // The native module a JS API talks to is chosen per platform and can only be confirmed on a
109
- // real device or simulator, not headless (a headless fake resolves any module name).
96
+ // lifecycle), so every adapter re-exports the same module. The native module a JS API talks to is
97
+ // chosen per platform and can only be confirmed on a real device or simulator, not headless.
110
98
  export { Alert } from './alert';
111
99
  export { Share } from './share';
112
100
  // Image statics (getSize/prefetch/queryCache/...): a stateful, native-bridge-touching module with
@@ -114,6 +102,7 @@ export { Share } from './share';
114
102
  // @symbiote-native/components' renderImage lives alongside it in image-source-resolver.
115
103
  export { imageStatics } from './image-loader.js';
116
104
  export { setImageSourceResolver, resolveImageSource, } from './image-source-resolver.js';
105
+ export { setAssetSourceResolver, resolveAssetSource, } from './asset-source-resolver.js';
117
106
  export { ActionSheetIOS } from './action-sheet-ios/index.js';
118
107
  export { Linking } from './linking';
119
108
  export { Vibration } from './vibration';
@@ -143,9 +132,7 @@ export { AccessibilityInfo } from './accessibility-info';
143
132
  export { applyStatusBarProps, statusBarImperative, statusBarCurrentHeight, } from './status-bar';
144
133
  export { hideTransition, STATUS_BAR_MANAGER, ANIMATED_HIDE_TRANSITION, STATIC_HIDE_TRANSITION, } from './status-bar/shared.js';
145
134
  export { registerHostBehavior, hasHostBehaviors, hostBehaviorFor, clearHostBehaviors, appListenerFor, addDerivedNode, SLOT_DERIVED_ALL, } from './host-behavior.js';
146
- // `markPropsDirty` is a behavior's only way to say "the fold reads state I just changed". Every
147
- // other dirtying route goes through a prop write, and a behavior whose payload is DERIVED — the
148
- // sticky header's debounced translateY lives in its own runtime, not in the node's props — has no
149
- // prop to write. Pair it with `requestCommitFor` (exported off `./imperative` above): dirtying is
150
- // not publishing.
135
+ // markPropsDirty is a behavior's only way to say "the fold reads state I just changed" — every
136
+ // other dirtying route goes through a prop write, and a derived-payload behavior has no prop to
137
+ // write. Pair it with requestCommitFor: dirtying is not publishing.
151
138
  export { setBehaviorListener, markPropsDirty } from './node.js';