@yiin/reactive-proxy-state 1.0.44 → 3.0.0

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
@@ -18,6 +18,10 @@ bun add @yiin/reactive-proxy-state
18
18
  # or yarn add @yiin/reactive-proxy-state
19
19
  ```
20
20
 
21
+ Version 2 needs ES2021 or later (it uses `WeakRef`). Coming from 1.x? Read [Upgrading to 2.0.0](./docs/guide/upgrading-to-2.md) first: payloads are copies now, and a few inputs are refused.
22
+
23
+ Version 3 changes only the Vue tracker, `trackVueReactiveEvents`: it needs Vue 3.5 and the app's own `@vue/reactivity`. See [Upgrading to 3.0.0](./docs/guide/upgrading-to-3.md).
24
+
21
25
  ## Quick Start
22
26
 
23
27
  The most common use case is creating reactive state for local applications:
@@ -269,6 +273,45 @@ state.data.delete("foo"); // Output: Data has "foo": false
269
273
  state.tags.add("urgent"); // Output: Tags: important, urgent
270
274
  ```
271
275
 
276
+ ## Synchronization Limits
277
+
278
+ ### Supported inputs
279
+
280
+ A replica fed every event always equals the state when the state is made of:
281
+
282
+ - plain objects, arrays, Maps and Sets, with enumerable data properties (no getters or setters);
283
+ - `sort` comparators and array callbacks that do not change the array or object they work on;
284
+ - numeric or numeric-string bounds for `splice`, `fill` and `copyWithin`.
285
+
286
+ Other input stays correct or is refused before anything changes:
287
+
288
+ - **Comparator writes.** A comparator that writes through the proxy gets the native result: `sort` writes back the items it read before comparing, sorted, and keeps a tail the comparator appended. The comparator's own writes send their own events first.
289
+ - **Bounds with side effects.** `fill` and `copyWithin` convert each bound once, in native order, and use the same numbers for the event and the write. A `BigInt` or `Symbol` bound throws a `TypeError` before any change, like native code.
290
+ - **Read-only and frozen arrays.** `sort`, `reverse`, `fill` and `copyWithin` check every index native code would write before the first write. A read-only index, a hole in a non-extensible array, or a non-configurable index to delete throws a `TypeError` with the array unchanged and no event. A one-item `sort` writes nothing, so it works on a frozen array, as natively.
291
+ - **Getters on a back-reference.** A payload that reaches back into the object it is written to (a cycle) is copied as it will be after the write. An accessor property in that object or in the payload throws an `UnsupportedStateError` (a `TypeError`) before the write. The written object's getter is not called. (The set trap reads the new value's own properties to unwrap stored proxies, so a getter on the new value itself runs once.)
292
+ - **Payloads too large to copy** throw a `SnapshotLimitError` (see the copy limit below).
293
+
294
+ A refused write sends no event and runs no effect: the state and every replica stay as they were. Keep the same root and retry with supported input.
295
+
296
+ These cases are not synchronized, and are not refused:
297
+
298
+ - **Getters elsewhere.** A getter is copied as the value it returns when the event is made. The replica keeps that value; a later change of what the getter returns sends no event.
299
+ - **Raw writes.** A write through `toRaw()`, through the raw items that iteration callbacks receive (`forEach`, `map`, `filter`, `for...of`), or on raw objects inside a `valueOf` bypasses the proxies and sends nothing.
300
+ - **Descriptor and prototype changes** after the state is made (`Object.defineProperty`, freezing part of the state, a setter on a prototype). Only the reorders above check the array first; `push`, `pop`, `shift`, `unshift`, `splice` and index writes on an array with a read-only index can fail part way, like native code, and send no event for the part they wrote.
301
+
302
+ Events carry paths, and `updateState` applies them on a replica. Some shapes cannot travel that way:
303
+
304
+ - **Map keys are path segments, written as strings.** A map with both `1` and `"1"` as keys gives the same path for both, so a write inside one value can land on the other on the replica. Object keys have no path at all: a write inside an object used as a key emits nothing. Use string keys, or one key type per map, in synced state.
305
+ - **Set members have no key.** A path into a set member uses its current position, and `updateState` cannot follow a path into a set. Writes inside an object stored in a set do not replay. Keep primitives in synced sets, or replace a member (`delete` then `add`) instead of changing it.
306
+ - **Raw objects can hide an alias.** An object at two places emits one event per place. The library finds the places when a value is stored (root creation, assignment, insertion, `sort`, `reverse`, `fill`, `copyWithin`), also inside a value that left the tree and is stored again. An object stored through a raw reference (from `toRaw`, or the raw items that iteration methods such as `filter`, `map` and `for...of` give) next to a place it already has elsewhere is not seen as an alias until it is stored again through a proxy.
307
+ - **Array properties depend on the event.** The initial replace, inserted items, a set's added value and removed values keep enumerable named and symbol properties. Reorders keep these properties on nested arrays too. The reordered array keeps its own properties because `updateState` replaces its items in place. An assignment's `newValue` (`set`, `map-set`) still copies only array items, as in 1.0.44. A read of a named array property returns the raw value, so a write inside it sends nothing.
308
+ - **A cycle through the written object travels as a copy.** A value that reaches back into the object it is written to (`state.x = [toRaw(state)]`) reaches the replica with a copy of that object at the back-reference, because a path cannot name the object itself. A later write to the object changes its own place on the replica, not that copy. Keep such back-references out of synced state.
309
+ - **Shared graphs have a copy limit.** Payloads copy a shared object once per place. A payload that would make more than 100,000 extra copies (`SNAPSHOT_SHARED_COPY_LIMIT`) throws a `SnapshotLimitError` (a `RangeError`) before anything changes: `reactive()` throws before it returns a proxy, and a write throws with the state as it was, no event sent and no effect run, so a replica still matches. The `items` of one call are one payload. A root with `{ async: true }` refuses to remove a part over the limit too, because its copy is the `oldValue`: remove it in smaller parts. In a cyclic graph every part can reach the whole graph, so even one property can be too large to remove. A synchronous root sends what a write removes without a copy, as 1.0.44 did, and never refuses a removal, `Set.delete()` included. Then rebuild the root: make the state again from new objects without that graph (for example with `structuredClone` of the parts you keep), pass it to `reactive()` with the same emit function, and stop using the old root and its proxies. The new root's first event, a `replace` at the root path, resets every replica.
310
+
311
+ [Upgrading to 2.0.0](./docs/guide/upgrading-to-2.md) lists what a 1.x user must change, what else changed, and the measured costs.
312
+
313
+ A write through a proxy whose object is no longer in the tree changes the object and emits nothing. `getProxyStats().detachedWrites` counts those writes, so you can alert on them.
314
+
272
315
  ## Advanced Usage
273
316
 
274
317
  For more complex scenarios like state synchronization between different contexts, manual event handling, and detailed API documentation, see our [comprehensive documentation](https://Yiin.github.io/reactive-proxy-state/).