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
10 changes: 8 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,18 @@ jobs:
run_install: |
- args: [--frozen-lockfile, --strict-peer-dependencies]
- name: Unit tests
run: pnpm run test
env:
TEST: FULL
run: pnpm run test:full
- name: Build
run: pnpm run build
- name: Post-build repo check
run: git diff --exit-code
- name: Archive Build artifacts
- name: Coveralls
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Archive Build
uses: actions/upload-artifact@v2
with:
name: dist
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
dist/
lib/
node_modules/
coverage/
34 changes: 33 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export default {
const baseConfig = {
preset: 'ts-jest',
testEnvironment: 'node',
transform: {
Expand All @@ -7,3 +7,35 @@ export default {
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.ts?$',
moduleFileExtensions: ['js', 'ts'],
};

const fullConfig = {
collectCoverage: true,
coverageDirectory: './coverage',
coverageThreshold: {
global: {
branches: 100,
functions: 100,
lines: 100,
statements: 100,
},
},
projects: [
{
displayName: 'prod',
setupFiles: ['./tests/variants/prod.js'],
...baseConfig,
},
{
displayName: 'dev',
setupFiles: ['./tests/variants/dev.js'],
...baseConfig,
},
{
displayName: 'check',
setupFiles: ['./tests/variants/check.js'],
...baseConfig,
},
],
};

export default process.env.TEST === 'FULL' ? fullConfig : baseConfig;
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
"module": "dist/esm/probe-core.js",
"unpkg": "dist/umd/probe-core.es6.js",
"scripts": {
"test": "jest --verbose",
"test:full": "set TEST=FULL&& jest",
"test:quick": "jest",
"test:watch": "jest --watch",
"test": "pnpm run test:quick",
"ci": "pnpm run lint",
"tsc": "tsc",
"lint": "eslint --ext .ts src/",
Expand Down
1 change: 1 addition & 0 deletions src/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export abstract class BaseNode implements IPNode {
_onDispose?: DisposeOp[];

_buildData?: NodeBuildData;
_uniqueNodeId?: number;
}

export class NodeImpl<T> extends BaseNode {
Expand Down
49 changes: 33 additions & 16 deletions src/Prober.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,29 @@ const isIntrinsic = <I>(cb: keyof I | ((...args: any[]) => any)): cb is keyof I
return typeof cb === 'string';
};

const addToDisposeQueue = (node: BaseNode, ops: DisposeOp[]) => {
if (!node._onDispose) {
node._onDispose = ops;
} else {
node._onDispose = node._onDispose.concat(ops);
}
};

let _NextUniqueNodeId = 0;

class Prober<I extends Intrinsics<I>> implements IProber {
private _intrinsics: I;
private _queueHead?: BaseNode;
private _insert?: BaseNode;
private _end?: BaseNode;

private _insertStack: (BaseNode | undefined)[] = [];
private _endStack: (BaseNode | undefined)[] = [];

private _end?: BaseNode;
private _pendingOnDispose: DisposeOp[] = [];
private _finalizeStack: {
_end: BaseNode | undefined;
_pendingOnDispose: DisposeOp[];
}[] = [];

_onDispose(op: DisposeOp): void {
this._pendingOnDispose.push(op);
}
Expand All @@ -38,6 +51,10 @@ class Prober<I extends Intrinsics<I>> implements IProber {
}

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

const _cb = this._getCb(what);
let _next: IPNode | undefined;

Expand All @@ -62,15 +79,14 @@ class Prober<I extends Intrinsics<I>> implements IProber {
_finalize(target: IPNode): void {
// This can be called recursively,
pushEnv(this);
this._endStack.push(this._end);

// We need to loop until target and any node probed while compiling target have been finalized.
// this._end will be updated accordingly inside of announce()
this._finalizeStack.push({ _end: this._end, _pendingOnDispose: this._pendingOnDispose });
this._pendingOnDispose = [];
this._end = target;

let currentNode: BaseNode;
do {
currentNode = this._queueHead!;
this._queueHead = currentNode._buildData!._next;

// If a component returns a Node (as opposed to a value), then we short-circuit to the parent.
let destinationNode = currentNode;
Expand All @@ -86,7 +102,12 @@ class Prober<I extends Intrinsics<I>> implements IProber {

if (isPNode(cbResult)) {
if (cbResult.finalized) {
// Post-ex-facto proxying.
destinationNode._result = cbResult._result;
if (cbResult._onDispose) {
addToDisposeQueue(destinationNode, cbResult._onDispose);
cbResult._onDispose = [];
}
} else {
cbResult._buildData!._resolveAs = destinationNode;
}
Expand All @@ -95,21 +116,17 @@ class Prober<I extends Intrinsics<I>> implements IProber {
}

if (this._pendingOnDispose.length > 0) {
if (!destinationNode._onDispose) {
destinationNode._onDispose = this._pendingOnDispose;
} else {
destinationNode._onDispose = destinationNode._onDispose.concat(this._pendingOnDispose);
}

addToDisposeQueue(destinationNode, this._pendingOnDispose);
this._pendingOnDispose = [];
}

this._queueHead = currentNode._buildData!._next;
currentNode._buildData = undefined;
this._insert = this._insertStack.pop();
} while (currentNode !== this._end);
} while (currentNode !== this._end && this._queueHead);

this._end = this._endStack.pop();
const finalizePop = this._finalizeStack.pop()!;
this._end = finalizePop._end;
this._pendingOnDispose = finalizePop._pendingOnDispose;
popEnv();
}

Expand Down
34 changes: 2 additions & 32 deletions src/dynamic/List.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,7 @@
*/

import { DynamicBaseImpl } from './Base';
import { DynamicValueImpl } from './Value';
import { ListValueType, DynamicValue, DynamicList } from '../ApiTypes';

interface MapCacheEntry<U> {
value: U;
index: number;
indexProp?: DynamicValue<number>;
}
import { ListValueType, DynamicList } from '../ApiTypes';

export class DynamicListImpl<T extends Array<unknown>> extends DynamicBaseImpl<T> implements DynamicList<T> {
push(v: ListValueType<T>): void {
Expand All @@ -31,36 +24,13 @@ export class DynamicListImpl<T extends Array<unknown>> extends DynamicBaseImpl<T
}

map<U>(cb: (value: ListValueType<T>, index: number, array: T) => U): DynamicList<U[]> {
let cache = new Map<ListValueType<T>, MapCacheEntry<U>>();
const indexSensitive = cb.length >= 2;

const regenerate = (newArray: T): U[] => {
const newCache = new Map<ListValueType<T>, MapCacheEntry<U>>();

// For loop instead of map, because this is confusing TypeScript.
const result: U[] = [];
const len = newArray.length;
for (let i = 0; i < len; ++i) {
const v = newArray[i];
const cacheEntry = cache.get(v);
if (cacheEntry) {
if (indexSensitive && i !== cacheEntry.index) {
cacheEntry.indexProp!.current = i;
}
newCache.set(v, cacheEntry);
result.push(cacheEntry.value);
} else {
const value = cb(v, i, newArray);
let indexProp: DynamicValue<number> | undefined = undefined;
if (indexSensitive) {
indexProp = new DynamicValueImpl<number>(i);
}
newCache.set(v, { value, index: i, indexProp });
result.push(value);
}
result.push(cb(newArray[i], i, newArray));
}

cache = newCache;
return result;
};

Expand Down
1 change: 1 addition & 0 deletions tests/dynamicList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,5 @@ describe('Dynamic Array', () => {
x.current = [2, 2, 2];
expect(y.current).toEqual([4, 4, 4]);
});

});
69 changes: 69 additions & 0 deletions tests/dynamicOperations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* 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.
*/

import { DisposeOp, popEnv, pushEnv } from '../src/Environment';

import { dynamic, listen, valType } from '../src';

let disposeQueue: DisposeOp[] = [];
const cleanup = () => {
disposeQueue.forEach((v) => v());
disposeQueue = [];
};

beforeEach(() => {
pushEnv({
_onDispose: (op: DisposeOp) => disposeQueue.push(op),
});
});

afterEach(() => {
cleanup();
popEnv();
});

describe('listen', () => {
it('Works on regular values', () => {
let y = 0;
const cb = (v: number) => (y += v);
const v = 12;
listen(v, cb);
expect(y).toBe(12);
});

it('Works on dynamic values', () => {
let y = 0;
const cb = (v: number) => (y += v);
const v = dynamic(12);
listen(v, cb);
expect(y).toBe(12);

v.current = 13;
expect(y).toBe(25);
});
});

describe('valType', () => {
it('Works on regular values', () => {
const v = 0;
expect(valType(v)).toBe('number');
});

it('Works on dynamic values', () => {
const v = dynamic(0);
expect(valType(v)).toBe('number');
});
});
16 changes: 14 additions & 2 deletions tests/dynamicVal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,10 @@ describe('Dynamic Value', () => {
const x = dynamic(12);

let y = 0;
x.addListener(() => {
const listener = () => {
y += 1;
});
};
x.addListener(listener);

x.current = 23;
expect(y).toBe(1);
Expand All @@ -85,6 +86,17 @@ describe('Dynamic Value', () => {
x.current = 23;
expect(y).toBe(1);
expect(x.valueOf()).toBe(23);

// Setting a different value triggers notification again.
x.current = 24;
expect(y).toBe(2);
expect(x.valueOf()).toBe(24);

// Removing the listener cancels notifications.
x.removeListener(listener);
x.current = 12;
expect(y).toBe(2);
expect(x.valueOf()).toBe(12);
});

it('Cleans up correctly', () => {
Expand Down
Loading