@amadeus-it-group/tansu
Advanced tools
Comparing version 0.0.17 to 0.0.18
361
index.cjs.js
@@ -13,2 +13,3 @@ 'use strict'; | ||
const symbolObservable = (typeof Symbol === 'function' && Symbol.observable) || '@@observable'; | ||
const oldSubscription = Symbol(); | ||
const noop = () => { }; | ||
@@ -21,19 +22,10 @@ const noopUnsubscribe = () => { }; | ||
}; | ||
const toSubscriberObject = (subscriber) => typeof subscriber === 'function' | ||
? { | ||
next: subscriber.bind(null), | ||
pause: noop, | ||
resume: noop, | ||
_value: undefined, | ||
_valueIndex: 0, | ||
_paused: false, | ||
} | ||
: { | ||
next: bind(subscriber, 'next'), | ||
pause: bind(subscriber, 'pause'), | ||
resume: bind(subscriber, 'resume'), | ||
_value: undefined, | ||
_valueIndex: 0, | ||
_paused: false, | ||
}; | ||
const toSubscriberObject = (subscriber) => ({ | ||
next: typeof subscriber === 'function' ? subscriber.bind(null) : bind(subscriber, 'next'), | ||
pause: bind(subscriber, 'pause'), | ||
resume: bind(subscriber, 'resume'), | ||
_value: undefined, | ||
_valueIndex: 0, | ||
_paused: false, | ||
}); | ||
const returnThis = function () { | ||
@@ -62,15 +54,26 @@ return this; | ||
}; | ||
const getNormalizedSubscribe = (input) => { | ||
const store = 'subscribe' in input ? input : input[symbolObservable](); | ||
return normalizeSubscribe(store); | ||
}; | ||
const getValue = (subscribe) => { | ||
let value; | ||
subscribe((v) => (value = v))(); | ||
return value; | ||
}; | ||
/** | ||
* Returns a wrapper (for the given store) which only exposes the {@link Readable} interface. | ||
* This converts any {@link StoreInput} to a {@link Readable} and exposes the store as read-only. | ||
* Returns a wrapper (for the given store) which only exposes the {@link ReadableSignal} interface. | ||
* This converts any {@link StoreInput} to a {@link ReadableSignal} and exposes the store as read-only. | ||
* | ||
* @param store - store to wrap | ||
* @returns A wrapper which only exposes the {@link Readable} interface. | ||
* @param extraProp - extra properties to add on the returned object | ||
* @returns A wrapper which only exposes the {@link ReadableSignal} interface. | ||
*/ | ||
function asReadable(input) { | ||
const store = 'subscribe' in input ? input : input[symbolObservable](); | ||
return { | ||
subscribe: normalizeSubscribe(store), | ||
function asReadable(store, extraProp) { | ||
const subscribe = getNormalizedSubscribe(store); | ||
const res = Object.assign(() => get(res), extraProp, { | ||
subscribe, | ||
[symbolObservable]: returnThis, | ||
}; | ||
}); | ||
return res; | ||
} | ||
@@ -154,2 +157,4 @@ const triggerUpdate = Symbol(); | ||
}; | ||
const defaultReactiveContext = (store) => getValue(getNormalizedSubscribe(store)); | ||
let reactiveContext = defaultReactiveContext; | ||
/** | ||
@@ -167,7 +172,3 @@ * A utility function to get the current value from a given store. | ||
*/ | ||
function get(store) { | ||
let value; | ||
asReadable(store).subscribe((v) => (value = v))(); | ||
return value; | ||
} | ||
const get = (store) => reactiveContext(store); | ||
const createNotEqualCache = (valueIndex) => ({ | ||
@@ -211,8 +212,9 @@ [valueIndex]: false, | ||
class Store { | ||
#subscribers; | ||
#cleanupFn; | ||
#subscribersPaused; | ||
#valueIndex; | ||
#subscribers = new Set(); | ||
#cleanupFn = null; | ||
#subscribersPaused = false; | ||
#valueIndex = 1; | ||
#value; | ||
#notEqualCache; | ||
#notEqualCache = createNotEqualCache(1); | ||
#oldSubscriptions = new WeakMap(); | ||
/** | ||
@@ -223,7 +225,2 @@ * | ||
constructor(value) { | ||
this.#subscribers = new Set(); | ||
this.#cleanupFn = null; | ||
this.#subscribersPaused = false; | ||
this.#valueIndex = 1; | ||
this.#notEqualCache = createNotEqualCache(1); | ||
this.#value = value; | ||
@@ -258,5 +255,4 @@ } | ||
} | ||
#triggerUpdate() { | ||
this.#cleanupFn?.[triggerUpdate]?.(); | ||
} | ||
/** @internal */ | ||
[triggerUpdate]() { } | ||
#notifySubscriber(subscriber) { | ||
@@ -302,3 +298,3 @@ const notEqualCache = this.#notEqualCache; | ||
* | ||
* The paused state prevents derived stores (both direct and transitive) from recomputing their value | ||
* The paused state prevents derived or computed stores (both direct and transitive) from recomputing their value | ||
* using the current value of this store. | ||
@@ -400,2 +396,10 @@ * | ||
const subscriberObject = toSubscriberObject(subscriber); | ||
const oldSubscriptionParam = subscriber?.[oldSubscription]; | ||
if (oldSubscriptionParam) { | ||
const oldSubscriberObject = this.#oldSubscriptions.get(oldSubscriptionParam); | ||
if (oldSubscriberObject) { | ||
subscriberObject._value = oldSubscriberObject._value; | ||
subscriberObject._valueIndex = oldSubscriberObject._valueIndex; | ||
} | ||
} | ||
this.#subscribers.add(subscriberObject); | ||
@@ -407,3 +411,3 @@ batch(() => { | ||
else { | ||
this.#triggerUpdate(); | ||
this[triggerUpdate](); | ||
} | ||
@@ -417,8 +421,11 @@ }); | ||
subscriberObject.resume = noop; | ||
if (removed && this.#subscribers.size === 0) { | ||
this.#stop(); | ||
if (removed) { | ||
this.#oldSubscriptions.set(unsubscribe, subscriberObject); | ||
if (this.#subscribers.size === 0) { | ||
this.#stop(); | ||
} | ||
} | ||
}; | ||
unsubscribe[triggerUpdate] = () => { | ||
this.#triggerUpdate(); | ||
this[triggerUpdate](); | ||
this.#notifySubscriber(subscriberObject); | ||
@@ -440,10 +447,9 @@ }; | ||
const subscribe = (subscriber) => { | ||
toSubscriberObject(subscriber).next(value); | ||
if (!subscriber?.[oldSubscription]) { | ||
toSubscriberObject(subscriber).next(value); | ||
} | ||
return noopUnsubscribe; | ||
}; | ||
normalizedSubscribe.add(subscribe); | ||
return { | ||
subscribe, | ||
[symbolObservable]: returnThis, | ||
}; | ||
return Object.assign(() => value, { subscribe, [symbolObservable]: returnThis }); | ||
} | ||
@@ -529,7 +535,6 @@ class WritableStore extends Store { | ||
const store = applyStoreOptions(new WritableStore(value), options); | ||
return { | ||
...asReadable(store), | ||
return asReadable(store, { | ||
set: store.set.bind(store), | ||
update: store.update.bind(store), | ||
}; | ||
}); | ||
} | ||
@@ -542,10 +547,9 @@ function isSyncDeriveFn(fn) { | ||
#isArray; | ||
#stores; | ||
#pending; | ||
#storesSubscribeFn; | ||
#pending = 0; | ||
constructor(stores, initialValue) { | ||
super(initialValue); | ||
this.#pending = 0; | ||
const isArray = Array.isArray(stores); | ||
this.#isArray = isArray; | ||
this.#stores = (isArray ? [...stores] : [stores]).map(asReadable); | ||
this.#storesSubscribeFn = (isArray ? [...stores] : [stores]).map(getNormalizedSubscribe); | ||
} | ||
@@ -563,4 +567,4 @@ resumeSubscribers() { | ||
const isArray = this.#isArray; | ||
const storesArr = this.#stores; | ||
const dependantValues = new Array(storesArr.length); | ||
const storesSubscribeFn = this.#storesSubscribeFn; | ||
const dependantValues = new Array(storesSubscribeFn.length); | ||
let cleanupFn = null; | ||
@@ -587,4 +591,4 @@ const callCleanup = () => { | ||
}; | ||
const unsubscribers = storesArr.map((store, idx) => store.subscribe({ | ||
next: (v) => { | ||
const unsubscribers = storesSubscribeFn.map((subscribe, idx) => { | ||
const subscriber = (v) => { | ||
dependantValues[idx] = v; | ||
@@ -594,16 +598,14 @@ changed |= 1 << idx; | ||
callDerive(); | ||
}, | ||
pause: () => { | ||
}; | ||
subscriber.next = subscriber; | ||
subscriber.pause = () => { | ||
this.#pending |= 1 << idx; | ||
this.pauseSubscribers(); | ||
}, | ||
resume: () => { | ||
}; | ||
subscriber.resume = () => { | ||
this.#pending &= ~(1 << idx); | ||
callDerive(); | ||
}, | ||
})); | ||
const clean = () => { | ||
callCleanup(); | ||
unsubscribers.forEach(callFn); | ||
}; | ||
}; | ||
return subscribe(subscriber); | ||
}); | ||
const triggerSubscriberPendingUpdate = (unsubscriber, idx) => { | ||
@@ -614,3 +616,3 @@ if (this.#pending & (1 << idx)) { | ||
}; | ||
clean[triggerUpdate] = () => { | ||
this[triggerUpdate] = () => { | ||
let iterations = 0; | ||
@@ -629,6 +631,9 @@ while (this.#pending) { | ||
}; | ||
clean.unsubscribe = clean; | ||
callDerive(true); | ||
clean[triggerUpdate](); | ||
return clean; | ||
this[triggerUpdate](); | ||
return () => { | ||
this[triggerUpdate] = noop; | ||
callCleanup(); | ||
unsubscribers.forEach(callFn); | ||
}; | ||
} | ||
@@ -660,2 +665,200 @@ } | ||
} | ||
/** | ||
* Stops the tracking of dependencies made by {@link computed} and calls the provided function. | ||
* After the function returns, the tracking of dependencies continues as before. | ||
* | ||
* @param fn - function to be called | ||
* @returns the value returned by the given function | ||
*/ | ||
const untrack = (fn) => { | ||
const previousReactiveContext = reactiveContext; | ||
try { | ||
reactiveContext = defaultReactiveContext; | ||
return fn(); | ||
} | ||
finally { | ||
reactiveContext = previousReactiveContext; | ||
} | ||
}; | ||
const callUnsubscribe = ({ unsubscribe }) => unsubscribe(); | ||
const callResubscribe = ({ resubscribe }) => resubscribe(); | ||
class ComputedStore extends Store { | ||
#computing = false; | ||
#skipCallCompute = false; | ||
#versionIndex = 0; | ||
#subscriptions = new Map(); | ||
#reactiveContext = (storeInput) => untrack(() => this.#getSubscriptionValue(storeInput)); | ||
constructor() { | ||
super(undefined); | ||
} | ||
#createSubscription(subscribe) { | ||
const res = { | ||
versionIndex: this.#versionIndex, | ||
unsubscribe: noop, | ||
resubscribe: noop, | ||
pending: false, | ||
usedValueIndex: 0, | ||
value: undefined, | ||
valueIndex: 0, | ||
}; | ||
const subscriber = (value) => { | ||
res.value = value; | ||
res.valueIndex++; | ||
res.pending = false; | ||
if (!this.#skipCallCompute && !this.#isPending()) { | ||
this.#callCompute(); | ||
} | ||
}; | ||
subscriber.next = subscriber; | ||
subscriber.pause = () => { | ||
res.pending = true; | ||
this.pauseSubscribers(); | ||
}; | ||
subscriber.resume = () => { | ||
res.pending = false; | ||
if (!this.#skipCallCompute && !this.#isPending()) { | ||
this.#callCompute(); | ||
} | ||
}; | ||
res.resubscribe = () => { | ||
res.unsubscribe = subscribe(subscriber); | ||
subscriber[oldSubscription] = res.unsubscribe; | ||
}; | ||
res.resubscribe(); | ||
return res; | ||
} | ||
#getSubscriptionValue(storeInput) { | ||
let res = this.#subscriptions.get(storeInput); | ||
if (res) { | ||
res.versionIndex = this.#versionIndex; | ||
res.unsubscribe[triggerUpdate]?.(); | ||
} | ||
else { | ||
res = this.#createSubscription(getNormalizedSubscribe(storeInput)); | ||
this.#subscriptions.set(storeInput, res); | ||
} | ||
res.usedValueIndex = res.valueIndex; | ||
return res.value; | ||
} | ||
#callCompute(resubscribe = false) { | ||
this.#computing = true; | ||
this.#skipCallCompute = true; | ||
try { | ||
if (this.#versionIndex > 0) { | ||
if (resubscribe) { | ||
this.#subscriptions.forEach(callResubscribe); | ||
} | ||
if (!this.#hasChange()) { | ||
this.resumeSubscribers(); | ||
return; | ||
} | ||
} | ||
this.#versionIndex++; | ||
const versionIndex = this.#versionIndex; | ||
const previousReactiveContext = reactiveContext; | ||
let value; | ||
try { | ||
reactiveContext = this.#reactiveContext; | ||
value = this.compute(); | ||
} | ||
finally { | ||
reactiveContext = previousReactiveContext; | ||
} | ||
this.set(value); | ||
for (const [store, info] of this.#subscriptions) { | ||
if (info.versionIndex !== versionIndex) { | ||
this.#subscriptions.delete(store); | ||
info.unsubscribe(); | ||
} | ||
} | ||
} | ||
finally { | ||
this.#skipCallCompute = false; | ||
this.#computing = false; | ||
} | ||
} | ||
#isPending() { | ||
for (const [, { pending }] of this.#subscriptions) { | ||
if (pending) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
#hasChange() { | ||
for (const [, { valueIndex, usedValueIndex }] of this.#subscriptions) { | ||
if (valueIndex != usedValueIndex) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
resumeSubscribers() { | ||
if (!this.#isPending()) { | ||
super.resumeSubscribers(); | ||
} | ||
} | ||
/** @internal */ | ||
[triggerUpdate]() { | ||
if (this.#computing) { | ||
throw new Error('recursive computed'); | ||
} | ||
let iterations = 0; | ||
while (this.#isPending()) { | ||
checkIterations(++iterations); | ||
this.#skipCallCompute = true; | ||
try { | ||
for (const [, { pending, unsubscribe }] of this.#subscriptions) { | ||
if (pending) { | ||
unsubscribe[triggerUpdate]?.(); | ||
} | ||
} | ||
} | ||
finally { | ||
this.#skipCallCompute = false; | ||
} | ||
if (this.#isPending()) { | ||
// safety check: if it is still pending after calling triggerUpdate, | ||
// it will always be and this is an endless loop | ||
break; | ||
} | ||
this.#callCompute(); | ||
} | ||
} | ||
onUse() { | ||
this.#callCompute(true); | ||
this[triggerUpdate](); | ||
return () => this.#subscriptions.forEach(callUnsubscribe); | ||
} | ||
} | ||
/** | ||
* Creates a store whose value is computed by the provided function. | ||
* | ||
* @remarks | ||
* | ||
* The computation function is first called the first time the store is used. | ||
* It can use the value of other stores or observables and the computation function is called again if the value of those dependencies | ||
* changed, as long as the store is still used. | ||
* Dependencies are detected automatically as the computation function gets their value either by calling the stores | ||
* as a function (as it is possible with stores implementing {@link ReadableSignal}), or by calling the {@link get} function | ||
* (with a store or any observable). If some calls made by the function should not be tracked as dependencies, it is possible | ||
* to wrap them in a call to {@link untrack}. | ||
* Note that dependencies can change between calls of the computation function. Internally, tansu will subscribe to new dependencies | ||
* when they are used and unsubscribe from dependencies that are no longer used after the call of the computation function. | ||
* | ||
* @param fn - computation function that returns the value of the store | ||
* @param options - store options | ||
* @returns store containing the value returned by the computation function | ||
*/ | ||
function computed(fn, options = {}) { | ||
const Computed = class extends ComputedStore { | ||
compute() { | ||
return fn(); | ||
} | ||
}; | ||
return asReadable(applyStoreOptions(new Computed(), { | ||
...options, | ||
onUse: undefined /* setting onUse is not allowed from computed */, | ||
})); | ||
} | ||
@@ -666,2 +869,3 @@ exports.DerivedStore = DerivedStore; | ||
exports.batch = batch; | ||
exports.computed = computed; | ||
exports.derived = derived; | ||
@@ -671,2 +875,3 @@ exports.get = get; | ||
exports.symbolObservable = symbolObservable; | ||
exports.untrack = untrack; | ||
exports.writable = writable; |
@@ -16,6 +16,7 @@ /** | ||
export declare const symbolObservable: typeof Symbol.observable; | ||
declare const oldSubscription: unique symbol; | ||
/** | ||
* A callback invoked when a store value changes. It is called with the latest value of a given store. | ||
*/ | ||
export type SubscriberFunction<T> = (value: T) => void; | ||
export type SubscriberFunction<T> = ((value: T) => void) & Partial<Omit<SubscriberObject<T>, 'next'>>; | ||
/** | ||
@@ -47,2 +48,9 @@ * A partial {@link https://github.com/tc39/proposal-observable#api | observer} notified when a store value changes. A store will call the {@link SubscriberObject.next | next} method every time the store's state is changing. | ||
resume: () => void; | ||
/** | ||
* @internal | ||
* Value returned from a previous call to subscribe, and corresponding to a subscription to resume. | ||
* This subscription must no longer be active. The new subscriber will not be called synchronously if | ||
* the value did not change compared to the last value received in this old subscription. | ||
*/ | ||
[oldSubscription]?: Unsubscriber; | ||
} | ||
@@ -103,2 +111,11 @@ /** | ||
/** | ||
* This interface augments the base {@link Readable} interface by adding the ability to call the store as a function to get its value. | ||
*/ | ||
export interface ReadableSignal<T> extends Readable<T> { | ||
/** | ||
* Returns the value of the store. This is a shortcut for calling {@link get} with the store. | ||
*/ | ||
(): T; | ||
} | ||
/** | ||
* A function that can be used to update store's value. This function is called with the current value and should return new store value. | ||
@@ -133,9 +150,17 @@ */ | ||
/** | ||
* Returns a wrapper (for the given store) which only exposes the {@link Readable} interface. | ||
* This converts any {@link StoreInput} to a {@link Readable} and exposes the store as read-only. | ||
* Represents a store that implements both {@link ReadableSignal} and {@link Writable}. | ||
* This is the type of objects returned by {@link writable}. | ||
*/ | ||
export interface WritableSignal<T, U = T> extends ReadableSignal<T>, Writable<T, U> { | ||
} | ||
/** | ||
* Returns a wrapper (for the given store) which only exposes the {@link ReadableSignal} interface. | ||
* This converts any {@link StoreInput} to a {@link ReadableSignal} and exposes the store as read-only. | ||
* | ||
* @param store - store to wrap | ||
* @returns A wrapper which only exposes the {@link Readable} interface. | ||
* @param extraProp - extra properties to add on the returned object | ||
* @returns A wrapper which only exposes the {@link ReadableSignal} interface. | ||
*/ | ||
export declare function asReadable<T>(input: StoreInput<T>): Readable<T>; | ||
export declare function asReadable<T, U = object>(store: StoreInput<T>, extraProp?: U): ReadableSignal<T> & U; | ||
declare const triggerUpdate: unique symbol; | ||
declare const queueProcess: unique symbol; | ||
@@ -195,3 +220,3 @@ /** | ||
*/ | ||
export declare function get<T>(store: StoreInput<T>): T; | ||
export declare const get: <T>(store: StoreInput<T>) => T; | ||
/** | ||
@@ -236,2 +261,4 @@ * Base class that can be extended to easily create a custom {@link Readable} store. | ||
private [queueProcess]; | ||
/** @internal */ | ||
protected [triggerUpdate](): void; | ||
/** | ||
@@ -255,3 +282,3 @@ * Compares two values and returns true if they are different. | ||
* | ||
* The paused state prevents derived stores (both direct and transitive) from recomputing their value | ||
* The paused state prevents derived or computed stores (both direct and transitive) from recomputing their value | ||
* using the current value of this store. | ||
@@ -379,3 +406,3 @@ * | ||
*/ | ||
export declare function readable<T>(value: T, options?: StoreOptions<T> | OnUseFn<T>): Readable<T>; | ||
export declare function readable<T>(value: T, options?: StoreOptions<T> | OnUseFn<T>): ReadableSignal<T>; | ||
/** | ||
@@ -397,3 +424,3 @@ * A convenience function to create {@link Writable} store instances. | ||
*/ | ||
export declare function writable<T>(value: T, options?: StoreOptions<T> | OnUseFn<T>): Writable<T>; | ||
export declare function writable<T>(value: T, options?: StoreOptions<T> | OnUseFn<T>): WritableSignal<T>; | ||
/** | ||
@@ -455,2 +482,30 @@ * Either a single {@link StoreInput} or a read-only array of at least one {@link StoreInput}. | ||
export declare function derived<T, S extends StoresInput>(stores: S, options: SyncDeriveFn<T, S> | SyncDeriveOptions<T, S>, initialValue?: T): Readable<T>; | ||
/** | ||
* Stops the tracking of dependencies made by {@link computed} and calls the provided function. | ||
* After the function returns, the tracking of dependencies continues as before. | ||
* | ||
* @param fn - function to be called | ||
* @returns the value returned by the given function | ||
*/ | ||
export declare const untrack: <T>(fn: () => T) => T; | ||
/** | ||
* Creates a store whose value is computed by the provided function. | ||
* | ||
* @remarks | ||
* | ||
* The computation function is first called the first time the store is used. | ||
* It can use the value of other stores or observables and the computation function is called again if the value of those dependencies | ||
* changed, as long as the store is still used. | ||
* Dependencies are detected automatically as the computation function gets their value either by calling the stores | ||
* as a function (as it is possible with stores implementing {@link ReadableSignal}), or by calling the {@link get} function | ||
* (with a store or any observable). If some calls made by the function should not be tracked as dependencies, it is possible | ||
* to wrap them in a call to {@link untrack}. | ||
* Note that dependencies can change between calls of the computation function. Internally, tansu will subscribe to new dependencies | ||
* when they are used and unsubscribe from dependencies that are no longer used after the call of the computation function. | ||
* | ||
* @param fn - computation function that returns the value of the store | ||
* @param options - store options | ||
* @returns store containing the value returned by the computation function | ||
*/ | ||
export declare function computed<T>(fn: () => T, options?: Omit<StoreOptions<T>, 'onUse'>): ReadableSignal<T>; | ||
export {}; |
361
index.es.js
@@ -11,2 +11,3 @@ /** | ||
const symbolObservable = (typeof Symbol === 'function' && Symbol.observable) || '@@observable'; | ||
const oldSubscription = Symbol(); | ||
const noop = () => { }; | ||
@@ -19,19 +20,10 @@ const noopUnsubscribe = () => { }; | ||
}; | ||
const toSubscriberObject = (subscriber) => typeof subscriber === 'function' | ||
? { | ||
next: subscriber.bind(null), | ||
pause: noop, | ||
resume: noop, | ||
_value: undefined, | ||
_valueIndex: 0, | ||
_paused: false, | ||
} | ||
: { | ||
next: bind(subscriber, 'next'), | ||
pause: bind(subscriber, 'pause'), | ||
resume: bind(subscriber, 'resume'), | ||
_value: undefined, | ||
_valueIndex: 0, | ||
_paused: false, | ||
}; | ||
const toSubscriberObject = (subscriber) => ({ | ||
next: typeof subscriber === 'function' ? subscriber.bind(null) : bind(subscriber, 'next'), | ||
pause: bind(subscriber, 'pause'), | ||
resume: bind(subscriber, 'resume'), | ||
_value: undefined, | ||
_valueIndex: 0, | ||
_paused: false, | ||
}); | ||
const returnThis = function () { | ||
@@ -60,15 +52,26 @@ return this; | ||
}; | ||
const getNormalizedSubscribe = (input) => { | ||
const store = 'subscribe' in input ? input : input[symbolObservable](); | ||
return normalizeSubscribe(store); | ||
}; | ||
const getValue = (subscribe) => { | ||
let value; | ||
subscribe((v) => (value = v))(); | ||
return value; | ||
}; | ||
/** | ||
* Returns a wrapper (for the given store) which only exposes the {@link Readable} interface. | ||
* This converts any {@link StoreInput} to a {@link Readable} and exposes the store as read-only. | ||
* Returns a wrapper (for the given store) which only exposes the {@link ReadableSignal} interface. | ||
* This converts any {@link StoreInput} to a {@link ReadableSignal} and exposes the store as read-only. | ||
* | ||
* @param store - store to wrap | ||
* @returns A wrapper which only exposes the {@link Readable} interface. | ||
* @param extraProp - extra properties to add on the returned object | ||
* @returns A wrapper which only exposes the {@link ReadableSignal} interface. | ||
*/ | ||
function asReadable(input) { | ||
const store = 'subscribe' in input ? input : input[symbolObservable](); | ||
return { | ||
subscribe: normalizeSubscribe(store), | ||
function asReadable(store, extraProp) { | ||
const subscribe = getNormalizedSubscribe(store); | ||
const res = Object.assign(() => get(res), extraProp, { | ||
subscribe, | ||
[symbolObservable]: returnThis, | ||
}; | ||
}); | ||
return res; | ||
} | ||
@@ -152,2 +155,4 @@ const triggerUpdate = Symbol(); | ||
}; | ||
const defaultReactiveContext = (store) => getValue(getNormalizedSubscribe(store)); | ||
let reactiveContext = defaultReactiveContext; | ||
/** | ||
@@ -165,7 +170,3 @@ * A utility function to get the current value from a given store. | ||
*/ | ||
function get(store) { | ||
let value; | ||
asReadable(store).subscribe((v) => (value = v))(); | ||
return value; | ||
} | ||
const get = (store) => reactiveContext(store); | ||
const createNotEqualCache = (valueIndex) => ({ | ||
@@ -209,8 +210,9 @@ [valueIndex]: false, | ||
class Store { | ||
#subscribers; | ||
#cleanupFn; | ||
#subscribersPaused; | ||
#valueIndex; | ||
#subscribers = new Set(); | ||
#cleanupFn = null; | ||
#subscribersPaused = false; | ||
#valueIndex = 1; | ||
#value; | ||
#notEqualCache; | ||
#notEqualCache = createNotEqualCache(1); | ||
#oldSubscriptions = new WeakMap(); | ||
/** | ||
@@ -221,7 +223,2 @@ * | ||
constructor(value) { | ||
this.#subscribers = new Set(); | ||
this.#cleanupFn = null; | ||
this.#subscribersPaused = false; | ||
this.#valueIndex = 1; | ||
this.#notEqualCache = createNotEqualCache(1); | ||
this.#value = value; | ||
@@ -256,5 +253,4 @@ } | ||
} | ||
#triggerUpdate() { | ||
this.#cleanupFn?.[triggerUpdate]?.(); | ||
} | ||
/** @internal */ | ||
[triggerUpdate]() { } | ||
#notifySubscriber(subscriber) { | ||
@@ -300,3 +296,3 @@ const notEqualCache = this.#notEqualCache; | ||
* | ||
* The paused state prevents derived stores (both direct and transitive) from recomputing their value | ||
* The paused state prevents derived or computed stores (both direct and transitive) from recomputing their value | ||
* using the current value of this store. | ||
@@ -398,2 +394,10 @@ * | ||
const subscriberObject = toSubscriberObject(subscriber); | ||
const oldSubscriptionParam = subscriber?.[oldSubscription]; | ||
if (oldSubscriptionParam) { | ||
const oldSubscriberObject = this.#oldSubscriptions.get(oldSubscriptionParam); | ||
if (oldSubscriberObject) { | ||
subscriberObject._value = oldSubscriberObject._value; | ||
subscriberObject._valueIndex = oldSubscriberObject._valueIndex; | ||
} | ||
} | ||
this.#subscribers.add(subscriberObject); | ||
@@ -405,3 +409,3 @@ batch(() => { | ||
else { | ||
this.#triggerUpdate(); | ||
this[triggerUpdate](); | ||
} | ||
@@ -415,8 +419,11 @@ }); | ||
subscriberObject.resume = noop; | ||
if (removed && this.#subscribers.size === 0) { | ||
this.#stop(); | ||
if (removed) { | ||
this.#oldSubscriptions.set(unsubscribe, subscriberObject); | ||
if (this.#subscribers.size === 0) { | ||
this.#stop(); | ||
} | ||
} | ||
}; | ||
unsubscribe[triggerUpdate] = () => { | ||
this.#triggerUpdate(); | ||
this[triggerUpdate](); | ||
this.#notifySubscriber(subscriberObject); | ||
@@ -438,10 +445,9 @@ }; | ||
const subscribe = (subscriber) => { | ||
toSubscriberObject(subscriber).next(value); | ||
if (!subscriber?.[oldSubscription]) { | ||
toSubscriberObject(subscriber).next(value); | ||
} | ||
return noopUnsubscribe; | ||
}; | ||
normalizedSubscribe.add(subscribe); | ||
return { | ||
subscribe, | ||
[symbolObservable]: returnThis, | ||
}; | ||
return Object.assign(() => value, { subscribe, [symbolObservable]: returnThis }); | ||
} | ||
@@ -527,7 +533,6 @@ class WritableStore extends Store { | ||
const store = applyStoreOptions(new WritableStore(value), options); | ||
return { | ||
...asReadable(store), | ||
return asReadable(store, { | ||
set: store.set.bind(store), | ||
update: store.update.bind(store), | ||
}; | ||
}); | ||
} | ||
@@ -540,10 +545,9 @@ function isSyncDeriveFn(fn) { | ||
#isArray; | ||
#stores; | ||
#pending; | ||
#storesSubscribeFn; | ||
#pending = 0; | ||
constructor(stores, initialValue) { | ||
super(initialValue); | ||
this.#pending = 0; | ||
const isArray = Array.isArray(stores); | ||
this.#isArray = isArray; | ||
this.#stores = (isArray ? [...stores] : [stores]).map(asReadable); | ||
this.#storesSubscribeFn = (isArray ? [...stores] : [stores]).map(getNormalizedSubscribe); | ||
} | ||
@@ -561,4 +565,4 @@ resumeSubscribers() { | ||
const isArray = this.#isArray; | ||
const storesArr = this.#stores; | ||
const dependantValues = new Array(storesArr.length); | ||
const storesSubscribeFn = this.#storesSubscribeFn; | ||
const dependantValues = new Array(storesSubscribeFn.length); | ||
let cleanupFn = null; | ||
@@ -585,4 +589,4 @@ const callCleanup = () => { | ||
}; | ||
const unsubscribers = storesArr.map((store, idx) => store.subscribe({ | ||
next: (v) => { | ||
const unsubscribers = storesSubscribeFn.map((subscribe, idx) => { | ||
const subscriber = (v) => { | ||
dependantValues[idx] = v; | ||
@@ -592,16 +596,14 @@ changed |= 1 << idx; | ||
callDerive(); | ||
}, | ||
pause: () => { | ||
}; | ||
subscriber.next = subscriber; | ||
subscriber.pause = () => { | ||
this.#pending |= 1 << idx; | ||
this.pauseSubscribers(); | ||
}, | ||
resume: () => { | ||
}; | ||
subscriber.resume = () => { | ||
this.#pending &= ~(1 << idx); | ||
callDerive(); | ||
}, | ||
})); | ||
const clean = () => { | ||
callCleanup(); | ||
unsubscribers.forEach(callFn); | ||
}; | ||
}; | ||
return subscribe(subscriber); | ||
}); | ||
const triggerSubscriberPendingUpdate = (unsubscriber, idx) => { | ||
@@ -612,3 +614,3 @@ if (this.#pending & (1 << idx)) { | ||
}; | ||
clean[triggerUpdate] = () => { | ||
this[triggerUpdate] = () => { | ||
let iterations = 0; | ||
@@ -627,6 +629,9 @@ while (this.#pending) { | ||
}; | ||
clean.unsubscribe = clean; | ||
callDerive(true); | ||
clean[triggerUpdate](); | ||
return clean; | ||
this[triggerUpdate](); | ||
return () => { | ||
this[triggerUpdate] = noop; | ||
callCleanup(); | ||
unsubscribers.forEach(callFn); | ||
}; | ||
} | ||
@@ -658,3 +663,201 @@ } | ||
} | ||
/** | ||
* Stops the tracking of dependencies made by {@link computed} and calls the provided function. | ||
* After the function returns, the tracking of dependencies continues as before. | ||
* | ||
* @param fn - function to be called | ||
* @returns the value returned by the given function | ||
*/ | ||
const untrack = (fn) => { | ||
const previousReactiveContext = reactiveContext; | ||
try { | ||
reactiveContext = defaultReactiveContext; | ||
return fn(); | ||
} | ||
finally { | ||
reactiveContext = previousReactiveContext; | ||
} | ||
}; | ||
const callUnsubscribe = ({ unsubscribe }) => unsubscribe(); | ||
const callResubscribe = ({ resubscribe }) => resubscribe(); | ||
class ComputedStore extends Store { | ||
#computing = false; | ||
#skipCallCompute = false; | ||
#versionIndex = 0; | ||
#subscriptions = new Map(); | ||
#reactiveContext = (storeInput) => untrack(() => this.#getSubscriptionValue(storeInput)); | ||
constructor() { | ||
super(undefined); | ||
} | ||
#createSubscription(subscribe) { | ||
const res = { | ||
versionIndex: this.#versionIndex, | ||
unsubscribe: noop, | ||
resubscribe: noop, | ||
pending: false, | ||
usedValueIndex: 0, | ||
value: undefined, | ||
valueIndex: 0, | ||
}; | ||
const subscriber = (value) => { | ||
res.value = value; | ||
res.valueIndex++; | ||
res.pending = false; | ||
if (!this.#skipCallCompute && !this.#isPending()) { | ||
this.#callCompute(); | ||
} | ||
}; | ||
subscriber.next = subscriber; | ||
subscriber.pause = () => { | ||
res.pending = true; | ||
this.pauseSubscribers(); | ||
}; | ||
subscriber.resume = () => { | ||
res.pending = false; | ||
if (!this.#skipCallCompute && !this.#isPending()) { | ||
this.#callCompute(); | ||
} | ||
}; | ||
res.resubscribe = () => { | ||
res.unsubscribe = subscribe(subscriber); | ||
subscriber[oldSubscription] = res.unsubscribe; | ||
}; | ||
res.resubscribe(); | ||
return res; | ||
} | ||
#getSubscriptionValue(storeInput) { | ||
let res = this.#subscriptions.get(storeInput); | ||
if (res) { | ||
res.versionIndex = this.#versionIndex; | ||
res.unsubscribe[triggerUpdate]?.(); | ||
} | ||
else { | ||
res = this.#createSubscription(getNormalizedSubscribe(storeInput)); | ||
this.#subscriptions.set(storeInput, res); | ||
} | ||
res.usedValueIndex = res.valueIndex; | ||
return res.value; | ||
} | ||
#callCompute(resubscribe = false) { | ||
this.#computing = true; | ||
this.#skipCallCompute = true; | ||
try { | ||
if (this.#versionIndex > 0) { | ||
if (resubscribe) { | ||
this.#subscriptions.forEach(callResubscribe); | ||
} | ||
if (!this.#hasChange()) { | ||
this.resumeSubscribers(); | ||
return; | ||
} | ||
} | ||
this.#versionIndex++; | ||
const versionIndex = this.#versionIndex; | ||
const previousReactiveContext = reactiveContext; | ||
let value; | ||
try { | ||
reactiveContext = this.#reactiveContext; | ||
value = this.compute(); | ||
} | ||
finally { | ||
reactiveContext = previousReactiveContext; | ||
} | ||
this.set(value); | ||
for (const [store, info] of this.#subscriptions) { | ||
if (info.versionIndex !== versionIndex) { | ||
this.#subscriptions.delete(store); | ||
info.unsubscribe(); | ||
} | ||
} | ||
} | ||
finally { | ||
this.#skipCallCompute = false; | ||
this.#computing = false; | ||
} | ||
} | ||
#isPending() { | ||
for (const [, { pending }] of this.#subscriptions) { | ||
if (pending) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
#hasChange() { | ||
for (const [, { valueIndex, usedValueIndex }] of this.#subscriptions) { | ||
if (valueIndex != usedValueIndex) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
resumeSubscribers() { | ||
if (!this.#isPending()) { | ||
super.resumeSubscribers(); | ||
} | ||
} | ||
/** @internal */ | ||
[triggerUpdate]() { | ||
if (this.#computing) { | ||
throw new Error('recursive computed'); | ||
} | ||
let iterations = 0; | ||
while (this.#isPending()) { | ||
checkIterations(++iterations); | ||
this.#skipCallCompute = true; | ||
try { | ||
for (const [, { pending, unsubscribe }] of this.#subscriptions) { | ||
if (pending) { | ||
unsubscribe[triggerUpdate]?.(); | ||
} | ||
} | ||
} | ||
finally { | ||
this.#skipCallCompute = false; | ||
} | ||
if (this.#isPending()) { | ||
// safety check: if it is still pending after calling triggerUpdate, | ||
// it will always be and this is an endless loop | ||
break; | ||
} | ||
this.#callCompute(); | ||
} | ||
} | ||
onUse() { | ||
this.#callCompute(true); | ||
this[triggerUpdate](); | ||
return () => this.#subscriptions.forEach(callUnsubscribe); | ||
} | ||
} | ||
/** | ||
* Creates a store whose value is computed by the provided function. | ||
* | ||
* @remarks | ||
* | ||
* The computation function is first called the first time the store is used. | ||
* It can use the value of other stores or observables and the computation function is called again if the value of those dependencies | ||
* changed, as long as the store is still used. | ||
* Dependencies are detected automatically as the computation function gets their value either by calling the stores | ||
* as a function (as it is possible with stores implementing {@link ReadableSignal}), or by calling the {@link get} function | ||
* (with a store or any observable). If some calls made by the function should not be tracked as dependencies, it is possible | ||
* to wrap them in a call to {@link untrack}. | ||
* Note that dependencies can change between calls of the computation function. Internally, tansu will subscribe to new dependencies | ||
* when they are used and unsubscribe from dependencies that are no longer used after the call of the computation function. | ||
* | ||
* @param fn - computation function that returns the value of the store | ||
* @param options - store options | ||
* @returns store containing the value returned by the computation function | ||
*/ | ||
function computed(fn, options = {}) { | ||
const Computed = class extends ComputedStore { | ||
compute() { | ||
return fn(); | ||
} | ||
}; | ||
return asReadable(applyStoreOptions(new Computed(), { | ||
...options, | ||
onUse: undefined /* setting onUse is not allowed from computed */, | ||
})); | ||
} | ||
export { DerivedStore, Store, asReadable, batch, derived, get, readable, symbolObservable, writable }; | ||
export { DerivedStore, Store, asReadable, batch, computed, derived, get, readable, symbolObservable, untrack, writable }; |
{ | ||
"name": "@amadeus-it-group/tansu", | ||
"version": "0.0.17", | ||
"version": "0.0.18", | ||
"description": "tansu is a lightweight, push-based state management library. It borrows the ideas and APIs originally designed and implemented by Svelte stores", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
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
87048
2205