@furystack/utils
Advanced tools
Comparing version 5.0.0 to 6.0.0
import type { Disposable } from './disposable.js'; | ||
import type { ValueObserverOptions } from './value-observer.js'; | ||
import { ValueObserver } from './value-observer.js'; | ||
@@ -13,2 +14,11 @@ /** | ||
export type ValueChangeCallback<T> = (next: T) => void; | ||
export type ObservableValueOptions<T> = { | ||
/** | ||
* Defines a custom compare function to determine if the value should be updated and the observers should be notified | ||
* @param lastValue the last value | ||
* @param nextValue the next value | ||
* @returns if the value should be updated and the observers should be notified | ||
*/ | ||
compare: (lastValue: T, nextValue: T) => boolean; | ||
}; | ||
/** | ||
@@ -46,6 +56,6 @@ * Defines an ObservableValue value object. | ||
* @param callback The callback method that will be called on each change | ||
* @param getLast Will call the callback with the last known value right after subscription | ||
* @param options Additional ObservableValue options | ||
* @returns The ValueObserver instance | ||
*/ | ||
subscribe(callback: ValueChangeCallback<T>, getLast?: boolean): ValueObserver<T>; | ||
subscribe(callback: ValueChangeCallback<T>, options?: ValueObserverOptions<T>): ValueObserver<T>; | ||
/** | ||
@@ -72,7 +82,9 @@ * The observer will unsubscribe from the Observable | ||
getObservers(): readonly ValueObserver<T>[]; | ||
private readonly options; | ||
/** | ||
* @param initialValue Optional initial value | ||
* @param options Additional options | ||
*/ | ||
constructor(initialValue: T); | ||
constructor(initialValue: T, options?: Partial<ObservableValueOptions<T>>); | ||
} | ||
//# sourceMappingURL=observable-value.d.ts.map |
@@ -10,2 +10,3 @@ import { ValueObserver } from './value-observer.js'; | ||
} | ||
const defaultComparer = (a, b) => a !== b; | ||
/** | ||
@@ -42,2 +43,4 @@ * Defines an ObservableValue value object. | ||
this._isDisposed = true; | ||
// @ts-expect-error getting currentValue after disposing is not allowed | ||
this.currentValue = null; | ||
} | ||
@@ -49,14 +52,11 @@ observers = new Set(); | ||
* @param callback The callback method that will be called on each change | ||
* @param getLast Will call the callback with the last known value right after subscription | ||
* @param options Additional ObservableValue options | ||
* @returns The ValueObserver instance | ||
*/ | ||
subscribe(callback, getLast = false) { | ||
subscribe(callback, options) { | ||
if (this._isDisposed) { | ||
throw new ObservableAlreadyDisposedError(); | ||
} | ||
const observer = new ValueObserver(this, callback); | ||
const observer = new ValueObserver(this, callback, options); | ||
this.observers.add(observer); | ||
if (getLast) { | ||
callback(this.currentValue); | ||
} | ||
return observer; | ||
@@ -90,7 +90,9 @@ } | ||
} | ||
if (this.currentValue !== newValue) { | ||
if (this.options.compare(this.currentValue, newValue)) { | ||
this.currentValue = newValue; | ||
for (const subscription of this.observers) { | ||
subscription.callback(newValue); | ||
} | ||
this.observers.forEach((observer) => { | ||
if (observer.options?.filter?.(this.currentValue, newValue) !== false) { | ||
observer.callback(newValue); | ||
} | ||
}); | ||
} | ||
@@ -105,6 +107,12 @@ } | ||
} | ||
options; | ||
/** | ||
* @param initialValue Optional initial value | ||
* @param options Additional options | ||
*/ | ||
constructor(initialValue) { | ||
constructor(initialValue, options) { | ||
this.options = { | ||
compare: defaultComparer, | ||
...options, | ||
}; | ||
this.currentValue = initialValue; | ||
@@ -111,0 +119,0 @@ } |
@@ -9,18 +9,8 @@ import { describe, it, expect, vi } from 'vitest'; | ||
const v = new ObservableValue(undefined); | ||
const doneCallback = vi.fn(); | ||
v.subscribe(() => { | ||
expect(v.getValue()).toBe(undefined); | ||
doneCallback(); | ||
}, true); | ||
expect(v).toBeInstanceOf(ObservableValue); | ||
expect(doneCallback).toBeCalled(); | ||
expect(v.getValue()).toBe(undefined); | ||
}); | ||
it('should be constructed with initial value', () => { | ||
const v = new ObservableValue(1); | ||
const doneCallback = vi.fn(); | ||
v.subscribe(() => { | ||
expect(v.getValue()).toBe(1); | ||
doneCallback(); | ||
}, true); | ||
expect(doneCallback).toBeCalled(); | ||
expect(v.getValue()).toBe(1); | ||
}); | ||
@@ -34,3 +24,3 @@ describe('Subscription callback', () => { | ||
doneCallback(); | ||
}, false); | ||
}); | ||
v.setValue(1); | ||
@@ -41,3 +31,3 @@ v.setValue(1); | ||
}); | ||
it('should be triggered only on change when getLast is false', () => { | ||
it('should be triggered only on change', () => { | ||
const v = new ObservableValue(1); | ||
@@ -48,3 +38,3 @@ const doneCallback = vi.fn(); | ||
doneCallback(); | ||
}, false); | ||
}); | ||
v.setValue(2); | ||
@@ -126,3 +116,39 @@ expect(doneCallback).toBeCalledTimes(1); | ||
}); | ||
describe('Custom Compare function', () => { | ||
it('Should compare the values with the custom compare function', () => { | ||
const v = new ObservableValue({ value: 2 }, { | ||
compare: (a, b) => a.value !== b.value, | ||
}); | ||
const onChange = vi.fn(); | ||
v.subscribe(onChange); | ||
v.setValue({ value: 2 }); | ||
expect(v.getValue()).toEqual({ value: 2 }); | ||
expect(onChange).not.toBeCalled(); | ||
v.setValue({ value: 3 }); | ||
expect(v.getValue()).toEqual({ value: 3 }); | ||
expect(onChange).toBeCalledTimes(1); | ||
expect(onChange).toBeCalledWith({ value: 3 }); | ||
v.setValue({ value: 3 }); | ||
expect(v.getValue()).toEqual({ value: 3 }); | ||
expect(onChange).toBeCalledTimes(1); | ||
}); | ||
}); | ||
describe('Filtered subscriptions', () => { | ||
it('should not trigger the callback if the filter returns false', () => { | ||
const v = new ObservableValue({ shouldNotify: true, value: 1 }); | ||
const onChange = vi.fn(); | ||
v.subscribe(onChange, { | ||
filter: (nextValue) => nextValue.shouldNotify, | ||
}); | ||
v.setValue({ shouldNotify: false, value: 1 }); | ||
expect(onChange).not.toBeCalled(); | ||
v.setValue({ shouldNotify: false, value: 2 }); | ||
expect(onChange).not.toBeCalled(); | ||
expect(v.getValue()).toEqual({ shouldNotify: false, value: 2 }); | ||
v.setValue({ shouldNotify: true, value: 3 }); | ||
expect(onChange).toBeCalledTimes(1); | ||
expect(onChange).toBeCalledWith({ shouldNotify: true, value: 3 }); | ||
}); | ||
}); | ||
}); | ||
//# sourceMappingURL=observable-value.spec.js.map |
import type { Disposable } from './disposable.js'; | ||
import type { ObservableValue, ValueChangeCallback } from './observable-value.js'; | ||
export type ValueObserverOptions<T> = { | ||
filter?: (nextValue: T, lastValue: T) => boolean; | ||
}; | ||
/** | ||
@@ -28,2 +31,3 @@ * Defines a generic ValueObserver instance | ||
callback: ValueChangeCallback<T>; | ||
readonly options?: ValueObserverOptions<T> | undefined; | ||
/** | ||
@@ -37,5 +41,6 @@ * Disposes the ValueObserver instance. Unsubscribes from the observable | ||
* @param callback The callback that will be fired on change | ||
* @param options Additional options | ||
*/ | ||
constructor(observable: ObservableValue<T>, callback: ValueChangeCallback<T>); | ||
constructor(observable: ObservableValue<T>, callback: ValueChangeCallback<T>, options?: ValueObserverOptions<T> | undefined); | ||
} | ||
//# sourceMappingURL=value-observer.d.ts.map |
@@ -26,2 +26,3 @@ /** | ||
callback; | ||
options; | ||
/** | ||
@@ -37,8 +38,10 @@ * Disposes the ValueObserver instance. Unsubscribes from the observable | ||
* @param callback The callback that will be fired on change | ||
* @param options Additional options | ||
*/ | ||
constructor(observable, callback) { | ||
constructor(observable, callback, options) { | ||
this.observable = observable; | ||
this.callback = callback; | ||
this.options = options; | ||
} | ||
} | ||
//# sourceMappingURL=value-observer.js.map |
{ | ||
"name": "@furystack/utils", | ||
"version": "5.0.0", | ||
"version": "6.0.0", | ||
"description": "General utilities", | ||
@@ -5,0 +5,0 @@ "type": "module", |
@@ -10,9 +10,4 @@ import { describe, it, expect, vi } from 'vitest' | ||
const v = new ObservableValue(undefined) | ||
const doneCallback = vi.fn() | ||
v.subscribe(() => { | ||
expect(v.getValue()).toBe(undefined) | ||
doneCallback() | ||
}, true) | ||
expect(v).toBeInstanceOf(ObservableValue) | ||
expect(doneCallback).toBeCalled() | ||
expect(v.getValue()).toBe(undefined) | ||
}) | ||
@@ -22,9 +17,3 @@ | ||
const v = new ObservableValue(1) | ||
const doneCallback = vi.fn() | ||
v.subscribe(() => { | ||
expect(v.getValue()).toBe(1) | ||
doneCallback() | ||
}, true) | ||
expect(doneCallback).toBeCalled() | ||
expect(v.getValue()).toBe(1) | ||
}) | ||
@@ -40,3 +29,3 @@ | ||
doneCallback() | ||
}, false) | ||
}) | ||
v.setValue(1) | ||
@@ -48,3 +37,3 @@ v.setValue(1) | ||
it('should be triggered only on change when getLast is false', () => { | ||
it('should be triggered only on change', () => { | ||
const v = new ObservableValue(1) | ||
@@ -56,3 +45,3 @@ const doneCallback = vi.fn() | ||
doneCallback() | ||
}, false) | ||
}) | ||
v.setValue(2) | ||
@@ -149,2 +138,49 @@ expect(doneCallback).toBeCalledTimes(1) | ||
}) | ||
describe('Custom Compare function', () => { | ||
it('Should compare the values with the custom compare function', () => { | ||
const v = new ObservableValue( | ||
{ value: 2 }, | ||
{ | ||
compare: (a, b) => a.value !== b.value, | ||
}, | ||
) | ||
const onChange = vi.fn() | ||
v.subscribe(onChange) | ||
v.setValue({ value: 2 }) | ||
expect(v.getValue()).toEqual({ value: 2 }) | ||
expect(onChange).not.toBeCalled() | ||
v.setValue({ value: 3 }) | ||
expect(v.getValue()).toEqual({ value: 3 }) | ||
expect(onChange).toBeCalledTimes(1) | ||
expect(onChange).toBeCalledWith({ value: 3 }) | ||
v.setValue({ value: 3 }) | ||
expect(v.getValue()).toEqual({ value: 3 }) | ||
expect(onChange).toBeCalledTimes(1) | ||
}) | ||
}) | ||
describe('Filtered subscriptions', () => { | ||
it('should not trigger the callback if the filter returns false', () => { | ||
const v = new ObservableValue({ shouldNotify: true, value: 1 }) | ||
const onChange = vi.fn() | ||
v.subscribe(onChange, { | ||
filter: (nextValue) => nextValue.shouldNotify, | ||
}) | ||
v.setValue({ shouldNotify: false, value: 1 }) | ||
expect(onChange).not.toBeCalled() | ||
v.setValue({ shouldNotify: false, value: 2 }) | ||
expect(onChange).not.toBeCalled() | ||
expect(v.getValue()).toEqual({ shouldNotify: false, value: 2 }) | ||
v.setValue({ shouldNotify: true, value: 3 }) | ||
expect(onChange).toBeCalledTimes(1) | ||
expect(onChange).toBeCalledWith({ shouldNotify: true, value: 3 }) | ||
}) | ||
}) | ||
}) |
import type { Disposable } from './disposable.js' | ||
import type { ValueObserverOptions } from './value-observer.js' | ||
import { ValueObserver } from './value-observer.js' | ||
@@ -18,2 +19,14 @@ | ||
export type ObservableValueOptions<T> = { | ||
/** | ||
* Defines a custom compare function to determine if the value should be updated and the observers should be notified | ||
* @param lastValue the last value | ||
* @param nextValue the next value | ||
* @returns if the value should be updated and the observers should be notified | ||
*/ | ||
compare: (lastValue: T, nextValue: T) => boolean | ||
} | ||
const defaultComparer = <T>(a: T, b: T) => a !== b | ||
/** | ||
@@ -52,2 +65,4 @@ * Defines an ObservableValue value object. | ||
this._isDisposed = true | ||
// @ts-expect-error getting currentValue after disposing is not allowed | ||
this.currentValue = null | ||
} | ||
@@ -60,14 +75,11 @@ private observers: Set<ValueObserver<T>> = new Set() | ||
* @param callback The callback method that will be called on each change | ||
* @param getLast Will call the callback with the last known value right after subscription | ||
* @param options Additional ObservableValue options | ||
* @returns The ValueObserver instance | ||
*/ | ||
public subscribe(callback: ValueChangeCallback<T>, getLast = false) { | ||
public subscribe(callback: ValueChangeCallback<T>, options?: ValueObserverOptions<T>) { | ||
if (this._isDisposed) { | ||
throw new ObservableAlreadyDisposedError() | ||
} | ||
const observer = new ValueObserver<T>(this, callback) | ||
const observer = new ValueObserver<T>(this, callback, options) | ||
this.observers.add(observer) | ||
if (getLast) { | ||
callback(this.currentValue) | ||
} | ||
return observer | ||
@@ -104,7 +116,9 @@ } | ||
} | ||
if (this.currentValue !== newValue) { | ||
if (this.options.compare(this.currentValue, newValue)) { | ||
this.currentValue = newValue | ||
for (const subscription of this.observers) { | ||
subscription.callback(newValue) | ||
} | ||
this.observers.forEach((observer) => { | ||
if (observer.options?.filter?.(this.currentValue, newValue) !== false) { | ||
observer.callback(newValue) | ||
} | ||
}) | ||
} | ||
@@ -121,8 +135,15 @@ } | ||
private readonly options: ObservableValueOptions<T> | ||
/** | ||
* @param initialValue Optional initial value | ||
* @param options Additional options | ||
*/ | ||
constructor(initialValue: T) { | ||
constructor(initialValue: T, options?: Partial<ObservableValueOptions<T>>) { | ||
this.options = { | ||
compare: defaultComparer, | ||
...options, | ||
} | ||
this.currentValue = initialValue | ||
} | ||
} |
import type { Disposable } from './disposable.js' | ||
import type { ObservableValue, ValueChangeCallback } from './observable-value.js' | ||
export type ValueObserverOptions<T> = { | ||
filter?: (nextValue: T, lastValue: T) => boolean | ||
} | ||
/** | ||
@@ -38,2 +42,3 @@ * Defines a generic ValueObserver instance | ||
* @param callback The callback that will be fired on change | ||
* @param options Additional options | ||
*/ | ||
@@ -43,3 +48,4 @@ constructor( | ||
public callback: ValueChangeCallback<T>, | ||
public readonly options?: ValueObserverOptions<T>, | ||
) {} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
182833
3039