// Inspired by S.js by Adam Haile, https://github.com/adamhaile/S import { requestCallback, Task } from "./scheduler.js"; import { setHydrateContext, sharedConfig } from "../render/hydration.js"; import type { JSX } from "../jsx.js"; import type { FlowComponent, FlowProps } from "../render/index.js"; export const equalFn = (a: T, b: T) => a === b; export const $PROXY = Symbol("solid-proxy"); export const $TRACK = Symbol("solid-track"); export const $DEVCOMP = Symbol("solid-dev-component"); const signalOptions = { equals: equalFn }; let ERROR: symbol | null = null; let runEffects = runQueue; const STALE = 1; const PENDING = 2; const UNOWNED: Owner = { owned: null, cleanups: null, context: null, owner: null }; const NO_INIT = {}; export var Owner: Owner | null = null; export let Transition: TransitionState | null = null; let Scheduler: ((fn: () => void) => any) | null = null; let ExternalSourceFactory: ExternalSourceFactory | null = null; let Listener: Computation | null = null; let Updates: Computation[] | null = null; let Effects: Computation[] | null = null; let ExecCount = 0; let rootCount = 0; // keep immediately evaluated module code, below its indirect declared let dependencies like Listener const [transPending, setTransPending] = /*@__PURE__*/ createSignal(false); declare global { var _$afterUpdate: () => void; var _$afterCreateRoot: (root: Owner) => void; } export interface SignalState { value?: T; observers: Computation[] | null; observerSlots: number[] | null; tValue?: T; comparator?: (prev: T, next: T) => boolean; name?: string; } export interface Owner { owned: Computation[] | null; cleanups: (() => void)[] | null; owner: Owner | null; context: any | null; sourceMap?: Record; name?: string; componentName?: string; } export interface Computation extends Owner { fn: EffectFunction; state: number; tState?: number; sources: SignalState[] | null; sourceSlots: number[] | null; value?: Init; updatedAt: number | null; pure: boolean; user?: boolean; suspense?: SuspenseContextType; } export interface TransitionState { sources: Set>; effects: Computation[]; promises: Set>; disposed: Set>; queue: Set>; scheduler?: (fn: () => void) => unknown; running: boolean; done?: Promise; resolve?: () => void; } type ExternalSourceFactory = ( fn: EffectFunction, trigger: () => void ) => ExternalSource; export interface ExternalSource { track: EffectFunction; dispose: () => void; } export type RootFunction = (dispose: () => void) => T; /** * Creates a new non-tracked reactive context that doesn't auto-dispose * * @param fn a function in which the reactive state is scoped * @param detachedOwner optional reactive context to bind the root to * @returns the output of `fn`. * * @description https://www.solidjs.com/docs/latest/api#createroot */ export function createRoot(fn: RootFunction, detachedOwner?: Owner): T { const listener = Listener, owner = Owner, unowned = fn.length === 0, root: Owner = unowned && !"_SOLID_DEV_" ? UNOWNED : { owned: null, cleanups: null, context: null, owner: detachedOwner || owner }, updateFn = unowned ? "_SOLID_DEV_" ? () => fn(() => { throw new Error("Dispose method must be an explicit argument to createRoot function"); }) : fn : () => fn(() => untrack(() => cleanNode(root))); if ("_SOLID_DEV_") { if (owner) root.name = `${owner.name}-r${rootCount++}`; globalThis._$afterCreateRoot && globalThis._$afterCreateRoot(root); } Owner = root; Listener = null; try { return runUpdates(updateFn as () => T, true)!; } finally { Listener = listener; Owner = owner; } } export type Accessor = () => T; export type Setter = (undefined extends T ? () => undefined : {}) & ((value: (prev: T) => U) => U) & ((value: Exclude) => U) & ((value: Exclude | ((prev: T) => U)) => U); export type Signal = [get: Accessor, set: Setter]; export interface SignalOptions extends MemoOptions { internal?: boolean; } /** * Creates a simple reactive state with a getter and setter * ```typescript * const [state: Accessor, setState: Setter] = createSignal( * value: T, * options?: { name?: string, equals?: false | ((prev: T, next: T) => boolean) } * ) * ``` * @param value initial value of the state; if empty, the state's type will automatically extended with undefined; otherwise you need to extend the type manually if you want setting to undefined not be an error * @param options optional object with a name for debugging purposes and equals, a comparator function for the previous and next value to allow fine-grained control over the reactivity * * @returns ```typescript * [state: Accessor, setState: Setter] * ``` * * the Accessor is merely a function that returns the current value and registers each call to the reactive root * * the Setter is a function that allows directly setting or mutating the value: * ```typescript * const [count, setCount] = createSignal(0); * setCount(count => count + 1); * ``` * * @description https://www.solidjs.com/docs/latest/api#createsignal */ export function createSignal(): Signal; export function createSignal(value: T, options?: SignalOptions): Signal; export function createSignal(value?: T, options?: SignalOptions): Signal { options = options ? Object.assign({}, signalOptions, options) : signalOptions; const s: SignalState = { value, observers: null, observerSlots: null, comparator: options.equals || undefined }; if ("_SOLID_DEV_" && !options.internal) s.name = registerGraph(options.name || hashValue(value), s as { value: unknown }); const setter: Setter = (value?: unknown) => { if (typeof value === "function") { if (Transition && Transition.running && Transition.sources.has(s)) value = value(s.tValue); else value = value(s.value); } return writeSignal(s, value); }; return [readSignal.bind(s), setter]; } export interface BaseOptions { name?: string; } // Magic type that when used at sites where generic types are inferred from, will prevent those sites from being involved in the inference. // https://github.com/microsoft/TypeScript/issues/14829 // TypeScript Discord conversation: https://discord.com/channels/508357248330760243/508357248330760249/911266491024949328 export type NoInfer = [T][T extends any ? 0 : never]; export interface EffectOptions extends BaseOptions {} // Also similar to OnEffectFunction export type EffectFunction = (v: Prev) => Next; /** * Creates a reactive computation that runs immediately before render, mainly used to write to other reactive primitives * ```typescript * export function createComputed( * fn: (v: Init | Next) => Next, * value?: Init, * options?: { name?: string } * ): void; * ``` * @param fn a function that receives its previous or the initial value, if set, and returns a new value used to react on a computation * @param value an optional initial value for the computation; if set, fn will never receive undefined as first argument * @param options allows to set a name in dev mode for debugging purposes * * @description https://www.solidjs.com/docs/latest/api#createcomputed */ export function createComputed(fn: EffectFunction, Next>): void; export function createComputed( fn: EffectFunction, value: Init, options?: EffectOptions ): void; export function createComputed( fn: EffectFunction, value?: Init, options?: EffectOptions ): void { const c = createComputation(fn, value!, true, STALE, "_SOLID_DEV_" ? options : undefined); if (Scheduler && Transition && Transition.running) Updates!.push(c); else updateComputation(c); } /** * Creates a reactive computation that runs during the render phase as DOM elements are created and updated but not necessarily connected * ```typescript * export function createRenderEffect( * fn: (v: T) => T, * value?: T, * options?: { name?: string } * ): void; * ``` * @param fn a function that receives its previous or the initial value, if set, and returns a new value used to react on a computation * @param value an optional initial value for the computation; if set, fn will never receive undefined as first argument * @param options allows to set a name in dev mode for debugging purposes * * @description https://www.solidjs.com/docs/latest/api#createrendereffect */ export function createRenderEffect(fn: EffectFunction, Next>): void; export function createRenderEffect( fn: EffectFunction, value: Init, options?: EffectOptions ): void; export function createRenderEffect( fn: EffectFunction, value?: Init, options?: EffectOptions ): void { const c = createComputation(fn, value!, false, STALE, "_SOLID_DEV_" ? options : undefined); if (Scheduler && Transition && Transition.running) Updates!.push(c); else updateComputation(c); } /** * Creates a reactive computation that runs after the render phase * ```typescript * export function createEffect( * fn: (v: T) => T, * value?: T, * options?: { name?: string } * ): void; * ``` * @param fn a function that receives its previous or the initial value, if set, and returns a new value used to react on a computation * @param value an optional initial value for the computation; if set, fn will never receive undefined as first argument * @param options allows to set a name in dev mode for debugging purposes * * @description https://www.solidjs.com/docs/latest/api#createeffect */ export function createEffect(fn: EffectFunction, Next>): void; export function createEffect( fn: EffectFunction, value: Init, options?: EffectOptions ): void; export function createEffect( fn: EffectFunction, value?: Init, options?: EffectOptions ): void { runEffects = runUserEffects; const c = createComputation(fn, value!, false, STALE, "_SOLID_DEV_" ? options : undefined), s = SuspenseContext && lookup(Owner, SuspenseContext.id); if (s) c.suspense = s; c.user = true; Effects ? Effects.push(c) : updateComputation(c); } /** * Creates a reactive computation that runs after the render phase with flexible tracking * ```typescript * export function createReaction( * onInvalidate: () => void, * options?: { name?: string } * ): (fn: () => void) => void; * ``` * @param invalidated a function that is called when tracked function is invalidated. * @param options allows to set a name in dev mode for debugging purposes * * @description https://www.solidjs.com/docs/latest/api#createreaction */ export function createReaction(onInvalidate: () => void, options?: EffectOptions) { let fn: (() => void) | undefined; const c = createComputation( () => { fn ? fn() : untrack(onInvalidate); fn = undefined; }, undefined, false, 0, "_SOLID_DEV_" ? options : undefined ), s = SuspenseContext && lookup(Owner, SuspenseContext.id); if (s) c.suspense = s; c.user = true; return (tracking: () => void) => { fn = tracking; updateComputation(c); }; } interface Memo extends SignalState, Computation { tOwned?: Computation[]; } export interface MemoOptions extends EffectOptions { equals?: false | ((prev: T, next: T) => boolean); } /** * Creates a readonly derived reactive memoized signal * ```typescript * export function createMemo( * fn: (v: T) => T, * value?: T, * options?: { name?: string, equals?: false | ((prev: T, next: T) => boolean) } * ): () => T; * ``` * @param fn a function that receives its previous or the initial value, if set, and returns a new value used to react on a computation * @param value an optional initial value for the computation; if set, fn will never receive undefined as first argument * @param options allows to set a name in dev mode for debugging purposes and use a custom comparison function in equals * * @description https://www.solidjs.com/docs/latest/api#creatememo */ // The extra Prev generic parameter separates inference of the effect input // parameter type from inference of the effect return type, so that the effect // return type is always used as the memo Accessor's return type. export function createMemo( fn: EffectFunction, Next> ): Accessor; export function createMemo( fn: EffectFunction, value: Init, options?: MemoOptions ): Accessor; export function createMemo( fn: EffectFunction, value?: Init, options?: MemoOptions ): Accessor { options = options ? Object.assign({}, signalOptions, options) : signalOptions; const c: Partial> = createComputation( fn, value!, true, 0, "_SOLID_DEV_" ? options : undefined ) as Partial>; c.observers = null; c.observerSlots = null; c.comparator = options.equals || undefined; if (Scheduler && Transition && Transition.running) { c.tState = STALE; Updates!.push(c as Memo); } else updateComputation(c as Memo); return readSignal.bind(c as Memo); } interface Unresolved { state: "unresolved"; loading: false; error: undefined; latest: undefined; (): undefined; } interface Pending { state: "pending"; loading: true; error: undefined; latest: undefined; (): undefined; } interface Ready { state: "ready"; loading: false; error: undefined; latest: T; (): T; } interface Refreshing { state: "refreshing"; loading: true; error: undefined; latest: T; (): T; } interface Errored { state: "errored"; loading: false; error: any; latest: never; (): never; } export type Resource = Unresolved | Pending | Ready | Refreshing | Errored; export type InitializedResource = Ready | Refreshing | Errored; export type ResourceActions = { mutate: Setter; refetch: (info?: R) => T | Promise | undefined | null; }; export type ResourceSource = S | false | null | undefined | (() => S | false | null | undefined); export type ResourceFetcher = ( k: S, info: ResourceFetcherInfo ) => T | Promise; export type ResourceFetcherInfo = { value: T | undefined; refetching: R | boolean; }; export type ResourceOptions = { initialValue?: T; name?: string; deferStream?: boolean; ssrLoadFrom?: "initial" | "server"; storage?: (init: T | undefined) => [Accessor, Setter]; onHydrated?: (k: S | undefined, info: { value: T | undefined }) => void; }; export type InitializedResourceOptions = ResourceOptions & { initialValue: T; }; export type ResourceReturn = [Resource, ResourceActions]; export type InitializedResourceReturn = [ InitializedResource, ResourceActions ]; /** * Creates a resource that wraps a repeated promise in a reactive pattern: * ```typescript * // Without source * const [resource, { mutate, refetch }] = createResource(fetcher, options); * // With source * const [resource, { mutate, refetch }] = createResource(source, fetcher, options); * ``` * @param source - reactive data function which has its non-nullish and non-false values passed to the fetcher, optional * @param fetcher - function that receives the source (true if source not provided), the last or initial value, and whether the resource is being refetched, and returns a value or a Promise: * ```typescript * const fetcher: ResourceFetcher = ( * sourceOutput: S, * info: { value: T | undefined, refetching: R | boolean } * ) => T | Promise; * ``` * @param options - an optional object with the initialValue and the name (for debugging purposes); see {@link ResourceOptions} * * @returns ```typescript * [Resource, { mutate: Setter, refetch: () => void }] * ``` * * * Setting an `initialValue` in the options will mean that both the prev() accessor and the resource should never return undefined (if that is wanted, you need to extend the type with undefined) * * `mutate` allows to manually overwrite the resource without calling the fetcher * * `refetch` will re-run the fetcher without changing the source, and if called with a value, that value will be passed to the fetcher via the `refetching` property on the fetcher's second parameter * * @description https://www.solidjs.com/docs/latest/api#createresource */ export function createResource( fetcher: ResourceFetcher, options: InitializedResourceOptions, true> ): InitializedResourceReturn; export function createResource( fetcher: ResourceFetcher, options?: ResourceOptions, true> ): ResourceReturn; export function createResource( source: ResourceSource, fetcher: ResourceFetcher, options: InitializedResourceOptions, S> ): InitializedResourceReturn; export function createResource( source: ResourceSource, fetcher: ResourceFetcher, options?: ResourceOptions, S> ): ResourceReturn; export function createResource( pSource: ResourceSource | ResourceFetcher, pFetcher?: ResourceFetcher | ResourceOptions, pOptions?: ResourceOptions | undefined ): ResourceReturn { let source: ResourceSource; let fetcher: ResourceFetcher; let options: ResourceOptions; if ((arguments.length === 2 && typeof pFetcher === "object") || arguments.length === 1) { source = true as ResourceSource; fetcher = pSource as ResourceFetcher; options = (pFetcher || {}) as ResourceOptions; } else { source = pSource as ResourceSource; fetcher = pFetcher as ResourceFetcher; options = pOptions || ({} as ResourceOptions); } let pr: Promise | null = null, initP: Promise | T | typeof NO_INIT = NO_INIT, id: string | null = null, loadedUnderTransition: boolean | null = false, scheduled = false, resolved = "initialValue" in options, dynamic = typeof source === "function" && createMemo(source as () => S | false | null | undefined); const contexts = new Set(), [value, setValue] = (options.storage || createSignal)(options.initialValue) as Signal< T | undefined >, [error, setError] = createSignal(undefined), [track, trigger] = createSignal(undefined, { equals: false }), [state, setState] = createSignal<"unresolved" | "pending" | "ready" | "refreshing" | "errored">( resolved ? "ready" : "unresolved" ); if (sharedConfig.context) { id = `${sharedConfig.context.id}${sharedConfig.context.count++}`; let v; if (options.ssrLoadFrom === "initial") initP = options.initialValue as T; else if (sharedConfig.load && (v = sharedConfig.load(id))) initP = v[0]; } function loadEnd(p: Promise | null, v: T | undefined, error?: any, key?: S) { if (pr === p) { pr = null; resolved = true; if ((p === initP || v === initP) && options.onHydrated) queueMicrotask(() => options.onHydrated!(key, { value: v })); initP = NO_INIT; if (Transition && p && loadedUnderTransition) { Transition.promises.delete(p); loadedUnderTransition = false; runUpdates(() => { Transition!.running = true; if (!Transition!.promises.size) { Effects!.push.apply(Effects, Transition!.effects); Transition!.effects = []; } completeLoad(v, error); }, false); } else completeLoad(v, error); } return v; } function completeLoad(v: T | undefined, err: any) { runUpdates(() => { if (!err) setValue(() => v); setError(err); setState(err ? "errored" : "ready"); for (const c of contexts.keys()) c.decrement!(); contexts.clear(); }, false); } function read() { const c = SuspenseContext && lookup(Owner, SuspenseContext.id), v = value(), err = error(); if (err && !pr) throw err; if (Listener && !Listener.user && c) { createComputed(() => { track(); if (pr) { if (c.resolved && Transition) Transition.promises.add(pr); else if (!contexts.has(c)) { c.increment(); contexts.add(c); } } }); } return v; } function load(refetching: R | boolean = true) { if (refetching !== false && scheduled) return; scheduled = false; const lookup = dynamic ? dynamic() : (source as S); loadedUnderTransition = Transition && Transition.running; if (lookup == null || lookup === false) { loadEnd(pr, untrack(value)); return; } if (Transition && pr) Transition.promises.delete(pr); const p = initP !== NO_INIT ? (initP as T | Promise) : untrack(() => fetcher(lookup, { value: value(), refetching }) ); if (typeof p !== "object" || !(p && "then" in p)) { loadEnd(pr, p); return p; } pr = p; scheduled = true; queueMicrotask(() => (scheduled = false)); runUpdates(() => { setState(resolved ? "refreshing" : "pending"); trigger(); }, false); return p.then( v => loadEnd(p, v, undefined, lookup), e => loadEnd(p, undefined, castError(e)) ) as Promise; } Object.defineProperties(read, { state: { get: () => state() }, error: { get: () => error() }, loading: { get() { const s = state(); return s === "pending" || s === "refreshing"; } }, latest: { get() { if (!resolved) return read(); const err = error(); if (err && !pr) throw err; return value(); } } }); if (dynamic) createComputed(() => load(false)); else load(false); return [read as Resource, { refetch: load, mutate: setValue }]; } export interface DeferredOptions { equals?: false | ((prev: T, next: T) => boolean); name?: string; timeoutMs?: number; } /** * Creates a reactive computation that only runs and notifies the reactive context when the browser is idle * ```typescript * export function createDeferred( * fn: (v: T) => T, * options?: { timeoutMs?: number, name?: string, equals?: false | ((prev: T, next: T) => boolean) } * ): () => T); * ``` * @param fn a function that receives its previous or the initial value, if set, and returns a new value used to react on a computation * @param options allows to set the timeout in milliseconds, use a custom comparison function and set a name in dev mode for debugging purposes * * @description https://www.solidjs.com/docs/latest/api#createdeferred */ export function createDeferred(source: Accessor, options?: DeferredOptions) { let t: Task, timeout = options ? options.timeoutMs : undefined; const node = createComputation( () => { if (!t || !t.fn) t = requestCallback( () => setDeferred(() => node.value as T), timeout !== undefined ? { timeout } : undefined ); return source(); }, undefined, true ); const [deferred, setDeferred] = createSignal(node.value as T, options); updateComputation(node); setDeferred(() => node.value as T); return deferred; } export type EqualityCheckerFunction = (a: U, b: T) => boolean; /** * Creates a conditional signal that only notifies subscribers when entering or exiting their key matching the value * ```typescript * export function createSelector( * source: () => T * fn: (a: U, b: T) => boolean, * options?: { name?: string } * ): (k: U) => boolean; * ``` * @param source * @param fn a function that receives its previous or the initial value, if set, and returns a new value used to react on a computation * @param options allows to set a name in dev mode for debugging purposes, optional * * ```typescript * const isSelected = createSelector(selectedId); * * {(item) =>
  • {item.name}
  • } *
    * ``` * * This makes the operation O(2) instead of O(n). * * @description https://www.solidjs.com/docs/latest/api#createselector */ export function createSelector( source: Accessor, fn: EqualityCheckerFunction = equalFn as TODO, options?: BaseOptions ): (key: U) => boolean { const subs = new Map>>(); const node = createComputation( (p: T | undefined) => { const v = source(); for (const [key, val] of subs.entries()) if (fn(key, v) !== fn(key, p!)) { for (const c of val.values()) { c.state = STALE; if (c.pure) Updates!.push(c); else Effects!.push(c); } } return v; }, undefined, true, STALE, "_SOLID_DEV_" ? options : undefined ) as Memo; updateComputation(node); return (key: U) => { const listener = Listener; if (listener) { let l: Set> | undefined; if ((l = subs.get(key))) l.add(listener); else subs.set(key, (l = new Set([listener]))); onCleanup(() => { l!.delete(listener!); !l!.size && subs.delete(key); }); } return fn( key, Transition && Transition.running && Transition.sources.has(node) ? node.tValue : node.value! ); }; } /** * Holds changes inside the block before the reactive context is updated * @param fn wraps the reactive updates that should be batched * @returns the return value from `fn` * * @description https://www.solidjs.com/docs/latest/api#batch */ export function batch(fn: Accessor): T { return runUpdates(fn, false) as T; } /** * Ignores tracking context inside its scope * @param fn the scope that is out of the tracking context * @returns the return value of `fn` * * @description https://www.solidjs.com/docs/latest/api#untrack */ export function untrack(fn: Accessor): T { let result: T, listener = Listener; Listener = null; result = fn(); Listener = listener; return result; } /** @deprecated */ export type ReturnTypes = T extends readonly Accessor[] ? { [K in keyof T]: T[K] extends Accessor ? I : never } : T extends Accessor ? I : never; // transforms a tuple to a tuple of accessors in a way that allows generics to be inferred export type AccessorArray = [...Extract<{ [K in keyof T]: Accessor }, readonly unknown[]>]; // Also similar to EffectFunction export type OnEffectFunction = ( input: S, prevInput: S | undefined, prev: Prev ) => Next; export interface OnOptions { defer?: boolean; } /** * on - make dependencies of a computation explicit * ```typescript * export function on( * deps: Accessor | AccessorArray, * fn: (input: S, prevInput: S | undefined, prevValue: U | undefined) => U, * options?: { defer?: boolean } = {} * ): (prevValue: U | undefined) => U; * ``` * @param deps list of reactive dependencies or a single reactive dependency * @param fn computation on input; the current previous content(s) of input and the previous value are given as arguments and it returns a new value * @param options optional, allows deferred computation until at the end of the next change * @returns an effect function that is passed into createEffect. For example: * * ```typescript * createEffect(on(a, (v) => console.log(v, b()))); * * // is equivalent to: * createEffect(() => { * const v = a(); * untrack(() => console.log(v, b())); * }); * ``` * * @description https://www.solidjs.com/docs/latest/api#on */ export function on( deps: AccessorArray | Accessor, fn: OnEffectFunction, Next>, options?: OnOptions & { defer?: false } ): EffectFunction, NoInfer>; export function on( deps: AccessorArray | Accessor, fn: OnEffectFunction, Next>, options: OnOptions & { defer: true } ): EffectFunction>; export function on( deps: AccessorArray | Accessor, fn: OnEffectFunction, Next>, options?: OnOptions ): EffectFunction> { const isArray = Array.isArray(deps); let prevInput: S; let defer = options && options.defer; return prevValue => { let input: S; if (isArray) { input = Array(deps.length) as unknown as S; for (let i = 0; i < deps.length; i++) (input as unknown as TODO[])[i] = deps[i](); } else input = deps(); if (defer) { defer = false; return undefined; } const result = untrack(() => fn(input, prevInput, prevValue)); prevInput = input; return result; }; } /** * onMount - run an effect only after initial render on mount * @param fn an effect that should run only once on mount * * @description https://www.solidjs.com/docs/latest/api#onmount */ export function onMount(fn: () => void) { createEffect(() => untrack(fn)); } /** * onCleanup - run an effect once before the reactive scope is disposed * @param fn an effect that should run only once on cleanup * * @description https://www.solidjs.com/docs/latest/api#oncleanup */ export function onCleanup(fn: () => void) { if (Owner === null) "_SOLID_DEV_" && 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); return fn; } /** * onError - run an effect whenever an error is thrown within the context of the child scopes * @param fn an error handler that receives the error * * * If the error is thrown again inside the error handler, it will trigger the next available parent handler * * @description https://www.solidjs.com/docs/latest/api#onerror */ export function onError(fn: (err: any) => void): void { ERROR || (ERROR = Symbol("error")); 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 Listener; } export function getOwner() { return Owner; } export function runWithOwner(o: Owner, fn: () => T): T { const prev = Owner; Owner = o; try { return runUpdates(fn, true)!; } finally { Owner = prev; } } // Transitions export function enableScheduling(scheduler = requestCallback) { Scheduler = scheduler; } /** * ```typescript * export function startTransition(fn: () => void) => Promise * * @description https://www.solidjs.com/docs/latest/api#usetransition */ export function startTransition(fn: () => unknown): Promise { if (Transition && Transition.running) { fn(); return Transition.done!; } const l = Listener; const o = Owner; return Promise.resolve().then(() => { Listener = l; Owner = o; let t: TransitionState | undefined; if (Scheduler || SuspenseContext) { t = Transition || (Transition = { sources: new Set(), effects: [], promises: new Set(), disposed: new Set(), queue: new Set(), running: true }); t.done || (t.done = new Promise(res => (t!.resolve = res))); t.running = true; } runUpdates(fn, false); Listener = Owner = null; return t ? t.done : undefined; }); } export type Transition = [Accessor, (fn: () => void) => Promise]; /** * ```typescript * export function useTransition(): [ * () => boolean, * (fn: () => void, cb?: () => void) => void * ]; * @returns a tuple; first value is an accessor if the transition is pending and a callback to start the transition * * @description https://www.solidjs.com/docs/latest/api#usetransition */ export function useTransition(): Transition { return [transPending, startTransition]; } export function resumeEffects(e: Computation[]) { Effects!.push.apply(Effects, e); e.length = 0; } export interface DevComponent extends Memo { props: T; componentName: string; } // Dev export function devComponent(Comp: (props: T) => JSX.Element, props: T) { const c = createComputation( () => untrack(() => { Object.assign(Comp, { [$DEVCOMP]: true }); return Comp(props); }), undefined, true ) as DevComponent; c.props = props; c.observers = null; c.observerSlots = null; c.state = 0; c.componentName = Comp.name; updateComputation(c); return c.tValue !== undefined ? c.tValue : c.value; } export function hashValue(v: any): string { const s = new Set(); return `s${ typeof v === "string" ? hash(v) : hash( untrack( () => JSON.stringify(v, (k, v) => { if (typeof v === "object" && v != null) { if (s.has(v)) return; s.add(v); const keys = Object.keys(v); const desc = Object.getOwnPropertyDescriptors(v); const newDesc = keys.reduce((memo, key) => { const value = desc[key]; // skip getters if (!value.get) memo[key] = value; return memo; }, {} as any); v = Object.create({}, newDesc); } if (typeof v === "bigint") { return `${v.toString()}n`; } return v; }) || "" ) ) }`; } export function registerGraph(name: string, value: { value: unknown }): string { let tryName = name; if (Owner) { let i = 0; Owner.sourceMap || (Owner.sourceMap = {}); while (Owner.sourceMap[tryName]) tryName = `${name}-${++i}`; Owner.sourceMap[tryName] = value; } return tryName; } interface GraphRecord { [k: string]: GraphRecord | unknown; } export function serializeGraph(owner?: Owner | null): GraphRecord { owner || (owner = Owner); if (!"_SOLID_DEV_" || !owner) return {}; return { ...serializeValues(owner.sourceMap), ...(owner.owned ? serializeChildren(owner) : {}) }; } export type ContextProviderComponent = FlowComponent<{ value: T }>; // Context API export interface Context { id: symbol; Provider: ContextProviderComponent; defaultValue: T; } /** * Creates a Context to handle a state scoped for the children of a component * ```typescript * interface Context { * id: symbol; * Provider: FlowComponent<{ value: T }>; * defaultValue: T; * } * export function createContext(defaultValue?: T): Context; * ``` * @param defaultValue optional default to inject into context * @returns The context that contains the Provider Component and that can be used with `useContext` * * @description https://www.solidjs.com/docs/latest/api#createcontext */ 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 }; } /** * use a context to receive a scoped state from a parent's Context.Provider * * @param context Context object made by `createContext` * @returns the current or `defaultValue`, if present * * @description https://www.solidjs.com/docs/latest/api#usecontext */ export function useContext(context: Context): T { let ctx; return (ctx = lookup(Owner, context.id)) !== undefined ? ctx : context.defaultValue; } export type ResolvedJSXElement = Exclude; export type ResolvedChildren = ResolvedJSXElement | ResolvedJSXElement[]; export type ChildrenReturn = Accessor & { toArray: () => ResolvedJSXElement[] }; /** * Resolves child elements to help interact with children * * @param fn an accessor for the children * @returns a accessor of the same children, but resolved * * @description https://www.solidjs.com/docs/latest/api#children */ export function children(fn: Accessor): ChildrenReturn { const children = createMemo(fn); const memo = createMemo(() => resolveChildren(children())); (memo as ChildrenReturn).toArray = () => { const c = memo(); return Array.isArray(c) ? c : c != null ? [c] : []; }; return memo as ChildrenReturn; } // Resource API export type SuspenseContextType = { increment?: () => void; decrement?: () => void; inFallback?: () => boolean; effects?: Computation[]; resolved?: boolean; }; type SuspenseContext = Context & { active?(): boolean; increment?(): void; decrement?(): void; }; let SuspenseContext: SuspenseContext; export function getSuspenseContext() { return SuspenseContext || (SuspenseContext = createContext({})); } // Interop export function enableExternalSource(factory: ExternalSourceFactory) { if (ExternalSourceFactory) { const oldFactory = ExternalSourceFactory; ExternalSourceFactory = (fn, trigger) => { const oldSource = oldFactory(fn, trigger); const source = factory(x => oldSource.track(x), trigger); return { track: x => source.track(x), dispose() { source.dispose(); oldSource.dispose(); } }; }; } else { ExternalSourceFactory = factory; } } // Internal export function readSignal(this: SignalState | Memo) { const runningTransition = Transition && Transition.running; if ( (this as Memo).sources && ((!runningTransition && (this as Memo).state) || (runningTransition && (this as Memo).tState)) ) { if ( (!runningTransition && (this as Memo).state === STALE) || (runningTransition && (this as Memo).tState === STALE) ) updateComputation(this as Memo); else { const updates = Updates; Updates = null; runUpdates(() => lookUpstream(this as Memo), false); Updates = updates; } } if (Listener) { const sSlot = this.observers ? this.observers.length : 0; if (!Listener.sources) { Listener.sources = [this]; Listener.sourceSlots = [sSlot]; } else { Listener.sources.push(this); Listener.sourceSlots!.push(sSlot); } if (!this.observers) { this.observers = [Listener]; this.observerSlots = [Listener.sources.length - 1]; } else { this.observers.push(Listener); this.observerSlots!.push(Listener.sources.length - 1); } } if (runningTransition && Transition!.sources.has(this)) return this.tValue; return this.value; } export function writeSignal(node: SignalState | Memo, value: any, isComp?: boolean) { let current = Transition && Transition.running && Transition.sources.has(node) ? node.tValue : node.value; if (!node.comparator || !node.comparator(current, value)) { if (Transition) { const TransitionRunning = Transition.running; if (TransitionRunning || (!isComp && Transition.sources.has(node))) { Transition.sources.add(node); node.tValue = value; } if (!TransitionRunning) node.value = value; } else node.value = value; if (node.observers && node.observers.length) { runUpdates(() => { for (let i = 0; i < node.observers!.length; i += 1) { const o = node.observers![i]; const TransitionRunning = Transition && Transition.running; if (TransitionRunning && Transition!.disposed.has(o)) continue; if ((TransitionRunning && !o.tState) || (!TransitionRunning && !o.state)) { if (o.pure) Updates!.push(o); else Effects!.push(o); if ((o as Memo).observers) markDownstream(o as Memo); } if (TransitionRunning) o.tState = STALE; else o.state = STALE; } if (Updates!.length > 10e5) { Updates = []; if ("_SOLID_DEV_") throw new Error("Potential Infinite Loop Detected."); throw new Error(); } }, false); } } return value; } function updateComputation(node: Computation) { if (!node.fn) return; cleanNode(node); const owner = Owner, listener = Listener, time = ExecCount; Listener = Owner = node; runComputation( node, Transition && Transition.running && Transition.sources.has(node as Memo) ? (node as Memo).tValue : node.value, time ); if (Transition && !Transition.running && Transition.sources.has(node as Memo)) { queueMicrotask(() => { runUpdates(() => { Transition && (Transition.running = true); runComputation(node, (node as Memo).tValue, time); }, false); }); } Listener = listener; Owner = owner; } function runComputation(node: Computation, value: any, time: number) { let nextValue; try { nextValue = node.fn(value); } catch (err) { if (node.pure) Transition && Transition.running ? (node.tState = STALE) : (node.state = STALE); handleError(err); } if (!node.updatedAt || node.updatedAt <= time) { if (node.updatedAt != null && "observers" in (node as Memo)) { writeSignal(node as Memo, nextValue, true); } else if (Transition && Transition.running && node.pure) { Transition.sources.add(node as Memo); (node as Memo).tValue = nextValue; } else node.value = nextValue; node.updatedAt = time; } } function createComputation( fn: EffectFunction, init: Init, pure: boolean, state: number = STALE, options?: EffectOptions ): Computation { const c: Computation = { fn, state: state, updatedAt: null, owned: null, sources: null, sourceSlots: null, cleanups: null, value: init, owner: Owner, context: null, pure }; if (Transition && Transition.running) { c.state = 0; c.tState = state; } if (Owner === null) "_SOLID_DEV_" && console.warn( "computations created outside a `createRoot` or `render` will never be disposed" ); else if (Owner !== UNOWNED) { if (Transition && Transition.running && (Owner as Memo).pure) { if (!(Owner as Memo).tOwned) (Owner as Memo).tOwned = [c]; else (Owner as Memo).tOwned!.push(c); } else { if (!Owner.owned) Owner.owned = [c]; else Owner.owned.push(c); } if ("_SOLID_DEV_") c.name = (options && options.name) || `${(Owner as Computation).name || "c"}-${ (Owner.owned || (Owner as Memo).tOwned!).length }`; } if (ExternalSourceFactory) { const [track, trigger] = createSignal(undefined, { equals: false }); const ordinary = ExternalSourceFactory(c.fn, trigger); onCleanup(() => ordinary.dispose()); const triggerInTransition: () => void = () => startTransition(trigger).then(() => inTransition.dispose()); const inTransition = ExternalSourceFactory(c.fn, triggerInTransition); c.fn = x => { track(); return Transition && Transition.running ? inTransition.track(x) : ordinary.track(x); }; } return c; } function runTop(node: Computation) { const runningTransition = Transition && Transition.running; if ((!runningTransition && node.state === 0) || (runningTransition && node.tState === 0)) return; if ( (!runningTransition && node.state === PENDING) || (runningTransition && node.tState === PENDING) ) return lookUpstream(node); if (node.suspense && untrack(node.suspense.inFallback!)) return node!.suspense.effects!.push(node!); const ancestors = [node]; while ( (node = node.owner as Computation) && (!node.updatedAt || node.updatedAt < ExecCount) ) { if (runningTransition && Transition!.disposed.has(node)) return; if ((!runningTransition && node.state) || (runningTransition && node.tState)) ancestors.push(node); } for (let i = ancestors.length - 1; i >= 0; i--) { node = ancestors[i]; if (runningTransition) { let top = node, prev = ancestors[i + 1]; while ((top = top.owner as Computation) && top !== prev) { if (Transition!.disposed.has(top)) return; } } if ( (!runningTransition && node.state === STALE) || (runningTransition && node.tState === STALE) ) { updateComputation(node); } else if ( (!runningTransition && node.state === PENDING) || (runningTransition && node.tState === PENDING) ) { const updates = Updates; Updates = null; runUpdates(() => lookUpstream(node, ancestors[0]), false); Updates = updates; } } } function runUpdates(fn: () => T, init: boolean) { if (Updates) return fn(); let wait = false; if (!init) Updates = []; if (Effects) wait = true; else Effects = []; ExecCount++; try { const res = fn(); completeUpdates(wait); return res; } catch (err) { if (!Updates) Effects = null; handleError(err); } } function completeUpdates(wait: boolean) { if (Updates) { if (Scheduler && Transition && Transition.running) scheduleQueue(Updates); else runQueue(Updates); Updates = null; } if (wait) return; let res; if (Transition && Transition.running) { if (Transition.promises.size || Transition.queue.size) { Transition.running = false; Transition.effects.push.apply(Transition.effects, Effects!); Effects = null; setTransPending(true); return; } // finish transition const sources = Transition.sources; const disposed = Transition.disposed; res = Transition.resolve; for (const e of Effects!) { "tState" in e && (e.state = e.tState!); delete e.tState; } Transition = null; runUpdates(() => { for (const d of disposed) cleanNode(d); for (const v of sources) { v.value = v.tValue; if ((v as Memo).owned) { for (let i = 0, len = (v as Memo).owned!.length; i < len; i++) cleanNode((v as Memo).owned![i]); } if ((v as Memo).tOwned) (v as Memo).owned = (v as Memo).tOwned!; delete v.tValue; delete (v as Memo).tOwned; (v as Memo).tState = 0; } setTransPending(false); }, false); } const e = Effects!; Effects = null; if (e!.length) runUpdates(() => runEffects(e), false); else if ("_SOLID_DEV_") globalThis._$afterUpdate && globalThis._$afterUpdate(); if (res) res(); } function runQueue(queue: Computation[]) { for (let i = 0; i < queue.length; i++) runTop(queue[i]); } function scheduleQueue(queue: Computation[]) { for (let i = 0; i < queue.length; i++) { const item = queue[i]; const tasks = Transition!.queue; if (!tasks.has(item)) { tasks.add(item); Scheduler!(() => { tasks.delete(item); runUpdates(() => { Transition!.running = true; runTop(item); if (!tasks.size) { Effects!.push.apply(Effects, Transition!.effects); Transition!.effects = []; } }, false); Transition && (Transition.running = false); }); } } } function runUserEffects(queue: Computation[]) { let i, userLength = 0; for (i = 0; i < queue.length; i++) { const e = queue[i]; if (!e.user) runTop(e); else queue[userLength++] = e; } if (sharedConfig.context) setHydrateContext(); for (i = 0; i < userLength; i++) runTop(queue[i]); } function lookUpstream(node: Computation, ignore?: Computation) { const runningTransition = Transition && Transition.running; if (runningTransition) node.tState = 0; else node.state = 0; for (let i = 0; i < node.sources!.length; i += 1) { const source = node.sources![i] as Memo; if (source.sources) { if ( (!runningTransition && source.state === STALE) || (runningTransition && source.tState === STALE) ) { if (source !== ignore) runTop(source); } else if ( (!runningTransition && source.state === PENDING) || (runningTransition && source.tState === PENDING) ) lookUpstream(source, ignore); } } } function markDownstream(node: Memo) { const runningTransition = Transition && Transition.running; for (let i = 0; i < node.observers!.length; i += 1) { const o = node.observers![i]; if ((!runningTransition && !o.state) || (runningTransition && !o.tState)) { if (runningTransition) o.tState = PENDING; else o.state = PENDING; if (o.pure) Updates!.push(o); else Effects!.push(o); (o as Memo).observers && markDownstream(o as Memo); } } } function cleanNode(node: Owner) { let i; if ((node as Computation).sources) { while ((node as Computation).sources!.length) { const source = (node as Computation).sources!.pop()!, index = (node as Computation).sourceSlots!.pop()!, obs = source.observers; if (obs && obs.length) { const n = obs.pop()!, s = source.observerSlots!.pop()!; if (index < obs.length) { n.sourceSlots![s] = index; obs[index] = n; source.observerSlots![index] = s; } } } } if (Transition && Transition.running && (node as Memo).pure) { if ((node as Memo).tOwned) { for (i = 0; i < (node as Memo).tOwned!.length; i++) cleanNode((node as Memo).tOwned![i]); delete (node as Memo).tOwned; } reset(node as Computation, true); } else if (node.owned) { for (i = 0; i < node.owned.length; i++) cleanNode(node.owned[i]); node.owned = null; } if (node.cleanups) { for (i = 0; i < node.cleanups.length; i++) node.cleanups[i](); node.cleanups = null; } if (Transition && Transition.running) (node as Computation).tState = 0; else (node as Computation).state = 0; node.context = null; "_SOLID_DEV_" && delete node.sourceMap; } function reset(node: Computation, top?: boolean) { if (!top) { node.tState = 0; Transition!.disposed.add(node); } if (node.owned) { for (let i = 0; i < node.owned.length; i++) reset(node.owned[i]); } } function castError(err: any) { if (err instanceof Error || typeof err === "string") return err; return new Error("Unknown error"); } function handleError(err: any) { err = castError(err); const fns = ERROR && lookup(Owner, ERROR); if (!fns) throw err; for (const f of fns) f(err); } function lookup(owner: Owner | null, key: symbol | string): any { return owner ? owner.context && owner.context[key] !== undefined ? owner.context[key] : lookup(owner.owner, key) : undefined; } function resolveChildren(children: JSX.Element): ResolvedChildren { if (typeof children === "function" && !children.length) return resolveChildren(children()); if (Array.isArray(children)) { const results: any[] = []; for (let i = 0; i < children.length; i++) { const result = resolveChildren(children[i]); Array.isArray(result) ? results.push.apply(results, result) : results.push(result); } return results; } return children as ResolvedChildren; } function createProvider(id: symbol) { return function provider(props: FlowProps<{ value: unknown }>) { let res; createRenderEffect( () => (res = untrack(() => { Owner!.context = { [id]: props.value }; return children(() => props.children); })) ); return res; }; } function hash(s: string) { for (var i = 0, h = 9; i < s.length; ) h = Math.imul(h ^ s.charCodeAt(i++), 9 ** 9); return `${h ^ (h >>> 9)}`; } function serializeValues(sources: Record = {}) { const k = Object.keys(sources); const result: Record = {}; for (let i = 0; i < k.length; i++) { const key = k[i]; result[key] = sources[key].value; } return result; } function serializeChildren(root: Owner): GraphRecord { const result: GraphRecord = {}; for (let i = 0, len = root.owned!.length; i < len; i++) { const node = root.owned![i]; result[node.componentName ? `${node.componentName}:${node.name}` : node.name!] = { ...serializeValues(node.sourceMap), ...(node.owned ? serializeChildren(node) : {}) }; } return result; } type TODO = any;