Skip to content

Commit

Permalink
Added more user-validation
Browse files Browse the repository at this point in the history
  • Loading branch information
FrancoisChabot committed Apr 23, 2021
1 parent 47d45c6 commit 4a715ca
Show file tree
Hide file tree
Showing 18 changed files with 467 additions and 126 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
settings:
import/resolver:
node:
extensions: [".ts"]
extensions: [".ts", '.js']

env:
node: true
Expand Down
5 changes: 3 additions & 2 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
const baseConfig = {
preset: 'ts-jest',
preset: 'ts-jest/presets/js-with-ts',
testEnvironment: 'node',
transform: {
'^.+\\.ts?$': 'ts-jest',
},
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.ts?$',
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(ts|js)?$',
moduleFileExtensions: ['js', 'ts'],
coveragePathIgnorePatterns: ['/node_modules/', 'src/internalValidation.ts'],
};

const fullConfig = {
Expand Down
13 changes: 7 additions & 6 deletions src/Environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import { ProbingContext } from './ApiTypes';
import { USER_VALIDATION_ENABLED } from './userValidation';

export type DisposeOp = () => void;
export interface Environment {
Expand All @@ -31,23 +32,23 @@ export const pushEnv = (ctx: Environment): void => {
};

export const popEnv = (): void => {
if (process.env.NODE_ENV !== 'production' && _envStack.length === 0) {
throw new Error('Environment underflow');
if (USER_VALIDATION_ENABLED && _envStack.length === 0) {
throw new Error('PROBE_USAGE: Environment underflow');
}
_currentEnv = _envStack.pop()!;
};

export const useOnDispose = (op: DisposeOp): void => {
if (process.env.NODE_ENV !== 'production' && !_currentEnv) {
throw new Error('Environment underflow');
if (USER_VALIDATION_ENABLED && !_currentEnv) {
throw new Error('PROBE_USAGE: Environment underflow');
}

_currentEnv!._onDispose(op);
};

export function useProbingContext(): ProbingContext {
if (process.env.NODE_ENV !== 'production' && !_currentEnv) {
throw new Error('Environment underflow');
if (USER_VALIDATION_ENABLED && !_currentEnv) {
throw new Error('PROBE_USAGE: Environment underflow');
}

return _currentEnv!._getProbingContext();
Expand Down
13 changes: 7 additions & 6 deletions src/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,16 @@

import { IPNode, ProbingContext } from './ApiTypes';
import { DisposeOp } from './Environment';

export interface IProber {
_finalize(target: IPNode): void;
}
import { assertUnmarked, mark } from './internalValidation';
import { IProberBase } from './internalInterfaces';

export interface NodeBuildData {
_cb: (...arg: unknown[]) => unknown;
_args: unknown[];

_next?: BaseNode;
_resolveAs: BaseNode;
_prober: IProber;
_prober: IProberBase;
_context: ProbingContext;
}

Expand All @@ -43,6 +41,8 @@ export abstract class BaseNode implements IPNode {
}

dispose(): void {
assertUnmarked(this, 'disposed');
mark(this, 'disposed');
// Nodes should only ever be disposed once
this._onDispose.forEach((c) => c());
}
Expand All @@ -63,7 +63,8 @@ export abstract class BaseNode implements IPNode {
_onDispose: DisposeOp[] = [];

_buildData?: NodeBuildData;
_uniqueNodeId?: number;

_nodeId?: number;
}

export class NodeImpl<T> extends BaseNode {
Expand Down
123 changes: 53 additions & 70 deletions src/Prober.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,65 +11,50 @@ import {
FuncMap,
ProbingContext,
} from './ApiTypes';
import { IProber, BaseNode, NodeImpl, UnwrapPNode, isPNode } from './Node';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ComponentCb = (...args: any[]) => any;

const isIntrinsic = <I>(cb: IKeys<I> | ((...args: unknown[]) => unknown)): cb is IKeys<I> => {
return typeof cb === 'string';
};

let _NextUniqueNodeId = 0;

interface NodeQueue {
_head: BaseNode;
_tail: BaseNode;
}

interface ProberStackFrame {
_node?: BaseNode;
_disposeOps: DisposeOp[];
_announced?: NodeQueue;
}

class Prober<I extends FuncMap> implements IProber {
private _intrinsics: Partial<I>;
private _fallback?: IntrinsicFallback<I>;
private _stack: ProberStackFrame[] = [];
private _currentFrame: ProberStackFrame = { _disposeOps: [] };
import { BaseNode, NodeImpl, UnwrapPNode, isPNode } from './Node';
import { mark, unmark, assertMarked } from './internalValidation';
import {
checkIsValidComponent,
checkIsValidFallback,
checkIsValidIntrinsics,
checkTargetNodeIsReachable,
} from './userValidation';
import { ComponentCb, IProber, isIntrinsic, ProberStackFrame } from './internalInterfaces';

export class Prober<I extends FuncMap> implements IProber<I> {
_intrinsics: Partial<I>;
_fallback?: IntrinsicFallback<I>;
_stack: ProberStackFrame[] = [];
_currentFrame: ProberStackFrame = { _disposeOps: [] };
_nextNodeId?: number;

_onDispose(op: DisposeOp): void {
assertMarked(this, 'finalizing');

this._currentFrame._disposeOps.push(op);
}

_getProbingContext(): ProbingContext {
assertMarked(this, 'finalizing');

return this._currentFrame!._node!._buildData!._context;
}

constructor(intrinsics: Partial<I>, fallback?: IntrinsicFallback<I>) {
checkIsValidIntrinsics(intrinsics);
checkIsValidFallback(fallback);

this._intrinsics = intrinsics;
this._fallback = fallback;
}

_announce<T extends IKeys<I> | ComponentCb>(what: T, ..._args: ProbedParams<T, I>): AsPNode<ProbedResult<T, I>> {
const { _cb, _name } = this._getCb(what);
if (process.env.NODE_ENV !== 'production') {
if (isIntrinsic<I>(what)) {
if (!this._intrinsics[what] && !this._fallback) {
throw Error(`"${what}" is not a registered intrinsic in this Prober`);
}
} else {
if (typeof what !== 'function') {
throw Error(`"${what}" is not a function`);
}
}
}
_announce<T extends IKeys<I> | ComponentCb>(
component: T,
..._args: ProbedParams<T, I>
): AsPNode<ProbedResult<T, I>> {
const { _cb, _name } = this._resolveComponent(component);

const newNode = new NodeImpl<UnwrapPNode<T>>();
if (process.env.NODE_ENV !== 'production') {
newNode._uniqueNodeId = _NextUniqueNodeId++;
}

if (!this._currentFrame._announced) {
this._currentFrame._announced = { _head: newNode, _tail: newNode };
Expand All @@ -78,6 +63,12 @@ class Prober<I extends FuncMap> implements IProber {
this._currentFrame._announced._tail = newNode;
}

if (process.env.NODE_ENV !== 'production') {
if (!this._nextNodeId) {
this._nextNodeId = 0;
}
newNode._nodeId = this._nextNodeId++;
}
newNode._buildData = {
_resolveAs: newNode,
_cb,
Expand All @@ -91,13 +82,13 @@ class Prober<I extends FuncMap> implements IProber {
return newNode as AsPNode<ProbedResult<T, I>>;
}

_finalizeNode(node: BaseNode) {
// If a component returns a Node (as opposed to a value), then we short-circuit to the parent.
const bd = node._buildData!;
const destinationNode = bd._resolveAs;
_finalizeNode(node: BaseNode): void {
const buildData = node._buildData!;
const destinationNode = buildData._resolveAs;

this._currentFrame._node = node;
const cbResult = bd._cb(...bd._args, bd._context);
const frame = this._currentFrame;
frame._node = node;
const cbResult = buildData._cb(...buildData._args);

if (isPNode(cbResult)) {
if (cbResult.finalized) {
Expand All @@ -109,25 +100,13 @@ class Prober<I extends FuncMap> implements IProber {
destinationNode._result = cbResult;
}

if (this._currentFrame._disposeOps.length > 0) {
destinationNode._addToDispose(this._currentFrame._disposeOps);
this._currentFrame._disposeOps = [];
}
destinationNode._addToDispose(frame._disposeOps);
frame._disposeOps = [];
}

_finalize(target: IPNode): void {
if (process.env.NODE_ENV === 'check') {
let lookup: BaseNode | undefined;
if (this._currentFrame._announced) {
lookup = this._currentFrame._announced._head;
}
while (lookup && lookup !== target) {
lookup = lookup._buildData!._next;
}
if (lookup !== target) {
throw Error("Can't find target from here.");
}
}
mark(this, 'finalizing');
checkTargetNodeIsReachable(this, target);

let node = this._currentFrame._announced!._head!;
let end = target as BaseNode;
Expand Down Expand Up @@ -164,18 +143,22 @@ class Prober<I extends FuncMap> implements IProber {

this._currentFrame = this._stack.pop()!;
popEnv();

unmark(this, 'finalizing');
}

private _getCb<T extends IKeys<I> | ComponentCb>(what: T): { _cb: ComponentCb; _name: string } {
if (isIntrinsic<I>(what)) {
let _cb: ComponentCb | undefined = this._intrinsics[what];
private _resolveComponent(component: IKeys<I> | ComponentCb): { _cb: ComponentCb; _name: string } {
checkIsValidComponent(this, component);

if (isIntrinsic<I>(component)) {
let _cb: ComponentCb | undefined = this._intrinsics[component];
if (!_cb) {
_cb = this._fallback!;
}

return { _cb: _cb!, _name: what.toString() };
return { _cb: _cb!, _name: component.toString() };
} else {
return { _cb: what as ComponentCb, _name: (what as ComponentCb).name };
return { _cb: component, _name: component.name };
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './dynamic';
export * from './Environment';
export * from './Node';
export * from './Prober';
export * from './userValidation';
31 changes: 31 additions & 0 deletions src/internalInterfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { FuncMap, IKeys, IntrinsicFallback, IPNode } from './ApiTypes';
import { DisposeOp } from './Environment';
import { BaseNode } from './Node';

interface NodeQueue {
_head: BaseNode;
_tail: BaseNode;
}

export interface ProberStackFrame {
_node?: BaseNode;
_disposeOps: DisposeOp[];
_announced?: NodeQueue;
}

export interface IProberBase {
_finalize(target: IPNode): void;
_currentFrame: ProberStackFrame;
}

export interface IProber<I extends FuncMap> {
_intrinsics: Partial<I>;
_fallback?: IntrinsicFallback<I>;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ComponentCb = (...args: any[]) => any;

export const isIntrinsic = <I>(cb: IKeys<I> | ((...args: unknown[]) => unknown)): cb is IKeys<I> => {
return typeof cb === 'string';
};
67 changes: 67 additions & 0 deletions src/internalValidation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright 2021 Francois Chabot
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// ATTENTION: This file does not contribute to coverage testing!

export const INTERNAL_VALIDATION_ENABLED = process.env.PROBED_INTERNAL_VALIDATION === 'ON';

/** Validates the internal state of the library. If one of these fail, then there's a problem with
* either the library itself, or the library's ability to handle invalid usages.
*/
export const assertInternal = (condition: boolean, msg?: string): void => {
if (INTERNAL_VALIDATION_ENABLED && !condition) {
throw new Error(msg || 'Internal consistency failure ');
}
};

const markKey = (key: string): string => {
return '_probed_mark_internal_' + key;
};

//eslint-disable-next-line @typescript-eslint/no-explicit-any
export const assertMarked = (what: Record<string, any>, key: string, msg?: string): void => {
assertInternal(what[markKey(key)] as boolean, msg || 'expecting mark: ' + key);
};

//eslint-disable-next-line @typescript-eslint/no-explicit-any
export const assertUnmarked = (what: Record<string, any>, key: string, msg?: string): void => {
assertInternal(!what[markKey(key)] as boolean, msg || 'unexpected mark: ' + key);
};

//eslint-disable-next-line @typescript-eslint/no-explicit-any
export const mark = (what: Record<string, any>, key: string): void => {
if (INTERNAL_VALIDATION_ENABLED) {
const k = markKey(key);
if (what[k]) {
what[k]++;
} else {
what[k] = 1;
}
}
};

//eslint-disable-next-line @typescript-eslint/no-explicit-any
export const unmark = (what: Record<string, any>, key: string): void => {
if (INTERNAL_VALIDATION_ENABLED) {
assertMarked(what, key);

const k = markKey(key);
what[k] -= 1;
if (what[k] == 0) {
delete what[k];
}
}
};

0 comments on commit 4a715ca

Please sign in to comment.