@reckona/mreact-compat 0.0.171 → 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/src/hooks.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import {
2
2
  flushPendingComputed,
3
3
  flushQueuedComputations,
4
+ notifySubscribers,
5
+ trackSource,
6
+ type Source,
4
7
  } from "@reckona/mreact-reactive-core/internal";
5
8
  import { scheduleCallback } from "./fiber-scheduler.js";
6
9
  import { removeChildIfPresent } from "./dom-children.js";
@@ -11,7 +14,10 @@ import {
11
14
  useContext,
12
15
  withContextReadObserver,
13
16
  } from "./context.js";
14
- import { REACTIVE_TEXT_BINDING_META } from "./element.js";
17
+ import {
18
+ REACTIVE_STATE_BINDING_META,
19
+ REACTIVE_TEXT_BINDING_META,
20
+ } from "./element.js";
15
21
  import { isThenable } from "./thenable.js";
16
22
 
17
23
  export interface RootRuntime {
@@ -127,7 +133,13 @@ export type DevToolsHookValue =
127
133
  | { kind: "effect"; effectKind: "insertion" | "layout" | "normal"; deps?: readonly unknown[] };
128
134
 
129
135
  type HookSlot =
130
- | { kind: "state"; value: unknown; hostCommitValue?: unknown; textBinding?: ReactiveTextBinding }
136
+ | {
137
+ kind: "state";
138
+ value: unknown;
139
+ hostCommitValue?: unknown;
140
+ textBinding?: ReactiveTextBinding;
141
+ stateBinding?: ReactiveStateBinding;
142
+ }
131
143
  | {
132
144
  kind: "action-state";
133
145
  state: unknown;
@@ -158,9 +170,21 @@ type HookSlot =
158
170
  strictReplay?: boolean;
159
171
  };
160
172
 
173
+ interface PendingInstanceContext {
174
+ runtime: RootRuntime;
175
+ path: string;
176
+ owner: unknown;
177
+ existing: ComponentInstance | undefined;
178
+ }
179
+
161
180
  interface HookRenderState {
162
181
  currentRuntime: RootRuntime | undefined;
163
182
  currentInstance: ComponentInstance | undefined;
183
+ // Deferred instance for the component currently rendering. The instance is
184
+ // only materialized (created, registered, prefix-indexed) when a hook or a
185
+ // context read first needs it, so components with no hooks/context (e.g. a
186
+ // pure host-rendering memo row) pay none of that per-render cost.
187
+ pendingInstance: PendingInstanceContext | undefined;
164
188
  currentCacheScope: CacheScope | undefined;
165
189
  hostCommitDepth: number;
166
190
  queuedHostCommitRerenders: Set<RootRuntime>;
@@ -174,6 +198,7 @@ const hookRenderState =
174
198
  ] ??= {
175
199
  currentRuntime: undefined,
176
200
  currentInstance: undefined,
201
+ pendingInstance: undefined,
177
202
  currentCacheScope: undefined,
178
203
  hostCommitDepth: 0,
179
204
  queuedHostCommitRerenders: new Set<RootRuntime>(),
@@ -204,6 +229,12 @@ export interface ReactiveTextBinding {
204
229
  subscribers: Set<Text>;
205
230
  }
206
231
 
232
+ export interface ReactiveStateBinding {
233
+ get(): unknown;
234
+ source: Source;
235
+ value: unknown;
236
+ }
237
+
207
238
  const reactiveTextBindingsByNode = new WeakMap<Text, ReactiveTextBinding>();
208
239
  const hydratedIdsByRuntime = new WeakMap<RootRuntime, Map<string, string>>();
209
240
 
@@ -378,6 +409,9 @@ export function createRootRuntime(
378
409
  flushPendingEffects(this.pendingImperativeHandleEffects);
379
410
  this.effectFlushPhase = "layout";
380
411
  const strictLayoutEffects = flushPendingEffects(this.pendingLayoutEffects);
412
+ if (flushHostCommitRerenders()) {
413
+ dedupePendingEffects(this.pendingEffects);
414
+ }
381
415
  this.effectFlushPhase = "normal";
382
416
  const strictEffects = flushPendingEffects(this.pendingEffects);
383
417
  this.effectFlushPhase = undefined;
@@ -502,25 +536,56 @@ export function renderWithRootRuntime<T>(
502
536
  ): T {
503
537
  const previousRuntime = hookRenderState.currentRuntime;
504
538
  const previousInstance = hookRenderState.currentInstance;
505
- let instance = runtime.instances.get(path);
539
+ const previousPending = hookRenderState.pendingInstance;
506
540
 
507
- if (instance !== undefined && owner !== undefined && instance.owner !== owner) {
508
- cleanupInstance(instance);
509
- instance = undefined;
541
+ let existing = runtime.instances.get(path);
542
+ if (existing !== undefined && owner !== undefined && existing.owner !== owner) {
543
+ cleanupInstance(existing);
544
+ runtime.instances.delete(path);
545
+ removeInstanceKeyFromIndex(runtime, path);
546
+ existing = undefined;
510
547
  }
511
548
 
512
- instance ??= {
513
- owner,
514
- path,
515
- hooks: [],
516
- hookIndex: 0,
517
- dirty: false,
518
- devToolsHookSuppressionDepth: 0,
519
- };
520
- instance.owner = owner;
521
- instance.path = path;
522
- runtime.instances.set(path, instance);
523
- indexInstanceKey(runtime, path);
549
+ // Defer instance materialization: hooks / context reads call
550
+ // materializeInstance() lazily. A component that touches neither never
551
+ // allocates or registers an instance.
552
+ hookRenderState.currentRuntime = runtime;
553
+ hookRenderState.currentInstance = undefined;
554
+ hookRenderState.pendingInstance = { runtime, path, owner, existing };
555
+
556
+ try {
557
+ return withContextReadObserver(recordContextDependency, render);
558
+ } finally {
559
+ hookRenderState.currentRuntime = previousRuntime;
560
+ hookRenderState.currentInstance = previousInstance;
561
+ hookRenderState.pendingInstance = previousPending;
562
+ }
563
+ }
564
+
565
+ // Materialize the deferred instance for the rendering component the first time
566
+ // a hook or context read needs it.
567
+ function materializeInstance(): ComponentInstance {
568
+ const pending = hookRenderState.pendingInstance;
569
+ if (pending === undefined) {
570
+ throw new Error("Hooks can only be called while rendering.");
571
+ }
572
+
573
+ const { runtime, path, owner } = pending;
574
+ let instance = pending.existing;
575
+ if (instance === undefined) {
576
+ instance = {
577
+ owner,
578
+ path,
579
+ hooks: [],
580
+ hookIndex: 0,
581
+ dirty: false,
582
+ devToolsHookSuppressionDepth: 0,
583
+ };
584
+ runtime.instances.set(path, instance);
585
+ indexInstanceKey(runtime, path);
586
+ } else {
587
+ instance.owner = owner;
588
+ }
524
589
  runtime.activeInstanceKeys?.add(path);
525
590
  instance.hookIndex = 0;
526
591
  instance.dirty = false;
@@ -534,17 +599,17 @@ export function renderWithRootRuntime<T>(
534
599
  delete instance.devToolsHookTypes;
535
600
  }
536
601
  instance.devToolsHookSuppressionDepth = 0;
537
- hookRenderState.currentRuntime = runtime;
538
602
  hookRenderState.currentInstance = instance;
603
+ hookRenderState.pendingInstance = undefined;
604
+ return instance;
605
+ }
539
606
 
540
- try {
541
- return withContextReadObserver((context, value) => {
542
- (instance.contextDependencies ??= new Map()).set(context, value);
543
- }, render);
544
- } finally {
545
- hookRenderState.currentRuntime = previousRuntime;
546
- hookRenderState.currentInstance = previousInstance;
547
- }
607
+ function recordContextDependency(
608
+ context: ReactCompatContextLike<unknown>,
609
+ value: unknown,
610
+ ): void {
611
+ const instance = hookRenderState.currentInstance ?? materializeInstance();
612
+ (instance.contextDependencies ??= new Map()).set(context, value);
548
613
  }
549
614
 
550
615
  export function hasChangedContextDependency(
@@ -575,22 +640,27 @@ export function hasContextDependency(
575
640
  return keys.some((key) => runtime.instances.get(key)?.contextDependencies !== undefined);
576
641
  }
577
642
 
643
+ // Shared read-only empty result so components with no registered instances
644
+ // (e.g. hookless rows under lazy instance materialization) don't each allocate
645
+ // a fresh array. Callers treat instance-key lists as read-only.
646
+ const EMPTY_INSTANCE_KEYS: string[] = [];
647
+
578
648
  export function collectRuntimeInstanceKeys(runtime: RootRuntime, prefix: string): string[] {
579
649
  const keys = runtime.instanceKeysByPrefix.get(prefix);
580
650
 
581
651
  if (keys === undefined) {
582
- return [];
652
+ return EMPTY_INSTANCE_KEYS;
583
653
  }
584
654
 
585
- const activeKeys: string[] = [];
655
+ let activeKeys: string[] | undefined;
586
656
 
587
657
  for (const key of keys) {
588
658
  if (runtime.instances.has(key)) {
589
- activeKeys.push(key);
659
+ (activeKeys ??= []).push(key);
590
660
  }
591
661
  }
592
662
 
593
- return activeKeys;
663
+ return activeKeys ?? EMPTY_INSTANCE_KEYS;
594
664
  }
595
665
 
596
666
  export function getDevToolsHookState(
@@ -813,7 +883,17 @@ export function useState<T>(
813
883
  optionsAllowDirectTextBinding(value) &&
814
884
  updateDirectTextBinding(slot.textBinding, nextValue);
815
885
 
816
- if (canUseDirectTextBinding) {
886
+ const canUseDirectStateBinding =
887
+ hookRenderState.hostCommitDepth === 0 &&
888
+ hookRenderState.currentRuntime !== runtime &&
889
+ hookRenderState.currentInstance !== instance &&
890
+ runtime.effectFlushPhase === undefined &&
891
+ eventBatchDepth === 0 &&
892
+ transitionDepth === 0 &&
893
+ optionsAllowDirectTextBinding(value) &&
894
+ updateDirectStateBinding(slot.stateBinding, nextValue);
895
+
896
+ if (canUseDirectTextBinding || canUseDirectStateBinding) {
817
897
  return;
818
898
  }
819
899
 
@@ -836,6 +916,7 @@ export function useState<T>(
836
916
  (value: T | ((previous: T) => T)) => void,
837
917
  ] & Record<PropertyKey, unknown>;
838
918
  result[REACTIVE_TEXT_BINDING_META] = getStateTextBinding(slot);
919
+ result[REACTIVE_STATE_BINDING_META] = getStateBinding(slot);
839
920
  return result;
840
921
  }
841
922
 
@@ -884,6 +965,19 @@ function getStateTextBinding(slot: Extract<HookSlot, { kind: "state" }>): Reacti
884
965
  return slot.textBinding;
885
966
  }
886
967
 
968
+ function getStateBinding(slot: Extract<HookSlot, { kind: "state" }>): ReactiveStateBinding {
969
+ slot.stateBinding ??= {
970
+ value: slot.value,
971
+ source: { subscribers: null },
972
+ get() {
973
+ trackSource(this.source);
974
+ return this.value;
975
+ },
976
+ };
977
+ slot.stateBinding.value = slot.value;
978
+ return slot.stateBinding;
979
+ }
980
+
887
981
  function optionsAllowDirectTextBinding(value: unknown): boolean {
888
982
  return typeof value !== "function";
889
983
  }
@@ -913,6 +1007,20 @@ function updateDirectTextBinding(binding: ReactiveTextBinding | undefined, value
913
1007
  return updated;
914
1008
  }
915
1009
 
1010
+ function updateDirectStateBinding(
1011
+ binding: ReactiveStateBinding | undefined,
1012
+ value: unknown,
1013
+ ): boolean {
1014
+ if (binding === undefined || binding.source.subscribers === null) {
1015
+ return false;
1016
+ }
1017
+
1018
+ binding.value = value;
1019
+ notifySubscribers(binding.source);
1020
+ flushQueuedComputations();
1021
+ return true;
1022
+ }
1023
+
916
1024
  function isReactiveTextBinding(value: unknown): value is ReactiveTextBinding {
917
1025
  return (
918
1026
  typeof value === "object" &&
@@ -928,11 +1036,12 @@ export function useReducer<TState, TAction, TInitial = TState>(
928
1036
  initialArg: TInitial,
929
1037
  init?: (initialArg: TInitial) => TState,
930
1038
  ): [TState, (action: TAction) => void] {
931
- const [state, setState] = runWithoutDevToolsHookTracking(() =>
1039
+ const stateTuple = runWithoutDevToolsHookTracking(() =>
932
1040
  useState<TState>(() =>
933
1041
  init === undefined ? (initialArg as unknown as TState) : init(initialArg),
934
1042
  ),
935
1043
  );
1044
+ const [state, setState] = stateTuple;
936
1045
  const reducerRef = runWithoutDevToolsHookTracking(() => useRef(reducer));
937
1046
  const stateRef = runWithoutDevToolsHookTracking(() => useRef(state));
938
1047
  const dispatchRef = runWithoutDevToolsHookTracking(() =>
@@ -956,7 +1065,18 @@ export function useReducer<TState, TAction, TInitial = TState>(
956
1065
  value: state,
957
1066
  });
958
1067
 
959
- return [state, dispatchRef.current];
1068
+ const result = [state, dispatchRef.current] as [
1069
+ TState,
1070
+ (action: TAction) => void,
1071
+ ] & Record<PropertyKey, unknown>;
1072
+ const stateBinding = (stateTuple as unknown as Record<PropertyKey, unknown>)[
1073
+ REACTIVE_STATE_BINDING_META
1074
+ ];
1075
+
1076
+ if (stateBinding !== undefined) {
1077
+ result[REACTIVE_STATE_BINDING_META] = stateBinding;
1078
+ }
1079
+ return result;
960
1080
  }
961
1081
 
962
1082
  /** Returns a stable mutable ref object for the component instance. */
@@ -2044,15 +2164,16 @@ function scheduleInstanceUpdate(
2044
2164
  scheduleRuntimeRerender(runtime, options);
2045
2165
  }
2046
2166
 
2047
- function flushHostCommitRerenders(): void {
2167
+ function flushHostCommitRerenders(): boolean {
2048
2168
  if (
2049
2169
  hostCommitRerenderDepth > 0 ||
2050
2170
  hookRenderState.hostCommitDepth > 0 ||
2051
2171
  hookRenderState.queuedHostCommitRerenders.size === 0
2052
2172
  ) {
2053
- return;
2173
+ return false;
2054
2174
  }
2055
2175
 
2176
+ let didRerender = false;
2056
2177
  hostCommitRerenderDepth += 1;
2057
2178
  try {
2058
2179
  for (
@@ -2069,6 +2190,7 @@ function flushHostCommitRerenders(): void {
2069
2190
  clearHostCommitStateBaselines(runtime);
2070
2191
 
2071
2192
  if (hasDirtyInstance) {
2193
+ didRerender = true;
2072
2194
  runtime.rerender("sync");
2073
2195
  }
2074
2196
  }
@@ -2077,6 +2199,20 @@ function flushHostCommitRerenders(): void {
2077
2199
  } finally {
2078
2200
  hostCommitRerenderDepth -= 1;
2079
2201
  }
2202
+ return didRerender;
2203
+ }
2204
+
2205
+ function dedupePendingEffects(queue: PendingEffect[]): void {
2206
+ if (queue.length < 2) {
2207
+ return;
2208
+ }
2209
+
2210
+ const latestBySlot = new Map<Extract<HookSlot, { kind: "effect" }>, PendingEffect>();
2211
+ for (const effect of queue) {
2212
+ latestBySlot.set(effect.slot, effect);
2213
+ }
2214
+ queue.length = 0;
2215
+ queue.push(...latestBySlot.values());
2080
2216
  }
2081
2217
 
2082
2218
  function flushEffectFlushRerenders(): void {
@@ -2299,11 +2435,7 @@ function requireRuntime(): RootRuntime {
2299
2435
  }
2300
2436
 
2301
2437
  function requireInstance(): ComponentInstance {
2302
- if (hookRenderState.currentInstance === undefined) {
2303
- throw new Error("Hooks can only be called while rendering.");
2304
- }
2305
-
2306
- return hookRenderState.currentInstance;
2438
+ return hookRenderState.currentInstance ?? materializeInstance();
2307
2439
  }
2308
2440
 
2309
2441
  function areHookInputsEqual(