value-enhancer
Advanced tools
Comparing version 3.1.5 to 4.0.0
@@ -1,56 +0,24 @@ | ||
import { R as ReadonlyVal, F as FlattenVal } from './typings-4a9c4c94.js'; | ||
import { R as ReadonlyVal } from './typings-8a4e5768.js'; | ||
interface ReactiveCollection<TKey = any, TValue = any> { | ||
/** | ||
* Watch collection value changes. | ||
* @param watcher Watcher to add. | ||
* The Watcher will receive an optional key whose value may have changed. | ||
* If the key is not provided, the collection do not know which values have changed. | ||
* @returns Disposer to remove the watcher. | ||
*/ | ||
watch(watcher: (key?: TKey) => void): () => void; | ||
/** | ||
* Remove a watcher. | ||
* @param watcher Watcher to remove. If not provided, all watchers are removed. | ||
*/ | ||
unwatch(watcher?: (...args: any[]) => any): void; | ||
/** | ||
* Notify watchers that some values may have changed. | ||
* @param key Optional key whose value may have changed. | ||
* If not provided, the collection do not know which values have changed. | ||
*/ | ||
notify(key?: TKey): void; | ||
/** | ||
* Get the value associated with the given key. | ||
* @param key Key to get the value for. | ||
* @returns The value associated with the given key, or undefined if the key is not in the collection. | ||
*/ | ||
get(key: TKey): TValue | undefined; | ||
} | ||
/** | ||
* A reactive list. Similar to an Array except bracket-notation(e.g. `arr[0]`) is not allowed to get/set elements. | ||
* Changes to the map will be notified to subscribers of `watch`. | ||
* Changes to the map will be notified to subscribers of `$`. | ||
* | ||
* @example | ||
* ```ts | ||
* import { ReactiveList, fromCollection } from "value-enhancer/collections" | ||
* import { derive } from "value-enhancer"; | ||
* import { ReactiveList } from "value-enhancer/collections"; | ||
* | ||
* const list = new ReactiveList(["a", "b", "c"]); | ||
* const item$ = derive(list.$, list => list.get(2)); // watch the item at index 2 | ||
* | ||
* const item$ = fromCollection(list, 2); // watch the item at index 2 | ||
* | ||
* console.log(item$.value); // "c" | ||
* | ||
* list.set(2, "d"); | ||
* | ||
* console.log(item$.value); // "d" | ||
* ``` | ||
*/ | ||
declare class ReactiveList<TValue> implements ReactiveCollection<number, TValue> { | ||
declare class ReactiveList<TValue> { | ||
#private; | ||
constructor(arrayLike?: ArrayLike<TValue>); | ||
watch(watcher: (key?: number) => void): () => void; | ||
unwatch(watcher: (...args: any[]) => any): void; | ||
notify(key?: number): void; | ||
$: ReadonlyVal<this>; | ||
/** | ||
@@ -180,33 +148,34 @@ * Get the internal array. Use it as a read-only array. | ||
toJSON(): unknown; | ||
dispose(): void; | ||
} | ||
declare const SET$: unique symbol; | ||
/** | ||
* A reactive map inherited from `Map`. | ||
* Changes to the map will be notified to subscribers of `watch`. | ||
* Changes to the map will be notified to subscribers of `$`. | ||
* | ||
* @example | ||
* ```ts | ||
* import { ReactiveMap, fromCollection } from "value-enhancer/collections" | ||
* import { val, flatten } from "value-enhancer"; | ||
* import { ReactiveMap } from "value-enhancer/collections"; | ||
* | ||
* const map = new ReactiveMap(); | ||
* const v = val("someValue"); | ||
* const item$ = flatten(map.$, map => map.get("someKey")); // watch the item at "someKey" | ||
* | ||
* const item$ = fromCollection(map, "someKey"); // watch the item at "someKey" | ||
* | ||
* console.log(item$.value); // undefined | ||
* | ||
* map.set("someKey", "someValue"); | ||
* | ||
* map.set("someKey", v); | ||
* console.log(item$.value); // "someValue" | ||
* v.set("someValue2"); | ||
* console.log(item$.value); // "someValue2" | ||
* ``` | ||
*/ | ||
declare class ReactiveMap<TKey, TValue> extends Map<TKey, TValue> implements ReactiveCollection<TKey, TValue> { | ||
declare class ReactiveMap<TKey, TValue> extends Map<TKey, TValue> { | ||
constructor(entries?: readonly (readonly [TKey, TValue])[] | null); | ||
private _watchers_; | ||
watch(watcher: (key?: TKey) => void): () => void; | ||
unwatch(watcher: (...args: any[]) => any): void; | ||
notify(key?: TKey): void; | ||
readonly $: ReadonlyVal<this>; | ||
private [SET$]?; | ||
delete(key: TKey): boolean; | ||
clear(): void; | ||
set(key: TKey, value: TValue): this; | ||
dispose(): void; | ||
/** | ||
@@ -218,2 +187,3 @@ * Replace all entries in the Map. | ||
replace(entries: Iterable<readonly [TKey, TValue]>): Map<TKey, TValue>; | ||
dispose(): void; | ||
} | ||
@@ -223,29 +193,21 @@ | ||
* A reactive set inherited from `Set`. | ||
* Changes to the set will be notified to subscribers of `watch`. | ||
* Changes to the set will be notified to subscribers of `$`. | ||
* | ||
* @example | ||
* ```ts | ||
* import { ReactiveSet, fromCollection } from "value-enhancer/collections" | ||
* import { derive } from "value-enhancer"; | ||
* import { ReactiveSet } from "value-enhancer/collections" | ||
* | ||
* const set = new ReactiveSet(); | ||
* const item$ = derive(set.$, set => set.has("someValue")); // watch the existence of "someValue" | ||
* | ||
* const item$ = fromCollection(map, "someValue"); // watch the existence of "someValue" | ||
* | ||
* console.log(item$.value); // false | ||
* | ||
* map.add("someValue"); | ||
* | ||
* set.add("someValue"); | ||
* console.log(item$.value); // true | ||
* ``` | ||
*/ | ||
declare class ReactiveSet<TValue> extends Set<TValue> implements ReactiveCollection<TValue, boolean> { | ||
declare class ReactiveSet<TValue> extends Set<TValue> { | ||
constructor(entries?: readonly TValue[] | null); | ||
private _watchers_; | ||
watch(watcher: (value?: TValue) => void): () => void; | ||
unwatch(watcher: (...args: any[]) => any): void; | ||
notify(value?: TValue): void; | ||
/** | ||
* @alias Set#has | ||
*/ | ||
get: (value: TValue) => boolean; | ||
readonly $: ReadonlyVal<this>; | ||
private [SET$]?; | ||
delete(key: TValue): boolean; | ||
@@ -263,56 +225,2 @@ clear(): void; | ||
/** | ||
* Create a readonly val from a reactive collection watching a specific key. | ||
* | ||
* @param collection The reactive collection | ||
* @param key The key to watch | ||
* @returns A readonly val watching the item at the specified key | ||
* | ||
* @example | ||
* ```ts | ||
* import { ReactiveMap, fromCollection } from "value-enhancer/collections" | ||
* | ||
* const map = new ReactiveMap(); | ||
* | ||
* const item$ = fromCollection(map, "someKey"); // watch the item at "someKey" | ||
* | ||
* console.log(item$.value); // undefined | ||
* | ||
* map.set("someKey", "someValue"); | ||
* | ||
* console.log(item$.value); // "someValue" | ||
* ``` | ||
*/ | ||
declare const fromCollection: <TKey = any, TValue = any>(collection: ReactiveCollection<TKey, TValue>, key: TKey) => ReadonlyVal<TValue | undefined>; | ||
/** | ||
* Create a readonly val from a reactive collection watching a specific key. | ||
* Auto-flatten the value of the item if it is a Val. | ||
* | ||
* @param collection The reactive collection | ||
* @param key The key to watch | ||
* @returns A readonly val watching the item at the specified key | ||
* | ||
* @example | ||
* ```ts | ||
* import { ReactiveMap, flattenFromCollection } from "value-enhancer/collections" | ||
* import { val } from "value-enhancer"; | ||
* | ||
* const map = new ReactiveMap(); | ||
* const v = val("someValue") | ||
* | ||
* const item$ = flattenFromCollection(map, "someKey"); // watch the item at "someKey" | ||
* | ||
* console.log(item$.value); // undefined | ||
* | ||
* map.set("someKey", v); | ||
* | ||
* console.log(item$.value); // "someValue" | ||
* | ||
* v.set("someValue2"); | ||
* | ||
* console.log(item$.value); // "someValue2" | ||
* ``` | ||
*/ | ||
declare const flattenFromCollection: <TKey = any, TValue = any>(collection: ReactiveCollection<TKey, TValue>, key: TKey) => ReadonlyVal<FlattenVal<TValue> | undefined>; | ||
export { ReactiveCollection, ReactiveList, ReactiveMap, ReactiveSet, flattenFromCollection, fromCollection }; | ||
export { ReactiveList, ReactiveMap, ReactiveSet }; |
'use strict'; | ||
// src/scheduler.ts | ||
var nextTick = /* @__PURE__ */ Promise.resolve(); | ||
var pendingSubs1 = /* @__PURE__ */ new Set(); | ||
var pendingSubs2 = /* @__PURE__ */ new Set(); | ||
var pendingSubs = pendingSubs1; | ||
var pending; | ||
var flush = () => { | ||
const curPendingSubs = pendingSubs; | ||
pendingSubs = pendingSubs === pendingSubs1 ? pendingSubs2 : pendingSubs1; | ||
pending = false; | ||
for (const subs of curPendingSubs) { | ||
subs.exec(1 /* Async */); | ||
} | ||
curPendingSubs.clear(); | ||
}; | ||
var schedule = (subs) => { | ||
pendingSubs.add(subs); | ||
pending = pending || nextTick.then(flush); | ||
}; | ||
var cancelTask = (subs) => pendingSubs.delete(subs); | ||
// src/utils.ts | ||
@@ -12,5 +33,189 @@ var defaultEqual = Object.is; | ||
}; | ||
var INIT_VALUE = {}; | ||
var isVal = (val) => !!val?.$valCompute; | ||
// src/subscribers.ts | ||
var Subscribers = class { | ||
constructor(getValue, start) { | ||
this.#getValue = getValue; | ||
this.#start = start; | ||
} | ||
dirty = false; | ||
notify() { | ||
this.#notReadySubscribers.clear(); | ||
if (this.subs.size > 0) { | ||
this.exec(3 /* Computed */); | ||
this.exec(2 /* Eager */); | ||
if (this[1 /* Async */] > 0) { | ||
schedule(this); | ||
} | ||
} else { | ||
this.dirty = false; | ||
} | ||
} | ||
add(subscriber, mode) { | ||
if (this.#start && this.subs.size <= 0) { | ||
this.#startDisposer = this.#start(this); | ||
} | ||
const currentMode = this.subs.get(subscriber); | ||
if (currentMode) { | ||
this[currentMode]--; | ||
} | ||
this.#notReadySubscribers.add(subscriber); | ||
this.subs.set(subscriber, mode); | ||
this[mode]++; | ||
return () => this.remove(subscriber); | ||
} | ||
remove(subscriber) { | ||
this.#notReadySubscribers.delete(subscriber); | ||
const mode = this.subs.get(subscriber); | ||
if (mode) { | ||
this.subs.delete(subscriber); | ||
this[mode]--; | ||
if (this.subs.size <= 0) { | ||
this.#stop(); | ||
} | ||
} | ||
} | ||
clear() { | ||
this.subs.clear(); | ||
this.#notReadySubscribers.clear(); | ||
this[1 /* Async */] = this[2 /* Eager */] = this[3 /* Computed */] = 0; | ||
cancelTask(this); | ||
this.#stop(); | ||
} | ||
exec(mode) { | ||
if (this[mode] > 0) { | ||
let value; | ||
if (mode === 3 /* Computed */) { | ||
if (this[1 /* Async */] + this[2 /* Eager */] <= 0) { | ||
this.dirty = false; | ||
} | ||
} else { | ||
value = this.#getValue(); | ||
if (!this.dirty) { | ||
return; | ||
} | ||
if (mode === 1 /* Async */ || /* mode === SubscriberMode.Eager && */ | ||
this[1 /* Async */] <= 0) { | ||
this.dirty = false; | ||
} | ||
} | ||
for (const [sub, subMode] of this.subs) { | ||
if (subMode === mode && !this.#notReadySubscribers.has(sub)) { | ||
invoke(sub, value); | ||
} | ||
} | ||
} | ||
} | ||
subs = /* @__PURE__ */ new Map(); | ||
#stop() { | ||
this.#startDisposer && (this.#startDisposer = this.#startDisposer()); | ||
} | ||
#getValue; | ||
[1 /* Async */] = 0; | ||
[2 /* Eager */] = 0; | ||
[3 /* Computed */] = 0; | ||
#notReadySubscribers = /* @__PURE__ */ new Set(); | ||
#start; | ||
#startDisposer; | ||
}; | ||
// src/readonly-val.ts | ||
var ReadonlyValImpl = class { | ||
/** | ||
* Manage subscribers for a val. | ||
*/ | ||
_subs; | ||
#eager; | ||
/** | ||
* @param get A pure function that returns the current value of the val. | ||
* @param config Custom config for the val. | ||
* @param start A function that is called when a val get its first subscriber. | ||
* The returned disposer will be called when the last subscriber unsubscribed from the val. | ||
*/ | ||
constructor(get, { equal = defaultEqual, eager } = {}, start) { | ||
this.get = get; | ||
if (equal) { | ||
this.$equal = equal; | ||
} | ||
this.#eager = eager; | ||
this._subs = new Subscribers(get, start); | ||
} | ||
get value() { | ||
return this.get(); | ||
} | ||
get; | ||
$equal; | ||
reaction(subscriber, eager = this.#eager) { | ||
return this._subs.add( | ||
subscriber, | ||
eager ? 2 /* Eager */ : 1 /* Async */ | ||
); | ||
} | ||
subscribe(subscriber, eager = this.#eager) { | ||
const disposer = this.reaction(subscriber, eager); | ||
invoke(subscriber, this.value); | ||
this._subs.dirty = false; | ||
return disposer; | ||
} | ||
$valCompute(subscriber) { | ||
return this._subs.add(subscriber, 3 /* Computed */); | ||
} | ||
unsubscribe(subscriber) { | ||
if (subscriber) { | ||
this._subs.remove(subscriber); | ||
} else { | ||
this._subs.clear(); | ||
} | ||
} | ||
dispose() { | ||
this._subs.clear(); | ||
} | ||
/** | ||
* @returns the string representation of `this.value`. | ||
* | ||
* @example | ||
* ```js | ||
* const v$ = val(val(val(1))); | ||
* console.log(`${v$}`); // "1" | ||
* ``` | ||
*/ | ||
toString() { | ||
return String(this.value); | ||
} | ||
/** | ||
* @returns the JSON representation of `this.value`. | ||
* | ||
* @example | ||
* ```js | ||
* const v$ = val(val(val({ a: 1 }))); | ||
* JSON.stringify(v$); // '{"a":1}' | ||
* ``` | ||
*/ | ||
toJSON(key) { | ||
const value = this.value; | ||
return value && value.toJSON ? value.toJSON(key) : value; | ||
} | ||
}; | ||
function readonlyVal(value, config) { | ||
let currentValue = value; | ||
let subs; | ||
const set = (value2) => { | ||
if (!val.$equal?.(value2, currentValue)) { | ||
currentValue = value2; | ||
if (subs) { | ||
subs.dirty = true; | ||
subs.notify(); | ||
} | ||
} | ||
}; | ||
const val = new ReadonlyValImpl( | ||
() => currentValue, | ||
config, | ||
(s) => { | ||
subs = s; | ||
} | ||
); | ||
return [val, set]; | ||
} | ||
// src/collections/list.ts | ||
@@ -21,16 +226,8 @@ var ReactiveList = class { | ||
this.#data = arrayLike ? Array.from(arrayLike) : []; | ||
const [val, setVal] = readonlyVal(this, { equal: false }); | ||
this.$ = val; | ||
this.#set$ = setVal; | ||
} | ||
#watchers = /* @__PURE__ */ new Set(); | ||
watch(watcher) { | ||
this.#watchers.add(watcher); | ||
return () => this.unwatch(watcher); | ||
} | ||
unwatch(watcher) { | ||
this.#watchers.delete(watcher); | ||
} | ||
notify(key) { | ||
for (const sub of this.#watchers) { | ||
invoke(sub, key); | ||
} | ||
} | ||
$; | ||
#set$; | ||
/** | ||
@@ -91,3 +288,3 @@ * Get the internal array. Use it as a read-only array. | ||
first() { | ||
if (this.#data.length) { | ||
if (this.#data.length > 0) { | ||
return this.#data[0]; | ||
@@ -100,3 +297,3 @@ } | ||
last() { | ||
if (this.#data.length) { | ||
if (this.#data.length > 0) { | ||
return this.#data[this.length - 1]; | ||
@@ -111,7 +308,5 @@ } | ||
push(...items) { | ||
this.#data.push(...items); | ||
if (items.length == 1) { | ||
this.notify(this.#data.length - 1); | ||
} else if (items.length > 1) { | ||
this.notify(); | ||
if (items.length > 0) { | ||
this.#data.push(...items); | ||
this.#set$(this); | ||
} | ||
@@ -127,3 +322,3 @@ } | ||
const result = this.#data.pop(); | ||
this.notify(this.#data.length); | ||
this.#set$(this); | ||
return result; | ||
@@ -138,5 +333,5 @@ } | ||
pushHead(...items) { | ||
this.#data.unshift(...items); | ||
if (items.length > 0) { | ||
this.notify(); | ||
this.#data.unshift(...items); | ||
this.#set$(this); | ||
} | ||
@@ -152,3 +347,3 @@ } | ||
const result = this.#data.shift(); | ||
this.notify(); | ||
this.#set$(this); | ||
return result; | ||
@@ -166,3 +361,3 @@ } | ||
this.#data[index] = item; | ||
this.notify(index); | ||
this.#set$(this); | ||
} | ||
@@ -179,3 +374,3 @@ } | ||
this.#data.splice(index, 0, ...items); | ||
this.notify(); | ||
this.#set$(this); | ||
} | ||
@@ -193,6 +388,4 @@ } | ||
const result = this.#data.splice(index, count); | ||
if (result.length === 1) { | ||
this.notify(index); | ||
} else if (result.length > 1) { | ||
this.notify(); | ||
if (result.length > 0) { | ||
this.#set$(this); | ||
} | ||
@@ -205,5 +398,5 @@ } | ||
clear() { | ||
if (this.length) { | ||
if (this.length > 0) { | ||
this.#data.length = 0; | ||
this.notify(); | ||
this.#set$(this); | ||
} | ||
@@ -226,3 +419,3 @@ return this; | ||
if (isDirty || cached.size > 0) { | ||
this.notify(); | ||
this.#set$(this); | ||
} | ||
@@ -238,3 +431,3 @@ return [...cached]; | ||
this.#data.reverse(); | ||
this.notify(); | ||
this.#set$(this); | ||
} | ||
@@ -256,3 +449,3 @@ return this; | ||
this.#data.sort(compareFn); | ||
this.notify(); | ||
this.#set$(this); | ||
} | ||
@@ -278,4 +471,10 @@ return this; | ||
} | ||
dispose() { | ||
this.$.dispose(); | ||
} | ||
}; | ||
// src/collections/utils.ts | ||
var SET$ = Symbol("set"); | ||
// src/collections/map.ts | ||
@@ -285,22 +484,12 @@ var ReactiveMap = class extends Map { | ||
super(entries); | ||
const [val, setVal] = readonlyVal(this, { equal: false }); | ||
this.$ = val; | ||
this[SET$] = setVal; | ||
} | ||
_ws = /* @__PURE__ */ new Set(); | ||
watch(watcher) { | ||
this._ws.add(watcher); | ||
return () => this.unwatch(watcher); | ||
} | ||
unwatch(watcher) { | ||
this._ws.delete(watcher); | ||
} | ||
notify(key) { | ||
if (this._ws) { | ||
for (const sub of this._ws) { | ||
invoke(sub, key); | ||
} | ||
} | ||
} | ||
$; | ||
[SET$]; | ||
delete(key) { | ||
const deleted = super.delete(key); | ||
if (deleted) { | ||
this.notify(key); | ||
this[SET$]?.(this); | ||
} | ||
@@ -312,3 +501,3 @@ return deleted; | ||
super.clear(); | ||
this.notify(); | ||
this[SET$]?.(this); | ||
} | ||
@@ -320,9 +509,6 @@ } | ||
if (isDirty) { | ||
this.notify(key); | ||
this[SET$]?.(this); | ||
} | ||
return this; | ||
} | ||
dispose() { | ||
this._ws.clear(); | ||
} | ||
/** | ||
@@ -343,6 +529,9 @@ * Replace all entries in the Map. | ||
if (isDirty || cached.size > 0) { | ||
this.notify(); | ||
this[SET$]?.(this); | ||
} | ||
return cached; | ||
} | ||
dispose() { | ||
this.$.dispose(); | ||
} | ||
}; | ||
@@ -354,27 +543,12 @@ | ||
super(entries); | ||
this.get = this.has; | ||
const [val, setVal] = readonlyVal(this, { equal: false }); | ||
this.$ = val; | ||
this[SET$] = setVal; | ||
} | ||
_ws = /* @__PURE__ */ new Set(); | ||
watch(watcher) { | ||
this._ws.add(watcher); | ||
return () => this.unwatch(watcher); | ||
} | ||
unwatch(watcher) { | ||
this._ws.delete(watcher); | ||
} | ||
notify(value) { | ||
if (this._ws) { | ||
for (const sub of this._ws) { | ||
invoke(sub, value); | ||
} | ||
} | ||
} | ||
/** | ||
* @alias Set#has | ||
*/ | ||
get; | ||
$; | ||
[SET$]; | ||
delete(key) { | ||
const deleted = super.delete(key); | ||
if (deleted) { | ||
this.notify(key); | ||
this[SET$]?.(this); | ||
} | ||
@@ -386,3 +560,3 @@ return deleted; | ||
super.clear(); | ||
this.notify(); | ||
this[SET$]?.(this); | ||
} | ||
@@ -394,3 +568,3 @@ } | ||
if (isDirty) { | ||
this.notify(value); | ||
this[SET$]?.(this); | ||
} | ||
@@ -400,3 +574,3 @@ return this; | ||
dispose() { | ||
this._ws.clear(); | ||
this.$.dispose(); | ||
} | ||
@@ -417,3 +591,3 @@ /** | ||
if (isDirty || cached.size > 0) { | ||
this.notify(); | ||
this[SET$]?.(this); | ||
} | ||
@@ -424,314 +598,4 @@ return cached; | ||
// src/scheduler.ts | ||
var nextTick = /* @__PURE__ */ Promise.resolve(); | ||
var pendingSubs1 = /* @__PURE__ */ new Set(); | ||
var pendingSubs2 = /* @__PURE__ */ new Set(); | ||
var pendingSubs = pendingSubs1; | ||
var pending; | ||
var flush = () => { | ||
const curPendingSubs = pendingSubs; | ||
pendingSubs = pendingSubs === pendingSubs1 ? pendingSubs2 : pendingSubs1; | ||
pending = false; | ||
for (const subs of curPendingSubs) { | ||
subs.exec(1 /* Async */); | ||
} | ||
curPendingSubs.clear(); | ||
}; | ||
var schedule = (subs) => { | ||
pendingSubs.add(subs); | ||
pending = pending || nextTick.then(flush); | ||
}; | ||
var cancelTask = (subs) => pendingSubs.delete(subs); | ||
// src/subscribers.ts | ||
var Subscribers = class { | ||
constructor(getValue, start) { | ||
this.#getValue = getValue; | ||
this.#start = start; | ||
} | ||
dirty = false; | ||
notify() { | ||
this.#notReadySubscribers.clear(); | ||
if (this.subs.size > 0) { | ||
this.exec(3 /* Computed */); | ||
this.exec(2 /* Eager */); | ||
if (this[1 /* Async */] > 0) { | ||
schedule(this); | ||
} | ||
} else { | ||
this.dirty = false; | ||
} | ||
} | ||
add(subscriber, mode) { | ||
if (this.#start && this.subs.size <= 0) { | ||
this.#startDisposer = this.#start(this); | ||
} | ||
const currentMode = this.subs.get(subscriber); | ||
if (currentMode) { | ||
this[currentMode]--; | ||
} | ||
this.#notReadySubscribers.add(subscriber); | ||
this.subs.set(subscriber, mode); | ||
this[mode]++; | ||
return () => this.remove(subscriber); | ||
} | ||
remove(subscriber) { | ||
this.#notReadySubscribers.delete(subscriber); | ||
const mode = this.subs.get(subscriber); | ||
if (mode) { | ||
this.subs.delete(subscriber); | ||
this[mode]--; | ||
if (this.subs.size <= 0) { | ||
this.#stop(); | ||
} | ||
} | ||
} | ||
clear() { | ||
this.subs.clear(); | ||
this.#notReadySubscribers.clear(); | ||
this[1 /* Async */] = this[2 /* Eager */] = this[3 /* Computed */] = 0; | ||
cancelTask(this); | ||
this.#stop(); | ||
} | ||
exec(mode) { | ||
if (this[mode] > 0) { | ||
let value; | ||
if (mode === 3 /* Computed */) { | ||
if (this[1 /* Async */] + this[2 /* Eager */] <= 0) { | ||
this.dirty = false; | ||
} | ||
} else { | ||
value = this.#getValue(); | ||
if (!this.dirty) { | ||
return; | ||
} | ||
if (mode === 1 /* Async */ || /* mode === SubscriberMode.Eager && */ | ||
this[1 /* Async */] <= 0) { | ||
this.dirty = false; | ||
} | ||
} | ||
for (const [sub, subMode] of this.subs) { | ||
if (subMode === mode && !this.#notReadySubscribers.has(sub)) { | ||
invoke(sub, value); | ||
} | ||
} | ||
} | ||
} | ||
subs = /* @__PURE__ */ new Map(); | ||
#stop() { | ||
this.#startDisposer && (this.#startDisposer = this.#startDisposer()); | ||
} | ||
#getValue; | ||
[1 /* Async */] = 0; | ||
[2 /* Eager */] = 0; | ||
[3 /* Computed */] = 0; | ||
#notReadySubscribers = /* @__PURE__ */ new Set(); | ||
#start; | ||
#startDisposer; | ||
}; | ||
// src/readonly-val.ts | ||
var ReadonlyValImpl = class { | ||
/** | ||
* Manage subscribers for a val. | ||
*/ | ||
_subs; | ||
#eager; | ||
/** | ||
* @param get A pure function that returns the current value of the val. | ||
* @param config Custom config for the val. | ||
* @param start A function that is called when a val get its first subscriber. | ||
* The returned disposer will be called when the last subscriber unsubscribed from the val. | ||
*/ | ||
constructor(get, { equal, compare, eager } = {}, start) { | ||
this.get = get; | ||
this.compare = this.equal = equal || compare || defaultEqual; | ||
this.#eager = eager; | ||
this._subs = new Subscribers(get, start); | ||
} | ||
get value() { | ||
return this.get(); | ||
} | ||
get; | ||
equal; | ||
/** | ||
* @ignore | ||
* @deprecated Use `equal` instead. | ||
*/ | ||
compare; | ||
reaction(subscriber, eager = this.#eager) { | ||
return this._subs.add( | ||
subscriber, | ||
eager ? 2 /* Eager */ : 1 /* Async */ | ||
); | ||
} | ||
subscribe(subscriber, eager = this.#eager) { | ||
const disposer = this.reaction(subscriber, eager); | ||
invoke(subscriber, this.value); | ||
this._subs.dirty = false; | ||
return disposer; | ||
} | ||
$valCompute(subscriber) { | ||
return this._subs.add(subscriber, 3 /* Computed */); | ||
} | ||
unsubscribe(subscriber) { | ||
if (subscriber) { | ||
this._subs.remove(subscriber); | ||
} else { | ||
this._subs.clear(); | ||
} | ||
} | ||
dispose() { | ||
this._subs.clear(); | ||
} | ||
/** | ||
* @returns the string representation of `this.value`. | ||
* | ||
* @example | ||
* ```js | ||
* const v$ = val(val(val(1))); | ||
* console.log(`${v$}`); // "1" | ||
* ``` | ||
*/ | ||
toString() { | ||
return String(this.value); | ||
} | ||
/** | ||
* @returns the JSON representation of `this.value`. | ||
* | ||
* @example | ||
* ```js | ||
* const v$ = val(val(val({ a: 1 }))); | ||
* JSON.stringify(v$); // '{"a":1}' | ||
* ``` | ||
*/ | ||
toJSON(key) { | ||
const value = this.value; | ||
return value && value.toJSON ? value.toJSON(key) : value; | ||
} | ||
}; | ||
// src/flatten-from.ts | ||
var FlattenFromImpl = class extends ReadonlyValImpl { | ||
constructor(getValue, listen, config) { | ||
const initialEqual = config && (config.equal || config.compare); | ||
let currentValue = INIT_VALUE; | ||
let dirty = false; | ||
let notified = false; | ||
let innerMaybeVal; | ||
let innerVal; | ||
let innerDisposer; | ||
const computeValue = () => { | ||
if (this._subs.subs.size <= 0) { | ||
updateInnerVal(); | ||
} | ||
return innerVal ? innerVal.value : innerMaybeVal; | ||
}; | ||
const get = () => { | ||
if (currentValue === INIT_VALUE || this._subs.subs.size <= 0) { | ||
currentValue = computeValue(); | ||
} else if (dirty) { | ||
const value = computeValue(); | ||
if (!this.equal(value, currentValue)) { | ||
this._subs.dirty = true; | ||
currentValue = value; | ||
} | ||
} | ||
dirty = notified = false; | ||
return currentValue; | ||
}; | ||
const updateInnerVal = () => { | ||
const maybeVal = getValue(); | ||
if (maybeVal !== innerMaybeVal) { | ||
innerMaybeVal = maybeVal; | ||
innerVal = isVal(maybeVal) ? maybeVal : null; | ||
innerDisposer?.(); | ||
innerDisposer = innerVal && innerVal.$valCompute(notify); | ||
currentEqual = initialEqual || (innerVal ? innerVal.equal : defaultEqual); | ||
} | ||
}; | ||
const notify = () => { | ||
dirty = true; | ||
if (!notified) { | ||
notified = true; | ||
this._subs.notify(); | ||
} | ||
}; | ||
super(get, config, () => { | ||
const outerDisposer = listen(() => { | ||
updateInnerVal(); | ||
notify(); | ||
}); | ||
updateInnerVal(); | ||
currentValue = innerVal ? innerVal.value : innerMaybeVal; | ||
dirty = notified = false; | ||
return () => { | ||
innerDisposer?.(); | ||
outerDisposer?.(); | ||
}; | ||
}); | ||
let currentEqual = this.equal; | ||
this.equal = (newValue, oldValue) => currentEqual(newValue, oldValue); | ||
} | ||
}; | ||
var flattenFrom = (getValue, listen, config) => new FlattenFromImpl(getValue, listen, config); | ||
// src/from.ts | ||
var FromImpl = class extends ReadonlyValImpl { | ||
constructor(getValue, listen, config) { | ||
let currentValue = INIT_VALUE; | ||
let dirty = false; | ||
let notified = false; | ||
const get = () => { | ||
if (currentValue === INIT_VALUE || this._subs.subs.size <= 0) { | ||
currentValue = getValue(); | ||
} else if (dirty) { | ||
const value = getValue(); | ||
if (!this.equal(value, currentValue)) { | ||
this._subs.dirty = true; | ||
currentValue = value; | ||
} | ||
} | ||
dirty = notified = false; | ||
return currentValue; | ||
}; | ||
const notify = () => { | ||
dirty = true; | ||
if (!notified) { | ||
notified = true; | ||
this._subs.notify(); | ||
} | ||
}; | ||
super(get, config, () => { | ||
const disposer = listen(notify); | ||
currentValue = getValue(); | ||
dirty = notified = false; | ||
return disposer; | ||
}); | ||
} | ||
}; | ||
var from = (getValue, listen, config) => new FromImpl(getValue, listen, config); | ||
// src/collections/utils.ts | ||
var fromCollection = (collection, key) => from( | ||
() => collection.get(key), | ||
(notify) => collection.watch((k) => { | ||
if (k === key || k == null) { | ||
notify(); | ||
} | ||
}) | ||
); | ||
var flattenFromCollection = (collection, key) => flattenFrom( | ||
() => collection.get(key), | ||
(notify) => collection.watch((k) => { | ||
if (k === key || k == null) { | ||
notify(); | ||
} | ||
}) | ||
); | ||
exports.ReactiveList = ReactiveList; | ||
exports.ReactiveMap = ReactiveMap; | ||
exports.ReactiveSet = ReactiveSet; | ||
exports.flattenFromCollection = flattenFromCollection; | ||
exports.fromCollection = fromCollection; |
@@ -1,3 +0,3 @@ | ||
import { R as ReadonlyVal, V as ValInputsValueTuple, a as ValConfig, F as FlattenVal, b as ValDisposer, c as ValSetValue, d as Val, e as ValSubscriber } from './typings-4a9c4c94.js'; | ||
export { f as ValCompare, g as ValEqual } from './typings-4a9c4c94.js'; | ||
import { R as ReadonlyVal, V as ValInputsValueTuple, a as ValConfig, F as FlattenVal, b as ValDisposer, c as ValSetValue, d as Val, e as ValSubscriber } from './typings-8a4e5768.js'; | ||
export { f as ValEqual } from './typings-8a4e5768.js'; | ||
@@ -12,7 +12,2 @@ /** Returns the value passed in. */ | ||
declare const isVal: <T>(val: T) => val is T extends ReadonlyVal<any> ? T : never; | ||
/** | ||
* @ignore | ||
* @deprecated No longer needed. `isVal` works without `markVal` now. | ||
*/ | ||
declare const markVal: <T extends ReadonlyVal>(val: T) => T; | ||
@@ -88,8 +83,2 @@ type CombineValTransform<TCombinedValue = any, TValues extends readonly any[] = any[]> = (newValues: TValues) => TCombinedValue; | ||
declare function flatten<TSrcValue = any, TValOrValue = any>(val: ReadonlyVal<TSrcValue>, get: (value: TSrcValue) => TValOrValue, config?: ValConfig<FlattenVal<TValOrValue>>): ReadonlyVal<FlattenVal<TValOrValue>>; | ||
/** | ||
* @ignore | ||
* @deprecated | ||
* Renamed to `flatten`. | ||
*/ | ||
declare const unwrap: typeof flatten; | ||
@@ -108,8 +97,2 @@ /** | ||
declare const flattenFrom: <TValOrValue = any>(getValue: () => TValOrValue, listen: (notify: () => void) => ValDisposer | void | undefined, config?: ValConfig<FlattenVal<TValOrValue>> | undefined) => ReadonlyVal<FlattenVal<TValOrValue>>; | ||
/** | ||
* @ignore | ||
* @deprecated | ||
* Renamed to `flattenFrom`. | ||
*/ | ||
declare const unwrapFrom: <TValOrValue = any>(getValue: () => TValOrValue, listen: (notify: () => void) => ValDisposer | void | undefined, config?: ValConfig<FlattenVal<TValOrValue>> | undefined) => ReadonlyVal<FlattenVal<TValOrValue>>; | ||
@@ -251,2 +234,2 @@ /** | ||
export { CombineValTransform, DerivedValTransform, FlattenVal, ReadonlyVal, Val, ValConfig, ValDisposer, ValSetValue, ValSubscriber, combine, derive, flatten, flattenFrom, from, groupVals, identity, isVal, markVal, reaction, readonlyVal, setValue, subscribe, unsubscribe, unwrap, unwrapFrom, val }; | ||
export { CombineValTransform, DerivedValTransform, FlattenVal, ReadonlyVal, Val, ValConfig, ValDisposer, ValSetValue, ValSubscriber, combine, derive, flatten, flattenFrom, from, groupVals, identity, isVal, reaction, readonlyVal, setValue, subscribe, unsubscribe, val }; |
@@ -11,3 +11,3 @@ 'use strict'; | ||
for (let i = valInputs.length - 1; i >= 0; i--) { | ||
if (!valInputs[i].equal(valInputs[i].value, cachedSrcValues[i])) { | ||
if (!valInputs[i].$equal?.(valInputs[i].value, cachedSrcValues[i])) { | ||
return false; | ||
@@ -29,3 +29,2 @@ } | ||
var isVal = (val2) => !!val2?.$valCompute; | ||
var markVal = identity; | ||
@@ -153,5 +152,7 @@ // src/scheduler.ts | ||
*/ | ||
constructor(get, { equal, compare, eager } = {}, start) { | ||
constructor(get, { equal = defaultEqual, eager } = {}, start) { | ||
this.get = get; | ||
this.compare = this.equal = equal || compare || defaultEqual; | ||
if (equal) { | ||
this.$equal = equal; | ||
} | ||
this.#eager = eager; | ||
@@ -164,8 +165,3 @@ this._subs = new Subscribers(get, start); | ||
get; | ||
equal; | ||
/** | ||
* @ignore | ||
* @deprecated Use `equal` instead. | ||
*/ | ||
compare; | ||
$equal; | ||
reaction(subscriber, eager = this.#eager) { | ||
@@ -226,3 +222,3 @@ return this._subs.add( | ||
const set = (value2) => { | ||
if (!val2.equal(value2, currentValue)) { | ||
if (!val2.$equal?.(value2, currentValue)) { | ||
currentValue = value2; | ||
@@ -266,3 +262,3 @@ if (subs) { | ||
const value = getValue2(); | ||
if (!this.equal(value, currentValue)) { | ||
if (!this.$equal?.(value, currentValue)) { | ||
this._subs.dirty = true; | ||
@@ -311,3 +307,3 @@ currentValue = value; | ||
return from( | ||
() => cachedSrcValue !== INIT_VALUE && val2.equal(val2.value, cachedSrcValue) ? cachedValue : cachedValue = transform(cachedSrcValue = val2.value), | ||
() => cachedSrcValue === INIT_VALUE || !val2.$equal?.(val2.value, cachedSrcValue) ? cachedValue = transform(cachedSrcValue = val2.value) : cachedValue, | ||
(notify) => val2.$valCompute(notify), | ||
@@ -321,3 +317,3 @@ config | ||
constructor(getValue2, listen, config) { | ||
const initialEqual = config && (config.equal || config.compare); | ||
const initialEqual = config?.equal; | ||
let currentValue = INIT_VALUE; | ||
@@ -340,3 +336,3 @@ let dirty = false; | ||
const value = computeValue(); | ||
if (!this.equal(value, currentValue)) { | ||
if (!this.$equal?.(value, currentValue)) { | ||
this._subs.dirty = true; | ||
@@ -356,3 +352,3 @@ currentValue = value; | ||
innerDisposer = innerVal && innerVal.$valCompute(notify); | ||
currentEqual = initialEqual || (innerVal ? innerVal.equal : defaultEqual); | ||
this.$equal = initialEqual || (initialEqual === false ? void 0 : innerVal ? innerVal.$equal : defaultEqual); | ||
} | ||
@@ -380,8 +376,5 @@ }; | ||
}); | ||
let currentEqual = this.equal; | ||
this.equal = (newValue, oldValue) => currentEqual(newValue, oldValue); | ||
} | ||
}; | ||
var flattenFrom = (getValue2, listen, config) => new FlattenFromImpl(getValue2, listen, config); | ||
var unwrapFrom = flattenFrom; | ||
@@ -396,3 +389,2 @@ // src/flatten.ts | ||
} | ||
var unwrap = flatten; | ||
@@ -405,3 +397,3 @@ // src/val.ts | ||
this.set = (value) => { | ||
if (!this.equal(value, currentValue)) { | ||
if (!this.$equal?.(value, currentValue)) { | ||
this._subs.dirty = true; | ||
@@ -439,3 +431,2 @@ currentValue = value; | ||
exports.isVal = isVal; | ||
exports.markVal = markVal; | ||
exports.reaction = reaction; | ||
@@ -446,4 +437,2 @@ exports.readonlyVal = readonlyVal; | ||
exports.unsubscribe = unsubscribe; | ||
exports.unwrap = unwrap; | ||
exports.unwrapFrom = unwrapFrom; | ||
exports.val = val; |
{ | ||
"name": "value-enhancer", | ||
"version": "3.1.5", | ||
"version": "4.0.0", | ||
"private": false, | ||
@@ -5,0 +5,0 @@ "description": "A tiny library to enhance value with reactive wrapper.", |
@@ -428,7 +428,8 @@ # [value-enhancer](https://github.com/crimx/value-enhancer) | ||
```ts | ||
import { ReactiveList, fromCollection } from "value-enhancer/collections"; | ||
import { derive } from "value-enhancer"; | ||
import { ReactiveList } from "value-enhancer/collections"; | ||
const list = new ReactiveList(["a", "b", "c"]); | ||
const item$ = fromCollection(list, 2); // watch the item at index 2 | ||
const item$ = derive(list.$, list => list.get(2)); // watch the item at index 2 | ||
@@ -443,4 +444,4 @@ console.log(item$.value); // "c" | ||
```ts | ||
import { ReactiveMap, flattenFromCollection } from "value-enhancer/collections"; | ||
import { val } from "value-enhancer"; | ||
import { val, flatten } from "value-enhancer"; | ||
import { ReactiveMap } from "value-enhancer/collections"; | ||
@@ -450,3 +451,3 @@ const map = new ReactiveMap(); | ||
const item$ = flattenFromCollection(map, "someKey"); // watch the item at "someKey" | ||
const item$ = flatten(map.$, map => map.get("someKey")); // watch the item at "someKey" | ||
@@ -453,0 +454,0 @@ console.log(item$.value); // undefined |
@@ -1,6 +0,3 @@ | ||
export type { ReactiveCollection } from "./typings"; | ||
export { ReactiveList } from "./list"; | ||
export { ReactiveMap } from "./map"; | ||
export { ReactiveSet } from "./set"; | ||
export { flattenFromCollection, fromCollection } from "./utils"; |
@@ -1,26 +0,22 @@ | ||
import { invoke } from "../utils"; | ||
import type { ReactiveCollection } from "./typings"; | ||
import { readonlyVal } from "../readonly-val"; | ||
import type { ReadonlyVal, ValSetValue } from "../typings"; | ||
/** | ||
* A reactive list. Similar to an Array except bracket-notation(e.g. `arr[0]`) is not allowed to get/set elements. | ||
* Changes to the map will be notified to subscribers of `watch`. | ||
* Changes to the map will be notified to subscribers of `$`. | ||
* | ||
* @example | ||
* ```ts | ||
* import { ReactiveList, fromCollection } from "value-enhancer/collections" | ||
* import { derive } from "value-enhancer"; | ||
* import { ReactiveList } from "value-enhancer/collections"; | ||
* | ||
* const list = new ReactiveList(["a", "b", "c"]); | ||
* const item$ = derive(list.$, list => list.get(2)); // watch the item at index 2 | ||
* | ||
* const item$ = fromCollection(list, 2); // watch the item at index 2 | ||
* | ||
* console.log(item$.value); // "c" | ||
* | ||
* list.set(2, "d"); | ||
* | ||
* console.log(item$.value); // "d" | ||
* ``` | ||
*/ | ||
export class ReactiveList<TValue> | ||
implements ReactiveCollection<number, TValue> | ||
{ | ||
export class ReactiveList<TValue> { | ||
#data: TValue[]; | ||
@@ -30,21 +26,11 @@ | ||
this.#data = arrayLike ? Array.from(arrayLike) : []; | ||
const [val, setVal] = readonlyVal(this, { equal: false }); | ||
this.$ = val; | ||
this.#set$ = setVal; | ||
} | ||
#watchers = new Set<(key?: number) => void>(); | ||
public $: ReadonlyVal<this>; | ||
public watch(watcher: (key?: number) => void): () => void { | ||
this.#watchers.add(watcher); | ||
return () => this.unwatch(watcher); | ||
} | ||
#set$: ValSetValue<this>; | ||
public unwatch(watcher: (...args: any[]) => any): void { | ||
this.#watchers.delete(watcher); | ||
} | ||
public notify(key?: number): void { | ||
for (const sub of this.#watchers) { | ||
invoke(sub, key); | ||
} | ||
} | ||
/** | ||
@@ -113,3 +99,3 @@ * Get the internal array. Use it as a read-only array. | ||
public first(): TValue | undefined { | ||
if (this.#data.length) { | ||
if (this.#data.length > 0) { | ||
return this.#data[0]; | ||
@@ -123,3 +109,3 @@ } | ||
public last(): TValue | undefined { | ||
if (this.#data.length) { | ||
if (this.#data.length > 0) { | ||
return this.#data[this.length - 1]; | ||
@@ -135,7 +121,5 @@ } | ||
public push(...items: TValue[]): void { | ||
this.#data.push(...items); | ||
if (items.length == 1) { | ||
this.notify(this.#data.length - 1); | ||
} else if (items.length > 1) { | ||
this.notify(); | ||
if (items.length > 0) { | ||
this.#data.push(...items); | ||
this.#set$(this); | ||
} | ||
@@ -152,3 +136,3 @@ } | ||
const result = this.#data.pop(); | ||
this.notify(this.#data.length); | ||
this.#set$(this); | ||
return result; | ||
@@ -164,5 +148,5 @@ } | ||
public pushHead(...items: TValue[]): void { | ||
this.#data.unshift(...items); | ||
if (items.length > 0) { | ||
this.notify(); | ||
this.#data.unshift(...items); | ||
this.#set$(this); | ||
} | ||
@@ -179,3 +163,3 @@ } | ||
const result = this.#data.shift(); | ||
this.notify(); | ||
this.#set$(this); | ||
return result; | ||
@@ -194,3 +178,3 @@ } | ||
this.#data[index] = item; | ||
this.notify(index); | ||
this.#set$(this); | ||
} | ||
@@ -208,3 +192,3 @@ } | ||
this.#data.splice(index, 0, ...items); | ||
this.notify(); | ||
this.#set$(this); | ||
} | ||
@@ -223,6 +207,4 @@ } | ||
const result = this.#data.splice(index, count); | ||
if (result.length === 1) { | ||
this.notify(index); | ||
} else if (result.length > 1) { | ||
this.notify(); | ||
if (result.length > 0) { | ||
this.#set$(this); | ||
} | ||
@@ -236,5 +218,5 @@ } | ||
public clear(): this { | ||
if (this.length) { | ||
if (this.length > 0) { | ||
this.#data.length = 0; | ||
this.notify(); | ||
this.#set$(this); | ||
} | ||
@@ -258,3 +240,3 @@ return this; | ||
if (isDirty || cached.size > 0) { | ||
this.notify(); | ||
this.#set$(this); | ||
} | ||
@@ -271,3 +253,3 @@ return [...cached]; | ||
this.#data.reverse(); | ||
this.notify(); | ||
this.#set$(this); | ||
} | ||
@@ -290,3 +272,3 @@ return this; | ||
this.#data.sort(compareFn); | ||
this.notify(); | ||
this.#set$(this); | ||
} | ||
@@ -315,2 +297,6 @@ return this; | ||
} | ||
public dispose(): void { | ||
this.$.dispose(); | ||
} | ||
} |
@@ -1,56 +0,41 @@ | ||
import type { ReactiveCollection } from "./typings"; | ||
import { readonlyVal } from "../readonly-val"; | ||
import type { ReadonlyVal, ValSetValue } from "../typings"; | ||
import { SET$ } from "./utils"; | ||
import { invoke } from "../utils"; | ||
/** | ||
* A reactive map inherited from `Map`. | ||
* Changes to the map will be notified to subscribers of `watch`. | ||
* Changes to the map will be notified to subscribers of `$`. | ||
* | ||
* @example | ||
* ```ts | ||
* import { ReactiveMap, fromCollection } from "value-enhancer/collections" | ||
* import { val, flatten } from "value-enhancer"; | ||
* import { ReactiveMap } from "value-enhancer/collections"; | ||
* | ||
* const map = new ReactiveMap(); | ||
* const v = val("someValue"); | ||
* const item$ = flatten(map.$, map => map.get("someKey")); // watch the item at "someKey" | ||
* | ||
* const item$ = fromCollection(map, "someKey"); // watch the item at "someKey" | ||
* | ||
* console.log(item$.value); // undefined | ||
* | ||
* map.set("someKey", "someValue"); | ||
* | ||
* map.set("someKey", v); | ||
* console.log(item$.value); // "someValue" | ||
* v.set("someValue2"); | ||
* console.log(item$.value); // "someValue2" | ||
* ``` | ||
*/ | ||
export class ReactiveMap<TKey, TValue> | ||
extends Map<TKey, TValue> | ||
implements ReactiveCollection<TKey, TValue> | ||
{ | ||
export class ReactiveMap<TKey, TValue> extends Map<TKey, TValue> { | ||
public constructor(entries?: readonly (readonly [TKey, TValue])[] | null) { | ||
super(entries); | ||
const [val, setVal] = readonlyVal(this, { equal: false }); | ||
this.$ = val; | ||
this[SET$] = setVal; | ||
} | ||
private _watchers_ = new Set<(key?: TKey) => void>(); | ||
public readonly $: ReadonlyVal<this>; | ||
public watch(watcher: (key?: TKey) => void): () => void { | ||
this._watchers_.add(watcher); | ||
return () => this.unwatch(watcher); | ||
} | ||
private [SET$]?: ValSetValue<this>; | ||
public unwatch(watcher: (...args: any[]) => any): void { | ||
this._watchers_.delete(watcher); | ||
} | ||
public notify(key?: TKey): void { | ||
// watchers may not exist during super constructor call | ||
if (this._watchers_) { | ||
for (const sub of this._watchers_) { | ||
invoke(sub, key); | ||
} | ||
} | ||
} | ||
public override delete(key: TKey): boolean { | ||
const deleted = super.delete(key); | ||
if (deleted) { | ||
this.notify(key); | ||
this[SET$]?.(this); | ||
} | ||
@@ -63,3 +48,3 @@ return deleted; | ||
super.clear(); | ||
this.notify(); | ||
this[SET$]?.(this); | ||
} | ||
@@ -72,3 +57,3 @@ } | ||
if (isDirty) { | ||
this.notify(key); | ||
this[SET$]?.(this); | ||
} | ||
@@ -78,6 +63,2 @@ return this; | ||
public dispose(): void { | ||
this._watchers_.clear(); | ||
} | ||
/** | ||
@@ -100,6 +81,10 @@ * Replace all entries in the Map. | ||
if (isDirty || cached.size > 0) { | ||
this.notify(); | ||
this[SET$]?.(this); | ||
} | ||
return cached; | ||
} | ||
public dispose(): void { | ||
this.$.dispose(); | ||
} | ||
} |
@@ -10,5 +10,3 @@ # Reactive Collections | ||
ReactiveList, | ||
fromCollection, | ||
flattenFromCollection, | ||
} from "value-enhancer/collections"; | ||
``` |
@@ -1,63 +0,39 @@ | ||
import type { ReactiveCollection } from "./typings"; | ||
import { readonlyVal } from "../readonly-val"; | ||
import type { ReadonlyVal, ValSetValue } from "../typings"; | ||
import { SET$ } from "./utils"; | ||
import { invoke } from "../utils"; | ||
/** | ||
* A reactive set inherited from `Set`. | ||
* Changes to the set will be notified to subscribers of `watch`. | ||
* Changes to the set will be notified to subscribers of `$`. | ||
* | ||
* @example | ||
* ```ts | ||
* import { ReactiveSet, fromCollection } from "value-enhancer/collections" | ||
* import { derive } from "value-enhancer"; | ||
* import { ReactiveSet } from "value-enhancer/collections" | ||
* | ||
* const set = new ReactiveSet(); | ||
* const item$ = derive(set.$, set => set.has("someValue")); // watch the existence of "someValue" | ||
* | ||
* const item$ = fromCollection(map, "someValue"); // watch the existence of "someValue" | ||
* | ||
* console.log(item$.value); // false | ||
* | ||
* map.add("someValue"); | ||
* | ||
* set.add("someValue"); | ||
* console.log(item$.value); // true | ||
* ``` | ||
*/ | ||
export class ReactiveSet<TValue> | ||
extends Set<TValue> | ||
implements ReactiveCollection<TValue, boolean> | ||
{ | ||
export class ReactiveSet<TValue> extends Set<TValue> { | ||
public constructor(entries?: readonly TValue[] | null) { | ||
super(entries); | ||
this.get = this.has; | ||
const [val, setVal] = readonlyVal(this, { equal: false }); | ||
this.$ = val; | ||
this[SET$] = setVal; | ||
} | ||
private _watchers_ = new Set<(value?: TValue) => void>(); | ||
public readonly $: ReadonlyVal<this>; | ||
public watch(watcher: (value?: TValue) => void): () => void { | ||
this._watchers_.add(watcher); | ||
return () => this.unwatch(watcher); | ||
} | ||
private [SET$]?: ValSetValue<this>; | ||
public unwatch(watcher: (...args: any[]) => any): void { | ||
this._watchers_.delete(watcher); | ||
} | ||
public notify(value?: TValue): void { | ||
// watchers may not exist during super constructor call | ||
if (this._watchers_) { | ||
for (const sub of this._watchers_) { | ||
invoke(sub, value); | ||
} | ||
} | ||
} | ||
/** | ||
* @alias Set#has | ||
*/ | ||
public get: (value: TValue) => boolean; | ||
public override delete(key: TValue): boolean { | ||
const deleted = super.delete(key); | ||
if (deleted) { | ||
this.notify(key); | ||
this[SET$]?.(this); | ||
} | ||
@@ -70,3 +46,3 @@ return deleted; | ||
super.clear(); | ||
this.notify(); | ||
this[SET$]?.(this); | ||
} | ||
@@ -79,3 +55,3 @@ } | ||
if (isDirty) { | ||
this.notify(value); | ||
this[SET$]?.(this); | ||
} | ||
@@ -86,3 +62,3 @@ return this; | ||
public dispose(): void { | ||
this._watchers_.clear(); | ||
this.$.dispose(); | ||
} | ||
@@ -104,3 +80,3 @@ | ||
if (isDirty || cached.size > 0) { | ||
this.notify(); | ||
this[SET$]?.(this); | ||
} | ||
@@ -107,0 +83,0 @@ return cached; |
@@ -1,84 +0,1 @@ | ||
import type { FlattenVal, ReadonlyVal } from "../typings"; | ||
import type { ReactiveCollection } from "./typings"; | ||
import { flattenFrom } from "../flatten-from"; | ||
import { from } from "../from"; | ||
/** | ||
* Create a readonly val from a reactive collection watching a specific key. | ||
* | ||
* @param collection The reactive collection | ||
* @param key The key to watch | ||
* @returns A readonly val watching the item at the specified key | ||
* | ||
* @example | ||
* ```ts | ||
* import { ReactiveMap, fromCollection } from "value-enhancer/collections" | ||
* | ||
* const map = new ReactiveMap(); | ||
* | ||
* const item$ = fromCollection(map, "someKey"); // watch the item at "someKey" | ||
* | ||
* console.log(item$.value); // undefined | ||
* | ||
* map.set("someKey", "someValue"); | ||
* | ||
* console.log(item$.value); // "someValue" | ||
* ``` | ||
*/ | ||
export const fromCollection = <TKey = any, TValue = any>( | ||
collection: ReactiveCollection<TKey, TValue>, | ||
key: TKey | ||
): ReadonlyVal<TValue | undefined> => | ||
from( | ||
() => collection.get(key), | ||
notify => | ||
collection.watch(k => { | ||
if (k === key || k == null) { | ||
notify(); | ||
} | ||
}) | ||
); | ||
/** | ||
* Create a readonly val from a reactive collection watching a specific key. | ||
* Auto-flatten the value of the item if it is a Val. | ||
* | ||
* @param collection The reactive collection | ||
* @param key The key to watch | ||
* @returns A readonly val watching the item at the specified key | ||
* | ||
* @example | ||
* ```ts | ||
* import { ReactiveMap, flattenFromCollection } from "value-enhancer/collections" | ||
* import { val } from "value-enhancer"; | ||
* | ||
* const map = new ReactiveMap(); | ||
* const v = val("someValue") | ||
* | ||
* const item$ = flattenFromCollection(map, "someKey"); // watch the item at "someKey" | ||
* | ||
* console.log(item$.value); // undefined | ||
* | ||
* map.set("someKey", v); | ||
* | ||
* console.log(item$.value); // "someValue" | ||
* | ||
* v.set("someValue2"); | ||
* | ||
* console.log(item$.value); // "someValue2" | ||
* ``` | ||
*/ | ||
export const flattenFromCollection = <TKey = any, TValue = any>( | ||
collection: ReactiveCollection<TKey, TValue>, | ||
key: TKey | ||
): ReadonlyVal<FlattenVal<TValue> | undefined> => | ||
flattenFrom( | ||
() => collection.get(key), | ||
notify => | ||
collection.watch(k => { | ||
if (k === key || k == null) { | ||
notify(); | ||
} | ||
}) | ||
); | ||
export const SET$ = Symbol("set"); |
@@ -43,5 +43,5 @@ import type { ReadonlyVal, ValConfig } from "./typings"; | ||
() => | ||
cachedSrcValue !== INIT_VALUE && val.equal(val.value, cachedSrcValue) | ||
? cachedValue | ||
: (cachedValue = transform((cachedSrcValue = val.value))), | ||
cachedSrcValue === INIT_VALUE || !val.$equal?.(val.value, cachedSrcValue) | ||
? (cachedValue = transform((cachedSrcValue = val.value))) | ||
: cachedValue, | ||
notify => val.$valCompute(notify), | ||
@@ -48,0 +48,0 @@ config |
@@ -20,3 +20,3 @@ import type { | ||
) { | ||
const initialEqual = config && (config.equal || config.compare); | ||
const initialEqual = config?.equal; | ||
@@ -43,3 +43,3 @@ let currentValue = INIT_VALUE as TValue; | ||
const value = computeValue(); | ||
if (!this.equal(value, currentValue)) { | ||
if (!this.$equal?.(value, currentValue)) { | ||
this._subs.dirty = true; | ||
@@ -60,4 +60,9 @@ currentValue = value; | ||
innerDisposer = innerVal && innerVal.$valCompute(notify); | ||
currentEqual = | ||
initialEqual || (innerVal ? innerVal.equal : defaultEqual); | ||
this.$equal = | ||
initialEqual || | ||
(initialEqual === false | ||
? void 0 | ||
: innerVal | ||
? innerVal.$equal | ||
: defaultEqual); | ||
} | ||
@@ -90,6 +95,2 @@ }; | ||
}); | ||
let currentEqual = this.equal; | ||
this.equal = (newValue: TValue, oldValue: TValue) => | ||
currentEqual(newValue, oldValue); | ||
} | ||
@@ -115,8 +116,1 @@ } | ||
new FlattenFromImpl(getValue, listen, config); | ||
/** | ||
* @ignore | ||
* @deprecated | ||
* Renamed to `flattenFrom`. | ||
*/ | ||
export const unwrapFrom = flattenFrom; |
@@ -60,8 +60,1 @@ import type { FlattenVal, ReadonlyVal, ValConfig } from "./typings"; | ||
} | ||
/** | ||
* @ignore | ||
* @deprecated | ||
* Renamed to `flatten`. | ||
*/ | ||
export const unwrap = flatten; |
@@ -20,3 +20,3 @@ import { ReadonlyValImpl } from "./readonly-val"; | ||
const value = getValue(); | ||
if (!this.equal(value, currentValue)) { | ||
if (!this.$equal?.(value, currentValue)) { | ||
this._subs.dirty = true; | ||
@@ -23,0 +23,0 @@ currentValue = value; |
@@ -5,3 +5,2 @@ export type { | ||
Val, | ||
ValCompare, | ||
ValConfig, | ||
@@ -14,8 +13,8 @@ ValDisposer, | ||
export { identity, isVal, markVal } from "./utils"; | ||
export { identity, isVal } from "./utils"; | ||
export { combine, type CombineValTransform } from "./combine"; | ||
export { derive, type DerivedValTransform } from "./derive"; | ||
export { flatten, unwrap } from "./flatten"; | ||
export { flattenFrom, unwrapFrom } from "./flatten-from"; | ||
export { flatten } from "./flatten"; | ||
export { flattenFrom } from "./flatten-from"; | ||
export { from } from "./from"; | ||
@@ -22,0 +21,0 @@ export { groupVals, readonlyVal } from "./readonly-val"; |
@@ -32,7 +32,9 @@ import type { ValOnStart } from "./subscribers"; | ||
get: () => TValue, | ||
{ equal, compare, eager }: ValConfig<TValue> = {}, | ||
{ equal = defaultEqual, eager }: ValConfig<TValue> = {}, | ||
start?: ValOnStart | ||
) { | ||
this.get = get; | ||
this.compare = this.equal = equal || compare || defaultEqual; | ||
if (equal) { | ||
this.$equal = equal; | ||
} | ||
this.#eager = eager; | ||
@@ -48,10 +50,4 @@ this._subs = new Subscribers<TValue>(get, start); | ||
public equal: (this: void, newValue: TValue, oldValue: TValue) => boolean; | ||
public $equal?: (this: void, newValue: TValue, oldValue: TValue) => boolean; | ||
/** | ||
* @ignore | ||
* @deprecated Use `equal` instead. | ||
*/ | ||
public compare: (this: void, newValue: TValue, oldValue: TValue) => boolean; | ||
public reaction( | ||
@@ -153,3 +149,3 @@ subscriber: ValSubscriber<TValue>, | ||
const set = (value: TValue | undefined): void => { | ||
if (!val.equal(value, currentValue)) { | ||
if (!val.$equal?.(value, currentValue)) { | ||
currentValue = value; | ||
@@ -156,0 +152,0 @@ if (subs) { |
@@ -7,9 +7,4 @@ export interface ReadonlyVal<TValue = any> { | ||
/** Compare two values. Default `Object.is`. */ | ||
equal(this: void, newValue: TValue, oldValue: TValue): boolean; | ||
$equal?: (this: void, newValue: TValue, oldValue: TValue) => boolean; | ||
/** | ||
* @ignore | ||
* @deprecated Use `equal` instead. | ||
*/ | ||
compare(this: void, newValue: TValue, oldValue: TValue): boolean; | ||
/** | ||
* Subscribe to value changes without immediate emission. | ||
@@ -60,11 +55,2 @@ * @param subscriber | ||
/** | ||
* @ignore | ||
* @deprecated Use Equal instead. | ||
*/ | ||
export type ValCompare<TValue = any> = ( | ||
newValue: TValue, | ||
oldValue: TValue | ||
) => boolean; | ||
export type ValSubscriber<TValue = any> = (newValue: TValue) => void; | ||
@@ -80,10 +66,6 @@ | ||
* Compare two values. Default `Object.is`. | ||
* `false` to disable equality check. | ||
*/ | ||
equal?: ValEqual<TValue>; | ||
equal?: ValEqual<TValue> | false; | ||
/** | ||
* @ignore | ||
* @deprecated Use `equal` instead. | ||
*/ | ||
compare?: ValEqual<TValue>; | ||
/** | ||
* Set the default behavior of subscription and reaction. | ||
@@ -90,0 +72,0 @@ * Emission triggers synchronously if `true`. Default `false`. |
@@ -19,3 +19,3 @@ import type { ReadonlyVal, ValInputsValueTuple } from "./typings"; | ||
for (let i = valInputs.length - 1; i >= 0; i--) { | ||
if (!valInputs[i].equal(valInputs[i].value, cachedSrcValues[i])) { | ||
if (!valInputs[i].$equal?.(valInputs[i].value, cachedSrcValues[i])) { | ||
return false; | ||
@@ -54,7 +54,1 @@ } | ||
!!(val as ReadonlyVal | undefined)?.$valCompute; | ||
/** | ||
* @ignore | ||
* @deprecated No longer needed. `isVal` works without `markVal` now. | ||
*/ | ||
export const markVal: <T extends ReadonlyVal>(val: T) => T = identity; |
@@ -12,3 +12,3 @@ import type { Val, ValConfig } from "./typings"; | ||
this.set = (value: TValue) => { | ||
if (!this.equal(value, currentValue)) { | ||
if (!this.$equal?.(value, currentValue)) { | ||
this._subs.dirty = true; | ||
@@ -15,0 +15,0 @@ currentValue = value; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
461
127994
29
3760