@reckona/mreact-compat 0.0.170 → 0.0.172

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 (58) hide show
  1. package/dist/devtools.js +2 -0
  2. package/dist/devtools.js.map +1 -1
  3. package/dist/dom-host-rules.d.ts.map +1 -1
  4. package/dist/dom-host-rules.js +4 -1
  5. package/dist/dom-host-rules.js.map +1 -1
  6. package/dist/dom-props.js +2 -1
  7. package/dist/dom-props.js.map +1 -1
  8. package/dist/element.d.ts +13 -1
  9. package/dist/element.d.ts.map +1 -1
  10. package/dist/element.js +30 -2
  11. package/dist/element.js.map +1 -1
  12. package/dist/fiber-commit.js +2 -1
  13. package/dist/fiber-commit.js.map +1 -1
  14. package/dist/fiber-host.d.ts +1 -1
  15. package/dist/fiber-host.d.ts.map +1 -1
  16. package/dist/fiber-host.js +1 -1
  17. package/dist/fiber-host.js.map +1 -1
  18. package/dist/fiber.d.ts +2 -1
  19. package/dist/fiber.d.ts.map +1 -1
  20. package/dist/fiber.js +2 -0
  21. package/dist/fiber.js.map +1 -1
  22. package/dist/hooks.d.ts +7 -0
  23. package/dist/hooks.d.ts.map +1 -1
  24. package/dist/hooks.js +122 -41
  25. package/dist/hooks.js.map +1 -1
  26. package/dist/host-reconciler.d.ts +1 -0
  27. package/dist/host-reconciler.d.ts.map +1 -1
  28. package/dist/host-reconciler.js +659 -50
  29. package/dist/host-reconciler.js.map +1 -1
  30. package/dist/jsx-dev-runtime.d.ts +3 -1
  31. package/dist/jsx-dev-runtime.d.ts.map +1 -1
  32. package/dist/jsx-dev-runtime.js +3 -1
  33. package/dist/jsx-dev-runtime.js.map +1 -1
  34. package/dist/jsx-runtime.d.ts +4 -2
  35. package/dist/jsx-runtime.d.ts.map +1 -1
  36. package/dist/jsx-runtime.js +9 -1
  37. package/dist/jsx-runtime.js.map +1 -1
  38. package/dist/reactive-prop-cell.d.ts +13 -0
  39. package/dist/reactive-prop-cell.d.ts.map +1 -0
  40. package/dist/reactive-prop-cell.js +36 -0
  41. package/dist/reactive-prop-cell.js.map +1 -0
  42. package/dist/root.d.ts.map +1 -1
  43. package/dist/root.js +3 -1
  44. package/dist/root.js.map +1 -1
  45. package/package.json +3 -3
  46. package/src/devtools.ts +2 -0
  47. package/src/dom-host-rules.ts +4 -1
  48. package/src/dom-props.ts +2 -1
  49. package/src/element.ts +60 -3
  50. package/src/fiber-commit.ts +3 -1
  51. package/src/fiber-host.ts +1 -0
  52. package/src/fiber.ts +4 -0
  53. package/src/hooks.ts +173 -41
  54. package/src/host-reconciler.ts +979 -64
  55. package/src/jsx-dev-runtime.ts +4 -0
  56. package/src/jsx-runtime.ts +16 -0
  57. package/src/reactive-prop-cell.ts +67 -0
  58. package/src/root.ts +3 -0
package/dist/hooks.js CHANGED
@@ -1,13 +1,14 @@
1
- import { flushPendingComputed, flushQueuedComputations, } from "@reckona/mreact-reactive-core/internal";
1
+ import { flushPendingComputed, flushQueuedComputations, notifySubscribers, trackSource, } from "@reckona/mreact-reactive-core/internal";
2
2
  import { scheduleCallback } from "./fiber-scheduler.js";
3
3
  import { removeChildIfPresent } from "./dom-children.js";
4
4
  import { isReactCompatContext, readContextValue, useContext, withContextReadObserver, } from "./context.js";
5
- import { REACTIVE_TEXT_BINDING_META } from "./element.js";
5
+ import { REACTIVE_STATE_BINDING_META, REACTIVE_TEXT_BINDING_META, } from "./element.js";
6
6
  import { isThenable } from "./thenable.js";
7
7
  const HOOK_RENDER_STATE_KEY = Symbol.for("modular.react.hook_render_state");
8
8
  const hookRenderState = (globalThis[HOOK_RENDER_STATE_KEY] ??= {
9
9
  currentRuntime: undefined,
10
10
  currentInstance: undefined,
11
+ pendingInstance: undefined,
11
12
  currentCacheScope: undefined,
12
13
  hostCommitDepth: 0,
13
14
  queuedHostCommitRerenders: new Set(),
@@ -156,6 +157,9 @@ export function createRootRuntime(rerender, options = {}) {
156
157
  flushPendingEffects(this.pendingImperativeHandleEffects);
157
158
  this.effectFlushPhase = "layout";
158
159
  const strictLayoutEffects = flushPendingEffects(this.pendingLayoutEffects);
160
+ if (flushHostCommitRerenders()) {
161
+ dedupePendingEffects(this.pendingEffects);
162
+ }
159
163
  this.effectFlushPhase = "normal";
160
164
  const strictEffects = flushPendingEffects(this.pendingEffects);
161
165
  this.effectFlushPhase = undefined;
@@ -258,23 +262,53 @@ export function renderWithProfiler(runtime, path, props, render) {
258
262
  export function renderWithRootRuntime(runtime, path, render, owner) {
259
263
  const previousRuntime = hookRenderState.currentRuntime;
260
264
  const previousInstance = hookRenderState.currentInstance;
261
- let instance = runtime.instances.get(path);
262
- if (instance !== undefined && owner !== undefined && instance.owner !== owner) {
263
- cleanupInstance(instance);
264
- instance = undefined;
265
- }
266
- instance ??= {
267
- owner,
268
- path,
269
- hooks: [],
270
- hookIndex: 0,
271
- dirty: false,
272
- devToolsHookSuppressionDepth: 0,
273
- };
274
- instance.owner = owner;
275
- instance.path = path;
276
- runtime.instances.set(path, instance);
277
- indexInstanceKey(runtime, path);
265
+ const previousPending = hookRenderState.pendingInstance;
266
+ let existing = runtime.instances.get(path);
267
+ if (existing !== undefined && owner !== undefined && existing.owner !== owner) {
268
+ cleanupInstance(existing);
269
+ runtime.instances.delete(path);
270
+ removeInstanceKeyFromIndex(runtime, path);
271
+ existing = undefined;
272
+ }
273
+ // Defer instance materialization: hooks / context reads call
274
+ // materializeInstance() lazily. A component that touches neither never
275
+ // allocates or registers an instance.
276
+ hookRenderState.currentRuntime = runtime;
277
+ hookRenderState.currentInstance = undefined;
278
+ hookRenderState.pendingInstance = { runtime, path, owner, existing };
279
+ try {
280
+ return withContextReadObserver(recordContextDependency, render);
281
+ }
282
+ finally {
283
+ hookRenderState.currentRuntime = previousRuntime;
284
+ hookRenderState.currentInstance = previousInstance;
285
+ hookRenderState.pendingInstance = previousPending;
286
+ }
287
+ }
288
+ // Materialize the deferred instance for the rendering component the first time
289
+ // a hook or context read needs it.
290
+ function materializeInstance() {
291
+ const pending = hookRenderState.pendingInstance;
292
+ if (pending === undefined) {
293
+ throw new Error("Hooks can only be called while rendering.");
294
+ }
295
+ const { runtime, path, owner } = pending;
296
+ let instance = pending.existing;
297
+ if (instance === undefined) {
298
+ instance = {
299
+ owner,
300
+ path,
301
+ hooks: [],
302
+ hookIndex: 0,
303
+ dirty: false,
304
+ devToolsHookSuppressionDepth: 0,
305
+ };
306
+ runtime.instances.set(path, instance);
307
+ indexInstanceKey(runtime, path);
308
+ }
309
+ else {
310
+ instance.owner = owner;
311
+ }
278
312
  runtime.activeInstanceKeys?.add(path);
279
313
  instance.hookIndex = 0;
280
314
  instance.dirty = false;
@@ -289,17 +323,13 @@ export function renderWithRootRuntime(runtime, path, render, owner) {
289
323
  delete instance.devToolsHookTypes;
290
324
  }
291
325
  instance.devToolsHookSuppressionDepth = 0;
292
- hookRenderState.currentRuntime = runtime;
293
326
  hookRenderState.currentInstance = instance;
294
- try {
295
- return withContextReadObserver((context, value) => {
296
- (instance.contextDependencies ??= new Map()).set(context, value);
297
- }, render);
298
- }
299
- finally {
300
- hookRenderState.currentRuntime = previousRuntime;
301
- hookRenderState.currentInstance = previousInstance;
302
- }
327
+ hookRenderState.pendingInstance = undefined;
328
+ return instance;
329
+ }
330
+ function recordContextDependency(context, value) {
331
+ const instance = hookRenderState.currentInstance ?? materializeInstance();
332
+ (instance.contextDependencies ??= new Map()).set(context, value);
303
333
  }
304
334
  export function hasChangedContextDependency(runtime, keys) {
305
335
  for (const key of keys) {
@@ -318,18 +348,22 @@ export function hasChangedContextDependency(runtime, keys) {
318
348
  export function hasContextDependency(runtime, keys) {
319
349
  return keys.some((key) => runtime.instances.get(key)?.contextDependencies !== undefined);
320
350
  }
351
+ // Shared read-only empty result so components with no registered instances
352
+ // (e.g. hookless rows under lazy instance materialization) don't each allocate
353
+ // a fresh array. Callers treat instance-key lists as read-only.
354
+ const EMPTY_INSTANCE_KEYS = [];
321
355
  export function collectRuntimeInstanceKeys(runtime, prefix) {
322
356
  const keys = runtime.instanceKeysByPrefix.get(prefix);
323
357
  if (keys === undefined) {
324
- return [];
358
+ return EMPTY_INSTANCE_KEYS;
325
359
  }
326
- const activeKeys = [];
360
+ let activeKeys;
327
361
  for (const key of keys) {
328
362
  if (runtime.instances.has(key)) {
329
- activeKeys.push(key);
363
+ (activeKeys ??= []).push(key);
330
364
  }
331
365
  }
332
- return activeKeys;
366
+ return activeKeys ?? EMPTY_INSTANCE_KEYS;
333
367
  }
334
368
  export function getDevToolsHookState(runtime, path) {
335
369
  const instance = runtime.instances.get(path);
@@ -500,7 +534,15 @@ export function useState(initial) {
500
534
  transitionDepth === 0 &&
501
535
  optionsAllowDirectTextBinding(value) &&
502
536
  updateDirectTextBinding(slot.textBinding, nextValue);
503
- if (canUseDirectTextBinding) {
537
+ const canUseDirectStateBinding = hookRenderState.hostCommitDepth === 0 &&
538
+ hookRenderState.currentRuntime !== runtime &&
539
+ hookRenderState.currentInstance !== instance &&
540
+ runtime.effectFlushPhase === undefined &&
541
+ eventBatchDepth === 0 &&
542
+ transitionDepth === 0 &&
543
+ optionsAllowDirectTextBinding(value) &&
544
+ updateDirectStateBinding(slot.stateBinding, nextValue);
545
+ if (canUseDirectTextBinding || canUseDirectStateBinding) {
504
546
  return;
505
547
  }
506
548
  if (hookRenderState.hostCommitDepth > 0) {
@@ -516,6 +558,7 @@ export function useState(initial) {
516
558
  });
517
559
  const result = [slot.value, setState];
518
560
  result[REACTIVE_TEXT_BINDING_META] = getStateTextBinding(slot);
561
+ result[REACTIVE_STATE_BINDING_META] = getStateBinding(slot);
519
562
  return result;
520
563
  }
521
564
  export function subscribeReactiveTextBinding(binding, node) {
@@ -554,6 +597,18 @@ function getStateTextBinding(slot) {
554
597
  slot.textBinding.value = slot.value;
555
598
  return slot.textBinding;
556
599
  }
600
+ function getStateBinding(slot) {
601
+ slot.stateBinding ??= {
602
+ value: slot.value,
603
+ source: { subscribers: null },
604
+ get() {
605
+ trackSource(this.source);
606
+ return this.value;
607
+ },
608
+ };
609
+ slot.stateBinding.value = slot.value;
610
+ return slot.stateBinding;
611
+ }
557
612
  function optionsAllowDirectTextBinding(value) {
558
613
  return typeof value !== "function";
559
614
  }
@@ -577,6 +632,15 @@ function updateDirectTextBinding(binding, value) {
577
632
  binding.value = value;
578
633
  return updated;
579
634
  }
635
+ function updateDirectStateBinding(binding, value) {
636
+ if (binding === undefined || binding.source.subscribers === null) {
637
+ return false;
638
+ }
639
+ binding.value = value;
640
+ notifySubscribers(binding.source);
641
+ flushQueuedComputations();
642
+ return true;
643
+ }
580
644
  function isReactiveTextBinding(value) {
581
645
  return (typeof value === "object" &&
582
646
  value !== null &&
@@ -585,7 +649,8 @@ function isReactiveTextBinding(value) {
585
649
  }
586
650
  /** Stores reducer-managed component state and returns the current state with a dispatch function. */
587
651
  export function useReducer(reducer, initialArg, init) {
588
- const [state, setState] = runWithoutDevToolsHookTracking(() => useState(() => init === undefined ? initialArg : init(initialArg)));
652
+ const stateTuple = runWithoutDevToolsHookTracking(() => useState(() => init === undefined ? initialArg : init(initialArg)));
653
+ const [state, setState] = stateTuple;
589
654
  const reducerRef = runWithoutDevToolsHookTracking(() => useRef(reducer));
590
655
  const stateRef = runWithoutDevToolsHookTracking(() => useRef(state));
591
656
  const dispatchRef = runWithoutDevToolsHookTracking(() => useRef(undefined));
@@ -602,7 +667,12 @@ export function useReducer(reducer, initialArg, init) {
602
667
  kind: "reducer",
603
668
  value: state,
604
669
  });
605
- return [state, dispatchRef.current];
670
+ const result = [state, dispatchRef.current];
671
+ const stateBinding = stateTuple[REACTIVE_STATE_BINDING_META];
672
+ if (stateBinding !== undefined) {
673
+ result[REACTIVE_STATE_BINDING_META] = stateBinding;
674
+ }
675
+ return result;
606
676
  }
607
677
  /** Returns a stable mutable ref object for the component instance. */
608
678
  export function useRef(initial) {
@@ -1433,8 +1503,9 @@ function flushHostCommitRerenders() {
1433
1503
  if (hostCommitRerenderDepth > 0 ||
1434
1504
  hookRenderState.hostCommitDepth > 0 ||
1435
1505
  hookRenderState.queuedHostCommitRerenders.size === 0) {
1436
- return;
1506
+ return false;
1437
1507
  }
1508
+ let didRerender = false;
1438
1509
  hostCommitRerenderDepth += 1;
1439
1510
  try {
1440
1511
  for (let attempt = 0; attempt < 3 && hookRenderState.queuedHostCommitRerenders.size > 0; attempt += 1) {
@@ -1444,6 +1515,7 @@ function flushHostCommitRerenders() {
1444
1515
  const hasDirtyInstance = Array.from(runtime.instances.values()).some((instance) => instance.dirty);
1445
1516
  clearHostCommitStateBaselines(runtime);
1446
1517
  if (hasDirtyInstance) {
1518
+ didRerender = true;
1447
1519
  runtime.rerender("sync");
1448
1520
  }
1449
1521
  }
@@ -1453,6 +1525,18 @@ function flushHostCommitRerenders() {
1453
1525
  finally {
1454
1526
  hostCommitRerenderDepth -= 1;
1455
1527
  }
1528
+ return didRerender;
1529
+ }
1530
+ function dedupePendingEffects(queue) {
1531
+ if (queue.length < 2) {
1532
+ return;
1533
+ }
1534
+ const latestBySlot = new Map();
1535
+ for (const effect of queue) {
1536
+ latestBySlot.set(effect.slot, effect);
1537
+ }
1538
+ queue.length = 0;
1539
+ queue.push(...latestBySlot.values());
1456
1540
  }
1457
1541
  function flushEffectFlushRerenders() {
1458
1542
  if (effectFlushRerenderDepth > 0 ||
@@ -1616,10 +1700,7 @@ function requireRuntime() {
1616
1700
  return hookRenderState.currentRuntime;
1617
1701
  }
1618
1702
  function requireInstance() {
1619
- if (hookRenderState.currentInstance === undefined) {
1620
- throw new Error("Hooks can only be called while rendering.");
1621
- }
1622
- return hookRenderState.currentInstance;
1703
+ return hookRenderState.currentInstance ?? materializeInstance();
1623
1704
  }
1624
1705
  function areHookInputsEqual(nextDeps, previousDeps) {
1625
1706
  if (nextDeps.length !== previousDeps.length) {