value-enhancer
Advanced tools
Comparing version 2.4.4 to 3.0.0
@@ -1,122 +0,4 @@ | ||
interface ReadonlyVal<TValue = any> { | ||
/** value */ | ||
readonly value: TValue; | ||
/** Compare two values. Default `===`. */ | ||
compare(newValue: TValue, oldValue: TValue): boolean; | ||
/** | ||
* Subscribe to value changes without immediate emission. | ||
* @param subscriber | ||
* @param eager by default subscribers will be notified on next tick. set `true` to notify subscribers of value changes synchronously. | ||
* @returns a disposer function that cancels the subscription | ||
*/ | ||
reaction(subscriber: ValSubscriber<TValue>, eager?: boolean): ValDisposer; | ||
/** | ||
* Subscribe to value changes with immediate emission. | ||
* @param subscriber | ||
* @param eager by default subscribers will be notified on next tick. set `true` to notify subscribers of value changes synchronously. | ||
* @returns a disposer function that cancels the subscription | ||
*/ | ||
subscribe(subscriber: ValSubscriber<TValue>, eager?: boolean): ValDisposer; | ||
/** | ||
* Remove the given subscriber. | ||
* Remove all if no subscriber provided. | ||
* @param subscriber | ||
*/ | ||
unsubscribe(subscriber?: (...args: any[]) => any): void; | ||
/** | ||
* Remove all subscribers. | ||
*/ | ||
dispose(): void; | ||
} | ||
interface Val<TValue = any> extends ReadonlyVal<TValue> { | ||
value: TValue; | ||
/** set new value */ | ||
readonly set: (this: void, value: TValue) => void; | ||
} | ||
type ValCompare<TValue = any> = (newValue: TValue, oldValue: TValue) => boolean; | ||
type ValSubscriber<TValue = any> = (newValue: TValue) => void; | ||
type ValDisposer = () => void; | ||
/** @ignore */ | ||
type ValOnStart = () => void | ValDisposer | undefined; | ||
interface ValConfig<TValue = any> { | ||
/** | ||
* Compare two values. Default `===`. | ||
*/ | ||
compare?: ValCompare<TValue>; | ||
/** | ||
* Set the default behavior of subscription and reaction. | ||
* Emission triggers synchronously if `true`. Default `false`. | ||
*/ | ||
eager?: boolean; | ||
} | ||
/** @ignore */ | ||
type ValInputsValueTuple<TValInputs extends readonly ReadonlyVal[]> = Readonly<{ | ||
[K in keyof TValInputs]: ExtractValValue<TValInputs[K]>; | ||
}>; | ||
/** @ignore */ | ||
type ExtractValValue<TVal> = TVal extends ReadonlyVal<infer TValue> ? TValue : never; | ||
import { R as ReadonlyVal, V as ValInputsValueTuple, a as ValConfig, b as ValDisposer, c as ValSetValue, U as UnwrapVal, d as Val, e as ValSubscriber } from './typings-c9d13ac0.js'; | ||
export { f as ValCompare } from './typings-c9d13ac0.js'; | ||
declare enum SubscriberMode { | ||
Async = 1, | ||
Eager = 2, | ||
Computed = 3 | ||
} | ||
declare class Subscribers<TValue = any> { | ||
constructor(val: ReadonlyVal<TValue>, start?: (() => void | ValDisposer | undefined) | null); | ||
invoke_(): void; | ||
add_(subscriber: ValSubscriber, mode: SubscriberMode): () => void; | ||
remove_(subscriber: ValSubscriber): void; | ||
clear_(): void; | ||
exec_(mode: SubscriberMode): void; | ||
shouldExec_: boolean; | ||
readonly subscribers_: Map<ValSubscriber<TValue>, SubscriberMode>; | ||
private _stop_; | ||
private _getValue_; | ||
private [SubscriberMode.Async]; | ||
private [SubscriberMode.Eager]; | ||
private [SubscriberMode.Computed]; | ||
private readonly _notReadySubscribers_; | ||
private _start_?; | ||
private _startDisposer_?; | ||
} | ||
declare class ReadonlyValImpl<TValue = any> implements ReadonlyVal<TValue> { | ||
protected _subs_: Subscribers<TValue>; | ||
protected _value_: TValue; | ||
protected _set_: (value: TValue) => void; | ||
constructor(value: TValue, { compare, eager }?: ValConfig<TValue>, start?: ValOnStart); | ||
get value(): TValue; | ||
eager: boolean; | ||
compare(newValue: TValue, oldValue: TValue): boolean; | ||
reaction(subscriber: ValSubscriber<TValue>, eager?: boolean): ValDisposer; | ||
subscribe(subscriber: ValSubscriber<TValue>, eager?: boolean): ValDisposer; | ||
/** | ||
* @internal | ||
* For computed vals | ||
*/ | ||
_compute_(subscriber: ValSubscriber<void>): ValDisposer; | ||
unsubscribe(subscriber?: (...args: any[]) => any): void; | ||
dispose(): void; | ||
/** | ||
* @returns the string representation of `this.value`. | ||
* | ||
* @example | ||
* ```js | ||
* const v$ = val(val(val(1))); | ||
* console.log(`${v$}`); // "1" | ||
* ``` | ||
*/ | ||
toString(): string; | ||
/** | ||
* @returns the JSON representation of `this.value`. | ||
* | ||
* @example | ||
* ```js | ||
* const v$ = val(val(val({ a: 1 }))); | ||
* JSON.stringify(v$); // '{"a":1}' | ||
* ``` | ||
*/ | ||
toJSON(key: string): unknown; | ||
} | ||
/** Returns the value passed in. */ | ||
@@ -131,19 +13,23 @@ declare const identity: <TValue>(value: TValue) => TValue; | ||
/** | ||
* Marks an object that implements `ReadonlyVal` interface to be `isVal` detectable. | ||
* @ignore | ||
* @deprecated No longer needed. `isVal` works without `markVal` now. | ||
*/ | ||
declare const markVal: <T extends ReadonlyVal<any>>(val: T) => T; | ||
declare const markVal: <T extends ReadonlyVal>(val: T) => T; | ||
type CombineValTransform<TDerivedValue = any, TValues extends readonly any[] = any[], TMeta = any> = (newValues: TValues, oldValues?: TValues, meta?: TMeta) => TDerivedValue; | ||
/** | ||
* Creates a writable val. | ||
* @returns A val with undefined value. | ||
* Combines an array of vals into a single val with the array of values. | ||
* @param valInputs An array of vals to combine. | ||
* @returns A readonly val with the combined values. | ||
*/ | ||
declare function val(): Val<undefined>; | ||
declare function combine<TValInputs extends readonly ReadonlyVal[] = ReadonlyVal[]>(valInputs: readonly [...TValInputs]): ReadonlyVal<[...ValInputsValueTuple<TValInputs>]>; | ||
/** | ||
* Creates a writable val. | ||
* @param value Initial value. | ||
* @param config Custom config. | ||
* Combines an array of vals into a single val with transformed value. | ||
* @param valInputs An array of vals to combine. | ||
* @param transform A pure function that takes an array of values and returns a new value. | ||
* @param config custom config for the combined val. | ||
* @returns A readonly val with the transformed values. | ||
*/ | ||
declare function val<TValue = any>(value: TValue, config?: ValConfig<TValue>): Val<TValue>; | ||
declare function combine<TValInputs extends readonly ReadonlyVal[] = ReadonlyVal[], TValue = any>(valInputs: readonly [...TValInputs], transform: CombineValTransform<TValue, [...ValInputsValueTuple<TValInputs>]>, config?: ValConfig<TValue>): ReadonlyVal<TValue>; | ||
/** @ignore */ | ||
type DerivedValTransform<TValue = any, TDerivedValue = any> = (newValue: TValue) => TDerivedValue; | ||
@@ -153,3 +39,3 @@ /** | ||
* @param val Input value. | ||
* @returns An readonly val with same value as the input val. | ||
* @returns A readonly val with same value as the input val. | ||
*/ | ||
@@ -162,45 +48,51 @@ declare function derive<TSrcValue = any, TValue = any>(val: ReadonlyVal<TSrcValue>): ReadonlyVal<TValue>; | ||
* @param config custom config for the combined val. | ||
* @returns An readonly val with transformed value from the input val. | ||
* @returns A readonly val with transformed value from the input val. | ||
*/ | ||
declare function derive<TSrcValue = any, TValue = any>(val: ReadonlyVal<TSrcValue>, transform: DerivedValTransform<TSrcValue, TValue>, config?: ValConfig<TValue>): ReadonlyVal<TValue>; | ||
/** @ignore */ | ||
type CombineValTransform<TDerivedValue = any, TValues extends readonly any[] = any[], TMeta = any> = (newValues: TValues, oldValues?: TValues, meta?: TMeta) => TDerivedValue; | ||
/** | ||
* Combines an array of vals into a single val with the array of values. | ||
* @param valInputs An array of vals to combine. | ||
* @returns A readonly val with the combined values. | ||
*/ | ||
declare function combine<TValInputs extends readonly ReadonlyVal[] = ReadonlyVal[]>(valInputs: readonly [...TValInputs]): ReadonlyVal<[...ValInputsValueTuple<TValInputs>]>; | ||
/** | ||
* Combines an array of vals into a single val with transformed value. | ||
* @param valInputs An array of vals to combine. | ||
* @param transform A pure function that takes an array of values and returns a new value. | ||
* @param config custom config for the combined val. | ||
* @returns A readonly val with the transformed values. | ||
*/ | ||
declare function combine<TValInputs extends readonly ReadonlyVal[] = ReadonlyVal[], TValue = any>(valInputs: readonly [...TValInputs], transform: CombineValTransform<TValue, [...ValInputsValueTuple<TValInputs>]>, config?: ValConfig<TValue>): ReadonlyVal<TValue>; | ||
/** | ||
* Unwrap a val of val to a val of the inner val value. | ||
* @param val Input value. | ||
* @returns An readonly val with value of inner val. | ||
* Creates a readonly val from a getter function and a listener function. | ||
* | ||
* @param getValue A function that returns the current value. | ||
* @param listen A function that takes a notify function and returns a disposer. | ||
* The notify function should be called when the value changes. | ||
* @param config custom config for the val. | ||
* @returns A readonly val. | ||
* | ||
* @example | ||
* ```js | ||
* import { unwrap, val } from "value-enhancer"; | ||
* ```ts | ||
* const prefersDark = window.matchMedia("(prefers-color-scheme: dark)"); | ||
* const isDarkMode$ = from( | ||
* () => prefersDark.matches, | ||
* notify => { | ||
* prefersDark.addEventListener("change", notify); | ||
* return () => prefersDark.removeEventListener("change", notify); | ||
* }, | ||
* ); | ||
* ``` | ||
* | ||
* const inner$ = val(12); | ||
* const outer$ = val(inner$); | ||
* | ||
* const unwrapped$ = unwrap(outer$); | ||
* | ||
* inner$.value === unwrapped$.value; // true | ||
* @example An implementation of the `derive` function: | ||
* ```ts | ||
* const derive = (val, transform, config) => from( | ||
* () => transform(val.value), | ||
* notify => val.subscribe(notify), | ||
* config, | ||
* ); | ||
* ``` | ||
*/ | ||
declare function unwrap<TValue = any>(val: ReadonlyVal<ReadonlyVal<TValue>>): ReadonlyVal<TValue>; | ||
declare const from: <TValue = any>(getValue: () => TValue, listen: (handler: () => void) => ValDisposer | void | undefined, config?: ValConfig<TValue> | undefined) => ReadonlyVal<TValue>; | ||
/** | ||
* Creates a readonly val with the given value. | ||
* | ||
* @param value Value for the val | ||
* @param config Custom config for the val. | ||
* @returns A tuple with the readonly val and a function to set the value. | ||
*/ | ||
declare const readonlyVal: <TValue = any>(value: TValue, config?: ValConfig<TValue> | undefined) => [ReadonlyVal<TValue>, ValSetValue<TValue>]; | ||
/** | ||
* Unwrap a val of val to a val of the inner val value. | ||
* @param val Input value. | ||
* @returns An readonly val with value of inner val. | ||
* @returns A readonly val with value of inner val. | ||
* | ||
@@ -219,3 +111,3 @@ * @example | ||
*/ | ||
declare function unwrap<TValue = any>(val: ReadonlyVal<Val<TValue>>): ReadonlyVal<TValue>; | ||
declare function unwrap<TValOrValue = any>(val: ReadonlyVal<TValOrValue>): ReadonlyVal<UnwrapVal<TValOrValue>>; | ||
/** | ||
@@ -225,3 +117,3 @@ * Unwrap an inner val extracted from a source val to a val of the inner val value. | ||
* @param get extract inner val or value from source val. | ||
* @returns An readonly val with value of inner val. | ||
* @returns A readonly val with value of inner val. | ||
* | ||
@@ -240,5 +132,30 @@ * @example | ||
*/ | ||
declare function unwrap<TSrcValue = any, TValue = any>(val: ReadonlyVal<TSrcValue>, get: (value: TSrcValue) => ReadonlyVal<TValue> | TValue, config?: ValConfig<TValue>): ReadonlyVal<TValue>; | ||
declare function unwrap<TSrcValue = any, TValOrValue = any>(val: ReadonlyVal<TSrcValue>, get: (value: TSrcValue) => TValOrValue, config?: ValConfig<UnwrapVal<TValOrValue>>): ReadonlyVal<UnwrapVal<TValOrValue>>; | ||
/** | ||
* Creates a readonly val from a getter function and a listener function. | ||
* If the value is a val, it will be auto-unwrapped. | ||
* | ||
* @param getValue A function that returns the current value. | ||
* If the value is a val, it will be auto-unwrapped. | ||
* @param listen A function that takes a notify function and returns a disposer. | ||
* The notify function should be called when the value changes. | ||
* @param config custom config for the val. | ||
* @returns A readonly val with value of inner val. | ||
*/ | ||
declare const unwrapFrom: <TValOrValue = any>(getValue: () => TValOrValue, listen: (notify: () => void) => ValDisposer | void | undefined, config?: ValConfig<UnwrapVal<TValOrValue>> | undefined) => ReadonlyVal<UnwrapVal<TValOrValue>>; | ||
/** | ||
* Creates a writable val. | ||
* @returns A val with undefined value. | ||
*/ | ||
declare function val<TValue>(): Val<TValue | undefined>; | ||
/** | ||
* Creates a writable val. | ||
* @param value Initial value. | ||
* @param config Custom config. | ||
*/ | ||
declare function val<TValue = any>(value: TValue, config?: ValConfig<TValue>): Val<TValue>; | ||
/** | ||
* Set the value of a val. | ||
@@ -273,2 +190,2 @@ * It works for both `Val` and `ReadonlyVal` type (if the `ReadonlyVal` is actually a `Val`). | ||
export { CombineValTransform, DerivedValTransform, ExtractValValue, ReadonlyVal, ReadonlyValImpl, Val, ValCompare, ValConfig, ValDisposer, ValInputsValueTuple, ValOnStart, ValSubscriber, combine, derive, identity, isVal, markVal, reaction, setValue, subscribe, unsubscribe, unwrap, val }; | ||
export { CombineValTransform, DerivedValTransform, ReadonlyVal, UnwrapVal, Val, ValConfig, ValDisposer, ValSetValue, ValSubscriber, combine, derive, from, identity, isVal, markVal, reaction, readonlyVal, setValue, subscribe, unsubscribe, unwrap, unwrapFrom, val }; |
@@ -1,2 +0,16 @@ | ||
'use strict'; | ||
// src/utils.ts | ||
var identity = (value) => value; | ||
var defaultCompare = (newValue, oldValue) => newValue === oldValue; | ||
var getValue = (val2) => val2.value; | ||
var getValues = (valInputs) => valInputs.map(getValue); | ||
var invoke = (fn, value) => { | ||
try { | ||
fn(value); | ||
} catch (e) { | ||
console.error(e); | ||
} | ||
}; | ||
var INIT_VALUE = {}; | ||
var isVal = (val2) => !!val2?.$valCompute; | ||
var markVal = identity; | ||
@@ -20,32 +34,11 @@ // src/scheduler.ts | ||
// src/utils.ts | ||
var identity = (value) => value; | ||
var getValue = (val2) => val2.value; | ||
var getValues = (valInputs) => valInputs.map(getValue); | ||
var invoke = (fn, value) => { | ||
try { | ||
fn(value); | ||
} catch (e) { | ||
console.error(e); | ||
} | ||
}; | ||
var INIT_VALUE = {}; | ||
var VAL_SYMBOL = "$\u2009val\u2009"; | ||
var isVal = (val2) => !!(val2 && val2[VAL_SYMBOL]); | ||
var markVal = (val2) => { | ||
Object.defineProperty(val2, VAL_SYMBOL, { value: 1 }); | ||
return val2; | ||
}; | ||
var compute = (val2, subscriber) => val2._c(subscriber); | ||
// src/subscribers.ts | ||
var Subscribers = class { | ||
constructor(val2, start) { | ||
this._g = () => val2.value; | ||
this.g = start; | ||
constructor(getValue2, start) { | ||
this._g = getValue2; | ||
this.h = start; | ||
} | ||
i() { | ||
if (this.d.size > 0) { | ||
this.d.clear(); | ||
} | ||
d = false; | ||
n() { | ||
this.f.clear(); | ||
if (this.b.size > 0) { | ||
@@ -58,8 +51,8 @@ this.e(3 /* Computed */); | ||
} else { | ||
this.s = false; | ||
this.d = false; | ||
} | ||
} | ||
a(subscriber, mode) { | ||
if (this.g && this.b.size <= 0) { | ||
this.f = this.g(); | ||
if (this.h && this.b.size <= 0) { | ||
this.g = this.h(this); | ||
} | ||
@@ -70,3 +63,3 @@ const currentMode = this.b.get(subscriber); | ||
} | ||
this.d.add(subscriber); | ||
this.f.add(subscriber); | ||
this.b.set(subscriber, mode); | ||
@@ -77,3 +70,3 @@ this[mode]++; | ||
r(subscriber) { | ||
this.d.delete(subscriber); | ||
this.f.delete(subscriber); | ||
const mode = this.b.get(subscriber); | ||
@@ -84,3 +77,3 @@ if (mode) { | ||
if (this.b.size <= 0) { | ||
this.h(); | ||
this.i(); | ||
} | ||
@@ -91,6 +84,6 @@ } | ||
this.b.clear(); | ||
this.d.clear(); | ||
this.f.clear(); | ||
this[1 /* Async */] = this[2 /* Eager */] = this[3 /* Computed */] = 0; | ||
cancelTask(this); | ||
this.h(); | ||
this.i(); | ||
} | ||
@@ -102,16 +95,16 @@ e(mode) { | ||
if (this[1 /* Async */] + this[2 /* Eager */] <= 0) { | ||
this.s = false; | ||
this.d = false; | ||
} | ||
} else { | ||
value = this._g(); | ||
if (!this.s) { | ||
if (!this.d) { | ||
return; | ||
} | ||
if (mode === 1 /* Async */ || /* mode === SubscriberMode.Computed */ | ||
if (mode === 1 /* Async */ || /* mode === SubscriberMode.Eager && */ | ||
this[1 /* Async */] <= 0) { | ||
this.s = false; | ||
this.d = false; | ||
} | ||
} | ||
for (const [sub, subMode] of this.b) { | ||
if (subMode === mode && !this.d.has(sub)) { | ||
if (subMode === mode && !this.f.has(sub)) { | ||
invoke(sub, value); | ||
@@ -122,6 +115,5 @@ } | ||
} | ||
s = false; | ||
b = /* @__PURE__ */ new Map(); | ||
h() { | ||
this.f && (this.f = this.f()); | ||
i() { | ||
this.g && (this.g = this.g()); | ||
} | ||
@@ -132,5 +124,5 @@ _g; | ||
[3 /* Computed */] = 0; | ||
d = /* @__PURE__ */ new Set(); | ||
f = /* @__PURE__ */ new Set(); | ||
h; | ||
g; | ||
f; | ||
}; | ||
@@ -140,29 +132,26 @@ | ||
var ReadonlyValImpl = class { | ||
_u; | ||
_v; | ||
_s = (value) => { | ||
if (!this.compare(value, this._v)) { | ||
this._u.s = true; | ||
this._v = value; | ||
this._u.i(); | ||
} | ||
}; | ||
constructor(value, { compare, eager = false } = {}, start) { | ||
markVal(this); | ||
this._v = value; | ||
this.eager = eager; | ||
if (compare) { | ||
this.compare = compare; | ||
} | ||
this._u = new Subscribers(this, start); | ||
/** | ||
* Manage subscribers for a val. | ||
*/ | ||
_s; | ||
_e; | ||
/** | ||
* @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, { compare = defaultCompare, eager } = {}, start) { | ||
this.get = get; | ||
this.compare = compare; | ||
this._e = eager; | ||
this._s = new Subscribers(get, start); | ||
} | ||
get value() { | ||
return this._v; | ||
return this.get(); | ||
} | ||
eager; | ||
compare(newValue, oldValue) { | ||
return newValue === oldValue; | ||
} | ||
reaction(subscriber, eager = this.eager) { | ||
return this._u.a( | ||
get; | ||
compare; | ||
reaction(subscriber, eager = this._e) { | ||
return this._s.a( | ||
subscriber, | ||
@@ -172,23 +161,20 @@ eager ? 2 /* Eager */ : 1 /* Async */ | ||
} | ||
subscribe(subscriber, eager = this.eager) { | ||
subscribe(subscriber, eager = this._e) { | ||
const disposer = this.reaction(subscriber, eager); | ||
invoke(subscriber, this.value); | ||
this._s.d = false; | ||
return disposer; | ||
} | ||
/** | ||
* @internal | ||
* For computed vals | ||
*/ | ||
_c(subscriber) { | ||
return this._u.a(subscriber, 3 /* Computed */); | ||
$valCompute(subscriber) { | ||
return this._s.a(subscriber, 3 /* Computed */); | ||
} | ||
unsubscribe(subscriber) { | ||
if (subscriber) { | ||
this._u.r(subscriber); | ||
this._s.r(subscriber); | ||
} else { | ||
this._u.c(); | ||
this._s.c(); | ||
} | ||
} | ||
dispose() { | ||
this._u.c(); | ||
this._s.c(); | ||
} | ||
@@ -221,168 +207,181 @@ /** | ||
}; | ||
// src/val.ts | ||
var ValImpl = class extends ReadonlyValImpl { | ||
constructor(value, config) { | ||
super(value, config); | ||
} | ||
get value() { | ||
return this._v; | ||
} | ||
set value(value) { | ||
this._s(value); | ||
} | ||
/** Set new value */ | ||
set = this._s; | ||
var readonlyVal = (value, config) => { | ||
let currentValue = value; | ||
let subs; | ||
const set = (value2) => { | ||
if (!val2.compare(value2, currentValue)) { | ||
currentValue = value2; | ||
if (subs) { | ||
subs.d = true; | ||
subs.n(); | ||
} | ||
} | ||
}; | ||
const val2 = new ReadonlyValImpl( | ||
() => currentValue, | ||
config, | ||
(s) => { | ||
subs = s; | ||
} | ||
); | ||
return [val2, set]; | ||
}; | ||
function val(value, config) { | ||
return new ValImpl(value, config); | ||
} | ||
// src/derived-val.ts | ||
var DerivedValImpl = class extends ReadonlyValImpl { | ||
constructor(val2, transform, config) { | ||
const getValue2 = () => transform(val2.value); | ||
super(INIT_VALUE, config, () => { | ||
if (this._v === INIT_VALUE) { | ||
this._v = getValue2(); | ||
} else { | ||
this._d = this._d || 1; | ||
// src/from.ts | ||
var FromImpl = class extends ReadonlyValImpl { | ||
constructor(getValue2, listen, config) { | ||
let currentValue = INIT_VALUE; | ||
let dirty = false; | ||
let notified = false; | ||
const get = () => { | ||
if (currentValue === INIT_VALUE || this._s.b.size <= 0) { | ||
currentValue = getValue2(); | ||
} else if (dirty) { | ||
const value = getValue2(); | ||
if (!this.compare(value, currentValue)) { | ||
this._s.d = true; | ||
currentValue = value; | ||
} | ||
} | ||
return compute(val2, () => { | ||
if (this._d < 2) { | ||
this._d = 2; | ||
this._u.i(); | ||
} | ||
}); | ||
dirty = notified = false; | ||
return currentValue; | ||
}; | ||
const notify = () => { | ||
dirty = true; | ||
if (!notified) { | ||
notified = true; | ||
this._s.n(); | ||
} | ||
}; | ||
super(get, config, () => { | ||
currentValue = getValue2(); | ||
dirty = notified = false; | ||
return listen(notify); | ||
}); | ||
this._g = getValue2; | ||
} | ||
get value() { | ||
if (this._v === INIT_VALUE) { | ||
this._v = this._g(); | ||
this._u.s = true; | ||
} else if (this._d || this._u.b.size <= 0) { | ||
const value = this._g(); | ||
if (!this.compare(value, this._v)) { | ||
this._u.s = true; | ||
this._v = value; | ||
} | ||
} | ||
this._d = 0; | ||
return this._v; | ||
} | ||
_g; | ||
_d = 0; | ||
}; | ||
function derive(val2, transform = identity, config) { | ||
return new DerivedValImpl(val2, transform, config); | ||
} | ||
var from = (getValue2, listen, config) => new FromImpl(getValue2, listen, config); | ||
// src/combine.ts | ||
var CombinedValImpl = class extends ReadonlyValImpl { | ||
constructor(valInputs, transform, config) { | ||
const getValue2 = () => transform(getValues(valInputs)); | ||
super(INIT_VALUE, config, () => { | ||
if (this._v === INIT_VALUE) { | ||
this._v = getValue2(); | ||
} else { | ||
this._d = this._d || 1; | ||
} | ||
const disposers = valInputs.map( | ||
(val2) => compute(val2, () => { | ||
if (this._d < 2) { | ||
this._d = 2; | ||
this._u.i(); | ||
} | ||
}) | ||
); | ||
function combine(valInputs, transform = identity, config) { | ||
return from( | ||
() => transform(getValues(valInputs)), | ||
(notify) => { | ||
const disposers = valInputs.map((val2) => val2.$valCompute(notify)); | ||
return () => disposers.forEach(invoke); | ||
}); | ||
this._g = getValue2; | ||
} | ||
get value() { | ||
if (this._v === INIT_VALUE) { | ||
this._v = this._g(); | ||
this._u.s = true; | ||
} else if (this._d || this._u.b.size <= 0) { | ||
const value = this._g(); | ||
if (!this.compare(value, this._v)) { | ||
this._u.s = true; | ||
this._v = value; | ||
} | ||
} | ||
this._d = 0; | ||
return this._v; | ||
} | ||
_g; | ||
_d = 0; | ||
}; | ||
function combine(valInputs, transform = identity, config) { | ||
return new CombinedValImpl(valInputs, transform, config); | ||
}, | ||
config | ||
); | ||
} | ||
// src/unwrap-val.ts | ||
var UnwrapValImpl = class extends ReadonlyValImpl { | ||
constructor(val2, get = identity, config) { | ||
let innerValue; | ||
super(INIT_VALUE, config, () => { | ||
innerValue = get(val2.value); | ||
if (this._v === INIT_VALUE) { | ||
this._v = isVal(innerValue) ? innerValue.value : innerValue; | ||
} else { | ||
this._d = this._d || 1; | ||
// src/derive.ts | ||
function derive(val2, transform = identity, config) { | ||
return from( | ||
() => transform(val2.value), | ||
(notify) => val2.$valCompute(notify), | ||
config | ||
); | ||
} | ||
// src/unwrap-from.ts | ||
var UnwrapFromImpl = class extends ReadonlyValImpl { | ||
constructor(getValue2, listen, config) { | ||
const initialCompare = config?.compare; | ||
let currentValue = INIT_VALUE; | ||
let dirty = false; | ||
let notified = false; | ||
let innerMaybeVal; | ||
let innerVal; | ||
let innerDisposer; | ||
const computeValue = () => { | ||
if (this._s.b.size <= 0) { | ||
updateInnerVal(); | ||
} | ||
const markDirty = () => { | ||
if (this._d < 2) { | ||
this._d = 2; | ||
this._u.i(); | ||
return innerVal ? innerVal.value : innerMaybeVal; | ||
}; | ||
const get = () => { | ||
if (currentValue === INIT_VALUE || this._s.b.size <= 0) { | ||
currentValue = computeValue(); | ||
} else if (dirty) { | ||
const value = computeValue(); | ||
if (!this.compare(value, currentValue)) { | ||
this._s.d = true; | ||
currentValue = value; | ||
} | ||
}; | ||
let innerDisposer = isVal(innerValue) && compute(innerValue, markDirty); | ||
const outerDisposer = compute(val2, () => { | ||
innerDisposer && innerDisposer(); | ||
innerValue = get(val2.value); | ||
innerDisposer = isVal(innerValue) && compute(innerValue, markDirty); | ||
markDirty(); | ||
} | ||
dirty = notified = false; | ||
return currentValue; | ||
}; | ||
const updateInnerVal = () => { | ||
const maybeVal = getValue2(); | ||
if (maybeVal !== innerMaybeVal) { | ||
innerMaybeVal = maybeVal; | ||
innerVal = isVal(maybeVal) ? maybeVal : null; | ||
innerDisposer?.(); | ||
innerDisposer = innerVal && innerVal.$valCompute(notify); | ||
currentCompare = initialCompare || (innerVal ? innerVal.compare : defaultCompare); | ||
} | ||
}; | ||
const notify = () => { | ||
dirty = true; | ||
if (!notified) { | ||
notified = true; | ||
this._s.n(); | ||
} | ||
}; | ||
super(get, config, () => { | ||
updateInnerVal(); | ||
currentValue = innerVal ? innerVal.value : innerMaybeVal; | ||
dirty = notified = false; | ||
const outerDisposer = listen(() => { | ||
updateInnerVal(); | ||
notify(); | ||
}); | ||
return () => { | ||
innerDisposer && innerDisposer(); | ||
outerDisposer(); | ||
innerDisposer?.(); | ||
outerDisposer?.(); | ||
}; | ||
}); | ||
this._g = () => { | ||
if (this._u.b.size <= 0 || !innerValue) { | ||
innerValue = get(val2.value); | ||
let currentCompare = this.compare; | ||
this.compare = (newValue, oldValue) => currentCompare(newValue, oldValue); | ||
} | ||
}; | ||
var unwrapFrom = (getValue2, listen, config) => new UnwrapFromImpl(getValue2, listen, config); | ||
// src/unwrap.ts | ||
function unwrap(val2, get = identity, config) { | ||
return unwrapFrom( | ||
() => get(val2.value), | ||
(notify) => val2.$valCompute(notify), | ||
config | ||
); | ||
} | ||
// src/val.ts | ||
var ValImpl = class extends ReadonlyValImpl { | ||
constructor(currentValue, config) { | ||
const get = () => currentValue; | ||
super(get, config); | ||
this.set = (value) => { | ||
if (!this.compare(value, currentValue)) { | ||
this._s.d = true; | ||
currentValue = value; | ||
this._s.n(); | ||
} | ||
return isVal(innerValue) ? innerValue.value : innerValue; | ||
}; | ||
this._m = (newValue, oldValue) => isVal(innerValue) ? innerValue.compare(newValue, oldValue) : super.compare(newValue, oldValue); | ||
} | ||
set; | ||
get value() { | ||
if (this._v === INIT_VALUE) { | ||
this._v = this._g(); | ||
this._u.s = true; | ||
} else if (this._d || this._u.b.size <= 0) { | ||
const value = this._g(); | ||
if (!this.compare(value, this._v)) { | ||
this._u.s = true; | ||
this._v = value; | ||
} | ||
} | ||
this._d = 0; | ||
return this._v; | ||
return this.get(); | ||
} | ||
compare(newValue, oldValue) { | ||
return this._m(newValue, oldValue); | ||
set value(value) { | ||
this.set(value); | ||
} | ||
_m; | ||
_g; | ||
_d = 0; | ||
}; | ||
function unwrap(val2, get, config) { | ||
return new UnwrapValImpl(val2, get, config); | ||
function val(value, config) { | ||
return new ValImpl(value, config); | ||
} | ||
// src/value-enhancer.ts | ||
var setValue = (val2, value) => val2.set && val2.set(value); | ||
var setValue = (val2, value) => val2.set?.(value); | ||
var subscribe = (val2, subscriber, eager) => val2.subscribe(subscriber, eager); | ||
@@ -392,15 +391,2 @@ var reaction = (val2, subscriber, eager) => val2.reaction(subscriber, eager); | ||
exports.ReadonlyValImpl = ReadonlyValImpl; | ||
exports.combine = combine; | ||
exports.derive = derive; | ||
exports.identity = identity; | ||
exports.isVal = isVal; | ||
exports.markVal = markVal; | ||
exports.reaction = reaction; | ||
exports.setValue = setValue; | ||
exports.subscribe = subscribe; | ||
exports.unsubscribe = unsubscribe; | ||
exports.unwrap = unwrap; | ||
exports.val = val; | ||
//# sourceMappingURL=out.js.map | ||
//# sourceMappingURL=index.js.map | ||
export { combine, derive, from, identity, isVal, markVal, reaction, readonlyVal, setValue, subscribe, unsubscribe, unwrap, unwrapFrom, val }; |
{ | ||
"name": "value-enhancer", | ||
"version": "2.4.4", | ||
"version": "3.0.0", | ||
"private": false, | ||
@@ -19,5 +19,27 @@ "description": "A tiny library to enhance value with reactive wrapper.", | ||
"sideEffects": false, | ||
"main": "./dist/index.js", | ||
"module": "./dist/index.mjs", | ||
"types": "./dist/index.d.ts", | ||
"type": "module", | ||
"main": "./dist/index.cjs", | ||
"exports": { | ||
".": { | ||
"types": "./dist/index.d.ts", | ||
"require": "./dist/index.cjs", | ||
"import": "./dist/index.js" | ||
}, | ||
"./collection": { | ||
"types": "./dist/collections.d.ts", | ||
"require": "./dist/collections.cjs", | ||
"import": "./dist/collections.js" | ||
} | ||
}, | ||
"types": "dist/index.d.ts", | ||
"typesVersions": { | ||
"*": { | ||
".": [ | ||
"dist/index.d.ts" | ||
], | ||
"collection": [ | ||
"dist/collections.d.ts" | ||
] | ||
} | ||
}, | ||
"files": [ | ||
@@ -31,6 +53,6 @@ "src", | ||
"test": "jest", | ||
"docs": "typedoc --cname value-enhancer.js.org --includeVersion --excludePrivate --excludeProtected --excludeInternal --out docs src/index.ts", | ||
"docs": "typedoc --options typedoc.json", | ||
"types": "cross-env NODE_ENV=production tsc --declaration --emitDeclarationOnly --jsx react --esModuleInterop --outDir dist", | ||
"build": "cross-env NODE_ENV=production tsup-node src/index.ts", | ||
"build:min": "cross-env NODE_ENV=production MINIFY=true tsup-node src/index.ts && node scripts/gzip.mjs", | ||
"build": "cross-env NODE_ENV=production tsup-node", | ||
"build:min": "cross-env NODE_ENV=production MINIFY=true tsup-node && node scripts/gzip.mjs", | ||
"build:dev": "cross-env NODE_ENV=development tsup-node src/index.ts", | ||
@@ -54,3 +76,3 @@ "release": "standard-version" | ||
"tsup": "^6.6.3", | ||
"typedoc": "^0.23.26", | ||
"typedoc": "^0.24.8", | ||
"typescript": "^4.9.5", | ||
@@ -57,0 +79,0 @@ "yoctocolors": "^1.0.0" |
121
README.md
@@ -12,3 +12,3 @@ # [value-enhancer](https://github.com/crimx/value-enhancer) | ||
[![full-size](https://img.shields.io/bundlephobia/minzip/value-enhancer)](https://bundlejs.com/?q=value-enhancer) | ||
[![core-size](https://runkit.io/crimx/bundlejs-badge/branches/master?q=value-enhancer&exports=val&label=core%20size)](https://bundlejs.com/?q=value-enhancer&treeshake=%5B%7Bval%7D%5D) | ||
[![core-size](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdeno.bundlejs.com%2F%3Fq%3Dvalue-enhancer%26treeshake%3D%255B%257B%2520val%2520%257D%255D&query=%24.size.size&label=core%20size)](https://bundlejs.com/?q=value-enhancer&treeshake=%5B%7Bval%7D%5D) | ||
[![tree-shakable](https://img.shields.io/badge/%20tree-shakable-success)](https://bundlejs.com/?q=value-enhancer) | ||
@@ -32,2 +32,6 @@ [![no-dependencies](https://img.shields.io/badge/dependencies-none-success)](https://bundlejs.com/?q=value-enhancer) | ||
## Docs | ||
<https://value-enhancer.js.org/> | ||
## Features | ||
@@ -39,3 +43,3 @@ | ||
It does not convert the value with `Object.defineProperty` nor `Proxy`. Keeping everything as plain JavaScript value makes it easier to work with other libraries and easier for the JavaScript engine to optimize. | ||
- Safe and fast. | ||
- Safe and fast lazy computation. | ||
It solves multi-level derivation issue (like in Svelte Stores) with smart lazy value evaluation. | ||
@@ -45,3 +49,3 @@ - Explicit. | ||
- Simple DX. | ||
No hidden rules for getting or setting values. What you see is what you get. | ||
Designed with ergonomics in mind. No hidden rules for getting or setting values. What you see is what you get. | ||
- Bundle size and Performance. | ||
@@ -57,3 +61,3 @@ By carefully defining scope and choosing right features that balance usability and performance, less work needed to be done in `value-enhancer` which makes it smaller and faster. | ||
MobX does not work well with other libraries. It could break other libraries if you forget to exclude instances from other libraries from making observable. `toJS` is also needed if data is passed to other libraries. | ||
MobX does not work well with other libraries. It could break other libraries if you forget to exclude third-party instances from making observable. `toJS` is also needed if data is passed to other libraries. | ||
@@ -138,2 +142,4 @@ MobX also prints error when it sees another version of MobX in the global. It is not a good choice for making SDK or library that will be delivered into customer's environment. | ||
`value-enhancer` also fixes the edge cases of Svelte stores by leveraging Vue's layered subscriber design. | ||
</details> | ||
@@ -205,3 +211,3 @@ | ||
## Create Val | ||
## Create Writable Val | ||
@@ -222,2 +228,18 @@ ```js | ||
## Create Readonly Val | ||
```js | ||
import { readonlyVal } from "value-enhancer"; | ||
const [count$, setCount] = readonlyVal(2); | ||
console.log(count$.value); // 2 | ||
setCount(3); | ||
console.log(count$.value); // 3 | ||
count$.value = 4; | ||
console.log(count$.value); // 4 | ||
``` | ||
## Subscribe to value changes | ||
@@ -246,3 +268,3 @@ | ||
await Promise.resolve(); // wait for the next tick | ||
await Promise.resolve(); // subscription triggered asynchronously by default | ||
@@ -258,3 +280,3 @@ // printed "subscribe: 4" | ||
Derive a new Val from another Val. | ||
`derive` a new Val from another Val. | ||
@@ -273,3 +295,3 @@ ```js | ||
Combine multiple Vals into a new Val. | ||
`combine` multiple Vals into a new Val. | ||
@@ -293,3 +315,3 @@ ```js | ||
Unwrap the inner Val from a Val of Val. This is useful for subscribing to a dynamic Val that is inside another Val. | ||
`unwrap` the inner Val from a Val of Val. This is useful for subscribing to a dynamic Val that is inside another Val. | ||
@@ -310,2 +332,24 @@ ```js | ||
## From | ||
`from` creates a Val from any value source. Both `derive` and `combine` are implemented using `from`. | ||
```ts | ||
import { from } from "value-enhancer"; | ||
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)"); | ||
const isDarkMode$ = from( | ||
() => prefersDark.matches, | ||
notify => { | ||
prefersDark.addEventListener("change", notify); | ||
return () => prefersDark.removeEventListener("change", notify); | ||
} | ||
); | ||
``` | ||
## UnwrapFrom | ||
`unwrapFrom` creates a Val from any value source like `from` but also unwrap the value if the value is a Val. `unwrap` is implemented using `unwrapFrom`. | ||
## Custom Compare | ||
@@ -346,53 +390,38 @@ | ||
## Custom Val | ||
## Reactive Collections | ||
You can create your own Val by extending `ReadonlyValImpl` class, or implement the `ReadonlyVal` interface and mark manually with `markVal` function. | ||
The Reactive Collections are a group of classes that expand on the built-in JavaScript collections, allowing changes to the collections to be observed. See [docs](https://value-enhancer.js.org/modules/collections.html) for API details. | ||
```js | ||
import { ReadonlyValImpl, isVal } from "value-enhancer"; | ||
```ts | ||
import { ReactiveList, fromCollection } from "value-enhancer/collections"; | ||
class MyVal extends ReadonlyValImpl { | ||
constructor(value) { | ||
super(value); | ||
} | ||
const list = new ReactiveList(["a", "b", "c"]); | ||
set(value) { | ||
console.log("set", value); | ||
this._set(value); | ||
} | ||
} | ||
const item$ = fromCollection(list, 2); // watch the item at index 2 | ||
const myVal = new MyVal(11); | ||
myVal.set(22); // printed "set 22" | ||
console.log(item$.value); // "c" | ||
console.log(isVal(myVal)); // true | ||
list.set(2, "d"); | ||
console.log(item$.value); // "d" | ||
``` | ||
```js | ||
import { val, isVal, markVal, identity } from "value-enhancer"; | ||
```ts | ||
import { ReactiveMap, unwrapFromCollection } from "value-enhancer/collections"; | ||
import { val } from "value-enhancer"; | ||
const createMyVal = value => { | ||
const v = val(value); | ||
const map = new ReactiveMap(); | ||
const v = val("someValue"); | ||
const myVal = { | ||
value: 11, | ||
compare: identity, | ||
reaction: v.reaction.bind(v), | ||
subscribe: v.subscribe.bind(v), | ||
unsubscribe: v.unsubscribe.bind(v), | ||
set(value) { | ||
console.log("set", value); | ||
this.value = value; | ||
}, | ||
}; | ||
const item$ = unwrapFromCollection(map, "someKey"); // watch the item at "someKey" | ||
markVal(myVal); | ||
console.log(item$.value); // undefined | ||
return myVal; | ||
}; | ||
map.set("someKey", v); | ||
const myVal = createMyVal(11); | ||
myVal.set(22); // printed "set 22" | ||
console.log(item$.value); // "someValue" | ||
console.log(isVal(myVal)); // true | ||
v.set("someValue2"); | ||
console.log(item$.value); // "someValue2" | ||
``` |
@@ -1,6 +0,6 @@ | ||
import { ReadonlyValImpl } from "./readonly-val"; | ||
import type { ReadonlyVal, ValInputsValueTuple, ValConfig } from "./typings"; | ||
import { invoke, getValues, INIT_VALUE, identity, compute } from "./utils"; | ||
import type { ReadonlyVal, ValConfig, ValInputsValueTuple } from "./typings"; | ||
/** @ignore */ | ||
import { from } from "./from"; | ||
import { getValues, identity, invoke } from "./utils"; | ||
export type CombineValTransform< | ||
@@ -12,58 +12,2 @@ TDerivedValue = any, | ||
class CombinedValImpl< | ||
TValInputs extends readonly ReadonlyVal[] = ReadonlyVal[], | ||
TValue = any | ||
> | ||
extends ReadonlyValImpl<TValue> | ||
implements ReadonlyVal<TValue> | ||
{ | ||
public constructor( | ||
valInputs: TValInputs, | ||
transform: CombineValTransform< | ||
TValue, | ||
[...ValInputsValueTuple<TValInputs>] | ||
>, | ||
config?: ValConfig<TValue> | ||
) { | ||
const getValue = () => transform(getValues(valInputs)); | ||
super(INIT_VALUE, config, () => { | ||
if (this._value_ === INIT_VALUE) { | ||
this._value_ = getValue(); | ||
} else { | ||
this._dirtyLevel_ = this._dirtyLevel_ || 1; | ||
} | ||
const disposers = valInputs.map(val => | ||
compute(val, () => { | ||
if (this._dirtyLevel_ < 2) { | ||
this._dirtyLevel_ = 2; | ||
this._subs_.invoke_(); | ||
} | ||
}) | ||
); | ||
return () => disposers.forEach(invoke); | ||
}); | ||
this._getValue_ = getValue; | ||
} | ||
public override get value(): TValue { | ||
if (this._value_ === INIT_VALUE) { | ||
this._value_ = this._getValue_(); | ||
this._subs_.shouldExec_ = true; | ||
} else if (this._dirtyLevel_ || this._subs_.subscribers_.size <= 0) { | ||
const value = this._getValue_(); | ||
if (!this.compare(value, this._value_)) { | ||
this._subs_.shouldExec_ = true; | ||
this._value_ = value; | ||
} | ||
} | ||
this._dirtyLevel_ = 0; | ||
return this._value_; | ||
} | ||
private _getValue_: () => TValue; | ||
private _dirtyLevel_ = 0; | ||
} | ||
/** | ||
@@ -108,3 +52,10 @@ * Combines an array of vals into a single val with the array of values. | ||
): ReadonlyVal<TValue> { | ||
return new CombinedValImpl(valInputs, transform, config); | ||
return from( | ||
() => transform(getValues(valInputs)), | ||
notify => { | ||
const disposers = valInputs.map(val => val.$valCompute(notify)); | ||
return () => disposers.forEach(invoke); | ||
}, | ||
config | ||
); | ||
} |
export type { | ||
ReadonlyVal, | ||
UnwrapVal, | ||
Val, | ||
ValCompare, | ||
ValConfig, | ||
ValDisposer, | ||
ValSetValue, | ||
ValSubscriber, | ||
ValDisposer, | ||
ValConfig, | ||
ValInputsValueTuple, | ||
ExtractValValue, | ||
ValOnStart, | ||
} from "./typings"; | ||
export { ReadonlyValImpl } from "./readonly-val"; | ||
export { identity, isVal, markVal } from "./utils"; | ||
export { combine, type CombineValTransform } from "./combine"; | ||
export { derive, type DerivedValTransform } from "./derive"; | ||
export { from } from "./from"; | ||
export { readonlyVal } from "./readonly-val"; | ||
export { unwrap } from "./unwrap"; | ||
export { unwrapFrom } from "./unwrap-from"; | ||
export { val } from "./val"; | ||
export { derive, type DerivedValTransform } from "./derived-val"; | ||
export { combine, type CombineValTransform } from "./combine"; | ||
export { unwrap } from "./unwrap-val"; | ||
export { setValue, subscribe, reaction, unsubscribe } from "./value-enhancer"; | ||
export { reaction, setValue, subscribe, unsubscribe } from "./value-enhancer"; |
@@ -1,55 +0,52 @@ | ||
import { SubscriberMode, Subscribers } from "./subscribers"; | ||
import type { ValOnStart } from "./subscribers"; | ||
import type { | ||
ReadonlyVal, | ||
ValConfig, | ||
ValDisposer, | ||
ValSetValue, | ||
ValSubscriber, | ||
ValConfig, | ||
ValOnStart, | ||
ReadonlyVal, | ||
} from "./typings"; | ||
import { invoke, markVal } from "./utils"; | ||
import { SubscriberMode, Subscribers } from "./subscribers"; | ||
import { defaultCompare, invoke } from "./utils"; | ||
/** | ||
* Bare minimum implementation of a readonly val. | ||
*/ | ||
export class ReadonlyValImpl<TValue = any> implements ReadonlyVal<TValue> { | ||
/** | ||
* Manage subscribers for a val. | ||
*/ | ||
protected _subs_: Subscribers<TValue>; | ||
protected _value_: TValue; | ||
private _eager_?: boolean; | ||
protected _set_ = (value: TValue): void => { | ||
if (!this.compare(value, this._value_)) { | ||
this._subs_.shouldExec_ = true; | ||
this._value_ = value; | ||
this._subs_.invoke_(); | ||
} | ||
}; | ||
/** | ||
* @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. | ||
*/ | ||
public constructor( | ||
value: TValue, | ||
{ compare, eager = false }: ValConfig<TValue> = {}, | ||
get: () => TValue, | ||
{ compare = defaultCompare, eager }: ValConfig<TValue> = {}, | ||
start?: ValOnStart | ||
) { | ||
markVal(this); | ||
this._value_ = value; | ||
this.eager = eager; | ||
if (compare) { | ||
this.compare = compare; | ||
} | ||
this._subs_ = new Subscribers<TValue>(this, start); | ||
this.get = get; | ||
this.compare = compare; | ||
this._eager_ = eager; | ||
this._subs_ = new Subscribers<TValue>(get, start); | ||
} | ||
public get value(): TValue { | ||
return this._value_; | ||
return this.get(); | ||
} | ||
public eager: boolean; | ||
public get: (this: void) => TValue; | ||
public compare(newValue: TValue, oldValue: TValue): boolean { | ||
return newValue === oldValue; | ||
} | ||
public compare: (this: void, newValue: TValue, oldValue: TValue) => boolean; | ||
public reaction( | ||
subscriber: ValSubscriber<TValue>, | ||
eager: boolean = this.eager | ||
eager = this._eager_ | ||
): ValDisposer { | ||
@@ -64,14 +61,11 @@ return this._subs_.add_( | ||
subscriber: ValSubscriber<TValue>, | ||
eager: boolean = this.eager | ||
eager = this._eager_ | ||
): ValDisposer { | ||
const disposer = this.reaction(subscriber, eager); | ||
invoke(subscriber, this.value); | ||
this._subs_.dirty_ = false; | ||
return disposer; | ||
} | ||
/** | ||
* @internal | ||
* For computed vals | ||
*/ | ||
public _compute_(subscriber: ValSubscriber<void>): ValDisposer { | ||
public $valCompute(subscriber: ValSubscriber<void>): ValDisposer { | ||
return this._subs_.add_(subscriber, SubscriberMode.Computed); | ||
@@ -122,1 +116,37 @@ } | ||
} | ||
/** | ||
* Creates a readonly val with the given value. | ||
* | ||
* @param value Value for the val | ||
* @param config Custom config for the val. | ||
* @returns A tuple with the readonly val and a function to set the value. | ||
*/ | ||
export const readonlyVal = <TValue = any>( | ||
value: TValue, | ||
config?: ValConfig<TValue> | ||
): [ReadonlyVal<TValue>, ValSetValue<TValue>] => { | ||
let currentValue = value; | ||
let subs: Subscribers; | ||
const set = (value: TValue): void => { | ||
if (!val.compare(value, currentValue)) { | ||
currentValue = value; | ||
if (subs) { | ||
subs.dirty_ = true; | ||
subs.notify_(); | ||
} | ||
} | ||
}; | ||
const val = new ReadonlyValImpl( | ||
() => currentValue, | ||
config, | ||
s => { | ||
subs = s; | ||
} | ||
); | ||
return [val, set]; | ||
}; |
@@ -0,3 +1,4 @@ | ||
import type { ValDisposer, ValSubscriber } from "./typings"; | ||
import { cancelTask, schedule } from "./scheduler"; | ||
import type { ReadonlyVal, ValDisposer, ValSubscriber } from "./typings"; | ||
import { invoke } from "./utils"; | ||
@@ -11,15 +12,21 @@ | ||
export class Subscribers<TValue = any> { | ||
public constructor( | ||
val: ReadonlyVal<TValue>, | ||
start?: (() => void | ValDisposer | undefined) | null | ||
) { | ||
this._getValue_ = () => val.value; | ||
/** | ||
* 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. | ||
*/ | ||
export type ValOnStart = (subs: Subscribers) => void | ValDisposer | undefined; | ||
/** | ||
* Manage subscribers for a val. | ||
*/ | ||
export class Subscribers<TValue = any> implements Subscribers { | ||
public constructor(getValue: () => TValue, start?: ValOnStart | null) { | ||
this._getValue_ = getValue; | ||
this._start_ = start; | ||
} | ||
public invoke_(): void { | ||
if (this._notReadySubscribers_.size > 0) { | ||
this._notReadySubscribers_.clear(); | ||
} | ||
public dirty_ = false; | ||
public notify_(): void { | ||
this._notReadySubscribers_.clear(); | ||
if (this.subscribers_.size > 0) { | ||
@@ -32,3 +39,3 @@ this.exec_(SubscriberMode.Computed); | ||
} else { | ||
this.shouldExec_ = false; | ||
this.dirty_ = false; | ||
} | ||
@@ -39,3 +46,4 @@ } | ||
if (this._start_ && this.subscribers_.size <= 0) { | ||
this._startDisposer_ = this._start_(); | ||
// Subscribe added, clear notified state | ||
this._startDisposer_ = this._start_(this); | ||
} | ||
@@ -82,7 +90,7 @@ | ||
if (this[SubscriberMode.Async] + this[SubscriberMode.Eager] <= 0) { | ||
this.shouldExec_ = false; | ||
this.dirty_ = false; | ||
} | ||
} else { | ||
value = this._getValue_(); | ||
if (!this.shouldExec_) { | ||
if (!this.dirty_) { | ||
return; | ||
@@ -92,5 +100,5 @@ } | ||
mode === SubscriberMode.Async || | ||
/* mode === SubscriberMode.Computed */ this[SubscriberMode.Async] <= 0 | ||
/* mode === SubscriberMode.Eager && */ this[SubscriberMode.Async] <= 0 | ||
) { | ||
this.shouldExec_ = false; | ||
this.dirty_ = false; | ||
} | ||
@@ -106,4 +114,2 @@ } | ||
public shouldExec_ = false; | ||
public readonly subscribers_ = new Map< | ||
@@ -126,4 +132,4 @@ ValSubscriber<TValue>, | ||
private _start_?: (() => void | ValDisposer | undefined) | null; | ||
private _start_?: ValOnStart | null; | ||
private _startDisposer_?: ValDisposer | void | null; | ||
} |
export interface ReadonlyVal<TValue = any> { | ||
/** value */ | ||
/** Current value of the val */ | ||
readonly value: TValue; | ||
/** Get current value of the val */ | ||
get(this: void): TValue; | ||
/** Compare two values. Default `===`. */ | ||
compare(newValue: TValue, oldValue: TValue): boolean; | ||
compare(this: void, newValue: TValue, oldValue: TValue): boolean; | ||
/** | ||
@@ -21,2 +23,8 @@ * Subscribe to value changes without immediate emission. | ||
/** | ||
* Subscribe to value changes and get invoked before {@link ReadonlyVal#subscribe} and {@link ReadonlyVal#reaction}. | ||
* @param subscriber | ||
* @returns a disposer function that cancels the subscription | ||
*/ | ||
$valCompute(subscriber: ValSubscriber<void>): ValDisposer; | ||
/** | ||
* Remove the given subscriber. | ||
@@ -34,5 +42,6 @@ * Remove all if no subscriber provided. | ||
export interface Val<TValue = any> extends ReadonlyVal<TValue> { | ||
/** Current value of the val */ | ||
value: TValue; | ||
/** set new value */ | ||
readonly set: (this: void, value: TValue) => void; | ||
/** Set new value */ | ||
set(this: void, value: TValue): void; | ||
} | ||
@@ -51,5 +60,5 @@ | ||
/** @ignore */ | ||
export type ValOnStart = () => void | ValDisposer | undefined; | ||
/** | ||
* Custom config for the val. | ||
*/ | ||
export interface ValConfig<TValue = any> { | ||
@@ -67,3 +76,5 @@ /** | ||
/** @ignore */ | ||
export type UnwrapVal<T> = T extends ReadonlyVal<infer TValue> ? TValue : T; | ||
/** @internal */ | ||
export type ValInputsValueTuple<TValInputs extends readonly ReadonlyVal[]> = | ||
@@ -74,5 +85,5 @@ Readonly<{ | ||
/** @ignore */ | ||
/** @internal */ | ||
export type ExtractValValue<TVal> = TVal extends ReadonlyVal<infer TValue> | ||
? TValue | ||
: never; |
@@ -1,8 +0,2 @@ | ||
import type { ReadonlyValImpl } from "./readonly-val"; | ||
import type { | ||
ReadonlyVal, | ||
ValInputsValueTuple, | ||
ValDisposer, | ||
ValSubscriber, | ||
} from "./typings"; | ||
import type { ReadonlyVal, ValInputsValueTuple } from "./typings"; | ||
@@ -12,2 +6,7 @@ /** Returns the value passed in. */ | ||
export const defaultCompare = <TValue = any>( | ||
newValue: TValue, | ||
oldValue: TValue | ||
): boolean => newValue === oldValue; | ||
const getValue = <TValue>(val: ReadonlyVal<TValue>): TValue => val.value; | ||
@@ -33,4 +32,2 @@ | ||
const VAL_SYMBOL = "$\u2009val\u2009"; | ||
/** | ||
@@ -42,15 +39,8 @@ * Checks if `val` is `ReadonlyVal` or `Val`. | ||
export const isVal = <T>(val: T): val is T extends ReadonlyVal ? T : never => | ||
!!(val && (val as any)[VAL_SYMBOL]); | ||
!!(val as ReadonlyVal | undefined)?.$valCompute; | ||
/** | ||
* Marks an object that implements `ReadonlyVal` interface to be `isVal` detectable. | ||
* @ignore | ||
* @deprecated No longer needed. `isVal` works without `markVal` now. | ||
*/ | ||
export const markVal = <T extends ReadonlyVal>(val: T): T => { | ||
Object.defineProperty(val, VAL_SYMBOL, { value: 1 }); | ||
return val; | ||
}; | ||
export const compute = ( | ||
val: ReadonlyVal, | ||
subscriber: ValSubscriber<void> | ||
): ValDisposer => (val as ReadonlyValImpl)._compute_(subscriber); | ||
export const markVal: <T extends ReadonlyVal>(val: T) => T = identity; |
@@ -1,22 +0,29 @@ | ||
import { ReadonlyValImpl } from "./readonly-val"; | ||
import type { Val, ValConfig } from "./typings"; | ||
class ValImpl<TValue = any> | ||
extends ReadonlyValImpl<TValue> | ||
implements Val<TValue> | ||
{ | ||
public constructor(value: TValue, config?: ValConfig<TValue>) { | ||
super(value, config); | ||
import { ReadonlyValImpl } from "./readonly-val"; | ||
class ValImpl<TValue = any> extends ReadonlyValImpl<TValue> { | ||
public constructor(currentValue: TValue, config?: ValConfig<TValue>) { | ||
const get = () => currentValue; | ||
super(get, config); | ||
this.set = (value: TValue) => { | ||
if (!this.compare(value, currentValue)) { | ||
this._subs_.dirty_ = true; | ||
currentValue = value; | ||
this._subs_.notify_(); | ||
} | ||
}; | ||
} | ||
public override get value(): TValue { | ||
return this._value_; | ||
public set: (this: void, value: TValue) => void; | ||
public override get value() { | ||
return this.get(); | ||
} | ||
public override set value(value: TValue) { | ||
this._set_(value); | ||
this.set(value); | ||
} | ||
/** Set new value */ | ||
public set: (value: TValue) => void = this._set_; | ||
} | ||
@@ -28,3 +35,3 @@ | ||
*/ | ||
export function val(): Val<undefined>; | ||
export function val<TValue>(): Val<TValue | undefined>; | ||
/** | ||
@@ -31,0 +38,0 @@ * Creates a writable val. |
@@ -1,2 +0,2 @@ | ||
import type { ReadonlyVal, Val, ValSubscriber, ValDisposer } from "./typings"; | ||
import type { ReadonlyVal, Val, ValDisposer, ValSubscriber } from "./typings"; | ||
@@ -11,3 +11,3 @@ /** | ||
value: TValue | ||
): void => (val as Val<TValue>).set && (val as Val<TValue>).set(value); | ||
): void => (val as Val<TValue>).set?.(value); | ||
@@ -14,0 +14,0 @@ /** |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
131104
30
3960
414
Yes
1