From bc8a35b62f66dac6bfe239d29d5ce9062981c1cd Mon Sep 17 00:00:00 2001 From: Francois Chabot Date: Thu, 22 Apr 2021 09:12:11 -0400 Subject: [PATCH 1/9] allow accessing Node result from base interface --- api/api.d.ts | 3 +++ src/ApiTypes.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/api/api.d.ts b/api/api.d.ts index 2ff2134..acf9216 100644 --- a/api/api.d.ts +++ b/api/api.d.ts @@ -26,6 +26,9 @@ export interface IPNode { /** Forces the node to be finalized. You generaly don't need to call this. */ finalize(): void; + + /** The result of the node's callback, finalizing it if needed. */ + readonly result: unknown; } /** A probed component of which the return type is known. */ diff --git a/src/ApiTypes.ts b/src/ApiTypes.ts index 3505b56..15eceda 100644 --- a/src/ApiTypes.ts +++ b/src/ApiTypes.ts @@ -26,6 +26,9 @@ export interface IPNode { /** Forces the node to be finalized. You generaly don't need to call this. */ finalize(): void; + + /** The result of the node's callback, finalizing it if needed. */ + readonly result: unknown; } /** A probed component of which the return type is known. */ From 92b5e6fc5ef997f71e3419a00cc891787e0b1601 Mon Sep 17 00:00:00 2001 From: Francois Chabot Date: Thu, 22 Apr 2021 09:17:33 -0400 Subject: [PATCH 2/9] added result to base node implementation interface --- src/Node.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Node.ts b/src/Node.ts index 4d14dd3..9f407cf 100644 --- a/src/Node.ts +++ b/src/Node.ts @@ -48,6 +48,8 @@ export abstract class BaseNode implements IPNode { } } + abstract get result(): unknown; + _result?: unknown; _probed_pnodetype?: number; _onDispose?: DisposeOp[]; From 1b5eafc4a300c19c746ec315867019300655057d Mon Sep 17 00:00:00 2001 From: Francois Chabot Date: Thu, 22 Apr 2021 13:23:38 -0400 Subject: [PATCH 3/9] Propagate component name in context --- .eslintrc.yaml | 4 +++- api/api.d.ts | 44 +++++++++++++++++++++++++++--------------- src/ApiTypes.ts | 33 ++++++++++++++++++++----------- src/Node.ts | 7 ++++--- src/Prober.ts | 32 +++++++++++++++++++----------- tests/probe.test.ts | 47 +++++++++++++++++++++++++++++++++++++++++---- 6 files changed, 122 insertions(+), 45 deletions(-) diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 0fd2e6e..9586fb7 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -26,7 +26,9 @@ plugins: rules: "@typescript-eslint/no-non-null-assertion": off - "@typescript-eslint/no-explicit-any": off + "@typescript-eslint/no-explicit-any": ["error", {"ignoreRestArgs": true}] + no-unused-vars: off + "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }] prettier/prettier: error no-console: warn require-atomic-updates: warn diff --git a/api/api.d.ts b/api/api.d.ts index acf9216..f94c0fe 100644 --- a/api/api.d.ts +++ b/api/api.d.ts @@ -38,11 +38,17 @@ export interface PNode extends IPNode { } /** A function that implements the probe() functionality for a given set of intrinsics. */ -export type ProbingFunction> = any)>( +export type ProbingFunction> = unknown)>( cb: T, ...args: ProbedParams ) => AsPNode>; +export interface ProbingContext { + componentName: string; +} + +export type Component = (...args: AddOptionalContext) => Ret; + /** * Creates a probe() function that is bound against a set of intrinsic components. * @@ -53,7 +59,10 @@ export declare function createProber>(intrinsics: I): Pr /** Default probe() function not bound to any intrinsic. */ // eslint-disable-next-line @typescript-eslint/ban-types -export declare function probe any>(cb: T, ...args: Parameters): AsPNode>; +export declare function probe unknown>( + cb: T, + ...args: Parameters +): AsPNode>; // ***************** Hooks ***************** // @@ -131,27 +140,32 @@ type DynamicReader = DynamicListReader ? T : never> type AsPNode = T extends PNode ? PNode : PNode; -type Component = (...arg: ArgsT) => RetT; - -type Intrinsics = { - [_ in keyof I]: Component; +export type Intrinsics = { + [K in keyof T]: T[K] extends (...args: any[]) => unknown ? T[K] : never; }; -type IntrinsicParams> = Parameters; -type IntrinsicResult> = ReturnType; +export type IKeys = keyof Intrinsics; + +type AddOptionalContext = [...T, ProbingContext]; +type RemoveContextArg = T extends [...infer Rest, infer LastArg] ? (LastArg extends ProbingContext ? Rest : T) : T; -type Probed = Record> = keyof I | Component; +export type IntrinsicParams, I extends Intrinsics> = RemoveContextArg< + Parameters[K]> +>; +export type IntrinsicResult, I extends Intrinsics> = ReturnType[K]>; -type ProbedParams, I extends Intrinsics = Record> = T extends keyof I +export type Probed = Record> = IKeys | ((...args: any[]) => unknown); + +export type ProbedParams, I extends Intrinsics = Record> = T extends keyof I ? IntrinsicParams - : T extends Component - ? P + : T extends (...arg: infer P) => unknown + ? RemoveContextArg

: never; -type ProbedResult, I extends Intrinsics = Record> = T extends keyof I +export type ProbedResult, I extends Intrinsics = Record> = T extends keyof I ? IntrinsicResult - : T extends Component + : T extends (...arg: any[]) => infer P ? P : never; -type ListValueType> = ArrayType[number]; +export type ListValueType> = ArrayType[number]; diff --git a/src/ApiTypes.ts b/src/ApiTypes.ts index 15eceda..1548e82 100644 --- a/src/ApiTypes.ts +++ b/src/ApiTypes.ts @@ -38,11 +38,17 @@ export interface PNode extends IPNode { } /** A function that implements the probe() functionality for a given set of intrinsics. */ -export type ProbingFunction> = any)>( +export type ProbingFunction> = unknown)>( cb: T, ...args: ProbedParams ) => AsPNode>; +export interface ProbingContext { + componentName: string; +} + +export type Component = (...args: AddOptionalContext) => Ret; + // ************ Dynamics - Readers ************ // /** Consumer API for a dynamic value. */ @@ -95,26 +101,31 @@ export type DynamicReader = DynamicListReader ? T : export type AsPNode = T extends PNode ? PNode : PNode; -export type Component = (...arg: ArgsT) => RetT; - -export type Intrinsics = { - [_ in keyof I]: Component; +export type Intrinsics = { + [K in keyof T]: T[K] extends (...args: any[]) => unknown ? T[K] : never; }; -export type IntrinsicParams> = Parameters; -export type IntrinsicResult> = ReturnType; +export type IKeys = keyof Intrinsics; -export type Probed = Record> = keyof I | Component; +type AddOptionalContext = [...T, ProbingContext]; +type RemoveContextArg = T extends [...infer Rest, infer LastArg] ? (LastArg extends ProbingContext ? Rest : T) : T; + +export type IntrinsicParams, I extends Intrinsics> = RemoveContextArg< + Parameters[K]> +>; +export type IntrinsicResult, I extends Intrinsics> = ReturnType[K]>; + +export type Probed = Record> = IKeys | ((...args: any[]) => unknown); export type ProbedParams, I extends Intrinsics = Record> = T extends keyof I ? IntrinsicParams - : T extends Component - ? P + : T extends (...arg: infer P) => unknown + ? RemoveContextArg

: never; export type ProbedResult, I extends Intrinsics = Record> = T extends keyof I ? IntrinsicResult - : T extends Component + : T extends (...arg: any[]) => infer P ? P : never; diff --git a/src/Node.ts b/src/Node.ts index 9f407cf..2eab458 100644 --- a/src/Node.ts +++ b/src/Node.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { IPNode } from './ApiTypes'; +import { IPNode, ProbingContext } from './ApiTypes'; import { DisposeOp } from './Environment'; export interface IProber { @@ -22,12 +22,13 @@ export interface IProber { } export interface NodeBuildData { - _cb: (...arg: any[]) => any; - _args: any[]; + _cb: (...arg: unknown[]) => unknown; + _args: unknown[]; _next?: IPNode; _resolveAs?: IPNode; _prober: IProber; + _context: ProbingContext; } export abstract class BaseNode implements IPNode { diff --git a/src/Prober.ts b/src/Prober.ts index 57568f2..5f00801 100644 --- a/src/Prober.ts +++ b/src/Prober.ts @@ -1,8 +1,10 @@ import { DisposeOp, popEnv, pushEnv } from './Environment'; -import { IPNode, AsPNode, Component, Intrinsics, ProbedParams, ProbedResult, ProbingFunction } from './ApiTypes'; +import { IPNode, AsPNode, Intrinsics, ProbedParams, ProbedResult, ProbingFunction, IKeys } from './ApiTypes'; import { IProber, BaseNode, NodeImpl, UnwrapPNode, isPNode } from './Node'; -const isIntrinsic = (cb: keyof I | ((...args: any[]) => any)): cb is keyof I => { +type ComponentCb = (...arg: any[]) => unknown; + +const isIntrinsic = (cb: keyof I | ((...args: unknown[]) => unknown)): cb is keyof I => { return typeof cb === 'string'; }; @@ -17,7 +19,7 @@ const addToDisposeQueue = (node: BaseNode, ops: DisposeOp[]) => { let _NextUniqueNodeId = 0; class Prober> implements IProber { - private _intrinsics: I; + private _intrinsics: Intrinsics; private _queueHead?: BaseNode; private _insert?: BaseNode; private _insertStack: (BaseNode | undefined)[] = []; @@ -37,7 +39,8 @@ class Prober> implements IProber { this._intrinsics = intrinsics; } - _announce(what: T, ..._args: ProbedParams): AsPNode> { + _announce | ComponentCb>(what: T, ..._args: ProbedParams): AsPNode> { + const { _cb, _name } = this._getCb(what); if (process.env.NODE_ENV !== 'production') { if (isIntrinsic(what)) { if (!this._intrinsics[what]) { @@ -55,7 +58,6 @@ class Prober> implements IProber { newNode._uniqueNodeId = _NextUniqueNodeId++; } - const _cb = this._getCb(what); let _next: IPNode | undefined; if (this._queueHead) { @@ -71,7 +73,15 @@ class Prober> implements IProber { this._end = newNode; } - newNode._buildData = { _cb, _prober: this, _args, _next }; + newNode._buildData = { + _cb, + _prober: this, + _args, + _next, + _context: { + componentName: _name, + }, + }; return newNode as AsPNode>; } @@ -98,7 +108,7 @@ class Prober> implements IProber { this._insertStack.push(this._insert); const { _cb, _args } = currentNode._buildData!; - const cbResult = _cb(..._args); + const cbResult = _cb(..._args, currentNode._buildData!._context); if (isPNode(cbResult)) { if (cbResult.finalized) { @@ -130,11 +140,11 @@ class Prober> implements IProber { popEnv(); } - private _getCb(what: T): Component { + private _getCb | ComponentCb>(what: T): { _cb: ComponentCb; _name: string } { if (isIntrinsic(what)) { - return this._intrinsics[what]; + return { _cb: this._intrinsics[what], _name: what.toString() }; } else { - return what as Component; + return { _cb: what as ComponentCb, _name: (what as ComponentCb).name }; } } } @@ -142,7 +152,7 @@ class Prober> implements IProber { export function createProber>(intrinsics: I): ProbingFunction { const newProber = new Prober(intrinsics); - const probe = | Component>(what: T, ...args: ProbedParams) => + const probe = | ComponentCb>(what: T, ...args: ProbedParams) => newProber._announce(what, ...args); return probe; diff --git a/tests/probe.test.ts b/tests/probe.test.ts index c2d8272..5d037f3 100644 --- a/tests/probe.test.ts +++ b/tests/probe.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { probe, createProber, PNode, useOnDispose, Component } from '../src'; +import { probe, createProber, PNode, useOnDispose, ProbingContext, Component } from '../src'; import { expectThrowInNotProd } from './utils'; describe('Basic prober', () => { @@ -30,6 +30,12 @@ describe('Basic prober', () => { expect(result.result).toBe(2); }); + it('Works with function That accepts context', () => { + const result = probe((v: number, _ctx: ProbingContext) => v + 1, 1); + + expect(result.result).toBe(2); + }); + it('Works with function with multiple arguments', () => { const result = probe((v1: number, v2: number) => v1 + v2, 1, 2); @@ -63,13 +69,34 @@ describe('Basic prober', () => { //@ts-expect-error expectThrowInNotProd(() => probe('', {})); }); + + // These cannot be made into runtime tests, because there's + // No way to determine at runtime if a function expects a context + // or not. + it('Fails when not passing enough arguments', () => { + //@ts-expect-error + () => probe((v1: number, v2: number) => v1 + v2, 12); + }); + + it('Fails when passing too many arguments', () => { + //@ts-expect-error + () => expectThrowInNotProd(() => probe((v1: number) => v1, 12, 13)); + + //@ts-expect-error + () => expectThrowInNotProd(() => probe((v1: number, _ctx: ProbingContext) => v1, 12, 13)); + }); }); describe('Prober with intrinsics', () => { - const sutProbe = createProber({ - aaa: (v: number) => v + 1, + const mapping = { + aaa: (v: number, ctx: ProbingContext) => { + expect(ctx.componentName).toBe('aaa'); + return v + 1; + }, bbb: (v: number) => v + 4, - }); + }; + + const sutProbe = createProber(mapping); it('Produces a payload', () => { const resultA = sutProbe('aaa', 1); @@ -92,6 +119,18 @@ describe('Prober with intrinsics', () => { //@ts-expect-error expectThrowInNotProd(() => sutProbe('', {})); }); + + it('Still handles functional', () => { + const result = sutProbe((v: number) => v + 1, 1); + expect(result.result).toBe(2); + }); + + it('works with higher-order components', () => { + const HOC = (c: Component<[number], unknown>) => sutProbe(c, 12); + + expect(sutProbe(HOC, mapping.aaa).result).toBe(13); + expect(sutProbe(HOC, mapping.bbb).result).toBe(16); + }); }); describe('Component With dispose', () => { From 4b9fa0cf9f07fe8d916c40aaf169e422cdf5c922 Mon Sep 17 00:00:00 2001 From: Francois Chabot Date: Thu, 22 Apr 2021 15:41:00 -0400 Subject: [PATCH 4/9] Added intrinsic fallback mechanism --- api/api.d.ts | 93 +++++++++++++++++++++++++++------------------ src/ApiTypes.ts | 29 ++++++++------ src/Prober.ts | 52 +++++++++++++++++++------ tests/probe.test.ts | 26 +++++++++++++ 4 files changed, 140 insertions(+), 60 deletions(-) diff --git a/api/api.d.ts b/api/api.d.ts index f94c0fe..c7064ec 100644 --- a/api/api.d.ts +++ b/api/api.d.ts @@ -38,41 +38,18 @@ export interface PNode extends IPNode { } /** A function that implements the probe() functionality for a given set of intrinsics. */ -export type ProbingFunction> = unknown)>( +export type ProbingFunction = | ((...args: any[]) => unknown)>( cb: T, ...args: ProbedParams ) => AsPNode>; +/** Meta information available to components. */ export interface ProbingContext { - componentName: string; + componentName: string; // The name of the component. } -export type Component = (...args: AddOptionalContext) => Ret; - -/** - * Creates a probe() function that is bound against a set of intrinsic components. - * - * ex: const probe = createProber({"hi": x=>x+5, "bye": x=>x*2}); - * probe("hi", 12); - */ -export declare function createProber>(intrinsics: I): ProbingFunction; -/** Default probe() function not bound to any intrinsic. */ -// eslint-disable-next-line @typescript-eslint/ban-types -export declare function probe unknown>( - cb: T, - ...args: Parameters -): AsPNode>; - -// ***************** Hooks ***************** // - -/** Registers a callback that will be invoked when component currently being probed is disposed. */ -export declare function useOnDispose(op: () => void): void; - -// ***************** Dynamics ***************** // - -/** Determines at runtime if a value is static or dynamic. */ -export declare function isDynamic(val: T | DynamicReader): val is DynamicReader; +export type Component = (...args: AddOptionalContext) => Ret; // ************ Dynamics - Readers ************ // @@ -82,6 +59,9 @@ export interface DynamicReaderBase { removeListener(lst: (v: T) => void): void; readonly current: T; + + /** gets string representation */ + toString(): string; } /** Consumer API for a dynamic primitive. */ @@ -117,6 +97,37 @@ export interface DynamicList> extends DynamicBase { push(v: ListValueType): void; } +/** + * Creates a probe() function that is bound against a set of intrinsic components. + * + * ex: const probe = createProber({"hi": x=>x+5, "bye": x=>x*2}); + * probe("hi", 12); + */ +export function createProber>(intrinsics: I): ProbingFunction; +export function createProber>( + intrinsics: Partial, + fallback: IntrinsicFallback, +): ProbingFunction; + +/** Default probe() function not bound to any intrinsic. */ +// eslint-disable-next-line @typescript-eslint/ban-types +export declare function probe unknown>( + cb: T, + ...args: Parameters +): AsPNode>; + +// ***************** Hooks ***************** // + +/** Registers a callback that will be invoked when component currently being probed is disposed. */ +export declare function useOnDispose(op: () => void): void; + +// ***************** Dynamics ***************** // + +/** Determines at runtime if a value is static or dynamic. */ +export declare function isDynamic(val: T | DynamicReader): val is DynamicReader; + +// ************ Dynamics - Readers ************ // + /** Creates a new Dynamic value. */ export declare function dynamic(init: T[]): DynamicList; export declare function dynamic(init: T): DynamicValue; @@ -136,36 +147,42 @@ export declare function valType(v: Reader): string; // ***************** Utility / not user-facing ***************** // -type DynamicReader = DynamicListReader ? T : never> | DynamicValueReader; +export type DynamicReader = DynamicListReader ? T : never> | DynamicValueReader; -type AsPNode = T extends PNode ? PNode : PNode; +export type AsPNode = T extends PNode ? PNode : PNode; export type Intrinsics = { - [K in keyof T]: T[K] extends (...args: any[]) => unknown ? T[K] : never; + [K in keyof T as T[K] extends (...args: any[]) => unknown ? K : never]: T[K]; }; -export type IKeys = keyof Intrinsics; +export type IKeys = keyof T; type AddOptionalContext = [...T, ProbingContext]; type RemoveContextArg = T extends [...infer Rest, infer LastArg] ? (LastArg extends ProbingContext ? Rest : T) : T; -export type IntrinsicParams, I extends Intrinsics> = RemoveContextArg< - Parameters[K]> ->; -export type IntrinsicResult, I extends Intrinsics> = ReturnType[K]>; +export type IntrinsicParams, I extends FuncMap> = RemoveContextArg>; +export type IntrinsicResult, I extends FuncMap> = ReturnType; -export type Probed = Record> = IKeys | ((...args: any[]) => unknown); +export type Probed = IKeys | ((...args: any[]) => unknown); -export type ProbedParams, I extends Intrinsics = Record> = T extends keyof I +export type ProbedParams, I extends FuncMap> = T extends IKeys ? IntrinsicParams : T extends (...arg: infer P) => unknown ? RemoveContextArg

: never; -export type ProbedResult, I extends Intrinsics = Record> = T extends keyof I +export type ProbedResult, I extends FuncMap> = T extends IKeys ? IntrinsicResult : T extends (...arg: any[]) => infer P ? P : never; +export type FuncMap = Record unknown>; + +type FallbackParams = Parameters; +type FallbackResult = ReturnType; +export type IntrinsicFallback = ( + ...args: AddOptionalContext> +) => FallbackResult; + export type ListValueType> = ArrayType[number]; diff --git a/src/ApiTypes.ts b/src/ApiTypes.ts index 1548e82..eb83444 100644 --- a/src/ApiTypes.ts +++ b/src/ApiTypes.ts @@ -38,13 +38,14 @@ export interface PNode extends IPNode { } /** A function that implements the probe() functionality for a given set of intrinsics. */ -export type ProbingFunction> = unknown)>( +export type ProbingFunction = | ((...args: any[]) => unknown)>( cb: T, ...args: ProbedParams ) => AsPNode>; +/** Meta information available to components. */ export interface ProbingContext { - componentName: string; + componentName: string; // The name of the component. } export type Component = (...args: AddOptionalContext) => Ret; @@ -102,31 +103,37 @@ export type DynamicReader = DynamicListReader ? T : export type AsPNode = T extends PNode ? PNode : PNode; export type Intrinsics = { - [K in keyof T]: T[K] extends (...args: any[]) => unknown ? T[K] : never; + [K in keyof T as T[K] extends (...args: any[]) => unknown ? K : never]: T[K]; }; -export type IKeys = keyof Intrinsics; +export type IKeys = keyof T; type AddOptionalContext = [...T, ProbingContext]; type RemoveContextArg = T extends [...infer Rest, infer LastArg] ? (LastArg extends ProbingContext ? Rest : T) : T; -export type IntrinsicParams, I extends Intrinsics> = RemoveContextArg< - Parameters[K]> ->; -export type IntrinsicResult, I extends Intrinsics> = ReturnType[K]>; +export type IntrinsicParams, I extends FuncMap> = RemoveContextArg>; +export type IntrinsicResult, I extends FuncMap> = ReturnType; -export type Probed = Record> = IKeys | ((...args: any[]) => unknown); +export type Probed = IKeys | ((...args: any[]) => unknown); -export type ProbedParams, I extends Intrinsics = Record> = T extends keyof I +export type ProbedParams, I extends FuncMap> = T extends IKeys ? IntrinsicParams : T extends (...arg: infer P) => unknown ? RemoveContextArg

: never; -export type ProbedResult, I extends Intrinsics = Record> = T extends keyof I +export type ProbedResult, I extends FuncMap> = T extends IKeys ? IntrinsicResult : T extends (...arg: any[]) => infer P ? P : never; +export type FuncMap = Record unknown>; + +type FallbackParams = Parameters; +type FallbackResult = ReturnType; +export type IntrinsicFallback = ( + ...args: AddOptionalContext> +) => FallbackResult; + export type ListValueType> = ArrayType[number]; diff --git a/src/Prober.ts b/src/Prober.ts index 5f00801..a405781 100644 --- a/src/Prober.ts +++ b/src/Prober.ts @@ -1,10 +1,21 @@ import { DisposeOp, popEnv, pushEnv } from './Environment'; -import { IPNode, AsPNode, Intrinsics, ProbedParams, ProbedResult, ProbingFunction, IKeys } from './ApiTypes'; +import { + IPNode, + AsPNode, + Intrinsics, + ProbedParams, + ProbedResult, + IntrinsicFallback, + ProbingFunction, + IKeys, + FuncMap, +} from './ApiTypes'; import { IProber, BaseNode, NodeImpl, UnwrapPNode, isPNode } from './Node'; -type ComponentCb = (...arg: any[]) => unknown; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type ComponentCb = (...args: any[]) => any; -const isIntrinsic = (cb: keyof I | ((...args: unknown[]) => unknown)): cb is keyof I => { +const isIntrinsic = (cb: IKeys | ((...args: unknown[]) => unknown)): cb is IKeys => { return typeof cb === 'string'; }; @@ -18,8 +29,10 @@ const addToDisposeQueue = (node: BaseNode, ops: DisposeOp[]) => { let _NextUniqueNodeId = 0; -class Prober> implements IProber { - private _intrinsics: Intrinsics; +class Prober implements IProber { + private _intrinsics: Partial; + private _fallback?: IntrinsicFallback; + private _queueHead?: BaseNode; private _insert?: BaseNode; private _insertStack: (BaseNode | undefined)[] = []; @@ -35,15 +48,16 @@ class Prober> implements IProber { this._pendingOnDispose.push(op); } - constructor(intrinsics: I) { + constructor(intrinsics: Partial, fallback?: IntrinsicFallback) { this._intrinsics = intrinsics; + this._fallback = fallback; } _announce | ComponentCb>(what: T, ..._args: ProbedParams): AsPNode> { const { _cb, _name } = this._getCb(what); if (process.env.NODE_ENV !== 'production') { if (isIntrinsic(what)) { - if (!this._intrinsics[what]) { + if (!this._intrinsics[what] && !this._fallback) { throw Error(`"${what}" is not a registered intrinsic in this Prober`); } } else { @@ -142,17 +156,33 @@ class Prober> implements IProber { private _getCb | ComponentCb>(what: T): { _cb: ComponentCb; _name: string } { if (isIntrinsic(what)) { - return { _cb: this._intrinsics[what], _name: what.toString() }; + let _cb: ComponentCb | undefined = this._intrinsics[what]; + const _name = what.toString(); + if (!_cb) { + // This is safe, it's caught at the start of _announce() + _cb = this._fallback!; + } + + return { _cb: _cb!, _name }; } else { return { _cb: what as ComponentCb, _name: (what as ComponentCb).name }; } } } -export function createProber>(intrinsics: I): ProbingFunction { - const newProber = new Prober(intrinsics); +export function createProber>(intrinsics: I): ProbingFunction; +export function createProber>( + intrinsics: Partial, + fallback: IntrinsicFallback, +): ProbingFunction; + +export function createProber( + intrinsics: I | Partial, + fallback?: IntrinsicFallback, +): ProbingFunction { + const newProber = new Prober(intrinsics, fallback); - const probe = | ComponentCb>(what: T, ...args: ProbedParams) => + const probe = | ComponentCb>(what: T, ...args: ProbedParams) => newProber._announce(what, ...args); return probe; diff --git a/tests/probe.test.ts b/tests/probe.test.ts index 5d037f3..e3bf213 100644 --- a/tests/probe.test.ts +++ b/tests/probe.test.ts @@ -133,6 +133,32 @@ describe('Prober with intrinsics', () => { }); }); +describe('Dynamic intrinsic lookup', () => { + it('works', () => { + interface Base { + x: string; + } + + interface Specialized extends Base { + y: number; + } + + interface TypeInfo { + aaa: (v: number) => Base; + bbb: (v: string) => Specialized; + } + + const componentImpl = (v: number | string, ctx: ProbingContext): Base | Specialized => { + return { x: ctx.componentName }; + }; + + const sutProbe = createProber({}, componentImpl); + + expect(sutProbe('aaa', 0).result.x).toBe('aaa'); + expect(sutProbe('bbb', 'allo').result.x).toBe('bbb'); + }); +}); + describe('Component With dispose', () => { interface ctx { v: number; From 8b3fb017afa0a6cf6077ae47ca5efe3161a2e982 Mon Sep 17 00:00:00 2001 From: Francois Chabot Date: Thu, 22 Apr 2021 16:43:05 -0400 Subject: [PATCH 5/9] Probe context now happens through a hook --- api/api.d.ts | 17 +++++++---------- src/ApiTypes.ts | 13 ++++--------- src/Environment.ts | 16 ++++++++++++++++ src/Prober.ts | 18 +++++++++++++++++- tests/environment.test.ts | 30 +++++++++++++++++++++++++++++- tests/probe.test.ts | 16 +++++----------- 6 files changed, 78 insertions(+), 32 deletions(-) diff --git a/api/api.d.ts b/api/api.d.ts index c7064ec..f3b330f 100644 --- a/api/api.d.ts +++ b/api/api.d.ts @@ -48,8 +48,7 @@ export interface ProbingContext { componentName: string; // The name of the component. } - -export type Component = (...args: AddOptionalContext) => Ret; +export type Component = (...args: Args) => Ret; // ************ Dynamics - Readers ************ // @@ -121,6 +120,9 @@ export declare function probe unknown>( /** Registers a callback that will be invoked when component currently being probed is disposed. */ export declare function useOnDispose(op: () => void): void; +/** Obtains the probing context for the node being probed */ +export declare function useProbingContext(): ProbingContext; + // ***************** Dynamics ***************** // /** Determines at runtime if a value is static or dynamic. */ @@ -157,10 +159,7 @@ export type Intrinsics = { export type IKeys = keyof T; -type AddOptionalContext = [...T, ProbingContext]; -type RemoveContextArg = T extends [...infer Rest, infer LastArg] ? (LastArg extends ProbingContext ? Rest : T) : T; - -export type IntrinsicParams, I extends FuncMap> = RemoveContextArg>; +export type IntrinsicParams, I extends FuncMap> = Parameters; export type IntrinsicResult, I extends FuncMap> = ReturnType; export type Probed = IKeys | ((...args: any[]) => unknown); @@ -168,7 +167,7 @@ export type Probed = IKeys | ((...args: any[]) => unknown) export type ProbedParams, I extends FuncMap> = T extends IKeys ? IntrinsicParams : T extends (...arg: infer P) => unknown - ? RemoveContextArg

+ ? P : never; export type ProbedResult, I extends FuncMap> = T extends IKeys @@ -181,8 +180,6 @@ export type FuncMap = Record unknown>; type FallbackParams = Parameters; type FallbackResult = ReturnType; -export type IntrinsicFallback = ( - ...args: AddOptionalContext> -) => FallbackResult; +export type IntrinsicFallback = (...args: FallbackParams) => FallbackResult; export type ListValueType> = ArrayType[number]; diff --git a/src/ApiTypes.ts b/src/ApiTypes.ts index eb83444..891029a 100644 --- a/src/ApiTypes.ts +++ b/src/ApiTypes.ts @@ -48,7 +48,7 @@ export interface ProbingContext { componentName: string; // The name of the component. } -export type Component = (...args: AddOptionalContext) => Ret; +export type Component = (...args: Args) => Ret; // ************ Dynamics - Readers ************ // @@ -108,10 +108,7 @@ export type Intrinsics = { export type IKeys = keyof T; -type AddOptionalContext = [...T, ProbingContext]; -type RemoveContextArg = T extends [...infer Rest, infer LastArg] ? (LastArg extends ProbingContext ? Rest : T) : T; - -export type IntrinsicParams, I extends FuncMap> = RemoveContextArg>; +export type IntrinsicParams, I extends FuncMap> = Parameters; export type IntrinsicResult, I extends FuncMap> = ReturnType; export type Probed = IKeys | ((...args: any[]) => unknown); @@ -119,7 +116,7 @@ export type Probed = IKeys | ((...args: any[]) => unknown) export type ProbedParams, I extends FuncMap> = T extends IKeys ? IntrinsicParams : T extends (...arg: infer P) => unknown - ? RemoveContextArg

+ ? P : never; export type ProbedResult, I extends FuncMap> = T extends IKeys @@ -132,8 +129,6 @@ export type FuncMap = Record unknown>; type FallbackParams = Parameters; type FallbackResult = ReturnType; -export type IntrinsicFallback = ( - ...args: AddOptionalContext> -) => FallbackResult; +export type IntrinsicFallback = (...args: FallbackParams) => FallbackResult; export type ListValueType> = ArrayType[number]; diff --git a/src/Environment.ts b/src/Environment.ts index 1b4b447..41509cc 100644 --- a/src/Environment.ts +++ b/src/Environment.ts @@ -14,9 +14,12 @@ * limitations under the License. */ +import { ProbingContext } from './ApiTypes'; + export type DisposeOp = () => void; export interface Environment { _onDispose: (op: DisposeOp) => void; + _getProbingContext: () => ProbingContext | undefined; } let _currentEnv: Environment | null = null; @@ -41,3 +44,16 @@ export const useOnDispose = (op: DisposeOp): void => { _currentEnv!._onDispose(op); }; + +export function useProbingContext(): ProbingContext { + if (process.env.NODE_ENV !== 'production' && !_currentEnv) { + throw new Error('Environment underflow'); + } + + const result = _currentEnv!._getProbingContext(); + if (process.env.NODE_ENV !== 'production' && !result) { + throw new Error('There is no active probing context'); + } + + return result!; +} diff --git a/src/Prober.ts b/src/Prober.ts index a405781..08335f5 100644 --- a/src/Prober.ts +++ b/src/Prober.ts @@ -9,6 +9,7 @@ import { ProbingFunction, IKeys, FuncMap, + ProbingContext, } from './ApiTypes'; import { IProber, BaseNode, NodeImpl, UnwrapPNode, isPNode } from './Node'; @@ -38,16 +39,23 @@ class Prober implements IProber { private _insertStack: (BaseNode | undefined)[] = []; private _end?: BaseNode; + private _currentNode?: BaseNode; + private _pendingOnDispose: DisposeOp[] = []; private _finalizeStack: { _end: BaseNode | undefined; _pendingOnDispose: DisposeOp[]; + _currentNode: BaseNode | undefined; }[] = []; _onDispose(op: DisposeOp): void { this._pendingOnDispose.push(op); } + _getProbingContext(): ProbingContext | undefined { + return this._currentNode?._buildData!._context; + } + constructor(intrinsics: Partial, fallback?: IntrinsicFallback) { this._intrinsics = intrinsics; this._fallback = fallback; @@ -103,13 +111,19 @@ class Prober implements IProber { _finalize(target: IPNode): void { // This can be called recursively, pushEnv(this); - this._finalizeStack.push({ _end: this._end, _pendingOnDispose: this._pendingOnDispose }); + this._finalizeStack.push({ + _end: this._end, + _pendingOnDispose: this._pendingOnDispose, + _currentNode: this._currentNode, + }); this._pendingOnDispose = []; this._end = target; let currentNode: BaseNode; do { currentNode = this._queueHead!; + this._currentNode = currentNode; + this._queueHead = currentNode._buildData!._next; // If a component returns a Node (as opposed to a value), then we short-circuit to the parent. @@ -151,6 +165,8 @@ class Prober implements IProber { const finalizePop = this._finalizeStack.pop()!; this._end = finalizePop._end; this._pendingOnDispose = finalizePop._pendingOnDispose; + this._currentNode = finalizePop._currentNode; + popEnv(); } diff --git a/tests/environment.test.ts b/tests/environment.test.ts index e6b57ef..6938e1c 100644 --- a/tests/environment.test.ts +++ b/tests/environment.test.ts @@ -1,4 +1,5 @@ -import { popEnv, pushEnv, useOnDispose, Environment } from '../src/Environment'; +import { ProbingContext } from '../src'; +import { popEnv, pushEnv, useOnDispose, Environment, useProbingContext } from '../src/Environment'; import { expectThrowInNotProd } from './utils'; const noop = () => { @@ -19,10 +20,16 @@ describe('useOnDispose ', () => { class TestEnv implements Environment { count = 0; + probeContext?: ProbingContext; _onDispose(): void { this.count += 1; } + + _getProbingContext(): ProbingContext | undefined { + return this.probeContext; + } } + describe('Environment', () => { it('Catches underflows', () => { expectThrowInNotProd(popEnv); @@ -64,3 +71,24 @@ describe('Environment', () => { expectThrowInNotProd(popEnv); }); }); + +describe('useProbingContext ', () => { + it('Fails if used out of context', () => { + expectThrowInNotProd(useProbingContext); + }); + + it('Fails if probing context is set', () => { + const env = new TestEnv(); + pushEnv(env); + expectThrowInNotProd(useProbingContext); + popEnv(); + }); + + it('Works if a probing context is set', () => { + const env = new TestEnv(); + env.probeContext = { componentName: 'aaa' }; + pushEnv(env); + expect(useProbingContext().componentName).toBe('aaa'); + popEnv(); + }); +}); diff --git a/tests/probe.test.ts b/tests/probe.test.ts index e3bf213..4b9b928 100644 --- a/tests/probe.test.ts +++ b/tests/probe.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { probe, createProber, PNode, useOnDispose, ProbingContext, Component } from '../src'; +import { probe, createProber, PNode, useOnDispose, ProbingContext, Component, useProbingContext } from '../src'; import { expectThrowInNotProd } from './utils'; describe('Basic prober', () => { @@ -30,12 +30,6 @@ describe('Basic prober', () => { expect(result.result).toBe(2); }); - it('Works with function That accepts context', () => { - const result = probe((v: number, _ctx: ProbingContext) => v + 1, 1); - - expect(result.result).toBe(2); - }); - it('Works with function with multiple arguments', () => { const result = probe((v1: number, v2: number) => v1 + v2, 1, 2); @@ -89,8 +83,8 @@ describe('Basic prober', () => { describe('Prober with intrinsics', () => { const mapping = { - aaa: (v: number, ctx: ProbingContext) => { - expect(ctx.componentName).toBe('aaa'); + aaa: (v: number) => { + expect(useProbingContext().componentName).toBe('aaa'); return v + 1; }, bbb: (v: number) => v + 4, @@ -148,8 +142,8 @@ describe('Dynamic intrinsic lookup', () => { bbb: (v: string) => Specialized; } - const componentImpl = (v: number | string, ctx: ProbingContext): Base | Specialized => { - return { x: ctx.componentName }; + const componentImpl = (_: number | string): Base | Specialized => { + return { x: useProbingContext().componentName }; }; const sutProbe = createProber({}, componentImpl); From e157851ae98b8b621efb870a34041783eab2daba Mon Sep 17 00:00:00 2001 From: Francois Chabot Date: Thu, 22 Apr 2021 16:50:02 -0400 Subject: [PATCH 6/9] All testss now use a full environment --- tests/dynamicList.test.ts | 2 +- tests/dynamicOperations.test.ts | 1 + tests/dynamicVal.test.ts | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/dynamicList.test.ts b/tests/dynamicList.test.ts index 4e07f47..7792124 100644 --- a/tests/dynamicList.test.ts +++ b/tests/dynamicList.test.ts @@ -27,6 +27,7 @@ const cleanup = () => { beforeEach(() => { pushEnv({ _onDispose: (op: DisposeOp) => disposeQueue.push(op), + _getProbingContext: () => undefined, }); }); @@ -60,5 +61,4 @@ describe('Dynamic Array', () => { x.current = [2, 2, 2]; expect(y.current).toEqual([4, 4, 4]); }); - }); diff --git a/tests/dynamicOperations.test.ts b/tests/dynamicOperations.test.ts index 5772184..583514a 100644 --- a/tests/dynamicOperations.test.ts +++ b/tests/dynamicOperations.test.ts @@ -27,6 +27,7 @@ const cleanup = () => { beforeEach(() => { pushEnv({ _onDispose: (op: DisposeOp) => disposeQueue.push(op), + _getProbingContext: () => undefined, }); }); diff --git a/tests/dynamicVal.test.ts b/tests/dynamicVal.test.ts index d7e64da..8294094 100644 --- a/tests/dynamicVal.test.ts +++ b/tests/dynamicVal.test.ts @@ -26,6 +26,7 @@ const cleanup = () => { beforeEach(() => { pushEnv({ _onDispose: (op: DisposeOp) => disposeQueue.push(op), + _getProbingContext: () => undefined, }); }); From 8252db480c2d4c1338650fb0357524a2fbb735c8 Mon Sep 17 00:00:00 2001 From: Francois Chabot Date: Thu, 22 Apr 2021 17:29:43 -0400 Subject: [PATCH 7/9] Listen does not fire on assignment anymore --- jest.config.js | 6 +++--- src/dynamic/operations.ts | 3 --- tests/dynamicOperations.test.ts | 7 +++++-- tests/{variants => setup}/check.js | 0 tests/{variants => setup}/dev.js | 0 tests/{variants => setup}/prod.js | 0 6 files changed, 8 insertions(+), 8 deletions(-) rename tests/{variants => setup}/check.js (100%) rename tests/{variants => setup}/dev.js (100%) rename tests/{variants => setup}/prod.js (100%) diff --git a/jest.config.js b/jest.config.js index 6908af2..09905a1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -22,17 +22,17 @@ const fullConfig = { projects: [ { displayName: 'prod', - setupFiles: ['./tests/variants/prod.js'], + setupFiles: ['./tests/setup/prod.js'], ...baseConfig, }, { displayName: 'dev', - setupFiles: ['./tests/variants/dev.js'], + setupFiles: ['./tests/setup/dev.js'], ...baseConfig, }, { displayName: 'check', - setupFiles: ['./tests/variants/check.js'], + setupFiles: ['./tests/setup/check.js'], ...baseConfig, }, ], diff --git a/src/dynamic/operations.ts b/src/dynamic/operations.ts index 38c9474..89bde6f 100644 --- a/src/dynamic/operations.ts +++ b/src/dynamic/operations.ts @@ -21,10 +21,7 @@ import { DynamicValueImpl } from './Value'; export function listen(v: T | DynamicReaderBase, cb: (v: T) => void): void { if (isDynamic(v)) { - cb(v.current); v.addListener(cb); - } else { - return cb(v); } } diff --git a/tests/dynamicOperations.test.ts b/tests/dynamicOperations.test.ts index 583514a..f13f0ef 100644 --- a/tests/dynamicOperations.test.ts +++ b/tests/dynamicOperations.test.ts @@ -42,7 +42,7 @@ describe('listen', () => { const cb = (v: number) => (y += v); const v = 12; listen(v, cb); - expect(y).toBe(12); + expect(y).toBe(0); }); it('Works on dynamic values', () => { @@ -50,9 +50,12 @@ describe('listen', () => { const cb = (v: number) => (y += v); const v = dynamic(12); listen(v, cb); - expect(y).toBe(12); + expect(y).toBe(0); v.current = 13; + expect(y).toBe(13); + + v.current = 12; expect(y).toBe(25); }); }); diff --git a/tests/variants/check.js b/tests/setup/check.js similarity index 100% rename from tests/variants/check.js rename to tests/setup/check.js diff --git a/tests/variants/dev.js b/tests/setup/dev.js similarity index 100% rename from tests/variants/dev.js rename to tests/setup/dev.js diff --git a/tests/variants/prod.js b/tests/setup/prod.js similarity index 100% rename from tests/variants/prod.js rename to tests/setup/prod.js From 3d5e0f213269db38c7baacc3e68badbe23c93b73 Mon Sep 17 00:00:00 2001 From: Francois Chabot Date: Thu, 22 Apr 2021 18:06:53 -0400 Subject: [PATCH 8/9] fix old Node versions build --- src/Prober.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Prober.ts b/src/Prober.ts index 08335f5..63482ef 100644 --- a/src/Prober.ts +++ b/src/Prober.ts @@ -53,7 +53,10 @@ class Prober implements IProber { } _getProbingContext(): ProbingContext | undefined { - return this._currentNode?._buildData!._context; + if (this._currentNode) { + return this._currentNode._buildData!._context; + } + return undefined; } constructor(intrinsics: Partial, fallback?: IntrinsicFallback) { From fb4a69b5a1f23b1f9cea60a9d2cc395a3bc397f2 Mon Sep 17 00:00:00 2001 From: Francois Chabot Date: Thu, 22 Apr 2021 18:20:33 -0400 Subject: [PATCH 9/9] removed redundant test for impossible scenario --- src/Prober.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Prober.ts b/src/Prober.ts index 63482ef..888a446 100644 --- a/src/Prober.ts +++ b/src/Prober.ts @@ -53,10 +53,7 @@ class Prober implements IProber { } _getProbingContext(): ProbingContext | undefined { - if (this._currentNode) { - return this._currentNode._buildData!._context; - } - return undefined; + return this._currentNode!._buildData!._context; } constructor(intrinsics: Partial, fallback?: IntrinsicFallback) {