@symbiote-native/engine 0.1.7 → 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 (87) hide show
  1. package/README.md +17 -10
  2. package/build/action-sheet-ios/index.js +4 -1
  3. package/build/alert/index.ios.js +10 -0
  4. package/build/animated/animations/composition.js +3 -1
  5. package/build/animated/animations/spring-config.js +4 -1
  6. package/build/animated/animations/spring.js +23 -9
  7. package/build/animated/animations/timing.js +3 -2
  8. package/build/animated/bezier.js +2 -1
  9. package/build/animated/color.d.ts +2 -8
  10. package/build/animated/color.js +8 -47
  11. package/build/animated/easing.js +0 -8
  12. package/build/animated/event.js +7 -3
  13. package/build/animated/graph.d.ts +13 -5
  14. package/build/animated/graph.js +92 -13
  15. package/build/animated/index.d.ts +3 -3
  16. package/build/animated/index.js +3 -3
  17. package/build/animated/interpolation.js +8 -3
  18. package/build/animated/leaf-lifecycle.d.ts +7 -0
  19. package/build/animated/leaf-lifecycle.js +185 -0
  20. package/build/animated/mock.js +1 -1
  21. package/build/animated/native/native-animated.js +2 -1
  22. package/build/animated/operators.js +27 -6
  23. package/build/animated/props.js +1 -1
  24. package/build/animated/rgba.d.ts +8 -0
  25. package/build/animated/rgba.js +70 -0
  26. package/build/animated/style.js +13 -3
  27. package/build/animated/value-xy.d.ts +8 -5
  28. package/build/animated/value-xy.js +4 -1
  29. package/build/animated/value.js +12 -7
  30. package/build/appearance/index.js +4 -1
  31. package/build/commit.d.ts +13 -0
  32. package/build/commit.js +146 -14
  33. package/build/debug.d.ts +1 -1
  34. package/build/debug.js +10 -3
  35. package/build/events/index.js +19 -10
  36. package/build/fabric-props.js +8 -2
  37. package/build/image-loader.js +3 -2
  38. package/build/index.d.ts +23 -19
  39. package/build/index.js +19 -11
  40. package/build/interaction-manager/index.js +2 -1
  41. package/build/layout-animation/index.js +12 -5
  42. package/build/native-events.js +2 -1
  43. package/build/native-modules/index.js +3 -1
  44. package/build/node.d.ts +25 -0
  45. package/build/node.js +168 -2
  46. package/build/pan-responder/index.js +20 -9
  47. package/build/permissions-android/index.js +3 -1
  48. package/build/platform/index.android.d.ts +1 -1
  49. package/build/platform/index.android.js +1 -1
  50. package/build/platform/index.ios.d.ts +1 -1
  51. package/build/platform-color/index.js +3 -1
  52. package/build/post-commit.d.ts +1 -0
  53. package/build/post-commit.js +6 -0
  54. package/build/process-background-image/index.js +30 -9
  55. package/build/process-filter.js +11 -4
  56. package/build/process-font-variant.js +0 -1
  57. package/build/process-transform/index.js +13 -19
  58. package/build/registry.js +10 -2
  59. package/build/report-error.d.ts +5 -0
  60. package/build/report-error.js +63 -0
  61. package/build/share/index.android.js +1 -1
  62. package/build/share/index.ios.js +1 -1
  63. package/build/status-bar/index.android.js +8 -10
  64. package/build/status-bar/index.ios.js +1 -1
  65. package/build/style-registry/index.d.ts +9 -2
  66. package/build/style-registry/index.js +149 -95
  67. package/build/style-registry/scope.d.ts +15 -1
  68. package/build/style-registry/scope.js +29 -27
  69. package/build/style-sheet/index.js +4 -2
  70. package/build/styles.d.ts +11 -1
  71. package/build/surface.d.ts +1 -1
  72. package/build/surface.js +7 -1
  73. package/build/tags.d.ts +1 -0
  74. package/build/tags.js +31 -1
  75. package/build/touch-history.js +9 -2
  76. package/build/vibration/index.android.js +2 -3
  77. package/build/vibration/index.ios.js +1 -1
  78. package/build/view-config.js +14 -2
  79. package/package.json +2 -2
  80. package/build/animated/interpolation-node.d.ts +0 -15
  81. package/build/animated/interpolation-node.js +0 -58
  82. package/build/style-registry/scope/index.d.ts +0 -3
  83. package/build/style-registry/scope/index.js +0 -41
  84. package/build/touch-history/index.d.ts +0 -23
  85. package/build/touch-history/index.js +0 -150
  86. package/build/type-guards/index.d.ts +0 -4
  87. package/build/type-guards/index.js +0 -15
@@ -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 --------------------------------------------------------
@@ -148,14 +154,15 @@ function configureNext(config, onAnimationDidEnd, onAnimationDidFail) {
148
154
  onAnimationDidEnd?.();
149
155
  };
150
156
  dlog(`LayoutAnimation.configureNext: dispatching config (duration=${config.duration})`);
151
- // Default to a no-op when the caller doesn't supply one.
152
157
  manager.configureNextLayoutAnimation(config, onComplete, onAnimationDidFail ?? (() => { }));
153
158
  }
154
159
  // ---- the LayoutAnimation facade ------------------------------------------
155
160
  class LayoutAnimationImpl {
156
161
  // Frozen so callers can't mutate the shared type/property tables.
157
162
  Types = Object.freeze({ ...ANIMATION_TYPE });
158
- Properties = Object.freeze({ ...ANIMATION_PROPERTY });
163
+ Properties = Object.freeze({
164
+ ...ANIMATION_PROPERTY,
165
+ });
159
166
  Presets = Presets;
160
167
  // Methods (not class fields) so they stay overridable under
161
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
  }
package/build/node.d.ts CHANGED
@@ -19,6 +19,7 @@ export interface ISymbioteNode {
19
19
  listeners: Map<string, IListener> | undefined;
20
20
  children: ISymbioteNode[];
21
21
  parent: ISymbioteNode | undefined;
22
+ dirty: boolean;
22
23
  }
23
24
  export declare function createElement(component: string, isText?: boolean): ISymbioteNode;
24
25
  export declare function createRawText(text: string): ISymbioteNode;
@@ -27,12 +28,36 @@ export declare function debugNodeId(node: ISymbioteNode): number;
27
28
  export declare const ANCHOR_COMPONENT = "#anchor";
28
29
  export declare function createAnchor(): ISymbioteNode;
29
30
  export declare function isAnchor(node: ISymbioteNode): boolean;
31
+ export declare function isEmptyRawText(node: ISymbioteNode): boolean;
32
+ export declare function markDirty(node: ISymbioteNode): void;
33
+ export declare function takePropStats(): {
34
+ writes: number;
35
+ noops: number;
36
+ };
30
37
  export declare function setProp(node: ISymbioteNode, key: string, value: unknown): void;
31
38
  export declare function setEventListener(node: ISymbioteNode, name: string, value: unknown): void;
39
+ /**
40
+ * Stop a node painting without unmounting it, or let it paint again.
41
+ *
42
+ * The seam React's `Activity`/`Suspense` reach for through `hideInstance`/`unhideInstance`. It
43
+ * lives in the engine rather than an adapter because the reversibility problem — restoring the
44
+ * author's style byte for byte — belongs to whoever owns the style merge, and that is here.
45
+ */
46
+ export declare function setNodeHidden(node: ISymbioteNode, hidden: boolean): void;
32
47
  export declare function getExplicitStyle(node: ISymbioteNode): unknown;
33
48
  export declare function routeProp(node: ISymbioteNode, key: string, value: unknown): void;
34
49
  export declare function setText(node: ISymbioteNode, text: string): void;
35
50
  export declare function appendChild(parent: ISymbioteNode, child: ISymbioteNode): void;
36
51
  export declare function insertBefore(parent: ISymbioteNode, child: ISymbioteNode, beforeChild: ISymbioteNode): void;
37
52
  export declare function removeChild(parent: ISymbioteNode, child: ISymbioteNode): void;
53
+ export interface ITreeCensus {
54
+ nodes: number;
55
+ anchors: number;
56
+ emptyRawTexts: number;
57
+ /** Nodes the commit walk actually reconciles: `nodes` minus everything it skips. */
58
+ renderable: number;
59
+ /** children.length of every parent holding at least one skipped child, widest first. */
60
+ flattenWidths: number[];
61
+ }
62
+ export declare function censusRetainedTree(roots: readonly ISymbioteNode[]): ITreeCensus;
38
63
  export {};
package/build/node.js CHANGED
@@ -34,6 +34,7 @@ export function createElement(component, isText = false) {
34
34
  listeners: undefined,
35
35
  children: [],
36
36
  parent: undefined,
37
+ dirty: true,
37
38
  };
38
39
  }
39
40
  export function createRawText(text) {
@@ -45,6 +46,7 @@ export function createRawText(text) {
45
46
  listeners: undefined,
46
47
  children: [],
47
48
  parent: undefined,
49
+ dirty: true,
48
50
  };
49
51
  }
50
52
  // `instanceHandle` round-trips through Fabric unchanged: the object we pass to
@@ -80,16 +82,97 @@ export function createAnchor() {
80
82
  export function isAnchor(node) {
81
83
  return node.component === ANCHOR_COMPONENT;
82
84
  }
85
+ // A raw text with no characters must not reach Fabric. Its fragment is dropped by
86
+ // AttributedString::appendFragment, but the text walk has already flagged "the last child was raw
87
+ // text", so the NEXT raw sibling merges into `fragments.back()` of an empty vector and the process
88
+ // aborts. The commit walk skips such a node exactly as it skips an anchor (commit.ts,
89
+ // renderableChildren); an empty string paints nothing either way, so nothing is lost. `''` only —
90
+ // a whitespace-only string is real content inside a <Text>.
91
+ export function isEmptyRawText(node) {
92
+ return node.component === RAW_TEXT_COMPONENT && node.props.text === '';
93
+ }
94
+ // Dirty-marking: lets reconcile return an untouched subtree by reference instead of rebuilding
95
+ // every node's Fabric props and deep-comparing them against the mirror. That walk costs ~13 us per
96
+ // node on device (Hermes, iOS Debug, examples/react benchmark screen), so without the flag a
97
+ // 1200-node tree burns a whole 16.6 ms frame no matter how small the change - the cost tracks TREE
98
+ // SIZE, not change size.
99
+ //
100
+ // Marking walks up to the first ALREADY-dirty ancestor and stops, so a burst of mutations under one
101
+ // subtree pays for one chain walk rather than one per mutation. Reconcile clears every node it
102
+ // visits, which keeps the invariant "an ancestor of a dirty node is dirty" across commits.
103
+ //
104
+ // Listener changes deliberately do NOT mark. `node.listeners` never reaches Fabric (event dispatch
105
+ // reads it straight off the retained node) and React hands us a fresh handler closure on nearly
106
+ // every render, so marking there would re-dirty the whole tree every commit and hand the win back.
107
+ // The one listener that DOES change a Fabric prop, `layout`, raises `onLayout` through setProp
108
+ // below and is marked that way.
109
+ export function markDirty(node) {
110
+ let current = node;
111
+ while (current !== undefined && !current.dirty) {
112
+ current.dirty = true;
113
+ current = current.parent;
114
+ }
115
+ }
116
+ // How many prop writes actually landed, and how many the no-op guard below turned away.
117
+ // Read-and-zeroed through readCommitProfile() (commit.ts), which folds them into the same window
118
+ // as the walk numbers so one read prices both halves: `propNoops` is the waste an adapter is
119
+ // generating above the engine, `nodesVisited` is what that waste costs below it.
120
+ //
121
+ // Not gated behind isDebug(), for the same reason the commit profile is not: an integer increment
122
+ // is noise next to the prop write it counts, and the figure is only meaningful from a release
123
+ // build. A per-call dlog was the obvious alternative and is deliberately NOT here - the Angular
124
+ // screen that motivated the guard emitted 90 000 no-op writes on one press, and a log line each
125
+ // would measure the logging rather than the code (see the `perf-claims-need-numbers` rule).
126
+ const propStats = { writes: 0, noops: 0 };
127
+ export function takePropStats() {
128
+ const snapshot = { writes: propStats.writes, noops: propStats.noops };
129
+ propStats.writes = 0;
130
+ propStats.noops = 0;
131
+ return snapshot;
132
+ }
83
133
  // A pure prop set: no event inference. `onTintColor` is a Switch prop and reaches
84
134
  // Fabric like any other; the event-vs-prop decision is made by routeProp, never by
85
135
  // the key's name.
136
+ //
137
+ // Writing a value the node already holds is a NO-OP and returns before markDirty. Fabric never saw
138
+ // a difference either way - reconcile rebuilds the node's Fabric props and `propsEqual` finds them
139
+ // identical, so no clone is emitted - but the mark itself is not free: it walks to the first
140
+ // already-dirty ancestor and strips every one of them of the commit walk's early exit, so an
141
+ // otherwise untouched subtree gets re-walked purely to prove it is untouched. The guard makes that
142
+ // whole bug class free for every adapter instead of each one having to remember to diff first
143
+ // (measured: Angular's Pressable host bag pushed 104 000 setProp calls for a screen Solid built in
144
+ // 12 000, 90 000 of them writing `undefined` over a key that was not there).
145
+ //
146
+ // Three deliberate choices:
147
+ //
148
+ // - `Object.hasOwn`, not `node.props[key] === undefined`. A key explicitly present holding
149
+ // `undefined` is not an absent key: `delete` genuinely changes the record's shape, and
150
+ // `setNativeProps` writes node.props directly and can leave exactly such a key behind. Fabric
151
+ // itself cannot tell the two apart (fabricProps skips undefined values), but node.props is also
152
+ // read outside the commit path, so the retained tree keeps the shape callers asked for.
153
+ // - `Object.is`, not a deep compare. A style object, an array, or a handler closure is a fresh
154
+ // reference on nearly every render, so the guard simply never fires for them - correct, since an
155
+ // adapter is free to hand back the SAME reference with mutated contents and identity cannot see
156
+ // that. A deep compare on every prop write would cost more than the walk it saves.
157
+ // - The in-place-mutation hazard that leaves is already instrumented: a node skipped as clean whose
158
+ // props have drifted is exactly what `warnIfStale` reports as DIRTY-MISS under DEBUG (commit.ts).
86
159
  export function setProp(node, key, value) {
87
160
  if (value === undefined) {
161
+ if (!Object.hasOwn(node.props, key)) {
162
+ propStats.noops += 1;
163
+ return;
164
+ }
88
165
  delete node.props[key];
89
166
  }
90
167
  else {
168
+ if (Object.is(node.props[key], value)) {
169
+ propStats.noops += 1;
170
+ return;
171
+ }
91
172
  node.props[key] = value;
92
173
  }
174
+ propStats.writes += 1;
175
+ markDirty(node);
93
176
  }
94
177
  // Fabric gates layout events behind a boolean prop (BaseViewProps.onLayout): unlike
95
178
  // scroll / touch / change, which the native component emits unconditionally, a
@@ -148,12 +231,41 @@ const RESPONDER_EVENTS = new Set([
148
231
  // dynamic" (the instance holds functions) and the surface paints black, while iOS silently
149
232
  // drops it. SFC/template authoring never produces them. Strip them here, once, so no
150
233
  // adapter leaks React JSX dev metadata to the host, mirroring React's host config.
151
- const REACT_JSX_DEV_PROPS = new Set(['__self', '__source']);
234
+ const REACT_JSX_DEV_PROPS = new Set([
235
+ '__self',
236
+ '__source',
237
+ ]);
152
238
  const classStyleParts = new WeakMap();
239
+ // The fresh array below means setProp's no-op guard never fires for `style`, and that is
240
+ // DELIBERATE - do not "finish the optimization" by skipping when both halves are unchanged.
241
+ // `classStyleParts` is a shadow copy of the declarative style, and setNativeProps bypasses it
242
+ // (it writes node.props.style directly, merging an Animated frame onto whatever is there). An
243
+ // app that hands over a hoisted style constant - StyleSheet.create, a module-level object - would
244
+ // then re-push an identity-equal half, get skipped, and never restore the declarative style the
245
+ // animation overwrote. The re-push IS the restore path.
153
246
  function commitClassStyle(node, patch) {
154
247
  const entry = { ...classStyleParts.get(node), ...patch };
155
248
  classStyleParts.set(node, entry);
156
- setProp(node, 'style', [entry.classStyle, entry.explicitStyle]);
249
+ // The third slot is APPENDED ONLY WHILE HIDDEN. Writing a permanent three-element array would
250
+ // change the style payload of every node in every app for a state almost none of them are ever
251
+ // in — and this project spent a day removing per-frame allocations, so a slot that is undefined
252
+ // 99.9% of the time does not get to ride along on every style write.
253
+ setProp(node, 'style', entry.hiddenStyle === undefined
254
+ ? [entry.classStyle, entry.explicitStyle]
255
+ : [entry.classStyle, entry.explicitStyle, entry.hiddenStyle]);
256
+ }
257
+ // `display: 'none'` is a real RN style value (Yoga's DisplayNone), so a hidden node keeps its
258
+ // place in the tree, its state and its children — it just stops laying out and painting.
259
+ const HIDDEN_STYLE = { display: 'none' };
260
+ /**
261
+ * Stop a node painting without unmounting it, or let it paint again.
262
+ *
263
+ * The seam React's `Activity`/`Suspense` reach for through `hideInstance`/`unhideInstance`. It
264
+ * lives in the engine rather than an adapter because the reversibility problem — restoring the
265
+ * author's style byte for byte — belongs to whoever owns the style merge, and that is here.
266
+ */
267
+ export function setNodeHidden(node, hidden) {
268
+ commitClassStyle(node, { hiddenStyle: hidden ? HIDDEN_STYLE : undefined });
157
269
  }
158
270
  // The explicit (non-class-derived) style half, for an adapter that builds its style prop up
159
271
  // key-by-key (Angular's Ivy ɵɵstyleProp/setStyle) instead of handing over one whole object —
@@ -199,9 +311,29 @@ export function routeProp(node, key, value) {
199
311
  }
200
312
  setProp(node, key, value);
201
313
  }
314
+ // The same no-op guard as setProp, and here it is strictly stronger: `text` is a string, so
315
+ // `Object.is` is a real value comparison rather than the reference check it degrades to for a style
316
+ // object or a handler. A framework that re-renders a subtree and hands back an unchanged label -
317
+ // every list row whose text did not move, on every update - stops stripping its ancestors of the
318
+ // commit walk's early exit. Counted in the same propStats, because a text write IS a prop write:
319
+ // it lands in node.props.text and reaches Fabric as RCTRawText's only prop.
320
+ //
321
+ // Safe against the one ordering hazard worth naming: a raw-text node REPARENTED under a <Text>
322
+ // commits as RCTVirtualText instead of RCTText (viewNameFor, commit.ts). That flip is not driven
323
+ // from here - a structural op marks the parent chain, and reconcile re-checks `committed.parent` on
324
+ // its early-exit path - so a same-text write not marking cannot hide it.
202
325
  export function setText(node, text) {
326
+ if (Object.is(node.props.text, text)) {
327
+ propStats.noops += 1;
328
+ return;
329
+ }
203
330
  node.props.text = text;
331
+ propStats.writes += 1;
332
+ markDirty(node);
204
333
  }
334
+ // Structural ops mark the PARENT chain (both the old and the new one), never the moved child:
335
+ // a child that only changed position may legitimately still be clean, and reconcile re-checks
336
+ // `committed.parent` on its early-exit path, so a reparent is caught there rather than by a flag.
205
337
  function detach(child) {
206
338
  const parent = child.parent;
207
339
  if (!parent)
@@ -210,21 +342,55 @@ function detach(child) {
210
342
  if (index >= 0)
211
343
  parent.children.splice(index, 1);
212
344
  child.parent = undefined;
345
+ markDirty(parent);
213
346
  }
214
347
  export function appendChild(parent, child) {
215
348
  detach(child);
216
349
  child.parent = parent;
217
350
  parent.children.push(child);
351
+ markDirty(parent);
218
352
  }
219
353
  export function insertBefore(parent, child, beforeChild) {
220
354
  detach(child);
221
355
  child.parent = parent;
222
356
  const index = parent.children.indexOf(beforeChild);
223
357
  parent.children.splice(index < 0 ? parent.children.length : index, 0, child);
358
+ markDirty(parent);
224
359
  }
225
360
  export function removeChild(parent, child) {
226
361
  const index = parent.children.indexOf(child);
227
362
  if (index >= 0)
228
363
  parent.children.splice(index, 1);
229
364
  child.parent = undefined;
365
+ markDirty(parent);
366
+ }
367
+ export function censusRetainedTree(roots) {
368
+ const census = {
369
+ nodes: 0,
370
+ anchors: 0,
371
+ emptyRawTexts: 0,
372
+ renderable: 0,
373
+ flattenWidths: [],
374
+ };
375
+ // Explicit stack, not recursion: a deep list under a benchmark screen would blow the JS stack
376
+ // on the very tree this is meant to measure.
377
+ const stack = [...roots];
378
+ while (stack.length > 0) {
379
+ const node = stack.pop();
380
+ if (node === undefined)
381
+ break;
382
+ census.nodes += 1;
383
+ if (isAnchor(node))
384
+ census.anchors += 1;
385
+ else if (isEmptyRawText(node))
386
+ census.emptyRawTexts += 1;
387
+ else
388
+ census.renderable += 1;
389
+ if (node.children.some(child => isAnchor(child) || isEmptyRawText(child)))
390
+ census.flattenWidths.push(node.children.length);
391
+ for (const child of node.children)
392
+ stack.push(child);
393
+ }
394
+ census.flattenWidths.sort((left, right) => right - left);
395
+ return census;
230
396
  }
@@ -17,7 +17,9 @@ const SINGLE_TOUCH_COUNT = 1;
17
17
  // onShouldBlockNativeResponder defaults to true (RN: block native by default).
18
18
  const DEFAULT_BLOCK_NATIVE_RESPONDER = true;
19
19
  function toFiniteNumber(value) {
20
- return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
20
+ return typeof value === 'number' && Number.isFinite(value)
21
+ ? value
22
+ : undefined;
21
23
  }
22
24
  // Pull one touch out of an untyped array element, skipping anything that does not
23
25
  // carry numeric pageX/pageY. `timestamp` falls back to 0 when absent so a touch
@@ -196,7 +198,7 @@ function initializeGestureState(gestureState) {
196
198
  // The timestamp this frame advances to: the touch-history clock when shared attached a
197
199
  // store, else the most recent of the live touches (headless direct-call path).
198
200
  function frameTimestampOf(event, touches) {
199
- return touchHistoryOf(event)?.mostRecentTimeStamp ?? mostRecentTimestamp(touches);
201
+ return (touchHistoryOf(event)?.mostRecentTimeStamp ?? mostRecentTimestamp(touches));
200
202
  }
201
203
  // Advance the gesture for a move frame. With a touch-history store (the device / shared
202
204
  // path) this defers to RN's accumulate-per-moved-touch math; without one it falls back to
@@ -275,7 +277,8 @@ const PanResponder = {
275
277
  // Skip a duplicate dispatch of the same frame: when two touches change at
276
278
  // once the responder system fires twice, but the geometry was already
277
279
  // folded in on the first call.
278
- if (gestureState._accountsForMovesUpTo === frameTimestampOf(event, touches)) {
280
+ if (gestureState._accountsForMovesUpTo ===
281
+ frameTimestampOf(event, touches)) {
279
282
  return false;
280
283
  }
281
284
  updateGestureStateOnMove(gestureState, event, touches);
@@ -288,14 +291,19 @@ const PanResponder = {
288
291
  const touches = readTouches(event);
289
292
  const touchHistory = touchHistoryOf(event);
290
293
  // x0/y0 is the non-cumulative centroid at grant time (RN: currentCentroid).
291
- gestureState.x0 = touchHistory ? currentCentroidXAll(touchHistory) : centroidX(touches);
292
- gestureState.y0 = touchHistory ? currentCentroidYAll(touchHistory) : centroidY(touches);
294
+ gestureState.x0 = touchHistory
295
+ ? currentCentroidXAll(touchHistory)
296
+ : centroidX(touches);
297
+ gestureState.y0 = touchHistory
298
+ ? currentCentroidYAll(touchHistory)
299
+ : centroidY(touches);
293
300
  gestureState.dx = 0;
294
301
  gestureState.dy = 0;
295
302
  // The grant frame is already accounted for, so the first move's velocity
296
303
  // is measured from here, not from time 0.
297
304
  gestureState._accountsForMovesUpTo = frameTimestampOf(event, touches);
298
- gestureState.numberActiveTouches = touchHistory?.numberActiveTouches ?? touches.length;
305
+ gestureState.numberActiveTouches =
306
+ touchHistory?.numberActiveTouches ?? touches.length;
299
307
  config.onPanResponderGrant?.(event, gestureState);
300
308
  return config.onShouldBlockNativeResponder === undefined
301
309
  ? DEFAULT_BLOCK_NATIVE_RESPONDER
@@ -306,13 +314,15 @@ const PanResponder = {
306
314
  },
307
315
  onResponderStart(event) {
308
316
  gestureState.numberActiveTouches =
309
- touchHistoryOf(event)?.numberActiveTouches ?? readTouches(event).length;
317
+ touchHistoryOf(event)?.numberActiveTouches ??
318
+ readTouches(event).length;
310
319
  config.onPanResponderStart?.(event, gestureState);
311
320
  },
312
321
  onResponderMove(event) {
313
322
  const touches = readTouches(event);
314
323
  // Same duplicate-frame guard as the capture path.
315
- if (gestureState._accountsForMovesUpTo === frameTimestampOf(event, touches)) {
324
+ if (gestureState._accountsForMovesUpTo ===
325
+ frameTimestampOf(event, touches)) {
316
326
  return;
317
327
  }
318
328
  updateGestureStateOnMove(gestureState, event, touches);
@@ -320,7 +330,8 @@ const PanResponder = {
320
330
  },
321
331
  onResponderEnd(event) {
322
332
  gestureState.numberActiveTouches =
323
- touchHistoryOf(event)?.numberActiveTouches ?? readTouches(event).length;
333
+ touchHistoryOf(event)?.numberActiveTouches ??
334
+ readTouches(event).length;
324
335
  config.onPanResponderEnd?.(event, gestureState);
325
336
  },
326
337
  onResponderRelease(event) {
@@ -84,7 +84,9 @@ export const PERMISSIONS = Object.freeze({
84
84
  // string is passed through as a IPermissionStatus only when it matches; otherwise
85
85
  // we fall back to DENIED: a runtime guard at the trust boundary, not an `as`.
86
86
  function toPermissionStatus(value) {
87
- if (value === RESULTS.GRANTED || value === RESULTS.DENIED || value === RESULTS.NEVER_ASK_AGAIN) {
87
+ if (value === RESULTS.GRANTED ||
88
+ value === RESULTS.DENIED ||
89
+ value === RESULTS.NEVER_ASK_AGAIN) {
88
90
  return value;
89
91
  }
90
92
  return RESULTS.DENIED;
@@ -1,5 +1,5 @@
1
1
  import { type IPlatformStatic } from './shared';
2
- export type { IPlatformOSType, IPlatformSelectSpec, IPlatformStatic } from './shared';
2
+ export type { IPlatformOSType, IPlatformSelectSpec, IPlatformStatic, } from './shared';
3
3
  export interface IPlatformConstantsAndroid {
4
4
  isTesting: boolean;
5
5
  isDisableAnimations?: boolean;
@@ -3,7 +3,7 @@
3
3
  // from the PlatformConstants native module (RN spec NativePlatformConstantsAndroid.js,
4
4
  // a different payload from iOS). Metro picks this on an Android host. The module name is
5
5
  // the same 'PlatformConstants'; only the shape differs.
6
- import { createConstantsResolver } from './shared.js';
6
+ import { createConstantsResolver, } from './shared.js';
7
7
  // The filename already selected this host: 'android' is a literal, not a probe.
8
8
  const OS_ANDROID = 'android';
9
9
  // RN reads isTV off the uiMode constant.
@@ -1,5 +1,5 @@
1
1
  import { type IPlatformStatic } from './shared';
2
- export type { IPlatformOSType, IPlatformSelectSpec, IPlatformStatic } from './shared';
2
+ export type { IPlatformOSType, IPlatformSelectSpec, IPlatformStatic, } from './shared';
3
3
  export interface IPlatformConstantsIOS {
4
4
  forceTouchAvailable: boolean;
5
5
  interfaceIdiom: string;
@@ -21,7 +21,9 @@ export function DynamicColorIOS(tuple) {
21
21
  // True for the opaque objects above: the color seam uses this to route them
22
22
  // through the platform processor alongside CSS-string colors.
23
23
  export function isOpaqueColorValue(value) {
24
- return typeof value === 'object' && value !== null && ('semantic' in value || 'dynamic' in value);
24
+ return (typeof value === 'object' &&
25
+ value !== null &&
26
+ ('semantic' in value || 'dynamic' in value));
25
27
  }
26
28
  // Color props must reach Fabric as platform ints, not CSS strings. Fabric's C++ color
27
29
  // parser silently drops strings. The actual conversion (processColor) is RN-platform-specific,
@@ -1,4 +1,5 @@
1
1
  type IPostCommitHook = () => void;
2
2
  export declare function registerPostCommit(hook: IPostCommitHook): void;
3
+ export declare function unregisterPostCommit(hook: IPostCommitHook): void;
3
4
  export declare function runPostCommitHooks(): void;
4
5
  export {};
@@ -10,6 +10,12 @@ const hooks = new Set();
10
10
  export function registerPostCommit(hook) {
11
11
  hooks.add(hook);
12
12
  }
13
+ // The counterpart, needed the moment a hook belongs to something with a lifetime (a screen, a
14
+ // component) rather than to the process. Without it a mounted-and-gone consumer keeps running
15
+ // after every commit forever, holding its whole closure alive.
16
+ export function unregisterPostCommit(hook) {
17
+ hooks.delete(hook);
18
+ }
13
19
  export function runPostCommitHooks() {
14
20
  for (const hook of hooks)
15
21
  hook();
@@ -15,16 +15,23 @@ const COLOR_STOP_PARTS_REGEX = /\S+\([^)]*\)|\S+/g;
15
15
  const WHITESPACE_NORMALIZE_REGEX = /\s+/g;
16
16
  const LINEAR_GRADIENT_DIRECTION_REGEX = /^to\s+(?:top|bottom|left|right)(?:\s+(?:top|bottom|left|right))?/i;
17
17
  const LINEAR_GRADIENT_ANGLE_UNIT_REGEX = /^([+-]?\d*\.?\d+)(deg|grad|rad|turn)$/i;
18
- const LINEAR_GRADIENT_DEFAULT_DIRECTION = { type: 'angle', value: 180 };
18
+ const LINEAR_GRADIENT_DEFAULT_DIRECTION = {
19
+ type: 'angle',
20
+ value: 180,
21
+ };
19
22
  const DEFAULT_RADIAL_SHAPE = 'ellipse';
20
23
  const DEFAULT_RADIAL_SIZE = 'farthest-corner';
21
- const DEFAULT_RADIAL_POSITION = { top: '50%', left: '50%' };
24
+ const DEFAULT_RADIAL_POSITION = {
25
+ top: '50%',
26
+ left: '50%',
27
+ };
22
28
  function isStringOrNumber(value) {
23
29
  return typeof value === 'string' || typeof value === 'number';
24
30
  }
25
31
  // A color-stop position is a plain px number or a percentage string; anything else is invalid.
26
32
  function isPositionValue(value) {
27
- return typeof value === 'number' || (typeof value === 'string' && value.endsWith('%'));
33
+ return (typeof value === 'number' ||
34
+ (typeof value === 'string' && value.endsWith('%')));
28
35
  }
29
36
  // A gradient color may already be a platform int (array form) or a CSS string/PlatformColor
30
37
  // object needing processColor; mirrors process-box-shadow's processShadowColor.
@@ -63,7 +70,9 @@ function getAngleInDegrees(angle) {
63
70
  }
64
71
  }
65
72
  function getDirectionForKeyword(direction) {
66
- const normalized = direction.replace(WHITESPACE_NORMALIZE_REGEX, ' ').toLowerCase();
73
+ const normalized = direction
74
+ .replace(WHITESPACE_NORMALIZE_REGEX, ' ')
75
+ .toLowerCase();
67
76
  switch (normalized) {
68
77
  case 'to top':
69
78
  return { type: 'angle', value: 0 };
@@ -106,7 +115,9 @@ function resolveRadialSize(value) {
106
115
  value === 'farthest-corner') {
107
116
  return value;
108
117
  }
109
- if (isRecord(value) && isStringOrNumber(value.x) && isStringOrNumber(value.y)) {
118
+ if (isRecord(value) &&
119
+ isStringOrNumber(value.x) &&
120
+ isStringOrNumber(value.y)) {
110
121
  return { x: value.x, y: value.y };
111
122
  }
112
123
  return null;
@@ -129,7 +140,9 @@ function processColorStopsArray(rawColorStops) {
129
140
  return null;
130
141
  const positions = rawStop.positions;
131
142
  // Color transition hint syntax (`red, 20%, blue`): a position with no color of its own.
132
- if (rawStop.color == null && Array.isArray(positions) && positions.length === 1) {
143
+ if (rawStop.color == null &&
144
+ Array.isArray(positions) &&
145
+ positions.length === 1) {
133
146
  const position = positions[0];
134
147
  if (!isPositionValue(position))
135
148
  return null;
@@ -205,7 +218,13 @@ function processBackgroundImageArray(rawList) {
205
218
  const position = isRadialPositionValue(rawBgImage.position)
206
219
  ? rawBgImage.position
207
220
  : DEFAULT_RADIAL_POSITION;
208
- result.push({ type: 'radial-gradient', shape, size, position, colorStops });
221
+ result.push({
222
+ type: 'radial-gradient',
223
+ shape,
224
+ size,
225
+ position,
226
+ colorStops,
227
+ });
209
228
  }
210
229
  }
211
230
  return result;
@@ -365,11 +384,13 @@ function parseRadialGradientPositionTokens(tokens) {
365
384
  const token2 = t2.toLowerCase().trim();
366
385
  const horizontalPositions = ['left', 'center', 'right'];
367
386
  const verticalPositions = ['top', 'center', 'bottom'];
368
- if (horizontalPositions.includes(token1) && verticalPositions.includes(token2)) {
387
+ if (horizontalPositions.includes(token1) &&
388
+ verticalPositions.includes(token2)) {
369
389
  left = token1 === 'left' ? '0%' : token1 === 'center' ? '50%' : '100%';
370
390
  top = token2 === 'top' ? '0%' : token2 === 'center' ? '50%' : '100%';
371
391
  }
372
- else if (verticalPositions.includes(token1) && horizontalPositions.includes(token2)) {
392
+ else if (verticalPositions.includes(token1) &&
393
+ horizontalPositions.includes(token2)) {
373
394
  left = token2 === 'left' ? '0%' : token2 === 'center' ? '50%' : '100%';
374
395
  top = token1 === 'top' ? '0%' : token1 === 'center' ? '50%' : '100%';
375
396
  }
@@ -74,7 +74,9 @@ export function processFilter(filter) {
74
74
  for (const filterFunction of filter) {
75
75
  const [filterName, filterValue] = Object.entries(filterFunction)[0];
76
76
  if (filterName === 'dropShadow') {
77
- const dropShadow = isDropShadowValue(filterValue) ? parseDropShadow(filterValue) : null;
77
+ const dropShadow = isDropShadowValue(filterValue)
78
+ ? parseDropShadow(filterValue)
79
+ : null;
78
80
  if (dropShadow == null) {
79
81
  dlog(`processFilter reject: invalid dropShadow in array`);
80
82
  return [];
@@ -154,7 +156,9 @@ function getFilterAmount(filterName, filterArgs) {
154
156
  if (unit !== 'deg' && unit !== 'rad') {
155
157
  return undefined;
156
158
  }
157
- return unit === 'rad' ? RADIANS_TO_DEGREES * filterArgAsNumber : filterArgAsNumber;
159
+ return unit === 'rad'
160
+ ? RADIANS_TO_DEGREES * filterArgAsNumber
161
+ : filterArgAsNumber;
158
162
  // blur takes any non-negative CSS length that is not a percent; RN only has DIPs.
159
163
  case 'blur':
160
164
  if ((unit != null && unit !== 'px') || filterArgAsNumber < 0) {
@@ -169,7 +173,8 @@ function getFilterAmount(filterName, filterArgs) {
169
173
  case 'opacity':
170
174
  case 'saturate':
171
175
  case 'sepia':
172
- if ((unit != null && unit !== '%' && unit !== 'px') || filterArgAsNumber < 0) {
176
+ if ((unit != null && unit !== '%' && unit !== 'px') ||
177
+ filterArgAsNumber < 0) {
173
178
  return undefined;
174
179
  }
175
180
  return unit === '%' ? filterArgAsNumber / 100 : filterArgAsNumber;
@@ -179,7 +184,9 @@ function getFilterAmount(filterName, filterArgs) {
179
184
  }
180
185
  // RN processFilter.js:188-256.
181
186
  function parseDropShadow(rawDropShadow) {
182
- const dropShadow = typeof rawDropShadow === 'string' ? parseDropShadowString(rawDropShadow) : rawDropShadow;
187
+ const dropShadow = typeof rawDropShadow === 'string'
188
+ ? parseDropShadowString(rawDropShadow)
189
+ : rawDropShadow;
183
190
  if (dropShadow == null) {
184
191
  return null;
185
192
  }
@@ -12,6 +12,5 @@ export function processFontVariant(fontVariant) {
12
12
  if (typeof fontVariant === 'string') {
13
13
  return fontVariant.split(' ').filter(Boolean);
14
14
  }
15
- // Neither an array nor a string: nothing to split, hand back an empty variant list.
16
15
  return [];
17
16
  }