New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

value-enhancer

Package Overview
Dependencies
Maintainers
1
Versions
87
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

value-enhancer - npm Package Compare versions

Comparing version 1.3.2 to 2.0.0-alpha.0

dist/scheduler.d.ts

25

dist/combine.d.ts

@@ -1,21 +0,20 @@

import { ReadonlyVal } from "./readonly-val";
import type { ValConfig } from "./typings";
import { ReadonlyValImpl } from "./readonly-val";
import type { ReadonlyVal, ValConfig } from "./typings";
export declare type TValInputsValueTuple<TValInputs extends readonly ReadonlyVal[]> = Readonly<{
[K in keyof TValInputs]: ExtractValValue<TValInputs[K]>;
}>;
export declare type ExtractValValue<TVal> = TVal extends ReadonlyVal<infer TValue, any> ? TValue : never;
export declare type ExtractValMeta<TVal> = TVal extends ReadonlyVal<any, infer TMeta> ? TMeta : never;
export declare type ExtractValValue<TVal> = TVal extends ReadonlyVal<infer TValue> ? TValue : never;
export declare type CombineValTransform<TDerivedValue = any, TValues extends readonly any[] = any[], TMeta = any> = (newValues: TValues, oldValues?: TValues, meta?: TMeta) => TDerivedValue;
export declare class CombinedVal<TValInputs extends readonly ReadonlyVal[] = ReadonlyVal[], TValue = any, TMeta = ExtractValMeta<TValInputs[number]>> extends ReadonlyVal<TValue, TMeta> {
export declare class CombinedValImpl<TValInputs extends readonly ReadonlyVal[] = ReadonlyVal[], TValue = any> extends ReadonlyValImpl<TValue> implements ReadonlyVal<TValue> {
constructor(valInputs: TValInputs, transform: CombineValTransform<TValue, [
...TValInputsValueTuple<TValInputs>
], TMeta>, config?: ValConfig<TValue, TMeta>);
]>, config: ValConfig<TValue>);
get value(): TValue;
private _srcValue;
private _sVals;
private _sOldValues;
private _transform;
private _dirty;
private _newValues;
}
export declare function combine<TValInputs extends readonly ReadonlyVal[] = ReadonlyVal[]>(valInputs: readonly [...TValInputs]): ReadonlyVal<[
...TValInputsValueTuple<TValInputs>
], ExtractValMeta<TValInputs[number]>>;
export declare function combine<TValInputs extends readonly ReadonlyVal[] = ReadonlyVal[], TValue = any, TMeta = ExtractValMeta<TValInputs[number]>>(valInputs: readonly [...TValInputs], transform: CombineValTransform<TValue, [
...TValInputsValueTuple<TValInputs>
], TMeta>, config?: ValConfig<TValue, TMeta>): ReadonlyVal<TValue, TMeta>;
export declare function combine<TValInputs extends readonly ReadonlyVal[] = ReadonlyVal[]>(valInputs: readonly [...TValInputs]): ReadonlyVal<[...TValInputsValueTuple<TValInputs>]>;
export declare function combine<TValInputs extends readonly ReadonlyVal[] = ReadonlyVal[], TValue = any>(valInputs: readonly [...TValInputs], transform: CombineValTransform<TValue, [...TValInputsValueTuple<TValInputs>]>, config?: ValConfig<TValue>): ReadonlyVal<TValue>;

@@ -1,9 +0,12 @@

import { ReadonlyVal } from "./readonly-val";
import type { ValConfig, ValTransform } from "./typings";
export declare class DerivedVal<TSrcValue = any, TValue = any, TMeta = any> extends ReadonlyVal<TValue, TMeta> {
constructor(val: ReadonlyVal<TSrcValue>, transform: ValTransform<TSrcValue, TValue>, config?: ValConfig<TValue, TMeta>);
import { ReadonlyValImpl } from "./readonly-val";
import type { ReadonlyVal, ValConfig, ValTransform } from "./typings";
export declare class DerivedValImpl<TSrcValue = any, TValue = any> extends ReadonlyValImpl<TValue> implements ReadonlyVal {
constructor(val: ReadonlyVal<TSrcValue>, transform: ValTransform<TSrcValue, TValue>, config: ValConfig<TValue>);
get value(): TValue;
private _srcValue;
private _sVal;
private _sOldValue;
private _transform;
private _dirty;
}
export declare function derive<TSrcValue = any, TValue = any, TMeta = any>(val: ReadonlyVal<TSrcValue>): ReadonlyVal<TValue, TMeta>;
export declare function derive<TSrcValue = any, TValue = any, TMeta = any>(val: ReadonlyVal<TSrcValue>, transform: ValTransform<TSrcValue, TValue>, config?: ValConfig<TValue, TMeta>): ReadonlyVal<TValue, TMeta>;
export declare function derive<TSrcValue = any, TValue = any>(val: ReadonlyVal<TSrcValue>): ReadonlyVal<TValue>;
export declare function derive<TSrcValue = any, TValue = any>(val: ReadonlyVal<TSrcValue>, transform: ValTransform<TSrcValue, TValue>, config?: ValConfig<TValue>): ReadonlyVal<TValue>;

@@ -1,25 +0,20 @@

import type { ValDisposer, ValSubscriber, ValConfig } from "./typings";
export declare class ReadonlyVal<TValue = any, TMeta = any> {
private _subscribers;
import { Subscribers } from "./subscribers";
import type { ValDisposer, ValSubscriber, ValConfig, ValOnStart, ReadonlyVal, ReadonlyValConfig } from "./typings";
export declare class ReadonlyValImpl<TValue = any> implements ReadonlyVal<TValue> {
protected _subs: Subscribers<TValue>;
protected _value: TValue;
protected _setValue(value: TValue, meta?: TMeta): void;
constructor(value: TValue, config?: ValConfig<TValue, TMeta>);
protected _compare(newValue: TValue, oldValue: TValue): boolean;
protected _set: (value: TValue) => void;
constructor(value: TValue, { compare }?: ValConfig<TValue>, start?: ValOnStart<TValue>);
get value(): TValue;
reaction(subscriber: ValSubscriber<TValue>, eager?: boolean): ValDisposer;
subscribe(subscriber: ValSubscriber<TValue>, eager?: boolean): ValDisposer;
/**
* Subscribe to value changes without immediate emission.
* @internal
* For computed vals
*/
reaction(subscriber: ValSubscriber<TValue, TMeta>): ValDisposer;
/**
* Subscribe to value changes with immediate emission.
* @param subscriber
* @param meta Meta for the immediate emission
*/
subscribe(subscriber: ValSubscriber<TValue, TMeta>, meta?: TMeta): ValDisposer;
destroy(): void;
_compute(subscriber: ValSubscriber<TValue>): ValDisposer;
unsubscribe(): void;
unsubscribe<T extends (...args: any[]) => any>(subscriber: T): void;
get size(): number;
/**
* Compare two values. Default `===`.
*/
compare(newValue: TValue, oldValue: TValue): boolean;
}
export declare function readonlyVal<TValue = any>(value: TValue, config?: ReadonlyValConfig<TValue>): ReadonlyVal<TValue>;

@@ -1,13 +0,27 @@

import type { ValDisposer, ValSubscriber } from "./typings";
export declare class Subscribers<TValue = any, TMeta = any> {
get size(): number;
constructor(beforeSubscribe?: () => void | ValDisposer | undefined);
invoke(newValue: TValue, meta?: TMeta): void;
add(subscribe: ValSubscriber): void;
import type { ReadonlyVal, ValDisposer, ValSubscriber } from "./typings";
export declare type SubscriberMode =
/** Async */
"sub0"
/** Eager */
| "sub1"
/** Computed */
| "sub2";
export declare class Subscribers<TValue = any> {
size: number;
constructor(val: ReadonlyVal<TValue>, initialValue: TValue, start?: (() => void | ValDisposer | undefined) | null);
invoke(): void;
add(subscribe: ValSubscriber, mode: SubscriberMode): void;
remove(subscriber: ValSubscriber): void;
clear(): void;
destroy(): void;
private _subscribers?;
private _bSub?;
private _bSubDisposer?;
exec(mode: SubscriberMode): void;
private val;
/** Async */
private sub0?;
/** Eager */
private sub1?;
/** Computed */
private sub2?;
private oldValue;
private start?;
private stop?;
}

@@ -1,15 +0,36 @@

export declare type ValSetValue<TValue = any, TMeta = any> = (value: TValue, meta?: TMeta) => void;
export interface ReadonlyVal<TValue = any> {
/** value */
readonly value: TValue;
/**
* Subscribe to value changes without immediate emission.
* @param subscriber
* @param eager notify subscribers of value changes synchronously. otherwise subscribers will be notified next tick.
* @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 notify subscribers of value changes synchronously. otherwise subscribers will be notified next tick.
* @returns a disposer function that cancels the subscription
*/
subscribe(subscriber: ValSubscriber<TValue>, eager?: boolean): ValDisposer;
/** remove all subscribers */
unsubscribe(): void;
/** remove the given subscriber */
unsubscribe<T extends (...args: any[]) => any>(subscriber: T): void;
}
export interface Val<TValue = any> extends ReadonlyVal<TValue> {
value: TValue;
/** set new value */
set(value: TValue): void;
}
export declare type ValSetValue<TValue = any> = (value: TValue) => void;
export declare type ValCompare<TValue = any> = (newValue: TValue, oldValue: TValue) => boolean;
export declare type ValSubscriber<TValue = any, TMeta = any> = (newValue: TValue, meta?: TMeta) => void;
export declare type ValSubscriber<TValue = any> = (newValue: TValue) => void;
export declare type ValTransform<TValue = any, TDerivedValue = any> = (newValue: TValue) => TDerivedValue;
export declare type ValDisposer = () => void;
export declare type ValOnStart<TValue = any, TMeta = any> = (setValue: ValSetValue<TValue, TMeta>) => void | ValDisposer | undefined;
export interface ValConfig<TValue = any, TMeta = any> {
export declare type ValOnStart<TValue = any> = (set: ValSetValue<TValue>) => void | ValDisposer | undefined;
export interface ValConfig<TValue = any> {
/**
* A function that is called when the number of subscribers goes from zero to one (but not from one to two, etc).
* That function will be passed a setValue function which changes the value of the val.
* It may optionally return a disposer function that is called when the subscriber count goes from one to zero.
*/
beforeSubscribe?: ValOnStart<TValue, TMeta>;
/**
* Compare two values. Default `===`.

@@ -19,1 +40,9 @@ */

}
export interface ReadonlyValConfig<TValue = any> extends ValConfig<TValue> {
/**
* A function that is called when the number of subscribers goes from zero to one (but not from one to two, etc).
* That function will be passed a set function which changes the value of the val.
* It may optionally return a disposer function that is called when the subscriber count goes from one to zero.
*/
start?: ValOnStart<TValue>;
}

@@ -1,4 +0,3 @@

import { ReadonlyVal } from "./readonly-val";
export declare class Val<TValue = any, TMeta = any> extends ReadonlyVal<TValue, TMeta> {
setValue: (value: TValue, meta?: TMeta) => void;
}
import type { Val, ValConfig } from "./typings";
export declare function val(): Val<undefined>;
export declare function val<TValue = any>(value: TValue, config?: ValConfig<TValue>): Val<TValue>;

@@ -1,8 +0,5 @@

export * from "./typings";
export * from "./readonly-val";
export * from "./val";
export * from "./derived-val";
export * from "./combine";
export * from "./with-readonly-value-enhancer";
export * from "./with-value-enhancer";
export * from "./val-manager";
export type { ReadonlyVal, Val, ValCompare, ValSubscriber, ValTransform, ValConfig, } from "./typings";
export { readonlyVal } from "./readonly-val";
export { val } from "./val";
export { derive } from "./derived-val";
export { combine } from "./combine";
{
"name": "value-enhancer",
"version": "1.3.2",
"version": "2.0.0-alpha.0",
"private": false,

@@ -10,4 +10,4 @@ "description": "A tiny library to enhance value with reactive wrapper.",

"sideEffects": false,
"main": "./dist/value-enhancer.cjs.js",
"module": "./dist/value-enhancer.es.js",
"main": "./dist/value-enhancer.js",
"module": "./dist/value-enhancer.mjs",
"types": "./dist/value-enhancer.d.ts",

@@ -19,15 +19,15 @@ "files": [

"devDependencies": {
"@jest/globals": "^28.1.3",
"@typescript-eslint/eslint-plugin": "^4.31.0",
"@typescript-eslint/parser": "^4.31.1",
"c8": "^7.11.0",
"cross-env": "^7.0.3",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"jsdom": "^19.0.0",
"jest": "^28.1.3",
"prettier": "^2.4.0",
"standard-version": "^9.3.1",
"ts-jest": "^28.0.8",
"typedoc": "^0.22.3",
"typescript": "^4.4.3",
"vite": "^2.5.6",
"vitest": "^0.8.4"
"vite": "^3.0.9"
},

@@ -37,3 +37,3 @@ "scripts": {

"lint": "eslint --ext .ts,.tsx . && prettier --check .",
"test": "vitest run --coverage",
"test": "jest --coverage",
"docs": "typedoc --includeVersion --excludePrivate --out docs src/value-enhancer.ts",

@@ -40,0 +40,0 @@ "types": "cross-env NODE_ENV=production tsc --declaration --emitDeclarationOnly --jsx react --esModuleInterop --outDir dist",

@@ -18,2 +18,4 @@ # value-enhancer

[(v1 docs)](https://github.com/crimx/value-enhancer/tree/v1)
## Install

@@ -25,193 +27,139 @@

## Why
## Features
The goal of this lib is to bring reactivity to values like MobX but without the implicit-cast magic. It is like RxJS but trimmed and simplified with the focus on value changes instead of async operations which resulted in much smaller codebase.
- RxJS-style reactivity.
Without the implicit-cast Proxy magic like Vue Reactivity and MobX.
- Single-layer shallow reactivity.
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.
- Explicit.
Reactive objects are easy to tell since their types are different from normal objects. Subscriptions also require explicit dependency declaration which reduce the work of repetitive dynamic dependency collection in Proxy implementations.
- Bundle size and Performance.
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.
## Usage
## Quick Q&A
```js
import { Val, combine, derive } from "value-enhancer";
<details>
<summary>Why not MobX?</summary>
const val = new Val(2);
MobX is cleverly designed to make properties magically reactive. But after using it in many of our large projects people started to complain about this implicit behavior. It is hard to tell if a property is reactive unless enforcing some kind of code style rules. Rules of MobX are easy to be broken especially for new team members.
console.log(val.value); // 2
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.
val.setValue(3);
console.log(val.value); // 3
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.
val.subscribe(value => console.log(`subscribe: ${value}`)); // subscribe: 3
</details>
val.reaction(value => console.log(`reaction: ${value}`)); // (nothing printed)
<details>
<summary>Why not Vue Reactivity?</summary>
val.setValue(3); // nothing happened
Vue3 brings Reactivity as standalone APIs. It is beautifully designed and I had learned a lot from its source code.
val.setValue(4); // subscribe: 4, reaction: 4
But even though it is made standalone, it is still very Vue centered. Many extra works related to Vue components is added under the hood.
const derived = derive(val, value => value * 3);
console.log(derived.value); // 12
derived.subscribe(value => console.log(`derived: ${value}`)); // derived: 12
Vue supports deep reactive lazy conversion. It converts plain JavaScript values into reactive values which means it also suffers from the same issues of MobX.
const combined = combine([val, derived], ([val, derived]) => val + derived);
console.log(combined.value); // 16
combined.subscribe(value => console.log(`combined: ${value}`)); // combined: 16
It is a good choice if you are choosing the Vue ecosystem. The implementation of `value-enhancer` absorbs many good parts from Vue Reactivity and stays framework agnostic.
val.setValue(5); // subscribe: 5, reaction: 5, derived: 15, combined: 20
```
</details>
`setValue` may carry any type of extra info via `meta` param.
<details>
<summary>Why not RxJS?</summary>
```ts
val.subscribe((value, meta) => console.log(value, meta));
I love RxJS and the reactive paradigm behind it. The goal of RxJS is to compose asynchronous or callback-based code.
val.setValue(5, { source: "remote" });
```
It requires you to write code in a pipe-able way which may not be acceptable from everyone.
### Bind Vals To An Instance
</details>
Bind Vals `value`, `setValue` and itself to properties of an instance.
<details>
<summary>What about React Hooks?</summary>
The signature of `combine` and `derive` may look familiar for those who have used React hooks.
```ts
import type { ValEnhancedResult } from "value-enhancer";
import { Val, withValueEnhancer } from "value-enhancer";
import { useMemo } from "react";
type ValConfig = {
apple: Val<string>;
banana: Val<string>;
};
const derived = useMemo(() => source + 1, [source]);
```
interface Obj extends ValEnhancedResult<ValConfig> {}
I really like the explicit dependency declaration. But in React it is error-prone since people keep forgetting to add or remove dependencies. The React team even made a `exhaustive-deps` linter rule for this.
class Obj {
constructor() {
const apple$ = new Val("apple");
const banana$ = new Val("banana");
`value-enhancer` solves this by absorbing the RxJS-style callbacks.
withValueEnhancer(this, {
apple: apple$,
banana: banana$,
});
}
}
```
```ts
import { val, derive, combine } from "value-enhancer";
`const obj = new Obj()` results in:
const source$ = val(1);
console.log(source$.value); // 1
- `obj.apple`, a getter that returns `apple$.value`, setter same as `apple$.setValue(value)`
- `obj._apple$`, the `apple$`
- `obj.setApple(value)`, same as `apple$.setValue(value)`
- `obj.banana`, a getter that returns `banana$.value`, setter same as `banana$.setValue(value)`
- `obj.setBanana(value)`, same as `banana$.setValue(value)`
- `obj._banana$`, the `banana$`
- `obj.onValChanged(key: "apple" | "banana", listener)`, equals to calling <code>obj[\`_${key}$\`].reaction</code>
const derived$ = derive(source$, source => source + 1);
console.log(derived$.value); // 2
### Bind ReadonlyVals/Vals To An Instance
const combined$ = combine(
[source$, derived$],
([source, derived]) => source + derived
);
console.log(combined$.value); // 3
```
Like `withValueEnhancer`, `withReadonlyValueEnhancer` binds ReadonlyVals/Vals to a instance but without setters
Since the type of reactive objects are different from its values, it is hard to have mismatched dependencies inside the `transform` function.
```ts
import type { ReadonlyValEnhancedResult } from "value-enhancer";
import { Val, withReadonlyValueEnhancer } from "value-enhancer";
`value-enhancer` can be used in React with [`use-value-enhancer`](https://www.npmjs.com/package/use-value-enhancer).
type ReadonlyValConfig = {
apple: Val<string>;
isApple: ReadonlyVal<boolean>;
};
</details>
interface Obj extends ReadonlyValEnhancedResult<ReadonlyValConfig> {}
<details>
<summary>Svelte Stores?</summary>
class Obj {
constructor() {
const apple$ = new Val("apple");
const isApple$ = derive(apple$, apple => apple === "apple");
Svelte offers excellent support for Observables. Svelte store is one of the simplest implementation. The code is really neat and clean.
withReadonlyValueEnhancer(this, {
apple: apple$,
isApple: isApple$,
});
}
}
```
Svelte store works well for simple cases but it also leaves some edge cases unresolved. For example, when `derived` a list of stores, the transform function could be [invoked with intermediate states](https://svelte.dev/repl/6218ae0ecf5c455195b4a76d7f0cff9f?version=3.49.0).
`const obj = new Obj()` results in:
`value-enhancer` is compatible with Svelte Store contract, which means it can be used in Svelte just like Svelte stores.
- `obj.apple`, a getter that returns `apple$.value`
- `obj._apple$`, the `apple$`
- `obj.isApple`, a getter that returns `isApple$.value`
- `obj._isApple$`, the `isApple$`
- `obj.onValChanged(key: "apple" | "isApple", listener)`, equals to calling <code>obj[\`_${key}$\`].reaction</code>
</details>
### ValManager
<details>
<summary>SolidJS?</summary>
Manage life-cycles of a list of Vals (e.g. auto cleanup).
SolidJS "create"s are like React hooks but with saner signatures. It is also thoughtfully optimized for edge cases.
```ts
import { Val, ValManager } from "value-enhancer";
A thing that one may feel odd in SolidJS is accessing reactive value by calling it as function. `value-enhancer` keeps the `xxx.value` way to access reactive value which I think should be more intuitive.
const valManager = new ValManager();
const val1 = valManager.attach(new Val("12345"));
const val2 = valManager.attach(new Val(""));
`value-enhancer` is compatible with SolidJS using [`from`](https://www.solidjs.com/docs/latest/api#from).
valManager.destroy(); // val1.destroy() and val2.destroy() are called
```
</details>
Combined with `withValueEnhancer` or `withReadonlyValueEnhancer`:
## Usage
```js
import type { ValEnhancedResult } from "value-enhancer";
import { Val, withValueEnhancer, ValManager } from "value-enhancer";
import { val, combine, derive } from "value-enhancer";
class Obj {
constructor() {
this.valManager = new ValManager();
const count$ = val(2);
const apple$ = new Val("apple");
const banana$ = new Val("banana");
console.log(count$.value); // 2
withValueEnhancer(
this,
{
apple: apple$,
banana: banana$,
},
this.valManager
);
}
count$.set(3);
console.log(count$.value); // 3
destroy() {
this.valManager.destroy();
}
}
```
count$.subscribe(count => console.log(`subscribe: ${count}`)); // subscribe: 3
And `valManager` may keep private with [`side-effect-manager`](https://github.com/crimx/side-effect-manager):
count$.reaction(count => console.log(`reaction: ${count}`)); // (nothing printed)
```js
import type { ValEnhancedResult } from "value-enhancer";
import { Val, withValueEnhancer, ValManager } from "value-enhancer";
import { SideEffectManager } from "side-effect-manager";
count$.set(3); // nothing happened
class Obj {
constructor() {
this.sideEffect = new SideEffectManager();
count$.value = 4; // subscribe: 4, reaction: 4
const valManager = new ValManager();
this.sideEffect.addDisposer(() => valManager.destroy());
const derive$ = derive(count$, count => count * 3);
console.log(derived$.value); // 12
derived$.subscribe(derived => console.log(`derived: ${derived}`)); // derived: 12
const apple$ = new Val("apple");
const banana$ = new Val("banana");
const combined$ = combine(
[count$, derived$],
([count, derived]) => count + derived
);
console.log(combined$.value); // 16
combined$.subscribe(combined => console.log(`combined: ${combined}`)); // combined: 16
withValueEnhancer(
this,
{
apple: apple$,
banana: banana$,
},
valManager
);
}
destroy() {
this.sideEffect.flushAll();
}
}
count$.set(5); // subscribe: 5, reaction: 5, derived: 15, combined: 20
```

@@ -1,3 +0,3 @@

import { ReadonlyVal } from "./readonly-val";
import type { ValConfig } from "./typings";
import { ReadonlyValImpl } from "./readonly-val";
import type { ReadonlyVal, ValConfig } from "./typings";

@@ -9,10 +9,6 @@ export type TValInputsValueTuple<TValInputs extends readonly ReadonlyVal[]> =

export type ExtractValValue<TVal> = TVal extends ReadonlyVal<infer TValue, any>
export type ExtractValValue<TVal> = TVal extends ReadonlyVal<infer TValue>
? TValue
: never;
export type ExtractValMeta<TVal> = TVal extends ReadonlyVal<any, infer TMeta>
? TMeta
: never;
export type CombineValTransform<

@@ -24,7 +20,9 @@ TDerivedValue = any,

export class CombinedVal<
TValInputs extends readonly ReadonlyVal[] = ReadonlyVal[],
TValue = any,
TMeta = ExtractValMeta<TValInputs[number]>
> extends ReadonlyVal<TValue, TMeta> {
export class CombinedValImpl<
TValInputs extends readonly ReadonlyVal[] = ReadonlyVal[],
TValue = any
>
extends ReadonlyValImpl<TValue>
implements ReadonlyVal<TValue>
{
public constructor(

@@ -34,44 +32,35 @@ valInputs: TValInputs,

TValue,
[...TValInputsValueTuple<TValInputs>],
TMeta
[...TValInputsValueTuple<TValInputs>]
>,
config: ValConfig<TValue, TMeta> = {}
config: ValConfig<TValue>
) {
super(transform(getValues(valInputs)), {
...config,
beforeSubscribe: setValue => {
let lastValueInputs = getValues(valInputs);
setValue(transform(lastValueInputs));
const disposers = valInputs.map((val, i) =>
val.reaction((value, meta) => {
lastValueInputs = lastValueInputs.slice() as [
...TValInputsValueTuple<TValInputs>
];
lastValueInputs[i] = value;
setValue(transform(lastValueInputs), meta);
})
);
const disposer = () => disposers.forEach(disposer => disposer());
if (config.beforeSubscribe) {
const beforeSubscribeDisposer = config.beforeSubscribe(setValue);
if (beforeSubscribeDisposer) {
return () => {
disposer();
beforeSubscribeDisposer();
};
const sOldValues = getValues(valInputs);
super(transform(sOldValues), config, () => {
const disposers = valInputs.map(val =>
(val as ReadonlyValImpl)._compute(() => {
if (!this._dirty) {
this._dirty = true;
this._subs.invoke();
}
}
return disposer;
},
})
);
return () => disposers.forEach(dispose);
});
this._srcValue = () => transform(getValues(valInputs));
this._sVals = valInputs;
this._sOldValues = sOldValues;
this._transform = transform;
}
public override get value(): TValue {
if (this.size <= 0) {
const value = this._srcValue();
return this.compare(value, this._value) ? this._value : value;
if (this._dirty || this._subs.size <= 0) {
this._dirty = false;
const sNewValues = this._newValues();
if (sNewValues !== this._sOldValues) {
this._sOldValues = sNewValues;
const value = this._transform(sNewValues);
if (!this._compare(value, this._value)) {
this._value = value;
}
}
}

@@ -81,3 +70,18 @@ return this._value;

private _srcValue: () => TValue;
private _sVals: TValInputs;
private _sOldValues: [...TValInputsValueTuple<TValInputs>];
private _transform: CombineValTransform<
TValue,
[...TValInputsValueTuple<TValInputs>]
>;
private _dirty = false;
private _newValues(): [...TValInputsValueTuple<TValInputs>] {
for (let i = 0; i < this._sVals.length; i++) {
if (this._sVals[i].value !== this._sOldValues[i]) {
return getValues(this._sVals);
}
}
return this._sOldValues;
}
}

@@ -95,2 +99,6 @@

function dispose(disposer: () => void) {
disposer();
}
export function combine<

@@ -100,23 +108,14 @@ TValInputs extends readonly ReadonlyVal[] = ReadonlyVal[]

valInputs: readonly [...TValInputs]
): ReadonlyVal<
[...TValInputsValueTuple<TValInputs>],
ExtractValMeta<TValInputs[number]>
>;
): ReadonlyVal<[...TValInputsValueTuple<TValInputs>]>;
export function combine<
TValInputs extends readonly ReadonlyVal[] = ReadonlyVal[],
TValue = any,
TMeta = ExtractValMeta<TValInputs[number]>
TValue = any
>(
valInputs: readonly [...TValInputs],
transform: CombineValTransform<
TValue,
[...TValInputsValueTuple<TValInputs>],
TMeta
>,
config?: ValConfig<TValue, TMeta>
): ReadonlyVal<TValue, TMeta>;
transform: CombineValTransform<TValue, [...TValInputsValueTuple<TValInputs>]>,
config?: ValConfig<TValue>
): ReadonlyVal<TValue>;
export function combine<
TValInputs extends readonly ReadonlyVal[] = ReadonlyVal[],
TValue = any,
TMeta = ExtractValMeta<TValInputs[number]>
TValue = any
>(

@@ -126,8 +125,7 @@ valInputs: readonly [...TValInputs],

TValue,
[...TValInputsValueTuple<TValInputs>],
TMeta
[...TValInputsValueTuple<TValInputs>]
> = value => value as TValue,
config: ValConfig<TValue, TMeta> = {}
): ReadonlyVal<TValue, TMeta> {
return new CombinedVal(valInputs, transform, config);
config: ValConfig<TValue> = {}
): ReadonlyVal<TValue> {
return new CombinedValImpl(valInputs, transform, config);
}

@@ -1,40 +0,38 @@

import { ReadonlyVal } from "./readonly-val";
import type { ValConfig, ValTransform } from "./typings";
import { ReadonlyValImpl } from "./readonly-val";
import type { ReadonlyVal, ValConfig, ValTransform } from "./typings";
export class DerivedVal<
TSrcValue = any,
TValue = any,
TMeta = any
> extends ReadonlyVal<TValue, TMeta> {
export class DerivedValImpl<TSrcValue = any, TValue = any>
extends ReadonlyValImpl<TValue>
implements ReadonlyVal
{
public constructor(
val: ReadonlyVal<TSrcValue>,
transform: ValTransform<TSrcValue, TValue>,
config: ValConfig<TValue, TMeta> = {}
config: ValConfig<TValue>
) {
super(transform(val.value), {
...config,
beforeSubscribe: setValue => {
const disposer = val.subscribe((newValue, meta) =>
setValue(transform(newValue), meta)
);
if (config.beforeSubscribe) {
const beforeSubscribeDisposer = config.beforeSubscribe(setValue);
if (beforeSubscribeDisposer) {
return () => {
disposer();
beforeSubscribeDisposer();
};
}
super(transform(val.value), config, () =>
(val as ReadonlyValImpl)._compute(() => {
if (!this._dirty) {
this._dirty = true;
this._subs.invoke();
}
return disposer;
},
});
})
);
this._srcValue = () => transform(val.value);
this._sVal = val;
this._sOldValue = val.value;
this._transform = transform;
}
public override get value(): TValue {
if (this.size <= 0) {
const value = this._srcValue();
return this.compare(value, this._value) ? this._value : value;
if (this._dirty || this._subs.size <= 0) {
this._dirty = false;
const newValue = this._sVal.value;
if (this._sOldValue !== newValue) {
this._sOldValue = newValue;
const value = this._transform(newValue);
if (!this._compare(value, this._value)) {
this._value = value;
}
}
}

@@ -44,20 +42,24 @@ return this._value;

private _srcValue: () => TValue;
private _sVal: ReadonlyVal<TSrcValue>;
private _sOldValue: TSrcValue;
private _transform: ValTransform<TSrcValue, TValue>;
private _dirty = false;
}
export function derive<TSrcValue = any, TValue = any, TMeta = any>(
export function derive<TSrcValue = any, TValue = any>(
val: ReadonlyVal<TSrcValue>
): ReadonlyVal<TValue, TMeta>;
export function derive<TSrcValue = any, TValue = any, TMeta = any>(
): ReadonlyVal<TValue>;
export function derive<TSrcValue = any, TValue = any>(
val: ReadonlyVal<TSrcValue>,
transform: ValTransform<TSrcValue, TValue>,
config?: ValConfig<TValue, TMeta>
): ReadonlyVal<TValue, TMeta>;
export function derive<TSrcValue = any, TValue = any, TMeta = any>(
config?: ValConfig<TValue>
): ReadonlyVal<TValue>;
export function derive<TSrcValue = any, TValue = any>(
val: ReadonlyVal<TSrcValue>,
transform: ValTransform<TSrcValue, TValue> = value =>
value as unknown as TValue,
config: ValConfig<TValue, TMeta> = {}
): ReadonlyVal<TValue, TMeta> {
return new DerivedVal(val, transform, config);
config: ValConfig<TValue> = {}
): ReadonlyVal<TValue> {
return new DerivedValImpl(val, transform, config);
}
import { Subscribers } from "./subscribers";
import type { ValDisposer, ValSubscriber, ValConfig } from "./typings";
import type {
ValDisposer,
ValSubscriber,
ValConfig,
ValOnStart,
ReadonlyVal,
ReadonlyValConfig,
} from "./typings";
export class ReadonlyVal<TValue = any, TMeta = any> {
private _subscribers: Subscribers<TValue, TMeta>;
export class ReadonlyValImpl<TValue = any> implements ReadonlyVal<TValue> {
protected _subs: Subscribers<TValue>;
protected _value: TValue;
protected _setValue(value: TValue, meta?: TMeta): void {
if (!this.compare(value, this._value)) {
protected _compare(newValue: TValue, oldValue: TValue): boolean {
return newValue === oldValue;
}
protected _set = (value: TValue): void => {
if (!this._compare(value, this._value)) {
this._value = value;
this._subscribers.invoke(value, meta);
this._subs.invoke();
}
}
};
public constructor(value: TValue, config?: ValConfig<TValue, TMeta>) {
public constructor(
value: TValue,
{ compare }: ValConfig<TValue> = {},
start?: ValOnStart<TValue>
) {
this._value = value;
let beforeSubscribe: undefined | (() => void | ValDisposer | undefined);
if (config) {
if (config.compare) {
this.compare = config.compare;
}
if (config.beforeSubscribe) {
const _beforeSubscribe = config.beforeSubscribe;
const _setValue = this._setValue.bind(this);
beforeSubscribe = () => _beforeSubscribe(_setValue);
}
if (compare) {
this._compare = compare;
}
this._subscribers = new Subscribers<TValue, TMeta>(beforeSubscribe);
this._subs = new Subscribers<TValue>(
this,
value,
start ? () => start(this._set) : null
);
}

@@ -39,45 +49,48 @@

/**
* Subscribe to value changes without immediate emission.
*/
public reaction(subscriber: ValSubscriber<TValue, TMeta>): ValDisposer {
this._subscribers.add(subscriber);
return (): void => {
this._subscribers.remove(subscriber);
};
public reaction(
subscriber: ValSubscriber<TValue>,
eager = false
): ValDisposer {
this._subs.add(subscriber, eager ? "sub1" : "sub0");
return (): void => this._subs.remove(subscriber);
}
/**
* Subscribe to value changes with immediate emission.
* @param subscriber
* @param meta Meta for the immediate emission
*/
public subscribe(
subscriber: ValSubscriber<TValue, TMeta>,
meta?: TMeta
subscriber: ValSubscriber<TValue>,
eager = false
): ValDisposer {
const disposer = this.reaction(subscriber);
subscriber(this._value, meta);
const disposer = this.reaction(subscriber, eager);
try {
subscriber(this.value);
} catch (e) {
console.error(e);
}
return disposer;
}
public destroy(): void {
this._subscribers.destroy();
/**
* @internal
* For computed vals
*/
public _compute(subscriber: ValSubscriber<TValue>): ValDisposer {
this._subs.add(subscriber, "sub2");
return (): void => this._subs.remove(subscriber);
}
public unsubscribe<T extends (...args: any[]) => any>(subscriber: T): void {
this._subscribers.remove(subscriber);
public unsubscribe(): void;
public unsubscribe<T extends (...args: any[]) => any>(subscriber: T): void;
public unsubscribe<T extends (...args: any[]) => any>(subscriber?: T): void {
if (subscriber) {
this._subs.remove(subscriber);
} else {
this._subs.clear();
}
}
}
public get size(): number {
return this._subscribers.size;
}
/**
* Compare two values. Default `===`.
*/
public compare(newValue: TValue, oldValue: TValue): boolean {
return newValue === oldValue;
}
export function readonlyVal<TValue = any>(
value: TValue,
config: ReadonlyValConfig<TValue> = {}
): ReadonlyVal<TValue> {
return new ReadonlyValImpl(value, config, config.start);
}

@@ -1,62 +0,108 @@

import type { ValDisposer, ValSubscriber } from "./typings";
import { cancelTask, schedule } from "./scheduler";
import type { ReadonlyVal, ValDisposer, ValSubscriber } from "./typings";
export class Subscribers<TValue = any, TMeta = any> {
public get size(): number {
return this._subscribers ? this._subscribers.size : 0;
}
export type SubscriberMode =
/** Async */
| "sub0"
/** Eager */
| "sub1"
/** Computed */
| "sub2";
public constructor(beforeSubscribe?: () => void | ValDisposer | undefined) {
this._bSub = beforeSubscribe;
export class Subscribers<TValue = any> {
public size = 0;
public constructor(
val: ReadonlyVal<TValue>,
initialValue: TValue,
start?: (() => void | ValDisposer | undefined) | null
) {
this.val = val;
this.oldValue = initialValue;
this.start = start;
}
public invoke(newValue: TValue, meta?: TMeta): void {
if (this._subscribers) {
this._subscribers.forEach(subscriber => subscriber(newValue, meta));
public invoke(): void {
if (this.size > 0) {
this.exec("sub2");
this.exec("sub1");
if (this.sub0?.size) {
schedule(this);
}
}
}
public add(subscribe: ValSubscriber): void {
if (this._bSub && (!this._subscribers || this._subscribers.size <= 0)) {
this._bSubDisposer = this._bSub();
public add(subscribe: ValSubscriber, mode: SubscriberMode): void {
if (this.start && this.size <= 0) {
this.stop = this.start();
}
if (!this._subscribers) {
this._subscribers = new Set();
if (!this[mode]) {
this[mode] = new Set<ValSubscriber<TValue>>();
}
this._subscribers.add(subscribe);
this[mode]!.add(subscribe);
this.size += 1;
}
public remove(subscriber: ValSubscriber): void {
if (this._subscribers) {
this._subscribers.delete(subscriber);
this.sub0?.delete(subscriber);
this.sub1?.delete(subscriber);
this.sub2?.delete(subscriber);
this.size =
(this.sub0?.size || 0) + (this.sub1?.size || 0) + (this.sub2?.size || 0);
if (this.size <= 0 && this.stop) {
const _bSubDisposer = this.stop;
this.stop = null;
_bSubDisposer();
}
if (this._subscribers && this._subscribers.size <= 0) {
if (this._bSubDisposer) {
const _bSubDisposer = this._bSubDisposer;
this._bSubDisposer = null;
_bSubDisposer();
}
}
}
public clear(): void {
if (this._subscribers) {
this._subscribers.clear();
this.sub0?.clear();
this.sub1?.clear();
this.sub2?.clear();
this.size = 0;
cancelTask(this);
if (this.stop) {
const disposer = this.stop;
this.stop = null;
disposer();
}
if (this._bSubDisposer) {
const _bSubDisposer = this._bSubDisposer;
this._bSubDisposer = null;
_bSubDisposer();
}
}
public destroy(): void {
this.clear();
public exec(mode: SubscriberMode): void {
const subs = this[mode];
if (subs) {
if (subs === this.sub0) {
const newValue = this.val.value;
if (this.oldValue === newValue) {
return;
}
this.oldValue = newValue;
}
const value = this.val.value;
for (const sub of subs) {
try {
sub(value);
} catch (e) {
console.error(e);
}
}
}
}
private _subscribers?: Set<ValSubscriber<TValue, TMeta>>;
private val: ReadonlyVal<TValue>;
private _bSub?: () => void | ValDisposer | undefined;
private _bSubDisposer?: ValDisposer | void | null;
/** Async */
private sub0?: Set<ValSubscriber<TValue>>;
/** Eager */
private sub1?: Set<ValSubscriber<TValue>>;
/** Computed */
private sub2?: Set<ValSubscriber<TValue>>;
private oldValue: TValue;
private start?: (() => void | ValDisposer | undefined) | null;
private stop?: ValDisposer | void | null;
}

@@ -1,6 +0,32 @@

export type ValSetValue<TValue = any, TMeta = any> = (
value: TValue,
meta?: TMeta
) => void;
export interface ReadonlyVal<TValue = any> {
/** value */
readonly value: TValue;
/**
* Subscribe to value changes without immediate emission.
* @param subscriber
* @param eager notify subscribers of value changes synchronously. otherwise subscribers will be notified next tick.
* @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 notify subscribers of value changes synchronously. otherwise subscribers will be notified next tick.
* @returns a disposer function that cancels the subscription
*/
subscribe(subscriber: ValSubscriber<TValue>, eager?: boolean): ValDisposer;
/** remove all subscribers */
unsubscribe(): void;
/** remove the given subscriber */
unsubscribe<T extends (...args: any[]) => any>(subscriber: T): void;
}
export interface Val<TValue = any> extends ReadonlyVal<TValue> {
value: TValue;
/** set new value */
set(value: TValue): void;
}
export type ValSetValue<TValue = any> = (value: TValue) => void;
export type ValCompare<TValue = any> = (

@@ -11,6 +37,3 @@ newValue: TValue,

export type ValSubscriber<TValue = any, TMeta = any> = (
newValue: TValue,
meta?: TMeta
) => void;
export type ValSubscriber<TValue = any> = (newValue: TValue) => void;

@@ -23,14 +46,8 @@ export type ValTransform<TValue = any, TDerivedValue = any> = (

export type ValOnStart<TValue = any, TMeta = any> = (
setValue: ValSetValue<TValue, TMeta>
export type ValOnStart<TValue = any> = (
set: ValSetValue<TValue>
) => void | ValDisposer | undefined;
export interface ValConfig<TValue = any, TMeta = any> {
export interface ValConfig<TValue = any> {
/**
* A function that is called when the number of subscribers goes from zero to one (but not from one to two, etc).
* That function will be passed a setValue function which changes the value of the val.
* It may optionally return a disposer function that is called when the subscriber count goes from one to zero.
*/
beforeSubscribe?: ValOnStart<TValue, TMeta>;
/**
* Compare two values. Default `===`.

@@ -40,1 +57,10 @@ */

}
export interface ReadonlyValConfig<TValue = any> extends ValConfig<TValue> {
/**
* A function that is called when the number of subscribers goes from zero to one (but not from one to two, etc).
* That function will be passed a set function which changes the value of the val.
* It may optionally return a disposer function that is called when the subscriber count goes from one to zero.
*/
start?: ValOnStart<TValue>;
}

@@ -1,5 +0,29 @@

import { ReadonlyVal } from "./readonly-val";
import { ReadonlyValImpl } from "./readonly-val";
import type { Val, ValConfig } from "./typings";
export class Val<TValue = any, TMeta = any> extends ReadonlyVal<TValue, TMeta> {
public setValue: (value: TValue, meta?: TMeta) => void = this._setValue;
class ValImpl<TValue = any>
extends ReadonlyValImpl<TValue>
implements Val<TValue>
{
public override get value(): TValue {
return this._value;
}
public override set value(value: TValue) {
this._set(value);
}
public set: (value: TValue) => void = this._set;
/** @alias set */
public setValue: (value: TValue) => void = this._set;
}
export function val(): Val<undefined>;
export function val<TValue = any>(
value: TValue,
config?: ValConfig<TValue>
): Val<TValue>;
export function val<TValue = any>(
value?: TValue,
config?: ValConfig<TValue>
): Val<TValue> {
return new ValImpl(value as TValue, config);
}

@@ -1,8 +0,12 @@

export * from "./typings";
export * from "./readonly-val";
export * from "./val";
export * from "./derived-val";
export * from "./combine";
export * from "./with-readonly-value-enhancer";
export * from "./with-value-enhancer";
export * from "./val-manager";
export type {
ReadonlyVal,
Val,
ValCompare,
ValSubscriber,
ValTransform,
ValConfig,
} from "./typings";
export { readonlyVal } from "./readonly-val";
export { val } from "./val";
export { derive } from "./derived-val";
export { combine } from "./combine";
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc