diff --git a/src/runtime/store/index.ts b/src/runtime/store/index.ts index 8ac0d4203c..abae70c500 100644 --- a/src/runtime/store/index.ts +++ b/src/runtime/store/index.ts @@ -1,4 +1,14 @@ -import { run_all, subscribe, noop, safe_not_equal, is_function, get_store_value } from 'svelte/internal'; +import { subscribe, noop, safe_not_equal, get_store_value } from 'svelte/internal'; + +/** Sets the value of a store. */ +type Setter = (value: T) => void; + +/** Callback called on last removed subscriber */ +type StopCallback = () => void; + +/** Function called on first added subscriber + * If a callback is returned it will be called on last removed subscriber */ +type StartStopNotifier = (set: Setter) => StopCallback | void; /** Callback to inform of a value updates. */ type Subscriber = (value: T) => void; @@ -7,13 +17,10 @@ type Subscriber = (value: T) => void; type Unsubscriber = () => void; /** Callback to update a value. */ -type Updater = (value: T) => T; +type Updater = (value: T) => any; /** Cleanup logic callback. */ -type Invalidator = (value?: T) => void; - -/** Start and stop notification callbacks. */ -type StartStopNotifier = (set: Subscriber) => Unsubscriber | void; +type Invalidator = () => void; /** Readable interface for subscribing. */ export interface Readable { @@ -22,7 +29,7 @@ export interface Readable { * @param run subscription callback * @param invalidate cleanup callback */ - subscribe(run: Subscriber, invalidate?: Invalidator): Unsubscriber; + subscribe(run: Subscriber, invalidate?: Invalidator): Unsubscriber; } /** Writable interface for both updating and subscribing. */ @@ -41,7 +48,7 @@ export interface Writable extends Readable { } /** Pair of subscriber and invalidator. */ -type SubscribeInvalidateTuple = [Subscriber, Invalidator]; +type SubscribeInvalidateTuple = [Subscriber, Invalidator]; const subscriber_queue = []; @@ -63,7 +70,7 @@ export function readable(value: T, start: StartStopNotifier): Readable */ export function writable(value: T, start: StartStopNotifier = noop): Writable { let stop: Unsubscriber; - const subscribers: Array> = []; + const subscribers: SubscribeInvalidateTuple[] = []; function set(new_value: T): void { if (!safe_not_equal(value, new_value)) return; @@ -85,10 +92,7 @@ export function writable(value: T, start: StartStopNotifier = noop): Writa set(fn(value)); } - function subscribe( - run: Subscriber, - invalidate: Invalidator = noop - ): Unsubscriber { + function subscribe(run: Subscriber, invalidate: Invalidator = noop): Unsubscriber { const subscriber: SubscribeInvalidateTuple = [run, invalidate]; subscribers.push(subscriber); if (subscribers.length === 1) stop = start(set) || noop; @@ -103,25 +107,21 @@ export function writable(value: T, start: StartStopNotifier = noop): Writa return { set, update, subscribe }; } -/** One or more `Readable`s. */ -type Stores = Readable | [Readable, ...Array>]; +type Store = Writable | Readable; -/** One or more values from `Readable` stores. */ -type StoresValues = T extends Readable ? U : - { [K in keyof T]: T[K] extends Readable ? U : never }; +type ValuesOf = { [K in keyof T]: T[K] extends Store ? U : never }; +type ValueOf = T extends Store ? U : never; +type StoreValues = T extends Store ? ValueOf : ValuesOf; -/** - * Derived value store by synchronizing one or more readable stores and - * applying an aggregation function over its input values. - * - * @param stores - input stores - * @param fn - function callback that aggregates the values - */ -export function derived( - stores: S, - fn: (values: StoresValues) => T -): Readable; +type AutoDeriver = (values: StoreValues) => T; +type ManualDeriver = (values: StoreValues, set: Setter) => Unsubscriber | void; +type Deriver = AutoDeriver | ManualDeriver; +type DerivedValue = T extends Deriver ? U : never; +type DeriverController = { + update(values: StoreValues, set: Setter): void; + cleanup?(): void; +}; /** * Derived value store by synchronizing one or more readable stores and * applying an aggregation function over its input values. @@ -130,62 +130,70 @@ export function derived( * @param fn - function callback that aggregates the values * @param initial_value - when used asynchronously */ -export function derived( +export function derived | Store[], F extends Deriver, T = DerivedValue>( stores: S, - fn: (values: StoresValues, set: (value: T) => void) => Unsubscriber | void, + fn: F, initial_value?: T -): Readable; - -export function derived(stores: Stores, fn: Function, initial_value?: T): Readable { - const single = !Array.isArray(stores); - const stores_array: Array> = single - ? [stores as Readable] - : stores as Array>; - - const auto = fn.length < 2; - - return readable(initial_value, (set) => { - let inited = false; - const values = []; - - let pending = 0; - let cleanup = noop; - - const sync = () => { - if (pending) return; - cleanup(); - const result = fn(single ? values[0] : values, set); - if (auto) { - set(result as T); - } else { - cleanup = is_function(result) ? result as Unsubscriber : noop; - } - }; +) { + const mode = fn.length < 2 ? auto(fn as AutoDeriver) : manual(fn as ManualDeriver); + const deriver = Array.isArray(stores) ? multiple(stores as Store[], mode) : single(stores as Store, mode); + return readable(initial_value, deriver) as Readable; +} - const unsubscribers = stores_array.map((store, i) => +function single(store: S, controller: DeriverController): StartStopNotifier { + return set => { + const unsub = subscribe(store, (value: ValueOf) => controller.update(value, set)); + return function stop() { + unsub(), controller.cleanup(); + }; + }; +} +function multiple[], T>(stores: S, controller: DeriverController): StartStopNotifier { + return set => { + let inited = false, + pending = 0; + const values = new Array(stores.length) as StoreValues; + function sync() { + if (inited && !pending) { + controller.update(values, set); + } + } + const unsubs = stores.map((store, index) => subscribe( store, - (value) => { - values[i] = value; - pending &= ~(1 << i); - if (inited) sync(); + value => { + values[index] = value; + pending &= ~(1 << index); + sync(); }, () => { - pending |= 1 << i; + pending |= 1 << index; } ) ); - - inited = true; - sync(); - + (inited = true), sync(); return function stop() { - run_all(unsubscribers); - cleanup(); + unsubs.forEach(v => v()), controller.cleanup(); }; - }); + }; +} +function auto(fn): DeriverController { + return { + update(payload, set) { + set(fn(payload)); + }, + }; +} +function manual(fn): DeriverController { + return { + update(payload, set) { + this.cleanup(); + this.cleanup = fn(payload, set) as Unsubscriber; + if (typeof this.cleanup !== 'function') this.cleanup = noop; + }, + cleanup: noop, + }; } - /** * Get the current value from a store by subscribing and immediately unsubscribing. * @param store readable