opshot 0.3.1 → 0.3.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.
package/README.md CHANGED
@@ -28,6 +28,12 @@ const user = useMutableState({ name: "Ada", age: 36 });
28
28
  user.age = 37;
29
29
  ```
30
30
 
31
+ The state is created once, on the first render. Pass a function to build the properties once as well, exactly as `useState` does:
32
+
33
+ ```tsx
34
+ const navigation = useMutableState(createNavigation);
35
+ ```
36
+
31
37
  ## Bounded re-renders
32
38
 
33
39
  React re-renders a component and its children when its state changes.
@@ -122,9 +128,10 @@ It can't track:
122
128
 
123
129
  - Internal slots (language level features like in Map)
124
130
  - #private fields (hidden at the language level)
125
- - Arrow methods on classes (they write to the original object, not the tracked one)
126
131
  - Array subclasses (the prototype is lost when copied)
127
132
 
133
+ And `this` for arrow methods on classes refers to the original and **not** the tracked state.
134
+
128
135
  Use `ignore` or `unsafeTrack` when dealing with these.
129
136
 
130
137
  ## Tracked collections
package/dist/index.d.ts CHANGED
@@ -180,6 +180,6 @@ declare function scope<P extends object>(Component: ComponentType<P>, options?:
180
180
 
181
181
  declare function useGroup(): Group;
182
182
 
183
- declare function useMutableState<T extends object>(properties: T, group?: Group): T;
183
+ declare function useMutableState<T extends object>(properties: (() => T) | T, group?: Group): T;
184
184
 
185
185
  export { type AddOperation, type Channel, type Context, type Group, type GroupListener, type Ignored, type Op, type Operation, type OperationPath, type RemoveOperation, type ReplaceOperation, type ScopeOptions, type StateListener, TrackedDate, TrackedMap, TrackedSet, type UnsafeTracked, applyOps, createChannel, createGroup, createMutableState, diffSnapshots, identify, ignore, isSameIdentity, isState, scope, subscribe, transact, unsafeTrack, useGroup, useMutableState };
package/dist/index.js CHANGED
@@ -337,65 +337,6 @@ function unstable_replaceInternalFunction(name, fn) {
337
337
  }
338
338
  }
339
339
 
340
- // src/react/wrapperRegistry.ts
341
- var wrapperTargets = /* @__PURE__ */ new WeakMap();
342
- var registerWrapperTarget = (wrapper, target) => {
343
- wrapperTargets.set(wrapper, target);
344
- };
345
- var getRegisteredWrapperTarget = (wrapper) => wrapperTargets.get(wrapper);
346
-
347
- // src/identity.ts
348
- var isObjectLike = (value) => value !== null && (typeof value === "object" || typeof value === "function");
349
- var targetRegistry = /* @__PURE__ */ new WeakMap();
350
- var identityTokenRegistry = /* @__PURE__ */ new WeakMap();
351
- var { proxyStateMap: proxyStateMap2 } = unstable_getInternalStates();
352
- function registerSnapshotCopy(copy, target) {
353
- targetRegistry.set(copy, target);
354
- }
355
- function getRegisteredTarget(copy) {
356
- return targetRegistry.get(copy);
357
- }
358
- function peelIdentityLayer(current) {
359
- const untracked = getUntracked(current);
360
- if (untracked !== null && untracked !== current) return untracked;
361
- const wrapperTarget = getRegisteredWrapperTarget(current);
362
- if (wrapperTarget !== void 0 && wrapperTarget !== current) return wrapperTarget;
363
- const registeredTarget = targetRegistry.get(current);
364
- if (registeredTarget !== void 0 && registeredTarget !== current) return registeredTarget;
365
- return void 0;
366
- }
367
- function resolveIdentity(value) {
368
- let current = value;
369
- while (isObjectLike(current)) {
370
- const peeled = peelIdentityLayer(current);
371
- if (peeled !== void 0) {
372
- current = peeled;
373
- continue;
374
- }
375
- const proxyState = proxyStateMap2.get(current);
376
- if (proxyState !== void 0 && proxyState[0] !== current) {
377
- current = proxyState[0];
378
- continue;
379
- }
380
- break;
381
- }
382
- return current;
383
- }
384
- function identify(value) {
385
- const target = resolveIdentity(value);
386
- if (!isObjectLike(target)) return value;
387
- const existing = identityTokenRegistry.get(target);
388
- if (existing !== void 0) return existing;
389
- const token = Object.freeze({});
390
- identityTokenRegistry.set(target, token);
391
- return token;
392
- }
393
- function isSameIdentity(first, second) {
394
- const resolvedFirst = resolveIdentity(first);
395
- const resolvedSecond = resolveIdentity(second);
396
- return resolvedFirst === resolvedSecond || resolvedFirst !== resolvedFirst && resolvedSecond !== resolvedSecond;
397
- }
398
-
399
340
  // src/unsafeTrack.ts
400
341
  var unsafeTrackedSet = /* @__PURE__ */ new WeakSet();
401
342
  function unsafeTrack(value) {
@@ -512,6 +453,65 @@ var cloneValue = (value, memo2, path) => {
512
453
  return clone;
513
454
  };
514
455
 
456
+ // src/react/wrapperRegistry.ts
457
+ var wrapperTargets = /* @__PURE__ */ new WeakMap();
458
+ var registerWrapperTarget = (wrapper, target) => {
459
+ wrapperTargets.set(wrapper, target);
460
+ };
461
+ var getRegisteredWrapperTarget = (wrapper) => wrapperTargets.get(wrapper);
462
+
463
+ // src/identity.ts
464
+ var isObjectLike = (value) => value !== null && (typeof value === "object" || typeof value === "function");
465
+ var targetRegistry = /* @__PURE__ */ new WeakMap();
466
+ var identityTokenRegistry = /* @__PURE__ */ new WeakMap();
467
+ var { proxyStateMap: proxyStateMap2 } = unstable_getInternalStates();
468
+ function registerSnapshotCopy(copy, target) {
469
+ targetRegistry.set(copy, target);
470
+ }
471
+ function getRegisteredTarget(copy) {
472
+ return targetRegistry.get(copy);
473
+ }
474
+ function peelIdentityLayer(current) {
475
+ const untracked = getUntracked(current);
476
+ if (untracked !== null && untracked !== current) return untracked;
477
+ const wrapperTarget = getRegisteredWrapperTarget(current);
478
+ if (wrapperTarget !== void 0 && wrapperTarget !== current) return wrapperTarget;
479
+ const registeredTarget = targetRegistry.get(current);
480
+ if (registeredTarget !== void 0 && registeredTarget !== current) return registeredTarget;
481
+ return void 0;
482
+ }
483
+ function resolveIdentity(value) {
484
+ let current = value;
485
+ while (isObjectLike(current)) {
486
+ const peeled = peelIdentityLayer(current);
487
+ if (peeled !== void 0) {
488
+ current = peeled;
489
+ continue;
490
+ }
491
+ const proxyState = proxyStateMap2.get(current);
492
+ if (proxyState !== void 0 && proxyState[0] !== current) {
493
+ current = proxyState[0];
494
+ continue;
495
+ }
496
+ break;
497
+ }
498
+ return current;
499
+ }
500
+ function identify(value) {
501
+ const target = resolveIdentity(value);
502
+ if (!isObjectLike(target)) return value;
503
+ const existing = identityTokenRegistry.get(target);
504
+ if (existing !== void 0) return existing;
505
+ const token = Object.freeze({});
506
+ identityTokenRegistry.set(target, token);
507
+ return token;
508
+ }
509
+ function isSameIdentity(first, second) {
510
+ const resolvedFirst = resolveIdentity(first);
511
+ const resolvedSecond = resolveIdentity(second);
512
+ return resolvedFirst === resolvedSecond || resolvedFirst !== resolvedFirst && resolvedSecond !== resolvedSecond;
513
+ }
514
+
515
515
  // src/ops/operation.ts
516
516
  var operationBrand = /* @__PURE__ */ Symbol.for("opshot.operation");
517
517
  var valueOriginals = /* @__PURE__ */ new WeakMap();
@@ -773,17 +773,17 @@ function diffSnapshots(before, after) {
773
773
  return ops;
774
774
  }
775
775
 
776
- // src/emitter.ts
777
- var emitters = /* @__PURE__ */ new WeakMap();
776
+ // src/emit/emitterDeliver.ts
777
+ var deliver = (record, ops, meta) => {
778
+ const groupDelivers = [...record.groupListeners?.values() ?? []].flatMap((byChannel) => [...byChannel.values()]);
779
+ for (const deliverToListener of groupDelivers) deliverToListener(record.target, ops, meta);
780
+ const ownDelivers = [...record.listeners.values()].flatMap((byChannel) => [...byChannel.values()]);
781
+ for (const deliverToListener of ownDelivers) deliverToListener(ops, meta);
782
+ };
783
+
784
+ // src/emit/resolveEmitterTarget.ts
778
785
  var { proxyStateMap: proxyStateMap3 } = unstable_getInternalStates();
779
786
  var isObjectLike3 = (value) => value !== null && (typeof value === "object" || typeof value === "function");
780
- var augmentBareCycleError = (error) => {
781
- const path = getCyclicPath(error);
782
- if (path === void 0) return void 0;
783
- return new Error(
784
- `opshot: a bare write created a cyclic value at ${formatOperationPath(path)}. Cycles cannot be tracked. This surfaced asynchronously because the write was not inside transact. Use transact for catchable cycle errors, ignore() for back-linked structures, or ids.`
785
- );
786
- };
787
787
  function resolveEmitterTarget(state) {
788
788
  let current = state;
789
789
  while (isObjectLike3(current)) {
@@ -795,10 +795,43 @@ function resolveEmitterTarget(state) {
795
795
  if (!isObjectLike3(current) || !proxyStateMap3.has(current)) throw new Error("opshot: expected a state object");
796
796
  return current;
797
797
  }
798
+
799
+ // src/emit/emitterRegistry.ts
800
+ var emitters = /* @__PURE__ */ new WeakMap();
798
801
  function getEmitter(state) {
799
802
  return emitters.get(resolveEmitterTarget(state));
800
803
  }
801
804
  var hasListeners = (record) => record.listeners.size > 0 || (record.groupListeners?.size ?? 0) > 0;
805
+ function getOrCreateEmitter(target, groupListeners) {
806
+ const resolved = resolveEmitterTarget(target);
807
+ const existing = emitters.get(resolved);
808
+ if (existing !== void 0) return existing;
809
+ const record = {
810
+ listeners: /* @__PURE__ */ new Map(),
811
+ groupListeners,
812
+ lastReported: snapshot(resolved),
813
+ isMutating: false,
814
+ target: resolved
815
+ };
816
+ emitters.set(resolved, record);
817
+ return record;
818
+ }
819
+ function deleteEmitter(target) {
820
+ emitters.delete(target);
821
+ }
822
+
823
+ // src/emit/emitterBare.ts
824
+ var augmentBareCycleError = (error) => {
825
+ const path = getCyclicPath(error);
826
+ if (path === void 0) return void 0;
827
+ return new Error(
828
+ `opshot: a bare write created a cyclic value at ${formatOperationPath(path)}. Cycles cannot be tracked. This surfaced asynchronously because the write was not inside transact. Use transact for catchable cycle errors, ignore() for back-linked structures, or ids.`
829
+ );
830
+ };
831
+ var requireObjectSnapshot = (value) => {
832
+ if (value !== null && (typeof value === "object" || typeof value === "function")) return value;
833
+ throw new Error("opshot: state snapshots must have an object root");
834
+ };
802
835
  var armWatchdog = (record) => {
803
836
  if (record.disarm !== void 0) return;
804
837
  record.lastReported = snapshot(record.target);
@@ -810,14 +843,6 @@ var disarmWatchdog = (record) => {
810
843
  record.disarm?.();
811
844
  record.disarm = void 0;
812
845
  };
813
- var deliver = (record, ops, meta) => {
814
- for (const listener of [...record.groupListeners ?? []]) listener(record.target, ops, meta);
815
- for (const listener of [...record.listeners]) listener(ops, meta);
816
- };
817
- var requireObjectSnapshot = (value) => {
818
- if (value !== null && (typeof value === "object" || typeof value === "function")) return value;
819
- throw new Error("opshot: state snapshots must have an object root");
820
- };
821
846
  var reportBareDiff = (record) => {
822
847
  const current = snapshot(record.target);
823
848
  if (current === record.lastReported) return;
@@ -835,53 +860,15 @@ var reportBareDiff = (record) => {
835
860
  var settlePendingBare = (record) => {
836
861
  reportBareDiff(record);
837
862
  };
838
- function getOrCreateEmitter(target, groupListeners) {
839
- const resolved = resolveEmitterTarget(target);
840
- const existing = emitters.get(resolved);
841
- if (existing !== void 0) return existing;
842
- const record = {
843
- listeners: /* @__PURE__ */ new Set(),
844
- groupListeners,
845
- lastReported: snapshot(resolved),
846
- isMutating: false,
847
- target: resolved
848
- };
849
- emitters.set(resolved, record);
850
- return record;
851
- }
852
- function mintGroupedEmitter(target, groupListeners) {
853
- const record = getOrCreateEmitter(target, groupListeners);
854
- armWatchdog(record);
855
- return record;
856
- }
857
863
  function emitBareFlush(target) {
858
864
  const record = getEmitter(target);
859
865
  if (record === void 0) return;
860
866
  reportBareDiff(record);
861
867
  }
862
- function addStateListener(state, listener) {
863
- const target = resolveEmitterTarget(state);
864
- let record = emitters.get(target);
865
- if (record === void 0) {
866
- record = getOrCreateEmitter(target);
867
- armWatchdog(record);
868
- } else if (record.disarm === void 0 && record.groupListeners === void 0) {
869
- armWatchdog(record);
870
- }
871
- record.listeners.add(listener);
872
- return () => {
873
- if (!record.listeners.delete(listener)) return;
874
- if (record.groupListeners === void 0 && record.listeners.size === 0) {
875
- disarmWatchdog(record);
876
- emitters.delete(target);
877
- }
878
- };
879
- }
880
- function addGroupListener(groupListeners, listener) {
881
- groupListeners.add(listener);
882
- return () => {
883
- groupListeners.delete(listener);
884
- };
868
+ function mintGroupedEmitter(target, groupListeners) {
869
+ const record = getOrCreateEmitter(target, groupListeners);
870
+ armWatchdog(record);
871
+ return record;
885
872
  }
886
873
 
887
874
  // src/utils/constructorName.ts
@@ -1231,7 +1218,7 @@ function getGroupListeners(group) {
1231
1218
  return listeners;
1232
1219
  }
1233
1220
  function createGroup() {
1234
- const listeners = /* @__PURE__ */ new Set();
1221
+ const listeners = /* @__PURE__ */ new Map();
1235
1222
  const group = {
1236
1223
  createMutableState(properties) {
1237
1224
  return createMutableState(properties, group);
@@ -1241,6 +1228,46 @@ function createGroup() {
1241
1228
  return group;
1242
1229
  }
1243
1230
 
1231
+ // src/emit/emitterListeners.ts
1232
+ function addStateListener(state, listener, channelId, deliver2) {
1233
+ const target = resolveEmitterTarget(state);
1234
+ const record = getOrCreateEmitter(target);
1235
+ if (record.disarm === void 0 && record.groupListeners === void 0) {
1236
+ armWatchdog(record);
1237
+ }
1238
+ let byChannel = record.listeners.get(listener);
1239
+ if (byChannel === void 0) {
1240
+ byChannel = /* @__PURE__ */ new Map();
1241
+ record.listeners.set(listener, byChannel);
1242
+ }
1243
+ byChannel.set(channelId, deliver2);
1244
+ return () => {
1245
+ const channels = record.listeners.get(listener);
1246
+ if (channels?.has(channelId) !== true) return;
1247
+ settlePendingBare(record);
1248
+ channels.delete(channelId);
1249
+ if (channels.size === 0) record.listeners.delete(listener);
1250
+ if (record.groupListeners === void 0 && record.listeners.size === 0) {
1251
+ disarmWatchdog(record);
1252
+ deleteEmitter(target);
1253
+ }
1254
+ };
1255
+ }
1256
+ function addGroupListener(groupListeners, listener, channelId, deliver2) {
1257
+ let byChannel = groupListeners.get(listener);
1258
+ if (byChannel === void 0) {
1259
+ byChannel = /* @__PURE__ */ new Map();
1260
+ groupListeners.set(listener, byChannel);
1261
+ }
1262
+ byChannel.set(channelId, deliver2);
1263
+ return () => {
1264
+ const channels = groupListeners.get(listener);
1265
+ if (channels?.has(channelId) !== true) return;
1266
+ channels.delete(channelId);
1267
+ if (channels.size === 0) groupListeners.delete(listener);
1268
+ };
1269
+ }
1270
+
1244
1271
  // src/transact.ts
1245
1272
  function transact(state, mutate, meta) {
1246
1273
  const record = getEmitter(state);
@@ -1440,11 +1467,11 @@ function applyOps(state, operations, meta) {
1440
1467
  // src/subscribe.ts
1441
1468
  function subscribe2(target, listener) {
1442
1469
  if (isGroup(target)) {
1443
- return addGroupListener(getGroupListeners(target), (state, ops, meta) => {
1470
+ return addGroupListener(getGroupListeners(target), listener, void 0, (state, ops, meta) => {
1444
1471
  listener(state, ops, unwrapTransportMeta(meta));
1445
1472
  });
1446
1473
  }
1447
- return addStateListener(target, (ops, meta) => {
1474
+ return addStateListener(target, listener, void 0, (ops, meta) => {
1448
1475
  listener(ops, unwrapTransportMeta(meta));
1449
1476
  });
1450
1477
  }
@@ -1477,7 +1504,7 @@ function createChannel(defaults) {
1477
1504
  }
1478
1505
  function subscribe3(target, listener) {
1479
1506
  if (isGroup(target)) {
1480
- return addGroupListener(getGroupListeners(target), (state, ops, meta) => {
1507
+ return addGroupListener(getGroupListeners(target), listener, channelId, (state, ops, meta) => {
1481
1508
  listener(
1482
1509
  state,
1483
1510
  ops,
@@ -1485,7 +1512,7 @@ function createChannel(defaults) {
1485
1512
  );
1486
1513
  });
1487
1514
  }
1488
- return addStateListener(target, (ops, meta) => {
1515
+ return addStateListener(target, listener, channelId, (ops, meta) => {
1489
1516
  listener(
1490
1517
  ops,
1491
1518
  toChannelContext(channelId, defaults, meta)
@@ -1986,6 +2013,7 @@ var bindMethodToWrapper = (wrapper, method) => {
1986
2013
  function createBoundary() {
1987
2014
  const partitions = /* @__PURE__ */ new Map();
1988
2015
  const targets = /* @__PURE__ */ new Map();
2016
+ let afterRender = false;
1989
2017
  const getPartition = (sourceProxy) => {
1990
2018
  let partition = partitions.get(sourceProxy);
1991
2019
  if (partition === void 0) {
@@ -2000,7 +2028,9 @@ function createBoundary() {
2000
2028
  }
2001
2029
  return partition;
2002
2030
  };
2031
+ const trackUsage = (partition, liveProxy) => afterRender ? {} : getUsage(partition.affected, liveProxy);
2003
2032
  const ensureBaseline = (partition, liveProxy) => {
2033
+ if (afterRender) return snapshot(liveProxy);
2004
2034
  const existing = partition.baselines.get(liveProxy);
2005
2035
  if (existing !== void 0) return existing;
2006
2036
  const baseline = snapshot(liveProxy);
@@ -2008,6 +2038,7 @@ function createBoundary() {
2008
2038
  return baseline;
2009
2039
  };
2010
2040
  const ensureRootBaseline = (partition) => {
2041
+ if (afterRender) return partition.previousRootSnapshot ?? snapshot(partition.sourceProxy);
2011
2042
  if (partition.previousRootSnapshot === void 0) {
2012
2043
  partition.previousRootSnapshot = snapshot(partition.sourceProxy);
2013
2044
  partition.baselines.set(partition.sourceProxy, partition.previousRootSnapshot);
@@ -2032,7 +2063,7 @@ function createBoundary() {
2032
2063
  ensureRootBaseline(partition);
2033
2064
  const value = Reflect.get(liveProxy, prop, liveProxy);
2034
2065
  const wrapper2 = wrapperBox.current;
2035
- const used = getUsage(partition.affected, liveProxy);
2066
+ const used = trackUsage(partition, liveProxy);
2036
2067
  recordKey(used, KEYS_PROPERTY2, prop);
2037
2068
  ensureBaseline(partition, liveProxy);
2038
2069
  if (typeof value === "function") {
@@ -2048,19 +2079,19 @@ function createBoundary() {
2048
2079
  return wrapLive(value, partition);
2049
2080
  },
2050
2081
  has(_target, prop) {
2051
- const used = getUsage(partition.affected, liveProxy);
2082
+ const used = trackUsage(partition, liveProxy);
2052
2083
  recordKey(used, HAS_KEY_PROPERTY2, prop);
2053
2084
  ensureBaseline(partition, liveProxy);
2054
2085
  return Reflect.has(liveProxy, prop);
2055
2086
  },
2056
2087
  getOwnPropertyDescriptor(_target, prop) {
2057
- const used = getUsage(partition.affected, liveProxy);
2088
+ const used = trackUsage(partition, liveProxy);
2058
2089
  recordKey(used, HAS_OWN_KEY_PROPERTY2, prop);
2059
2090
  ensureBaseline(partition, liveProxy);
2060
2091
  return Reflect.getOwnPropertyDescriptor(liveProxy, prop);
2061
2092
  },
2062
2093
  ownKeys() {
2063
- const used = getUsage(partition.affected, liveProxy);
2094
+ const used = trackUsage(partition, liveProxy);
2064
2095
  used[ALL_OWN_KEYS_PROPERTY2] = true;
2065
2096
  ensureBaseline(partition, liveProxy);
2066
2097
  return Reflect.ownKeys(liveProxy);
@@ -2104,6 +2135,9 @@ function createBoundary() {
2104
2135
  const nextRoot = snapshot(sourceProxy);
2105
2136
  return isChanged(partition.previousRootSnapshot, nextRoot, translated, /* @__PURE__ */ new WeakMap());
2106
2137
  },
2138
+ captureReads() {
2139
+ afterRender = true;
2140
+ },
2107
2141
  evictChangedTargets() {
2108
2142
  for (const [liveProxy, entry] of targets) {
2109
2143
  const current = snapshot(liveProxy);
@@ -2116,11 +2150,21 @@ function createBoundary() {
2116
2150
  }
2117
2151
  },
2118
2152
  resetReads() {
2153
+ afterRender = false;
2119
2154
  for (const partition of partitions.values()) {
2120
2155
  partition.previousRootSnapshot = void 0;
2121
2156
  partition.affected.clear();
2122
2157
  partition.baselines.clear();
2123
2158
  }
2159
+ },
2160
+ advanceBaselines() {
2161
+ for (const partition of partitions.values()) {
2162
+ for (const liveProxy of [...partition.baselines.keys()]) {
2163
+ partition.baselines.set(liveProxy, snapshot(liveProxy));
2164
+ }
2165
+ partition.previousRootSnapshot = snapshot(partition.sourceProxy);
2166
+ partition.baselines.set(partition.sourceProxy, partition.previousRootSnapshot);
2167
+ }
2124
2168
  }
2125
2169
  };
2126
2170
  }
@@ -2207,6 +2251,22 @@ function setAtPath(object, path, value) {
2207
2251
  return clone;
2208
2252
  }
2209
2253
  var sourcesKey = (sources) => `${sources.length}:${sources.map((source) => addressOf(source)).join(",")}`;
2254
+ var arePropsEqual = (previous, next) => {
2255
+ const previousRecord = previous;
2256
+ const nextRecord = next;
2257
+ const keys = Object.keys(previousRecord);
2258
+ if (keys.length !== Object.keys(nextRecord).length) return false;
2259
+ for (const key of keys) {
2260
+ const before = previousRecord[key];
2261
+ const after = nextRecord[key];
2262
+ if (Object.is(before, after)) continue;
2263
+ if (typeof before === "object" && before !== null && typeof after === "object" && after !== null && isState(before) && isState(after) && isSameIdentity(before, after)) {
2264
+ continue;
2265
+ }
2266
+ return false;
2267
+ }
2268
+ return true;
2269
+ };
2210
2270
  function scope(Component, options) {
2211
2271
  const maxDepth = options?.maxDepth ?? 10;
2212
2272
  const Scoped = (props) => {
@@ -2233,6 +2293,9 @@ function scope(Component, options) {
2233
2293
  }
2234
2294
  const renderedProps = changed ? nextProps : props;
2235
2295
  const versionsAtRender = sources.map((source) => getVersion(source));
2296
+ useEffect(() => {
2297
+ boundary.captureReads();
2298
+ });
2236
2299
  useEffect(() => {
2237
2300
  const unsubscribes = sources.map(
2238
2301
  (source) => subscribe(
@@ -2240,6 +2303,7 @@ function scope(Component, options) {
2240
2303
  () => {
2241
2304
  boundary.evictChangedTargets();
2242
2305
  if (boundary.readsChanged(source)) bump();
2306
+ else boundary.advanceBaselines();
2243
2307
  },
2244
2308
  true
2245
2309
  )
@@ -2265,24 +2329,28 @@ function scope(Component, options) {
2265
2329
  };
2266
2330
  const baseName = Component.displayName ?? Component.name;
2267
2331
  Scoped.displayName = `scope(${typeof baseName === "string" && baseName !== "" ? baseName : "Component"})`;
2268
- return memo(Scoped);
2332
+ return memo(Scoped, arePropsEqual);
2269
2333
  }
2270
2334
  function useGroup() {
2271
2335
  return useState(() => createGroup())[0];
2272
2336
  }
2273
2337
  function useMutableState(properties, group) {
2274
2338
  const [{ proxy: proxy2, boundary }] = useState(() => ({
2275
- proxy: createMutableState(properties, group),
2339
+ proxy: createMutableState(typeof properties === "function" ? properties() : properties, group),
2276
2340
  boundary: createBoundary()
2277
2341
  }));
2278
2342
  const [, bump] = useReducer((value) => value + 1, 0);
2279
2343
  const versionAtRender = getVersion(proxy2);
2280
2344
  boundary.resetReads();
2281
2345
  const wrapper = boundary.wrap(proxy2);
2346
+ useEffect(() => {
2347
+ boundary.captureReads();
2348
+ });
2282
2349
  useEffect(() => {
2283
2350
  const onSignal = () => {
2284
2351
  boundary.evictChangedTargets();
2285
2352
  if (boundary.readsChanged(proxy2)) bump();
2353
+ else boundary.advanceBaselines();
2286
2354
  };
2287
2355
  return subscribe(proxy2, onSignal, true);
2288
2356
  }, [proxy2, boundary]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opshot",
3
- "version": "0.3.1",
3
+ "version": "0.3.4",
4
4
  "description": "Plain-object state for React: mutate it directly, re-render only the components that read what changed, and track every change operation",
5
5
  "type": "module",
6
6
  "exports": {