Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
4 changes: 3 additions & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import { terser } from 'rollup-plugin-terser';

const prod = replace({
preventAssignment: true,
'process.env.NODE_ENV': JSON.stringify('production'),
'process.env.PROBED_INTERNAL_VALIDATION': 'undefined',
'process.env.PROBED_USER_VALIDATION': 'undefined',
});


const cleanup = rollupCleanup({ comments: 'none', extensions: ['.ts', '.js'] });
const typescript = rollupTypescript({ tsconfig: 'tsconfig.prod.json', outDir: './dist' });
const typscriptES5 = rollupTypescript({ target: 'ES5', tsconfig: 'tsconfig.prod.json', outDir: './dist' });
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
124 changes: 54 additions & 70 deletions src/Prober.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,65 +11,51 @@ 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,
USER_VALIDATION_ENABLED,
} 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 +64,12 @@ class Prober<I extends FuncMap> implements IProber {
this._currentFrame._announced._tail = newNode;
}

if (USER_VALIDATION_ENABLED) {
if (!this._nextNodeId) {
this._nextNodeId = 0;
}
newNode._nodeId = this._nextNodeId++;
}
newNode._buildData = {
_resolveAs: newNode,
_cb,
Expand All @@ -91,13 +83,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 +101,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 +144,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
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';
};
Loading