From fd1503662579f4e94b5bf48950dd95bd2608c0c4 Mon Sep 17 00:00:00 2001 From: Jonas Caetano Date: Sun, 31 Jan 2021 03:54:13 -0300 Subject: [PATCH 1/3] Update on functions typings (#324) --- packages/solid/src/reactive/signal.ts | 36 ++++++--------------------- packages/solid/src/static/reactive.ts | 34 +++++-------------------- 2 files changed, 13 insertions(+), 57 deletions(-) diff --git a/packages/solid/src/reactive/signal.ts b/packages/solid/src/reactive/signal.ts index c61bacec7..57e61feed 100644 --- a/packages/solid/src/reactive/signal.ts +++ b/packages/solid/src/reactive/signal.ts @@ -269,34 +269,12 @@ export function untrack(fn: () => T): T { return result; } -export function on(w: () => T, fn: (v: T, prev: T, prevResult: U) => U): (prev?: U) => U; -export function on( - w1: () => T1, - w2: () => T2, - fn: (v: [T1, T2], prev: [T1, T2], prevResult: U) => U -): (prev?: U) => U; -export function on( - w1: () => T1, - w2: () => T2, - w3: () => T3, - fn: (v: [T1, T2, T3], p: [T1, T2, T3], prevResult: U) => U -): (prev?: U) => U; -export function on( - w1: () => T1, - w2: () => T2, - w3: () => T3, - w4: () => T4, - fn: (v: [T1, T2, T3, T4], p: [T1, T2, T3, T4], prevResult: U) => U -): (prev?: U) => U; -export function on( - w1: () => T1, - w2: () => T2, - w3: () => T3, - w4: () => T4, - w5: () => T5, - fn: (v: [T1, T2, T3, T4, T5], p: [T1, T2, T3, T4, T5], prevResults: U) => U -): (prev?: U) => U; -export function on(...args: Array): (prev?: U) => U { +type ReturnTypeArray = { [P in keyof T]: T[P] extends (() => infer U) ? U : never }; +export function on T>, U>( + ...args: X['length'] extends 1 + ? [w: () => T, fn: (v: T, prev: T | undefined, prevResults?: U) => U] + : [...w: X, fn: (v: ReturnTypeArray, prev: ReturnTypeArray | [], prevResults?: U) => U] +): (prev?: U) => U { const fn = args.pop() as (v: T | Array, p?: T | Array, r?: U) => U; let deps: (() => T) | Array<() => T>; let isArray = true; @@ -314,7 +292,7 @@ export function on(...args: Array): (prev?: U) => U { } else value = (deps as () => T)(); const result = untrack(() => fn!(value, prev, prevResult)); prev = value; - return result as U; + return result; }; } diff --git a/packages/solid/src/static/reactive.ts b/packages/solid/src/static/reactive.ts index 2d7c45115..76808a390 100644 --- a/packages/solid/src/static/reactive.ts +++ b/packages/solid/src/static/reactive.ts @@ -78,34 +78,12 @@ export function untrack(fn: () => T): T { return fn(); } -export function on(w: () => T, fn: (v: T, prev: T, prevResult: U) => U): (prev?: U) => U; -export function on( - w1: () => T1, - w2: () => T2, - fn: (v: [T1, T2], prev: [T1, T2], prevResult: U) => U -): (prev?: U) => U; -export function on( - w1: () => T1, - w2: () => T2, - w3: () => T3, - fn: (v: [T1, T2, T3], p: [T1, T2, T3], prevResult: U) => U -): (prev?: U) => U; -export function on( - w1: () => T1, - w2: () => T2, - w3: () => T3, - w4: () => T4, - fn: (v: [T1, T2, T3, T4], p: [T1, T2, T3, T4], prevResult: U) => U -): (prev?: U) => U; -export function on( - w1: () => T1, - w2: () => T2, - w3: () => T3, - w4: () => T4, - w5: () => T5, - fn: (v: [T1, T2, T3, T4, T5], p: [T1, T2, T3, T4, T5], prevResults: U) => U -): (prev?: U) => U; -export function on(...args: Array): (prev?: U) => U { +type ReturnTypeArray = { [P in keyof T]: T[P] extends (() => infer U) ? U : never }; +export function on T>, U>( + ...args: X['length'] extends 1 + ? [w: () => T, fn: (v: T, prev: T | undefined, prevResults?: U) => U] + : [...w: X, fn: (v: ReturnTypeArray, prev: ReturnTypeArray | [], prevResults?: U) => U] +): (prev?: U) => U { const fn = args.pop() as (v: T | Array, p?: T | Array, r?: U) => U; let deps: (() => T) | Array<() => T>; let isArray = true; From cdfeab18eedbe48fe27b83fd10818dad3714af7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kwa=C5=9Bniak?= Date: Mon, 1 Feb 2021 07:43:11 +0100 Subject: [PATCH 2/3] Add runWithOwner (#326) --- packages/solid/bench/s-mod.js | 1209 +++++++++++++------------ packages/solid/src/index.ts | 1 + packages/solid/src/reactive/signal.ts | 10 + packages/solid/src/static/index.ts | 1 + packages/solid/src/static/reactive.ts | 10 + 5 files changed, 630 insertions(+), 601 deletions(-) diff --git a/packages/solid/bench/s-mod.js b/packages/solid/bench/s-mod.js index 35ebe9a30..32b94c44d 100644 --- a/packages/solid/bench/s-mod.js +++ b/packages/solid/bench/s-mod.js @@ -1,601 +1,608 @@ -// Modified version of S.js[https://github.com/adamhaile/S] by Adam Haile -// Comparator memos from VSJolund fork https://github.com/VSjolund/vs-bind -const equalFn = (a, b) => a === b; -const ERROR = Symbol("error"); -// Public interface -function createRoot(fn, detachedOwner) { - detachedOwner && (Owner = detachedOwner); - let owner = Owner, - listener = Listener, - root = fn.length === 0 ? UNOWNED : createComputationNode(null, null), - result = undefined, - disposer = function _dispose() { - if (RunningClock !== null) { - RootClock.disposes.add(root); - } else { - dispose(root); - } - }; - Owner = root; - Listener = null; - try { - result = fn(disposer); - } catch (err) { - const fns = lookup(Owner, ERROR); - if (!fns) throw err; - fns.forEach(f => f(err)); - } finally { - RootClock.afters.run(f => f()); - Listener = listener; - Owner = owner; - } - return result; -} -function createSignal(value, areEqual) { - const d = new DataNode(value); - let setter; - if (areEqual) { - let age = -1; - setter = v => { - if (!areEqual(v, value)) { - const time = RootClock.time; - if (time === age) { - throw new Error(`Conflicting value update: ${v} is not the same as ${value}`); - } - age = time; - value = v; - d.next(v); - } - }; - } else setter = d.next.bind(d); - return [d.current.bind(d), setter]; -} -function createEffect(fn, value) { - createComputationNode(fn, value); -} -function createDependentEffect(fn, deps, defer) { - const resolved = Array.isArray(deps) ? callAll(deps) : deps; - defer = !!defer; - createComputationNode(value => { - const listener = Listener; - resolved(); - if (defer) defer = false; - else { - Listener = null; - value = fn(value); - Listener = listener; - } - return value; - }); -} -function createMemo(fn, value, areEqual) { - var node = createComputationNode(fn, value); - node.comparator = areEqual || null; - return () => { - if (Listener !== null) { - const state = node.state; - if ((state & 7) !== 0) { - liftComputation(node); - } - if (node.age === RootClock.time && state === 8) { - throw new Error("Circular dependency."); - } - if ((state & 16) === 0) { - if (node.log === null) node.log = createLog(); - logRead(node.log); - } - } - return node.value; - }; -} -function batch(fn) { - let result = undefined; - if (RunningClock !== null) result = fn(); - else { - RunningClock = RootClock; - RunningClock.changes.reset(); - try { - result = fn(); - event(); - } finally { - RunningClock = null; - } - } - return result; -} -function sample(fn) { - let result, - listener = Listener; - Listener = null; - result = fn(); - Listener = listener; - return result; -} -function afterEffects(fn) { - if (RunningClock !== null) RunningClock.afters.add(fn); - else RootClock.afters.add(fn); -} -function onCleanup(fn) { - if (Owner === null) - console.warn("cleanups created outside a `createRoot` or `render` will never be run"); - else if (Owner.cleanups === null) Owner.cleanups = [fn]; - else Owner.cleanups.push(fn); -} -function onError(fn) { - if (Owner === null) - console.warn("error handlers created outside a `createRoot` or `render` will never be run"); - else if (Owner.context === null) Owner.context = { [ERROR]: [fn] }; - else if (!Owner.context[ERROR]) Owner.context[ERROR] = [fn]; - else Owner.context[ERROR].push(fn); -} -function isListening() { - return Listener !== null; -} -function createContext(defaultValue) { - const id = Symbol("context"); - return { id, Provider: createProvider(id), defaultValue }; -} -function useContext(context) { - return lookup(Owner, context.id) || context.defaultValue; -} -function getContextOwner() { - return Owner; -} -// Internal implementation -/// Graph classes and operations -class DataNode { - constructor(value) { - this.value = value; - this.pending = NOTPENDING; - this.log = null; - } - current() { - if (Listener !== null) { - if (this.log === null) this.log = createLog(); - logRead(this.log); - } - return this.value; - } - next(value) { - if (RunningClock !== null) { - if (this.pending !== NOTPENDING) { - // value has already been set once, check for conflicts - if (value !== this.pending) { - throw new Error("conflicting changes: " + value + " !== " + this.pending); - } - } else { - // add to list of changes - this.pending = value; - RootClock.changes.add(this); - } - } else { - // not batching, respond to change now - if (this.log !== null) { - this.pending = value; - RootClock.changes.add(this); - event(); - } else { - this.value = value; - } - } - return value; - } -} -function createComputationNode(fn, value) { - const node = { - fn, - value, - age: RootClock.time, - state: 0, - comparator: null, - source1: null, - source1slot: 0, - sources: null, - sourceslots: null, - dependents: null, - dependentslot: 0, - dependentcount: 0, - owner: Owner, - owned: null, - log: null, - context: null, - cleanups: null - }; - if (fn === null) return node; - let owner = Owner, - listener = Listener; - if (owner === null) - console.warn("computations created outside a `createRoot` or `render` will never be disposed"); - Owner = Listener = node; - if (RunningClock === null) { - toplevelComputation(node); - } else node.value = node.fn(node.value); - if (owner && owner !== UNOWNED) { - if (owner.owned === null) owner.owned = [node]; - else owner.owned.push(node); - } - Owner = owner; - Listener = listener; - return node; -} -function createClock() { - return { - time: 0, - changes: new Queue(), - updates: new Queue(), - disposes: new Queue(), - afters: new Queue() - }; -} -function createLog() { - return { - node1: null, - node1slot: 0, - nodes: null, - nodeslots: null - }; -} -class Queue { - constructor() { - this.items = []; - this.count = 0; - } - reset() { - this.count = 0; - } - add(item) { - this.items[this.count++] = item; - } - run(fn) { - let items = this.items; - for (let i = 0; i < this.count; i++) { - try { - const item = items[i]; - items[i] = null; - fn(item); - } catch (err) { - const fns = lookup(Owner, ERROR); - if (!fns) throw err; - fns.forEach(f => f(err)); - } - } - this.count = 0; - } -} -// "Globals" used to keep track of current system state -let RootClock = createClock(), - RunningClock = null, // currently running clock - Listener = null, // currently listening computation - Owner = null, // owner for new computations - Pending = null; // pending node -// Constants -let NOTPENDING = {}, - UNOWNED = createComputationNode(null, null); -// State -// 1 - Stale, 2 - Pending, 4 - Pending Disposal, 8 - Running, 16 - Disposed -// Functions -function callAll(ss) { - return function all() { - for (let i = 0; i < ss.length; i++) ss[i](); - }; -} -function lookup(owner, key) { - return ( - owner && ((owner.context && owner.context[key]) || (owner.owner && lookup(owner.owner, key))) - ); -} -function resolveChildren(children) { - if (typeof children === "function") return createMemo(() => resolveChildren(children())); - if (Array.isArray(children)) { - const results = []; - for (let i = 0; i < children.length; i++) { - let result = resolveChildren(children[i]); - Array.isArray(result) ? results.push.apply(results, result) : results.push(result); - } - return results; - } - return children; -} -function createProvider(id) { - return function provider(props) { - let rendered; - createComputationNode(() => { - Owner.context = { [id]: props.value }; - rendered = sample(() => resolveChildren(props.children)); - }); - return rendered; - }; -} -function logRead(from) { - let to = Listener, - fromslot, - toslot = to.source1 === null ? -1 : to.sources === null ? 0 : to.sources.length; - if (from.node1 === null) { - from.node1 = to; - from.node1slot = toslot; - fromslot = -1; - } else if (from.nodes === null) { - if (from.node1 === to) return; - from.nodes = [to]; - from.nodeslots = [toslot]; - fromslot = 0; - } else { - fromslot = from.nodes.length; - if (from.nodes[fromslot - 1] === to) return; - from.nodes.push(to); - from.nodeslots.push(toslot); - } - if (to.source1 === null) { - to.source1 = from; - to.source1slot = fromslot; - } else if (to.sources === null) { - to.sources = [from]; - to.sourceslots = [fromslot]; - } else { - to.sources.push(from); - to.sourceslots.push(fromslot); - } -} -function liftComputation(node) { - if ((node.state & 6) !== 0) { - applyUpstreamUpdates(node); - } - if ((node.state & 1) !== 0) { - updateNode(node); - } - resetComputation(node, 31); -} -function event() { - // b/c we might be under a top level S.root(), have to preserve current root - let owner = Owner; - RootClock.updates.reset(); - RootClock.time++; - try { - run(RootClock); - } finally { - RunningClock = Listener = null; - Owner = owner; - } -} -function toplevelComputation(node) { - RunningClock = RootClock; - RootClock.changes.reset(); - RootClock.updates.reset(); - try { - node.value = node.fn(node.value); - if (RootClock.changes.count > 0 || RootClock.updates.count > 0) { - RootClock.time++; - run(RootClock); - } - } catch (err) { - const fns = lookup(Owner, ERROR); - if (!fns) throw err; - fns.forEach(f => f(err)); - } finally { - RunningClock = Owner = Listener = null; - } -} -function run(clock) { - let running = RunningClock, - count = 0; - RunningClock = clock; - clock.disposes.reset(); - // for each batch ... - while (clock.changes.count !== 0 || clock.updates.count !== 0 || clock.disposes.count !== 0) { - if (count > 0) - // don't tick on first run, or else we expire already scheduled updates - clock.time++; - clock.changes.run(applyDataChange); - clock.updates.run(updateNode); - clock.disposes.run(dispose); - // if there are still changes after excessive batches, assume runaway - if (count++ > 1e5) { - throw new Error("Runaway clock detected"); - } - } - clock.afters.run(f => f()); - RunningClock = running; -} -function applyDataChange(data) { - data.value = data.pending; - data.pending = NOTPENDING; - if (data.log) setComputationState(data.log, stateStale); -} -function updateNode(node) { - const state = node.state; - if ((state & 16) === 0) { - if ((state & 2) !== 0) { - node.dependents[node.dependentslot++] = null; - if (node.dependentslot === node.dependentcount) { - resetComputation(node, 14); - } - } else if ((state & 1) !== 0) { - if ((state & 4) !== 0) { - liftComputation(node); - } else if (node.comparator) { - const current = updateComputation(node); - const comparator = node.comparator; - if (!comparator(current, node.value)) { - markDownstreamComputations(node, false, true); - } - } else { - updateComputation(node); - } - } - } -} -function updateComputation(node) { - const value = node.value, - owner = Owner, - listener = Listener; - Owner = Listener = node; - node.state = 8; - cleanupNode(node, false); - node.value = node.fn(node.value); - resetComputation(node, 31); - Owner = owner; - Listener = listener; - return value; -} -function stateStale(node) { - const time = RootClock.time; - if (node.age < time) { - node.state |= 1; - node.age = time; - setDownstreamState(node, !!node.comparator); - } -} -function statePending(node) { - const time = RootClock.time; - if (node.age < time) { - node.state |= 2; - let dependents = node.dependents || (node.dependents = []); - dependents[node.dependentcount++] = Pending; - setDownstreamState(node, true); - } -} -function pendingStateStale(node) { - if ((node.state & 2) !== 0) { - node.state = 1; - const time = RootClock.time; - if (node.age < time) { - node.age = time; - if (!node.comparator) { - markDownstreamComputations(node, false, true); - } - } - } -} -function setDownstreamState(node, pending) { - RootClock.updates.add(node); - if (node.comparator) { - const pending = Pending; - Pending = node; - markDownstreamComputations(node, true, false); - Pending = pending; - } else { - markDownstreamComputations(node, pending, false); - } -} -function markDownstreamComputations(node, onchange, dirty) { - const owned = node.owned; - if (owned !== null) { - const pending = onchange && !dirty; - markForDisposal(owned, pending, RootClock.time); - } - const log = node.log; - if (log !== null) { - setComputationState(log, dirty ? pendingStateStale : onchange ? statePending : stateStale); - } -} -function setComputationState(log, stateFn) { - const node1 = log.node1, - nodes = log.nodes; - if (node1 !== null) stateFn(node1); - if (nodes !== null) { - for (let i = 0, ln = nodes.length; i < ln; i++) { - stateFn(nodes[i]); - } - } -} -function markForDisposal(children, pending, time) { - for (let i = 0, ln = children.length; i < ln; i++) { - const child = children[i]; - if (child !== null) { - if (pending) { - if ((child.state & 16) === 0) { - child.state |= 4; - } - } else { - child.age = time; - child.state = 16; - } - const owned = child.owned; - if (owned !== null) markForDisposal(owned, pending, time); - } - } -} -function applyUpstreamUpdates(node) { - if ((node.state & 4) !== 0) { - const owner = node.owner; - if ((owner.state & 7) !== 0) liftComputation(owner); - node.state &= ~4; - } - if ((node.state & 2) !== 0) { - const slots = node.dependents; - for (let i = node.dependentslot, ln = node.dependentcount; i < ln; i++) { - const slot = slots[i]; - if (slot != null) liftComputation(slot); - slots[i] = null; - } - node.state &= ~2; - } -} -function cleanupNode(node, final) { - let source1 = node.source1, - sources = node.sources, - sourceslots = node.sourceslots, - cleanups = node.cleanups, - owned = node.owned, - i, - len; - if (cleanups !== null) { - for (i = 0; i < cleanups.length; i++) { - cleanups[i](final); - } - node.cleanups = null; - } - node.context = null; - if (owned !== null) { - for (i = 0; i < owned.length; i++) { - dispose(owned[i]); - } - node.owned = null; - } - if (source1 !== null) { - cleanupSource(source1, node.source1slot); - node.source1 = null; - } - if (sources !== null) { - for (i = 0, len = sources.length; i < len; i++) { - cleanupSource(sources.pop(), sourceslots.pop()); - } - } -} -function cleanupSource(source, slot) { - let nodes = source.nodes, - nodeslots = source.nodeslots, - last, - lastslot; - if (slot === -1) { - source.node1 = null; - } else { - last = nodes.pop(); - lastslot = nodeslots.pop(); - if (slot !== nodes.length) { - nodes[slot] = last; - nodeslots[slot] = lastslot; - if (lastslot === -1) { - last.source1slot = slot; - } else { - last.sourceslots[lastslot] = slot; - } - } - } -} -function resetComputation(node, flags) { - node.state &= ~flags; - node.dependentslot = 0; - node.dependentcount = 0; -} -function dispose(node) { - node.fn = null; - node.log = null; - node.dependents = null; - cleanupNode(node, true); - resetComputation(node, 31); -} - -module.exports = { - createRoot, createComputed: createEffect, createSignal -} \ No newline at end of file +// Modified version of S.js[https://github.com/adamhaile/S] by Adam Haile +// Comparator memos from VSJolund fork https://github.com/VSjolund/vs-bind +const equalFn = (a, b) => a === b; +const ERROR = Symbol("error"); +// Public interface +function createRoot(fn, detachedOwner) { + detachedOwner && (Owner = detachedOwner); + let owner = Owner, + listener = Listener, + root = fn.length === 0 ? UNOWNED : createComputationNode(null, null), + result = undefined, + disposer = function _dispose() { + if (RunningClock !== null) { + RootClock.disposes.add(root); + } else { + dispose(root); + } + }; + Owner = root; + Listener = null; + try { + result = fn(disposer); + } catch (err) { + const fns = lookup(Owner, ERROR); + if (!fns) throw err; + fns.forEach(f => f(err)); + } finally { + RootClock.afters.run(f => f()); + Listener = listener; + Owner = owner; + } + return result; +} +function createSignal(value, areEqual) { + const d = new DataNode(value); + let setter; + if (areEqual) { + let age = -1; + setter = v => { + if (!areEqual(v, value)) { + const time = RootClock.time; + if (time === age) { + throw new Error(`Conflicting value update: ${v} is not the same as ${value}`); + } + age = time; + value = v; + d.next(v); + } + }; + } else setter = d.next.bind(d); + return [d.current.bind(d), setter]; +} +function createEffect(fn, value) { + createComputationNode(fn, value); +} +function createDependentEffect(fn, deps, defer) { + const resolved = Array.isArray(deps) ? callAll(deps) : deps; + defer = !!defer; + createComputationNode(value => { + const listener = Listener; + resolved(); + if (defer) defer = false; + else { + Listener = null; + value = fn(value); + Listener = listener; + } + return value; + }); +} +function createMemo(fn, value, areEqual) { + var node = createComputationNode(fn, value); + node.comparator = areEqual || null; + return () => { + if (Listener !== null) { + const state = node.state; + if ((state & 7) !== 0) { + liftComputation(node); + } + if (node.age === RootClock.time && state === 8) { + throw new Error("Circular dependency."); + } + if ((state & 16) === 0) { + if (node.log === null) node.log = createLog(); + logRead(node.log); + } + } + return node.value; + }; +} +function batch(fn) { + let result = undefined; + if (RunningClock !== null) result = fn(); + else { + RunningClock = RootClock; + RunningClock.changes.reset(); + try { + result = fn(); + event(); + } finally { + RunningClock = null; + } + } + return result; +} +function sample(fn) { + let result, + listener = Listener; + Listener = null; + result = fn(); + Listener = listener; + return result; +} +function afterEffects(fn) { + if (RunningClock !== null) RunningClock.afters.add(fn); + else RootClock.afters.add(fn); +} +function onCleanup(fn) { + if (Owner === null) + console.warn("cleanups created outside a `createRoot` or `render` will never be run"); + else if (Owner.cleanups === null) Owner.cleanups = [fn]; + else Owner.cleanups.push(fn); +} +function onError(fn) { + if (Owner === null) + console.warn("error handlers created outside a `createRoot` or `render` will never be run"); + else if (Owner.context === null) Owner.context = { [ERROR]: [fn] }; + else if (!Owner.context[ERROR]) Owner.context[ERROR] = [fn]; + else Owner.context[ERROR].push(fn); +} +function isListening() { + return Listener !== null; +} +function createContext(defaultValue) { + const id = Symbol("context"); + return { id, Provider: createProvider(id), defaultValue }; +} +function useContext(context) { + return lookup(Owner, context.id) || context.defaultValue; +} +function getContextOwner() { + return Owner; +} +function runWithOwner(owner, callback) { + const currentOwner = getContextOwner(); + Owner = owner; + const result = callback(); + Owner = currentOwner; + return result; +} +// Internal implementation +/// Graph classes and operations +class DataNode { + constructor(value) { + this.value = value; + this.pending = NOTPENDING; + this.log = null; + } + current() { + if (Listener !== null) { + if (this.log === null) this.log = createLog(); + logRead(this.log); + } + return this.value; + } + next(value) { + if (RunningClock !== null) { + if (this.pending !== NOTPENDING) { + // value has already been set once, check for conflicts + if (value !== this.pending) { + throw new Error("conflicting changes: " + value + " !== " + this.pending); + } + } else { + // add to list of changes + this.pending = value; + RootClock.changes.add(this); + } + } else { + // not batching, respond to change now + if (this.log !== null) { + this.pending = value; + RootClock.changes.add(this); + event(); + } else { + this.value = value; + } + } + return value; + } +} +function createComputationNode(fn, value) { + const node = { + fn, + value, + age: RootClock.time, + state: 0, + comparator: null, + source1: null, + source1slot: 0, + sources: null, + sourceslots: null, + dependents: null, + dependentslot: 0, + dependentcount: 0, + owner: Owner, + owned: null, + log: null, + context: null, + cleanups: null + }; + if (fn === null) return node; + let owner = Owner, + listener = Listener; + if (owner === null) + console.warn("computations created outside a `createRoot` or `render` will never be disposed"); + Owner = Listener = node; + if (RunningClock === null) { + toplevelComputation(node); + } else node.value = node.fn(node.value); + if (owner && owner !== UNOWNED) { + if (owner.owned === null) owner.owned = [node]; + else owner.owned.push(node); + } + Owner = owner; + Listener = listener; + return node; +} +function createClock() { + return { + time: 0, + changes: new Queue(), + updates: new Queue(), + disposes: new Queue(), + afters: new Queue() + }; +} +function createLog() { + return { + node1: null, + node1slot: 0, + nodes: null, + nodeslots: null + }; +} +class Queue { + constructor() { + this.items = []; + this.count = 0; + } + reset() { + this.count = 0; + } + add(item) { + this.items[this.count++] = item; + } + run(fn) { + let items = this.items; + for (let i = 0; i < this.count; i++) { + try { + const item = items[i]; + items[i] = null; + fn(item); + } catch (err) { + const fns = lookup(Owner, ERROR); + if (!fns) throw err; + fns.forEach(f => f(err)); + } + } + this.count = 0; + } +} +// "Globals" used to keep track of current system state +let RootClock = createClock(), + RunningClock = null, // currently running clock + Listener = null, // currently listening computation + Owner = null, // owner for new computations + Pending = null; // pending node +// Constants +let NOTPENDING = {}, + UNOWNED = createComputationNode(null, null); +// State +// 1 - Stale, 2 - Pending, 4 - Pending Disposal, 8 - Running, 16 - Disposed +// Functions +function callAll(ss) { + return function all() { + for (let i = 0; i < ss.length; i++) ss[i](); + }; +} +function lookup(owner, key) { + return ( + owner && ((owner.context && owner.context[key]) || (owner.owner && lookup(owner.owner, key))) + ); +} +function resolveChildren(children) { + if (typeof children === "function") return createMemo(() => resolveChildren(children())); + if (Array.isArray(children)) { + const results = []; + for (let i = 0; i < children.length; i++) { + let result = resolveChildren(children[i]); + Array.isArray(result) ? results.push.apply(results, result) : results.push(result); + } + return results; + } + return children; +} +function createProvider(id) { + return function provider(props) { + let rendered; + createComputationNode(() => { + Owner.context = { [id]: props.value }; + rendered = sample(() => resolveChildren(props.children)); + }); + return rendered; + }; +} +function logRead(from) { + let to = Listener, + fromslot, + toslot = to.source1 === null ? -1 : to.sources === null ? 0 : to.sources.length; + if (from.node1 === null) { + from.node1 = to; + from.node1slot = toslot; + fromslot = -1; + } else if (from.nodes === null) { + if (from.node1 === to) return; + from.nodes = [to]; + from.nodeslots = [toslot]; + fromslot = 0; + } else { + fromslot = from.nodes.length; + if (from.nodes[fromslot - 1] === to) return; + from.nodes.push(to); + from.nodeslots.push(toslot); + } + if (to.source1 === null) { + to.source1 = from; + to.source1slot = fromslot; + } else if (to.sources === null) { + to.sources = [from]; + to.sourceslots = [fromslot]; + } else { + to.sources.push(from); + to.sourceslots.push(fromslot); + } +} +function liftComputation(node) { + if ((node.state & 6) !== 0) { + applyUpstreamUpdates(node); + } + if ((node.state & 1) !== 0) { + updateNode(node); + } + resetComputation(node, 31); +} +function event() { + // b/c we might be under a top level S.root(), have to preserve current root + let owner = Owner; + RootClock.updates.reset(); + RootClock.time++; + try { + run(RootClock); + } finally { + RunningClock = Listener = null; + Owner = owner; + } +} +function toplevelComputation(node) { + RunningClock = RootClock; + RootClock.changes.reset(); + RootClock.updates.reset(); + try { + node.value = node.fn(node.value); + if (RootClock.changes.count > 0 || RootClock.updates.count > 0) { + RootClock.time++; + run(RootClock); + } + } catch (err) { + const fns = lookup(Owner, ERROR); + if (!fns) throw err; + fns.forEach(f => f(err)); + } finally { + RunningClock = Owner = Listener = null; + } +} +function run(clock) { + let running = RunningClock, + count = 0; + RunningClock = clock; + clock.disposes.reset(); + // for each batch ... + while (clock.changes.count !== 0 || clock.updates.count !== 0 || clock.disposes.count !== 0) { + if (count > 0) + // don't tick on first run, or else we expire already scheduled updates + clock.time++; + clock.changes.run(applyDataChange); + clock.updates.run(updateNode); + clock.disposes.run(dispose); + // if there are still changes after excessive batches, assume runaway + if (count++ > 1e5) { + throw new Error("Runaway clock detected"); + } + } + clock.afters.run(f => f()); + RunningClock = running; +} +function applyDataChange(data) { + data.value = data.pending; + data.pending = NOTPENDING; + if (data.log) setComputationState(data.log, stateStale); +} +function updateNode(node) { + const state = node.state; + if ((state & 16) === 0) { + if ((state & 2) !== 0) { + node.dependents[node.dependentslot++] = null; + if (node.dependentslot === node.dependentcount) { + resetComputation(node, 14); + } + } else if ((state & 1) !== 0) { + if ((state & 4) !== 0) { + liftComputation(node); + } else if (node.comparator) { + const current = updateComputation(node); + const comparator = node.comparator; + if (!comparator(current, node.value)) { + markDownstreamComputations(node, false, true); + } + } else { + updateComputation(node); + } + } + } +} +function updateComputation(node) { + const value = node.value, + owner = Owner, + listener = Listener; + Owner = Listener = node; + node.state = 8; + cleanupNode(node, false); + node.value = node.fn(node.value); + resetComputation(node, 31); + Owner = owner; + Listener = listener; + return value; +} +function stateStale(node) { + const time = RootClock.time; + if (node.age < time) { + node.state |= 1; + node.age = time; + setDownstreamState(node, !!node.comparator); + } +} +function statePending(node) { + const time = RootClock.time; + if (node.age < time) { + node.state |= 2; + let dependents = node.dependents || (node.dependents = []); + dependents[node.dependentcount++] = Pending; + setDownstreamState(node, true); + } +} +function pendingStateStale(node) { + if ((node.state & 2) !== 0) { + node.state = 1; + const time = RootClock.time; + if (node.age < time) { + node.age = time; + if (!node.comparator) { + markDownstreamComputations(node, false, true); + } + } + } +} +function setDownstreamState(node, pending) { + RootClock.updates.add(node); + if (node.comparator) { + const pending = Pending; + Pending = node; + markDownstreamComputations(node, true, false); + Pending = pending; + } else { + markDownstreamComputations(node, pending, false); + } +} +function markDownstreamComputations(node, onchange, dirty) { + const owned = node.owned; + if (owned !== null) { + const pending = onchange && !dirty; + markForDisposal(owned, pending, RootClock.time); + } + const log = node.log; + if (log !== null) { + setComputationState(log, dirty ? pendingStateStale : onchange ? statePending : stateStale); + } +} +function setComputationState(log, stateFn) { + const node1 = log.node1, + nodes = log.nodes; + if (node1 !== null) stateFn(node1); + if (nodes !== null) { + for (let i = 0, ln = nodes.length; i < ln; i++) { + stateFn(nodes[i]); + } + } +} +function markForDisposal(children, pending, time) { + for (let i = 0, ln = children.length; i < ln; i++) { + const child = children[i]; + if (child !== null) { + if (pending) { + if ((child.state & 16) === 0) { + child.state |= 4; + } + } else { + child.age = time; + child.state = 16; + } + const owned = child.owned; + if (owned !== null) markForDisposal(owned, pending, time); + } + } +} +function applyUpstreamUpdates(node) { + if ((node.state & 4) !== 0) { + const owner = node.owner; + if ((owner.state & 7) !== 0) liftComputation(owner); + node.state &= ~4; + } + if ((node.state & 2) !== 0) { + const slots = node.dependents; + for (let i = node.dependentslot, ln = node.dependentcount; i < ln; i++) { + const slot = slots[i]; + if (slot != null) liftComputation(slot); + slots[i] = null; + } + node.state &= ~2; + } +} +function cleanupNode(node, final) { + let source1 = node.source1, + sources = node.sources, + sourceslots = node.sourceslots, + cleanups = node.cleanups, + owned = node.owned, + i, + len; + if (cleanups !== null) { + for (i = 0; i < cleanups.length; i++) { + cleanups[i](final); + } + node.cleanups = null; + } + node.context = null; + if (owned !== null) { + for (i = 0; i < owned.length; i++) { + dispose(owned[i]); + } + node.owned = null; + } + if (source1 !== null) { + cleanupSource(source1, node.source1slot); + node.source1 = null; + } + if (sources !== null) { + for (i = 0, len = sources.length; i < len; i++) { + cleanupSource(sources.pop(), sourceslots.pop()); + } + } +} +function cleanupSource(source, slot) { + let nodes = source.nodes, + nodeslots = source.nodeslots, + last, + lastslot; + if (slot === -1) { + source.node1 = null; + } else { + last = nodes.pop(); + lastslot = nodeslots.pop(); + if (slot !== nodes.length) { + nodes[slot] = last; + nodeslots[slot] = lastslot; + if (lastslot === -1) { + last.source1slot = slot; + } else { + last.sourceslots[lastslot] = slot; + } + } + } +} +function resetComputation(node, flags) { + node.state &= ~flags; + node.dependentslot = 0; + node.dependentcount = 0; +} +function dispose(node) { + node.fn = null; + node.log = null; + node.dependents = null; + cleanupNode(node, true); + resetComputation(node, 31); +} + +module.exports = { + createRoot, createComputed: createEffect, createSignal +} diff --git a/packages/solid/src/index.ts b/packages/solid/src/index.ts index 07d13749a..d0904c9a6 100644 --- a/packages/solid/src/index.ts +++ b/packages/solid/src/index.ts @@ -19,6 +19,7 @@ export { createContext, useContext, getContextOwner, + runWithOwner, equalFn, serializeGraph } from "./reactive/signal"; diff --git a/packages/solid/src/reactive/signal.ts b/packages/solid/src/reactive/signal.ts index 57e61feed..6916db9d1 100644 --- a/packages/solid/src/reactive/signal.ts +++ b/packages/solid/src/reactive/signal.ts @@ -327,6 +327,16 @@ export function getContextOwner() { return Owner; } +export function runWithOwner(owner: Owner | null, callback: () => T) { + const currentOwner = getContextOwner(); + + Owner = owner; + const result = callback(); + Owner = currentOwner; + + return result; +} + export function hashValue(v: any) { return "s" + (typeof v === "string" ? hash(v) : hash(JSON.stringify(v) || "")); } diff --git a/packages/solid/src/static/index.ts b/packages/solid/src/static/index.ts index 40c897b0f..ba327cfe9 100644 --- a/packages/solid/src/static/index.ts +++ b/packages/solid/src/static/index.ts @@ -17,6 +17,7 @@ export { createContext, useContext, getContextOwner, + runWithOwner, equalFn, requestCallback, createState, diff --git a/packages/solid/src/static/reactive.ts b/packages/solid/src/static/reactive.ts index 76808a390..c315e4121 100644 --- a/packages/solid/src/static/reactive.ts +++ b/packages/solid/src/static/reactive.ts @@ -140,6 +140,16 @@ export function getContextOwner() { return Owner; } +export function runWithOwner(owner: Owner | null, callback: () => T) { + const currentOwner = getContextOwner(); + + Owner = owner; + const result = callback(); + Owner = currentOwner; + + return result; +} + function lookup(owner: Owner | null, key: symbol | string): any { return ( owner && ((owner.context && owner.context[key]) || (owner.owner && lookup(owner.owner, key))) From 851bd63202d6e9eb6907c77a26ac323ce728d206 Mon Sep 17 00:00:00 2001 From: Jager567 Date: Tue, 2 Feb 2021 09:22:51 +0100 Subject: [PATCH 3/3] Fix some inaccurate typings and Selector bug (#327) * Fix incorrect argument type of createEffect When an initial value is passed to createComputed, createRenderEffect or createEffect, and the return type of the passed function doesn't include undefined, the argument will never be undefined either. Additionally, when no initial value is given, there is no reason to restrict the return type to a defined value. The code can handle undefined values just fine. Some examples that fail to typecheck, but work in practice have been added as test cases. This commit adds overloads to the mentioned functions to remove undefined from the argument type when an initial value is given, and allow undefined in the return type otherwise. * Fix types of functions which can return undefined The setter returned by `createSignal()` accepts an optional argument, but returns a non-optional type. This commit adds a test case where this results in incorrectly typing an undefined value as defined. Similarly, `createContext()` creates a context with a default value of undefined. However, when used in `useContext()`, it will return a non-undefined type. This is possible because `lookup()` returns `any`. Typescript incorrectly assumed that `any` equals `T` and forgets that it could also be undefined. This commit works around that by encoding the possible undefined in the `T` stored in `Context<>`. * Fix selector not working when passing falsy value The primary use for selectors is using an array index as key. This means that the value `0` is likely to be used quite often. However, due to the use of a falsy check instead of an undefined check, passing 0 will not cause an update when the key is unset. This is demonstrated by a test. This commit replaces the falsy check with an undefined check. --- packages/solid/src/reactive/signal.ts | 16 +++-- packages/solid/test/signals.spec.ts | 94 ++++++++++++++++++++++++++- 2 files changed, 105 insertions(+), 5 deletions(-) diff --git a/packages/solid/src/reactive/signal.ts b/packages/solid/src/reactive/signal.ts index 6916db9d1..8c71f6db3 100644 --- a/packages/solid/src/reactive/signal.ts +++ b/packages/solid/src/reactive/signal.ts @@ -96,7 +96,7 @@ export function createRoot(fn: (dispose: () => void) => T, detachedOwner?: Ow return result!; } -export function createSignal(): [() => T | undefined, (v?: T) => T]; +export function createSignal(): [() => T | undefined, (v?: U) => U]; export function createSignal( value: T, areEqual?: boolean | ((prev: T, next: T) => boolean), @@ -120,14 +120,20 @@ export function createSignal( return [readSignal.bind(s), writeSignal.bind(s)]; } +export function createComputed(fn: (v: T) => T, value: T): void; +export function createComputed(fn: (v?: T) => T | undefined): void; export function createComputed(fn: (v?: T) => T, value?: T): void { updateComputation(createComputation(fn, value, true)); } +export function createRenderEffect(fn: (v: T) => T, value: T): void; +export function createRenderEffect(fn: (v?: T) => T | undefined): void; export function createRenderEffect(fn: (v?: T) => T, value?: T): void { updateComputation(createComputation(fn, value, false)); } +export function createEffect(fn: (v: T) => T, value: T): void; +export function createEffect(fn: (v?: T) => T | undefined): void; export function createEffect(fn: (v?: T) => T, value?: T): void { if (globalThis._$HYDRATION && globalThis._$HYDRATION.asyncSSR) return; runEffects = runUserEffects; @@ -199,7 +205,7 @@ export function createSelector( (p: T | undefined) => { const v = source(); for (const key of subs.keys()) - if (fn(key, v) || (p && fn(key, p))) { + if (fn(key, v) || (p !== undefined && fn(key, p))) { const c = subs.get(key)!; c.state = STALE; if (c.pure) Updates!.push(c); @@ -367,10 +373,12 @@ export function serializeGraph(owner?: Owner | null): GraphRecord { export interface Context { id: symbol; Provider: (props: { value: T; children: any }) => any; - defaultValue?: T; + defaultValue: T; } -export function createContext(defaultValue?: T): Context { +export function createContext(): Context +export function createContext(defaultValue: T): Context +export function createContext(defaultValue?: T): Context { const id = Symbol("context"); return { id, Provider: createProvider(id), defaultValue }; } diff --git a/packages/solid/test/signals.spec.ts b/packages/solid/test/signals.spec.ts index 034dd4e48..e85c8e4a1 100644 --- a/packages/solid/test/signals.spec.ts +++ b/packages/solid/test/signals.spec.ts @@ -2,6 +2,7 @@ import { createRoot, createSignal, createEffect, + createRenderEffect, createComputed, createDeferred, createMemo, @@ -10,7 +11,9 @@ import { on, onMount, onCleanup, - onError + onError, + createContext, + useContext } from "../src"; describe("Create signals", () => { @@ -111,6 +114,17 @@ describe("Update signals", () => { }); }); }); + test("Set signal returns argument", () => { + const [_, setValue] = createSignal(); + const res1: undefined = setValue(undefined); + expect(res1).toBe(undefined); + const res2: number = setValue(12); + expect(res2).toBe(12); + const res3 = setValue(Math.random() >= 0 ? 12 : undefined); + expect(res3).toBe(12); + const res4 = setValue(); + expect(res4).toBe(undefined); + }); }); describe("Untrack signals", () => { @@ -129,6 +143,49 @@ describe("Untrack signals", () => { }); }); +describe("Typecheck computed and effects", () => { + test("No default value can return undefined", () => { + createRoot(() => { + let count = 0; + const [sign, setSign] = createSignal("thoughts"); + const fn = (arg?: number) => { + count++; + sign(); + expect(arg).toBe(undefined); + return arg; + }; + createComputed(fn); + createRenderEffect(fn); + createEffect(fn); + setTimeout(() => { + expect(count).toBe(3); + setSign("update"); + expect(count).toBe(6); + }); + }); + }); + test("Default value never receives undefined", () => { + createRoot(() => { + let count = 0; + const [sign, setSign] = createSignal("thoughts"); + const fn = (arg: number) => { + count++; + sign(); + expect(arg).toBe(12); + return arg; + }; + createComputed(fn, 12); + createRenderEffect(fn, 12); + createEffect(fn, 12); + setTimeout(() => { + expect(count).toBe(3); + setSign("update"); + expect(count).toBe(6); + }); + }); + }); +}); + describe("onCleanup", () => { test("Clean an effect", done => { createRoot(() => { @@ -318,4 +375,39 @@ describe("createSelector", () => { }); }); }); + + test("zero index", done => { + createRoot(() => { + const [s, set] = createSignal(-1), + isSelected = createSelector(s); + let count = 0; + const list = [ + createMemo(() => { + count++; + return isSelected(0) ? "selected" : "no"; + }) + ]; + expect(count).toBe(1); + expect(list[0]()).toBe("no"); + setTimeout(() => { + count = 0; + set(0); + expect(count).toBe(1); + expect(list[0]()).toBe("selected"); + count = 0; + set(-1); + expect(count).toBe(1); + expect(list[0]()).toBe("no"); + done(); + }); + }); + }); }); + +describe("create and use context", () => { + test("createContext without arguments defaults to undefined", () => { + const context = createContext() + const res = useContext(context); + expect(res).toBe(undefined) + }) +})