From ccfe1945db9ec980dcddc034139659ddd49e243b Mon Sep 17 00:00:00 2001 From: Frank Wagner Date: Wed, 24 Mar 2021 13:41:25 +0100 Subject: [PATCH 1/8] fix raising errors from JSON parse to be strings --- core/src/TeaCup/Decode.test.ts | 260 +++++++++++++++++---------------- core/src/TeaCup/Decode.ts | 58 ++++---- 2 files changed, 168 insertions(+), 150 deletions(-) diff --git a/core/src/TeaCup/Decode.test.ts b/core/src/TeaCup/Decode.test.ts index cb7cadb..771b813 100644 --- a/core/src/TeaCup/Decode.test.ts +++ b/core/src/TeaCup/Decode.test.ts @@ -29,6 +29,10 @@ import { just, nothing } from './Maybe'; const num = Decode.num; const field = Decode.field; +test('syntax error', () => { + expect(num.decodeString(' { broken ')).toEqual(err('Unexpected token b in JSON at position 3')); +}); + test('primitives', () => { expect(num.decodeValue(1)).toEqual(ok(1)); expect(num.decodeValue('yeah')).toEqual(err('value is not a number : "yeah"')); @@ -154,82 +158,91 @@ test('map8', () => { field('h', num), ).decodeValue(o), ).toEqual(ok(o)); - }); describe('mapObject', () => { type MyType = { - foo: string, - bar: number + foo: string; + bar: number; }; const expected: MyType = { foo: 'a foo', - bar: 13 - } + bar: 13, + }; test('simple', () => { - const value = { foo: 'a foo', bar: 13 } - expect(Decode.mapObject({ - foo: Decode.field('foo', Decode.str), - bar: Decode.field('bar', Decode.num) - }).decodeValue(value)).toEqual(ok(expected)); - }) - + const value = { foo: 'a foo', bar: 13 }; + expect( + Decode.mapObject({ + foo: Decode.field('foo', Decode.str), + bar: Decode.field('bar', Decode.num), + }).decodeValue(value), + ).toEqual(ok(expected)); + }); test('simpler', () => { - const value = { foo: 'a foo', bar: 13 } - expect(Decode.mapObject(Decode.mapRequiredFields({ - foo: Decode.str, - bar: Decode.num - })).decodeValue(value)).toEqual(ok(expected)); - }) - + const value = { foo: 'a foo', bar: 13 }; + expect( + Decode.mapObject( + Decode.mapRequiredFields({ + foo: Decode.str, + bar: Decode.num, + }), + ).decodeValue(value), + ).toEqual(ok(expected)); + }); test('missing field', () => { - const value = { foo: 'a foo' } - expect(Decode.mapObject({ - foo: Decode.field('foo', Decode.str), - bar: Decode.field('bar', Decode.num) - }).decodeValue(value)).toEqual(err('path not found [bar] on {"foo":"a foo"}')); - }) + const value = { foo: 'a foo' }; + expect( + Decode.mapObject({ + foo: Decode.field('foo', Decode.str), + bar: Decode.field('bar', Decode.num), + }).decodeValue(value), + ).toEqual(err('path not found [bar] on {"foo":"a foo"}')); + }); test('superfluous field', () => { - const value = { foo: 'a foo', bar: 13, toto: true } - expect(Decode.mapObject({ - foo: Decode.field('foo', Decode.str), - bar: Decode.field('bar', Decode.num) - }).decodeValue(value)).toEqual(ok(expected)); - }) + const value = { foo: 'a foo', bar: 13, toto: true }; + expect( + Decode.mapObject({ + foo: Decode.field('foo', Decode.str), + bar: Decode.field('bar', Decode.num), + }).decodeValue(value), + ).toEqual(ok(expected)); + }); test('optional field', () => { type MyType2 = { - foo: string, - bar?: number + foo: string; + bar?: number; }; const expected: MyType2 = { foo: 'a foo', - } + }; - const value = { foo: 'a foo', toto: true } - expect(Decode.mapObject({ - foo: Decode.field('foo', Decode.str), - bar: Decode.optionalField('bar', Decode.num) - }).decodeValue(value)).toEqual(ok(expected)); + const value = { foo: 'a foo', toto: true }; + expect( + Decode.mapObject({ + foo: Decode.field('foo', Decode.str), + bar: Decode.optionalField('bar', Decode.num), + }).decodeValue(value), + ).toEqual(ok(expected)); // the type system will compile fail this test: // expect(Decode.mapObject({ // foo: Decode.field('foo', Decode.str), // }).decodeValue(value)).toEqual(ok(expected)); - }) + }); test('simpler optional field', () => { type MyType2 = { - foo: string, - bar?: number + foo: string; + bar?: number; }; const expected: MyType2 = { foo: 'a foo', - } + }; const decoder: DecoderObject = { ...Decode.mapRequiredFields({ @@ -237,12 +250,12 @@ describe('mapObject', () => { }), ...Decode.mapOptionalFields({ bar: Decode.num, - }) - } + }), + }; - const value = { foo: 'a foo', toto: true } + const value = { foo: 'a foo', toto: true }; expect(Decode.mapObject(decoder).decodeValue(value)).toEqual(ok(expected)); - }) + }); it('decode array of mapObject', () => { type MyItem = { gnu: number; foo: string }; @@ -268,30 +281,23 @@ describe('mapObject', () => { const r = Decode.array(MyItemDecoder).decodeValue(payload); expect(r).toEqual(ok(payload)); }); -}) +}); describe('mapArray', () => { - type MyType = [ - string, - number - ] - const expected: MyType = [ - 'a foo', - 13 - ] + type MyType = [string, number]; + const expected: MyType = ['a foo', 13]; test('simple', () => { - type ValueType = [string, number] - const value: ValueType = ['a foo', 13] - expect(Decode.mapTuple([ - Decode.str, - Decode.num - ]).decodeValue(value)).toEqual(ok(expected)); - }) + type ValueType = [string, number]; + const value: ValueType = ['a foo', 13]; + expect( + Decode.mapTuple([Decode.str, Decode.num]).decodeValue(value), + ).toEqual(ok(expected)); + }); test('type mismatch', () => { - type ValueType = [string, number] - const value: ValueType = ['a foo', 13] + type ValueType = [string, number]; + const value: ValueType = ['a foo', 13]; // the type system will compile fail this test: // expect(Decode.mapArray([ @@ -300,38 +306,33 @@ describe('mapArray', () => { // ]).decodeValue(value)).toEqual(err('ran into decoder error at [1] : value is not a string : 13')); // the type system will let though to runtime: - expect(Decode.mapTuple([ - Decode.str, - Decode.str - ]).decodeValue(value)).toEqual(err('ran into decoder error at [1] : value is not a string : 13')); - }) + expect(Decode.mapTuple([Decode.str, Decode.str]).decodeValue(value)).toEqual( + err('ran into decoder error at [1] : value is not a string : 13'), + ); + }); test('missing item', () => { - type ValueType = [string, number] + type ValueType = [string, number]; // the type system will compile fail this test: // const value: ValueType = ['a foo'] // the type system will let though to runtime: - const value = ['a foo'] - expect(Decode.mapTuple([ - Decode.str, - Decode.num - ]).decodeValue(value)).toEqual(err('path not found [1] on [\"a foo\"]')); - }) + const value = ['a foo']; + expect(Decode.mapTuple([Decode.str, Decode.num]).decodeValue(value)).toEqual( + err('path not found [1] on ["a foo"]'), + ); + }); test('too many items', () => { - type ValueType = [string, number] + type ValueType = [string, number]; // the type system will compile fail this test: // const value: ValueType = ['a foo', 13, true] // the type system will let though to runtime: - const value = ['a foo', 13, true] - expect(Decode.mapTuple([ - Decode.str, - Decode.num - ]).decodeValue(value)).toEqual(ok(expected)); - }) -}) + const value = ['a foo', 13, true]; + expect(Decode.mapTuple([Decode.str, Decode.num]).decodeValue(value)).toEqual(ok(expected)); + }); +}); test('andThen', () => { type Stuff = { readonly tag: 'stuff1'; readonly foo: string } | { readonly tag: 'stuff2'; readonly bar: string }; @@ -444,78 +445,87 @@ test('any value', () => { }); describe('optional field', () => { - test("is present", () => { + test('is present', () => { const value = { foo: 'bar', gnu: 13 }; expect(Decode.optionalField('gnu', Decode.num).decodeValue(value)).toEqual(ok(13)); - }) - test("is missing", () => { + }); + test('is missing', () => { const value = { foo: 'bar' }; expect(Decode.optionalField('gnu', Decode.num).decodeValue(value)).toEqual(ok(undefined)); - }) + }); - test("typical use case", () => { + test('typical use case', () => { type MyType = { foo: string; gnu?: number; - } + }; const value = { foo: 'bar' }; const expected: MyType = { - foo: 'bar' + foo: 'bar', }; - expect(Decode.map2( - (foo, gnu) => { return { foo, gnu } }, - Decode.field('foo', Decode.str), - Decode.optionalField('gnu', Decode.num)).decodeValue(value) + expect( + Decode.map2( + (foo, gnu) => { + return { foo, gnu }; + }, + Decode.field('foo', Decode.str), + Decode.optionalField('gnu', Decode.num), + ).decodeValue(value), ).toEqual(ok(expected)); - }) + }); test('simpler optional field', () => { type MyType2 = { - foo: string, - bar?: number + foo: string; + bar?: number; }; const expected: MyType2 = { foo: 'a foo', - } + }; - const value = { foo: 'a foo', toto: true } - expect(Decode.mapObject({ - ...Decode.mapRequiredFields({ - foo: Decode.str, - }), - bar: Decode.optionalField('bar', Decode.num) - }).decodeValue(value)).toEqual(ok(expected)); - }) + const value = { foo: 'a foo', toto: true }; + expect( + Decode.mapObject({ + ...Decode.mapRequiredFields({ + foo: Decode.str, + }), + bar: Decode.optionalField('bar', Decode.num), + }).decodeValue(value), + ).toEqual(ok(expected)); + }); }); describe('null types', () => { - test("non null value", () => { + test('non null value', () => { const value = { foo: 'bar' }; - const result: Result = Decode.orNull(Decode.field('foo', Decode.str)).decodeValue(value) + const result: Result = Decode.orNull(Decode.field('foo', Decode.str)).decodeValue(value); expect(result).toEqual(ok('bar')); - }) + }); - test("null value", () => { + test('null value', () => { const value = { foo: null }; - const result: Result = (Decode.field('foo', Decode.orNull(Decode.str))).decodeValue(value) + const result: Result = Decode.field('foo', Decode.orNull(Decode.str)).decodeValue(value); expect(result).toEqual(ok(null)); - }) + }); - test("typical use case", () => { + test('typical use case', () => { type MyType = { gnu: number | null; - foo: string | null - } + foo: string | null; + }; const value = { foo: null, gnu: null }; const expected: MyType = { foo: null, - gnu: null + gnu: null, }; - expect(Decode.map2( - (foo, gnu) => { return { foo, gnu } }, - Decode.field('foo', Decode.orNull(Decode.str)), - Decode.field('gnu', Decode.orNull(Decode.num))) - .decodeValue(value) + expect( + Decode.map2( + (foo, gnu) => { + return { foo, gnu }; + }, + Decode.field('foo', Decode.orNull(Decode.str)), + Decode.field('gnu', Decode.orNull(Decode.num)), + ).decodeValue(value), ).toEqual(ok(expected)); - }) -}) + }); +}); diff --git a/core/src/TeaCup/Decode.ts b/core/src/TeaCup/Decode.ts index b20769d..a91fbb4 100644 --- a/core/src/TeaCup/Decode.ts +++ b/core/src/TeaCup/Decode.ts @@ -46,7 +46,7 @@ export class Decoder { const o = JSON.parse(s); return this.decodeValue(o); } catch (e) { - return err(e); + return err(e.message ?? 'unknown JSON error'); } } @@ -138,7 +138,7 @@ export class Decode { * @param d the decoder to be used if the value is not null */ static orNull(d: Decoder): Decoder { - return this.map(v => v.map(v => v).withDefault(null), this.nullable(d)); + return this.map((v) => v.map((v) => v).withDefault(null), this.nullable(d)); } /** @@ -193,8 +193,13 @@ export class Decode { */ static optionalField(key: string, d: Decoder): Decoder { return Decode.andThen( - (value) => value.map>(v => new Decoder(() => d.decodeValue(v))).withDefault(Decode.succeed(undefined)), - Decode.maybe(Decode.field(key, Decode.value)) + (value) => + value + .map>( + (v) => new Decoder(() => d.decodeValue(v)), + ) + .withDefault(Decode.succeed(undefined)), + Decode.maybe(Decode.field(key, Decode.value)), ); } @@ -449,16 +454,18 @@ export class Decode { * @param dobject an object with decoders */ static mapObject(dobject: DecoderObject): Decoder { - const keys = Object.keys(dobject) as Array + const keys = Object.keys(dobject) as Array; return new Decoder((value: any) => keys.reduce((acc, key) => { const propertyDecoder = getProperty(dobject, key); const v = propertyDecoder.decodeValue(value); - return v.andThen(v => acc.map(acc => { - acc[key] = v; - return acc; - })) - }, ok({} as T)) + return v.andThen((v) => + acc.map((acc) => { + acc[key] = v; + return acc; + }), + ); + }, ok({} as T)), ); } @@ -468,13 +475,13 @@ export class Decode { * @param fun the mapper function */ static mapFields(decoders: DecoderObject, fun: DecoderObjectMapper): DecoderObject { - const keys = Object.keys(decoders) as Array + const keys = Object.keys(decoders) as Array; const partial: Partial> = keys.reduce((acc, key) => { const propertyDecoder = getProperty(decoders, key); const [key2, propertyDecoder2] = fun(key, propertyDecoder); acc[key2] = propertyDecoder2; return acc; - }, {} as Partial>) + }, {} as Partial>); return partial as DecoderObject; } @@ -487,12 +494,14 @@ export class Decode { } /** - * Convenience, map decoders to optional field decoders - * @param decoders an object with decoders - */ + * Convenience, map decoders to optional field decoders + * @param decoders an object with decoders + */ static mapOptionalFields(decoders: DecoderObject): DecoderObject> { - const mapper: DecoderObjectMapper> = - (k: keyof T, d: Decoder) => [k, Decode.optionalField(k as string, d)] + const mapper: DecoderObjectMapper> = (k: keyof T, d: Decoder) => [ + k, + Decode.optionalField(k as string, d), + ]; return this.mapFields(decoders, mapper); } @@ -501,7 +510,7 @@ export class Decode { * @param decoders an array with decoders */ static mapTuple(decoders: DecoderArray): Decoder { - return Decode.map(v => Object.values(v) as T, this.mapObject(this.mapRequiredFields(decoders))); + return Decode.map((v) => Object.values(v) as T, this.mapObject(this.mapRequiredFields(decoders))); } // Fancy Decoding @@ -575,18 +584,17 @@ export class Decode { } } - function getProperty(o: T, key: K): T[K] { return o[key]; } -export type DecoderObject = Required<{ [P in keyof T]: Decoder }> -export type DecoderArray = Required<{ [P in keyof A]: A[P] extends A[number] ? Decoder : never }> +export type DecoderObject = Required<{ [P in keyof T]: Decoder }>; +export type DecoderArray = Required< + { [P in keyof A]: A[P] extends A[number] ? Decoder : never } +>; export type OptionalFields = { - [P in keyof T]: (T[P] | undefined); + [P in keyof T]: T[P] | undefined; }; -export type DecoderObjectMapper = - (k: keyof T, d: Decoder) => [keyof T2, Decoder] - +export type DecoderObjectMapper = (k: keyof T, d: Decoder) => [keyof T2, Decoder]; From 8303abbfc9ce31266dd676ed272ec3d7cbd0838d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Apr 2021 06:30:11 +0000 Subject: [PATCH 2/8] Bump y18n from 3.2.1 to 3.2.2 Bumps [y18n](https://github.com/yargs/y18n) from 3.2.1 to 3.2.2. - [Release notes](https://github.com/yargs/y18n/releases) - [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md) - [Commits](https://github.com/yargs/y18n/commits) Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index feb68cc..019b80f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13207,9 +13207,9 @@ xtend@^4.0.0, xtend@~4.0.1: integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== y18n@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" - integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= + version "3.2.2" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.2.tgz#85c901bd6470ce71fc4bb723ad209b70f7f28696" + integrity sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ== y18n@^4.0.0: version "4.0.0" From f52cb8a649e7e16ece5b812dde9dbadffc8fc9c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Van=20Keisbelck?= Date: Mon, 19 Apr 2021 15:59:45 +0200 Subject: [PATCH 3/8] performance improvements : * do nothing if CmdNone * do not forceUpdate if updated model is ref equals updated model. --- core/src/TeaCup/Cmd.ts | 3 ++- tea-cup/src/TeaCup/Program.ts | 23 +++++++++++++++-------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/core/src/TeaCup/Cmd.ts b/core/src/TeaCup/Cmd.ts index d59931c..34abf44 100644 --- a/core/src/TeaCup/Cmd.ts +++ b/core/src/TeaCup/Cmd.ts @@ -63,7 +63,8 @@ export abstract class Cmd { /** * A command that does nothing. */ -class CmdNone extends Cmd { +// exported for perf optimisation reasons +export class CmdNone extends Cmd { execute(dispatch: Dispatcher): void { // it's a noop ! } diff --git a/tea-cup/src/TeaCup/Program.ts b/tea-cup/src/TeaCup/Program.ts index 85740a1..9258c4d 100644 --- a/tea-cup/src/TeaCup/Program.ts +++ b/tea-cup/src/TeaCup/Program.ts @@ -24,7 +24,7 @@ */ import { Component, ReactNode } from 'react'; -import { Dispatcher, Cmd, Sub, nextUuid } from 'tea-cup-core'; +import { Dispatcher, Cmd, Sub, nextUuid, CmdNone } from 'tea-cup-core'; import { DevToolsEvent, DevTools } from './DevTools'; /** @@ -93,18 +93,25 @@ export class Program extends Component, nev // perform commands in a separate timout, to // make sure that this dispatch is done - setTimeout(() => { - // console.log("dispatch: processing commands"); - // debug("performing command", updated[1]); - updated[1].execute(d); - // debug("<<< done"); - }, 0); + const cmd = updated[1]; + if (!(cmd instanceof CmdNone)) { + setTimeout(() => { + // console.log("dispatch: processing commands"); + // debug("performing command", updated[1]); + updated[1].execute(d); + // debug("<<< done"); + }, 0); + } + + const needsUpdate = this.currentModel === updated[0]; this.currentModel = updated[0]; this.currentSub = newSub; // trigger rendering - this.forceUpdate(); + if (needsUpdate) { + this.forceUpdate(); + } } } From 0be35f49e899f7a3752bd17e2aaa4f4dba72ff40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Van=20Keisbelck?= Date: Mon, 19 Apr 2021 16:12:01 +0200 Subject: [PATCH 4/8] prettify --- core/jest.config.js | 4 +- core/src/TeaCup/Either.test.ts | 4 +- core/src/TeaCup/Either.ts | 2 +- core/src/TeaCup/Maybe.test.ts | 6 +- core/src/TeaCup/Maybe.ts | 3 +- core/src/TeaCup/Port.ts | 18 +- core/src/TeaCup/Task.ts | 10 +- core/src/TeaCup/UUID.ts | 2 +- tea-cup/package.json | 3 +- tea-cup/src/TeaCup/DevTools.ts | 2 +- tea-cup/src/TeaCup/DocumentEvents.test.tsx | 186 ++++++++++----------- tea-cup/src/TeaCup/DocumentEvents.ts | 64 +++---- tea-cup/src/TeaCup/Testing.tsx | 104 +++++++----- 13 files changed, 213 insertions(+), 195 deletions(-) diff --git a/core/jest.config.js b/core/jest.config.js index 5dda4bf..d17cf18 100644 --- a/core/jest.config.js +++ b/core/jest.config.js @@ -27,7 +27,5 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', testPathIgnorePatterns: ['node_modules', 'samples'], - testMatch: [ - '**/*.test.ts' - ], + testMatch: ['**/*.test.ts'], }; diff --git a/core/src/TeaCup/Either.test.ts b/core/src/TeaCup/Either.test.ts index d23225d..c50f502 100644 --- a/core/src/TeaCup/Either.test.ts +++ b/core/src/TeaCup/Either.test.ts @@ -29,7 +29,7 @@ test('left', () => { const e: Either = left('yeah'); expect(e.isLeft()).toBe(true); expect(e.isRight()).toBe(false); - expect(e.left.map(s => s + '!').withDefault('')).toBe('yeah!'); + expect(e.left.map((s) => s + '!').withDefault('')).toBe('yeah!'); expect(e.right.withDefault(123)).toBe(123); }); @@ -38,7 +38,7 @@ test('right', () => { expect(e.isLeft()).toBe(false); expect(e.isRight()).toBe(true); expect(e.left.withDefault('!!!')).toBe('!!!'); - expect(e.right.map(x => x + 1).withDefault(456)).toBe(124); + expect(e.right.map((x) => x + 1).withDefault(456)).toBe(124); }); test('mapLeft', () => { diff --git a/core/src/TeaCup/Either.ts b/core/src/TeaCup/Either.ts index b90eefe..8e719fb 100644 --- a/core/src/TeaCup/Either.ts +++ b/core/src/TeaCup/Either.ts @@ -23,7 +23,7 @@ * */ -import {just, Maybe, nothing} from "./Maybe"; +import { just, Maybe, nothing } from './Maybe'; /** * Either left, or right. diff --git a/core/src/TeaCup/Maybe.test.ts b/core/src/TeaCup/Maybe.test.ts index b7fee7c..ba9bfbf 100644 --- a/core/src/TeaCup/Maybe.test.ts +++ b/core/src/TeaCup/Maybe.test.ts @@ -142,6 +142,6 @@ test('isNothing', () => { test('filter', () => { const m: Maybe = just(123); - expect(m.filter(x => x === 123).isJust()).toBe(true); - expect(m.filter(x => x === 456).isJust()).toBe(false); -}) + expect(m.filter((x) => x === 123).isJust()).toBe(true); + expect(m.filter((x) => x === 456).isJust()).toBe(false); +}); diff --git a/core/src/TeaCup/Maybe.ts b/core/src/TeaCup/Maybe.ts index 8bd5108..aca5fae 100644 --- a/core/src/TeaCup/Maybe.ts +++ b/core/src/TeaCup/Maybe.ts @@ -79,7 +79,7 @@ export class Just { return false; } - filter(f:(t:T) => boolean): Maybe { + filter(f: (t: T) => boolean): Maybe { if (f(this.value)) { return this; } @@ -145,7 +145,6 @@ export class Nothing { filter(): Maybe { return nothing; } - } /** diff --git a/core/src/TeaCup/Port.ts b/core/src/TeaCup/Port.ts index 736e8e3..b8cbec2 100644 --- a/core/src/TeaCup/Port.ts +++ b/core/src/TeaCup/Port.ts @@ -23,7 +23,7 @@ * */ -import {Sub} from "./Sub"; +import { Sub } from './Sub'; export class Port { private subs: PortSub[] = []; @@ -34,20 +34,20 @@ export class Port { subscribe(f: (t: T) => M): Sub { return new PortSub( - f, - (p) => this.subs.push(p), - (p) => { - this.subs = this.subs.filter((x) => x !== p); - }, + f, + (p) => this.subs.push(p), + (p) => { + this.subs = this.subs.filter((x) => x !== p); + }, ); } } class PortSub extends Sub { constructor( - private readonly f: (t: T) => M, - private readonly _onInit: (p: PortSub) => void, - private readonly _onRelease: (p: PortSub) => void, + private readonly f: (t: T) => M, + private readonly _onInit: (p: PortSub) => void, + private readonly _onRelease: (p: PortSub) => void, ) { super(); } diff --git a/core/src/TeaCup/Task.ts b/core/src/TeaCup/Task.ts index e19e33c..1f0e884 100644 --- a/core/src/TeaCup/Task.ts +++ b/core/src/TeaCup/Task.ts @@ -151,10 +151,12 @@ class TRecover extends Task { } execute(callback: (r: Result) => void): void { - this.t.execute((tRes) => tRes.match( - tOk => callback(ok(tOk)), - tErr => callback(ok(this.f(tErr))) - )); + this.t.execute((tRes) => + tRes.match( + (tOk) => callback(ok(tOk)), + (tErr) => callback(ok(this.f(tErr))), + ), + ); } } diff --git a/core/src/TeaCup/UUID.ts b/core/src/TeaCup/UUID.ts index e5f1892..ab37f85 100644 --- a/core/src/TeaCup/UUID.ts +++ b/core/src/TeaCup/UUID.ts @@ -23,7 +23,7 @@ * */ -import {Task} from "./Task"; +import { Task } from './Task'; /** * Generate a UUID. Side effect, use uuid() that returns a Task instead diff --git a/tea-cup/package.json b/tea-cup/package.json index 1e25002..faa5a0f 100644 --- a/tea-cup/package.json +++ b/tea-cup/package.json @@ -19,8 +19,7 @@ "compile": "rimraf dist && tsc", "samples": "tsc --outFile ./dist/Samples/index.js && cp ./src/Samples/index.html ./dist/Samples" }, - "dependencies": { - }, + "dependencies": {}, "peerDependencies": { "react": "^16.7.0", "tea-cup-core": "^2.0.0" diff --git a/tea-cup/src/TeaCup/DevTools.ts b/tea-cup/src/TeaCup/DevTools.ts index 0ac2862..15300a3 100644 --- a/tea-cup/src/TeaCup/DevTools.ts +++ b/tea-cup/src/TeaCup/DevTools.ts @@ -200,7 +200,7 @@ export class DevTools { this.resume(); } this.events = []; - console.log("All events cleared") + console.log('All events cleared'); } setMaxEvents(maxEvents: number): DevTools { diff --git a/tea-cup/src/TeaCup/DocumentEvents.test.tsx b/tea-cup/src/TeaCup/DocumentEvents.test.tsx index 3c88ffd..9256e60 100644 --- a/tea-cup/src/TeaCup/DocumentEvents.test.tsx +++ b/tea-cup/src/TeaCup/DocumentEvents.test.tsx @@ -23,129 +23,125 @@ * */ -import { JSDOM } from "jsdom" -import { DocumentEvents } from "./DocumentEvents"; +import { JSDOM } from 'jsdom'; +import { DocumentEvents } from './DocumentEvents'; -const dom = new JSDOM() -global.document = dom.window.document +const dom = new JSDOM(); +global.document = dom.window.document; -describe("DocumentEvents Test", () => { +describe('DocumentEvents Test', () => { + let documentEvents = new DocumentEvents(); - let documentEvents = new DocumentEvents(); + const addSpy = jest.fn(); + const removeSpy = jest.fn(); - const addSpy = jest.fn() - const removeSpy = jest.fn() + document.addEventListener = addSpy; + document.removeEventListener = removeSpy; - document.addEventListener = addSpy - document.removeEventListener = removeSpy + beforeEach(() => { + documentEvents = new DocumentEvents(); + addSpy.mockReset(); + removeSpy.mockReset(); + }); - beforeEach(() => { - documentEvents = new DocumentEvents(); - addSpy.mockReset(); - removeSpy.mockReset(); - }) + it('first sub adds listener', () => { + const sub = documentEvents.on('click', (e) => 'clicked'); + sub.init(() => ({})); - it("first sub adds listener", () => { - const sub = documentEvents.on('click', (e) => 'clicked'); - sub.init(() => ({})) + expect(addSpy.mock.calls.length).toBe(1); + }); - expect(addSpy.mock.calls.length).toBe(1) - }) + it('second sub adds no listener', () => { + const sub = documentEvents.on('click', (e) => 'clicked'); + sub.init(() => ({})); - it("second sub adds no listener", () => { - const sub = documentEvents.on('click', (e) => 'clicked'); - sub.init(() => ({})) + expect(addSpy.mock.calls.length).toBe(1); - expect(addSpy.mock.calls.length).toBe(1) + const sub2 = documentEvents.on('click', (e) => 'clicked2'); + sub2.init(() => ({})); - const sub2 = documentEvents.on('click', (e) => 'clicked2'); - sub2.init(() => ({})) + expect(addSpy.mock.calls.length).toBe(1); + }); - expect(addSpy.mock.calls.length).toBe(1) - }) + it('last sub removes listener', () => { + const sub = documentEvents.on('click', (e) => 'clicked'); + sub.init(() => ({})); - it("last sub removes listener", () => { - const sub = documentEvents.on('click', (e) => 'clicked'); - sub.init(() => ({})) + const sub2 = documentEvents.on('click', (e) => 'clicked2'); + sub2.init(() => ({})); - const sub2 = documentEvents.on('click', (e) => 'clicked2'); - sub2.init(() => ({})) + sub.release(); + expect(removeSpy.mock.calls.length).toBe(0); - sub.release() - expect(removeSpy.mock.calls.length).toBe(0) + sub2.release(); + expect(removeSpy.mock.calls.length).toBe(1); + }); - sub2.release() - expect(removeSpy.mock.calls.length).toBe(1) - }) + it('sub receives event from listener', () => { + const msgs: string[] = []; + const collectMsgs = (msg: string): void => { + msgs.push(msg); + }; - it("sub receives event from listener", () => { - const msgs: string[] = []; - const collectMsgs = (msg: string): void => { - msgs.push(msg); - }; + const sub = documentEvents.on('click', (e) => 'clicked1'); + sub.init(collectMsgs); - const sub = documentEvents.on('click', (e) => 'clicked1'); - sub.init(collectMsgs); + expect(addSpy.mock.calls.length).toBe(1); + const listener = addSpy.mock.calls[0][1]; - expect(addSpy.mock.calls.length).toBe(1) - const listener = addSpy.mock.calls[0][1]; + listener({ event: 'event' }); + expect(msgs).toEqual(['clicked1']); + }); - listener({ event: 'event' }); - expect(msgs).toEqual(['clicked1']); - }) + it('two subs receive events from listener', () => { + const msgs: string[] = []; + const collectMsgs = (msg: string): void => { + msgs.push(msg); + }; + const sub = documentEvents.on('click', (e) => 'clicked1'); + const sub2 = documentEvents.on('click', (e) => 'clicked2'); - it("two subs receive events from listener", () => { - const msgs: string[] = []; - const collectMsgs = (msg: string): void => { - msgs.push(msg); - }; + sub.init(collectMsgs); + sub2.init(collectMsgs); - const sub = documentEvents.on('click', (e) => 'clicked1'); - const sub2 = documentEvents.on('click', (e) => 'clicked2'); + expect(addSpy.mock.calls.length).toBe(1); + const listener = addSpy.mock.calls[0][1]; - sub.init(collectMsgs); - sub2.init(collectMsgs); + listener({ event: 'event' }); + expect(msgs).toEqual(['clicked1', 'clicked2']); + }); - expect(addSpy.mock.calls.length).toBe(1) - const listener = addSpy.mock.calls[0][1]; + it('sub stops receiving events from listener', () => { + const sub = documentEvents.on('click', (e) => 'clicked1'); - listener({ event: 'event' }); - expect(msgs).toEqual(['clicked1', 'clicked2']); - }) + const msgs: string[] = []; + sub.init((msg) => { + msgs.push(msg); + }); + expect(addSpy.mock.calls.length).toBe(1); + const listener = addSpy.mock.calls[0][1]; - it("sub stops receiving events from listener", () => { - const sub = documentEvents.on('click', (e) => 'clicked1'); + listener({ event: 'event' }); + listener({ event: 'event' }); + expect(msgs).toEqual(['clicked1', 'clicked1']); - const msgs: string[] = []; - sub.init((msg) => { - msgs.push(msg) - }); + sub.release(); + listener({ event: 'event' }); + expect(msgs).toEqual(['clicked1', 'clicked1']); + }); - expect(addSpy.mock.calls.length).toBe(1) - const listener = addSpy.mock.calls[0][1]; + it('release removes all listeners', () => { + const sub = documentEvents.on('click', (e) => 'clicked1'); + sub.init(() => ({})); + const sub2 = documentEvents.on('click', (e) => 'clicked2'); + sub2.init(() => ({})); + const sub3 = documentEvents.on('mousemove', (e) => 'moved3'); + sub3.init(() => ({})); + expect(addSpy.mock.calls.length).toBe(2); - listener({ event: 'event' }); - listener({ event: 'event' }); - expect(msgs).toEqual(['clicked1', 'clicked1']); - - sub.release(); - listener({ event: 'event' }); - expect(msgs).toEqual(['clicked1', 'clicked1']); - }) - - it("release removes all listeners", () => { - const sub = documentEvents.on('click', (e) => 'clicked1'); - sub.init(() => ({})) - const sub2 = documentEvents.on('click', (e) => 'clicked2'); - sub2.init(() => ({})) - const sub3 = documentEvents.on('mousemove', (e) => 'moved3'); - sub3.init(() => ({})) - expect(addSpy.mock.calls.length).toBe(2) - - documentEvents.release(); - expect(removeSpy.mock.calls.length).toBe(2) - }) - -}) \ No newline at end of file + documentEvents.release(); + expect(removeSpy.mock.calls.length).toBe(2); + }); +}); diff --git a/tea-cup/src/TeaCup/DocumentEvents.ts b/tea-cup/src/TeaCup/DocumentEvents.ts index dda85b7..3588f8f 100644 --- a/tea-cup/src/TeaCup/DocumentEvents.ts +++ b/tea-cup/src/TeaCup/DocumentEvents.ts @@ -28,8 +28,8 @@ import { Sub } from 'tea-cup-core'; type Listener = (ev: E) => any; type ListenerMap = { - [K in keyof T]?: Listener -} + [K in keyof T]?: Listener; +}; function setListener(o: ListenerMap, k: K, v: Listener | undefined) { o[k] = v; @@ -40,17 +40,23 @@ function getListener(o: ListenerMap, k: K): Listener = { - [K in keyof Map]?: ReadonlyArray> -} + [K in keyof Map]?: ReadonlyArray>; +}; -function addOneSub(sub: DocSub, subs: SubsMap[K]): SubsMap[K] { +function addOneSub( + sub: DocSub, + subs: SubsMap[K], +): SubsMap[K] { const result = new Array().concat(subs); result.push(sub); return result; } -function removeOneSub(sub: DocSub, subs: SubsMap[K]): SubsMap[K] | undefined { - const result = new Array().concat(subs).filter(s => s !== sub); +function removeOneSub( + sub: DocSub, + subs: SubsMap[K], +): SubsMap[K] | undefined { + const result = new Array().concat(subs).filter((s) => s !== sub); return result.length !== 0 ? result : undefined; } @@ -63,18 +69,17 @@ function setSubs(o: SubsMap, k: K, v: S } class DocSub extends Sub { - constructor( private readonly documentEvents: EventMapEvents, private readonly key: K, - private readonly mapper: (t: Map[K]) => Msg + private readonly mapper: (t: Map[K]) => Msg, ) { super(); } protected onInit() { super.onInit(); - this.documentEvents.addSub({ key: this.key, sub: this }) + this.documentEvents.addSub({ key: this.key, sub: this }); } protected onRelease() { @@ -88,32 +93,31 @@ class DocSub extends Sub { } abstract class EventMapEvents { - private readonly listeners: ListenerMap = {} - private readonly subs: SubsMap = {} + private readonly listeners: ListenerMap = {}; + private readonly subs: SubsMap = {}; - constructor() { - } + constructor() {} public release() { const listeners = this.listeners; - const keys = Object.keys(listeners) as Array - keys.forEach(key => this.releaseListener(key)); + const keys = Object.keys(listeners) as Array; + keys.forEach((key) => this.releaseListener(key)); } - abstract doAddListener(key: K, listener: (ev: Map[K]) => any): void + abstract doAddListener(key: K, listener: (ev: Map[K]) => any): void; - abstract doRemoveListener(key: K, listener: (ev: Map[K]) => any): void + abstract doRemoveListener(key: K, listener: (ev: Map[K]) => any): void; - addSub({ key, sub }: { key: K; sub: DocSub; }) { + addSub({ key, sub }: { key: K; sub: DocSub }) { this.initListener(key); const subs: SubsMap[K] = getSubs(this.subs, key) ?? []; - setSubs(this.subs, key, addOneSub(sub, subs)) + setSubs(this.subs, key, addOneSub(sub, subs)); } removeSub(key: K, sub: DocSub) { const list: SubsMap[K] = getSubs(this.subs, key) ?? []; - const list1 = removeOneSub(sub, list) - setSubs(this.subs, key, list1) + const list1 = removeOneSub(sub, list); + setSubs(this.subs, key, list1); if (!list1) { this.releaseListener(key); } @@ -123,7 +127,7 @@ abstract class EventMapEvents { if (!this.listeners[key]) { const listener = (ev: Map[K]) => { const subs: SubsMap[K] = getSubs(this.subs, key) ?? []; - new Array().concat(subs).forEach((s: DocSub) => s.event(ev)) + new Array().concat(subs).forEach((s: DocSub) => s.event(ev)); return {}; }; setListener(this.listeners, key, listener); @@ -135,7 +139,7 @@ abstract class EventMapEvents { const listener: Listener | undefined = getListener(this.listeners, key); if (listener) { setListener(this.listeners, key, undefined); - this.doRemoveListener(key, listener) + this.doRemoveListener(key, listener); } } @@ -153,13 +157,12 @@ abstract class EventMapEvents { * Subscribe to document events. */ export class DocumentEvents extends EventMapEvents { - doAddListener(key: K, listener: (ev: DocumentEventMap[K]) => any): void { - document.addEventListener(key, listener) + document.addEventListener(key, listener); } doRemoveListener(key: K, listener: (ev: DocumentEventMap[K]) => any): void { - document.removeEventListener(key, listener) + document.removeEventListener(key, listener); } } @@ -167,12 +170,11 @@ export class DocumentEvents extends EventMapEvents { * Bonus, WindowEvents */ export class WindowEvents extends EventMapEvents { - doAddListener(key: K, listener: (ev: WindowEventMap[K]) => any): void { - window.addEventListener(key, listener) + window.addEventListener(key, listener); } doRemoveListener(key: K, listener: (ev: WindowEventMap[K]) => any): void { - window.removeEventListener(key, listener) + window.removeEventListener(key, listener); } -} \ No newline at end of file +} diff --git a/tea-cup/src/TeaCup/Testing.tsx b/tea-cup/src/TeaCup/Testing.tsx index de9c654..bba5a12 100644 --- a/tea-cup/src/TeaCup/Testing.tsx +++ b/tea-cup/src/TeaCup/Testing.tsx @@ -55,9 +55,9 @@ declare global { export class Testing { private _dispatched: M | undefined; - public Testing() { } + public Testing() {} - public readonly noop: Dispatcher = () => { }; + public readonly noop: Dispatcher = () => {}; public get dispatcher(): Dispatcher { this._dispatched = undefined; @@ -80,74 +80,96 @@ export class Testing { } } -type Trigger = (node: ReactElement>) => T +type Trigger = (node: ReactElement>) => T; type ResolveType = (idle: [Model, T]) => void; -export function updateUntilIdle(props: ProgramProps, fun: Trigger): Promise<[Model, T]> { - return new Promise(resolve => { - fun() - }) +export function updateUntilIdle( + props: ProgramProps, + fun: Trigger, +): Promise<[Model, T]> { + return new Promise((resolve) => { + fun(); + }); } -function testableProps(resolve: ResolveType, props: ProgramProps, fun: Trigger) { +function testableProps( + resolve: ResolveType, + props: ProgramProps, + fun: Trigger, +) { const tprops: ProgramProps, Msg> = { init: initTestable(resolve, props.init), view: viewTestable(props.view), - update: updateTestable((props.update)), - subscriptions: suscriptionsTestable(props, fun) - } - return tprops + update: updateTestable(props.update), + subscriptions: suscriptionsTestable(props, fun), + }; + return tprops; } type TestableModel = { readonly resolve: ResolveType; readonly cmds: Cmd[]; readonly model: Model; -} +}; -function initTestable(resolve: ResolveType, init: ProgramProps['init']): ProgramProps, Msg>['init'] { +function initTestable( + resolve: ResolveType, + init: ProgramProps['init'], +): ProgramProps, Msg>['init'] { const mac = init(); - return () => [{ - resolve, - cmds: [mac[1]], - model: mac[0] - }, Cmd.none()]; + return () => [ + { + resolve, + cmds: [mac[1]], + model: mac[0], + }, + Cmd.none(), + ]; } -function viewTestable(view: ProgramProps['view']): ProgramProps, Msg>['view'] { +function viewTestable( + view: ProgramProps['view'], +): ProgramProps, Msg>['view'] { return (dispatch: Dispatcher, model: TestableModel) => view(dispatch, model.model); } -function updateTestable(update: ProgramProps['update']): ProgramProps, Msg>['update'] { +function updateTestable( + update: ProgramProps['update'], +): ProgramProps, Msg>['update'] { return (msg: Msg, model: TestableModel) => { const [model1, cmd1] = update(msg, model.model); - const cmds = [cmd1].filter(cmd => cmd.constructor.name !== 'CmdNone') - return [{ - ...model, - cmds, - model: model1, - }, Cmd.none()]; - } + const cmds = [cmd1].filter((cmd) => cmd.constructor.name !== 'CmdNone'); + return [ + { + ...model, + cmds, + model: model1, + }, + Cmd.none(), + ]; + }; } -function suscriptionsTestable(props: ProgramProps, fun: Trigger): ProgramProps, Msg>['subscriptions'] { +function suscriptionsTestable( + props: ProgramProps, + fun: Trigger, +): ProgramProps, Msg>['subscriptions'] { return (model: TestableModel) => { const subs = props.subscriptions(model.model); if (model.cmds.length === 0) { - const result = fun( [model.model, Cmd.none()] - } - update={(msg, model) => [model, Cmd.none()] - } - view={(d, m) => props.view(d, m) - } - subscriptions={(d) => Sub.none()} - />) + const result = fun( + [model.model, Cmd.none()]} + update={(msg, model) => [model, Cmd.none()]} + view={(d, m) => props.view(d, m)} + subscriptions={(d) => Sub.none()} + />, + ); model.resolve([model.model, result]); return subs; } return Sub.batch([new TestableSub(model.cmds), subs]); - } + }; } class TestableSub extends Sub { @@ -159,8 +181,8 @@ class TestableSub extends Sub { setTimeout(() => { if (this.dispatcher !== undefined) { const d = this.dispatcher.bind(this); - this.cmds.map(cmd => cmd.execute(d)); + this.cmds.map((cmd) => cmd.execute(d)); } - }, 0) + }, 0); } } From b237ac94ec52e590de27348422e443205b09e90a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Van=20Keisbelck?= Date: Wed, 21 Apr 2021 16:24:49 +0200 Subject: [PATCH 5/8] more perf --- core/src/TeaCup/Memoize.test.ts | 113 +++++++++++++++++++++++++++ core/src/TeaCup/Memoize.ts | 33 ++++++++ samples/src/Samples/EventsSample.tsx | 48 ++++++------ tea-cup/src/TeaCup/Memo.ts | 15 +++- tea-cup/src/TeaCup/Program.ts | 2 + 5 files changed, 183 insertions(+), 28 deletions(-) create mode 100644 core/src/TeaCup/Memoize.test.ts create mode 100644 core/src/TeaCup/Memoize.ts diff --git a/core/src/TeaCup/Memoize.test.ts b/core/src/TeaCup/Memoize.test.ts new file mode 100644 index 0000000..c4358ba --- /dev/null +++ b/core/src/TeaCup/Memoize.test.ts @@ -0,0 +1,113 @@ +/* + * MIT License + * + * Copyright (c) 2019 Rémi Van Keisbelck + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + + +import {memoize} from "./Memoize"; + +interface User { + readonly name: string; + readonly size: number; +} + +describe('memoize function', () => { + + let count = 0; + + function invoke(f: (t: T) => R, arg:T, expectedResult: R, expectedCount: number) { + const r = f(arg); + expect(r).toEqual(expectedResult); + expect(count).toEqual(expectedCount); + } + + beforeEach(() => { + count = 0; + }) + + test("primitive number", () => { + const f = memoize((x: number) => { + count++; + return x + 1; + }); + [ + [0, 1, 1], + [1, 2, 2], + [1, 2, 2], + [0, 1, 3], + [0, 1, 3] + ].forEach(([v, er, ec]) => { + invoke(f, v, er, ec); + }); + }); + + test("object ref equality", () => { + const f_: (user: User) => string = u => { + count++; + return u.name + " " + u.size; + } + const f = memoize(f_); + const user: User = { + name: "John", + size: 48, + }; + invoke(f, user, "John 48", 1); + invoke(f, user, "John 48", 1); + const user2: User = { + ...user, + size: 12, + }; + invoke(f, user2, "John 12", 2); + invoke(f, user2, "John 12", 2); + }); + + test("object with compare fn", () => { + const f_: (user: User) => string = u => { + count++; + return u.name + " " + u.size; + } + const f = memoize(f_, (o1, o2) => o1.name === o2.name && o1.size === o2.size); + const user: User = { + name: "John", + size: 48, + }; + invoke(f, user, "John 48", 1); + invoke(f, { ...user }, "John 48", 1); + invoke(f, { name: "John", size: 48 }, "John 48", 1); + }); + + test("array ref equals", () => { + const f_: (a: number[]) => string = a => { + count++; + return a.join("_"); + } + const f = memoize(f_); + const a = [1, 2, 3]; + invoke(f, a, "1_2_3", 1); + invoke(f, a, "1_2_3", 1); + const a2 = [...a, 4]; + invoke(f, a2, "1_2_3_4", 2); + invoke(f, a2, "1_2_3_4", 2); + }); + +}); diff --git a/core/src/TeaCup/Memoize.ts b/core/src/TeaCup/Memoize.ts new file mode 100644 index 0000000..ecfc7db --- /dev/null +++ b/core/src/TeaCup/Memoize.ts @@ -0,0 +1,33 @@ +export type F = (t:T) => R; + +interface Data { + readonly arg: T; + readonly res: R; +} + +export function memoize(f: F, compareFn?: (o1: T, o2: T) => boolean): F { + let data: Data | undefined; + + function invoke(t:T): R { + const res = f(t); + data = { + arg: t, + res, + } + return res; + } + + const compare = compareFn ?? ((o1, o2) => o1 === o2); + + return (t:T) => { + if (data) { + if (compare(t, data.arg)) { + return data.res; + } else { + return invoke(t); + } + } else { + return invoke(t); + } + } +} diff --git a/samples/src/Samples/EventsSample.tsx b/samples/src/Samples/EventsSample.tsx index 19f1717..460f329 100644 --- a/samples/src/Samples/EventsSample.tsx +++ b/samples/src/Samples/EventsSample.tsx @@ -110,30 +110,30 @@ const windowEvents = new WindowEvents(); export function subscriptions(model: Model): Sub { return Sub.batch([ - documentEvents.on('click', (e: MouseEvent) => ( - { - type: 'clicked', - position: { - pos: [e.x, e.y], - page: [e.pageX, e.pageY], - offset: [e.offsetX, e.offsetY] - } - } as Msg - )), - documentEvents.on('mousemove', (e: MouseEvent) => ({ - type: 'moved', - position: { - pos: [e.x, e.y], - page: [e.pageX, e.pageY], - offset: [e.offsetX, e.offsetY] - } - } as Msg)), - windowEvents.on('scroll', (e: Event) => { - return { - type: 'scrolled', - scroll: [window.scrollX, window.scrollY] - } as Msg; - }) + // documentEvents.on('click', (e: MouseEvent) => ( + // { + // type: 'clicked', + // position: { + // pos: [e.x, e.y], + // page: [e.pageX, e.pageY], + // offset: [e.offsetX, e.offsetY] + // } + // } as Msg + // )), + // documentEvents.on('mousemove', (e: MouseEvent) => ({ + // type: 'moved', + // position: { + // pos: [e.x, e.y], + // page: [e.pageX, e.pageY], + // offset: [e.offsetX, e.offsetY] + // } + // } as Msg)), + // windowEvents.on('scroll', (e: Event) => { + // return { + // type: 'scrolled', + // scroll: [window.scrollX, window.scrollY] + // } as Msg; + // }) ]); } diff --git a/tea-cup/src/TeaCup/Memo.ts b/tea-cup/src/TeaCup/Memo.ts index 1abeef2..86f9676 100644 --- a/tea-cup/src/TeaCup/Memo.ts +++ b/tea-cup/src/TeaCup/Memo.ts @@ -25,18 +25,24 @@ import * as React from 'react'; +function compareRefEquals(o1: T, o2: T): boolean { + return o1 === o2; +} + /** - * Memoize the view for passed data, and wrap the function's result - * into a component. + * Memoize the view for passed data. * @param t the data to memoize. + * @param compareFn optional comparison function. Defaults to ref equality */ -export function memo(t: T) { +export function memo(t: T, compareFn?: (o1: T, o2: T) => boolean) { + const equals = compareFn ?? compareRefEquals; return (f: (t: T) => React.ReactNode) => { return React.createElement(Memo, { value: t, renderer: (x: any) => { return f(x); }, + compareFn: equals, }); }; } @@ -44,6 +50,7 @@ export function memo(t: T) { interface MemoProps { value: any; renderer: (x: any) => React.ReactNode; + compareFn: (o1: any, o2: any) => boolean; } class Memo extends React.Component { @@ -52,6 +59,6 @@ class Memo extends React.Component { } shouldComponentUpdate(nextProps: Readonly, nextState: Readonly<{}>, nextContext: any): boolean { - return this.props.value !== nextProps.value; + return this.props.compareFn(this.props.value, nextProps.value); } } diff --git a/tea-cup/src/TeaCup/Program.ts b/tea-cup/src/TeaCup/Program.ts index 9258c4d..0ed8341 100644 --- a/tea-cup/src/TeaCup/Program.ts +++ b/tea-cup/src/TeaCup/Program.ts @@ -109,7 +109,9 @@ export class Program extends Component, nev this.currentSub = newSub; // trigger rendering + console.log('render') if (needsUpdate) { + console.log('render update') this.forceUpdate(); } } From 0b8a8192423d2d3b6cba211ee3c380a1ec1447ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Van=20Keisbelck?= Date: Wed, 21 Apr 2021 17:43:34 +0200 Subject: [PATCH 6/8] tbc --- tea-cup/src/TeaCup/Program.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tea-cup/src/TeaCup/Program.ts b/tea-cup/src/TeaCup/Program.ts index 0ed8341..439806b 100644 --- a/tea-cup/src/TeaCup/Program.ts +++ b/tea-cup/src/TeaCup/Program.ts @@ -94,26 +94,26 @@ export class Program extends Component, nev // perform commands in a separate timout, to // make sure that this dispatch is done const cmd = updated[1]; - if (!(cmd instanceof CmdNone)) { + // if (!(cmd instanceof CmdNone)) { setTimeout(() => { // console.log("dispatch: processing commands"); // debug("performing command", updated[1]); updated[1].execute(d); // debug("<<< done"); }, 0); - } + // } - const needsUpdate = this.currentModel === updated[0]; + // const needsUpdate = this.currentModel === updated[0]; this.currentModel = updated[0]; this.currentSub = newSub; // trigger rendering - console.log('render') - if (needsUpdate) { - console.log('render update') + // console.log('render') + // if (needsUpdate) { + // console.log('render update') this.forceUpdate(); - } + // } } } From 4de557fcc1544bbe7358b156bc5f2d4d8c810ba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Van=20Keisbelck?= Date: Wed, 21 Apr 2021 17:52:10 +0200 Subject: [PATCH 7/8] fix perf improvements --- tea-cup/src/TeaCup/Memo.ts | 2 +- tea-cup/src/TeaCup/Program.ts | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/tea-cup/src/TeaCup/Memo.ts b/tea-cup/src/TeaCup/Memo.ts index 86f9676..6e81424 100644 --- a/tea-cup/src/TeaCup/Memo.ts +++ b/tea-cup/src/TeaCup/Memo.ts @@ -59,6 +59,6 @@ class Memo extends React.Component { } shouldComponentUpdate(nextProps: Readonly, nextState: Readonly<{}>, nextContext: any): boolean { - return this.props.compareFn(this.props.value, nextProps.value); + return !this.props.compareFn(this.props.value, nextProps.value); } } diff --git a/tea-cup/src/TeaCup/Program.ts b/tea-cup/src/TeaCup/Program.ts index 439806b..214206b 100644 --- a/tea-cup/src/TeaCup/Program.ts +++ b/tea-cup/src/TeaCup/Program.ts @@ -94,26 +94,24 @@ export class Program extends Component, nev // perform commands in a separate timout, to // make sure that this dispatch is done const cmd = updated[1]; - // if (!(cmd instanceof CmdNone)) { + if (!(cmd instanceof CmdNone)) { setTimeout(() => { // console.log("dispatch: processing commands"); // debug("performing command", updated[1]); updated[1].execute(d); // debug("<<< done"); }, 0); - // } + } - // const needsUpdate = this.currentModel === updated[0]; + const needsUpdate = this.currentModel !== updated[0]; this.currentModel = updated[0]; this.currentSub = newSub; // trigger rendering - // console.log('render') - // if (needsUpdate) { - // console.log('render update') + if (needsUpdate) { this.forceUpdate(); - // } + } } } From bbf203c2cb5e5511872fcbbb6432e3f4fd545b90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Van=20Keisbelck?= Date: Wed, 21 Apr 2021 18:31:35 +0200 Subject: [PATCH 8/8] bump 2.0.1 --- core/package.json | 2 +- samples/package.json | 4 ++-- tea-cup/package.json | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/package.json b/core/package.json index cc10390..fb3b367 100644 --- a/core/package.json +++ b/core/package.json @@ -1,6 +1,6 @@ { "name": "tea-cup-core", - "version": "2.0.0", + "version": "2.0.1", "description": "react-tea-cup core classes and utilities (Maybe etc)", "author": "Rémi Van Keisbelck ", "license": "MIT", diff --git a/samples/package.json b/samples/package.json index 616a044..9821672 100644 --- a/samples/package.json +++ b/samples/package.json @@ -11,8 +11,8 @@ "react": "^16.7.0", "react-dom": "^16.7.0", "react-scripts": "3.4.3", - "react-tea-cup": "^2.0.0", - "tea-cup-core": "^2.0.0" + "react-tea-cup": "^2.0.1", + "tea-cup-core": "^2.0.1" }, "scripts": { "start": "react-scripts start", diff --git a/tea-cup/package.json b/tea-cup/package.json index faa5a0f..4bc1640 100644 --- a/tea-cup/package.json +++ b/tea-cup/package.json @@ -1,6 +1,6 @@ { "name": "react-tea-cup", - "version": "2.0.0", + "version": "2.0.1", "description": "Put some TEA in your React.", "author": "Rémi Van Keisbelck ", "license": "MIT", @@ -22,7 +22,7 @@ "dependencies": {}, "peerDependencies": { "react": "^16.7.0", - "tea-cup-core": "^2.0.0" + "tea-cup-core": "^2.0.1" }, "devDependencies": { "@types/jsdom": "^16.2.5",