universal-stores
Advanced tools
Comparing version 2.3.2 to 2.4.0
@@ -1,181 +0,3 @@ | ||
/** | ||
* @license | ||
* Copyright (c) 2016-22 [these people](https://github.com/sveltejs/svelte/graphs/contributors) | ||
* | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: | ||
* | ||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. | ||
* | ||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||
*/ | ||
import { Subscriber, Unsubscribe } from '@cdellacqua/signals'; | ||
export type { Subscriber, Unsubscribe } from '@cdellacqua/signals'; | ||
/** A generic setter function. Used in {@link Store} */ | ||
export declare type Setter<T> = (newValue: T) => void; | ||
/** A generic getter function. Used in {@link Store} */ | ||
export declare type Getter<T> = () => T; | ||
/** A generic updater function. Used in {@link Store} */ | ||
export declare type Updater<T> = (current: T) => T; | ||
/** A generic update function. Used in {@link Store} */ | ||
export declare type Update<T> = (updater: (current: T) => T) => void; | ||
/** A comparison function used to optimize subscribers notifications. Used in {@link Store} */ | ||
export declare type EqualityComparator<T> = (a: T, b: T) => boolean; | ||
/** A function that gets called once a store reaches 0 subscribers. Used in {@link Store} */ | ||
export declare type StopHandler = () => void; | ||
/** A function that gets called once a store gets at least one subscriber. Used in {@link Store} */ | ||
export declare type StartHandler<T> = (set: Setter<T>) => StopHandler | void; | ||
/** | ||
* A store that can have subscribers and emit values to them. It also | ||
* provides the current value upon subscription. It's readonly in the | ||
* sense that it doesn't provide direct set/update methods, unlike {@link Store}, | ||
* therefore its value can only be changed by a {@link StartHandler} (see also {@link makeReadonlyStore}). | ||
*/ | ||
export declare type ReadonlyStore<T> = { | ||
/** | ||
* Subscribe a function to this store. | ||
* | ||
* Note: subscribers are deduplicated, if you need to subscribe the same | ||
* function more than once wrap it in an arrow function, e.g. | ||
* `signal$.subscribe((v) => myFunc(v));` | ||
* @param subscriber a function that will be called upon subscription and whenever the store value changes. | ||
*/ | ||
subscribe(subscriber: Subscriber<T>): Unsubscribe; | ||
/** | ||
* Return the current number of active subscriptions. | ||
*/ | ||
nOfSubscriptions(): number; | ||
/** | ||
* Get the current value wrapped by the store. | ||
*/ | ||
content(): T; | ||
}; | ||
/** | ||
* A store that can have subscribers and emit values to them. It also | ||
* provides the current value upon subscription. | ||
*/ | ||
export declare type Store<T> = ReadonlyStore<T> & { | ||
/** | ||
* Set a value and send it to all subscribers. | ||
* @param v the new value of this store. | ||
*/ | ||
set(v: T): void; | ||
/** | ||
* Set the new value of the store through an updater function that takes the current one as an argument | ||
* and send the returned value to all subscribers. | ||
* @param updater the update function that will receive the current value and return the new one. | ||
*/ | ||
update(updater: Updater<T>): void; | ||
}; | ||
/** | ||
* Configurations for Store<T> and ReadonlyStore<T>. | ||
*/ | ||
export declare type StoreConfig<T> = { | ||
/** (optional) a {@link StartHandler} that will get called once there is at least one subscriber to this store. */ | ||
start?: StartHandler<T>; | ||
/** | ||
* (optional, defaults to `(a, b) => a === b`) a function that's used to determine if the current value of the store value is different from | ||
* the one being set and thus if the store needs to be updated and the subscribers notified. | ||
*/ | ||
comparator?: EqualityComparator<T>; | ||
}; | ||
/** | ||
* Make a store of type T. | ||
* | ||
* Example usage: | ||
* ```ts | ||
* const store$ = makeStore(0); | ||
* console.log(store$.content()); // 0 | ||
* store$.subscribe((v) => console.log(v)); | ||
* store$.set(10); // will trigger the above console log, printing 10 | ||
* ``` | ||
* @param initialValue the initial value of the store. | ||
* @param start a {@link StartHandler} that will get called once there is at least one subscriber to this store. | ||
* @returns a Store | ||
*/ | ||
export declare function makeStore<T>(initialValue: T | undefined, start?: StartHandler<T>): Store<T>; | ||
/** | ||
* Make a store of type T. | ||
* | ||
* Example usage: | ||
* ```ts | ||
* const store$ = makeStore(0); | ||
* console.log(store$.content()); // 0 | ||
* store$.subscribe((v) => console.log(v)); | ||
* store$.set(10); // will trigger the above console log, printing 10 | ||
* ``` | ||
* @param initialValue the initial value of the store. | ||
* @param config a {@link StoreConfig} which contains configuration information such as a value comparator to avoid needless notifications to subscribers and a {@link StartHandler}. | ||
* @returns a Store | ||
*/ | ||
export declare function makeStore<T>(initialValue: T | undefined, config?: StoreConfig<T>): Store<T>; | ||
/** | ||
* Make a store of type T. | ||
* | ||
* Example usage: | ||
* ```ts | ||
* const store$ = makeStore(0); | ||
* console.log(store$.content()); // 0 | ||
* store$.subscribe((v) => console.log(v)); | ||
* store$.set(10); // will trigger the above console log, printing 10 | ||
* ``` | ||
* @param initialValue the initial value of the store. | ||
* @param startOrConfig a {@link StartHandler} or a {@link StoreConfig} which contains configuration information such as a value comparator to avoid needless notifications to subscribers and a {@link StartHandler}. | ||
* @returns a Store | ||
*/ | ||
export declare function makeStore<T>(initialValue: T | undefined, startOrConfig?: StartHandler<T> | StoreConfig<T>): Store<T>; | ||
/** | ||
* Make a store of type T. | ||
* | ||
* Example usage: | ||
* ```ts | ||
* let value = 0; | ||
* const store$ = makeReadonlyStore(value, (set) => { | ||
* value++; | ||
* set(value); | ||
* }); | ||
* console.log(store$.content()); // 1 | ||
* store$.subscribe((v) => console.log(v)); // immediately prints 2 | ||
* console.log(store$.content()); // 2 | ||
* ``` | ||
* @param initialValue the initial value of the store. | ||
* @param start a {@link StartHandler} that will get called once there is at least one subscriber to this store. | ||
* @returns a ReadonlyStore | ||
*/ | ||
export declare function makeReadonlyStore<T>(initialValue: T | undefined, start?: StartHandler<T>): ReadonlyStore<T>; | ||
/** | ||
* Make a store of type T. | ||
* | ||
* Example usage: | ||
* ```ts | ||
* const store$ = makeReadonlyStore({prop: 'some value'}, { | ||
* comparator: (a, b) => a.prop === b.prop, | ||
* start: (set) => { | ||
* // ... | ||
* }, | ||
* }); | ||
* ``` | ||
* @param initialValue the initial value of the store. | ||
* @param config a {@link StoreConfig} which contains configuration information such as a value comparator to avoid needless notifications to subscribers and a {@link StartHandler}. | ||
* @returns a ReadonlyStore | ||
*/ | ||
export declare function makeReadonlyStore<T>(initialValue: T | undefined, config?: StoreConfig<T>): ReadonlyStore<T>; | ||
/** | ||
* Make a store of type T. | ||
* | ||
* Example usage: | ||
* ```ts | ||
* let value = 0; | ||
* const store$ = makeReadonlyStore(value, (set) => { | ||
* value++; | ||
* set(value); | ||
* }); | ||
* console.log(store$.content()); // 1 | ||
* store$.subscribe((v) => console.log(v)); // immediately prints 2 | ||
* console.log(store$.content()); // 2 | ||
* ``` | ||
* @param initialValue the initial value of the store. | ||
* @param startOrConfig a {@link StartHandler} or a {@link StoreConfig} which contains configuration information such as a value comparator to avoid needless notifications to subscribers and a {@link StartHandler}. | ||
* @returns a ReadonlyStore | ||
*/ | ||
export declare function makeReadonlyStore<T>(initialValue: T | undefined, startOrConfig?: StartHandler<T> | StoreConfig<T>): ReadonlyStore<T>; | ||
export * from './store'; | ||
export * from './composition'; | ||
export { MissingEffectError, ReactiveRootDisposeError, batchEffects, makeReactiveRoot, BatchingEffectError, type ReactiveRoot, } from './effect'; |
@@ -56,2 +56,144 @@ var __defProp = Object.defineProperty; | ||
} | ||
const effectRuntime = { | ||
instantiatingEffectId: void 0, | ||
effectCount: 0, | ||
isBatching: 0, | ||
pendingEffectBatch: new Map(), | ||
effectById: new Map(), | ||
reactiveRootCount: 0, | ||
rootByEffectId: new Map(), | ||
rootUnsubscribes: new Map() | ||
}; | ||
class MissingEffectError extends Error { | ||
} | ||
class ReactiveRootDisposeError extends Error { | ||
constructor(errors) { | ||
super("some of the registered cleanup functions threw an exception"); | ||
this.errors = errors; | ||
} | ||
} | ||
class BatchingEffectError extends Error { | ||
constructor(errors) { | ||
super("some of the batched effects threw an exception"); | ||
this.errors = errors; | ||
} | ||
} | ||
class NestedEffectError extends Error { | ||
constructor() { | ||
super("makeEffect called inside an effect"); | ||
} | ||
} | ||
function makeReactiveRoot() { | ||
effectRuntime.reactiveRootCount++; | ||
const subscriptionsHolderId = effectRuntime.reactiveRootCount; | ||
const ownedEffectIds = new Set(); | ||
function makeEffect(fn) { | ||
if (effectRuntime.instantiatingEffectId !== void 0) { | ||
throw new NestedEffectError(); | ||
} | ||
try { | ||
effectRuntime.effectCount++; | ||
const effectId = effectRuntime.effectCount; | ||
ownedEffectIds.add(effectId); | ||
const effect = { | ||
effectFn: fn | ||
}; | ||
effectRuntime.effectById.set(effectId, effect); | ||
effectRuntime.rootByEffectId.set(effectId, subscriptionsHolderId); | ||
effectRuntime.instantiatingEffectId = effectId; | ||
effect.cleanupFn = effect.effectFn(); | ||
} finally { | ||
effectRuntime.instantiatingEffectId = void 0; | ||
} | ||
} | ||
return { | ||
makeEffect, | ||
dispose() { | ||
var _a, _b, _c; | ||
(_a = effectRuntime.rootUnsubscribes.get(subscriptionsHolderId)) == null ? void 0 : _a.forEach((unsubscribe) => unsubscribe()); | ||
effectRuntime.rootUnsubscribes.delete(subscriptionsHolderId); | ||
const errors = []; | ||
for (const eId of ownedEffectIds) { | ||
try { | ||
(_c = (_b = effectRuntime.effectById.get(eId)) == null ? void 0 : _b.cleanupFn) == null ? void 0 : _c.call(_b); | ||
} catch (err) { | ||
errors.push(err); | ||
} | ||
effectRuntime.effectById.delete(eId); | ||
effectRuntime.rootByEffectId.delete(eId); | ||
} | ||
if (errors.length > 0) { | ||
throw new ReactiveRootDisposeError(errors); | ||
} | ||
} | ||
}; | ||
} | ||
function batchEffects(action) { | ||
effectRuntime.isBatching++; | ||
const errors = []; | ||
try { | ||
action(); | ||
} catch (err) { | ||
errors.push(err); | ||
} | ||
effectRuntime.isBatching--; | ||
if (effectRuntime.isBatching === 0) { | ||
const pendingEffects = Array.from(effectRuntime.pendingEffectBatch.values()); | ||
effectRuntime.pendingEffectBatch.clear(); | ||
for (const effectRunner of pendingEffects) { | ||
try { | ||
effectRunner(); | ||
} catch (err) { | ||
errors.push(err); | ||
} | ||
} | ||
} | ||
if (errors.length > 0) { | ||
throw new BatchingEffectError(errors); | ||
} | ||
} | ||
function radioActiveContent(store$) { | ||
if (effectRuntime.instantiatingEffectId !== void 0 && effectRuntime.instantiatingEffectId === effectRuntime.effectCount) { | ||
let v; | ||
const instanceId = effectRuntime.instantiatingEffectId; | ||
let firstRun = true; | ||
const rootId = effectRuntime.rootByEffectId.get(instanceId); | ||
let unsubscribes = effectRuntime.rootUnsubscribes.get(rootId); | ||
if (!unsubscribes) { | ||
unsubscribes = []; | ||
effectRuntime.rootUnsubscribes.set(rootId, unsubscribes); | ||
} | ||
const unsubscribe = store$.subscribe((current) => { | ||
v = current; | ||
if (firstRun) { | ||
firstRun = false; | ||
return; | ||
} | ||
const effect = effectRuntime.effectById.get(instanceId); | ||
if (!effect) { | ||
throw new MissingEffectError(`effect with id "${instanceId}" not registered`); | ||
} else { | ||
const effectRunner = () => { | ||
var _a; | ||
const prevRunning = effectRuntime.instantiatingEffectId; | ||
effectRuntime.instantiatingEffectId = void 0; | ||
try { | ||
(_a = effect.cleanupFn) == null ? void 0 : _a.call(effect); | ||
effect.cleanupFn = effect.effectFn(); | ||
} finally { | ||
effectRuntime.instantiatingEffectId = prevRunning; | ||
} | ||
}; | ||
if (effectRuntime.isBatching > 0) { | ||
effectRuntime.pendingEffectBatch.set(instanceId, effectRunner); | ||
} else { | ||
effectRunner(); | ||
} | ||
} | ||
}); | ||
unsubscribes.push(unsubscribe); | ||
return v; | ||
} | ||
return store$.content(); | ||
} | ||
/** | ||
@@ -67,2 +209,70 @@ * @license | ||
*/ | ||
function makeStore(initialValue, startOrConfig) { | ||
var _a; | ||
let mutableValue = initialValue; | ||
const signal = makeSignal(); | ||
let stopHandler; | ||
const startHandler = typeof startOrConfig === "function" ? startOrConfig : startOrConfig == null ? void 0 : startOrConfig.start; | ||
const comparator = (_a = typeof startOrConfig === "function" ? void 0 : startOrConfig == null ? void 0 : startOrConfig.comparator) != null ? _a : (a, b) => a === b; | ||
const content = () => { | ||
if (signal.nOfSubscriptions() > 0) { | ||
return mutableValue; | ||
} | ||
let v; | ||
const unsubscribe = subscribe((current) => v = current); | ||
unsubscribe(); | ||
return v; | ||
}; | ||
const set = (newValue) => { | ||
if (mutableValue !== void 0 && comparator(mutableValue, newValue)) { | ||
return; | ||
} | ||
mutableValue = newValue; | ||
signal.emit(mutableValue); | ||
}; | ||
const subscribe = (s) => { | ||
if (signal.nOfSubscriptions() === 0) { | ||
stopHandler = startHandler == null ? void 0 : startHandler(set); | ||
} | ||
const unsubscribe = signal.subscribe(s); | ||
s(mutableValue); | ||
return () => { | ||
unsubscribe(); | ||
if (signal.nOfSubscriptions() === 0) { | ||
stopHandler == null ? void 0 : stopHandler(); | ||
stopHandler = void 0; | ||
} | ||
}; | ||
}; | ||
const update = (updater) => { | ||
set(updater(content())); | ||
}; | ||
return { | ||
content, | ||
set, | ||
watch: () => radioActiveContent({ content, subscribe }), | ||
subscribe, | ||
update, | ||
nOfSubscriptions: signal.nOfSubscriptions | ||
}; | ||
} | ||
function makeReadonlyStore(initialValue, startOrConfig) { | ||
const { content, nOfSubscriptions, subscribe, watch } = makeStore(initialValue, startOrConfig); | ||
return { | ||
content, | ||
nOfSubscriptions, | ||
subscribe, | ||
watch | ||
}; | ||
} | ||
/** | ||
* @license | ||
* Copyright (c) 2016-22 [these people](https://github.com/sveltejs/svelte/graphs/contributors) | ||
* | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: | ||
* | ||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. | ||
* | ||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||
*/ | ||
function makeDerivedStore(readonlyStoreOrStores, map, config) { | ||
@@ -125,68 +335,2 @@ const isArray = Array.isArray(readonlyStoreOrStores); | ||
} | ||
/** | ||
* @license | ||
* Copyright (c) 2016-22 [these people](https://github.com/sveltejs/svelte/graphs/contributors) | ||
* | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: | ||
* | ||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. | ||
* | ||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||
*/ | ||
function makeStore(initialValue, startOrConfig) { | ||
var _a; | ||
let mutableValue = initialValue; | ||
const signal = makeSignal(); | ||
let stopHandler; | ||
const startHandler = typeof startOrConfig === "function" ? startOrConfig : startOrConfig == null ? void 0 : startOrConfig.start; | ||
const comparator = (_a = typeof startOrConfig === "function" ? void 0 : startOrConfig == null ? void 0 : startOrConfig.comparator) != null ? _a : (a, b) => a === b; | ||
const get = () => { | ||
if (signal.nOfSubscriptions() > 0) { | ||
return mutableValue; | ||
} | ||
let v; | ||
const unsubscribe = subscribe((current) => v = current); | ||
unsubscribe(); | ||
return v; | ||
}; | ||
const set = (newValue) => { | ||
if (mutableValue !== void 0 && comparator(mutableValue, newValue)) { | ||
return; | ||
} | ||
mutableValue = newValue; | ||
signal.emit(mutableValue); | ||
}; | ||
const subscribe = (s) => { | ||
if (signal.nOfSubscriptions() === 0) { | ||
stopHandler = startHandler == null ? void 0 : startHandler(set); | ||
} | ||
const unsubscribe = signal.subscribe(s); | ||
s(mutableValue); | ||
return () => { | ||
unsubscribe(); | ||
if (signal.nOfSubscriptions() === 0) { | ||
stopHandler == null ? void 0 : stopHandler(); | ||
stopHandler = void 0; | ||
} | ||
}; | ||
}; | ||
const update = (updater) => { | ||
set(updater(get())); | ||
}; | ||
return { | ||
content: get, | ||
set, | ||
subscribe, | ||
update, | ||
nOfSubscriptions: signal.nOfSubscriptions | ||
}; | ||
} | ||
function makeReadonlyStore(initialValue, startOrConfig) { | ||
const { content, nOfSubscriptions, subscribe } = makeStore(initialValue, startOrConfig); | ||
return { | ||
content, | ||
nOfSubscriptions, | ||
subscribe | ||
}; | ||
} | ||
export { makeDerivedStore, makeReadonlyStore, makeStore }; | ||
export { BatchingEffectError, MissingEffectError, ReactiveRootDisposeError, batchEffects, makeDerivedStore, makeReactiveRoot, makeReadonlyStore, makeStore }; |
@@ -1,2 +0,2 @@ | ||
var T=Object.defineProperty;var j=Object.getOwnPropertySymbols;var D=Object.prototype.hasOwnProperty,M=Object.prototype.propertyIsEnumerable;var x=(s,u,r)=>u in s?T(s,u,{enumerable:!0,configurable:!0,writable:!0,value:r}):s[u]=r,y=(s,u)=>{for(var r in u||(u={}))D.call(u,r)&&x(s,r,u[r]);if(j)for(var r of j(u))M.call(u,r)&&x(s,r,u[r]);return s};(function(s,u){typeof exports=="object"&&typeof module!="undefined"?u(exports):typeof define=="function"&&define.amd?define(["exports"],u):(s=typeof globalThis!="undefined"?globalThis:s||self,u(s.universalStores={}))})(this,function(s){"use strict";function u(){const n=[];function i(e){if(n.length!==0)for(const m of n.slice())m(e)}function b(e){const m=n.indexOf(e);m!==-1&&n.splice(m,1)}function t(e){return n.indexOf(e)===-1&&n.push(e),()=>b(e)}function f(e){const m=t(p=>{m(),e(p)});return m}return{emit:i,subscribe:t,subscribeOnce:f,nOfSubscriptions(){return n.length}}}/** | ||
var j=Object.defineProperty;var R=Object.getOwnPropertySymbols;var D=Object.prototype.hasOwnProperty,S=Object.prototype.propertyIsEnumerable;var w=(f,b,e)=>b in f?j(f,b,{enumerable:!0,configurable:!0,writable:!0,value:e}):f[b]=e,k=(f,b)=>{for(var e in b||(b={}))D.call(b,e)&&w(f,e,b[e]);if(R)for(var e of R(b))S.call(b,e)&&w(f,e,b[e]);return f};(function(f,b){typeof exports=="object"&&typeof module!="undefined"?b(exports):typeof define=="function"&&define.amd?define(["exports"],b):(f=typeof globalThis!="undefined"?globalThis:f||self,b(f.universalStores={}))})(this,function(f){"use strict";function b(){const t=[];function c(n){if(t.length!==0)for(const p of t.slice())p(n)}function i(n){const p=t.indexOf(n);p!==-1&&t.splice(p,1)}function s(n){return t.indexOf(n)===-1&&t.push(n),()=>i(n)}function o(n){const p=s(r=>{p(),n(r)});return p}return{emit:c,subscribe:s,subscribeOnce:o,nOfSubscriptions(){return t.length}}}const e={instantiatingEffectId:void 0,effectCount:0,isBatching:0,pendingEffectBatch:new Map,effectById:new Map,reactiveRootCount:0,rootByEffectId:new Map,rootUnsubscribes:new Map};class v extends Error{}class I extends Error{constructor(c){super("some of the registered cleanup functions threw an exception");this.errors=c}}class m extends Error{constructor(c){super("some of the batched effects threw an exception");this.errors=c}}class M extends Error{constructor(){super("makeEffect called inside an effect")}}function A(){e.reactiveRootCount++;const t=e.reactiveRootCount,c=new Set;function i(s){if(e.instantiatingEffectId!==void 0)throw new M;try{e.effectCount++;const o=e.effectCount;c.add(o);const n={effectFn:s};e.effectById.set(o,n),e.rootByEffectId.set(o,t),e.instantiatingEffectId=o,n.cleanupFn=n.effectFn()}finally{e.instantiatingEffectId=void 0}}return{makeEffect:i,dispose(){var o,n,p;(o=e.rootUnsubscribes.get(t))==null||o.forEach(r=>r()),e.rootUnsubscribes.delete(t);const s=[];for(const r of c){try{(p=(n=e.effectById.get(r))==null?void 0:n.cleanupFn)==null||p.call(n)}catch(u){s.push(u)}e.effectById.delete(r),e.rootByEffectId.delete(r)}if(s.length>0)throw new I(s)}}}function F(t){e.isBatching++;const c=[];try{t()}catch(i){c.push(i)}if(e.isBatching--,e.isBatching===0){const i=Array.from(e.pendingEffectBatch.values());e.pendingEffectBatch.clear();for(const s of i)try{s()}catch(o){c.push(o)}}if(c.length>0)throw new m(c)}function x(t){if(e.instantiatingEffectId!==void 0&&e.instantiatingEffectId===e.effectCount){let c;const i=e.instantiatingEffectId;let s=!0;const o=e.rootByEffectId.get(i);let n=e.rootUnsubscribes.get(o);n||(n=[],e.rootUnsubscribes.set(o,n));const p=t.subscribe(r=>{if(c=r,s){s=!1;return}const u=e.effectById.get(i);if(u){const a=()=>{var h;const g=e.instantiatingEffectId;e.instantiatingEffectId=void 0;try{(h=u.cleanupFn)==null||h.call(u),u.cleanupFn=u.effectFn()}finally{e.instantiatingEffectId=g}};e.isBatching>0?e.pendingEffectBatch.set(i,a):a()}else throw new v(`effect with id "${i}" not registered`)});return n.push(p),c}return t.content()}/** | ||
* @license | ||
@@ -10,3 +10,3 @@ * Copyright (c) 2016-22 [these people](https://github.com/sveltejs/svelte/graphs/contributors) | ||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||
*/function r(n,i,b){const t=Array.isArray(n),f=!t&&"subscribe"in n&&"nOfSubscriptions"in n&&"content"in n,e=f?1:t?n.length:Object.keys(n).length;return A(void 0,{comparator:b==null?void 0:b.comparator,start:e===0?p=>{p(i(t?[]:{}))}:f?p=>n.subscribe(o=>p(i(o))):t?p=>{let d=new Array(n.length),o=0;const a=n.map((h,c)=>h.subscribe(l=>{if(o<e&&(d[c]=l,o++),o===e){const v=[...d];v[c]=l,p(i(v)),d=v}}));return()=>{for(const h of a)h();o=0}}:p=>{let d={},o=0;const a=Object.entries(n).map(([h,c])=>c.subscribe(l=>{if(o<e&&(d[h]=l,o++),o===e){const v=y({},d);v[h]=l,p(i(v)),d=v}}));return()=>{for(const h of a)h();o=0}}})}/** | ||
*/function y(t,c){var h;let i=t;const s=b();let o;const n=typeof c=="function"?c:c==null?void 0:c.start,p=(h=typeof c=="function"||c==null?void 0:c.comparator)!=null?h:(d,l)=>d===l,r=()=>{if(s.nOfSubscriptions()>0)return i;let d;return a(E=>d=E)(),d},u=d=>{i!==void 0&&p(i,d)||(i=d,s.emit(i))},a=d=>{s.nOfSubscriptions()===0&&(o=n==null?void 0:n(u));const l=s.subscribe(d);return d(i),()=>{l(),s.nOfSubscriptions()===0&&(o==null||o(),o=void 0)}};return{content:r,set:u,watch:()=>x({content:r,subscribe:a}),subscribe:a,update:d=>{u(d(r()))},nOfSubscriptions:s.nOfSubscriptions}}function B(t,c){const{content:i,nOfSubscriptions:s,subscribe:o,watch:n}=y(t,c);return{content:i,nOfSubscriptions:s,subscribe:o,watch:n}}/** | ||
* @license | ||
@@ -20,2 +20,2 @@ * Copyright (c) 2016-22 [these people](https://github.com/sveltejs/svelte/graphs/contributors) | ||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||
*/function k(n,i){var h;let b=n;const t=u();let f;const e=typeof i=="function"?i:i==null?void 0:i.start,m=(h=typeof i=="function"||i==null?void 0:i.comparator)!=null?h:(c,l)=>c===l,p=()=>{if(t.nOfSubscriptions()>0)return b;let c;return o(v=>c=v)(),c},d=c=>{b!==void 0&&m(b,c)||(b=c,t.emit(b))},o=c=>{t.nOfSubscriptions()===0&&(f=e==null?void 0:e(d));const l=t.subscribe(c);return c(b),()=>{l(),t.nOfSubscriptions()===0&&(f==null||f(),f=void 0)}};return{content:p,set:d,subscribe:o,update:c=>{d(c(p()))},nOfSubscriptions:t.nOfSubscriptions}}function A(n,i){const{content:b,nOfSubscriptions:t,subscribe:f}=k(n,i);return{content:b,nOfSubscriptions:t,subscribe:f}}s.makeDerivedStore=r,s.makeReadonlyStore=A,s.makeStore=k,Object.defineProperty(s,"__esModule",{value:!0}),s[Symbol.toStringTag]="Module"}); | ||
*/function U(t,c,i){const s=Array.isArray(t),o=!s&&"subscribe"in t&&"nOfSubscriptions"in t&&"content"in t,n=o?1:s?t.length:Object.keys(t).length;return B(void 0,{comparator:i==null?void 0:i.comparator,start:n===0?r=>{r(c(s?[]:{}))}:o?r=>t.subscribe(a=>r(c(a))):s?r=>{let u=new Array(t.length),a=0;const g=t.map((h,d)=>h.subscribe(l=>{if(a<n&&(u[d]=l,a++),a===n){const E=[...u];E[d]=l,r(c(E)),u=E}}));return()=>{for(const h of g)h();a=0}}:r=>{let u={},a=0;const g=Object.entries(t).map(([h,d])=>d.subscribe(l=>{if(a<n&&(u[h]=l,a++),a===n){const E=k({},u);E[h]=l,r(c(E)),u=E}}));return()=>{for(const h of g)h();a=0}}})}f.BatchingEffectError=m,f.MissingEffectError=v,f.ReactiveRootDisposeError=I,f.batchEffects=F,f.makeDerivedStore=U,f.makeReactiveRoot=A,f.makeReadonlyStore=B,f.makeStore=y,Object.defineProperty(f,"__esModule",{value:!0}),f[Symbol.toStringTag]="Module"}); |
@@ -5,3 +5,3 @@ { | ||
"description": "State management made simple", | ||
"version": "2.3.2", | ||
"version": "2.4.0", | ||
"type": "module", | ||
@@ -8,0 +8,0 @@ "types": "dist/index.d.ts", |
144
README.md
@@ -5,6 +5,9 @@ # universal-stores | ||
**✨ with an integrated effect system ✨** | ||
Stores are a simple yet powerful way to manage an application | ||
state. Some examples of stores can be found in Svelte (e.g. writable, readable) and Solid.js (e.g. createSignal). | ||
This package provides a framework-agnostic implementation of this concept. | ||
This package provides a framework-agnostic implementation of this concept and a supporting effect system that can be used in conjunction with (or as an alternative to) | ||
explicit subscriptions. | ||
@@ -30,5 +33,5 @@ [NPM Package](https://www.npmjs.com/package/universal-stores) | ||
- `update(updater)`, to update the value using a function that takes the current one as an argument. | ||
- `content()`, to retrieve the current content of a store. | ||
- `watch()`, to retrieve the current content of a store and register the store as a dependency of a running effect (more on this later). | ||
There is also a getter `value` that retrieves the current value of the store: | ||
```ts | ||
@@ -96,4 +99,4 @@ import {makeStore} from 'universal-stores'; | ||
If you ever needed to add the same function | ||
more than once you can still achieve that by simply wrapping it inside an arrow function: | ||
If you ever need to add the same function | ||
more than once you can still achieve this by simply wrapping it inside an arrow function: | ||
@@ -287,2 +290,131 @@ ```ts | ||
## Effect system | ||
An "effect" is function that usually causes "side effects" (e.g. writing to the console, changing a DOM node, making an HTTP request, etc.) and that is tied to one or more stores | ||
in a semi-automatic manner. | ||
This package provides the following API for the effect system: | ||
- `makeReactiveRoot`, which instantiate an object containing `makeEffect` and `dispose`; | ||
- `makeEffect`, the primitive that registers effects; | ||
- `dispose`, a destructor for the reactive root that unregisters all effects, calling their cleanup functions (if present); | ||
- `batchEffects`, which enables "glitch-free" updates by enqueueing and deduplicating effects during multiple store updates; | ||
- `store$.watch()`, a method present in all readable stores similar to `.content()` that adds the store to the dependency list of an effect during a `makeEffect` call. | ||
In practice, effects look like this: | ||
```ts | ||
import {makeReactiveRoot, makeStore} from 'universal-stores'; | ||
const {makeEffect, dispose} = makeReactiveRoot(); | ||
const store$ = makeStore(1); | ||
makeEffect(() => { | ||
console.log(store$.watch()); // immediately prints 1 | ||
}); | ||
store$.set(2); // makes the effect above print 2 | ||
dispose(); | ||
store$.set(3); // does nothing, as the effect above has been unregistered | ||
``` | ||
The `dispose` is the equivalent, in observable terms, to the `unsubscribe` function. The difference is that it's a bulk operation, acting on all effects registered under the same root. | ||
The cleanup function of an effect is simply the function optionally returned inside | ||
a `makeEffect` call: | ||
```ts | ||
import {makeReactiveRoot, makeStore} from 'universal-stores'; | ||
const {makeEffect, dispose} = makeReactiveRoot(); | ||
const store$ = makeStore(1); | ||
makeEffect(() => { | ||
console.log(store$.watch()); // immediately prints 1 | ||
return () => console.clear(); | ||
}); | ||
store$.set(2); // makes the effect above clean the console, then print 2 | ||
dispose(); // cleans the console | ||
store$.set(3); // does nothing, as the effect above has been unregistered | ||
``` | ||
As shown in the example above, the cleanup function gets invoked when `dispose` is | ||
called or whenever the effect needs to re-run because at least one of its dependencies | ||
has changed. | ||
### Batching | ||
Consider the following code: | ||
```ts | ||
import {makeReactiveRoot, makeStore} from 'universal-stores'; | ||
const {makeEffect} = makeReactiveRoot(); | ||
const greeting$ = makeStore('Hello'); | ||
const name$ = makeStore('John'); | ||
makeEffect(() => { | ||
console.log(`${greeting$.watch()}, ${name$.watch()}`); // immediately prints "Hello, John" | ||
}); | ||
greeting$.set('Bye'); // prints "Bye, John" | ||
name$.set('Jack'); // prints "Bye, Jack" | ||
``` | ||
Sometimes it may be desirable to avoid triggering effects multiple times while updating different stores. The `batchEffects` function exists exactly for this use case. With a slight change to the code above we can update `greeting$` and `name$` "instantaneously": | ||
```ts | ||
import {makeReactiveRoot, makeStore, batchEffects} from 'universal-stores'; | ||
const {makeEffect} = makeReactiveRoot(); | ||
const greeting$ = makeStore('Hello'); | ||
const name$ = makeStore('John'); | ||
makeEffect(() => { | ||
console.log(`${greeting$.watch()}, ${name$.watch()}`); // immediately prints "Hello, John" | ||
}); | ||
batchEffects(() => { | ||
greeting$.set('Bye'); // doesn't trigger | ||
name$.set('Jack'); // doesn't trigger | ||
}); // triggers, printing "Bye, Jack" | ||
``` | ||
### Derive-like behavior | ||
The `.watch()` method doesn't need to be "physically" inside the `makeEffect` callback, the only condition needed for the effect to correctly register its dependencies is for `.watch()` to | ||
be called synchronously during the first execution of the effect. | ||
For example, this code: | ||
```ts | ||
import {makeReactiveRoot, makeStore} from 'universal-stores'; | ||
const {makeEffect} = makeReactiveRoot(); | ||
const greeting$ = makeStore('Hello'); | ||
const name$ = makeStore('John'); | ||
makeEffect(() => { | ||
console.log(`${greeting$.watch()}, ${name$.watch()}`); // immediately prints "Hello, John" | ||
}); | ||
greeting$.set('Bye'); // prints "Bye, John" | ||
``` | ||
is equivalent to this: | ||
```ts | ||
import {makeReactiveRoot, makeStore} from 'universal-stores'; | ||
const {makeEffect} = makeReactiveRoot(); | ||
const greeting$ = makeStore('Hello'); | ||
const name$ = makeStore('John'); | ||
const greet = () => `${greeting$.watch()}, ${name$.watch()}`; | ||
makeEffect(() => { | ||
console.log(greet()); // immediately prints "Hello, John" | ||
}); | ||
greeting$.set('Bye'); // prints "Bye, John" | ||
``` | ||
## Motivation | ||
@@ -293,3 +425,3 @@ | ||
State management, however, should **not** be coupled to | ||
State management, however, should **not** be coupled with | ||
the UI framework or library you're currently working with. Moreover, state management | ||
@@ -296,0 +428,0 @@ is also useful in non-UI applications (e.g. backend, background processes, etc.). |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
61999
10
807
441
1