@symbiote-native/engine 0.3.0 → 0.5.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.
@@ -1,6 +1,6 @@
1
1
  // The AnimatedProps leaf lifecycle every Animated.* wrapper needs: build a leaf from the current
2
- // props, swap it into the value graph, bind it to the committed node, go native when asked, and
3
- // rebind native event props. Framework-agnostic on purpose - it knows nothing about hooks,
2
+ // props, swap it into the value graph, bind it to the committed node, and go native when asked.
3
+ // Framework-agnostic on purpose - it knows nothing about hooks,
4
4
  // effects, change detection or reactivity, only the engine's own Animated primitives.
5
5
  //
6
6
  // WHY THIS LIVES IN THE ENGINE. It used to live four times, once per adapter (React's
@@ -15,7 +15,6 @@
15
15
  // What stays in the adapter: WHEN to call reconcile (an effect, onUpdated, ngOnChanges, a $effect)
16
16
  // and HOW to find the host node (a ref, a ViewChild, a shim). Everything below is the same for all.
17
17
  import { AnimatedProps } from './props.js';
18
- import { attachNativeEventHandler } from './event.js';
19
18
  import { dlog } from '../debug.js';
20
19
  // Diagnostic-only, DEBUG-gated: a process-wide sequence number so every reconcile call across
21
20
  // every Animated.* instance in a log dump is individually identifiable and orderable.
@@ -66,7 +65,6 @@ function describeTransform(value) {
66
65
  }
67
66
  export function createAnimatedLeafLifecycle(label) {
68
67
  let attached = null;
69
- let eventDetachers = [];
70
68
  // Diagnostic-only: the last node identity, plus a reentrancy flag to catch reconcile() being
71
69
  // called AGAIN from inside its own call stack - the smoking-gun shape for a synchronous
72
70
  // same-flush loop, as opposed to merely "called often".
@@ -77,11 +75,6 @@ export function createAnimatedLeafLifecycle(label) {
77
75
  let lastWantsNative = false;
78
76
  // Canceller for a native bind the caller deferred and that has not run yet.
79
77
  let cancelPendingBind;
80
- function detachEvents() {
81
- for (const detach of eventDetachers)
82
- detach();
83
- eventDetachers = [];
84
- }
85
78
  return {
86
79
  reconcile(props, node, wantsNative, scheduleNativeBind) {
87
80
  const seq = ++globalReconcileSeq;
@@ -142,23 +135,19 @@ export function createAnimatedLeafLifecycle(label) {
142
135
  if (attached !== null && attached !== newLeaf)
143
136
  attached.__detach();
144
137
  attached = newLeaf;
145
- // The native half, which a caller may defer. Rebinds events each reconcile so a new inline
146
- // event re-attaches, detaching first so the prior binding does not leak;
147
- // attachNativeEventHandler no-ops unless the prop really is a native event handler on a
148
- // committed node, so the JS path stays the fallback.
138
+ // The native half, which a caller may defer.
139
+ //
140
+ // It used to native-attach every `Animated.event` prop here as well. It must not any more:
141
+ // a wrapper hands those same props DOWN to a host element, so they reach `routeProp`,
142
+ // which binds them itself (`host-binding.ts`, `bindAnimatedEvent`) — that is what makes a
143
+ // bare `<scroll-view onScroll={…} />` work with no wrapper at all. Doing it here too
144
+ // registered the SAME mapping twice on one view tag, caught by the Vue and Svelte
145
+ // wrapper tests the day the engine half landed.
149
146
  const bindNative = () => {
150
147
  if (node !== null)
151
148
  newLeaf.setNativeView(node);
152
149
  if (wantsNative)
153
150
  newLeaf.__makeNative();
154
- detachEvents();
155
- if (node === null)
156
- return;
157
- for (const key of Object.keys(props)) {
158
- const attachment = attachNativeEventHandler(node, key, props[key]);
159
- if (attachment !== undefined)
160
- eventDetachers.push(attachment.detach);
161
- }
162
151
  };
163
152
  cancelPendingBind?.();
164
153
  cancelPendingBind = undefined;
@@ -175,7 +164,6 @@ export function createAnimatedLeafLifecycle(label) {
175
164
  teardown() {
176
165
  cancelPendingBind?.();
177
166
  cancelPendingBind = undefined;
178
- detachEvents();
179
167
  if (attached !== null) {
180
168
  attached.__detach();
181
169
  attached = null;
@@ -14,6 +14,7 @@ export type ITaskCanceller = () => void;
14
14
  export type ITaskCancelProvider = () => ITaskCanceller;
15
15
  export interface IHostRegistrar {
16
16
  registerRunnable(appKey: string, run: IRunnable): string;
17
+ registerCancellableHeadlessTask?(taskKey: string, taskProvider: ITaskProvider, taskCancelProvider: ITaskCancelProvider): void;
17
18
  unmountAtRootTag?(rootTag: IRootTag): void;
18
19
  }
19
20
  export interface IAppRegistry<TComponentProvider, TWrapperComponentProvider> {
@@ -6,11 +6,9 @@
6
6
  // the one thing each adapter supplies; everything else (registry bookkeeping, sections, the
7
7
  // host-registrar bridge, headless tasks) is byte-identical across adapters and lives here once.
8
8
  //
9
- // The catch: the native Fabric host invokes RN's AppRegistry (a registered callable module) by
10
- // app key, and it can't see ours. So registerComponent must also hand its runnable to RN's
11
- // registrar. Reached the same way shared reaches processColor: a dependency-injected seam
12
- // (setHostRegistrar), so the core stays react-native-free and the app glue wires the host once
13
- // at startup.
9
+ // The catch: native invokes RN's registered callable AppRegistry for both surface runnables and
10
+ // headless tasks; it cannot see this local registry. `setHostRegistrar` mirrors those native-facing
11
+ // registrations there while the core remains react-native-free.
14
12
  import { getNativeModule } from '../native-modules/index.js';
15
13
  import { dlog } from '../debug.js';
16
14
  const HEADLESS_TASK_MODULE = 'HeadlessJsTaskSupport';
@@ -121,6 +119,7 @@ export function createAppRegistry(runnableFor) {
121
119
  }
122
120
  taskProviders.set(taskKey, taskProvider);
123
121
  taskCancelProviders.set(taskKey, taskCancelProvider);
122
+ hostRegistrar?.registerCancellableHeadlessTask?.(taskKey, taskProvider, taskCancelProvider);
124
123
  },
125
124
  startHeadlessTask(taskId, taskKey, data) {
126
125
  runHeadlessTask(taskId, taskKey, data);
@@ -138,7 +137,18 @@ export function createAppRegistry(runnableFor) {
138
137
  return {
139
138
  AppRegistry,
140
139
  setHostRegistrar(registrar) {
140
+ if (hostRegistrar === registrar)
141
+ return;
141
142
  hostRegistrar = registrar;
143
+ // Headless tasks are commonly registered before adapter bootstrap attaches RN's callable
144
+ // AppRegistry. Replay only this missing native-facing registry; app runnables retain their
145
+ // existing register-after-bootstrap contract.
146
+ for (const [taskKey, taskProvider] of taskProviders) {
147
+ const taskCancelProvider = taskCancelProviders.get(taskKey);
148
+ if (taskCancelProvider !== undefined) {
149
+ registrar.registerCancellableHeadlessTask?.(taskKey, taskProvider, taskCancelProvider);
150
+ }
151
+ }
142
152
  },
143
153
  };
144
154
  }
package/build/commit.d.ts CHANGED
@@ -1,12 +1,19 @@
1
1
  import { type IFabricNode, type IRootTag, type IMeasureOnSuccess, type IMeasureInWindowOnSuccess, type IMeasureLayoutOnSuccess } from './fabric';
2
2
  import { type ISymbioteNode } from './node';
3
3
  export { processColor, setColorProcessor } from './platform-color';
4
+ declare global {
5
+ var __SYMBIOTE_BATCH_CREATE__: boolean | undefined;
6
+ }
4
7
  export declare function disposeRoot(rootTag: IRootTag): void;
5
8
  export declare function commitChildren(rootTag: IRootTag, children: readonly ISymbioteNode[]): void;
6
9
  export interface ICommitProfile {
7
10
  commits: number;
8
11
  walkMs: number;
9
12
  nodesVisited: number;
13
+ /** Update-path nodes that rebuilt their Fabric payload and deep-compared it. */
14
+ propsBuilt: number;
15
+ /** Update-path nodes that re-cloned for a child but reused the committed payload by reference. */
16
+ propsReused: number;
10
17
  propWrites: number;
11
18
  propNoops: number;
12
19
  childScans: number;
@@ -16,7 +23,22 @@ export interface ICommitProfile {
16
23
  childFlattenWidest: number;
17
24
  }
18
25
  export declare function readCommitProfile(): ICommitProfile;
26
+ export declare function flushNativeProps(): void;
19
27
  export declare function setNativeProps(node: ISymbioteNode, partial: Record<string, unknown>): void;
28
+ /**
29
+ * Publish a node whose props were changed OUTSIDE any renderer mutation.
30
+ *
31
+ * Dirtying is not publishing. Every other write reaches Fabric because the framework's own commit
32
+ * follows it; a change driven by a NATIVE EVENT has no such follow-up — `native-events.ts`
33
+ * requests no commit and no adapter does either, so a node marked dirty from an event handler
34
+ * simply stays dirty. The host-behavior press path (`setNodePressed` for `:active`) is the first
35
+ * caller that is not `setNativeProps`, and `setNodeHidden`'s React twin never needed it because
36
+ * the reconciler is already in its commit phase when it calls.
37
+ *
38
+ * Queued rather than committed on the spot, sharing `setNativeProps`' batch: several writes in one
39
+ * task publish together at the microtask boundary, one commit per surface.
40
+ */
41
+ export declare function requestCommitFor(node: ISymbioteNode): void;
20
42
  export declare function getNativeTag(node: ISymbioteNode): number | undefined;
21
43
  export declare function whenCommitted(node: ISymbioteNode, action: () => void): () => void;
22
44
  export declare function getNativeNode(node: ISymbioteNode): IFabricNode | undefined;