diff --git a/packages/solid/src/index.ts b/packages/solid/src/index.ts index 96087c90a..4121c2b48 100644 --- a/packages/solid/src/index.ts +++ b/packages/solid/src/index.ts @@ -1,50 +1,51 @@ -export { - createRoot, - createSignal, - createEffect, - createRenderEffect, - createComputed, - createDeferred, - createSelector, - createMemo, - createResource, - getListener, - onMount, - onCleanup, - onError, - untrack, - batch, - on, - useTransition, - createContext, - useContext, - children, - getOwner, - equalFn, - serializeGraph -} from "./reactive/signal"; -export type { Resource } from "./reactive/signal"; - -export { createState, unwrap, $RAW } from "./reactive/state"; -export type { State, SetStateFunction } from "./reactive/state"; -export * from "./reactive/mutable"; - -export { reconcile, produce } from "./reactive/stateModifiers"; - -export * from "./reactive/scheduler"; -export * from "./reactive/array"; -export * from "./render"; -export type { JSX } from "./jsx"; - -// handle multiple instance check -declare global { - var Solid$$: boolean; -} - -if ("_SOLID_DEV_" && globalThis) { - if (!globalThis.Solid$$) globalThis.Solid$$ = true; - else - console.warn( - "You appear to have multiple instances of Solid. This can lead to unexpected behavior." - ); -} +export { + createRoot, + createSignal, + createEffect, + createRenderEffect, + createComputed, + createDeferred, + createSelector, + createMemo, + createResource, + getListener, + onMount, + onCleanup, + onError, + untrack, + batch, + on, + useTransition, + createContext, + useContext, + children, + getOwner, + runWithOwner, + equalFn, + serializeGraph +} from "./reactive/signal"; +export type { Resource } from "./reactive/signal"; + +export { createState, unwrap, $RAW } from "./reactive/state"; +export type { State, SetStateFunction } from "./reactive/state"; +export * from "./reactive/mutable"; + +export { reconcile, produce } from "./reactive/stateModifiers"; + +export * from "./reactive/scheduler"; +export * from "./reactive/array"; +export * from "./render"; +export type { JSX } from "./jsx"; + +// handle multiple instance check +declare global { + var Solid$$: boolean; +} + +if ("_SOLID_DEV_" && globalThis) { + if (!globalThis.Solid$$) globalThis.Solid$$ = true; + else + console.warn( + "You appear to have multiple instances of Solid. This can lead to unexpected behavior." + ); +} diff --git a/packages/solid/src/reactive/signal.ts b/packages/solid/src/reactive/signal.ts index 36e18e0e4..49d81641e 100644 --- a/packages/solid/src/reactive/signal.ts +++ b/packages/solid/src/reactive/signal.ts @@ -98,7 +98,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), @@ -122,14 +122,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 { runEffects = runUserEffects; const c = createComputation(fn, value, false), @@ -200,7 +206,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); @@ -270,34 +276,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; @@ -315,7 +299,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; }; } @@ -350,6 +334,16 @@ export function getOwner() { 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) || "")); } @@ -380,10 +374,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/src/static/index.ts b/packages/solid/src/static/index.ts index 116e79469..422452dd0 100644 --- a/packages/solid/src/static/index.ts +++ b/packages/solid/src/static/index.ts @@ -1,53 +1,54 @@ -export { - createRoot, - createSignal, - createComputed, - createRenderEffect, - createEffect, - createDeferred, - createSelector, - createMemo, - getListener, - onMount, - onCleanup, - onError, - untrack, - batch, - on, - createContext, - useContext, - getOwner, - equalFn, - requestCallback, - createState, - unwrap, - $RAW, - reconcile, - produce, - mapArray -} from "./reactive"; - -export { - awaitSuspense, - mergeProps, - splitProps, - createComponent, - For, - Index, - Show, - Switch, - Match, - ErrorBoundary, - Suspense, - SuspenseList, - createResource, - useTransition, - lazy, - sharedConfig -} from "./rendering"; - -export type { State, SetStateFunction } from "./reactive"; -export type { Component, Resource } from "./rendering"; - - - +export { + createRoot, + createSignal, + createComputed, + createRenderEffect, + createEffect, + createDeferred, + createSelector, + createMemo, + getListener, + onMount, + onCleanup, + onError, + untrack, + batch, + on, + createContext, + useContext, + getOwner, + runWithOwner, + equalFn, + requestCallback, + createState, + unwrap, + $RAW, + reconcile, + produce, + mapArray +} from "./reactive"; + +export { + awaitSuspense, + mergeProps, + splitProps, + createComponent, + For, + Index, + Show, + Switch, + Match, + ErrorBoundary, + Suspense, + SuspenseList, + createResource, + useTransition, + lazy, + sharedConfig +} from "./rendering"; + +export type { State, SetStateFunction } from "./reactive"; +export type { Component, Resource } from "./rendering"; + + + diff --git a/packages/solid/src/static/reactive.ts b/packages/solid/src/static/reactive.ts index 1de169aff..035da76f0 100644 --- a/packages/solid/src/static/reactive.ts +++ b/packages/solid/src/static/reactive.ts @@ -1,460 +1,438 @@ -export const equalFn = (a: T, b: T) => a === b; -const ERROR = Symbol("error"); - -const UNOWNED: Owner = { context: null, owner: null }; -export let Owner: Owner | null = null; - -interface Owner { - owner: Owner | null; - context: any | null; -} - -export function createRoot(fn: (dispose: () => void) => T, detachedOwner?: Owner): T { - detachedOwner && (Owner = detachedOwner); - const owner = Owner, - root: Owner = fn.length === 0 ? UNOWNED : { context: null, owner }; - Owner = root; - let result: T; - try { - result = fn(() => {}); - } catch (err) { - const fns = lookup(Owner, ERROR); - if (!fns) throw err; - fns.forEach((f: (err: any) => void) => f(err)); - } finally { - Owner = owner; - } - return result!; -} - -export function createSignal( - value?: T, - areEqual?: boolean | ((prev: T, next: T) => boolean) -): [() => T, (v: T) => T] { - return [() => value as T, (v: T) => (value = v)]; -} - -export function createComputed(fn: (v?: T) => T, value?: T): void { - Owner = { owner: Owner, context: null }; - fn(value); - Owner = Owner.owner; -} - -export const createRenderEffect = createComputed; - -export function createEffect(fn: (v?: T) => T, value?: T): void {} - -export function createMemo( - fn: (v?: T) => T, - value?: T, - areEqual?: boolean | ((prev: T, next: T) => boolean) -): () => T { - Owner = { owner: Owner, context: null }; - const v = fn(value); - Owner = Owner.owner; - return () => v; -} - -export function createDeferred(source: () => T, options?: { timeoutMs: number }) { - return source; -} - -export function createSelector( - source: () => T, - fn: (k: T, value: T, prevValue: T | undefined) => boolean -) { - return source; -} - -export function batch(fn: () => T): T { - return fn(); -} - -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 { - const fn = args.pop() as (v: T | Array, p?: T | Array, r?: U) => U; - let deps: (() => T) | Array<() => T>; - let isArray = true; - let prev: T | T[]; - if (args.length < 2) { - deps = args[0] as () => T; - isArray = false; - } else deps = args as Array<() => T>; - return prevResult => { - let value: T | Array; - if (isArray) { - value = []; - if (!prev) prev = []; - for (let i = 0; i < deps.length; i++) value.push((deps as Array<() => T>)[i]()); - } else value = (deps as () => T)(); - return fn!(value, prev, prevResult); - }; -} - -export function onMount(fn: () => void) {} - -export function onCleanup(fn: () => void) {} - -export function onError(fn: (err: any) => void): void { - if (Owner === null) - "_SOLID_DEV_" && - 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); -} - -export function getListener() { - return null; -} - -// Context API -export interface Context { - id: symbol; - Provider: (props: { value: T; children: any }) => any; - defaultValue?: T; -} - -export function createContext(defaultValue?: T): Context { - const id = Symbol("context"); - return { id, Provider: createProvider(id), defaultValue }; -} - -export function useContext(context: Context): T { - return lookup(Owner, context.id) || context.defaultValue; -} - -export function getOwner() { - return Owner; -} - -export function children(fn: () => any) { - return resolveChildren(fn()) -} - -export function runWithOwner(o: Owner, fn: () => any) { - const prev = Owner; - Owner = o; - try { - return fn(); - } finally { - Owner = prev; - } -} - -export function lookup(owner: Owner | null, key: symbol | string): any { - return ( - owner && ((owner.context && owner.context[key]) || (owner.owner && lookup(owner.owner, key))) - ); -} - -function resolveChildren(children: any): any { - if (typeof children === "function") return resolveChildren(children()); - if (Array.isArray(children)) { - const results: any[] = []; - 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: symbol) { - return function provider(props: { value: unknown; children: any }) { - let rendered; - createRenderEffect(() => { - Owner!.context = { [id]: props.value }; - rendered = resolveChildren(props.children); - }); - return rendered; - }; -} - -export interface Task { - id: number; - fn: ((didTimeout: boolean) => void) | null; - startTime: number; - expirationTime: number; -} -export function requestCallback(fn: () => void, options?: { timeout: number }): Task { - return { id: 0, fn: () => {}, startTime: 0, expirationTime: 0 }; -} -export function cancelCallback(task: Task) {} - -export const $RAW = Symbol("state-raw"); - -// well-known symbols need special treatment until https://github.com/microsoft/TypeScript/issues/24622 is implemented. -type AddSymbolToPrimitive = T extends { [Symbol.toPrimitive]: infer V } - ? { [Symbol.toPrimitive]: V } - : {}; -type AddCallable = T extends { (...x: any[]): infer V } ? { (...x: Parameters): V } : {}; - -type NotWrappable = string | number | boolean | Function | null; -export type State = { - [P in keyof T]: T[P] extends object ? State : T[P]; -} & { - [$RAW]?: T; -} & AddSymbolToPrimitive & - AddCallable; - -export function isWrappable(obj: any) { - return ( - obj != null && - typeof obj === "object" && - (obj.__proto__ === Object.prototype || Array.isArray(obj)) - ); -} - -export function unwrap(item: any): T { - return item; -} - -export function setProperty(state: any, property: string | number, value: any, force?: boolean) { - if (!force && state[property] === value) return; - if (value === undefined) { - delete state[property]; - } else state[property] = value; -} - -function mergeState(state: any, value: any, force?: boolean) { - const keys = Object.keys(value); - for (let i = 0; i < keys.length; i += 1) { - const key = keys[i]; - setProperty(state, key, value[key], force); - } -} - -export function updatePath(current: any, path: any[], traversed: (number | string)[] = []) { - let part, - next = current; - if (path.length > 1) { - part = path.shift(); - const partType = typeof part, - isArray = Array.isArray(current); - - if (Array.isArray(part)) { - // Ex. update('data', [2, 23], 'label', l => l + ' !!!'); - for (let i = 0; i < part.length; i++) { - updatePath(current, [part[i]].concat(path), [part[i]].concat(traversed)); - } - return; - } else if (isArray && partType === "function") { - // Ex. update('data', i => i.id === 42, 'label', l => l + ' !!!'); - for (let i = 0; i < current.length; i++) { - if (part(current[i], i)) - updatePath(current, [i].concat(path), ([i] as (number | string)[]).concat(traversed)); - } - return; - } else if (isArray && partType === "object") { - // Ex. update('data', { from: 3, to: 12, by: 2 }, 'label', l => l + ' !!!'); - const { from = 0, to = current.length - 1, by = 1 } = part; - for (let i = from; i <= to; i += by) { - updatePath(current, [i].concat(path), ([i] as (number | string)[]).concat(traversed)); - } - return; - } else if (path.length > 1) { - updatePath(current[part], path, [part].concat(traversed)); - return; - } - next = current[part]; - traversed = [part].concat(traversed); - } - let value = path[0]; - if (typeof value === "function") { - value = value(next, traversed); - if (value === next) return; - } - if (part === undefined && value == undefined) return; - if (part === undefined || (isWrappable(next) && isWrappable(value) && !Array.isArray(value))) { - mergeState(next, value); - } else setProperty(current, part, value); -} - -type StateSetter = - | Partial - | (( - prevState: T extends NotWrappable ? T : State, - traversed?: (string | number)[] - ) => Partial | void); -type StatePathRange = { from?: number; to?: number; by?: number }; - -type ArrayFilterFn = (item: T extends any[] ? T[number] : never, index: number) => boolean; - -type Part = keyof T | Array | StatePathRange | ArrayFilterFn; // changing this to "T extends any[] ? ArrayFilterFn : never" results in depth limit errors - -type Next = K extends keyof T - ? T[K] - : K extends Array - ? T[K[number]] - : T extends any[] - ? K extends StatePathRange - ? T[number] - : K extends ArrayFilterFn - ? T[number] - : never - : never; - -export interface SetStateFunction { - >(...args: [Setter]): void; - , Setter extends StateSetter>>(...args: [K1, Setter]): void; - < - K1 extends Part, - K2 extends Part>, - Setter extends StateSetter, K2>> - >( - ...args: [K1, K2, Setter] - ): void; - < - K1 extends Part, - K2 extends Part>, - K3 extends Part, K2>>, - Setter extends StateSetter, K2>, K3>> - >( - ...args: [K1, K2, K3, Setter] - ): void; - < - K1 extends Part, - K2 extends Part>, - K3 extends Part, K2>>, - K4 extends Part, K2>, K3>>, - Setter extends StateSetter, K2>, K3>, K4>> - >( - ...args: [K1, K2, K3, K4, Setter] - ): void; - < - K1 extends Part, - K2 extends Part>, - K3 extends Part, K2>>, - K4 extends Part, K2>, K3>>, - K5 extends Part, K2>, K3>, K4>>, - Setter extends StateSetter, K2>, K3>, K4>, K5>> - >( - ...args: [K1, K2, K3, K4, K5, Setter] - ): void; - < - K1 extends Part, - K2 extends Part>, - K3 extends Part, K2>>, - K4 extends Part, K2>, K3>>, - K5 extends Part, K2>, K3>, K4>>, - K6 extends Part, K2>, K3>, K4>, K5>>, - Setter extends StateSetter, K2>, K3>, K4>, K5>, K6>> - >( - ...args: [K1, K2, K3, K4, K5, K6, Setter] - ): void; - < - K1 extends Part, - K2 extends Part>, - K3 extends Part, K2>>, - K4 extends Part, K2>, K3>>, - K5 extends Part, K2>, K3>, K4>>, - K6 extends Part, K2>, K3>, K4>, K5>>, - K7 extends Part, K2>, K3>, K4>, K5>, K6>>, - Setter extends StateSetter< - Next, K2>, K3>, K4>, K5>, K6>, K7> - > - >( - ...args: [K1, K2, K3, K4, K5, K6, K7, Setter] - ): void; - - // and here we give up on being accurate after 8 args - < - K1 extends Part, - K2 extends Part>, - K3 extends Part, K2>>, - K4 extends Part, K2>, K3>>, - K5 extends Part, K2>, K3>, K4>>, - K6 extends Part, K2>, K3>, K4>, K5>>, - K7 extends Part, K2>, K3>, K4>, K5>, K6>>, - K8 extends Part, K2>, K3>, K4>, K5>, K6>, K7>> - >( - ...args: [K1, K2, K3, K4, K5, K6, K7, K8, ...(Part | StateSetter)[]] - ): void; -} - -export function createState(state: T | State): [State, SetStateFunction] { - function setState(...args: any[]): void { - updatePath(state, args); - } - return [state as State, setState]; -} - -type ReconcileOptions = { - key?: string | null; - merge?: boolean; -}; - -// Diff method for setState -export function reconcile( - value: T | State, - options: ReconcileOptions = {} -): (state: T extends NotWrappable ? T : State) => void { - return state => { - if (!isWrappable(state)) return value; - const targetKeys = Object.keys(value) as (keyof T)[]; - for (let i = 0, len = targetKeys.length; i < len; i++) { - const key = targetKeys[i]; - setProperty(state, key as string, value[key]); - } - const previousKeys = Object.keys(state) as (keyof T)[]; - for (let i = 0, len = previousKeys.length; i < len; i++) { - if (value[previousKeys[i]] === undefined) - setProperty(state, previousKeys[i] as string, undefined); - } - }; -} - -// Immer style mutation style -export function produce( - fn: (state: T) => void -): (state: T extends NotWrappable ? T : State) => T extends NotWrappable ? T : State { - return state => { - if (isWrappable(state)) fn(state as T); - return state; - }; -} - -export function mapArray( - list: () => T[], - mapFn: (v: T, i: () => number) => U, - options: { fallback?: () => any } = {} -): () => U[] { - const items = list(); - let s: U[] = []; - if (items.length) { - for (let i = 0, len = items.length; i < len; i++) s.push(mapFn(items[i], () => i)); - } else if (options.fallback) s = [options.fallback()]; - return () => s; -} +export const equalFn = (a: T, b: T) => a === b; +const ERROR = Symbol("error"); + +const UNOWNED: Owner = { context: null, owner: null }; +export let Owner: Owner | null = null; + +interface Owner { + owner: Owner | null; + context: any | null; +} + +export function createRoot(fn: (dispose: () => void) => T, detachedOwner?: Owner): T { + detachedOwner && (Owner = detachedOwner); + const owner = Owner, + root: Owner = fn.length === 0 ? UNOWNED : { context: null, owner }; + Owner = root; + let result: T; + try { + result = fn(() => {}); + } catch (err) { + const fns = lookup(Owner, ERROR); + if (!fns) throw err; + fns.forEach((f: (err: any) => void) => f(err)); + } finally { + Owner = owner; + } + return result!; +} + +export function createSignal( + value?: T, + areEqual?: boolean | ((prev: T, next: T) => boolean) +): [() => T, (v: T) => T] { + return [() => value as T, (v: T) => (value = v)]; +} + +export function createComputed(fn: (v?: T) => T, value?: T): void { + Owner = { owner: Owner, context: null }; + fn(value); + Owner = Owner.owner; +} + +export const createRenderEffect = createComputed; + +export function createEffect(fn: (v?: T) => T, value?: T): void {} + +export function createMemo( + fn: (v?: T) => T, + value?: T, + areEqual?: boolean | ((prev: T, next: T) => boolean) +): () => T { + Owner = { owner: Owner, context: null }; + const v = fn(value); + Owner = Owner.owner; + return () => v; +} + +export function createDeferred(source: () => T, options?: { timeoutMs: number }) { + return source; +} + +export function createSelector( + source: () => T, + fn: (k: T, value: T, prevValue: T | undefined) => boolean +) { + return source; +} + +export function batch(fn: () => T): T { + return fn(); +} + +export function untrack(fn: () => T): T { + return fn(); +} + +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; + let prev: T | T[]; + if (args.length < 2) { + deps = args[0] as () => T; + isArray = false; + } else deps = args as Array<() => T>; + return prevResult => { + let value: T | Array; + if (isArray) { + value = []; + if (!prev) prev = []; + for (let i = 0; i < deps.length; i++) value.push((deps as Array<() => T>)[i]()); + } else value = (deps as () => T)(); + return fn!(value, prev, prevResult); + }; +} + +export function onMount(fn: () => void) {} + +export function onCleanup(fn: () => void) {} + +export function onError(fn: (err: any) => void): void { + if (Owner === null) + "_SOLID_DEV_" && + 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); +} + +export function getListener() { + return null; +} + +// Context API +export interface Context { + id: symbol; + Provider: (props: { value: T; children: any }) => any; + defaultValue?: T; +} + +export function createContext(defaultValue?: T): Context { + const id = Symbol("context"); + return { id, Provider: createProvider(id), defaultValue }; +} + +export function useContext(context: Context): T { + return lookup(Owner, context.id) || context.defaultValue; +} + +export function getOwner() { + return Owner; +} + +export function children(fn: () => any) { + return resolveChildren(fn()) +} + +export function runWithOwner(o: Owner, fn: () => any) { + const prev = Owner; + Owner = o; + try { + return fn(); + } finally { + Owner = prev; + } +} + +export function lookup(owner: Owner | null, key: symbol | string): any { + return ( + owner && ((owner.context && owner.context[key]) || (owner.owner && lookup(owner.owner, key))) + ); +} + +function resolveChildren(children: any): any { + if (typeof children === "function") return resolveChildren(children()); + if (Array.isArray(children)) { + const results: any[] = []; + 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: symbol) { + return function provider(props: { value: unknown; children: any }) { + let rendered; + createRenderEffect(() => { + Owner!.context = { [id]: props.value }; + rendered = resolveChildren(props.children); + }); + return rendered; + }; +} + +export interface Task { + id: number; + fn: ((didTimeout: boolean) => void) | null; + startTime: number; + expirationTime: number; +} +export function requestCallback(fn: () => void, options?: { timeout: number }): Task { + return { id: 0, fn: () => {}, startTime: 0, expirationTime: 0 }; +} +export function cancelCallback(task: Task) {} + +export const $RAW = Symbol("state-raw"); + +// well-known symbols need special treatment until https://github.com/microsoft/TypeScript/issues/24622 is implemented. +type AddSymbolToPrimitive = T extends { [Symbol.toPrimitive]: infer V } + ? { [Symbol.toPrimitive]: V } + : {}; +type AddCallable = T extends { (...x: any[]): infer V } ? { (...x: Parameters): V } : {}; + +type NotWrappable = string | number | boolean | Function | null; +export type State = { + [P in keyof T]: T[P] extends object ? State : T[P]; +} & { + [$RAW]?: T; +} & AddSymbolToPrimitive & + AddCallable; + +export function isWrappable(obj: any) { + return ( + obj != null && + typeof obj === "object" && + (obj.__proto__ === Object.prototype || Array.isArray(obj)) + ); +} + +export function unwrap(item: any): T { + return item; +} + +export function setProperty(state: any, property: string | number, value: any, force?: boolean) { + if (!force && state[property] === value) return; + if (value === undefined) { + delete state[property]; + } else state[property] = value; +} + +function mergeState(state: any, value: any, force?: boolean) { + const keys = Object.keys(value); + for (let i = 0; i < keys.length; i += 1) { + const key = keys[i]; + setProperty(state, key, value[key], force); + } +} + +export function updatePath(current: any, path: any[], traversed: (number | string)[] = []) { + let part, + next = current; + if (path.length > 1) { + part = path.shift(); + const partType = typeof part, + isArray = Array.isArray(current); + + if (Array.isArray(part)) { + // Ex. update('data', [2, 23], 'label', l => l + ' !!!'); + for (let i = 0; i < part.length; i++) { + updatePath(current, [part[i]].concat(path), [part[i]].concat(traversed)); + } + return; + } else if (isArray && partType === "function") { + // Ex. update('data', i => i.id === 42, 'label', l => l + ' !!!'); + for (let i = 0; i < current.length; i++) { + if (part(current[i], i)) + updatePath(current, [i].concat(path), ([i] as (number | string)[]).concat(traversed)); + } + return; + } else if (isArray && partType === "object") { + // Ex. update('data', { from: 3, to: 12, by: 2 }, 'label', l => l + ' !!!'); + const { from = 0, to = current.length - 1, by = 1 } = part; + for (let i = from; i <= to; i += by) { + updatePath(current, [i].concat(path), ([i] as (number | string)[]).concat(traversed)); + } + return; + } else if (path.length > 1) { + updatePath(current[part], path, [part].concat(traversed)); + return; + } + next = current[part]; + traversed = [part].concat(traversed); + } + let value = path[0]; + if (typeof value === "function") { + value = value(next, traversed); + if (value === next) return; + } + if (part === undefined && value == undefined) return; + if (part === undefined || (isWrappable(next) && isWrappable(value) && !Array.isArray(value))) { + mergeState(next, value); + } else setProperty(current, part, value); +} + +type StateSetter = + | Partial + | (( + prevState: T extends NotWrappable ? T : State, + traversed?: (string | number)[] + ) => Partial | void); +type StatePathRange = { from?: number; to?: number; by?: number }; + +type ArrayFilterFn = (item: T extends any[] ? T[number] : never, index: number) => boolean; + +type Part = keyof T | Array | StatePathRange | ArrayFilterFn; // changing this to "T extends any[] ? ArrayFilterFn : never" results in depth limit errors + +type Next = K extends keyof T + ? T[K] + : K extends Array + ? T[K[number]] + : T extends any[] + ? K extends StatePathRange + ? T[number] + : K extends ArrayFilterFn + ? T[number] + : never + : never; + +export interface SetStateFunction { + >(...args: [Setter]): void; + , Setter extends StateSetter>>(...args: [K1, Setter]): void; + < + K1 extends Part, + K2 extends Part>, + Setter extends StateSetter, K2>> + >( + ...args: [K1, K2, Setter] + ): void; + < + K1 extends Part, + K2 extends Part>, + K3 extends Part, K2>>, + Setter extends StateSetter, K2>, K3>> + >( + ...args: [K1, K2, K3, Setter] + ): void; + < + K1 extends Part, + K2 extends Part>, + K3 extends Part, K2>>, + K4 extends Part, K2>, K3>>, + Setter extends StateSetter, K2>, K3>, K4>> + >( + ...args: [K1, K2, K3, K4, Setter] + ): void; + < + K1 extends Part, + K2 extends Part>, + K3 extends Part, K2>>, + K4 extends Part, K2>, K3>>, + K5 extends Part, K2>, K3>, K4>>, + Setter extends StateSetter, K2>, K3>, K4>, K5>> + >( + ...args: [K1, K2, K3, K4, K5, Setter] + ): void; + < + K1 extends Part, + K2 extends Part>, + K3 extends Part, K2>>, + K4 extends Part, K2>, K3>>, + K5 extends Part, K2>, K3>, K4>>, + K6 extends Part, K2>, K3>, K4>, K5>>, + Setter extends StateSetter, K2>, K3>, K4>, K5>, K6>> + >( + ...args: [K1, K2, K3, K4, K5, K6, Setter] + ): void; + < + K1 extends Part, + K2 extends Part>, + K3 extends Part, K2>>, + K4 extends Part, K2>, K3>>, + K5 extends Part, K2>, K3>, K4>>, + K6 extends Part, K2>, K3>, K4>, K5>>, + K7 extends Part, K2>, K3>, K4>, K5>, K6>>, + Setter extends StateSetter< + Next, K2>, K3>, K4>, K5>, K6>, K7> + > + >( + ...args: [K1, K2, K3, K4, K5, K6, K7, Setter] + ): void; + + // and here we give up on being accurate after 8 args + < + K1 extends Part, + K2 extends Part>, + K3 extends Part, K2>>, + K4 extends Part, K2>, K3>>, + K5 extends Part, K2>, K3>, K4>>, + K6 extends Part, K2>, K3>, K4>, K5>>, + K7 extends Part, K2>, K3>, K4>, K5>, K6>>, + K8 extends Part, K2>, K3>, K4>, K5>, K6>, K7>> + >( + ...args: [K1, K2, K3, K4, K5, K6, K7, K8, ...(Part | StateSetter)[]] + ): void; +} + +export function createState(state: T | State): [State, SetStateFunction] { + function setState(...args: any[]): void { + updatePath(state, args); + } + return [state as State, setState]; +} + +type ReconcileOptions = { + key?: string | null; + merge?: boolean; +}; + +// Diff method for setState +export function reconcile( + value: T | State, + options: ReconcileOptions = {} +): (state: T extends NotWrappable ? T : State) => void { + return state => { + if (!isWrappable(state)) return value; + const targetKeys = Object.keys(value) as (keyof T)[]; + for (let i = 0, len = targetKeys.length; i < len; i++) { + const key = targetKeys[i]; + setProperty(state, key as string, value[key]); + } + const previousKeys = Object.keys(state) as (keyof T)[]; + for (let i = 0, len = previousKeys.length; i < len; i++) { + if (value[previousKeys[i]] === undefined) + setProperty(state, previousKeys[i] as string, undefined); + } + }; +} + +// Immer style mutation style +export function produce( + fn: (state: T) => void +): (state: T extends NotWrappable ? T : State) => T extends NotWrappable ? T : State { + return state => { + if (isWrappable(state)) fn(state as T); + return state; + }; +} + +export function mapArray( + list: () => T[], + mapFn: (v: T, i: () => number) => U, + options: { fallback?: () => any } = {} +): () => U[] { + const items = list(); + let s: U[] = []; + if (items.length) { + for (let i = 0, len = items.length; i < len; i++) s.push(mapFn(items[i], () => i)); + } else if (options.fallback) s = [options.fallback()]; + return () => s; +} 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) + }) +})