@symbiote-native/engine 0.5.0 → 1.1.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 (91) 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 -8
  10. package/build/accessibility-props.js +13 -16
  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/host-binding.d.ts +1 -1
  17. package/build/animated/host-binding.js +19 -4
  18. package/build/animated/index.d.ts +1 -1
  19. package/build/animated/mock.d.ts +1 -19
  20. package/build/animated/props.js +1 -1
  21. package/build/animated/rgba.js +16 -50
  22. package/build/events/index.js +88 -40
  23. package/build/fabric-props.d.ts +1 -1
  24. package/build/fabric-props.js +116 -184
  25. package/build/fabric.d.ts +9 -0
  26. package/build/fabric.js +32 -0
  27. package/build/host-access.d.ts +145 -0
  28. package/build/host-access.js +315 -0
  29. package/build/host-behavior.d.ts +84 -21
  30. package/build/host-behavior.js +236 -51
  31. package/build/image-source-write.d.ts +16 -0
  32. package/build/image-source-write.js +65 -0
  33. package/build/imperative.d.ts +49 -0
  34. package/build/imperative.js +258 -0
  35. package/build/index.d.ts +14 -7
  36. package/build/index.js +53 -10
  37. package/build/mutation-buffer.d.ts +238 -0
  38. package/build/mutation-buffer.js +513 -0
  39. package/build/native-engine.d.ts +185 -0
  40. package/build/native-engine.js +182 -0
  41. package/build/native-tree-host.d.ts +25 -0
  42. package/build/native-tree-host.js +68 -0
  43. package/build/node.d.ts +195 -58
  44. package/build/node.js +852 -383
  45. package/build/pan-responder/index.js +27 -52
  46. package/build/platform-color/index.d.ts +1 -1
  47. package/build/platform-color/index.js +11 -4
  48. package/build/process-background-image/index.js +30 -566
  49. package/build/process-background-longhands.d.ts +4 -0
  50. package/build/process-background-longhands.js +44 -0
  51. package/build/process-box-shadow/index.js +23 -187
  52. package/build/process-filter.js +27 -300
  53. package/build/process-transform/index.d.ts +1 -1
  54. package/build/process-transform/index.js +25 -107
  55. package/build/process-transform-origin/index.d.ts +1 -1
  56. package/build/process-transform-origin/index.js +29 -102
  57. package/build/registry.d.ts +36 -0
  58. package/build/registry.js +73 -0
  59. package/build/sound-manager/index.d.ts +3 -0
  60. package/build/sound-manager/index.js +36 -0
  61. package/build/structured-style.d.ts +10 -0
  62. package/build/structured-style.js +180 -0
  63. package/build/style-registry/index.d.ts +14 -0
  64. package/build/style-registry/index.js +60 -11
  65. package/build/surface.d.ts +31 -2
  66. package/build/surface.js +138 -56
  67. package/build/text-input-state.d.ts +1 -0
  68. package/build/text-input-state.js +17 -3
  69. package/build/tree-host.d.ts +322 -0
  70. package/build/tree-host.js +211 -0
  71. package/build/view-config.js +4 -4
  72. package/codegen-specs/NativeSymbioteEngine.ts +27 -0
  73. package/cpp/SymbioteDebug.cpp +51 -0
  74. package/cpp/SymbioteDebug.h +54 -0
  75. package/cpp/SymbioteEngineBindings.cpp +234 -0
  76. package/cpp/SymbioteEngineBindings.h +59 -0
  77. package/cpp/SymbioteFabricProps.cpp +2619 -0
  78. package/cpp/SymbioteFabricProps.h +223 -0
  79. package/cpp/SymbioteTree.cpp +2593 -0
  80. package/cpp/SymbioteTree.h +294 -0
  81. package/ios/SymbioteEngineModule.h +25 -0
  82. package/ios/SymbioteEngineModule.mm +44 -0
  83. package/package.json +31 -3
  84. package/react-native.config.cjs +23 -0
  85. package/symbiote-engine.podspec +42 -0
  86. package/build/animated/bezier.d.ts +0 -1
  87. package/build/animated/bezier.js +0 -102
  88. package/build/commit.d.ts +0 -49
  89. package/build/commit.js +0 -1058
  90. package/build/tags.d.ts +0 -2
  91. package/build/tags.js +0 -40
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
- import { type IPayloadFold } from './host-behavior';
3
+ import { type IHostBehavior, 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,19 +19,59 @@ 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;
49
+ /**
50
+ * The behavior that attached to this node, or `undefined` for the vast majority that have none.
51
+ *
52
+ * A field for the same reason `payloadFold` above it is one, and set on the same line: every
53
+ * reader is a per-node path at list scale — the teardown sweep touches every node of a removed
54
+ * subtree, and `routeProp`'s slot/owned-listener questions run per prop write. A `WeakMap` probe
55
+ * is the dearest way to ask a question whose answer is almost always "none".
56
+ *
57
+ * Owned by `host-behavior.ts`; `attachHostBehavior` is the only writer.
58
+ */
59
+ hostBehavior: IHostBehavior | undefined;
31
60
  styleParts: IClassStyleParts | undefined;
32
61
  childHost: ISymbioteNode | undefined;
33
62
  wrapper: ISymbioteNode | undefined;
63
+ mayHaveChildren: boolean;
64
+ /**
65
+ * Whether the teardown sweep has released this node and not seen it come back.
66
+ *
67
+ * Owned by `host-behavior.ts` — see `detachOne` / `reattachHostBehaviors`. A field rather than
68
+ * the `WeakSet` it was, because the sweep reads and writes it for EVERY node of a removed
69
+ * subtree (ten thousand on a thousand-row clear) and every insert reads it to decide whether to
70
+ * walk at all, which is the ~9 000-call path of a create.
71
+ */
72
+ isTornDown: boolean;
73
+ slot: number;
74
+ slotBatch: number;
34
75
  measure(callback: IMeasureOnSuccess): void;
35
76
  measureInWindow(callback: IMeasureInWindowOnSuccess): void;
36
77
  measureLayout(relativeToNativeNode: ISymbioteNode | number, onSuccess: IMeasureLayoutOnSuccess, onFail?: () => void): void;
@@ -47,68 +88,106 @@ export interface ISymbioteNode {
47
88
  }): void;
48
89
  flashScrollIndicators(): void;
49
90
  }
50
- export interface IMirror {
51
- handle: IFabricNode;
52
- tag: number;
53
- rootTag: IRootTag;
54
- props: IFabricProps;
55
- children: readonly ISymbioteNode[];
56
- viewName: string;
57
- parent: ISymbioteNode | undefined;
58
- owner: ISymbioteNode;
59
- }
60
91
  /**
61
- * The committed record for `node`, or `undefined` if it has never been committed - or if `node` is
62
- * not the raw retained node at all.
92
+ * Mint an element and record its creation.
63
93
  *
64
- * That second case is the reason this is a function rather than a bare `node.committed` read. The
65
- * engine identifies a node BY IDENTITY, and the classic way to break that is to hand the engine a
66
- * wrapper instead of the node: a Vue `reactive()`/deep-`ref()` Proxy around a host element is the
67
- * one that actually happens (see the vue-adapter-reactivity skill; `shallowRef` is the fix).
94
+ * The node object IS the handle: it is what the ops address, what the host attaches its native node
95
+ * to, and what Fabric hands back as an event target. Nothing else is allocated.
96
+ */
97
+ export declare function createElement(component: string, isText?: boolean, tag?: string): ISymbioteNode;
98
+ /**
99
+ * `tag` mirrors `createElement`'s, and a raw text needs it for the same reason an element does: the
100
+ * behavior registry is keyed by tag, so a node that does not hand one over cannot have a rule.
68
101
  *
69
- * The old WeakMap caught this for free - a Proxy is a different object, so `mirror.get(proxy)` missed
70
- * and every imperative API bailed with a clear "node not committed". A plain property read does NOT:
71
- * a Proxy forwards `proxy.committed` straight to the target and hands back a real record, whose
72
- * `handle` Vue would then deep-wrap on the way out. That handle is a JSI host object; a Proxy around
73
- * it reaches `cloneNodeWithNewProps` and fails somewhere deep in native, far from the cause.
102
+ * A raw text carrying a tag looks odd and is not. It has no props an app can write — its whole
103
+ * payload is `text` — but its CONTENT can still be a function of the platform rather than of the
104
+ * app: Button renders its title uppercased on Android (`Button.js:352-353`), which is a user-agent
105
+ * decision about a control, not anything the app asked for. That rule needs the node to be
106
+ * identifiable, and a tag is how this codebase identifies one.
74
107
  *
75
- * So the identity check that was implicit in the WeakMap is explicit here: a record written on the
76
- * raw node names it, and `record.owner !== node` means whatever we were handed is not that node.
77
- * One reference comparison, and the wrap now fails LOUDER than it used to rather than quieter.
108
+ * Defaulted to the raw-text component, so every existing caller is unchanged and pays the same
109
+ * lookup miss `createElement` already pays for a node nobody registered.
78
110
  */
79
- export declare function committedOf(node: ISymbioteNode): IMirror | undefined;
80
- export declare function createElement(component: string, isText?: boolean, tag?: string): ISymbioteNode;
81
- export declare function createRawText(text: string): ISymbioteNode;
111
+ export declare function createRawText(text: string, tag?: string): ISymbioteNode;
82
112
  export declare function isSymbioteNode(value: unknown): value is ISymbioteNode;
83
113
  export declare function debugNodeId(node: ISymbioteNode): number;
84
114
  export declare const ANCHOR_COMPONENT = "#anchor";
85
115
  export declare function createAnchor(): ISymbioteNode;
116
+ export declare const VOID_COMPONENT = "#void";
117
+ export declare function createVoid(): ISymbioteNode;
118
+ /**
119
+ * The sentinel a SURFACE's own root node carries, so `parentOf` can stop there.
120
+ *
121
+ * A top-level node must answer `undefined` for its parent, and adapters depend on the exact miss:
122
+ * Angular reads `null` as "defer, `<ng-content>` will place this" (answering the surface once
123
+ * mounted every FlatList cell at top level), while Vue and Solid spell `?? surface` at their call
124
+ * sites and would be handed an object that is not the `SymbioteSurface` they compare against. One
125
+ * component name, read in JS, keeps all three right without a second structure.
126
+ *
127
+ * It is a JS-side name only. What goes over the wire is `RCTView`, because this node is REAL.
128
+ */
129
+ export declare const SURFACE_COMPONENT = "#surface";
130
+ /**
131
+ * One persistent root view per surface, mirroring RN's own AppContainer — `renderApplication` wraps
132
+ * the app in `<View style={{flex:1}} pointerEvents="box-none">`.
133
+ *
134
+ * It is not decoration. Without `flex: 1` a non-flex root collapses to content height, and without
135
+ * `box-none` a touch landing outside the app's own children has no escape. Living here rather than
136
+ * in each adapter's `mount()` gives every framework a full-screen root for free and keeps layout in
137
+ * the shared layer (`<adapters_stay_thin>`).
138
+ *
139
+ * The surface therefore commits as ONE node rather than hoisting its children into the child set —
140
+ * which is why the host materializes the node `OP_COMMIT` names instead of walking its children. An
141
+ * anchor in that position still hoists, so the host handles both without a special case.
142
+ */
143
+ export declare function createSurfaceRoot(): ISymbioteNode;
86
144
  export declare function isAnchor(node: ISymbioteNode): boolean;
87
- export declare function isEmptyRawText(node: ISymbioteNode): boolean;
88
145
  /**
89
146
  * Change which Fabric view a node commits as, keeping the node's identity.
90
147
  *
91
- * The commit walk already re-creates a node whose `viewName` no longer matches its committed one —
92
- * that is how a `<Text>` moving in or out of another `<Text>` flips between RCTText and
93
- * RCTVirtualText (`commit.ts`, reason `view-kind`). This exposes the same door for a prop-driven
94
- * view choice, so `intrinsicWhen` is honoured on UPDATE and not only at create.
95
- *
96
148
  * The POLICY stays out of the engine: which prop decides, and which view it decides between, lives
97
149
  * in `HOST_PRIMITIVES` and is read by `resolveIntrinsicTag` in `@symbiote-native/components`. The
98
150
  * engine only knows how to swap the name — the same split every other spec-driven fold has here.
99
151
  *
100
152
  * A no-op when the name is unchanged, so a renderer may call it on every update without comparing
101
153
  * first.
154
+ *
155
+ * The JS field and the op BOTH move, and both are load-bearing. `node.component` is what the aria
156
+ * fold, the behavior registry and `fabricProps` key on; the op is what makes the host re-create the
157
+ * node under the new name, since no prop write moves a node between native views.
102
158
  */
103
159
  export declare function setNodeComponent(node: ISymbioteNode, component: string): void;
104
- export declare function markDirty(node: ISymbioteNode): void;
105
- export declare function markPropsDirty(node: ISymbioteNode): void;
106
- export declare function markStructureDirty(parent: ISymbioteNode): void;
107
160
  export declare function takePropStats(): {
108
161
  writes: number;
109
- noops: number;
110
162
  };
163
+ export declare function takePropKeyTally(): ReadonlyMap<string, number>;
111
164
  export declare function setProp(node: ISymbioteNode, key: string, value: unknown): void;
165
+ /**
166
+ * The one place a prop reaches the wire, and the only place that can keep a function off it.
167
+ *
168
+ * `setNativeProps` calls this rather than `recordSetProp` for that reason — it is the path that has
169
+ * no `routeProp` in front of it.
170
+ */
171
+ export declare function writeProp(node: ISymbioteNode, key: string, value: unknown): void;
172
+ /** What `propOf` consults before asking the host. `undefined` when nothing was stashed. */
173
+ export declare function functionPropOf(node: ISymbioteNode, key: string): unknown;
174
+ /**
175
+ * The same stash, whole — what `propsOf` layers over the host's answer.
176
+ *
177
+ * `undefined` rather than an empty Map for a node that stashed nothing, which is nearly every node:
178
+ * the caller then hands back the host's own object instead of copying it.
179
+ */
180
+ export declare function functionPropsOf(node: ISymbioteNode): ReadonlyMap<string, unknown> | undefined;
181
+ /**
182
+ * "Rebuild this node's payload — the fold reads state I just changed."
183
+ *
184
+ * A behavior whose payload is DERIVED has no prop to write: the sticky header's debounced
185
+ * translateY lives in its own runtime, not on the node, so nothing names the node and the host
186
+ * never marks it. This is the one route that says so directly.
187
+ *
188
+ * Dirtying is not publishing — pair it with `requestCommitFor` (imperative.ts).
189
+ */
190
+ export declare function markPropsDirty(node: ISymbioteNode): void;
112
191
  /**
113
192
  * Install a listener the BEHAVIOR owns, bypassing the ownership check.
114
193
  *
@@ -131,7 +210,30 @@ export interface IClassStyleParts {
131
210
  isPressed: boolean;
132
211
  activeStyle: unknown;
133
212
  activeStyleFromCallback: boolean;
213
+ published: readonly unknown[] | undefined;
134
214
  }
215
+ /**
216
+ * Is this rebuilt style the same style, key for key?
217
+ *
218
+ * A component body that writes its style inline hands over a FRESH object every render, equal to
219
+ * the one already standing — the commonest shape any app produces, and one `Object.is` cannot see.
220
+ * Without this the write crosses into the host, becomes a `folly::dynamic`, and is only THEN found
221
+ * to be unchanged. Measured on `build-release` (`no-op-rerender-cost.itest.ts`), 1 000 rows
222
+ * re-rendered with nothing changed: 9.7 ms against 0.3 ms for the same app with its style hoisted,
223
+ * and 5.2 ms of that was the conversion. The cheapest place to refuse a write is the earliest place
224
+ * that can see it is a no-op.
225
+ *
226
+ * SHALLOW AND CONSERVATIVE, both deliberately. A nested value (a transform list, a shadow, a style
227
+ * array) reports "not the same" rather than being compared deeply, because a deep compare makes this
228
+ * guard cost the size of the style — which is the cost it exists to avoid. Those keep crossing and
229
+ * the host's own `diffProps` refuses them exactly as before, so being wrong here is slow, never
230
+ * incorrect.
231
+ *
232
+ * `undefined` on either side also reports "not the same", which is what lets the key COUNT stand in
233
+ * for a key-set comparison: equal counts plus every key of `next` matching a defined value in
234
+ * `standing` cannot leave a key unaccounted for.
235
+ */
236
+ export declare function isSameShallowStyle(next: unknown, standing: unknown): boolean;
135
237
  /**
136
238
  * Stop a node painting without unmounting it, or let it paint again.
137
239
  *
@@ -154,20 +256,55 @@ export declare function setNodeHidden(node: ISymbioteNode, hidden: boolean): voi
154
256
  * and the node is never dirtied.
155
257
  */
156
258
  export declare function setNodePressed(node: ISymbioteNode, pressed: boolean): void;
259
+ /**
260
+ * Tell the host a behavior's FEEDBACK is showing — TouchableHighlight's underlay, and only that.
261
+ *
262
+ * The sibling of `setNodePressed` and deliberately NOT the same bit. Press state drives `:active`
263
+ * class resolution, which happens in JS because a class name resolves against a JS registry; this
264
+ * drives a rule that lives in C++ (`foldTouchableHighlightUnderlay`), so it crosses as one op rather
265
+ * than resolving to a style here. And the two are genuinely different facts: RN holds the underlay
266
+ * past release so a fast tap still flashes, so `shown` LAGS `pressed` by a `delayPressOut` timer.
267
+ *
268
+ * No style is computed on this side at all, which is the whole point — the two props the rule reads
269
+ * (`underlayColor`, `activeOpacity`) are ones the engine already strips from the payload, so the
270
+ * values and their defaults live in one place instead of being erased in C++ and reached around for
271
+ * in JS.
272
+ */
273
+ export declare function setNodeUnderlayShown(node: ISymbioteNode, shown: boolean): void;
274
+ /**
275
+ * Forget what was last published, so the next `pushClassStyle` cannot be turned away.
276
+ *
277
+ * The one caller is `setNativeProps` (imperative.ts), which writes the style slot past this file —
278
+ * see the note on `IClassStyleParts.published` for why the restore path depends on this. A no-op for
279
+ * a node nobody has styled, which is why it is not `stylePartsOf(node).published = undefined`: that
280
+ * would allocate the parts on a node that has none.
281
+ */
282
+ export declare function clearPublishedStyle(node: ISymbioteNode): void;
157
283
  export declare function getExplicitStyle(node: ISymbioteNode): unknown;
284
+ /**
285
+ * The `[classStyle, explicitStyle]` pair the node currently PUBLISHES — the same value
286
+ * `commitClassStyle` writes, in the same order, so `flattenStyle` collapses it the way Fabric will.
287
+ *
288
+ * For a caller that wants the merged answer without a host: the pair reaches the payload as an op,
289
+ * and only a host holds ops. A test that has not installed one — `core/css-parser` reaches into the
290
+ * engine by relative path and depends on neither package — can read it here instead of reaching
291
+ * into `styleParts`, which is engine-owned and not a shape anything outside may bind to.
292
+ */
293
+ export declare function getPublishedStyle(node: ISymbioteNode): readonly unknown[];
158
294
  export declare function routeProp(node: ISymbioteNode, key: string, value: unknown): void;
159
295
  export declare function setText(node: ISymbioteNode, text: string): void;
160
296
  export declare function appendChild(requestedParent: ISymbioteNode, child: ISymbioteNode): void;
161
- export declare function insertBefore(requestedParent: ISymbioteNode, child: ISymbioteNode, beforeChild: ISymbioteNode | null): void;
297
+ export declare function insertBefore(requestedParent: ISymbioteNode, child: ISymbioteNode, beforeChild: ISymbioteNode | null | undefined): void;
162
298
  export declare function removeChild(requestedParent: ISymbioteNode, child: ISymbioteNode): void;
163
- export interface ITreeCensus {
164
- nodes: number;
165
- anchors: number;
166
- emptyRawTexts: number;
167
- /** Nodes the commit walk actually reconciles: `nodes` minus everything it skips. */
168
- renderable: number;
169
- /** children.length of every parent holding at least one skipped child, widest first. */
170
- flattenWidths: number[];
171
- }
299
+ export type { ITreeCensus } from './tree-host';
300
+ /**
301
+ * A structural census of the tree the HOST holds — see `ITreeCensus` (tree-host.ts) for what each
302
+ * number is for and why the anchor count says more about the adapter than about the app.
303
+ *
304
+ * It walks nothing here: the walk needs `props.text` to tell an empty raw text from a real one, and
305
+ * a child list to measure a flatten width, and JS has neither. `undefined` from `treeHost()` means
306
+ * nothing is installed, and the empty census is the honest answer — every probe that reads this
307
+ * asserts against a mounted tree, so a zero from an uninstalled host cannot be mistaken for one from
308
+ * an empty one.
309
+ */
172
310
  export declare function censusRetainedTree(roots: readonly ISymbioteNode[]): ITreeCensus;
173
- export {};