@yiin/reactive-proxy-state 1.0.10 → 1.0.12

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 (2) hide show
  1. package/README.md +114 -208
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -18,96 +18,66 @@ bun add @yiin/reactive-proxy-state
18
18
  # or yarn add @yiin/reactive-proxy-state
19
19
  ```
20
20
 
21
- ## Core Concepts
21
+ ## Quick Start
22
22
 
23
- 1. **Reactive State**: Create reactive versions of your objects using `reactive`. Any mutations to these wrapped objects will be tracked.
24
- 2. **Dependency Tracking**: When code inside a `watchEffect` reads a property of a reactive object, a dependency is established.
25
- 3. **Effect Triggering**: When a tracked property is mutated, any dependent effects (`watchEffect` or `watch` callbacks) are re-run **synchronously**.
23
+ The most common use case is creating reactive state for local applications:
26
24
 
27
- ## State Synchronization with `updateState`
25
+ ```typescript
26
+ import { reactive, watchEffect } from "@yiin/reactive-proxy-state";
28
27
 
29
- A key feature is `updateState`, which allows applying changes from a plain JavaScript object (often received from serialization) onto an existing reactive state object. It intelligently updates properties, adds/removes array elements, and modifies Maps/Sets to match the target structure, triggering reactive effects only where necessary.
28
+ // Create reactive state
29
+ const state = reactive({
30
+ count: 0,
31
+ user: { name: "Alice" },
32
+ items: ["apple", "banana"],
33
+ });
30
34
 
31
- This is typically used with the `emit` callback of `reactive` for state synchronization:
35
+ // Watch for changes
36
+ watchEffect(() => {
37
+ console.log(`Count: ${state.count}, User: ${state.user.name}`);
38
+ });
39
+ // Output: Count: 0, User: Alice
32
40
 
33
- ```typescript
34
- import { reactive, updateState, watchEffect } from '@yiin/reactive-proxy-state';
41
+ // Mutations automatically trigger effects
42
+ state.count++;
43
+ // Output: Count: 1, User: Alice
35
44
 
36
- // Assume these functions exist:
37
- // - getInitialStateFromServer(): Fetches the initial state snapshot.
38
- // - sendEventToClient(event): Sends a state change event to the client.
39
- // - listenForServerEvents(callback): Sets up a listener for events from the server.
45
+ state.user.name = "Bob";
46
+ // Output: Count: 1, User: Bob
40
47
 
41
- // --- Source State (e.g., Server) ---
42
- const sourceData = {
43
- counter: 0,
44
- user: { name: 'Alice' },
45
- items: ['a']
46
- };
48
+ state.items.push("orange");
49
+ // Arrays, Maps, and Sets are also reactive
50
+ ```
47
51
 
48
- // 1. Server creates reactive state & emits deltas via sendEventToClient
49
- const sourceState = reactive(sourceData, sendEventToClient);
52
+ ## Core Concepts
50
53
 
54
+ 1. **Reactive State**: Create reactive versions of your objects using `reactive`. Any mutations to these wrapped objects will be tracked.
55
+ 2. **Dependency Tracking**: When code inside a `watchEffect` reads a property of a reactive object, a dependency is established.
56
+ 3. **Effect Triggering**: When a tracked property is mutated, any dependent effects (`watchEffect` or `watch` callbacks) are re-run **synchronously**.
51
57
 
52
- // --- Client Initialization & Sync ---
53
- // 2. Client creates its reactive state holder *before* data arrives
54
- const targetState = reactive({});
55
- console.log('Client: Initial empty target state created:', targetState);
58
+ ## Advanced: State Synchronization
56
59
 
57
- // 3. Client watches its local state for changes (e.g., for UI updates)
58
- watchEffect(() => {
59
- console.log('Client: Target state updated:', targetState);
60
- });
61
- // Initial output: Client: Target state updated: {}
62
-
63
- // 4. Client fetches the initial state snapshot
64
- const initialSnapshot = getInitialStateFromServer(); // Assume fetches { counter: 0, ... }
65
- console.log('\n--- Client Received Initial Snapshot ---');
66
- console.log(initialSnapshot);
67
-
68
- // 5. Client applies the initial snapshot using a 'replace' action
69
- // (Requires updateState implementation to support action: 'replace')
70
- updateState(targetState, {
71
- action: 'replace',
72
- path: [], // Apply to the root
73
- newValue: initialSnapshot
74
- });
75
- // Output after 'replace':
76
- // Client: Target state updated: { counter: 0, user: { name: 'Alice' }, items: [ 'a' ] }
77
-
78
- // 6. Client starts listening for subsequent delta events from the server
79
- listenForServerEvents((event) => {
80
- console.log(`Client: Received delta event <- Server:`, event);
81
- // Apply the delta event normally
82
- updateState(targetState, event);
60
+ For advanced use cases like server-client synchronization, you can track state changes and apply them elsewhere:
61
+
62
+ ```typescript
63
+ import { reactive, updateState } from "@yiin/reactive-proxy-state";
64
+
65
+ // Server: Track changes and send them to clients
66
+ const serverState = reactive({ count: 0 }, (event) => {
67
+ // Send event to all connected clients
68
+ broadcastToClients(event);
83
69
  });
84
70
 
71
+ // Client: Apply changes received from server
72
+ const clientState = reactive({});
73
+
74
+ // When client receives events from server
75
+ onServerEvent((event) => {
76
+ updateState(clientState, event);
77
+ });
85
78
 
86
- // --- Subsequent Server Modifications ---
87
- // 7. Server state is modified *after* the client has initialized
88
- console.log('\n--- Server Modifying State (Post-Init) ---');
89
- sourceState.counter++;
90
- // Output: (sendEventToClient called with { action: 'set', path: [ 'counter' ], ... })
91
- sourceState.user.name = 'Charlie';
92
- // Output: (sendEventToClient called with { action: 'set', path: [ 'user', 'name' ], ... })
93
- sourceState.items.push('b');
94
- // Output: (sendEventToClient called with { action: 'array-push', path: [ 'items' ], ... })
95
-
96
- // --- Delta Events arrive and are processed asynchronously by the client ---
97
-
98
- // Example Console Output Order (assuming async processing):
99
- // Client: Initial empty target state created: {}
100
- // Client: Target state updated: {}
101
- // --- Client Received Initial Snapshot ---
102
- // { counter: 0, user: { name: 'Alice' }, items: ['a'] }
103
- // Client: Target state updated: { counter: 0, user: { name: 'Alice' }, items: [ 'a' ] } // From 'replace'
104
- // --- Server Modifying State (Post-Init) ---
105
- // Client: Received delta event <- Server: { action: 'set', path: [ 'counter' ], oldValue: 0, newValue: 1 }
106
- // Client: Target state updated: { counter: 1, user: { name: 'Alice' }, items: [ 'a' ] }
107
- // Client: Received delta event <- Server: { action: 'set', path: [ 'user', 'name' ], oldValue: 'Alice', newValue: 'Charlie' }
108
- // Client: Target state updated: { counter: 1, user: { name: 'Charlie' }, items: [ 'a' ] }
109
- // Client: Received delta event <- Server: { action: 'array-push', path: [ 'items' ], key: 1, items: [ 'b' ] }
110
- // Client: Target state updated: { counter: 1, user: { name: 'Charlie' }, items: [ 'a', 'b' ] }
79
+ // Now both states stay in sync
80
+ serverState.count = 5; // Automatically synced to all clients
111
81
  ```
112
82
 
113
83
  See the [`updateState` documentation](./docs/api/update-state.md) and [`reactive` documentation](./docs/api/reactive.md) for more details on event emission and application.
@@ -119,21 +89,21 @@ See the [`updateState` documentation](./docs/api/update-state.md) and [`reactive
119
89
  Creates a reactive proxy for the given object, Array, Map, or Set. Nested objects/collections are also recursively wrapped.
120
90
 
121
91
  ```typescript
122
- import { reactive } from '@yiin/reactive-proxy-state';
92
+ import { reactive } from "@yiin/reactive-proxy-state";
123
93
 
124
94
  const state = reactive({
125
95
  count: 0,
126
- user: { name: 'Alice' },
127
- items: ['a', 'b'],
128
- settings: new Map([['theme', 'dark']]),
129
- ids: new Set([1, 2])
96
+ user: { name: "Alice" },
97
+ items: ["a", "b"],
98
+ settings: new Map([["theme", "dark"]]),
99
+ ids: new Set([1, 2]),
130
100
  });
131
101
 
132
102
  // Mutations to 'state' and its nested properties/elements will be tracked.
133
103
  state.count++;
134
- state.user.name = 'Bob';
135
- state.items.push('c');
136
- state.settings.set('theme', 'light');
104
+ state.user.name = "Bob";
105
+ state.items.push("c");
106
+ state.settings.set("theme", "light");
137
107
  state.ids.add(3);
138
108
  ```
139
109
 
@@ -141,17 +111,17 @@ state.ids.add(3);
141
111
 
142
112
  Creates a reactive "reference" object for any value type (primitive or object). The value is accessed and mutated through the `.value` property. Reactivity is tracked on the `.value` property itself.
143
113
 
144
- **Note:** If a plain object is passed to `ref`, the object *itself* is not made deeply reactive. Only assignment to the `.value` property is tracked. Use `reactive` for deep object reactivity.
114
+ **Note:** If a plain object is passed to `ref`, the object _itself_ is not made deeply reactive. Only assignment to the `.value` property is tracked. Use `reactive` for deep object reactivity.
145
115
 
146
116
  ```typescript
147
- import { ref, watchEffect, isRef, unref } from '@yiin/reactive-proxy-state';
117
+ import { ref, watchEffect, isRef, unref } from "@yiin/reactive-proxy-state";
148
118
 
149
119
  // Ref for a primitive
150
120
  const count = ref(0);
151
121
  console.log(count.value); // 0
152
122
 
153
123
  watchEffect(() => {
154
- console.log('Count is:', count.value);
124
+ console.log("Count is:", count.value);
155
125
  });
156
126
  // Output: Count is: 0
157
127
 
@@ -159,19 +129,19 @@ count.value++; // Triggers the effect
159
129
  // Output: Count is: 1
160
130
 
161
131
  // Ref for an object
162
- const user = ref({ name: 'Alice' });
132
+ const user = ref({ name: "Alice" });
163
133
 
164
134
  watchEffect(() => {
165
135
  // This effect depends on the object reference stored in user.value
166
- console.log('User object:', user.value);
136
+ console.log("User object:", user.value);
167
137
  });
168
138
  // Output: User object: { name: 'Alice' }
169
139
 
170
140
  // Mutating the inner object DOES NOT trigger the effect above
171
- user.value.name = 'Bob';
141
+ user.value.name = "Bob";
172
142
 
173
143
  // Assigning a new object DOES trigger the effect
174
- user.value = { name: 'Charles' };
144
+ user.value = { name: "Charles" };
175
145
  // Output: User object: { name: 'Charles' }
176
146
 
177
147
  // Helpers
@@ -183,100 +153,39 @@ console.log(unref(123)); // 123 (returns non-refs as is)
183
153
  ```
184
154
 
185
155
  ### `computed<T>(getter: () => T): ComputedRef<T>`
156
+
186
157
  ### `computed<T>(options: { get: () => T, set: (value: T) => void }): WritableComputedRef<T>`
187
158
 
188
159
  Creates a computed property based on a getter function or a getter/setter pair.
189
160
 
190
- - **Getter-only:** The getter tracks reactive dependencies (`ref`s or reactive object properties) and its result is cached. The computed value only recalculates when a dependency changes. Computed refs created this way are **read-only**.
191
- - **Getter/Setter:** Provides both a getter for deriving the value and a setter for mutating underlying reactive state when the computed ref's `.value` is assigned.
161
+ - **Getter-only:** The getter tracks reactive dependencies (`ref`s or reactive object properties) and its result is cached. The computed value only recalculates when a dependency changes. Computed refs created this way are **read-only**.
162
+ - **Getter/Setter:** Provides both a getter for deriving the value and a setter for mutating underlying reactive state when the computed ref's `.value` is assigned.
192
163
 
193
164
  ```typescript
194
- import { ref, computed, watchEffect, isComputed } from '@yiin/reactive-proxy-state';
165
+ import { ref, computed } from "@yiin/reactive-proxy-state";
195
166
 
196
167
  // Read-only computed
197
- const firstName = ref('John');
198
- const lastName = ref('Doe');
199
-
200
- const readOnlyFullName = computed(() => {
201
- console.log('Computing readOnlyFullName...');
202
- return `${firstName.value} ${lastName.value}`;
203
- });
204
-
205
- // Accessing .value triggers computation
206
- console.log(readOnlyFullName.value);
207
- // Output: Computing readOnlyFullName...
208
- // Output: John Doe
168
+ const firstName = ref("John");
169
+ const lastName = ref("Doe");
209
170
 
210
- // Accessing again uses the cache
211
- console.log(readOnlyFullName.value);
212
- // Output: John Doe
171
+ const fullName = computed(() => `${firstName.value} ${lastName.value}`);
172
+ console.log(fullName.value); // John Doe
213
173
 
214
- watchEffect(() => {
215
- console.log('Read-only full name changed:', readOnlyFullName.value);
216
- });
217
- // Output: Read-only full name changed: John Doe
218
-
219
- // Changing a dependency marks computed as dirty
220
- firstName.value = 'Jane';
221
-
222
- // Accessing .value again triggers re-computation and the effect
223
- console.log(readOnlyFullName.value);
224
- // Output: Computing readOnlyFullName...
225
- // Output: Read-only full name changed: Jane Doe
226
- // Output: Jane Doe
227
-
228
- // Chained computed
229
- const message = computed(() => `User: ${readOnlyFullName.value}`);
230
- console.log(message.value); // User: Jane Doe
231
-
232
- lastName.value = 'Smith';
233
- // Output: Computing readOnlyFullName...
234
- // Output: Read-only full name changed: Jane Smith
235
- console.log(message.value); // User: Jane Smith (message recomputed automatically)
236
-
237
- // Read-only check
238
- console.warn = () => console.log('Warning triggered!'); // Mock console.warn
239
- try {
240
- (readOnlyFullName as any).value = 'Test'; // Triggers warning
241
- } catch (e) { /* ... */ }
242
- // Output: Warning triggered!
243
- console.log(readOnlyFullName.value); // Jane Smith (value unchanged)
174
+ firstName.value = "Jane";
175
+ console.log(fullName.value); // Jane Doe
244
176
 
245
177
  // Writable computed
246
- const source = ref(1);
247
- const plusOne = computed({
248
- get: () => source.value + 1,
249
- set: (newValue) => {
250
- console.log(`Setting source based on new value: ${newValue}`);
251
- source.value = newValue - 1;
252
- }
253
- });
254
-
255
- console.log(plusOne.value); // 2 (Initial get)
256
- console.log(source.value); // 1
257
-
258
- watchEffect(() => {
259
- console.log('Writable computed changed:', plusOne.value);
178
+ const count = ref(1);
179
+ const doubled = computed({
180
+ get: () => count.value * 2,
181
+ set: (value) => {
182
+ count.value = value / 2;
183
+ },
260
184
  });
261
- // Output: Writable computed changed: 2
262
-
263
- // Set the writable computed value
264
- plusOne.value = 10;
265
- // Output: Setting source based on new value: 10
266
- // Output: Writable computed changed: 10
267
185
 
268
- console.log(plusOne.value); // 10
269
- console.log(source.value); // 9 (Source was updated by the setter)
270
-
271
- // Changing the source ref also updates the computed
272
- source.value = 20;
273
- // Output: Writable computed changed: 21
274
- console.log(plusOne.value); // 21
275
-
276
- // Helper
277
- console.log(isComputed(readOnlyFullName)); // true
278
- console.log(isComputed(plusOne)); // true
279
- console.log(isComputed(firstName)); // false
186
+ console.log(doubled.value); // 2
187
+ doubled.value = 10;
188
+ console.log(count.value); // 5
280
189
  ```
281
190
 
282
191
  ### `watchEffect(effect: () => void, options?: WatchEffectOptions)`
@@ -284,22 +193,22 @@ console.log(isComputed(firstName)); // false
284
193
  Runs a function immediately, tracks its reactive dependencies, and re-runs it synchronously whenever any of those dependencies change.
285
194
 
286
195
  `WatchEffectOptions`:
287
- * `onTrack?(event)`: Debug hook called when a dependency is tracked.
288
- * `onTrigger?(event)`: Debug hook called when the effect is triggered by a mutation.
196
+
197
+ - `onTrack?(event)`: Debug hook called when a dependency is tracked.
198
+ - `onTrigger?(event)`: Debug hook called when the effect is triggered by a mutation.
289
199
 
290
200
  ```typescript
291
- import { reactive, ref, watchEffect } from '@yiin/reactive-proxy-state';
201
+ import { reactive, watchEffect } from "@yiin/reactive-proxy-state";
292
202
 
293
- // ... existing watchEffect example using reactive ...
203
+ const state = reactive({ count: 0 });
294
204
 
295
- // Using watchEffect with refs
296
- const counter = ref(10);
297
205
  watchEffect(() => {
298
- console.log('Counter:', counter.value);
206
+ console.log("Count:", state.count);
299
207
  });
300
- // Output: Counter: 10
301
- counter.value--;
302
- // Output: Counter: 9
208
+ // Output: Count: 0
209
+
210
+ state.count++;
211
+ // Output: Count: 1
303
212
  ```
304
213
 
305
214
  ### `watch<T>(source: WatchSource<T> | T, callback: (newValue: T, oldValue: T | undefined) => void, options?: WatchOptions)`
@@ -309,30 +218,23 @@ Watches a specific reactive source (either a getter function, a direct reactive
309
218
  `WatchSource<T>`: A function that returns the value to watch, or a `ref`.
310
219
  `callback`: Function executed on change. Receives the new value and the old value.
311
220
  `WatchOptions`:
312
- * `immediate?: boolean`: If `true`, runs the callback immediately with the initial value (oldValue will be `undefined`). Defaults to `false`.
313
- * `deep?: boolean`: If `true`, deeply traverses the source for dependency tracking and uses deep comparison logic. **Defaults to `true`**. Set to `false` for shallow watching (only triggers on direct assignment or identity change).
314
221
 
315
- ```typescript
316
- import { reactive, ref, watch } from '@yiin/reactive-proxy-state';
222
+ - `immediate?: boolean`: If `true`, runs the callback immediately with the initial value (oldValue will be `undefined`). Defaults to `false`.
223
+ - `deep?: boolean`: If `true`, deeply traverses the source for dependency tracking and uses deep comparison logic. **Defaults to `true`**. Set to `false` for shallow watching (only triggers on direct assignment or identity change).
317
224
 
318
- // ... existing watch examples using reactive ...
225
+ ```typescript
226
+ import { reactive, watch } from "@yiin/reactive-proxy-state";
319
227
 
320
- // Watching a ref
321
- const count = ref(0);
322
- watch(count, (newVal, oldVal) => {
323
- console.log(`Count changed from ${oldVal} to ${newVal}`);
324
- });
325
- count.value = 5; // Output: Count changed from 0 to 5
228
+ const state = reactive({ count: 0 });
326
229
 
327
- // Watching a getter involving a ref
328
- const doubleCount = ref(100);
329
230
  watch(
330
- () => doubleCount.value * 2,
331
- (newDouble, oldDouble) => {
332
- console.log(`Double changed from ${oldDouble} to ${newDouble}`);
231
+ () => state.count,
232
+ (newVal, oldVal) => {
233
+ console.log(`Count changed from ${oldVal} to ${newVal}`);
333
234
  }
334
235
  );
335
- doubleCount.value = 110; // Output: Double changed from 200 to 220
236
+
237
+ state.count = 5; // Output: Count changed from 0 to 5
336
238
  ```
337
239
 
338
240
  ## Collections (Arrays, Maps, Sets)
@@ -340,21 +242,25 @@ doubleCount.value = 110; // Output: Double changed from 200 to 220
340
242
  `reactive` automatically handles Arrays, Maps, and Sets. Mutations via standard methods (`push`, `pop`, `splice`, `set`, `delete`, `add`, `clear`, etc.) are reactive and will trigger effects that depend on the collection or its contents (if watched deeply).
341
243
 
342
244
  ```typescript
343
- import { reactive, watchEffect } from '@yiin/reactive-proxy-state';
245
+ import { reactive, watchEffect } from "@yiin/reactive-proxy-state";
344
246
 
345
247
  const state = reactive({
346
248
  list: [1, 2],
347
249
  data: new Map<string, number>(),
348
- tags: new Set<string>()
250
+ tags: new Set<string>(),
349
251
  });
350
252
 
351
- watchEffect(() => console.log('List size:', state.list.length));
352
- watchEffect(() => console.log('Data has "foo":', state.data.has('foo')));
353
- watchEffect(() => console.log('Tags:', Array.from(state.tags).join(', ')));
253
+ watchEffect(() => console.log("List size:", state.list.length));
254
+ watchEffect(() => console.log('Data has "foo":', state.data.has("foo")));
255
+ watchEffect(() => console.log("Tags:", Array.from(state.tags).join(", ")));
354
256
 
355
- state.list.push(3); // Output: List size: 3
356
- state.data.set('foo', 100); // Output: Data has "foo": true
357
- state.tags.add('important'); // Output: Tags: important
358
- state.data.delete('foo'); // Output: Data has "foo": false
359
- state.tags.add('urgent'); // Output: Tags: important, urgent
257
+ state.list.push(3); // Output: List size: 3
258
+ state.data.set("foo", 100); // Output: Data has "foo": true
259
+ state.tags.add("important"); // Output: Tags: important
260
+ state.data.delete("foo"); // Output: Data has "foo": false
261
+ state.tags.add("urgent"); // Output: Tags: important, urgent
360
262
  ```
263
+
264
+ ## Advanced Usage
265
+
266
+ 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/).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yiin/reactive-proxy-state",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "author": "Yiin <stanislovas@yiin.lt>",
5
5
  "repository": {
6
6
  "type": "git",