@symbiote-native/engine 0.4.0 → 1.0.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 (96) hide show
  1. package/README.md +39 -14
  2. package/android/CMakeLists.txt +51 -0
  3. package/android/build.gradle +90 -0
  4. package/android/src/main/AndroidManifest.xml +1 -0
  5. package/android/src/main/cpp/SymbioteEngineJni.cpp +72 -0
  6. package/android/src/main/java/dev/symbiotenative/engine/SymbioteEngineModule.kt +43 -0
  7. package/android/src/main/java/dev/symbiotenative/engine/SymbioteEnginePackage.kt +35 -0
  8. package/build/accessibility-info/shared.js +1 -1
  9. package/build/accessibility-props.d.ts +1 -7
  10. package/build/accessibility-props.js +22 -21
  11. package/build/animated/animations/composition.d.ts +1 -1
  12. package/build/animated/animations/composition.js +18 -4
  13. package/build/animated/easing.d.ts +3 -2
  14. package/build/animated/easing.js +17 -88
  15. package/build/animated/event.js +6 -1
  16. package/build/animated/graph.d.ts +2 -0
  17. package/build/animated/graph.js +14 -0
  18. package/build/animated/host-binding.d.ts +39 -0
  19. package/build/animated/host-binding.js +278 -0
  20. package/build/animated/index.d.ts +1 -1
  21. package/build/animated/leaf-lifecycle.js +10 -22
  22. package/build/animated/mock.d.ts +1 -19
  23. package/build/animated/props.js +1 -1
  24. package/build/animated/rgba.js +16 -50
  25. package/build/events/index.js +123 -33
  26. package/build/fabric-props.d.ts +1 -1
  27. package/build/fabric-props.js +129 -182
  28. package/build/fabric.d.ts +10 -0
  29. package/build/fabric.js +40 -0
  30. package/build/host-access.d.ts +125 -0
  31. package/build/host-access.js +280 -0
  32. package/build/host-behavior.d.ts +107 -7
  33. package/build/host-behavior.js +309 -31
  34. package/build/image-source-write.d.ts +16 -0
  35. package/build/image-source-write.js +65 -0
  36. package/build/imperative.d.ts +49 -0
  37. package/build/imperative.js +258 -0
  38. package/build/index.d.ts +18 -10
  39. package/build/index.js +65 -10
  40. package/build/mutation-buffer.d.ts +222 -0
  41. package/build/mutation-buffer.js +491 -0
  42. package/build/native-engine.d.ts +182 -0
  43. package/build/native-engine.js +178 -0
  44. package/build/native-tree-host.d.ts +25 -0
  45. package/build/native-tree-host.js +66 -0
  46. package/build/node.d.ts +191 -60
  47. package/build/node.js +1034 -327
  48. package/build/pan-responder/index.d.ts +2 -2
  49. package/build/pan-responder/index.js +37 -56
  50. package/build/platform-color/index.d.ts +1 -1
  51. package/build/platform-color/index.js +11 -4
  52. package/build/process-background-image/index.js +30 -566
  53. package/build/process-background-longhands.d.ts +4 -0
  54. package/build/process-background-longhands.js +44 -0
  55. package/build/process-box-shadow/index.js +23 -187
  56. package/build/process-filter.js +27 -300
  57. package/build/process-transform/index.d.ts +1 -1
  58. package/build/process-transform/index.js +25 -107
  59. package/build/process-transform-origin/index.d.ts +1 -1
  60. package/build/process-transform-origin/index.js +29 -102
  61. package/build/registry.d.ts +36 -0
  62. package/build/registry.js +73 -0
  63. package/build/sound-manager/index.d.ts +3 -0
  64. package/build/sound-manager/index.js +36 -0
  65. package/build/structured-style.d.ts +10 -0
  66. package/build/structured-style.js +180 -0
  67. package/build/style-registry/index.d.ts +14 -0
  68. package/build/style-registry/index.js +60 -11
  69. package/build/styles.d.ts +5 -1
  70. package/build/surface.d.ts +31 -2
  71. package/build/surface.js +138 -42
  72. package/build/text-input-state.d.ts +1 -0
  73. package/build/text-input-state.js +17 -3
  74. package/build/tree-host.d.ts +307 -0
  75. package/build/tree-host.js +211 -0
  76. package/build/view-config.js +4 -4
  77. package/codegen-specs/NativeSymbioteEngine.ts +27 -0
  78. package/cpp/SymbioteDebug.cpp +51 -0
  79. package/cpp/SymbioteDebug.h +54 -0
  80. package/cpp/SymbioteEngineBindings.cpp +232 -0
  81. package/cpp/SymbioteEngineBindings.h +59 -0
  82. package/cpp/SymbioteFabricProps.cpp +2619 -0
  83. package/cpp/SymbioteFabricProps.h +223 -0
  84. package/cpp/SymbioteTree.cpp +2478 -0
  85. package/cpp/SymbioteTree.h +257 -0
  86. package/ios/SymbioteEngineModule.h +25 -0
  87. package/ios/SymbioteEngineModule.mm +44 -0
  88. package/package.json +31 -3
  89. package/react-native.config.cjs +23 -0
  90. package/symbiote-engine.podspec +42 -0
  91. package/build/animated/bezier.d.ts +0 -1
  92. package/build/animated/bezier.js +0 -102
  93. package/build/commit.d.ts +0 -49
  94. package/build/commit.js +0 -1030
  95. package/build/tags.d.ts +0 -2
  96. package/build/tags.js +0 -40
@@ -0,0 +1,178 @@
1
+ // The JS half of our own native module (item 8c of `symbiote-fabric-cxx-surface`). It answers one
2
+ // question — is there a native store on this platform — and every caller must be able to take `no`.
3
+ //
4
+ // `no` is not an error case, it is the majority case:
5
+ //
6
+ // headless (vitest, 5 600 tests) no native anything; the JS backing store is the only one
7
+ // Android the module is iOS-only for now, by choice, not by oversight
8
+ // an app that skipped `pod install` the standing hazard of this repo's local-dev loop
9
+ // an older pod against newer JS two artefacts, two install steps, no shared version gate
10
+ //
11
+ // So the contract is: `nativeEngine()` returns the bindings or `undefined`, and nothing in the engine
12
+ // may require them. The native store is an ACCELERATOR behind the same seam the JS one sits behind.
13
+ //
14
+ // ── THIS IS NOT A LICENCE FOR TWO IMPLEMENTATIONS, AND THE DISTINCTION IS THE POINT ──────────────
15
+ //
16
+ // Two different things get called "the native path" and only one of them is a fork:
17
+ //
18
+ // the STORE (8b') `new Int32Array(n)` vs an `Int32Array` over native memory. `node-table.ts`
19
+ // is byte-identical either way — one implementation, two allocators. There is
20
+ // nothing here that can drift.
21
+ // the APPLIER (8c-1) `replayChildOps` in commit.ts vs `cloneMultiple` inside a commit hook. That
22
+ // IS two implementations of one fold, and a permanent fallback to the JS half
23
+ // would be exactly the silent divergence this repo keeps finding.
24
+ //
25
+ // So the graceful `undefined` below is correct for the store and TRANSITIONAL for the applier. When
26
+ // the applier moves, the JS one is DELETED — the way `node.children` and `node.parent` were — and a
27
+ // missing or mismatched module has to fail LOUD rather than quietly run a second implementation. A
28
+ // silent fallback at that point is a measurement that lies, not resilience.
29
+ //
30
+ // The expensive part of that transition, named here because it is easy to discover too late: ~5 600
31
+ // headless tests drive the JS applier against a fake Fabric slot. Faking `new Int32Array` is free;
32
+ // faking C++ tree cloning is not, so those tests stop covering what actually runs.
33
+ //
34
+ // Fantom was the intended answer and it is NOT available — measured 2026-09-08, four independent
35
+ // blockers. `@react-native/fantom` is not on npm at all (404); it exists only as vendored source
36
+ // under `.vendors/react-native/private/`, marked `private: true`. That vendored tree is RN **main**
37
+ // while this repo consumes 0.86.0, so a tester built from it would exercise the wrong C++. Building
38
+ // it needs a yarn install plus a gradle/CMake build of Hermes, folly and ReactCommon, all written
39
+ // INTO `.vendors/`. And `config/metro.config.js` computes `projectRoot` from `__dirname` with an
40
+ // empty `watchFolders`, so a test file outside that monorepo cannot be resolved — not overridable
41
+ // by env, config merge or CLI. RN's own README says it plainly: "not currently supported for
42
+ // testing application-specific code in React Native apps."
43
+ //
44
+ // So the oracle for a C++ commit path has to be built here: a TypeScript reference implementation
45
+ // the existing suite already exercises, plus a gtest target linking ReactCommon from the installed
46
+ // react-native. Do not cite Fantom as the plan again without re-running that check.
47
+ import { dlog } from './debug.js';
48
+ import { getNativeModule } from './native-modules/index.js';
49
+ import { isRecord } from './type-guards.js';
50
+ /**
51
+ * The ABI this file knows how to talk to. A binary reporting anything else is refused outright rather
52
+ * than probed method by method — the two artefacts ship separately (a pod and an npm package), so
53
+ * disagreement is routine, and a partial match is the shape that corrupts memory quietly.
54
+ *
55
+ * Exported for the tests, which must DERIVE their supported and unsupported arms from it rather than
56
+ * restate the number: a bump that leaves a fixture behind reads as "the binary is stale", which is
57
+ * exactly the message this constant exists to produce, so the failure looks like the feature working.
58
+ */
59
+ export const SUPPORTED_NATIVE_VERSION = 4;
60
+ function isBindings(value) {
61
+ if (!isRecord(value))
62
+ return false;
63
+ if (typeof value.version !== 'number')
64
+ return false;
65
+ if (typeof value.allocInt32Array !== 'function')
66
+ return false;
67
+ if (typeof value.probeUIManager !== 'function')
68
+ return false;
69
+ if (typeof value.applyOps !== 'function')
70
+ return false;
71
+ // The tree reads, checked one by one rather than as a blob for the reason the whole guard is
72
+ // written this way: a member added to the type without a line here resolves fine at bring-up and
73
+ // throws at the first call site, which is a gesture or a `measure()` — one language and several
74
+ // seconds away from the install that caused it.
75
+ if (typeof value.getProp !== 'function')
76
+ return false;
77
+ if (typeof value.getProps !== 'function')
78
+ return false;
79
+ if (typeof value.markPropsDirty !== 'function')
80
+ return false;
81
+ if (typeof value.getViewName !== 'function')
82
+ return false;
83
+ if (typeof value.parentOf !== 'function')
84
+ return false;
85
+ if (typeof value.childrenOf !== 'function')
86
+ return false;
87
+ if (typeof value.nextSiblingOf !== 'function')
88
+ return false;
89
+ if (typeof value.parentsOf !== 'function')
90
+ return false;
91
+ if (typeof value.subtreesOf !== 'function')
92
+ return false;
93
+ if (typeof value.ancestorsOf !== 'function')
94
+ return false;
95
+ if (typeof value.committedRecordOf !== 'function')
96
+ return false;
97
+ // The imperative six, checked by name for the same reason as the rest: a pod that has `applyOps`
98
+ // but not these is an OLDER binary, and accepting it would leave `measure()` reaching a method
99
+ // that is not there — at the moment an app measures a ref, not at bring-up.
100
+ if (typeof value.dispatchCommand !== 'function')
101
+ return false;
102
+ if (typeof value.sendAccessibilityEvent !== 'function')
103
+ return false;
104
+ if (typeof value.measure !== 'function')
105
+ return false;
106
+ if (typeof value.measureInWindow !== 'function')
107
+ return false;
108
+ if (typeof value.measureLayout !== 'function')
109
+ return false;
110
+ return typeof value.setIsJSResponder === 'function';
111
+ }
112
+ // Why `SUPPORTED_NATIVE_VERSION` did NOT move when `probeUIManager` was added, since bumping it is
113
+ // the reflex. The version field guards a MEMORY LAYOUT — it is what stops JS reading a store whose
114
+ // fields moved. Adding a host function changes no layout, and the shape guard above already refuses
115
+ // an older pod that lacks the name, giving the same clean degrade for free. Bumping instead would
116
+ // cost every app on an older pod its native STORE to announce a probe it does not use. Bump the
117
+ // version when the meaning of the bytes changes, not when the object gains a key.
118
+ //
119
+ // ── AND WHY IT DID MOVE, 1 -> 2, FOR THE HANDLE REWRITE ──────────────────────────────────────────
120
+ //
121
+ // Because the shape guard cannot see this one. Every NAME is unchanged: a v1 pod has `applyOps` and
122
+ // all five, so `isBindings` passes it. What changed is what the arguments MEAN — `applyOps` gained
123
+ // `handles`, which a v1 binary silently ignores, and the five went from taking an integer id to
124
+ // taking the handle object, which a v1 binary reads with `.asNumber()`. The first fails by
125
+ // committing nothing and leaking; the second throws one language away from the cause. That is the
126
+ // same class as a moved field, arriving through a calling convention rather than a struct, and it is
127
+ // exactly what this number exists to refuse.
128
+ //
129
+ // ── AND 3 -> 4, FOR THE SAME REASON THE SHAPE GUARD CANNOT HELP WITH ─────────────────────────────
130
+ //
131
+ // The four tree reads are new NAMES, so the guard above would turn a v3 pod away on its own. But
132
+ // `applyOps` also went from six arguments to five, and the name did not move: a v3 binary reads
133
+ // argument 1 as `childIds` (an `Int32Array`) where JS now passes `strings`, and argument 2 as a
134
+ // props table where JS now passes `values`. Every one of those reads succeeds against the wrong
135
+ // object and commits a wrong tree rather than throwing. That is a calling convention change, which
136
+ // is what this number is for — the guard would have refused this pod by luck, on the reads, and a
137
+ // refusal by luck is not a refusal.
138
+ // `undefined` means "resolved, and there is none" — distinct from `resolved === false`, which means
139
+ // nobody has looked. Collapsing the two would re-run the lookup on every miss, and the miss is the
140
+ // common case.
141
+ let resolved = false;
142
+ let bindings;
143
+ /**
144
+ * The native bindings, or `undefined` when this platform has none.
145
+ *
146
+ * Resolving the TurboModule is done for its SIDE EFFECT: `RCTTurboModuleManager` runs
147
+ * `installJSIBindingsWithRuntime:` at the moment it creates a module, so touching the module by name
148
+ * is what puts the global there. The module's own methods are not the capability and are not called
149
+ * here — which is why a reader looking for the payload in the spec file will not find it.
150
+ */
151
+ export function nativeEngine() {
152
+ if (resolved)
153
+ return bindings;
154
+ resolved = true;
155
+ getNativeModule('SymbioteEngine');
156
+ const installed = globalThis.__symbioteEngineNative;
157
+ if (!isBindings(installed)) {
158
+ dlog('native-engine: no native store on this platform ' +
159
+ `(global is ${typeof installed}) — using the JS backing arrays`);
160
+ return undefined;
161
+ }
162
+ if (installed.version !== SUPPORTED_NATIVE_VERSION) {
163
+ // Loud, because the repair is a `pod install` and the symptom otherwise is a wrong memory layout
164
+ // rather than a missing feature. `<examples_vs_dot_examples>` records how routinely an install
165
+ // replaces the JS half without the native one.
166
+ dlog(`native-engine: REFUSING native store, ABI ${installed.version} ` +
167
+ `against supported ${SUPPORTED_NATIVE_VERSION} — run \`pod install\`. Using the JS arrays`);
168
+ return undefined;
169
+ }
170
+ dlog(`native-engine: native store available, ABI ${installed.version}`);
171
+ bindings = installed;
172
+ return bindings;
173
+ }
174
+ /** Test seam: forget what was resolved, so a fixture can install or remove the global between cases. */
175
+ export function resetNativeEngine() {
176
+ resolved = false;
177
+ bindings = undefined;
178
+ }
@@ -0,0 +1,25 @@
1
+ import { type INativeEngineBindings } from './native-engine';
2
+ import { type ITreeHost } from './tree-host';
3
+ /**
4
+ * Present one resolved set of bindings as the engine's tree host.
5
+ *
6
+ * `census` is the only member that does not reach native, and it is deliberately not on the ABI: it
7
+ * has exactly ONE engine caller (`censusRetainedTree`), it is diagnostics, and answering it honestly
8
+ * would cost a full native walk of the very tree the design exists to stop walking. The empty census
9
+ * is what `censusRetainedTree` already answers with no host at all, so a probe reading it sees the
10
+ * same zeroes it has always seen off a device.
11
+ */
12
+ export declare function nativeTreeHost(bindings: INativeEngineBindings): ITreeHost;
13
+ /**
14
+ * Install it, if this runtime has a native module and nothing has claimed the seam already.
15
+ *
16
+ * PRECEDENCE: an installed host WINS. `installFabric()` is the only other caller of `setTreeHost`,
17
+ * and it puts the TypeScript applier in before any fixture can bind a slot — so a headless run that
18
+ * also happens to carry fake bindings must keep the applier, or the ~5 500 tests written against it
19
+ * would silently start driving a stub. The reverse ordering cannot occur on a device: nothing there
20
+ * installs a host but this.
21
+ *
22
+ * No native module is the ORDINARY answer (`native-engine.ts`'s header lists where), and it stays a
23
+ * quiet one here: the ops simply keep accumulating, exactly as they did before this file existed.
24
+ */
25
+ export declare function installNativeTreeHost(): void;
@@ -0,0 +1,66 @@
1
+ // The NATIVE tree host — `INativeEngineBindings` presented as an `ITreeHost`.
2
+ //
3
+ // It is the half that makes the buffer mean anything on a device. `tree-host.ts` records ops and
4
+ // then asks a host to turn them into a tree; headlessly `installFabric()` installs the TypeScript
5
+ // applier, and until this file existed a device installed NOTHING — `commitSurfaceOps` returned
6
+ // early, the ops stayed pending forever, and the screen stayed blank with nothing red anywhere.
7
+ //
8
+ // There is no logic here on purpose. Everything the host is asked is something native already
9
+ // answers, so this is a rename and an argument spread; a mapping thin enough to read in one pass is
10
+ // what keeps the two sides auditable against each other.
11
+ import { nativeEngine } from './native-engine.js';
12
+ import { EMPTY_CENSUS, setTreeHost, treeHost, } from './tree-host.js';
13
+ /**
14
+ * Present one resolved set of bindings as the engine's tree host.
15
+ *
16
+ * `census` is the only member that does not reach native, and it is deliberately not on the ABI: it
17
+ * has exactly ONE engine caller (`censusRetainedTree`), it is diagnostics, and answering it honestly
18
+ * would cost a full native walk of the very tree the design exists to stop walking. The empty census
19
+ * is what `censusRetainedTree` already answers with no host at all, so a probe reading it sees the
20
+ * same zeroes it has always seen off a device.
21
+ */
22
+ export function nativeTreeHost(bindings) {
23
+ return {
24
+ // Spread rather than passed as the batch object: JSI reads five arguments cheaper than five
25
+ // properties, and this is the one member on a commit path.
26
+ applyOps: batch => bindings.applyOps(batch.ops, batch.strings, batch.values, batch.instanceHandles, batch.handles),
27
+ propOf: bindings.getProp,
28
+ propsOf: bindings.getProps,
29
+ markPropsDirty: bindings.markPropsDirty,
30
+ committedRecordOf: bindings.committedRecordOf,
31
+ committedPayloadOf: bindings.committedPayloadOf,
32
+ parentOf: bindings.parentOf,
33
+ childrenOf: bindings.childrenOf,
34
+ nextSiblingOf: bindings.nextSiblingOf,
35
+ parentsOf: bindings.parentsOf,
36
+ subtreesOf: bindings.subtreesOf,
37
+ ancestorsOf: bindings.ancestorsOf,
38
+ census: () => EMPTY_CENSUS,
39
+ // Straight through: native already takes the placeholder, which is what `committedRecordOf`
40
+ // above hands back in its `handle` field for exactly this reason.
41
+ dispatchCommand: bindings.dispatchCommand,
42
+ sendAccessibilityEvent: bindings.sendAccessibilityEvent,
43
+ measure: bindings.measure,
44
+ measureInWindow: bindings.measureInWindow,
45
+ measureLayout: bindings.measureLayout,
46
+ setIsJSResponder: bindings.setIsJSResponder,
47
+ };
48
+ }
49
+ /**
50
+ * Install it, if this runtime has a native module and nothing has claimed the seam already.
51
+ *
52
+ * PRECEDENCE: an installed host WINS. `installFabric()` is the only other caller of `setTreeHost`,
53
+ * and it puts the TypeScript applier in before any fixture can bind a slot — so a headless run that
54
+ * also happens to carry fake bindings must keep the applier, or the ~5 500 tests written against it
55
+ * would silently start driving a stub. The reverse ordering cannot occur on a device: nothing there
56
+ * installs a host but this.
57
+ *
58
+ * No native module is the ORDINARY answer (`native-engine.ts`'s header lists where), and it stays a
59
+ * quiet one here: the ops simply keep accumulating, exactly as they did before this file existed.
60
+ */
61
+ export function installNativeTreeHost() {
62
+ const bindings = nativeEngine();
63
+ if (bindings === undefined || treeHost() !== undefined)
64
+ return;
65
+ setTreeHost(nativeTreeHost(bindings));
66
+ }
package/build/node.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- import type { IFabricNode, IFabricProps, IRootTag, IMeasureOnSuccess, IMeasureInWindowOnSuccess, IMeasureLayoutOnSuccess } from './fabric';
1
+ import type { IMeasureOnSuccess, IMeasureInWindowOnSuccess, IMeasureLayoutOnSuccess } from './fabric';
2
2
  import { type IClassNameValue } from './style-registry';
3
3
  import { type IPayloadFold } from './host-behavior';
4
+ import { type ITreeCensus } from './tree-host';
4
5
  declare const BRAND: unique symbol;
5
6
  export declare const RAW_TEXT_COMPONENT = "RCTRawText";
6
7
  export declare const TEXT_COMPONENT = "RCTText";
@@ -18,94 +19,166 @@ export interface ISymbioteNode {
18
19
  readonly [BRAND]: true;
19
20
  component: string;
20
21
  readonly isText: boolean;
21
- props: Record<string, unknown>;
22
22
  listeners: Map<string, IListener> | undefined;
23
- children: ISymbioteNode[];
24
- parent: ISymbioteNode | undefined;
25
- dirty: boolean;
26
- propsDirty: boolean;
27
- hasAriaAlias: boolean;
23
+ /**
24
+ * Whether this node's host behavior declared `afterCommit`.
25
+ *
26
+ * Read on every prop write, exactly as `hasAriaAlias` beside it is and for the same reason: the
27
+ * alternative is a Set lookup per write, on the hottest path in the engine. What it gates is the
28
+ * NARROWING of the post-commit beat — the hook runs for nodes whose props moved rather than for
29
+ * every mounted behavior, which is what took TextInput's `propOf` off every commit (F-67).
30
+ *
31
+ * Set once at `createElement`, by `attachHostBehavior`. Not sticky in the `hasAriaAlias` sense:
32
+ * it describes the behavior's shape, and a behavior is attached once and detached whole.
33
+ */
34
+ hasCommitHook: boolean;
35
+ /**
36
+ * Whether this node's three image-source props are resolved on the way IN — see
37
+ * `image-source-write.ts` for why the asset lookup happens at write time and not in the payload.
38
+ *
39
+ * A boolean read per write, same shape and same reason as `hasCommitHook` above. It has to be
40
+ * gated on the NODE rather than applied to the key everywhere, because the resolution normalises
41
+ * to Image's ARRAY shape: a `WebView` or a third-party video view also spells `source`, and
42
+ * wrapping theirs would hand native a shape it does not read.
43
+ *
44
+ * Set once at `createElement`, by `attachHostBehavior`, for the behavior that declares it.
45
+ */
46
+ resolvesImageSources: boolean;
47
+ nativeIdWinsOverId: boolean;
28
48
  payloadFold: IPayloadFold | undefined;
29
- structureDirty: boolean;
30
- committed: IMirror | undefined;
31
49
  styleParts: IClassStyleParts | undefined;
50
+ childHost: ISymbioteNode | undefined;
51
+ wrapper: ISymbioteNode | undefined;
52
+ mayHaveChildren: boolean;
32
53
  measure(callback: IMeasureOnSuccess): void;
33
54
  measureInWindow(callback: IMeasureInWindowOnSuccess): void;
34
55
  measureLayout(relativeToNativeNode: ISymbioteNode | number, onSuccess: IMeasureLayoutOnSuccess, onFail?: () => void): void;
35
56
  setNativeProps(nativeProps: Record<string, unknown>): void;
36
57
  focus(): void;
37
58
  blur(): void;
38
- }
39
- export interface IMirror {
40
- handle: IFabricNode;
41
- tag: number;
42
- rootTag: IRootTag;
43
- props: IFabricProps;
44
- children: readonly ISymbioteNode[];
45
- viewName: string;
46
- parent: ISymbioteNode | undefined;
47
- owner: ISymbioteNode;
59
+ scrollTo(options?: {
60
+ x?: number;
61
+ y?: number;
62
+ animated?: boolean;
63
+ }): void;
64
+ scrollToEnd(options?: {
65
+ animated?: boolean;
66
+ }): void;
67
+ flashScrollIndicators(): void;
48
68
  }
49
69
  /**
50
- * The committed record for `node`, or `undefined` if it has never been committed - or if `node` is
51
- * not the raw retained node at all.
70
+ * Mint an element and record its creation.
52
71
  *
53
- * That second case is the reason this is a function rather than a bare `node.committed` read. The
54
- * engine identifies a node BY IDENTITY, and the classic way to break that is to hand the engine a
55
- * wrapper instead of the node: a Vue `reactive()`/deep-`ref()` Proxy around a host element is the
56
- * one that actually happens (see the vue-adapter-reactivity skill; `shallowRef` is the fix).
72
+ * The node object IS the handle: it is what the ops address, what the host attaches its native node
73
+ * to, and what Fabric hands back as an event target. Nothing else is allocated.
74
+ */
75
+ export declare function createElement(component: string, isText?: boolean, tag?: string): ISymbioteNode;
76
+ /**
77
+ * `tag` mirrors `createElement`'s, and a raw text needs it for the same reason an element does: the
78
+ * behavior registry is keyed by tag, so a node that does not hand one over cannot have a rule.
57
79
  *
58
- * The old WeakMap caught this for free - a Proxy is a different object, so `mirror.get(proxy)` missed
59
- * and every imperative API bailed with a clear "node not committed". A plain property read does NOT:
60
- * a Proxy forwards `proxy.committed` straight to the target and hands back a real record, whose
61
- * `handle` Vue would then deep-wrap on the way out. That handle is a JSI host object; a Proxy around
62
- * it reaches `cloneNodeWithNewProps` and fails somewhere deep in native, far from the cause.
80
+ * A raw text carrying a tag looks odd and is not. It has no props an app can write — its whole
81
+ * payload is `text` — but its CONTENT can still be a function of the platform rather than of the
82
+ * app: Button renders its title uppercased on Android (`Button.js:352-353`), which is a user-agent
83
+ * decision about a control, not anything the app asked for. That rule needs the node to be
84
+ * identifiable, and a tag is how this codebase identifies one.
63
85
  *
64
- * So the identity check that was implicit in the WeakMap is explicit here: a record written on the
65
- * raw node names it, and `record.owner !== node` means whatever we were handed is not that node.
66
- * One reference comparison, and the wrap now fails LOUDER than it used to rather than quieter.
86
+ * Defaulted to the raw-text component, so every existing caller is unchanged and pays the same
87
+ * lookup miss `createElement` already pays for a node nobody registered.
67
88
  */
68
- export declare function committedOf(node: ISymbioteNode): IMirror | undefined;
69
- export declare function createElement(component: string, isText?: boolean, tag?: string): ISymbioteNode;
70
- export declare function createRawText(text: string): ISymbioteNode;
89
+ export declare function createRawText(text: string, tag?: string): ISymbioteNode;
71
90
  export declare function isSymbioteNode(value: unknown): value is ISymbioteNode;
72
91
  export declare function debugNodeId(node: ISymbioteNode): number;
73
92
  export declare const ANCHOR_COMPONENT = "#anchor";
74
93
  export declare function createAnchor(): ISymbioteNode;
94
+ export declare const VOID_COMPONENT = "#void";
95
+ export declare function createVoid(): ISymbioteNode;
96
+ /**
97
+ * The sentinel a SURFACE's own root node carries, so `parentOf` can stop there.
98
+ *
99
+ * A top-level node must answer `undefined` for its parent, and adapters depend on the exact miss:
100
+ * Angular reads `null` as "defer, `<ng-content>` will place this" (answering the surface once
101
+ * mounted every FlatList cell at top level), while Vue and Solid spell `?? surface` at their call
102
+ * sites and would be handed an object that is not the `SymbioteSurface` they compare against. One
103
+ * component name, read in JS, keeps all three right without a second structure.
104
+ *
105
+ * It is a JS-side name only. What goes over the wire is `RCTView`, because this node is REAL.
106
+ */
107
+ export declare const SURFACE_COMPONENT = "#surface";
108
+ /**
109
+ * One persistent root view per surface, mirroring RN's own AppContainer — `renderApplication` wraps
110
+ * the app in `<View style={{flex:1}} pointerEvents="box-none">`.
111
+ *
112
+ * It is not decoration. Without `flex: 1` a non-flex root collapses to content height, and without
113
+ * `box-none` a touch landing outside the app's own children has no escape. Living here rather than
114
+ * in each adapter's `mount()` gives every framework a full-screen root for free and keeps layout in
115
+ * the shared layer (`<adapters_stay_thin>`).
116
+ *
117
+ * The surface therefore commits as ONE node rather than hoisting its children into the child set —
118
+ * which is why the host materializes the node `OP_COMMIT` names instead of walking its children. An
119
+ * anchor in that position still hoists, so the host handles both without a special case.
120
+ */
121
+ export declare function createSurfaceRoot(): ISymbioteNode;
75
122
  export declare function isAnchor(node: ISymbioteNode): boolean;
76
- export declare function isEmptyRawText(node: ISymbioteNode): boolean;
77
123
  /**
78
124
  * Change which Fabric view a node commits as, keeping the node's identity.
79
125
  *
80
- * The commit walk already re-creates a node whose `viewName` no longer matches its committed one —
81
- * that is how a `<Text>` moving in or out of another `<Text>` flips between RCTText and
82
- * RCTVirtualText (`commit.ts`, reason `view-kind`). This exposes the same door for a prop-driven
83
- * view choice, so `intrinsicWhen` is honoured on UPDATE and not only at create.
84
- *
85
126
  * The POLICY stays out of the engine: which prop decides, and which view it decides between, lives
86
127
  * in `HOST_PRIMITIVES` and is read by `resolveIntrinsicTag` in `@symbiote-native/components`. The
87
128
  * engine only knows how to swap the name — the same split every other spec-driven fold has here.
88
129
  *
89
130
  * A no-op when the name is unchanged, so a renderer may call it on every update without comparing
90
131
  * first.
132
+ *
133
+ * The JS field and the op BOTH move, and both are load-bearing. `node.component` is what the aria
134
+ * fold, the behavior registry and `fabricProps` key on; the op is what makes the host re-create the
135
+ * node under the new name, since no prop write moves a node between native views.
91
136
  */
92
137
  export declare function setNodeComponent(node: ISymbioteNode, component: string): void;
93
- export declare function markDirty(node: ISymbioteNode): void;
94
- export declare function markPropsDirty(node: ISymbioteNode): void;
95
- export declare function markStructureDirty(parent: ISymbioteNode): void;
96
138
  export declare function takePropStats(): {
97
139
  writes: number;
98
- noops: number;
99
140
  };
141
+ export declare function takePropKeyTally(): ReadonlyMap<string, number>;
100
142
  export declare function setProp(node: ISymbioteNode, key: string, value: unknown): void;
143
+ /**
144
+ * The one place a prop reaches the wire, and the only place that can keep a function off it.
145
+ *
146
+ * `setNativeProps` calls this rather than `recordSetProp` for that reason — it is the path that has
147
+ * no `routeProp` in front of it.
148
+ */
149
+ export declare function writeProp(node: ISymbioteNode, key: string, value: unknown): void;
150
+ /** What `propOf` consults before asking the host. `undefined` when nothing was stashed. */
151
+ export declare function functionPropOf(node: ISymbioteNode, key: string): unknown;
152
+ /**
153
+ * The same stash, whole — what `propsOf` layers over the host's answer.
154
+ *
155
+ * `undefined` rather than an empty Map for a node that stashed nothing, which is nearly every node:
156
+ * the caller then hands back the host's own object instead of copying it.
157
+ */
158
+ export declare function functionPropsOf(node: ISymbioteNode): ReadonlyMap<string, unknown> | undefined;
159
+ /**
160
+ * "Rebuild this node's payload — the fold reads state I just changed."
161
+ *
162
+ * A behavior whose payload is DERIVED has no prop to write: the sticky header's debounced
163
+ * translateY lives in its own runtime, not on the node, so nothing names the node and the host
164
+ * never marks it. This is the one route that says so directly.
165
+ *
166
+ * Dirtying is not publishing — pair it with `requestCommitFor` (imperative.ts).
167
+ */
168
+ export declare function markPropsDirty(node: ISymbioteNode): void;
101
169
  /**
102
170
  * Install a listener the BEHAVIOR owns, bypassing the ownership check.
103
171
  *
104
172
  * `setEventListener` diverts an owned name into the stash, which is right for an app listener and
105
173
  * circular for the behavior's own dispatcher — it would stash itself and never occupy the slot it
106
174
  * exists to hold. This is the one writer allowed past that gate.
175
+ *
176
+ * `undefined` removes it, gate flag included. A behavior whose dispatcher is conditional needs
177
+ * that as much as it needs the install: ScrollView takes the owner's `layout` only while the app
178
+ * or an inverted sticky header wants it, and a one-way installer leaves `onLayout: true` standing
179
+ * in the payload of a ScrollView that no longer reads the event.
107
180
  */
108
- export declare function setBehaviorListener(node: ISymbioteNode, name: string, listener: IListener): void;
181
+ export declare function setBehaviorListener(node: ISymbioteNode, name: string, listener: IListener | undefined): void;
109
182
  export declare function setEventListener(node: ISymbioteNode, name: string, value: unknown): void;
110
183
  export interface IClassStyleParts {
111
184
  classStyle: unknown;
@@ -115,7 +188,30 @@ export interface IClassStyleParts {
115
188
  isPressed: boolean;
116
189
  activeStyle: unknown;
117
190
  activeStyleFromCallback: boolean;
191
+ published: readonly unknown[] | undefined;
118
192
  }
193
+ /**
194
+ * Is this rebuilt style the same style, key for key?
195
+ *
196
+ * A component body that writes its style inline hands over a FRESH object every render, equal to
197
+ * the one already standing — the commonest shape any app produces, and one `Object.is` cannot see.
198
+ * Without this the write crosses into the host, becomes a `folly::dynamic`, and is only THEN found
199
+ * to be unchanged. Measured on `build-release` (`no-op-rerender-cost.itest.ts`), 1 000 rows
200
+ * re-rendered with nothing changed: 9.7 ms against 0.3 ms for the same app with its style hoisted,
201
+ * and 5.2 ms of that was the conversion. The cheapest place to refuse a write is the earliest place
202
+ * that can see it is a no-op.
203
+ *
204
+ * SHALLOW AND CONSERVATIVE, both deliberately. A nested value (a transform list, a shadow, a style
205
+ * array) reports "not the same" rather than being compared deeply, because a deep compare makes this
206
+ * guard cost the size of the style — which is the cost it exists to avoid. Those keep crossing and
207
+ * the host's own `diffProps` refuses them exactly as before, so being wrong here is slow, never
208
+ * incorrect.
209
+ *
210
+ * `undefined` on either side also reports "not the same", which is what lets the key COUNT stand in
211
+ * for a key-set comparison: equal counts plus every key of `next` matching a defined value in
212
+ * `standing` cannot leave a key unaccounted for.
213
+ */
214
+ export declare function isSameShallowStyle(next: unknown, standing: unknown): boolean;
119
215
  /**
120
216
  * Stop a node painting without unmounting it, or let it paint again.
121
217
  *
@@ -138,20 +234,55 @@ export declare function setNodeHidden(node: ISymbioteNode, hidden: boolean): voi
138
234
  * and the node is never dirtied.
139
235
  */
140
236
  export declare function setNodePressed(node: ISymbioteNode, pressed: boolean): void;
237
+ /**
238
+ * Tell the host a behavior's FEEDBACK is showing — TouchableHighlight's underlay, and only that.
239
+ *
240
+ * The sibling of `setNodePressed` and deliberately NOT the same bit. Press state drives `:active`
241
+ * class resolution, which happens in JS because a class name resolves against a JS registry; this
242
+ * drives a rule that lives in C++ (`foldTouchableHighlightUnderlay`), so it crosses as one op rather
243
+ * than resolving to a style here. And the two are genuinely different facts: RN holds the underlay
244
+ * past release so a fast tap still flashes, so `shown` LAGS `pressed` by a `delayPressOut` timer.
245
+ *
246
+ * No style is computed on this side at all, which is the whole point — the two props the rule reads
247
+ * (`underlayColor`, `activeOpacity`) are ones the engine already strips from the payload, so the
248
+ * values and their defaults live in one place instead of being erased in C++ and reached around for
249
+ * in JS.
250
+ */
251
+ export declare function setNodeUnderlayShown(node: ISymbioteNode, shown: boolean): void;
252
+ /**
253
+ * Forget what was last published, so the next `pushClassStyle` cannot be turned away.
254
+ *
255
+ * The one caller is `setNativeProps` (imperative.ts), which writes the style slot past this file —
256
+ * see the note on `IClassStyleParts.published` for why the restore path depends on this. A no-op for
257
+ * a node nobody has styled, which is why it is not `stylePartsOf(node).published = undefined`: that
258
+ * would allocate the parts on a node that has none.
259
+ */
260
+ export declare function clearPublishedStyle(node: ISymbioteNode): void;
141
261
  export declare function getExplicitStyle(node: ISymbioteNode): unknown;
262
+ /**
263
+ * The `[classStyle, explicitStyle]` pair the node currently PUBLISHES — the same value
264
+ * `commitClassStyle` writes, in the same order, so `flattenStyle` collapses it the way Fabric will.
265
+ *
266
+ * For a caller that wants the merged answer without a host: the pair reaches the payload as an op,
267
+ * and only a host holds ops. A test that has not installed one — `core/css-parser` reaches into the
268
+ * engine by relative path and depends on neither package — can read it here instead of reaching
269
+ * into `styleParts`, which is engine-owned and not a shape anything outside may bind to.
270
+ */
271
+ export declare function getPublishedStyle(node: ISymbioteNode): readonly unknown[];
142
272
  export declare function routeProp(node: ISymbioteNode, key: string, value: unknown): void;
143
273
  export declare function setText(node: ISymbioteNode, text: string): void;
144
- export declare function appendChild(parent: ISymbioteNode, child: ISymbioteNode): void;
145
- export declare function insertBefore(parent: ISymbioteNode, child: ISymbioteNode, beforeChild: ISymbioteNode): void;
146
- export declare function removeChild(parent: ISymbioteNode, child: ISymbioteNode): void;
147
- export interface ITreeCensus {
148
- nodes: number;
149
- anchors: number;
150
- emptyRawTexts: number;
151
- /** Nodes the commit walk actually reconciles: `nodes` minus everything it skips. */
152
- renderable: number;
153
- /** children.length of every parent holding at least one skipped child, widest first. */
154
- flattenWidths: number[];
155
- }
274
+ export declare function appendChild(requestedParent: ISymbioteNode, child: ISymbioteNode): void;
275
+ export declare function insertBefore(requestedParent: ISymbioteNode, child: ISymbioteNode, beforeChild: ISymbioteNode | null | undefined): void;
276
+ export declare function removeChild(requestedParent: ISymbioteNode, child: ISymbioteNode): void;
277
+ export type { ITreeCensus } from './tree-host';
278
+ /**
279
+ * A structural census of the tree the HOST holds — see `ITreeCensus` (tree-host.ts) for what each
280
+ * number is for and why the anchor count says more about the adapter than about the app.
281
+ *
282
+ * It walks nothing here: the walk needs `props.text` to tell an empty raw text from a real one, and
283
+ * a child list to measure a flatten width, and JS has neither. `undefined` from `treeHost()` means
284
+ * nothing is installed, and the empty census is the honest answer — every probe that reads this
285
+ * asserts against a mounted tree, so a zero from an uninstalled host cannot be mistaken for one from
286
+ * an empty one.
287
+ */
156
288
  export declare function censusRetainedTree(roots: readonly ISymbioteNode[]): ITreeCensus;
157
- export {};