@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
@@ -0,0 +1,513 @@
1
+ // THE CONTRACT. Native owns the tree; JS emits nothing but a command buffer.
2
+ //
3
+ // This file is the spec and `core/engine/cpp/SymbioteTree.cpp` must agree with it, for the reason a
4
+ // wire format is written as code at all: described in prose it gets reimplemented, written as code
5
+ // it gets exercised.
6
+ //
7
+ // ── WHAT CHANGED, AND WHY IT IS SMALLER THAN IT SOUNDS ───────────────────────────────────────────
8
+ //
9
+ // The adapter used to mutate a JS tree, `commit.ts` walked it to work out what changed, and the
10
+ // batch that crossed carried FABRIC operations — `createNode` / `cloneNode` / `appendChild`. The
11
+ // walk is
12
+ // 2 041 lines because it re-derives a diff.
13
+ //
14
+ // The framework already had that diff. A reconciler's entire job is knowing what changed, and every
15
+ // adapter hands us the answer call by call before we throw it away and recompute it. So the buffer
16
+ // carries the ADAPTER's alphabet instead, and the derivation disappears rather than moving:
17
+ //
18
+ // the walk over dirty subtrees gone — the framework names the nodes
19
+ // desired-vs-committed diffing gone — the framework names the operations
20
+ // IMirror gone — it was a JS re-implementation of `ShadowNode`, which
21
+ // already carries children and props (ShadowNode.h:133-134),
22
+ // duplicated only because reading it from JS costs a crossing
23
+ // the parent table, the edit log gone — native holds parent and children itself
24
+ // clone-on-write STAYS, in C++, along the paths the ops marked
25
+ //
26
+ // ── THE ALPHABET ─────────────────────────────────────────────────────────────────────────────────
27
+ //
28
+ // Nine operations. It is the mutation API `node.ts` already exports, minus the four that were never
29
+ // primitive:
30
+ //
31
+ // setNodeHidden / setNodePressed resolve a CSS class to a style IN JS and then call setProp.
32
+ // The class registry stays in JS, so what crosses is the result.
33
+ // setEventListener / a listener is a closure and cannot be `folly::dynamic`. It
34
+ // setBehaviorListener stays in a JS registry; what crosses is the boolean gate flag
35
+ // (`GATED_EVENT_PROPS`), which is an ordinary setProp.
36
+ //
37
+ // ── WHAT STAYS IN JS, AND IT IS NOT A TREE ───────────────────────────────────────────────────────
38
+ //
39
+ // the handle per node a bare `{}` carrying native's node on `NativeState`. No parent, no
40
+ // children, no order — an address. Its lifetime IS the node's lifetime,
41
+ // which is how `nativeFabricUIManager` and a browser both work.
42
+ // listeners Map<handle, Map<name, fn>>. Flat.
43
+ // the CSS class registry class -> style, resolved before any op is written. Native never sees a
44
+ // class name.
45
+ // host behaviors the press / text-input / switch machines. They read props back over
46
+ // JSI at GESTURE rate — ~10 reads per touch, not 19 009 per commit.
47
+ //
48
+ // ── WHY PROPS GO NATIVE, WHICH WAS DECIDED THE OTHER WAY FIRST ───────────────────────────────────
49
+ //
50
+ // The first draft of this contract kept prop VALUES in JS and gave native only the structure, on
51
+ // the reasoning that `RawProps` is lazy so handing Fabric a JS object costs nothing. That is wrong.
52
+ // `RawProps` is lazy only until `parse()`, and `ConcreteComponentDescriptor::cloneProps` calls it
53
+ // UNCONDITIONALLY. In `Mode::JSI` the preparse walks every key of every node —
54
+ // `getPropertyNames`, then per key `getValueAtIndex` + `getString` + `utf8()` (a `std::string`
55
+ // allocation) + `getProperty` — and `enableCppPropsIteratorSetter()` defaults to FALSE in 0.86, so
56
+ // that is the live path. `Mode::Dynamic` does the identical work with zero JSI.
57
+ //
58
+ // So a 1 000-row create already pays ~44 001 JSI property reads and as many string allocations
59
+ // INSIDE `createNode`. Props held natively as `folly::dynamic`, built incrementally as `setProp`
60
+ // ops arrive, are cheaper than what ships today, not more expensive.
61
+ /**
62
+ * The opcodes. These numbers ARE the contract: renumbering here without renumbering the C++ commits
63
+ * a different tree, silently, and no test in either language can see across the boundary.
64
+ *
65
+ * Stride is fixed so the ops array is addressable as memory rather than parsed. Operands are either
66
+ * a SLOT (an index into the batch's `handles` array — see below) or an index into a side table.
67
+ */
68
+ export const OP_STRIDE = 6;
69
+ export const OP_CREATE_ELEMENT = 0; // [slot, viewName, isText, tag, instanceHandle]
70
+ export const OP_CREATE_RAW_TEXT = 1; // [slot, text]
71
+ export const OP_CREATE_ANCHOR = 2; // [slot]
72
+ export const OP_APPEND_CHILD = 3; // [parent, child]
73
+ export const OP_INSERT_BEFORE = 4; // [parent, child, before]
74
+ export const OP_REMOVE_CHILD = 5; // [parent, child]
75
+ export const OP_SET_PROP = 6; // [slot, key, value] — value = NO_VALUE deletes the key
76
+ export const OP_SET_TEXT = 7; // [slot, text]
77
+ export const OP_COMMIT = 8; // [rootTag, surface]
78
+ export const OP_SET_COMPONENT = 9; // [slot, viewName]
79
+ /**
80
+ * The INTRINSIC TAG this node came from — `pressable`, not `RCTView`. `[slot, tag]`.
81
+ *
82
+ * Emitted only for a node a host behavior actually attached to, from `attachHostBehavior`, which is
83
+ * where the tag is already in hand and already matched. Every other node pays nothing.
84
+ *
85
+ * WHY THE TAG HAS TO CROSS AT ALL. A tag's platform props are resolved natively now, and the native
86
+ * side keys them off what it knows the node IS. For `<text-input>` the Fabric view name answers
87
+ * that by itself (`RCTSinglelineTextInputView` names nothing else); for `<pressable>` it does not —
88
+ * it commits as `RCTView`, byte-identical to a plain view. The tag is the only fact that separates
89
+ * them, and it is the same fact a browser keys user-agent behavior off.
90
+ */
91
+ export const OP_SET_TAG = 10; // [slot, tag]
92
+ /**
93
+ * An app callback appeared on, or disappeared from, an event name the behavior OWNS.
94
+ * `[slot, name, present]`. See `recordSetOwnedListener` for why the bit crosses and the closure
95
+ * does not.
96
+ */
97
+ export const OP_SET_OWNED_LISTENER = 11; // [slot, name, present]
98
+ /**
99
+ * A behavior's FEEDBACK state flipped — TouchableHighlight's underlay is showing, or stopped.
100
+ * `[slot, shown]`.
101
+ *
102
+ * The second bit to cross for the same reason the first did. `OP_SET_OWNED_LISTENER` carries whether
103
+ * the app wired a handler; this carries whether the control is currently giving feedback, which is
104
+ * the platform's own `:active` in everything but the timing. `setNodePressed`'s header already calls
105
+ * the press state "the engine-owned half of what `:active` is on the web", and this is its twin for
106
+ * the one control whose feedback does NOT track the press exactly: RN holds the underlay past
107
+ * release so a fast tap still flashes (`TouchableHighlight.js:270-293`).
108
+ *
109
+ * THE TIMER STAYS IN JS and that is the boundary, not an omission. WHEN the bit flips is Pressability
110
+ * plus a `delayPressOut` hold, running at gesture rate and calling back into app code
111
+ * (`onShowUnderlay` / `onHideUnderlay`). WHAT a showing underlay looks like — a background colour and
112
+ * a dimmed child, from two props no ViewConfig declares — is the platform's, and it is
113
+ * `foldTouchableHighlightUnderlay` now.
114
+ *
115
+ * A flip is a GESTURE-rate event rather than a per-render one: twice a tap, against a `payloadFold`
116
+ * that was charged on every commit the node was dirty in for the life of the screen.
117
+ */
118
+ export const OP_SET_UNDERLAY_SHOWN = 12; // [slot, shown]
119
+ /**
120
+ * A VOID node: commits nothing of its own AND does not hoist its children into Fabric either —
121
+ * unlike an anchor, which hoists. `[slot]`.
122
+ *
123
+ * `InputAccessoryView.js` renders `null` on Android: the whole component, children included,
124
+ * contributes nothing to the host tree. An anchor cannot express that — it exists precisely to hand
125
+ * its children up in its own place — so a node whose entire subtree must vanish from Fabric needs
126
+ * its own kind. Everything else about it (the retained JS tree, `appendChild`, adapter bookkeeping)
127
+ * is unaffected; only the commit walk treats it as contributing zero Fabric nodes, recursively.
128
+ */
129
+ export const OP_CREATE_VOID = 13; // [slot]
130
+ /**
131
+ * A `setProp` whose value slot is this DELETES the key.
132
+ *
133
+ * `undefined` cannot carry it: `null` is a legitimate Fabric prop value meaning "reset to the
134
+ * default", and the two must stay distinguishable — `cloneNodeWithNewProps` merges, so a removed
135
+ * key has to be sent as an explicit `null` while a key that was never there must not be sent at
136
+ * all. The JS `setProp` already collapses `undefined` to a delete before anything is encoded.
137
+ */
138
+ export const NO_VALUE = -1;
139
+ /**
140
+ * A node kind. Native needs it because two of the three never become a Fabric node the same way,
141
+ * and both rules are decided at INSERT or at child-set build — never by a walk:
142
+ *
143
+ * ELEMENT an ordinary view. Its Fabric view name is fixed at creation, with ONE exception:
144
+ * a text element inside another text element commits as `RCTVirtualText` instead of
145
+ * `RCTText`. Native resolves that when the node acquires a parent, because that is when
146
+ * it first knows the answer, and re-resolves it on a reparent.
147
+ * RAW_TEXT an `RCTRawText` leaf. Skipped from its parent's child set when its text is empty —
148
+ * an empty one would paint.
149
+ * ANCHOR a position marker the frameworks insert (`{#if}`, a fragment, a `{@render}` slot).
150
+ * It never becomes a Fabric node at all: native keeps it in ITS OWN structure so
151
+ * `insertBefore(parent, node, anchor)` resolves, and hoists its children into its
152
+ * parent's child list when building the child set. This is why our own store cannot BE
153
+ * the Fabric tree — there is no Fabric node to hold an anchor, which is the one thing
154
+ * `IMirror` was genuinely for.
155
+ *
156
+ * A SURFACE is an anchor too, and that is not a trick: an anchor is a node whose children belong to
157
+ * its parent's list, and a surface is a node whose children belong to the root's child set. Same
158
+ * shape, so `OP_COMMIT` names one and the native side needs no `rootTag -> node` map — which keeps
159
+ * the applier stateless, the property the whole lifetime design rests on.
160
+ */
161
+ export const KIND_ELEMENT = 0;
162
+ export const KIND_RAW_TEXT = 1;
163
+ export const KIND_ANCHOR = 2;
164
+ // ── THE RECORDER ─────────────────────────────────────────────────────────────────────────────────
165
+ //
166
+ // One buffer per process, drained by `takeBatch()` at commit. Not per surface: a node belongs to
167
+ // exactly one surface, so ops for another surface are inert until that surface commits — the same
168
+ // argument `edit-buffer.ts` made for not threading a surface through every mutation site, and the
169
+ // same conclusion.
170
+ // TYPED FROM THE START, because the alternative is to walk the whole thing once per commit.
171
+ //
172
+ // This was a `number[]` and `takeBatch` ended in `Int32Array.from(ops)`. Measured on a 1 000-row
173
+ // create through the work ledger: **210 042 slots**, every one of them converted element by element
174
+ // through the ITERATOR PROTOCOL on every commit. `new Int32Array(array)` is not the escape — it
175
+ // takes the same path, checked rather than assumed (F-55). The escape is not building an Array.
176
+ //
177
+ // Capacity DOUBLES and is never given back: a commit that needed 210 042 slots once will need them
178
+ // again, and re-growing from a small start would pay the same copies every commit for the memory of
179
+ // a single benchmark row list.
180
+ const INITIAL_OP_CAPACITY = 1_024;
181
+ let ops = new Int32Array(INITIAL_OP_CAPACITY);
182
+ let opCount = 0;
183
+ let strings = [];
184
+ let values = [];
185
+ let instanceHandles = [];
186
+ let handles = [];
187
+ // Interning matters more here than it looks: a 1 000-row create emits about a dozen distinct view
188
+ // names across 10 000 elements, and every prop KEY is drawn from a set of a few hundred.
189
+ const stringIds = new Map();
190
+ // Which batch the `slot` standing on a handle belongs to. Bumped by `takeBatch`, which is what
191
+ // invalidates every slot at once without walking the handles that hold them.
192
+ //
193
+ // It starts at 1 because a fresh node's `slotBatch` is 0, so an untouched handle can never match a
194
+ // live batch and needs no separate "is it in this batch" flag.
195
+ let batchId = 1;
196
+ // The same table for prop VALUES, and the reason it pays is the far side rather than this one: the
197
+ // host turns each entry into a `folly::dynamic` when the op is applied, so a style object reused
198
+ // across a thousand rows was a thousand conversions of one object. Measured on `build-release`,
199
+ // 12 005 `setProp` ops spent 20-29 ms converting inside a 35 ms `applyOps`.
200
+ const valueIds = new Map();
201
+ // Booleans skip that table entirely — see `internValue`. Two slots, reset with the batch alongside
202
+ // everything else the tables hold.
203
+ const NOT_INTERNED = -1;
204
+ let trueId = NOT_INTERNED;
205
+ let falseId = NOT_INTERNED;
206
+ function intern(text) {
207
+ const existing = stringIds.get(text);
208
+ if (existing !== undefined)
209
+ return existing;
210
+ strings.push(text);
211
+ stringIds.set(text, strings.length - 1);
212
+ return strings.length - 1;
213
+ }
214
+ /**
215
+ * The index the ops address this value by — deduplicated when it is worth deduplicating.
216
+ *
217
+ * Objects, functions and strings go through the `Map`: they are the ones that repeat (one
218
+ * `StyleSheet.create` object per screen, `ellipsizeMode: 'tail'` on every text node) and the ones
219
+ * whose conversion costs something.
220
+ *
221
+ * BOOLEANS ARE FOLDED WITHOUT THE MAP, because there are two of them. A dedicated slot each is a
222
+ * branch rather than a hash, so the argument that once excluded them — a lookup costs what the
223
+ * conversion costs — does not reach this shape. And the conversion was never the whole cost:
224
+ * `values` is a JSI array the host reads entry by entry, so a duplicate is a crossing whatever it
225
+ * holds. Measured on the create fixture, three adapters seed `allowFontScaling: true` at
226
+ * `createElement`, which alone wrote one entry per text node on the screen.
227
+ *
228
+ * NUMBERS STAY OUT. They would need the `Map`, whose keys compare by SameValueZero — that folds
229
+ * `-0` into `0` and `NaN` into itself, and a value this cheap to convert is not worth opening the
230
+ * question for.
231
+ *
232
+ * Identity, never structural equality: comparing deeply would make the buffer's cost depend on the
233
+ * size of what it is handed, which is the opposite of the point.
234
+ *
235
+ * Reusing an index is safe against MUTATION of the value between two ops, and not by luck — the host
236
+ * converts at apply time, after the batch has closed, so both ops already saw the object's final
237
+ * state whether they shared an index or not.
238
+ */
239
+ function internValue(value) {
240
+ if (value === true) {
241
+ if (trueId === NOT_INTERNED)
242
+ trueId = pushValue(value);
243
+ return trueId;
244
+ }
245
+ if (value === false) {
246
+ if (falseId === NOT_INTERNED)
247
+ falseId = pushValue(value);
248
+ return falseId;
249
+ }
250
+ const kind = typeof value;
251
+ const isWorthInterning = kind === 'string' ||
252
+ kind === 'function' ||
253
+ (kind === 'object' && value !== null);
254
+ if (!isWorthInterning)
255
+ return pushValue(value);
256
+ const existing = valueIds.get(value);
257
+ if (existing !== undefined)
258
+ return existing;
259
+ const id = pushValue(value);
260
+ valueIds.set(value, id);
261
+ return id;
262
+ }
263
+ function pushValue(value) {
264
+ values.push(value);
265
+ return values.length - 1;
266
+ }
267
+ /**
268
+ * The index the ops address this handle by, for the duration of THIS batch.
269
+ *
270
+ * Assigned on first mention rather than at creation, so a batch carries exactly the nodes it names.
271
+ * A handle created by an earlier batch arrives already owning its native node, which is what lets a
272
+ * clone source three commits old resolve with no bookkeeping on either side.
273
+ *
274
+ * THE SLOT LIVES ON THE HANDLE, not in a `Map` keyed by it, and that is a measured decision. This
275
+ * function runs once per handle OPERAND — every `setProp` names its node and every append names
276
+ * two, so a thousand-row create mentions handles about forty thousand times — and a `Map<object,
277
+ * number>` charges a hash for each one, plus a second for the `set` on a miss. Two fields on a shape
278
+ * the node already carries turn that into a compare.
279
+ *
280
+ * Measured on `build-release` by `mutation-api-fill-cost.itest.ts`, both arms in one sitting, three
281
+ * runs each: `createRawText` 0.70 -> 0.51 us, `appendChild` 0.65 -> 0.58, `recordSetProp` 0.34 ->
282
+ * 0.30, and the whole `fill` phase of a 10 001-node create 25.0 -> 23.2 ms. `createElement` barely
283
+ * moved (0.72 -> 0.70), which is the control: it does enough else that one lookup is noise in it.
284
+ *
285
+ * WHY AN EPOCH RATHER THAN CLEARING: a slot is meaningless outside its batch (see `IMutationBatch`),
286
+ * so every slot must die when the batch drains. Walking the handles to reset them would cost exactly
287
+ * what the `Map.clear` cost; bumping one counter invalidates all of them at once, and a handle that
288
+ * is never mentioned again is never touched.
289
+ */
290
+ function slotOf(handle) {
291
+ // The retained tree used to ABSORB a handle that was not a node: `children.indexOf(x)` returned
292
+ // -1 and the mutation was a silent no-op. A buffer cannot — the op is recorded, and the failure
293
+ // surfaces in the HOST, on a later op, in another batch, as a node whose create it appears never
294
+ // to have seen. Both of the real cases found this way were framework spellings the old tree had
295
+ // been swallowing for months: solid-js passing `null` as "insert at the end", and Angular asking
296
+ // to remove the SURFACE at teardown.
297
+ //
298
+ // Deliberately only "an object", not "one of OUR nodes": a handle is an identity to this file and
299
+ // nothing more, and the applier's own fixtures address bare `{}`. The wrong-KIND-of-object case
300
+ // (Angular handing over a `SymbioteSurface` at teardown) is caught one layer on, where the host
301
+ // fails to resolve it and can name the opcode.
302
+ if (typeof handle !== 'object' || handle === null) {
303
+ throw new Error(`symbiote engine: a mutation named ${String(handle)}, which is not a node. An adapter is ` +
304
+ `passing a framework sentinel straight through — an absent insert anchor is spelled by ` +
305
+ `calling appendChild.`);
306
+ }
307
+ if (handle.slotBatch === batchId)
308
+ return handle.slot;
309
+ handles.push(handle);
310
+ handle.slot = handles.length - 1;
311
+ handle.slotBatch = batchId;
312
+ return handle.slot;
313
+ }
314
+ // Has anything changed the TREE since the last commit drained?
315
+ //
316
+ // NOT the same question as `hasPendingOps()`, and the difference is the whole reason this exists:
317
+ // every structural READ calls `flushOps`, so a reconciler that navigates the tree it is building
318
+ // empties the buffer many times between commits. `hasPendingOps()` then answers "no" for a surface
319
+ // with a screen's worth of unpublished work. This survives the drain and is cleared only by a
320
+ // commit, which is what "is there anything to publish" actually means.
321
+ //
322
+ // `OP_COMMIT` is excluded deliberately: recording a commit is not a change to the tree, and counting
323
+ // it would make every commit look like it had work.
324
+ let changedSinceCommit = false;
325
+ function push(op, a = 0, b = 0, c = 0, d = 0, e = 0) {
326
+ if (op !== OP_COMMIT)
327
+ changedSinceCommit = true;
328
+ if (opCount + OP_STRIDE > ops.length) {
329
+ const grown = new Int32Array(ops.length * 2);
330
+ grown.set(ops);
331
+ ops = grown;
332
+ }
333
+ ops[opCount] = op;
334
+ ops[opCount + 1] = a;
335
+ ops[opCount + 2] = b;
336
+ ops[opCount + 3] = c;
337
+ ops[opCount + 4] = d;
338
+ ops[opCount + 5] = e;
339
+ opCount += OP_STRIDE;
340
+ }
341
+ /** Whether a commit would publish anything. See `changedSinceCommit`. */
342
+ export function hasChangedSinceCommit() {
343
+ return changedSinceCommit;
344
+ }
345
+ /**
346
+ * Called by `commitSurfaceOps` once it has drained. Separate from `takeBatch` because a READ drains
347
+ * too, and a read is not a commit — clearing there would make the next commit believe its work had
348
+ * already been published.
349
+ */
350
+ export function noteCommitDrained() {
351
+ changedSinceCommit = false;
352
+ }
353
+ /**
354
+ * The one dirtying route that writes no op: a behavior with a DERIVED payload asks the host to
355
+ * rebuild a node the buffer never named. Without this the commit that follows would look idle and
356
+ * be skipped, and the derived payload would sit unpublished until something unrelated changed.
357
+ */
358
+ export function noteHostSideChange() {
359
+ changedSinceCommit = true;
360
+ }
361
+ // Nodes whose PLACEMENT this batch has not published yet — created, or named as the child of a
362
+ // structural op.
363
+ //
364
+ // It exists so a read does not have to drain. `parentOf` is unconditional otherwise, and Angular's
365
+ // 1 000-row create measured 1 002 drains for 1 000 reads of which ONE asked about a node the pending
366
+ // batch had touched (`adapters/angular/src/read-fragmentation.probe.test.ts`).
367
+ //
368
+ // THE CLAIM THAT MAKES IT SOUND: a node's parent link changes only through an op that names that
369
+ // node AS THE CHILD. `before` on an insert is a position reference and moves nothing; a parent
370
+ // argument moves the parent's list, not the parent's own link. So a handle absent from here has the
371
+ // same parent in the host as it has here.
372
+ //
373
+ // Creation is in it because the host cannot answer about a node it has never been told exists, which
374
+ // is the other half — and the half a "was it re-parented" set alone would get wrong.
375
+ //
376
+ // NOT A TREE, and that is the line this has to stay on the right side of (`node.ts:2` — no parent,
377
+ // no children, no mirror). It answers about the BUFFER: is this node's placement unpublished. It is
378
+ // emptied by `takeBatch`, so it never outlives one batch and can never disagree with the host.
379
+ let placementPending = new Set();
380
+ /** Does the pending batch hold anything that could change what the host says this node's parent is? */
381
+ export function hasPendingPlacement(handle) {
382
+ return placementPending.has(handle);
383
+ }
384
+ export function recordCreateElement(handle, viewName, isText, instanceHandle) {
385
+ instanceHandles.push(instanceHandle);
386
+ placementPending.add(handle);
387
+ push(OP_CREATE_ELEMENT, slotOf(handle), intern(viewName), isText ? 1 : 0, instanceHandles.length - 1);
388
+ }
389
+ export function recordCreateRawText(handle, text) {
390
+ placementPending.add(handle);
391
+ push(OP_CREATE_RAW_TEXT, slotOf(handle), intern(text));
392
+ }
393
+ export function recordCreateAnchor(handle) {
394
+ placementPending.add(handle);
395
+ push(OP_CREATE_ANCHOR, slotOf(handle));
396
+ }
397
+ export function recordCreateVoid(handle) {
398
+ placementPending.add(handle);
399
+ push(OP_CREATE_VOID, slotOf(handle));
400
+ }
401
+ export function recordAppendChild(parent, child) {
402
+ placementPending.add(child);
403
+ push(OP_APPEND_CHILD, slotOf(parent), slotOf(child));
404
+ }
405
+ export function recordInsertBefore(parent, child, before) {
406
+ placementPending.add(child);
407
+ push(OP_INSERT_BEFORE, slotOf(parent), slotOf(child), slotOf(before));
408
+ }
409
+ export function recordRemoveChild(parent, child) {
410
+ placementPending.add(child);
411
+ push(OP_REMOVE_CHILD, slotOf(parent), slotOf(child));
412
+ }
413
+ /** `undefined` DELETES the key — the collapse `setProp` has always performed, spelled on the wire. */
414
+ export function recordSetProp(handle, key, value) {
415
+ if (value === undefined) {
416
+ push(OP_SET_PROP, slotOf(handle), intern(key), NO_VALUE);
417
+ return;
418
+ }
419
+ push(OP_SET_PROP, slotOf(handle), intern(key), internValue(value));
420
+ }
421
+ export function recordSetText(handle, text) {
422
+ push(OP_SET_TEXT, slotOf(handle), intern(text));
423
+ }
424
+ /**
425
+ * Change a node's Fabric view name after creation.
426
+ *
427
+ * It exists for exactly one thing and would not otherwise: `TextInput`'s `multiline` decides between
428
+ * `RCTSinglelineTextInputView` and `RCTMultilineTextInputView`, and an app can flip it. No prop write
429
+ * moves a node between native views, so the host has to RE-CREATE the node under the new name and
430
+ * re-parent its children — which is the same path a reparent already takes, and why this needs no
431
+ * new machinery on the far side beyond honouring the name.
432
+ *
433
+ * A JS-only workaround was the alternative and it is the one thing this design rules out: to know
434
+ * what to rebuild, JS would have to hold the tree.
435
+ */
436
+ export function recordSetComponent(handle, viewName) {
437
+ push(OP_SET_COMPONENT, slotOf(handle), intern(viewName));
438
+ }
439
+ export function recordSetTag(handle, tag) {
440
+ push(OP_SET_TAG, slotOf(handle), intern(tag));
441
+ }
442
+ /**
443
+ * Whether an app callback is currently wired to an event name the BEHAVIOR owns. `[slot, name,
444
+ * present]`, `present` being 1 or 0.
445
+ *
446
+ * The EXISTENCE, never the function. A name a behavior owns is diverted into a JS stash by
447
+ * `setEventListener` and never becomes a prop, so the payload builder sees no trace of it — and
448
+ * `focusable` on a touchable is `focusable !== false && onPress !== undefined && !disabled`
449
+ * (`TouchableOpacity.js:336-339`), two props and one thing only JS knew. This is the one bit that
450
+ * closes that gap.
451
+ *
452
+ * The browser is the argument rather than convenience: a UA computes focusability itself and CAN,
453
+ * because `addEventListener` is its own API — it knows which elements carry a click handler, while
454
+ * the handler's body stays the application's. Same split.
455
+ *
456
+ * Emitted on a FLIP only, from `setEventListener`, which already refuses to notify on listener
457
+ * identity because a framework hands a fresh closure nearly every render. So this is a mount-time
458
+ * op, not a per-render one — against the per-commit fold it replaces.
459
+ */
460
+ export function recordSetOwnedListener(handle, name, isPresent) {
461
+ push(OP_SET_OWNED_LISTENER, slotOf(handle), intern(name), isPresent ? 1 : 0);
462
+ }
463
+ /** See `OP_SET_UNDERLAY_SHOWN`. Emitted on a flip only, from the behavior that owns the timer. */
464
+ export function recordSetUnderlayShown(handle, shown) {
465
+ push(OP_SET_UNDERLAY_SHOWN, slotOf(handle), shown ? 1 : 0);
466
+ }
467
+ export function recordCommit(rootTag, surface) {
468
+ push(OP_COMMIT, rootTag, slotOf(surface));
469
+ }
470
+ /** Whether anything is pending. The commit path asks before paying for a drain. */
471
+ export function hasPendingOps() {
472
+ return opCount > 0;
473
+ }
474
+ /**
475
+ * Drain the buffer.
476
+ *
477
+ * Fresh arrays rather than reused ones: the batch outlives this call on the native path (`applyOps`
478
+ * reads `handles` while attaching state), and a recycled array would be mutated under it by the next
479
+ * mutation the adapter makes.
480
+ *
481
+ * `slice` for the ops and not `subarray` for exactly that reason — a subarray would share the
482
+ * backing store the very next `push` writes into. It is a typed-array copy rather than the
483
+ * element-by-element iterator walk this used to be, and the ops buffer itself is KEPT so its
484
+ * capacity survives the drain.
485
+ */
486
+ export function takeBatch() {
487
+ const batch = {
488
+ ops: ops.slice(0, opCount),
489
+ strings,
490
+ values,
491
+ instanceHandles,
492
+ handles,
493
+ };
494
+ opCount = 0;
495
+ strings = [];
496
+ values = [];
497
+ instanceHandles = [];
498
+ handles = [];
499
+ // A fresh Set rather than `.clear()`: the old one is handed to nobody, and clearing a set that
500
+ // held ten thousand handles on a benchmark create costs more than dropping it.
501
+ placementPending = new Set();
502
+ stringIds.clear();
503
+ // Every slot standing on a handle dies here, without touching one of them — see `slotOf`.
504
+ batchId += 1;
505
+ valueIds.clear();
506
+ trueId = NOT_INTERNED;
507
+ falseId = NOT_INTERNED;
508
+ return batch;
509
+ }
510
+ /** Test seam. Drops everything pending without applying it. */
511
+ export function resetMutationBuffer() {
512
+ takeBatch();
513
+ }