@coherent.js/state 1.1.2 → 2.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.
@@ -14,11 +14,107 @@ var globalErrorHandler = {
14
14
  console.error("State Error:", error.message, context);
15
15
  }
16
16
  };
17
- var Observable = class _Observable {
17
+ var MAX_FLUSH_ITERATIONS = 100;
18
+ var activeComputed = null;
19
+ var batchDepth = 0;
20
+ var flushing = false;
21
+ var pendingObservables = /* @__PURE__ */ new Map();
22
+ var pendingComputeds = /* @__PURE__ */ new Set();
23
+ var globalVersion = 0;
24
+ function reportError(source, error, type, context = {}) {
25
+ const onError = source?._options?.onError;
26
+ if (typeof onError === "function") {
27
+ try {
28
+ onError(error, { type, ...context });
29
+ return;
30
+ } catch (handlerError) {
31
+ error = handlerError;
32
+ }
33
+ }
34
+ globalErrorHandler.handle(error, { type, context });
35
+ }
36
+ function runObserver(source, observer, newValue, oldValue) {
37
+ try {
38
+ observer.callback(newValue, oldValue, observer.unwatch);
39
+ } catch (error) {
40
+ reportError(source, error, "watcher-error", { newValue, oldValue });
41
+ }
42
+ }
43
+ function flush() {
44
+ flushing = true;
45
+ let iterations = 0;
46
+ try {
47
+ while (pendingObservables.size > 0 || pendingComputeds.size > 0) {
48
+ if (++iterations > MAX_FLUSH_ITERATIONS) {
49
+ const culprits = [...pendingObservables.keys(), ...pendingComputeds];
50
+ pendingObservables.clear();
51
+ pendingComputeds.clear();
52
+ reportError(
53
+ culprits[0],
54
+ new StateError(
55
+ `Watchers kept changing state after ${MAX_FLUSH_ITERATIONS} rounds; a watcher probably writes a new value to a state it (indirectly) watches.`,
56
+ { type: "update-depth" }
57
+ ),
58
+ "update-depth"
59
+ );
60
+ return;
61
+ }
62
+ const observables = [...pendingObservables];
63
+ pendingObservables.clear();
64
+ for (const [source, oldValue] of observables) {
65
+ const newValue = source._value;
66
+ if (!source._changed(oldValue, newValue)) continue;
67
+ for (const observer of [...source._observers]) {
68
+ if (source._observers.has(observer)) {
69
+ runObserver(source, observer, newValue, oldValue);
70
+ }
71
+ }
72
+ }
73
+ const computeds = [...pendingComputeds];
74
+ pendingComputeds.clear();
75
+ for (const source of computeds) {
76
+ if (source._observers.size === 0) continue;
77
+ try {
78
+ source._refresh();
79
+ } catch (error) {
80
+ reportError(source, error, "computed-error");
81
+ continue;
82
+ }
83
+ const newValue = source._value;
84
+ const oldValue = source._lastNotified;
85
+ if (Object.is(newValue, oldValue)) continue;
86
+ source._lastNotified = newValue;
87
+ for (const observer of [...source._observers]) {
88
+ if (source._observers.has(observer)) {
89
+ runObserver(source, observer, newValue, oldValue);
90
+ }
91
+ }
92
+ }
93
+ }
94
+ } finally {
95
+ flushing = false;
96
+ }
97
+ }
98
+ function scheduleFlush() {
99
+ if (batchDepth === 0 && !flushing) {
100
+ flush();
101
+ }
102
+ }
103
+ function batch(fn) {
104
+ batchDepth++;
105
+ try {
106
+ return fn();
107
+ } finally {
108
+ batchDepth--;
109
+ scheduleFlush();
110
+ }
111
+ }
112
+ var Observable = class {
18
113
  constructor(value, options = {}) {
19
114
  this._value = value;
115
+ this._version = 0;
20
116
  this._observers = /* @__PURE__ */ new Set();
21
- this._computedDependents = /* @__PURE__ */ new Set();
117
+ this._subscribers = /* @__PURE__ */ new Set();
22
118
  this._options = {
23
119
  deep: options.deep !== false,
24
120
  immediate: options.immediate !== false,
@@ -26,107 +122,281 @@ var Observable = class _Observable {
26
122
  };
27
123
  }
28
124
  get value() {
29
- if (_Observable._currentComputed) {
30
- this._computedDependents.add(_Observable._currentComputed);
31
- }
125
+ activeComputed?._track(this);
32
126
  return this._value;
33
127
  }
34
128
  set value(newValue) {
35
- if (this._value === newValue && !this._options.deep) {
129
+ this._write(newValue);
130
+ }
131
+ /** Read without registering a dependency. */
132
+ peek() {
133
+ return this._value;
134
+ }
135
+ /**
136
+ * Whether a write from `oldValue` to `newValue` is a change. Identical
137
+ * primitives never are; with `deep` (the default) re-assigning the same
138
+ * object is, since it may have been mutated in place.
139
+ * @private
140
+ */
141
+ _changed(oldValue, newValue) {
142
+ if (!Object.is(oldValue, newValue)) return true;
143
+ return this._options.deep && newValue !== null && typeof newValue === "object";
144
+ }
145
+ /** @private */
146
+ _write(newValue) {
147
+ const oldValue = this._value;
148
+ if (!this._changed(oldValue, newValue)) {
36
149
  return;
37
150
  }
38
- const oldValue = this._value;
39
151
  this._value = newValue;
40
- this._observers.forEach((observer) => {
41
- try {
42
- observer(newValue, oldValue);
43
- } catch (_error) {
44
- globalErrorHandler.handle(_error, {
45
- type: "watcher-_error",
46
- context: { newValue, oldValue }
47
- });
48
- }
49
- });
50
- this._computedDependents.forEach((computed2) => {
51
- computed2._invalidate();
52
- });
152
+ this._version++;
153
+ globalVersion++;
154
+ for (const subscriber of [...this._subscribers]) {
155
+ subscriber._markDirty();
156
+ }
157
+ if (this._observers.size > 0 && !pendingObservables.has(this)) {
158
+ pendingObservables.set(this, oldValue);
159
+ }
160
+ scheduleFlush();
161
+ }
162
+ /** @private */
163
+ _addSubscriber(computed2) {
164
+ this._subscribers.add(computed2);
165
+ }
166
+ /** @private */
167
+ _removeSubscriber(computed2) {
168
+ this._subscribers.delete(computed2);
169
+ }
170
+ /** @private */
171
+ _addObserver(observer) {
172
+ this._observers.add(observer);
173
+ }
174
+ /** @private */
175
+ _removeObserver(observer) {
176
+ this._observers.delete(observer);
53
177
  }
54
178
  watch(callback, options = {}) {
55
179
  if (typeof callback !== "function") {
56
180
  throw new StateError("Watch callback must be a function");
57
181
  }
58
- const observer = (newValue, oldValue) => {
59
- callback(newValue, oldValue, () => this.unwatch(observer));
60
- };
61
- this._observers.add(observer);
182
+ const observer = { callback, unwatch: null };
183
+ observer.unwatch = () => this._removeObserver(observer);
184
+ this._addObserver(observer);
62
185
  if (options.immediate !== false) {
63
- observer(this._value, void 0);
186
+ runObserver(this, observer, this._value, void 0);
64
187
  }
65
- return () => this.unwatch(observer);
188
+ return observer.unwatch;
66
189
  }
67
- unwatch(observer) {
68
- this._observers.delete(observer);
190
+ /**
191
+ * Remove a watcher, by the callback passed to watch()
192
+ * @param {Function} callback
193
+ */
194
+ unwatch(callback) {
195
+ for (const observer of [...this._observers]) {
196
+ if (observer.callback === callback || observer.unwatch === callback) {
197
+ this._removeObserver(observer);
198
+ }
199
+ }
69
200
  }
201
+ /** Remove every watcher. */
70
202
  unwatchAll() {
71
- this._observers.clear();
72
- this._computedDependents.clear();
203
+ for (const observer of [...this._observers]) {
204
+ this._removeObserver(observer);
205
+ }
73
206
  }
74
207
  };
75
- var Computed = class extends Observable {
208
+ var Computed = class _Computed extends Observable {
76
209
  constructor(getter, options = {}) {
77
- super(void 0, options);
78
- this._getter = getter;
79
- this._cached = false;
80
- this._dirty = true;
81
210
  if (typeof getter !== "function") {
82
211
  throw new StateError("Computed getter must be a function");
83
212
  }
213
+ super(void 0, options);
214
+ this._getter = getter;
215
+ this._deps = /* @__PURE__ */ new Map();
216
+ this._dirty = true;
217
+ this._computing = false;
218
+ this._globalVersionSeen = -1;
219
+ this._lastNotified = void 0;
84
220
  }
85
221
  get value() {
86
- if (this._dirty || !this._cached) {
87
- this._compute();
88
- }
222
+ this._refresh();
223
+ activeComputed?._track(this);
89
224
  return this._value;
90
225
  }
91
- set value(newValue) {
226
+ set value(_newValue) {
92
227
  throw new StateError("Cannot set value on computed property");
93
228
  }
94
- _compute() {
95
- const prevComputed = Observable._currentComputed;
96
- Observable._currentComputed = this;
97
- try {
98
- const newValue = this._getter();
99
- if (newValue !== this._value) {
100
- const oldValue = this._value;
101
- this._value = newValue;
102
- this._observers.forEach((observer) => {
103
- observer(newValue, oldValue);
104
- });
105
- }
106
- this._cached = true;
107
- this._dirty = false;
108
- } catch (_error) {
109
- globalErrorHandler.handle(_error, {
110
- type: "computed-_error",
111
- context: { getter: this._getter.toString() }
229
+ peek() {
230
+ this._refresh();
231
+ return this._value;
232
+ }
233
+ /** Watched, or a dependency of a live computed. @private */
234
+ get _live() {
235
+ return this._observers.size > 0 || this._subscribers.size > 0;
236
+ }
237
+ /** @private */
238
+ _track(source) {
239
+ if (!this._deps.has(source)) {
240
+ this._deps.set(source, source._version);
241
+ }
242
+ }
243
+ /** Bring the cached value up to date. @private */
244
+ _refresh() {
245
+ if (this._computing) {
246
+ throw new StateError("Circular dependency between computed properties", {
247
+ type: "computed-cycle",
248
+ context: { getter: this._getter.name || "anonymous" }
112
249
  });
250
+ }
251
+ if (!this._dirty) {
252
+ if (this._live || this._globalVersionSeen === globalVersion) return;
253
+ if (!this._dependenciesChanged()) {
254
+ this._globalVersionSeen = globalVersion;
255
+ return;
256
+ }
257
+ }
258
+ this._recompute();
259
+ }
260
+ /** @private */
261
+ _dependenciesChanged() {
262
+ for (const [source, version] of this._deps) {
263
+ if (source instanceof _Computed) {
264
+ source._refresh();
265
+ }
266
+ if (source._version !== version) return true;
267
+ }
268
+ return false;
269
+ }
270
+ /** @private */
271
+ _recompute() {
272
+ const previousDeps = this._deps;
273
+ const previousActive = activeComputed;
274
+ this._deps = /* @__PURE__ */ new Map();
275
+ this._computing = true;
276
+ activeComputed = this;
277
+ let newValue;
278
+ try {
279
+ newValue = this._getter();
280
+ } catch (error) {
281
+ this._deps = previousDeps;
282
+ this._dirty = true;
283
+ throw error;
113
284
  } finally {
114
- Observable._currentComputed = prevComputed;
285
+ this._computing = false;
286
+ activeComputed = previousActive;
287
+ }
288
+ if (this._live) {
289
+ for (const source of previousDeps.keys()) {
290
+ if (!this._deps.has(source)) source._removeSubscriber(this);
291
+ }
292
+ for (const source of this._deps.keys()) {
293
+ if (!previousDeps.has(source)) source._addSubscriber(this);
294
+ }
295
+ }
296
+ this._dirty = false;
297
+ this._globalVersionSeen = globalVersion;
298
+ if (!Object.is(newValue, this._value)) {
299
+ this._value = newValue;
300
+ this._version++;
115
301
  }
116
302
  }
117
- _invalidate() {
303
+ /** A dependency changed. @private */
304
+ _markDirty() {
305
+ if (this._dirty) return;
118
306
  this._dirty = true;
119
- this._computedDependents.forEach((computed2) => {
120
- computed2._invalidate();
121
- });
307
+ if (this._observers.size > 0) {
308
+ pendingComputeds.add(this);
309
+ }
310
+ for (const subscriber of [...this._subscribers]) {
311
+ subscriber._markDirty();
312
+ }
313
+ }
314
+ /** Subscribe to dependencies. @private */
315
+ _goLive() {
316
+ this._refresh();
317
+ for (const source of this._deps.keys()) {
318
+ source._addSubscriber(this);
319
+ }
320
+ }
321
+ /** Unsubscribe from dependencies. @private */
322
+ _goLazy() {
323
+ for (const source of this._deps.keys()) {
324
+ source._removeSubscriber(this);
325
+ }
326
+ }
327
+ /** @private */
328
+ _addSubscriber(computed2) {
329
+ const wasLive = this._live;
330
+ this._subscribers.add(computed2);
331
+ if (!wasLive) this._goLive();
332
+ }
333
+ /** @private */
334
+ _removeSubscriber(computed2) {
335
+ this._subscribers.delete(computed2);
336
+ if (!this._live) this._goLazy();
337
+ }
338
+ /** @private */
339
+ _addObserver(observer) {
340
+ const hadObservers = this._observers.size > 0;
341
+ const wasLive = this._live;
342
+ this._observers.add(observer);
343
+ try {
344
+ if (wasLive) {
345
+ this._refresh();
346
+ } else {
347
+ this._goLive();
348
+ }
349
+ } catch (error) {
350
+ this._observers.delete(observer);
351
+ throw error;
352
+ }
353
+ if (!hadObservers) {
354
+ this._lastNotified = this._value;
355
+ }
356
+ }
357
+ /** @private */
358
+ _removeObserver(observer) {
359
+ if (!this._observers.delete(observer)) return;
360
+ if (!this._live) this._goLazy();
122
361
  }
123
362
  };
124
- Observable._currentComputed = null;
363
+ var ABSENT = /* @__PURE__ */ Symbol("absent");
364
+ function isPath(key) {
365
+ return typeof key === "string" && key.includes(".");
366
+ }
367
+ function readPath(value, segments) {
368
+ let current = value;
369
+ for (const segment of segments) {
370
+ if (current === null || current === void 0) return void 0;
371
+ current = current[segment];
372
+ }
373
+ return current;
374
+ }
375
+ function writePath(target, segments, value) {
376
+ const [head, ...rest] = segments;
377
+ const base = target !== null && typeof target === "object" ? Array.isArray(target) ? [...target] : { ...target } : {};
378
+ const next = rest.length === 0 ? value : writePath(base[head], rest, value);
379
+ Object.defineProperty(base, head, { value: next, enumerable: true, writable: true, configurable: true });
380
+ return base;
381
+ }
382
+ function deletePath(target, segments) {
383
+ if (target === null || typeof target !== "object") return target;
384
+ const [head, ...rest] = segments;
385
+ if (!Object.prototype.hasOwnProperty.call(target, head)) return target;
386
+ const base = Array.isArray(target) ? [...target] : { ...target };
387
+ if (rest.length === 0) {
388
+ delete base[head];
389
+ } else {
390
+ base[head] = deletePath(base[head], rest);
391
+ }
392
+ return base;
393
+ }
125
394
  var ReactiveState = class {
126
395
  constructor(initialState = {}, options = {}) {
127
396
  this._state = /* @__PURE__ */ new Map();
128
397
  this._computed = /* @__PURE__ */ new Map();
129
398
  this._watchers = /* @__PURE__ */ new Map();
399
+ this._expressionWatchers = /* @__PURE__ */ new Set();
130
400
  this._middleware = [];
131
401
  this._history = [];
132
402
  this._options = {
@@ -140,57 +410,115 @@ var ReactiveState = class {
140
410
  this.set(key, value);
141
411
  });
142
412
  }
413
+ /** Observable for a key, created as an absent placeholder if needed. @private */
414
+ _observable(key) {
415
+ let observable2 = this._state.get(key);
416
+ if (!observable2) {
417
+ observable2 = new Observable(ABSENT, this._options);
418
+ this._state.set(key, observable2);
419
+ }
420
+ return observable2;
421
+ }
422
+ /** Whether a key is stored under its full name. @private */
423
+ _hasOwnKey(key) {
424
+ const observable2 = this._state.get(key);
425
+ return Boolean(observable2) && observable2._value !== ABSENT;
426
+ }
427
+ /** [rootKey, pathSegments] for a dot path, or null for a plain key. @private */
428
+ _splitPath(key) {
429
+ if (!isPath(key)) return null;
430
+ const [root, ...segments] = key.split(".");
431
+ return [root, segments];
432
+ }
143
433
  /**
144
434
  * Get reactive state value
145
435
  */
146
436
  get(key) {
147
- const observable2 = this._state.get(key);
148
- return observable2 ? observable2.value : void 0;
437
+ const path = this._splitPath(key);
438
+ if (path) {
439
+ return readPath(this.get(path[0]), path[1]);
440
+ }
441
+ const observable2 = activeComputed ? this._observable(key) : this._state.get(key);
442
+ if (!observable2) return void 0;
443
+ const value = observable2.value;
444
+ return value === ABSENT ? void 0 : value;
149
445
  }
150
446
  /**
151
447
  * Set reactive state value
152
448
  */
153
449
  set(key, value, options = {}) {
154
450
  const config = { ...this._options, ...options };
451
+ const oldValue = this.get(key);
155
452
  if (config.enableMiddleware) {
156
- const middlewareResult = this._runMiddleware("set", { key, value, oldValue: this.get(key) });
453
+ const middlewareResult = this._runMiddleware("set", { key, value, oldValue });
157
454
  if (middlewareResult.cancelled) {
158
455
  return false;
159
456
  }
160
457
  value = middlewareResult.value !== void 0 ? middlewareResult.value : value;
161
458
  }
162
- let observable2 = this._state.get(key);
163
- if (!observable2) {
164
- observable2 = new Observable(value, config);
165
- this._state.set(key, observable2);
166
- } else {
459
+ const path = this._splitPath(key);
460
+ if (path) {
461
+ const [root, segments] = path;
167
462
  if (config.enableHistory) {
168
- this._addToHistory("set", key, observable2.value, value);
463
+ this._addToHistory("set", key, oldValue, value);
169
464
  }
170
- observable2.value = value;
465
+ this._writeKey(root, writePath(this.get(root), segments, value));
466
+ return true;
467
+ }
468
+ if (config.enableHistory && this._hasOwnKey(key)) {
469
+ this._addToHistory("set", key, oldValue, value);
171
470
  }
471
+ this._writeKey(key, value);
172
472
  return true;
173
473
  }
474
+ /** @private */
475
+ _writeKey(key, value) {
476
+ this._observable(key)._write(value);
477
+ }
174
478
  /**
175
479
  * Check if state has a key
176
480
  */
177
481
  has(key) {
178
- return this._state.has(key);
482
+ const path = this._splitPath(key);
483
+ if (path) {
484
+ const parent = readPath(this.get(path[0]), path[1].slice(0, -1));
485
+ return parent !== null && typeof parent === "object" && Object.prototype.hasOwnProperty.call(parent, path[1][path[1].length - 1]);
486
+ }
487
+ return this._hasOwnKey(key);
179
488
  }
180
489
  /**
181
- * Delete state key
490
+ * Delete state key. Its watchers are removed; computed properties that
491
+ * read it update.
182
492
  */
183
493
  delete(key) {
184
- const observable2 = this._state.get(key);
185
- if (observable2) {
494
+ const path = this._splitPath(key);
495
+ if (path) {
496
+ if (!this.has(key)) return false;
186
497
  if (this._options.enableHistory) {
187
- this._addToHistory("delete", key, observable2.value, void 0);
498
+ this._addToHistory("delete", key, this.get(key), void 0);
188
499
  }
189
- observable2.unwatchAll();
190
- this._state.delete(key);
500
+ this._writeKey(path[0], deletePath(this.get(path[0]), path[1]));
191
501
  return true;
192
502
  }
193
- return false;
503
+ if (!this._hasOwnKey(key)) {
504
+ return false;
505
+ }
506
+ const observable2 = this._state.get(key);
507
+ if (this._options.enableHistory) {
508
+ this._addToHistory("delete", key, observable2._value, void 0);
509
+ }
510
+ observable2.unwatchAll();
511
+ this._releaseKeyWatchers(key);
512
+ observable2._write(ABSENT);
513
+ return true;
514
+ }
515
+ /** @private */
516
+ _releaseKeyWatchers(key) {
517
+ const unwatchers = this._watchers.get(key);
518
+ if (unwatchers) {
519
+ for (const unwatch of unwatchers) unwatch();
520
+ this._watchers.delete(key);
521
+ }
194
522
  }
195
523
  /**
196
524
  * Clear all state
@@ -199,10 +527,13 @@ var ReactiveState = class {
199
527
  if (this._options.enableHistory) {
200
528
  this._addToHistory("clear", null, this.toObject(), {});
201
529
  }
202
- for (const observable2 of this._state.values()) {
203
- observable2.unwatchAll();
204
- }
205
- this._state.clear();
530
+ batch(() => {
531
+ for (const [key, observable2] of this._state) {
532
+ observable2.unwatchAll();
533
+ this._releaseKeyWatchers(key);
534
+ observable2._write(ABSENT);
535
+ }
536
+ });
206
537
  this._computed.clear();
207
538
  this._watchers.clear();
208
539
  }
@@ -225,40 +556,62 @@ var ReactiveState = class {
225
556
  return computed2 ? computed2.value : void 0;
226
557
  }
227
558
  /**
228
- * Watch state changes
559
+ * Watch state changes: a key, a dot path into a key, or a getter
560
+ * expression (re-evaluated whenever what it reads changes).
229
561
  */
230
562
  watch(key, callback, options = {}) {
231
563
  if (typeof key === "function") {
232
564
  return this._watchComputed(key, callback, options);
233
565
  }
234
- const observable2 = this._state.get(key);
235
- if (!observable2) {
566
+ const path = this._splitPath(key);
567
+ if (path) {
568
+ if (!this._hasOwnKey(path[0])) {
569
+ throw new StateError(`Cannot watch undefined state key: ${path[0]}`);
570
+ }
571
+ const computed2 = new Computed(() => this.get(key), { ...this._options, ...options });
572
+ return this._track(key, computed2.watch(callback, options));
573
+ }
574
+ if (!this._hasOwnKey(key)) {
236
575
  throw new StateError(`Cannot watch undefined state key: ${key}`);
237
576
  }
238
- const unwatch = observable2.watch(callback, options);
577
+ return this._track(key, this._state.get(key).watch(callback, options));
578
+ }
579
+ /** Remember an unwatch function for cleanup. @private */
580
+ _track(key, unwatch) {
239
581
  if (!this._watchers.has(key)) {
240
582
  this._watchers.set(key, /* @__PURE__ */ new Set());
241
583
  }
242
- this._watchers.get(key).add(unwatch);
243
- return unwatch;
584
+ const unwatchers = this._watchers.get(key);
585
+ const release = () => {
586
+ unwatch();
587
+ unwatchers.delete(release);
588
+ };
589
+ unwatchers.add(release);
590
+ return release;
244
591
  }
245
592
  /**
246
593
  * Watch computed expression
247
594
  */
248
595
  _watchComputed(expression, callback, options = {}) {
249
- const computed2 = new Computed(expression, options);
596
+ const computed2 = new Computed(expression, { ...this._options, ...options });
250
597
  const unwatch = computed2.watch(callback, options);
251
- return unwatch;
598
+ const release = () => {
599
+ unwatch();
600
+ this._expressionWatchers.delete(release);
601
+ };
602
+ this._expressionWatchers.add(release);
603
+ return release;
252
604
  }
253
605
  /**
254
- * Batch state updates
606
+ * Batch state updates: watchers run once, after every update, with the
607
+ * final values.
255
608
  */
256
609
  batch(updates) {
257
610
  if (typeof updates === "function") {
258
611
  const oldEnableHistory = this._options.enableHistory;
259
612
  this._options.enableHistory = false;
260
613
  try {
261
- const result = updates(this);
614
+ const result = batch(() => updates(this));
262
615
  if (oldEnableHistory) {
263
616
  this._addToHistory("batch", null, null, this.toObject());
264
617
  }
@@ -318,11 +671,8 @@ var ReactiveState = class {
318
671
  break;
319
672
  }
320
673
  }
321
- } catch (_error) {
322
- globalErrorHandler.handle(_error, {
323
- type: "middleware-_error",
324
- context: { action, middleware: middleware.toString() }
325
- });
674
+ } catch (error) {
675
+ reportError(this, error, "middleware-error", { action });
326
676
  }
327
677
  }
328
678
  return result;
@@ -386,28 +736,28 @@ var ReactiveState = class {
386
736
  * Convert state to plain object
387
737
  */
388
738
  toObject() {
389
- const result = {};
390
- for (const [key, observable2] of this._state.entries()) {
391
- result[key] = observable2.value;
392
- }
393
- return result;
739
+ return Object.fromEntries(
740
+ [...this._state].filter(([, observable2]) => observable2._value !== ABSENT).map(([key, observable2]) => [key, observable2._value])
741
+ );
394
742
  }
395
743
  /**
396
744
  * Convert computed properties to object
397
745
  */
398
746
  getComputedValues() {
399
- const result = {};
400
- for (const [key, computed2] of this._computed.entries()) {
401
- result[key] = computed2.value;
402
- }
403
- return result;
747
+ return Object.fromEntries(
748
+ [...this._computed].map(([key, computed2]) => [key, computed2.value])
749
+ );
404
750
  }
405
751
  /**
406
752
  * Get state statistics
407
753
  */
408
754
  getStats() {
755
+ let stateKeys = 0;
756
+ for (const observable2 of this._state.values()) {
757
+ if (observable2._value !== ABSENT) stateKeys++;
758
+ }
409
759
  return {
410
- stateKeys: this._state.size,
760
+ stateKeys,
411
761
  computedKeys: this._computed.size,
412
762
  watcherKeys: this._watchers.size,
413
763
  historyLength: this._history.length,
@@ -418,6 +768,12 @@ var ReactiveState = class {
418
768
  * Cleanup and destroy
419
769
  */
420
770
  destroy() {
771
+ for (const key of [...this._watchers.keys()]) {
772
+ this._releaseKeyWatchers(key);
773
+ }
774
+ for (const release of [...this._expressionWatchers]) {
775
+ release();
776
+ }
421
777
  for (const observable2 of this._state.values()) {
422
778
  observable2.unwatchAll();
423
779
  }
@@ -502,6 +858,7 @@ export {
502
858
  Observable,
503
859
  ReactiveState,
504
860
  StateError,
861
+ batch,
505
862
  computed,
506
863
  createReactiveState,
507
864
  reactive_state_default as default,