@tamagui/native-registry 3.0.3 → 3.0.4

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.
@@ -2,6 +2,8 @@
2
2
 
3
3
  #include <jsi/JSIDynamic.h>
4
4
  #include <react/renderer/components/root/RootShadowNode.h>
5
+ #include <react/renderer/components/text/ParagraphShadowNode.h>
6
+ #include <react/renderer/textlayoutmanager/TextMeasureCache.h>
5
7
  #include <react/renderer/core/PropsParserContext.h>
6
8
  #include <react/renderer/core/RawProps.h>
7
9
  #include <react/renderer/core/ShadowNode.h>
@@ -12,7 +14,9 @@
12
14
  #include <react/renderer/uimanager/UIManagerBinding.h>
13
15
  #include <react/renderer/uimanager/primitives.h>
14
16
 
17
+ #include <cstring>
15
18
  #include <unordered_set>
19
+ #include <vector>
16
20
 
17
21
  namespace margelo::nitro::tamagui::registry {
18
22
 
@@ -22,12 +26,13 @@ using margelo::nitro::Prototype;
22
26
  void HybridTamaguiRegistry::loadHybridMethods() {
23
27
  HybridTamaguiRegistrySpec::loadHybridMethods();
24
28
  registerHybrids(this, [](Prototype& prototype) {
25
- prototype.registerRawHybridMethod("link", 3, &HybridTamaguiRegistry::link);
29
+ prototype.registerRawHybridMethod("link", 4, &HybridTamaguiRegistry::link);
30
+ prototype.registerRawHybridMethod("linkTables", 4,
31
+ &HybridTamaguiRegistry::linkTables);
26
32
  prototype.registerRawHybridMethod("applyViewStates", 1,
27
33
  &HybridTamaguiRegistry::applyViewStates);
28
- prototype.registerRawHybridMethod(
29
- "updateViewStateTables", 1,
30
- &HybridTamaguiRegistry::updateViewStateTables);
34
+ prototype.registerRawHybridMethod("setTableState", 3,
35
+ &HybridTamaguiRegistry::setTableState);
31
36
  prototype.registerRawHybridMethod("getViewState", 1,
32
37
  &HybridTamaguiRegistry::getViewState);
33
38
  });
@@ -36,7 +41,7 @@ void HybridTamaguiRegistry::loadHybridMethods() {
36
41
  jsi::Value HybridTamaguiRegistry::link(jsi::Runtime& rt, const jsi::Value&,
37
42
  const jsi::Value* args, size_t count) {
38
43
  if (count < 3) {
39
- throw jsi::JSError(rt, "link(shadowNode, slots, scopeId) requires 3 arguments");
44
+ throw jsi::JSError(rt, "link(shadowNode, slots | tableId, scopeId) requires 3 arguments");
40
45
  }
41
46
  if (!args[0].isObject()) return jsi::Value::undefined();
42
47
  auto obj = args[0].asObject(rt);
@@ -46,17 +51,90 @@ jsi::Value HybridTamaguiRegistry::link(jsi::Runtime& rt, const jsi::Value&,
46
51
 
47
52
  runtime_ = &rt;
48
53
 
49
- auto slots = jsi::dynamicFromValue(rt, args[1]);
50
54
  const double id = nextId_++;
51
- views_[id] = LinkedView{
52
- wrapper->shadowNode,
53
- args[2].asString(rt).utf8(rt),
54
- slots.getDefault("base", nullptr),
55
- slots.getDefault("state", nullptr),
56
- };
55
+ auto& view = views_[id];
56
+ view.node = wrapper->shadowNode;
57
+ view.scopeId = args[2].asString(rt).utf8(rt);
58
+ if (args[1].isNumber()) {
59
+ auto& table = tables_[args[1].asNumber()];
60
+ if (!table) table = std::make_shared<Table>();
61
+ view.table = table;
62
+ } else {
63
+ auto slots = jsi::dynamicFromValue(rt, args[1]);
64
+ view.baseProps = slots.getDefault("base", nullptr);
65
+ view.stateProps = slots.getDefault("state", nullptr);
66
+ }
67
+
68
+ if (count > 3 && args[3].isBool() && args[3].getBool()) {
69
+ if (const auto* name = viewStateName(view)) {
70
+ LeafUpdates updates;
71
+ buildViewUpdate(view, *name, updates);
72
+ commitUpdates(rt, updates);
73
+ }
74
+ }
57
75
  return jsi::Value(id);
58
76
  }
59
77
 
78
+ // links many views to shared tables in one call: entry i is shadow node
79
+ // nodes[i] on table tableIds[i] under scope scopes[scopeIndexes[i]]. ids are
80
+ // issued in order starting at the returned id; an entry without a live shadow
81
+ // node still consumes its id and stays unlinked
82
+ jsi::Value HybridTamaguiRegistry::linkTables(jsi::Runtime& rt, const jsi::Value&,
83
+ const jsi::Value* args, size_t count) {
84
+ if (count < 4 || !args[0].isObject() || !args[1].isObject() ||
85
+ !args[2].isObject() || !args[3].isObject()) {
86
+ throw jsi::JSError(
87
+ rt, "linkTables(nodes, tableIds, scopeIndexes, scopes) requires 4 arrays");
88
+ }
89
+ runtime_ = &rt;
90
+ auto nodes = args[0].asObject(rt).asArray(rt);
91
+ auto tableIds = args[1].asObject(rt).asArray(rt);
92
+ auto scopeIndexes = args[2].asObject(rt).asArray(rt);
93
+ auto scopeValues = args[3].asObject(rt).asArray(rt);
94
+
95
+ std::vector<std::string> scopes(scopeValues.size(rt));
96
+ for (size_t i = 0; i < scopes.size(); i++) {
97
+ scopes[i] = scopeValues.getValueAtIndex(rt, i).asString(rt).utf8(rt);
98
+ }
99
+
100
+ const size_t size = nodes.size(rt);
101
+ const double firstId = nextId_;
102
+ nextId_ += static_cast<double>(size);
103
+
104
+ std::shared_ptr<Table> table;
105
+ double tableId = -1;
106
+ for (size_t i = 0; i < size; i++) {
107
+ auto value = nodes.getValueAtIndex(rt, i);
108
+ if (!value.isObject()) continue;
109
+ auto obj = value.asObject(rt);
110
+ if (!obj.hasNativeState(rt)) continue;
111
+ auto wrapper = obj.getNativeState<ShadowNodeWrapper>(rt);
112
+ if (!wrapper || !wrapper->shadowNode) continue;
113
+
114
+ // consecutive entries usually share a table
115
+ const double nextTableId = tableIds.getValueAtIndex(rt, i).asNumber();
116
+ if (!table || nextTableId != tableId) {
117
+ tableId = nextTableId;
118
+ auto& entry = tables_[tableId];
119
+ if (!entry) entry = std::make_shared<Table>();
120
+ table = entry;
121
+ }
122
+
123
+ auto& view = views_[firstId + static_cast<double>(i)];
124
+ view.node = wrapper->shadowNode;
125
+ view.scopeId = scopes.at(
126
+ static_cast<size_t>(scopeIndexes.getValueAtIndex(rt, i).asNumber()));
127
+ view.table = table;
128
+ }
129
+ return jsi::Value(firstId);
130
+ }
131
+
132
+ const std::string* HybridTamaguiRegistry::viewStateName(
133
+ const LinkedView& view) const {
134
+ return view.activeState.empty() ? activeStateName(view.scopeId)
135
+ : &view.activeState;
136
+ }
137
+
60
138
  void HybridTamaguiRegistry::unlink(double id) {
61
139
  views_.erase(id);
62
140
  }
@@ -123,10 +201,32 @@ using LeafUpdates = HybridTamaguiRegistry::LeafUpdates;
123
201
  using AffectedNodes =
124
202
  std::unordered_map<const ShadowNodeFamily*, std::unordered_set<size_t>>;
125
203
 
204
+ // records, for every ancestor of an updated view, which children lead to one.
205
+ // returns whether `node` is or contains an updated view
206
+ bool collectAffected(const ShadowNode& node, const LeafUpdates& updates,
207
+ AffectedNodes& affected) {
208
+ bool hit = updates.contains(&node.getFamily());
209
+ const auto& children = node.getChildren();
210
+ for (size_t index = 0; index < children.size(); index++) {
211
+ if (collectAffected(*children[index], updates, affected)) {
212
+ affected[&node.getFamily()].insert(index);
213
+ hit = true;
214
+ }
215
+ }
216
+ return hit;
217
+ }
218
+
126
219
  AffectedNodes findAffectedNodes(const RootShadowNode& root,
127
220
  const LeafUpdates& updates) {
128
221
  AffectedNodes affected;
129
- for (const auto& [family, props] : updates) {
222
+ // getAncestors scans each parent's children to find the child, so many
223
+ // updates under one wide parent (a long list) cost updates x siblings; past
224
+ // a few updates one walk of the tree is cheaper
225
+ if (updates.size() > 32) {
226
+ collectAffected(root, updates, affected);
227
+ return affected;
228
+ }
229
+ for (const auto& [family, update] : updates) {
130
230
  // families of unmounted views have no ancestors in this root: skipped
131
231
  for (const auto& [parentNode, childIndex] : family->getAncestors(root)) {
132
232
  affected[&parentNode.get().getFamily()].insert(childIndex);
@@ -135,61 +235,165 @@ AffectedNodes findAffectedNodes(const RootShadowNode& root,
135
235
  return affected;
136
236
  }
137
237
 
238
+ // RN dirties the measurement of every paragraph cloned with props, and the
239
+ // dirty flag relays out each ancestor: one text color in each row of a long
240
+ // list relays out the whole list. for a paragraph of plain strings, whose
241
+ // fragments take exactly its own text attributes, rebuild those attributes
242
+ // the way ParagraphShadowNode::getContent does; when RN's own measure cache
243
+ // would call them layout-equivalent (colors, decorations, shadows, opacity),
244
+ // restyle the laid-out text directly and build the node without the clone
245
+ // step that dirties it. null when the update needs a real layout
246
+ std::shared_ptr<ShadowNode> restyleParagraph(const ShadowNode& node,
247
+ const Props::Shared& props) {
248
+ if (std::strcmp(node.getComponentName(), ParagraphComponentName) != 0) {
249
+ return nullptr;
250
+ }
251
+ for (const auto& child : node.getChildren()) {
252
+ if (std::strcmp(child->getComponentName(), "RawText") != 0) return nullptr;
253
+ }
254
+ const auto& nextProps = static_cast<const ParagraphProps&>(*props);
255
+ const auto& paragraph = static_cast<const ParagraphShadowNode&>(node);
256
+ if (!(nextProps.paragraphAttributes ==
257
+ paragraph.getConcreteProps().paragraphAttributes)) {
258
+ return nullptr;
259
+ }
260
+
261
+ auto data = paragraph.getStateData();
262
+ auto layoutManager = data.layoutManager.lock();
263
+ if (!layoutManager || data.attributedString.getFragments().empty()) {
264
+ return nullptr;
265
+ }
266
+ const auto& laidOut = data.attributedString.getBaseTextAttributes();
267
+ auto attributes = TextAttributes::defaultTextAttributes();
268
+ attributes.fontSizeMultiplier = laidOut.fontSizeMultiplier;
269
+ attributes.apply(nextProps.textAttributes);
270
+ attributes.layoutDirection = laidOut.layoutDirection;
271
+ if (!areTextAttributesEquivalentLayoutWise(laidOut, attributes)) {
272
+ return nullptr;
273
+ }
274
+ for (auto& fragment : data.attributedString.getFragments()) {
275
+ fragment.textAttributes = attributes;
276
+ }
277
+ data.attributedString.setBaseTextAttributes(attributes);
278
+
279
+ const ShadowNodeFragment fragment{
280
+ .props = props,
281
+ .children = ShadowNodeFragment::childrenPlaceholder(),
282
+ .state = node.getComponentDescriptor().createState(
283
+ node.getFamily(), std::make_shared<const ParagraphState>(std::move(data))),
284
+ };
285
+ // what ConcreteComponentDescriptor::cloneShadowNode does, minus
286
+ // completeClone (the dirtying step); adopt only sets the layout manager.
287
+ // a yoga style change still dirties the node through its constructor
288
+ auto clone = std::make_shared<ParagraphShadowNode>(node, fragment);
289
+ clone->setTextLayoutManager(std::move(layoutManager));
290
+ node.transferRuntimeShadowNodeReference(clone, fragment);
291
+ return clone;
292
+ }
293
+
138
294
  std::shared_ptr<ShadowNode> cloneSubtree(const ShadowNode& node,
139
295
  const LeafUpdates& updates,
140
296
  const AffectedNodes& affected) {
141
297
  const auto* family = &node.getFamily();
142
- auto children = node.getChildren();
143
298
 
299
+ // only an ancestor of an updated view gets new children: RN dirties the
300
+ // layout of any measurable node (text) cloned with a children fragment,
301
+ // so passing them to the updated views themselves would re-measure every
302
+ // text and re-run layout for a color change
303
+ auto children = ShadowNodeFragment::childrenPlaceholder();
144
304
  if (auto it = affected.find(family); it != affected.end()) {
305
+ auto cloned = node.getChildren();
145
306
  for (auto index : it->second) {
146
- children[index] = cloneSubtree(*children[index], updates, affected);
307
+ cloned[index] = cloneSubtree(*cloned[index], updates, affected);
147
308
  }
309
+ children = std::make_shared<std::vector<std::shared_ptr<const ShadowNode>>>(
310
+ std::move(cloned));
148
311
  }
149
312
 
150
313
  Props::Shared newProps = ShadowNodeFragment::propsPlaceholder();
151
314
  if (auto it = updates.find(family); it != updates.end()) {
152
- PropsParserContext ctx{node.getSurfaceId(), *node.getContextContainer()};
153
- newProps = node.getComponentDescriptor().cloneProps(ctx, node.getProps(),
154
- RawProps(it->second));
315
+ const auto& update = it->second;
316
+ const auto parse = [&] {
317
+ PropsParserContext ctx{node.getSurfaceId(), *node.getContextContainer()};
318
+ return node.getComponentDescriptor().cloneProps(ctx, node.getProps(),
319
+ RawProps(update.props()));
320
+ };
321
+ if (auto* warm = update.warm) {
322
+ if (warm->last != node.getProps() || warm->revision != update.revision) {
323
+ warm->byState.clear();
324
+ warm->revision = update.revision;
325
+ }
326
+ auto& props = warm->byState[*update.state];
327
+ if (!props) props = parse();
328
+ warm->last = props;
329
+ newProps = props;
330
+ } else {
331
+ newProps = parse();
332
+ }
333
+ if (auto restyled = restyleParagraph(node, newProps)) {
334
+ return restyled;
335
+ }
155
336
  }
156
337
 
157
338
  return node.clone({
158
339
  .props = newProps,
159
- .children = std::make_shared<std::vector<std::shared_ptr<const ShadowNode>>>(
160
- std::move(children)),
161
- .state = node.getState(),
340
+ .children = children,
341
+ .state = ShadowNodeFragment::statePlaceholder(),
162
342
  });
163
343
  }
164
344
 
165
345
  } // namespace
166
346
 
167
- // merge base + the named state's props for one view into `out`, syncing the
168
- // family's native props so React re-commits of unchanged children merge our
169
- // values instead of resurrecting a stale absorbed node (phase 0: the JS
170
- // mirror alone provably does not cover this). returns false on a state miss.
347
+ // merge base + the named state's props for one view into `out`. a runtime
348
+ // view also syncs its family's native props so React re-commits of unchanged
349
+ // children merge our values instead of resurrecting a stale absorbed node.
350
+ // returns false on a state miss.
171
351
  bool HybridTamaguiRegistry::buildViewUpdate(LinkedView& view,
172
352
  const std::string& stateName,
173
353
  LeafUpdates& out) {
174
- folly::dynamic props =
175
- view.baseProps.isObject() ? view.baseProps : folly::dynamic::object();
176
-
177
- if (view.stateProps.isObject()) {
178
- auto it = view.stateProps.find(stateName);
179
- if (it == view.stateProps.items().end()) {
354
+ LeafUpdate update;
355
+ if (view.table) {
356
+ // a table view commits only its state keys, React rendered the rest
357
+ auto it = view.table->states.find(stateName);
358
+ if (it == view.table->states.items().end()) {
180
359
  missCount_ += 1;
181
360
  return false;
182
361
  }
183
- if (props.empty()) {
184
- props = it->second;
185
- } else {
186
- props.update(it->second);
362
+ update.shared = &it->second;
363
+ update.warm = &view.warm;
364
+ update.state = &it->first.getString();
365
+ update.revision = view.table->revision;
366
+ } else {
367
+ auto& props = update.owned;
368
+ props = view.baseProps.isObject() ? view.baseProps : folly::dynamic::object();
369
+ if (view.stateProps.isObject()) {
370
+ auto it = view.stateProps.find(stateName);
371
+ if (it == view.stateProps.items().end()) {
372
+ missCount_ += 1;
373
+ return false;
374
+ }
375
+ if (props.empty()) {
376
+ props = it->second;
377
+ } else {
378
+ props.update(it->second);
379
+ }
380
+ } else if (props.empty()) {
381
+ return false;
187
382
  }
188
- } else if (props.empty()) {
189
- return false;
190
383
  }
384
+ const auto& props = update.props();
191
385
 
192
386
  const auto& family = view.node->getFamily();
387
+ // the retained node keeps its family alive; unmounted views resolve to
388
+ // no ancestors during the transaction and are skipped safely
389
+ if (view.table) {
390
+ // React renders a table view with its scope's current state and the
391
+ // engine's clones take over React's handles, so nothing goes stale. a
392
+ // sticky copy would make RN re-parse the view's props on every clone
393
+ out[&family] = std::move(update);
394
+ return true;
395
+ }
396
+ std::lock_guard<std::mutex> lock(family.nativePropsMutex);
193
397
  if (family.nativeProps_DEPRECATED) {
194
398
  family.nativeProps_DEPRECATED->update(props);
195
399
  } else {
@@ -205,9 +409,7 @@ bool HybridTamaguiRegistry::buildViewUpdate(LinkedView& view,
205
409
  if (item.second.isNull()) sticky.erase(item.first.asString());
206
410
  }
207
411
 
208
- // the retained node keeps its family alive; unmounted views resolve to
209
- // no ancestors during the transaction and are skipped safely
210
- out[&family] = std::move(props);
412
+ out[&family] = std::move(update);
211
413
  return true;
212
414
  }
213
415
 
@@ -218,6 +420,12 @@ void HybridTamaguiRegistry::commitUpdates(jsi::Runtime& rt,
218
420
  auto binding = UIManagerBinding::getBinding(rt);
219
421
  if (!binding) return;
220
422
 
423
+ // the clones this commit makes take over React's handles to the nodes they
424
+ // replace (RN's per-thread switch for updateRuntimeShadowNodeReferences), so
425
+ // React's next commit builds on them instead of its own stale nodes
426
+ const bool updatedReferences =
427
+ ShadowNode::getUseRuntimeShadowNodeReferenceUpdateOnThread();
428
+ ShadowNode::setUseRuntimeShadowNodeReferenceUpdateOnThread(true);
221
429
  binding->getUIManager().getShadowTreeRegistry().enumerate(
222
430
  [this, &updates](const ShadowTree& tree, bool& /*stop*/) {
223
431
  auto status = tree.commit(
@@ -233,6 +441,7 @@ void HybridTamaguiRegistry::commitUpdates(jsi::Runtime& rt,
233
441
  commitCount_ += 1;
234
442
  }
235
443
  });
444
+ ShadowNode::setUseRuntimeShadowNodeReferenceUpdateOnThread(updatedReferences);
236
445
  }
237
446
 
238
447
  void HybridTamaguiRegistry::applyUpdates(jsi::Runtime& rt,
@@ -246,9 +455,9 @@ void HybridTamaguiRegistry::applyUpdates(jsi::Runtime& rt,
246
455
  // broadcasts must not fight their controller
247
456
  if (!view.activeState.empty()) continue;
248
457
 
249
- const auto* name =
250
- view.stateProps.isObject() ? activeStateName(view.scopeId) : nullptr;
251
- if (view.stateProps.isObject() && !name) continue;
458
+ const bool hasStates = view.table || view.stateProps.isObject();
459
+ const auto* name = hasStates ? activeStateName(view.scopeId) : nullptr;
460
+ if (hasStates && !name) continue;
252
461
 
253
462
  buildViewUpdate(view, name ? *name : "", updates);
254
463
  }
@@ -286,6 +495,11 @@ jsi::Value HybridTamaguiRegistry::applyViewStates(jsi::Runtime& rt,
286
495
 
287
496
  const std::string stateName = stateIt->second.asString();
288
497
  auto props = entry.getDefault("props", nullptr);
498
+ if (props.isObject() && view.table) {
499
+ // a runtime controller takes the view over: it owns its tables now
500
+ view.stateProps = view.table->states;
501
+ view.table = nullptr;
502
+ }
289
503
  if (props.isObject()) {
290
504
  if (!view.stateProps.isObject()) {
291
505
  view.stateProps = folly::dynamic::object();
@@ -301,48 +515,28 @@ jsi::Value HybridTamaguiRegistry::applyViewStates(jsi::Runtime& rt,
301
515
  return jsi::Value::undefined();
302
516
  }
303
517
 
304
- jsi::Value HybridTamaguiRegistry::updateViewStateTables(
305
- jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args,
306
- size_t count) {
307
- if (count < 1 || !args[0].isObject()) {
308
- throw jsi::JSError(rt, "updateViewStateTables(entries) requires an array");
518
+ jsi::Value HybridTamaguiRegistry::setTableState(jsi::Runtime& rt,
519
+ const jsi::Value&,
520
+ const jsi::Value* args,
521
+ size_t count) {
522
+ if (count < 3 || !args[0].isNumber() || !args[1].isString() ||
523
+ !args[2].isObject()) {
524
+ throw jsi::JSError(rt, "setTableState(tableId, state, props) requires 3 arguments");
309
525
  }
310
526
  runtime_ = &rt;
311
527
 
312
- auto entries = jsi::dynamicFromValue(rt, args[0]);
313
- if (!entries.isArray()) {
314
- throw jsi::JSError(rt, "updateViewStateTables(entries) requires an array");
315
- }
528
+ auto& table = tables_[args[0].asNumber()];
529
+ if (!table) table = std::make_shared<Table>();
530
+ const std::string stateName = args[1].asString(rt).utf8(rt);
531
+ table->states[stateName] = jsi::dynamicFromValue(rt, args[2]);
532
+ table->revision += 1;
316
533
 
317
534
  LeafUpdates updates;
318
- updates.reserve(entries.size());
319
- for (auto& entry : entries) {
320
- if (!entry.isObject()) continue;
321
- auto idIt = entry.find("id");
322
- auto stateIt = entry.find("state");
323
- auto propsIt = entry.find("props");
324
- if (idIt == entry.items().end() || stateIt == entry.items().end() ||
325
- propsIt == entry.items().end() || !propsIt->second.isObject()) {
326
- continue;
327
- }
328
-
329
- auto viewIt = views_.find(idIt->second.asDouble());
330
- if (viewIt == views_.end()) continue;
331
- auto& view = viewIt->second;
332
- const std::string stateName = stateIt->second.asString();
333
- if (!view.stateProps.isObject()) {
334
- view.stateProps = folly::dynamic::object();
335
- }
336
- view.stateProps[stateName] = propsIt->second;
337
-
338
- const auto* activeName = view.activeState.empty()
339
- ? activeStateName(view.scopeId)
340
- : &view.activeState;
341
- if (activeName && *activeName == stateName) {
342
- buildViewUpdate(view, stateName, updates);
343
- }
535
+ for (auto& [id, view] : views_) {
536
+ if (view.table != table) continue;
537
+ const auto* name = viewStateName(view);
538
+ if (name && *name == stateName) buildViewUpdate(view, stateName, updates);
344
539
  }
345
-
346
540
  commitUpdates(rt, updates);
347
541
  return jsi::Value::undefined();
348
542
  }
@@ -358,8 +552,17 @@ jsi::Value HybridTamaguiRegistry::getViewState(jsi::Runtime& rt,
358
552
 
359
553
  folly::dynamic out = folly::dynamic::object("scopeId", view.scopeId)(
360
554
  "activeState", view.activeState)("base", view.baseProps)(
361
- "states", view.stateProps);
555
+ "states", view.table ? view.table->states : view.stateProps);
556
+ if (view.table) {
557
+ const auto* name = viewStateName(view);
558
+ auto state = name ? view.table->states.find(*name) : view.table->states.items().end();
559
+ out["nativeProps"] = state != view.table->states.items().end()
560
+ ? state->second
561
+ : folly::dynamic(nullptr);
562
+ return jsi::valueFromDynamic(rt, out);
563
+ }
362
564
  const auto& family = view.node->getFamily();
565
+ std::lock_guard<std::mutex> lock(family.nativePropsMutex);
363
566
  out["nativeProps"] = family.nativeProps_DEPRECATED
364
567
  ? *family.nativeProps_DEPRECATED
365
568
  : folly::dynamic(nullptr);
@@ -6,6 +6,9 @@
6
6
  #include <jsi/jsi.h>
7
7
  #include <react/renderer/core/ShadowNode.h>
8
8
 
9
+ #include <cstdint>
10
+ #include <memory>
11
+ #include <mutex>
9
12
  #include <string>
10
13
  #include <unordered_map>
11
14
 
@@ -38,9 +41,30 @@ namespace jsi = facebook::jsi;
38
41
  */
39
42
  class HybridTamaguiRegistry : public HybridTamaguiRegistrySpec {
40
43
  public:
44
+ // one view's props for a commit. table views point at their table's
45
+ // state entry, shared by every view of the table, so the commit can parse
46
+ // each distinct (props, update) pair once; per-view slots own theirs
47
+ // the props a table view committed per state. while the node still carries
48
+ // the props this engine committed last, a switch back to a state reuses
49
+ // that state's props instead of parsing them again; a React render or a
50
+ // table refill invalidates them
51
+ struct WarmProps {
52
+ facebook::react::Props::Shared last;
53
+ uint64_t revision = 0;
54
+ std::unordered_map<std::string, facebook::react::Props::Shared> byState;
55
+ };
56
+
57
+ struct LeafUpdate {
58
+ const folly::dynamic* shared = nullptr;
59
+ folly::dynamic owned;
60
+ // table views only
61
+ WarmProps* warm = nullptr;
62
+ const std::string* state = nullptr;
63
+ uint64_t revision = 0;
64
+ const folly::dynamic& props() const { return shared ? *shared : owned; }
65
+ };
41
66
  using LeafUpdates =
42
- std::unordered_map<const facebook::react::ShadowNodeFamily*,
43
- folly::dynamic>;
67
+ std::unordered_map<const facebook::react::ShadowNodeFamily*, LeafUpdate>;
44
68
 
45
69
  HybridTamaguiRegistry() : HybridObject(TAG) {}
46
70
 
@@ -56,6 +80,14 @@ class HybridTamaguiRegistry : public HybridTamaguiRegistrySpec {
56
80
  double getMissCount() override;
57
81
 
58
82
  private:
83
+ // one state table shared by every view a compiler mapping links: state
84
+ // name -> props. filled once per mapping and state, never per view
85
+ struct Table {
86
+ folly::dynamic states = folly::dynamic::object();
87
+ // bumped on every refill so views drop props parsed from older states
88
+ uint64_t revision = 0;
89
+ };
90
+
59
91
  struct LinkedView {
60
92
  // retained so the family stays valid for in-transaction ancestor lookup;
61
93
  // unmounted views resolve to no ancestors and are skipped
@@ -63,13 +95,19 @@ class HybridTamaguiRegistry : public HybridTamaguiRegistrySpec {
63
95
  std::string scopeId;
64
96
  folly::dynamic baseProps; // object, or nullptr when absent
65
97
  folly::dynamic stateProps; // stateName -> props object, or nullptr
98
+ // shared table (compiler mappings); when set it replaces base/stateProps
99
+ // and a commit carries only the state's keys, React already rendered the rest
100
+ std::shared_ptr<Table> table;
66
101
  // per-view active state (runtime mode). empty = follow scope state.
67
102
  // runtime integrations resolve state names per view (nested themes);
68
103
  // scope broadcast stays for compiler-emitted tables.
69
104
  std::string activeState;
105
+ WarmProps warm;
70
106
  };
71
107
 
72
- // raw JSI: link(shadowNode, slots, scopeId) -> id
108
+ // raw JSI: link(shadowNode, slots | tableId, scopeId, commitNow?) -> id
109
+ // commitNow commits the view's active state once (its render raced a
110
+ // scope change)
73
111
  jsi::Value link(jsi::Runtime& rt, const jsi::Value& thisValue,
74
112
  const jsi::Value* args, size_t count);
75
113
 
@@ -77,15 +115,16 @@ class HybridTamaguiRegistry : public HybridTamaguiRegistrySpec {
77
115
  // batched per-view state selection: for each entry, `props` (when given)
78
116
  // replaces the view's state table entry under `state`, the view's active
79
117
  // state becomes `state`, and all changed views commit in ONE transaction.
118
+ jsi::Value linkTables(jsi::Runtime& rt, const jsi::Value& thisValue,
119
+ const jsi::Value* args, size_t count);
120
+
80
121
  jsi::Value applyViewStates(jsi::Runtime& rt, const jsi::Value& thisValue,
81
122
  const jsi::Value* args, size_t count);
82
123
 
83
- // raw JSI: updateViewStateTables([{id, state, props}]) -> void
84
- // compiler mappings resolve a state once in JS, then fill every matching
85
- // linked view without moving it off scope-broadcast control.
86
- jsi::Value updateViewStateTables(jsi::Runtime& rt,
87
- const jsi::Value& thisValue,
88
- const jsi::Value* args, size_t count);
124
+ // raw JSI: setTableState(tableId, state, props) -> void
125
+ // fills one state of a shared table; linked views showing that state commit
126
+ jsi::Value setTableState(jsi::Runtime& rt, const jsi::Value& thisValue,
127
+ const jsi::Value* args, size_t count);
89
128
 
90
129
  // raw JSI: getViewState(id) -> {scopeId, activeState, base, states} | null
91
130
  // debug/test introspection of a linked view's tables; not a hot path
@@ -106,7 +145,11 @@ class HybridTamaguiRegistry : public HybridTamaguiRegistrySpec {
106
145
  // commit current props for all views (scopeFilter == nullptr) or one scope
107
146
  void applyUpdates(jsi::Runtime& rt, const std::string* scopeFilter);
108
147
 
148
+ // the state a view shows right now: its own runtime state, else its scope's
149
+ const std::string* viewStateName(const LinkedView& view) const;
150
+
109
151
  std::unordered_map<double, LinkedView> views_;
152
+ std::unordered_map<double, std::shared_ptr<Table>> tables_;
110
153
  std::unordered_map<std::string, std::string> scopeStates_; // "" = root
111
154
  double nextId_ = 1;
112
155
  double commitCount_ = 0;
@@ -27,11 +27,13 @@ __export(index_exports, {
27
27
  getViewState: () => getViewState,
28
28
  isAvailable: () => isAvailable,
29
29
  link: () => link,
30
+ linkTables: () => linkTables,
30
31
  processStyleColors: () => processStyleColors,
31
32
  removeScope: () => removeScope,
32
33
  resolveSlots: () => resolveSlots,
33
34
  setStateName: () => setStateName,
34
- updateViewStateTables: () => updateViewStateTables
35
+ setTableState: () => setTableState,
36
+ unlink: () => unlink
35
37
  });
36
38
  module.exports = __toCommonJS(index_exports);
37
39
  var import_mirror = require("./mirror.cjs");
@@ -39,11 +41,15 @@ var import_mirror2 = require("./mirror.cjs");
39
41
  function isAvailable() {
40
42
  return false;
41
43
  }
42
- function link(_ref, _slots, _scopeId) {
44
+ function link(_ref, _table, _scopeId, _commitNow) {
43
45
  return null;
44
46
  }
47
+ function linkTables(_refs, _tableIds, _scopeIndexes, _scopes) {
48
+ return 0;
49
+ }
50
+ function unlink(_id) {}
45
51
  function applyViewStates(_entries) {}
46
- function updateViewStateTables(_entries) {}
52
+ function setTableState(_tableId, _state, _props) {}
47
53
  function getViewState(_id) {
48
54
  return null;
49
55
  }