@reckona/mreact-compat 0.0.173 → 0.0.175

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.
@@ -12,6 +12,7 @@ import {
12
12
  STRICT_MODE_TYPE,
13
13
  Suspense,
14
14
  SuspenseList,
15
+ type MemoType,
15
16
  type ReactCompatElement,
16
17
  type ReactCompatPortal,
17
18
  type ReactiveDomBlockProps,
@@ -21,6 +22,7 @@ import {
21
22
  type ReactCompatNode,
22
23
  } from "./element.js";
23
24
  import {
25
+ batchReactivePropCellUpdates,
24
26
  createReactivePropCell,
25
27
  createReactivePropProxy,
26
28
  setReactivePropCell,
@@ -227,13 +229,18 @@ export function renderHostFiberRoot(
227
229
  ): Fiber {
228
230
  const workInProgress = createWorkInProgress(root.current, { children: element });
229
231
  const rootDocument = root.container.ownerDocument;
230
- const result = reconcileHostChild(
231
- workInProgress,
232
- root.current.child,
233
- element,
234
- runtime,
235
- options.previousNodes === undefined ? "0" : "",
236
- { ...options, documentRef: options.documentRef ?? rootDocument },
232
+ const result = batchReactivePropCellUpdates(() =>
233
+ reconcileHostChild(
234
+ workInProgress,
235
+ root.current.child,
236
+ element,
237
+ runtime,
238
+ options.previousNodes === undefined ? "0" : "",
239
+ {
240
+ ...options,
241
+ documentRef: options.documentRef ?? rootDocument,
242
+ },
243
+ ),
237
244
  );
238
245
  workInProgress.child = result.fiber;
239
246
  workInProgress.memoizedProps = { children: element };
@@ -778,6 +785,7 @@ function isKeyedMemoRowCandidate(node: ReactCompatNode): boolean {
778
785
  // reconciler can drive its committed block straight through the prop cell instead
779
786
  // of re-invoking the component (the block's bound DOM updates via subscriptions).
780
787
  const STATIC_REACTIVE_BLOCK_MARKER = "__mreactStaticBlock";
788
+ const MEMO_COMPARE_PROPS_MARKER = "__mreactMemoCompareProps";
781
789
 
782
790
  function isStaticReactiveBlockComponent(type: unknown): boolean {
783
791
  return (
@@ -786,6 +794,118 @@ function isStaticReactiveBlockComponent(type: unknown): boolean {
786
794
  );
787
795
  }
788
796
 
797
+ function areCompilerMemoComparePropsEqual(
798
+ memoType: MemoType<Record<string, unknown>>,
799
+ previousProps: Record<string, unknown>,
800
+ nextProps: Record<string, unknown>,
801
+ ): boolean | undefined {
802
+ const keys = memoType[MEMO_COMPARE_PROPS_MARKER];
803
+ if (keys === undefined) {
804
+ return undefined;
805
+ }
806
+
807
+ if (keys.length === 1) {
808
+ const key = keys[0] as string;
809
+ return previousProps[key] === nextProps[key];
810
+ }
811
+
812
+ if (keys.length === 2) {
813
+ const first = keys[0] as string;
814
+ const second = keys[1] as string;
815
+ return previousProps[first] === nextProps[first] &&
816
+ previousProps[second] === nextProps[second];
817
+ }
818
+
819
+ if (keys.length === 3) {
820
+ const first = keys[0] as string;
821
+ const second = keys[1] as string;
822
+ const third = keys[2] as string;
823
+ return previousProps[first] === nextProps[first] &&
824
+ previousProps[second] === nextProps[second] &&
825
+ previousProps[third] === nextProps[third];
826
+ }
827
+
828
+ for (const key of keys) {
829
+ if (previousProps[key] !== nextProps[key]) {
830
+ return false;
831
+ }
832
+ }
833
+
834
+ return true;
835
+ }
836
+
837
+ function tryCreateInitialStaticBlockMemoFiber(
838
+ node: ReactCompatElement,
839
+ key: string | undefined,
840
+ memoType: {
841
+ type: ReactCompatElement["type"];
842
+ compare?: (
843
+ previous: Record<string, unknown>,
844
+ next: Record<string, unknown>,
845
+ ) => boolean;
846
+ },
847
+ runtime: RootRuntime,
848
+ memoPath: string,
849
+ options: FiberHydrationOptions,
850
+ ): FiberReconcileResult | undefined {
851
+ if (
852
+ options.previousNodes !== undefined ||
853
+ node.ref !== null ||
854
+ !isStaticReactiveBlockComponent(memoType.type)
855
+ ) {
856
+ return undefined;
857
+ }
858
+
859
+ const props = node.props as Record<string, unknown>;
860
+ const componentType = memoType.type as (props: Record<string, unknown>) => ReactCompatNode;
861
+ const rendered = componentType(props);
862
+
863
+ if (!isReactCompatElement(rendered) || rendered.type !== REACTIVE_DOM_BLOCK_TYPE) {
864
+ return undefined;
865
+ }
866
+
867
+ const memoFiber = createFiber("memo", props, key);
868
+ memoFiber.type = memoType;
869
+
870
+ const componentFiber = createFiber("function-component", props, key);
871
+ componentFiber.type = componentType;
872
+
873
+ const childResult = createHostFiber(
874
+ componentFiber,
875
+ undefined,
876
+ rendered,
877
+ key,
878
+ runtime,
879
+ `${memoPath}.0`,
880
+ options,
881
+ );
882
+ componentFiber.child = childResult.fiber;
883
+ if (componentFiber.child !== undefined) {
884
+ componentFiber.child.return = componentFiber;
885
+ componentFiber.child.sibling = undefined;
886
+ bubbleHostChild(componentFiber, componentFiber.child);
887
+ }
888
+ componentFiber.stateNode = {
889
+ element: node,
890
+ props,
891
+ instanceKeys: emptyInstanceKeys,
892
+ hasContextDependencies: false,
893
+ } satisfies FunctionFiberState;
894
+
895
+ memoFiber.child = componentFiber;
896
+ componentFiber.return = memoFiber;
897
+ bubbleHostChild(memoFiber, componentFiber);
898
+ memoFiber.memoizedState = {
899
+ props,
900
+ instanceKeys: emptyInstanceKeys,
901
+ hasDirtyInstanceDependencies: false,
902
+ hasUnflushedEffectDependencies: false,
903
+ hasRetainedInstanceDependencies: false,
904
+ } satisfies MemoFiberState;
905
+
906
+ return { fiber: memoFiber, consumed: childResult.consumed };
907
+ }
908
+
789
909
  // In-place bailout for a keyed memo row whose props are unchanged and which has
790
910
  // no hook/context/effect dependencies. The caller has proven the key order is
791
911
  // unchanged (hasSameKeyOrderPrefix), so the fiber keeps its position and can be
@@ -814,16 +934,21 @@ function tryReuseDependencyFreeKeyedMemoRow(
814
934
  state === undefined ||
815
935
  state.hasDirtyInstanceDependencies !== false ||
816
936
  state.hasUnflushedEffectDependencies !== false ||
817
- state.hasRetainedInstanceDependencies !== false ||
818
- !areMemoPropsEqual(
819
- child.type as { compare?: (a: Record<string, unknown>, b: Record<string, unknown>) => boolean },
820
- state.props,
821
- child.props as Record<string, unknown>,
822
- )
937
+ state.hasRetainedInstanceDependencies !== false
823
938
  ) {
824
939
  return undefined;
825
940
  }
826
941
 
942
+ const memoType = child.type as MemoType<Record<string, unknown>>;
943
+ const nextProps = child.props as Record<string, unknown>;
944
+ const propsEqual =
945
+ areCompilerMemoComparePropsEqual(memoType, state.props, nextProps) ??
946
+ areMemoPropsEqual(memoType, state.props, nextProps);
947
+
948
+ if (!propsEqual) {
949
+ return undefined;
950
+ }
951
+
827
952
  matched.pendingProps = child.props;
828
953
  matched.flags = NoFlags;
829
954
  matched.subtreeFlags = NoFlags;
@@ -890,8 +1015,10 @@ function tryCellUpdateStaticBlockMemoRow(
890
1015
 
891
1016
  // Drive the bound DOM through the prop cell (the marker guarantees the block's
892
1017
  // props are the component's props verbatim), then reuse the memo fiber in place.
893
- setReactivePropCell(blockState.propCell, child.props as Record<string, unknown>);
894
- matched.memoizedState = { ...state, props: child.props as Record<string, unknown> };
1018
+ const nextProps = child.props as Record<string, unknown>;
1019
+ setReactivePropCell(blockState.propCell, nextProps);
1020
+ state.props = nextProps;
1021
+ matched.memoizedState = state;
895
1022
  matched.pendingProps = child.props;
896
1023
  matched.flags = NoFlags;
897
1024
  matched.subtreeFlags = NoFlags;
@@ -903,12 +1030,15 @@ function tryCellUpdateStaticBlockMemoRow(
903
1030
  }
904
1031
 
905
1032
  // Fast path for a keyed list of memo-wrapped rows (e.g. `<RowMemo key={id} />`)
906
- // whose key order is UNCHANGED between renders the js-framework-benchmark
907
- // "select row" and "partial update" shapes. Walks current fibers and new
908
- // children in lockstep: unchanged rows bail in place (no allocation, no general
909
- // keyed-reconcile machinery), changed rows re-render through the proven
910
- // per-child path. Any reorder / insert / delete / non-memo child / length change
911
- // makes it return undefined so the general reconcile takes over.
1033
+ // whose key order is UNCHANGED, append-only, or a single deletion between
1034
+ // renders — the js-framework-benchmark "select row", "partial update",
1035
+ // "append rows", and "remove row" shapes. Walks current fibers and new children
1036
+ // in lockstep: unchanged rows bail in place (no allocation, no general
1037
+ // keyed-reconcile machinery), changed rows cell-update through the proven
1038
+ // per-child path, appended rows are created as a suffix, and a single deleted
1039
+ // row is removed while the suffix is retained. Any reorder / insert /
1040
+ // multi-delete / non-memo child makes it return undefined so the general
1041
+ // reconcile takes over.
912
1042
  function reconcileKeyedMemoRowHostChildren(
913
1043
  parent: Fiber,
914
1044
  currentFirstChild: Fiber | undefined,
@@ -922,12 +1052,16 @@ function reconcileKeyedMemoRowHostChildren(
922
1052
  currentFirstChild === undefined ||
923
1053
  runtime === undefined ||
924
1054
  options.previousNodes !== undefined ||
925
- !isKeyedMemoRowCandidate(children[0]) ||
926
- !hasSameKeyOrderPrefix(currentFirstChild, children)
1055
+ !isKeyedMemoRowCandidate(children[0])
927
1056
  ) {
928
1057
  return undefined;
929
1058
  }
930
1059
 
1060
+ const memoRowType = (children[0] as ReactCompatElement).type;
1061
+ if (!hasSameAppendOrSingleDeleteKeyOrder(currentFirstChild, children, memoRowType)) {
1062
+ return undefined;
1063
+ }
1064
+
931
1065
  let currentKeyed: Fiber | undefined = currentFirstChild;
932
1066
  let first: Fiber | undefined;
933
1067
  let previous: Fiber | undefined;
@@ -935,22 +1069,38 @@ function reconcileKeyedMemoRowHostChildren(
935
1069
  let subtreeChildListChanged = false;
936
1070
  let hasRefSubtree = false;
937
1071
  let hasDisposableResources = false;
1072
+ let listShapeChanged = false;
1073
+ let appendSuffix: AppendSuffixCommitHint | undefined;
1074
+ let removedFiber: Fiber | undefined;
938
1075
  let dirtyChildren: Fiber[] | undefined;
939
1076
 
940
1077
  for (let index = 0; index < children.length; index += 1) {
941
- const child = children[index];
1078
+ const child = children[index] as ReactCompatElement;
1079
+ const childElement = child;
1080
+ let matched: Fiber | undefined = currentKeyed;
942
1081
 
943
- if (currentKeyed === undefined || !isKeyedMemoRowCandidate(child)) {
1082
+ if (matched === undefined) {
1083
+ // append-only suffix
1084
+ } else if (matched.key === childElement.key) {
1085
+ currentKeyed = matched.sibling;
1086
+ } else if (
1087
+ removedFiber === undefined &&
1088
+ matched.sibling?.key === childElement.key &&
1089
+ canSkipSingleDeletedKeyedFiber(children, index, matched.sibling)
1090
+ ) {
1091
+ removedFiber = matched;
1092
+ matched = matched.sibling;
1093
+ currentKeyed = matched.sibling;
1094
+ listShapeChanged = true;
1095
+ } else {
944
1096
  return undefined;
945
1097
  }
946
1098
 
947
- const matched = currentKeyed;
948
- currentKeyed = currentKeyed.sibling;
949
- const childElement = child as ReactCompatElement;
950
-
951
1099
  let fiber =
952
- tryReuseDependencyFreeKeyedMemoRow(matched, childElement) ??
953
- tryCellUpdateStaticBlockMemoRow(matched, childElement);
1100
+ matched === undefined
1101
+ ? undefined
1102
+ : tryReuseDependencyFreeKeyedMemoRow(matched, childElement) ??
1103
+ tryCellUpdateStaticBlockMemoRow(matched, childElement);
954
1104
  if (fiber === undefined) {
955
1105
  const result = createHostFiber(
956
1106
  parent,
@@ -968,11 +1118,16 @@ function reconcileKeyedMemoRowHostChildren(
968
1118
  return undefined;
969
1119
  }
970
1120
 
971
- if (fiber !== matched && fiber.alternate !== matched) {
1121
+ if (matched !== undefined && fiber !== matched && fiber.alternate !== matched) {
972
1122
  markOptimizedChildForDeletion(parent, matched);
973
1123
  }
974
1124
  }
975
1125
 
1126
+ if (matched === undefined) {
1127
+ listShapeChanged = true;
1128
+ appendSuffix ??= { fiber, index };
1129
+ }
1130
+
976
1131
  if (first === undefined) {
977
1132
  first = fiber;
978
1133
  } else if (previous !== undefined) {
@@ -1003,18 +1158,84 @@ function reconcileKeyedMemoRowHostChildren(
1003
1158
  }
1004
1159
 
1005
1160
  if (currentKeyed !== undefined) {
1006
- return undefined;
1161
+ if (removedFiber !== undefined || currentKeyed.sibling !== undefined) {
1162
+ return undefined;
1163
+ }
1164
+ removedFiber = currentKeyed;
1165
+ listShapeChanged = true;
1007
1166
  }
1008
1167
 
1009
1168
  parent.hasRefSubtree = hasRefSubtree;
1010
1169
  parent.hasDisposableResources = hasDisposableResources;
1011
1170
  parent.subtreeFlags = subtreeFlags;
1012
1171
  parent.subtreeChildListChanged = subtreeChildListChanged;
1013
- parent.childListChanged = false;
1172
+ parent.childListChanged = listShapeChanged;
1173
+ if (appendSuffix !== undefined && canStoreAppendSuffixCommitHint(parent)) {
1174
+ parent.memoizedState = appendSuffix;
1175
+ }
1176
+ if (removedFiber !== undefined) {
1177
+ parent.deletions = [removedFiber];
1178
+ markOptimizedChildForDeletion(parent, removedFiber);
1179
+ }
1014
1180
  recordDirtyChildCommitHints(parent, dirtyChildren);
1015
1181
  return { fiber: first, consumed: 0 };
1016
1182
  }
1017
1183
 
1184
+ function hasSameAppendOrSingleDeleteKeyOrder(
1185
+ currentFirstChild: Fiber,
1186
+ children: readonly ReactCompatNode[],
1187
+ expectedType: ReactCompatElement["type"],
1188
+ ): boolean {
1189
+ let current: Fiber | undefined = currentFirstChild;
1190
+ let skippedDeletion = false;
1191
+
1192
+ for (let index = 0; index < children.length; index += 1) {
1193
+ const child = children[index];
1194
+ if (!isSameKeyedMemoRowCandidate(child, expectedType)) {
1195
+ return false;
1196
+ }
1197
+
1198
+ const key = (child as ReactCompatElement).key;
1199
+
1200
+ if (current === undefined) {
1201
+ return true;
1202
+ }
1203
+
1204
+ if (current.key === key) {
1205
+ current = current.sibling;
1206
+ continue;
1207
+ }
1208
+
1209
+ const sibling = current.sibling;
1210
+ if (
1211
+ !skippedDeletion &&
1212
+ sibling !== undefined &&
1213
+ sibling.key === key &&
1214
+ canSkipSingleDeletedKeyedFiber(children, index, sibling)
1215
+ ) {
1216
+ skippedDeletion = true;
1217
+ current = sibling.sibling;
1218
+ continue;
1219
+ }
1220
+
1221
+ return false;
1222
+ }
1223
+
1224
+ return current === undefined || (!skippedDeletion && current.sibling === undefined);
1225
+ }
1226
+
1227
+ function isSameKeyedMemoRowCandidate(
1228
+ node: ReactCompatNode,
1229
+ expectedType: ReactCompatElement["type"],
1230
+ ): boolean {
1231
+ return (
1232
+ isReactCompatElement(node) &&
1233
+ node.key !== null &&
1234
+ node.ref === null &&
1235
+ node.type === expectedType
1236
+ );
1237
+ }
1238
+
1018
1239
  function canReuseDependencyFreeMemoAtKey(
1019
1240
  current: Fiber | undefined,
1020
1241
  node: ReactCompatElement,
@@ -1926,6 +2147,22 @@ function createHostFiberImpl(
1926
2147
  const memoPath = `${path}.memo`;
1927
2148
  const previousMemoFiber =
1928
2149
  current?.tag === "memo" && current.type === memoType ? current : undefined;
2150
+ const initialStaticBlockMemoFiber =
2151
+ previousMemoFiber === undefined
2152
+ ? tryCreateInitialStaticBlockMemoFiber(
2153
+ node,
2154
+ key,
2155
+ memoType,
2156
+ runtime,
2157
+ memoPath,
2158
+ options,
2159
+ )
2160
+ : undefined;
2161
+
2162
+ if (initialStaticBlockMemoFiber !== undefined) {
2163
+ return initialStaticBlockMemoFiber;
2164
+ }
2165
+
1929
2166
  const previousMemoState =
1930
2167
  previousMemoFiber !== undefined
1931
2168
  ? (previousMemoFiber.memoizedState as MemoFiberState | undefined)
@@ -2935,6 +3172,7 @@ function commitHostSingleRemoval(fiber: Fiber, parent: ParentNode): boolean {
2935
3172
  }
2936
3173
 
2937
3174
  if (removedAny) {
3175
+ disposeHostFiberResources(removed);
2938
3176
  fiber.deletions = undefined;
2939
3177
  }
2940
3178
 
@@ -1,3 +1,4 @@
1
+ import { batch } from "@reckona/mreact-reactive-core";
1
2
  import {
2
3
  flushQueuedComputations,
3
4
  notifySubscribers,
@@ -15,6 +16,8 @@ import type { ReactiveDomBlockResult } from "./element.js";
15
16
  export interface ReactivePropCell {
16
17
  value: Record<string, unknown>;
17
18
  source: Source;
19
+ propertySources?: Map<PropertyKey, Source> | undefined;
20
+ propertySnapshots?: Map<PropertyKey, ShallowObjectSnapshot> | undefined;
18
21
  }
19
22
 
20
23
  // What a reactive-dom-block fiber stores in stateNode: the committed node and
@@ -23,17 +26,35 @@ export interface ReactiveDomBlockState extends ReactiveDomBlockResult {
23
26
  propCell?: ReactivePropCell | undefined;
24
27
  }
25
28
 
29
+ let propCellBatchDepth = 0;
30
+ let propCellBatchNeedsFlush = false;
31
+
26
32
  export function createReactivePropCell(props: Record<string, unknown>): ReactivePropCell {
27
33
  return { value: props, source: { subscribers: null } };
28
34
  }
29
35
 
36
+ export function batchReactivePropCellUpdates<T>(run: () => T): T {
37
+ propCellBatchDepth += 1;
38
+ try {
39
+ return batch(run);
40
+ } finally {
41
+ propCellBatchDepth -= 1;
42
+ if (propCellBatchDepth === 0 && propCellBatchNeedsFlush) {
43
+ propCellBatchNeedsFlush = false;
44
+ flushQueuedComputations();
45
+ }
46
+ }
47
+ }
48
+
30
49
  const PROP_PROXY_HANDLER: ProxyHandler<ReactivePropCell> = {
31
50
  get(cell, property) {
32
- trackSource(cell.source);
33
- return cell.value[property as string];
51
+ trackSource(getReactivePropPropertySource(cell, property));
52
+ const value = Reflect.get(cell.value, property);
53
+ rememberReactivePropObjectSnapshot(cell, property, value);
54
+ return value;
34
55
  },
35
56
  has(cell, property) {
36
- trackSource(cell.source);
57
+ trackSource(getReactivePropPropertySource(cell, property));
37
58
  return property in cell.value;
38
59
  },
39
60
  ownKeys(cell) {
@@ -50,6 +71,107 @@ export function createReactivePropProxy<P extends object>(cell: ReactivePropCell
50
71
  return new Proxy(cell, PROP_PROXY_HANDLER) as unknown as P;
51
72
  }
52
73
 
74
+ function getReactivePropPropertySource(cell: ReactivePropCell, property: PropertyKey): Source {
75
+ let propertySources = cell.propertySources;
76
+
77
+ if (propertySources === undefined) {
78
+ propertySources = new Map();
79
+ cell.propertySources = propertySources;
80
+ }
81
+
82
+ let source = propertySources.get(property);
83
+
84
+ if (source === undefined) {
85
+ source = { subscribers: null };
86
+ propertySources.set(property, source);
87
+ }
88
+
89
+ return source;
90
+ }
91
+
92
+ interface ShallowObjectSnapshot {
93
+ value: object;
94
+ entries: Map<PropertyKey, unknown>;
95
+ }
96
+
97
+ function rememberReactivePropObjectSnapshot(
98
+ cell: ReactivePropCell,
99
+ property: PropertyKey,
100
+ value: unknown,
101
+ ): void {
102
+ if (!isObjectLike(value)) {
103
+ cell.propertySnapshots?.delete(property);
104
+ return;
105
+ }
106
+
107
+ let snapshots = cell.propertySnapshots;
108
+ if (snapshots === undefined) {
109
+ snapshots = new Map();
110
+ cell.propertySnapshots = snapshots;
111
+ }
112
+
113
+ snapshots.set(property, createShallowObjectSnapshot(value));
114
+ }
115
+
116
+ function createShallowObjectSnapshot(value: object): ShallowObjectSnapshot {
117
+ const entries = new Map<PropertyKey, unknown>();
118
+ for (const key of Reflect.ownKeys(value)) {
119
+ const descriptor = Reflect.getOwnPropertyDescriptor(value, key);
120
+ if (descriptor !== undefined && "value" in descriptor) {
121
+ entries.set(key, descriptor.value);
122
+ }
123
+ }
124
+
125
+ return { value, entries };
126
+ }
127
+
128
+ function isObjectLike(value: unknown): value is object {
129
+ return (typeof value === "object" && value !== null) || typeof value === "function";
130
+ }
131
+
132
+ function hasShallowObjectSnapshotChanged(
133
+ snapshot: ShallowObjectSnapshot | undefined,
134
+ nextValue: unknown,
135
+ ): boolean {
136
+ if (snapshot === undefined || snapshot.value !== nextValue || !isObjectLike(nextValue)) {
137
+ return false;
138
+ }
139
+
140
+ for (const key of Reflect.ownKeys(nextValue)) {
141
+ const descriptor = Reflect.getOwnPropertyDescriptor(nextValue, key);
142
+ if (descriptor !== undefined && "value" in descriptor) {
143
+ if (!snapshot.entries.has(key) || !Object.is(snapshot.entries.get(key), descriptor.value)) {
144
+ return true;
145
+ }
146
+ }
147
+ }
148
+
149
+ for (const key of snapshot.entries.keys()) {
150
+ const descriptor = Reflect.getOwnPropertyDescriptor(nextValue, key);
151
+ if (descriptor === undefined || !("value" in descriptor)) {
152
+ return true;
153
+ }
154
+ }
155
+
156
+ return false;
157
+ }
158
+
159
+ function shouldNotifyReactivePropProperty(
160
+ previous: Record<string, unknown>,
161
+ next: Record<string, unknown>,
162
+ property: PropertyKey,
163
+ snapshot: ShallowObjectSnapshot | undefined,
164
+ ): boolean {
165
+ if ((property in previous) !== (property in next)) {
166
+ return true;
167
+ }
168
+
169
+ const previousValue = Reflect.get(previous, property);
170
+ const nextValue = Reflect.get(next, property);
171
+ return !Object.is(previousValue, nextValue) ||
172
+ hasShallowObjectSnapshotChanged(snapshot, nextValue);
173
+ }
174
+
53
175
  export function setReactivePropCell(
54
176
  cell: ReactivePropCell,
55
177
  next: Record<string, unknown>,
@@ -58,10 +180,38 @@ export function setReactivePropCell(
58
180
  return;
59
181
  }
60
182
 
183
+ const previous = cell.value;
184
+ const propertySources = cell.propertySources;
185
+ let notified = false;
186
+
61
187
  cell.value = next;
62
188
 
189
+ if (propertySources !== undefined) {
190
+ for (const [property, source] of propertySources) {
191
+ if (
192
+ shouldNotifyReactivePropProperty(
193
+ previous,
194
+ next,
195
+ property,
196
+ cell.propertySnapshots?.get(property),
197
+ )
198
+ ) {
199
+ notifySubscribers(source);
200
+ notified = true;
201
+ }
202
+ }
203
+ }
204
+
63
205
  if (cell.source.subscribers !== null) {
64
206
  notifySubscribers(cell.source);
65
- flushQueuedComputations();
207
+ notified = true;
208
+ }
209
+
210
+ if (notified) {
211
+ if (propCellBatchDepth === 0) {
212
+ flushQueuedComputations();
213
+ } else {
214
+ propCellBatchNeedsFlush = true;
215
+ }
66
216
  }
67
217
  }