diff --git a/README.md b/README.md index 6a4f331..ff92de1 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ Once minified and compressed, this module is actually [0.5KB](https://cdn.jsdeli ```js // basic core features import { + Signal, // class for brand check + Computed, // extends Signal: brand check batch, // Preact-like API computed, // Preact-like API effect, // Preact-like API @@ -29,38 +31,15 @@ Exposes a Preact-like [createModel](https://github.com/preactjs/signals/blob/mai import { // extra: disposable, // equivalent of createModel(fn) - // ... same as core features ... - batch, - computed, - effect, - signal, - untracked, + // all other exports from core + ...core } from '@webreflection/signals/disposable'; ``` -### branded - -This variant offers an `isSignal` utility that returns `true` or `false` if the passed argument is either a `signal` or a `computed` reference. - -```js -// extra core features -import { - // extra: - disposable, // equivalent of createModel(fn) - isSignal, // true if `isSignal(ref)` is signal or computed - // ... same as core features ... - batch, - computed, - effect, - signal, - untracked, -} from '@webreflection/signals/branded'; -``` - ### In Depth - * simply stack-based, maybe not the best approach, but one that can guarantee reasonable performance with minimal code size. + * simply (swapped) stack-based, maybe not the best approach, but one that can guarantee reasonable performance with minimal code size. * only `signal` and `computed` subscribe while reading values, unless `sig_or_comp.peek()` is used. * any `effect` updates synchronously but then runs only in isolation. Every effect is disposed of if the outer effect is running, meaning stacked effects work out of the box and always™ do the right thing. * `disposable` uses the very same `effect` logic to dispose itself when not needed anymore. @@ -73,26 +52,10 @@ import { You know, nowadays it's hard to find libraries that are still 100% under control, minimalistic, not bloated, yet correct, and this one would like to be one of those 😇 -#### The Beauty - - * [signal](https://github.com/WebReflection/signals/blob/main/src/signal.js) is 26 LOC. - * [computed](https://github.com/WebReflection/signals/blob/main/src/computed.js) is 31 LOC. - * the shared [stack](https://github.com/WebReflection/signals/blob/main/src/stack.js) is 18 LOC. - * [effect](https://github.com/WebReflection/signals/blob/main/src/effect.js) is where business happens, 65 LOC. - * [disposable](https://github.com/WebReflection/signals/blob/main/src/disposable.js) is 10 LOC, based on the core library mentioned in the previous points. - * [branded](https://github.com/WebReflection/signals/blob/main/src/branded.js) is 25 LOC extra needed only for libraries building on top. - -I mean ... that's coding, isn't it ... today I really needed something that would remind me why I love what I do ❤️ - - #### Benchmark ![benchmark](https://raw.githubusercontent.com/WebReflection/usignal/main/test/benchmark.png) -There is a *huge* difference between *NodeJS* and *Bun* but that's likely because *JSC* handles *Set* or *Map* in a better way, meaning all *WebKit* based browsers and mobile devices will have similar *Preact* performance, while *Chromium* based browsers will have half Preact size, but 1.5X slowdown. - -However, in common scenarios with no more than 10 to 100 signals per *effect*, the performance are consistently better or really close to Preact. - ## Architecture Fine-tuned signals are a piece of art: @@ -179,8 +142,8 @@ No *effect*? No reactivity! This is the *signals* contract, but there is a *catc Great questions. Here are the details about why that's never a concern: - * *effect* never subscribes to changes, it just registers itself as an *observer* - * *effect* never runs if it knows outer *effects* are queued to resolve the latest change + * *effect* never add subscribers to itself, like signals or computeds do, it just registers itself as an *observer* (*subscriber*) + * *effect* never runs if it knows outer *effects* are queued to resolve the latest change or changes are happening while it's running * the previous point means if `signal.value` is registered both at the *inner* effect level and at the *outer* one, the *outer* one will dictate the execution because ... * only the top-most subscribed effects will eventually execute, and ... * any *effect* previously registered for its outer *effect* will be **disposed** and never react to anything again! @@ -190,7 +153,7 @@ I am not sure you are still following, but because *effect* is a bottom-up probl ### Batch -If you followed everything else I've explained around this architecture, `batch(callback)` simply represents a running *callback* with no tracking whatsoever, except the implementation keeps tracing what should run fresh and what doesn't exist anymore. The latest *effect* that got it right will trigger and eventually access, or *re-subscribe* to, anything in it, but just once, after the *batch* callback has finished. +If you followed everything else I've explained around this architecture, `batch(callback)` simply represents a running *callback* with no instant reactivity, it simply accumulates changes and trigger after all changes happend for whatever effect was involved. ### Untracked diff --git a/dist/branded.js b/dist/branded.js deleted file mode 100644 index 2667cfb..0000000 --- a/dist/branded.js +++ /dev/null @@ -1 +0,0 @@ -var f=!0,l=t=>{f=t},s,u=t=>{f&&s&&t.add(s)},a=(t,e)=>{let r=s;s=t;try{return e()}finally{s=r}};var i=new WeakMap,p,S=t=>{let e=p;e||(p=[]);try{return t()}finally{if(!e){[e,p]=[p,e];for(let[r,o]of e)i.has(r)&&o()}}},d=t=>{t.c?.(),t.s.length&&t.s.splice(0).forEach(x)},x=t=>{let e=i.get(t);e.d=!0,d(e),i.delete(t)},b=t=>{let e=()=>{o||c.d||(o=!0,s||(p?p.push([e,r]):r()))},r=()=>{for(;o;)if(o=!1,d(c),c.c=a(e,t),c.d)return},o=!0,n,c={s:[],d:!o,c:n};return s&&i.get(s).s.push(e),i.set(e,c),r(),()=>{c.d||x(e)}};var g=t=>{let e=new Set,r=!0,o,n=()=>{for(;r;)r=!1,o=a(c,t);return o},c=()=>{if(r)return;r=!0;let k=e;e=new Set;for(let w of k)w()};return{get value(){return u(e),n()},peek:n}};var h=t=>{let e=new Set;return{get value(){return u(e),t},set value(r){if(t=r,f){let o=e;e=new Set;for(let n of o)n()}},peek(){return t}}};var j=t=>{let e=f;l(!1);try{return t()}finally{l(e)}};var G=t=>function(...r){let o,n=b(()=>{o??=t.apply(this,r)??this});return o[Symbol.dispose]=n,o};var m=new WeakSet,L=t=>{let e=g(t);return m.add(e),e},N=t=>m.has(t),O=t=>{let e=h(t);return m.add(e),e};export{S as batch,L as computed,G as disposable,b as effect,N as isSignal,O as signal,j as untracked}; diff --git a/dist/disposable.js b/dist/disposable.js index 162fd5f..087667c 100644 --- a/dist/disposable.js +++ b/dist/disposable.js @@ -1 +1 @@ -var c=!0,a=t=>{c=t},s,p=t=>{c&&s&&t.add(s)},l=(t,e)=>{let r=s;s=t;try{return e()}finally{s=r}};var i=new WeakMap,u,y=t=>{let e=u;e||(u=[]);try{return t()}finally{if(!e){[e,u]=[u,e];for(let[r,o]of e)i.has(r)&&o()}}},b=t=>{t.c?.(),t.s.length&&t.s.splice(0).forEach(m)},m=t=>{let e=i.get(t);e.d=!0,b(e),i.delete(t)},d=t=>{let e=()=>{o||f.d||(o=!0,s||(u?u.push([e,r]):r()))},r=()=>{for(;o;)if(o=!1,b(f),f.c=l(e,t),f.d)return},o=!0,n,f={s:[],d:!o,c:n};return s&&i.get(s).s.push(e),i.set(e,f),r(),()=>{f.d||m(e)}};var S=t=>{let e=new Set,r=!0,o,n=()=>{for(;r;)r=!1,o=l(f,t);return o},f=()=>{if(r)return;r=!0;let x=e;e=new Set;for(let h of x)h()};return{get value(){return p(e),n()},peek:n}};var M=t=>{let e=new Set;return{get value(){return p(e),t},set value(r){if(t=r,c){let o=e;e=new Set;for(let n of o)n()}},peek(){return t}}};var j=t=>{let e=c;a(!1);try{return t()}finally{a(e)}};var G=t=>function(...r){let o,n=d(()=>{o??=t.apply(this,r)??this});return o[Symbol.dispose]=n,o};export{y as batch,S as computed,G as disposable,d as effect,M as signal,j as untracked}; +var r,f,u,b,i,o=!0,n=class{static{f=s=>s.#t,u=s=>s.#s,b=(s,e)=>{s.#s=e}}#s=new Set;#t;constructor(s){this.#t=s}get value(){return x(this.#s),this.#t}set value(s){if(this.#t=s,o){let e=this.#s;this.#s=new Set,y(e)}}peek(){return this.#t}},a=Symbol(),h=class extends n{#s=!0;#t;[a](){if(this.#s)return;this.#s=!0;let s=u(this);b(this,new Set),y(s)}get value(){return x(u(this)),this.peek()}peek(){for(;this.#s;)this.#s=!1,this.#t=w(this,f(this));return this.#t}},l=class{disposed=!1;invalid=!0;sub=[];cleanup;fn;constructor(s){this.fn=s}[a](){this.invalid||this.disposed||(this.invalid=!0,i||(r?r.push(this):this.peek()))}peek(){for(;this.invalid&&!this.disposed;)this.invalid=!1,d(this),this.cleanup=w(this,this.fn)}},m=t=>{let s=r;s||(r=[]);try{return t()}finally{if(!s){[s,r]=[r,s];for(let e of s)e.peek()}}},d=t=>{t.cleanup?.(),t.sub.length&&t.sub.splice(0).forEach(k)},S=t=>new h(t),k=t=>{t.disposed=!0,d(t)},v=t=>{let s=new l(t);return i&&i.sub.push(s),s.peek(),()=>{s.disposed||k(s)}},p=t=>{o=t},y=t=>{for(let s of t)s[a]()},x=t=>{o&&i&&t.add(i)},w=(t,s)=>{let e=i;i=t;try{return s()}finally{i=e}},T=t=>new n(t),V=t=>{let s=o;p(!1);try{return t()}finally{p(s)}};var z=t=>function(...e){let c,g=v(()=>{c??=t.apply(this,e)??this});return c[Symbol.dispose]=g,c};export{h as Computed,n as Signal,m as batch,S as computed,z as disposable,v as effect,T as signal,V as untracked}; diff --git a/dist/signals.js b/dist/signals.js index 10dba65..f468a7e 100644 --- a/dist/signals.js +++ b/dist/signals.js @@ -1 +1 @@ -var f=!0,a=t=>{f=t},s,l=t=>{f&&s&&t.add(s)},p=(t,e)=>{let r=s;s=t;try{return e()}finally{s=r}};var i=new WeakMap,u,k=t=>{let e=u;e||(u=[]);try{return t()}finally{if(!e){[e,u]=[u,e];for(let[r,o]of e)i.has(r)&&o()}}},b=t=>{t.c?.(),t.s.length&&t.s.splice(0).forEach(x)},x=t=>{let e=i.get(t);e.d=!0,b(e),i.delete(t)},w=t=>{let e=()=>{o||n.d||(o=!0,s||(u?u.push([e,r]):r()))},r=()=>{for(;o;)if(o=!1,b(n),n.c=p(e,t),n.d)return},o=!0,c,n={s:[],d:!o,c};return s&&i.get(s).s.push(e),i.set(e,n),r(),()=>{n.d||x(e)}};var S=t=>{let e=new Set,r=!0,o,c=()=>{for(;r;)r=!1,o=p(n,t);return o},n=()=>{if(r)return;r=!0;let d=e;e=new Set;for(let m of d)m()};return{get value(){return l(e),c()},peek:c}};var M=t=>{let e=new Set;return{get value(){return l(e),t},set value(r){if(t=r,f){let o=e;e=new Set;for(let c of o)c()}},peek(){return t}}};var j=t=>{let e=f;a(!1);try{return t()}finally{a(e)}};export{k as batch,S as computed,w as effect,M as signal,j as untracked}; +var r,f,o,p,e,c=!0,n=class{static{f=s=>s.#t,o=s=>s.#s,p=(s,i)=>{s.#s=i}}#s=new Set;#t;constructor(s){this.#t=s}get value(){return v(this.#s),this.#t}set value(s){if(this.#t=s,c){let i=this.#s;this.#s=new Set,k(i)}}peek(){return this.#t}},l=Symbol(),u=class extends n{#s=!0;#t;[l](){if(this.#s)return;this.#s=!0;let s=o(this);p(this,new Set),k(s)}get value(){return v(o(this)),this.peek()}peek(){for(;this.#s;)this.#s=!1,this.#t=w(this,f(this));return this.#t}},h=class{disposed=!1;invalid=!0;sub=[];cleanup;fn;constructor(s){this.fn=s}[l](){this.invalid||this.disposed||(this.invalid=!0,e||(r?r.push(this):this.peek()))}peek(){for(;this.invalid&&!this.disposed;)this.invalid=!1,b(this),this.cleanup=w(this,this.fn)}},y=t=>{let s=r;s||(r=[]);try{return t()}finally{if(!s){[s,r]=[r,s];for(let i of s)i.peek()}}},b=t=>{t.cleanup?.(),t.sub.length&&t.sub.splice(0).forEach(d)},g=t=>new u(t),d=t=>{t.disposed=!0,b(t)},x=t=>{let s=new h(t);return e&&e.sub.push(s),s.peek(),()=>{s.disposed||d(s)}},a=t=>{c=t},k=t=>{for(let s of t)s[l]()},v=t=>{c&&e&&t.add(e)},w=(t,s)=>{let i=e;e=t;try{return s()}finally{e=i}},S=t=>new n(t),m=t=>{let s=c;a(!1);try{return t()}finally{a(s)}};export{u as Computed,n as Signal,y as batch,g as computed,x as effect,S as signal,m as untracked}; diff --git a/package-lock.json b/package-lock.json index 1224de3..47d7e32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@webreflection/signals", - "version": "0.1.9", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@webreflection/signals", - "version": "0.1.9", + "version": "0.2.0", "license": "MIT", "devDependencies": { "@preact/signals-core": "^1.14.2", diff --git a/package.json b/package.json index a74dfc2..67358a7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@webreflection/signals", - "version": "0.1.9", + "version": "0.2.0", "description": "A minimalistic Preact-like signals implementation", "main": "src/index.js", "module": "src/index.js", @@ -13,19 +13,27 @@ ], "types": { ".": "./types/index.d.ts", - "./branded": "./types/branded.d.ts", "./disposable": "./types/disposable.d.ts", "./disp": "./types/disposable.d.ts", - "./dist": "./types/index.d.ts", - "./package.json": "./types/package.d.json" + "./dist": "./types/index.d.ts" }, "exports": { - ".": "./src/index.js", - "./brand": "./dist/branded.js", - "./branded": "./src/branded.js", - "./disposable": "./src/disposable.js", - "./disp": "./dist/disposable.js", - "./dist": "./dist/signals.js", + ".": { + "types": "./types/index.d.ts", + "import": "./src/index.js" + }, + "./disposable": { + "types": "./types/disposable.d.ts", + "import": "./src/disposable.js" + }, + "./disp": { + "types": "./types/disposable.d.ts", + "import": "./dist/disposable.js" + }, + "./dist": { + "types": "./types/index.d.ts", + "import": "./dist/signals.js" + }, "./package.json": "./package.json" }, "overrides": { @@ -34,13 +42,12 @@ } }, "scripts": { - "build": "npm run build:dist && npm run build:disp && npm run build:branded && npm run build:types && npm run test && npm run size", - "build:branded": "esbuild src/branded.js --bundle --minify --platform=neutral --format=esm --outfile=dist/branded.js", + "build": "npm run build:dist && npm run build:disp && npm run build:types && npm run test && npm run size", "build:dist": "esbuild src/index.js --bundle --minify --platform=neutral --format=esm --outfile=dist/signals.js", "build:disp": "esbuild src/disposable.js --bundle --minify --platform=neutral --format=esm --outfile=dist/disposable.js", - "build:types": "tsc --allowJs --declaration --emitDeclarationOnly --stripInternal --outDir types --target es2022 --lib es2024 --module nodenext --moduleResolution nodenext src/*.js", + "build:types": "tsc --allowJs --declaration --emitDeclarationOnly --stripInternal --removeComments --outDir types --target es2022 --lib es2024 --module nodenext --moduleResolution nodenext src/*.js", "coverage": "mkdir -p ./coverage; c8 report --reporter=text-lcov > ./coverage/lcov.info", - "size": "echo 'signals'; cat dist/signals.js | brotli | wc -c; echo '\ndisposable'; cat dist/disposable.js | brotli | wc -c; echo '\nbranded'; cat dist/branded.js | brotli | wc -c", + "size": "echo 'signals'; cat dist/signals.js | brotli | wc -c; echo '\ndisposable'; cat dist/disposable.js | brotli | wc -c", "test": "c8 node --expose-gc test/coverage.js" }, "keywords": [ diff --git a/src/branded.js b/src/branded.js deleted file mode 100644 index 0126ea6..0000000 --- a/src/branded.js +++ /dev/null @@ -1,25 +0,0 @@ -export * from './disposable.js'; -export * from './effect.js'; -export * from './untracked.js'; - -import { computed as $computed } from './computed.js'; -import { signal as $signal } from './signal.js'; - -const branded = new WeakSet; - -/** @type {(fn: () => T) => { readonly value: T, peek: () => T }} */ -export const computed = fn => { - const computed = $computed(fn); - branded.add(computed); - return computed; -}; - -/** @type {(value: unknown) => boolean} */ -export const isSignal = value => branded.has(value); - -/** @type {(init: T) => { value: T, peek: () => T }} */ -export const signal = init => { - const signal = $signal(init); - branded.add(signal); - return signal; -}; diff --git a/src/computed.js b/src/computed.js deleted file mode 100644 index e2f0399..0000000 --- a/src/computed.js +++ /dev/null @@ -1,31 +0,0 @@ -import { push, run } from './stack.js'; - -/** @type {(fn: () => T) => { readonly value: T, peek: () => T }} */ -export const computed = fn => { - let subscribers = new Set, invalid = true, value; - - const peek = () => { - while (invalid) { - invalid = false; - value = run(subscriber, fn); - } - return value; - }; - - const subscriber = () => { - if (invalid) return; - invalid = true; - const before = subscribers; - subscribers = new Set; - for (const sub of before) sub(); - }; - - return { - get value() { - push(subscribers); - return peek(); - }, - - peek, - }; -}; diff --git a/src/disposable.js b/src/disposable.js index 546ea35..e5e8bfd 100644 --- a/src/disposable.js +++ b/src/disposable.js @@ -1,5 +1,5 @@ export * from './index.js'; -import { effect } from './effect.js'; +import { effect } from './index.js'; export const disposable = fn => function disposable(...args) { let ref, value = effect(() => { diff --git a/src/effect.js b/src/effect.js deleted file mode 100644 index c6c041f..0000000 --- a/src/effect.js +++ /dev/null @@ -1,65 +0,0 @@ -import { run, stack } from './stack.js'; - -const effects = new WeakMap; - -let batches; - -/** @type {(fn: () => T) => T} */ -export const batch = fn => { - let before = batches; - if (!before) batches = []; - try { return fn() } - finally { - if (!before) { - [before, batches] = [batches, before]; - for (const [sub, loop] of before) { - if (effects.has(sub)) loop(); - } - } - } -}; - -const cleanUp = state => { - state.c?.(); - if (state.s.length) state.s.splice(0).forEach(dispose); -}; - -const dispose = subscriber => { - const state = effects.get(subscriber); - state.d = true; - cleanUp(state); - effects.delete(subscriber); -}; - -/** @type {(fn: (() => void | (() => void))) => (() => void)} */ -export const effect = fn => { - const subscriber = () => { - if (invalid || state.d) return; - invalid = true; - if (!stack) { - if (batches) batches.push([subscriber, loop]); - else loop(); - } - }; - - const loop = () => { - while (invalid) { - invalid = false; - cleanUp(state); - state.c = run(subscriber, fn); - if (state.d) return; - } - }; - - let invalid = true, c, state = { s: [], d: !invalid, c }; - - if (stack) effects.get(stack).s.push(subscriber); - - effects.set(subscriber, state); - - loop(); - - return () => { - if (!state.d) dispose(subscriber); - }; -}; diff --git a/src/index.js b/src/index.js index e1b8417..611d232 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,175 @@ -export * from './effect.js'; -export * from './computed.js'; -export * from './signal.js'; -export * from './untracked.js'; +let batches, getValue, getSubscribers, setSubscribers, stack, tracking = true; + +/** @template T */ +export class Signal { + static { + getValue = self => self.#value; + getSubscribers = self => self.#subscribers; + setSubscribers = (self, subscribers) => { + self.#subscribers = subscribers; + }; + } + + #subscribers = new Set; + #value; + + /** @param {T} init */ + constructor(init) { this.#value = init } + + get value() { + push(this.#subscribers); + return this.#value; + } + + set value(value) { + this.#value = value; + if (tracking) { + const subscribers = this.#subscribers; + this.#subscribers = new Set; + notify(subscribers); + } + } + + peek() { + return this.#value; + } +} + +/** @type {never} */ +const compute = Symbol(); + +/** + * @template T + * @extends {Signal<() => T>} + */ +export class Computed extends Signal { + #invalid = true; + #result; + + [compute]() { + if (this.#invalid) return; + this.#invalid = true; + const subscribers = getSubscribers(this); + setSubscribers(this, new Set); + notify(subscribers); + } + + /** @readonly @returns {T} */ + get value() { + push(getSubscribers(this)); + return this.peek(); + } + + /** @returns {T} */ + peek() { + while (this.#invalid) { + this.#invalid = false; + this.#result = run(this, getValue(this)); + } + return this.#result; + } +} + +class Effect { + disposed = false; + invalid = true; + sub = []; + cleanup; + fn; + + constructor(fn) { this.fn = fn } + + [compute]() { + if (this.invalid || this.disposed) return; + this.invalid = true; + if (!stack) { + if (batches) batches.push(this); + else this.peek(); + } + } + + peek() { + while (this.invalid && !this.disposed) { + this.invalid = false; + cleanup(this); + this.cleanup = run(this, this.fn); + } + } +} + +/** @type {(fn: () => T) => T} */ +export const batch = fn => { + let before = batches; + if (!before) batches = []; + try { return fn() } + finally { + if (!before) { + [before, batches] = [batches, before]; + for (const fx of before) fx.peek(); + } + } +}; + +const cleanup = fx => { + fx.cleanup?.(); + if (fx.sub.length) fx.sub.splice(0).forEach(dispose); +}; + +/** + * @template T + * @param {() => T} fn + * @returns {Computed} + */ +export const computed = fn => new Computed(fn); + +const dispose = fx => { + fx.disposed = true; + cleanup(fx); +}; + +/** + * @param {() => (void | (() => void))} fn + * @returns {() => void} + */ +export const effect = fn => { + const fx = new Effect(fn); + if (stack) stack.sub.push(fx); + fx.peek(); + return () => { + if (!fx.disposed) dispose(fx); + }; +}; + +const forceTracking = value => { + tracking = value; +}; + +const notify = subscribers => { + for (const subscriber of subscribers) subscriber[compute](); +}; + +const push = subscribers => { + if (tracking && stack) subscribers.add(stack); +}; + +const run = (state, callback) => { + const before = stack; + stack = state; + try { return callback() } + finally { stack = before } +}; + +/** + * @template T + * @param {T} init + * @returns + */ +export const signal = init => new Signal(init); + +/** @type {(fn: () => T) => T} */ +export const untracked = fn => { + const before = tracking; + forceTracking(false); + try { return fn() } + finally { forceTracking(before) } +}; diff --git a/src/signal.js b/src/signal.js deleted file mode 100644 index 2e89a66..0000000 --- a/src/signal.js +++ /dev/null @@ -1,26 +0,0 @@ -import { push, tracking } from './stack.js'; - -/** @type {(init: T) => { value: T, peek: () => T }} */ -export const signal = init => { - let subscribers = new Set; - - return { - get value() { - push(subscribers); - return init; - }, - - set value(value) { - init = value; - if (tracking) { - const before = subscribers; - subscribers = new Set; - for (const sub of before) sub(); - } - }, - - peek() { - return init; - }, - }; -}; diff --git a/src/stack.js b/src/stack.js deleted file mode 100644 index e772fbb..0000000 --- a/src/stack.js +++ /dev/null @@ -1,18 +0,0 @@ -export let tracking = true; - -export const forceTracking = value => { - tracking = value; -}; - -export let stack; - -export const push = subscribers => { - if (tracking && stack) subscribers.add(stack); -}; - -export const run = (subscriber, callback) => { - const before = stack; - stack = subscriber; - try { return callback() } - finally { stack = before } -}; diff --git a/src/untracked.js b/src/untracked.js deleted file mode 100644 index e7ad313..0000000 --- a/src/untracked.js +++ /dev/null @@ -1,9 +0,0 @@ -import { forceTracking, tracking } from './stack.js'; - -/** @type {(fn: () => T) => T} */ -export const untracked = fn => { - const before = tracking; - forceTracking(false); - try { return fn() } - finally { forceTracking(before) } -}; diff --git a/test/coverage.js b/test/coverage.js index c5dc1ef..8770a26 100644 --- a/test/coverage.js +++ b/test/coverage.js @@ -1,12 +1,12 @@ import { + Signal, Computed, batch, computed, disposable, effect, - isSignal, signal, untracked, -} from '../src/branded.js'; +} from '../src/disposable.js'; const assert = (a, b, message) => { if (a !== b) { @@ -22,8 +22,8 @@ const s3 = signal(3); const c1 = computed(() => (s1.value + s2.value)); const c2 = computed(() => (s3.value + c1.value)); -assert(isSignal(s1), true, 's1 is a signal'); -assert(isSignal(c1), true, 'c1 is a signal'); +assert((s1 instanceof Signal), true, 's1 is a signal'); +assert((c1 instanceof Signal), true, 'c1 is a signal'); assert(s1.value, 1, 's1.value === 1'); assert(c1.value, 3, 'c1.value === 3'); diff --git a/types/branded.d.ts b/types/branded.d.ts deleted file mode 100644 index 7f552ef..0000000 --- a/types/branded.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -export * from "./disposable.js"; -export * from "./effect.js"; -export * from "./untracked.js"; -/** @type {(fn: () => T) => { readonly value: T, peek: () => T }} */ -export const computed: (fn: () => T) => { - readonly value: T; - peek: () => T; -}; -/** @type {(value: unknown) => boolean} */ -export const isSignal: (value: unknown) => boolean; -/** @type {(init: T) => { value: T, peek: () => T }} */ -export const signal: (init: T) => { - value: T; - peek: () => T; -}; diff --git a/types/computed.d.ts b/types/computed.d.ts deleted file mode 100644 index 81c8bf3..0000000 --- a/types/computed.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** @type {(fn: () => T) => { readonly value: T, peek: () => T }} */ -export const computed: (fn: () => T) => { - readonly value: T; - peek: () => T; -}; diff --git a/types/effect.d.ts b/types/effect.d.ts deleted file mode 100644 index 6ff2752..0000000 --- a/types/effect.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** @type {(fn: () => T) => T} */ -export const batch: (fn: () => T) => T; -/** @type {(fn: (() => void | (() => void))) => (() => void)} */ -export const effect: (fn: (() => void | (() => void))) => (() => void); diff --git a/types/index.d.ts b/types/index.d.ts index 5ca23e7..609fa15 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,4 +1,19 @@ -export * from "./effect.js"; -export * from "./computed.js"; -export * from "./signal.js"; -export * from "./untracked.js"; +export class Signal { + constructor(init: T); + set value(value: T); + get value(): T; + peek(): T; + #private; +} +export class Computed extends Signal<() => T> { + [x: number]: () => void; + constructor(init: () => T); + readonly get value(): T; + peek(): T; + #private; +} +export const batch: (fn: () => T) => T; +export function computed(fn: () => T): Computed; +export function effect(fn: () => (void | (() => void))): () => void; +export function signal(init: T): Signal; +export const untracked: (fn: () => T) => T; diff --git a/types/signal.d.ts b/types/signal.d.ts deleted file mode 100644 index 9dfe9ae..0000000 --- a/types/signal.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** @type {(init: T) => { value: T, peek: () => T }} */ -export const signal: (init: T) => { - value: T; - peek: () => T; -}; diff --git a/types/stack.d.ts b/types/stack.d.ts deleted file mode 100644 index 5daccf0..0000000 --- a/types/stack.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -export let tracking: boolean; -export function forceTracking(value: any): void; -export let stack: any; -export function push(subscribers: any): void; -export function run(subscriber: any, callback: any): any; diff --git a/types/untracked.d.ts b/types/untracked.d.ts deleted file mode 100644 index fab9e38..0000000 --- a/types/untracked.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/** @type {(fn: () => T) => T} */ -export const untracked: (fn: () => T) => T;