import React from "react";
import { ISvs, ISvsInternal, NOT_FOUND } from "./createSvs";
export type Wrapper = (children?: React.ReactNode) => React.ReactNode;
export interface IScope {
/**
* Create a **child scope** of **this scope**.
* Run the service in the **child scope**.
* Put the service output in **this scope**.
* The output will be visible by following services in this scope and the component subtree wrapped by `injectTo`.
*
* The "parent scope - child scope" structure will form a "enviroment tree" to resolve context requests properly.
* It just works like enviroment model of most programing languages.
*/
useProvideSvs(
svs: ISvs,
...input: Input
): Output;
/**
* Create a **child scope** of **this scope**.
* Run the service in the **child scope**.
* Don't put the service output in **this scope**.
*/
useRunSvs(
_svs: ISvs,
...input: Input
): Output;
/**
* Find a service output from this scope, or its ancestor scope, or React context.
*/
useConsumeSvs(svs: ISvs): Output;
useConsumeSvs(
svs: ISvs,
optional: boolean
): Output | typeof NOT_FOUND;
/**
* Make the service outputs in this scope be visible by the wrapped JSX subtree. So that descendant components can consume them.
* It takes a JSX tree, wrap it with React context provider, and return a new JSX tree that you should render.
*/
injectTo: Wrapper;
/**
* Create a child scope.
* Finding a service output in a scope works like JS prototype chain.
* You don't need to call this API manually in most cases.
*/
createChild(): IScope;
}
export class ScopeInternal implements IScope {
private readonly parent: ScopeInternal | undefined;
private readonly loaded: Map, any> = new Map();
private _wrapper: Wrapper = (c) => c;
constructor(parent?: ScopeInternal) {
this.parent = parent;
if (parent) this._wrapper = parent._wrapper;
}
private _useRunSvs(
_svs: ISvs,
...input: Input
): [Output, Wrapper] {
const svs = _svs as ISvsInternal;
if (this.loaded.has(svs)) {
throw new Error(`Service is already provided in this scope.
You should create a child scope to run it.`);
}
// the hooks inside 'useHooks' shouldn't affect current scope
const [output, wrapper] = svs.__useRun(this.createChild(), ...input);
return [output, wrapper];
}
useRunSvs(
_svs: ISvs,
...input: Input
): Output {
return this._useRunSvs(_svs, ...input)[0];
}
useProvideSvs(
svs: ISvs,
...input: Input
): Output {
const [output, wrapper] = this._useRunSvs(svs, ...input);
const oldWrapper = this._wrapper;
this._wrapper = (c) => oldWrapper(wrapper(c));
this.loaded.set(svs, output);
return output;
}
useConsumeSvs(svs: ISvs): Output;
useConsumeSvs(
svs: ISvs,
optional: boolean
): Output | typeof NOT_FOUND;
useConsumeSvs(
svs: ISvs,
optional?: boolean
): Output | typeof NOT_FOUND {
const ctxVal = svs.useCtxConsume(true);
// This find can be optimised
// based on the fact that
// hooks are are called with the same order in every render
const scopeVal = this.find(svs);
if (scopeVal !== NOT_FOUND) return scopeVal;
if (ctxVal !== NOT_FOUND) return ctxVal;
if (optional) return NOT_FOUND;
throw new Error(`This service is not available in neither scope or react context.
You should provide it in current scope or ancester component.`);
}
private find(
svs: ISvs
): Output | typeof NOT_FOUND {
if (this.loaded.has(svs)) return this.loaded.get(svs);
if (this.parent) return this.parent.find(svs);
return NOT_FOUND;
}
createChild() {
return new ScopeInternal(this);
}
get injectTo() {
if (this.parent) {
throw new Error(
`You should avoid injecting a child scope. It will make things difficult.`
);
}
return this._wrapper;
}
}
export function useScope(): IScope {
return new ScopeInternal();
}