@symbiote-native/engine 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +17 -10
  2. package/build/action-sheet-ios/index.js +4 -1
  3. package/build/animated/animations/composition.js +3 -1
  4. package/build/animated/animations/spring-config.js +4 -1
  5. package/build/animated/animations/spring.js +23 -9
  6. package/build/animated/animations/timing.js +3 -2
  7. package/build/animated/bezier.js +2 -1
  8. package/build/animated/color.js +4 -2
  9. package/build/animated/event.js +7 -3
  10. package/build/animated/graph.js +8 -3
  11. package/build/animated/index.d.ts +2 -2
  12. package/build/animated/index.js +2 -2
  13. package/build/animated/interpolation.js +7 -2
  14. package/build/animated/mock.js +1 -1
  15. package/build/animated/native/native-animated.js +1 -1
  16. package/build/animated/operators.js +27 -6
  17. package/build/animated/props.js +1 -1
  18. package/build/animated/rgba.js +18 -3
  19. package/build/animated/style.js +13 -3
  20. package/build/animated/value-xy.d.ts +8 -5
  21. package/build/animated/value-xy.js +4 -1
  22. package/build/appearance/index.js +4 -1
  23. package/build/commit.d.ts +13 -0
  24. package/build/commit.js +145 -9
  25. package/build/events/index.js +19 -8
  26. package/build/fabric-props.js +8 -2
  27. package/build/image-loader.js +3 -2
  28. package/build/index.d.ts +19 -17
  29. package/build/index.js +17 -10
  30. package/build/interaction-manager/index.js +2 -1
  31. package/build/layout-animation/index.js +12 -4
  32. package/build/native-events.js +2 -1
  33. package/build/native-modules/index.js +3 -1
  34. package/build/node.d.ts +25 -0
  35. package/build/node.js +168 -2
  36. package/build/pan-responder/index.js +20 -9
  37. package/build/permissions-android/index.js +3 -1
  38. package/build/platform/index.android.d.ts +1 -1
  39. package/build/platform/index.android.js +1 -1
  40. package/build/platform/index.ios.d.ts +1 -1
  41. package/build/platform-color/index.js +3 -1
  42. package/build/post-commit.d.ts +1 -0
  43. package/build/post-commit.js +6 -0
  44. package/build/process-background-image/index.js +30 -9
  45. package/build/process-filter.js +11 -4
  46. package/build/registry.js +10 -2
  47. package/build/report-error.js +4 -1
  48. package/build/share/index.android.js +1 -1
  49. package/build/share/index.ios.js +1 -1
  50. package/build/status-bar/index.android.js +3 -4
  51. package/build/status-bar/index.ios.js +1 -1
  52. package/build/style-registry/index.d.ts +9 -2
  53. package/build/style-registry/index.js +145 -170
  54. package/build/style-registry/scope.d.ts +15 -1
  55. package/build/style-registry/scope.js +29 -27
  56. package/build/styles.d.ts +11 -1
  57. package/build/surface.d.ts +1 -1
  58. package/build/surface.js +5 -0
  59. package/build/tags.d.ts +1 -0
  60. package/build/tags.js +31 -1
  61. package/build/touch-history.js +9 -2
  62. package/build/vibration/index.ios.js +1 -1
  63. package/build/view-config.js +14 -2
  64. package/package.json +2 -2
package/build/commit.js CHANGED
@@ -13,7 +13,7 @@
13
13
  // references to specific child handles. That bubble is inherent to a persistent
14
14
  // tree and is exactly what React's own Fabric renderer does.
15
15
  import { getSlot, } from './fabric.js';
16
- import { createElement, debugNodeId, isAnchor, VIRTUAL_TEXT_COMPONENT, } from './node.js';
16
+ import { createElement, debugNodeId, isAnchor, isEmptyRawText, markDirty, takePropStats, VIRTUAL_TEXT_COMPONENT, } from './node.js';
17
17
  import { dlog, isDebug } from './debug.js';
18
18
  import { flattenStyle } from './style/index.js';
19
19
  import { nextTag } from './tags.js';
@@ -25,6 +25,40 @@ export { processColor, setColorProcessor } from './platform-color/index.js';
25
25
  // Per-commit work counters, surfaced via dlog so a device run can prove the
26
26
  // engine is incremental (created=0 with clones after the first mount).
27
27
  const stats = { created: 0, cloneProps: 0, cloneChildren: 0, reused: 0 };
28
+ // Cumulative cost of the reconcile walk. Unlike `stats` (per-commit, zeroed at the top of every
29
+ // commit), this ACCUMULATES: one scroll frame produces a burst of commits and the frame's real cost
30
+ // is only visible as their sum. Read-and-zeroed via readCommitProfile().
31
+ //
32
+ // Deliberately NOT gated behind isDebug(): two performance.now() calls per commit are noise next to
33
+ // the walk they measure, and the number is only meaningful from a RELEASE build - dev-mode JS
34
+ // drowns the signal. The dlog below stays gated as usual.
35
+ const profile = { commits: 0, walkMs: 0, nodesVisited: 0 };
36
+ // What `renderableChildren` costs, accumulated over the same window as `profile`. Anchors are the
37
+ // only reason that function is not free, and how many a tree carries is a property of the ADAPTER,
38
+ // not of the app: a Vue/React/Svelte/Solid component is a function returning children and allocates
39
+ // no node, while an Angular component is bound to a host ELEMENT and therefore always has one
40
+ // (anchor-host-registry.ts keeps it from painting). So the same screen is a no-anchor tree under
41
+ // four adapters and an anchor-per-composed-component tree under the fifth, and only a counter tells
42
+ // them apart at runtime - a source-level grep for "anchor" measures instrumentation density, not
43
+ // trees.
44
+ //
45
+ // `scans` counts invocations; `probed` counts children actually examined by the fast-path probe,
46
+ // which is why that probe is a counted loop rather than `.some()` (a `.some` short-circuits at the
47
+ // first anchor, so children.length would overstate a defeated scan by however much it skipped).
48
+ // `flattens` is the probe defeated: a fresh array, a second pass, and a recursion into every
49
+ // anchor. `widest` is the widest single flatten in the window, because a defeated scan over 3
50
+ // children is noise and one over 1000 is not - a count alone cannot separate them.
51
+ //
52
+ // Not gated behind isDebug(), for the same reason the rest of the profile is not: integer
53
+ // increments are noise next to the array work they price, and the number is only meaningful from a
54
+ // release build.
55
+ const childScan = {
56
+ scans: 0,
57
+ probed: 0,
58
+ flattens: 0,
59
+ flattenProbed: 0,
60
+ widest: 0,
61
+ };
28
62
  // Diagnostic (gated): Fabric serializes props to folly::dynamic, which rejects a JS
29
63
  // Symbol or function with "JS Symbols are not convertible to dynamic". A hard native
30
64
  // throw deep in cloneNode*. Walk a props payload and return the dotted path of the
@@ -79,7 +113,9 @@ function guardSerializable(propsDiff, viewName, tag) {
79
113
  function viewNameFor(node, hasTextAncestor) {
80
114
  // The only position-dependent name: a <Text> inside another <Text> becomes a
81
115
  // virtual span. Everything else is the component string the adapter chose.
82
- return node.isText && hasTextAncestor ? VIRTUAL_TEXT_COMPONENT : node.component;
116
+ return node.isText && hasTextAncestor
117
+ ? VIRTUAL_TEXT_COMPONENT
118
+ : node.component;
83
119
  }
84
120
  // Fabric's clone*WithNewProps MERGES the raw payload onto the node's existing props,
85
121
  // so the payload must be a MINIMAL diff: only the keys that actually changed, plus any
@@ -129,18 +165,46 @@ function jsonEqual(a, b) {
129
165
  return keys.every(key => key in b && jsonEqual(a[key], b[key]));
130
166
  }
131
167
  const mirror = new WeakMap();
168
+ function isSkippedAtCommit(node) {
169
+ return isAnchor(node) || isEmptyRawText(node);
170
+ }
132
171
  function renderableChildren(node) {
133
172
  // Anchor nodes (Vue fragment/v-if/v-for placeholders, Angular component hosts that should
134
173
  // not paint) live in the retained tree for sibling ordering but never become Fabric views.
135
174
  // When an anchor owns children, flatten them into the parent's renderable list: this lets a
136
175
  // DOM-less framework use an anchor as a fragment/component host without adding a native
137
176
  // wrapper node. Fast path: no anchors reuses the array, so the common case allocates nothing.
138
- if (!node.children.some(isAnchor))
177
+ // An empty raw text is skipped for a different reason and with no flattening: it has no
178
+ // children, and committing it aborts the app inside Fabric's text walk (isEmptyRawText).
179
+ //
180
+ // The probe is a counted loop rather than `.some()` only so childScan can price it honestly;
181
+ // see the childScan declaration for what the five counters mean and what they were built to
182
+ // settle.
183
+ childScan.scans += 1;
184
+ const total = node.children.length;
185
+ let index = 0;
186
+ while (index < total && !isSkippedAtCommit(node.children[index]))
187
+ index += 1;
188
+ // The defeating child was examined too, so it counts.
189
+ childScan.probed += index === total ? total : index + 1;
190
+ if (index === total)
139
191
  return node.children;
192
+ childScan.flattens += 1;
193
+ childScan.flattenProbed += total;
194
+ if (total > childScan.widest)
195
+ childScan.widest = total;
140
196
  const children = [];
141
197
  for (const child of node.children) {
142
- if (isAnchor(child))
143
- children.push(...renderableChildren(child));
198
+ if (isSkippedAtCommit(child)) {
199
+ // A skipped child is flattened away here and never reaches reconcile, so this is the only
200
+ // place that can clear its dirty flag. Leaving it set would be a silent stale-UI bug:
201
+ // markDirty stops at the first dirty ancestor, so a permanently-dirty skipped node swallows
202
+ // every later mark - from an anchor's subtree, or from the setText that turns an empty raw
203
+ // text back into real content - and the real parent never learns anything changed.
204
+ child.dirty = false;
205
+ if (isAnchor(child))
206
+ children.push(...renderableChildren(child));
207
+ }
144
208
  else
145
209
  children.push(child);
146
210
  }
@@ -166,11 +230,41 @@ function logScrollChildren(node, viewName, selfTag) {
166
230
  const flag = kids.length === 1 ? 'OK' : 'MULTI!!';
167
231
  dlog(`SCROLL-${flag} ${viewName} tag=${selfTag} children(${kids.length})=[${kids.join(',')}]`);
168
232
  }
233
+ // The failure mode dirty-marking introduces: a mutation path that forgets to markDirty leaves a
234
+ // node whose desired props have drifted from what Fabric holds, and the screen silently keeps
235
+ // showing the old value - no error, no crash, nothing to grep for. So under DEBUG pay back the full
236
+ // price just saved and verify the skip was honest, naming the node loudly enough to find the
237
+ // missing mark.
238
+ function warnIfStale(node, committed) {
239
+ if (!isDebug())
240
+ return;
241
+ const fresh = fabricProps(node);
242
+ if (propsEqual(committed.props, fresh))
243
+ return;
244
+ dlog(`DIRTY-MISS ${committed.viewName}#${committed.tag} node=${debugNodeId(node)} skipped as clean ` +
245
+ `but props differ: committed=${JSON.stringify(committed.props)} desired=${JSON.stringify(fresh)}`);
246
+ }
169
247
  function reconcile(slot, node, rootTag, hasTextAncestor, renderableParent, forceFreshFamily) {
248
+ profile.nodesVisited += 1;
170
249
  const viewName = viewNameFor(node, hasTextAncestor);
250
+ const committed = mirror.get(node);
251
+ // Nothing under here changed: hand back the committed handle without rebuilding this node's
252
+ // Fabric props or descending into it at all. The walk below costs ~13 us/node on device, and an
253
+ // untouched sibling subtree would pay it on every commit. `committed.parent` is re-checked rather
254
+ // than trusted to the flag because a MOVED node can still be legitimately clean (see node.ts's
255
+ // structural ops), and a reparent must fall through to the fresh-family path below.
256
+ if (!forceFreshFamily &&
257
+ !node.dirty &&
258
+ committed !== undefined &&
259
+ committed.parent === renderableParent &&
260
+ committed.viewName === viewName) {
261
+ stats.reused += 1;
262
+ warnIfStale(node, committed);
263
+ return { handle: committed.handle, changed: false };
264
+ }
265
+ node.dirty = false;
171
266
  const props = fabricProps(node);
172
267
  const childInText = node.isText || hasTextAncestor;
173
- const committed = mirror.get(node);
174
268
  const kids = renderableChildren(node);
175
269
  // First mount, or the view kind flipped (RCTText <-> RCTVirtualText when a
176
270
  // <Text> moves in or out of another <Text>): a different native component
@@ -195,7 +289,8 @@ function reconcile(slot, node, rootTag, hasTextAncestor, renderableParent, force
195
289
  `bg=${JSON.stringify(props.backgroundColor)} color=${JSON.stringify(props.color)} ` +
196
290
  `opacity=${JSON.stringify(props.opacity)}`);
197
291
  }
198
- if (viewName === 'AndroidSwipeRefreshLayout' || viewName === 'RCTScrollView') {
292
+ if (viewName === 'AndroidSwipeRefreshLayout' ||
293
+ viewName === 'RCTScrollView') {
199
294
  dlog(`commit root=${rootTag} layoutProbe tag=${tag} view=${viewName} ` +
200
295
  `flex=${JSON.stringify(props.flex)} height=${JSON.stringify(props.height)} ` +
201
296
  `width=${JSON.stringify(props.width)} minHeight=${JSON.stringify(props.minHeight)} ` +
@@ -329,6 +424,13 @@ export function commitChildren(rootTag, children) {
329
424
  function commitContainer(rootTag) {
330
425
  const slot = getSlot();
331
426
  const container = rootContainerFor(rootTag);
427
+ // The synthetic container is dirtied here, at the one entry point, because markDirty can never
428
+ // bubble up to it: a surface's top-level nodes carry `parent === undefined` (surface.ts sets it
429
+ // deliberately), so a mark stops at the top-level node and the container above it stays clean -
430
+ // it would then early-exit and swallow the whole commit. Marking unconditionally costs one node's
431
+ // props rebuild per commit and closes the hole for both callers, mutation commit and
432
+ // setNativeProps alike.
433
+ markDirty(container);
332
434
  stats.created = 0;
333
435
  stats.cloneProps = 0;
334
436
  stats.cloneChildren = 0;
@@ -338,7 +440,11 @@ function commitContainer(rootTag) {
338
440
  // tree walk); if `start` itself never prints, the stall is upstream: React's commit
339
441
  // phase or the mutation ops before we are even called.
340
442
  dlog(`commit root=${rootTag} start children=${container.children.length}`);
443
+ const walkStart = performance.now();
341
444
  const result = reconcile(slot, container, rootTag, false, undefined, false);
445
+ const walkMs = performance.now() - walkStart;
446
+ profile.walkMs += walkMs;
447
+ profile.commits += 1;
342
448
  // Boundary seam: prints once reconcile returns. If a commit hangs and this line
343
449
  // never appears, the stall is inside reconcile (JS); if it appears but the
344
450
  // post-completeRoot line below never does, the stall is inside the native commit.
@@ -361,9 +467,34 @@ function commitContainer(rootTag) {
361
467
  const mode = stats.created > 0 && stats.reused === 0 ? 'full' : 'incremental';
362
468
  dlog(`commit root=${rootTag} ${mode} ` +
363
469
  `created=${stats.created} cloneProps=${stats.cloneProps} ` +
364
- `cloneChildren=${stats.cloneChildren} reused=${stats.reused}`);
470
+ `cloneChildren=${stats.cloneChildren} reused=${stats.reused} ` +
471
+ `walk=${walkMs.toFixed(3)}ms`);
365
472
  }
366
473
  }
474
+ export function readCommitProfile() {
475
+ const props = takePropStats();
476
+ const snapshot = {
477
+ commits: profile.commits,
478
+ walkMs: profile.walkMs,
479
+ nodesVisited: profile.nodesVisited,
480
+ propWrites: props.writes,
481
+ propNoops: props.noops,
482
+ childScans: childScan.scans,
483
+ childScanProbed: childScan.probed,
484
+ childFlattens: childScan.flattens,
485
+ childFlattenProbed: childScan.flattenProbed,
486
+ childFlattenWidest: childScan.widest,
487
+ };
488
+ profile.commits = 0;
489
+ profile.walkMs = 0;
490
+ profile.nodesVisited = 0;
491
+ childScan.scans = 0;
492
+ childScan.probed = 0;
493
+ childScan.flattens = 0;
494
+ childScan.flattenProbed = 0;
495
+ childScan.widest = 0;
496
+ return snapshot;
497
+ }
367
498
  // Targeted per-frame prop write for the JS-driven Animated path. RN flushes an
368
499
  // animation frame with an in-place `instance.setNativeProps(...)`; we have no
369
500
  // in-place mutation (Fabric is persistent), so a frame is one scoped commit: mutate
@@ -382,12 +513,17 @@ export function setNativeProps(node, partial) {
382
513
  // A partial style override MERGES onto the declarative style (RN semantics):
383
514
  // setNativeProps({style:{backgroundColor}}) recolors without dropping height
384
515
  // or radius. Transient: the next React commit re-applies the full style.
385
- node.props.style = { ...flattenStyle(node.props.style), ...flattenStyle(value) };
516
+ node.props.style = {
517
+ ...flattenStyle(node.props.style),
518
+ ...flattenStyle(value),
519
+ };
386
520
  }
387
521
  else {
388
522
  node.props[key] = value;
389
523
  }
390
524
  }
525
+ // Writes node.props directly rather than through setProp, so it owes its own mark.
526
+ markDirty(node);
391
527
  dlog(`setNativeProps root=${record.rootTag} tag=${record.tag} keys=${Object.keys(partial)}`);
392
528
  commitContainer(record.rootTag);
393
529
  }
@@ -8,7 +8,7 @@
8
8
  import { dlog } from '../debug.js';
9
9
  import { runWrapped } from '../dispatch.js';
10
10
  import { getSlot } from '../fabric.js';
11
- import { isAnchor, isSymbioteNode } from '../node.js';
11
+ import { isAnchor, isSymbioteNode, } from '../node.js';
12
12
  import { registeredNativeEvent } from '../registry.js';
13
13
  import { attachTouchHistory, recordTouchTrack, resetTouchHistory, touchHistory, } from '../touch-history.js';
14
14
  import { isRecord } from '../type-guards.js';
@@ -228,12 +228,14 @@ function lowestCommonAncestor(a, b) {
228
228
  // handler, so asking the responder again would zero its move).
229
229
  function findWantsResponder(path, captureName, bubbleName, nativeEvent, skip) {
230
230
  for (let i = path.length - 1; i >= 0; i--) {
231
- if (path[i] !== skip && callOwnListener(path[i], captureName, nativeEvent) === true) {
231
+ if (path[i] !== skip &&
232
+ callOwnListener(path[i], captureName, nativeEvent) === true) {
232
233
  return path[i];
233
234
  }
234
235
  }
235
236
  for (const node of path) {
236
- if (node !== skip && callOwnListener(node, bubbleName, nativeEvent) === true)
237
+ if (node !== skip &&
238
+ callOwnListener(node, bubbleName, nativeEvent) === true)
237
239
  return node;
238
240
  }
239
241
  return undefined;
@@ -248,7 +250,9 @@ function negotiateResponder(target, phase, nativeEvent) {
248
250
  // the responder) and skips the deepest node when it IS the responder (Responder-
249
251
  // EventPlugin.setResponderAndExtractTransfer). At touch start currentResponder is
250
252
  // cleared, so this collapses to the plain target->root start walk.
251
- const from = currentResponder === undefined ? target : lowestCommonAncestor(currentResponder, target);
253
+ const from = currentResponder === undefined
254
+ ? target
255
+ : lowestCommonAncestor(currentResponder, target);
252
256
  if (!from)
253
257
  return;
254
258
  const path = pathToRoot(from);
@@ -268,7 +272,9 @@ function negotiateResponder(target, phase, nativeEvent) {
268
272
  // A missing termination-request listener means implicit consent (RN default true);
269
273
  // only an explicit non-true answer keeps the incumbent and rejects the taker.
270
274
  const guarded = incumbent.listeners?.has(RESPONDER_TERMINATION_REQUEST) === true;
271
- const allowed = !guarded || callOwnListener(incumbent, RESPONDER_TERMINATION_REQUEST, nativeEvent) === true;
275
+ const allowed = !guarded ||
276
+ callOwnListener(incumbent, RESPONDER_TERMINATION_REQUEST, nativeEvent) ===
277
+ true;
272
278
  if (allowed) {
273
279
  // RN's transfer order (setResponderAndExtractTransfer): grant the TAKER first, then
274
280
  // terminate the incumbent. RN dispatches grant ahead of the terminationRequest too,
@@ -362,7 +368,8 @@ export function installEventHandler() {
362
368
  // started inside it; lifting ONE finger in a multi-touch gesture must NOT release.
363
369
  // onResponderEnd still fires on every finger-up. (ResponderEventPlugin: responderEnd
364
370
  // is unconditional, responderRelease is gated on noResponderTouches.)
365
- const releases = responder !== undefined && !hasRemainingResponderTouch(responder, nativeEvent);
371
+ const releases = responder !== undefined &&
372
+ !hasRemainingResponderTouch(responder, nativeEvent);
366
373
  if (releases)
367
374
  currentResponder = undefined;
368
375
  // A completed long press eats the tap (RN), but pressOut still fires below.
@@ -495,7 +502,9 @@ function bubble(target, listenerName, nativeEvent) {
495
502
  // machinery (already delivered directly, e.g. Angular's EventEmitter.subscribe) also
496
503
  // registered it through Renderer2.listen. Bubbling into it would refire that same
497
504
  // callback a second time, so anchors are transparent to listener lookup, not just paint.
498
- const listener = isAnchor(node) ? undefined : node.listeners?.get(captureName);
505
+ const listener = isAnchor(node)
506
+ ? undefined
507
+ : node.listeners?.get(captureName);
499
508
  if (listener) {
500
509
  dlog(`event ${listenerName} capture on ${node.component}`);
501
510
  listener({
@@ -513,7 +522,9 @@ function bubble(target, listenerName, nativeEvent) {
513
522
  // transparent here too, same reason (see the capture-phase comment above).
514
523
  let node = target;
515
524
  while (node) {
516
- const listener = isAnchor(node) ? undefined : node.listeners?.get(listenerName);
525
+ const listener = isAnchor(node)
526
+ ? undefined
527
+ : node.listeners?.get(listenerName);
517
528
  if (listener) {
518
529
  const event = {
519
530
  type: listenerName,
@@ -70,11 +70,17 @@ const COLOR_PROPS = new Set([
70
70
  const STYLE_PROCESSORS = new Map([
71
71
  ['boxShadow', value => processBoxShadow(asBoxShadowInput(value))],
72
72
  ['filter', value => processFilter(asFilterInput(value))],
73
- ['transformOrigin', value => processTransformOrigin(asTransformOriginInput(value))],
73
+ [
74
+ 'transformOrigin',
75
+ value => processTransformOrigin(asTransformOriginInput(value)),
76
+ ],
74
77
  ['transform', processTransformValue],
75
78
  ['aspectRatio', value => processAspectRatio(asAspectRatioInput(value))],
76
79
  ['fontVariant', value => processFontVariant(asFontVariantInput(value))],
77
- ['experimental_backgroundImage', value => processBackgroundImage(asBackgroundImageInput(value))],
80
+ [
81
+ 'experimental_backgroundImage',
82
+ value => processBackgroundImage(asBackgroundImageInput(value)),
83
+ ],
78
84
  ]);
79
85
  // boxShadow accepts a CSS string or an array of shadow objects; anything else is
80
86
  // undefined to processBoxShadow (which returns []). Narrowing avoids an `as` cast.
@@ -14,7 +14,7 @@
14
14
  // alongside Alert/Share, not in a view/render-*.ts file (whose contract is zero state / zero
15
15
  // native bridge).
16
16
  import { dlog } from './debug.js';
17
- import { resolveImageSource } from './image-source-resolver.js';
17
+ import { resolveImageSource, } from './image-source-resolver.js';
18
18
  import { getNativeModule } from './native-modules/index.js';
19
19
  import { Platform } from './platform';
20
20
  import { isNumber } from './type-guards.js';
@@ -26,7 +26,8 @@ const IMAGE_LOADER_MODULE = 'ImageLoader';
26
26
  let imageLoaderModule;
27
27
  function getImageLoader() {
28
28
  if (imageLoaderModule === undefined) {
29
- imageLoaderModule = getNativeModule(IMAGE_LOADER_MODULE);
29
+ imageLoaderModule =
30
+ getNativeModule(IMAGE_LOADER_MODULE);
30
31
  dlog(`Image: ImageLoader module ${imageLoaderModule ? 'resolved' : 'NOT resolved (null)'}`);
31
32
  }
32
33
  return imageLoaderModule;
package/build/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- export { createElement, createRawText, createAnchor, isAnchor, appendChild, insertBefore, removeChild, setProp, setEventListener, routeProp, getExplicitStyle, setText, isSymbioteNode, isSymbioteEvent, RAW_TEXT_COMPONENT, debugNodeId, } from './node';
1
+ export { createElement, createRawText, createAnchor, isAnchor, appendChild, insertBefore, removeChild, setProp, setEventListener, routeProp, censusRetainedTree, getExplicitStyle, setNodeHidden, setText, isSymbioteNode, isSymbioteEvent, RAW_TEXT_COMPONENT, debugNodeId, } from './node';
2
2
  export { isEventFor } from './view-config';
3
3
  export { registerComponent, setNativeViewConfigSource } from './registry';
4
4
  export { isRecord } from './type-guards';
5
- export { InteractionManager, Events as InteractionManagerEvents } from './interaction-manager';
5
+ export { InteractionManager, Events as InteractionManagerEvents, } from './interaction-manager';
6
6
  export type { IInteractionEvent, ISimpleTask, IPromiseTask, ITask, IHandle, ICancellable, } from './interaction-manager';
7
7
  export { default as PanResponder } from './pan-responder';
8
8
  export type { IPanResponderGestureState, IPanResponderCallbacks, IGestureResponderHandlers, IPanResponderInstance, } from './pan-responder';
@@ -10,11 +10,13 @@ export type { IComponentRegistration, INativeEventBinding, INativeViewConfig, IN
10
10
  export type { ISymbioteNode, ISymbioteEvent, IListener } from './node';
11
11
  export { SymbioteSurface, createSurface } from './surface';
12
12
  export { setEventDispatcher } from './dispatch';
13
- export { setColorProcessor, processColor, dispatchViewCommand, sendAccessibilityEvent, setNativeProps, getNativeTag, getNativeNode, whenCommitted, measure, measureInWindow, measureLayout, disposeRoot, } from './commit';
13
+ export { setColorProcessor, processColor, dispatchViewCommand, sendAccessibilityEvent, setNativeProps, getNativeTag, getNativeNode, whenCommitted, measure, measureInWindow, measureLayout, disposeRoot, readCommitProfile, } from './commit';
14
+ export type { ICommitProfile } from './commit';
15
+ export { registerPostCommit, unregisterPostCommit } from './post-commit';
14
16
  export { toPublicInstance } from './host-instance';
15
17
  export type { IHostInstance } from './host-instance';
16
- export { PlatformColor, DynamicColorIOS, isOpaqueColorValue } from './platform-color';
17
- export type { IColorValue, IOpaqueColorValue, IDynamicColorIOSTuple } from './platform-color';
18
+ export { PlatformColor, DynamicColorIOS, isOpaqueColorValue, } from './platform-color';
19
+ export type { IColorValue, IOpaqueColorValue, IDynamicColorIOSTuple, } from './platform-color';
18
20
  export { processBoxShadow } from './process-box-shadow';
19
21
  export type { IParsedBoxShadow } from './process-box-shadow';
20
22
  export { processFilter } from './process-filter';
@@ -28,19 +30,19 @@ export type { IParsedBackgroundImage } from './process-background-image';
28
30
  export { flattenStyle } from './style';
29
31
  export type { IViewStyle, ITextStyle, IStyleProp, IDimensionValue, IFlexAlign, IFlexJustify, ITransformProp, IBoxShadowValue, IDropShadowValue, IFilterFunction, IColorStopValue, ILinearGradientValue, IRadialGradientValue, IRadialGradientPosition, IRadialGradientShape, IRadialGradientSize, IBackgroundImageValue, IBlendMode, } from './styles';
30
32
  export { StyleSheet, computeHairlineWidth } from './style-sheet';
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';
33
+ export { registerRules, resolveClassName, clearGlobalStyles, isClassNameValue, } from './style-registry';
34
+ export type { IClassNameValue, IStyleRule } from './style-registry';
35
+ export { renameClassTokens } from './style-registry/scope';
36
+ export type { IClassNameRenames, IClassToggleMap, IScopableClassValue, } from './style-registry/scope';
35
37
  export { Platform } from './platform';
36
- export type { IPlatformStatic, IPlatformOSType, IPlatformSelectSpec } from './platform';
38
+ export type { IPlatformStatic, IPlatformOSType, IPlatformSelectSpec, } from './platform';
37
39
  export type { IPlatformConstantsIOS } from './platform/index.ios';
38
40
  export type { IPlatformConstantsAndroid } from './platform/index.android';
39
41
  export { dlog, isDebug } from './debug';
40
42
  export { reportUncaughtError } from './report-error';
41
43
  export type { IUncaughtErrorInfo } from './report-error';
42
44
  export { getNativeModule, getEnforcingNativeModule } from './native-modules';
43
- export { installDeviceEventHub, NativeEventEmitter, setDeviceEventSource } from './native-events';
45
+ export { installDeviceEventHub, NativeEventEmitter, setDeviceEventSource, } from './native-events';
44
46
  export type { IEventSubscription, IEventEmitterModule, INativeEventListener, IDeviceEventSource, } from './native-events';
45
47
  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, createAnimatedLeafLifecycle, AnimatedStyle, AnimatedTransform, AnimatedMock, reduceProps, isAnimatedNode, readPassthroughStyle, resolveHostNode, } from './animated';
46
48
  export type { IAnimatedLeafLifecycle, IValueXY, IRgbaValue, IColorInput, IEventConfig, IEventListener, IAnimatedEventHandler, INativeEventAttachment, IValueListener, IEasingFunction, IInterpolationConfig, IExtrapolateType, IAnimation, IEndCallback, IEndResult, ICompositeAnimation, ITimingConfig, ISpringConfig, IDecayConfig, IParallelConfig, ILoopAnimationConfig, INativeNodeConfig, INativeAnimationConfig, INativeEventMapping, IPlatformConfig, } from './animated';
@@ -51,8 +53,8 @@ export type { IAlertType, IAlertButtonStyle, IAlertButton, IAlertButtons, IAlert
51
53
  export { Share } from './share';
52
54
  export type { IShareContent, IShareOptions, IShareAction } from './share';
53
55
  export { imageStatics } from './image-loader';
54
- export type { IImageStatics, IImageSize, IImageCacheStatus } from './image-loader';
55
- export { setImageSourceResolver, resolveImageSource } from './image-source-resolver';
56
+ export type { IImageStatics, IImageSize, IImageCacheStatus, } from './image-loader';
57
+ export { setImageSourceResolver, resolveImageSource, } from './image-source-resolver';
56
58
  export type { IImageSource, IImageSourceProp } from './image-source-resolver';
57
59
  export { ActionSheetIOS } from './action-sheet-ios';
58
60
  export type { IActionSheetIOSOptions, IShareActionSheetIOSOptions, IShareActionSheetError, } from './action-sheet-ios';
@@ -74,16 +76,16 @@ export type { IAppStateStatus, IAppStateEvent } from './app-state';
74
76
  export { createAppRegistry } from './app-registry';
75
77
  export type { IAppRegistry, ICreateAppRegistryResult, IAppParameters, IRunnable, IHostRegistrar, IRegistry, IHeadlessTask, ITaskProvider, ITaskCanceller, ITaskCancelProvider, } from './app-registry';
76
78
  export { Keyboard, KEYBOARD_EVENT } from './keyboard';
77
- export type { IKeyboardEventName, IKeyboardEvent, IKeyboardMetrics } from './keyboard';
79
+ export type { IKeyboardEventName, IKeyboardEvent, IKeyboardMetrics, } from './keyboard';
78
80
  export { currentlyFocusedInput, setInputFocused, setInputBlurred, blurTextInput, } from './text-input-state';
79
81
  export { LayoutAnimation } from './layout-animation';
80
82
  export type { ILayoutAnimationType, ILayoutAnimationProperty, ILayoutAnimationConfig, ILayoutAnimationAnim, ILayoutAnimationTypes, ILayoutAnimationProperties, } from './layout-animation';
81
83
  export { BackHandler } from './back-handler';
82
84
  export type { IBackPressEventName, IBackPressHandler } from './back-handler';
83
- export { PermissionsAndroid, PERMISSIONS, RESULTS } from './permissions-android';
84
- export type { IPermission, IPermissionStatus, IRationale } from './permissions-android';
85
+ export { PermissionsAndroid, PERMISSIONS, RESULTS, } from './permissions-android';
86
+ export type { IPermission, IPermissionStatus, IRationale, } from './permissions-android';
85
87
  export { AccessibilityInfo } from './accessibility-info';
86
88
  export type { IAccessibilityChangeEvent, IAccessibilityChangeEventName, IAccessibilityChangeEventHandler, IAccessibilityAnnouncementFinishedEvent, IAnnounceForAccessibilityOptions, IAccessibilityInfoStatic, IAccessibilityEventType, IAccessibilityHandle, } from './accessibility-info/shared';
87
- export { applyStatusBarProps, statusBarImperative, statusBarCurrentHeight } from './status-bar';
89
+ export { applyStatusBarProps, statusBarImperative, statusBarCurrentHeight, } from './status-bar';
88
90
  export { hideTransition, STATUS_BAR_MANAGER, ANIMATED_HIDE_TRANSITION, STATIC_HIDE_TRANSITION, } from './status-bar/shared';
89
91
  export type { IStatusBarProps, IStatusBarStyle, IStatusBarAnimation, IStatusBarImperative, } from './status-bar/shared';
package/build/index.js CHANGED
@@ -2,7 +2,7 @@
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, isSymbioteEvent, RAW_TEXT_COMPONENT, debugNodeId, } from './node.js';
5
+ export { createElement, createRawText, createAnchor, isAnchor, appendChild, insertBefore, removeChild, setProp, setEventListener, routeProp, censusRetainedTree, getExplicitStyle, setNodeHidden, 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
8
  // Real cross-package consumer: core/components' KeyboardAvoidingView render narrows
@@ -10,18 +10,25 @@ export { registerComponent, setNativeViewConfigSource } from './registry.js';
10
10
  export { isRecord } from './type-guards.js';
11
11
  // InteractionManager: pure JS (timers + emitter), framework-agnostic, so it lives
12
12
  // here; every adapter re-exports it.
13
- export { InteractionManager, Events as InteractionManagerEvents } from './interaction-manager/index.js';
13
+ export { InteractionManager, Events as InteractionManagerEvents, } from './interaction-manager/index.js';
14
14
  // PanResponder: pure JS gesture recognition over the View responder props,
15
15
  // framework-agnostic, so it lives here; every adapter re-exports it.
16
16
  export { default as PanResponder } from './pan-responder/index.js';
17
17
  export { SymbioteSurface, createSurface } from './surface.js';
18
18
  export { setEventDispatcher } from './dispatch.js';
19
- export { setColorProcessor, processColor, dispatchViewCommand, sendAccessibilityEvent, setNativeProps, getNativeTag, getNativeNode, whenCommitted, measure, measureInWindow, measureLayout, disposeRoot, } from './commit.js';
19
+ export { setColorProcessor, processColor, dispatchViewCommand, sendAccessibilityEvent, setNativeProps, getNativeTag, getNativeNode, whenCommitted, measure, measureInWindow, measureLayout, disposeRoot, readCommitProfile, } from './commit.js';
20
+ // "A commit just reached completeRoot." The one seam that means the same thing under every
21
+ // adapter: React commits synchronously inside its own commit phase, while Vue / Svelte / Angular
22
+ // schedule completeRoot on a microtask, so each framework's own after-render hook fires at a
23
+ // DIFFERENT point relative to the native commit. Anything timing or comparing the commit path
24
+ // across adapters has to hang off this, not off a per-framework lifecycle hook, or it measures a
25
+ // different quantity in each one under the same name.
26
+ export { registerPostCommit, unregisterPostCommit } from './post-commit.js';
20
27
  // The public instance grafted onto a host node by every adapter (React's getPublicInstance,
21
28
  // the Vue renderer's createElement): the imperative measure/setNativeProps/focus API. Lives
22
29
  // here because it depends only on engine internals, so all adapters inherit it identically.
23
30
  export { toPublicInstance } from './host-instance/index.js';
24
- export { PlatformColor, DynamicColorIOS, isOpaqueColorValue } from './platform-color/index.js';
31
+ export { PlatformColor, DynamicColorIOS, isOpaqueColorValue, } from './platform-color/index.js';
25
32
  // CSS-style processors (boxShadow/filter): RN parses these in JS before native because
26
33
  // enableNativeCSSParsing() defaults to false. Exported so an adapter / test can reuse them.
27
34
  export { processBoxShadow } from './process-box-shadow/index.js';
@@ -33,13 +40,13 @@ export { processFontVariant } from './process-font-variant.js';
33
40
  export { processBackgroundImage } from './process-background-image/index.js';
34
41
  export { flattenStyle } from './style/index.js';
35
42
  export { StyleSheet, computeHairlineWidth } from './style-sheet/index.js';
36
- export { registerStyles, resolveClassName, clearGlobalStyles, isClassNameValue, } from './style-registry/index.js';
37
- export { scopeClassName } from './style-registry/scope.js';
43
+ export { registerRules, resolveClassName, clearGlobalStyles, isClassNameValue, } from './style-registry/index.js';
44
+ export { renameClassTokens } from './style-registry/scope.js';
38
45
  export { Platform } from './platform';
39
46
  export { dlog, isDebug } from './debug.js';
40
47
  export { reportUncaughtError } from './report-error.js';
41
48
  export { getNativeModule, getEnforcingNativeModule } from './native-modules/index.js';
42
- export { installDeviceEventHub, NativeEventEmitter, setDeviceEventSource } from './native-events.js';
49
+ export { installDeviceEventHub, NativeEventEmitter, setDeviceEventSource, } from './native-events.js';
43
50
  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, createAnimatedLeafLifecycle, AnimatedStyle, AnimatedTransform, AnimatedMock, reduceProps, isAnimatedNode, readPassthroughStyle, resolveHostNode, } from './animated/index.js';
44
51
  export { getSlot } from './fabric.js';
45
52
  // Imperative runtime modules: framework-agnostic native-bridge consumers (no visual, no
@@ -52,7 +59,7 @@ export { Share } from './share';
52
59
  // no view of its own, same shape as Alert/Share. The source-resolution seam it shares with
53
60
  // @symbiote-native/components' renderImage lives alongside it in image-source-resolver.
54
61
  export { imageStatics } from './image-loader.js';
55
- export { setImageSourceResolver, resolveImageSource } from './image-source-resolver.js';
62
+ export { setImageSourceResolver, resolveImageSource, } from './image-source-resolver.js';
56
63
  export { ActionSheetIOS } from './action-sheet-ios/index.js';
57
64
  export { Linking } from './linking';
58
65
  export { Vibration } from './vibration';
@@ -74,9 +81,9 @@ export { Keyboard, KEYBOARD_EVENT } from './keyboard/index.js';
74
81
  export { currentlyFocusedInput, setInputFocused, setInputBlurred, blurTextInput, } from './text-input-state.js';
75
82
  export { LayoutAnimation } from './layout-animation/index.js';
76
83
  export { BackHandler } from './back-handler/index.js';
77
- export { PermissionsAndroid, PERMISSIONS, RESULTS } from './permissions-android/index.js';
84
+ export { PermissionsAndroid, PERMISSIONS, RESULTS, } from './permissions-android/index.js';
78
85
  export { AccessibilityInfo } from './accessibility-info';
79
86
  // StatusBar: values from the platform-selected './status-bar', types from '-shared' (the
80
87
  // .ios re-export would otherwise duplicate-export the type symbols).
81
- export { applyStatusBarProps, statusBarImperative, statusBarCurrentHeight } from './status-bar';
88
+ export { applyStatusBarProps, statusBarImperative, statusBarCurrentHeight, } from './status-bar';
82
89
  export { hideTransition, STATUS_BAR_MANAGER, ANIMATED_HIDE_TRANSITION, STATIC_HIDE_TRANSITION, } from './status-bar/shared.js';
@@ -202,7 +202,8 @@ class InteractionManagerImpl {
202
202
  break;
203
203
  if (typeof next === 'function')
204
204
  next();
205
- if (this.deadline > DEFAULT_DEADLINE && Date.now() - startTime >= this.deadline) {
205
+ if (this.deadline > DEFAULT_DEADLINE &&
206
+ Date.now() - startTime >= this.deadline) {
206
207
  if (this.taskQueue.length > 0) {
207
208
  scheduleTimeout(() => {
208
209
  this.flushQueue();
@@ -42,7 +42,7 @@ const ANIMATION_PROPERTY = {
42
42
  // (`FabricUIManager?.configureNextLayoutAnimation` in LayoutAnimation.js) before
43
43
  // calling it, no `as` cast needed.
44
44
  function hasConfigureNextLayoutAnimation(value) {
45
- return isRecord(value) && typeof value.configureNextLayoutAnimation === 'function';
45
+ return (isRecord(value) && typeof value.configureNextLayoutAnimation === 'function');
46
46
  }
47
47
  // Resolved FRESH on every configureNext, deliberately NOT memoized. Either mechanism can
48
48
  // come and go at runtime (the Fabric global installs during bootstrap; a TurboModule can be
@@ -93,9 +93,15 @@ const Presets = {
93
93
  linear: createLayoutAnimation(PRESET_DURATION.linear, ANIMATION_TYPE.linear, ANIMATION_PROPERTY.opacity),
94
94
  spring: {
95
95
  duration: PRESET_DURATION.spring,
96
- create: { type: ANIMATION_TYPE.linear, property: ANIMATION_PROPERTY.opacity },
96
+ create: {
97
+ type: ANIMATION_TYPE.linear,
98
+ property: ANIMATION_PROPERTY.opacity,
99
+ },
97
100
  update: { type: ANIMATION_TYPE.spring, springDamping: SPRING_DAMPING },
98
- delete: { type: ANIMATION_TYPE.linear, property: ANIMATION_PROPERTY.opacity },
101
+ delete: {
102
+ type: ANIMATION_TYPE.linear,
103
+ property: ANIMATION_PROPERTY.opacity,
104
+ },
99
105
  },
100
106
  };
101
107
  // ---- enabled gate --------------------------------------------------------
@@ -154,7 +160,9 @@ function configureNext(config, onAnimationDidEnd, onAnimationDidFail) {
154
160
  class LayoutAnimationImpl {
155
161
  // Frozen so callers can't mutate the shared type/property tables.
156
162
  Types = Object.freeze({ ...ANIMATION_TYPE });
157
- Properties = Object.freeze({ ...ANIMATION_PROPERTY });
163
+ Properties = Object.freeze({
164
+ ...ANIMATION_PROPERTY,
165
+ });
158
166
  Presets = Presets;
159
167
  // Methods (not class fields) so they stay overridable under
160
168
  // useDefineForClassFields.
@@ -70,7 +70,8 @@ export function setDeviceEventSource(source) {
70
70
  // would throw "undefined is not a function" — the `?.` guards the module, not a
71
71
  // missing method. Mirrors RN's NativeEventEmitter constructor probe.
72
72
  function hasObserveCounters(module) {
73
- return typeof module.addListener === 'function' && typeof module.removeListeners === 'function';
73
+ return (typeof module.addListener === 'function' &&
74
+ typeof module.removeListeners === 'function');
74
75
  }
75
76
  // Subscribe to events for one native module. Mirrors RN's NativeEventEmitter: each
76
77
  // `addListener` also pings the module's `addListener` counter, and removal pings
@@ -110,7 +110,9 @@ export function createDeviceEventModule(config) {
110
110
  installDeviceEventHub();
111
111
  const resolved = getModule();
112
112
  const bindModule = config.bindModuleToEmitter ?? true;
113
- const boundModule = bindModule && resolved !== null && hasEventEmitterShape(resolved) ? resolved : undefined;
113
+ const boundModule = bindModule && resolved !== null && hasEventEmitterShape(resolved)
114
+ ? resolved
115
+ : undefined;
114
116
  emitter = new NativeEventEmitter(boundModule);
115
117
  config.onEmitterCreated?.(emitter, resolved);
116
118
  }