From 1d2cf7b08af504067d145642cb1112d45acb42a9 Mon Sep 17 00:00:00 2001 From: Robin Kneepkens Date: Tue, 7 Nov 2023 19:51:02 +0100 Subject: [PATCH 01/30] Added sinon and chai back to devDependencies --- package-lock.json | 580 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 5 + 2 files changed, 585 insertions(+) diff --git a/package-lock.json b/package-lock.json index 8fc96be..b61a57d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,10 +38,14 @@ "@nx/node": "16.3.2", "@nx/workspace": "16.3.2", "@schematics/angular": "^16.1.0", + "@types/chai": "^4.1.7", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", + "@types/sinon-chai": "^3.2.2", "@typescript-eslint/eslint-plugin": "^5.60.0", "@typescript-eslint/parser": "^5.60.0", + "chai": "^4.2.0", + "chai-immutable": "^2.0.0-alpha.1", "codelyzer": "^6.0.2", "dotenv": "^16.3.1", "eslint": "^8.43.0", @@ -58,6 +62,7 @@ "nx": "16.3.2", "postcss-preset-env": "^8.5.0", "prettier": "2.8.8", + "sinon-chai": "^3.3.0", "standard-version": "^9.5.0", "ts-jest": "29.1.0", "ts-node": "10.9.1", @@ -7019,6 +7024,56 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@sinonjs/formatio": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.2.tgz", + "integrity": "sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==", + "dev": true, + "peer": true, + "dependencies": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^3.1.0" + } + }, + "node_modules/@sinonjs/formatio/node_modules/@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "dev": true, + "peer": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz", + "integrity": "sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==", + "dev": true, + "peer": true, + "dependencies": { + "@sinonjs/commons": "^1.3.0", + "array-from": "^2.1.1", + "lodash": "^4.17.15" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "dev": true, + "peer": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true, + "peer": true + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -7121,6 +7176,12 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz", + "integrity": "sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA==", + "dev": true + }, "node_modules/@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -7383,6 +7444,31 @@ "@types/node": "*" } }, + "node_modules/@types/sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-Q2Go6TJetYn5Za1+RJA1Aik61Oa2FS8SuJ0juIqUuJ5dZR4wvhKfmSdIqWtQ3P6gljKWjW0/R7FZkA4oXVL6OA==", + "dev": true, + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinon-chai": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.2.tgz", + "integrity": "sha512-5zSs2AslzyPZdOsbm2NRtuSNAI2aTWzNKOHa/GRecKo7a5efYD7qGcPxMZXQDayVXT2Vnd5waXxBvV31eCZqiA==", + "dev": true, + "dependencies": { + "@types/chai": "*", + "@types/sinon": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true + }, "node_modules/@types/sockjs": { "version": "0.3.33", "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", @@ -8229,6 +8315,13 @@ "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", "dev": true }, + "node_modules/array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha512-GQTc6Uupx1FCavi5mPzBvVT7nEOeWMmUA9P95wpfpW1XwMSKs+KaymD5C2Up7KAUKg/mYwbsUYzdZWcoajlNZg==", + "dev": true, + "peer": true + }, "node_modules/array-ify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", @@ -8308,6 +8401,15 @@ "node": ">=0.10.0" } }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/ast-types-flow": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", @@ -9039,6 +9141,32 @@ } ] }, + "node_modules/chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chai-immutable": { + "version": "2.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/chai-immutable/-/chai-immutable-2.0.0-alpha.1.tgz", + "integrity": "sha512-VsyYOdzimJrylK690LdS1dEZPMB42bL5Qgz6JMudrV3jC9Kv6M+B31Decke9Dwn0tf3xD9Qcng/9NfxLFbetmQ==", + "dev": true, + "peerDependencies": { + "chai": "^4.0.0" + } + }, "node_modules/chalk": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", @@ -9070,6 +9198,18 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -10562,6 +10702,18 @@ "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", "dev": true }, + "node_modules/deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/deep-equal": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.1.tgz", @@ -12802,6 +12954,15 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/get-intrinsic": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", @@ -15334,6 +15495,13 @@ "node": "*" } }, + "node_modules/just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true, + "peer": true + }, "node_modules/karma-source-map-support": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", @@ -15635,6 +15803,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lolex": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.2.0.tgz", + "integrity": "sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==", + "dev": true, + "peer": true + }, "node_modules/long": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", @@ -16348,6 +16523,57 @@ "node-gyp-build": "^4.2.2" } }, + "node_modules/nise": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.3.tgz", + "integrity": "sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ==", + "dev": true, + "peer": true, + "dependencies": { + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^5.0.1", + "path-to-regexp": "^1.7.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "dev": true, + "peer": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/nise/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true, + "peer": true + }, + "node_modules/nise/node_modules/lolex": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", + "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", + "dev": true, + "peer": true, + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "node_modules/nise/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "peer": true, + "dependencies": { + "isarray": "0.0.1" + } + }, "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", @@ -17288,6 +17514,15 @@ "node": ">=4" } }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -19812,6 +20047,76 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/sinon": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.5.0.tgz", + "integrity": "sha512-AoD0oJWerp0/rY9czP/D6hDTTUYGpObhZjMpd7Cl/A6+j0xBE+ayL/ldfggkBXUs0IkvIiM1ljM8+WkOc5k78Q==", + "deprecated": "16.1.1", + "dev": true, + "peer": true, + "dependencies": { + "@sinonjs/commons": "^1.4.0", + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/samsam": "^3.3.3", + "diff": "^3.5.0", + "lolex": "^4.2.0", + "nise": "^1.5.2", + "supports-color": "^5.5.0" + } + }, + "node_modules/sinon-chai": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.3.0.tgz", + "integrity": "sha512-r2JhDY7gbbmh5z3Q62pNbrjxZdOAjpsqW/8yxAZRSqLZqowmfGZPGUZPFf3UX36NLis0cv8VEM5IJh9HgkSOAA==", + "dev": true, + "peerDependencies": { + "chai": "^4.0.0", + "sinon": ">=4.0.0 <8.0.0" + } + }, + "node_modules/sinon/node_modules/@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "dev": true, + "peer": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "peer": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -27040,6 +27345,60 @@ "@sinonjs/commons": "^3.0.0" } }, + "@sinonjs/formatio": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.2.tgz", + "integrity": "sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==", + "dev": true, + "peer": true, + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^3.1.0" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "dev": true, + "peer": true, + "requires": { + "type-detect": "4.0.8" + } + } + } + }, + "@sinonjs/samsam": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz", + "integrity": "sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==", + "dev": true, + "peer": true, + "requires": { + "@sinonjs/commons": "^1.3.0", + "array-from": "^2.1.1", + "lodash": "^4.17.15" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "dev": true, + "peer": true, + "requires": { + "type-detect": "4.0.8" + } + } + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true, + "peer": true + }, "@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -27136,6 +27495,12 @@ "@types/node": "*" } }, + "@types/chai": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz", + "integrity": "sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA==", + "dev": true + }, "@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -27391,6 +27756,31 @@ "@types/node": "*" } }, + "@types/sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-Q2Go6TJetYn5Za1+RJA1Aik61Oa2FS8SuJ0juIqUuJ5dZR4wvhKfmSdIqWtQ3P6gljKWjW0/R7FZkA4oXVL6OA==", + "dev": true, + "requires": { + "@types/sinonjs__fake-timers": "*" + } + }, + "@types/sinon-chai": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.2.tgz", + "integrity": "sha512-5zSs2AslzyPZdOsbm2NRtuSNAI2aTWzNKOHa/GRecKo7a5efYD7qGcPxMZXQDayVXT2Vnd5waXxBvV31eCZqiA==", + "dev": true, + "requires": { + "@types/chai": "*", + "@types/sinon": "*" + } + }, + "@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true + }, "@types/sockjs": { "version": "0.3.33", "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", @@ -28037,6 +28427,13 @@ "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", "dev": true }, + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha512-GQTc6Uupx1FCavi5mPzBvVT7nEOeWMmUA9P95wpfpW1XwMSKs+KaymD5C2Up7KAUKg/mYwbsUYzdZWcoajlNZg==", + "dev": true, + "peer": true + }, "array-ify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", @@ -28092,6 +28489,12 @@ "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", "dev": true }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, "ast-types-flow": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", @@ -28625,6 +29028,27 @@ "integrity": "sha512-jaAOR5zVtxHfL0NjZyflVTtXm3D3J9P15zSJ7HmQF8dSKGA6tqzQq+0ZI3xkjyQj46I4/M0K2GbMpcAFOcbr3A==", "dev": true }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, + "chai-immutable": { + "version": "2.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/chai-immutable/-/chai-immutable-2.0.0-alpha.1.tgz", + "integrity": "sha512-VsyYOdzimJrylK690LdS1dEZPMB42bL5Qgz6JMudrV3jC9Kv6M+B31Decke9Dwn0tf3xD9Qcng/9NfxLFbetmQ==", + "dev": true, + "requires": {} + }, "chalk": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", @@ -28647,6 +29071,15 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "requires": { + "get-func-name": "^2.0.2" + } + }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -29768,6 +30201,15 @@ "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", "dev": true }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, "deep-equal": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.1.tgz", @@ -31478,6 +31920,12 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, + "get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true + }, "get-intrinsic": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", @@ -33306,6 +33754,13 @@ "through": ">=2.2.7 <3" } }, + "just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true, + "peer": true + }, "karma-source-map-support": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", @@ -33533,6 +33988,13 @@ "is-unicode-supported": "^0.1.0" } }, + "lolex": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.2.0.tgz", + "integrity": "sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==", + "dev": true, + "peer": true + }, "long": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", @@ -34069,6 +34531,59 @@ "node-gyp-build": "^4.2.2" } }, + "nise": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.3.tgz", + "integrity": "sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ==", + "dev": true, + "peer": true, + "requires": { + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^5.0.1", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "dev": true, + "peer": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true, + "peer": true + }, + "lolex": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", + "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", + "dev": true, + "peer": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "peer": true, + "requires": { + "isarray": "0.0.1" + } + } + } + }, "node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", @@ -34767,6 +35282,12 @@ } } }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true + }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -36448,6 +36969,65 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "sinon": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.5.0.tgz", + "integrity": "sha512-AoD0oJWerp0/rY9czP/D6hDTTUYGpObhZjMpd7Cl/A6+j0xBE+ayL/ldfggkBXUs0IkvIiM1ljM8+WkOc5k78Q==", + "dev": true, + "peer": true, + "requires": { + "@sinonjs/commons": "^1.4.0", + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/samsam": "^3.3.3", + "diff": "^3.5.0", + "lolex": "^4.2.0", + "nise": "^1.5.2", + "supports-color": "^5.5.0" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "dev": true, + "peer": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true, + "peer": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "peer": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "peer": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "sinon-chai": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.3.0.tgz", + "integrity": "sha512-r2JhDY7gbbmh5z3Q62pNbrjxZdOAjpsqW/8yxAZRSqLZqowmfGZPGUZPFf3UX36NLis0cv8VEM5IJh9HgkSOAA==", + "dev": true, + "requires": {} + }, "sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", diff --git a/package.json b/package.json index 1cc7d43..8dfde51 100644 --- a/package.json +++ b/package.json @@ -70,10 +70,14 @@ "@nx/node": "16.3.2", "@nx/workspace": "16.3.2", "@schematics/angular": "^16.1.0", + "@types/chai": "^4.1.7", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", + "@types/sinon-chai": "^3.2.2", "@typescript-eslint/eslint-plugin": "^5.60.0", "@typescript-eslint/parser": "^5.60.0", + "chai": "^4.2.0", + "chai-immutable": "^2.0.0-alpha.1", "codelyzer": "^6.0.2", "dotenv": "^16.3.1", "eslint": "^8.43.0", @@ -90,6 +94,7 @@ "nx": "16.3.2", "postcss-preset-env": "^8.5.0", "prettier": "2.8.8", + "sinon-chai": "^3.3.0", "standard-version": "^9.5.0", "ts-jest": "29.1.0", "ts-node": "10.9.1", From e18daad3e22868f6de1c2423fd54868fdb1e3fdb Mon Sep 17 00:00:00 2001 From: Robin Kneepkens Date: Tue, 7 Nov 2023 19:52:34 +0100 Subject: [PATCH 02/30] Added tutorial folder from politie-sherlock Additionally fixed the broken imports of sherlock and sherlock-utils --- tutorial/1 - intro.ts | 125 ++++++++++++ tutorial/2 - deriving.ts | 173 ++++++++++++++++ tutorial/3 - reacting.ts | 332 ++++++++++++++++++++++++++++++ tutorial/4 - inner workings.ts | 315 +++++++++++++++++++++++++++++ tutorial/5 - unresolved.ts | 154 ++++++++++++++ tutorial/6 - errors.ts | 0 tutorial/7 - conversion.ts | 138 +++++++++++++ tutorial/8 - advanced.ts | 316 +++++++++++++++++++++++++++++ tutorial/9 - expert.ts | 357 +++++++++++++++++++++++++++++++++ 9 files changed, 1910 insertions(+) create mode 100644 tutorial/1 - intro.ts create mode 100644 tutorial/2 - deriving.ts create mode 100644 tutorial/3 - reacting.ts create mode 100644 tutorial/4 - inner workings.ts create mode 100644 tutorial/5 - unresolved.ts create mode 100644 tutorial/6 - errors.ts create mode 100644 tutorial/7 - conversion.ts create mode 100644 tutorial/8 - advanced.ts create mode 100644 tutorial/9 - expert.ts diff --git a/tutorial/1 - intro.ts b/tutorial/1 - intro.ts new file mode 100644 index 0000000..34f394b --- /dev/null +++ b/tutorial/1 - intro.ts @@ -0,0 +1,125 @@ +import { expect, use } from 'chai'; +import { atom } from '../libs/sherlock/src'; + +// tslint:disable: no-var-requires +use(require('sinon-chai')); +use(require('chai-immutable')); +// tslint:enable: no-var-requires + +/** + * **Your Turn** + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +/** + * Welcome to the `@politie/sherlock` tutorial. + * + * It is set up as a collection of specs, with the goal of getting all the specs to pass. + * The `expect()`s and basic setup are there, you just need to get it to work. + * + * All specs except the first one are set to `.skip`. Remove this to start on that part of the tutorial. + * + * Start the tutorial by running: `npm run tutorial`. + * + * *Hint: most methods and functions are fairly well documented in jsDoc, which is easily accessed through TypeScript* + */ +describe.skip('intro', () => { + it('should be clear what to do next', () => { + // At the start of the spec, there will be some setup. + let bool = false; + // Sometimes including an expectation, to show the current state. + expect(bool).to.be.false; + + /** + * If **Your Turn** is shown in a comment, there is work for you to do. + * This can also be indicated with the `__YOUR_TURN__` variable. + * It should be clear what to do here... + */ + bool = __YOUR_TURN__; + + // We use expectations like this to verify the result. + expect(bool, ` +--- Welcome to the tutorial! --- + +Please look in \`./tutorial/1 - intro.ts\` to see what to do next.. + `).to.be.true; + }); +}); + +/** + * Let's start with the `Derivable` basics. + */ +describe.skip('the basics', () => { + /** + * The `Atom` is the basic building block of `@politie/sherlock`. + * It holds a value which you can `get()` and `set()`. + */ + it('the `Atom`', () => { + // An `Atom` can be created with the `atom()` function. The parameter of this function is used as the initial value of the `Atom`. + const myValue$ = atom(1); + // Variables containing `Atom`s or any other `Derivable` are usually postfixed with a `$` to indicate this. Hence `myValue$`. + + // The `.get()` method can be used to get the current value of the `Atom`. + expect(myValue$.get()).to.equal(1); + + /** + * **Your Turn** + * Use the `.set()` method to change the value of the `Atom`. + */ + + expect(myValue$.get()).to.equal(2); + }); + + /** + * The `Atom` is a `Derivable`. This means it can be used to create a derived value. + * This derived value stays up to date with the original `Atom`. + * + * The easiest way to do this, is to call `.derive()` on another `Derivable`. + * Let's try this. + */ + it('the `Derivable`', () => { + const myValue$ = atom(1); + expect(myValue$.get()).to.equal(1); + + /** + * **Your Turn** + * We want to create a new `Derivable` that outputs the inverse of the original `Atom`. + * Use `myValue$.derive(val => ...)` to create the `myInverse$` variable. + */ + const myInverse$ = myValue$.derive(__YOUR_TURN__); + + expect(myInverse$.get()).to.equal(-1); + + // So if we set `myValue$` to -2: + myValue$.set(-2); + // `myInverse$` will change accordingly. + expect(myInverse$.get()).to.equal(2); + }); + + /** + * Of course, `Derivable`s are not only meant to get, set and derive state. + * You can also listen to the changes. + * + * This is done with the `.react()` method. + * This method is given a `function` that is executed every time the value of the `Derivable` changes. + */ + it('reacting to `Derivable`s', () => { + const myCounter$ = atom(0); + + let reacted = 0; + /** + * **Your Turn** + * Now react to `myCounter$`. In every `react()`, increase the `reacted` variable by one. + */ + + expect(reacted).to.equal(1); // `react()` will react immediately, more on that later. + + // And then we set the `Atom` a couple of times to make the `Derivable` react. + for (let i = 0; i <= 100; i++) { + // Set the value of the `Atom`. + myCounter$.set(i); + } + expect(reacted).to.equal(101); + }); +}); diff --git a/tutorial/2 - deriving.ts b/tutorial/2 - deriving.ts new file mode 100644 index 0000000..fa29289 --- /dev/null +++ b/tutorial/2 - deriving.ts @@ -0,0 +1,173 @@ +import { expect } from 'chai'; +import { atom, Derivable, derive } from '../libs/sherlock/src'; + +/** + * **Your Turn** + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +/** + * Any `Derivable` (including `Atom`s) can be used (and/or combined) to create a derived state. + * This derived state is in turn a `Derivable`. + * + * There are a couple of ways to do this. + */ +describe.skip('deriving', () => { + /** + * In the 'intro' we have created a derivable by using the `.derive()` method. + * This method allows the state of that `Derivable` to be used to create a new `Derivable`. + * + * In the derivation, other `Derivable`s can be used as well. + * If a `Derivable.get()` is called inside a derivation, the changes to that `Derivable` are also tracked and kept up to date. + */ + it('combining `Derivable`s', () => { + const repeat$ = atom(1); + const text$ = atom(`It won't be long`); + + /** + * **Your Turn** + * Let's create some lyrics by combining `text$` and `repeat$`. + * As you might have guessed, we want to repeat the text a couple of times. + * (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat should do fine) + */ + const lyric$ = text$.derive(txt => txt); // We can combine txt with `repeat$.get()` here. + + expect(lyric$.get()).to.equal(`It won't be long`); + + text$.set(' yeah'); + repeat$.set(3); + expect(lyric$.get()).to.equal(` yeah yeah yeah`); + }); + + /** + * Now that we have used `.get()` in a `.derive()`. You may wonder, can we skip the original `Derivable` and just call the function `derive()`? + * Of course you can! + * + * And you can use any `Derivable` you want, even if they all have the same `Atom` as a parent. + */ + it('the `derive()` function', () => { + const myCounter$ = atom(1); + + /** + * **Your Turn** + * Let's try creating a `Derivable` [FizzBuzz](https://en.wikipedia.org/wiki/Fizz_buzz) + * `fizzBuzz$` should combine `fizz$`, `buzz$` and `myCounter$` to produce the correct output. + * + * Multiple `Derivable`s can be combined to create a new one. To do this, just use `.get()` on (other) `Derivable`s in the `.derive()` step. + * This can be done both when `derive()` is used standalone or as a method on another `Derivable`. + */ + const fizz$: Derivable = myCounter$.derive(__YOUR_TURN__); // Should return 'Fizz' when `myCounter$` is a multiple of 3 and '' otherwise. + const buzz$: Derivable = myCounter$.derive(__YOUR_TURN__); // Should return 'Buzz' when `myCounter$` is a multiple of 5 and '' otherwise. + const fizzBuzz$: Derivable = derive(__YOUR_TURN__); + + expect(fizz$.get()).to.equal(''); + expect(buzz$.get()).to.equal(''); + expect(fizzBuzz$.get()).to.equal(1); + for (let count = 1; count <= 100; count++) { + // Set the value of the `Atom`, + myCounter$.set(count); + + // and check if the output changed accordingly. + checkFizzBuzz(count, fizzBuzz$.get()); + } + }); + + function checkFizzBuzz(count: number, out: string | number) { + if (count % 3 + count % 5 === 0) { // If `count` is a multiple of 3 AND 5, output 'FizzBuzz'. + expect(out).to.equal('FizzBuzz'); + } else if (count % 3 === 0) { // If `count` is a multiple of 3, output 'Fizz'. + expect(out).to.equal('Fizz'); + } else if (count % 5 === 0) { // If `count` is a multiple of 5, output 'Buzz'. + expect(out).to.equal('Buzz'); + } else { // Otherwise just output the `count` itself. + expect(out).to.equal(count); + } + } + + /** + * The automatic tracking of `.get()` calls will also happen inside called `function`s. + * This can be really powerful, but also dangerous. One of the dangers is shown here. + */ + it('indirect derivations', () => { + const pastTweets = [] as string[]; + const currentUser$ = atom('Barack'); + function log(tweet: string) { + pastTweets.push(`${currentUser$.get()} - ${tweet}`); + } + + const tweet$ = atom('First tweet'); + + tweet$.derive(log).react(txt => { + // Normally we would do something with the tweet here. + return txt; + }); + + // The first tweet should have automatically been added to the `pastTweets` array. + expect(pastTweets).to.have.length(1); + expect(pastTweets[0]).to.contain('Barack'); + expect(pastTweets[0]).to.contain('First tweet'); + + // Let's add a famous quote by Mr Barack: + tweet$.set('We need to reject any politics that targets people because of race or religion.'); + // As expected this is automatically added to the log. + expect(pastTweets).to.have.length(2); + expect(pastTweets[1]).to.contain('Barack'); + expect(pastTweets[1]).to.contain('reject'); + + // But what if the user changes? + currentUser$.set('Donald'); + + /** + * **Your Turn** + * Time to set your own expectations. + */ + expect(pastTweets).to.have.length(2); // Is there a new tweet? + expect(pastTweets[2]).to.contain(__YOUR_TURN__); // Who sent it? Donald? Or Barack? + expect(pastTweets[2]).to.contain(__YOUR_TURN__); // What did he tweet? + + /** + * As you can see, this is something to look out for. + * Luckily there are ways to circumvent this. But more on that later. + * + * *Note that this behavior can also be really helpful if you know what you are doing* + */ + }); + + /** + * Every `Derivable` has a couple of convenience methods. + * These are methods that make common derivations a bit easier. + * + * These methods are: `.and()`, `.or()`, `.is()` and `.not()`. + * Their function is as you would expect from `boolean` operators in a JavaScript environment. + * The first three will take a `Derivable` or regular value as parameter. + * `.not()` does not need any input. + * + * `.is()` will resolve equality in the same way as `@politie/sherlock` would do internally. + * More on the equality check in the 'inner workings' part. But know that the first check is + * [Object.is()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) + */ + it('convenience methods', () => { + const myCounter$ = atom(1); + + /** + * **Your Turn** + * The FizzBuzz example above can be rewritten using the convenience methods. + * This is not how you would normally write it, but it looks like a fun excercise. + * + * `fizz$` and `buzz$` can be completed with only `.is(...)`, `.and(...)` and `.or(...)`; + * Make sure the output of those `Derivable`s is either 'Fizz'/'Buzz' or ''. + */ + const fizz$ = myCounter$.derive(count => count % 3).is(__YOUR_TURN__).and(__YOUR_TURN__).or(__YOUR_TURN__) as Derivable; + const buzz$ = myCounter$.derive(count => count % 5).is(__YOUR_TURN__).and(__YOUR_TURN__).or(__YOUR_TURN__) as Derivable; + const fizzBuzz$ = derive(() => fizz$.get() + buzz$.get()).or(__YOUR_TURN__); + + for (let count = 1; count <= 100; count++) { + // Set the value of the `Atom`, + myCounter$.set(count); + + // and check if the output changed accordingly. + checkFizzBuzz(count, fizzBuzz$.get()); + } + }); +}); diff --git a/tutorial/3 - reacting.ts b/tutorial/3 - reacting.ts new file mode 100644 index 0000000..20bd021 --- /dev/null +++ b/tutorial/3 - reacting.ts @@ -0,0 +1,332 @@ +import { expect } from 'chai'; +import { atom } from '../libs/sherlock/src'; + +/** + * **Your Turn** + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +/** + * In the intro we have seen a basic usage of the `.react()` method. + * Let's dive a bit deeper into the details of this method. + */ +describe.skip('reacting', () => { + // For easy testing we can count the number of times a reactor was called, + let wasCalledTimes: number; + // and record the last value it reacted to. + let lastValue: any; + beforeEach('reset the values', () => { + wasCalledTimes = 0; + lastValue = undefined; + }); + // The reactor to be given to the `.react()` method. + function reactor(val: any) { + wasCalledTimes++; + lastValue = val; + } + // Of course we are lazy and don't want to type these assertions over and over. :-) + function expectReact(reactions: number, value?: any) { + expect(wasCalledTimes, 'Reaction was called # times').to.equal(reactions); + expect(lastValue, 'Last value of the reaction was #').to.equal(value); + } + + /** + * Every `Derivable` always has a current state. So the `.react()` method does not need to wait for a value, there already is one. + * This means that `.react()` will fire directly when called. + * When the `Derivable` has a new state, this will also fire `.react()` synchronously. + * So the very next line after `.set()` is called, the `.react()` has already fired! + * + * (Except when the `Derivable` is `unresolved`, but more on that later.) + */ + it('reacting synchronously', () => { + const myAtom$ = atom('initial value'); + // A trivial `expect` to silence TypeScript's noUnusedLocals. + expect(myAtom$.get()).to.equal('initial value'); + + // There should not have been a reaction yet + expectReact(0); + + /** + * **Your Turn** + * Time to react to `myAtom$` with the `reactor()` function defined above. + */ + + expectReact(1, 'initial value'); + + // Now set a 'new value' to `myAtom$`. + + expectReact(2, 'new value'); + }); + + /** + * A reactor will go on forever. This is often not what you want, and almost always a memory leak. + * So it is important to stop a reactor at some point. The `.react()` method has different ways of dealing with this. + */ + describe('stopping a reaction', () => { + /** + * The easiest is the 'stopper' function, every `.react()` call will return a `function` that will stop the reaction. + */ + it('with the stopper function', () => { + const myAtom$ = atom('initial value'); + // A trivial `expect` to silence TypeScript's noUnusedLocals + expect(myAtom$.get()).to.equal('initial value'); + + /** + * **Your Turn** + * catch the returned `stopper` in a variable + */ + myAtom$.react(reactor); + + expectReact(1, 'initial value'); + + /** + * **Your Turn** + * Call the `stopper`. + */ + + myAtom$.set('new value'); + + // And the reaction stopped. + expectReact(1, 'initial value'); + }); + + /** + * Everytime the reaction is called, it also gets the stopper `function` as a second parameter. + */ + it('with the stopper callback', () => { + const myAtom$ = atom('initial value'); + // A trivial `expect` to silence TypeScript's noUnusedLocals + expect(myAtom$.get()).to.equal('initial value'); + + /** + * **Your Turn** + * In the reaction below, use the stopper callback to stop the reaction + */ + myAtom$.react((val, __YOUR_TURN___) => { + reactor(val); + __YOUR_TURN___; + }); + + expectReact(1, 'initial value'); + + myAtom$.set('new value'); + + // And the reaction stopped. + expectReact(1, 'initial value'); + }); + + }); + + /** + * The reactor `options` are a way to modify when and how the reactor will react to changes in the `Derivable`. + */ + describe('reactor options', () => { + /** + * Another way to make a reactor stop at a certain point, is by specifying an `until` in the `options`. + * `until` can be given either a `Derivable` or a `function`. + * If a `function` is given, this `function` will be given the `Derivable` that is the source of the reaction as a parameter. + * This `function` will track all `.get()`s, so can use any `Derivable`. It can return a `boolean` or a `Derivable`. + * *Note: the reactor options `when` and `from` can also be set to a `Derivable`/`function` as described here.* + * + * The reactor will stop directly when `until` becomes true. + * If that happens at exactly the same time as the `Derivable` getting a new value, it will not react again. + */ + describe('reacting `until`', () => { + const boolean$ = atom(false); + const string$ = atom('Value'); + beforeEach('reset', () => { + boolean$.set(false); + string$.set('Value'); + }); + + /** + * If a `Derivable` is given, the reaction will stop once that `Derivable` becomes `true`/truthy. + */ + it('an external `Derivable`', () => { + /** + * **Your Turn** + * Try giving `boolean$` as `until` option. + */ + string$.react(reactor, __YOUR_TURN__); + expectReact(1, 'Value'); // It should react directly as usual. + + string$.set('New value'); + expectReact(2, 'New value'); // It should keep reacting as usual. + + boolean$.set(true); // We set `boolean$` to true, to stop the reaction + expectReact(2, 'New value'); // The reactor has immediately stopped, so it still reacted only twice. + + boolean$.set(false); // Even when `boolean$` is set to `false` again + string$.set('Another value'); // And a new value is introduced + expectReact(2, 'New value'); // The reactor won't start up again, so it still reacted only twice. + }); + + /** + * A function can also be given as `until` this function will be executed in a derivation. + * This way any `Derivable` can be used in the calculation. + */ + it('a function', () => { + /** + * **Your Turn** + * Since the reactor options expect a boolean, you will sometimes need to calculate the option. + * Try giving a `function` as `until` option, use `!string$.get()` to return `true` when the `string` is empty. + */ + string$.react(reactor, __YOUR_TURN__); + string$.set('New value'); + string$.set('Newer Value'); + expectReact(3, 'Newer Value'); // It should react as usual. + + string$.set(''); // We set `string$` to an empty string, to stop the reaction + expectReact(3, 'Newer Value'); // The reactor was immediately stopped, so even the empty string was never given to the reactor + }); + + /** + * Since the example above, where the `until` is based on the parent `Derivable` occurs very frequently. + * This `Derivable` is given as a parameter to the `until` function. + */ + it('the parent `Derivable`', () => { + /** + * **Your Turn** + * Try using the first parameter of the `until` function to do the same as above. + */ + string$.react(reactor, __YOUR_TURN__); + string$.set('New value'); + string$.set('Newer Value'); + expectReact(3, 'Newer Value'); // It should react as usual. + + string$.set(''); // We set `string$` to an empty string, to stop the reaction + expectReact(3, 'Newer Value'); // The reactor was immediately stopped, so even the empty string was never given to the reactor + }); + }); + + /** + * Sometimes you may not need to react to the first couple of values of the `Derivable`. + * This can be because of the value of the `Derivable` or due to external conditions. + * The `from` option is meant to help with this. The reactor will only start after it becomes true. + * Once it has become true, the reactor will not listen to this option any more and react as usual. + * + * *Note: when using `from`, `.react()` will (most often) not react synchronously any more. As that is the function of this option.* + */ + it('reacting `from`', () => { + const sherlock$ = atom(''); + + /** + * **Your Turn** + * We can react here, but restrict the reactions to start when the keyword 'dear' is set. + * This will skip the first three reactions, but react as usual after that. + * + * *Hint: remember the `.is()` method?* + */ + sherlock$.react(reactor, __YOUR_TURN__); + + expectReact(0); + ['Elementary,', 'my', 'dear', 'Watson'].forEach(txt => sherlock$.set(txt)); + + expectReact(2, 'Watson'); + }); + + /** + * Sometimes you may want to react only on certain values or when certain conditions are met. + * It works exactly as `from` + * + * *Note: as with `from` this can prevent `.react()` from reacting synchronously.* + */ + it('reacting `when`', () => { + const count$ = atom(0); + + /** + * Now, let's react to all even numbers. + * Except 4, we don't want to make it too easy now. + */ + count$.react(reactor, __YOUR_TURN__); + + expectReact(1, 0); + + for (let i = 0; i <= 4; i++) { + count$.set(i); + } + expectReact(2, 2); + for (let i = 4; i <= 10; i++) { + count$.set(i); + } + expectReact(5, 10); + }); + + /** + * Normally the reactor will immediately fire with the current value. + * If you want the reactor to fire normally, just not the first time, there is also a `boolean` option: `skipFirst`. + */ + it('reacting with `skipFirst`', () => { + const done$ = atom(false); + + /** + * **Your Turn** + * Say you want to react when `done$` is true. But not right away.. + */ + done$.react(reactor, __YOUR_TURN__); + expectReact(0); + + done$.set(true); + expectReact(1, true); + }); + + /** + * With `once` you can stop the reactor after it has emitted exactly one value. This is a `boolean` option. + * + * Without any other `options`, this is just a strange way of typing `.get()`. + * But when combined with `when`, `from` or `skipFirst`, it can be very useful. + */ + it('reacting `once`', () => { + const finished$ = atom(false); + + /** + * **Your Turn** + * Say you want to react when `finished$` is true. It can not finish twice. + * + * *Hint: you will need to combine `once` with another option* + */ + finished$.react(reactor, __YOUR_TURN__); + expectReact(0); + + // When finished it should react once. + finished$.set(true); + expectReact(1, true); + + // After that it should really be finished. :-) + finished$.set(false); + finished$.set(true); + expectReact(1, true); + }); + + }); + + describe('challenge', () => { + it('onDisconnect', () => { + const connected$ = atom(false); + + /** + * **Your Turn** + * We want our reactor to trigger once, when the user disconnects (eg for cleanup). + * `connected$` indicates the current connection status. + * This should be possible with three simple ReactorOptions + */ + connected$.react(reactor, __YOUR_TURN__); + + // It starts as 'not connected' + expectReact(0); + + // At this point, the user connects, no reaction should occur yet. + connected$.set(true); + expectReact(0); + + // When the user disconnects, the reaction should fire once + connected$.set(false); + expectReact(1, false); + + // It should not react again after this + expect(connected$.connected).to.be.false; + }); + + }); +}); diff --git a/tutorial/4 - inner workings.ts b/tutorial/4 - inner workings.ts new file mode 100644 index 0000000..5324868 --- /dev/null +++ b/tutorial/4 - inner workings.ts @@ -0,0 +1,315 @@ +import { expect } from 'chai'; +import { Seq } from 'immutable'; +import { spy } from 'sinon'; +import { atom } from '../libs/sherlock/src'; + +/** + * **Your Turn** + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +/** + * Time to dive a bit deeper into the inner workings of `@politie/sherlock`. + */ +describe.skip('inner workings', () => { + /** + * What if there is a derivation that reads from one of two `Derivable`s dynamically? + * Will both of those `Derivable`s be tracked for changes? + */ + it('dynamic/inactive dependencies', () => { + const switch$ = atom(true); + const number$ = atom(1); + const string$ = atom('one'); + + const reacted = spy(); + + switch$ + // This `.derive()` is the one we are testing when true, it will return the `number` otherwise the `string` + .derive(s => s ? number$.get() : string$.get()) + .react(reacted); + + // The first time should not surprise anyone, the derivation was called and returned the right result + expect(reacted).to.have.been.calledOnceWith(1); + + // `switch$` is still set to true (number) + string$.set('two'); + + /** + * **Your Turn** + * What do you expect? + */ + expect(reacted).to.have.callCount(__YOUR_TURN__); + expect(reacted.lastCall).to.be.calledWith(__YOUR_TURN__); + + // `switch$` is still set to true (number) + number$.set(2); + + /** + * **Your Turn** + * What do you expect? + */ + expect(reacted).to.have.callCount(__YOUR_TURN__); + expect(reacted.lastCall).to.be.calledWith(__YOUR_TURN__); + + // Now let's reset the spy, so callCount should be 0 again. + reacted.resetHistory(); + + // `switch$` is set to false (string) + switch$.set(false); + number$.set(3); + + /** + * **Your Turn** + * What do you expect now? + */ + expect(reacted).to.have.callCount(__YOUR_TURN__); + expect(reacted.lastCall).to.be.calledWith(__YOUR_TURN__); + }); + + /** + * One thing to know about `Derivable`s is that derivations are not executed, until someone asks. + * So let's test this. + */ + it('lazy execution', () => { + const hasDerived = spy(); + + const myAtom$ = atom(true); + const myDerivation$ = myAtom$.derive(hasDerived); + + /** + * **Your Turn** + * We have created a new `Derivable` by deriving the `Atom`. But have not called `.get()` on that new `Derivable`. + * Do you think the `hasDerived` function has been called? And how many times? + * + * *Hint: you can use sinonChai's `.to.have.been.called`/`.to.have.been.calledOnce`/`to.have.callCount(...)`/etc..* + */ + expect(hasDerived).to.have.callCount(__YOUR_TURN__); // Well, what do you expect? + + myDerivation$.get(); + + expect(hasDerived).to.have.callCount(__YOUR_TURN__); // And after a `.get()`? + + myDerivation$.get(); + + expect(hasDerived).to.have.callCount(__YOUR_TURN__); // And after the second `.get()`? Is there an extra call? + + /** + * The state of any `Derivable` can change at any moment. + * But you don't want to keep a record of the state and changes to a `Derivable` that no one is listening to. + * That's why a `Derivable` has to recalculate it's internal state every time `.get()` is called. + */ + }); + + /** + * So what if the `Derivable` is reacting? + * When a `Derivable` is reacting, the current state is known. + * And since changes are derived/reacted to synchronously, the state is always up to date. + * So a `.get()` should not have to be calculated. + */ + it('while reacting', () => { + const hasDerived = spy(); + + const myAtom$ = atom(true); + const myDerivation$ = myAtom$.derive(hasDerived); + + // It should not have done anything at this moment + expect(hasDerived).to.not.have.been.called; + + const stopper = myDerivation$.react(() => ''); + + /** + * **Your Turn** + * Ok, it's your turn to complete the expectations. + * *Hint: you can use `.calledOnce`/`.calledTwice` etc or `.callCount()`* + */ + expect(hasDerived).to.have.callCount(__YOUR_TURN__); + + myDerivation$.get(); + + expect(hasDerived).to.have.callCount(__YOUR_TURN__); + + myAtom$.set(false); + + expect(hasDerived).to.have.callCount(__YOUR_TURN__); + + myDerivation$.get(); + + expect(hasDerived).to.have.callCount(__YOUR_TURN__); + + stopper(); + + expect(hasDerived).to.have.callCount(__YOUR_TURN__); + + myDerivation$.get(); + + expect(hasDerived).to.have.callCount(__YOUR_TURN__); + + /** + * Since the `.react()` already listens to the value(changes) there is no need to recalculate whenever a `.get()` is called. + * But when the reactor has stopped, the derivation has to be calculated again. + */ + }); + + /** + * The basics of `Derivable` caching are seen above. + * But there is one more trick up it's sleeve. + */ + it('cached changes', () => { + const first = spy(); + const second = spy(); + + const myAtom$ = atom(1); + const first$ = myAtom$.derive(i => { + first(i); // Call the spy, to let it know we were here + return i > 2; + }); + const second$ = first$.derive(second); + + // As always, they should not have fired yet + expect(first).to.not.have.been.called; + expect(second).to.not.have.been.called; + + second$.react(() => ''); + + // And as expected, they now should both have fired once + expect(first).to.have.been.calledOnce; + expect(second).to.have.been.calledOnce; + + /** + * **Your Turn** + * But what to expect now? + */ + myAtom$.set(1); // Note that this is the same value as it was initialized with + + expect(first).to.have.callCount(__YOUR_TURN__); + expect(second).to.have.callCount(__YOUR_TURN__); + + myAtom$.set(2); + + expect(first).to.have.callCount(__YOUR_TURN__); + expect(second).to.have.callCount(__YOUR_TURN__); + + myAtom$.set(3); + + expect(first).to.have.callCount(__YOUR_TURN__); + expect(second).to.have.callCount(__YOUR_TURN__); + + myAtom$.set(4); + + expect(first).to.have.callCount(__YOUR_TURN__); + expect(second).to.have.callCount(__YOUR_TURN__); + + /** + * Can you explain the behavior above? + * + * It is why we say that `@politie/sherlock` deals with reactive state and not events (as RxJS does for example). + * Events can be very useful, but when data is involved, you are probably only interested in value changes. + * So these changes can and need to be cached and deduplicated. + */ + }); + + /** + * So if the new value of a `Derivable` is equal to the old, it won't propagate a new event. + * But what does it mean to be equal in a `Derivable`. + * + * Strict `===` equality would mean that `NaN` and `NaN` would not even be equal. + * `Object.is()` equality would be better, but would mean that structurally equal objects could be different. + */ + it('equality', () => { + const atom$ = atom({}); + const hasReacted = spy(); + + atom$.react(hasReacted, { skipFirst: true }); + + atom$.set({}); + + /** + * **Your Turn** + * The `Atom` is set with exactly the same object as before. Will the `.react()` fire? + */ + expect(hasReacted).to.have.callCount(__YOUR_TURN__); + + /** + * But what if you use an object, that can be easily compared through a library like `ImmutableJS` + * Let's try an `Immutable.Seq` + */ + atom$.set(Seq.Indexed.of(1, 2, 3)); + // Let's reset the spy here, to start over + hasReacted.resetHistory(); + expect(hasReacted).to.not.have.been.called; + + atom$.set(Seq.Indexed.of(1, 2, 3)); + /** + * **Your Turn** + * Do you think the `.react()` fired with this new value? + */ + expect(hasReacted).to.have.callCount(__YOUR_TURN__); + + atom$.set(Seq.Indexed.of(1, 2)); + + /** + * **Your Turn** + * And now? + */ + expect(hasReacted).to.have.callCount(__YOUR_TURN__); + + /** + * In `@politie/sherlock` equality is a bit complex. + * First we check `Object.is()` equality, if that is true, it is the same, you can't deny that. + * After that it is pluggable. It can be anything you want. + * By default we try to use `.equals()`, to support libraries like `ImmutableJS`. + */ + }); + + /** + * What if there is a derivation that reads from one of two `Derivable`s dynamically? + * Will both of those `Derivable`s be tracked for changes? + */ + it('dynamic/inactive dependencies', () => { + const switch$ = atom(true); + const number$ = atom(1); + const string$ = atom('one'); + + const reacted = spy(); + + switch$ + // This `.derive()` is the one we are testing when true, it will return the `number` otherwise the `string` + .derive(s => s ? number$.get() : string$.get()) + .react(reacted); + + // The first time should not surprise anyone, the derivation was called and returned the right result + expect(reacted).to.have.been.calledOnceWith(1); + + // `switch$` is still set to true (number) + string$.set('two'); + + /** + * **Your Turn** + * What do you expect? + */ + expect(reacted).to.have.callCount(__YOUR_TURN__); + + // `switch$` is still set to true (number) + number$.set(2); + + /** + * **Your Turn** + * What do you expect? + */ + expect(reacted).to.have.callCount(__YOUR_TURN__); + + // Now let's reset the spy, so callCount should be 0 again. + reacted.resetHistory(); + + // `switch$` is set to false (string) + switch$.set(false); + number$.set(3); + + /** + * **Your Turn** + * What do you expect now? + */ + expect(reacted).to.have.callCount(__YOUR_TURN__); + }); +}); diff --git a/tutorial/5 - unresolved.ts b/tutorial/5 - unresolved.ts new file mode 100644 index 0000000..97500b0 --- /dev/null +++ b/tutorial/5 - unresolved.ts @@ -0,0 +1,154 @@ +import { expect } from 'chai'; +import { spy } from 'sinon'; +import { atom, Derivable, DerivableAtom } from '../libs/sherlock/src'; + +/** + * **Your Turn** + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +/** + * Sometimes your data isn't available yet. For example if it is still being fetched from the server. + * At that point you probably still want your `Derivable` to exist, to start deriving and reacting when the data becomes available. + * + * To support this, `Derivable`s in `@politie/sherlock` support a separate state, called `unresolved`. + * This indicates that the data is not available yet, but (probably) will be at some point. + */ +describe.skip('unresolved', () => { + /** + * Let's start by creating an `unresolved` `Derivable`. + */ + it('can be checked on the `Derivable`', () => { + // By using the `.unresolved()` method, you can create an `unresolved` atom + // Note that you will need to indicate the type of this atom, since it can't be inferred by TypeScript this way. + const myAtom$ = atom.unresolved(); + + expect(myAtom$.resolved).to.equal(__YOUR_TURN__); + + /** + * **Your Turn** + * Resolve the atom, it's pretty easy + */ + + expect(myAtom$.resolved).to.be.true; + }); + + /** + * An `unresolved` `Derivable` is not able to provide a value yet. + * So `.get()` will throw if you try. + */ + it('cannot `.get()`', () => { + /** + * **Your Turn** + * Time to create an `unresolved` Atom.. + */ + const myAtom$: DerivableAtom = __YOUR_TURN__; + + expect(myAtom$.resolved).to.be.false; + expect(() => myAtom$.get()).to.throw('Could not get value, derivable is unresolved'); + + myAtom$.set('finally!'); + + /** + * **Your Turn** + * What do you expect? + */ + expect(myAtom$.resolved).to.equal(__YOUR_TURN__); + expect(() => myAtom$.get()).to; // .throw()/.not.to.throw()? + }); + + /** + * If a `Derivable` is `unresolved` it can't react yet. But it will `.react()` if a value becomes available. + * + * *Note that this can prevent `.react()` from executing immediately* + */ + it('reacting to `unresolved`', () => { + const myAtom$ = atom.unresolved(); + + const hasReacted = spy(); + myAtom$.react(hasReacted); + + /** + * **Your Turn** + * What do you expect? + */ + expect(hasReacted).to.have.callCount(__YOUR_TURN__) + .and.calledWith(__YOUR_TURN__); + + /** + * **Your Turn** + * Now make the last expect succeed + */ + + expect(myAtom$.resolved).to.be.true; + expect(hasReacted).to.have.been.calledOnceWith(`woohoow, I was called`); + }); + + /** + * In `@politie/sherlock` there is no reason why a `Derivable` should not become `unresolved` again, + * after it has been set. + */ + it('can become `unresolved` again', () => { + const myAtom$ = atom.unresolved(); + + expect(myAtom$.resolved).to.be.false; + + /** + * **Your Turn** + * Set the value.. + */ + + expect(myAtom$.get()).to.equal(`it's alive!`); + + /** + * **Your Turn** + * Unset the value.. (*Hint: TypeScript is your friend*) + */ + + expect(myAtom$.resolved).to.be.false; + }); + + /** + * When a `Derivable` is dependent on another `unresolved` `Derivable`, this `Derivable` should also become `unresolved`. + * + * *Note that this will only become `unresolved` when there is an active dependency (see 'inner workings#dynamic dependencies')* + */ + it('will propagate', () => { + const myString$ = atom.unresolved(); + const myOtherString$ = atom.unresolved(); + + /** + * **Your Turn** + * Combine the two `Atom`s into one `Derivable` + */ + const myDerivable$: Derivable = __YOUR_TURN__; + + /** + * **Your Turn** + * Is `myDerivable$` expected to be `resolved`? + */ + expect(myDerivable$.resolved).to.equal(__YOUR_TURN__); + + // Now let's set one of the two source `Atom`s + myString$.set('some'); + + /** + * **Your Turn** + * What do you expect to see in `myDerivable$`. + * And what if we set `myOtherString$`? + */ + expect(myDerivable$.resolved).to.equal(__YOUR_TURN__); + myOtherString$.set('data'); + expect(myDerivable$.resolved).to.equal(__YOUR_TURN__); + expect(myDerivable$.get()).to.equal(__YOUR_TURN__); + + /** + * **Your Turn** + * Now we will unset one of the `Atom`s. + * What do you expect `myDerivable$` to be? + */ + myString$.unset(); + expect(myDerivable$.resolved).to.equal(__YOUR_TURN__); + }); +}); diff --git a/tutorial/6 - errors.ts b/tutorial/6 - errors.ts new file mode 100644 index 0000000..e69de29 diff --git a/tutorial/7 - conversion.ts b/tutorial/7 - conversion.ts new file mode 100644 index 0000000..c2fbe68 --- /dev/null +++ b/tutorial/7 - conversion.ts @@ -0,0 +1,138 @@ +import { expect } from 'chai'; +import { spy } from 'sinon'; +import { pairwise, scan, struct } from '../libs/sherlock-utils/src'; +// import { fromObservable, toObservable } from '../extensions/sherlock-rxjs'; +import { atom } from '../libs/sherlock/src'; + +/** + * **Your Turn** + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +describe('conversion', () => { + /** + * `@politie/sherlock` has the ability to produce and use Promises + */ + describe('promises', () => { + it('toPromise'); + it('fromPromise'); + }); + + describe('RxJS', () => { + it('toObservable'); + it('fromObservable'); + }); + + /** + * In the `@politie/sherlock-utils` lib, there are a couple of functions that can combine multiple values of a single `Derivable` + * or combine multiple `Derivable`s into one. We will show a couple of those here. + */ + describe.skip('utils', () => { + /** + * As the name suggests, `pairwise()` will call the given function with both the current and the previous state. + * + * *Note functions like `pairwise` and `scan` can be used with any callback. So it can be used both in a `.derive()` step and in a `.react()`* + */ + it('pairwise', () => { + expect(pairwise).to.exist; // use `pairwise` so the import is used. + + const myCounter$ = atom(1); + const reactSpy = spy(); + + /** + * **Your Turn** + * Now, use `pairwise()`, to subtract the previous value from the current + */ + myCounter$.derive(__YOUR_TURN__).react(reactSpy); + + expect(reactSpy).to.have.been.calledOnceWith(1); + + myCounter$.set(3); + + expect(reactSpy).to.have.been.calledTwice.and.calledWith(2); + + myCounter$.set(45); + + expect(reactSpy).to.have.been.calledThrice.and.calledWith(42); + }); + + /** + * `scan` is the `Derivable` version of `Array.prototype.reduce`. It will be called with the current state and the last emitted value. + * + * *Note: as with `pairwise()` this is useable in both a `.derive()` and `.react()` method* + */ + it('scan', () => { + expect(scan).to.exist; // use `scan` so the import is used. + + const myCounter$ = atom(1); + const reactSpy = spy(); + + /** + * **Your Turn** + * Now, use `scan()`, to add all the emitted values together + */ + myCounter$.derive(__YOUR_TURN__).react(reactSpy); + + expect(reactSpy).to.have.been.calledOnceWith(1); + + myCounter$.set(3); + + expect(reactSpy).to.have.been.calledTwice.and.calledWith(4); + + myCounter$.set(45); + + expect(reactSpy).to.have.been.calledThrice.and.calledWith(49); + + /** + * *BONUS: Try using `scan()` (or `pairwise()`) directly in the `.react()` method.* + */ + }); + + /** + * A `struct()` can combine an Object/Array of `Derivable`s into one `Derivable`, that contains the values of that `Derivable`. + * The Object/Array that is in the output of `struct()` will have the same structure as the original Object/Array. + * + * This is best explained in practice. + */ + it('struct', () => { + expect(struct).to.exist; // use `struct` so the import is used. + + const allMyAtoms = { + regularProp: 'prop', + string: atom('my string'), + number: atom(1), + sub: { + string: atom('my substring'), + }, + }; + + const myOneAtom$ = struct(allMyAtoms); + + expect(myOneAtom$.get()).to.deep.equal({ + regularProp: 'prop', + string: 'my string', + number: 1, + sub: { + string: 'my substring', + }, + }); + + allMyAtoms.regularProp = 'new value'; + allMyAtoms.sub.string.set('my new substring'); + + /** + * **Your Turn** + * Now have a look at the properties of `myOneAtom$`. Is this what you expect? + */ + expect(myOneAtom$.get()).to.deep.equal({ + regularProp: __YOUR_TURN__, + string: __YOUR_TURN__, + number: __YOUR_TURN__, + sub: { + string: __YOUR_TURN__, + }, + }); + }); + }); +}); diff --git a/tutorial/8 - advanced.ts b/tutorial/8 - advanced.ts new file mode 100644 index 0000000..4ac152b --- /dev/null +++ b/tutorial/8 - advanced.ts @@ -0,0 +1,316 @@ +import { expect } from 'chai'; +import { Map as ImmutableMap } from 'immutable'; +import { spy } from 'sinon'; +import { atom, constant, Derivable, derive, SettableDerivable } from '../libs/sherlock/src'; + +/** + * **Your Turn** + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +describe.skip('advanced', () => { + /** + * In the case a `Derivable` is required, but the value is immutable. + * You can use a `constant()`. + * + * This will create a readonly `Derivable`. + */ + it('`constant`', () => { + /** + * We cast to `SettableDerivable` to trick TypeScript for this test. + * It can be valueable to know what a `constant()` is, though. + * So try and remove the `cast`, see what happens! + */ + const c = constant('value') as unknown as SettableDerivable; + + /** + * **Your Turn** + * What do you expect this `Derivable` to do on `.set()`, `.get()` etc? + */ + expect(() => c.get()).to; // .throw()/.not.to.throw()? + expect(() => c.set('new value')).to; // .throw()/.not.to.throw()? + }); + + /** + * Collections in `ImmutableJS` are immutable, so any modification to a collection will create a new one. + * This results in every change needing a `.get()` and a `.set()` on a `Derivable`. + * + * To make this pattern a little bit easier, the `.swap()` method can be used. + * The given function will get the current value of the `Derivable` and any return value will be set as the new value. + */ + it('`.swap()`', () => { + // This is a separate function, because you might be able to use this later + function plusOne(num: number) { return num + 1; } + + const myCounter$ = atom(0); + /** + * **Your Turn** + * Rewrite the `.get()`/`.set()` combos below using `.swap()`. + */ + myCounter$.set(plusOne(myCounter$.get())); + expect(myCounter$.get()).to.equal(1); + + myCounter$.set(plusOne(myCounter$.get())); + expect(myCounter$.get()).to.equal(2); + }); + + /** + * As an alternative to `.get()` and `.set()`, there is also the `.value` accessor. + */ + describe('.value', () => { + /** + * `.value` can be used as an alternative to `.get()` and `.set()`. + * This helps when a property is expected instead of two methods. + */ + it('as a getter/setter', () => { + const myAtom$ = atom('foo'); + + /** + * **Your Turn** + * Use the `.value` accessor to get the current value. + */ + expect(__YOUR_TURN__).to.equal('foo'); + + /** + * **Your Turn** + * Now use the `.value` accessor to set a 'new value'. + */ + myAtom$.value = __YOUR_TURN__; + + expect(myAtom$.get()).to.equal('new value'); + }); + + /** + * If a `Derivable` is `unresolved`, `.get()` will normally throw. + * `.value` will return `undefined` instead. + */ + it('will not throw when `unresolved`', () => { + const myAtom$ = atom.unresolved(); + + /** + * **Your Turn** + */ + expect(myAtom$.value).to.equal(__YOUR_TURN__); + }); + + /** + * As a result, if `.value` is used inside a derivation, it will also replace `unresolved` with `undefined`. + * So `unresolved` will not automatically propagate when using `.value`. + */ + it('will stop propagation of `unresolved` in `.derive()`', () => { + const myAtom$ = atom('foo'); + + const usingGet$ = derive(() => myAtom$.get()); + const usingVal$ = derive(() => myAtom$.value); + + expect(usingGet$.get()).to.equal('foo'); + expect(usingVal$.get()).to.equal('foo'); + + /** + * **Your Turn** + * We just created two `Derivable`s that are almost exactly the same. + * But what happens when their source becomes `unresolved`? + */ + expect(usingGet$.resolved).to.equal(__YOUR_TURN__); + expect(usingVal$.resolved).to.equal(__YOUR_TURN__); + myAtom$.unset(); + expect(usingGet$.resolved).to.equal(__YOUR_TURN__); + expect(usingVal$.resolved).to.equal(__YOUR_TURN__); + }); + }); + + /** + * The `.map()` method is comparable to `.derive()`. + * But there are a couple of differences: + * - It only triggers when the source `Derivable` changes + * - It does not track any other `Derivable` used in the function + * - It can be made to be settable + */ + describe('`.map()`', () => { + const mapReactSpy = spy(); + beforeEach('reset the spy', () => mapReactSpy.resetHistory()); + + it('triggers when the source changes', () => { + const myAtom$ = atom(1); + /** + * **Your Turn** + * Use the `.map()` method to create the expected output below + */ + const mappedAtom$: Derivable = __YOUR_TURN__; + + mappedAtom$.react(mapReactSpy); + + expect(mapReactSpy).to.have.been.calledOnceWith('1'); + + myAtom$.set(3); + + expect(mapReactSpy).to.have.been.calledTwice + .and.calledWith('333'); + }); + + it('does not trigger when any other `Derivable` changes', () => { + const myRepeat$ = atom(1); + const myString$ = atom('ho'); + const deriveReactSpy = spy(); + + // Note that the `.map` uses both `myRepeat$` and `myString$` + myRepeat$.map(r => myString$.get().repeat(r)).react(mapReactSpy); + myRepeat$.derive(r => myString$.get().repeat(r)).react(deriveReactSpy); + + expect(mapReactSpy).to.have.been.calledOnceWith('ho'); + expect(deriveReactSpy).to.have.been.calledOnceWith('ho'); + + myRepeat$.value = 3; + /** + * **Your Turn** + * We changed`myRepeat$` to equal 3. + * Do you expect both reactors to have fired? And with what? + */ + expect(deriveReactSpy).to.have.callCount(__YOUR_TURN__); + expect(deriveReactSpy.lastCall).to.be.calledWith(__YOUR_TURN__); + expect(mapReactSpy).to.have.callCount(__YOUR_TURN__); + expect(mapReactSpy.lastCall).to.be.calledWith(__YOUR_TURN__); + + myString$.value = 'ha'; + /** + * **Your Turn** + * And now that we have changed `myString$`? And when `myRepeat$` changed again? + */ + expect(deriveReactSpy).to.have.callCount(__YOUR_TURN__); + expect(deriveReactSpy.lastCall).to.be.calledWith(__YOUR_TURN__); + expect(mapReactSpy).to.have.callCount(__YOUR_TURN__); + expect(mapReactSpy.lastCall).to.be.calledWith(__YOUR_TURN__); + + myRepeat$.value = 2; + expect(deriveReactSpy).to.have.callCount(__YOUR_TURN__); + expect(deriveReactSpy.lastCall).to.be.calledWith(__YOUR_TURN__); + expect(mapReactSpy).to.have.callCount(__YOUR_TURN__); + expect(mapReactSpy.lastCall).to.be.calledWith(__YOUR_TURN__); + + /** + * As you can see, a change in `myString$` will not trigger an update. + * But if an update is triggered, `myString$` will be called and the new value will be used. + */ + }); + + /** + * Since `.map()` is a relatively simple mapping of input value to output value. + * It can often be reversed. In that case you can use that reverse mapping to create a `SettableDerivable`. + */ + it('can be settable', () => { + const myAtom$ = atom(1); + + /** + * **Your Turn** + */ + const myInverse$ = myAtom$.map( + // This first function is called when getting + n => -n, + // The second is called when setting, you may want to fix this one though + __YOUR_TURN__, + ); + + expect(myInverse$.get()).to.equal(-1); + + myInverse$.set(-2); + + /** + * **Your Turn** + */ + expect(myAtom$.get()).to.equal(__YOUR_TURN__); + expect(myInverse$.get()).to.equal(__YOUR_TURN__); + }); + }); + + /** + * `.pluck()` is a special case of the `.map()` method. + * If a collection of values, like an Object, Map, Array is the result of a `Derivable` one of those values can be plucked into a new `Derivable`. + * This plucked `Derivable` can be settable, if the source supports it. + * + * The way properties are plucked is pluggable, but by default both `.get()` and `[]` are supported. + * To support basic Objects, Maps and Arrays. + * + * *Note that normally when a value of a collection changes, the reference does not.* + * *This means that setting a plucked property of a regular Object/Array/Map will not cause any reaction on that source `Derivable`.* + * *ImmutableJS can help fix this problem* + */ + describe('`.pluck()`', () => { + const reactSpy = spy(); + const reactPropSpy = spy(); + let myMap$: SettableDerivable>; + let firstProp$: SettableDerivable; + + beforeEach('reset', () => { + reactPropSpy.resetHistory(); + reactSpy.resetHistory(); + myMap$ = atom>(ImmutableMap({ + firstProp: 'firstValue', + secondProp: 'secondValue', + })); + /** + * **Your Turn** + * `.pluck()` 'firstProp' from `myMap$`. + */ + firstProp$ = __YOUR_TURN__; + }); + + /** + * Once a property is plucked in a new `Derivable`. This `Derivable` can be used as a regular `Derivable`. + */ + it('can be used as a normal `Derivable`', () => { + firstProp$.react(reactPropSpy, { skipFirst: true }); + + /** + * **Your Turn** + * What do you expect the plucked `Derivable` to look like? And what happens when we `.set()` it? + */ + expect(firstProp$.get()).to.equal(__YOUR_TURN__); + + firstProp$.set('other value'); // the plucked `Derivable` should be settable + expect(firstProp$.get()).to.equal(__YOUR_TURN__); // is the `Derivable` value the same as was set? + + expect(reactPropSpy).to.have.callCount(__YOUR_TURN__) // how many times was the spy called? Note the `skipFirst`.. + .and.calledWith(__YOUR_TURN__); // and what was the value? + }); + + /** + * If the source of the plucked `Derivable` changes, the plucked `Derivable` will change as well. + * As long as the change affects the plucked property of course. + */ + it('will react to changes in the source `Derivable`', () => { + firstProp$.react(reactPropSpy, { skipFirst: true }); + + /** + * **Your Turn** + * We will set `secondProp`, will this affect `firstProp$`? + */ + myMap$.swap(map => map.set('secondProp', 'new value')); + expect(reactPropSpy).to.have.callCount(__YOUR_TURN__) // how many times was the spy called? + .and.calledWith(__YOUR_TURN__); // and with what value? + + /** + * **Your Turn** + * And what if we set `firstProp`? + */ + myMap$.swap(map => map.set('firstProp', 'new value')); + expect(reactPropSpy).to.have.callCount(__YOUR_TURN__) // how many times was the spy called? + .and.calledWith(__YOUR_TURN__); // and with what value? + }); + + /** + * + */ + it('will write through to the source `Derivable`', () => { + myMap$.react(reactSpy, { skipFirst: true }); + + /** + * **Your Turn** + * So what if we set `firstProp$`? Does this propagate to the source `Derivable`? + */ + firstProp$.set(__YOUR_TURN__); + expect(reactSpy).to.have.callCount(__YOUR_TURN__); + expect(myMap$.get()).to.equal(__YOUR_TURN__); + }); + }); +}); diff --git a/tutorial/9 - expert.ts b/tutorial/9 - expert.ts new file mode 100644 index 0000000..ea4faec --- /dev/null +++ b/tutorial/9 - expert.ts @@ -0,0 +1,357 @@ +import { expect } from 'chai'; +import { SinonStub, spy, stub } from 'sinon'; +import { derivableCache } from '../libs/sherlock-utils/src'; +import { DerivableAtom, atom, derive } from '../libs/sherlock/src'; + +/** + * **Your Turn** + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +describe.skip('expert', () => { + describe('`.autoCache()`', () => { + /** + * If a `.get()` is called on a `Derivable` all derivations will be executed. + * But what if a `Derivable` is used multiple times in another `Derivable`. + */ + it('multiple executions', () => { + const hasDerived = spy(); + + const myAtom$ = atom(true); + const myFirstDerivation$ = myAtom$.derive(hasDerived); + const mySecondDerivation$ = myFirstDerivation$.derive( + () => myFirstDerivation$.get() + myFirstDerivation$.get(), + ); + + /** + * **Your Turn** + * `hasDerived` is used in the first derivation. But has it been called at this point? + */ + expect(hasDerived).to.not.have.callCount(__YOUR_TURN__); + + mySecondDerivation$.get(); + + /** + * **Your Turn** + * Now that we have gotten `mySecondDerivation$`, which calls `.get()` on the first multiple times. + * How many times has the first `Derivable` actually executed it's derivation? + */ + expect(hasDerived).to.have.callCount(__YOUR_TURN__); // how many times? + }); + + /** + * So when a `Derivable` is reacting the value is cached and can be gotten from cache. + * But if this `Derivable` is used multiple times in a row, even in another derivation it isn't cached. + * To fix this issue, `.autoCache()` exists. It will cache the `Derivable`s value until the next Event Loop `tick`. + * + * So let's try the example above with this feature + */ + it('autoCaching', async () => { + const firstHasDerived = spy(); + const secondHasDerived = spy(); + + /** + * **Your Turn** + * Use `.autoCache()` on one of the `Derivable`s below. To make the expectations pass. + */ + const myAtom$ = atom(true); + const myFirstDerivation$ = myAtom$.derive(firstHasDerived); + const mySecondDerivation$ = myFirstDerivation$.derive(() => + secondHasDerived(myFirstDerivation$.get() + myFirstDerivation$.get()), + ); + + expect(firstHasDerived, 'first before .get()').to.have.not.been.called; + expect(secondHasDerived, 'second before .get()').to.have.not.been.called; + + mySecondDerivation$.get(); + + expect(firstHasDerived, 'first after first .get()').to.have.been.calledOnce; + expect(secondHasDerived, 'second after first .get()').to.have.been.calledOnce; + + mySecondDerivation$.get(); + + expect(firstHasDerived, 'first after second .get()').to.have.been.calledOnce; + expect(secondHasDerived, 'second after second .get()').to.have.been.calledTwice; + + /** + * Notice that the first `Derivable` has only been executed once, even though the second `Derivable` executed twice. + * Now we wait a tick + */ + + await new Promise(r => setTimeout(r, 1)); + + firstHasDerived.resetHistory(); + secondHasDerived.resetHistory(); + + mySecondDerivation$.get(); + + /** + * **Your Turn** + * Now what do you expect? + */ + expect(firstHasDerived, 'first after last .get()').to.have.callCount(__YOUR_TURN__); // How many times was it called? + expect(secondHasDerived, 'second after last .get()').to.have.callCount(__YOUR_TURN__); // How many times was it called? + }); + }); + + /** + * Some `Derivable`s need an input to be calculated. If this `Derivable` is async or has a big setup process, + * you may still want to create it only once, even if the `Derivable` is requested more than once for the same resource. + * + * Let's imagine a `stockPrice$(stock: string)` function, which returns a `Derivable` with the current price for the given stock. + * This `Derivable` is async, since it will try to retrieve the current price on a distant server. + * + * Let's see what can go wrong first, and we will try to fix it after that. + * + * *Note that a `Derivable` without an input is (hopefully) created only once, so it does not have this problem* + */ + describe('`derivableCache`', () => { + type Stocks = 'GOOGL' | 'MSFT' | 'APPL'; + let stockPrice$: SinonStub<[Stocks], DerivableAtom>; + beforeEach(() => (stockPrice$ = stub<[Stocks], DerivableAtom>().callsFake(() => atom.unresolved()))); + + const reactSpy = spy(); + beforeEach(() => reactSpy.resetHistory()); + function reactor(v: any) { + reactSpy(v); + } + + /** + * If the function to create the `Derivable` is called multiple times, the `Derivable` will be created multiple times. + * Any setup this `Derivable` does, will be executed every time. + */ + it('multiple setups', () => { + // To not make things difficult with `unresolved` for this example, imagine we get a response synchronously + stockPrice$.returns(atom(1079.11)); + + const html$ = derive( + () => ` +

Alphabet Price ($${stockPrice$('GOOGL').get().toFixed(2)})

+

Some important text that uses the current price ($${stockPrice$('GOOGL') + .get() + .toFixed()}) as well

+ `, + ); + html$.react(reactor); + + expect(html$.connected).to.be.true; + expect(reactSpy).to.have.been.calledOnce; + + /** + * **Your Turn** + * The `Derivable` is connected and has emitted once, but in that value the 'GOOGL' stockprice was displayed twice. + * We know that using a `Derivable` twice in a connected `Derivable` will make the second `.get()` use a cached value. + * + * But does that apply here? + * How many times has the setup run, for the price `Derivable`. + */ + expect(stockPrice$).to.have.callCount(__YOUR_TURN__); + + /** Can you explain this behavior? */ + }); + + /** + * An other problem can arise when the setup is done inside a derivation + */ + describe('setup inside a derivation', () => { + /** + * When the setup of a `Derivable` is done inside the same derivation as where `.get()` is called. + * You may be creating some problems. + */ + it('unresolveable values', () => { + // First setup an `Atom` with the company we are currently interested in + const company$ = atom('GOOGL'); + + // Based on that `Atom` we derive the stockPrice + const price$ = company$.derive(company => stockPrice$(company).get()); + + price$.react(reactor); + + // Because the stockPrice is still `unresolved` the reactor should not have emitted anything yet + expect(reactSpy).to.have.not.been.called; + + // Now let's increase the price + // First we have to get the atom that was given by the `stockPrice$` stub + const googlPrice$ = stockPrice$.firstCall.returnValue as DerivableAtom; + // Check if it is the right `Derivable` + expect(googlPrice$.connected).to.be.true; + + // Then we set the price + googlPrice$.set(1079.11); + + /** + * **Your Turn** + * So the value was increased. What do you think happened? + */ + expect(reactSpy).to.have.callCount(__YOUR_TURN__); + expect(reactSpy).to.have.been.calledWith(__YOUR_TURN__); + // And how many times did the setup run? + expect(stockPrice$).to.have.callCount(__YOUR_TURN__); + expect(googlPrice$.connected).to.equal(__YOUR_TURN__); + + /** + * Can you explain this behavior? + * + * Thought about it? Here is what happened: + * - Initially `stockPrice$('GOOGL')` emits a `Derivable` (`googlPrice$`), which is unresolved + * - Inside the `.derive()` we subscribe to updates on that `Derivable` + * - When `googlPrice$` emits a new value, the `.derive()` step is run again + * - Inside this step, the setup is run again and a new `Derivable` (`newGooglPrice$`) is created and subscribed to + * - Unsubscribing from the old `googlPrice$` + * + * This `newGooglPrice$` is newly created and `unresolved` again. So the end result is an `unresolved` `price$` `Derivable`. + */ + }); + + /** + * **Bonus** + * + * The problem above can be fixed without a `derivableCache`. + * If we split the `.derive()` step into two steps, where the first does the setup, and the second unwraps the `Derivable` created in the first. + * This way, a newly emitted value from the created `Derivable` will not run the setup again and everything should work as expected. + * + * **Your Turn** + * + * *Hint: there is even an `unwrap` helper function for just such an occasion, try it!* + */ + + /** + * But even when you split the setup and the `unwrap`, you may not be out of the woods yet! + * This is actually a problem that most libraries have a problem with, if not properly accounted for. + */ + it('uncached Derivables', () => { + // First we setup an `Atom` with the company we are currently interested in + // This time we support multiple companies, though + const companies$ = atom(['GOOGL']); + + // Based on that `Atom` we derive the stockPrices + const prices$ = companies$ + /** + * There is no need derive anything here, so we use `.map()` on `companies$` + * And since `companies` is an array of strings, we `.map()` over that array to create an array of `Derivable`s + */ + .map(companies => companies.map(company => stockPrice$(company))) + // Then we get the prices from the created `Derivable`s in a separate step + .derive(price$s => price$s.map(price$ => price$.value)); + + prices$.react(reactor); + + // Because we use `.value` instead of `.get()` the reactor should emit immediately, this time + expect(reactSpy) + .to.have.been.calledOnce // But it should emit `undefined` + .and.calledWithExactly([undefined]); + + // Now let's increase the price + // First we have to get the atom that was given by the `stockPrice$` stub + const googlPrice$ = stockPrice$.firstCall.returnValue; + // Check if it is the right `Derivable` + expect(googlPrice$.connected).to.be.true; + + // Then we set the price, as before + googlPrice$.set(1079.11); + + /** + * **Your Turn** + * So the value was increased. What do you think happened now? + */ + expect(reactSpy).to.have.callCount(__YOUR_TURN__); + expect(reactSpy).to.have.been.calledWith([__YOUR_TURN__]); + + /** + * So that worked, now let's try and add another company to the list + */ + companies$.swap(current => [...current, 'APPL']); + + expect(companies$.get()).to.deep.equal(['GOOGL', 'APPL']); + + /** + * **Your Turn** + * With both 'GOOGL' and 'APPL' in the list, what do we expect as an output? + * We had a price for 'GOOGL', but not for 'APPL'... + */ + expect(reactSpy).to.have.callCount(__YOUR_TURN__); + expect(reactSpy).to.have.been.calledWith([__YOUR_TURN__, __YOUR_TURN__]); + }); + }); + /** + * So we know a couple of problems that can arise, but how do we fix them. + */ + describe('a solution', () => { + /** + * Let's try putting `stockPrice$` inside a `derivableCache`. + * `derivableCache` requires a `derivableFactory`, this specifies the setup for a given key. + * We know the key, and what to do with it, so let's try it! + */ + const priceCache$ = derivableCache({ + derivableFactory: (company: Stocks) => stockPrice$(company), + }); + /** + * *Note that from this point forward we use `priceCache$` where we used to use `stockPrice$` directly* + */ + + it('should fix everything :-)', () => { + // First setup an `Atom` with the company we are currently interested in + const companies$ = atom(['GOOGL']); + + const html$ = companies$.derive(companies => + companies.map( + company => ` +

Alphabet Price ($ ${priceCache$(company).value || 'unknown'})

+

Some important text that uses the current price ($ ${ + priceCache$(company).value || 'unknown' + }) as well

`, + ), + ); + + html$.react(reactor); + + expect(html$.connected).to.be.true; + expect(reactSpy).to.have.been.calledOnce; + // Convenience function to return the first argument of the last call to the reactor + function lastEmittedHTMLs() { + return reactSpy.lastCall.args[0]; + } + // The last call, should have the array of HTML's as first argument + expect(lastEmittedHTMLs()[0]).to.contain('$ unknown'); + + /** + * **Your Turn** + * The `Derivable` is connected and has emitted once. + * The price for the given company 'GOOGL' is displayed twice, just as in the first test. + * + * Has anything changed, by using the `derivableCache`? + */ + expect(stockPrice$).to.have.callCount(__YOUR_TURN__); + + // Now let's resolve the price + stockPrice$.firstCall.returnValue.set(1079.11); + + /** + * **Your Turn** + * Last time this caused the setup to run again, resolving to `unresolved` yet again. + * What happens this time? Has the setup run again? + */ + expect(stockPrice$).to.have.callCount(__YOUR_TURN__); + // Ok, but did it update the HTML? + expect(reactSpy).to.have.callCount(__YOUR_TURN__); + expect(lastEmittedHTMLs()[0]).to.contain(__YOUR_TURN__); + + // Last chance, what if we add a company + companies$.swap(current => [...current, 'APPL']); + + /** + * **Your Turn** + * Now the `stockPrice$` function should have at least run again for 'APPL'. + * But did it calculate 'GOOGL' again too? + */ + expect(stockPrice$).to.have.callCount(__YOUR_TURN__); + expect(reactSpy).to.have.callCount(__YOUR_TURN__); + // The first should be 'GOOGL' + expect(lastEmittedHTMLs()[0]).to.contain(__YOUR_TURN__); + // The first should be 'APPL' + expect(lastEmittedHTMLs()[1]).to.contain(__YOUR_TURN__); + }); + }); + }); +}); From c57801ae79c06f763c08a4cd5f91036d9e3bfab4 Mon Sep 17 00:00:00 2001 From: Robin Kneepkens Date: Thu, 9 Nov 2023 12:05:00 +0100 Subject: [PATCH 03/30] Made the tests runnable with jest Use npm run tutorial to run all the test cases in the tutorial directory. --- package.json | 3 +- tutorial/1 - intro.test.ts | 117 ++++++++++++++++ tutorial/1 - intro.ts | 125 ------------------ .../{2 - deriving.ts => 2 - deriving.test.ts} | 0 .../{3 - reacting.ts => 3 - reacting.test.ts} | 0 ...workings.ts => 4 - inner workings.test.ts} | 0 ...- unresolved.ts => 5 - unresolved.test.ts} | 0 .../{6 - errors.ts => 6 - errors.test.ts} | 0 ...- conversion.ts => 7 - conversion.test.ts} | 0 .../{8 - advanced.ts => 8 - advanced.test.ts} | 0 .../{9 - expert.ts => 9 - expert.test.ts} | 0 tutorial/jest.config.ts | 18 +++ tutorial/tsconfig.json | 7 + 13 files changed, 144 insertions(+), 126 deletions(-) create mode 100644 tutorial/1 - intro.test.ts delete mode 100644 tutorial/1 - intro.ts rename tutorial/{2 - deriving.ts => 2 - deriving.test.ts} (100%) rename tutorial/{3 - reacting.ts => 3 - reacting.test.ts} (100%) rename tutorial/{4 - inner workings.ts => 4 - inner workings.test.ts} (100%) rename tutorial/{5 - unresolved.ts => 5 - unresolved.test.ts} (100%) rename tutorial/{6 - errors.ts => 6 - errors.test.ts} (100%) rename tutorial/{7 - conversion.ts => 7 - conversion.test.ts} (100%) rename tutorial/{8 - advanced.ts => 8 - advanced.test.ts} (100%) rename tutorial/{9 - expert.ts => 9 - expert.test.ts} (100%) create mode 100644 tutorial/jest.config.ts create mode 100644 tutorial/tsconfig.json diff --git a/package.json b/package.json index 8dfde51..9cc2ade 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "update": "nx migrate latest", "workspace-generator": "nx workspace-generator", "dep-graph": "nx dep-graph", - "help": "nx help" + "help": "nx help", + "tutorial": "jest tutorial/* -c tutorial/jest.config.ts" }, "standard-version": { "bumpFiles": [ diff --git a/tutorial/1 - intro.test.ts b/tutorial/1 - intro.test.ts new file mode 100644 index 0000000..df382b2 --- /dev/null +++ b/tutorial/1 - intro.test.ts @@ -0,0 +1,117 @@ +/** + * **Your Turn** + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +/** + * Welcome to the `@politie/sherlock` tutorial. + * + * It is set up as a collection of specs, with the goal of getting all the specs to pass. + * The `expect()`s and basic setup are there, you just need to get it to work. + * + * All specs except the first one are set to `.skip`. Remove this to start on that part of the tutorial. + * + * Start the tutorial by running: `npm run tutorial`. + * + * *Hint: most methods and functions are fairly well documented in jsDoc, which is easily accessed through TypeScript* + */ +describe('intro', () => { + it('should be clear what to do next', () => { + // At the start of the spec, there will be some setup. + let bool = false; + // Sometimes including an expectation, to show the current state. + expect(bool).toBeFalse(); + + /** + * If **Your Turn** is shown in a comment, there is work for you to do. + * This can also be indicated with the `__YOUR_TURN__` variable. + * It should be clear what to do here... + */ + bool = __YOUR_TURN__; + + // We use expectations like this to verify the result. + expect(` +--- Welcome to the tutorial! --- + +Please look in \`./tutorial/1 - intro.ts\` to see what to do next.. + `).toBeFalsy(); + }); +}); + +/** + * Let's start with the `Derivable` basics. + */ +// describe.skip('the basics', () => { +// /** +// * The `Atom` is the basic building block of `@politie/sherlock`. +// * It holds a value which you can `get()` and `set()`. +// */ +// it('the `Atom`', () => { +// // An `Atom` can be created with the `atom()` function. The parameter of this function is used as the initial value of the `Atom`. +// const myValue$ = atom(1); +// // Variables containing `Atom`s or any other `Derivable` are usually postfixed with a `$` to indicate this. Hence `myValue$`. + +// // The `.get()` method can be used to get the current value of the `Atom`. +// expect(myValue$.get()).to.equal(1); + +// /** +// * **Your Turn** +// * Use the `.set()` method to change the value of the `Atom`. +// */ + +// expect(myValue$.get()).to.equal(2); +// }); + +// /** +// * The `Atom` is a `Derivable`. This means it can be used to create a derived value. +// * This derived value stays up to date with the original `Atom`. +// * +// * The easiest way to do this, is to call `.derive()` on another `Derivable`. +// * Let's try this. +// */ +// it('the `Derivable`', () => { +// const myValue$ = atom(1); +// expect(myValue$.get()).to.equal(1); + +// /** +// * **Your Turn** +// * We want to create a new `Derivable` that outputs the inverse of the original `Atom`. +// * Use `myValue$.derive(val => ...)` to create the `myInverse$` variable. +// */ +// const myInverse$ = myValue$.derive(__YOUR_TURN__); + +// expect(myInverse$.get()).to.equal(-1); + +// // So if we set `myValue$` to -2: +// myValue$.set(-2); +// // `myInverse$` will change accordingly. +// expect(myInverse$.get()).to.equal(2); +// }); + +// /** +// * Of course, `Derivable`s are not only meant to get, set and derive state. +// * You can also listen to the changes. +// * +// * This is done with the `.react()` method. +// * This method is given a `function` that is executed every time the value of the `Derivable` changes. +// */ +// it('reacting to `Derivable`s', () => { +// const myCounter$ = atom(0); + +// let reacted = 0; +// /** +// * **Your Turn** +// * Now react to `myCounter$`. In every `react()`, increase the `reacted` variable by one. +// */ + +// expect(reacted).to.equal(1); // `react()` will react immediately, more on that later. + +// // And then we set the `Atom` a couple of times to make the `Derivable` react. +// for (let i = 0; i <= 100; i++) { +// // Set the value of the `Atom`. +// myCounter$.set(i); +// } +// expect(reacted).to.equal(101); +// }); +// }); diff --git a/tutorial/1 - intro.ts b/tutorial/1 - intro.ts deleted file mode 100644 index 34f394b..0000000 --- a/tutorial/1 - intro.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { expect, use } from 'chai'; -import { atom } from '../libs/sherlock/src'; - -// tslint:disable: no-var-requires -use(require('sinon-chai')); -use(require('chai-immutable')); -// tslint:enable: no-var-requires - -/** - * **Your Turn** - * If you see this variable, you should do something about it. :-) - */ -export const __YOUR_TURN__ = {} as any; - -/** - * Welcome to the `@politie/sherlock` tutorial. - * - * It is set up as a collection of specs, with the goal of getting all the specs to pass. - * The `expect()`s and basic setup are there, you just need to get it to work. - * - * All specs except the first one are set to `.skip`. Remove this to start on that part of the tutorial. - * - * Start the tutorial by running: `npm run tutorial`. - * - * *Hint: most methods and functions are fairly well documented in jsDoc, which is easily accessed through TypeScript* - */ -describe.skip('intro', () => { - it('should be clear what to do next', () => { - // At the start of the spec, there will be some setup. - let bool = false; - // Sometimes including an expectation, to show the current state. - expect(bool).to.be.false; - - /** - * If **Your Turn** is shown in a comment, there is work for you to do. - * This can also be indicated with the `__YOUR_TURN__` variable. - * It should be clear what to do here... - */ - bool = __YOUR_TURN__; - - // We use expectations like this to verify the result. - expect(bool, ` ---- Welcome to the tutorial! --- - -Please look in \`./tutorial/1 - intro.ts\` to see what to do next.. - `).to.be.true; - }); -}); - -/** - * Let's start with the `Derivable` basics. - */ -describe.skip('the basics', () => { - /** - * The `Atom` is the basic building block of `@politie/sherlock`. - * It holds a value which you can `get()` and `set()`. - */ - it('the `Atom`', () => { - // An `Atom` can be created with the `atom()` function. The parameter of this function is used as the initial value of the `Atom`. - const myValue$ = atom(1); - // Variables containing `Atom`s or any other `Derivable` are usually postfixed with a `$` to indicate this. Hence `myValue$`. - - // The `.get()` method can be used to get the current value of the `Atom`. - expect(myValue$.get()).to.equal(1); - - /** - * **Your Turn** - * Use the `.set()` method to change the value of the `Atom`. - */ - - expect(myValue$.get()).to.equal(2); - }); - - /** - * The `Atom` is a `Derivable`. This means it can be used to create a derived value. - * This derived value stays up to date with the original `Atom`. - * - * The easiest way to do this, is to call `.derive()` on another `Derivable`. - * Let's try this. - */ - it('the `Derivable`', () => { - const myValue$ = atom(1); - expect(myValue$.get()).to.equal(1); - - /** - * **Your Turn** - * We want to create a new `Derivable` that outputs the inverse of the original `Atom`. - * Use `myValue$.derive(val => ...)` to create the `myInverse$` variable. - */ - const myInverse$ = myValue$.derive(__YOUR_TURN__); - - expect(myInverse$.get()).to.equal(-1); - - // So if we set `myValue$` to -2: - myValue$.set(-2); - // `myInverse$` will change accordingly. - expect(myInverse$.get()).to.equal(2); - }); - - /** - * Of course, `Derivable`s are not only meant to get, set and derive state. - * You can also listen to the changes. - * - * This is done with the `.react()` method. - * This method is given a `function` that is executed every time the value of the `Derivable` changes. - */ - it('reacting to `Derivable`s', () => { - const myCounter$ = atom(0); - - let reacted = 0; - /** - * **Your Turn** - * Now react to `myCounter$`. In every `react()`, increase the `reacted` variable by one. - */ - - expect(reacted).to.equal(1); // `react()` will react immediately, more on that later. - - // And then we set the `Atom` a couple of times to make the `Derivable` react. - for (let i = 0; i <= 100; i++) { - // Set the value of the `Atom`. - myCounter$.set(i); - } - expect(reacted).to.equal(101); - }); -}); diff --git a/tutorial/2 - deriving.ts b/tutorial/2 - deriving.test.ts similarity index 100% rename from tutorial/2 - deriving.ts rename to tutorial/2 - deriving.test.ts diff --git a/tutorial/3 - reacting.ts b/tutorial/3 - reacting.test.ts similarity index 100% rename from tutorial/3 - reacting.ts rename to tutorial/3 - reacting.test.ts diff --git a/tutorial/4 - inner workings.ts b/tutorial/4 - inner workings.test.ts similarity index 100% rename from tutorial/4 - inner workings.ts rename to tutorial/4 - inner workings.test.ts diff --git a/tutorial/5 - unresolved.ts b/tutorial/5 - unresolved.test.ts similarity index 100% rename from tutorial/5 - unresolved.ts rename to tutorial/5 - unresolved.test.ts diff --git a/tutorial/6 - errors.ts b/tutorial/6 - errors.test.ts similarity index 100% rename from tutorial/6 - errors.ts rename to tutorial/6 - errors.test.ts diff --git a/tutorial/7 - conversion.ts b/tutorial/7 - conversion.test.ts similarity index 100% rename from tutorial/7 - conversion.ts rename to tutorial/7 - conversion.test.ts diff --git a/tutorial/8 - advanced.ts b/tutorial/8 - advanced.test.ts similarity index 100% rename from tutorial/8 - advanced.ts rename to tutorial/8 - advanced.test.ts diff --git a/tutorial/9 - expert.ts b/tutorial/9 - expert.test.ts similarity index 100% rename from tutorial/9 - expert.ts rename to tutorial/9 - expert.test.ts diff --git a/tutorial/jest.config.ts b/tutorial/jest.config.ts new file mode 100644 index 0000000..b2128a6 --- /dev/null +++ b/tutorial/jest.config.ts @@ -0,0 +1,18 @@ +import type { Config } from 'jest'; + +export default { + displayName: 'tutorial', + preset: '../jest.preset.js', + globals: {}, + testEnvironment: 'node', + transform: { + '^.+\\.[tj]sx?$': [ + 'ts-jest', + { + tsconfig: '/tsconfig.json', + }, + ], + }, + collectCoverage: false, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], +} satisfies Config; diff --git a/tutorial/tsconfig.json b/tutorial/tsconfig.json new file mode 100644 index 0000000..23dd4ff --- /dev/null +++ b/tutorial/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "types": ["jest", "jest-extended", "node"] + } +} From 89b4d83935bb31131bff2c59dd43b596f4a82143 Mon Sep 17 00:00:00 2001 From: Robin Kneepkens Date: Thu, 9 Nov 2023 14:40:05 +0100 Subject: [PATCH 04/30] Got test 1 in working order --- tutorial/1 - intro.test.ts | 158 ++++++++++++++++++------------------- 1 file changed, 79 insertions(+), 79 deletions(-) diff --git a/tutorial/1 - intro.test.ts b/tutorial/1 - intro.test.ts index df382b2..f27b3e3 100644 --- a/tutorial/1 - intro.test.ts +++ b/tutorial/1 - intro.test.ts @@ -1,3 +1,5 @@ +import { atom } from '../libs/sherlock/src'; + /** * **Your Turn** * If you see this variable, you should do something about it. :-) @@ -17,7 +19,9 @@ export const __YOUR_TURN__ = {} as any; * *Hint: most methods and functions are fairly well documented in jsDoc, which is easily accessed through TypeScript* */ describe('intro', () => { - it('should be clear what to do next', () => { + it(`--- Welcome to the tutorial! --- + + Please look in \`./tutorial/1 - intro.ts\` to see what to do next.`, () => { // At the start of the spec, there will be some setup. let bool = false; // Sometimes including an expectation, to show the current state. @@ -31,87 +35,83 @@ describe('intro', () => { bool = __YOUR_TURN__; // We use expectations like this to verify the result. - expect(` ---- Welcome to the tutorial! --- - -Please look in \`./tutorial/1 - intro.ts\` to see what to do next.. - `).toBeFalsy(); + expect(bool).toBeTrue(); }); }); /** * Let's start with the `Derivable` basics. */ -// describe.skip('the basics', () => { -// /** -// * The `Atom` is the basic building block of `@politie/sherlock`. -// * It holds a value which you can `get()` and `set()`. -// */ -// it('the `Atom`', () => { -// // An `Atom` can be created with the `atom()` function. The parameter of this function is used as the initial value of the `Atom`. -// const myValue$ = atom(1); -// // Variables containing `Atom`s or any other `Derivable` are usually postfixed with a `$` to indicate this. Hence `myValue$`. - -// // The `.get()` method can be used to get the current value of the `Atom`. -// expect(myValue$.get()).to.equal(1); - -// /** -// * **Your Turn** -// * Use the `.set()` method to change the value of the `Atom`. -// */ - -// expect(myValue$.get()).to.equal(2); -// }); - -// /** -// * The `Atom` is a `Derivable`. This means it can be used to create a derived value. -// * This derived value stays up to date with the original `Atom`. -// * -// * The easiest way to do this, is to call `.derive()` on another `Derivable`. -// * Let's try this. -// */ -// it('the `Derivable`', () => { -// const myValue$ = atom(1); -// expect(myValue$.get()).to.equal(1); - -// /** -// * **Your Turn** -// * We want to create a new `Derivable` that outputs the inverse of the original `Atom`. -// * Use `myValue$.derive(val => ...)` to create the `myInverse$` variable. -// */ -// const myInverse$ = myValue$.derive(__YOUR_TURN__); - -// expect(myInverse$.get()).to.equal(-1); - -// // So if we set `myValue$` to -2: -// myValue$.set(-2); -// // `myInverse$` will change accordingly. -// expect(myInverse$.get()).to.equal(2); -// }); - -// /** -// * Of course, `Derivable`s are not only meant to get, set and derive state. -// * You can also listen to the changes. -// * -// * This is done with the `.react()` method. -// * This method is given a `function` that is executed every time the value of the `Derivable` changes. -// */ -// it('reacting to `Derivable`s', () => { -// const myCounter$ = atom(0); - -// let reacted = 0; -// /** -// * **Your Turn** -// * Now react to `myCounter$`. In every `react()`, increase the `reacted` variable by one. -// */ - -// expect(reacted).to.equal(1); // `react()` will react immediately, more on that later. - -// // And then we set the `Atom` a couple of times to make the `Derivable` react. -// for (let i = 0; i <= 100; i++) { -// // Set the value of the `Atom`. -// myCounter$.set(i); -// } -// expect(reacted).to.equal(101); -// }); -// }); +describe.skip('the basics', () => { + /** + * The `Atom` is the basic building block of `@politie/sherlock`. + * It holds a value which you can `get()` and `set()`. + */ + it('the `Atom`', () => { + // An `Atom` can be created with the `atom()` function. The parameter of this function is used as the initial value of the `Atom`. + const myValue$ = atom(1); + // Variables containing `Atom`s or any other `Derivable` are usually postfixed with a `$` to indicate this. Hence `myValue$`. + + // The `.get()` method can be used to get the current value of the `Atom`. + expect(myValue$.get()).toEqual(1); + + /** + * **Your Turn** + * Use the `.set()` method to change the value of the `Atom`. + */ + + expect(myValue$.get()).toEqual(2); + }); + + /** + * The `Atom` is a `Derivable`. This means it can be used to create a derived value. + * This derived value stays up to date with the original `Atom`. + * + * The easiest way to do this, is to call `.derive()` on another `Derivable`. + * Let's try this. + */ + it('the `Derivable`', () => { + const myValue$ = atom(1); + expect(myValue$.get()).toEqual(1); + + /** + * **Your Turn** + * We want to create a new `Derivable` that outputs the inverse of the original `Atom`. + * Use `myValue$.derive(val => ...)` to create the `myInverse$` variable. + */ + const myInverse$ = myValue$.derive(__YOUR_TURN__); + + expect(myInverse$.get()).toEqual(-1); + + // So if we set `myValue$` to -2: + myValue$.set(-2); + // `myInverse$` will change accordingly. + expect(myInverse$.get()).toEqual(2); + }); + + /** + * Of course, `Derivable`s are not only meant to get, set and derive state. + * You can also listen to the changes. + * + * This is done with the `.react()` method. + * This method is given a `function` that is executed every time the value of the `Derivable` changes. + */ + it('reacting to `Derivable`s', () => { + const myCounter$ = atom(0); + + let reacted = 0; + /** + * **Your Turn** + * Now react to `myCounter$`. In every `react()`, increase the `reacted` variable by one. + */ + + expect(reacted).toEqual(1); // `react()` will react immediately, more on that later. + + // And then we set the `Atom` a couple of times to make the `Derivable` react. + for (let i = 0; i <= 100; i++) { + // Set the value of the `Atom`. + myCounter$.set(i); + } + expect(reacted).toEqual(101); + }); +}); From 5c15c5e0df2b4dddd36ec23c79bc92d8604e7c74 Mon Sep 17 00:00:00 2001 From: Robin Kneepkens Date: Thu, 9 Nov 2023 15:00:48 +0100 Subject: [PATCH 05/30] Got test 2 in working order Note that it's currently skipping the test cases. They have been validated to run (and fail) before committing though. --- tutorial/2 - deriving.test.ts | 61 +++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/tutorial/2 - deriving.test.ts b/tutorial/2 - deriving.test.ts index fa29289..452aba0 100644 --- a/tutorial/2 - deriving.test.ts +++ b/tutorial/2 - deriving.test.ts @@ -1,4 +1,3 @@ -import { expect } from 'chai'; import { atom, Derivable, derive } from '../libs/sherlock/src'; /** @@ -33,11 +32,11 @@ describe.skip('deriving', () => { */ const lyric$ = text$.derive(txt => txt); // We can combine txt with `repeat$.get()` here. - expect(lyric$.get()).to.equal(`It won't be long`); + expect(lyric$.get()).toEqual(`It won't be long`); text$.set(' yeah'); repeat$.set(3); - expect(lyric$.get()).to.equal(` yeah yeah yeah`); + expect(lyric$.get()).toEqual(` yeah yeah yeah`); }); /** @@ -61,9 +60,9 @@ describe.skip('deriving', () => { const buzz$: Derivable = myCounter$.derive(__YOUR_TURN__); // Should return 'Buzz' when `myCounter$` is a multiple of 5 and '' otherwise. const fizzBuzz$: Derivable = derive(__YOUR_TURN__); - expect(fizz$.get()).to.equal(''); - expect(buzz$.get()).to.equal(''); - expect(fizzBuzz$.get()).to.equal(1); + expect(fizz$.get()).toEqual(''); + expect(buzz$.get()).toEqual(''); + expect(fizzBuzz$.get()).toEqual(1); for (let count = 1; count <= 100; count++) { // Set the value of the `Atom`, myCounter$.set(count); @@ -74,14 +73,18 @@ describe.skip('deriving', () => { }); function checkFizzBuzz(count: number, out: string | number) { - if (count % 3 + count % 5 === 0) { // If `count` is a multiple of 3 AND 5, output 'FizzBuzz'. - expect(out).to.equal('FizzBuzz'); - } else if (count % 3 === 0) { // If `count` is a multiple of 3, output 'Fizz'. - expect(out).to.equal('Fizz'); - } else if (count % 5 === 0) { // If `count` is a multiple of 5, output 'Buzz'. - expect(out).to.equal('Buzz'); - } else { // Otherwise just output the `count` itself. - expect(out).to.equal(count); + if ((count % 3) + (count % 5) === 0) { + // If `count` is a multiple of 3 AND 5, output 'FizzBuzz'. + expect(out).toEqual('FizzBuzz'); + } else if (count % 3 === 0) { + // If `count` is a multiple of 3, output 'Fizz'. + expect(out).toEqual('Fizz'); + } else if (count % 5 === 0) { + // If `count` is a multiple of 5, output 'Buzz'. + expect(out).toEqual('Buzz'); + } else { + // Otherwise just output the `count` itself. + expect(out).toEqual(count); } } @@ -104,16 +107,16 @@ describe.skip('deriving', () => { }); // The first tweet should have automatically been added to the `pastTweets` array. - expect(pastTweets).to.have.length(1); - expect(pastTweets[0]).to.contain('Barack'); - expect(pastTweets[0]).to.contain('First tweet'); + expect(pastTweets).toHaveLength(1); + expect(pastTweets[0]).toContain('Barack'); + expect(pastTweets[0]).toContain('First tweet'); // Let's add a famous quote by Mr Barack: tweet$.set('We need to reject any politics that targets people because of race or religion.'); // As expected this is automatically added to the log. - expect(pastTweets).to.have.length(2); - expect(pastTweets[1]).to.contain('Barack'); - expect(pastTweets[1]).to.contain('reject'); + expect(pastTweets).toHaveLength(2); + expect(pastTweets[1]).toContain('Barack'); + expect(pastTweets[1]).toContain('reject'); // But what if the user changes? currentUser$.set('Donald'); @@ -122,9 +125,9 @@ describe.skip('deriving', () => { * **Your Turn** * Time to set your own expectations. */ - expect(pastTweets).to.have.length(2); // Is there a new tweet? - expect(pastTweets[2]).to.contain(__YOUR_TURN__); // Who sent it? Donald? Or Barack? - expect(pastTweets[2]).to.contain(__YOUR_TURN__); // What did he tweet? + expect(pastTweets).toHaveLength(2); // Is there a new tweet? + expect(pastTweets[2]).toContain(__YOUR_TURN__); // Who sent it? Donald? Or Barack? + expect(pastTweets[2]).toContain(__YOUR_TURN__); // What did he tweet? /** * As you can see, this is something to look out for. @@ -158,8 +161,16 @@ describe.skip('deriving', () => { * `fizz$` and `buzz$` can be completed with only `.is(...)`, `.and(...)` and `.or(...)`; * Make sure the output of those `Derivable`s is either 'Fizz'/'Buzz' or ''. */ - const fizz$ = myCounter$.derive(count => count % 3).is(__YOUR_TURN__).and(__YOUR_TURN__).or(__YOUR_TURN__) as Derivable; - const buzz$ = myCounter$.derive(count => count % 5).is(__YOUR_TURN__).and(__YOUR_TURN__).or(__YOUR_TURN__) as Derivable; + const fizz$ = myCounter$ + .derive(count => count % 3) + .is(__YOUR_TURN__) + .and(__YOUR_TURN__) + .or(__YOUR_TURN__) as Derivable; + const buzz$ = myCounter$ + .derive(count => count % 5) + .is(__YOUR_TURN__) + .and(__YOUR_TURN__) + .or(__YOUR_TURN__) as Derivable; const fizzBuzz$ = derive(() => fizz$.get() + buzz$.get()).or(__YOUR_TURN__); for (let count = 1; count <= 100; count++) { From d377fa088c067a47972ecae141c45d1145400eee Mon Sep 17 00:00:00 2001 From: Robin Kneepkens Date: Thu, 9 Nov 2023 15:53:14 +0100 Subject: [PATCH 06/30] Got test 3 in working order Again skipping by default. --- tutorial/3 - reacting.test.ts | 41 +++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/tutorial/3 - reacting.test.ts b/tutorial/3 - reacting.test.ts index 20bd021..3788236 100644 --- a/tutorial/3 - reacting.test.ts +++ b/tutorial/3 - reacting.test.ts @@ -1,4 +1,3 @@ -import { expect } from 'chai'; import { atom } from '../libs/sherlock/src'; /** @@ -16,19 +15,25 @@ describe.skip('reacting', () => { let wasCalledTimes: number; // and record the last value it reacted to. let lastValue: any; - beforeEach('reset the values', () => { + + // reset the values + beforeEach(() => { wasCalledTimes = 0; lastValue = undefined; }); + // The reactor to be given to the `.react()` method. function reactor(val: any) { wasCalledTimes++; lastValue = val; } + // Of course we are lazy and don't want to type these assertions over and over. :-) function expectReact(reactions: number, value?: any) { - expect(wasCalledTimes, 'Reaction was called # times').to.equal(reactions); - expect(lastValue, 'Last value of the reaction was #').to.equal(value); + // Reaction was called # times + expect(wasCalledTimes).toEqual(reactions); + // Last value of the reaction was # + expect(lastValue).toEqual(value); } /** @@ -42,7 +47,7 @@ describe.skip('reacting', () => { it('reacting synchronously', () => { const myAtom$ = atom('initial value'); // A trivial `expect` to silence TypeScript's noUnusedLocals. - expect(myAtom$.get()).to.equal('initial value'); + expect(myAtom$.get()).toEqual('initial value'); // There should not have been a reaction yet expectReact(0); @@ -70,7 +75,7 @@ describe.skip('reacting', () => { it('with the stopper function', () => { const myAtom$ = atom('initial value'); // A trivial `expect` to silence TypeScript's noUnusedLocals - expect(myAtom$.get()).to.equal('initial value'); + expect(myAtom$.get()).toEqual('initial value'); /** * **Your Turn** @@ -97,7 +102,7 @@ describe.skip('reacting', () => { it('with the stopper callback', () => { const myAtom$ = atom('initial value'); // A trivial `expect` to silence TypeScript's noUnusedLocals - expect(myAtom$.get()).to.equal('initial value'); + expect(myAtom$.get()).toEqual('initial value'); /** * **Your Turn** @@ -115,7 +120,6 @@ describe.skip('reacting', () => { // And the reaction stopped. expectReact(1, 'initial value'); }); - }); /** @@ -135,7 +139,8 @@ describe.skip('reacting', () => { describe('reacting `until`', () => { const boolean$ = atom(false); const string$ = atom('Value'); - beforeEach('reset', () => { + beforeEach(() => { + // reset boolean$.set(false); string$.set('Value'); }); @@ -149,17 +154,17 @@ describe.skip('reacting', () => { * Try giving `boolean$` as `until` option. */ string$.react(reactor, __YOUR_TURN__); - expectReact(1, 'Value'); // It should react directly as usual. + expectReact(1, 'Value'); // It should react directly as usual. string$.set('New value'); expectReact(2, 'New value'); // It should keep reacting as usual. - boolean$.set(true); // We set `boolean$` to true, to stop the reaction + boolean$.set(true); // We set `boolean$` to true, to stop the reaction expectReact(2, 'New value'); // The reactor has immediately stopped, so it still reacted only twice. - boolean$.set(false); // Even when `boolean$` is set to `false` again - string$.set('Another value'); // And a new value is introduced - expectReact(2, 'New value'); // The reactor won't start up again, so it still reacted only twice. + boolean$.set(false); // Even when `boolean$` is set to `false` again + string$.set('Another value'); // And a new value is introduced + expectReact(2, 'New value'); // The reactor won't start up again, so it still reacted only twice. }); /** @@ -177,7 +182,7 @@ describe.skip('reacting', () => { string$.set('Newer Value'); expectReact(3, 'Newer Value'); // It should react as usual. - string$.set(''); // We set `string$` to an empty string, to stop the reaction + string$.set(''); // We set `string$` to an empty string, to stop the reaction expectReact(3, 'Newer Value'); // The reactor was immediately stopped, so even the empty string was never given to the reactor }); @@ -195,7 +200,7 @@ describe.skip('reacting', () => { string$.set('Newer Value'); expectReact(3, 'Newer Value'); // It should react as usual. - string$.set(''); // We set `string$` to an empty string, to stop the reaction + string$.set(''); // We set `string$` to an empty string, to stop the reaction expectReact(3, 'Newer Value'); // The reactor was immediately stopped, so even the empty string was never given to the reactor }); }); @@ -298,7 +303,6 @@ describe.skip('reacting', () => { finished$.set(true); expectReact(1, true); }); - }); describe('challenge', () => { @@ -325,8 +329,7 @@ describe.skip('reacting', () => { expectReact(1, false); // It should not react again after this - expect(connected$.connected).to.be.false; + expect(connected$.connected).toBeFalse; }); - }); }); From a20df399117df5ac455a35a25a9c28d0654eed3b Mon Sep 17 00:00:00 2001 From: Robin Kneepkens Date: Fri, 10 Nov 2023 13:17:01 +0100 Subject: [PATCH 07/30] Got test 4 in working order (I think) There's still one part at line 121 where I'm not sure what's happening --- tutorial/4 - inner workings.test.ts | 108 ++++++++++++++-------------- 1 file changed, 55 insertions(+), 53 deletions(-) diff --git a/tutorial/4 - inner workings.test.ts b/tutorial/4 - inner workings.test.ts index 5324868..257a1b4 100644 --- a/tutorial/4 - inner workings.test.ts +++ b/tutorial/4 - inner workings.test.ts @@ -1,6 +1,4 @@ -import { expect } from 'chai'; import { Seq } from 'immutable'; -import { spy } from 'sinon'; import { atom } from '../libs/sherlock/src'; /** @@ -22,15 +20,20 @@ describe.skip('inner workings', () => { const number$ = atom(1); const string$ = atom('one'); - const reacted = spy(); + const reacted = jest.fn(); switch$ // This `.derive()` is the one we are testing when true, it will return the `number` otherwise the `string` - .derive(s => s ? number$.get() : string$.get()) + .derive(s => (s ? number$.get() : string$.get())) + // Note: reacted is being called as reacted(value, stop), + // where stop is a function used to stop the reaction from within reacted. .react(reacted); - // The first time should not surprise anyone, the derivation was called and returned the right result - expect(reacted).to.have.been.calledOnceWith(1); + // The first time should not surprise anyone, the derivation + // was called and returned the right result. + // Again note here the second expectation (.toBeFunction()) to + // catch the stop function that was part of the .react() signature. + expect(reacted).toHaveBeenCalledExactlyOnceWith(1, expect.toBeFunction()); // `switch$` is still set to true (number) string$.set('two'); @@ -39,8 +42,8 @@ describe.skip('inner workings', () => { * **Your Turn** * What do you expect? */ - expect(reacted).to.have.callCount(__YOUR_TURN__); - expect(reacted.lastCall).to.be.calledWith(__YOUR_TURN__); + expect(reacted).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reacted).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); // `switch$` is still set to true (number) number$.set(2); @@ -49,11 +52,11 @@ describe.skip('inner workings', () => { * **Your Turn** * What do you expect? */ - expect(reacted).to.have.callCount(__YOUR_TURN__); - expect(reacted.lastCall).to.be.calledWith(__YOUR_TURN__); + expect(reacted).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reacted).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); // Now let's reset the spy, so callCount should be 0 again. - reacted.resetHistory(); + reacted.mockClear(); // `switch$` is set to false (string) switch$.set(false); @@ -63,8 +66,8 @@ describe.skip('inner workings', () => { * **Your Turn** * What do you expect now? */ - expect(reacted).to.have.callCount(__YOUR_TURN__); - expect(reacted.lastCall).to.be.calledWith(__YOUR_TURN__); + expect(reacted).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reacted).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); }); /** @@ -72,7 +75,7 @@ describe.skip('inner workings', () => { * So let's test this. */ it('lazy execution', () => { - const hasDerived = spy(); + const hasDerived = jest.fn(); const myAtom$ = atom(true); const myDerivation$ = myAtom$.derive(hasDerived); @@ -81,18 +84,17 @@ describe.skip('inner workings', () => { * **Your Turn** * We have created a new `Derivable` by deriving the `Atom`. But have not called `.get()` on that new `Derivable`. * Do you think the `hasDerived` function has been called? And how many times? - * * *Hint: you can use sinonChai's `.to.have.been.called`/`.to.have.been.calledOnce`/`to.have.callCount(...)`/etc..* */ - expect(hasDerived).to.have.callCount(__YOUR_TURN__); // Well, what do you expect? + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); // Well, what do you expect? myDerivation$.get(); - expect(hasDerived).to.have.callCount(__YOUR_TURN__); // And after a `.get()`? + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); // And after a `.get()`? myDerivation$.get(); - expect(hasDerived).to.have.callCount(__YOUR_TURN__); // And after the second `.get()`? Is there an extra call? + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); // And after the second `.get()`? Is there an extra call? /** * The state of any `Derivable` can change at any moment. @@ -108,13 +110,13 @@ describe.skip('inner workings', () => { * So a `.get()` should not have to be calculated. */ it('while reacting', () => { - const hasDerived = spy(); + const hasDerived = jest.fn(); const myAtom$ = atom(true); const myDerivation$ = myAtom$.derive(hasDerived); // It should not have done anything at this moment - expect(hasDerived).to.not.have.been.called; + expect(hasDerived).not.toHaveBeenCalled(); const stopper = myDerivation$.react(() => ''); @@ -123,27 +125,27 @@ describe.skip('inner workings', () => { * Ok, it's your turn to complete the expectations. * *Hint: you can use `.calledOnce`/`.calledTwice` etc or `.callCount()`* */ - expect(hasDerived).to.have.callCount(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); myDerivation$.get(); - expect(hasDerived).to.have.callCount(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); myAtom$.set(false); - expect(hasDerived).to.have.callCount(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); myDerivation$.get(); - expect(hasDerived).to.have.callCount(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); stopper(); - expect(hasDerived).to.have.callCount(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); myDerivation$.get(); - expect(hasDerived).to.have.callCount(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); /** * Since the `.react()` already listens to the value(changes) there is no need to recalculate whenever a `.get()` is called. @@ -156,8 +158,8 @@ describe.skip('inner workings', () => { * But there is one more trick up it's sleeve. */ it('cached changes', () => { - const first = spy(); - const second = spy(); + const first = jest.fn(); + const second = jest.fn(); const myAtom$ = atom(1); const first$ = myAtom$.derive(i => { @@ -167,14 +169,14 @@ describe.skip('inner workings', () => { const second$ = first$.derive(second); // As always, they should not have fired yet - expect(first).to.not.have.been.called; - expect(second).to.not.have.been.called; + expect(first).not.toHaveBeenCalled(); + expect(second).not.toHaveBeenCalled(); second$.react(() => ''); // And as expected, they now should both have fired once - expect(first).to.have.been.calledOnce; - expect(second).to.have.been.calledOnce; + expect(first).toHaveBeenCalledOnce(); + expect(second).toHaveBeenCalledOnce(); /** * **Your Turn** @@ -182,23 +184,23 @@ describe.skip('inner workings', () => { */ myAtom$.set(1); // Note that this is the same value as it was initialized with - expect(first).to.have.callCount(__YOUR_TURN__); - expect(second).to.have.callCount(__YOUR_TURN__); + expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); myAtom$.set(2); - expect(first).to.have.callCount(__YOUR_TURN__); - expect(second).to.have.callCount(__YOUR_TURN__); + expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); myAtom$.set(3); - expect(first).to.have.callCount(__YOUR_TURN__); - expect(second).to.have.callCount(__YOUR_TURN__); + expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); myAtom$.set(4); - expect(first).to.have.callCount(__YOUR_TURN__); - expect(second).to.have.callCount(__YOUR_TURN__); + expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); /** * Can you explain the behavior above? @@ -218,7 +220,7 @@ describe.skip('inner workings', () => { */ it('equality', () => { const atom$ = atom({}); - const hasReacted = spy(); + const hasReacted = jest.fn(); atom$.react(hasReacted, { skipFirst: true }); @@ -228,7 +230,7 @@ describe.skip('inner workings', () => { * **Your Turn** * The `Atom` is set with exactly the same object as before. Will the `.react()` fire? */ - expect(hasReacted).to.have.callCount(__YOUR_TURN__); + expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); /** * But what if you use an object, that can be easily compared through a library like `ImmutableJS` @@ -236,15 +238,15 @@ describe.skip('inner workings', () => { */ atom$.set(Seq.Indexed.of(1, 2, 3)); // Let's reset the spy here, to start over - hasReacted.resetHistory(); - expect(hasReacted).to.not.have.been.called; + hasReacted.mockClear(); + expect(hasReacted).not.toHaveBeenCalled(); atom$.set(Seq.Indexed.of(1, 2, 3)); /** * **Your Turn** * Do you think the `.react()` fired with this new value? */ - expect(hasReacted).to.have.callCount(__YOUR_TURN__); + expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); atom$.set(Seq.Indexed.of(1, 2)); @@ -252,7 +254,7 @@ describe.skip('inner workings', () => { * **Your Turn** * And now? */ - expect(hasReacted).to.have.callCount(__YOUR_TURN__); + expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); /** * In `@politie/sherlock` equality is a bit complex. @@ -271,15 +273,15 @@ describe.skip('inner workings', () => { const number$ = atom(1); const string$ = atom('one'); - const reacted = spy(); + const reacted = jest.fn(); switch$ // This `.derive()` is the one we are testing when true, it will return the `number` otherwise the `string` - .derive(s => s ? number$.get() : string$.get()) + .derive(s => (s ? number$.get() : string$.get())) .react(reacted); // The first time should not surprise anyone, the derivation was called and returned the right result - expect(reacted).to.have.been.calledOnceWith(1); + expect(reacted).toHaveBeenCalledExactlyOnceWith(1, expect.toBeFunction()); // `switch$` is still set to true (number) string$.set('two'); @@ -288,7 +290,7 @@ describe.skip('inner workings', () => { * **Your Turn** * What do you expect? */ - expect(reacted).to.have.callCount(__YOUR_TURN__); + expect(reacted).toHaveBeenCalledTimes(__YOUR_TURN__); // `switch$` is still set to true (number) number$.set(2); @@ -297,10 +299,10 @@ describe.skip('inner workings', () => { * **Your Turn** * What do you expect? */ - expect(reacted).to.have.callCount(__YOUR_TURN__); + expect(reacted).toHaveBeenCalledTimes(__YOUR_TURN__); // Now let's reset the spy, so callCount should be 0 again. - reacted.resetHistory(); + reacted.mockClear(); // `switch$` is set to false (string) switch$.set(false); @@ -310,6 +312,6 @@ describe.skip('inner workings', () => { * **Your Turn** * What do you expect now? */ - expect(reacted).to.have.callCount(__YOUR_TURN__); + expect(reacted).toHaveBeenCalledTimes(__YOUR_TURN__); }); }); From c35c5eb3795a8e215ba86132a9e655bb662aaa56 Mon Sep 17 00:00:00 2001 From: Robin Kneepkens Date: Fri, 10 Nov 2023 13:46:45 +0100 Subject: [PATCH 08/30] Got test 5 in working order As always, set to skip by default. --- tutorial/5 - unresolved.test.ts | 42 ++++++++++++++++----------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/tutorial/5 - unresolved.test.ts b/tutorial/5 - unresolved.test.ts index 97500b0..4620798 100644 --- a/tutorial/5 - unresolved.test.ts +++ b/tutorial/5 - unresolved.test.ts @@ -1,5 +1,3 @@ -import { expect } from 'chai'; -import { spy } from 'sinon'; import { atom, Derivable, DerivableAtom } from '../libs/sherlock/src'; /** @@ -24,14 +22,14 @@ describe.skip('unresolved', () => { // Note that you will need to indicate the type of this atom, since it can't be inferred by TypeScript this way. const myAtom$ = atom.unresolved(); - expect(myAtom$.resolved).to.equal(__YOUR_TURN__); + expect(myAtom$.resolved).toEqual(__YOUR_TURN__); /** * **Your Turn** * Resolve the atom, it's pretty easy */ - expect(myAtom$.resolved).to.be.true; + expect(myAtom$.resolved).toBeTrue(); }); /** @@ -45,8 +43,8 @@ describe.skip('unresolved', () => { */ const myAtom$: DerivableAtom = __YOUR_TURN__; - expect(myAtom$.resolved).to.be.false; - expect(() => myAtom$.get()).to.throw('Could not get value, derivable is unresolved'); + expect(myAtom$.resolved).toBeFalse(); + expect(() => myAtom$.get()).toThrow('Could not get value, derivable is unresolved'); myAtom$.set('finally!'); @@ -54,8 +52,10 @@ describe.skip('unresolved', () => { * **Your Turn** * What do you expect? */ - expect(myAtom$.resolved).to.equal(__YOUR_TURN__); - expect(() => myAtom$.get()).to; // .throw()/.not.to.throw()? + expect(myAtom$.resolved).toEqual(__YOUR_TURN__); + + // .toThrow() or .not.toThrow()? + expect(() => myAtom$.get()) /*__YOUR_TURN__*/; }); /** @@ -66,23 +66,23 @@ describe.skip('unresolved', () => { it('reacting to `unresolved`', () => { const myAtom$ = atom.unresolved(); - const hasReacted = spy(); + const hasReacted = jest.fn(); myAtom$.react(hasReacted); /** * **Your Turn** * What do you expect? */ - expect(hasReacted).to.have.callCount(__YOUR_TURN__) - .and.calledWith(__YOUR_TURN__); + expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasReacted).toHaveBeenCalledWith(__YOUR_TURN__); /** * **Your Turn** * Now make the last expect succeed */ - expect(myAtom$.resolved).to.be.true; - expect(hasReacted).to.have.been.calledOnceWith(`woohoow, I was called`); + expect(myAtom$.resolved).toBeTrue(); + expect(hasReacted).toHaveBeenCalledExactlyOnceWith(`woohoow, I was called`); }); /** @@ -92,21 +92,21 @@ describe.skip('unresolved', () => { it('can become `unresolved` again', () => { const myAtom$ = atom.unresolved(); - expect(myAtom$.resolved).to.be.false; + expect(myAtom$.resolved).toBeFalse(); /** * **Your Turn** * Set the value.. */ - expect(myAtom$.get()).to.equal(`it's alive!`); + expect(myAtom$.get()).toEqual(`it's alive!`); /** * **Your Turn** * Unset the value.. (*Hint: TypeScript is your friend*) */ - expect(myAtom$.resolved).to.be.false; + expect(myAtom$.resolved).toBeFalse(); }); /** @@ -128,7 +128,7 @@ describe.skip('unresolved', () => { * **Your Turn** * Is `myDerivable$` expected to be `resolved`? */ - expect(myDerivable$.resolved).to.equal(__YOUR_TURN__); + expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); // Now let's set one of the two source `Atom`s myString$.set('some'); @@ -138,10 +138,10 @@ describe.skip('unresolved', () => { * What do you expect to see in `myDerivable$`. * And what if we set `myOtherString$`? */ - expect(myDerivable$.resolved).to.equal(__YOUR_TURN__); + expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); myOtherString$.set('data'); - expect(myDerivable$.resolved).to.equal(__YOUR_TURN__); - expect(myDerivable$.get()).to.equal(__YOUR_TURN__); + expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); + expect(myDerivable$.get()).toEqual(__YOUR_TURN__); /** * **Your Turn** @@ -149,6 +149,6 @@ describe.skip('unresolved', () => { * What do you expect `myDerivable$` to be? */ myString$.unset(); - expect(myDerivable$.resolved).to.equal(__YOUR_TURN__); + expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); }); }); From 7f7ff7d3895e4ffdb61abab3a53c2aa75ac26a3c Mon Sep 17 00:00:00 2001 From: Robin Kneepkens Date: Wed, 15 Nov 2023 11:54:47 +0100 Subject: [PATCH 09/30] Fixed weird import issues when running tutorials Some weird config/import spaghetti was going on. Now everything works with the '@' import notation. Co-authored-by: Paco van der Linden --- tutorial/1 - intro.test.ts | 2 +- tutorial/2 - deriving.test.ts | 2 +- tutorial/3 - reacting.test.ts | 2 +- tutorial/4 - inner workings.test.ts | 2 +- tutorial/5 - unresolved.test.ts | 2 +- tutorial/jest.config.ts | 2 +- tutorial/tsconfig.json | 11 +++++++---- tutorial/tsconfig.spec.json | 8 ++++++++ 8 files changed, 21 insertions(+), 10 deletions(-) create mode 100644 tutorial/tsconfig.spec.json diff --git a/tutorial/1 - intro.test.ts b/tutorial/1 - intro.test.ts index f27b3e3..0fe01b0 100644 --- a/tutorial/1 - intro.test.ts +++ b/tutorial/1 - intro.test.ts @@ -1,4 +1,4 @@ -import { atom } from '../libs/sherlock/src'; +import { atom } from '@skunkteam/sherlock'; /** * **Your Turn** diff --git a/tutorial/2 - deriving.test.ts b/tutorial/2 - deriving.test.ts index 452aba0..ae0f9ee 100644 --- a/tutorial/2 - deriving.test.ts +++ b/tutorial/2 - deriving.test.ts @@ -1,4 +1,4 @@ -import { atom, Derivable, derive } from '../libs/sherlock/src'; +import { atom, Derivable, derive } from '@skunkteam/sherlock'; /** * **Your Turn** diff --git a/tutorial/3 - reacting.test.ts b/tutorial/3 - reacting.test.ts index 3788236..9907680 100644 --- a/tutorial/3 - reacting.test.ts +++ b/tutorial/3 - reacting.test.ts @@ -1,4 +1,4 @@ -import { atom } from '../libs/sherlock/src'; +import { atom } from '@skunkteam/sherlock'; /** * **Your Turn** diff --git a/tutorial/4 - inner workings.test.ts b/tutorial/4 - inner workings.test.ts index 257a1b4..fcdf9d7 100644 --- a/tutorial/4 - inner workings.test.ts +++ b/tutorial/4 - inner workings.test.ts @@ -1,5 +1,5 @@ +import { atom } from '@skunkteam/sherlock'; import { Seq } from 'immutable'; -import { atom } from '../libs/sherlock/src'; /** * **Your Turn** diff --git a/tutorial/5 - unresolved.test.ts b/tutorial/5 - unresolved.test.ts index 4620798..a9dd2f7 100644 --- a/tutorial/5 - unresolved.test.ts +++ b/tutorial/5 - unresolved.test.ts @@ -1,4 +1,4 @@ -import { atom, Derivable, DerivableAtom } from '../libs/sherlock/src'; +import { atom, Derivable, DerivableAtom } from '@skunkteam/sherlock'; /** * **Your Turn** diff --git a/tutorial/jest.config.ts b/tutorial/jest.config.ts index b2128a6..d27ca0f 100644 --- a/tutorial/jest.config.ts +++ b/tutorial/jest.config.ts @@ -9,7 +9,7 @@ export default { '^.+\\.[tj]sx?$': [ 'ts-jest', { - tsconfig: '/tsconfig.json', + tsconfig: '/tsconfig.spec.json', }, ], }, diff --git a/tutorial/tsconfig.json b/tutorial/tsconfig.json index 23dd4ff..89cc8af 100644 --- a/tutorial/tsconfig.json +++ b/tutorial/tsconfig.json @@ -1,7 +1,10 @@ { "extends": "../tsconfig.base.json", - "compilerOptions": { - "module": "commonjs", - "types": ["jest", "jest-extended", "node"] - } + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.spec.json" + } + ] } diff --git a/tutorial/tsconfig.spec.json b/tutorial/tsconfig.spec.json new file mode 100644 index 0000000..750d7b4 --- /dev/null +++ b/tutorial/tsconfig.spec.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "types": ["jest", "jest-extended", "node"] + }, + "include": ["**/*.test.ts", "**/*.tests.ts", "**/*.d.ts", "jest.config.ts"] +} From 8957c53f815134cf8e2cd86747be65b4299b5fca Mon Sep 17 00:00:00 2001 From: Robin Kneepkens Date: Wed, 15 Nov 2023 12:14:32 +0100 Subject: [PATCH 10/30] Got tutorial 7 in a working state Currently skipped as always. Also removed some unused pieces of code: - Imports - Outer test suite that didn't do anything useful --- tutorial/7 - conversion.test.ts | 138 -------------------------------- tutorial/7 - utils.test.ts | 125 +++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 138 deletions(-) delete mode 100644 tutorial/7 - conversion.test.ts create mode 100644 tutorial/7 - utils.test.ts diff --git a/tutorial/7 - conversion.test.ts b/tutorial/7 - conversion.test.ts deleted file mode 100644 index c2fbe68..0000000 --- a/tutorial/7 - conversion.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { expect } from 'chai'; -import { spy } from 'sinon'; -import { pairwise, scan, struct } from '../libs/sherlock-utils/src'; -// import { fromObservable, toObservable } from '../extensions/sherlock-rxjs'; -import { atom } from '../libs/sherlock/src'; - -/** - * **Your Turn** - * If you see this variable, you should do something about it. :-) - */ -export const __YOUR_TURN__ = {} as any; - -describe('conversion', () => { - /** - * `@politie/sherlock` has the ability to produce and use Promises - */ - describe('promises', () => { - it('toPromise'); - it('fromPromise'); - }); - - describe('RxJS', () => { - it('toObservable'); - it('fromObservable'); - }); - - /** - * In the `@politie/sherlock-utils` lib, there are a couple of functions that can combine multiple values of a single `Derivable` - * or combine multiple `Derivable`s into one. We will show a couple of those here. - */ - describe.skip('utils', () => { - /** - * As the name suggests, `pairwise()` will call the given function with both the current and the previous state. - * - * *Note functions like `pairwise` and `scan` can be used with any callback. So it can be used both in a `.derive()` step and in a `.react()`* - */ - it('pairwise', () => { - expect(pairwise).to.exist; // use `pairwise` so the import is used. - - const myCounter$ = atom(1); - const reactSpy = spy(); - - /** - * **Your Turn** - * Now, use `pairwise()`, to subtract the previous value from the current - */ - myCounter$.derive(__YOUR_TURN__).react(reactSpy); - - expect(reactSpy).to.have.been.calledOnceWith(1); - - myCounter$.set(3); - - expect(reactSpy).to.have.been.calledTwice.and.calledWith(2); - - myCounter$.set(45); - - expect(reactSpy).to.have.been.calledThrice.and.calledWith(42); - }); - - /** - * `scan` is the `Derivable` version of `Array.prototype.reduce`. It will be called with the current state and the last emitted value. - * - * *Note: as with `pairwise()` this is useable in both a `.derive()` and `.react()` method* - */ - it('scan', () => { - expect(scan).to.exist; // use `scan` so the import is used. - - const myCounter$ = atom(1); - const reactSpy = spy(); - - /** - * **Your Turn** - * Now, use `scan()`, to add all the emitted values together - */ - myCounter$.derive(__YOUR_TURN__).react(reactSpy); - - expect(reactSpy).to.have.been.calledOnceWith(1); - - myCounter$.set(3); - - expect(reactSpy).to.have.been.calledTwice.and.calledWith(4); - - myCounter$.set(45); - - expect(reactSpy).to.have.been.calledThrice.and.calledWith(49); - - /** - * *BONUS: Try using `scan()` (or `pairwise()`) directly in the `.react()` method.* - */ - }); - - /** - * A `struct()` can combine an Object/Array of `Derivable`s into one `Derivable`, that contains the values of that `Derivable`. - * The Object/Array that is in the output of `struct()` will have the same structure as the original Object/Array. - * - * This is best explained in practice. - */ - it('struct', () => { - expect(struct).to.exist; // use `struct` so the import is used. - - const allMyAtoms = { - regularProp: 'prop', - string: atom('my string'), - number: atom(1), - sub: { - string: atom('my substring'), - }, - }; - - const myOneAtom$ = struct(allMyAtoms); - - expect(myOneAtom$.get()).to.deep.equal({ - regularProp: 'prop', - string: 'my string', - number: 1, - sub: { - string: 'my substring', - }, - }); - - allMyAtoms.regularProp = 'new value'; - allMyAtoms.sub.string.set('my new substring'); - - /** - * **Your Turn** - * Now have a look at the properties of `myOneAtom$`. Is this what you expect? - */ - expect(myOneAtom$.get()).to.deep.equal({ - regularProp: __YOUR_TURN__, - string: __YOUR_TURN__, - number: __YOUR_TURN__, - sub: { - string: __YOUR_TURN__, - }, - }); - }); - }); -}); diff --git a/tutorial/7 - utils.test.ts b/tutorial/7 - utils.test.ts new file mode 100644 index 0000000..6e329e9 --- /dev/null +++ b/tutorial/7 - utils.test.ts @@ -0,0 +1,125 @@ +import { atom } from '@skunkteam/sherlock'; +import { pairwise, scan, struct } from '@skunkteam/sherlock-utils'; + +/** + * **Your Turn** + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +/** + * In the `sherlock-utils` lib, there are a couple of functions that can combine multiple values of a single `Derivable` + * or combine multiple `Derivable`s into one. We will show a couple of those here. + */ +describe.skip('utils', () => { + /** + * As the name suggests, `pairwise()` will call the given function with both the current and the previous state. + * + * *Note functions like `pairwise` and `scan` can be used with any callback. So it can be used both in a `.derive()` step and in a `.react()`* + */ + it('pairwise', () => { + expect(pairwise).toBe(pairwise); // use `pairwise` so the import is used. --> Does it work like this? + + const myCounter$ = atom(1); + const reactSpy = jest.fn(); + + /** + * **Your Turn** + * Now, use `pairwise()`, to subtract the previous value from the current + */ + myCounter$.derive(__YOUR_TURN__).react(reactSpy); + + expect(reactSpy).toHaveBeenCalledExactlyOnceWith(1, expect.toBeFunction()); + + myCounter$.set(3); + + // Note: two `expect`s in a row is the same as chaining with `.and`, right? + expect(reactSpy).toHaveBeenCalledTimes(2); + expect(reactSpy).toHaveBeenCalledWith(2, expect.toBeFunction()); + + myCounter$.set(45); + + expect(reactSpy).toHaveBeenCalledTimes(3); + expect(reactSpy).toHaveBeenCalledWith(42, expect.toBeFunction()); + }); + + /** + * `scan` is the `Derivable` version of `Array.prototype.reduce`. It will be called with the current state and the last emitted value. + * + * *Note: as with `pairwise()` this is useable in both a `.derive()` and `.react()` method* + */ + it('scan', () => { + expect(scan).toBe(scan); // use `scan` so the import is used. + + const myCounter$ = atom(1); + const reactSpy = jest.fn(); + + /** + * **Your Turn** + * Now, use `scan()`, to add all the emitted values together + */ + myCounter$.derive(__YOUR_TURN__).react(reactSpy); + + expect(reactSpy).toHaveBeenCalledExactlyOnceWith(1, expect.toBeFunction()); + + myCounter$.set(3); + + expect(reactSpy).toHaveBeenCalledTimes(2); + expect(reactSpy).toHaveBeenCalledWith(4, expect.toBeFunction()); + + myCounter$.set(45); + + expect(reactSpy).toHaveBeenCalledTimes(3); + expect(reactSpy).toHaveBeenCalledWith(49, expect.toBeFunction()); + + /** + * *BONUS: Try using `scan()` (or `pairwise()`) directly in the `.react()` method.* + */ + }); + + /** + * A `struct()` can combine an Object/Array of `Derivable`s into one `Derivable`, that contains the values of that `Derivable`. + * The Object/Array that is in the output of `struct()` will have the same structure as the original Object/Array. + * + * This is best explained in practice. + */ + it('struct', () => { + expect(struct).toBe(struct); // use `struct` so the import is used. + + const allMyAtoms = { + regularProp: 'prop', + string: atom('my string'), + number: atom(1), + sub: { + string: atom('my substring'), + }, + }; + + const myOneAtom$ = struct(allMyAtoms); + + expect(myOneAtom$.get()).toEqual({ + regularProp: 'prop', + string: 'my string', + number: 1, + sub: { + string: 'my substring', + }, + }); + + allMyAtoms.regularProp = 'new value'; + allMyAtoms.sub.string.set('my new substring'); + + /** + * **Your Turn** + * Now have a look at the properties of `myOneAtom$`. Is this what you expect? + */ + expect(myOneAtom$.get()).toEqual({ + regularProp: __YOUR_TURN__, + string: __YOUR_TURN__, + number: __YOUR_TURN__, + sub: { + string: __YOUR_TURN__, + }, + }); + }); +}); From a1982442a14f939b6ec6691bd86c5750e0b808fc Mon Sep 17 00:00:00 2001 From: Robin Kneepkens Date: Fri, 17 Nov 2023 14:14:08 +0100 Subject: [PATCH 11/30] Got tutorial 8 in working state Still have to iron out some details probably --- tutorial/8 - advanced.test.ts | 143 ++++++++++++++++++++-------------- 1 file changed, 83 insertions(+), 60 deletions(-) diff --git a/tutorial/8 - advanced.test.ts b/tutorial/8 - advanced.test.ts index 4ac152b..457b95b 100644 --- a/tutorial/8 - advanced.test.ts +++ b/tutorial/8 - advanced.test.ts @@ -1,7 +1,5 @@ -import { expect } from 'chai'; +import { atom, constant, Derivable, derive, SettableDerivable } from '@skunkteam/sherlock'; import { Map as ImmutableMap } from 'immutable'; -import { spy } from 'sinon'; -import { atom, constant, Derivable, derive, SettableDerivable } from '../libs/sherlock/src'; /** * **Your Turn** @@ -28,8 +26,12 @@ describe.skip('advanced', () => { * **Your Turn** * What do you expect this `Derivable` to do on `.set()`, `.get()` etc? */ - expect(() => c.get()).to; // .throw()/.not.to.throw()? - expect(() => c.set('new value')).to; // .throw()/.not.to.throw()? + + // Remove this after taking your turn below. + expect(false).toBe(true); + // .toThrow() or .not.toThrow()? ↴ (2x) + expect(() => c.get()) /* __YOUR_TURN__ */; + expect(() => c.set('new value')) /* __YOUR_TURN__ */; }); /** @@ -41,18 +43,22 @@ describe.skip('advanced', () => { */ it('`.swap()`', () => { // This is a separate function, because you might be able to use this later - function plusOne(num: number) { return num + 1; } + function plusOne(num: number) { + return num + 1; + } const myCounter$ = atom(0); /** * **Your Turn** * Rewrite the `.get()`/`.set()` combos below using `.swap()`. */ + expect(false).toBe(true); + // Remove this after taking your turn below. myCounter$.set(plusOne(myCounter$.get())); - expect(myCounter$.get()).to.equal(1); + expect(myCounter$.get()).toEqual(1); myCounter$.set(plusOne(myCounter$.get())); - expect(myCounter$.get()).to.equal(2); + expect(myCounter$.get()).toEqual(2); }); /** @@ -70,7 +76,7 @@ describe.skip('advanced', () => { * **Your Turn** * Use the `.value` accessor to get the current value. */ - expect(__YOUR_TURN__).to.equal('foo'); + expect(__YOUR_TURN__).toEqual('foo'); /** * **Your Turn** @@ -78,7 +84,7 @@ describe.skip('advanced', () => { */ myAtom$.value = __YOUR_TURN__; - expect(myAtom$.get()).to.equal('new value'); + expect(myAtom$.get()).toEqual('new value'); }); /** @@ -91,7 +97,7 @@ describe.skip('advanced', () => { /** * **Your Turn** */ - expect(myAtom$.value).to.equal(__YOUR_TURN__); + expect(myAtom$.value).toEqual(__YOUR_TURN__); }); /** @@ -104,19 +110,19 @@ describe.skip('advanced', () => { const usingGet$ = derive(() => myAtom$.get()); const usingVal$ = derive(() => myAtom$.value); - expect(usingGet$.get()).to.equal('foo'); - expect(usingVal$.get()).to.equal('foo'); + expect(usingGet$.get()).toEqual('foo'); + expect(usingVal$.get()).toEqual('foo'); /** * **Your Turn** * We just created two `Derivable`s that are almost exactly the same. * But what happens when their source becomes `unresolved`? */ - expect(usingGet$.resolved).to.equal(__YOUR_TURN__); - expect(usingVal$.resolved).to.equal(__YOUR_TURN__); + expect(usingGet$.resolved).toEqual(__YOUR_TURN__); + expect(usingVal$.resolved).toEqual(__YOUR_TURN__); myAtom$.unset(); - expect(usingGet$.resolved).to.equal(__YOUR_TURN__); - expect(usingVal$.resolved).to.equal(__YOUR_TURN__); + expect(usingGet$.resolved).toEqual(__YOUR_TURN__); + expect(usingVal$.resolved).toEqual(__YOUR_TURN__); }); }); @@ -128,8 +134,9 @@ describe.skip('advanced', () => { * - It can be made to be settable */ describe('`.map()`', () => { - const mapReactSpy = spy(); - beforeEach('reset the spy', () => mapReactSpy.resetHistory()); + const mapReactSpy = jest.fn(); + // Clear the spy before each test case. + beforeEach(() => mapReactSpy.mockClear()); it('triggers when the source changes', () => { const myAtom$ = atom(1); @@ -141,25 +148,25 @@ describe.skip('advanced', () => { mappedAtom$.react(mapReactSpy); - expect(mapReactSpy).to.have.been.calledOnceWith('1'); + expect(mapReactSpy).toHaveBeenCalledExactlyOnceWith('1', expect.toBeFunction()); myAtom$.set(3); - expect(mapReactSpy).to.have.been.calledTwice - .and.calledWith('333'); + expect(mapReactSpy).toHaveBeenCalledTimes(2); + expect(mapReactSpy).toHaveBeenLastCalledWith('333', expect.toBeFunction()); }); it('does not trigger when any other `Derivable` changes', () => { const myRepeat$ = atom(1); const myString$ = atom('ho'); - const deriveReactSpy = spy(); + const deriveReactSpy = jest.fn(); // Note that the `.map` uses both `myRepeat$` and `myString$` myRepeat$.map(r => myString$.get().repeat(r)).react(mapReactSpy); myRepeat$.derive(r => myString$.get().repeat(r)).react(deriveReactSpy); - expect(mapReactSpy).to.have.been.calledOnceWith('ho'); - expect(deriveReactSpy).to.have.been.calledOnceWith('ho'); + expect(mapReactSpy).toHaveBeenCalledExactlyOnceWith('ho', expect.toBeFunction()); + expect(deriveReactSpy).toHaveBeenCalledExactlyOnceWith('ho', expect.toBeFunction()); myRepeat$.value = 3; /** @@ -167,26 +174,29 @@ describe.skip('advanced', () => { * We changed`myRepeat$` to equal 3. * Do you expect both reactors to have fired? And with what? */ - expect(deriveReactSpy).to.have.callCount(__YOUR_TURN__); - expect(deriveReactSpy.lastCall).to.be.calledWith(__YOUR_TURN__); - expect(mapReactSpy).to.have.callCount(__YOUR_TURN__); - expect(mapReactSpy.lastCall).to.be.calledWith(__YOUR_TURN__); + expect(deriveReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(deriveReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + + expect(mapReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(mapReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); myString$.value = 'ha'; /** * **Your Turn** * And now that we have changed `myString$`? And when `myRepeat$` changed again? */ - expect(deriveReactSpy).to.have.callCount(__YOUR_TURN__); - expect(deriveReactSpy.lastCall).to.be.calledWith(__YOUR_TURN__); - expect(mapReactSpy).to.have.callCount(__YOUR_TURN__); - expect(mapReactSpy.lastCall).to.be.calledWith(__YOUR_TURN__); + expect(deriveReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(deriveReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + + expect(mapReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(mapReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); myRepeat$.value = 2; - expect(deriveReactSpy).to.have.callCount(__YOUR_TURN__); - expect(deriveReactSpy.lastCall).to.be.calledWith(__YOUR_TURN__); - expect(mapReactSpy).to.have.callCount(__YOUR_TURN__); - expect(mapReactSpy.lastCall).to.be.calledWith(__YOUR_TURN__); + expect(deriveReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(deriveReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + + expect(mapReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(mapReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); /** * As you can see, a change in `myString$` will not trigger an update. @@ -211,15 +221,15 @@ describe.skip('advanced', () => { __YOUR_TURN__, ); - expect(myInverse$.get()).to.equal(-1); + expect(myInverse$.get()).toEqual(-1); myInverse$.set(-2); /** * **Your Turn** */ - expect(myAtom$.get()).to.equal(__YOUR_TURN__); - expect(myInverse$.get()).to.equal(__YOUR_TURN__); + expect(myAtom$.get()).toEqual(__YOUR_TURN__); + expect(myInverse$.get()).toEqual(__YOUR_TURN__); }); }); @@ -236,18 +246,21 @@ describe.skip('advanced', () => { * *ImmutableJS can help fix this problem* */ describe('`.pluck()`', () => { - const reactSpy = spy(); - const reactPropSpy = spy(); + const reactSpy = jest.fn(); + const reactPropSpy = jest.fn(); let myMap$: SettableDerivable>; let firstProp$: SettableDerivable; - beforeEach('reset', () => { - reactPropSpy.resetHistory(); - reactSpy.resetHistory(); - myMap$ = atom>(ImmutableMap({ - firstProp: 'firstValue', - secondProp: 'secondValue', - })); + // Reset + beforeEach(() => { + reactPropSpy.mockClear(); + reactSpy.mockClear(); + myMap$ = atom>( + ImmutableMap({ + firstProp: 'firstValue', + secondProp: 'secondValue', + }), + ); /** * **Your Turn** * `.pluck()` 'firstProp' from `myMap$`. @@ -265,13 +278,16 @@ describe.skip('advanced', () => { * **Your Turn** * What do you expect the plucked `Derivable` to look like? And what happens when we `.set()` it? */ - expect(firstProp$.get()).to.equal(__YOUR_TURN__); + expect(firstProp$.get()).toEqual(__YOUR_TURN__); - firstProp$.set('other value'); // the plucked `Derivable` should be settable - expect(firstProp$.get()).to.equal(__YOUR_TURN__); // is the `Derivable` value the same as was set? + firstProp$.set('other value'); // the plucked `Derivable` should be settable + expect(firstProp$.get()).toEqual(__YOUR_TURN__); // is the `Derivable` value the same as was set? - expect(reactPropSpy).to.have.callCount(__YOUR_TURN__) // how many times was the spy called? Note the `skipFirst`.. - .and.calledWith(__YOUR_TURN__); // and what was the value? + // How many times was the spy called? Note the `skipFirst`.. + expect(reactPropSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + + // ... and what was the value? + expect(reactPropSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); }); /** @@ -286,16 +302,23 @@ describe.skip('advanced', () => { * We will set `secondProp`, will this affect `firstProp$`? */ myMap$.swap(map => map.set('secondProp', 'new value')); - expect(reactPropSpy).to.have.callCount(__YOUR_TURN__) // how many times was the spy called? - .and.calledWith(__YOUR_TURN__); // and with what value? + // How many times was the spy called? Note the `skipFirst`.. + expect(reactPropSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + + // ... and what was the value? + expect(reactPropSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); /** * **Your Turn** * And what if we set `firstProp`? */ myMap$.swap(map => map.set('firstProp', 'new value')); - expect(reactPropSpy).to.have.callCount(__YOUR_TURN__) // how many times was the spy called? - .and.calledWith(__YOUR_TURN__); // and with what value? + + // How many times was the spy called? Note the `skipFirst`.. + expect(reactPropSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + + // ... and what was the value? + expect(reactPropSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); }); /** @@ -309,8 +332,8 @@ describe.skip('advanced', () => { * So what if we set `firstProp$`? Does this propagate to the source `Derivable`? */ firstProp$.set(__YOUR_TURN__); - expect(reactSpy).to.have.callCount(__YOUR_TURN__); - expect(myMap$.get()).to.equal(__YOUR_TURN__); + expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(myMap$.get()).toEqual(__YOUR_TURN__); }); }); }); From dfc094fbe2a7e7a5e6896e571d9fe30ff322f736 Mon Sep 17 00:00:00 2001 From: Robin Kneepkens Date: Fri, 17 Nov 2023 14:14:47 +0100 Subject: [PATCH 12/30] Got tutorial 9 in working state Probably have to tweak it a bit more though --- tutorial/9 - expert.test.ts | 135 ++++++++++++++++++++---------------- 1 file changed, 74 insertions(+), 61 deletions(-) diff --git a/tutorial/9 - expert.test.ts b/tutorial/9 - expert.test.ts index ea4faec..3612099 100644 --- a/tutorial/9 - expert.test.ts +++ b/tutorial/9 - expert.test.ts @@ -1,7 +1,5 @@ -import { expect } from 'chai'; -import { SinonStub, spy, stub } from 'sinon'; -import { derivableCache } from '../libs/sherlock-utils/src'; -import { DerivableAtom, atom, derive } from '../libs/sherlock/src'; +import { DerivableAtom, atom, derive } from '@skunkteam/sherlock'; +import { derivableCache } from '@skunkteam/sherlock-utils'; /** * **Your Turn** @@ -16,7 +14,7 @@ describe.skip('expert', () => { * But what if a `Derivable` is used multiple times in another `Derivable`. */ it('multiple executions', () => { - const hasDerived = spy(); + const hasDerived = jest.fn(); const myAtom$ = atom(true); const myFirstDerivation$ = myAtom$.derive(hasDerived); @@ -28,7 +26,7 @@ describe.skip('expert', () => { * **Your Turn** * `hasDerived` is used in the first derivation. But has it been called at this point? */ - expect(hasDerived).to.not.have.callCount(__YOUR_TURN__); + expect(hasDerived).not.toHaveBeenCalledTimes(__YOUR_TURN__); mySecondDerivation$.get(); @@ -37,7 +35,7 @@ describe.skip('expert', () => { * Now that we have gotten `mySecondDerivation$`, which calls `.get()` on the first multiple times. * How many times has the first `Derivable` actually executed it's derivation? */ - expect(hasDerived).to.have.callCount(__YOUR_TURN__); // how many times? + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); // how many times? }); /** @@ -48,8 +46,8 @@ describe.skip('expert', () => { * So let's try the example above with this feature */ it('autoCaching', async () => { - const firstHasDerived = spy(); - const secondHasDerived = spy(); + const firstHasDerived = jest.fn(); + const secondHasDerived = jest.fn(); /** * **Your Turn** @@ -61,18 +59,25 @@ describe.skip('expert', () => { secondHasDerived(myFirstDerivation$.get() + myFirstDerivation$.get()), ); - expect(firstHasDerived, 'first before .get()').to.have.not.been.called; - expect(secondHasDerived, 'second before .get()').to.have.not.been.called; + // first before .get() + expect(firstHasDerived).not.toHaveBeenCalled(); + + // second before .get() + expect(secondHasDerived).not.toHaveBeenCalled(); mySecondDerivation$.get(); - expect(firstHasDerived, 'first after first .get()').to.have.been.calledOnce; - expect(secondHasDerived, 'second after first .get()').to.have.been.calledOnce; + // first after first .get() + expect(firstHasDerived).toHaveBeenCalledOnce(); + // second after first .get() + expect(secondHasDerived).toHaveBeenCalledOnce(); mySecondDerivation$.get(); - expect(firstHasDerived, 'first after second .get()').to.have.been.calledOnce; - expect(secondHasDerived, 'second after second .get()').to.have.been.calledTwice; + // first after second .get() + expect(firstHasDerived).toHaveBeenCalledOnce(); + // second after second .get() + expect(secondHasDerived).toHaveBeenCalledTimes(2); /** * Notice that the first `Derivable` has only been executed once, even though the second `Derivable` executed twice. @@ -81,8 +86,8 @@ describe.skip('expert', () => { await new Promise(r => setTimeout(r, 1)); - firstHasDerived.resetHistory(); - secondHasDerived.resetHistory(); + firstHasDerived.mockClear(); + secondHasDerived.mockClear(); mySecondDerivation$.get(); @@ -90,8 +95,10 @@ describe.skip('expert', () => { * **Your Turn** * Now what do you expect? */ - expect(firstHasDerived, 'first after last .get()').to.have.callCount(__YOUR_TURN__); // How many times was it called? - expect(secondHasDerived, 'second after last .get()').to.have.callCount(__YOUR_TURN__); // How many times was it called? + // first after last .get() + expect(firstHasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); // How many times was it called? + // second after last .get() + expect(secondHasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); // How many times was it called? }); }); @@ -108,12 +115,18 @@ describe.skip('expert', () => { */ describe('`derivableCache`', () => { type Stocks = 'GOOGL' | 'MSFT' | 'APPL'; - let stockPrice$: SinonStub<[Stocks], DerivableAtom>; - beforeEach(() => (stockPrice$ = stub<[Stocks], DerivableAtom>().callsFake(() => atom.unresolved()))); - const reactSpy = spy(); - beforeEach(() => reactSpy.resetHistory()); + let stockPrice$: jest.Mock, [Stocks], any>; + const reactSpy = jest.fn(); + + beforeEach(() => { + // By default the stock price retriever returns unresolved. + stockPrice$ = jest.fn(_ => atom.unresolved()); + reactSpy.mockClear(); + }); + function reactor(v: any) { + // TODO: add stopper function in type definition? reactSpy(v); } @@ -122,8 +135,9 @@ describe.skip('expert', () => { * Any setup this `Derivable` does, will be executed every time. */ it('multiple setups', () => { - // To not make things difficult with `unresolved` for this example, imagine we get a response synchronously - stockPrice$.returns(atom(1079.11)); + // To not make things difficult with `unresolved` + // for this example, imagine we get a response synchronously + stockPrice$ = jest.fn(_ => atom(1079.11)); const html$ = derive( () => ` @@ -135,8 +149,8 @@ describe.skip('expert', () => { ); html$.react(reactor); - expect(html$.connected).to.be.true; - expect(reactSpy).to.have.been.calledOnce; + expect(html$.connected).toEqual(true); + expect(reactSpy).toHaveBeenCalledOnce(); /** * **Your Turn** @@ -146,7 +160,7 @@ describe.skip('expert', () => { * But does that apply here? * How many times has the setup run, for the price `Derivable`. */ - expect(stockPrice$).to.have.callCount(__YOUR_TURN__); + expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); /** Can you explain this behavior? */ }); @@ -169,13 +183,13 @@ describe.skip('expert', () => { price$.react(reactor); // Because the stockPrice is still `unresolved` the reactor should not have emitted anything yet - expect(reactSpy).to.have.not.been.called; + expect(reactSpy).not.toHaveBeenCalled(); // Now let's increase the price // First we have to get the atom that was given by the `stockPrice$` stub - const googlPrice$ = stockPrice$.firstCall.returnValue as DerivableAtom; + const googlPrice$ = stockPrice$.mock.results[0].value as DerivableAtom; // Check if it is the right `Derivable` - expect(googlPrice$.connected).to.be.true; + expect(googlPrice$.connected).toEqual(true); // Then we set the price googlPrice$.set(1079.11); @@ -184,11 +198,12 @@ describe.skip('expert', () => { * **Your Turn** * So the value was increased. What do you think happened? */ - expect(reactSpy).to.have.callCount(__YOUR_TURN__); - expect(reactSpy).to.have.been.calledWith(__YOUR_TURN__); + expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reactSpy).toHaveBeenCalledWith(__YOUR_TURN__, expect.toBeFunction()); + // And how many times did the setup run? - expect(stockPrice$).to.have.callCount(__YOUR_TURN__); - expect(googlPrice$.connected).to.equal(__YOUR_TURN__); + expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(googlPrice$.connected).toEqual(__YOUR_TURN__); /** * Can you explain this behavior? @@ -238,15 +253,13 @@ describe.skip('expert', () => { prices$.react(reactor); // Because we use `.value` instead of `.get()` the reactor should emit immediately, this time - expect(reactSpy) - .to.have.been.calledOnce // But it should emit `undefined` - .and.calledWithExactly([undefined]); + expect(reactSpy).toHaveBeenCalledExactlyOnceWith([undefined]); // But it should emit `undefined` // TODO: Might also need the expect.toBeFunction() generic. // Now let's increase the price // First we have to get the atom that was given by the `stockPrice$` stub - const googlPrice$ = stockPrice$.firstCall.returnValue; + const googlPrice$ = stockPrice$.mock.results[0].value as DerivableAtom; // Check if it is the right `Derivable` - expect(googlPrice$.connected).to.be.true; + expect(googlPrice$.connected).toBe(true); // Then we set the price, as before googlPrice$.set(1079.11); @@ -255,23 +268,23 @@ describe.skip('expert', () => { * **Your Turn** * So the value was increased. What do you think happened now? */ - expect(reactSpy).to.have.callCount(__YOUR_TURN__); - expect(reactSpy).to.have.been.calledWith([__YOUR_TURN__]); + expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reactSpy).toHaveBeenCalledWith([__YOUR_TURN__], expect.toBeFunction()); /** * So that worked, now let's try and add another company to the list */ companies$.swap(current => [...current, 'APPL']); - expect(companies$.get()).to.deep.equal(['GOOGL', 'APPL']); + expect(companies$.get()).toEqual(['GOOGL', 'APPL']); /** * **Your Turn** * With both 'GOOGL' and 'APPL' in the list, what do we expect as an output? * We had a price for 'GOOGL', but not for 'APPL'... */ - expect(reactSpy).to.have.callCount(__YOUR_TURN__); - expect(reactSpy).to.have.been.calledWith([__YOUR_TURN__, __YOUR_TURN__]); + expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reactSpy).toHaveBeenCalledWith([__YOUR_TURN__, __YOUR_TURN__], expect.toBeFunction()); }); }); /** @@ -283,9 +296,7 @@ describe.skip('expert', () => { * `derivableCache` requires a `derivableFactory`, this specifies the setup for a given key. * We know the key, and what to do with it, so let's try it! */ - const priceCache$ = derivableCache({ - derivableFactory: (company: Stocks) => stockPrice$(company), - }); + const priceCache$ = derivableCache((company: Stocks) => stockPrice$(company)); /** * *Note that from this point forward we use `priceCache$` where we used to use `stockPrice$` directly* */ @@ -306,14 +317,16 @@ describe.skip('expert', () => { html$.react(reactor); - expect(html$.connected).to.be.true; - expect(reactSpy).to.have.been.calledOnce; + expect(html$.connected).toEqual(true); + expect(reactSpy).toHaveBeenCalledOnce(); // Convenience function to return the first argument of the last call to the reactor function lastEmittedHTMLs() { - return reactSpy.lastCall.args[0]; + // TODO: lastCall may be undefined, but it might be fine here. + return reactSpy.mock.lastCall[0]; } + // The last call, should have the array of HTML's as first argument - expect(lastEmittedHTMLs()[0]).to.contain('$ unknown'); + expect(lastEmittedHTMLs()[0]).toContain('$ unknown'); /** * **Your Turn** @@ -322,20 +335,20 @@ describe.skip('expert', () => { * * Has anything changed, by using the `derivableCache`? */ - expect(stockPrice$).to.have.callCount(__YOUR_TURN__); + expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); // Now let's resolve the price - stockPrice$.firstCall.returnValue.set(1079.11); + stockPrice$.mock.results[0].value.set(1079.11); /** * **Your Turn** * Last time this caused the setup to run again, resolving to `unresolved` yet again. * What happens this time? Has the setup run again? */ - expect(stockPrice$).to.have.callCount(__YOUR_TURN__); + expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); // Ok, but did it update the HTML? - expect(reactSpy).to.have.callCount(__YOUR_TURN__); - expect(lastEmittedHTMLs()[0]).to.contain(__YOUR_TURN__); + expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(lastEmittedHTMLs()[0]).toContain(__YOUR_TURN__); // Last chance, what if we add a company companies$.swap(current => [...current, 'APPL']); @@ -345,12 +358,12 @@ describe.skip('expert', () => { * Now the `stockPrice$` function should have at least run again for 'APPL'. * But did it calculate 'GOOGL' again too? */ - expect(stockPrice$).to.have.callCount(__YOUR_TURN__); - expect(reactSpy).to.have.callCount(__YOUR_TURN__); + expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); // The first should be 'GOOGL' - expect(lastEmittedHTMLs()[0]).to.contain(__YOUR_TURN__); + expect(lastEmittedHTMLs()[0]).toContain(__YOUR_TURN__); // The first should be 'APPL' - expect(lastEmittedHTMLs()[1]).to.contain(__YOUR_TURN__); + expect(lastEmittedHTMLs()[1]).toContain(__YOUR_TURN__); }); }); }); From 8a60b74dc6587a4d82eb3b7f017574e5dcddcc15 Mon Sep 17 00:00:00 2001 From: Robin Kneepkens Date: Fri, 17 Nov 2023 14:15:11 +0100 Subject: [PATCH 13/30] Updated some comments --- tutorial/5 - unresolved.test.ts | 2 +- tutorial/7 - utils.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tutorial/5 - unresolved.test.ts b/tutorial/5 - unresolved.test.ts index a9dd2f7..137ec15 100644 --- a/tutorial/5 - unresolved.test.ts +++ b/tutorial/5 - unresolved.test.ts @@ -54,7 +54,7 @@ describe.skip('unresolved', () => { */ expect(myAtom$.resolved).toEqual(__YOUR_TURN__); - // .toThrow() or .not.toThrow()? + // .toThrow() or .not.toThrow()? ↴ expect(() => myAtom$.get()) /*__YOUR_TURN__*/; }); diff --git a/tutorial/7 - utils.test.ts b/tutorial/7 - utils.test.ts index 6e329e9..460fac3 100644 --- a/tutorial/7 - utils.test.ts +++ b/tutorial/7 - utils.test.ts @@ -18,7 +18,7 @@ describe.skip('utils', () => { * *Note functions like `pairwise` and `scan` can be used with any callback. So it can be used both in a `.derive()` step and in a `.react()`* */ it('pairwise', () => { - expect(pairwise).toBe(pairwise); // use `pairwise` so the import is used. --> Does it work like this? + expect(pairwise).toBe(pairwise); // use `pairwise` so the import is used. const myCounter$ = atom(1); const reactSpy = jest.fn(); From bfec2d7da117181c90170a5352309b044747e2de Mon Sep 17 00:00:00 2001 From: Robin Kneepkens Date: Fri, 17 Nov 2023 14:19:49 +0100 Subject: [PATCH 14/30] Added placeholder code for tutorial 6 This way it doesn't count as a failed test case when running npm run tutorial. --- tutorial/6 - errors.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tutorial/6 - errors.test.ts b/tutorial/6 - errors.test.ts index e69de29..1c72480 100644 --- a/tutorial/6 - errors.test.ts +++ b/tutorial/6 - errors.test.ts @@ -0,0 +1,5 @@ +describe.skip('errors', () => { + it('placeholder', () => { + expect(true).toEqual(true); + }); +}); From 8526f9485e0a8d2a531910fadc05f8e1b8f8d566 Mon Sep 17 00:00:00 2001 From: Robin Kneepkens Date: Fri, 17 Nov 2023 15:07:46 +0100 Subject: [PATCH 15/30] Finalized tutorial 1 --- tutorial/1 - intro.test.ts | 93 ++++++++++++++++++++++++-------------- 1 file changed, 59 insertions(+), 34 deletions(-) diff --git a/tutorial/1 - intro.test.ts b/tutorial/1 - intro.test.ts index 0fe01b0..1cd9f20 100644 --- a/tutorial/1 - intro.test.ts +++ b/tutorial/1 - intro.test.ts @@ -1,35 +1,47 @@ import { atom } from '@skunkteam/sherlock'; /** - * **Your Turn** + * ** Your Turn ** * If you see this variable, you should do something about it. :-) */ export const __YOUR_TURN__ = {} as any; /** - * Welcome to the `@politie/sherlock` tutorial. + * Welcome to the `@skunkteam/sherlock` tutorial. * - * It is set up as a collection of specs, with the goal of getting all the specs to pass. - * The `expect()`s and basic setup are there, you just need to get it to work. + * It is set up as a collection of specs, with the goal of getting all the specs + * to pass. The `expect()`s and basic setup are there, you just need to get it + * to work. * - * All specs except the first one are set to `.skip`. Remove this to start on that part of the tutorial. + * All specs except the first one are set to `.skip`. Remove this to start on + * that part of the tutorial. * - * Start the tutorial by running: `npm run tutorial`. + * Start the tutorial by running: + * `npm run tutorial`. * - * *Hint: most methods and functions are fairly well documented in jsDoc, which is easily accessed through TypeScript* + * To not manually re-enter the command, use: + * `npm run tutorial -- --watch` + * This will automatically rerun the tests when a file change has been detected. + * + * *Hint: most methods and functions are fairly well documented in jsDoc, + * which is easily accessed through TypeScript* */ describe('intro', () => { - it(`--- Welcome to the tutorial! --- + it(` + + --- Welcome to the tutorial! --- Please look in \`./tutorial/1 - intro.ts\` to see what to do next.`, () => { // At the start of the spec, there will be some setup. let bool = false; + // Sometimes including an expectation, to show the current state. expect(bool).toBeFalse(); /** * If **Your Turn** is shown in a comment, there is work for you to do. * This can also be indicated with the `__YOUR_TURN__` variable. + * * It should be clear what to do here... */ bool = __YOUR_TURN__; @@ -41,33 +53,39 @@ describe('intro', () => { /** * Let's start with the `Derivable` basics. + * + * ** Your Turn ** + * Remove the `.skip` so this part of the tutorial will run. */ describe.skip('the basics', () => { /** - * The `Atom` is the basic building block of `@politie/sherlock`. + * The `Atom` is the basic building block of `@skunkteam/sherlock`. * It holds a value which you can `get()` and `set()`. */ it('the `Atom`', () => { - // An `Atom` can be created with the `atom()` function. The parameter of this function is used as the initial value of the `Atom`. + // An `Atom` can be created with the `atom()` function. The parameter + // of this function is used as the initial value of the `Atom`. const myValue$ = atom(1); - // Variables containing `Atom`s or any other `Derivable` are usually postfixed with a `$` to indicate this. Hence `myValue$`. + // Variables containing `Atom`s or any other `Derivable` are usually + // postfixed with a `$` to indicate this. Hence `myValue$`. - // The `.get()` method can be used to get the current value of the `Atom`. + // The `.get()` method can be used to get the current value of + // the `Atom`. expect(myValue$.get()).toEqual(1); - /** - * **Your Turn** - * Use the `.set()` method to change the value of the `Atom`. - */ - + // ** Your Turn ** + // Use the `.set()` method to change the value of the `Atom`. expect(myValue$.get()).toEqual(2); }); /** - * The `Atom` is a `Derivable`. This means it can be used to create a derived value. - * This derived value stays up to date with the original `Atom`. + * The `Atom` is a `Derivable`. This means it can be used to create a + * derived value. This derived value stays up to date with the original + * `Atom`. + * + * The easiest way to do this, is to call `.derive()` on another + * `Derivable`. * - * The easiest way to do this, is to call `.derive()` on another `Derivable`. * Let's try this. */ it('the `Derivable`', () => { @@ -75,14 +93,14 @@ describe.skip('the basics', () => { expect(myValue$.get()).toEqual(1); /** - * **Your Turn** - * We want to create a new `Derivable` that outputs the inverse of the original `Atom`. - * Use `myValue$.derive(val => ...)` to create the `myInverse$` variable. + * ** Your Turn ** + * + * We want to create a new `Derivable` that outputs the inverse (from a + * negative to a positive number and vice versa) of the original `Atom`. */ - const myInverse$ = myValue$.derive(__YOUR_TURN__); - + // Use `myValue$.derive(val => ...)` to implement `myInverse$`. + const myInverse$ = myValue$.derive(__YOUR_TURN__ => __YOUR_TURN__); expect(myInverse$.get()).toEqual(-1); - // So if we set `myValue$` to -2: myValue$.set(-2); // `myInverse$` will change accordingly. @@ -94,24 +112,31 @@ describe.skip('the basics', () => { * You can also listen to the changes. * * This is done with the `.react()` method. - * This method is given a `function` that is executed every time the value of the `Derivable` changes. + * This method is given a function that is executed every time the value of + * the `Derivable` changes. */ it('reacting to `Derivable`s', () => { const myCounter$ = atom(0); - let reacted = 0; - /** - * **Your Turn** - * Now react to `myCounter$`. In every `react()`, increase the `reacted` variable by one. - */ - expect(reacted).toEqual(1); // `react()` will react immediately, more on that later. + /** + * ** Your Turn ** + * + * Now react to `myCounter$`. In every `react()`. + * Increase the `reacted` variable by one. */ + myCounter$.react(() => __YOUR_TURN__); + expect(reacted).toEqual(1); + // `react()` will react immediately, more on that later. - // And then we set the `Atom` a couple of times to make the `Derivable` react. + /** + * And then we set the `Atom` a couple of times + * to make the `Derivable` react. + * */ for (let i = 0; i <= 100; i++) { // Set the value of the `Atom`. myCounter$.set(i); } + expect(reacted).toEqual(101); }); }); From 9468e19e205725a7e1397971cbc6d67c2470e979 Mon Sep 17 00:00:00 2001 From: Robin Kneepkens Date: Fri, 17 Nov 2023 16:37:21 +0100 Subject: [PATCH 16/30] Finalized tutorial 2 Also updated a comment in tutorial 1 slightly. --- tutorial/1 - intro.test.ts | 8 +-- tutorial/2 - deriving.test.ts | 109 +++++++++++++++++++++++----------- 2 files changed, 77 insertions(+), 40 deletions(-) diff --git a/tutorial/1 - intro.test.ts b/tutorial/1 - intro.test.ts index 1cd9f20..c7671f5 100644 --- a/tutorial/1 - intro.test.ts +++ b/tutorial/1 - intro.test.ts @@ -39,15 +39,13 @@ describe('intro', () => { expect(bool).toBeFalse(); /** - * If **Your Turn** is shown in a comment, there is work for you to do. + * If ** Your Turn ** is shown in a comment, there is work for you to do. * This can also be indicated with the `__YOUR_TURN__` variable. * - * It should be clear what to do here... - */ + * It should be clear what to do here... */ bool = __YOUR_TURN__; - - // We use expectations like this to verify the result. expect(bool).toBeTrue(); + // We use expectations like this to verify the result. }); }); diff --git a/tutorial/2 - deriving.test.ts b/tutorial/2 - deriving.test.ts index ae0f9ee..afd75d8 100644 --- a/tutorial/2 - deriving.test.ts +++ b/tutorial/2 - deriving.test.ts @@ -1,36 +1,43 @@ import { atom, Derivable, derive } from '@skunkteam/sherlock'; /** - * **Your Turn** + * ** Your Turn ** + * * If you see this variable, you should do something about it. :-) */ export const __YOUR_TURN__ = {} as any; /** - * Any `Derivable` (including `Atom`s) can be used (and/or combined) to create a derived state. - * This derived state is in turn a `Derivable`. + * Any `Derivable` (including `Atom`s) can be used (and/or combined) to create + * a derived state. This derived state is in turn a `Derivable`. * * There are a couple of ways to do this. */ -describe.skip('deriving', () => { +describe('deriving', () => { /** * In the 'intro' we have created a derivable by using the `.derive()` method. - * This method allows the state of that `Derivable` to be used to create a new `Derivable`. + * This method allows the state of that `Derivable` to be used to create a + * new `Derivable`. * * In the derivation, other `Derivable`s can be used as well. - * If a `Derivable.get()` is called inside a derivation, the changes to that `Derivable` are also tracked and kept up to date. + * If a `Derivable.get()` is called inside a derivation, the changes to that + * `Derivable` are also tracked and kept up to date. */ it('combining `Derivable`s', () => { const repeat$ = atom(1); const text$ = atom(`It won't be long`); /** - * **Your Turn** + * ** Your Turn ** + * * Let's create some lyrics by combining `text$` and `repeat$`. * As you might have guessed, we want to repeat the text a couple of times. + * * (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat should do fine) */ - const lyric$ = text$.derive(txt => txt); // We can combine txt with `repeat$.get()` here. + + // We can combine txt with `repeat$.get()` here. + const lyric$ = text$.derive(txt => txt /* __YOUR_TURN__ */ ); expect(lyric$.get()).toEqual(`It won't be long`); @@ -40,24 +47,38 @@ describe.skip('deriving', () => { }); /** - * Now that we have used `.get()` in a `.derive()`. You may wonder, can we skip the original `Derivable` and just call the function `derive()`? + * Now that we have used `.get()` in a `.derive()`. You may wonder, can + * we skip the original `Derivable` and just call the function `derive()`? + * * Of course you can! * - * And you can use any `Derivable` you want, even if they all have the same `Atom` as a parent. + * And you can use any `Derivable` you want, even if they all have the same + * `Atom` as a parent. */ it('the `derive()` function', () => { const myCounter$ = atom(1); /** - * **Your Turn** - * Let's try creating a `Derivable` [FizzBuzz](https://en.wikipedia.org/wiki/Fizz_buzz) - * `fizzBuzz$` should combine `fizz$`, `buzz$` and `myCounter$` to produce the correct output. + * ** Your Turn ** + * + * Let's try creating a `Derivable` [FizzBuzz](https://en.wikipedia.org/wiki/Fizz_buzz). + * `fizzBuzz$` should combine `fizz$`, `buzz$` and `myCounter$` to + * produce the correct output. * - * Multiple `Derivable`s can be combined to create a new one. To do this, just use `.get()` on (other) `Derivable`s in the `.derive()` step. - * This can be done both when `derive()` is used standalone or as a method on another `Derivable`. + * Multiple `Derivable`s can be combined to create a new one. To do + * this, just use `.get()` on (other) `Derivable`s in the `.derive()` + * step. + * + * This can be done both when `derive()` is used standalone or as a + * method on another `Derivable`. */ - const fizz$: Derivable = myCounter$.derive(__YOUR_TURN__); // Should return 'Fizz' when `myCounter$` is a multiple of 3 and '' otherwise. - const buzz$: Derivable = myCounter$.derive(__YOUR_TURN__); // Should return 'Buzz' when `myCounter$` is a multiple of 5 and '' otherwise. + + // Should return 'Fizz' when `myCounter$` is a multiple of 3 and '' otherwise. + const fizz$: Derivable = myCounter$.derive(__YOUR_TURN__); + + // Should return 'Buzz' when `myCounter$` is a multiple of 5 and '' otherwise. + const buzz$: Derivable = myCounter$.derive(__YOUR_TURN__); + const fizzBuzz$: Derivable = derive(__YOUR_TURN__); expect(fizz$.get()).toEqual(''); @@ -89,18 +110,21 @@ describe.skip('deriving', () => { } /** - * The automatic tracking of `.get()` calls will also happen inside called `function`s. - * This can be really powerful, but also dangerous. One of the dangers is shown here. + * The automatic tracking of `.get()` calls will also happen inside called + * `function`s. + * + * This can be really powerful, but also dangerous. One of the dangers is + * shown here. */ it('indirect derivations', () => { const pastTweets = [] as string[]; const currentUser$ = atom('Barack'); + const tweet$ = atom('First tweet'); + function log(tweet: string) { pastTweets.push(`${currentUser$.get()} - ${tweet}`); } - const tweet$ = atom('First tweet'); - tweet$.derive(log).react(txt => { // Normally we would do something with the tweet here. return txt; @@ -122,18 +146,23 @@ describe.skip('deriving', () => { currentUser$.set('Donald'); /** - * **Your Turn** + * ** Your Turn ** + * * Time to set your own expectations. */ - expect(pastTweets).toHaveLength(2); // Is there a new tweet? - expect(pastTweets[2]).toContain(__YOUR_TURN__); // Who sent it? Donald? Or Barack? - expect(pastTweets[2]).toContain(__YOUR_TURN__); // What did he tweet? + const tweetCount = pastTweets.length; + const lastTweet = pastTweets[tweetCount - 1]; + + expect(tweetCount).toEqual(__YOUR_TURN__); // Is there a new tweet? + expect(lastTweet).toContain(__YOUR_TURN__); // Who sent it? Donald? Or Barack? + expect(lastTweet).toContain(__YOUR_TURN__); // What did he tweet? /** * As you can see, this is something to look out for. * Luckily there are ways to circumvent this. But more on that later. * - * *Note that this behavior can also be really helpful if you know what you are doing* + * * Note that this behavior can also be really helpful if you know what + * you are doing * */ }); @@ -142,35 +171,45 @@ describe.skip('deriving', () => { * These are methods that make common derivations a bit easier. * * These methods are: `.and()`, `.or()`, `.is()` and `.not()`. - * Their function is as you would expect from `boolean` operators in a JavaScript environment. + * + * Their function is as you would expect from `boolean` operators in a + * JavaScript environment. + * * The first three will take a `Derivable` or regular value as parameter. * `.not()` does not need any input. * - * `.is()` will resolve equality in the same way as `@politie/sherlock` would do internally. - * More on the equality check in the 'inner workings' part. But know that the first check is - * [Object.is()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) + * `.is()` will resolve equality in the same way as `@skunkteam/sherlock` + * would do internally. + * + * More on the equality check in the 'inner workings' part. But know that + * the first check is [Object.is()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) */ it('convenience methods', () => { const myCounter$ = atom(1); /** - * **Your Turn** - * The FizzBuzz example above can be rewritten using the convenience methods. - * This is not how you would normally write it, but it looks like a fun excercise. + * ** Your Turn ** + * + * The FizzBuzz example above can be rewritten using the convenience + * methods. This is not how you would normally write it, but it looks + * like a fun excercise. * - * `fizz$` and `buzz$` can be completed with only `.is(...)`, `.and(...)` and `.or(...)`; - * Make sure the output of those `Derivable`s is either 'Fizz'/'Buzz' or ''. + * `fizz$` and `buzz$` can be completed with only `.is(...)`, + * `.and(...)` and `.or(...)`. Make sure the output of those `Derivable`s + * is either 'Fizz'/'Buzz' or ''. */ const fizz$ = myCounter$ .derive(count => count % 3) .is(__YOUR_TURN__) .and(__YOUR_TURN__) .or(__YOUR_TURN__) as Derivable; + const buzz$ = myCounter$ .derive(count => count % 5) .is(__YOUR_TURN__) .and(__YOUR_TURN__) .or(__YOUR_TURN__) as Derivable; + const fizzBuzz$ = derive(() => fizz$.get() + buzz$.get()).or(__YOUR_TURN__); for (let count = 1; count <= 100; count++) { From 077a3f83f9f7ebced8a1743e1c2e52501b944074 Mon Sep 17 00:00:00 2001 From: Robin Kneepkens Date: Mon, 20 Nov 2023 17:58:45 +0100 Subject: [PATCH 17/30] Finalized tutorial 3 --- tutorial/3 - reacting.test.ts | 239 ++++++++++++++++++++++++---------- 1 file changed, 170 insertions(+), 69 deletions(-) diff --git a/tutorial/3 - reacting.test.ts b/tutorial/3 - reacting.test.ts index 9907680..8a24172 100644 --- a/tutorial/3 - reacting.test.ts +++ b/tutorial/3 - reacting.test.ts @@ -1,7 +1,8 @@ import { atom } from '@skunkteam/sherlock'; /** - * **Your Turn** + * ** Your Turn ** + * * If you see this variable, you should do something about it. :-) */ export const __YOUR_TURN__ = {} as any; @@ -10,13 +11,13 @@ export const __YOUR_TURN__ = {} as any; * In the intro we have seen a basic usage of the `.react()` method. * Let's dive a bit deeper into the details of this method. */ -describe.skip('reacting', () => { +describe('reacting', () => { // For easy testing we can count the number of times a reactor was called, let wasCalledTimes: number; // and record the last value it reacted to. let lastValue: any; - // reset the values + // reset the values before each test case beforeEach(() => { wasCalledTimes = 0; lastValue = undefined; @@ -28,19 +29,29 @@ describe.skip('reacting', () => { lastValue = val; } - // Of course we are lazy and don't want to type these assertions over and over. :-) + // Of course we are lazy and don't want to type these assertions over + // and over. :-) function expectReact(reactions: number, value?: any) { // Reaction was called # times expect(wasCalledTimes).toEqual(reactions); + // Note the actual point of failure is: + // at Object. (3 - reacting.test.ts:LINE_NUMBER:_) + // V ~~~~~~~~~~~~ + // Last value of the reaction was # expect(lastValue).toEqual(value); } /** - * Every `Derivable` always has a current state. So the `.react()` method does not need to wait for a value, there already is one. - * This means that `.react()` will fire directly when called. - * When the `Derivable` has a new state, this will also fire `.react()` synchronously. - * So the very next line after `.set()` is called, the `.react()` has already fired! + * Every `Derivable` always has a current state. So the `.react()` method + * does not need to wait for a value, there already is one. + * + * This means that `.react()` will fire directly when called. When the + * `Derivable` has a new state, this will also fire `.react()` + * synchronously. + * + * So the very next line after `.set()` is called, the `.react()` has + * already fired! * * (Except when the `Derivable` is `unresolved`, but more on that later.) */ @@ -53,8 +64,10 @@ describe.skip('reacting', () => { expectReact(0); /** - * **Your Turn** - * Time to react to `myAtom$` with the `reactor()` function defined above. + * ** Your Turn ** + * + * Time to react to `myAtom$` with the `reactor()` function defined + * above. */ expectReact(1, 'initial value'); @@ -65,12 +78,16 @@ describe.skip('reacting', () => { }); /** - * A reactor will go on forever. This is often not what you want, and almost always a memory leak. - * So it is important to stop a reactor at some point. The `.react()` method has different ways of dealing with this. + * A reactor will go on forever. This is often not what you want, and almost + * always a memory leak. + * + * So it is important to stop a reactor at some point. The `.react()` method + * has different ways of dealing with this. */ describe('stopping a reaction', () => { /** - * The easiest is the 'stopper' function, every `.react()` call will return a `function` that will stop the reaction. + * The easiest is the 'stopper' function, every `.react()` call will + * return a `function` that will stop the reaction. */ it('with the stopper function', () => { const myAtom$ = atom('initial value'); @@ -78,7 +95,8 @@ describe.skip('reacting', () => { expect(myAtom$.get()).toEqual('initial value'); /** - * **Your Turn** + * ** Your Turn ** + * * catch the returned `stopper` in a variable */ myAtom$.react(reactor); @@ -86,7 +104,8 @@ describe.skip('reacting', () => { expectReact(1, 'initial value'); /** - * **Your Turn** + * ** Your Turn ** + * * Call the `stopper`. */ @@ -97,7 +116,8 @@ describe.skip('reacting', () => { }); /** - * Everytime the reaction is called, it also gets the stopper `function` as a second parameter. + * Everytime the reaction is called, it also gets the stopper `function` + * as a second parameter. */ it('with the stopper callback', () => { const myAtom$ = atom('initial value'); @@ -105,8 +125,10 @@ describe.skip('reacting', () => { expect(myAtom$.get()).toEqual('initial value'); /** - * **Your Turn** - * In the reaction below, use the stopper callback to stop the reaction + * ** Your Turn ** + * + * In the reaction below, use the stopper callback to stop the + * reaction */ myAtom$.react((val, __YOUR_TURN___) => { reactor(val); @@ -123,18 +145,26 @@ describe.skip('reacting', () => { }); /** - * The reactor `options` are a way to modify when and how the reactor will react to changes in the `Derivable`. + * The reactor `options` are a way to modify when and how the reactor will + * react to changes in the `Derivable`. */ describe('reactor options', () => { /** - * Another way to make a reactor stop at a certain point, is by specifying an `until` in the `options`. + * Another way to make a reactor stop at a certain point, is by + * specifying an `until` in the `options`. * `until` can be given either a `Derivable` or a `function`. - * If a `function` is given, this `function` will be given the `Derivable` that is the source of the reaction as a parameter. - * This `function` will track all `.get()`s, so can use any `Derivable`. It can return a `boolean` or a `Derivable`. - * *Note: the reactor options `when` and `from` can also be set to a `Derivable`/`function` as described here.* + * + * If a `function` is given, this `function` will be given the + * `Derivable` that is the source of the reaction as a parameter. + * This `function` will track all `.get()`s, so can use any `Derivable`. + * It can return a `boolean` or a `Derivable`. + * + * *Note: the reactor options `when` and `from` can also be set to a + * `Derivable`/`function` as described here.* * * The reactor will stop directly when `until` becomes true. - * If that happens at exactly the same time as the `Derivable` getting a new value, it will not react again. + * If that happens at exactly the same time as the `Derivable` getting a + * new value, it will not react again. */ describe('reacting `until`', () => { const boolean$ = atom(false); @@ -146,82 +176,134 @@ describe.skip('reacting', () => { }); /** - * If a `Derivable` is given, the reaction will stop once that `Derivable` becomes `true`/truthy. + * If a `Derivable` is given, the reaction will stop once that + * `Derivable` becomes `true`/truthy. */ it('an external `Derivable`', () => { /** - * **Your Turn** + * ** Your Turn ** + * * Try giving `boolean$` as `until` option. */ string$.react(reactor, __YOUR_TURN__); - expectReact(1, 'Value'); // It should react directly as usual. + // It should react directly as usual. + expectReact(1, 'Value'); + + // It should keep reacting as usual. string$.set('New value'); - expectReact(2, 'New value'); // It should keep reacting as usual. + expectReact(2, 'New value'); + + // We set `boolean$` to true, to stop the reaction + boolean$.set(true); + + // The reactor has immediately stopped, so it still reacted + // only twice: + expectReact(2, 'New value'); + + // Even when `boolean$` is set to `false` again... + boolean$.set(false); - boolean$.set(true); // We set `boolean$` to true, to stop the reaction - expectReact(2, 'New value'); // The reactor has immediately stopped, so it still reacted only twice. + // ... and a new value is introduced: + string$.set('Another value'); - boolean$.set(false); // Even when `boolean$` is set to `false` again - string$.set('Another value'); // And a new value is introduced - expectReact(2, 'New value'); // The reactor won't start up again, so it still reacted only twice. + // The reactor won't start up again, so it still reacted + // only twice: + expectReact(2, 'New value'); }); /** - * A function can also be given as `until` this function will be executed in a derivation. + * A function can also be given as `until`. This function will be + * executed in every derivation. Just like using a `Derivable` as + * an `until`, the Reactor will keep reacting until the result of + * this function evaluates thruthy. + * * This way any `Derivable` can be used in the calculation. */ it('a function', () => { /** - * **Your Turn** - * Since the reactor options expect a boolean, you will sometimes need to calculate the option. - * Try giving a `function` as `until` option, use `!string$.get()` to return `true` when the `string` is empty. + * ** Your Turn ** + * + * Since the reactor options expect a boolean, you will + * sometimes need to calculate the option. + * + * Try giving an externally defined `function` that takes no + * parameters as `until` option. + * + * Use `!string$.get()` to return `true` when the `string` is + * empty. */ string$.react(reactor, __YOUR_TURN__); + + // It should react as usual: string$.set('New value'); string$.set('Newer Value'); - expectReact(3, 'Newer Value'); // It should react as usual. - - string$.set(''); // We set `string$` to an empty string, to stop the reaction - expectReact(3, 'Newer Value'); // The reactor was immediately stopped, so even the empty string was never given to the reactor + expectReact(3, 'Newer Value'); + + // Until we set `string$` to an empty string to stop the + // reaction: + string$.set(''); + // The reactor was immediately stopped, so even the empty string + // was never given to the reactor: + expectReact(3, 'Newer Value'); }); /** - * Since the example above, where the `until` is based on the parent `Derivable` occurs very frequently. + * Since the example above where the `until` is based on the parent + * `Derivable` occurs very frequently. + * * This `Derivable` is given as a parameter to the `until` function. */ it('the parent `Derivable`', () => { /** - * **Your Turn** - * Try using the first parameter of the `until` function to do the same as above. + * ** Your Turn ** + * + * Try using the first parameter of the `until` function to do + * the same as above. */ string$.react(reactor, __YOUR_TURN__); + + // It should react as usual. string$.set('New value'); string$.set('Newer Value'); - expectReact(3, 'Newer Value'); // It should react as usual. + expectReact(3, 'Newer Value'); + + // Until we set `string$` to an empty string, to stop + // the reaction: + string$.set(''); - string$.set(''); // We set `string$` to an empty string, to stop the reaction - expectReact(3, 'Newer Value'); // The reactor was immediately stopped, so even the empty string was never given to the reactor + // The reactor was immediately stopped, so even the empty string + // was never given to the reactor: + expectReact(3, 'Newer Value'); }); }); /** - * Sometimes you may not need to react to the first couple of values of the `Derivable`. - * This can be because of the value of the `Derivable` or due to external conditions. - * The `from` option is meant to help with this. The reactor will only start after it becomes true. - * Once it has become true, the reactor will not listen to this option any more and react as usual. + * Sometimes you may not need to react to the first couple of values of + * the `Derivable`. This can be because of the value of the `Derivable` + * or due to external conditions. + * + * The `from` option is meant to help with this. The reactor will only + * start after it becomes true. Once it has become true, the reactor + * will not listen to this option any more and react as usual. + * + * The interface of `from` is the same as `until` (i.e. it also gets + * the parent derivable as first parameter when it's called.) * - * *Note: when using `from`, `.react()` will (most often) not react synchronously any more. As that is the function of this option.* + * * Note: when using `from`, `.react()` will (most often) not react + * synchronously any more. As that is the function of this option.* */ it('reacting `from`', () => { const sherlock$ = atom(''); /** - * **Your Turn** - * We can react here, but restrict the reactions to start when the keyword 'dear' is set. - * This will skip the first three reactions, but react as usual after that. + * ** Your Turn ** * - * *Hint: remember the `.is()` method?* + * We can react here, but restrict the reactions to start when the + * keyword 'dear' is set. This will skip the first three reactions, + * but react as usual after that. + * + * *Hint: remember the `.is()` method from tutorial 2?* */ sherlock$.react(reactor, __YOUR_TURN__); @@ -232,15 +314,23 @@ describe.skip('reacting', () => { }); /** - * Sometimes you may want to react only on certain values or when certain conditions are met. - * It works exactly as `from` + * Sometimes you may want to react only on certain values or when + * certain conditions are met. + * + * This can be achieved by using the `when` reactor option. + * Where `until` and `from` can only be triggered once to stop or start + * reacting, `when` can be flipped as often as you like and the reactor + * will respect the current state of the `when` function/Derivable. * - * *Note: as with `from` this can prevent `.react()` from reacting synchronously.* + * *Note: as with `from` this can prevent `.react()` from reacting + * synchronously.* */ it('reacting `when`', () => { const count$ = atom(0); /** + * ** Your Turn ** + * * Now, let's react to all even numbers. * Except 4, we don't want to make it too easy now. */ @@ -260,13 +350,15 @@ describe.skip('reacting', () => { /** * Normally the reactor will immediately fire with the current value. - * If you want the reactor to fire normally, just not the first time, there is also a `boolean` option: `skipFirst`. + * If you want the reactor to fire normally, just not the first time, + * there is also a `boolean` option: `skipFirst`. */ it('reacting with `skipFirst`', () => { const done$ = atom(false); /** - * **Your Turn** + * ** Your Turn ** + * * Say you want to react when `done$` is true. But not right away.. */ done$.react(reactor, __YOUR_TURN__); @@ -277,17 +369,21 @@ describe.skip('reacting', () => { }); /** - * With `once` you can stop the reactor after it has emitted exactly one value. This is a `boolean` option. + * With `once` you can stop the reactor after it has emitted exactly + * one value. This is a `boolean` option. * - * Without any other `options`, this is just a strange way of typing `.get()`. - * But when combined with `when`, `from` or `skipFirst`, it can be very useful. + * Without any other `options`, this is just a strange way of typing + * `.get()`. But when combined with `when`, `from` or `skipFirst`, it + * can be very useful. */ it('reacting `once`', () => { const finished$ = atom(false); /** - * **Your Turn** - * Say you want to react when `finished$` is true. It can not finish twice. + * ** Your Turn ** + * + * Say you want to react when `finished$` is true. It can not finish + * twice. * * *Hint: you will need to combine `once` with another option* */ @@ -310,8 +406,11 @@ describe.skip('reacting', () => { const connected$ = atom(false); /** - * **Your Turn** - * We want our reactor to trigger once, when the user disconnects (eg for cleanup). + * ** Your Turn ** + * + * We want our reactor to trigger once, when the user disconnects + * (eg for cleanup). + * * `connected$` indicates the current connection status. * This should be possible with three simple ReactorOptions */ @@ -328,8 +427,10 @@ describe.skip('reacting', () => { connected$.set(false); expectReact(1, false); - // It should not react again after this + // It should not react again after this. expect(connected$.connected).toBeFalse; + // * Note: this `.connected` refers to whether this `Derivable` + // is being (indirectly) observed by a reactor. }); }); }); From 0883841db4b5c1899965d9b2dca82aa709dbbd5b Mon Sep 17 00:00:00 2001 From: Robin Kneepkens Date: Tue, 21 Nov 2023 16:27:58 +0100 Subject: [PATCH 18/30] Finalized tutorial 4 --- tutorial/4 - inner workings.test.ts | 192 ++++++++++++++-------------- 1 file changed, 93 insertions(+), 99 deletions(-) diff --git a/tutorial/4 - inner workings.test.ts b/tutorial/4 - inner workings.test.ts index fcdf9d7..33fbfe2 100644 --- a/tutorial/4 - inner workings.test.ts +++ b/tutorial/4 - inner workings.test.ts @@ -2,18 +2,20 @@ import { atom } from '@skunkteam/sherlock'; import { Seq } from 'immutable'; /** - * **Your Turn** + * ** Your Turn ** + * * If you see this variable, you should do something about it. :-) */ export const __YOUR_TURN__ = {} as any; /** - * Time to dive a bit deeper into the inner workings of `@politie/sherlock`. + * Time to dive a bit deeper into the inner workings of `@skunkteam/sherlock`. */ describe.skip('inner workings', () => { +describe('inner workings', () => { /** - * What if there is a derivation that reads from one of two `Derivable`s dynamically? - * Will both of those `Derivable`s be tracked for changes? + * What if there is a derivation that reads from one of two `Derivable`s + * dynamically? Will both of those `Derivable`s be tracked for changes? */ it('dynamic/inactive dependencies', () => { const switch$ = atom(true); @@ -23,23 +25,23 @@ describe.skip('inner workings', () => { const reacted = jest.fn(); switch$ - // This `.derive()` is the one we are testing when true, it will return the `number` otherwise the `string` + // This `.derive()` is the one we are testing when true, it will + // return the `number` otherwise the `string` .derive(s => (s ? number$.get() : string$.get())) - // Note: reacted is being called as reacted(value, stop), - // where stop is a function used to stop the reaction from within reacted. .react(reacted); // The first time should not surprise anyone, the derivation // was called and returned the right result. - // Again note here the second expectation (.toBeFunction()) to - // catch the stop function that was part of the .react() signature. expect(reacted).toHaveBeenCalledExactlyOnceWith(1, expect.toBeFunction()); + // Note here the second expectation `.toBeFunction()` to + // catch the stop function that was part of the .react() signature. // `switch$` is still set to true (number) string$.set('two'); /** - * **Your Turn** + * ** Your Turn ** + * * What do you expect? */ expect(reacted).toHaveBeenCalledTimes(__YOUR_TURN__); @@ -49,21 +51,25 @@ describe.skip('inner workings', () => { number$.set(2); /** - * **Your Turn** + * ** Your Turn ** + * * What do you expect? */ expect(reacted).toHaveBeenCalledTimes(__YOUR_TURN__); expect(reacted).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); - // Now let's reset the spy, so callCount should be 0 again. + // Now let's reset the mock function, so the call count should + // be 0 again. reacted.mockClear(); + expect(reacted).toHaveBeenCalledTimes(0); // `switch$` is set to false (string) switch$.set(false); number$.set(3); /** - * **Your Turn** + * ** Your Turn ** + * * What do you expect now? */ expect(reacted).toHaveBeenCalledTimes(__YOUR_TURN__); @@ -71,7 +77,9 @@ describe.skip('inner workings', () => { }); /** - * One thing to know about `Derivable`s is that derivations are not executed, until someone asks. + * One thing to know about `Derivable`s is that derivations are not + * executed, until someone asks. + * * So let's test this. */ it('lazy execution', () => { @@ -81,32 +89,47 @@ describe.skip('inner workings', () => { const myDerivation$ = myAtom$.derive(hasDerived); /** - * **Your Turn** - * We have created a new `Derivable` by deriving the `Atom`. But have not called `.get()` on that new `Derivable`. - * Do you think the `hasDerived` function has been called? And how many times? - * *Hint: you can use sinonChai's `.to.have.been.called`/`.to.have.been.calledOnce`/`to.have.callCount(...)`/etc..* + * ** Your Turn ** + * + * We have created a new `Derivable` by deriving the `Atom`. But have + * not called `.get()` on that new `Derivable`. + * + * How many times do you think the `hasDerived` function has been + * called? 0 is also an option of course. */ - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); // Well, what do you expect? + + // Well, what do you expect? + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); myDerivation$.get(); - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); // And after a `.get()`? + // And after a `.get()`? + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); myDerivation$.get(); - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); // And after the second `.get()`? Is there an extra call? + // And after the second `.get()`? Is there an extra call? + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); /** * The state of any `Derivable` can change at any moment. - * But you don't want to keep a record of the state and changes to a `Derivable` that no one is listening to. - * That's why a `Derivable` has to recalculate it's internal state every time `.get()` is called. + * + * But you don't want to keep a record of the state and changes to a + * `Derivable` that no one is listening to. + * + * That's why a `Derivable` has to recalculate it's internal state every + * time `.get()` is called. */ }); /** * So what if the `Derivable` is reacting? + * * When a `Derivable` is reacting, the current state is known. - * And since changes are derived/reacted to synchronously, the state is always up to date. + * + * And since changes are derived/reacted to synchronously, the state is + * always up to date. + * * So a `.get()` should not have to be calculated. */ it('while reacting', () => { @@ -121,9 +144,9 @@ describe.skip('inner workings', () => { const stopper = myDerivation$.react(() => ''); /** - * **Your Turn** + * ** Your Turn ** + * * Ok, it's your turn to complete the expectations. - * *Hint: you can use `.calledOnce`/`.calledTwice` etc or `.callCount()`* */ expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); @@ -148,8 +171,11 @@ describe.skip('inner workings', () => { expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); /** - * Since the `.react()` already listens to the value(changes) there is no need to recalculate whenever a `.get()` is called. - * But when the reactor has stopped, the derivation has to be calculated again. + * Since the `.react()` already listens to the value(changes) there is + * no need to recalculate whenever a `.get()` is called. + * + * But when the reactor has stopped, the derivation has to be calculated + * again. */ }); @@ -163,7 +189,7 @@ describe.skip('inner workings', () => { const myAtom$ = atom(1); const first$ = myAtom$.derive(i => { - first(i); // Call the spy, to let it know we were here + first(i); // Call the mock function, to let it know we were here return i > 2; }); const second$ = first$.derive(second); @@ -179,10 +205,13 @@ describe.skip('inner workings', () => { expect(second).toHaveBeenCalledOnce(); /** - * **Your Turn** + * ** Your Turn ** + * * But what to expect now? */ - myAtom$.set(1); // Note that this is the same value as it was initialized with + + // Note that this is the same value as it was initialized with + myAtom$.set(1); expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); @@ -205,18 +234,23 @@ describe.skip('inner workings', () => { /** * Can you explain the behavior above? * - * It is why we say that `@politie/sherlock` deals with reactive state and not events (as RxJS does for example). - * Events can be very useful, but when data is involved, you are probably only interested in value changes. - * So these changes can and need to be cached and deduplicated. + * It is why we say that `@skunkteam/sherlock` deals with reactive state + * and not events (as RxJS does for example). + * + * Events can be very useful, but when data is involved, you are + * probably only interested in value changes. So these changes can and + * need to be cached and deduplicated. */ }); /** - * So if the new value of a `Derivable` is equal to the old, it won't propagate a new event. - * But what does it mean to be equal in a `Derivable`. + * So if the new value of a `Derivable` is equal to the old, it won't + * propagate a new event. But what does it mean to be equal in a + * `Derivable`? * - * Strict `===` equality would mean that `NaN` and `NaN` would not even be equal. - * `Object.is()` equality would be better, but would mean that structurally equal objects could be different. + * Strict `===` equality would mean that `NaN` and `NaN` would not even be + * equal. `Object.is()` equality would be better, but would mean that + * structurally equal objects could be different. */ it('equality', () => { const atom$ = atom({}); @@ -227,13 +261,17 @@ describe.skip('inner workings', () => { atom$.set({}); /** - * **Your Turn** - * The `Atom` is set with exactly the same object as before. Will the `.react()` fire? + * ** Your Turn ** + * + * The `Atom` is set with exactly the same object as before. Will the + * `.react()` fire? */ expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); /** - * But what if you use an object, that can be easily compared through a library like `ImmutableJS` + * But what if you use an object, that can be easily compared through a + * library like `ImmutableJS`? + * * Let's try an `Immutable.Seq` */ atom$.set(Seq.Indexed.of(1, 2, 3)); @@ -243,75 +281,31 @@ describe.skip('inner workings', () => { atom$.set(Seq.Indexed.of(1, 2, 3)); /** - * **Your Turn** + * ** Your Turn ** + * * Do you think the `.react()` fired with this new value? */ - expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasReacted).toHaveBeenCalledTimes(0); atom$.set(Seq.Indexed.of(1, 2)); /** - * **Your Turn** + * ** Your Turn ** + * * And now? */ - expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasReacted).toHaveBeenCalledTimes(1); /** - * In `@politie/sherlock` equality is a bit complex. - * First we check `Object.is()` equality, if that is true, it is the same, you can't deny that. + * In `@skunkteam/sherlock` equality is a bit complex: + * + * First we check `Object.is()` equality, if that is true, it is the + * same, you can't deny that. + * * After that it is pluggable. It can be anything you want. - * By default we try to use `.equals()`, to support libraries like `ImmutableJS`. - */ - }); - - /** - * What if there is a derivation that reads from one of two `Derivable`s dynamically? - * Will both of those `Derivable`s be tracked for changes? - */ - it('dynamic/inactive dependencies', () => { - const switch$ = atom(true); - const number$ = atom(1); - const string$ = atom('one'); - - const reacted = jest.fn(); - - switch$ - // This `.derive()` is the one we are testing when true, it will return the `number` otherwise the `string` - .derive(s => (s ? number$.get() : string$.get())) - .react(reacted); - - // The first time should not surprise anyone, the derivation was called and returned the right result - expect(reacted).toHaveBeenCalledExactlyOnceWith(1, expect.toBeFunction()); - - // `switch$` is still set to true (number) - string$.set('two'); - - /** - * **Your Turn** - * What do you expect? - */ - expect(reacted).toHaveBeenCalledTimes(__YOUR_TURN__); - - // `switch$` is still set to true (number) - number$.set(2); - - /** - * **Your Turn** - * What do you expect? - */ - expect(reacted).toHaveBeenCalledTimes(__YOUR_TURN__); - - // Now let's reset the spy, so callCount should be 0 again. - reacted.mockClear(); - - // `switch$` is set to false (string) - switch$.set(false); - number$.set(3); - - /** - * **Your Turn** - * What do you expect now? + * + * By default we try to use `.equals()`, to support libraries like + * `ImmutableJS`. */ - expect(reacted).toHaveBeenCalledTimes(__YOUR_TURN__); }); }); From feec43d4e93daaa8ceafd5c7d8f6638d414deff1 Mon Sep 17 00:00:00 2001 From: Robin Kneepkens Date: Tue, 21 Nov 2023 16:58:43 +0100 Subject: [PATCH 19/30] Finalized tutorial 5 --- tutorial/5 - unresolved.test.ts | 74 +++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 26 deletions(-) diff --git a/tutorial/5 - unresolved.test.ts b/tutorial/5 - unresolved.test.ts index 137ec15..998b121 100644 --- a/tutorial/5 - unresolved.test.ts +++ b/tutorial/5 - unresolved.test.ts @@ -1,31 +1,37 @@ import { atom, Derivable, DerivableAtom } from '@skunkteam/sherlock'; /** - * **Your Turn** + * ** Your Turn ** + * * If you see this variable, you should do something about it. :-) */ export const __YOUR_TURN__ = {} as any; /** - * Sometimes your data isn't available yet. For example if it is still being fetched from the server. - * At that point you probably still want your `Derivable` to exist, to start deriving and reacting when the data becomes available. + * Sometimes your data isn't available yet. For example if it is still being + * fetched from the server. At that point you probably still want your + * `Derivable` to exist, to start deriving and reacting when the data becomes + * available. * - * To support this, `Derivable`s in `@politie/sherlock` support a separate state, called `unresolved`. - * This indicates that the data is not available yet, but (probably) will be at some point. + * To support this, `Derivable`s in `@skunkteam/sherlock` support a separate + * state, called `unresolved`. This indicates that the data is not available + * yet, but (probably) will be at some point. */ describe.skip('unresolved', () => { /** * Let's start by creating an `unresolved` `Derivable`. */ it('can be checked on the `Derivable`', () => { - // By using the `.unresolved()` method, you can create an `unresolved` atom - // Note that you will need to indicate the type of this atom, since it can't be inferred by TypeScript this way. + // By using the `.unresolved()` method, you can create an `unresolved` + // atom. Note that you will need to indicate the type of this atom, + // since it can't be inferred by TypeScript this way. const myAtom$ = atom.unresolved(); expect(myAtom$.resolved).toEqual(__YOUR_TURN__); /** - * **Your Turn** + * ** Your Turn ** + * * Resolve the atom, it's pretty easy */ @@ -38,18 +44,23 @@ describe.skip('unresolved', () => { */ it('cannot `.get()`', () => { /** - * **Your Turn** + * ** Your Turn ** + * * Time to create an `unresolved` Atom.. */ const myAtom$: DerivableAtom = __YOUR_TURN__; expect(myAtom$.resolved).toBeFalse(); + + // By this test passing, we see that `.get()` on an unresolved + // `Derivable` indeed throws an error. expect(() => myAtom$.get()).toThrow('Could not get value, derivable is unresolved'); myAtom$.set('finally!'); /** - * **Your Turn** + * ** Your Turn ** + * * What do you expect? */ expect(myAtom$.resolved).toEqual(__YOUR_TURN__); @@ -59,7 +70,8 @@ describe.skip('unresolved', () => { }); /** - * If a `Derivable` is `unresolved` it can't react yet. But it will `.react()` if a value becomes available. + * If a `Derivable` is `unresolved` it can't react yet. But it will + * `.react()` if a value becomes available. * * *Note that this can prevent `.react()` from executing immediately* */ @@ -70,24 +82,25 @@ describe.skip('unresolved', () => { myAtom$.react(hasReacted); /** - * **Your Turn** + * ** Your Turn ** + * * What do you expect? */ expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(hasReacted).toHaveBeenCalledWith(__YOUR_TURN__); /** - * **Your Turn** + * ** Your Turn ** + * * Now make the last expect succeed */ expect(myAtom$.resolved).toBeTrue(); - expect(hasReacted).toHaveBeenCalledExactlyOnceWith(`woohoow, I was called`); + expect(hasReacted).toHaveBeenCalledExactlyOnceWith(`woohoow, I was called`, expect.toBeFunction()); }); /** - * In `@politie/sherlock` there is no reason why a `Derivable` should not become `unresolved` again, - * after it has been set. + * In `@skunkteam/sherlock` there is no reason why a `Derivable` should not + * become `unresolved` again after it has been set. */ it('can become `unresolved` again', () => { const myAtom$ = atom.unresolved(); @@ -95,14 +108,16 @@ describe.skip('unresolved', () => { expect(myAtom$.resolved).toBeFalse(); /** - * **Your Turn** + * ** Your Turn ** + * * Set the value.. */ expect(myAtom$.get()).toEqual(`it's alive!`); /** - * **Your Turn** + * ** Your Turn ** + * * Unset the value.. (*Hint: TypeScript is your friend*) */ @@ -110,22 +125,26 @@ describe.skip('unresolved', () => { }); /** - * When a `Derivable` is dependent on another `unresolved` `Derivable`, this `Derivable` should also become `unresolved`. + * When a `Derivable` is dependent on another `unresolved` `Derivable`, this + * `Derivable` should also become `unresolved`. * - * *Note that this will only become `unresolved` when there is an active dependency (see 'inner workings#dynamic dependencies')* + * *Note that this will only become `unresolved` when there is an active + * dependency (see 'inner workings#dynamic dependencies')* */ it('will propagate', () => { const myString$ = atom.unresolved(); const myOtherString$ = atom.unresolved(); /** - * **Your Turn** + * ** Your Turn ** + * * Combine the two `Atom`s into one `Derivable` */ const myDerivable$: Derivable = __YOUR_TURN__; /** - * **Your Turn** + * ** Your Turn ** + * * Is `myDerivable$` expected to be `resolved`? */ expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); @@ -134,17 +153,20 @@ describe.skip('unresolved', () => { myString$.set('some'); /** - * **Your Turn** + * ** Your Turn ** + * * What do you expect to see in `myDerivable$`. - * And what if we set `myOtherString$`? */ expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); + + // And what if we set `myOtherString$`? myOtherString$.set('data'); expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); expect(myDerivable$.get()).toEqual(__YOUR_TURN__); /** - * **Your Turn** + * ** Your Turn ** + * * Now we will unset one of the `Atom`s. * What do you expect `myDerivable$` to be? */ From 03b5aa06596b7b9cc419d62cc5fb7304bf6503a3 Mon Sep 17 00:00:00 2001 From: Robin Kneepkens Date: Wed, 22 Nov 2023 15:12:47 +0100 Subject: [PATCH 20/30] Finalized tutorial 7 --- tutorial/7 - utils.test.ts | 116 ++++++++++++++++++++++++++++++------- 1 file changed, 94 insertions(+), 22 deletions(-) diff --git a/tutorial/7 - utils.test.ts b/tutorial/7 - utils.test.ts index 460fac3..3e49e8e 100644 --- a/tutorial/7 - utils.test.ts +++ b/tutorial/7 - utils.test.ts @@ -2,30 +2,42 @@ import { atom } from '@skunkteam/sherlock'; import { pairwise, scan, struct } from '@skunkteam/sherlock-utils'; /** - * **Your Turn** + * ** Your Turn ** + * * If you see this variable, you should do something about it. :-) */ export const __YOUR_TURN__ = {} as any; +// Silence TypeScript's import not used errors. +expect(pairwise).toBe(pairwise); +expect(scan).toBe(scan); +expect(struct).toBe(struct); + /** - * In the `sherlock-utils` lib, there are a couple of functions that can combine multiple values of a single `Derivable` - * or combine multiple `Derivable`s into one. We will show a couple of those here. + * In the `sherlock-utils` lib, there are a couple of functions that can combine + * multiple values of a single `Derivable` or combine multiple `Derivable`s into + * one. We will show a couple of those here. */ describe.skip('utils', () => { /** - * As the name suggests, `pairwise()` will call the given function with both the current and the previous state. + * As the name suggests, `pairwise()` will call the given function with both + * the current and the previous state. * - * *Note functions like `pairwise` and `scan` can be used with any callback. So it can be used both in a `.derive()` step and in a `.react()`* + * *Note functions like `pairwise` and `scan` can be used with any callback, + * so it can be used both in a `.derive()` step and in a `.react()`* */ it('pairwise', () => { - expect(pairwise).toBe(pairwise); // use `pairwise` so the import is used. - const myCounter$ = atom(1); const reactSpy = jest.fn(); /** - * **Your Turn** - * Now, use `pairwise()`, to subtract the previous value from the current + * ** Your Turn ** + * + * Now, use `pairwise()`, to subtract the previous value from the + * current. + * + * *Hint: check the overloads of pairwise if you're struggling with + * `oldVal`.* */ myCounter$.derive(__YOUR_TURN__).react(reactSpy); @@ -33,7 +45,6 @@ describe.skip('utils', () => { myCounter$.set(3); - // Note: two `expect`s in a row is the same as chaining with `.and`, right? expect(reactSpy).toHaveBeenCalledTimes(2); expect(reactSpy).toHaveBeenCalledWith(2, expect.toBeFunction()); @@ -44,18 +55,21 @@ describe.skip('utils', () => { }); /** - * `scan` is the `Derivable` version of `Array.prototype.reduce`. It will be called with the current state and the last emitted value. + * `scan` is the `Derivable` version of `Array.prototype.reduce`. It will be + * called with the current state and the last emitted value. * - * *Note: as with `pairwise()` this is useable in both a `.derive()` and `.react()` method* + * (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce) + * + * *Note: as with `pairwise()` this is useable in both a `.derive()` and + * `.react()` method* */ it('scan', () => { - expect(scan).toBe(scan); // use `scan` so the import is used. - const myCounter$ = atom(1); const reactSpy = jest.fn(); /** - * **Your Turn** + * ** Your Turn ** + * * Now, use `scan()`, to add all the emitted values together */ myCounter$.derive(__YOUR_TURN__).react(reactSpy); @@ -73,19 +87,75 @@ describe.skip('utils', () => { expect(reactSpy).toHaveBeenCalledWith(49, expect.toBeFunction()); /** - * *BONUS: Try using `scan()` (or `pairwise()`) directly in the `.react()` method.* + * *BONUS: Try using `scan()` (or `pairwise()`) directly in the + * `.react()` method.* + */ + }); + + it.skip('pairwise - BONUS', () => { + const myCounter$ = atom(1); + let lastPairwiseResult = 0; + + /** + * ** Your Turn ** + * ** BONUS ** + * + * Now, use `pairwise()` directly in `.react()`. Implement the same + * derivation as before: subtract the previous value from the current. + * + * Instead of returning the computed value, assign it + * `lastPairwiseResult` instead. This is so the implementation can be + * validated. + */ + myCounter$.react(__YOUR_TURN__); + + expect(lastPairwiseResult).toEqual(1); + + myCounter$.set(3); + + expect(lastPairwiseResult).toEqual(2); + + myCounter$.set(45); + + expect(lastPairwiseResult).toEqual(42); + }); + + it.skip('scan - BONUS', () => { + const myCounter$ = atom(1); + let lastScanResult = 0; + + /** + * ** Your Turn ** + * ** BONUS ** + * + * Now, use `scan()` directly in `.react()`. Implement the same + * derivation as before: add all the emitted values together. + * + * In addition to returning the computed value, assign it + * `lastScanResult` instead. This is so the implementation can be + * validated. */ + myCounter$.react(__YOUR_TURN__); + + expect(lastScanResult).toEqual(1); + + myCounter$.set(3); + expect(lastScanResult).toEqual(4); + + myCounter$.set(45); + expect(lastScanResult).toEqual(49); }); /** - * A `struct()` can combine an Object/Array of `Derivable`s into one `Derivable`, that contains the values of that `Derivable`. - * The Object/Array that is in the output of `struct()` will have the same structure as the original Object/Array. + * A `struct()` can combine an Object/Array of `Derivable`s into one + * `Derivable`, that contains the values of that `Derivable`. + * + * The Object/Array that is in the output of `struct()` will have the same + * structure as the original Object/Array. * * This is best explained in practice. */ it('struct', () => { - expect(struct).toBe(struct); // use `struct` so the import is used. - const allMyAtoms = { regularProp: 'prop', string: atom('my string'), @@ -110,8 +180,10 @@ describe.skip('utils', () => { allMyAtoms.sub.string.set('my new substring'); /** - * **Your Turn** - * Now have a look at the properties of `myOneAtom$`. Is this what you expect? + * ** Your Turn ** + * + * Now have a look at the properties of `myOneAtom$`. Is this what you + * expect? */ expect(myOneAtom$.get()).toEqual({ regularProp: __YOUR_TURN__, From 748978d9212d5f7a58cf543a3fd759d16aba9f11 Mon Sep 17 00:00:00 2001 From: Robin Kneepkens Date: Wed, 22 Nov 2023 16:04:23 +0100 Subject: [PATCH 21/30] Finalized tutorial 8 --- tutorial/8 - advanced.test.ts | 154 +++++++++++++++++++++------------- 1 file changed, 98 insertions(+), 56 deletions(-) diff --git a/tutorial/8 - advanced.test.ts b/tutorial/8 - advanced.test.ts index 457b95b..48e9944 100644 --- a/tutorial/8 - advanced.test.ts +++ b/tutorial/8 - advanced.test.ts @@ -2,7 +2,8 @@ import { atom, constant, Derivable, derive, SettableDerivable } from '@skunkteam import { Map as ImmutableMap } from 'immutable'; /** - * **Your Turn** + * ** Your Turn ** + * * If you see this variable, you should do something about it. :-) */ export const __YOUR_TURN__ = {} as any; @@ -23,7 +24,8 @@ describe.skip('advanced', () => { const c = constant('value') as unknown as SettableDerivable; /** - * **Your Turn** + * ** Your Turn ** + * * What do you expect this `Derivable` to do on `.set()`, `.get()` etc? */ @@ -35,21 +37,24 @@ describe.skip('advanced', () => { }); /** - * Collections in `ImmutableJS` are immutable, so any modification to a collection will create a new one. - * This results in every change needing a `.get()` and a `.set()` on a `Derivable`. + * Collections in `ImmutableJS` are immutable, so any modification to a + * collection will create a new one. This results in every change needing a + * `.get()` and a `.set()` on a `Derivable`. * - * To make this pattern a little bit easier, the `.swap()` method can be used. - * The given function will get the current value of the `Derivable` and any return value will be set as the new value. + * To make this pattern a little bit easier, the `.swap()` method can be + * used. The given function will get the current value of the `Derivable` + * and any return value will be set as the new value. */ it('`.swap()`', () => { - // This is a separate function, because you might be able to use this later + // This is a separate function because you might want to use this later. function plusOne(num: number) { return num + 1; } const myCounter$ = atom(0); /** - * **Your Turn** + * ** Your Turn ** + * * Rewrite the `.get()`/`.set()` combos below using `.swap()`. */ expect(false).toBe(true); @@ -62,7 +67,8 @@ describe.skip('advanced', () => { }); /** - * As an alternative to `.get()` and `.set()`, there is also the `.value` accessor. + * As an alternative to `.get()` and `.set()`, there is also the `.value` + * accessor. */ describe('.value', () => { /** @@ -73,13 +79,15 @@ describe.skip('advanced', () => { const myAtom$ = atom('foo'); /** - * **Your Turn** + * ** Your Turn ** + * * Use the `.value` accessor to get the current value. */ expect(__YOUR_TURN__).toEqual('foo'); /** - * **Your Turn** + * ** Your Turn ** + * * Now use the `.value` accessor to set a 'new value'. */ myAtom$.value = __YOUR_TURN__; @@ -95,14 +103,15 @@ describe.skip('advanced', () => { const myAtom$ = atom.unresolved(); /** - * **Your Turn** + * ** Your Turn ** */ expect(myAtom$.value).toEqual(__YOUR_TURN__); }); /** - * As a result, if `.value` is used inside a derivation, it will also replace `unresolved` with `undefined`. - * So `unresolved` will not automatically propagate when using `.value`. + * As a result, if `.value` is used inside a derivation, it will also + * replace `unresolved` with `undefined`. So `unresolved` will not + * automatically propagate when using `.value`. */ it('will stop propagation of `unresolved` in `.derive()`', () => { const myAtom$ = atom('foo'); @@ -114,7 +123,8 @@ describe.skip('advanced', () => { expect(usingVal$.get()).toEqual('foo'); /** - * **Your Turn** + * ** Your Turn ** + * * We just created two `Derivable`s that are almost exactly the same. * But what happens when their source becomes `unresolved`? */ @@ -141,7 +151,8 @@ describe.skip('advanced', () => { it('triggers when the source changes', () => { const myAtom$ = atom(1); /** - * **Your Turn** + * ** Your Turn ** + * * Use the `.map()` method to create the expected output below */ const mappedAtom$: Derivable = __YOUR_TURN__; @@ -170,7 +181,8 @@ describe.skip('advanced', () => { myRepeat$.value = 3; /** - * **Your Turn** + * ** Your Turn ** + * * We changed`myRepeat$` to equal 3. * Do you expect both reactors to have fired? And with what? */ @@ -182,7 +194,8 @@ describe.skip('advanced', () => { myString$.value = 'ha'; /** - * **Your Turn** + * ** Your Turn ** + * * And now that we have changed `myString$`? And when `myRepeat$` changed again? */ expect(deriveReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); @@ -199,51 +212,60 @@ describe.skip('advanced', () => { expect(mapReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); /** - * As you can see, a change in `myString$` will not trigger an update. - * But if an update is triggered, `myString$` will be called and the new value will be used. + * As you can see, a change in `myString$` will not trigger an + * update. But if an update is triggered, `myString$` will be called + * and the new value will be used. */ }); /** - * Since `.map()` is a relatively simple mapping of input value to output value. - * It can often be reversed. In that case you can use that reverse mapping to create a `SettableDerivable`. + * Since `.map()` is a relatively simple mapping of input value to + * output value. It can often be reversed. In that case you can use that + * reverse mapping to create a `SettableDerivable`. */ it('can be settable', () => { const myAtom$ = atom(1); /** - * **Your Turn** + * ** Your Turn ** + * + * Check the comments and `expect`s below to see what should be + * implemented exactly. */ const myInverse$ = myAtom$.map( - // This first function is called when getting + // This first function is called when getting... n => -n, - // The second is called when setting, you may want to fix this one though + // ...and this second function is called when setting. __YOUR_TURN__, ); + // The original `atom` was set to 1, so we want the inverse to + // be equal -1. expect(myInverse$.get()).toEqual(-1); + // Now we set the inverse to -2 directly, so we expect the original + // `atom` to be equal to 2. myInverse$.set(-2); - - /** - * **Your Turn** - */ - expect(myAtom$.get()).toEqual(__YOUR_TURN__); - expect(myInverse$.get()).toEqual(__YOUR_TURN__); + expect(myAtom$.get()).toEqual(2); + expect(myInverse$.get()).toEqual(-2); }); }); /** * `.pluck()` is a special case of the `.map()` method. - * If a collection of values, like an Object, Map, Array is the result of a `Derivable` one of those values can be plucked into a new `Derivable`. + * If a collection of values, like an Object, Map, Array is the result of a + * `Derivable` one of those values can be plucked into a new `Derivable`. * This plucked `Derivable` can be settable, if the source supports it. * - * The way properties are plucked is pluggable, but by default both `.get()` and `[]` are supported. - * To support basic Objects, Maps and Arrays. + * The way properties are plucked is pluggable, but by default both + * `.get()` and `[]` are supported to support + * basic Objects, Maps and Arrays. + * + * *Note that normally when a value of a collection changes, the reference + * does not. This means that setting a plucked property of a regular + * Object/Array/Map will not cause any reaction on that source `Derivable`. * - * *Note that normally when a value of a collection changes, the reference does not.* - * *This means that setting a plucked property of a regular Object/Array/Map will not cause any reaction on that source `Derivable`.* - * *ImmutableJS can help fix this problem* + * ImmutableJS can help fix this problem* */ describe('`.pluck()`', () => { const reactSpy = jest.fn(); @@ -262,54 +284,66 @@ describe.skip('advanced', () => { }), ); /** - * **Your Turn** + * ** Your Turn ** + * * `.pluck()` 'firstProp' from `myMap$`. + * + * * Hint: you'll have to cast the result from `.pluck()`. */ firstProp$ = __YOUR_TURN__; }); /** - * Once a property is plucked in a new `Derivable`. This `Derivable` can be used as a regular `Derivable`. + * Once a property is plucked in a new `Derivable`. This `Derivable` can + * be used as a regular `Derivable`. */ it('can be used as a normal `Derivable`', () => { firstProp$.react(reactPropSpy, { skipFirst: true }); /** - * **Your Turn** - * What do you expect the plucked `Derivable` to look like? And what happens when we `.set()` it? + * ** Your Turn ** + * + * What do you expect the plucked `Derivable` to look like? And what + * happens when we `.set()` it? */ expect(firstProp$.get()).toEqual(__YOUR_TURN__); - firstProp$.set('other value'); // the plucked `Derivable` should be settable - expect(firstProp$.get()).toEqual(__YOUR_TURN__); // is the `Derivable` value the same as was set? + // the plucked `Derivable` should be settable + firstProp$.set('other value'); + // is the `Derivable` value the same as was set? + expect(firstProp$.get()).toEqual(__YOUR_TURN__); // How many times was the spy called? Note the `skipFirst`.. expect(reactPropSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - // ... and what was the value? + // ...and what was the value? expect(reactPropSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); }); /** - * If the source of the plucked `Derivable` changes, the plucked `Derivable` will change as well. - * As long as the change affects the plucked property of course. + * If the source of the plucked `Derivable` changes, the plucked + * `Derivable` will change as well. As long as the change affects the + * plucked property of course. */ it('will react to changes in the source `Derivable`', () => { firstProp$.react(reactPropSpy, { skipFirst: true }); /** - * **Your Turn** + * ** Your Turn ** + * * We will set `secondProp`, will this affect `firstProp$`? + * + * *Note: this `map` refers to `ImmutableMap`, not to the + * `Derivable.map()` we saw earlier in the tutorial.* */ myMap$.swap(map => map.set('secondProp', 'new value')); - // How many times was the spy called? Note the `skipFirst`.. - expect(reactPropSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - // ... and what was the value? - expect(reactPropSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + // How many times was the spy called? Note the `skipFirst`. + expect(reactPropSpy).toHaveBeenCalledTimes(__YOUR_TURN__); /** - * **Your Turn** + * ** Your Turn ** + * * And what if we set `firstProp`? */ myMap$.swap(map => map.set('firstProp', 'new value')); @@ -317,23 +351,31 @@ describe.skip('advanced', () => { // How many times was the spy called? Note the `skipFirst`.. expect(reactPropSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - // ... and what was the value? + // ...and what was the value? expect(reactPropSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); }); /** + * Before, we saw how a change in the source of the plucked `Derivable` + * propagates to it. Now the question is: does this go the other way + * too? * + * We saw that we can `.set()` the value of the plucked `Derivable`, so + * what happens to the source if we do that? */ it('will write through to the source `Derivable`', () => { myMap$.react(reactSpy, { skipFirst: true }); /** - * **Your Turn** - * So what if we set `firstProp$`? Does this propagate to the source `Derivable`? + * ** Your Turn ** + * + * So what if we set `firstProp$`? Does this propagate to the source + * `Derivable`? */ firstProp$.set(__YOUR_TURN__); expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(myMap$.get()).toEqual(__YOUR_TURN__); + expect(myMap$.get().get('firstProp')).toEqual(__YOUR_TURN__); + expect(myMap$.get().get('secondProp')).toEqual(__YOUR_TURN__); }); }); }); From ec2aac7b9bcda45bd2372091cabdd3e0760e73e9 Mon Sep 17 00:00:00 2001 From: Robin Kneepkens Date: Wed, 22 Nov 2023 18:15:05 +0100 Subject: [PATCH 22/30] Finalized tutorial 9 --- package-lock.json | 580 ---------------------------- package.json | 5 - tutorial/2 - deriving.test.ts | 4 +- tutorial/3 - reacting.test.ts | 2 +- tutorial/4 - inner workings.test.ts | 1 - tutorial/9 - expert.test.ts | 263 ++++++++----- 6 files changed, 172 insertions(+), 683 deletions(-) diff --git a/package-lock.json b/package-lock.json index b61a57d..8fc96be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,14 +38,10 @@ "@nx/node": "16.3.2", "@nx/workspace": "16.3.2", "@schematics/angular": "^16.1.0", - "@types/chai": "^4.1.7", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", - "@types/sinon-chai": "^3.2.2", "@typescript-eslint/eslint-plugin": "^5.60.0", "@typescript-eslint/parser": "^5.60.0", - "chai": "^4.2.0", - "chai-immutable": "^2.0.0-alpha.1", "codelyzer": "^6.0.2", "dotenv": "^16.3.1", "eslint": "^8.43.0", @@ -62,7 +58,6 @@ "nx": "16.3.2", "postcss-preset-env": "^8.5.0", "prettier": "2.8.8", - "sinon-chai": "^3.3.0", "standard-version": "^9.5.0", "ts-jest": "29.1.0", "ts-node": "10.9.1", @@ -7024,56 +7019,6 @@ "@sinonjs/commons": "^3.0.0" } }, - "node_modules/@sinonjs/formatio": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.2.tgz", - "integrity": "sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==", - "dev": true, - "peer": true, - "dependencies": { - "@sinonjs/commons": "^1", - "@sinonjs/samsam": "^3.1.0" - } - }, - "node_modules/@sinonjs/formatio/node_modules/@sinonjs/commons": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", - "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", - "dev": true, - "peer": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/samsam": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz", - "integrity": "sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==", - "dev": true, - "peer": true, - "dependencies": { - "@sinonjs/commons": "^1.3.0", - "array-from": "^2.1.1", - "lodash": "^4.17.15" - } - }, - "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", - "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", - "dev": true, - "peer": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/text-encoding": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", - "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", - "dev": true, - "peer": true - }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -7176,12 +7121,6 @@ "@types/node": "*" } }, - "node_modules/@types/chai": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz", - "integrity": "sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA==", - "dev": true - }, "node_modules/@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -7444,31 +7383,6 @@ "@types/node": "*" } }, - "node_modules/@types/sinon": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.1.tgz", - "integrity": "sha512-Q2Go6TJetYn5Za1+RJA1Aik61Oa2FS8SuJ0juIqUuJ5dZR4wvhKfmSdIqWtQ3P6gljKWjW0/R7FZkA4oXVL6OA==", - "dev": true, - "dependencies": { - "@types/sinonjs__fake-timers": "*" - } - }, - "node_modules/@types/sinon-chai": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.2.tgz", - "integrity": "sha512-5zSs2AslzyPZdOsbm2NRtuSNAI2aTWzNKOHa/GRecKo7a5efYD7qGcPxMZXQDayVXT2Vnd5waXxBvV31eCZqiA==", - "dev": true, - "dependencies": { - "@types/chai": "*", - "@types/sinon": "*" - } - }, - "node_modules/@types/sinonjs__fake-timers": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", - "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", - "dev": true - }, "node_modules/@types/sockjs": { "version": "0.3.33", "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", @@ -8315,13 +8229,6 @@ "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", "dev": true }, - "node_modules/array-from": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", - "integrity": "sha512-GQTc6Uupx1FCavi5mPzBvVT7nEOeWMmUA9P95wpfpW1XwMSKs+KaymD5C2Up7KAUKg/mYwbsUYzdZWcoajlNZg==", - "dev": true, - "peer": true - }, "node_modules/array-ify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", @@ -8401,15 +8308,6 @@ "node": ">=0.10.0" } }, - "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/ast-types-flow": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", @@ -9141,32 +9039,6 @@ } ] }, - "node_modules/chai": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", - "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", - "dev": true, - "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "pathval": "^1.1.0", - "type-detect": "^4.0.5" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chai-immutable": { - "version": "2.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/chai-immutable/-/chai-immutable-2.0.0-alpha.1.tgz", - "integrity": "sha512-VsyYOdzimJrylK690LdS1dEZPMB42bL5Qgz6JMudrV3jC9Kv6M+B31Decke9Dwn0tf3xD9Qcng/9NfxLFbetmQ==", - "dev": true, - "peerDependencies": { - "chai": "^4.0.0" - } - }, "node_modules/chalk": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", @@ -9198,18 +9070,6 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, - "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.2" - }, - "engines": { - "node": "*" - } - }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -10702,18 +10562,6 @@ "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", "dev": true }, - "node_modules/deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=0.12" - } - }, "node_modules/deep-equal": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.1.tgz", @@ -12954,15 +12802,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/get-intrinsic": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", @@ -15495,13 +15334,6 @@ "node": "*" } }, - "node_modules/just-extend": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", - "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", - "dev": true, - "peer": true - }, "node_modules/karma-source-map-support": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", @@ -15803,13 +15635,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lolex": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.2.0.tgz", - "integrity": "sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==", - "dev": true, - "peer": true - }, "node_modules/long": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", @@ -16523,57 +16348,6 @@ "node-gyp-build": "^4.2.2" } }, - "node_modules/nise": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.3.tgz", - "integrity": "sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ==", - "dev": true, - "peer": true, - "dependencies": { - "@sinonjs/formatio": "^3.2.1", - "@sinonjs/text-encoding": "^0.7.1", - "just-extend": "^4.0.2", - "lolex": "^5.0.1", - "path-to-regexp": "^1.7.0" - } - }, - "node_modules/nise/node_modules/@sinonjs/commons": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", - "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", - "dev": true, - "peer": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/nise/node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "dev": true, - "peer": true - }, - "node_modules/nise/node_modules/lolex": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", - "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", - "dev": true, - "peer": true, - "dependencies": { - "@sinonjs/commons": "^1.7.0" - } - }, - "node_modules/nise/node_modules/path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", - "dev": true, - "peer": true, - "dependencies": { - "isarray": "0.0.1" - } - }, "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", @@ -17514,15 +17288,6 @@ "node": ">=4" } }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -20047,76 +19812,6 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, - "node_modules/sinon": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.5.0.tgz", - "integrity": "sha512-AoD0oJWerp0/rY9czP/D6hDTTUYGpObhZjMpd7Cl/A6+j0xBE+ayL/ldfggkBXUs0IkvIiM1ljM8+WkOc5k78Q==", - "deprecated": "16.1.1", - "dev": true, - "peer": true, - "dependencies": { - "@sinonjs/commons": "^1.4.0", - "@sinonjs/formatio": "^3.2.1", - "@sinonjs/samsam": "^3.3.3", - "diff": "^3.5.0", - "lolex": "^4.2.0", - "nise": "^1.5.2", - "supports-color": "^5.5.0" - } - }, - "node_modules/sinon-chai": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.3.0.tgz", - "integrity": "sha512-r2JhDY7gbbmh5z3Q62pNbrjxZdOAjpsqW/8yxAZRSqLZqowmfGZPGUZPFf3UX36NLis0cv8VEM5IJh9HgkSOAA==", - "dev": true, - "peerDependencies": { - "chai": "^4.0.0", - "sinon": ">=4.0.0 <8.0.0" - } - }, - "node_modules/sinon/node_modules/@sinonjs/commons": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", - "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", - "dev": true, - "peer": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/sinon/node_modules/diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/sinon/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/sinon/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "peer": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -27345,60 +27040,6 @@ "@sinonjs/commons": "^3.0.0" } }, - "@sinonjs/formatio": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.2.tgz", - "integrity": "sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==", - "dev": true, - "peer": true, - "requires": { - "@sinonjs/commons": "^1", - "@sinonjs/samsam": "^3.1.0" - }, - "dependencies": { - "@sinonjs/commons": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", - "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", - "dev": true, - "peer": true, - "requires": { - "type-detect": "4.0.8" - } - } - } - }, - "@sinonjs/samsam": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz", - "integrity": "sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==", - "dev": true, - "peer": true, - "requires": { - "@sinonjs/commons": "^1.3.0", - "array-from": "^2.1.1", - "lodash": "^4.17.15" - }, - "dependencies": { - "@sinonjs/commons": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", - "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", - "dev": true, - "peer": true, - "requires": { - "type-detect": "4.0.8" - } - } - } - }, - "@sinonjs/text-encoding": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", - "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", - "dev": true, - "peer": true - }, "@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -27495,12 +27136,6 @@ "@types/node": "*" } }, - "@types/chai": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz", - "integrity": "sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA==", - "dev": true - }, "@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -27756,31 +27391,6 @@ "@types/node": "*" } }, - "@types/sinon": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.1.tgz", - "integrity": "sha512-Q2Go6TJetYn5Za1+RJA1Aik61Oa2FS8SuJ0juIqUuJ5dZR4wvhKfmSdIqWtQ3P6gljKWjW0/R7FZkA4oXVL6OA==", - "dev": true, - "requires": { - "@types/sinonjs__fake-timers": "*" - } - }, - "@types/sinon-chai": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.2.tgz", - "integrity": "sha512-5zSs2AslzyPZdOsbm2NRtuSNAI2aTWzNKOHa/GRecKo7a5efYD7qGcPxMZXQDayVXT2Vnd5waXxBvV31eCZqiA==", - "dev": true, - "requires": { - "@types/chai": "*", - "@types/sinon": "*" - } - }, - "@types/sinonjs__fake-timers": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", - "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", - "dev": true - }, "@types/sockjs": { "version": "0.3.33", "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", @@ -28427,13 +28037,6 @@ "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", "dev": true }, - "array-from": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", - "integrity": "sha512-GQTc6Uupx1FCavi5mPzBvVT7nEOeWMmUA9P95wpfpW1XwMSKs+KaymD5C2Up7KAUKg/mYwbsUYzdZWcoajlNZg==", - "dev": true, - "peer": true - }, "array-ify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", @@ -28489,12 +28092,6 @@ "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", "dev": true }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, "ast-types-flow": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", @@ -29028,27 +28625,6 @@ "integrity": "sha512-jaAOR5zVtxHfL0NjZyflVTtXm3D3J9P15zSJ7HmQF8dSKGA6tqzQq+0ZI3xkjyQj46I4/M0K2GbMpcAFOcbr3A==", "dev": true }, - "chai": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", - "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", - "dev": true, - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "pathval": "^1.1.0", - "type-detect": "^4.0.5" - } - }, - "chai-immutable": { - "version": "2.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/chai-immutable/-/chai-immutable-2.0.0-alpha.1.tgz", - "integrity": "sha512-VsyYOdzimJrylK690LdS1dEZPMB42bL5Qgz6JMudrV3jC9Kv6M+B31Decke9Dwn0tf3xD9Qcng/9NfxLFbetmQ==", - "dev": true, - "requires": {} - }, "chalk": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", @@ -29071,15 +28647,6 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, - "check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", - "dev": true, - "requires": { - "get-func-name": "^2.0.2" - } - }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -30201,15 +29768,6 @@ "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", "dev": true }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "dev": true, - "requires": { - "type-detect": "^4.0.0" - } - }, "deep-equal": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.1.tgz", @@ -31920,12 +31478,6 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, - "get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true - }, "get-intrinsic": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", @@ -33754,13 +33306,6 @@ "through": ">=2.2.7 <3" } }, - "just-extend": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", - "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", - "dev": true, - "peer": true - }, "karma-source-map-support": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", @@ -33988,13 +33533,6 @@ "is-unicode-supported": "^0.1.0" } }, - "lolex": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.2.0.tgz", - "integrity": "sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==", - "dev": true, - "peer": true - }, "long": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", @@ -34531,59 +34069,6 @@ "node-gyp-build": "^4.2.2" } }, - "nise": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.3.tgz", - "integrity": "sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ==", - "dev": true, - "peer": true, - "requires": { - "@sinonjs/formatio": "^3.2.1", - "@sinonjs/text-encoding": "^0.7.1", - "just-extend": "^4.0.2", - "lolex": "^5.0.1", - "path-to-regexp": "^1.7.0" - }, - "dependencies": { - "@sinonjs/commons": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", - "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", - "dev": true, - "peer": true, - "requires": { - "type-detect": "4.0.8" - } - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "dev": true, - "peer": true - }, - "lolex": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", - "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", - "dev": true, - "peer": true, - "requires": { - "@sinonjs/commons": "^1.7.0" - } - }, - "path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", - "dev": true, - "peer": true, - "requires": { - "isarray": "0.0.1" - } - } - } - }, "node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", @@ -35282,12 +34767,6 @@ } } }, - "pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true - }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -36969,65 +36448,6 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, - "sinon": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.5.0.tgz", - "integrity": "sha512-AoD0oJWerp0/rY9czP/D6hDTTUYGpObhZjMpd7Cl/A6+j0xBE+ayL/ldfggkBXUs0IkvIiM1ljM8+WkOc5k78Q==", - "dev": true, - "peer": true, - "requires": { - "@sinonjs/commons": "^1.4.0", - "@sinonjs/formatio": "^3.2.1", - "@sinonjs/samsam": "^3.3.3", - "diff": "^3.5.0", - "lolex": "^4.2.0", - "nise": "^1.5.2", - "supports-color": "^5.5.0" - }, - "dependencies": { - "@sinonjs/commons": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", - "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", - "dev": true, - "peer": true, - "requires": { - "type-detect": "4.0.8" - } - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true, - "peer": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "peer": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "peer": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "sinon-chai": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.3.0.tgz", - "integrity": "sha512-r2JhDY7gbbmh5z3Q62pNbrjxZdOAjpsqW/8yxAZRSqLZqowmfGZPGUZPFf3UX36NLis0cv8VEM5IJh9HgkSOAA==", - "dev": true, - "requires": {} - }, "sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", diff --git a/package.json b/package.json index 9cc2ade..9e3b39d 100644 --- a/package.json +++ b/package.json @@ -71,14 +71,10 @@ "@nx/node": "16.3.2", "@nx/workspace": "16.3.2", "@schematics/angular": "^16.1.0", - "@types/chai": "^4.1.7", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", - "@types/sinon-chai": "^3.2.2", "@typescript-eslint/eslint-plugin": "^5.60.0", "@typescript-eslint/parser": "^5.60.0", - "chai": "^4.2.0", - "chai-immutable": "^2.0.0-alpha.1", "codelyzer": "^6.0.2", "dotenv": "^16.3.1", "eslint": "^8.43.0", @@ -95,7 +91,6 @@ "nx": "16.3.2", "postcss-preset-env": "^8.5.0", "prettier": "2.8.8", - "sinon-chai": "^3.3.0", "standard-version": "^9.5.0", "ts-jest": "29.1.0", "ts-node": "10.9.1", diff --git a/tutorial/2 - deriving.test.ts b/tutorial/2 - deriving.test.ts index afd75d8..5691dd9 100644 --- a/tutorial/2 - deriving.test.ts +++ b/tutorial/2 - deriving.test.ts @@ -13,7 +13,7 @@ export const __YOUR_TURN__ = {} as any; * * There are a couple of ways to do this. */ -describe('deriving', () => { +describe.skip('deriving', () => { /** * In the 'intro' we have created a derivable by using the `.derive()` method. * This method allows the state of that `Derivable` to be used to create a @@ -37,7 +37,7 @@ describe('deriving', () => { */ // We can combine txt with `repeat$.get()` here. - const lyric$ = text$.derive(txt => txt /* __YOUR_TURN__ */ ); + const lyric$ = text$.derive(txt => txt /* __YOUR_TURN__ */); expect(lyric$.get()).toEqual(`It won't be long`); diff --git a/tutorial/3 - reacting.test.ts b/tutorial/3 - reacting.test.ts index 8a24172..162d4d3 100644 --- a/tutorial/3 - reacting.test.ts +++ b/tutorial/3 - reacting.test.ts @@ -11,7 +11,7 @@ export const __YOUR_TURN__ = {} as any; * In the intro we have seen a basic usage of the `.react()` method. * Let's dive a bit deeper into the details of this method. */ -describe('reacting', () => { +describe.skip('reacting', () => { // For easy testing we can count the number of times a reactor was called, let wasCalledTimes: number; // and record the last value it reacted to. diff --git a/tutorial/4 - inner workings.test.ts b/tutorial/4 - inner workings.test.ts index 33fbfe2..9daf82b 100644 --- a/tutorial/4 - inner workings.test.ts +++ b/tutorial/4 - inner workings.test.ts @@ -12,7 +12,6 @@ export const __YOUR_TURN__ = {} as any; * Time to dive a bit deeper into the inner workings of `@skunkteam/sherlock`. */ describe.skip('inner workings', () => { -describe('inner workings', () => { /** * What if there is a derivation that reads from one of two `Derivable`s * dynamically? Will both of those `Derivable`s be tracked for changes? diff --git a/tutorial/9 - expert.test.ts b/tutorial/9 - expert.test.ts index 3612099..4f6b6fb 100644 --- a/tutorial/9 - expert.test.ts +++ b/tutorial/9 - expert.test.ts @@ -2,7 +2,8 @@ import { DerivableAtom, atom, derive } from '@skunkteam/sherlock'; import { derivableCache } from '@skunkteam/sherlock-utils'; /** - * **Your Turn** + * ** Your Turn ** + * * If you see this variable, you should do something about it. :-) */ export const __YOUR_TURN__ = {} as any; @@ -10,8 +11,9 @@ export const __YOUR_TURN__ = {} as any; describe.skip('expert', () => { describe('`.autoCache()`', () => { /** - * If a `.get()` is called on a `Derivable` all derivations will be executed. - * But what if a `Derivable` is used multiple times in another `Derivable`. + * If a `.get()` is called on a `Derivable` all derivations will be + * executed. But what if a `Derivable` is used multiple times in another + * `Derivable`? */ it('multiple executions', () => { const hasDerived = jest.fn(); @@ -23,25 +25,35 @@ describe.skip('expert', () => { ); /** - * **Your Turn** - * `hasDerived` is used in the first derivation. But has it been called at this point? + * ** Your Turn ** + * + * `hasDerived` is used in the first derivation. But has it been + * called at this point? */ - expect(hasDerived).not.toHaveBeenCalledTimes(__YOUR_TURN__); + + // `.toHaveBeenCalled()` or `.not.toHaveBeenCalled()`? ↴ + expect(hasDerived) /* Your Turn */; mySecondDerivation$.get(); /** - * **Your Turn** - * Now that we have gotten `mySecondDerivation$`, which calls `.get()` on the first multiple times. - * How many times has the first `Derivable` actually executed it's derivation? + * ** Your Turn ** + * + * Now that we have gotten `mySecondDerivation$`, which calls + * `.get()` on the first multiple times. How many times has the + * first `Derivable` actually executed its derivation? */ - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); // how many times? + // how many times? + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); }); /** - * So when a `Derivable` is reacting the value is cached and can be gotten from cache. - * But if this `Derivable` is used multiple times in a row, even in another derivation it isn't cached. - * To fix this issue, `.autoCache()` exists. It will cache the `Derivable`s value until the next Event Loop `tick`. + * So when a `Derivable` is reacting the value is cached and can be + * gotten from cache. But if this `Derivable` is used multiple times in + * a row, even in another derivation it isn't cached. + * + * To fix this issue, `.autoCache()` exists. It will cache the + * `Derivable`s value until the next Event Loop `tick`. * * So let's try the example above with this feature */ @@ -50,8 +62,10 @@ describe.skip('expert', () => { const secondHasDerived = jest.fn(); /** - * **Your Turn** - * Use `.autoCache()` on one of the `Derivable`s below. To make the expectations pass. + * ** Your Turn ** + * + * Use `.autoCache()` on one of the `Derivable`s below. To make the + * expectations pass. */ const myAtom$ = atom(true); const myFirstDerivation$ = myAtom$.derive(firstHasDerived); @@ -80,38 +94,43 @@ describe.skip('expert', () => { expect(secondHasDerived).toHaveBeenCalledTimes(2); /** - * Notice that the first `Derivable` has only been executed once, even though the second `Derivable` executed twice. - * Now we wait a tick + * Notice that the first `Derivable` has only been executed once, + * even though the second `Derivable` executed twice. + * + * Now we wait a tick for the cache to be invalidated. */ await new Promise(r => setTimeout(r, 1)); - firstHasDerived.mockClear(); - secondHasDerived.mockClear(); - - mySecondDerivation$.get(); - /** - * **Your Turn** + * ** Your Turn ** + * * Now what do you expect? */ + mySecondDerivation$.get(); + // first after last .get() - expect(firstHasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); // How many times was it called? + expect(firstHasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); // second after last .get() - expect(secondHasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); // How many times was it called? + expect(secondHasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); }); }); /** - * Some `Derivable`s need an input to be calculated. If this `Derivable` is async or has a big setup process, - * you may still want to create it only once, even if the `Derivable` is requested more than once for the same resource. + * Some `Derivable`s need an input to be calculated. If this `Derivable` is + * async or has a big setup process, you may still want to create it only + * once, even if the `Derivable` is requested more than once for the same + * resource. * - * Let's imagine a `stockPrice$(stock: string)` function, which returns a `Derivable` with the current price for the given stock. - * This `Derivable` is async, since it will try to retrieve the current price on a distant server. + * Let's imagine a `stockPrice$(stock: string)` function, which returns a + * `Derivable` with the current price for the given stock. This `Derivable` + * is async, since it will try to retrieve the current price on a distant + * server. * * Let's see what can go wrong first, and we will try to fix it after that. * - * *Note that a `Derivable` without an input is (hopefully) created only once, so it does not have this problem* + * *Note that a `Derivable` without an input is (hopefully) created only + * once, so it does not have this problem* */ describe('`derivableCache`', () => { type Stocks = 'GOOGL' | 'MSFT' | 'APPL'; @@ -126,17 +145,17 @@ describe.skip('expert', () => { }); function reactor(v: any) { - // TODO: add stopper function in type definition? reactSpy(v); } /** - * If the function to create the `Derivable` is called multiple times, the `Derivable` will be created multiple times. - * Any setup this `Derivable` does, will be executed every time. + * If the function to create the `Derivable` is called multiple times, + * the `Derivable` will be created multiple times. Any setup this + * `Derivable` does, will be executed every time. */ it('multiple setups', () => { - // To not make things difficult with `unresolved` - // for this example, imagine we get a response synchronously + // To not make things difficult with `unresolved` for this example, + // imagine we get a response synchronously stockPrice$ = jest.fn(_ => atom(1079.11)); const html$ = derive( @@ -153,9 +172,12 @@ describe.skip('expert', () => { expect(reactSpy).toHaveBeenCalledOnce(); /** - * **Your Turn** - * The `Derivable` is connected and has emitted once, but in that value the 'GOOGL' stockprice was displayed twice. - * We know that using a `Derivable` twice in a connected `Derivable` will make the second `.get()` use a cached value. + * ** Your Turn ** + * + * The `Derivable` is connected and has emitted once, but in that + * value the 'GOOGL' stockprice was displayed twice. We know that + * using a `Derivable` twice in a connected `Derivable` will make + * the second `.get()` use a cached value. * * But does that apply here? * How many times has the setup run, for the price `Derivable`. @@ -170,11 +192,13 @@ describe.skip('expert', () => { */ describe('setup inside a derivation', () => { /** - * When the setup of a `Derivable` is done inside the same derivation as where `.get()` is called. - * You may be creating some problems. + * When the setup of a `Derivable` is done inside the same + * derivation as where `.get()` is called. You may be creating some + * problems. */ it('unresolveable values', () => { - // First setup an `Atom` with the company we are currently interested in + // First setup an `Atom` with the company we are currently + // interested in const company$ = atom('GOOGL'); // Based on that `Atom` we derive the stockPrice @@ -182,69 +206,97 @@ describe.skip('expert', () => { price$.react(reactor); - // Because the stockPrice is still `unresolved` the reactor should not have emitted anything yet + // Because the stockPrice is still `unresolved` the reactor + // should not have emitted anything yet expect(reactSpy).not.toHaveBeenCalled(); // Now let's increase the price - // First we have to get the atom that was given by the `stockPrice$` stub + // First we have to get the atom that was given by the + // `stockPrice$` stub const googlPrice$ = stockPrice$.mock.results[0].value as DerivableAtom; + // Check if it is the right `Derivable` expect(googlPrice$.connected).toEqual(true); + expect(googlPrice$.value).toEqual(undefined); // Then we set the price googlPrice$.set(1079.11); /** - * **Your Turn** + * ** Your Turn ** + * * So the value was increased. What do you think happened? */ + + // How often was the reactor on price$ called? expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(reactSpy).toHaveBeenCalledWith(__YOUR_TURN__, expect.toBeFunction()); // And how many times did the setup run? expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); + + // What's the value of price$ now? + expect(price$.value).toEqual(__YOUR_TURN__); + + // And the value of googlPrice$? + expect(googlPrice$.value).toEqual(__YOUR_TURN__); + + // Is googlPrice$ still even driving any reactors? expect(googlPrice$.connected).toEqual(__YOUR_TURN__); /** * Can you explain this behavior? * * Thought about it? Here is what happened: - * - Initially `stockPrice$('GOOGL')` emits a `Derivable` (`googlPrice$`), which is unresolved - * - Inside the `.derive()` we subscribe to updates on that `Derivable` - * - When `googlPrice$` emits a new value, the `.derive()` step is run again - * - Inside this step, the setup is run again and a new `Derivable` (`newGooglPrice$`) is created and subscribed to - * - Unsubscribing from the old `googlPrice$` + * - Initially `stockPrice$('GOOGL')` emits a `Derivable` + * (`googlPrice$`), which is unresolved. + * - Inside the `.derive()` we subscribe to updates on that + * `Derivable`. + * - When `googlPrice$` emits a new value, the `.derive()` step + * is run again. + * - Inside this step, the setup is run again and a new + * `Derivable` (`newGooglPrice$`) is created and subscribed + * to. + * - Unsubscribing from the old `googlPrice$`. * - * This `newGooglPrice$` is newly created and `unresolved` again. So the end result is an `unresolved` `price$` `Derivable`. + * This `newGooglPrice$` is newly created and `unresolved` + * again. So the end result is an `unresolved` `price$` + * `Derivable`. */ - }); - /** - * **Bonus** - * - * The problem above can be fixed without a `derivableCache`. - * If we split the `.derive()` step into two steps, where the first does the setup, and the second unwraps the `Derivable` created in the first. - * This way, a newly emitted value from the created `Derivable` will not run the setup again and everything should work as expected. - * - * **Your Turn** - * - * *Hint: there is even an `unwrap` helper function for just such an occasion, try it!* - */ + /** + * ** BONUS ** + * + * The problem above can be fixed without a `derivableCache`. + * If we split the `.derive()` step into two steps, where the + * first does the setup, and the second unwraps the `Derivable` + * created in the first. This way, a newly emitted value from + * the created `Derivable` will not run the setup again and + * everything should work as expected. + * + * ** Your Turn ** + * + * *Hint: there is even an `unwrap` helper function for just + * such an occasion, try it!* + */ + }); /** - * But even when you split the setup and the `unwrap`, you may not be out of the woods yet! - * This is actually a problem that most libraries have a problem with, if not properly accounted for. + * But even when you split the setup and the `unwrap`, you may not + * be out of the woods yet! This is actually a problem that most + * libraries have a problem with, if not properly accounted for. */ it('uncached Derivables', () => { - // First we setup an `Atom` with the company we are currently interested in - // This time we support multiple companies, though + // First we setup an `Atom` with the company we are currently + // interested in. This time we support multiple companies though const companies$ = atom(['GOOGL']); // Based on that `Atom` we derive the stockPrices const prices$ = companies$ /** - * There is no need derive anything here, so we use `.map()` on `companies$` - * And since `companies` is an array of strings, we `.map()` over that array to create an array of `Derivable`s + * There is no need derive anything here, so we use `.map()` + * on `companies$`. And since `companies` is an array of + * strings, we `.map()` over that array to create an array + * of `Derivable`s. */ .map(companies => companies.map(company => stockPrice$(company))) // Then we get the prices from the created `Derivable`s in a separate step @@ -252,11 +304,14 @@ describe.skip('expert', () => { prices$.react(reactor); - // Because we use `.value` instead of `.get()` the reactor should emit immediately, this time - expect(reactSpy).toHaveBeenCalledExactlyOnceWith([undefined]); // But it should emit `undefined` // TODO: Might also need the expect.toBeFunction() generic. + // Because we use `.value` instead of `.get()` the reactor + // should emit immediately this time. - // Now let's increase the price - // First we have to get the atom that was given by the `stockPrice$` stub + // But it should emit `undefined`. + expect(reactSpy).toHaveBeenCalledExactlyOnceWith([undefined]); + + // Now let's increase the price. First we have to get the atom + // that was given by the `stockPrice$` stub: const googlPrice$ = stockPrice$.mock.results[0].value as DerivableAtom; // Check if it is the right `Derivable` expect(googlPrice$.connected).toBe(true); @@ -265,44 +320,55 @@ describe.skip('expert', () => { googlPrice$.set(1079.11); /** - * **Your Turn** + * ** Your Turn ** + * * So the value was increased. What do you think happened now? */ expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(reactSpy).toHaveBeenCalledWith([__YOUR_TURN__], expect.toBeFunction()); + expect(reactSpy).toHaveBeenLastCalledWith([__YOUR_TURN__]); /** - * So that worked, now let's try and add another company to the list + * So that worked, now let's try and add another company to the + * list. */ companies$.swap(current => [...current, 'APPL']); expect(companies$.get()).toEqual(['GOOGL', 'APPL']); /** - * **Your Turn** - * With both 'GOOGL' and 'APPL' in the list, what do we expect as an output? + * ** Your Turn ** + * + * With both 'GOOGL' and 'APPL' in the list, what do we expect + * as an output? + * * We had a price for 'GOOGL', but not for 'APPL'... */ expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(reactSpy).toHaveBeenCalledWith([__YOUR_TURN__, __YOUR_TURN__], expect.toBeFunction()); + expect(reactSpy).toHaveBeenCalledWith([__YOUR_TURN__, __YOUR_TURN__]); }); }); + /** - * So we know a couple of problems that can arise, but how do we fix them. + * So we know a couple of problems that can arise, but how do we fix + * them. */ describe('a solution', () => { /** * Let's try putting `stockPrice$` inside a `derivableCache`. - * `derivableCache` requires a `derivableFactory`, this specifies the setup for a given key. + * `derivableCache` requires a `derivableFactory`, this specifies + * the setup for a given key. + * * We know the key, and what to do with it, so let's try it! */ const priceCache$ = derivableCache((company: Stocks) => stockPrice$(company)); /** - * *Note that from this point forward we use `priceCache$` where we used to use `stockPrice$` directly* + * *Note that from this point forward we use `priceCache$` where we + * used to use `stockPrice$` directly* */ it('should fix everything :-)', () => { - // First setup an `Atom` with the company we are currently interested in + // First setup an `Atom` with the company we are currently + // interested in const companies$ = atom(['GOOGL']); const html$ = companies$.derive(companies => @@ -319,19 +385,22 @@ describe.skip('expert', () => { expect(html$.connected).toEqual(true); expect(reactSpy).toHaveBeenCalledOnce(); - // Convenience function to return the first argument of the last call to the reactor + // Convenience function to return the first argument of the last + // call to the reactor function lastEmittedHTMLs() { - // TODO: lastCall may be undefined, but it might be fine here. return reactSpy.mock.lastCall[0]; } - // The last call, should have the array of HTML's as first argument + // The last call, should have the array of HTML's as first + // argument expect(lastEmittedHTMLs()[0]).toContain('$ unknown'); /** - * **Your Turn** + * ** Your Turn ** + * * The `Derivable` is connected and has emitted once. - * The price for the given company 'GOOGL' is displayed twice, just as in the first test. + * The price for the given company 'GOOGL' is displayed twice, + * just as in the first test. * * Has anything changed, by using the `derivableCache`? */ @@ -341,8 +410,11 @@ describe.skip('expert', () => { stockPrice$.mock.results[0].value.set(1079.11); /** - * **Your Turn** - * Last time this caused the setup to run again, resolving to `unresolved` yet again. + * ** Your Turn ** + * + * Last time this caused the setup to run again, resolving to + * `unresolved` yet again. + * * What happens this time? Has the setup run again? */ expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); @@ -354,15 +426,18 @@ describe.skip('expert', () => { companies$.swap(current => [...current, 'APPL']); /** - * **Your Turn** - * Now the `stockPrice$` function should have at least run again for 'APPL'. + * ** Your Turn ** + * + * Now the `stockPrice$` function should have at least run again + * for 'APPL'. + * * But did it calculate 'GOOGL' again too? */ expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - // The first should be 'GOOGL' + // The first should be the generated HTML for 'GOOGL'. expect(lastEmittedHTMLs()[0]).toContain(__YOUR_TURN__); - // The first should be 'APPL' + // The second should be the generated HTML for 'APPL'. expect(lastEmittedHTMLs()[1]).toContain(__YOUR_TURN__); }); }); From eef2e333aebbf81a865e4b7f57f959d0bad5c1d4 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 16 Jul 2024 15:19:02 +0200 Subject: [PATCH 23/30] Added lots of content to the tutorials --- .vscode/settings.json | 4 +- README.md | 2 + tutorial/1 - intro.test.ts | 13 +- tutorial/2 - deriving.test.ts | 34 ++-- tutorial/3 - reacting.test.ts | 178 ++++++++++++++--- tutorial/4 - inner workings.test.ts | 61 +++--- tutorial/5 - unresolved.test.ts | 33 ++-- tutorial/6 - errors.test.ts | 260 +++++++++++++++++++++++- tutorial/7 - utils.test.ts | 297 ++++++++++++++++++++++++---- tutorial/8 - advanced.test.ts | 119 +++++++---- tutorial/9 - expert.test.ts | 59 +++--- 11 files changed, 859 insertions(+), 201 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 7e4750a..6e33d9d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,8 +2,8 @@ "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.organizeImports": true, - "source.fixAll": true + "source.organizeImports": "explicit", + "source.fixAll": "explicit" }, "json.schemas": [ { diff --git a/README.md b/README.md index 9ae9c59..47c5448 100644 --- a/README.md +++ b/README.md @@ -185,3 +185,5 @@ _Coming soon_ ### Cyclic reactors _Coming soon_ + +TODO: FIX THE README! diff --git a/tutorial/1 - intro.test.ts b/tutorial/1 - intro.test.ts index c7671f5..58a6fac 100644 --- a/tutorial/1 - intro.test.ts +++ b/tutorial/1 - intro.test.ts @@ -31,7 +31,8 @@ describe('intro', () => { --- Welcome to the tutorial! --- - Please look in \`./tutorial/1 - intro.ts\` to see what to do next.`, () => { + Please look in \`./tutorial/1 - intro.test.ts\` to see what to do next.`, () => { + // TODO: // At the start of the spec, there will be some setup. let bool = false; @@ -43,7 +44,7 @@ describe('intro', () => { * This can also be indicated with the `__YOUR_TURN__` variable. * * It should be clear what to do here... */ - bool = __YOUR_TURN__; + bool = true; expect(bool).toBeTrue(); // We use expectations like this to verify the result. }); @@ -55,7 +56,7 @@ describe('intro', () => { * ** Your Turn ** * Remove the `.skip` so this part of the tutorial will run. */ -describe.skip('the basics', () => { +describe('the basics', () => { /** * The `Atom` is the basic building block of `@skunkteam/sherlock`. * It holds a value which you can `get()` and `set()`. @@ -71,7 +72,7 @@ describe.skip('the basics', () => { // the `Atom`. expect(myValue$.get()).toEqual(1); - // ** Your Turn ** + myValue$.set(2); // Use the `.set()` method to change the value of the `Atom`. expect(myValue$.get()).toEqual(2); }); @@ -97,7 +98,7 @@ describe.skip('the basics', () => { * negative to a positive number and vice versa) of the original `Atom`. */ // Use `myValue$.derive(val => ...)` to implement `myInverse$`. - const myInverse$ = myValue$.derive(__YOUR_TURN__ => __YOUR_TURN__); + const myInverse$ = myValue$.derive(val => -val); expect(myInverse$.get()).toEqual(-1); // So if we set `myValue$` to -2: myValue$.set(-2); @@ -122,7 +123,7 @@ describe.skip('the basics', () => { * * Now react to `myCounter$`. In every `react()`. * Increase the `reacted` variable by one. */ - myCounter$.react(() => __YOUR_TURN__); + myCounter$.react(() => reacted++); expect(reacted).toEqual(1); // `react()` will react immediately, more on that later. diff --git a/tutorial/2 - deriving.test.ts b/tutorial/2 - deriving.test.ts index 5691dd9..1a51956 100644 --- a/tutorial/2 - deriving.test.ts +++ b/tutorial/2 - deriving.test.ts @@ -13,7 +13,7 @@ export const __YOUR_TURN__ = {} as any; * * There are a couple of ways to do this. */ -describe.skip('deriving', () => { +describe('deriving', () => { /** * In the 'intro' we have created a derivable by using the `.derive()` method. * This method allows the state of that `Derivable` to be used to create a @@ -37,7 +37,7 @@ describe.skip('deriving', () => { */ // We can combine txt with `repeat$.get()` here. - const lyric$ = text$.derive(txt => txt /* __YOUR_TURN__ */); + const lyric$ = text$.derive(txt => txt.repeat(repeat$.get())); expect(lyric$.get()).toEqual(`It won't be long`); @@ -74,12 +74,14 @@ describe.skip('deriving', () => { */ // Should return 'Fizz' when `myCounter$` is a multiple of 3 and '' otherwise. - const fizz$: Derivable = myCounter$.derive(__YOUR_TURN__); + const fizz$: Derivable = myCounter$.derive(v => (v % 3 === 0 ? 'Fizz' : '')); // Should return 'Buzz' when `myCounter$` is a multiple of 5 and '' otherwise. - const buzz$: Derivable = myCounter$.derive(__YOUR_TURN__); + const buzz$: Derivable = myCounter$.derive(v => (v % 5 === 0 ? 'Buzz' : '')); - const fizzBuzz$: Derivable = derive(__YOUR_TURN__); + const fizzBuzz$: Derivable = derive( + () => (fizz$.get() + buzz$.get() === '' ? myCounter$.get() : fizz$.get() + buzz$.get()), // TODO: why not put it on the counter then? + ); expect(fizz$.get()).toEqual(''); expect(buzz$.get()).toEqual(''); @@ -153,9 +155,9 @@ describe.skip('deriving', () => { const tweetCount = pastTweets.length; const lastTweet = pastTweets[tweetCount - 1]; - expect(tweetCount).toEqual(__YOUR_TURN__); // Is there a new tweet? - expect(lastTweet).toContain(__YOUR_TURN__); // Who sent it? Donald? Or Barack? - expect(lastTweet).toContain(__YOUR_TURN__); // What did he tweet? + expect(tweetCount).toEqual(3); // Is there a new tweet? + expect(lastTweet).toContain('Donald'); // Who sent it? Donald? Or Barack? + expect(lastTweet).toContain('race'); // What did he tweet? /** * As you can see, this is something to look out for. @@ -200,22 +202,22 @@ describe.skip('deriving', () => { */ const fizz$ = myCounter$ .derive(count => count % 3) - .is(__YOUR_TURN__) - .and(__YOUR_TURN__) - .or(__YOUR_TURN__) as Derivable; + .is(0) + .and('Fizz') + .or(''); const buzz$ = myCounter$ .derive(count => count % 5) - .is(__YOUR_TURN__) - .and(__YOUR_TURN__) - .or(__YOUR_TURN__) as Derivable; + .is(0) + .and('Buzz') + .or(''); - const fizzBuzz$ = derive(() => fizz$.get() + buzz$.get()).or(__YOUR_TURN__); + const fizzBuzz$ = derive(() => fizz$.get() + buzz$.get()).or(myCounter$); // TODO: for (let count = 1; count <= 100; count++) { // Set the value of the `Atom`, myCounter$.set(count); - + // console.log(myCounter$.get() + ', ' + fizzBuzz$.get()); // and check if the output changed accordingly. checkFizzBuzz(count, fizzBuzz$.get()); } diff --git a/tutorial/3 - reacting.test.ts b/tutorial/3 - reacting.test.ts index 162d4d3..d7dbd05 100644 --- a/tutorial/3 - reacting.test.ts +++ b/tutorial/3 - reacting.test.ts @@ -11,7 +11,7 @@ export const __YOUR_TURN__ = {} as any; * In the intro we have seen a basic usage of the `.react()` method. * Let's dive a bit deeper into the details of this method. */ -describe.skip('reacting', () => { +describe('reacting', () => { // For easy testing we can count the number of times a reactor was called, let wasCalledTimes: number; // and record the last value it reacted to. @@ -69,10 +69,13 @@ describe.skip('reacting', () => { * Time to react to `myAtom$` with the `reactor()` function defined * above. */ + myAtom$.react((val, _) => reactor(val)); + // myAtom$.react(reactor); // OR this. TS will ignore any additional arguments you might give it. expectReact(1, 'initial value'); // Now set a 'new value' to `myAtom$`. + myAtom$.set('new value'); expectReact(2, 'new value'); }); @@ -99,8 +102,13 @@ describe.skip('reacting', () => { * * catch the returned `stopper` in a variable */ - myAtom$.react(reactor); + // let stopFunc: () => void = () => {}; // dummy initial value + // myAtom$.react((val, stop) => { + // reactor(val); + // stopFunc = stop; + // }); + const stopFunc = myAtom$.react((val, _) => reactor(val)); expectReact(1, 'initial value'); /** @@ -108,6 +116,7 @@ describe.skip('reacting', () => { * * Call the `stopper`. */ + stopFunc(); myAtom$.set('new value'); @@ -130,9 +139,9 @@ describe.skip('reacting', () => { * In the reaction below, use the stopper callback to stop the * reaction */ - myAtom$.react((val, __YOUR_TURN___) => { + myAtom$.react((val, stop) => { reactor(val); - __YOUR_TURN___; + stop(); }); expectReact(1, 'initial value'); @@ -185,7 +194,7 @@ describe.skip('reacting', () => { * * Try giving `boolean$` as `until` option. */ - string$.react(reactor, __YOUR_TURN__); + string$.react(reactor, { until: boolean$ }); // It should react directly as usual. expectReact(1, 'Value'); @@ -233,7 +242,7 @@ describe.skip('reacting', () => { * Use `!string$.get()` to return `true` when the `string` is * empty. */ - string$.react(reactor, __YOUR_TURN__); + string$.react(reactor, { until: () => !string$.get() }); // It should react as usual: string$.set('New value'); @@ -250,9 +259,8 @@ describe.skip('reacting', () => { /** * Since the example above where the `until` is based on the parent - * `Derivable` occurs very frequently. - * - * This `Derivable` is given as a parameter to the `until` function. + * `Derivable` occurs very frequently, this `Derivable` is given as + * a parameter to the `until` function. */ it('the parent `Derivable`', () => { /** @@ -261,7 +269,7 @@ describe.skip('reacting', () => { * Try using the first parameter of the `until` function to do * the same as above. */ - string$.react(reactor, __YOUR_TURN__); + string$.react(reactor, { until: s => !s.get() }); // It should react as usual. string$.set('New value'); @@ -276,6 +284,31 @@ describe.skip('reacting', () => { // was never given to the reactor: expectReact(3, 'Newer Value'); }); + + /** + * Sometimes, the syntax may leave you confused. + */ + it('syntax issues', () => { + // It looks this will start reacting until `boolean$`s value is false... + let stopper = boolean$.react(reactor, { until: b => !b }); + + // ...but does it? (Remember: `boolean$` starts out as `false`) + expect(boolean$.connected).toBe(__YOUR_TURN__); + + // The `b` it obtains as argument is a `Derivable`. This is a + // reference value which will evaluate to `true` as it is not `undefined`. + // Thus, the negation will evaluate to `false`, independent of the value of + // the boolean. You can get the boolean value our of the `Derivable` using `.get()`: + stopper(); + stopper = boolean$.react(reactor, { until: b => !b.get() }); + expect(boolean$.connected).toBe(__YOUR_TURN__); + + // You can also return the `Derivable` and apply the negation + // with the method designed for it: + stopper(); + boolean$.react(reactor, { until: b => b.not() }); + expect(boolean$.connected).toBe(__YOUR_TURN__); + }); }); /** @@ -291,7 +324,7 @@ describe.skip('reacting', () => { * the parent derivable as first parameter when it's called.) * * * Note: when using `from`, `.react()` will (most often) not react - * synchronously any more. As that is the function of this option.* + * synchronously any more. As that is the function of this option.* // TODO: word differently... is not a `note`, but the intended effect. */ it('reacting `from`', () => { const sherlock$ = atom(''); @@ -305,7 +338,7 @@ describe.skip('reacting', () => { * * *Hint: remember the `.is()` method from tutorial 2?* */ - sherlock$.react(reactor, __YOUR_TURN__); + sherlock$.react(reactor, { from: sherlock$.is('dear') }); expectReact(0); ['Elementary,', 'my', 'dear', 'Watson'].forEach(txt => sherlock$.set(txt)); @@ -321,9 +354,6 @@ describe.skip('reacting', () => { * Where `until` and `from` can only be triggered once to stop or start * reacting, `when` can be flipped as often as you like and the reactor * will respect the current state of the `when` function/Derivable. - * - * *Note: as with `from` this can prevent `.react()` from reacting - * synchronously.* */ it('reacting `when`', () => { const count$ = atom(0); @@ -334,7 +364,10 @@ describe.skip('reacting', () => { * Now, let's react to all even numbers. * Except 4, we don't want to make it too easy now. */ - count$.react(reactor, __YOUR_TURN__); + count$.react(reactor, { when: v => v.get() % 2 === 0 && v.is(4).not() }); + count$.react(reactor, { when: v => v.get() % 2 === 0 && v.get() !== 4 }); + // TODO: why can I apply `&&` to `number` and Derivable?? + // >>> e.g. `when` kan zowel booleans and Derivable vanwege Unwrappable type xD expectReact(1, 0); @@ -359,9 +392,26 @@ describe.skip('reacting', () => { /** * ** Your Turn ** * - * Say you want to react when `done$` is true. But not right away.. + * Say you want to react when `done$` is true. But not right away.. // TODO: change to use number? */ - done$.react(reactor, __YOUR_TURN__); + done$.react(reactor, { when: d => d.is(true) }); // TODO: true expected answer given description: the test case needs asjustment! + // SKIPFIRST negeert de eerste keer dat WHEN true is! Niet de eerste keer in general. + // `// Doesn't react, because the new value equals the previous value that was seen by the reactor.` + // libs/sherlock/src/lib/reactor/reactor.test.ts:136 + // Hij accepteert alleen waardes die anders zijn dan zijn huidige. Omdat hij alleen `true` accepteert, kan hij nooit meer updaten! + // => false accepteert de `when` niet; + // => true is zelfde als voorheen. + // Ik denk dat hij, ondanks dat `skipFirst` de eerste true genegeerd heeft, hij hem wel onthouden heeft als last seen value. Expected! + // Zie libs/sherlock/src/lib/derivable/mixins/take.ts voor volgorde van events? + // Als je `events` wilt, kan je beter Observables ofzo gebruiken. Je wilt dit patroon van "elke keer dat je true ziet, pas aan" eigenlijk niet hier. + // kan beter numbers gebruiken om dit te testen! `<= 4` ofzo + // En extra testje hiervoor! + expectReact(0); + + done$.set(true); + expectReact(0); + + done$.set(false); expectReact(0); done$.set(true); @@ -387,7 +437,8 @@ describe.skip('reacting', () => { * * *Hint: you will need to combine `once` with another option* */ - finished$.react(reactor, __YOUR_TURN__); + finished$.react(reactor, { once: true, when: f => f.get() }); // TODO: make sure the test captures the diff between `f` and `f.get()` here! + // see next `challenge` for a case where there is a difference. expectReact(0); // When finished it should react once. @@ -401,9 +452,71 @@ describe.skip('reacting', () => { }); }); + describe('order of execution', () => { + // the interactions between `from`, `until`, `when`, `skipFirst`, `once`... - that order! + // als het goed is nog niet behandeld (libs/sherlock/src/lib/derivable/mixins/take.ts) + + /** + * The options `from`, `until`, `when`, `skipFirst` and `once` are tested in this specific order: + * 1) firstly, `from` is checked. If `from` is/was true (or is not set in the options), we continue: + * 2) secondly, `until` is checked. If `until` is false (or is not set in the options), we continue: + * 3) thirdly, `when` is checked. If `when` is true (or is not set in the options), we continue: + * 4) fourthly, `skipFirst` is checked. If `skipFirst` is false (or is not set in the options), we continue: + * 5) lastly, `once` is checked. + * + * This means, for example, that `skipFirst` is only checked when `from` is true or unset, `until` is false or unset, + * and `when` is true or unset. If e.g. `when` evaluates to false, `skipFirst` cannot trigger. + */ + it('`from` and `until`', () => { + const myAtom$ = atom(0); + myAtom$.react(reactor, { from: v => v.is(3), until: v => v.is(2) }); + + for (let i = 1; i <= 5; i++) { + myAtom$.set(i); + } + + // the reactor starts reacting when `myAtom` gets the value 3, but stops when it gets the value 2. + // But because `myAtom` obtains the value 2 before it obtains 3... + // ...how many times was the reactor called, if any? + expectReact(__YOUR_TURN__); + }); + + it('`when` and `skipFirst`', () => { + const myAtom$ = atom(0); + myAtom$.react(reactor, { when: v => v.is(1), skipFirst: true }); + + myAtom$.set(1); + + // the reactor reacts when `myAtom` is 1 but skips the first number. + // `myAtom` starts at 0. Does the reactor skip the 0 or the 1? + expectReact(__YOUR_TURN__); + }); + + it('`from`, `until`, `when`, `skipFirst`, and `once`', () => { + const myAtom$ = atom(0); + myAtom$.react(reactor, { + from: v => v.is(5), + until: v => v.is(1), + when: v => [2, 3, 4].includes(v.get()), + skipFirst: true, + once: true, + }); + + for (let v of [1, 2, 3, 5, 4, 3, 2, 1, 2, 3]) { + myAtom$.set(v); + } + + // `from` and `until` allow the reactor to respectively start when `myAtom` has value 5, and stop when it has value 1. + // Meanwhile, `when` allows neither of those values and only allows the values 2, 3, and 4. + // `skipFirst` and `once` are also added, just to bring the whole group together. + // so, how many times is the reactor called, and what was the last argument (if any)? + expectReact(__YOUR_TURN__); + }); + }); + describe('challenge', () => { it('onDisconnect', () => { - const connected$ = atom(false); + const connected$ = atom(false); // TODO: change to use number /** * ** Your Turn ** @@ -413,8 +526,23 @@ describe.skip('reacting', () => { * * `connected$` indicates the current connection status. * This should be possible with three simple ReactorOptions + * Hint: do not use `when`! */ - connected$.react(reactor, __YOUR_TURN__); + connected$.react(reactor, { from: c => c, skipFirst: true, once: true }); // WORKS, and intended + connected$.react(reactor, { from: _ => connected$, skipFirst: true, once: true }); // WORKS, and intended + connected$.react(reactor, { from: connected$, skipFirst: true, once: true }); // WORKS, and intended + + // TODO: + // `when: c => !c.get()` gets the boolean out of the Derivable, applies `not`, and returns + // `when: c => !c` coerces the Derivable to a boolean (whether it exists: true), applies `not` to this boolean, and returns false. + // `when: c => c.not()` takes the boolean out of the Derivable, applies `not`, puts it back in a Derivable, and `when` is overloaded + // ...to also be able to take the boolean out of the Derivable! So that is how you can also pass a Derivable - `when` takes the boolean out! + // connected$.react(reactor, { when: c => !c.get(), from: c => c.get() }); // 1. DOES NOT WORK - the connection is not false afterwards + // connected$.react(reactor, { when: c => !c, from: c => c }); // 2. DOES NOT WORK - see above + // connected$.react(reactor, { when: c => !c.get(), skipFirst: true }); // 3. DOES NOT WORK... + // ...as the first time c is false, this is accepted in the system even though skipfirst is true. Then... + // ...the second time that c is false, it is seen as the same value and thus not accepted (only changes are accepted)! Hence: + // setting a Derivable with a value it already has does not trigger it. It does not even go to `when`. // It starts as 'not connected' expectReact(0); @@ -427,8 +555,14 @@ describe.skip('reacting', () => { connected$.set(false); expectReact(1, false); + // After that, nothing should change anymore. + connected$.set(true); + expectReact(1, false); + connected$.set(false); + expectReact(1, false); + // It should not react again after this. - expect(connected$.connected).toBeFalse; + expect(connected$.connected).toBeFalse(); // * Note: this `.connected` refers to whether this `Derivable` // is being (indirectly) observed by a reactor. }); diff --git a/tutorial/4 - inner workings.test.ts b/tutorial/4 - inner workings.test.ts index 9daf82b..faf51cc 100644 --- a/tutorial/4 - inner workings.test.ts +++ b/tutorial/4 - inner workings.test.ts @@ -11,7 +11,7 @@ export const __YOUR_TURN__ = {} as any; /** * Time to dive a bit deeper into the inner workings of `@skunkteam/sherlock`. */ -describe.skip('inner workings', () => { +describe('inner workings', () => { /** * What if there is a derivation that reads from one of two `Derivable`s * dynamically? Will both of those `Derivable`s be tracked for changes? @@ -43,8 +43,9 @@ describe.skip('inner workings', () => { * * What do you expect? */ - expect(reacted).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(reacted).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + expect(reacted).toHaveBeenCalledTimes(1); + expect(reacted).toHaveBeenLastCalledWith(1, expect.toBeFunction()); // TODO: NIET omdat hij weet dat de string niets verandert, + // maar juist omdat hij dezelfde waarde binnen krijgt en dus niet nogmaals triggered!! // `switch$` is still set to true (number) number$.set(2); @@ -54,8 +55,8 @@ describe.skip('inner workings', () => { * * What do you expect? */ - expect(reacted).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(reacted).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + expect(reacted).toHaveBeenCalledTimes(2); + expect(reacted).toHaveBeenLastCalledWith(2, expect.toBeFunction()); // Now it gets a different value!! // Now let's reset the mock function, so the call count should // be 0 again. @@ -71,8 +72,8 @@ describe.skip('inner workings', () => { * * What do you expect now? */ - expect(reacted).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(reacted).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + expect(reacted).toHaveBeenCalledTimes(1); + expect(reacted).toHaveBeenLastCalledWith('two', expect.toBeFunction()); // It gets a different value than last time again. No need for the reset...? TODO: }); /** @@ -85,7 +86,7 @@ describe.skip('inner workings', () => { const hasDerived = jest.fn(); const myAtom$ = atom(true); - const myDerivation$ = myAtom$.derive(hasDerived); + const myDerivation$ = myAtom$.derive(hasDerived); // NOTE: React causes an immediate update. Derive does not! /** * ** Your Turn ** @@ -98,17 +99,17 @@ describe.skip('inner workings', () => { */ // Well, what do you expect? - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(0); myDerivation$.get(); // And after a `.get()`? - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(1); myDerivation$.get(); // And after the second `.get()`? Is there an extra call? - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(2); /** * The state of any `Derivable` can change at any moment. @@ -147,27 +148,27 @@ describe.skip('inner workings', () => { * * Ok, it's your turn to complete the expectations. */ - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(1); // because of the react. myDerivation$.get(); - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(1); // no update because someone is reacting, and there has been no update in value. myAtom$.set(false); - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(2); // `myDerivation`s value has changed, so update. myDerivation$.get(); - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(2); // no update. stopper(); - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(2); // stopping doesn't change the value... myDerivation$.get(); - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(3); // ...but now, it is not being reacted to, so it goes back to updating every time `.get()` is called! /** * Since the `.react()` already listens to the value(changes) there is @@ -176,6 +177,9 @@ describe.skip('inner workings', () => { * But when the reactor has stopped, the derivation has to be calculated * again. */ + // Okay, clear, but why? + // I see... because we don't want to keep internal states and such and track changes when no-one is listening! It is a waste of effort. + // So we only keep track of changes when a react is listening. }); /** @@ -212,23 +216,23 @@ describe.skip('inner workings', () => { // Note that this is the same value as it was initialized with myAtom$.set(1); - expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(first).toHaveBeenCalledTimes(1); + expect(second).toHaveBeenCalledTimes(1); myAtom$.set(2); - expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(first).toHaveBeenCalledTimes(2); // different INPUT [2], so call again + expect(second).toHaveBeenCalledTimes(1); // same INPUT [false], so no change myAtom$.set(3); - expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(first).toHaveBeenCalledTimes(3); // different INPUT [3] + expect(second).toHaveBeenCalledTimes(2); // different INPUT [true] myAtom$.set(4); - expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(first).toHaveBeenCalledTimes(4); // different INPUT [4] + expect(second).toHaveBeenCalledTimes(2); // same INPUT [true] /** * Can you explain the behavior above? @@ -256,6 +260,7 @@ describe.skip('inner workings', () => { const hasReacted = jest.fn(); atom$.react(hasReacted, { skipFirst: true }); + expect(hasReacted).toHaveBeenCalledTimes(0); // added for clarity, in case people missed the `skipFirst` or its implication atom$.set({}); @@ -265,7 +270,7 @@ describe.skip('inner workings', () => { * The `Atom` is set with exactly the same object as before. Will the * `.react()` fire? */ - expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasReacted).toHaveBeenCalledTimes(1); // not considered equal (`{} !== {}`) /** * But what if you use an object, that can be easily compared through a @@ -284,7 +289,7 @@ describe.skip('inner workings', () => { * * Do you think the `.react()` fired with this new value? */ - expect(hasReacted).toHaveBeenCalledTimes(0); + expect(hasReacted).toHaveBeenCalledTimes(0); // TODO: answer already given - is considered equal atom$.set(Seq.Indexed.of(1, 2)); @@ -293,7 +298,7 @@ describe.skip('inner workings', () => { * * And now? */ - expect(hasReacted).toHaveBeenCalledTimes(1); + expect(hasReacted).toHaveBeenCalledTimes(1); // TODO: answer already given - obviously unequal /** * In `@skunkteam/sherlock` equality is a bit complex: diff --git a/tutorial/5 - unresolved.test.ts b/tutorial/5 - unresolved.test.ts index 998b121..24c9091 100644 --- a/tutorial/5 - unresolved.test.ts +++ b/tutorial/5 - unresolved.test.ts @@ -17,7 +17,7 @@ export const __YOUR_TURN__ = {} as any; * state, called `unresolved`. This indicates that the data is not available * yet, but (probably) will be at some point. */ -describe.skip('unresolved', () => { +describe('unresolved', () => { /** * Let's start by creating an `unresolved` `Derivable`. */ @@ -27,13 +27,14 @@ describe.skip('unresolved', () => { // since it can't be inferred by TypeScript this way. const myAtom$ = atom.unresolved(); - expect(myAtom$.resolved).toEqual(__YOUR_TURN__); + expect(myAtom$.resolved).toEqual(false); /** * ** Your Turn ** * * Resolve the atom, it's pretty easy */ + myAtom$.set(1); expect(myAtom$.resolved).toBeTrue(); }); @@ -48,7 +49,7 @@ describe.skip('unresolved', () => { * * Time to create an `unresolved` Atom.. */ - const myAtom$: DerivableAtom = __YOUR_TURN__; + const myAtom$: DerivableAtom = atom.unresolved(); expect(myAtom$.resolved).toBeFalse(); @@ -63,17 +64,18 @@ describe.skip('unresolved', () => { * * What do you expect? */ - expect(myAtom$.resolved).toEqual(__YOUR_TURN__); + expect(myAtom$.resolved).toEqual(true); // .toThrow() or .not.toThrow()? ↴ - expect(() => myAtom$.get()) /*__YOUR_TURN__*/; + expect(() => myAtom$.get()).not.toThrow(); }); /** * If a `Derivable` is `unresolved` it can't react yet. But it will * `.react()` if a value becomes available. * - * *Note that this can prevent `.react()` from executing immediately* + * *Note that this can prevent `.react()` from executing immediately* // TODO: what annoying messages... I want to change them. + * It is not a 'Note: side-effect' but the expected intended behavior! */ it('reacting to `unresolved`', () => { const myAtom$ = atom.unresolved(); @@ -86,13 +88,14 @@ describe.skip('unresolved', () => { * * What do you expect? */ - expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasReacted).toHaveBeenCalledTimes(0); /** * ** Your Turn ** * * Now make the last expect succeed */ + myAtom$.set(`woohoow, I was called`); expect(myAtom$.resolved).toBeTrue(); expect(hasReacted).toHaveBeenCalledExactlyOnceWith(`woohoow, I was called`, expect.toBeFunction()); @@ -100,7 +103,7 @@ describe.skip('unresolved', () => { /** * In `@skunkteam/sherlock` there is no reason why a `Derivable` should not - * become `unresolved` again after it has been set. + * be able to become `unresolved` again after it has been set. */ it('can become `unresolved` again', () => { const myAtom$ = atom.unresolved(); @@ -112,6 +115,7 @@ describe.skip('unresolved', () => { * * Set the value.. */ + myAtom$.set(`it's alive!`); expect(myAtom$.get()).toEqual(`it's alive!`); @@ -120,6 +124,7 @@ describe.skip('unresolved', () => { * * Unset the value.. (*Hint: TypeScript is your friend*) */ + myAtom$.unset(); expect(myAtom$.resolved).toBeFalse(); }); @@ -140,14 +145,14 @@ describe.skip('unresolved', () => { * * Combine the two `Atom`s into one `Derivable` */ - const myDerivable$: Derivable = __YOUR_TURN__; + const myDerivable$: Derivable = myString$.derive(s => s + myOtherString$.get()); /** * ** Your Turn ** * * Is `myDerivable$` expected to be `resolved`? */ - expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); + expect(myDerivable$.resolved).toEqual(false); // Now let's set one of the two source `Atom`s myString$.set('some'); @@ -157,12 +162,12 @@ describe.skip('unresolved', () => { * * What do you expect to see in `myDerivable$`. */ - expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); + expect(myDerivable$.resolved).toEqual(false); // And what if we set `myOtherString$`? myOtherString$.set('data'); - expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); - expect(myDerivable$.get()).toEqual(__YOUR_TURN__); + expect(myDerivable$.resolved).toEqual(true); + expect(myDerivable$.get()).toEqual('somedata'); /** * ** Your Turn ** @@ -171,6 +176,6 @@ describe.skip('unresolved', () => { * What do you expect `myDerivable$` to be? */ myString$.unset(); - expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); + expect(myDerivable$.resolved).toEqual(false); }); }); diff --git a/tutorial/6 - errors.test.ts b/tutorial/6 - errors.test.ts index 1c72480..e8ed4b4 100644 --- a/tutorial/6 - errors.test.ts +++ b/tutorial/6 - errors.test.ts @@ -1,5 +1,259 @@ -describe.skip('errors', () => { - it('placeholder', () => { - expect(true).toEqual(true); +import { + atom, + DerivableAtom, + error, + ErrorWrapper, + final, + FinalWrapper, + MaybeFinalState, + unresolved, +} from '@skunkteam/sherlock'; +import { finalGetter, makeFinalMethod, setFinalMethod } from 'libs/sherlock/src/lib/derivable/mixins'; + +/** + * ** Your Turn ** + * + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +// In libs/sherlock/src/lib/interfaces.ts:289, the basic states a Derivable can have are shown. +// > `export type State = V | unresolved | ErrorWrapper;` +// A state can be either any type `V` (`number`, `string`, etc.), `unresolved` as we saw in the +// previous tutorial, or `ErrorWrapper`. This last state is explained here. +describe('errors', () => { + let myDerivable: DerivableAtom; + + beforeEach(() => { + myDerivable = atom(1); + }); + + it('basic errors', () => { + // `errored` shows whether the last statement resulted in an error. + // It does NOT show whether the `Derivable` is in an error state. + expect(myDerivable.errored).toBe(false); + expect(myDerivable.error).toBeUndefined; + expect(myDerivable.getState()).toBe(1); // as explained above, any type can be a state + + // We can set errors using the `setError()` function. + myDerivable.setError('my Error'); + + expect(myDerivable.errored).toBe(true); + expect(myDerivable.error).toBe('my Error'); + // The `ErrorWrapper` state only holds an error string. The `error` function returns + // such an `ErrorWrapper` which we can use to compare. + expect(myDerivable.getState()).toMatchObject(error('my Error')); + + // As expected, calling `get()` on `myDerivable` gives an error. + expect(myDerivable.get).toThrow("Cannot read properties of undefined (reading 'getState')"); // TODO: WHAT - normally this works, but internal JEST just fucks with me....? + expect(() => myDerivable.get()).toThrow('my Error'); + expect(myDerivable.errored).toBe(true); + + // ** __YOUR_TURN__ ** + // What will happen if you try to call `set()` on `myDerivable`? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myDerivable.set(2)).not.toThrow(); + // expect(() => myDerivable.set(2)) /* __YOUR_TURN__ */ + // expect(myDerivable.errored).toBe(__YOUR_TURN__); + // `.toBe(2)` or `.toMatchObject(error('my Error'))`? ↴ + expect(myDerivable.getState()).toBe(2); + // expect(myDerivable.getState()) /* __YOUR_TURN__ */ + + // Interestingly, calling `set()` does not throw an error. In fact, it removes the error state + // altogether. This means we can call `get()` again. + expect(() => myDerivable.get()).not.toThrow(); + }); + + it('deriving to an error', () => { + const myDerivable2 = myDerivable.derive(v => v + 1); + + // If the original derivable suddenly errors... + myDerivable.setError('division by zero'); + + // ...what happens to `myDerivable2`? + // `.toBe(2)` or `.toMatchObject(error('division by zero'))`? ↴ + expect(myDerivable2.getState()).toMatchObject(error('division by zero')); + // expect(myDerivable2.getState()) /* __YOUR_TURN__ */ + + // EXPLANATION AND MORE TODO: + }); + + it('reacting to an error', () => { + const doNothing: (v: number) => void = _ => {}; + myDerivable.react(doNothing); + + // ** __YOUR_TURN__ ** + // Will an error be thrown when reacting to a Derivable that throws an error? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myDerivable.setError('my Error')).toThrow('my Error'); + // expect(() => myDerivable.setError('my Error')) + + // Reacting to a Derivable that throws an error will make the reactor throw as well. + // Because the reactor will usually fire when it gets connected, it also throws when + // you try to connect it after the error has already been set. + + myDerivable = atom(1); + myDerivable.setError('my second Error'); + + // ** __YOUR_TURN__ ** + // Will an error be thrown when you use `skipFirst`? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myDerivable.react(doNothing, { skipFirst: true })).toThrow('my second Error'); + // expect(() => myDerivable.react(doSomething, { skipFirst: true })) + + // And will an error be thrown when `from = false`? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myDerivable.react(doNothing, { from: false })).not.toThrow(); + // expect(() => myDerivable.react(doNothing, { from: false })) + + // When `from = false`, the reactor is disconnected, preventing the error message from entering. + // `skipFirst`, on the other hand, does allow the value in, but just does not trigger an update. + // This is similar if you change the boolean afterwards. + + // TODO: This is probably redundant. + let b = false; + expect(() => myDerivable.react(doNothing, { from: b })).not.toThrow(); + expect(() => (b = true)).not.toThrow(); + }); + + // always is `stopOnError` used in a DERIVABLE.TAKE, not a DERIVABLE.REACT...? + // libs/sherlock/src/lib/derivable/mixins/take.tests.ts + // 1034, 825... + + it('`mapState` to reason over errors', () => { + const mapping$ = myDerivable.mapState(state => { + if (state === unresolved) { + return atom('unresolved'); + } + if (state instanceof ErrorWrapper) { + return atom('error'); + } + return atom(myDerivable.get().toString()); + }); + + // You can get the mapped value out by using `.get()`. But then, to check the value of that atom, again `.get()`. + expect(mapping$.get().get()).toBe('1'); + + myDerivable.unset(); + expect(mapping$.get().get()).toBe('unresolved'); + + myDerivable.setError('Just a random error.'); + expect(mapping$.get().get()).toBe('error'); + }); + + it('TEMP', () => { + // FINAL + // libs/sherlock/src/lib/utils/final-wrapper.ts + + // TODO: EXPLAIN WHY YOU WOULD WANT THIS + let myAtom$ = atom(1); + + // every atom has a `final` property. + expect(myAtom$.final).toBeFalse(); + + // you can make an atom final using the `makeFinal()` function. + myAtom$.makeFinal(); + expect(myAtom$.final).toBeTrue(); + + // final atoms cannot be set anymore, but can be get. + expect(() => myAtom$.set(2)).toThrow('cannot set a final derivable'); + expect(() => myAtom$.get()).not.toThrow(); + + // alternatively, you can set a last value before setting it to `final`. + // Obviously, if the state is already `final`, this function will also throw an error. + expect(() => myAtom$.setFinal(2)).toThrow('cannot set a final derivable'); + myAtom$ = atom(1); // reset + myAtom$.setFinal(2); // try again + expect(myAtom$.final).toBeTrue(); + + // Every Derivable has a state. We have seen that states (`State`) can be `undefined`, `ErrorWrapper`, + // or any regular type `V`. Other states exist, such as the `MaybeFinalState`. This state can be either + // a normal state `State` or a special `FinalWrapper>` state. Let's see that in action. + myAtom$ = atom(1); + expect(myAtom$.getMaybeFinalState()).toBe(1); // `getMaybeFinalState` can return a normal state, which in turn can be a normal type + myAtom$.makeFinal(); + expect(myAtom$.getMaybeFinalState()).toBeInstanceOf(FinalWrapper); // but `getMaybeFinalState` can also return a `FinalWrapper` type! + + myAtom$ = atom(1); + // But what is the point of this? What can we do with these "states"? + // You can pattern match on the state to find out what the situation is. + // + // + // FIXME: But no seriously, what is the point of this STATE? You already have the boolean to check for final. + // FIXME: and what is the difference between Constants and Finals? Just that you can SET a final whenever you want? + // then isn't a Final just more powerful than a constant? + // FIXME: and when would you use this, in a real scenario? + // + // + // Let's first define a small checking function as we don't know exactly what type we deal with. + function verifyState(state: MaybeFinalState, value: T, final: boolean): void { + if (state instanceof FinalWrapper) { + expect(final).toBeTrue(); + expect(state.value).toBe(value); + } else { + expect(final).toBeFalse(); + expect(state).toBe(value); + } + } + + let myAtomState$ = myAtom$.getMaybeFinalState(); + verifyState(myAtomState$, 1, false); // the state is the same as my value. + + myAtom$.setFinal(2); + myAtomState$ = myAtom$.getMaybeFinalState(); + verifyState(myAtomState$, 2, true); // the final state still contains my value! + + // + // + // + + const final$ = final(1); + // finals cannot be `set`. See for yourself by uncommenting the next line. + // const final$.value = 2; + // finals can be `get` + expect(final$.value).toBe(1); + + finalGetter; + setFinalMethod; + makeFinalMethod; + // markFinal; + + // const myAtomMaybeFinal$ = myAtom$.getMaybeFinalState(); + // myAtomMaybeFinal$ + + // A normal state is called `State; a final state is `FinalWrapper>`, so a + // `MaybeFinalState` can be either! :: + // export type MaybeFinalState = State | FinalWrapper>; + // export type State = V | unresolved | ErrorWrapper; + + let a: MaybeFinalState = 1; + // a FinalWrapper can be made using the `final` function. + a = final(1); // similar to `atom`, but makes a FinalWrapper instead of an Atom. + // This is just syntactic sugar for: + a = FinalWrapper.wrap(1); + a = FinalWrapper.wrap(a); // this does nothing. + expect(a).toBeInstanceOf(FinalWrapper); + + // You can also use other functions. + a = FinalWrapper.unwrap(a); // does the opposite: get the V out of the FinalWrapper. + expect(a).not.toBeInstanceOf(FinalWrapper); // now it is not a FinalWrapper, but a State! + + // also has its own Map function + a = FinalWrapper.map(1, v => v + 1); + expect(a).toBe(2); + a = FinalWrapper.map(final(1), v => v + 1); + expect(a).toMatchObject(final(2)); }); }); + +/** + * Final States; (finalGetter, finalMethod, getMaybeFinalState, FinalWrapper, MaybeFinalState, _isFinal, makeFinal, markFinal, .final, .finalized, setFinal...) + * Lens; (libs/sherlock/src/lib/derivable/lens.ts) - ??? + * x Lift; (libs/sherlock-utils/src/lib/lift.ts) + * Peek; (libs/sherlock-utils/src/lib/peek.ts) - ??? + * x Template; (libs/sherlock-utils/src/lib/template.ts) - to make a string using a template literal. (Uses unwrap!!) + * / Factory; (libs/sherlock/src/lib/derivable/factories.ts) - simply contains functions to create objects, namely + * lens; atom; constant; derive. + * Flat-map; (libs/sherlock/src/lib/derivable/mixins/flat-map.ts) - ??? + * Fallback-to; + */ diff --git a/tutorial/7 - utils.test.ts b/tutorial/7 - utils.test.ts index 3e49e8e..695aa53 100644 --- a/tutorial/7 - utils.test.ts +++ b/tutorial/7 - utils.test.ts @@ -1,5 +1,7 @@ -import { atom } from '@skunkteam/sherlock'; -import { pairwise, scan, struct } from '@skunkteam/sherlock-utils'; +import { atom, Derivable } from '@skunkteam/sherlock'; +import { lift, pairwise, scan, struct } from '@skunkteam/sherlock-utils'; + +// FIXME: // interne review document, mocht ik iets hebben om te laten zien! In Google Drive, zet het erin! /** * ** Your Turn ** @@ -16,14 +18,14 @@ expect(struct).toBe(struct); /** * In the `sherlock-utils` lib, there are a couple of functions that can combine * multiple values of a single `Derivable` or combine multiple `Derivable`s into - * one. We will show a couple of those here. + * one. We will show a couple of those here. TODO: Hmm, I want to see some others too! */ -describe.skip('utils', () => { +describe('utils', () => { /** * As the name suggests, `pairwise()` will call the given function with both * the current and the previous state. * - * *Note functions like `pairwise` and `scan` can be used with any callback, + * *Note: functions like `pairwise` and `scan` can be used with any callback, * so it can be used both in a `.derive()` step and in a `.react()`* */ it('pairwise', () => { @@ -33,25 +35,26 @@ describe.skip('utils', () => { /** * ** Your Turn ** * - * Now, use `pairwise()`, to subtract the previous value from the + * Now, use `pairwise()` to subtract the previous value from the * current. * * *Hint: check the overloads of pairwise if you're struggling with * `oldVal`.* */ - myCounter$.derive(__YOUR_TURN__).react(reactSpy); + myCounter$.derive(pairwise((newVal, oldVal) => (oldVal ? newVal - oldVal : newVal))).react(reactSpy); + // myCounter$.derive(pairwise((newVal, oldVal) => newVal - oldVal, 0)).react(reactSpy); // OR: alternatively. expect(reactSpy).toHaveBeenCalledExactlyOnceWith(1, expect.toBeFunction()); - myCounter$.set(3); + myCounter$.set(5); expect(reactSpy).toHaveBeenCalledTimes(2); - expect(reactSpy).toHaveBeenCalledWith(2, expect.toBeFunction()); + expect(reactSpy).toHaveBeenCalledWith(4, expect.toBeFunction()); myCounter$.set(45); expect(reactSpy).toHaveBeenCalledTimes(3); - expect(reactSpy).toHaveBeenCalledWith(42, expect.toBeFunction()); + expect(reactSpy).toHaveBeenCalledWith(40, expect.toBeFunction()); }); /** @@ -70,21 +73,22 @@ describe.skip('utils', () => { /** * ** Your Turn ** * - * Now, use `scan()`, to add all the emitted values together + * Now, use `scan()` to subtract the previous value from the + * current. TODO: */ - myCounter$.derive(__YOUR_TURN__).react(reactSpy); + myCounter$.derive(scan((acc, val) => val + acc, 0)).react(reactSpy); expect(reactSpy).toHaveBeenCalledExactlyOnceWith(1, expect.toBeFunction()); - myCounter$.set(3); + myCounter$.set(5); expect(reactSpy).toHaveBeenCalledTimes(2); - expect(reactSpy).toHaveBeenCalledWith(4, expect.toBeFunction()); + expect(reactSpy).toHaveBeenCalledWith(6, expect.toBeFunction()); myCounter$.set(45); expect(reactSpy).toHaveBeenCalledTimes(3); - expect(reactSpy).toHaveBeenCalledWith(49, expect.toBeFunction()); + expect(reactSpy).toHaveBeenCalledWith(51, expect.toBeFunction()); /** * *BONUS: Try using `scan()` (or `pairwise()`) directly in the @@ -92,9 +96,195 @@ describe.skip('utils', () => { */ }); - it.skip('pairwise - BONUS', () => { + // TODO: dit laat niet mooi het verschil zien. Hier lijkt het net alsof ze hetzelfde doen! + // En `scan` naar `val - acc` veranderen werkt niet. Geeft weird gedrag. + + it('scan2', () => { + // const myList$ = atom([]); + const myInt$ = atom(1); + const reactSpy = jest.fn(); + const f: (n1: number, n2: number) => number[] = (newVal, oldVal) => [newVal + oldVal]; + const d: number = 0; + let stopper: () => void; + + myInt$.derive(pairwise(f, d)); + // this is actually the same as: + myInt$.derive(v => pairwise(f, d)(v)); + // it just uses partial application. Pairwise itself is a function after all, which you apply to some value. + // This value then internally has a `previous state` property somewhere. + + // 1) this is one way or writing a derivable + react. + const myList$: Derivable = myInt$.derive(pairwise(f, d)); + stopper = myList$.react(reactSpy); + stopper(); + + // 2) the value of `myList$` is now directly passed to `reactSpy` without it being an extra variable. + // since we can get the value out of the `reactSpy`, this might be all we need. + stopper = myInt$.derive(pairwise(f, d)).react(reactSpy); + stopper(); + + // Let's try it out for real. + // The previous exercise made it seem like `pairwise` and `scan` do similar things. This is not true. + stopper = myInt$.derive(pairwise(f, d)).react(reactSpy); + + myInt$.set(2); + myInt$.set(3); + myInt$.set(4); + + expect(reactSpy).toHaveBeenCalledWith([3 + 4], expect.toBeFunction()); + stopper(); + + // Now let's to the same with scan. Already, the types don't match. + // The return type must be number (uncomment to see error). + // stopper = myInt$.derive(scan(f, d)).react(reactSpy); + // TODO: why? + + const f2: (n1: number, n2: number) => number = (newVal, oldVal) => newVal + oldVal; + stopper = myInt$.derive(scan(f2, d)).react(reactSpy); // starts at 4 + + myInt$.set(2); // then becomes 6 + myInt$.set(3); // then becomes 9 + myInt$.set(4); // lastly, becomes 13 + + expect(reactSpy).toHaveBeenCalledWith(13, expect.toBeFunction()); + stopper(); + + // ------- + // ------- + // ------- + // ------- + + // expect(reactSpy).toHaveBeenCalledExactlyOnceWith(1, expect.toBeFunction()); + + // myCounter$.set(5); + + // expect(reactSpy).toHaveBeenCalledTimes(2); + // expect(reactSpy).toHaveBeenCalledWith(6, expect.toBeFunction()); + + // myCounter$.set(45); + + // expect(reactSpy).toHaveBeenCalledTimes(3); + // expect(reactSpy).toHaveBeenCalledWith(51, expect.toBeFunction()); + + /** + * *BONUS: Try using `scan()` (or `pairwise()`) directly in the + * `.react()` method.* + */ + }); + + it('`pairwise()` on normal arrays', () => { + // Functions like `pairwise()` and `scan()` work on normal lists too. They are often + // used in combination with `map()` and `filter()`. + const myList = [1, 2, 3, 5, 10]; + let myList2: number[]; + + /** + * ** Your Turn ** + * + * Use a `pairwise()` combined with a `map` on `myList` + * to subtract the previous value from the current. + * + * Hint: do not use a lambda function! + */ + + // let oldValue = init; - libs/sherlock-utils/src/lib/pairwise.ts:24 + // Closures?? TODO: + + // myList2 = myList.map(pairwise((newV, oldV) => newV - oldV, 0)); + myList2 = myList.map(__YOUR_TURN__); + expect(myList2).toMatchObject([1, 1, 1, 2, 5]); + + // However, we should be careful with this, as this does not always behave as intended. + myList2 = myList.map(v => __YOUR_TURN__(v)); // copy the same implementation here + expect(myList2).toMatchObject([1, 2, 3, 5, 10]); + + // Even if we are more clear about what we pass, this unintended behavior does not go away. + myList2 = myList.map((v, _, _2) => __YOUR_TURN__(v)); // copy the same implementation here + expect(myList2).toMatchObject([1, 2, 3, 5, 10]); + + // `pairwise()` keeps track of the previous value under the hood. Using a lambda of + // the form `v => pairwise(...)(v)` would create a new `pairwise` function every call, + // essentially resetting the previous value every call. And resetting the previous value + // to 0 causes the input to stay the same (after all: x - 0 = x). + // We can solve this by saving the `pairwise` in a variable and reusing it for every call. + + // let f = pairwise((newV, oldV) => newV - oldV, 0); + let f: (v: number) => number = __YOUR_TURN__; // copy the same implementation here + myList2 = myList.map(v => f(v)); + expect(myList2).toMatchObject([1, 1, 1, 2, 5]); + + // To get more insight in the `pairwise()` function, you can also just call it + // manually. Here, we show what happens under the hood. + + // f = pairwise((newV, oldV) => newV - oldV, 0); + f = pairwise(__YOUR_TURN__); // copy the same implementation here + + myList2 = []; + myList2[0] = f(myList[0]); // f is newly created with `init = 0`, so applies `1 - 0`. + myList2[1] = f(myList[1]); // f has saved `1` internally, so applies `2 - 1`. + myList2[2] = f(myList[2]); // f has saved `2` internally, so applies `3 - 2`. + myList2[3] = f(myList[3]); // f has saved `3` internally, so applies `5 - 3`. + myList2[4] = f(myList[4]); // f has saved `5` internally, so applies `10 - 5`. + + expect(myList2).toMatchObject([1, 1, 1, 2, 5]); + + // This also works for functions other than `map()`, such as `filter()`. + // Use `pairwise()` to filter out all values which produce `1` when subtracted + // with their previous value. + + // myList2 = myList.filter(pairwise((newV, oldV) => newV - oldV === 1, 0)); + myList2 = myList.filter(__YOUR_TURN__); + expect(myList2).toMatchObject([1, 2, 3]); + }); + + it('`scan()` on normal arrays', () => { + // As with `pairwise()` in the last test, `scan()` can be used with arrays too. + const myList = [1, 2, 3, 5, 10]; + let myList2: number[]; + + /** + * ** Your Turn ** + * + * Use a `scan()` combined with a `map` on `myList` + * to subtract the previous value from the current. + * + * Hint: do not use a lambda function! + * TODO: instead, make them write expectancies rather than the implementation. Is way nicer? + */ + + myList2 = myList.map(scan((acc, val) => val - acc, 0)); + // myList2 = myList.map(__YOUR_TURN__); + expect(myList2).toMatchObject([1, 1, 2, 3, 7]); + + // again, it is useful to consider what happens internally. + let f: (v: number) => number = scan((acc, val) => val - acc, 0); + // let f: (v: number) => number = pairwise(__YOUR_TURN__); // copy the same implementation here + + myList2 = []; + myList2[0] = f(myList[0]); // 1 :: f is newly created with `init = 0`, so applies `1 - 0 = 1`. + myList2[1] = f(myList[1]); // 1 :: f has saved the result `1` internally, so applies `2 - 1 = 1`. + myList2[2] = f(myList[2]); // 2 :: f has saved the result `1` internally, so applies `3 - 1 = 2`. + myList2[3] = f(myList[3]); // 3 :: f has saved the result `2` internally, so applies `5 - 2 = 3`. + myList2[4] = f(myList[4]); // 7 :: f has saved the result `3` internally, so applies `10 - 3 = 7`. + + expect(myList2).toMatchObject([1, 1, 2, 3, 7]); + + // This also works for functions other than `map()`, such as `filter()`. + // Use `scan()` to filter out all values which produce `1` when subtracted + // with the previous result. + // TODO: note (earlier) that `scan()` must return the same type as it gets as input. This is required + // as this returned value is also used for the accumulator value for the next call! + + // f = scan((acc, val) => val - acc, 0); + // myList2 = myList.filter(v => f(v) == 1); + f = scan(__YOUR_TURN__); + myList2 = myList.filter(__YOUR_TURN__); + expect(myList2).toMatchObject([1, 2]); // Only the numbers `1` and `2` from `myList` return `1`. + }); + + it('pairwise - BONUS', () => { const myCounter$ = atom(1); - let lastPairwiseResult = 0; + let reactSpy = jest.fn(); /** * ** Your Turn ** @@ -102,48 +292,50 @@ describe.skip('utils', () => { * * Now, use `pairwise()` directly in `.react()`. Implement the same * derivation as before: subtract the previous value from the current. - * - * Instead of returning the computed value, assign it - * `lastPairwiseResult` instead. This is so the implementation can be - * validated. */ - myCounter$.react(__YOUR_TURN__); - expect(lastPairwiseResult).toEqual(1); + reactSpy = jest.fn(pairwise((newV, oldV) => newV - oldV, 0)); + // reactSpy = jest.fn(__YOUR_TURN__); + myCounter$.react(reactSpy); + + expect(reactSpy).toHaveLastReturnedWith(1); myCounter$.set(3); - expect(lastPairwiseResult).toEqual(2); + expect(reactSpy).toHaveLastReturnedWith(2); myCounter$.set(45); - expect(lastPairwiseResult).toEqual(42); + expect(reactSpy).toHaveLastReturnedWith(42); // 45 - 3 (last value of `myCounter$`) }); - it.skip('scan - BONUS', () => { + it('scan - BONUS', () => { const myCounter$ = atom(1); - let lastScanResult = 0; + let reactSpy = jest.fn(); /** * ** Your Turn ** * ** BONUS ** * * Now, use `scan()` directly in `.react()`. Implement the same - * derivation as before: add all the emitted values together. - * - * In addition to returning the computed value, assign it - * `lastScanResult` instead. This is so the implementation can be - * validated. + * derivation as before: subtract all the emitted values. */ - myCounter$.react(__YOUR_TURN__); - expect(lastScanResult).toEqual(1); + reactSpy = jest.fn(scan((acc, val) => val - acc, 0)); + // reactSpy = jest.fn(__YOUR_TURN__); + // NOTE: acc is the last returned value, not the last value of `myCounter$`!! They are not the same! + myCounter$.react(reactSpy); + // TODO: can I also get all reactors within `myCounter$`? + + expect(reactSpy).toHaveLastReturnedWith(1); myCounter$.set(3); - expect(lastScanResult).toEqual(4); + + expect(reactSpy).toHaveLastReturnedWith(2); myCounter$.set(45); - expect(lastScanResult).toEqual(49); + + expect(reactSpy).toHaveLastReturnedWith(43); // 45 - 2 (last returned value) = 43 TODO: show this difference better! }); /** @@ -186,12 +378,37 @@ describe.skip('utils', () => { * expect? */ expect(myOneAtom$.get()).toEqual({ - regularProp: __YOUR_TURN__, - string: __YOUR_TURN__, - number: __YOUR_TURN__, + regularProp: 'new value', // it turns everything in a atom, sure + string: 'my string', // but why does changing the original normal string work?? TODO: does it listen to that actual struct (string) now?? + number: 1, sub: { - string: __YOUR_TURN__, + string: 'my new substring', }, }); }); + + it('lift', () => { + // Derivables can feel like a language build on top of Typescript. Sometimes + // you might want to use normal objects and functions and not have to rewrite + // your code. + // In other words, just like keywords like `atom(V)` lift the type `V` to the higher + // level of Derivables, the `lift(F)` keyword lifts a function `F` to the higher + // level of Derivables. + + // Example: after years of effort, I finally finished my super-long function: + const isEvenNumber = (v: number) => v % 2 == 0; + + // TODO: + // So rewriting this function to work with derivables would be a waste of time. + // YOUR TURN, use lift to reuse `isEvenNumber` on derivable level. + const isEvenDerivable = lift(isEvenNumber); + + expect(isEvenNumber(2)).toBe(true); + expect(isEvenNumber(13)).toBe(true); + expect(isEvenDerivable(atom(2))).toMatchObject(atom(true)); + expect(isEvenDerivable(atom(13))).toMatchObject(atom(false)); + }); + + // TODO: + it('peek', () => {}); }); diff --git a/tutorial/8 - advanced.test.ts b/tutorial/8 - advanced.test.ts index 48e9944..87a026a 100644 --- a/tutorial/8 - advanced.test.ts +++ b/tutorial/8 - advanced.test.ts @@ -1,4 +1,5 @@ import { atom, constant, Derivable, derive, SettableDerivable } from '@skunkteam/sherlock'; +import { template } from '@skunkteam/sherlock-utils'; import { Map as ImmutableMap } from 'immutable'; /** @@ -8,7 +9,7 @@ import { Map as ImmutableMap } from 'immutable'; */ export const __YOUR_TURN__ = {} as any; -describe.skip('advanced', () => { +describe('advanced', () => { /** * In the case a `Derivable` is required, but the value is immutable. * You can use a `constant()`. @@ -21,7 +22,7 @@ describe.skip('advanced', () => { * It can be valueable to know what a `constant()` is, though. * So try and remove the `cast`, see what happens! */ - const c = constant('value') as unknown as SettableDerivable; + const c = constant('value') as SettableDerivable; /** * ** Your Turn ** @@ -32,8 +33,17 @@ describe.skip('advanced', () => { // Remove this after taking your turn below. expect(false).toBe(true); // .toThrow() or .not.toThrow()? ↴ (2x) - expect(() => c.get()) /* __YOUR_TURN__ */; - expect(() => c.set('new value')) /* __YOUR_TURN__ */; + expect(() => c.get()).not.toThrow(); /* __YOUR_TURN__ */ + expect(() => c.set('new value')).toThrow() /* __YOUR_TURN__ */; + }); + + it('`templates`', () => { + // Staying in the theme of redefining normal Typescript code in our Derivable language + // we also have a special syntax to copy template literals to a Derivable. + const one = 1; + const myDerivable = template`I want to go to ${one} party`; + // expect(myDerivable.value).toBe(`I want to go to 1 party`); + expect(myDerivable.value).toBe(__YOUR_TURN__); /* __YOUR_TURN__ */ }); /** @@ -57,12 +67,13 @@ describe.skip('advanced', () => { * * Rewrite the `.get()`/`.set()` combos below using `.swap()`. */ - expect(false).toBe(true); // Remove this after taking your turn below. - myCounter$.set(plusOne(myCounter$.get())); + // expect(false).toBe(true); + + myCounter$.swap(plusOne); expect(myCounter$.get()).toEqual(1); - myCounter$.set(plusOne(myCounter$.get())); + myCounter$.swap(plusOne); expect(myCounter$.get()).toEqual(2); }); @@ -83,19 +94,19 @@ describe.skip('advanced', () => { * * Use the `.value` accessor to get the current value. */ - expect(__YOUR_TURN__).toEqual('foo'); + expect(myAtom$.value).toEqual('foo'); /** * ** Your Turn ** * * Now use the `.value` accessor to set a 'new value'. */ - myAtom$.value = __YOUR_TURN__; + myAtom$.value = 'new value'; expect(myAtom$.get()).toEqual('new value'); }); - /** + /** FIXME: SAME FOR ERRORS!! * If a `Derivable` is `unresolved`, `.get()` will normally throw. * `.value` will return `undefined` instead. */ @@ -105,7 +116,7 @@ describe.skip('advanced', () => { /** * ** Your Turn ** */ - expect(myAtom$.value).toEqual(__YOUR_TURN__); + expect(myAtom$.value).toEqual(undefined); }); /** @@ -128,11 +139,11 @@ describe.skip('advanced', () => { * We just created two `Derivable`s that are almost exactly the same. * But what happens when their source becomes `unresolved`? */ - expect(usingGet$.resolved).toEqual(__YOUR_TURN__); - expect(usingVal$.resolved).toEqual(__YOUR_TURN__); + expect(usingGet$.resolved).toEqual(true); + expect(usingVal$.resolved).toEqual(true); myAtom$.unset(); - expect(usingGet$.resolved).toEqual(__YOUR_TURN__); - expect(usingVal$.resolved).toEqual(__YOUR_TURN__); + expect(usingGet$.resolved).toEqual(false); + expect(usingVal$.resolved).toEqual(true); }); }); @@ -155,7 +166,7 @@ describe.skip('advanced', () => { * * Use the `.map()` method to create the expected output below */ - const mappedAtom$: Derivable = __YOUR_TURN__; + const mappedAtom$: Derivable = myAtom$.map(base => base.toString().repeat(base)); mappedAtom$.react(mapReactSpy); @@ -186,11 +197,11 @@ describe.skip('advanced', () => { * We changed`myRepeat$` to equal 3. * Do you expect both reactors to have fired? And with what? */ - expect(deriveReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(deriveReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + expect(deriveReactSpy).toHaveBeenCalledTimes(2); + expect(deriveReactSpy).toHaveBeenLastCalledWith('hohoho', expect.toBeFunction()); - expect(mapReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(mapReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + expect(mapReactSpy).toHaveBeenCalledTimes(2); + expect(mapReactSpy).toHaveBeenLastCalledWith('hohoho', expect.toBeFunction()); myString$.value = 'ha'; /** @@ -198,18 +209,18 @@ describe.skip('advanced', () => { * * And now that we have changed `myString$`? And when `myRepeat$` changed again? */ - expect(deriveReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(deriveReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + expect(deriveReactSpy).toHaveBeenCalledTimes(3); + expect(deriveReactSpy).toHaveBeenLastCalledWith('hahaha', expect.toBeFunction()); - expect(mapReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(mapReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + expect(mapReactSpy).toHaveBeenCalledTimes(2); + expect(mapReactSpy).toHaveBeenLastCalledWith('hohoho', expect.toBeFunction()); myRepeat$.value = 2; - expect(deriveReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(deriveReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + expect(deriveReactSpy).toHaveBeenCalledTimes(4); + expect(deriveReactSpy).toHaveBeenLastCalledWith('haha', expect.toBeFunction()); - expect(mapReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(mapReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + expect(mapReactSpy).toHaveBeenCalledTimes(3); + expect(mapReactSpy).toHaveBeenLastCalledWith('haha', expect.toBeFunction()); /** * As you can see, a change in `myString$` will not trigger an @@ -236,7 +247,7 @@ describe.skip('advanced', () => { // This first function is called when getting... n => -n, // ...and this second function is called when setting. - __YOUR_TURN__, + (newV, _) => -newV, ); // The original `atom` was set to 1, so we want the inverse to @@ -249,15 +260,37 @@ describe.skip('advanced', () => { expect(myAtom$.get()).toEqual(2); expect(myInverse$.get()).toEqual(-2); }); + + it('similar to `map()` on arrays', () => { + // if the similarity is not clear yet, here is a comparison between + // the normal `map()` on arrays and our `Derivable` `map()`. + // both get values out of a container (`Array` or `Derivable`), apply + // some function, and put it back in the container. + + const addOne: (v: number) => number = v => v + 1; + + const myList = [1, 2, 3]; + const myList2 = myList.map(addOne); + expect(myList2).toMatchObject([2, 3, 4]); + + const myDerivable = atom(1); + const myDerivable2 = myDerivable.map(addOne); + expect(myDerivable2.value).toBe(2); + + // you can combine them too + const myDerivable3 = atom([1, 2, 3]); + const myDerivable4 = myDerivable3.map(v => v.map(addOne)); + expect(myDerivable4.value).toMatchObject([2, 3, 4]); + }); }); /** * `.pluck()` is a special case of the `.map()` method. * If a collection of values, like an Object, Map, Array is the result of a - * `Derivable` one of those values can be plucked into a new `Derivable`. + * `Derivable`, one of those values can be plucked into a new `Derivable`. * This plucked `Derivable` can be settable, if the source supports it. * - * The way properties are plucked is pluggable, but by default both + * The way properties are plucked is pluggable, but by default both // TODO: no-one here knows what "pluggable" is. Or ImmutableJS. * `.get()` and `[]` are supported to support * basic Objects, Maps and Arrays. * @@ -290,7 +323,7 @@ describe.skip('advanced', () => { * * * Hint: you'll have to cast the result from `.pluck()`. */ - firstProp$ = __YOUR_TURN__; + firstProp$ = myMap$.pluck('firstProp') as SettableDerivable; }); /** @@ -306,18 +339,18 @@ describe.skip('advanced', () => { * What do you expect the plucked `Derivable` to look like? And what * happens when we `.set()` it? */ - expect(firstProp$.get()).toEqual(__YOUR_TURN__); + expect(firstProp$.get()).toEqual('firstValue'); // the plucked `Derivable` should be settable firstProp$.set('other value'); // is the `Derivable` value the same as was set? - expect(firstProp$.get()).toEqual(__YOUR_TURN__); + expect(firstProp$.get()).toEqual('other value'); // How many times was the spy called? Note the `skipFirst`.. - expect(reactPropSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reactPropSpy).toHaveBeenCalledTimes(1); // ...and what was the value? - expect(reactPropSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + expect(reactPropSpy).toHaveBeenLastCalledWith('other value', expect.toBeFunction()); }); /** @@ -339,7 +372,7 @@ describe.skip('advanced', () => { myMap$.swap(map => map.set('secondProp', 'new value')); // How many times was the spy called? Note the `skipFirst`. - expect(reactPropSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reactPropSpy).toHaveBeenCalledTimes(0); /** * ** Your Turn ** @@ -349,10 +382,10 @@ describe.skip('advanced', () => { myMap$.swap(map => map.set('firstProp', 'new value')); // How many times was the spy called? Note the `skipFirst`.. - expect(reactPropSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reactPropSpy).toHaveBeenCalledTimes(1); // ...and what was the value? - expect(reactPropSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + expect(reactPropSpy).toHaveBeenLastCalledWith('new value', expect.toBeFunction()); }); /** @@ -372,10 +405,10 @@ describe.skip('advanced', () => { * So what if we set `firstProp$`? Does this propagate to the source * `Derivable`? */ - firstProp$.set(__YOUR_TURN__); - expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(myMap$.get().get('firstProp')).toEqual(__YOUR_TURN__); - expect(myMap$.get().get('secondProp')).toEqual(__YOUR_TURN__); + firstProp$.set('new value'); + expect(reactSpy).toHaveBeenCalledTimes(1); + expect(myMap$.get().get('firstProp')).toEqual('new value'); + expect(myMap$.get().get('secondProp')).toEqual('secondValue'); }); }); }); diff --git a/tutorial/9 - expert.test.ts b/tutorial/9 - expert.test.ts index 4f6b6fb..5b3e1c1 100644 --- a/tutorial/9 - expert.test.ts +++ b/tutorial/9 - expert.test.ts @@ -8,7 +8,7 @@ import { derivableCache } from '@skunkteam/sherlock-utils'; */ export const __YOUR_TURN__ = {} as any; -describe.skip('expert', () => { +describe('expert', () => { describe('`.autoCache()`', () => { /** * If a `.get()` is called on a `Derivable` all derivations will be @@ -32,7 +32,7 @@ describe.skip('expert', () => { */ // `.toHaveBeenCalled()` or `.not.toHaveBeenCalled()`? ↴ - expect(hasDerived) /* Your Turn */; + expect(hasDerived).not.toHaveBeenCalled() /* Your Turn */; mySecondDerivation$.get(); @@ -44,7 +44,7 @@ describe.skip('expert', () => { * first `Derivable` actually executed its derivation? */ // how many times? - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(3); }); /** @@ -68,7 +68,7 @@ describe.skip('expert', () => { * expectations pass. */ const myAtom$ = atom(true); - const myFirstDerivation$ = myAtom$.derive(firstHasDerived); + const myFirstDerivation$ = myAtom$.derive(firstHasDerived).autoCache(); const mySecondDerivation$ = myFirstDerivation$.derive(() => secondHasDerived(myFirstDerivation$.get() + myFirstDerivation$.get()), ); @@ -110,9 +110,9 @@ describe.skip('expert', () => { mySecondDerivation$.get(); // first after last .get() - expect(firstHasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(firstHasDerived).toHaveBeenCalledTimes(2); // Ohhhh, it does not reset the value of last time. It just adds to it. But caches again apparently, because 2, not 4. Weetnie, is met `clear` niet beter? // second after last .get() - expect(secondHasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(secondHasDerived).toHaveBeenCalledTimes(3); }); }); @@ -182,9 +182,10 @@ describe.skip('expert', () => { * But does that apply here? * How many times has the setup run, for the price `Derivable`. */ - expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(stockPrice$).toHaveBeenCalledTimes(2); /** Can you explain this behavior? */ + // Yes: it creates a different Derivable every time, so it cannot use any caching! A similar issue to the `pairwise()` issue from tutorial 7. }); /** @@ -229,19 +230,19 @@ describe.skip('expert', () => { */ // How often was the reactor on price$ called? - expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reactSpy).toHaveBeenCalledTimes(0); // And how many times did the setup run? - expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(stockPrice$).toHaveBeenCalledTimes(2); // What's the value of price$ now? - expect(price$.value).toEqual(__YOUR_TURN__); + expect(price$.value).toEqual(undefined); // And the value of googlPrice$? - expect(googlPrice$.value).toEqual(__YOUR_TURN__); + expect(googlPrice$.value).toEqual(1079.11); // Is googlPrice$ still even driving any reactors? - expect(googlPrice$.connected).toEqual(__YOUR_TURN__); + expect(googlPrice$.connected).toEqual(false); /** * Can you explain this behavior? @@ -273,7 +274,7 @@ describe.skip('expert', () => { * the created `Derivable` will not run the setup again and * everything should work as expected. * - * ** Your Turn ** + * ** Your Turn ** TODO: not in the SOLUTIONS!! * * *Hint: there is even an `unwrap` helper function for just * such an occasion, try it!* @@ -300,12 +301,14 @@ describe.skip('expert', () => { */ .map(companies => companies.map(company => stockPrice$(company))) // Then we get the prices from the created `Derivable`s in a separate step - .derive(price$s => price$s.map(price$ => price$.value)); + .derive(price$s => price$s.map(price$ => price$.value)); // TODO: yeah, you lost me. I need to dive into `.map()` again... + // So, in practice: the `.map()` maps the `companies` to a new Derivable, one where the list is changed to a new list of prices. + // We save this new Derivable in `prices$`, and then this new list is derived on such that, when it changes, we do something with it. prices$.react(reactor); // Because we use `.value` instead of `.get()` the reactor - // should emit immediately this time. + // should emit immediately this time. TODO: is there truly a difference? // But it should emit `undefined`. expect(reactSpy).toHaveBeenCalledExactlyOnceWith([undefined]); @@ -324,8 +327,8 @@ describe.skip('expert', () => { * * So the value was increased. What do you think happened now? */ - expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(reactSpy).toHaveBeenLastCalledWith([__YOUR_TURN__]); + expect(reactSpy).toHaveBeenCalledTimes(2); + expect(reactSpy).toHaveBeenLastCalledWith([1079.11]); /** * So that worked, now let's try and add another company to the @@ -343,8 +346,10 @@ describe.skip('expert', () => { * * We had a price for 'GOOGL', but not for 'APPL'... */ - expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(reactSpy).toHaveBeenCalledWith([__YOUR_TURN__, __YOUR_TURN__]); + expect(reactSpy).toHaveBeenCalledTimes(3); + expect(reactSpy).toHaveBeenCalledWith([undefined, undefined]); + // Because companies was the central component on which a derive lied... + // ...changing it made the whole thing reset?? Idk man. }); }); @@ -404,7 +409,7 @@ describe.skip('expert', () => { * * Has anything changed, by using the `derivableCache`? */ - expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(stockPrice$).toHaveBeenCalledTimes(1); // Now let's resolve the price stockPrice$.mock.results[0].value.set(1079.11); @@ -417,10 +422,10 @@ describe.skip('expert', () => { * * What happens this time? Has the setup run again? */ - expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(stockPrice$).toHaveBeenCalledTimes(1); // Ok, but did it update the HTML? - expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(lastEmittedHTMLs()[0]).toContain(__YOUR_TURN__); + expect(reactSpy).toHaveBeenCalledTimes(2); + expect(lastEmittedHTMLs()[0]).toContain('$ 1079.11'); // Last chance, what if we add a company companies$.swap(current => [...current, 'APPL']); @@ -433,12 +438,12 @@ describe.skip('expert', () => { * * But did it calculate 'GOOGL' again too? */ - expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(stockPrice$).toHaveBeenCalledTimes(2); + expect(reactSpy).toHaveBeenCalledTimes(3); // The first should be the generated HTML for 'GOOGL'. - expect(lastEmittedHTMLs()[0]).toContain(__YOUR_TURN__); + expect(lastEmittedHTMLs()[0]).toContain('$ 1079.11'); // The second should be the generated HTML for 'APPL'. - expect(lastEmittedHTMLs()[1]).toContain(__YOUR_TURN__); + expect(lastEmittedHTMLs()[1]).toContain('$ unknown'); }); }); }); From bf9c62188ef5aa2c785b9b5eff24e55b04caf5f4 Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 18 Jul 2024 16:49:51 +0200 Subject: [PATCH 24/30] added a tutorial-and-solution-generator; added tons of tests --- CHANGELOG.md | 2 + generateTutorialAndSolution.js | 88 +++ generateTutorialAndSolution.ts | 39 ++ .../1 - intro.test.ts | 9 +- .../2 - deriving.test.ts | 18 +- .../3 - reacting.test.ts | 170 +++-- .../4 - inner workings.test.ts | 71 ++- .../5 - unresolved.test.ts | 31 +- generated_solution/6 - errors.test.ts | 263 ++++++++ .../7 - advanced.test.ts | 175 ++++-- generated_solution/8 - utils.test.ts | 382 ++++++++++++ .../9 - expert.test.ts | 60 +- generated_solution/jest.config.ts | 18 + .../tsconfig.json | 0 .../tsconfig.spec.json | 0 generated_tutorial/1 - intro.test.ts | 140 +++++ generated_tutorial/2 - deriving.test.ts | 226 +++++++ generated_tutorial/3 - reacting.test.ts | 547 ++++++++++++++++ generated_tutorial/4 - inner workings.test.ts | 310 ++++++++++ generated_tutorial/5 - unresolved.test.ts | 178 ++++++ generated_tutorial/6 - errors.test.ts | 263 ++++++++ generated_tutorial/7 - advanced.test.ts | 507 +++++++++++++++ generated_tutorial/8 - utils.test.ts | 381 ++++++++++++ generated_tutorial/9 - expert.test.ts | 450 ++++++++++++++ generated_tutorial/jest.config.ts | 18 + generated_tutorial/tsconfig.json | 10 + generated_tutorial/tsconfig.spec.json | 8 + generator/1 - intro.test.ts | 144 +++++ generator/2 - deriving.test.ts | 252 ++++++++ generator/3 - reacting.test.ts | 585 ++++++++++++++++++ generator/4 - inner workings.test.ts | 348 +++++++++++ generator/5 - unresolved.test.ts | 192 ++++++ generator/6 - errors.test.ts | 275 ++++++++ generator/7 - advanced.test.ts | 570 +++++++++++++++++ generator/8 - utils.test.ts | 408 ++++++++++++ generator/9 - expert.test.ts | 473 ++++++++++++++ {tutorial => generator}/jest.config.ts | 2 +- generator/tsconfig.json | 10 + generator/tsconfig.spec.json | 8 + tutorial/6 - errors.test.ts | 259 -------- tutorial/7 - utils.test.ts | 414 ------------- 41 files changed, 7398 insertions(+), 906 deletions(-) create mode 100644 generateTutorialAndSolution.js create mode 100644 generateTutorialAndSolution.ts rename {tutorial => generated_solution}/1 - intro.test.ts (96%) rename {tutorial => generated_solution}/2 - deriving.test.ts (94%) rename {tutorial => generated_solution}/3 - reacting.test.ts (74%) rename {tutorial => generated_solution}/4 - inner workings.test.ts (79%) rename {tutorial => generated_solution}/5 - unresolved.test.ts (85%) create mode 100644 generated_solution/6 - errors.test.ts rename tutorial/8 - advanced.test.ts => generated_solution/7 - advanced.test.ts (69%) create mode 100644 generated_solution/8 - utils.test.ts rename {tutorial => generated_solution}/9 - expert.test.ts (89%) create mode 100644 generated_solution/jest.config.ts rename {tutorial => generated_solution}/tsconfig.json (100%) rename {tutorial => generated_solution}/tsconfig.spec.json (100%) create mode 100644 generated_tutorial/1 - intro.test.ts create mode 100644 generated_tutorial/2 - deriving.test.ts create mode 100644 generated_tutorial/3 - reacting.test.ts create mode 100644 generated_tutorial/4 - inner workings.test.ts create mode 100644 generated_tutorial/5 - unresolved.test.ts create mode 100644 generated_tutorial/6 - errors.test.ts create mode 100644 generated_tutorial/7 - advanced.test.ts create mode 100644 generated_tutorial/8 - utils.test.ts create mode 100644 generated_tutorial/9 - expert.test.ts create mode 100644 generated_tutorial/jest.config.ts create mode 100644 generated_tutorial/tsconfig.json create mode 100644 generated_tutorial/tsconfig.spec.json create mode 100644 generator/1 - intro.test.ts create mode 100644 generator/2 - deriving.test.ts create mode 100644 generator/3 - reacting.test.ts create mode 100644 generator/4 - inner workings.test.ts create mode 100644 generator/5 - unresolved.test.ts create mode 100644 generator/6 - errors.test.ts create mode 100644 generator/7 - advanced.test.ts create mode 100644 generator/8 - utils.test.ts create mode 100644 generator/9 - expert.test.ts rename {tutorial => generator}/jest.config.ts (93%) create mode 100644 generator/tsconfig.json create mode 100644 generator/tsconfig.spec.json delete mode 100644 tutorial/6 - errors.test.ts delete mode 100644 tutorial/7 - utils.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c78c0f..3c3a5e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +TODO: should I put my shit in? + ## [8.0.0](https://github.com/skunkteam/sherlock/compare/v7.0.0...v8.0.0) (2023-10-17) ### ⚠ BREAKING CHANGES diff --git a/generateTutorialAndSolution.js b/generateTutorialAndSolution.js new file mode 100644 index 0000000..b974d70 --- /dev/null +++ b/generateTutorialAndSolution.js @@ -0,0 +1,88 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +// import * as fs from 'fs'; +var fs = require("node:fs/promises"); +// Run with: tsc generateTutorialAndSolution.ts && node generateTutorialAndSolution.js +function generateTutorialAndSolutions() { + return __awaiter(this, void 0, void 0, function () { + var filenames, _i, filenames_1, filename, originalContent, tutorialContent, solutionContent; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, fs.readdir('generator')]; + case 1: + filenames = (_a.sent()).filter(function (f) { return f.endsWith("test.ts"); }); + _i = 0, filenames_1 = filenames; + _a.label = 2; + case 2: + if (!(_i < filenames_1.length)) return [3 /*break*/, 7]; + filename = filenames_1[_i]; + return [4 /*yield*/, fs.readFile("generator/".concat(filename), 'utf8')]; + case 3: + originalContent = _a.sent(); + tutorialContent = originalContent + .replace(/describe(?!\.skip)/g, "describe.skip") // change `describe` to `describe.skip` + .replace(/\/\/ #QUESTION-BLOCK-(START|END)/g, "") + .replace(/\/\/ #QUESTION/g, "") // remove `// #QUESTION` comments + .replace(/\/\/ #ANSWER-BLOCK-START[\s\S]*?\/\/ #ANSWER-BLOCK-END/g, "") // remove // #ANSWER blocks + .replace(/\n.*?\/\/ #ANSWER/g, "") // remove the entire `// #ANSWER` line, including comment + .replace(/\n\s*\n\s*\n/g, "\n\n"); + return [4 /*yield*/, fs.writeFile("generated_tutorial/".concat(filename), tutorialContent)]; + case 4: + _a.sent(); + solutionContent = originalContent + .replace(/describe\.skip/g, "describe") // change `describe.skip` to `describe` + .replace(/\/\/ #ANSWER-BLOCK-(START|END)/g, "") + .replace(/\/\/ #ANSWER/g, "") // remove `// #ANSWER` comments + .replace(/\/\/ #QUESTION-BLOCK-START[\s\S]*?\/\/ #QUESTION-BLOCK-END/g, "") // remove // #QUESTION blocks + .replace(/\n.*?\/\/ #QUESTION/g, "") // remove the entire `// #QUESTION` line, including comment + .replace(/\n\s*\n\s*\n/g, "\n\n"); + return [4 /*yield*/, fs.writeFile("generated_solution/".concat(filename), solutionContent)]; + case 5: + _a.sent(); + console.log("\u001B[33m ".concat(filename, " saved! \u001B[0m")); + _a.label = 6; + case 6: + _i++; + return [3 /*break*/, 2]; + case 7: return [2 /*return*/]; + } + }); + }); +} +generateTutorialAndSolutions(); diff --git a/generateTutorialAndSolution.ts b/generateTutorialAndSolution.ts new file mode 100644 index 0000000..a8ec8bd --- /dev/null +++ b/generateTutorialAndSolution.ts @@ -0,0 +1,39 @@ +// import * as fs from 'fs'; +import * as fs from 'node:fs/promises'; + +// Run with: tsc generateTutorialAndSolution.ts && node generateTutorialAndSolution.js + +async function generateTutorialAndSolutions() { + // get all filenames in the folder 'tutorial' that end on '.ts' + let filenames: string[] = (await fs.readdir('generator')).filter(f => f.endsWith(`test.ts`)); + + for (let filename of filenames) { + // // Read the original text file + const originalContent = await fs.readFile(`generator/${filename}`, 'utf8'); + + // ** TUTORIAL ** + let tutorialContent = originalContent + .replace(/describe(?!\.skip)/g, `describe.skip`) // change `describe` to `describe.skip` + .replace(/\/\/ #QUESTION-BLOCK-(START|END)/g, ``) + .replace(/\/\/ #QUESTION/g, ``) // remove `// #QUESTION` comments + .replace(/\/\/ #ANSWER-BLOCK-START[\s\S]*?\/\/ #ANSWER-BLOCK-END/g, ``) // remove // #ANSWER blocks + .replace(/\n.*?\/\/ #ANSWER/g, ``) // remove the entire `// #ANSWER` line, including comment + .replace(/\n\s*\n\s*\n/g, `\n\n`); // remove excess whitespaces/newlines + + await fs.writeFile(`generated_tutorial/${filename}`, tutorialContent); + + // ** SOLUTION ** + let solutionContent = originalContent + .replace(/describe\.skip/g, `describe`) // change `describe.skip` to `describe` + .replace(/\/\/ #ANSWER-BLOCK-(START|END)/g, ``) + .replace(/\/\/ #ANSWER/g, ``) // remove `// #ANSWER` comments + .replace(/\/\/ #QUESTION-BLOCK-START[\s\S]*?\/\/ #QUESTION-BLOCK-END/g, ``) // remove // #QUESTION blocks + .replace(/\n.*?\/\/ #QUESTION/g, ``) // remove the entire `// #QUESTION` line, including comment + .replace(/\n\s*\n\s*\n/g, `\n\n`); // remove excess whitespaces/newlines + + await fs.writeFile(`generated_solution/${filename}`, solutionContent); + console.log(`\x1b[33m ${filename} saved! \x1b[0m`); + } +} + +generateTutorialAndSolutions(); diff --git a/tutorial/1 - intro.test.ts b/generated_solution/1 - intro.test.ts similarity index 96% rename from tutorial/1 - intro.test.ts rename to generated_solution/1 - intro.test.ts index 58a6fac..6ee2307 100644 --- a/tutorial/1 - intro.test.ts +++ b/generated_solution/1 - intro.test.ts @@ -32,7 +32,6 @@ describe('intro', () => { --- Welcome to the tutorial! --- Please look in \`./tutorial/1 - intro.test.ts\` to see what to do next.`, () => { - // TODO: // At the start of the spec, there will be some setup. let bool = false; @@ -44,7 +43,7 @@ describe('intro', () => { * This can also be indicated with the `__YOUR_TURN__` variable. * * It should be clear what to do here... */ - bool = true; + bool = true; expect(bool).toBeTrue(); // We use expectations like this to verify the result. }); @@ -72,7 +71,7 @@ describe('the basics', () => { // the `Atom`. expect(myValue$.get()).toEqual(1); - myValue$.set(2); + myValue$.set(2); // Use the `.set()` method to change the value of the `Atom`. expect(myValue$.get()).toEqual(2); }); @@ -98,7 +97,7 @@ describe('the basics', () => { * negative to a positive number and vice versa) of the original `Atom`. */ // Use `myValue$.derive(val => ...)` to implement `myInverse$`. - const myInverse$ = myValue$.derive(val => -val); + const myInverse$ = myValue$.derive(val => -val); expect(myInverse$.get()).toEqual(-1); // So if we set `myValue$` to -2: myValue$.set(-2); @@ -123,7 +122,7 @@ describe('the basics', () => { * * Now react to `myCounter$`. In every `react()`. * Increase the `reacted` variable by one. */ - myCounter$.react(() => reacted++); + myCounter$.react(() => reacted++); expect(reacted).toEqual(1); // `react()` will react immediately, more on that later. diff --git a/tutorial/2 - deriving.test.ts b/generated_solution/2 - deriving.test.ts similarity index 94% rename from tutorial/2 - deriving.test.ts rename to generated_solution/2 - deriving.test.ts index 1a51956..e88fc49 100644 --- a/tutorial/2 - deriving.test.ts +++ b/generated_solution/2 - deriving.test.ts @@ -73,15 +73,15 @@ describe('deriving', () => { * method on another `Derivable`. */ + // Should return 'Buzz' when `myCounter$` is a multiple of 5 and '' otherwise. + // Should return 'Fizz' when `myCounter$` is a multiple of 3 and '' otherwise. - const fizz$: Derivable = myCounter$.derive(v => (v % 3 === 0 ? 'Fizz' : '')); + const fizz$: Derivable = myCounter$.derive(v => (v % 3 ? 'Fizz' : '')); // Shorthand for `v % 3 === 0` // Should return 'Buzz' when `myCounter$` is a multiple of 5 and '' otherwise. - const buzz$: Derivable = myCounter$.derive(v => (v % 5 === 0 ? 'Buzz' : '')); + const buzz$: Derivable = myCounter$.derive(v => (v % 5 ? 'Buzz' : '')); - const fizzBuzz$: Derivable = derive( - () => (fizz$.get() + buzz$.get() === '' ? myCounter$.get() : fizz$.get() + buzz$.get()), // TODO: why not put it on the counter then? - ); + const fizzBuzz$: Derivable = derive(() => fizz$.get() + buzz$.get() || myCounter$.get()); expect(fizz$.get()).toEqual(''); expect(buzz$.get()).toEqual(''); @@ -157,7 +157,7 @@ describe('deriving', () => { expect(tweetCount).toEqual(3); // Is there a new tweet? expect(lastTweet).toContain('Donald'); // Who sent it? Donald? Or Barack? - expect(lastTweet).toContain('race'); // What did he tweet? + expect(lastTweet).toContain('politics'); // What did he tweet? /** * As you can see, this is something to look out for. @@ -200,6 +200,7 @@ describe('deriving', () => { * `.and(...)` and `.or(...)`. Make sure the output of those `Derivable`s * is either 'Fizz'/'Buzz' or ''. */ + const fizz$ = myCounter$ .derive(count => count % 3) .is(0) @@ -212,12 +213,13 @@ describe('deriving', () => { .and('Buzz') .or(''); - const fizzBuzz$ = derive(() => fizz$.get() + buzz$.get()).or(myCounter$); // TODO: + const fizzBuzz$ = derive(() => fizz$.get() + buzz$.get()).or(myCounter$); + // This will check whether `fizz$.get() + buzz$.get()` is truthy: if so, return it; if not, return `myCounter$` for (let count = 1; count <= 100; count++) { // Set the value of the `Atom`, myCounter$.set(count); - // console.log(myCounter$.get() + ', ' + fizzBuzz$.get()); + // and check if the output changed accordingly. checkFizzBuzz(count, fizzBuzz$.get()); } diff --git a/tutorial/3 - reacting.test.ts b/generated_solution/3 - reacting.test.ts similarity index 74% rename from tutorial/3 - reacting.test.ts rename to generated_solution/3 - reacting.test.ts index d7dbd05..2ae2582 100644 --- a/tutorial/3 - reacting.test.ts +++ b/generated_solution/3 - reacting.test.ts @@ -7,6 +7,13 @@ import { atom } from '@skunkteam/sherlock'; */ export const __YOUR_TURN__ = {} as any; +// FIXME: check my solutions with the actual solutions +// FIXME: remove all TODO: and FIXME: +// FIXME: check whether the generated tutorials and solutions actually work (e.g. are all solutions correct? No weird shenanigans?) +// FIXME: deze file niet linten / builden (voor automatische test). Tutorial ook niet. Maar solutions juist wel! OP EIND. +// FIXME: interne review document, mocht ik iets hebben om te laten zien! In Google Drive, zet het erin! +// FIXME: werkt `npm run tutorial` nog??? + /** * In the intro we have seen a basic usage of the `.react()` method. * Let's dive a bit deeper into the details of this method. @@ -69,8 +76,10 @@ describe('reacting', () => { * Time to react to `myAtom$` with the `reactor()` function defined * above. */ - myAtom$.react((val, _) => reactor(val)); - // myAtom$.react(reactor); // OR this. TS will ignore any additional arguments you might give it. + + myAtom$.react(reactor); + // myAtom$.react(val => reactor(val)); // Alternatively, this would work too. + // myAtom$.react((val, _) => reactor(val)); // Or this. expectReact(1, 'initial value'); @@ -102,13 +111,8 @@ describe('reacting', () => { * * catch the returned `stopper` in a variable */ + const stopper = myAtom$.react(reactor); - // let stopFunc: () => void = () => {}; // dummy initial value - // myAtom$.react((val, stop) => { - // reactor(val); - // stopFunc = stop; - // }); - const stopFunc = myAtom$.react((val, _) => reactor(val)); expectReact(1, 'initial value'); /** @@ -116,7 +120,7 @@ describe('reacting', () => { * * Call the `stopper`. */ - stopFunc(); + stopper(); myAtom$.set('new value'); @@ -139,9 +143,10 @@ describe('reacting', () => { * In the reaction below, use the stopper callback to stop the * reaction */ - myAtom$.react((val, stop) => { + + myAtom$.react((val, stopper) => { reactor(val); - stop(); + stopper(); }); expectReact(1, 'initial value'); @@ -194,7 +199,7 @@ describe('reacting', () => { * * Try giving `boolean$` as `until` option. */ - string$.react(reactor, { until: boolean$ }); + string$.react(reactor, { until: boolean$ }); // It should react directly as usual. expectReact(1, 'Value'); @@ -242,7 +247,7 @@ describe('reacting', () => { * Use `!string$.get()` to return `true` when the `string` is * empty. */ - string$.react(reactor, { until: () => !string$.get() }); + string$.react(reactor, { until: () => !string$.get() }); // It should react as usual: string$.set('New value'); @@ -269,7 +274,7 @@ describe('reacting', () => { * Try using the first parameter of the `until` function to do * the same as above. */ - string$.react(reactor, { until: s => !s.get() }); + string$.react(reactor, { until: s => !s.get() }); // It should react as usual. string$.set('New value'); @@ -293,21 +298,21 @@ describe('reacting', () => { let stopper = boolean$.react(reactor, { until: b => !b }); // ...but does it? (Remember: `boolean$` starts out as `false`) - expect(boolean$.connected).toBe(__YOUR_TURN__); + expect(boolean$.connected).toBe(true); // The `b` it obtains as argument is a `Derivable`. This is a // reference value which will evaluate to `true` as it is not `undefined`. // Thus, the negation will evaluate to `false`, independent of the value of // the boolean. You can get the boolean value our of the `Derivable` using `.get()`: - stopper(); + stopper(); // reset stopper = boolean$.react(reactor, { until: b => !b.get() }); - expect(boolean$.connected).toBe(__YOUR_TURN__); + expect(boolean$.connected).toBe(false); - // You can also return the `Derivable` and apply the negation - // with the method designed for it: + // You can also return the `Derivable` after appling the negation + // using the method designed for negating Derivables: stopper(); boolean$.react(reactor, { until: b => b.not() }); - expect(boolean$.connected).toBe(__YOUR_TURN__); + expect(boolean$.connected).toBe(false); }); }); @@ -322,9 +327,6 @@ describe('reacting', () => { * * The interface of `from` is the same as `until` (i.e. it also gets * the parent derivable as first parameter when it's called.) - * - * * Note: when using `from`, `.react()` will (most often) not react - * synchronously any more. As that is the function of this option.* // TODO: word differently... is not a `note`, but the intended effect. */ it('reacting `from`', () => { const sherlock$ = atom(''); @@ -338,7 +340,7 @@ describe('reacting', () => { * * *Hint: remember the `.is()` method from tutorial 2?* */ - sherlock$.react(reactor, { from: sherlock$.is('dear') }); + sherlock$.react(reactor, { from: sherlock$.is('dear') }); expectReact(0); ['Elementary,', 'my', 'dear', 'Watson'].forEach(txt => sherlock$.set(txt)); @@ -364,10 +366,7 @@ describe('reacting', () => { * Now, let's react to all even numbers. * Except 4, we don't want to make it too easy now. */ - count$.react(reactor, { when: v => v.get() % 2 === 0 && v.is(4).not() }); - count$.react(reactor, { when: v => v.get() % 2 === 0 && v.get() !== 4 }); - // TODO: why can I apply `&&` to `number` and Derivable?? - // >>> e.g. `when` kan zowel booleans and Derivable vanwege Unwrappable type xD + count$.react(reactor, { when: v => v.get() % 2 === 0 && v.get() !== 4 }); expectReact(1, 0); @@ -387,35 +386,26 @@ describe('reacting', () => { * there is also a `boolean` option: `skipFirst`. */ it('reacting with `skipFirst`', () => { - const done$ = atom(false); + const count$ = atom(0); /** * ** Your Turn ** * - * Say you want to react when `done$` is true. But not right away.. // TODO: change to use number? + * Say you want to react when `count$` is larger than 3. But not the first time... */ - done$.react(reactor, { when: d => d.is(true) }); // TODO: true expected answer given description: the test case needs asjustment! - // SKIPFIRST negeert de eerste keer dat WHEN true is! Niet de eerste keer in general. - // `// Doesn't react, because the new value equals the previous value that was seen by the reactor.` - // libs/sherlock/src/lib/reactor/reactor.test.ts:136 - // Hij accepteert alleen waardes die anders zijn dan zijn huidige. Omdat hij alleen `true` accepteert, kan hij nooit meer updaten! - // => false accepteert de `when` niet; - // => true is zelfde als voorheen. - // Ik denk dat hij, ondanks dat `skipFirst` de eerste true genegeerd heeft, hij hem wel onthouden heeft als last seen value. Expected! - // Zie libs/sherlock/src/lib/derivable/mixins/take.ts voor volgorde van events? - // Als je `events` wilt, kan je beter Observables ofzo gebruiken. Je wilt dit patroon van "elke keer dat je true ziet, pas aan" eigenlijk niet hier. - // kan beter numbers gebruiken om dit te testen! `<= 4` ofzo - // En extra testje hiervoor! - expectReact(0); + count$.react(reactor, { when: d => d.get() > 3, skipFirst: true }); - done$.set(true); expectReact(0); - done$.set(false); - expectReact(0); + for (let i = 0; i <= 5; i++) { + count$.set(i); + } + expectReact(1, 5); // it should have skipped the 4 - done$.set(true); - expectReact(1, true); + for (let i = 0; i <= 5; i++) { + count$.set(i); + } + expectReact(3, 5); // now it should not have skipped the 4 }); /** @@ -437,8 +427,8 @@ describe('reacting', () => { * * *Hint: you will need to combine `once` with another option* */ - finished$.react(reactor, { once: true, when: f => f.get() }); // TODO: make sure the test captures the diff between `f` and `f.get()` here! - // see next `challenge` for a case where there is a difference. + finished$.react(reactor, { once: true, when: f => f }); // `f => f.get()` is fine as well + expectReact(0); // When finished it should react once. @@ -453,11 +443,9 @@ describe('reacting', () => { }); describe('order of execution', () => { - // the interactions between `from`, `until`, `when`, `skipFirst`, `once`... - that order! - // als het goed is nog niet behandeld (libs/sherlock/src/lib/derivable/mixins/take.ts) - /** - * The options `from`, `until`, `when`, `skipFirst` and `once` are tested in this specific order: + * As you can see for yourself in libs/sherlock/src/lib/derivable/mixins/take.ts, + * the options `from`, `until`, `when`, `skipFirst` and `once` are tested in this specific order: * 1) firstly, `from` is checked. If `from` is/was true (or is not set in the options), we continue: * 2) secondly, `until` is checked. If `until` is false (or is not set in the options), we continue: * 3) thirdly, `when` is checked. If `when` is true (or is not set in the options), we continue: @@ -475,10 +463,10 @@ describe('reacting', () => { myAtom$.set(i); } - // the reactor starts reacting when `myAtom` gets the value 3, but stops when it gets the value 2. + // The reactor starts reacting when `myAtom` gets the value 3, but stops when it gets the value 2. // But because `myAtom` obtains the value 2 before it obtains 3... // ...how many times was the reactor called, if any? - expectReact(__YOUR_TURN__); + expectReact(3, 5); // `from` evaluates before `until`. }); it('`when` and `skipFirst`', () => { @@ -487,9 +475,10 @@ describe('reacting', () => { myAtom$.set(1); - // the reactor reacts when `myAtom` is 1 but skips the first number. - // `myAtom` starts at 0. Does the reactor skip the 0 or the 1? - expectReact(__YOUR_TURN__); + // The reactor reacts when `myAtom` is 1 but skips the first number. + // The first number of `myAtom` is 0, its initial number. + // Does the reactor skip the 0 or the 1? + expectReact(0); // `skipFirst` triggers only when `when` evaluates to true. }); it('`from`, `until`, `when`, `skipFirst`, and `once`', () => { @@ -502,7 +491,7 @@ describe('reacting', () => { once: true, }); - for (let v of [1, 2, 3, 5, 4, 3, 2, 1, 2, 3]) { + for (let v of [1, 2, 3, 4, 5, 4, 3, 2, 1, 2, 3]) { myAtom$.set(v); } @@ -510,56 +499,51 @@ describe('reacting', () => { // Meanwhile, `when` allows neither of those values and only allows the values 2, 3, and 4. // `skipFirst` and `once` are also added, just to bring the whole group together. // so, how many times is the reactor called, and what was the last argument (if any)? - expectReact(__YOUR_TURN__); + + expectReact(1, 3); + // `from` makes it start at the first `5`. `when` allows the next `4`,`3`, and `2`, but + // `skipFirst` ensures that the first `4` is skipped. `once` then ensures that only the `3` + // reacted to. Before the `until` can trigger from a `1`, the `once` has already stopped it. + }); }); describe('challenge', () => { it('onDisconnect', () => { - const connected$ = atom(false); // TODO: change to use number - + const connected$ = atom('disconnected'); /** * ** Your Turn ** * - * We want our reactor to trigger once, when the user disconnects - * (eg for cleanup). + * `connected$` indicates the current connection status: + * > 'connected'; + * > 'disconnected'; + * > 'standby'. + * + * We want our reactor to trigger once, when the device is not connected, + * which means it is either `standby` or `disconnected` (eg for cleanup). * - * `connected$` indicates the current connection status. * This should be possible with three simple ReactorOptions - * Hint: do not use `when`! */ - connected$.react(reactor, { from: c => c, skipFirst: true, once: true }); // WORKS, and intended - connected$.react(reactor, { from: _ => connected$, skipFirst: true, once: true }); // WORKS, and intended - connected$.react(reactor, { from: connected$, skipFirst: true, once: true }); // WORKS, and intended - - // TODO: - // `when: c => !c.get()` gets the boolean out of the Derivable, applies `not`, and returns - // `when: c => !c` coerces the Derivable to a boolean (whether it exists: true), applies `not` to this boolean, and returns false. - // `when: c => c.not()` takes the boolean out of the Derivable, applies `not`, puts it back in a Derivable, and `when` is overloaded - // ...to also be able to take the boolean out of the Derivable! So that is how you can also pass a Derivable - `when` takes the boolean out! - // connected$.react(reactor, { when: c => !c.get(), from: c => c.get() }); // 1. DOES NOT WORK - the connection is not false afterwards - // connected$.react(reactor, { when: c => !c, from: c => c }); // 2. DOES NOT WORK - see above - // connected$.react(reactor, { when: c => !c.get(), skipFirst: true }); // 3. DOES NOT WORK... - // ...as the first time c is false, this is accepted in the system even though skipfirst is true. Then... - // ...the second time that c is false, it is seen as the same value and thus not accepted (only changes are accepted)! Hence: - // setting a Derivable with a value it already has does not trigger it. It does not even go to `when`. - - // It starts as 'not connected' + connected$.react(reactor, { when: s => s.is('connected').not(), skipFirst: true, once: true }); + + // It starts as 'disconnected' expectReact(0); - // At this point, the user connects, no reaction should occur yet. - connected$.set(true); + // At this point, the device connects, no reaction should occur yet. + connected$.set('connected'); expectReact(0); - // When the user disconnects, the reaction should fire once - connected$.set(false); - expectReact(1, false); + // When the device goes to standby, the reaction should fire once + connected$.set('standby'); + expectReact(1, 'standby'); // After that, nothing should change anymore. - connected$.set(true); - expectReact(1, false); - connected$.set(false); - expectReact(1, false); + connected$.set('disconnected'); + expectReact(1, 'standby'); + connected$.set('standby'); + expectReact(1, 'standby'); + connected$.set('connected'); + expectReact(1, 'standby'); // It should not react again after this. expect(connected$.connected).toBeFalse(); diff --git a/tutorial/4 - inner workings.test.ts b/generated_solution/4 - inner workings.test.ts similarity index 79% rename from tutorial/4 - inner workings.test.ts rename to generated_solution/4 - inner workings.test.ts index faf51cc..9d838cf 100644 --- a/tutorial/4 - inner workings.test.ts +++ b/generated_solution/4 - inner workings.test.ts @@ -43,9 +43,12 @@ describe('inner workings', () => { * * What do you expect? */ + expect(reacted).toHaveBeenCalledTimes(1); - expect(reacted).toHaveBeenLastCalledWith(1, expect.toBeFunction()); // TODO: NIET omdat hij weet dat de string niets verandert, - // maar juist omdat hij dezelfde waarde binnen krijgt en dus niet nogmaals triggered!! + expect(reacted).toHaveBeenLastCalledWith(1, expect.toBeFunction()); + // Note: the reactor doesn't know that changing `string$` will not generate a different + // answer by looking at the code of `switch$`, but instead it simply noticed that + // `switch$` got the same value it already had and prevented triggering because of that. // `switch$` is still set to true (number) number$.set(2); @@ -55,15 +58,12 @@ describe('inner workings', () => { * * What do you expect? */ - expect(reacted).toHaveBeenCalledTimes(2); - expect(reacted).toHaveBeenLastCalledWith(2, expect.toBeFunction()); // Now it gets a different value!! - // Now let's reset the mock function, so the call count should - // be 0 again. - reacted.mockClear(); - expect(reacted).toHaveBeenCalledTimes(0); + expect(reacted).toHaveBeenCalledTimes(2); + expect(reacted).toHaveBeenLastCalledWith(2, expect.toBeFunction()); + // As it got a different value (`2` instead of `1`), it triggered. - // `switch$` is set to false (string) + // `switch$` is now set to false (string) switch$.set(false); number$.set(3); @@ -72,8 +72,10 @@ describe('inner workings', () => { * * What do you expect now? */ - expect(reacted).toHaveBeenCalledTimes(1); - expect(reacted).toHaveBeenLastCalledWith('two', expect.toBeFunction()); // It gets a different value than last time again. No need for the reset...? TODO: + + expect(reacted).toHaveBeenCalledTimes(3); + expect(reacted).toHaveBeenLastCalledWith('two', expect.toBeFunction()); + }); /** @@ -86,7 +88,7 @@ describe('inner workings', () => { const hasDerived = jest.fn(); const myAtom$ = atom(true); - const myDerivation$ = myAtom$.derive(hasDerived); // NOTE: React causes an immediate update. Derive does not! + const myDerivation$ = myAtom$.derive(hasDerived); /** * ** Your Turn ** @@ -99,17 +101,17 @@ describe('inner workings', () => { */ // Well, what do you expect? - expect(hasDerived).toHaveBeenCalledTimes(0); + expect(hasDerived).toHaveBeenCalledTimes(0); myDerivation$.get(); // And after a `.get()`? - expect(hasDerived).toHaveBeenCalledTimes(1); + expect(hasDerived).toHaveBeenCalledTimes(1); myDerivation$.get(); // And after the second `.get()`? Is there an extra call? - expect(hasDerived).toHaveBeenCalledTimes(2); + expect(hasDerived).toHaveBeenCalledTimes(2); /** * The state of any `Derivable` can change at any moment. @@ -148,38 +150,35 @@ describe('inner workings', () => { * * Ok, it's your turn to complete the expectations. */ - expect(hasDerived).toHaveBeenCalledTimes(1); // because of the react. + expect(hasDerived).toHaveBeenCalledTimes(1); // because of the react. myDerivation$.get(); - expect(hasDerived).toHaveBeenCalledTimes(1); // no update because someone is reacting, and there has been no update in value. + expect(hasDerived).toHaveBeenCalledTimes(1); // no update because someone is reacting, and there has been no update in value. myAtom$.set(false); - expect(hasDerived).toHaveBeenCalledTimes(2); // `myDerivation`s value has changed, so update. + expect(hasDerived).toHaveBeenCalledTimes(2); // `myDerivation$`s value has changed, so update. myDerivation$.get(); - expect(hasDerived).toHaveBeenCalledTimes(2); // no update. + expect(hasDerived).toHaveBeenCalledTimes(2); // no update. stopper(); - expect(hasDerived).toHaveBeenCalledTimes(2); // stopping doesn't change the value... + expect(hasDerived).toHaveBeenCalledTimes(2); // stopping doesn't change the value... myDerivation$.get(); - expect(hasDerived).toHaveBeenCalledTimes(3); // ...but now, it is not being reacted to, so it goes back to updating every time `.get()` is called! + expect(hasDerived).toHaveBeenCalledTimes(3); // ...but now, it is not being reacted to, so it goes back to updating every time `.get()` is called. /** - * Since the `.react()` already listens to the value(changes) there is + * Since the `.react()` already listens to the value-changes, there is * no need to recalculate whenever a `.get()` is called. * * But when the reactor has stopped, the derivation has to be calculated * again. */ - // Okay, clear, but why? - // I see... because we don't want to keep internal states and such and track changes when no-one is listening! It is a waste of effort. - // So we only keep track of changes when a react is listening. }); /** @@ -216,23 +215,23 @@ describe('inner workings', () => { // Note that this is the same value as it was initialized with myAtom$.set(1); - expect(first).toHaveBeenCalledTimes(1); - expect(second).toHaveBeenCalledTimes(1); + expect(first).toHaveBeenCalledTimes(1); // `myAtom$` has the same value (`1`), so no need to be called + expect(second).toHaveBeenCalledTimes(1); // `first$` has the same value (`false`), so no need to be called myAtom$.set(2); - expect(first).toHaveBeenCalledTimes(2); // different INPUT [2], so call again - expect(second).toHaveBeenCalledTimes(1); // same INPUT [false], so no change + expect(first).toHaveBeenCalledTimes(2); // `myAtom$` has a different value (`2`), so call again + expect(second).toHaveBeenCalledTimes(1); // `first$` has the same value (`false`), so no need to be called myAtom$.set(3); - expect(first).toHaveBeenCalledTimes(3); // different INPUT [3] - expect(second).toHaveBeenCalledTimes(2); // different INPUT [true] + expect(first).toHaveBeenCalledTimes(3); // `myAtom$` has a different value (`3`), so call again + expect(second).toHaveBeenCalledTimes(2); // `first$` has a different value (`true`), so call again myAtom$.set(4); - expect(first).toHaveBeenCalledTimes(4); // different INPUT [4] - expect(second).toHaveBeenCalledTimes(2); // same INPUT [true] + expect(first).toHaveBeenCalledTimes(4); // `myAtom$` has a different value (`4`), so call again + expect(second).toHaveBeenCalledTimes(2); // `first$` has the same value (`true`), so no need to be called /** * Can you explain the behavior above? @@ -270,7 +269,7 @@ describe('inner workings', () => { * The `Atom` is set with exactly the same object as before. Will the * `.react()` fire? */ - expect(hasReacted).toHaveBeenCalledTimes(1); // not considered equal (`{} !== {}`) + expect(hasReacted).toHaveBeenCalledTimes(1); // `{} !== {}`, as they have different references /** * But what if you use an object, that can be easily compared through a @@ -289,7 +288,7 @@ describe('inner workings', () => { * * Do you think the `.react()` fired with this new value? */ - expect(hasReacted).toHaveBeenCalledTimes(0); // TODO: answer already given - is considered equal + expect(hasReacted).toHaveBeenCalledTimes(0); atom$.set(Seq.Indexed.of(1, 2)); @@ -298,7 +297,7 @@ describe('inner workings', () => { * * And now? */ - expect(hasReacted).toHaveBeenCalledTimes(1); // TODO: answer already given - obviously unequal + expect(hasReacted).toHaveBeenCalledTimes(1); /** * In `@skunkteam/sherlock` equality is a bit complex: diff --git a/tutorial/5 - unresolved.test.ts b/generated_solution/5 - unresolved.test.ts similarity index 85% rename from tutorial/5 - unresolved.test.ts rename to generated_solution/5 - unresolved.test.ts index 24c9091..71fa8bb 100644 --- a/tutorial/5 - unresolved.test.ts +++ b/generated_solution/5 - unresolved.test.ts @@ -27,14 +27,14 @@ describe('unresolved', () => { // since it can't be inferred by TypeScript this way. const myAtom$ = atom.unresolved(); - expect(myAtom$.resolved).toEqual(false); + expect(myAtom$.resolved).toEqual(false); /** * ** Your Turn ** * * Resolve the atom, it's pretty easy */ - myAtom$.set(1); + myAtom$.set(1); expect(myAtom$.resolved).toBeTrue(); }); @@ -49,7 +49,7 @@ describe('unresolved', () => { * * Time to create an `unresolved` Atom.. */ - const myAtom$: DerivableAtom = atom.unresolved(); + const myAtom$: DerivableAtom = atom.unresolved(); expect(myAtom$.resolved).toBeFalse(); @@ -64,18 +64,15 @@ describe('unresolved', () => { * * What do you expect? */ - expect(myAtom$.resolved).toEqual(true); + expect(myAtom$.resolved).toEqual(true); // .toThrow() or .not.toThrow()? ↴ - expect(() => myAtom$.get()).not.toThrow(); + expect(() => myAtom$.get()).not.toThrow(); }); /** * If a `Derivable` is `unresolved` it can't react yet. But it will * `.react()` if a value becomes available. - * - * *Note that this can prevent `.react()` from executing immediately* // TODO: what annoying messages... I want to change them. - * It is not a 'Note: side-effect' but the expected intended behavior! */ it('reacting to `unresolved`', () => { const myAtom$ = atom.unresolved(); @@ -88,14 +85,14 @@ describe('unresolved', () => { * * What do you expect? */ - expect(hasReacted).toHaveBeenCalledTimes(0); + expect(hasReacted).toHaveBeenCalledTimes(0); /** * ** Your Turn ** * * Now make the last expect succeed */ - myAtom$.set(`woohoow, I was called`); + myAtom$.set(`woohoow, I was called`); expect(myAtom$.resolved).toBeTrue(); expect(hasReacted).toHaveBeenCalledExactlyOnceWith(`woohoow, I was called`, expect.toBeFunction()); @@ -115,7 +112,7 @@ describe('unresolved', () => { * * Set the value.. */ - myAtom$.set(`it's alive!`); + myAtom$.set(`it's alive!`); expect(myAtom$.get()).toEqual(`it's alive!`); @@ -124,7 +121,7 @@ describe('unresolved', () => { * * Unset the value.. (*Hint: TypeScript is your friend*) */ - myAtom$.unset(); + myAtom$.unset(); expect(myAtom$.resolved).toBeFalse(); }); @@ -145,14 +142,14 @@ describe('unresolved', () => { * * Combine the two `Atom`s into one `Derivable` */ - const myDerivable$: Derivable = myString$.derive(s => s + myOtherString$.get()); + const myDerivable$: Derivable = myString$.derive(s => s + myOtherString$.get()); /** * ** Your Turn ** * * Is `myDerivable$` expected to be `resolved`? */ - expect(myDerivable$.resolved).toEqual(false); + expect(myDerivable$.resolved).toEqual(false); // Now let's set one of the two source `Atom`s myString$.set('some'); @@ -166,8 +163,8 @@ describe('unresolved', () => { // And what if we set `myOtherString$`? myOtherString$.set('data'); - expect(myDerivable$.resolved).toEqual(true); - expect(myDerivable$.get()).toEqual('somedata'); + expect(myDerivable$.resolved).toEqual(true); + expect(myDerivable$.get()).toEqual('somedata'); /** * ** Your Turn ** @@ -176,6 +173,6 @@ describe('unresolved', () => { * What do you expect `myDerivable$` to be? */ myString$.unset(); - expect(myDerivable$.resolved).toEqual(false); + expect(myDerivable$.resolved).toEqual(false); }); }); diff --git a/generated_solution/6 - errors.test.ts b/generated_solution/6 - errors.test.ts new file mode 100644 index 0000000..abf2d3c --- /dev/null +++ b/generated_solution/6 - errors.test.ts @@ -0,0 +1,263 @@ +import { atom, DerivableAtom, error, FinalWrapper, unresolved } from '@skunkteam/sherlock'; + +/** + * ** Your Turn ** + * + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +// Silence TypeScript's import not used errors. +expect(FinalWrapper).toBe(FinalWrapper); + +// In libs/sherlock/src/lib/interfaces.ts:289, the basic states a Derivable can have are shown. +// > `export type State = V | unresolved | ErrorWrapper;` +// A state can be either any type `V` (`number`, `string`, etc.), `unresolved` as we saw in the +// previous tutorial, or `ErrorWrapper`. This last state is explained here. +describe('errors', () => { + let myAtom$: DerivableAtom; + + beforeEach(() => { + myAtom$ = atom(1); + }); + + it('basic errors', () => { + // The `errored` property shows whether the last statement resulted in an error. + expect(myAtom$.errored).toBe(false); + expect(myAtom$.error).toBeUndefined; // by default, the `error` property is undefined. + expect(myAtom$.getState()).toBe(1); // as explained above, any type can be a state + + // We can set errors using the `setError()` function. + myAtom$.setError('my Error'); + + expect(myAtom$.errored).toBe(true); + expect(myAtom$.error).toBe('my Error'); + + // The `ErrorWrapper` state only holds an error string. The `error()` function returns + // such an `ErrorWrapper` which we can use to compare. + expect(myAtom$.getState()).toMatchObject(error('my Error')); + + // expect(myAtom$.get).toThrow("Cannot read properties of undefined (reading 'getState')"); + // TODO: WHAT - normally this works, but internal JEST just fucks with me....? + + // Calling `get()` on `myAtom$` gives the error. + expect(() => myAtom$.get()).toThrow('my Error'); + expect(myAtom$.errored).toBe(true); + + // ** __YOUR_TURN__ ** + // What will happen if you try to call `set()` on `myAtom$`? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myAtom$.set(2)).not.toThrow(); + expect(myAtom$.errored).toBe(false); + + // Interestingly, calling `set()` does not throw an error. In fact, it removes the error state + // altogether. This means we can call `get()` again. + expect(() => myAtom$.get()).not.toThrow(); + }); + + it('deriving an error', () => { + const myDerivable$ = myAtom$.derive(v => v + 1); + + // If `myAtom$` suddenly errors... + myAtom$.setError('division by zero'); + + // ...what happens to `myDerivable$`? + expect(myDerivable$.errored).toBe(true); + + // If any Derivable tries to derive from an atom in an error state, + // this Derivable will itself throw an error too. This makes sense, + // given that it cannot obtain the value it needs anymore. + }); + + it('reacting to an error', () => { + // Without a reactor, setting an error to an Atom does not throw an error. + expect(() => myAtom$.setError('my Error')).not.toThrow(); + myAtom$.set(1); + + // Now we set a reactor to `myAtom$`. This reactor does not use the value of `myAtom$`. + const reactor = jest.fn(); + myAtom$.react(reactor); + + // ** __YOUR_TURN__ ** + // Will an error be thrown when `myAtom$` is now set to an error state? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myAtom$.setError('my Error')).toThrow('my Error'); + + // Reacting to a Derivable that throws an error will make the reactor throw as well. + // Because the reactor will usually fire when it gets connected, it also throws when + // you try to connect it after the error has already been set. + + myAtom$ = atom(1); + myAtom$.setError('my second Error'); + + // ** __YOUR_TURN__ ** + // Will an error be thrown when you use `skipFirst`? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myAtom$.react(reactor, { skipFirst: true })).toThrow('my second Error'); + + // And will an error be thrown when `from = false`? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myAtom$.react(reactor, { from: false })).not.toThrow(); + + // When `from = false`, the reactor is disconnected, preventing the error message from entering. + // `skipFirst`, on the other hand, does allow the error in, but does not trigger an update. + }); + + /** + * Similarly to `constants` which we'll explain in tutorial 7, + * you might want to specify that a variable cannot be updated. + * This can be useful for the programmers themselves, to not + * accidentally update the variable, but it can also be useful for + * optimization. You can do this using the `final` concept. + */ + describe('TEMP `final`', () => { + let myAtom$ = atom(1); + + beforeEach(() => { + myAtom$ = atom(1); + }); + + it('`final` basics', () => { + // Every atom has a `final` property. + expect(myAtom$.final).toBeFalse(); + + // You can make an atom final using the `.makeFinal()` function. + myAtom$.makeFinal(); + expect(myAtom$.final).toBeTrue(); + + /** + * ** Your Turn ** + * What do you think will happen when we try to `.get()` or `.set()` this atom? + */ + // .toThrow() or .not.toThrow()? ↴ + expect(() => myAtom$.get()).not.toThrow(); + expect(() => myAtom$.set(2)).toThrow('cannot set a final derivable'); + + // This behavior is consistent with normal variables created using `const`. + // Alternatively, you can set a last value before setting it to `final`. + // .toThrow() or .not.toThrow()? ↴ + expect(() => myAtom$.setFinal(2)).toThrow('cannot set a final derivable'); + + // There is no way to 'unfinalize' a Derivable, so the only solution to reset is to + // create a whole new Derivable. + myAtom$ = atom(1); + myAtom$.setFinal(2); + expect(myAtom$.final).toBeTrue(); + }); + + it('deriving a `final` Derivable', () => { + const myDerivable$ = myAtom$.derive(v => v + 1); + + const hasReacted = jest.fn(); + myDerivable$.react(hasReacted); + + expect(myDerivable$.final).toBeFalse(); + expect(myDerivable$.connected).toBeTrue(); + + myAtom$.makeFinal(); + + /** + * ** Your Turn ** + * + * What will happen to `myDerivable$` when I change `myAtom$` to be `final`? + */ + expect(myDerivable$.final).toBe(true); + expect(myDerivable$.connected).toBe(false); + + /** + * Derivables that are final (or constant) are no longer tracked. This can save + * a lot of memory and time by cleaning up unused data. Also, when all the variables + * that a Derivable depends on become final, that Derivable itself also becomes final. + * Similarly to `unresolved` and `error`, this chains. + */ + }); + + it('`final` State', () => { + /** A property such as `.final`, similar to variables like `.errored` and `.resolved` + * is useful for checking whenever a Derivable is in a certain state, but these properties + * are just a boolean. This means that these properties cannot be derived and we cannot + * have certain functions execute whenever there is a change in the state. For this reason, + * every Derivable holds an internal state, retrievable using `.getState()` which can be + * derived. TODO: Have a clear place where I explain this! Now I have info up top here too. + * + * We have seen that states (`State`) can be `undefined`, `ErrorWrapper`, + * or any regular type `V`. Other states exist, such as the `MaybeFinalState`. This state can be either + * a normal state `State` or a special `FinalWrapper>` state. Let's see that in action. + */ + expect(myAtom$.getMaybeFinalState()).toBe(1); // `getMaybeFinalState` can return a normal state, which in turn can be any normal type. + + myAtom$.makeFinal(); + + expect(myAtom$.getMaybeFinalState()).toBeInstanceOf(FinalWrapper); // but `getMaybeFinalState` can also return a `FinalWrapper` type. + expect(myAtom$.getState()).toBe(1); // the normal type is still the final it contains. + + // TODO: MAAR JE KAN EEN STATE HELEMAAL NIET DERIVEN! + // Dus dat is allemaal onzin lijkt me....??? Bovendien, kan je normale variabelen niet deriven door het gewoon te + // wrappen in een atom ofzo? + }); + }); + + /** + * It is nice to be able to have a backup plan when an error occurs. + * The `.fallbackTo()` function allows you to specify a default value + * whenever your Derivable gets an error state. + */ + it('Fallback-to', () => { + const myAtom$ = atom(0); + + /** + * ** Your Turn ** + * Use the `.fallbackTo()` method to create a `mySafeAtom$` which + * gets the backup value `3` when `myAtom$` gets an error state. + */ + const mySafeAtom$ = myAtom$.fallbackTo(() => 3); + + expect(myAtom$.getState()).toBe(0); + expect(myAtom$.value).toBe(0); + expect(mySafeAtom$.value).toBe(0); + + myAtom$.unset(); + + expect(myAtom$.getState()).toBe(unresolved); + expect(myAtom$.value).toBeUndefined(); + expect(mySafeAtom$.value).toBe(3); + }); + + it('TEMP Flat-map', () => { + // const myAtom$ = atom(0); + // const mapping = (v: any) => atom(v); + // Sometimes you use `map()`, but the result within the `map()` is also a Derivable. + // The result would here be a `Derivable>` (hover over `derive` to see this). + // You would have to use `.get()` to go back to a single Derivable. Similarly how `flatMap` can + // reduce lists of lists to a single list, it can help reduce Derivables of Derivables to a + // single Derivable. If you have something like this: + // let myAtom$$ = myAtom$.map(n => mapping(n)).derive(v => v.get()); + // You can now rewrite it to this: + // myAtom$$ = myAtom$.flatMap(n => mapping(n)); + // It only results in slightly shorter code. + // TODO: right? + }); +}); + +/** + * !! Final States; (finalGetter, finalMethod, getMaybeFinalState, FinalWrapper, MaybeFinalState, _isFinal, makeFinal, markFinal, .final, .finalized, setFinal...) + * ? Lens; (libs/sherlock/src/lib/derivable/lens.ts) - map die twee kanten op kan gaan. Maar een map kan dat al? Maar hier kan + * je dat los definieren! Je kan gewoon `lens` ipv `var.lens`. Zelden dat je dit gebruikt. Output is een Derivable though. + * x Lift; (libs/sherlock-utils/src/lib/lift.ts) + * !! Peek; (libs/sherlock-utils/src/lib/peek.ts) - dan track je niet. In een derivable, deze tracked hij dan niet (ipv .get() waar het wel getracked wordt) + * x Template; (libs/sherlock-utils/src/lib/template.ts) - to make a string using a template literal. (Uses unwrap!!) + * / Factory; (libs/sherlock/src/lib/derivable/factories.ts) - simply contains functions to create objects, namely + * lens; atom; constant; derive. + * !! Flat-map; (libs/sherlock/src/lib/derivable/mixins/flat-map.ts) - ??? + * array: nested arrays naar array + * Derivable: gooit er derive.get() achteraan? + * Derivable (input van inputveld). Flatmap geeft Derivable terug. Derivable.flatmap() returned misschien + * Derivable, returned dan de number. flatMap is een `derive`, maar wat hij returned haalt hij uit de Derivable. + * ofzoiets. Maakt code korter. + * !! Fallback-to; - op een derivable. Als een atom `unresolved` is, dan fallt het back to this value. Ofwel, initial value, maar + * ook als hij later unresolved wordt, dan wordt hij dit (vaak wel initial value). + * !! Take - react options gebruiken buiten react. In een derivable chain, halverwege die options gebruiken. + * e.g. (from)Promise. Zodra die een waarde aanneemt kan hij niet meer veranderen. + * Let FromPromise, FromObservable, FromEventPattern ook uit (in utils?), ToPromise, ToObservable, in praktijk ook handig. + * FromEventPattern (haily mary, als alles niet werkt, dan dit doen). + */ diff --git a/tutorial/8 - advanced.test.ts b/generated_solution/7 - advanced.test.ts similarity index 69% rename from tutorial/8 - advanced.test.ts rename to generated_solution/7 - advanced.test.ts index 87a026a..18c8b6c 100644 --- a/tutorial/8 - advanced.test.ts +++ b/generated_solution/7 - advanced.test.ts @@ -1,5 +1,5 @@ -import { atom, constant, Derivable, derive, SettableDerivable } from '@skunkteam/sherlock'; -import { template } from '@skunkteam/sherlock-utils'; +import { atom, constant, Derivable, derive, SettableDerivable, unresolved } from '@skunkteam/sherlock'; +import { lift, template } from '@skunkteam/sherlock-utils'; import { Map as ImmutableMap } from 'immutable'; /** @@ -30,20 +30,17 @@ describe('advanced', () => { * What do you expect this `Derivable` to do on `.set()`, `.get()` etc? */ - // Remove this after taking your turn below. - expect(false).toBe(true); // .toThrow() or .not.toThrow()? ↴ (2x) - expect(() => c.get()).not.toThrow(); /* __YOUR_TURN__ */ - expect(() => c.set('new value')).toThrow() /* __YOUR_TURN__ */; + expect(() => c.get()).not.toThrow(); /* __YOUR_TURN__ */ + expect(() => c.set('new value')).toThrow() /* __YOUR_TURN__ */; }); it('`templates`', () => { - // Staying in the theme of redefining normal Typescript code in our Derivable language + // Staying in the theme of redefining normal Typescript code in our Derivable language, // we also have a special syntax to copy template literals to a Derivable. const one = 1; const myDerivable = template`I want to go to ${one} party`; - // expect(myDerivable.value).toBe(`I want to go to 1 party`); - expect(myDerivable.value).toBe(__YOUR_TURN__); /* __YOUR_TURN__ */ + expect(myDerivable.get()).toBe(`I want to go to 1 party`); }); /** @@ -67,21 +64,40 @@ describe('advanced', () => { * * Rewrite the `.get()`/`.set()` combos below using `.swap()`. */ - // Remove this after taking your turn below. - // expect(false).toBe(true); - myCounter$.swap(plusOne); + myCounter$.swap(plusOne); expect(myCounter$.get()).toEqual(1); - myCounter$.swap(plusOne); + myCounter$.swap(plusOne); expect(myCounter$.get()).toEqual(2); }); + /** + * You might want to use the reactor options such as + * `when`, `until`, and `skipFirst` when deriving as well. + * In such cases, you could use `.take()`. + */ + it('`.take()`', () => { + const myAtom$ = atom('denied'); + + /** + * ** Your Turn ** + * Use the `.take()` method on `myAtom$` to only accept the input string + * when it is `allowed`. + */ + const myLimitedAtom$ = myAtom$.take({ when: v => v.is('allowed') }); + + expect(myLimitedAtom$.resolved).toBe(false); + myAtom$.set('allowed'); + expect(myLimitedAtom$.resolved).toBe(true); + expect(myLimitedAtom$.value).toBe('allowed'); + }); + /** * As an alternative to `.get()` and `.set()`, there is also the `.value` * accessor. */ - describe('.value', () => { + describe('`.value`', () => { /** * `.value` can be used as an alternative to `.get()` and `.set()`. * This helps when a property is expected instead of two methods. @@ -94,19 +110,18 @@ describe('advanced', () => { * * Use the `.value` accessor to get the current value. */ - expect(myAtom$.value).toEqual('foo'); - + expect(myAtom$.value).toEqual('foo'); /** * ** Your Turn ** * * Now use the `.value` accessor to set a 'new value'. */ - myAtom$.value = 'new value'; + myAtom$.value = 'new value'; expect(myAtom$.get()).toEqual('new value'); }); - /** FIXME: SAME FOR ERRORS!! + /** * If a `Derivable` is `unresolved`, `.get()` will normally throw. * `.value` will return `undefined` instead. */ @@ -116,7 +131,7 @@ describe('advanced', () => { /** * ** Your Turn ** */ - expect(myAtom$.value).toEqual(undefined); + expect(myAtom$.value).toEqual(undefined); }); /** @@ -139,11 +154,13 @@ describe('advanced', () => { * We just created two `Derivable`s that are almost exactly the same. * But what happens when their source becomes `unresolved`? */ + expect(usingGet$.resolved).toEqual(true); expect(usingVal$.resolved).toEqual(true); myAtom$.unset(); expect(usingGet$.resolved).toEqual(false); expect(usingVal$.resolved).toEqual(true); + }); }); @@ -166,7 +183,7 @@ describe('advanced', () => { * * Use the `.map()` method to create the expected output below */ - const mappedAtom$: Derivable = myAtom$.map(base => base.toString().repeat(base)); + const mappedAtom$: Derivable = myAtom$.map(base => base.toString().repeat(base)); mappedAtom$.react(mapReactSpy); @@ -197,6 +214,7 @@ describe('advanced', () => { * We changed`myRepeat$` to equal 3. * Do you expect both reactors to have fired? And with what? */ + expect(deriveReactSpy).toHaveBeenCalledTimes(2); expect(deriveReactSpy).toHaveBeenLastCalledWith('hohoho', expect.toBeFunction()); @@ -209,6 +227,7 @@ describe('advanced', () => { * * And now that we have changed `myString$`? And when `myRepeat$` changed again? */ + expect(deriveReactSpy).toHaveBeenCalledTimes(3); expect(deriveReactSpy).toHaveBeenLastCalledWith('hahaha', expect.toBeFunction()); @@ -221,7 +240,7 @@ describe('advanced', () => { expect(mapReactSpy).toHaveBeenCalledTimes(3); expect(mapReactSpy).toHaveBeenLastCalledWith('haha', expect.toBeFunction()); - + /** * As you can see, a change in `myString$` will not trigger an * update. But if an update is triggered, `myString$` will be called @@ -247,7 +266,7 @@ describe('advanced', () => { // This first function is called when getting... n => -n, // ...and this second function is called when setting. - (newV, _) => -newV, + (newV, _) => -newV, ); // The original `atom` was set to 1, so we want the inverse to @@ -262,25 +281,93 @@ describe('advanced', () => { }); it('similar to `map()` on arrays', () => { - // if the similarity is not clear yet, here is a comparison between - // the normal `map()` on arrays and our `Derivable` `map()`. - // both get values out of a container (`Array` or `Derivable`), apply + // If the similarity is not clear yet, here is a comparison between + // the normal `.map()` on arrays and our `Derivable` `.map()`. + // Both get values out of a container (`Array` or `Derivable`), apply // some function, and put it back in the container. - const addOne: (v: number) => number = v => v + 1; + const addOne = jest.fn((v: number) => v + 1); const myList = [1, 2, 3]; - const myList2 = myList.map(addOne); - expect(myList2).toMatchObject([2, 3, 4]); + const myMappedList = myList.map(addOne); + expect(myMappedList).toMatchObject([2, 3, 4]); - const myDerivable = atom(1); - const myDerivable2 = myDerivable.map(addOne); - expect(myDerivable2.value).toBe(2); + const myAtom$ = atom(1); + let myMappedDerivable$ = myAtom$.map(addOne); + expect(myMappedDerivable$.value).toBe(2); + + // Or, as we have seen before, you can use `lift()` for this. + myMappedDerivable$ = lift(addOne)(myAtom$); + expect(myMappedDerivable$.value).toBe(2); - // you can combine them too - const myDerivable3 = atom([1, 2, 3]); - const myDerivable4 = myDerivable3.map(v => v.map(addOne)); - expect(myDerivable4.value).toMatchObject([2, 3, 4]); + // You can combine them too. + const myAtom2$ = atom([1, 2, 3]); + const myMappedDerivable2$ = myAtom2$.map(v => v.map(addOne)); + expect(myMappedDerivable2$.value).toMatchObject([2, 3, 4]); + }); + + /** + * In order to reason over the state of a Derivable, we can + * use `.mapState()`. This will map one state to another, and + * can be used to get rid of pesky `unresolved` or `Errorwrapper` + * states (or to introduce them!). + */ + it('`.mapState()`', () => { + const myAtom$ = atom(1); + + // like `.map()`, we can specify it both ways. + const myMappedAtom$ = myAtom$.mapState( + state => (state === unresolved ? 3 : state), // `myAtom$` => `myMappedAtom$` + state => (state === 2 ? unresolved : state), // `myMappedAtom$` => `myAtom$` + ); + + myAtom$.set(2); + expect(myAtom$.resolved).toBe(true); + expect(myMappedAtom$.resolved).toBe(true); + + myAtom$.unset(); + expect(myAtom$.resolved).toBe(false); + expect(myMappedAtom$.resolved).toBe(true); + + myMappedAtom$.set(2); + expect(myAtom$.resolved).toBe(false); + expect(myMappedAtom$.resolved).toBe(true); + + // This is a tricky one: + myMappedAtom$.unset(); + expect(myAtom$.resolved).toBe(false); + expect(myMappedAtom$.resolved).toBe(true); + + /** + * The results, especially of the last case, may seem weird. + * In the first exercise, `myAtom$` is set to 2, causing the state to be 2 as well. + * By setting the state of `myAtom$`, the first line of `mapState()` is triggered. + * Since `2` is not equal to `unresolved`, we return the state `2`, causing + * `myMappedAtom$` to also get state 2 (and thus: value 2). Neither are unresolved. + * + * In the second case, `myAtom$` is set to `unresolved`, triggering the first line of + * `mapState()`, letting `myMappedAtom$` become 3. `myAtom$` is now `unresolved`, and + * `myMappedAtom$` is not. + * + * In the third case, `myMappedAtom$` is set to 2, it triggers the second line of + * `mapState()`, causing `myAtom$` to become `unresolved`. However, what we don't + * notice is that this change in state triggers the first line of `mapState()` again, + * causing `myMappedAtom$` to get state `3`. We can check this: + */ + + myMappedAtom$.set(2); + expect(myMappedAtom$.get()).toBe(3); // the state and value are linked, so this is identical to `.getState()` + /** + * You might think that this change in state would cause `myAtom$` to now also get + * `3` as its state, but this does not happen. Why not? TODO: maximally one cycle? + * Since both `2` and `3` are not `unresolved`, it does not matter to our answer. + * + * The same cannot be said for the fourth case. Setting `myMappedAtom$` to `unresolved` + * triggers the second line of `mapState()`, causing `myAtom$` to also become `unresolved`. This, in turn, + * triggers the first line of `mapState()`, causing `myMappedAtom$` to become `3`. + * As such, `myMappedAtom$` is not `unresolved` even though we set it as such. + * TODO: change this to be for MAP. Then make MAPSTATE a trivial one right after. + */ }); }); @@ -323,7 +410,7 @@ describe('advanced', () => { * * * Hint: you'll have to cast the result from `.pluck()`. */ - firstProp$ = myMap$.pluck('firstProp') as SettableDerivable; + firstProp$ = myMap$.pluck('firstProp') as SettableDerivable; }); /** @@ -339,18 +426,18 @@ describe('advanced', () => { * What do you expect the plucked `Derivable` to look like? And what * happens when we `.set()` it? */ - expect(firstProp$.get()).toEqual('firstValue'); + expect(firstProp$.get()).toEqual('firstValue'); // the plucked `Derivable` should be settable firstProp$.set('other value'); // is the `Derivable` value the same as was set? - expect(firstProp$.get()).toEqual('other value'); + expect(firstProp$.get()).toEqual('other value'); // How many times was the spy called? Note the `skipFirst`.. - expect(reactPropSpy).toHaveBeenCalledTimes(1); + expect(reactPropSpy).toHaveBeenCalledTimes(1); // ...and what was the value? - expect(reactPropSpy).toHaveBeenLastCalledWith('other value', expect.toBeFunction()); + expect(reactPropSpy).toHaveBeenLastCalledWith('other value', expect.toBeFunction()); }); /** @@ -372,7 +459,7 @@ describe('advanced', () => { myMap$.swap(map => map.set('secondProp', 'new value')); // How many times was the spy called? Note the `skipFirst`. - expect(reactPropSpy).toHaveBeenCalledTimes(0); + expect(reactPropSpy).toHaveBeenCalledTimes(0); /** * ** Your Turn ** @@ -382,10 +469,10 @@ describe('advanced', () => { myMap$.swap(map => map.set('firstProp', 'new value')); // How many times was the spy called? Note the `skipFirst`.. - expect(reactPropSpy).toHaveBeenCalledTimes(1); + expect(reactPropSpy).toHaveBeenCalledTimes(1); // ...and what was the value? - expect(reactPropSpy).toHaveBeenLastCalledWith('new value', expect.toBeFunction()); + expect(reactPropSpy).toHaveBeenLastCalledWith('new value', expect.toBeFunction()); }); /** @@ -405,10 +492,12 @@ describe('advanced', () => { * So what if we set `firstProp$`? Does this propagate to the source * `Derivable`? */ + firstProp$.set('new value'); expect(reactSpy).toHaveBeenCalledTimes(1); expect(myMap$.get().get('firstProp')).toEqual('new value'); expect(myMap$.get().get('secondProp')).toEqual('secondValue'); + }); }); }); diff --git a/generated_solution/8 - utils.test.ts b/generated_solution/8 - utils.test.ts new file mode 100644 index 0000000..4fda7ad --- /dev/null +++ b/generated_solution/8 - utils.test.ts @@ -0,0 +1,382 @@ +import { atom, derive } from '@skunkteam/sherlock'; +import { lift, pairwise, peek, scan, struct } from '@skunkteam/sherlock-utils'; + +/** + * ** Your Turn ** + * + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +// Silence TypeScript's import not used errors. +expect(pairwise).toBe(pairwise); +expect(scan).toBe(scan); +expect(struct).toBe(struct); +expect(peek).toBe(peek); +expect(lift).toBe(lift); + +/** + * In the `sherlock-utils` lib, there are a couple of functions that can combine + * multiple values of a single `Derivable` or combine multiple `Derivable`s into + * one. We will show a couple of those here. + */ +describe('utils', () => { + /** + * As the name suggests, `pairwise()` will call the given function with both + * the current and the previous state. + * + * *Note: functions like `pairwise` and `scan` can be used with any callback, + * so it can be used both in a `.derive()` step and in a `.react()`* + */ + it('pairwise', () => { + const myCounter$ = atom(1); + const reactSpy = jest.fn(); + + /** + * ** Your Turn ** + * + * Now, use `pairwise()` to subtract the previous value from the + * current. + * + * *Hint: check the overloads of pairwise if you're struggling with + * `oldValue`.* + */ + myCounter$.derive(pairwise((newVal, oldVal) => newVal - oldVal, 0)).react(reactSpy); + // myCounter$.derive(pairwise((newVal, oldVal) => (oldVal ? newVal - oldVal : newVal))).react(reactSpy); // OR: alternatively. + + expect(reactSpy).toHaveBeenCalledTimes(1); + expect(reactSpy).toHaveBeenLastCalledWith(1, expect.toBeFunction()); + + myCounter$.set(3); + + expect(reactSpy).toHaveBeenCalledTimes(2); + expect(reactSpy).toHaveBeenLastCalledWith(2, expect.toBeFunction()); // 3 (current value of `myCounter$`) - 1 (previous value of `myCounter$`) + + myCounter$.set(10); + + expect(reactSpy).toHaveBeenCalledTimes(3); + expect(reactSpy).toHaveBeenLastCalledWith(7, expect.toBeFunction()); // 10 (current value of `myCounter$`) - 3 (previous value of `myCounter$`) + }); + + /** + * `scan()` is the `Derivable` version of `Array.prototype.reduce()`. It will be + * called with the current state and the last emitted value. + * + * (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce) + * + * *Note: as with `pairwise()` this is useable in both a `.derive()` and + * `.react()` method* + */ + it('scan', () => { + const myCounter$ = atom(1); + const reactSpy = jest.fn(); + + /** + * ** Your Turn ** + * + * Now, use `scan()` to subtract the previous value from the + * current. + * + * Note that `scan()` must return the same type as it gets as input. This is required + * as this returned value is also used for the accumulator (`acc`) value for the next call. + * This `acc` parameter of `scan()` is the last returned value, not the last value + * of `myCounter$`, as is the case with `pairwise()`. + */ + myCounter$.derive(scan((acc, val) => val - acc, 0)).react(reactSpy); + + expect(reactSpy).toHaveBeenCalledTimes(1); + expect(reactSpy).toHaveBeenLastCalledWith(1, expect.toBeFunction()); + + myCounter$.set(3); + + expect(reactSpy).toHaveBeenCalledTimes(2); + expect(reactSpy).toHaveBeenLastCalledWith(2, expect.toBeFunction()); // 3 (current value of `myCounter$`) - 1 (previous returned value) + + myCounter$.set(10); + + expect(reactSpy).toHaveBeenCalledTimes(3); + expect(reactSpy).toHaveBeenLastCalledWith(8, expect.toBeFunction()); // 10 (current value of `myCounter$`) - 2 (previous returned value) + }); + + it('`pairwise()` on normal arrays', () => { + // Functions like `pairwise()` and `scan()` work on normal lists too. They are often + // used in combination with `map()` and `filter()`. + const myList = [1, 2, 3, 5, 10]; + let myList2: number[]; + + /** + * ** Your Turn ** + * + * Use a `pairwise()` combined with a `.map()` on `myList` + * to subtract the previous value from the current. + * + * Hint: do not use a lambda function! + */ + myList2 = myList.map(pairwise((newV, oldV) => newV - oldV, 0)); + expect(myList2).toMatchObject([1, 1, 1, 2, 5]); + + // However, we should be careful with this, as this does not always behave as intended. + myList2 = myList.map(v => pairwise((newV, oldV) => newV - oldV, 0)(v)); // copy the same implementation here + expect(myList2).toMatchObject([1, 2, 3, 5, 10]); + + // Even if we are more clear about what we pass, this unintended behavior does not go away. + myList2 = myList.map((v, _, _2) => pairwise((newV, oldV) => newV - oldV, 0)(v)); // copy the same implementation here + expect(myList2).toMatchObject([1, 2, 3, 5, 10]); + + // `pairwise()` keeps track of the previous value under the hood. Using a lambda of + // the form `v => pairwise(...)(v)` would create a new `pairwise` function every call, + // essentially resetting the previous value every call. And resetting the previous value + // to 0 causes the input to stay the same (after all: x - 0 = x). + // Other than by not using a lambda function, we can fix this by + // saving the `pairwise` in a variable and reusing it for every call. + + let f = pairwise((newV, oldV) => newV - oldV, 0); + myList2 = myList.map(v => f(v)); + expect(myList2).toMatchObject([1, 1, 1, 2, 5]); + + // To get more insight in the `pairwise()` function, you can call it + // manually. Here, we show what happens under the hood. + + f = pairwise((newV, oldV) => newV - oldV, 0); + + myList2 = []; + myList2[0] = f(myList[0]); // f is newly created with `init = 0`, so applies `1 - 0`. + myList2[1] = f(myList[1]); // f has saved `1` internally, so applies `2 - 1`. + myList2[2] = f(myList[2]); // f has saved `2` internally, so applies `3 - 2`. + myList2[3] = f(myList[3]); // f has saved `3` internally, so applies `5 - 3`. + myList2[4] = f(myList[4]); // f has saved `5` internally, so applies `10 - 5`. + + expect(myList2).toMatchObject([1, 1, 1, 2, 5]); + // This also works for functions other than `.map()`, such as `.filter()`. + + /** ** Your Turn ** + * Use `pairwise()` to filter out all values which produce `1` when subtracted + * with their previous value. + */ + myList2 = myList.filter(pairwise((newV, oldV) => newV - oldV === 1, 0)); + expect(myList2).toMatchObject([1, 2, 3]); + }); + + it('`scan()` on normal arrays', () => { + // As with `pairwise()` in the last test, `scan()` can be used with arrays too. + const myList = [1, 2, 3, 5, 10]; + let myList2: number[]; + + /** + * ** Your Turn ** + * + * Use a `scan()` combined with a `map` on `myList` + * to subtract the previous value from the current. + */ + + let f: (v: number) => number = scan((acc, val) => val - acc, 0); + myList2 = myList.map(f); + + expect(myList2).toMatchObject([1, 1, 2, 3, 7]); + + // again, it is useful to consider what happens internally. + f(7); // resets the internal `acc` value to 0, as the current `acc` value was 7, and 7-7 = 0. + + myList2 = []; + myList2[0] = f(myList[0]); // 1 :: `f` is newly created with `init = 0`, so applies `1 - 0 = 1`. + myList2[1] = f(myList[1]); // 1 :: `f` has saved the result `1` internally, so applies `2 - 1 = 1`. + myList2[2] = f(myList[2]); // 2 :: `f` has saved the result `1` internally, so applies `3 - 1 = 2`. + myList2[3] = f(myList[3]); // 3 :: `f` has saved the result `2` internally, so applies `5 - 2 = 3`. + myList2[4] = f(myList[4]); // 7 :: `f` has saved the result `3` internally, so applies `10 - 3 = 7`. + + expect(myList2).toMatchObject([1, 1, 2, 3, 7]); + + // This also works for functions other than `map()`, such as `filter()`. + // Use `scan()` to filter out all values from `myList` which produce a value + // of 8 or higher when added with the previous result. In other words, it should + // go through `myList` and add the values producing: (1), (1+2), (1+2+3), (1+2+3+5), + // (1+2+3+5+10), and since this sum only prouces a value higher than 8 when the + // values `5` and `10` are added, the result should be `[5,10]`. + + f = scan((acc, val) => val + acc, 0); + myList2 = myList.filter(v => f(v) >= 8); + expect(myList2).toMatchObject([5, 10]); + }); + + it('pairwise - BONUS', () => { + const myCounter$ = atom(1); + let reactSpy = jest.fn(); + + /** + * ** Your Turn ** + * ** BONUS ** + * + * Now, use `pairwise()` directly in `.react()`. Implement the same + * derivation as before: subtract the previous value from the current. + */ + reactSpy = jest.fn(pairwise((newV, oldV) => newV - oldV, 0)); + myCounter$.react(reactSpy); + + expect(reactSpy).toHaveLastReturnedWith(1); + + myCounter$.set(3); + + expect(reactSpy).toHaveLastReturnedWith(2); + + myCounter$.set(10); + + expect(reactSpy).toHaveLastReturnedWith(7); + }); + + it('scan - BONUS', () => { + const myCounter$ = atom(1); + let reactSpy = jest.fn(); + + /** + * ** Your Turn ** + * ** BONUS ** + * + * Now, use `scan()` directly in `.react()`. Implement the same + * derivation as before: subtract all the emitted values. + */ + + reactSpy = jest.fn(scan((acc, val) => val - acc, 0)); + myCounter$.react(reactSpy); + + expect(reactSpy).toHaveLastReturnedWith(1); + + myCounter$.set(3); + + expect(reactSpy).toHaveLastReturnedWith(2); + + myCounter$.set(10); + + expect(reactSpy).toHaveLastReturnedWith(8); + }); + + /** + * A `struct()` can combine an Object/Array of `Derivable`s into one + * `Derivable`, that contains the values of that `Derivable`. + * + * The Object/Array that is in the output of `struct()` will have the same + * structure as the original Object/Array. + * + * This is best explained in practice. + */ + it('struct', () => { + const allMyAtoms = { + regularProp: 'prop', + string: atom('my string'), + number: atom(1), + sub: { + string: atom('my substring'), + }, + }; + + const myOneAtom$ = struct(allMyAtoms); + + expect(myOneAtom$.get()).toEqual({ + regularProp: 'prop', + string: 'my string', + number: 1, + sub: { + string: 'my substring', + }, + }); + + // Note: we change the original object, not the struct. + allMyAtoms.regularProp = 'new value'; + allMyAtoms.sub.string.set('my new substring'); + + /** + * ** Your Turn ** + * + * Now have a look at the properties of `myOneAtom$`. Is this what you + * expect? + */ + + expect(myOneAtom$.get()).toEqual({ + regularProp: 'new value', + string: 'my string', + number: 1, + sub: { + string: 'my new substring', + }, + }); + + }); + + describe('lift()', () => { + /** + * Derivables can feel like a language build on top of Typescript. Sometimes + * you might want to use normal objects and functions and not have to rewrite + * your code. + * In other words, just like keywords like `atom(V)` lifts a variable V to the higher + * level of Derivables, the `lift(F)` keyword lifts a function `F` to the higher + * level of Derivables. + */ + it('example', () => { + // Example: after years of effort, Bob finally finished his oh-so complicated function: + const isEvenNumber = (v: number) => v % 2 == 0; + + // Rewriting this function to work with derivables would now be a waste of time. + /** + * ** Your Turn ** + * Use the `lift()` function to change `isEvenNumber` to work on Derivables instead. + * In other words: the new function should take a `Derivable` (or more specifically: + * an `Unwrappable`) and return a `Derivable`. + */ + const isEvenDerivable = lift(isEvenNumber); + + expect(isEvenNumber(2)).toBe(true); + expect(isEvenNumber(13)).toBe(false); + expect(isEvenDerivable(atom(2)).get()).toBe(true); + expect(isEvenDerivable(atom(13)).get()).toBe(false); + }); + + it('`lift()` as alternative to `.map()`', () => { + // In tutorial 7, we saw `.map()` used in the following context: + const addOne = jest.fn((v: number) => v + 1); + const myAtom$ = atom(1); + + let myMappedDerivable$ = myAtom$.map(addOne); + + expect(myMappedDerivable$.value).toBe(2); + + /** + * ** Your Turn ** + * Now, use `lift()` as alternative to `.map()`. + */ + myMappedDerivable$ = lift(addOne)(myAtom$); + + expect(myMappedDerivable$.value).toBe(2); + }); + }); + + /** + * Sometimes you want to use `derive` but still want to keep certain + * variables in it untracked. In such cases, you can use `peek()`. + */ + it('`peek()`', () => { + const myTrackedAtom$ = atom(1); + const myUntrackedAtom$ = atom(2); + + /** + * ** Your Turn ** + * Use `peek()` to get the value of `myUntrackedAtom$` and add it to the + * value of `myTrackedAtom$`, which should be tracked. + */ + const reactor = jest.fn(v => v); + derive(() => myTrackedAtom$.get() + peek(myUntrackedAtom$)).react(reactor); + + expect(reactor).toHaveBeenCalledOnce(); + expect(reactor).toHaveLastReturnedWith(3); + + myTrackedAtom$.set(2); + expect(reactor).toHaveBeenCalledTimes(2); + expect(reactor).toHaveLastReturnedWith(4); + + myUntrackedAtom$.set(3); + expect(reactor).toHaveBeenCalledTimes(2); + expect(reactor).toHaveLastReturnedWith(4); + + myTrackedAtom$.set(3); + expect(reactor).toHaveBeenCalledTimes(3); + expect(reactor).toHaveLastReturnedWith(6); + }); +}); diff --git a/tutorial/9 - expert.test.ts b/generated_solution/9 - expert.test.ts similarity index 89% rename from tutorial/9 - expert.test.ts rename to generated_solution/9 - expert.test.ts index 5b3e1c1..49830ed 100644 --- a/tutorial/9 - expert.test.ts +++ b/generated_solution/9 - expert.test.ts @@ -32,7 +32,7 @@ describe('expert', () => { */ // `.toHaveBeenCalled()` or `.not.toHaveBeenCalled()`? ↴ - expect(hasDerived).not.toHaveBeenCalled() /* Your Turn */; + expect(hasDerived).not.toHaveBeenCalled(); mySecondDerivation$.get(); @@ -44,7 +44,7 @@ describe('expert', () => { * first `Derivable` actually executed its derivation? */ // how many times? - expect(hasDerived).toHaveBeenCalledTimes(3); + expect(hasDerived).toHaveBeenCalledTimes(3); }); /** @@ -68,7 +68,7 @@ describe('expert', () => { * expectations pass. */ const myAtom$ = atom(true); - const myFirstDerivation$ = myAtom$.derive(firstHasDerived).autoCache(); + const myFirstDerivation$ = myAtom$.derive(firstHasDerived).autoCache(); const mySecondDerivation$ = myFirstDerivation$.derive(() => secondHasDerived(myFirstDerivation$.get() + myFirstDerivation$.get()), ); @@ -110,9 +110,9 @@ describe('expert', () => { mySecondDerivation$.get(); // first after last .get() - expect(firstHasDerived).toHaveBeenCalledTimes(2); // Ohhhh, it does not reset the value of last time. It just adds to it. But caches again apparently, because 2, not 4. Weetnie, is met `clear` niet beter? + expect(firstHasDerived).toHaveBeenCalledTimes(2); // second after last .get() - expect(secondHasDerived).toHaveBeenCalledTimes(3); + expect(secondHasDerived).toHaveBeenCalledTimes(3); }); }); @@ -182,10 +182,14 @@ describe('expert', () => { * But does that apply here? * How many times has the setup run, for the price `Derivable`. */ - expect(stockPrice$).toHaveBeenCalledTimes(2); + expect(stockPrice$).toHaveBeenCalledTimes(2); /** Can you explain this behavior? */ - // Yes: it creates a different Derivable every time, so it cannot use any caching! A similar issue to the `pairwise()` issue from tutorial 7. + // ANSWER-BLOCK-START + // Yes: it creates a different Derivable every time, so it cannot use any caching. + // This is a similar issue to the `pairwise()` issue from tutorial 7, where, when we + // used lambda functions, we made a new pairwise object every time. + // ANSWER-BLOCK-END }); /** @@ -230,19 +234,19 @@ describe('expert', () => { */ // How often was the reactor on price$ called? - expect(reactSpy).toHaveBeenCalledTimes(0); + expect(reactSpy).toHaveBeenCalledTimes(0); // And how many times did the setup run? - expect(stockPrice$).toHaveBeenCalledTimes(2); + expect(stockPrice$).toHaveBeenCalledTimes(2); // What's the value of price$ now? - expect(price$.value).toEqual(undefined); + expect(price$.value).toEqual(undefined); // And the value of googlPrice$? - expect(googlPrice$.value).toEqual(1079.11); + expect(googlPrice$.value).toEqual(1079.11); // Is googlPrice$ still even driving any reactors? - expect(googlPrice$.connected).toEqual(false); + expect(googlPrice$.connected).toEqual(false); /** * Can you explain this behavior? @@ -301,14 +305,12 @@ describe('expert', () => { */ .map(companies => companies.map(company => stockPrice$(company))) // Then we get the prices from the created `Derivable`s in a separate step - .derive(price$s => price$s.map(price$ => price$.value)); // TODO: yeah, you lost me. I need to dive into `.map()` again... - // So, in practice: the `.map()` maps the `companies` to a new Derivable, one where the list is changed to a new list of prices. - // We save this new Derivable in `prices$`, and then this new list is derived on such that, when it changes, we do something with it. + .derive(price$s => price$s.map(price$ => price$.value)); prices$.react(reactor); // Because we use `.value` instead of `.get()` the reactor - // should emit immediately this time. TODO: is there truly a difference? + // should emit immediately this time. // But it should emit `undefined`. expect(reactSpy).toHaveBeenCalledExactlyOnceWith([undefined]); @@ -327,8 +329,8 @@ describe('expert', () => { * * So the value was increased. What do you think happened now? */ - expect(reactSpy).toHaveBeenCalledTimes(2); - expect(reactSpy).toHaveBeenLastCalledWith([1079.11]); + expect(reactSpy).toHaveBeenCalledTimes(2); + expect(reactSpy).toHaveBeenLastCalledWith([1079.11]); /** * So that worked, now let's try and add another company to the @@ -346,10 +348,8 @@ describe('expert', () => { * * We had a price for 'GOOGL', but not for 'APPL'... */ - expect(reactSpy).toHaveBeenCalledTimes(3); - expect(reactSpy).toHaveBeenCalledWith([undefined, undefined]); - // Because companies was the central component on which a derive lied... - // ...changing it made the whole thing reset?? Idk man. + expect(reactSpy).toHaveBeenCalledTimes(3); + expect(reactSpy).toHaveBeenCalledWith([undefined, undefined]); }); }); @@ -409,7 +409,7 @@ describe('expert', () => { * * Has anything changed, by using the `derivableCache`? */ - expect(stockPrice$).toHaveBeenCalledTimes(1); + expect(stockPrice$).toHaveBeenCalledTimes(1); // Now let's resolve the price stockPrice$.mock.results[0].value.set(1079.11); @@ -422,10 +422,10 @@ describe('expert', () => { * * What happens this time? Has the setup run again? */ - expect(stockPrice$).toHaveBeenCalledTimes(1); + expect(stockPrice$).toHaveBeenCalledTimes(1); // Ok, but did it update the HTML? - expect(reactSpy).toHaveBeenCalledTimes(2); - expect(lastEmittedHTMLs()[0]).toContain('$ 1079.11'); + expect(reactSpy).toHaveBeenCalledTimes(2); + expect(lastEmittedHTMLs()[0]).toContain('$ 1079.11'); // Last chance, what if we add a company companies$.swap(current => [...current, 'APPL']); @@ -438,12 +438,12 @@ describe('expert', () => { * * But did it calculate 'GOOGL' again too? */ - expect(stockPrice$).toHaveBeenCalledTimes(2); - expect(reactSpy).toHaveBeenCalledTimes(3); + expect(stockPrice$).toHaveBeenCalledTimes(2); + expect(reactSpy).toHaveBeenCalledTimes(3); // The first should be the generated HTML for 'GOOGL'. - expect(lastEmittedHTMLs()[0]).toContain('$ 1079.11'); + expect(lastEmittedHTMLs()[0]).toContain('$ 1079.11'); // The second should be the generated HTML for 'APPL'. - expect(lastEmittedHTMLs()[1]).toContain('$ unknown'); + expect(lastEmittedHTMLs()[1]).toContain('$ unknown'); }); }); }); diff --git a/generated_solution/jest.config.ts b/generated_solution/jest.config.ts new file mode 100644 index 0000000..1291563 --- /dev/null +++ b/generated_solution/jest.config.ts @@ -0,0 +1,18 @@ +import type { Config } from 'jest'; + +export default { + displayName: 'generated_solution', + preset: '../jest.preset.js', + globals: {}, + testEnvironment: 'node', + transform: { + '^.+\\.[tj]sx?$': [ + 'ts-jest', + { + tsconfig: '/tsconfig.spec.json', + }, + ], + }, + collectCoverage: false, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], +} satisfies Config; diff --git a/tutorial/tsconfig.json b/generated_solution/tsconfig.json similarity index 100% rename from tutorial/tsconfig.json rename to generated_solution/tsconfig.json diff --git a/tutorial/tsconfig.spec.json b/generated_solution/tsconfig.spec.json similarity index 100% rename from tutorial/tsconfig.spec.json rename to generated_solution/tsconfig.spec.json diff --git a/generated_tutorial/1 - intro.test.ts b/generated_tutorial/1 - intro.test.ts new file mode 100644 index 0000000..7bf0fc6 --- /dev/null +++ b/generated_tutorial/1 - intro.test.ts @@ -0,0 +1,140 @@ +import { atom } from '@skunkteam/sherlock'; + +/** + * ** Your Turn ** + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +/** + * Welcome to the `@skunkteam/sherlock` tutorial. + * + * It is set up as a collection of specs, with the goal of getting all the specs + * to pass. The `expect()`s and basic setup are there, you just need to get it + * to work. + * + * All specs except the first one are set to `.skip`. Remove this to start on + * that part of the tutorial. + * + * Start the tutorial by running: + * `npm run tutorial`. + * + * To not manually re-enter the command, use: + * `npm run tutorial -- --watch` + * This will automatically rerun the tests when a file change has been detected. + * + * *Hint: most methods and functions are fairly well documented in jsDoc, + * which is easily accessed through TypeScript* + */ +describe.skip('intro', () => { + it(` + + --- Welcome to the tutorial! --- + + Please look in \`./tutorial/1 - intro.test.ts\` to see what to do next.`, () => { + // At the start of the spec, there will be some setup. + let bool = false; + + // Sometimes including an expectation, to show the current state. + expect(bool).toBeFalse(); + + /** + * If ** Your Turn ** is shown in a comment, there is work for you to do. + * This can also be indicated with the `__YOUR_TURN__` variable. + * + * It should be clear what to do here... */ + bool = __YOUR_TURN__; + expect(bool).toBeTrue(); + // We use expectations like this to verify the result. + }); +}); + +/** + * Let's start with the `Derivable` basics. + * + * ** Your Turn ** + * Remove the `.skip` so this part of the tutorial will run. + */ +describe.skip('the basics', () => { + /** + * The `Atom` is the basic building block of `@skunkteam/sherlock`. + * It holds a value which you can `get()` and `set()`. + */ + it('the `Atom`', () => { + // An `Atom` can be created with the `atom()` function. The parameter + // of this function is used as the initial value of the `Atom`. + const myValue$ = atom(1); + // Variables containing `Atom`s or any other `Derivable` are usually + // postfixed with a `$` to indicate this. Hence `myValue$`. + + // The `.get()` method can be used to get the current value of + // the `Atom`. + expect(myValue$.get()).toEqual(1); + + // ** Your Turn ** + // Use the `.set()` method to change the value of the `Atom`. + expect(myValue$.get()).toEqual(2); + }); + + /** + * The `Atom` is a `Derivable`. This means it can be used to create a + * derived value. This derived value stays up to date with the original + * `Atom`. + * + * The easiest way to do this, is to call `.derive()` on another + * `Derivable`. + * + * Let's try this. + */ + it('the `Derivable`', () => { + const myValue$ = atom(1); + expect(myValue$.get()).toEqual(1); + + /** + * ** Your Turn ** + * + * We want to create a new `Derivable` that outputs the inverse (from a + * negative to a positive number and vice versa) of the original `Atom`. + */ + // Use `myValue$.derive(val => ...)` to implement `myInverse$`. + const myInverse$ = myValue$.derive(__YOUR_TURN__ => __YOUR_TURN__); + expect(myInverse$.get()).toEqual(-1); + // So if we set `myValue$` to -2: + myValue$.set(-2); + // `myInverse$` will change accordingly. + expect(myInverse$.get()).toEqual(2); + }); + + /** + * Of course, `Derivable`s are not only meant to get, set and derive state. + * You can also listen to the changes. + * + * This is done with the `.react()` method. + * This method is given a function that is executed every time the value of + * the `Derivable` changes. + */ + it('reacting to `Derivable`s', () => { + const myCounter$ = atom(0); + let reacted = 0; + + /** + * ** Your Turn ** + * + * Now react to `myCounter$`. In every `react()`. + * Increase the `reacted` variable by one. */ + myCounter$.react(() => __YOUR_TURN__); + expect(reacted).toEqual(1); + // `react()` will react immediately, more on that later. + + /** + * And then we set the `Atom` a couple of times + * to make the `Derivable` react. + * */ + for (let i = 0; i <= 100; i++) { + // Set the value of the `Atom`. + myCounter$.set(i); + } + + expect(reacted).toEqual(101); + }); +}); diff --git a/generated_tutorial/2 - deriving.test.ts b/generated_tutorial/2 - deriving.test.ts new file mode 100644 index 0000000..f25b808 --- /dev/null +++ b/generated_tutorial/2 - deriving.test.ts @@ -0,0 +1,226 @@ +import { atom, Derivable, derive } from '@skunkteam/sherlock'; + +/** + * ** Your Turn ** + * + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +/** + * Any `Derivable` (including `Atom`s) can be used (and/or combined) to create + * a derived state. This derived state is in turn a `Derivable`. + * + * There are a couple of ways to do this. + */ +describe.skip('deriving', () => { + /** + * In the 'intro' we have created a derivable by using the `.derive()` method. + * This method allows the state of that `Derivable` to be used to create a + * new `Derivable`. + * + * In the derivation, other `Derivable`s can be used as well. + * If a `Derivable.get()` is called inside a derivation, the changes to that + * `Derivable` are also tracked and kept up to date. + */ + it('combining `Derivable`s', () => { + const repeat$ = atom(1); + const text$ = atom(`It won't be long`); + + /** + * ** Your Turn ** + * + * Let's create some lyrics by combining `text$` and `repeat$`. + * As you might have guessed, we want to repeat the text a couple of times. + * + * (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat should do fine) + */ + + // We can combine txt with `repeat$.get()` here. + const lyric$ = text$.derive(txt => txt /* __YOUR_TURN__ */); + + expect(lyric$.get()).toEqual(`It won't be long`); + + text$.set(' yeah'); + repeat$.set(3); + expect(lyric$.get()).toEqual(` yeah yeah yeah`); + }); + + /** + * Now that we have used `.get()` in a `.derive()`. You may wonder, can + * we skip the original `Derivable` and just call the function `derive()`? + * + * Of course you can! + * + * And you can use any `Derivable` you want, even if they all have the same + * `Atom` as a parent. + */ + it('the `derive()` function', () => { + const myCounter$ = atom(1); + + /** + * ** Your Turn ** + * + * Let's try creating a `Derivable` [FizzBuzz](https://en.wikipedia.org/wiki/Fizz_buzz). + * `fizzBuzz$` should combine `fizz$`, `buzz$` and `myCounter$` to + * produce the correct output. + * + * Multiple `Derivable`s can be combined to create a new one. To do + * this, just use `.get()` on (other) `Derivable`s in the `.derive()` + * step. + * + * This can be done both when `derive()` is used standalone or as a + * method on another `Derivable`. + */ + + // Should return 'Buzz' when `myCounter$` is a multiple of 5 and '' otherwise. + + // Should return 'Fizz' when `myCounter$` is a multiple of 3 and '' otherwise. + const fizz$: Derivable = myCounter$.derive(__YOUR_TURN__); + + // Should return 'Buzz' when `myCounter$` is a multiple of 5 and '' otherwise. + const buzz$: Derivable = myCounter$.derive(__YOUR_TURN__); + + const fizzBuzz$: Derivable = derive(__YOUR_TURN__); + + expect(fizz$.get()).toEqual(''); + expect(buzz$.get()).toEqual(''); + expect(fizzBuzz$.get()).toEqual(1); + for (let count = 1; count <= 100; count++) { + // Set the value of the `Atom`, + myCounter$.set(count); + + // and check if the output changed accordingly. + checkFizzBuzz(count, fizzBuzz$.get()); + } + }); + + function checkFizzBuzz(count: number, out: string | number) { + if ((count % 3) + (count % 5) === 0) { + // If `count` is a multiple of 3 AND 5, output 'FizzBuzz'. + expect(out).toEqual('FizzBuzz'); + } else if (count % 3 === 0) { + // If `count` is a multiple of 3, output 'Fizz'. + expect(out).toEqual('Fizz'); + } else if (count % 5 === 0) { + // If `count` is a multiple of 5, output 'Buzz'. + expect(out).toEqual('Buzz'); + } else { + // Otherwise just output the `count` itself. + expect(out).toEqual(count); + } + } + + /** + * The automatic tracking of `.get()` calls will also happen inside called + * `function`s. + * + * This can be really powerful, but also dangerous. One of the dangers is + * shown here. + */ + it('indirect derivations', () => { + const pastTweets = [] as string[]; + const currentUser$ = atom('Barack'); + const tweet$ = atom('First tweet'); + + function log(tweet: string) { + pastTweets.push(`${currentUser$.get()} - ${tweet}`); + } + + tweet$.derive(log).react(txt => { + // Normally we would do something with the tweet here. + return txt; + }); + + // The first tweet should have automatically been added to the `pastTweets` array. + expect(pastTweets).toHaveLength(1); + expect(pastTweets[0]).toContain('Barack'); + expect(pastTweets[0]).toContain('First tweet'); + + // Let's add a famous quote by Mr Barack: + tweet$.set('We need to reject any politics that targets people because of race or religion.'); + // As expected this is automatically added to the log. + expect(pastTweets).toHaveLength(2); + expect(pastTweets[1]).toContain('Barack'); + expect(pastTweets[1]).toContain('reject'); + + // But what if the user changes? + currentUser$.set('Donald'); + + /** + * ** Your Turn ** + * + * Time to set your own expectations. + */ + const tweetCount = pastTweets.length; + const lastTweet = pastTweets[tweetCount - 1]; + + expect(tweetCount).toEqual(__YOUR_TURN__); // Is there a new tweet? + expect(lastTweet).toContain(__YOUR_TURN__); // Who sent it? Donald? Or Barack? + expect(lastTweet).toContain(__YOUR_TURN__); // What did he tweet? + + /** + * As you can see, this is something to look out for. + * Luckily there are ways to circumvent this. But more on that later. + * + * * Note that this behavior can also be really helpful if you know what + * you are doing * + */ + }); + + /** + * Every `Derivable` has a couple of convenience methods. + * These are methods that make common derivations a bit easier. + * + * These methods are: `.and()`, `.or()`, `.is()` and `.not()`. + * + * Their function is as you would expect from `boolean` operators in a + * JavaScript environment. + * + * The first three will take a `Derivable` or regular value as parameter. + * `.not()` does not need any input. + * + * `.is()` will resolve equality in the same way as `@skunkteam/sherlock` + * would do internally. + * + * More on the equality check in the 'inner workings' part. But know that + * the first check is [Object.is()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) + */ + it('convenience methods', () => { + const myCounter$ = atom(1); + + /** + * ** Your Turn ** + * + * The FizzBuzz example above can be rewritten using the convenience + * methods. This is not how you would normally write it, but it looks + * like a fun excercise. + * + * `fizz$` and `buzz$` can be completed with only `.is(...)`, + * `.and(...)` and `.or(...)`. Make sure the output of those `Derivable`s + * is either 'Fizz'/'Buzz' or ''. + */ + + const fizz$ = myCounter$ + .derive(count => count % 3) + .is(__YOUR_TURN__) + .and(__YOUR_TURN__) + .or(__YOUR_TURN__) as Derivable; + + const buzz$ = myCounter$ + .derive(count => count % 5) + .is(__YOUR_TURN__) + .and(__YOUR_TURN__) + .or(__YOUR_TURN__) as Derivable; + + const fizzBuzz$ = derive(() => fizz$.get() + buzz$.get()).or(__YOUR_TURN__); + + for (let count = 1; count <= 100; count++) { + // Set the value of the `Atom`, + myCounter$.set(count); + + // and check if the output changed accordingly. + checkFizzBuzz(count, fizzBuzz$.get()); + } + }); +}); diff --git a/generated_tutorial/3 - reacting.test.ts b/generated_tutorial/3 - reacting.test.ts new file mode 100644 index 0000000..cf732b7 --- /dev/null +++ b/generated_tutorial/3 - reacting.test.ts @@ -0,0 +1,547 @@ +import { atom } from '@skunkteam/sherlock'; + +/** + * ** Your Turn ** + * + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +// FIXME: check my solutions with the actual solutions +// FIXME: remove all TODO: and FIXME: +// FIXME: check whether the generated tutorials and solutions actually work (e.g. are all solutions correct? No weird shenanigans?) +// FIXME: deze file niet linten / builden (voor automatische test). Tutorial ook niet. Maar solutions juist wel! OP EIND. +// FIXME: interne review document, mocht ik iets hebben om te laten zien! In Google Drive, zet het erin! +// FIXME: werkt `npm run tutorial` nog??? + +/** + * In the intro we have seen a basic usage of the `.react()` method. + * Let's dive a bit deeper into the details of this method. + */ +describe.skip('reacting', () => { + // For easy testing we can count the number of times a reactor was called, + let wasCalledTimes: number; + // and record the last value it reacted to. + let lastValue: any; + + // reset the values before each test case + beforeEach(() => { + wasCalledTimes = 0; + lastValue = undefined; + }); + + // The reactor to be given to the `.react()` method. + function reactor(val: any) { + wasCalledTimes++; + lastValue = val; + } + + // Of course we are lazy and don't want to type these assertions over + // and over. :-) + function expectReact(reactions: number, value?: any) { + // Reaction was called # times + expect(wasCalledTimes).toEqual(reactions); + // Note the actual point of failure is: + // at Object. (3 - reacting.test.ts:LINE_NUMBER:_) + // V ~~~~~~~~~~~~ + + // Last value of the reaction was # + expect(lastValue).toEqual(value); + } + + /** + * Every `Derivable` always has a current state. So the `.react()` method + * does not need to wait for a value, there already is one. + * + * This means that `.react()` will fire directly when called. When the + * `Derivable` has a new state, this will also fire `.react()` + * synchronously. + * + * So the very next line after `.set()` is called, the `.react()` has + * already fired! + * + * (Except when the `Derivable` is `unresolved`, but more on that later.) + */ + it('reacting synchronously', () => { + const myAtom$ = atom('initial value'); + // A trivial `expect` to silence TypeScript's noUnusedLocals. + expect(myAtom$.get()).toEqual('initial value'); + + // There should not have been a reaction yet + expectReact(0); + + /** + * ** Your Turn ** + * + * Time to react to `myAtom$` with the `reactor()` function defined + * above. + */ + __YOUR_TURN__; + + expectReact(1, 'initial value'); + + // Now set a 'new value' to `myAtom$`. + myAtom$.set('new value'); + + expectReact(2, 'new value'); + }); + + /** + * A reactor will go on forever. This is often not what you want, and almost + * always a memory leak. + * + * So it is important to stop a reactor at some point. The `.react()` method + * has different ways of dealing with this. + */ + describe.skip('stopping a reaction', () => { + /** + * The easiest is the 'stopper' function, every `.react()` call will + * return a `function` that will stop the reaction. + */ + it('with the stopper function', () => { + const myAtom$ = atom('initial value'); + // A trivial `expect` to silence TypeScript's noUnusedLocals + expect(myAtom$.get()).toEqual('initial value'); + + /** + * ** Your Turn ** + * + * catch the returned `stopper` in a variable + */ + __YOUR_TURN__; + + expectReact(1, 'initial value'); + + /** + * ** Your Turn ** + * + * Call the `stopper`. + */ + __YOUR_TURN__; + + myAtom$.set('new value'); + + // And the reaction stopped. + expectReact(1, 'initial value'); + }); + + /** + * Everytime the reaction is called, it also gets the stopper `function` + * as a second parameter. + */ + it('with the stopper callback', () => { + const myAtom$ = atom('initial value'); + // A trivial `expect` to silence TypeScript's noUnusedLocals + expect(myAtom$.get()).toEqual('initial value'); + + /** + * ** Your Turn ** + * + * In the reaction below, use the stopper callback to stop the + * reaction + */ + + myAtom$.react((val, __YOUR_TURN___) => { + reactor(val); + __YOUR_TURN___; + }); + + expectReact(1, 'initial value'); + + myAtom$.set('new value'); + + // And the reaction stopped. + expectReact(1, 'initial value'); + }); + }); + + /** + * The reactor `options` are a way to modify when and how the reactor will + * react to changes in the `Derivable`. + */ + describe.skip('reactor options', () => { + /** + * Another way to make a reactor stop at a certain point, is by + * specifying an `until` in the `options`. + * `until` can be given either a `Derivable` or a `function`. + * + * If a `function` is given, this `function` will be given the + * `Derivable` that is the source of the reaction as a parameter. + * This `function` will track all `.get()`s, so can use any `Derivable`. + * It can return a `boolean` or a `Derivable`. + * + * *Note: the reactor options `when` and `from` can also be set to a + * `Derivable`/`function` as describe.skipd here.* + * + * The reactor will stop directly when `until` becomes true. + * If that happens at exactly the same time as the `Derivable` getting a + * new value, it will not react again. + */ + describe.skip('reacting `until`', () => { + const boolean$ = atom(false); + const string$ = atom('Value'); + beforeEach(() => { + // reset + boolean$.set(false); + string$.set('Value'); + }); + + /** + * If a `Derivable` is given, the reaction will stop once that + * `Derivable` becomes `true`/truthy. + */ + it('an external `Derivable`', () => { + /** + * ** Your Turn ** + * + * Try giving `boolean$` as `until` option. + */ + string$.react(reactor, __YOUR_TURN__); + + // It should react directly as usual. + expectReact(1, 'Value'); + + // It should keep reacting as usual. + string$.set('New value'); + expectReact(2, 'New value'); + + // We set `boolean$` to true, to stop the reaction + boolean$.set(true); + + // The reactor has immediately stopped, so it still reacted + // only twice: + expectReact(2, 'New value'); + + // Even when `boolean$` is set to `false` again... + boolean$.set(false); + + // ... and a new value is introduced: + string$.set('Another value'); + + // The reactor won't start up again, so it still reacted + // only twice: + expectReact(2, 'New value'); + }); + + /** + * A function can also be given as `until`. This function will be + * executed in every derivation. Just like using a `Derivable` as + * an `until`, the Reactor will keep reacting until the result of + * this function evaluates thruthy. + * + * This way any `Derivable` can be used in the calculation. + */ + it('a function', () => { + /** + * ** Your Turn ** + * + * Since the reactor options expect a boolean, you will + * sometimes need to calculate the option. + * + * Try giving an externally defined `function` that takes no + * parameters as `until` option. + * + * Use `!string$.get()` to return `true` when the `string` is + * empty. + */ + string$.react(reactor, __YOUR_TURN__); + + // It should react as usual: + string$.set('New value'); + string$.set('Newer Value'); + expectReact(3, 'Newer Value'); + + // Until we set `string$` to an empty string to stop the + // reaction: + string$.set(''); + // The reactor was immediately stopped, so even the empty string + // was never given to the reactor: + expectReact(3, 'Newer Value'); + }); + + /** + * Since the example above where the `until` is based on the parent + * `Derivable` occurs very frequently, this `Derivable` is given as + * a parameter to the `until` function. + */ + it('the parent `Derivable`', () => { + /** + * ** Your Turn ** + * + * Try using the first parameter of the `until` function to do + * the same as above. + */ + string$.react(reactor, __YOUR_TURN__); + + // It should react as usual. + string$.set('New value'); + string$.set('Newer Value'); + expectReact(3, 'Newer Value'); + + // Until we set `string$` to an empty string, to stop + // the reaction: + string$.set(''); + + // The reactor was immediately stopped, so even the empty string + // was never given to the reactor: + expectReact(3, 'Newer Value'); + }); + + /** + * Sometimes, the syntax may leave you confused. + */ + it('syntax issues', () => { + // It looks this will start reacting until `boolean$`s value is false... + let stopper = boolean$.react(reactor, { until: b => !b }); + + // ...but does it? (Remember: `boolean$` starts out as `false`) + expect(boolean$.connected).toBe(__YOUR_TURN__); + + // The `b` it obtains as argument is a `Derivable`. This is a + // reference value which will evaluate to `true` as it is not `undefined`. + // Thus, the negation will evaluate to `false`, independent of the value of + // the boolean. You can get the boolean value our of the `Derivable` using `.get()`: + stopper(); // reset + stopper = boolean$.react(reactor, { until: b => !b.get() }); + expect(boolean$.connected).toBe(__YOUR_TURN__); + + // You can also return the `Derivable` after appling the negation + // using the method designed for negating Derivables: + stopper(); + boolean$.react(reactor, { until: b => b.not() }); + expect(boolean$.connected).toBe(__YOUR_TURN__); + }); + }); + + /** + * Sometimes you may not need to react to the first couple of values of + * the `Derivable`. This can be because of the value of the `Derivable` + * or due to external conditions. + * + * The `from` option is meant to help with this. The reactor will only + * start after it becomes true. Once it has become true, the reactor + * will not listen to this option any more and react as usual. + * + * The interface of `from` is the same as `until` (i.e. it also gets + * the parent derivable as first parameter when it's called.) + */ + it('reacting `from`', () => { + const sherlock$ = atom(''); + + /** + * ** Your Turn ** + * + * We can react here, but restrict the reactions to start when the + * keyword 'dear' is set. This will skip the first three reactions, + * but react as usual after that. + * + * *Hint: remember the `.is()` method from tutorial 2?* + */ + sherlock$.react(reactor, __YOUR_TURN__); + + expectReact(0); + ['Elementary,', 'my', 'dear', 'Watson'].forEach(txt => sherlock$.set(txt)); + + expectReact(2, 'Watson'); + }); + + /** + * Sometimes you may want to react only on certain values or when + * certain conditions are met. + * + * This can be achieved by using the `when` reactor option. + * Where `until` and `from` can only be triggered once to stop or start + * reacting, `when` can be flipped as often as you like and the reactor + * will respect the current state of the `when` function/Derivable. + */ + it('reacting `when`', () => { + const count$ = atom(0); + + /** + * ** Your Turn ** + * + * Now, let's react to all even numbers. + * Except 4, we don't want to make it too easy now. + */ + count$.react(reactor, __YOUR_TURN__); + + expectReact(1, 0); + + for (let i = 0; i <= 4; i++) { + count$.set(i); + } + expectReact(2, 2); + for (let i = 4; i <= 10; i++) { + count$.set(i); + } + expectReact(5, 10); + }); + + /** + * Normally the reactor will immediately fire with the current value. + * If you want the reactor to fire normally, just not the first time, + * there is also a `boolean` option: `skipFirst`. + */ + it('reacting with `skipFirst`', () => { + const count$ = atom(0); + + /** + * ** Your Turn ** + * + * Say you want to react when `count$` is larger than 3. But not the first time... + */ + count$.react(reactor, __YOUR_TURN__); + + expectReact(0); + + for (let i = 0; i <= 5; i++) { + count$.set(i); + } + expectReact(1, 5); // it should have skipped the 4 + + for (let i = 0; i <= 5; i++) { + count$.set(i); + } + expectReact(3, 5); // now it should not have skipped the 4 + }); + + /** + * With `once` you can stop the reactor after it has emitted exactly + * one value. This is a `boolean` option. + * + * Without any other `options`, this is just a strange way of typing + * `.get()`. But when combined with `when`, `from` or `skipFirst`, it + * can be very useful. + */ + it('reacting `once`', () => { + const finished$ = atom(false); + + /** + * ** Your Turn ** + * + * Say you want to react when `finished$` is true. It can not finish + * twice. + * + * *Hint: you will need to combine `once` with another option* + */ + // finished$.react(reactor, __YOUR_TURN__); + + expectReact(0); + + // When finished it should react once. + finished$.set(true); + expectReact(1, true); + + // After that it should really be finished. :-) + finished$.set(false); + finished$.set(true); + expectReact(1, true); + }); + }); + + describe.skip('order of execution', () => { + /** + * As you can see for yourself in libs/sherlock/src/lib/derivable/mixins/take.ts, + * the options `from`, `until`, `when`, `skipFirst` and `once` are tested in this specific order: + * 1) firstly, `from` is checked. If `from` is/was true (or is not set in the options), we continue: + * 2) secondly, `until` is checked. If `until` is false (or is not set in the options), we continue: + * 3) thirdly, `when` is checked. If `when` is true (or is not set in the options), we continue: + * 4) fourthly, `skipFirst` is checked. If `skipFirst` is false (or is not set in the options), we continue: + * 5) lastly, `once` is checked. + * + * This means, for example, that `skipFirst` is only checked when `from` is true or unset, `until` is false or unset, + * and `when` is true or unset. If e.g. `when` evaluates to false, `skipFirst` cannot trigger. + */ + it('`from` and `until`', () => { + const myAtom$ = atom(0); + myAtom$.react(reactor, { from: v => v.is(3), until: v => v.is(2) }); + + for (let i = 1; i <= 5; i++) { + myAtom$.set(i); + } + + // The reactor starts reacting when `myAtom` gets the value 3, but stops when it gets the value 2. + // But because `myAtom` obtains the value 2 before it obtains 3... + // ...how many times was the reactor called, if any? + expectReact(__YOUR_TURN__); + }); + + it('`when` and `skipFirst`', () => { + const myAtom$ = atom(0); + myAtom$.react(reactor, { when: v => v.is(1), skipFirst: true }); + + myAtom$.set(1); + + // The reactor reacts when `myAtom` is 1 but skips the first number. + // The first number of `myAtom` is 0, its initial number. + // Does the reactor skip the 0 or the 1? + expectReact(__YOUR_TURN__); + }); + + it('`from`, `until`, `when`, `skipFirst`, and `once`', () => { + const myAtom$ = atom(0); + myAtom$.react(reactor, { + from: v => v.is(5), + until: v => v.is(1), + when: v => [2, 3, 4].includes(v.get()), + skipFirst: true, + once: true, + }); + + for (let v of [1, 2, 3, 4, 5, 4, 3, 2, 1, 2, 3]) { + myAtom$.set(v); + } + + // `from` and `until` allow the reactor to respectively start when `myAtom` has value 5, and stop when it has value 1. + // Meanwhile, `when` allows neither of those values and only allows the values 2, 3, and 4. + // `skipFirst` and `once` are also added, just to bring the whole group together. + // so, how many times is the reactor called, and what was the last argument (if any)? + expectReact(__YOUR_TURN__); + + }); + }); + + describe.skip('challenge', () => { + it('onDisconnect', () => { + const connected$ = atom('disconnected'); + /** + * ** Your Turn ** + * + * `connected$` indicates the current connection status: + * > 'connected'; + * > 'disconnected'; + * > 'standby'. + * + * We want our reactor to trigger once, when the device is not connected, + * which means it is either `standby` or `disconnected` (eg for cleanup). + * + * This should be possible with three simple ReactorOptions + */ + connected$.react(reactor, __YOUR_TURN__); + + // It starts as 'disconnected' + expectReact(0); + + // At this point, the device connects, no reaction should occur yet. + connected$.set('connected'); + expectReact(0); + + // When the device goes to standby, the reaction should fire once + connected$.set('standby'); + expectReact(1, 'standby'); + + // After that, nothing should change anymore. + connected$.set('disconnected'); + expectReact(1, 'standby'); + connected$.set('standby'); + expectReact(1, 'standby'); + connected$.set('connected'); + expectReact(1, 'standby'); + + // It should not react again after this. + expect(connected$.connected).toBeFalse(); + // * Note: this `.connected` refers to whether this `Derivable` + // is being (indirectly) observed by a reactor. + }); + }); +}); diff --git a/generated_tutorial/4 - inner workings.test.ts b/generated_tutorial/4 - inner workings.test.ts new file mode 100644 index 0000000..4e3223f --- /dev/null +++ b/generated_tutorial/4 - inner workings.test.ts @@ -0,0 +1,310 @@ +import { atom } from '@skunkteam/sherlock'; +import { Seq } from 'immutable'; + +/** + * ** Your Turn ** + * + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +/** + * Time to dive a bit deeper into the inner workings of `@skunkteam/sherlock`. + */ +describe.skip('inner workings', () => { + /** + * What if there is a derivation that reads from one of two `Derivable`s + * dynamically? Will both of those `Derivable`s be tracked for changes? + */ + it('dynamic/inactive dependencies', () => { + const switch$ = atom(true); + const number$ = atom(1); + const string$ = atom('one'); + + const reacted = jest.fn(); + + switch$ + // This `.derive()` is the one we are testing when true, it will + // return the `number` otherwise the `string` + .derive(s => (s ? number$.get() : string$.get())) + .react(reacted); + + // The first time should not surprise anyone, the derivation + // was called and returned the right result. + expect(reacted).toHaveBeenCalledExactlyOnceWith(1, expect.toBeFunction()); + // Note here the second expectation `.toBeFunction()` to + // catch the stop function that was part of the .react() signature. + + // `switch$` is still set to true (number) + string$.set('two'); + + /** + * ** Your Turn ** + * + * What do you expect? + */ + + expect(reacted).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reacted).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + + // `switch$` is still set to true (number) + number$.set(2); + + /** + * ** Your Turn ** + * + * What do you expect? + */ + + expect(reacted).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reacted).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + + // `switch$` is now set to false (string) + switch$.set(false); + number$.set(3); + + /** + * ** Your Turn ** + * + * What do you expect now? + */ + + expect(reacted).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reacted).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + + }); + + /** + * One thing to know about `Derivable`s is that derivations are not + * executed, until someone asks. + * + * So let's test this. + */ + it('lazy execution', () => { + const hasDerived = jest.fn(); + + const myAtom$ = atom(true); + const myDerivation$ = myAtom$.derive(hasDerived); + + /** + * ** Your Turn ** + * + * We have created a new `Derivable` by deriving the `Atom`. But have + * not called `.get()` on that new `Derivable`. + * + * How many times do you think the `hasDerived` function has been + * called? 0 is also an option of course. + */ + + // Well, what do you expect? + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + + myDerivation$.get(); + + // And after a `.get()`? + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + + myDerivation$.get(); + + // And after the second `.get()`? Is there an extra call? + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + + /** + * The state of any `Derivable` can change at any moment. + * + * But you don't want to keep a record of the state and changes to a + * `Derivable` that no one is listening to. + * + * That's why a `Derivable` has to recalculate it's internal state every + * time `.get()` is called. + */ + }); + + /** + * So what if the `Derivable` is reacting? + * + * When a `Derivable` is reacting, the current state is known. + * + * And since changes are derived/reacted to synchronously, the state is + * always up to date. + * + * So a `.get()` should not have to be calculated. + */ + it('while reacting', () => { + const hasDerived = jest.fn(); + + const myAtom$ = atom(true); + const myDerivation$ = myAtom$.derive(hasDerived); + + // It should not have done anything at this moment + expect(hasDerived).not.toHaveBeenCalled(); + + const stopper = myDerivation$.react(() => ''); + + /** + * ** Your Turn ** + * + * Ok, it's your turn to complete the expectations. + */ + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + + myDerivation$.get(); + + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + + myAtom$.set(false); + + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + + myDerivation$.get(); + + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + + stopper(); + + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + + myDerivation$.get(); + + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + + /** + * Since the `.react()` already listens to the value-changes, there is + * no need to recalculate whenever a `.get()` is called. + * + * But when the reactor has stopped, the derivation has to be calculated + * again. + */ + }); + + /** + * The basics of `Derivable` caching are seen above. + * But there is one more trick up it's sleeve. + */ + it('cached changes', () => { + const first = jest.fn(); + const second = jest.fn(); + + const myAtom$ = atom(1); + const first$ = myAtom$.derive(i => { + first(i); // Call the mock function, to let it know we were here + return i > 2; + }); + const second$ = first$.derive(second); + + // As always, they should not have fired yet + expect(first).not.toHaveBeenCalled(); + expect(second).not.toHaveBeenCalled(); + + second$.react(() => ''); + + // And as expected, they now should both have fired once + expect(first).toHaveBeenCalledOnce(); + expect(second).toHaveBeenCalledOnce(); + + /** + * ** Your Turn ** + * + * But what to expect now? + */ + + // Note that this is the same value as it was initialized with + myAtom$.set(1); + + expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); + + myAtom$.set(2); + + expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); + + myAtom$.set(3); + + expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); + + myAtom$.set(4); + + expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); + + /** + * Can you explain the behavior above? + * + * It is why we say that `@skunkteam/sherlock` deals with reactive state + * and not events (as RxJS does for example). + * + * Events can be very useful, but when data is involved, you are + * probably only interested in value changes. So these changes can and + * need to be cached and deduplicated. + */ + }); + + /** + * So if the new value of a `Derivable` is equal to the old, it won't + * propagate a new event. But what does it mean to be equal in a + * `Derivable`? + * + * Strict `===` equality would mean that `NaN` and `NaN` would not even be + * equal. `Object.is()` equality would be better, but would mean that + * structurally equal objects could be different. + */ + it('equality', () => { + const atom$ = atom({}); + const hasReacted = jest.fn(); + + atom$.react(hasReacted, { skipFirst: true }); + expect(hasReacted).toHaveBeenCalledTimes(0); // added for clarity, in case people missed the `skipFirst` or its implication + + atom$.set({}); + + /** + * ** Your Turn ** + * + * The `Atom` is set with exactly the same object as before. Will the + * `.react()` fire? + */ + expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); + + /** + * But what if you use an object, that can be easily compared through a + * library like `ImmutableJS`? + * + * Let's try an `Immutable.Seq` + */ + atom$.set(Seq.Indexed.of(1, 2, 3)); + // Let's reset the spy here, to start over + hasReacted.mockClear(); + expect(hasReacted).not.toHaveBeenCalled(); + + atom$.set(Seq.Indexed.of(1, 2, 3)); + /** + * ** Your Turn ** + * + * Do you think the `.react()` fired with this new value? + */ + expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); + + atom$.set(Seq.Indexed.of(1, 2)); + + /** + * ** Your Turn ** + * + * And now? + */ + expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); + + /** + * In `@skunkteam/sherlock` equality is a bit complex: + * + * First we check `Object.is()` equality, if that is true, it is the + * same, you can't deny that. + * + * After that it is pluggable. It can be anything you want. + * + * By default we try to use `.equals()`, to support libraries like + * `ImmutableJS`. + */ + }); +}); diff --git a/generated_tutorial/5 - unresolved.test.ts b/generated_tutorial/5 - unresolved.test.ts new file mode 100644 index 0000000..ace2845 --- /dev/null +++ b/generated_tutorial/5 - unresolved.test.ts @@ -0,0 +1,178 @@ +import { atom, Derivable, DerivableAtom } from '@skunkteam/sherlock'; + +/** + * ** Your Turn ** + * + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +/** + * Sometimes your data isn't available yet. For example if it is still being + * fetched from the server. At that point you probably still want your + * `Derivable` to exist, to start deriving and reacting when the data becomes + * available. + * + * To support this, `Derivable`s in `@skunkteam/sherlock` support a separate + * state, called `unresolved`. This indicates that the data is not available + * yet, but (probably) will be at some point. + */ +describe.skip('unresolved', () => { + /** + * Let's start by creating an `unresolved` `Derivable`. + */ + it('can be checked on the `Derivable`', () => { + // By using the `.unresolved()` method, you can create an `unresolved` + // atom. Note that you will need to indicate the type of this atom, + // since it can't be inferred by TypeScript this way. + const myAtom$ = atom.unresolved(); + + expect(myAtom$.resolved).toEqual(__YOUR_TURN__); + + /** + * ** Your Turn ** + * + * Resolve the atom, it's pretty easy + */ + __YOUR_TURN__; + + expect(myAtom$.resolved).toBeTrue(); + }); + + /** + * An `unresolved` `Derivable` is not able to provide a value yet. + * So `.get()` will throw if you try. + */ + it('cannot `.get()`', () => { + /** + * ** Your Turn ** + * + * Time to create an `unresolved` Atom.. + */ + const myAtom$: DerivableAtom = __YOUR_TURN__; + + expect(myAtom$.resolved).toBeFalse(); + + // By this test passing, we see that `.get()` on an unresolved + // `Derivable` indeed throws an error. + expect(() => myAtom$.get()).toThrow('Could not get value, derivable is unresolved'); + + myAtom$.set('finally!'); + + /** + * ** Your Turn ** + * + * What do you expect? + */ + expect(myAtom$.resolved).toEqual(__YOUR_TURN__); + + // .toThrow() or .not.toThrow()? ↴ + expect(() => myAtom$.get()) /*__YOUR_TURN__*/; + }); + + /** + * If a `Derivable` is `unresolved` it can't react yet. But it will + * `.react()` if a value becomes available. + */ + it('reacting to `unresolved`', () => { + const myAtom$ = atom.unresolved(); + + const hasReacted = jest.fn(); + myAtom$.react(hasReacted); + + /** + * ** Your Turn ** + * + * What do you expect? + */ + expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); + + /** + * ** Your Turn ** + * + * Now make the last expect succeed + */ + __YOUR_TURN__; + + expect(myAtom$.resolved).toBeTrue(); + expect(hasReacted).toHaveBeenCalledExactlyOnceWith(`woohoow, I was called`, expect.toBeFunction()); + }); + + /** + * In `@skunkteam/sherlock` there is no reason why a `Derivable` should not + * be able to become `unresolved` again after it has been set. + */ + it('can become `unresolved` again', () => { + const myAtom$ = atom.unresolved(); + + expect(myAtom$.resolved).toBeFalse(); + + /** + * ** Your Turn ** + * + * Set the value.. + */ + __YOUR_TURN__; + + expect(myAtom$.get()).toEqual(`it's alive!`); + + /** + * ** Your Turn ** + * + * Unset the value.. (*Hint: TypeScript is your friend*) + */ + __YOUR_TURN__; + + expect(myAtom$.resolved).toBeFalse(); + }); + + /** + * When a `Derivable` is dependent on another `unresolved` `Derivable`, this + * `Derivable` should also become `unresolved`. + * + * *Note that this will only become `unresolved` when there is an active + * dependency (see 'inner workings#dynamic dependencies')* + */ + it('will propagate', () => { + const myString$ = atom.unresolved(); + const myOtherString$ = atom.unresolved(); + + /** + * ** Your Turn ** + * + * Combine the two `Atom`s into one `Derivable` + */ + const myDerivable$: Derivable = __YOUR_TURN__; + + /** + * ** Your Turn ** + * + * Is `myDerivable$` expected to be `resolved`? + */ + expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); + + // Now let's set one of the two source `Atom`s + myString$.set('some'); + + /** + * ** Your Turn ** + * + * What do you expect to see in `myDerivable$`. + */ + expect(myDerivable$.resolved).toEqual(false); + + // And what if we set `myOtherString$`? + myOtherString$.set('data'); + expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); + expect(myDerivable$.get()).toEqual(__YOUR_TURN__); + + /** + * ** Your Turn ** + * + * Now we will unset one of the `Atom`s. + * What do you expect `myDerivable$` to be? + */ + myString$.unset(); + expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); + }); +}); diff --git a/generated_tutorial/6 - errors.test.ts b/generated_tutorial/6 - errors.test.ts new file mode 100644 index 0000000..a040ed0 --- /dev/null +++ b/generated_tutorial/6 - errors.test.ts @@ -0,0 +1,263 @@ +import { atom, DerivableAtom, error, FinalWrapper, unresolved } from '@skunkteam/sherlock'; + +/** + * ** Your Turn ** + * + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +// Silence TypeScript's import not used errors. +expect(FinalWrapper).toBe(FinalWrapper); + +// In libs/sherlock/src/lib/interfaces.ts:289, the basic states a Derivable can have are shown. +// > `export type State = V | unresolved | ErrorWrapper;` +// A state can be either any type `V` (`number`, `string`, etc.), `unresolved` as we saw in the +// previous tutorial, or `ErrorWrapper`. This last state is explained here. +describe.skip('errors', () => { + let myAtom$: DerivableAtom; + + beforeEach(() => { + myAtom$ = atom(1); + }); + + it('basic errors', () => { + // The `errored` property shows whether the last statement resulted in an error. + expect(myAtom$.errored).toBe(false); + expect(myAtom$.error).toBeUndefined; // by default, the `error` property is undefined. + expect(myAtom$.getState()).toBe(1); // as explained above, any type can be a state + + // We can set errors using the `setError()` function. + myAtom$.setError('my Error'); + + expect(myAtom$.errored).toBe(true); + expect(myAtom$.error).toBe('my Error'); + + // The `ErrorWrapper` state only holds an error string. The `error()` function returns + // such an `ErrorWrapper` which we can use to compare. + expect(myAtom$.getState()).toMatchObject(error('my Error')); + + // expect(myAtom$.get).toThrow("Cannot read properties of undefined (reading 'getState')"); + // TODO: WHAT - normally this works, but internal JEST just fucks with me....? + + // Calling `get()` on `myAtom$` gives the error. + expect(() => myAtom$.get()).toThrow('my Error'); + expect(myAtom$.errored).toBe(true); + + // ** __YOUR_TURN__ ** + // What will happen if you try to call `set()` on `myAtom$`? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myAtom$.set(2)) /* __YOUR_TURN__ */; + expect(myAtom$.errored).toBe(__YOUR_TURN__); + + // Interestingly, calling `set()` does not throw an error. In fact, it removes the error state + // altogether. This means we can call `get()` again. + expect(() => myAtom$.get()).not.toThrow(); + }); + + it('deriving an error', () => { + const myDerivable$ = myAtom$.derive(v => v + 1); + + // If `myAtom$` suddenly errors... + myAtom$.setError('division by zero'); + + // ...what happens to `myDerivable$`? + expect(myDerivable$.errored).toBe(__YOUR_TURN__); + + // If any Derivable tries to derive from an atom in an error state, + // this Derivable will itself throw an error too. This makes sense, + // given that it cannot obtain the value it needs anymore. + }); + + it('reacting to an error', () => { + // Without a reactor, setting an error to an Atom does not throw an error. + expect(() => myAtom$.setError('my Error')).not.toThrow(); + myAtom$.set(1); + + // Now we set a reactor to `myAtom$`. This reactor does not use the value of `myAtom$`. + const reactor = jest.fn(); + myAtom$.react(reactor); + + // ** __YOUR_TURN__ ** + // Will an error be thrown when `myAtom$` is now set to an error state? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myAtom$.setError('my Error')) /* __YOUR_TURN__ */; + + // Reacting to a Derivable that throws an error will make the reactor throw as well. + // Because the reactor will usually fire when it gets connected, it also throws when + // you try to connect it after the error has already been set. + + myAtom$ = atom(1); + myAtom$.setError('my second Error'); + + // ** __YOUR_TURN__ ** + // Will an error be thrown when you use `skipFirst`? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myAtom$.react(reactor, { skipFirst: true })) /* __YOUR_TURN__ */; + + // And will an error be thrown when `from = false`? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myAtom$.react(reactor, { from: false })) /* __YOUR_TURN__ */; + + // When `from = false`, the reactor is disconnected, preventing the error message from entering. + // `skipFirst`, on the other hand, does allow the error in, but does not trigger an update. + }); + + /** + * Similarly to `constants` which we'll explain in tutorial 7, + * you might want to specify that a variable cannot be updated. + * This can be useful for the programmers themselves, to not + * accidentally update the variable, but it can also be useful for + * optimization. You can do this using the `final` concept. + */ + describe.skip('TEMP `final`', () => { + let myAtom$ = atom(1); + + beforeEach(() => { + myAtom$ = atom(1); + }); + + it('`final` basics', () => { + // Every atom has a `final` property. + expect(myAtom$.final).toBeFalse(); + + // You can make an atom final using the `.makeFinal()` function. + myAtom$.makeFinal(); + expect(myAtom$.final).toBeTrue(); + + /** + * ** Your Turn ** + * What do you think will happen when we try to `.get()` or `.set()` this atom? + */ + // .toThrow() or .not.toThrow()? ↴ + expect(() => myAtom$.get()) /*__YOUR_TURN__*/; + expect(() => myAtom$.set(2)) /*__YOUR_TURN__*/; + + // This behavior is consistent with normal variables created using `const`. + // Alternatively, you can set a last value before setting it to `final`. + // .toThrow() or .not.toThrow()? ↴ + expect(() => myAtom$.setFinal(2)) /*__YOUR_TURN__*/; + + // There is no way to 'unfinalize' a Derivable, so the only solution to reset is to + // create a whole new Derivable. + myAtom$ = atom(1); + myAtom$.setFinal(2); + expect(myAtom$.final).toBeTrue(); + }); + + it('deriving a `final` Derivable', () => { + const myDerivable$ = myAtom$.derive(v => v + 1); + + const hasReacted = jest.fn(); + myDerivable$.react(hasReacted); + + expect(myDerivable$.final).toBeFalse(); + expect(myDerivable$.connected).toBeTrue(); + + myAtom$.makeFinal(); + + /** + * ** Your Turn ** + * + * What will happen to `myDerivable$` when I change `myAtom$` to be `final`? + */ + expect(myDerivable$.final).toBe(__YOUR_TURN__); + expect(myDerivable$.connected).toBe(__YOUR_TURN__); + + /** + * Derivables that are final (or constant) are no longer tracked. This can save + * a lot of memory and time by cleaning up unused data. Also, when all the variables + * that a Derivable depends on become final, that Derivable itself also becomes final. + * Similarly to `unresolved` and `error`, this chains. + */ + }); + + it('`final` State', () => { + /** A property such as `.final`, similar to variables like `.errored` and `.resolved` + * is useful for checking whenever a Derivable is in a certain state, but these properties + * are just a boolean. This means that these properties cannot be derived and we cannot + * have certain functions execute whenever there is a change in the state. For this reason, + * every Derivable holds an internal state, retrievable using `.getState()` which can be + * derived. TODO: Have a clear place where I explain this! Now I have info up top here too. + * + * We have seen that states (`State`) can be `undefined`, `ErrorWrapper`, + * or any regular type `V`. Other states exist, such as the `MaybeFinalState`. This state can be either + * a normal state `State` or a special `FinalWrapper>` state. Let's see that in action. + */ + expect(myAtom$.getMaybeFinalState()).toBe(1); // `getMaybeFinalState` can return a normal state, which in turn can be any normal type. + + myAtom$.makeFinal(); + + expect(myAtom$.getMaybeFinalState()).toBeInstanceOf(FinalWrapper); // but `getMaybeFinalState` can also return a `FinalWrapper` type. + expect(myAtom$.getState()).toBe(1); // the normal type is still the final it contains. + + // TODO: MAAR JE KAN EEN STATE HELEMAAL NIET DERIVEN! + // Dus dat is allemaal onzin lijkt me....??? Bovendien, kan je normale variabelen niet deriven door het gewoon te + // wrappen in een atom ofzo? + }); + }); + + /** + * It is nice to be able to have a backup plan when an error occurs. + * The `.fallbackTo()` function allows you to specify a default value + * whenever your Derivable gets an error state. + */ + it('Fallback-to', () => { + const myAtom$ = atom(0); + + /** + * ** Your Turn ** + * Use the `.fallbackTo()` method to create a `mySafeAtom$` which + * gets the backup value `3` when `myAtom$` gets an error state. + */ + // const mySafeAtom$ = myAtom$.fallbackTo(__YOUR_TURN__); + + expect(myAtom$.getState()).toBe(0); + expect(myAtom$.value).toBe(0); + expect(mySafeAtom$.value).toBe(0); + + myAtom$.unset(); + + expect(myAtom$.getState()).toBe(unresolved); + expect(myAtom$.value).toBeUndefined(); + expect(mySafeAtom$.value).toBe(3); + }); + + it('TEMP Flat-map', () => { + // const myAtom$ = atom(0); + // const mapping = (v: any) => atom(v); + // Sometimes you use `map()`, but the result within the `map()` is also a Derivable. + // The result would here be a `Derivable>` (hover over `derive` to see this). + // You would have to use `.get()` to go back to a single Derivable. Similarly how `flatMap` can + // reduce lists of lists to a single list, it can help reduce Derivables of Derivables to a + // single Derivable. If you have something like this: + // let myAtom$$ = myAtom$.map(n => mapping(n)).derive(v => v.get()); + // You can now rewrite it to this: + // myAtom$$ = myAtom$.flatMap(n => mapping(n)); + // It only results in slightly shorter code. + // TODO: right? + }); +}); + +/** + * !! Final States; (finalGetter, finalMethod, getMaybeFinalState, FinalWrapper, MaybeFinalState, _isFinal, makeFinal, markFinal, .final, .finalized, setFinal...) + * ? Lens; (libs/sherlock/src/lib/derivable/lens.ts) - map die twee kanten op kan gaan. Maar een map kan dat al? Maar hier kan + * je dat los definieren! Je kan gewoon `lens` ipv `var.lens`. Zelden dat je dit gebruikt. Output is een Derivable though. + * x Lift; (libs/sherlock-utils/src/lib/lift.ts) + * !! Peek; (libs/sherlock-utils/src/lib/peek.ts) - dan track je niet. In een derivable, deze tracked hij dan niet (ipv .get() waar het wel getracked wordt) + * x Template; (libs/sherlock-utils/src/lib/template.ts) - to make a string using a template literal. (Uses unwrap!!) + * / Factory; (libs/sherlock/src/lib/derivable/factories.ts) - simply contains functions to create objects, namely + * lens; atom; constant; derive. + * !! Flat-map; (libs/sherlock/src/lib/derivable/mixins/flat-map.ts) - ??? + * array: nested arrays naar array + * Derivable: gooit er derive.get() achteraan? + * Derivable (input van inputveld). Flatmap geeft Derivable terug. Derivable.flatmap() returned misschien + * Derivable, returned dan de number. flatMap is een `derive`, maar wat hij returned haalt hij uit de Derivable. + * ofzoiets. Maakt code korter. + * !! Fallback-to; - op een derivable. Als een atom `unresolved` is, dan fallt het back to this value. Ofwel, initial value, maar + * ook als hij later unresolved wordt, dan wordt hij dit (vaak wel initial value). + * !! Take - react options gebruiken buiten react. In een derivable chain, halverwege die options gebruiken. + * e.g. (from)Promise. Zodra die een waarde aanneemt kan hij niet meer veranderen. + * Let FromPromise, FromObservable, FromEventPattern ook uit (in utils?), ToPromise, ToObservable, in praktijk ook handig. + * FromEventPattern (haily mary, als alles niet werkt, dan dit doen). + */ diff --git a/generated_tutorial/7 - advanced.test.ts b/generated_tutorial/7 - advanced.test.ts new file mode 100644 index 0000000..bd6b9fb --- /dev/null +++ b/generated_tutorial/7 - advanced.test.ts @@ -0,0 +1,507 @@ +import { atom, constant, Derivable, derive, SettableDerivable, unresolved } from '@skunkteam/sherlock'; +import { lift, template } from '@skunkteam/sherlock-utils'; +import { Map as ImmutableMap } from 'immutable'; + +/** + * ** Your Turn ** + * + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +describe.skip('advanced', () => { + /** + * In the case a `Derivable` is required, but the value is immutable. + * You can use a `constant()`. + * + * This will create a readonly `Derivable`. + */ + it('`constant`', () => { + /** + * We cast to `SettableDerivable` to trick TypeScript for this test. + * It can be valueable to know what a `constant()` is, though. + * So try and remove the `cast`, see what happens! + */ + const c = constant('value') as SettableDerivable; + + /** + * ** Your Turn ** + * + * What do you expect this `Derivable` to do on `.set()`, `.get()` etc? + */ + + // Remove this after taking your turn below. + expect(false).toBe(true); + // .toThrow() or .not.toThrow()? ↴ (2x) + expect(() => c.get()) /* __YOUR_TURN__ */; + expect(() => c.set('new value')) /* __YOUR_TURN__ */; + }); + + it('`templates`', () => { + // Staying in the theme of redefining normal Typescript code in our Derivable language, + // we also have a special syntax to copy template literals to a Derivable. + const one = 1; + const myDerivable = template`I want to go to ${one} party`; + expect(myDerivable.get()).toBe(__YOUR_TURN__) /* __YOUR_TURN__ */; + }); + + /** + * Collections in `ImmutableJS` are immutable, so any modification to a + * collection will create a new one. This results in every change needing a + * `.get()` and a `.set()` on a `Derivable`. + * + * To make this pattern a little bit easier, the `.swap()` method can be + * used. The given function will get the current value of the `Derivable` + * and any return value will be set as the new value. + */ + it('`.swap()`', () => { + // This is a separate function because you might want to use this later. + function plusOne(num: number) { + return num + 1; + } + + const myCounter$ = atom(0); + /** + * ** Your Turn ** + * + * Rewrite the `.get()`/`.set()` combos below using `.swap()`. + */ + // Remove this after taking your turn below. + expect(false).toBe(true); + + myCounter$.set(plusOne(myCounter$.get())); + expect(myCounter$.get()).toEqual(1); + + myCounter$.set(plusOne(myCounter$.get())); + expect(myCounter$.get()).toEqual(2); + }); + + /** + * You might want to use the reactor options such as + * `when`, `until`, and `skipFirst` when deriving as well. + * In such cases, you could use `.take()`. + */ + it('`.take()`', () => { + const myAtom$ = atom('denied'); + + /** + * ** Your Turn ** + * Use the `.take()` method on `myAtom$` to only accept the input string + * when it is `allowed`. + */ + const myLimitedAtom$ = myAtom$.take(__YOUR_TURN__); + + expect(myLimitedAtom$.resolved).toBe(false); + myAtom$.set('allowed'); + expect(myLimitedAtom$.resolved).toBe(true); + expect(myLimitedAtom$.value).toBe('allowed'); + }); + + /** + * As an alternative to `.get()` and `.set()`, there is also the `.value` + * accessor. + */ + describe.skip('`.value`', () => { + /** + * `.value` can be used as an alternative to `.get()` and `.set()`. + * This helps when a property is expected instead of two methods. + */ + it('as a getter/setter', () => { + const myAtom$ = atom('foo'); + + /** + * ** Your Turn ** + * + * Use the `.value` accessor to get the current value. + */ + expect(__YOUR_TURN__).toEqual('foo'); + /** + * ** Your Turn ** + * + * Now use the `.value` accessor to set a 'new value'. + */ + myAtom$.value = __YOUR_TURN__; + + expect(myAtom$.get()).toEqual('new value'); + }); + + /** + * If a `Derivable` is `unresolved`, `.get()` will normally throw. + * `.value` will return `undefined` instead. + */ + it('will not throw when `unresolved`', () => { + const myAtom$ = atom.unresolved(); + + /** + * ** Your Turn ** + */ + expect(myAtom$.value).toEqual(__YOUR_TURN__); + }); + + /** + * As a result, if `.value` is used inside a derivation, it will also + * replace `unresolved` with `undefined`. So `unresolved` will not + * automatically propagate when using `.value`. + */ + it('will stop propagation of `unresolved` in `.derive()`', () => { + const myAtom$ = atom('foo'); + + const usingGet$ = derive(() => myAtom$.get()); + const usingVal$ = derive(() => myAtom$.value); + + expect(usingGet$.get()).toEqual('foo'); + expect(usingVal$.get()).toEqual('foo'); + + /** + * ** Your Turn ** + * + * We just created two `Derivable`s that are almost exactly the same. + * But what happens when their source becomes `unresolved`? + */ + + expect(usingGet$.resolved).toEqual(__YOUR_TURN__); + expect(usingVal$.resolved).toEqual(__YOUR_TURN__); + myAtom$.unset(); + expect(usingGet$.resolved).toEqual(__YOUR_TURN__); + expect(usingVal$.resolved).toEqual(__YOUR_TURN__); + + }); + }); + + /** + * The `.map()` method is comparable to `.derive()`. + * But there are a couple of differences: + * - It only triggers when the source `Derivable` changes + * - It does not track any other `Derivable` used in the function + * - It can be made to be settable + */ + describe.skip('`.map()`', () => { + const mapReactSpy = jest.fn(); + // Clear the spy before each test case. + beforeEach(() => mapReactSpy.mockClear()); + + it('triggers when the source changes', () => { + const myAtom$ = atom(1); + /** + * ** Your Turn ** + * + * Use the `.map()` method to create the expected output below + */ + const mappedAtom$: Derivable = __YOUR_TURN__; + + mappedAtom$.react(mapReactSpy); + + expect(mapReactSpy).toHaveBeenCalledExactlyOnceWith('1', expect.toBeFunction()); + + myAtom$.set(3); + + expect(mapReactSpy).toHaveBeenCalledTimes(2); + expect(mapReactSpy).toHaveBeenLastCalledWith('333', expect.toBeFunction()); + }); + + it('does not trigger when any other `Derivable` changes', () => { + const myRepeat$ = atom(1); + const myString$ = atom('ho'); + const deriveReactSpy = jest.fn(); + + // Note that the `.map` uses both `myRepeat$` and `myString$` + myRepeat$.map(r => myString$.get().repeat(r)).react(mapReactSpy); + myRepeat$.derive(r => myString$.get().repeat(r)).react(deriveReactSpy); + + expect(mapReactSpy).toHaveBeenCalledExactlyOnceWith('ho', expect.toBeFunction()); + expect(deriveReactSpy).toHaveBeenCalledExactlyOnceWith('ho', expect.toBeFunction()); + + myRepeat$.value = 3; + /** + * ** Your Turn ** + * + * We changed`myRepeat$` to equal 3. + * Do you expect both reactors to have fired? And with what? + */ + + expect(deriveReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(deriveReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + + expect(mapReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(mapReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + + myString$.value = 'ha'; + /** + * ** Your Turn ** + * + * And now that we have changed `myString$`? And when `myRepeat$` changed again? + */ + + expect(deriveReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(deriveReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + + expect(mapReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(mapReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + + myRepeat$.value = 2; + expect(deriveReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(deriveReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + + expect(mapReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(mapReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + + /** + * As you can see, a change in `myString$` will not trigger an + * update. But if an update is triggered, `myString$` will be called + * and the new value will be used. + */ + }); + + /** + * Since `.map()` is a relatively simple mapping of input value to + * output value. It can often be reversed. In that case you can use that + * reverse mapping to create a `SettableDerivable`. + */ + it('can be settable', () => { + const myAtom$ = atom(1); + + /** + * ** Your Turn ** + * + * Check the comments and `expect`s below to see what should be + * implemented exactly. + */ + const myInverse$ = myAtom$.map( + // This first function is called when getting... + n => -n, + // ...and this second function is called when setting. + __YOUR_TURN__, + ); + + // The original `atom` was set to 1, so we want the inverse to + // be equal -1. + expect(myInverse$.get()).toEqual(-1); + + // Now we set the inverse to -2 directly, so we expect the original + // `atom` to be equal to 2. + myInverse$.set(-2); + expect(myAtom$.get()).toEqual(2); + expect(myInverse$.get()).toEqual(-2); + }); + + it('similar to `map()` on arrays', () => { + // If the similarity is not clear yet, here is a comparison between + // the normal `.map()` on arrays and our `Derivable` `.map()`. + // Both get values out of a container (`Array` or `Derivable`), apply + // some function, and put it back in the container. + + const addOne = jest.fn((v: number) => v + 1); + + const myList = [1, 2, 3]; + const myMappedList = myList.map(addOne); + expect(myMappedList).toMatchObject([2, 3, 4]); + + const myAtom$ = atom(1); + let myMappedDerivable$ = myAtom$.map(addOne); + expect(myMappedDerivable$.value).toBe(2); + + // Or, as we have seen before, you can use `lift()` for this. + myMappedDerivable$ = lift(addOne)(myAtom$); + expect(myMappedDerivable$.value).toBe(2); + + // You can combine them too. + const myAtom2$ = atom([1, 2, 3]); + const myMappedDerivable2$ = myAtom2$.map(v => v.map(addOne)); + expect(myMappedDerivable2$.value).toMatchObject([2, 3, 4]); + }); + + /** + * In order to reason over the state of a Derivable, we can + * use `.mapState()`. This will map one state to another, and + * can be used to get rid of pesky `unresolved` or `Errorwrapper` + * states (or to introduce them!). + */ + it('`.mapState()`', () => { + const myAtom$ = atom(1); + + // like `.map()`, we can specify it both ways. + const myMappedAtom$ = myAtom$.mapState( + state => (state === unresolved ? 3 : state), // `myAtom$` => `myMappedAtom$` + state => (state === 2 ? unresolved : state), // `myMappedAtom$` => `myAtom$` + ); + + myAtom$.set(2); + expect(myAtom$.resolved).toBe(__YOUR_TURN__); + expect(myMappedAtom$.resolved).toBe(__YOUR_TURN__); + + myAtom$.unset(); + expect(myAtom$.resolved).toBe(__YOUR_TURN__); + expect(myMappedAtom$.resolved).toBe(__YOUR_TURN__); + + myMappedAtom$.set(2); + expect(myAtom$.resolved).toBe(__YOUR_TURN__); + expect(myMappedAtom$.resolved).toBe(__YOUR_TURN__); + + // This is a tricky one: + myMappedAtom$.unset(); + expect(myAtom$.resolved).toBe(__YOUR_TURN__); + expect(myMappedAtom$.resolved).toBe(__YOUR_TURN__); + + /** + * The results, especially of the last case, may seem weird. + * In the first exercise, `myAtom$` is set to 2, causing the state to be 2 as well. + * By setting the state of `myAtom$`, the first line of `mapState()` is triggered. + * Since `2` is not equal to `unresolved`, we return the state `2`, causing + * `myMappedAtom$` to also get state 2 (and thus: value 2). Neither are unresolved. + * + * In the second case, `myAtom$` is set to `unresolved`, triggering the first line of + * `mapState()`, letting `myMappedAtom$` become 3. `myAtom$` is now `unresolved`, and + * `myMappedAtom$` is not. + * + * In the third case, `myMappedAtom$` is set to 2, it triggers the second line of + * `mapState()`, causing `myAtom$` to become `unresolved`. However, what we don't + * notice is that this change in state triggers the first line of `mapState()` again, + * causing `myMappedAtom$` to get state `3`. We can check this: + */ + + myMappedAtom$.set(2); + expect(myMappedAtom$.get()).toBe(3); // the state and value are linked, so this is identical to `.getState()` + /** + * You might think that this change in state would cause `myAtom$` to now also get + * `3` as its state, but this does not happen. Why not? TODO: maximally one cycle? + * Since both `2` and `3` are not `unresolved`, it does not matter to our answer. + * + * The same cannot be said for the fourth case. Setting `myMappedAtom$` to `unresolved` + * triggers the second line of `mapState()`, causing `myAtom$` to also become `unresolved`. This, in turn, + * triggers the first line of `mapState()`, causing `myMappedAtom$` to become `3`. + * As such, `myMappedAtom$` is not `unresolved` even though we set it as such. + * TODO: change this to be for MAP. Then make MAPSTATE a trivial one right after. + */ + }); + }); + + /** + * `.pluck()` is a special case of the `.map()` method. + * If a collection of values, like an Object, Map, Array is the result of a + * `Derivable`, one of those values can be plucked into a new `Derivable`. + * This plucked `Derivable` can be settable, if the source supports it. + * + * The way properties are plucked is pluggable, but by default both // TODO: no-one here knows what "pluggable" is. Or ImmutableJS. + * `.get()` and `[]` are supported to support + * basic Objects, Maps and Arrays. + * + * *Note that normally when a value of a collection changes, the reference + * does not. This means that setting a plucked property of a regular + * Object/Array/Map will not cause any reaction on that source `Derivable`. + * + * ImmutableJS can help fix this problem* + */ + describe.skip('`.pluck()`', () => { + const reactSpy = jest.fn(); + const reactPropSpy = jest.fn(); + let myMap$: SettableDerivable>; + let firstProp$: SettableDerivable; + + // Reset + beforeEach(() => { + reactPropSpy.mockClear(); + reactSpy.mockClear(); + myMap$ = atom>( + ImmutableMap({ + firstProp: 'firstValue', + secondProp: 'secondValue', + }), + ); + /** + * ** Your Turn ** + * + * `.pluck()` 'firstProp' from `myMap$`. + * + * * Hint: you'll have to cast the result from `.pluck()`. + */ + firstProp$ = __YOUR_TURN__; + }); + + /** + * Once a property is plucked in a new `Derivable`. This `Derivable` can + * be used as a regular `Derivable`. + */ + it('can be used as a normal `Derivable`', () => { + firstProp$.react(reactPropSpy, { skipFirst: true }); + + /** + * ** Your Turn ** + * + * What do you expect the plucked `Derivable` to look like? And what + * happens when we `.set()` it? + */ + expect(firstProp$.get()).toEqual(__YOUR_TURN__); + + // the plucked `Derivable` should be settable + firstProp$.set('other value'); + // is the `Derivable` value the same as was set? + expect(firstProp$.get()).toEqual(__YOUR_TURN__); + + // How many times was the spy called? Note the `skipFirst`.. + expect(reactPropSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + + // ...and what was the value? + expect(reactPropSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + }); + + /** + * If the source of the plucked `Derivable` changes, the plucked + * `Derivable` will change as well. As long as the change affects the + * plucked property of course. + */ + it('will react to changes in the source `Derivable`', () => { + firstProp$.react(reactPropSpy, { skipFirst: true }); + + /** + * ** Your Turn ** + * + * We will set `secondProp`, will this affect `firstProp$`? + * + * *Note: this `map` refers to `ImmutableMap`, not to the + * `Derivable.map()` we saw earlier in the tutorial.* + */ + myMap$.swap(map => map.set('secondProp', 'new value')); + + // How many times was the spy called? Note the `skipFirst`. + expect(reactPropSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + + /** + * ** Your Turn ** + * + * And what if we set `firstProp`? + */ + myMap$.swap(map => map.set('firstProp', 'new value')); + + // How many times was the spy called? Note the `skipFirst`.. + expect(reactPropSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + + // ...and what was the value? + expect(reactPropSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + }); + + /** + * Before, we saw how a change in the source of the plucked `Derivable` + * propagates to it. Now the question is: does this go the other way + * too? + * + * We saw that we can `.set()` the value of the plucked `Derivable`, so + * what happens to the source if we do that? + */ + it('will write through to the source `Derivable`', () => { + myMap$.react(reactSpy, { skipFirst: true }); + + /** + * ** Your Turn ** + * + * So what if we set `firstProp$`? Does this propagate to the source + * `Derivable`? + */ + + firstProp$.set(__YOUR_TURN__); + expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(myMap$.get().get('firstProp')).toEqual(__YOUR_TURN__); + expect(myMap$.get().get('secondProp')).toEqual(__YOUR_TURN__); + + }); + }); +}); diff --git a/generated_tutorial/8 - utils.test.ts b/generated_tutorial/8 - utils.test.ts new file mode 100644 index 0000000..0c926bb --- /dev/null +++ b/generated_tutorial/8 - utils.test.ts @@ -0,0 +1,381 @@ +import { atom, derive } from '@skunkteam/sherlock'; +import { lift, pairwise, peek, scan, struct } from '@skunkteam/sherlock-utils'; + +/** + * ** Your Turn ** + * + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +// Silence TypeScript's import not used errors. +expect(pairwise).toBe(pairwise); +expect(scan).toBe(scan); +expect(struct).toBe(struct); +expect(peek).toBe(peek); +expect(lift).toBe(lift); + +/** + * In the `sherlock-utils` lib, there are a couple of functions that can combine + * multiple values of a single `Derivable` or combine multiple `Derivable`s into + * one. We will show a couple of those here. + */ +describe.skip('utils', () => { + /** + * As the name suggests, `pairwise()` will call the given function with both + * the current and the previous state. + * + * *Note: functions like `pairwise` and `scan` can be used with any callback, + * so it can be used both in a `.derive()` step and in a `.react()`* + */ + it('pairwise', () => { + const myCounter$ = atom(1); + const reactSpy = jest.fn(); + + /** + * ** Your Turn ** + * + * Now, use `pairwise()` to subtract the previous value from the + * current. + * + * *Hint: check the overloads of pairwise if you're struggling with + * `oldValue`.* + */ + myCounter$.derive(__YOUR_TURN__).react(reactSpy); + + expect(reactSpy).toHaveBeenCalledTimes(1); + expect(reactSpy).toHaveBeenLastCalledWith(1, expect.toBeFunction()); + + myCounter$.set(3); + + expect(reactSpy).toHaveBeenCalledTimes(2); + expect(reactSpy).toHaveBeenLastCalledWith(2, expect.toBeFunction()); // 3 (current value of `myCounter$`) - 1 (previous value of `myCounter$`) + + myCounter$.set(10); + + expect(reactSpy).toHaveBeenCalledTimes(3); + expect(reactSpy).toHaveBeenLastCalledWith(7, expect.toBeFunction()); // 10 (current value of `myCounter$`) - 3 (previous value of `myCounter$`) + }); + + /** + * `scan()` is the `Derivable` version of `Array.prototype.reduce()`. It will be + * called with the current state and the last emitted value. + * + * (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce) + * + * *Note: as with `pairwise()` this is useable in both a `.derive()` and + * `.react()` method* + */ + it('scan', () => { + const myCounter$ = atom(1); + const reactSpy = jest.fn(); + + /** + * ** Your Turn ** + * + * Now, use `scan()` to subtract the previous value from the + * current. + * + * Note that `scan()` must return the same type as it gets as input. This is required + * as this returned value is also used for the accumulator (`acc`) value for the next call. + * This `acc` parameter of `scan()` is the last returned value, not the last value + * of `myCounter$`, as is the case with `pairwise()`. + */ + myCounter$.derive(__YOUR_TURN__).react(reactSpy); + + expect(reactSpy).toHaveBeenCalledTimes(1); + expect(reactSpy).toHaveBeenLastCalledWith(1, expect.toBeFunction()); + + myCounter$.set(3); + + expect(reactSpy).toHaveBeenCalledTimes(2); + expect(reactSpy).toHaveBeenLastCalledWith(2, expect.toBeFunction()); // 3 (current value of `myCounter$`) - 1 (previous returned value) + + myCounter$.set(10); + + expect(reactSpy).toHaveBeenCalledTimes(3); + expect(reactSpy).toHaveBeenLastCalledWith(8, expect.toBeFunction()); // 10 (current value of `myCounter$`) - 2 (previous returned value) + }); + + it('`pairwise()` on normal arrays', () => { + // Functions like `pairwise()` and `scan()` work on normal lists too. They are often + // used in combination with `map()` and `filter()`. + const myList = [1, 2, 3, 5, 10]; + let myList2: number[]; + + /** + * ** Your Turn ** + * + * Use a `pairwise()` combined with a `.map()` on `myList` + * to subtract the previous value from the current. + * + * Hint: do not use a lambda function! + */ + myList2 = myList.map(__YOUR_TURN__); + expect(myList2).toMatchObject([1, 1, 1, 2, 5]); + + // However, we should be careful with this, as this does not always behave as intended. + myList2 = myList.map(v => __YOUR_TURN__(v)); // copy the same implementation here + expect(myList2).toMatchObject([1, 2, 3, 5, 10]); + + // Even if we are more clear about what we pass, this unintended behavior does not go away. + myList2 = myList.map((v, _, _2) => __YOUR_TURN__(v)); // copy the same implementation here + expect(myList2).toMatchObject([1, 2, 3, 5, 10]); + + // `pairwise()` keeps track of the previous value under the hood. Using a lambda of + // the form `v => pairwise(...)(v)` would create a new `pairwise` function every call, + // essentially resetting the previous value every call. And resetting the previous value + // to 0 causes the input to stay the same (after all: x - 0 = x). + // Other than by not using a lambda function, we can fix this by + // saving the `pairwise` in a variable and reusing it for every call. + + let f: (v: number) => number = __YOUR_TURN__; // copy the same implementation here + myList2 = myList.map(v => f(v)); + expect(myList2).toMatchObject([1, 1, 1, 2, 5]); + + // To get more insight in the `pairwise()` function, you can call it + // manually. Here, we show what happens under the hood. + + f = pairwise(__YOUR_TURN__); // copy the same implementation here + + myList2 = []; + myList2[0] = f(myList[0]); // f is newly created with `init = 0`, so applies `1 - 0`. + myList2[1] = f(myList[1]); // f has saved `1` internally, so applies `2 - 1`. + myList2[2] = f(myList[2]); // f has saved `2` internally, so applies `3 - 2`. + myList2[3] = f(myList[3]); // f has saved `3` internally, so applies `5 - 3`. + myList2[4] = f(myList[4]); // f has saved `5` internally, so applies `10 - 5`. + + expect(myList2).toMatchObject([1, 1, 1, 2, 5]); + // This also works for functions other than `.map()`, such as `.filter()`. + + /** ** Your Turn ** + * Use `pairwise()` to filter out all values which produce `1` when subtracted + * with their previous value. + */ + myList2 = myList.filter(__YOUR_TURN__); + expect(myList2).toMatchObject([1, 2, 3]); + }); + + it('`scan()` on normal arrays', () => { + // As with `pairwise()` in the last test, `scan()` can be used with arrays too. + const myList = [1, 2, 3, 5, 10]; + let myList2: number[]; + + /** + * ** Your Turn ** + * + * Use a `scan()` combined with a `map` on `myList` + * to subtract the previous value from the current. + */ + + let f: (v: number) => number = scan(__YOUR_TURN__); + myList2 = myList.map(f); + + expect(myList2).toMatchObject([1, 1, 2, 3, 7]); + + // again, it is useful to consider what happens internally. + f(7); // resets the internal `acc` value to 0, as the current `acc` value was 7, and 7-7 = 0. + + myList2 = []; + myList2[0] = f(myList[0]); // 1 :: `f` is newly created with `init = 0`, so applies `1 - 0 = 1`. + myList2[1] = f(myList[1]); // 1 :: `f` has saved the result `1` internally, so applies `2 - 1 = 1`. + myList2[2] = f(myList[2]); // 2 :: `f` has saved the result `1` internally, so applies `3 - 1 = 2`. + myList2[3] = f(myList[3]); // 3 :: `f` has saved the result `2` internally, so applies `5 - 2 = 3`. + myList2[4] = f(myList[4]); // 7 :: `f` has saved the result `3` internally, so applies `10 - 3 = 7`. + + expect(myList2).toMatchObject([1, 1, 2, 3, 7]); + + // This also works for functions other than `map()`, such as `filter()`. + // Use `scan()` to filter out all values from `myList` which produce a value + // of 8 or higher when added with the previous result. In other words, it should + // go through `myList` and add the values producing: (1), (1+2), (1+2+3), (1+2+3+5), + // (1+2+3+5+10), and since this sum only prouces a value higher than 8 when the + // values `5` and `10` are added, the result should be `[5,10]`. + + f = scan(__YOUR_TURN__); + myList2 = myList.filter(__YOUR_TURN__); + expect(myList2).toMatchObject([5, 10]); + }); + + it('pairwise - BONUS', () => { + const myCounter$ = atom(1); + let reactSpy = jest.fn(); + + /** + * ** Your Turn ** + * ** BONUS ** + * + * Now, use `pairwise()` directly in `.react()`. Implement the same + * derivation as before: subtract the previous value from the current. + */ + reactSpy = jest.fn(__YOUR_TURN__); + myCounter$.react(reactSpy); + + expect(reactSpy).toHaveLastReturnedWith(1); + + myCounter$.set(3); + + expect(reactSpy).toHaveLastReturnedWith(2); + + myCounter$.set(10); + + expect(reactSpy).toHaveLastReturnedWith(7); + }); + + it('scan - BONUS', () => { + const myCounter$ = atom(1); + let reactSpy = jest.fn(); + + /** + * ** Your Turn ** + * ** BONUS ** + * + * Now, use `scan()` directly in `.react()`. Implement the same + * derivation as before: subtract all the emitted values. + */ + + reactSpy = jest.fn(__YOUR_TURN__); + myCounter$.react(reactSpy); + + expect(reactSpy).toHaveLastReturnedWith(1); + + myCounter$.set(3); + + expect(reactSpy).toHaveLastReturnedWith(2); + + myCounter$.set(10); + + expect(reactSpy).toHaveLastReturnedWith(8); + }); + + /** + * A `struct()` can combine an Object/Array of `Derivable`s into one + * `Derivable`, that contains the values of that `Derivable`. + * + * The Object/Array that is in the output of `struct()` will have the same + * structure as the original Object/Array. + * + * This is best explained in practice. + */ + it('struct', () => { + const allMyAtoms = { + regularProp: 'prop', + string: atom('my string'), + number: atom(1), + sub: { + string: atom('my substring'), + }, + }; + + const myOneAtom$ = struct(allMyAtoms); + + expect(myOneAtom$.get()).toEqual({ + regularProp: 'prop', + string: 'my string', + number: 1, + sub: { + string: 'my substring', + }, + }); + + // Note: we change the original object, not the struct. + allMyAtoms.regularProp = 'new value'; + allMyAtoms.sub.string.set('my new substring'); + + /** + * ** Your Turn ** + * + * Now have a look at the properties of `myOneAtom$`. Is this what you + * expect? + */ + + expect(myOneAtom$.get()).toEqual({ + regularProp: __YOUR_TURN__, + string: __YOUR_TURN__, + number: __YOUR_TURN__, + sub: { + string: __YOUR_TURN__, + }, + }); + + }); + + describe.skip('lift()', () => { + /** + * Derivables can feel like a language build on top of Typescript. Sometimes + * you might want to use normal objects and functions and not have to rewrite + * your code. + * In other words, just like keywords like `atom(V)` lifts a variable V to the higher + * level of Derivables, the `lift(F)` keyword lifts a function `F` to the higher + * level of Derivables. + */ + it('example', () => { + // Example: after years of effort, Bob finally finished his oh-so complicated function: + const isEvenNumber = (v: number) => v % 2 == 0; + + // Rewriting this function to work with derivables would now be a waste of time. + /** + * ** Your Turn ** + * Use the `lift()` function to change `isEvenNumber` to work on Derivables instead. + * In other words: the new function should take a `Derivable` (or more specifically: + * an `Unwrappable`) and return a `Derivable`. + */ + const isEvenDerivable = __YOUR_TURN__; + + expect(isEvenNumber(2)).toBe(true); + expect(isEvenNumber(13)).toBe(false); + expect(isEvenDerivable(atom(2)).get()).toBe(true); + expect(isEvenDerivable(atom(13)).get()).toBe(false); + }); + + it('`lift()` as alternative to `.map()`', () => { + // In tutorial 7, we saw `.map()` used in the following context: + const addOne = jest.fn((v: number) => v + 1); + const myAtom$ = atom(1); + + let myMappedDerivable$ = myAtom$.map(addOne); + + expect(myMappedDerivable$.value).toBe(2); + + /** + * ** Your Turn ** + * Now, use `lift()` as alternative to `.map()`. + */ + myMappedDerivable$ = __YOUR_TURN__; + + expect(myMappedDerivable$.value).toBe(2); + }); + }); + + /** + * Sometimes you want to use `derive` but still want to keep certain + * variables in it untracked. In such cases, you can use `peek()`. + */ + it('`peek()`', () => { + const myTrackedAtom$ = atom(1); + const myUntrackedAtom$ = atom(2); + + /** + * ** Your Turn ** + * Use `peek()` to get the value of `myUntrackedAtom$` and add it to the + * value of `myTrackedAtom$`, which should be tracked. + */ + const reactor = jest.fn(v => v); + derive(__YOUR_TURN__).react(reactor); + + expect(reactor).toHaveBeenCalledOnce(); + expect(reactor).toHaveLastReturnedWith(3); + + myTrackedAtom$.set(2); + expect(reactor).toHaveBeenCalledTimes(2); + expect(reactor).toHaveLastReturnedWith(4); + + myUntrackedAtom$.set(3); + expect(reactor).toHaveBeenCalledTimes(2); + expect(reactor).toHaveLastReturnedWith(4); + + myTrackedAtom$.set(3); + expect(reactor).toHaveBeenCalledTimes(3); + expect(reactor).toHaveLastReturnedWith(6); + }); +}); diff --git a/generated_tutorial/9 - expert.test.ts b/generated_tutorial/9 - expert.test.ts new file mode 100644 index 0000000..cd382f7 --- /dev/null +++ b/generated_tutorial/9 - expert.test.ts @@ -0,0 +1,450 @@ +import { DerivableAtom, atom, derive } from '@skunkteam/sherlock'; +import { derivableCache } from '@skunkteam/sherlock-utils'; + +/** + * ** Your Turn ** + * + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +describe.skip('expert', () => { + describe.skip('`.autoCache()`', () => { + /** + * If a `.get()` is called on a `Derivable` all derivations will be + * executed. But what if a `Derivable` is used multiple times in another + * `Derivable`? + */ + it('multiple executions', () => { + const hasDerived = jest.fn(); + + const myAtom$ = atom(true); + const myFirstDerivation$ = myAtom$.derive(hasDerived); + const mySecondDerivation$ = myFirstDerivation$.derive( + () => myFirstDerivation$.get() + myFirstDerivation$.get(), + ); + + /** + * ** Your Turn ** + * + * `hasDerived` is used in the first derivation. But has it been + * called at this point? + */ + + // `.toHaveBeenCalled()` or `.not.toHaveBeenCalled()`? ↴ + expect(hasDerived) /* Your Turn */; + + mySecondDerivation$.get(); + + /** + * ** Your Turn ** + * + * Now that we have gotten `mySecondDerivation$`, which calls + * `.get()` on the first multiple times. How many times has the + * first `Derivable` actually executed its derivation? + */ + // how many times? + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + }); + + /** + * So when a `Derivable` is reacting the value is cached and can be + * gotten from cache. But if this `Derivable` is used multiple times in + * a row, even in another derivation it isn't cached. + * + * To fix this issue, `.autoCache()` exists. It will cache the + * `Derivable`s value until the next Event Loop `tick`. + * + * So let's try the example above with this feature + */ + it('autoCaching', async () => { + const firstHasDerived = jest.fn(); + const secondHasDerived = jest.fn(); + + /** + * ** Your Turn ** + * + * Use `.autoCache()` on one of the `Derivable`s below. To make the + * expectations pass. + */ + const myAtom$ = atom(true); + const myFirstDerivation$ = myAtom$.derive(firstHasDerived); + const mySecondDerivation$ = myFirstDerivation$.derive(() => + secondHasDerived(myFirstDerivation$.get() + myFirstDerivation$.get()), + ); + + // first before .get() + expect(firstHasDerived).not.toHaveBeenCalled(); + + // second before .get() + expect(secondHasDerived).not.toHaveBeenCalled(); + + mySecondDerivation$.get(); + + // first after first .get() + expect(firstHasDerived).toHaveBeenCalledOnce(); + // second after first .get() + expect(secondHasDerived).toHaveBeenCalledOnce(); + + mySecondDerivation$.get(); + + // first after second .get() + expect(firstHasDerived).toHaveBeenCalledOnce(); + // second after second .get() + expect(secondHasDerived).toHaveBeenCalledTimes(2); + + /** + * Notice that the first `Derivable` has only been executed once, + * even though the second `Derivable` executed twice. + * + * Now we wait a tick for the cache to be invalidated. + */ + + await new Promise(r => setTimeout(r, 1)); + + /** + * ** Your Turn ** + * + * Now what do you expect? + */ + mySecondDerivation$.get(); + + // first after last .get() + expect(firstHasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + // second after last .get() + expect(secondHasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + }); + }); + + /** + * Some `Derivable`s need an input to be calculated. If this `Derivable` is + * async or has a big setup process, you may still want to create it only + * once, even if the `Derivable` is requested more than once for the same + * resource. + * + * Let's imagine a `stockPrice$(stock: string)` function, which returns a + * `Derivable` with the current price for the given stock. This `Derivable` + * is async, since it will try to retrieve the current price on a distant + * server. + * + * Let's see what can go wrong first, and we will try to fix it after that. + * + * *Note that a `Derivable` without an input is (hopefully) created only + * once, so it does not have this problem* + */ + describe.skip('`derivableCache`', () => { + type Stocks = 'GOOGL' | 'MSFT' | 'APPL'; + + let stockPrice$: jest.Mock, [Stocks], any>; + const reactSpy = jest.fn(); + + beforeEach(() => { + // By default the stock price retriever returns unresolved. + stockPrice$ = jest.fn(_ => atom.unresolved()); + reactSpy.mockClear(); + }); + + function reactor(v: any) { + reactSpy(v); + } + + /** + * If the function to create the `Derivable` is called multiple times, + * the `Derivable` will be created multiple times. Any setup this + * `Derivable` does, will be executed every time. + */ + it('multiple setups', () => { + // To not make things difficult with `unresolved` for this example, + // imagine we get a response synchronously + stockPrice$ = jest.fn(_ => atom(1079.11)); + + const html$ = derive( + () => ` +

Alphabet Price ($${stockPrice$('GOOGL').get().toFixed(2)})

+

Some important text that uses the current price ($${stockPrice$('GOOGL') + .get() + .toFixed()}) as well

+ `, + ); + html$.react(reactor); + + expect(html$.connected).toEqual(true); + expect(reactSpy).toHaveBeenCalledOnce(); + + /** + * ** Your Turn ** + * + * The `Derivable` is connected and has emitted once, but in that + * value the 'GOOGL' stockprice was displayed twice. We know that + * using a `Derivable` twice in a connected `Derivable` will make + * the second `.get()` use a cached value. + * + * But does that apply here? + * How many times has the setup run, for the price `Derivable`. + */ + expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); + + /** Can you explain this behavior? */ + // ANSWER-BLOCK-START + // Yes: it creates a different Derivable every time, so it cannot use any caching. + // This is a similar issue to the `pairwise()` issue from tutorial 7, where, when we + // used lambda functions, we made a new pairwise object every time. + // ANSWER-BLOCK-END + }); + + /** + * An other problem can arise when the setup is done inside a derivation + */ + describe.skip('setup inside a derivation', () => { + /** + * When the setup of a `Derivable` is done inside the same + * derivation as where `.get()` is called. You may be creating some + * problems. + */ + it('unresolveable values', () => { + // First setup an `Atom` with the company we are currently + // interested in + const company$ = atom('GOOGL'); + + // Based on that `Atom` we derive the stockPrice + const price$ = company$.derive(company => stockPrice$(company).get()); + + price$.react(reactor); + + // Because the stockPrice is still `unresolved` the reactor + // should not have emitted anything yet + expect(reactSpy).not.toHaveBeenCalled(); + + // Now let's increase the price + // First we have to get the atom that was given by the + // `stockPrice$` stub + const googlPrice$ = stockPrice$.mock.results[0].value as DerivableAtom; + + // Check if it is the right `Derivable` + expect(googlPrice$.connected).toEqual(true); + expect(googlPrice$.value).toEqual(undefined); + + // Then we set the price + googlPrice$.set(1079.11); + + /** + * ** Your Turn ** + * + * So the value was increased. What do you think happened? + */ + + // How often was the reactor on price$ called? + expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + + // And how many times did the setup run? + expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); + + // What's the value of price$ now? + expect(price$.value).toEqual(__YOUR_TURN__); + + // And the value of googlPrice$? + expect(googlPrice$.value).toEqual(__YOUR_TURN__); + + // Is googlPrice$ still even driving any reactors? + expect(googlPrice$.connected).toEqual(__YOUR_TURN__); + + /** + * Can you explain this behavior? + * + * Thought about it? Here is what happened: + * - Initially `stockPrice$('GOOGL')` emits a `Derivable` + * (`googlPrice$`), which is unresolved. + * - Inside the `.derive()` we subscribe to updates on that + * `Derivable`. + * - When `googlPrice$` emits a new value, the `.derive()` step + * is run again. + * - Inside this step, the setup is run again and a new + * `Derivable` (`newGooglPrice$`) is created and subscribed + * to. + * - Unsubscribing from the old `googlPrice$`. + * + * This `newGooglPrice$` is newly created and `unresolved` + * again. So the end result is an `unresolved` `price$` + * `Derivable`. + */ + + /** + * ** BONUS ** + * + * The problem above can be fixed without a `derivableCache`. + * If we split the `.derive()` step into two steps, where the + * first does the setup, and the second unwraps the `Derivable` + * created in the first. This way, a newly emitted value from + * the created `Derivable` will not run the setup again and + * everything should work as expected. + * + * ** Your Turn ** TODO: not in the SOLUTIONS!! + * + * *Hint: there is even an `unwrap` helper function for just + * such an occasion, try it!* + */ + }); + + /** + * But even when you split the setup and the `unwrap`, you may not + * be out of the woods yet! This is actually a problem that most + * libraries have a problem with, if not properly accounted for. + */ + it('uncached Derivables', () => { + // First we setup an `Atom` with the company we are currently + // interested in. This time we support multiple companies though + const companies$ = atom(['GOOGL']); + + // Based on that `Atom` we derive the stockPrices + const prices$ = companies$ + /** + * There is no need derive anything here, so we use `.map()` + * on `companies$`. And since `companies` is an array of + * strings, we `.map()` over that array to create an array + * of `Derivable`s. + */ + .map(companies => companies.map(company => stockPrice$(company))) + // Then we get the prices from the created `Derivable`s in a separate step + .derive(price$s => price$s.map(price$ => price$.value)); + + prices$.react(reactor); + + // Because we use `.value` instead of `.get()` the reactor + // should emit immediately this time. + + // But it should emit `undefined`. + expect(reactSpy).toHaveBeenCalledExactlyOnceWith([undefined]); + + // Now let's increase the price. First we have to get the atom + // that was given by the `stockPrice$` stub: + const googlPrice$ = stockPrice$.mock.results[0].value as DerivableAtom; + // Check if it is the right `Derivable` + expect(googlPrice$.connected).toBe(true); + + // Then we set the price, as before + googlPrice$.set(1079.11); + + /** + * ** Your Turn ** + * + * So the value was increased. What do you think happened now? + */ + expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reactSpy).toHaveBeenLastCalledWith([__YOUR_TURN__]); + + /** + * So that worked, now let's try and add another company to the + * list. + */ + companies$.swap(current => [...current, 'APPL']); + + expect(companies$.get()).toEqual(['GOOGL', 'APPL']); + + /** + * ** Your Turn ** + * + * With both 'GOOGL' and 'APPL' in the list, what do we expect + * as an output? + * + * We had a price for 'GOOGL', but not for 'APPL'... + */ + expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reactSpy).toHaveBeenCalledWith([__YOUR_TURN__, __YOUR_TURN__]); + }); + }); + + /** + * So we know a couple of problems that can arise, but how do we fix + * them. + */ + describe.skip('a solution', () => { + /** + * Let's try putting `stockPrice$` inside a `derivableCache`. + * `derivableCache` requires a `derivableFactory`, this specifies + * the setup for a given key. + * + * We know the key, and what to do with it, so let's try it! + */ + const priceCache$ = derivableCache((company: Stocks) => stockPrice$(company)); + /** + * *Note that from this point forward we use `priceCache$` where we + * used to use `stockPrice$` directly* + */ + + it('should fix everything :-)', () => { + // First setup an `Atom` with the company we are currently + // interested in + const companies$ = atom(['GOOGL']); + + const html$ = companies$.derive(companies => + companies.map( + company => ` +

Alphabet Price ($ ${priceCache$(company).value || 'unknown'})

+

Some important text that uses the current price ($ ${ + priceCache$(company).value || 'unknown' + }) as well

`, + ), + ); + + html$.react(reactor); + + expect(html$.connected).toEqual(true); + expect(reactSpy).toHaveBeenCalledOnce(); + // Convenience function to return the first argument of the last + // call to the reactor + function lastEmittedHTMLs() { + return reactSpy.mock.lastCall[0]; + } + + // The last call, should have the array of HTML's as first + // argument + expect(lastEmittedHTMLs()[0]).toContain('$ unknown'); + + /** + * ** Your Turn ** + * + * The `Derivable` is connected and has emitted once. + * The price for the given company 'GOOGL' is displayed twice, + * just as in the first test. + * + * Has anything changed, by using the `derivableCache`? + */ + expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); + + // Now let's resolve the price + stockPrice$.mock.results[0].value.set(1079.11); + + /** + * ** Your Turn ** + * + * Last time this caused the setup to run again, resolving to + * `unresolved` yet again. + * + * What happens this time? Has the setup run again? + */ + expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); + // Ok, but did it update the HTML? + expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(lastEmittedHTMLs()[0]).toContain(__YOUR_TURN__); + + // Last chance, what if we add a company + companies$.swap(current => [...current, 'APPL']); + + /** + * ** Your Turn ** + * + * Now the `stockPrice$` function should have at least run again + * for 'APPL'. + * + * But did it calculate 'GOOGL' again too? + */ + expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + // The first should be the generated HTML for 'GOOGL'. + expect(lastEmittedHTMLs()[0]).toContain(__YOUR_TURN__); + // The second should be the generated HTML for 'APPL'. + expect(lastEmittedHTMLs()[1]).toContain(__YOUR_TURN__); + }); + }); + }); +}); diff --git a/generated_tutorial/jest.config.ts b/generated_tutorial/jest.config.ts new file mode 100644 index 0000000..98e00f2 --- /dev/null +++ b/generated_tutorial/jest.config.ts @@ -0,0 +1,18 @@ +import type { Config } from 'jest'; + +export default { + displayName: 'generated_tutorial', + preset: '../jest.preset.js', + globals: {}, + testEnvironment: 'node', + transform: { + '^.+\\.[tj]sx?$': [ + 'ts-jest', + { + tsconfig: '/tsconfig.spec.json', + }, + ], + }, + collectCoverage: false, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], +} satisfies Config; diff --git a/generated_tutorial/tsconfig.json b/generated_tutorial/tsconfig.json new file mode 100644 index 0000000..89cc8af --- /dev/null +++ b/generated_tutorial/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/generated_tutorial/tsconfig.spec.json b/generated_tutorial/tsconfig.spec.json new file mode 100644 index 0000000..750d7b4 --- /dev/null +++ b/generated_tutorial/tsconfig.spec.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "types": ["jest", "jest-extended", "node"] + }, + "include": ["**/*.test.ts", "**/*.tests.ts", "**/*.d.ts", "jest.config.ts"] +} diff --git a/generator/1 - intro.test.ts b/generator/1 - intro.test.ts new file mode 100644 index 0000000..23db949 --- /dev/null +++ b/generator/1 - intro.test.ts @@ -0,0 +1,144 @@ +import { atom } from '@skunkteam/sherlock'; + +/** + * ** Your Turn ** + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +/** + * Welcome to the `@skunkteam/sherlock` tutorial. + * + * It is set up as a collection of specs, with the goal of getting all the specs + * to pass. The `expect()`s and basic setup are there, you just need to get it + * to work. + * + * All specs except the first one are set to `.skip`. Remove this to start on + * that part of the tutorial. + * + * Start the tutorial by running: + * `npm run tutorial`. + * + * To not manually re-enter the command, use: + * `npm run tutorial -- --watch` + * This will automatically rerun the tests when a file change has been detected. + * + * *Hint: most methods and functions are fairly well documented in jsDoc, + * which is easily accessed through TypeScript* + */ +describe('intro', () => { + it(` + + --- Welcome to the tutorial! --- + + Please look in \`./tutorial/1 - intro.test.ts\` to see what to do next.`, () => { + // At the start of the spec, there will be some setup. + let bool = false; + + // Sometimes including an expectation, to show the current state. + expect(bool).toBeFalse(); + + /** + * If ** Your Turn ** is shown in a comment, there is work for you to do. + * This can also be indicated with the `__YOUR_TURN__` variable. + * + * It should be clear what to do here... */ + bool = __YOUR_TURN__; // #QUESTION + bool = true; // #ANSWER + expect(bool).toBeTrue(); + // We use expectations like this to verify the result. + }); +}); + +/** + * Let's start with the `Derivable` basics. + * + * ** Your Turn ** + * Remove the `.skip` so this part of the tutorial will run. + */ +describe('the basics', () => { + /** + * The `Atom` is the basic building block of `@skunkteam/sherlock`. + * It holds a value which you can `get()` and `set()`. + */ + it('the `Atom`', () => { + // An `Atom` can be created with the `atom()` function. The parameter + // of this function is used as the initial value of the `Atom`. + const myValue$ = atom(1); + // Variables containing `Atom`s or any other `Derivable` are usually + // postfixed with a `$` to indicate this. Hence `myValue$`. + + // The `.get()` method can be used to get the current value of + // the `Atom`. + expect(myValue$.get()).toEqual(1); + + // ** Your Turn ** // #QUESTION + myValue$.set(2); // #ANSWER + // Use the `.set()` method to change the value of the `Atom`. + expect(myValue$.get()).toEqual(2); + }); + + /** + * The `Atom` is a `Derivable`. This means it can be used to create a + * derived value. This derived value stays up to date with the original + * `Atom`. + * + * The easiest way to do this, is to call `.derive()` on another + * `Derivable`. + * + * Let's try this. + */ + it('the `Derivable`', () => { + const myValue$ = atom(1); + expect(myValue$.get()).toEqual(1); + + /** + * ** Your Turn ** + * + * We want to create a new `Derivable` that outputs the inverse (from a + * negative to a positive number and vice versa) of the original `Atom`. + */ + // Use `myValue$.derive(val => ...)` to implement `myInverse$`. + const myInverse$ = myValue$.derive(__YOUR_TURN__ => __YOUR_TURN__); // #QUESTION + const myInverse$ = myValue$.derive(val => -val); // #ANSWER + expect(myInverse$.get()).toEqual(-1); + // So if we set `myValue$` to -2: + myValue$.set(-2); + // `myInverse$` will change accordingly. + expect(myInverse$.get()).toEqual(2); + }); + + /** + * Of course, `Derivable`s are not only meant to get, set and derive state. + * You can also listen to the changes. + * + * This is done with the `.react()` method. + * This method is given a function that is executed every time the value of + * the `Derivable` changes. + */ + it('reacting to `Derivable`s', () => { + const myCounter$ = atom(0); + let reacted = 0; + + /** + * ** Your Turn ** + * + * Now react to `myCounter$`. In every `react()`. + * Increase the `reacted` variable by one. */ + myCounter$.react(() => __YOUR_TURN__); // #QUESTION + myCounter$.react(() => reacted++); // #ANSWER + expect(reacted).toEqual(1); + // `react()` will react immediately, more on that later. + + /** + * And then we set the `Atom` a couple of times + * to make the `Derivable` react. + * */ + for (let i = 0; i <= 100; i++) { + // Set the value of the `Atom`. + myCounter$.set(i); + } + + expect(reacted).toEqual(101); + }); +}); diff --git a/generator/2 - deriving.test.ts b/generator/2 - deriving.test.ts new file mode 100644 index 0000000..149d05b --- /dev/null +++ b/generator/2 - deriving.test.ts @@ -0,0 +1,252 @@ +import { atom, Derivable, derive } from '@skunkteam/sherlock'; + +/** + * ** Your Turn ** + * + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +/** + * Any `Derivable` (including `Atom`s) can be used (and/or combined) to create + * a derived state. This derived state is in turn a `Derivable`. + * + * There are a couple of ways to do this. + */ +describe('deriving', () => { + /** + * In the 'intro' we have created a derivable by using the `.derive()` method. + * This method allows the state of that `Derivable` to be used to create a + * new `Derivable`. + * + * In the derivation, other `Derivable`s can be used as well. + * If a `Derivable.get()` is called inside a derivation, the changes to that + * `Derivable` are also tracked and kept up to date. + */ + it('combining `Derivable`s', () => { + const repeat$ = atom(1); + const text$ = atom(`It won't be long`); + + /** + * ** Your Turn ** + * + * Let's create some lyrics by combining `text$` and `repeat$`. + * As you might have guessed, we want to repeat the text a couple of times. + * + * (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat should do fine) + */ + + // We can combine txt with `repeat$.get()` here. + const lyric$ = text$.derive(txt => txt /* __YOUR_TURN__ */); // #QUESTION + const lyric$ = text$.derive(txt => txt.repeat(repeat$.get())); // #ANSWER + + expect(lyric$.get()).toEqual(`It won't be long`); + + text$.set(' yeah'); + repeat$.set(3); + expect(lyric$.get()).toEqual(` yeah yeah yeah`); + }); + + /** + * Now that we have used `.get()` in a `.derive()`. You may wonder, can + * we skip the original `Derivable` and just call the function `derive()`? + * + * Of course you can! + * + * And you can use any `Derivable` you want, even if they all have the same + * `Atom` as a parent. + */ + it('the `derive()` function', () => { + const myCounter$ = atom(1); + + /** + * ** Your Turn ** + * + * Let's try creating a `Derivable` [FizzBuzz](https://en.wikipedia.org/wiki/Fizz_buzz). + * `fizzBuzz$` should combine `fizz$`, `buzz$` and `myCounter$` to + * produce the correct output. + * + * Multiple `Derivable`s can be combined to create a new one. To do + * this, just use `.get()` on (other) `Derivable`s in the `.derive()` + * step. + * + * This can be done both when `derive()` is used standalone or as a + * method on another `Derivable`. + */ + + // Should return 'Buzz' when `myCounter$` is a multiple of 5 and '' otherwise. + + // Should return 'Fizz' when `myCounter$` is a multiple of 3 and '' otherwise. + const fizz$: Derivable = myCounter$.derive(__YOUR_TURN__); // #QUESTION + const fizz$: Derivable = myCounter$.derive(v => (v % 3 ? 'Fizz' : '')); // Shorthand for `v % 3 === 0` // #ANSWER + + // Should return 'Buzz' when `myCounter$` is a multiple of 5 and '' otherwise. + const buzz$: Derivable = myCounter$.derive(__YOUR_TURN__); // #QUESTION + const buzz$: Derivable = myCounter$.derive(v => (v % 5 ? 'Buzz' : '')); // #ANSWER + + const fizzBuzz$: Derivable = derive(__YOUR_TURN__); // #QUESTION + // #ANSWER-BLOCK-START + const fizzBuzz$: Derivable = derive(() => fizz$.get() + buzz$.get() || myCounter$.get()); + // #ANSWER-BLOCK-END + + expect(fizz$.get()).toEqual(''); + expect(buzz$.get()).toEqual(''); + expect(fizzBuzz$.get()).toEqual(1); + for (let count = 1; count <= 100; count++) { + // Set the value of the `Atom`, + myCounter$.set(count); + + // and check if the output changed accordingly. + checkFizzBuzz(count, fizzBuzz$.get()); + } + }); + + function checkFizzBuzz(count: number, out: string | number) { + if ((count % 3) + (count % 5) === 0) { + // If `count` is a multiple of 3 AND 5, output 'FizzBuzz'. + expect(out).toEqual('FizzBuzz'); + } else if (count % 3 === 0) { + // If `count` is a multiple of 3, output 'Fizz'. + expect(out).toEqual('Fizz'); + } else if (count % 5 === 0) { + // If `count` is a multiple of 5, output 'Buzz'. + expect(out).toEqual('Buzz'); + } else { + // Otherwise just output the `count` itself. + expect(out).toEqual(count); + } + } + + /** + * The automatic tracking of `.get()` calls will also happen inside called + * `function`s. + * + * This can be really powerful, but also dangerous. One of the dangers is + * shown here. + */ + it('indirect derivations', () => { + const pastTweets = [] as string[]; + const currentUser$ = atom('Barack'); + const tweet$ = atom('First tweet'); + + function log(tweet: string) { + pastTweets.push(`${currentUser$.get()} - ${tweet}`); + } + + tweet$.derive(log).react(txt => { + // Normally we would do something with the tweet here. + return txt; + }); + + // The first tweet should have automatically been added to the `pastTweets` array. + expect(pastTweets).toHaveLength(1); + expect(pastTweets[0]).toContain('Barack'); + expect(pastTweets[0]).toContain('First tweet'); + + // Let's add a famous quote by Mr Barack: + tweet$.set('We need to reject any politics that targets people because of race or religion.'); + // As expected this is automatically added to the log. + expect(pastTweets).toHaveLength(2); + expect(pastTweets[1]).toContain('Barack'); + expect(pastTweets[1]).toContain('reject'); + + // But what if the user changes? + currentUser$.set('Donald'); + + /** + * ** Your Turn ** + * + * Time to set your own expectations. + */ + const tweetCount = pastTweets.length; + const lastTweet = pastTweets[tweetCount - 1]; + + expect(tweetCount).toEqual(__YOUR_TURN__); // Is there a new tweet? // #QUESTION + expect(tweetCount).toEqual(3); // Is there a new tweet?// #ANSWER + expect(lastTweet).toContain(__YOUR_TURN__); // Who sent it? Donald? Or Barack?// #QUESTION + expect(lastTweet).toContain('Donald'); // Who sent it? Donald? Or Barack?// #ANSWER + expect(lastTweet).toContain(__YOUR_TURN__); // What did he tweet?// #QUESTION + expect(lastTweet).toContain('politics'); // What did he tweet?// #ANSWER + + /** + * As you can see, this is something to look out for. + * Luckily there are ways to circumvent this. But more on that later. + * + * * Note that this behavior can also be really helpful if you know what + * you are doing * + */ + }); + + /** + * Every `Derivable` has a couple of convenience methods. + * These are methods that make common derivations a bit easier. + * + * These methods are: `.and()`, `.or()`, `.is()` and `.not()`. + * + * Their function is as you would expect from `boolean` operators in a + * JavaScript environment. + * + * The first three will take a `Derivable` or regular value as parameter. + * `.not()` does not need any input. + * + * `.is()` will resolve equality in the same way as `@skunkteam/sherlock` + * would do internally. + * + * More on the equality check in the 'inner workings' part. But know that + * the first check is [Object.is()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) + */ + it('convenience methods', () => { + const myCounter$ = atom(1); + + /** + * ** Your Turn ** + * + * The FizzBuzz example above can be rewritten using the convenience + * methods. This is not how you would normally write it, but it looks + * like a fun excercise. + * + * `fizz$` and `buzz$` can be completed with only `.is(...)`, + * `.and(...)` and `.or(...)`. Make sure the output of those `Derivable`s + * is either 'Fizz'/'Buzz' or ''. + */ + // #QUESTION-BLOCK-START + const fizz$ = myCounter$ + .derive(count => count % 3) + .is(__YOUR_TURN__) + .and(__YOUR_TURN__) + .or(__YOUR_TURN__) as Derivable; + + const buzz$ = myCounter$ + .derive(count => count % 5) + .is(__YOUR_TURN__) + .and(__YOUR_TURN__) + .or(__YOUR_TURN__) as Derivable; + // #QUESTION-BLOCK-END + + // #ANSWER-BLOCK-START + const fizz$ = myCounter$ + .derive(count => count % 3) + .is(0) + .and('Fizz') + .or(''); + + const buzz$ = myCounter$ + .derive(count => count % 5) + .is(0) + .and('Buzz') + .or(''); + // #ANSWER-BLOCK-END + + const fizzBuzz$ = derive(() => fizz$.get() + buzz$.get()).or(__YOUR_TURN__); // #QUESTION + const fizzBuzz$ = derive(() => fizz$.get() + buzz$.get()).or(myCounter$); // #ANSWER + // This will check whether `fizz$.get() + buzz$.get()` is truthy: if so, return it; if not, return `myCounter$` // #ANSWER + + for (let count = 1; count <= 100; count++) { + // Set the value of the `Atom`, + myCounter$.set(count); + + // and check if the output changed accordingly. + checkFizzBuzz(count, fizzBuzz$.get()); + } + }); +}); diff --git a/generator/3 - reacting.test.ts b/generator/3 - reacting.test.ts new file mode 100644 index 0000000..3dc7c14 --- /dev/null +++ b/generator/3 - reacting.test.ts @@ -0,0 +1,585 @@ +import { atom } from '@skunkteam/sherlock'; + +/** + * ** Your Turn ** + * + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +// FIXME: check my solutions with the actual solutions +/** + * x 1 + * x 2 + * + */ +// FIXME: remove all TODO: and FIXME: +// FIXME: check whether the generated tutorials and solutions actually work (e.g. are all solutions correct? No weird shenanigans?) +// FIXME: deze file niet linten / builden (voor automatische test). Tutorial ook niet. Maar solutions juist wel! OP EIND. +// FIXME: interne review document, mocht ik iets hebben om te laten zien! In Google Drive, zet het erin! +// FIXME: werkt `npm run tutorial` nog??? + +/** + * In the intro we have seen a basic usage of the `.react()` method. + * Let's dive a bit deeper into the details of this method. + */ +describe('reacting', () => { + // For easy testing we can count the number of times a reactor was called, + let wasCalledTimes: number; + // and record the last value it reacted to. + let lastValue: any; + + // reset the values before each test case + beforeEach(() => { + wasCalledTimes = 0; + lastValue = undefined; + }); + + // The reactor to be given to the `.react()` method. + function reactor(val: any) { + wasCalledTimes++; + lastValue = val; + } + + // Of course we are lazy and don't want to type these assertions over + // and over. :-) + function expectReact(reactions: number, value?: any) { + // Reaction was called # times + expect(wasCalledTimes).toEqual(reactions); + // Note the actual point of failure is: + // at Object. (3 - reacting.test.ts:LINE_NUMBER:_) + // V ~~~~~~~~~~~~ + + // Last value of the reaction was # + expect(lastValue).toEqual(value); + } + + /** + * Every `Derivable` always has a current state. So the `.react()` method + * does not need to wait for a value, there already is one. + * + * This means that `.react()` will fire directly when called. When the + * `Derivable` has a new state, this will also fire `.react()` + * synchronously. + * + * So the very next line after `.set()` is called, the `.react()` has + * already fired! + * + * (Except when the `Derivable` is `unresolved`, but more on that later.) + */ + it('reacting synchronously', () => { + const myAtom$ = atom('initial value'); + // A trivial `expect` to silence TypeScript's noUnusedLocals. + expect(myAtom$.get()).toEqual('initial value'); + + // There should not have been a reaction yet + expectReact(0); + + /** + * ** Your Turn ** + * + * Time to react to `myAtom$` with the `reactor()` function defined + * above. + */ + __YOUR_TURN__; // #QUESTION + // #ANSWER-BLOCK-START + myAtom$.react(reactor); + // myAtom$.react(val => reactor(val)); // Alternatively, this would work too. + // myAtom$.react((val, _) => reactor(val)); // Or this. + // #ANSWER-BLOCK-END + + expectReact(1, 'initial value'); + + // Now set a 'new value' to `myAtom$`. + myAtom$.set('new value'); + + expectReact(2, 'new value'); + }); + + /** + * A reactor will go on forever. This is often not what you want, and almost + * always a memory leak. + * + * So it is important to stop a reactor at some point. The `.react()` method + * has different ways of dealing with this. + */ + describe('stopping a reaction', () => { + /** + * The easiest is the 'stopper' function, every `.react()` call will + * return a `function` that will stop the reaction. + */ + it('with the stopper function', () => { + const myAtom$ = atom('initial value'); + // A trivial `expect` to silence TypeScript's noUnusedLocals + expect(myAtom$.get()).toEqual('initial value'); + + /** + * ** Your Turn ** + * + * catch the returned `stopper` in a variable + */ + __YOUR_TURN__; // #QUESTION + const stopper = myAtom$.react(reactor); // #ANSWER + + expectReact(1, 'initial value'); + + /** + * ** Your Turn ** + * + * Call the `stopper`. + */ + __YOUR_TURN__; // #QUESTION + stopper(); // #ANSWER + + myAtom$.set('new value'); + + // And the reaction stopped. + expectReact(1, 'initial value'); + }); + + /** + * Everytime the reaction is called, it also gets the stopper `function` + * as a second parameter. + */ + it('with the stopper callback', () => { + const myAtom$ = atom('initial value'); + // A trivial `expect` to silence TypeScript's noUnusedLocals + expect(myAtom$.get()).toEqual('initial value'); + + /** + * ** Your Turn ** + * + * In the reaction below, use the stopper callback to stop the + * reaction + */ + // #QUESTION-BLOCK-START + myAtom$.react((val, __YOUR_TURN___) => { + reactor(val); + __YOUR_TURN___; + }); + // #QUESTION-BLOCK-END + + // #ANSWER-BLOCK-START + myAtom$.react((val, stopper) => { + reactor(val); + stopper(); + }); + // #ANSWER-BLOCK-END + + expectReact(1, 'initial value'); + + myAtom$.set('new value'); + + // And the reaction stopped. + expectReact(1, 'initial value'); + }); + }); + + /** + * The reactor `options` are a way to modify when and how the reactor will + * react to changes in the `Derivable`. + */ + describe('reactor options', () => { + /** + * Another way to make a reactor stop at a certain point, is by + * specifying an `until` in the `options`. + * `until` can be given either a `Derivable` or a `function`. + * + * If a `function` is given, this `function` will be given the + * `Derivable` that is the source of the reaction as a parameter. + * This `function` will track all `.get()`s, so can use any `Derivable`. + * It can return a `boolean` or a `Derivable`. + * + * *Note: the reactor options `when` and `from` can also be set to a + * `Derivable`/`function` as described here.* + * + * The reactor will stop directly when `until` becomes true. + * If that happens at exactly the same time as the `Derivable` getting a + * new value, it will not react again. + */ + describe('reacting `until`', () => { + const boolean$ = atom(false); + const string$ = atom('Value'); + beforeEach(() => { + // reset + boolean$.set(false); + string$.set('Value'); + }); + + /** + * If a `Derivable` is given, the reaction will stop once that + * `Derivable` becomes `true`/truthy. + */ + it('an external `Derivable`', () => { + /** + * ** Your Turn ** + * + * Try giving `boolean$` as `until` option. + */ + string$.react(reactor, __YOUR_TURN__); // #QUESTION + string$.react(reactor, { until: boolean$ }); // #ANSWER + + // It should react directly as usual. + expectReact(1, 'Value'); + + // It should keep reacting as usual. + string$.set('New value'); + expectReact(2, 'New value'); + + // We set `boolean$` to true, to stop the reaction + boolean$.set(true); + + // The reactor has immediately stopped, so it still reacted + // only twice: + expectReact(2, 'New value'); + + // Even when `boolean$` is set to `false` again... + boolean$.set(false); + + // ... and a new value is introduced: + string$.set('Another value'); + + // The reactor won't start up again, so it still reacted + // only twice: + expectReact(2, 'New value'); + }); + + /** + * A function can also be given as `until`. This function will be + * executed in every derivation. Just like using a `Derivable` as + * an `until`, the Reactor will keep reacting until the result of + * this function evaluates thruthy. + * + * This way any `Derivable` can be used in the calculation. + */ + it('a function', () => { + /** + * ** Your Turn ** + * + * Since the reactor options expect a boolean, you will + * sometimes need to calculate the option. + * + * Try giving an externally defined `function` that takes no + * parameters as `until` option. + * + * Use `!string$.get()` to return `true` when the `string` is + * empty. + */ + string$.react(reactor, __YOUR_TURN__); // #QUESTION + string$.react(reactor, { until: () => !string$.get() }); // #ANSWER + + // It should react as usual: + string$.set('New value'); + string$.set('Newer Value'); + expectReact(3, 'Newer Value'); + + // Until we set `string$` to an empty string to stop the + // reaction: + string$.set(''); + // The reactor was immediately stopped, so even the empty string + // was never given to the reactor: + expectReact(3, 'Newer Value'); + }); + + /** + * Since the example above where the `until` is based on the parent + * `Derivable` occurs very frequently, this `Derivable` is given as + * a parameter to the `until` function. + */ + it('the parent `Derivable`', () => { + /** + * ** Your Turn ** + * + * Try using the first parameter of the `until` function to do + * the same as above. + */ + string$.react(reactor, __YOUR_TURN__); // #QUESTION + string$.react(reactor, { until: s => !s.get() }); // #ANSWER + + // It should react as usual. + string$.set('New value'); + string$.set('Newer Value'); + expectReact(3, 'Newer Value'); + + // Until we set `string$` to an empty string, to stop + // the reaction: + string$.set(''); + + // The reactor was immediately stopped, so even the empty string + // was never given to the reactor: + expectReact(3, 'Newer Value'); + }); + + /** + * Sometimes, the syntax may leave you confused. + */ + it('syntax issues', () => { + // It looks this will start reacting until `boolean$`s value is false... + let stopper = boolean$.react(reactor, { until: b => !b }); + + // ...but does it? (Remember: `boolean$` starts out as `false`) + expect(boolean$.connected).toBe(__YOUR_TURN__); // #QUESTION + expect(boolean$.connected).toBe(true); // #ANSWER + + // The `b` it obtains as argument is a `Derivable`. This is a + // reference value which will evaluate to `true` as it is not `undefined`. + // Thus, the negation will evaluate to `false`, independent of the value of + // the boolean. You can get the boolean value our of the `Derivable` using `.get()`: + stopper(); // reset + stopper = boolean$.react(reactor, { until: b => !b.get() }); + expect(boolean$.connected).toBe(__YOUR_TURN__); // #QUESTION + expect(boolean$.connected).toBe(false); // #ANSWER + + // You can also return the `Derivable` after appling the negation + // using the method designed for negating Derivables: + stopper(); + boolean$.react(reactor, { until: b => b.not() }); + expect(boolean$.connected).toBe(__YOUR_TURN__); // #QUESTION + expect(boolean$.connected).toBe(false); // #ANSWER + }); + }); + + /** + * Sometimes you may not need to react to the first couple of values of + * the `Derivable`. This can be because of the value of the `Derivable` + * or due to external conditions. + * + * The `from` option is meant to help with this. The reactor will only + * start after it becomes true. Once it has become true, the reactor + * will not listen to this option any more and react as usual. + * + * The interface of `from` is the same as `until` (i.e. it also gets + * the parent derivable as first parameter when it's called.) + */ + it('reacting `from`', () => { + const sherlock$ = atom(''); + + /** + * ** Your Turn ** + * + * We can react here, but restrict the reactions to start when the + * keyword 'dear' is set. This will skip the first three reactions, + * but react as usual after that. + * + * *Hint: remember the `.is()` method from tutorial 2?* + */ + sherlock$.react(reactor, __YOUR_TURN__); // #QUESTION + sherlock$.react(reactor, { from: sherlock$.is('dear') }); // #ANSWER + + expectReact(0); + ['Elementary,', 'my', 'dear', 'Watson'].forEach(txt => sherlock$.set(txt)); + + expectReact(2, 'Watson'); + }); + + /** + * Sometimes you may want to react only on certain values or when + * certain conditions are met. + * + * This can be achieved by using the `when` reactor option. + * Where `until` and `from` can only be triggered once to stop or start + * reacting, `when` can be flipped as often as you like and the reactor + * will respect the current state of the `when` function/Derivable. + */ + it('reacting `when`', () => { + const count$ = atom(0); + + /** + * ** Your Turn ** + * + * Now, let's react to all even numbers. + * Except 4, we don't want to make it too easy now. + */ + count$.react(reactor, __YOUR_TURN__); // #QUESTION + count$.react(reactor, { when: v => v.get() % 2 === 0 && v.get() !== 4 }); // #ANSWER + + expectReact(1, 0); + + for (let i = 0; i <= 4; i++) { + count$.set(i); + } + expectReact(2, 2); + for (let i = 4; i <= 10; i++) { + count$.set(i); + } + expectReact(5, 10); + }); + + /** + * Normally the reactor will immediately fire with the current value. + * If you want the reactor to fire normally, just not the first time, + * there is also a `boolean` option: `skipFirst`. + */ + it('reacting with `skipFirst`', () => { + const count$ = atom(0); + + /** + * ** Your Turn ** + * + * Say you want to react when `count$` is larger than 3. But not the first time... + */ + count$.react(reactor, __YOUR_TURN__); // #QUESTION + count$.react(reactor, { when: d => d.get() > 3, skipFirst: true }); // #ANSWER + + expectReact(0); + + for (let i = 0; i <= 5; i++) { + count$.set(i); + } + expectReact(1, 5); // it should have skipped the 4 + + for (let i = 0; i <= 5; i++) { + count$.set(i); + } + expectReact(3, 5); // now it should not have skipped the 4 + }); + + /** + * With `once` you can stop the reactor after it has emitted exactly + * one value. This is a `boolean` option. + * + * Without any other `options`, this is just a strange way of typing + * `.get()`. But when combined with `when`, `from` or `skipFirst`, it + * can be very useful. + */ + it('reacting `once`', () => { + const finished$ = atom(false); + + /** + * ** Your Turn ** + * + * Say you want to react when `finished$` is true. It can not finish + * twice. + * + * *Hint: you will need to combine `once` with another option* + */ + // finished$.react(reactor, __YOUR_TURN__); // #QUESTION + finished$.react(reactor, { once: true, when: f => f }); // `f => f.get()` is fine as well // #ANSWER + + expectReact(0); + + // When finished it should react once. + finished$.set(true); + expectReact(1, true); + + // After that it should really be finished. :-) + finished$.set(false); + finished$.set(true); + expectReact(1, true); + }); + }); + + describe('order of execution', () => { + /** + * As you can see for yourself in libs/sherlock/src/lib/derivable/mixins/take.ts, + * the options `from`, `until`, `when`, `skipFirst` and `once` are tested in this specific order: + * 1) firstly, `from` is checked. If `from` is/was true (or is not set in the options), we continue: + * 2) secondly, `until` is checked. If `until` is false (or is not set in the options), we continue: + * 3) thirdly, `when` is checked. If `when` is true (or is not set in the options), we continue: + * 4) fourthly, `skipFirst` is checked. If `skipFirst` is false (or is not set in the options), we continue: + * 5) lastly, `once` is checked. + * + * This means, for example, that `skipFirst` is only checked when `from` is true or unset, `until` is false or unset, + * and `when` is true or unset. If e.g. `when` evaluates to false, `skipFirst` cannot trigger. + */ + it('`from` and `until`', () => { + const myAtom$ = atom(0); + myAtom$.react(reactor, { from: v => v.is(3), until: v => v.is(2) }); + + for (let i = 1; i <= 5; i++) { + myAtom$.set(i); + } + + // The reactor starts reacting when `myAtom` gets the value 3, but stops when it gets the value 2. + // But because `myAtom` obtains the value 2 before it obtains 3... + // ...how many times was the reactor called, if any? + expectReact(__YOUR_TURN__); // #QUESTION + expectReact(3, 5); // `from` evaluates before `until`. // #ANSWER + }); + + it('`when` and `skipFirst`', () => { + const myAtom$ = atom(0); + myAtom$.react(reactor, { when: v => v.is(1), skipFirst: true }); + + myAtom$.set(1); + + // The reactor reacts when `myAtom` is 1 but skips the first number. + // The first number of `myAtom` is 0, its initial number. + // Does the reactor skip the 0 or the 1? + expectReact(__YOUR_TURN__); // #QUESTION + expectReact(0); // `skipFirst` triggers only when `when` evaluates to true. // #ANSWER + }); + + it('`from`, `until`, `when`, `skipFirst`, and `once`', () => { + const myAtom$ = atom(0); + myAtom$.react(reactor, { + from: v => v.is(5), + until: v => v.is(1), + when: v => [2, 3, 4].includes(v.get()), + skipFirst: true, + once: true, + }); + + for (let v of [1, 2, 3, 4, 5, 4, 3, 2, 1, 2, 3]) { + myAtom$.set(v); + } + + // `from` and `until` allow the reactor to respectively start when `myAtom` has value 5, and stop when it has value 1. + // Meanwhile, `when` allows neither of those values and only allows the values 2, 3, and 4. + // `skipFirst` and `once` are also added, just to bring the whole group together. + // so, how many times is the reactor called, and what was the last argument (if any)? + expectReact(__YOUR_TURN__); // #QUESTION + // #ANSWER-BLOCK-START + expectReact(1, 3); + // `from` makes it start at the first `5`. `when` allows the next `4`,`3`, and `2`, but + // `skipFirst` ensures that the first `4` is skipped. `once` then ensures that only the `3` + // reacted to. Before the `until` can trigger from a `1`, the `once` has already stopped it. + // #ANSWER-BLOCK-END + }); + }); + + describe('challenge', () => { + it('onDisconnect', () => { + const connected$ = atom('disconnected'); + /** + * ** Your Turn ** + * + * `connected$` indicates the current connection status: + * > 'connected'; + * > 'disconnected'; + * > 'standby'. + * + * We want our reactor to trigger once, when the device is not connected, + * which means it is either `standby` or `disconnected` (eg for cleanup). + * + * This should be possible with three simple ReactorOptions + */ + connected$.react(reactor, __YOUR_TURN__); // #QUESTION + connected$.react(reactor, { when: s => s.is('connected').not(), skipFirst: true, once: true }); // #ANSWER + + // It starts as 'disconnected' + expectReact(0); + + // At this point, the device connects, no reaction should occur yet. + connected$.set('connected'); + expectReact(0); + + // When the device goes to standby, the reaction should fire once + connected$.set('standby'); + expectReact(1, 'standby'); + + // After that, nothing should change anymore. + connected$.set('disconnected'); + expectReact(1, 'standby'); + connected$.set('standby'); + expectReact(1, 'standby'); + connected$.set('connected'); + expectReact(1, 'standby'); + + // It should not react again after this. + expect(connected$.connected).toBeFalse(); + // * Note: this `.connected` refers to whether this `Derivable` + // is being (indirectly) observed by a reactor. + }); + }); +}); diff --git a/generator/4 - inner workings.test.ts b/generator/4 - inner workings.test.ts new file mode 100644 index 0000000..05188ea --- /dev/null +++ b/generator/4 - inner workings.test.ts @@ -0,0 +1,348 @@ +import { atom } from '@skunkteam/sherlock'; +import { Seq } from 'immutable'; + +/** + * ** Your Turn ** + * + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +/** + * Time to dive a bit deeper into the inner workings of `@skunkteam/sherlock`. + */ +describe('inner workings', () => { + /** + * What if there is a derivation that reads from one of two `Derivable`s + * dynamically? Will both of those `Derivable`s be tracked for changes? + */ + it('dynamic/inactive dependencies', () => { + const switch$ = atom(true); + const number$ = atom(1); + const string$ = atom('one'); + + const reacted = jest.fn(); + + switch$ + // This `.derive()` is the one we are testing when true, it will + // return the `number` otherwise the `string` + .derive(s => (s ? number$.get() : string$.get())) + .react(reacted); + + // The first time should not surprise anyone, the derivation + // was called and returned the right result. + expect(reacted).toHaveBeenCalledExactlyOnceWith(1, expect.toBeFunction()); + // Note here the second expectation `.toBeFunction()` to + // catch the stop function that was part of the .react() signature. + + // `switch$` is still set to true (number) + string$.set('two'); + + /** + * ** Your Turn ** + * + * What do you expect? + */ + // #QUESTION-BLOCK-START + expect(reacted).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reacted).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + // #QUESTION-BLOCK-END + // #ANSWER-BLOCK-START + expect(reacted).toHaveBeenCalledTimes(1); + expect(reacted).toHaveBeenLastCalledWith(1, expect.toBeFunction()); + // Note: the reactor doesn't know that changing `string$` will not generate a different + // answer by looking at the code of `switch$`, but instead it simply noticed that + // `switch$` got the same value it already had and prevented triggering because of that. + // #ANSWER-BLOCK-END + + // `switch$` is still set to true (number) + number$.set(2); + + /** + * ** Your Turn ** + * + * What do you expect? + */ + // #QUESTION-BLOCK-START + expect(reacted).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reacted).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + // #QUESTION-BLOCK-END + // #ANSWER-BLOCK-START + expect(reacted).toHaveBeenCalledTimes(2); + expect(reacted).toHaveBeenLastCalledWith(2, expect.toBeFunction()); + // As it got a different value (`2` instead of `1`), it triggered. + // #ANSWER-BLOCK-END + + // `switch$` is now set to false (string) + switch$.set(false); + number$.set(3); + + /** + * ** Your Turn ** + * + * What do you expect now? + */ + // #QUESTION-BLOCK-START + expect(reacted).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reacted).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + // #QUESTION-BLOCK-END + // #ANSWER-BLOCK-START + expect(reacted).toHaveBeenCalledTimes(3); + expect(reacted).toHaveBeenLastCalledWith('two', expect.toBeFunction()); + // #ANSWER-BLOCK-END + }); + + /** + * One thing to know about `Derivable`s is that derivations are not + * executed, until someone asks. + * + * So let's test this. + */ + it('lazy execution', () => { + const hasDerived = jest.fn(); + + const myAtom$ = atom(true); + const myDerivation$ = myAtom$.derive(hasDerived); + + /** + * ** Your Turn ** + * + * We have created a new `Derivable` by deriving the `Atom`. But have + * not called `.get()` on that new `Derivable`. + * + * How many times do you think the `hasDerived` function has been + * called? 0 is also an option of course. + */ + + // Well, what do you expect? + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(hasDerived).toHaveBeenCalledTimes(0); // #ANSWER + + myDerivation$.get(); + + // And after a `.get()`? + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(hasDerived).toHaveBeenCalledTimes(1); // #ANSWER + + myDerivation$.get(); + + // And after the second `.get()`? Is there an extra call? + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(hasDerived).toHaveBeenCalledTimes(2); // #ANSWER + + /** + * The state of any `Derivable` can change at any moment. + * + * But you don't want to keep a record of the state and changes to a + * `Derivable` that no one is listening to. + * + * That's why a `Derivable` has to recalculate it's internal state every + * time `.get()` is called. + */ + }); + + /** + * So what if the `Derivable` is reacting? + * + * When a `Derivable` is reacting, the current state is known. + * + * And since changes are derived/reacted to synchronously, the state is + * always up to date. + * + * So a `.get()` should not have to be calculated. + */ + it('while reacting', () => { + const hasDerived = jest.fn(); + + const myAtom$ = atom(true); + const myDerivation$ = myAtom$.derive(hasDerived); + + // It should not have done anything at this moment + expect(hasDerived).not.toHaveBeenCalled(); + + const stopper = myDerivation$.react(() => ''); + + /** + * ** Your Turn ** + * + * Ok, it's your turn to complete the expectations. + */ + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(hasDerived).toHaveBeenCalledTimes(1); // because of the react. // #ANSWER + + myDerivation$.get(); + + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(hasDerived).toHaveBeenCalledTimes(1); // no update because someone is reacting, and there has been no update in value. // #ANSWER + + myAtom$.set(false); + + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(hasDerived).toHaveBeenCalledTimes(2); // `myDerivation$`s value has changed, so update. // #ANSWER + + myDerivation$.get(); + + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(hasDerived).toHaveBeenCalledTimes(2); // no update. // #ANSWER + + stopper(); + + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(hasDerived).toHaveBeenCalledTimes(2); // stopping doesn't change the value... // #ANSWER + + myDerivation$.get(); + + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(hasDerived).toHaveBeenCalledTimes(3); // ...but now, it is not being reacted to, so it goes back to updating every time `.get()` is called. // #ANSWER + + /** + * Since the `.react()` already listens to the value-changes, there is + * no need to recalculate whenever a `.get()` is called. + * + * But when the reactor has stopped, the derivation has to be calculated + * again. + */ + }); + + /** + * The basics of `Derivable` caching are seen above. + * But there is one more trick up it's sleeve. + */ + it('cached changes', () => { + const first = jest.fn(); + const second = jest.fn(); + + const myAtom$ = atom(1); + const first$ = myAtom$.derive(i => { + first(i); // Call the mock function, to let it know we were here + return i > 2; + }); + const second$ = first$.derive(second); + + // As always, they should not have fired yet + expect(first).not.toHaveBeenCalled(); + expect(second).not.toHaveBeenCalled(); + + second$.react(() => ''); + + // And as expected, they now should both have fired once + expect(first).toHaveBeenCalledOnce(); + expect(second).toHaveBeenCalledOnce(); + + /** + * ** Your Turn ** + * + * But what to expect now? + */ + + // Note that this is the same value as it was initialized with + myAtom$.set(1); + + expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(first).toHaveBeenCalledTimes(1); // `myAtom$` has the same value (`1`), so no need to be called // #ANSWER + expect(second).toHaveBeenCalledTimes(1); // `first$` has the same value (`false`), so no need to be called // #ANSWER + + myAtom$.set(2); + + expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(first).toHaveBeenCalledTimes(2); // `myAtom$` has a different value (`2`), so call again // #ANSWER + expect(second).toHaveBeenCalledTimes(1); // `first$` has the same value (`false`), so no need to be called // #ANSWER + + myAtom$.set(3); + + expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(first).toHaveBeenCalledTimes(3); // `myAtom$` has a different value (`3`), so call again // #ANSWER + expect(second).toHaveBeenCalledTimes(2); // `first$` has a different value (`true`), so call again // #ANSWER + + myAtom$.set(4); + + expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(first).toHaveBeenCalledTimes(4); // `myAtom$` has a different value (`4`), so call again // #ANSWER + expect(second).toHaveBeenCalledTimes(2); // `first$` has the same value (`true`), so no need to be called // #ANSWER + + /** + * Can you explain the behavior above? + * + * It is why we say that `@skunkteam/sherlock` deals with reactive state + * and not events (as RxJS does for example). + * + * Events can be very useful, but when data is involved, you are + * probably only interested in value changes. So these changes can and + * need to be cached and deduplicated. + */ + }); + + /** + * So if the new value of a `Derivable` is equal to the old, it won't + * propagate a new event. But what does it mean to be equal in a + * `Derivable`? + * + * Strict `===` equality would mean that `NaN` and `NaN` would not even be + * equal. `Object.is()` equality would be better, but would mean that + * structurally equal objects could be different. + */ + it('equality', () => { + const atom$ = atom({}); + const hasReacted = jest.fn(); + + atom$.react(hasReacted, { skipFirst: true }); + expect(hasReacted).toHaveBeenCalledTimes(0); // added for clarity, in case people missed the `skipFirst` or its implication + + atom$.set({}); + + /** + * ** Your Turn ** + * + * The `Atom` is set with exactly the same object as before. Will the + * `.react()` fire? + */ + expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(hasReacted).toHaveBeenCalledTimes(1); // `{} !== {}`, as they have different references // #ANSWER + + /** + * But what if you use an object, that can be easily compared through a + * library like `ImmutableJS`? + * + * Let's try an `Immutable.Seq` + */ + atom$.set(Seq.Indexed.of(1, 2, 3)); + // Let's reset the spy here, to start over + hasReacted.mockClear(); + expect(hasReacted).not.toHaveBeenCalled(); + + atom$.set(Seq.Indexed.of(1, 2, 3)); + /** + * ** Your Turn ** + * + * Do you think the `.react()` fired with this new value? + */ + expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(hasReacted).toHaveBeenCalledTimes(0); // #ANSWER + + atom$.set(Seq.Indexed.of(1, 2)); + + /** + * ** Your Turn ** + * + * And now? + */ + expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(hasReacted).toHaveBeenCalledTimes(1); // #ANSWER + + /** + * In `@skunkteam/sherlock` equality is a bit complex: + * + * First we check `Object.is()` equality, if that is true, it is the + * same, you can't deny that. + * + * After that it is pluggable. It can be anything you want. + * + * By default we try to use `.equals()`, to support libraries like + * `ImmutableJS`. + */ + }); +}); diff --git a/generator/5 - unresolved.test.ts b/generator/5 - unresolved.test.ts new file mode 100644 index 0000000..f676c5a --- /dev/null +++ b/generator/5 - unresolved.test.ts @@ -0,0 +1,192 @@ +import { atom, Derivable, DerivableAtom } from '@skunkteam/sherlock'; + +/** + * ** Your Turn ** + * + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +/** + * Sometimes your data isn't available yet. For example if it is still being + * fetched from the server. At that point you probably still want your + * `Derivable` to exist, to start deriving and reacting when the data becomes + * available. + * + * To support this, `Derivable`s in `@skunkteam/sherlock` support a separate + * state, called `unresolved`. This indicates that the data is not available + * yet, but (probably) will be at some point. + */ +describe('unresolved', () => { + /** + * Let's start by creating an `unresolved` `Derivable`. + */ + it('can be checked on the `Derivable`', () => { + // By using the `.unresolved()` method, you can create an `unresolved` + // atom. Note that you will need to indicate the type of this atom, + // since it can't be inferred by TypeScript this way. + const myAtom$ = atom.unresolved(); + + expect(myAtom$.resolved).toEqual(__YOUR_TURN__); // #QUESTION + expect(myAtom$.resolved).toEqual(false); // #ANSWER + + /** + * ** Your Turn ** + * + * Resolve the atom, it's pretty easy + */ + __YOUR_TURN__; // #QUESTION + myAtom$.set(1); // #ANSWER + + expect(myAtom$.resolved).toBeTrue(); + }); + + /** + * An `unresolved` `Derivable` is not able to provide a value yet. + * So `.get()` will throw if you try. + */ + it('cannot `.get()`', () => { + /** + * ** Your Turn ** + * + * Time to create an `unresolved` Atom.. + */ + const myAtom$: DerivableAtom = __YOUR_TURN__; // #QUESTION + const myAtom$: DerivableAtom = atom.unresolved(); // #ANSWER + + expect(myAtom$.resolved).toBeFalse(); + + // By this test passing, we see that `.get()` on an unresolved + // `Derivable` indeed throws an error. + expect(() => myAtom$.get()).toThrow('Could not get value, derivable is unresolved'); + + myAtom$.set('finally!'); + + /** + * ** Your Turn ** + * + * What do you expect? + */ + expect(myAtom$.resolved).toEqual(__YOUR_TURN__); // #QUESTION + expect(myAtom$.resolved).toEqual(true); // #ANSWER + + // .toThrow() or .not.toThrow()? ↴ + expect(() => myAtom$.get()) /*__YOUR_TURN__*/; // #QUESTION + expect(() => myAtom$.get()).not.toThrow(); // #ANSWER + }); + + /** + * If a `Derivable` is `unresolved` it can't react yet. But it will + * `.react()` if a value becomes available. + */ + it('reacting to `unresolved`', () => { + const myAtom$ = atom.unresolved(); + + const hasReacted = jest.fn(); + myAtom$.react(hasReacted); + + /** + * ** Your Turn ** + * + * What do you expect? + */ + expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(hasReacted).toHaveBeenCalledTimes(0); // #ANSWER + + /** + * ** Your Turn ** + * + * Now make the last expect succeed + */ + __YOUR_TURN__; // #QUESTION + myAtom$.set(`woohoow, I was called`); // #ANSWER + + expect(myAtom$.resolved).toBeTrue(); + expect(hasReacted).toHaveBeenCalledExactlyOnceWith(`woohoow, I was called`, expect.toBeFunction()); + }); + + /** + * In `@skunkteam/sherlock` there is no reason why a `Derivable` should not + * be able to become `unresolved` again after it has been set. + */ + it('can become `unresolved` again', () => { + const myAtom$ = atom.unresolved(); + + expect(myAtom$.resolved).toBeFalse(); + + /** + * ** Your Turn ** + * + * Set the value.. + */ + __YOUR_TURN__; // #QUESTION + myAtom$.set(`it's alive!`); // #ANSWER + + expect(myAtom$.get()).toEqual(`it's alive!`); + + /** + * ** Your Turn ** + * + * Unset the value.. (*Hint: TypeScript is your friend*) + */ + __YOUR_TURN__; // #QUESTION + myAtom$.unset(); // #ANSWER + + expect(myAtom$.resolved).toBeFalse(); + }); + + /** + * When a `Derivable` is dependent on another `unresolved` `Derivable`, this + * `Derivable` should also become `unresolved`. + * + * *Note that this will only become `unresolved` when there is an active + * dependency (see 'inner workings#dynamic dependencies')* + */ + it('will propagate', () => { + const myString$ = atom.unresolved(); + const myOtherString$ = atom.unresolved(); + + /** + * ** Your Turn ** + * + * Combine the two `Atom`s into one `Derivable` + */ + const myDerivable$: Derivable = __YOUR_TURN__; // #QUESTION + const myDerivable$: Derivable = myString$.derive(s => s + myOtherString$.get()); // #ANSWER + + /** + * ** Your Turn ** + * + * Is `myDerivable$` expected to be `resolved`? + */ + expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); // #QUESTION + expect(myDerivable$.resolved).toEqual(false); // #ANSWER + + // Now let's set one of the two source `Atom`s + myString$.set('some'); + + /** + * ** Your Turn ** + * + * What do you expect to see in `myDerivable$`. + */ + expect(myDerivable$.resolved).toEqual(false); + + // And what if we set `myOtherString$`? + myOtherString$.set('data'); + expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); // #QUESTION + expect(myDerivable$.get()).toEqual(__YOUR_TURN__); // #QUESTION + expect(myDerivable$.resolved).toEqual(true); // #ANSWER + expect(myDerivable$.get()).toEqual('somedata'); // #ANSWER + + /** + * ** Your Turn ** + * + * Now we will unset one of the `Atom`s. + * What do you expect `myDerivable$` to be? + */ + myString$.unset(); + expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); // #QUESTION + expect(myDerivable$.resolved).toEqual(false); // #ANSWER + }); +}); diff --git a/generator/6 - errors.test.ts b/generator/6 - errors.test.ts new file mode 100644 index 0000000..71a2720 --- /dev/null +++ b/generator/6 - errors.test.ts @@ -0,0 +1,275 @@ +import { atom, DerivableAtom, error, FinalWrapper, unresolved } from '@skunkteam/sherlock'; + +/** + * ** Your Turn ** + * + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +// Silence TypeScript's import not used errors. +expect(FinalWrapper).toBe(FinalWrapper); + +// In libs/sherlock/src/lib/interfaces.ts:289, the basic states a Derivable can have are shown. +// > `export type State = V | unresolved | ErrorWrapper;` +// A state can be either any type `V` (`number`, `string`, etc.), `unresolved` as we saw in the +// previous tutorial, or `ErrorWrapper`. This last state is explained here. +describe('errors', () => { + let myAtom$: DerivableAtom; + + beforeEach(() => { + myAtom$ = atom(1); + }); + + it('basic errors', () => { + // The `errored` property shows whether the last statement resulted in an error. + expect(myAtom$.errored).toBe(false); + expect(myAtom$.error).toBeUndefined; // by default, the `error` property is undefined. + expect(myAtom$.getState()).toBe(1); // as explained above, any type can be a state + + // We can set errors using the `setError()` function. + myAtom$.setError('my Error'); + + expect(myAtom$.errored).toBe(true); + expect(myAtom$.error).toBe('my Error'); + + // The `ErrorWrapper` state only holds an error string. The `error()` function returns + // such an `ErrorWrapper` which we can use to compare. + expect(myAtom$.getState()).toMatchObject(error('my Error')); + + // expect(myAtom$.get).toThrow("Cannot read properties of undefined (reading 'getState')"); + // TODO: WHAT - normally this works, but internal JEST just fucks with me....? + + // Calling `get()` on `myAtom$` gives the error. + expect(() => myAtom$.get()).toThrow('my Error'); + expect(myAtom$.errored).toBe(true); + + // ** __YOUR_TURN__ ** + // What will happen if you try to call `set()` on `myAtom$`? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myAtom$.set(2)) /* __YOUR_TURN__ */; // #QUESTION + expect(() => myAtom$.set(2)).not.toThrow(); // #ANSWER + expect(myAtom$.errored).toBe(__YOUR_TURN__); // #QUESTION + expect(myAtom$.errored).toBe(false); // #ANSWER + + // Interestingly, calling `set()` does not throw an error. In fact, it removes the error state + // altogether. This means we can call `get()` again. + expect(() => myAtom$.get()).not.toThrow(); + }); + + it('deriving an error', () => { + const myDerivable$ = myAtom$.derive(v => v + 1); + + // If `myAtom$` suddenly errors... + myAtom$.setError('division by zero'); + + // ...what happens to `myDerivable$`? + expect(myDerivable$.errored).toBe(__YOUR_TURN__); // #QUESTION + expect(myDerivable$.errored).toBe(true); // #ANSWER + + // If any Derivable tries to derive from an atom in an error state, + // this Derivable will itself throw an error too. This makes sense, + // given that it cannot obtain the value it needs anymore. + }); + + it('reacting to an error', () => { + // Without a reactor, setting an error to an Atom does not throw an error. + expect(() => myAtom$.setError('my Error')).not.toThrow(); + myAtom$.set(1); + + // Now we set a reactor to `myAtom$`. This reactor does not use the value of `myAtom$`. + const reactor = jest.fn(); + myAtom$.react(reactor); + + // ** __YOUR_TURN__ ** + // Will an error be thrown when `myAtom$` is now set to an error state? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myAtom$.setError('my Error')) /* __YOUR_TURN__ */; // #QUESTION + expect(() => myAtom$.setError('my Error')).toThrow('my Error'); // #ANSWER + + // Reacting to a Derivable that throws an error will make the reactor throw as well. + // Because the reactor will usually fire when it gets connected, it also throws when + // you try to connect it after the error has already been set. + + myAtom$ = atom(1); + myAtom$.setError('my second Error'); + + // ** __YOUR_TURN__ ** + // Will an error be thrown when you use `skipFirst`? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myAtom$.react(reactor, { skipFirst: true })) /* __YOUR_TURN__ */; // #QUESTION + expect(() => myAtom$.react(reactor, { skipFirst: true })).toThrow('my second Error'); // #ANSWER + + // And will an error be thrown when `from = false`? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myAtom$.react(reactor, { from: false })) /* __YOUR_TURN__ */; // #QUESTION + expect(() => myAtom$.react(reactor, { from: false })).not.toThrow(); // #ANSWER + + // When `from = false`, the reactor is disconnected, preventing the error message from entering. + // `skipFirst`, on the other hand, does allow the error in, but does not trigger an update. + }); + + /** + * Similarly to `constants` which we'll explain in tutorial 7, + * you might want to specify that a variable cannot be updated. + * This can be useful for the programmers themselves, to not + * accidentally update the variable, but it can also be useful for + * optimization. You can do this using the `final` concept. + */ + describe('TEMP `final`', () => { + let myAtom$ = atom(1); + + beforeEach(() => { + myAtom$ = atom(1); + }); + + it('`final` basics', () => { + // Every atom has a `final` property. + expect(myAtom$.final).toBeFalse(); + + // You can make an atom final using the `.makeFinal()` function. + myAtom$.makeFinal(); + expect(myAtom$.final).toBeTrue(); + + /** + * ** Your Turn ** + * What do you think will happen when we try to `.get()` or `.set()` this atom? + */ + // .toThrow() or .not.toThrow()? ↴ + expect(() => myAtom$.get()) /*__YOUR_TURN__*/; // #QUESTION + expect(() => myAtom$.set(2)) /*__YOUR_TURN__*/; // #QUESTION + expect(() => myAtom$.get()).not.toThrow(); // #ANSWER + expect(() => myAtom$.set(2)).toThrow('cannot set a final derivable'); // #ANSWER + + // This behavior is consistent with normal variables created using `const`. + // Alternatively, you can set a last value before setting it to `final`. + // .toThrow() or .not.toThrow()? ↴ + expect(() => myAtom$.setFinal(2)) /*__YOUR_TURN__*/; // #QUESTION + expect(() => myAtom$.setFinal(2)).toThrow('cannot set a final derivable'); // #ANSWER + + // There is no way to 'unfinalize' a Derivable, so the only solution to reset is to + // create a whole new Derivable. + myAtom$ = atom(1); + myAtom$.setFinal(2); + expect(myAtom$.final).toBeTrue(); + }); + + it('deriving a `final` Derivable', () => { + const myDerivable$ = myAtom$.derive(v => v + 1); + + const hasReacted = jest.fn(); + myDerivable$.react(hasReacted); + + expect(myDerivable$.final).toBeFalse(); + expect(myDerivable$.connected).toBeTrue(); + + myAtom$.makeFinal(); + + /** + * ** Your Turn ** + * + * What will happen to `myDerivable$` when I change `myAtom$` to be `final`? + */ + expect(myDerivable$.final).toBe(__YOUR_TURN__); // #QUESTION + expect(myDerivable$.final).toBe(true); // #ANSWER + expect(myDerivable$.connected).toBe(__YOUR_TURN__); // #QUESTION + expect(myDerivable$.connected).toBe(false); // #ANSWER + + /** + * Derivables that are final (or constant) are no longer tracked. This can save + * a lot of memory and time by cleaning up unused data. Also, when all the variables + * that a Derivable depends on become final, that Derivable itself also becomes final. + * Similarly to `unresolved` and `error`, this chains. + */ + }); + + it('`final` State', () => { + /** A property such as `.final`, similar to variables like `.errored` and `.resolved` + * is useful for checking whenever a Derivable is in a certain state, but these properties + * are just a boolean. This means that these properties cannot be derived and we cannot + * have certain functions execute whenever there is a change in the state. For this reason, + * every Derivable holds an internal state, retrievable using `.getState()` which can be + * derived. TODO: Have a clear place where I explain this! Now I have info up top here too. + * + * We have seen that states (`State`) can be `undefined`, `ErrorWrapper`, + * or any regular type `V`. Other states exist, such as the `MaybeFinalState`. This state can be either + * a normal state `State` or a special `FinalWrapper>` state. Let's see that in action. + */ + expect(myAtom$.getMaybeFinalState()).toBe(1); // `getMaybeFinalState` can return a normal state, which in turn can be any normal type. + + myAtom$.makeFinal(); + + expect(myAtom$.getMaybeFinalState()).toBeInstanceOf(FinalWrapper); // but `getMaybeFinalState` can also return a `FinalWrapper` type. + expect(myAtom$.getState()).toBe(1); // the normal type is still the final it contains. + + // TODO: MAAR JE KAN EEN STATE HELEMAAL NIET DERIVEN! + // Dus dat is allemaal onzin lijkt me....??? Bovendien, kan je normale variabelen niet deriven door het gewoon te + // wrappen in een atom ofzo? + }); + }); + + /** + * It is nice to be able to have a backup plan when an error occurs. + * The `.fallbackTo()` function allows you to specify a default value + * whenever your Derivable gets an error state. + */ + it('Fallback-to', () => { + const myAtom$ = atom(0); + + /** + * ** Your Turn ** + * Use the `.fallbackTo()` method to create a `mySafeAtom$` which + * gets the backup value `3` when `myAtom$` gets an error state. + */ + // const mySafeAtom$ = myAtom$.fallbackTo(__YOUR_TURN__); // #QUESTION + const mySafeAtom$ = myAtom$.fallbackTo(() => 3); // #ANSWER + + expect(myAtom$.getState()).toBe(0); + expect(myAtom$.value).toBe(0); + expect(mySafeAtom$.value).toBe(0); + + myAtom$.unset(); + + expect(myAtom$.getState()).toBe(unresolved); + expect(myAtom$.value).toBeUndefined(); + expect(mySafeAtom$.value).toBe(3); + }); + + it('TEMP Flat-map', () => { + // const myAtom$ = atom(0); + // const mapping = (v: any) => atom(v); + // Sometimes you use `map()`, but the result within the `map()` is also a Derivable. + // The result would here be a `Derivable>` (hover over `derive` to see this). + // You would have to use `.get()` to go back to a single Derivable. Similarly how `flatMap` can + // reduce lists of lists to a single list, it can help reduce Derivables of Derivables to a + // single Derivable. If you have something like this: + // let myAtom$$ = myAtom$.map(n => mapping(n)).derive(v => v.get()); + // You can now rewrite it to this: + // myAtom$$ = myAtom$.flatMap(n => mapping(n)); + // It only results in slightly shorter code. + // TODO: right? + }); +}); + +/** + * !! Final States; (finalGetter, finalMethod, getMaybeFinalState, FinalWrapper, MaybeFinalState, _isFinal, makeFinal, markFinal, .final, .finalized, setFinal...) + * ? Lens; (libs/sherlock/src/lib/derivable/lens.ts) - map die twee kanten op kan gaan. Maar een map kan dat al? Maar hier kan + * je dat los definieren! Je kan gewoon `lens` ipv `var.lens`. Zelden dat je dit gebruikt. Output is een Derivable though. + * x Lift; (libs/sherlock-utils/src/lib/lift.ts) + * !! Peek; (libs/sherlock-utils/src/lib/peek.ts) - dan track je niet. In een derivable, deze tracked hij dan niet (ipv .get() waar het wel getracked wordt) + * x Template; (libs/sherlock-utils/src/lib/template.ts) - to make a string using a template literal. (Uses unwrap!!) + * / Factory; (libs/sherlock/src/lib/derivable/factories.ts) - simply contains functions to create objects, namely + * lens; atom; constant; derive. + * !! Flat-map; (libs/sherlock/src/lib/derivable/mixins/flat-map.ts) - ??? + * array: nested arrays naar array + * Derivable: gooit er derive.get() achteraan? + * Derivable (input van inputveld). Flatmap geeft Derivable terug. Derivable.flatmap() returned misschien + * Derivable, returned dan de number. flatMap is een `derive`, maar wat hij returned haalt hij uit de Derivable. + * ofzoiets. Maakt code korter. + * !! Fallback-to; - op een derivable. Als een atom `unresolved` is, dan fallt het back to this value. Ofwel, initial value, maar + * ook als hij later unresolved wordt, dan wordt hij dit (vaak wel initial value). + * !! Take - react options gebruiken buiten react. In een derivable chain, halverwege die options gebruiken. + * e.g. (from)Promise. Zodra die een waarde aanneemt kan hij niet meer veranderen. + * Let FromPromise, FromObservable, FromEventPattern ook uit (in utils?), ToPromise, ToObservable, in praktijk ook handig. + * FromEventPattern (haily mary, als alles niet werkt, dan dit doen). + */ diff --git a/generator/7 - advanced.test.ts b/generator/7 - advanced.test.ts new file mode 100644 index 0000000..37c3114 --- /dev/null +++ b/generator/7 - advanced.test.ts @@ -0,0 +1,570 @@ +import { atom, constant, Derivable, derive, SettableDerivable, unresolved } from '@skunkteam/sherlock'; +import { lift, template } from '@skunkteam/sherlock-utils'; +import { Map as ImmutableMap } from 'immutable'; + +/** + * ** Your Turn ** + * + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +describe('advanced', () => { + /** + * In the case a `Derivable` is required, but the value is immutable. + * You can use a `constant()`. + * + * This will create a readonly `Derivable`. + */ + it('`constant`', () => { + /** + * We cast to `SettableDerivable` to trick TypeScript for this test. + * It can be valueable to know what a `constant()` is, though. + * So try and remove the `cast`, see what happens! + */ + const c = constant('value') as SettableDerivable; + + /** + * ** Your Turn ** + * + * What do you expect this `Derivable` to do on `.set()`, `.get()` etc? + */ + + // Remove this after taking your turn below. // #QUESTION + expect(false).toBe(true); // #QUESTION + // .toThrow() or .not.toThrow()? ↴ (2x) + expect(() => c.get()) /* __YOUR_TURN__ */; // #QUESTION + expect(() => c.set('new value')) /* __YOUR_TURN__ */; // #QUESTION + expect(() => c.get()).not.toThrow(); /* __YOUR_TURN__ */ // #ANSWER + expect(() => c.set('new value')).toThrow() /* __YOUR_TURN__ */; // #ANSWER + }); + + it('`templates`', () => { + // Staying in the theme of redefining normal Typescript code in our Derivable language, + // we also have a special syntax to copy template literals to a Derivable. + const one = 1; + const myDerivable = template`I want to go to ${one} party`; + expect(myDerivable.get()).toBe(__YOUR_TURN__) /* __YOUR_TURN__ */; // #QUESTION + expect(myDerivable.get()).toBe(`I want to go to 1 party`); // #ANSWER + }); + + /** + * Collections in `ImmutableJS` are immutable, so any modification to a + * collection will create a new one. This results in every change needing a + * `.get()` and a `.set()` on a `Derivable`. + * + * To make this pattern a little bit easier, the `.swap()` method can be + * used. The given function will get the current value of the `Derivable` + * and any return value will be set as the new value. + */ + it('`.swap()`', () => { + // This is a separate function because you might want to use this later. + function plusOne(num: number) { + return num + 1; + } + + const myCounter$ = atom(0); + /** + * ** Your Turn ** + * + * Rewrite the `.get()`/`.set()` combos below using `.swap()`. + */ + // Remove this after taking your turn below. // #QUESTION + expect(false).toBe(true); // #QUESTION + + myCounter$.set(plusOne(myCounter$.get())); // #QUESTION + myCounter$.swap(plusOne); // #ANSWER + expect(myCounter$.get()).toEqual(1); + + myCounter$.set(plusOne(myCounter$.get())); // #QUESTION + myCounter$.swap(plusOne); // #ANSWER + expect(myCounter$.get()).toEqual(2); + }); + + /** + * You might want to use the reactor options such as + * `when`, `until`, and `skipFirst` when deriving as well. + * In such cases, you could use `.take()`. + */ + it('`.take()`', () => { + const myAtom$ = atom('denied'); + + /** + * ** Your Turn ** + * Use the `.take()` method on `myAtom$` to only accept the input string + * when it is `allowed`. + */ + const myLimitedAtom$ = myAtom$.take(__YOUR_TURN__); // #QUESTION + const myLimitedAtom$ = myAtom$.take({ when: v => v.is('allowed') }); // #ANSWER + + expect(myLimitedAtom$.resolved).toBe(false); + myAtom$.set('allowed'); + expect(myLimitedAtom$.resolved).toBe(true); + expect(myLimitedAtom$.value).toBe('allowed'); + }); + + /** + * As an alternative to `.get()` and `.set()`, there is also the `.value` + * accessor. + */ + describe('`.value`', () => { + /** + * `.value` can be used as an alternative to `.get()` and `.set()`. + * This helps when a property is expected instead of two methods. + */ + it('as a getter/setter', () => { + const myAtom$ = atom('foo'); + + /** + * ** Your Turn ** + * + * Use the `.value` accessor to get the current value. + */ + expect(__YOUR_TURN__).toEqual('foo'); // #QUESTION + expect(myAtom$.value).toEqual('foo'); // #ANSWER + /** + * ** Your Turn ** + * + * Now use the `.value` accessor to set a 'new value'. + */ + myAtom$.value = __YOUR_TURN__; // #QUESTION + myAtom$.value = 'new value'; // #ANSWER + + expect(myAtom$.get()).toEqual('new value'); + }); + + /** + * If a `Derivable` is `unresolved`, `.get()` will normally throw. + * `.value` will return `undefined` instead. + */ + it('will not throw when `unresolved`', () => { + const myAtom$ = atom.unresolved(); + + /** + * ** Your Turn ** + */ + expect(myAtom$.value).toEqual(__YOUR_TURN__); // #QUESTION + expect(myAtom$.value).toEqual(undefined); // #ANSWER + }); + + /** + * As a result, if `.value` is used inside a derivation, it will also + * replace `unresolved` with `undefined`. So `unresolved` will not + * automatically propagate when using `.value`. + */ + it('will stop propagation of `unresolved` in `.derive()`', () => { + const myAtom$ = atom('foo'); + + const usingGet$ = derive(() => myAtom$.get()); + const usingVal$ = derive(() => myAtom$.value); + + expect(usingGet$.get()).toEqual('foo'); + expect(usingVal$.get()).toEqual('foo'); + + /** + * ** Your Turn ** + * + * We just created two `Derivable`s that are almost exactly the same. + * But what happens when their source becomes `unresolved`? + */ + // #QUESTION-BLOCK-START + expect(usingGet$.resolved).toEqual(__YOUR_TURN__); + expect(usingVal$.resolved).toEqual(__YOUR_TURN__); + myAtom$.unset(); + expect(usingGet$.resolved).toEqual(__YOUR_TURN__); + expect(usingVal$.resolved).toEqual(__YOUR_TURN__); + // #QUESTION-BLOCK-END + // #ANSWER-BLOCK-START + expect(usingGet$.resolved).toEqual(true); + expect(usingVal$.resolved).toEqual(true); + myAtom$.unset(); + expect(usingGet$.resolved).toEqual(false); + expect(usingVal$.resolved).toEqual(true); + // #ANSWER-BLOCK-END + }); + }); + + /** + * The `.map()` method is comparable to `.derive()`. + * But there are a couple of differences: + * - It only triggers when the source `Derivable` changes + * - It does not track any other `Derivable` used in the function + * - It can be made to be settable + */ + describe('`.map()`', () => { + const mapReactSpy = jest.fn(); + // Clear the spy before each test case. + beforeEach(() => mapReactSpy.mockClear()); + + it('triggers when the source changes', () => { + const myAtom$ = atom(1); + /** + * ** Your Turn ** + * + * Use the `.map()` method to create the expected output below + */ + const mappedAtom$: Derivable = __YOUR_TURN__; // #QUESTION + const mappedAtom$: Derivable = myAtom$.map(base => base.toString().repeat(base)); // #ANSWER + + mappedAtom$.react(mapReactSpy); + + expect(mapReactSpy).toHaveBeenCalledExactlyOnceWith('1', expect.toBeFunction()); + + myAtom$.set(3); + + expect(mapReactSpy).toHaveBeenCalledTimes(2); + expect(mapReactSpy).toHaveBeenLastCalledWith('333', expect.toBeFunction()); + }); + + it('does not trigger when any other `Derivable` changes', () => { + const myRepeat$ = atom(1); + const myString$ = atom('ho'); + const deriveReactSpy = jest.fn(); + + // Note that the `.map` uses both `myRepeat$` and `myString$` + myRepeat$.map(r => myString$.get().repeat(r)).react(mapReactSpy); + myRepeat$.derive(r => myString$.get().repeat(r)).react(deriveReactSpy); + + expect(mapReactSpy).toHaveBeenCalledExactlyOnceWith('ho', expect.toBeFunction()); + expect(deriveReactSpy).toHaveBeenCalledExactlyOnceWith('ho', expect.toBeFunction()); + + myRepeat$.value = 3; + /** + * ** Your Turn ** + * + * We changed`myRepeat$` to equal 3. + * Do you expect both reactors to have fired? And with what? + */ + // #QUESTION-BLOCK-START + expect(deriveReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(deriveReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + + expect(mapReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(mapReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + // #QUESTION-BLOCK-END + // #ANSWER-BLOCK-START + expect(deriveReactSpy).toHaveBeenCalledTimes(2); + expect(deriveReactSpy).toHaveBeenLastCalledWith('hohoho', expect.toBeFunction()); + + expect(mapReactSpy).toHaveBeenCalledTimes(2); + expect(mapReactSpy).toHaveBeenLastCalledWith('hohoho', expect.toBeFunction()); + // #ANSWER-BLOCK-END + + myString$.value = 'ha'; + /** + * ** Your Turn ** + * + * And now that we have changed `myString$`? And when `myRepeat$` changed again? + */ + // #QUESTION-BLOCK-START + expect(deriveReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(deriveReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + + expect(mapReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(mapReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + + myRepeat$.value = 2; + expect(deriveReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(deriveReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + + expect(mapReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(mapReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + // #QUESTION-BLOCK-END + + // #ANSWER-BLOCK-START + expect(deriveReactSpy).toHaveBeenCalledTimes(3); + expect(deriveReactSpy).toHaveBeenLastCalledWith('hahaha', expect.toBeFunction()); + + expect(mapReactSpy).toHaveBeenCalledTimes(2); + expect(mapReactSpy).toHaveBeenLastCalledWith('hohoho', expect.toBeFunction()); + + myRepeat$.value = 2; + expect(deriveReactSpy).toHaveBeenCalledTimes(4); + expect(deriveReactSpy).toHaveBeenLastCalledWith('haha', expect.toBeFunction()); + + expect(mapReactSpy).toHaveBeenCalledTimes(3); + expect(mapReactSpy).toHaveBeenLastCalledWith('haha', expect.toBeFunction()); + // #ANSWER-BLOCK-END + /** + * As you can see, a change in `myString$` will not trigger an + * update. But if an update is triggered, `myString$` will be called + * and the new value will be used. + */ + }); + + /** + * Since `.map()` is a relatively simple mapping of input value to + * output value. It can often be reversed. In that case you can use that + * reverse mapping to create a `SettableDerivable`. + */ + it('can be settable', () => { + const myAtom$ = atom(1); + + /** + * ** Your Turn ** + * + * Check the comments and `expect`s below to see what should be + * implemented exactly. + */ + const myInverse$ = myAtom$.map( + // This first function is called when getting... + n => -n, + // ...and this second function is called when setting. + __YOUR_TURN__, // #QUESTION + (newV, _) => -newV, // #ANSWER + ); + + // The original `atom` was set to 1, so we want the inverse to + // be equal -1. + expect(myInverse$.get()).toEqual(-1); + + // Now we set the inverse to -2 directly, so we expect the original + // `atom` to be equal to 2. + myInverse$.set(-2); + expect(myAtom$.get()).toEqual(2); + expect(myInverse$.get()).toEqual(-2); + }); + + it('similar to `map()` on arrays', () => { + // If the similarity is not clear yet, here is a comparison between + // the normal `.map()` on arrays and our `Derivable` `.map()`. + // Both get values out of a container (`Array` or `Derivable`), apply + // some function, and put it back in the container. + + const addOne = jest.fn((v: number) => v + 1); + + const myList = [1, 2, 3]; + const myMappedList = myList.map(addOne); + expect(myMappedList).toMatchObject([2, 3, 4]); + + const myAtom$ = atom(1); + let myMappedDerivable$ = myAtom$.map(addOne); + expect(myMappedDerivable$.value).toBe(2); + + // Or, as we have seen before, you can use `lift()` for this. + myMappedDerivable$ = lift(addOne)(myAtom$); + expect(myMappedDerivable$.value).toBe(2); + + // You can combine them too. + const myAtom2$ = atom([1, 2, 3]); + const myMappedDerivable2$ = myAtom2$.map(v => v.map(addOne)); + expect(myMappedDerivable2$.value).toMatchObject([2, 3, 4]); + }); + + /** + * In order to reason over the state of a Derivable, we can + * use `.mapState()`. This will map one state to another, and + * can be used to get rid of pesky `unresolved` or `Errorwrapper` + * states (or to introduce them!). + */ + it('`.mapState()`', () => { + const myAtom$ = atom(1); + + // like `.map()`, we can specify it both ways. + const myMappedAtom$ = myAtom$.mapState( + state => (state === unresolved ? 3 : state), // `myAtom$` => `myMappedAtom$` + state => (state === 2 ? unresolved : state), // `myMappedAtom$` => `myAtom$` + ); + + myAtom$.set(2); + expect(myAtom$.resolved).toBe(__YOUR_TURN__); // #QUESTION + expect(myAtom$.resolved).toBe(true); // #ANSWER + expect(myMappedAtom$.resolved).toBe(__YOUR_TURN__); // #QUESTION + expect(myMappedAtom$.resolved).toBe(true); // #ANSWER + + myAtom$.unset(); + expect(myAtom$.resolved).toBe(__YOUR_TURN__); // #QUESTION + expect(myAtom$.resolved).toBe(false); // #ANSWER + expect(myMappedAtom$.resolved).toBe(__YOUR_TURN__); // #QUESTION + expect(myMappedAtom$.resolved).toBe(true); // #ANSWER + + myMappedAtom$.set(2); + expect(myAtom$.resolved).toBe(__YOUR_TURN__); // #QUESTION + expect(myAtom$.resolved).toBe(false); // #ANSWER + expect(myMappedAtom$.resolved).toBe(__YOUR_TURN__); // #QUESTION + expect(myMappedAtom$.resolved).toBe(true); // #ANSWER + + // This is a tricky one: + myMappedAtom$.unset(); + expect(myAtom$.resolved).toBe(__YOUR_TURN__); // #QUESTION + expect(myAtom$.resolved).toBe(false); // #ANSWER + expect(myMappedAtom$.resolved).toBe(__YOUR_TURN__); // #QUESTION + expect(myMappedAtom$.resolved).toBe(true); // #ANSWER + + /** + * The results, especially of the last case, may seem weird. + * In the first exercise, `myAtom$` is set to 2, causing the state to be 2 as well. + * By setting the state of `myAtom$`, the first line of `mapState()` is triggered. + * Since `2` is not equal to `unresolved`, we return the state `2`, causing + * `myMappedAtom$` to also get state 2 (and thus: value 2). Neither are unresolved. + * + * In the second case, `myAtom$` is set to `unresolved`, triggering the first line of + * `mapState()`, letting `myMappedAtom$` become 3. `myAtom$` is now `unresolved`, and + * `myMappedAtom$` is not. + * + * In the third case, `myMappedAtom$` is set to 2, it triggers the second line of + * `mapState()`, causing `myAtom$` to become `unresolved`. However, what we don't + * notice is that this change in state triggers the first line of `mapState()` again, + * causing `myMappedAtom$` to get state `3`. We can check this: + */ + + myMappedAtom$.set(2); + expect(myMappedAtom$.get()).toBe(3); // the state and value are linked, so this is identical to `.getState()` + /** + * You might think that this change in state would cause `myAtom$` to now also get + * `3` as its state, but this does not happen. Why not? TODO: maximally one cycle? + * Since both `2` and `3` are not `unresolved`, it does not matter to our answer. + * + * The same cannot be said for the fourth case. Setting `myMappedAtom$` to `unresolved` + * triggers the second line of `mapState()`, causing `myAtom$` to also become `unresolved`. This, in turn, + * triggers the first line of `mapState()`, causing `myMappedAtom$` to become `3`. + * As such, `myMappedAtom$` is not `unresolved` even though we set it as such. + * TODO: change this to be for MAP. Then make MAPSTATE a trivial one right after. + */ + }); + }); + + /** + * `.pluck()` is a special case of the `.map()` method. + * If a collection of values, like an Object, Map, Array is the result of a + * `Derivable`, one of those values can be plucked into a new `Derivable`. + * This plucked `Derivable` can be settable, if the source supports it. + * + * The way properties are plucked is pluggable, but by default both // TODO: no-one here knows what "pluggable" is. Or ImmutableJS. + * `.get()` and `[]` are supported to support + * basic Objects, Maps and Arrays. + * + * *Note that normally when a value of a collection changes, the reference + * does not. This means that setting a plucked property of a regular + * Object/Array/Map will not cause any reaction on that source `Derivable`. + * + * ImmutableJS can help fix this problem* + */ + describe('`.pluck()`', () => { + const reactSpy = jest.fn(); + const reactPropSpy = jest.fn(); + let myMap$: SettableDerivable>; + let firstProp$: SettableDerivable; + + // Reset + beforeEach(() => { + reactPropSpy.mockClear(); + reactSpy.mockClear(); + myMap$ = atom>( + ImmutableMap({ + firstProp: 'firstValue', + secondProp: 'secondValue', + }), + ); + /** + * ** Your Turn ** + * + * `.pluck()` 'firstProp' from `myMap$`. + * + * * Hint: you'll have to cast the result from `.pluck()`. + */ + firstProp$ = __YOUR_TURN__; // #QUESTION + firstProp$ = myMap$.pluck('firstProp') as SettableDerivable; // #ANSWER + }); + + /** + * Once a property is plucked in a new `Derivable`. This `Derivable` can + * be used as a regular `Derivable`. + */ + it('can be used as a normal `Derivable`', () => { + firstProp$.react(reactPropSpy, { skipFirst: true }); + + /** + * ** Your Turn ** + * + * What do you expect the plucked `Derivable` to look like? And what + * happens when we `.set()` it? + */ + expect(firstProp$.get()).toEqual(__YOUR_TURN__); // #QUESTION + expect(firstProp$.get()).toEqual('firstValue'); // #ANSWER + + // the plucked `Derivable` should be settable + firstProp$.set('other value'); + // is the `Derivable` value the same as was set? + expect(firstProp$.get()).toEqual(__YOUR_TURN__); // #QUESTION + expect(firstProp$.get()).toEqual('other value'); // #ANSWER + + // How many times was the spy called? Note the `skipFirst`.. + expect(reactPropSpy).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(reactPropSpy).toHaveBeenCalledTimes(1); // #ANSWER + + // ...and what was the value? + expect(reactPropSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); // #QUESTION + expect(reactPropSpy).toHaveBeenLastCalledWith('other value', expect.toBeFunction()); // #ANSWER + }); + + /** + * If the source of the plucked `Derivable` changes, the plucked + * `Derivable` will change as well. As long as the change affects the + * plucked property of course. + */ + it('will react to changes in the source `Derivable`', () => { + firstProp$.react(reactPropSpy, { skipFirst: true }); + + /** + * ** Your Turn ** + * + * We will set `secondProp`, will this affect `firstProp$`? + * + * *Note: this `map` refers to `ImmutableMap`, not to the + * `Derivable.map()` we saw earlier in the tutorial.* + */ + myMap$.swap(map => map.set('secondProp', 'new value')); + + // How many times was the spy called? Note the `skipFirst`. + expect(reactPropSpy).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(reactPropSpy).toHaveBeenCalledTimes(0); // #ANSWER + + /** + * ** Your Turn ** + * + * And what if we set `firstProp`? + */ + myMap$.swap(map => map.set('firstProp', 'new value')); + + // How many times was the spy called? Note the `skipFirst`.. + expect(reactPropSpy).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(reactPropSpy).toHaveBeenCalledTimes(1); // #ANSWER + + // ...and what was the value? + expect(reactPropSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); // #QUESTION + expect(reactPropSpy).toHaveBeenLastCalledWith('new value', expect.toBeFunction()); // #ANSWER + }); + + /** + * Before, we saw how a change in the source of the plucked `Derivable` + * propagates to it. Now the question is: does this go the other way + * too? + * + * We saw that we can `.set()` the value of the plucked `Derivable`, so + * what happens to the source if we do that? + */ + it('will write through to the source `Derivable`', () => { + myMap$.react(reactSpy, { skipFirst: true }); + + /** + * ** Your Turn ** + * + * So what if we set `firstProp$`? Does this propagate to the source + * `Derivable`? + */ + // #QUESTION-BLOCK-START + firstProp$.set(__YOUR_TURN__); + expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(myMap$.get().get('firstProp')).toEqual(__YOUR_TURN__); + expect(myMap$.get().get('secondProp')).toEqual(__YOUR_TURN__); + // #QUESTION-BLOCK-END + // #ANSWER-BLOCK-START + firstProp$.set('new value'); + expect(reactSpy).toHaveBeenCalledTimes(1); + expect(myMap$.get().get('firstProp')).toEqual('new value'); + expect(myMap$.get().get('secondProp')).toEqual('secondValue'); + // #ANSWER-BLOCK-END + }); + }); +}); diff --git a/generator/8 - utils.test.ts b/generator/8 - utils.test.ts new file mode 100644 index 0000000..c1c1dd6 --- /dev/null +++ b/generator/8 - utils.test.ts @@ -0,0 +1,408 @@ +import { atom, derive } from '@skunkteam/sherlock'; +import { lift, pairwise, peek, scan, struct } from '@skunkteam/sherlock-utils'; + +/** + * ** Your Turn ** + * + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +// Silence TypeScript's import not used errors. +expect(pairwise).toBe(pairwise); +expect(scan).toBe(scan); +expect(struct).toBe(struct); +expect(peek).toBe(peek); +expect(lift).toBe(lift); + +/** + * In the `sherlock-utils` lib, there are a couple of functions that can combine + * multiple values of a single `Derivable` or combine multiple `Derivable`s into + * one. We will show a couple of those here. + */ +describe('utils', () => { + /** + * As the name suggests, `pairwise()` will call the given function with both + * the current and the previous state. + * + * *Note: functions like `pairwise` and `scan` can be used with any callback, + * so it can be used both in a `.derive()` step and in a `.react()`* + */ + it('pairwise', () => { + const myCounter$ = atom(1); + const reactSpy = jest.fn(); + + /** + * ** Your Turn ** + * + * Now, use `pairwise()` to subtract the previous value from the + * current. + * + * *Hint: check the overloads of pairwise if you're struggling with + * `oldValue`.* + */ + myCounter$.derive(__YOUR_TURN__).react(reactSpy); // #QUESTION + myCounter$.derive(pairwise((newVal, oldVal) => newVal - oldVal, 0)).react(reactSpy); // #ANSWER + // myCounter$.derive(pairwise((newVal, oldVal) => (oldVal ? newVal - oldVal : newVal))).react(reactSpy); // OR: alternatively. // #ANSWER + + expect(reactSpy).toHaveBeenCalledTimes(1); + expect(reactSpy).toHaveBeenLastCalledWith(1, expect.toBeFunction()); + + myCounter$.set(3); + + expect(reactSpy).toHaveBeenCalledTimes(2); + expect(reactSpy).toHaveBeenLastCalledWith(2, expect.toBeFunction()); // 3 (current value of `myCounter$`) - 1 (previous value of `myCounter$`) + + myCounter$.set(10); + + expect(reactSpy).toHaveBeenCalledTimes(3); + expect(reactSpy).toHaveBeenLastCalledWith(7, expect.toBeFunction()); // 10 (current value of `myCounter$`) - 3 (previous value of `myCounter$`) + }); + + /** + * `scan()` is the `Derivable` version of `Array.prototype.reduce()`. It will be + * called with the current state and the last emitted value. + * + * (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce) + * + * *Note: as with `pairwise()` this is useable in both a `.derive()` and + * `.react()` method* + */ + it('scan', () => { + const myCounter$ = atom(1); + const reactSpy = jest.fn(); + + /** + * ** Your Turn ** + * + * Now, use `scan()` to subtract the previous value from the + * current. + * + * Note that `scan()` must return the same type as it gets as input. This is required + * as this returned value is also used for the accumulator (`acc`) value for the next call. + * This `acc` parameter of `scan()` is the last returned value, not the last value + * of `myCounter$`, as is the case with `pairwise()`. + */ + myCounter$.derive(__YOUR_TURN__).react(reactSpy); // #QUESTION + myCounter$.derive(scan((acc, val) => val - acc, 0)).react(reactSpy); // #ANSWER + + expect(reactSpy).toHaveBeenCalledTimes(1); + expect(reactSpy).toHaveBeenLastCalledWith(1, expect.toBeFunction()); + + myCounter$.set(3); + + expect(reactSpy).toHaveBeenCalledTimes(2); + expect(reactSpy).toHaveBeenLastCalledWith(2, expect.toBeFunction()); // 3 (current value of `myCounter$`) - 1 (previous returned value) + + myCounter$.set(10); + + expect(reactSpy).toHaveBeenCalledTimes(3); + expect(reactSpy).toHaveBeenLastCalledWith(8, expect.toBeFunction()); // 10 (current value of `myCounter$`) - 2 (previous returned value) + }); + + it('`pairwise()` on normal arrays', () => { + // Functions like `pairwise()` and `scan()` work on normal lists too. They are often + // used in combination with `map()` and `filter()`. + const myList = [1, 2, 3, 5, 10]; + let myList2: number[]; + + /** + * ** Your Turn ** + * + * Use a `pairwise()` combined with a `.map()` on `myList` + * to subtract the previous value from the current. + * + * Hint: do not use a lambda function! + */ + myList2 = myList.map(__YOUR_TURN__); // #QUESTION + myList2 = myList.map(pairwise((newV, oldV) => newV - oldV, 0)); // #ANSWER + expect(myList2).toMatchObject([1, 1, 1, 2, 5]); + + // However, we should be careful with this, as this does not always behave as intended. + myList2 = myList.map(v => __YOUR_TURN__(v)); // copy the same implementation here // #QUESTION + myList2 = myList.map(v => pairwise((newV, oldV) => newV - oldV, 0)(v)); // copy the same implementation here // #ANSWER + expect(myList2).toMatchObject([1, 2, 3, 5, 10]); + + // Even if we are more clear about what we pass, this unintended behavior does not go away. + myList2 = myList.map((v, _, _2) => __YOUR_TURN__(v)); // copy the same implementation here // #QUESTION + myList2 = myList.map((v, _, _2) => pairwise((newV, oldV) => newV - oldV, 0)(v)); // copy the same implementation here // #ANSWER + expect(myList2).toMatchObject([1, 2, 3, 5, 10]); + + // `pairwise()` keeps track of the previous value under the hood. Using a lambda of + // the form `v => pairwise(...)(v)` would create a new `pairwise` function every call, + // essentially resetting the previous value every call. And resetting the previous value + // to 0 causes the input to stay the same (after all: x - 0 = x). + // Other than by not using a lambda function, we can fix this by + // saving the `pairwise` in a variable and reusing it for every call. + + let f: (v: number) => number = __YOUR_TURN__; // copy the same implementation here // #QUESTION + let f = pairwise((newV, oldV) => newV - oldV, 0); // #ANSWER + myList2 = myList.map(v => f(v)); + expect(myList2).toMatchObject([1, 1, 1, 2, 5]); + + // To get more insight in the `pairwise()` function, you can call it + // manually. Here, we show what happens under the hood. + + f = pairwise(__YOUR_TURN__); // copy the same implementation here // #QUESTION + f = pairwise((newV, oldV) => newV - oldV, 0); // #ANSWER + + myList2 = []; + myList2[0] = f(myList[0]); // f is newly created with `init = 0`, so applies `1 - 0`. + myList2[1] = f(myList[1]); // f has saved `1` internally, so applies `2 - 1`. + myList2[2] = f(myList[2]); // f has saved `2` internally, so applies `3 - 2`. + myList2[3] = f(myList[3]); // f has saved `3` internally, so applies `5 - 3`. + myList2[4] = f(myList[4]); // f has saved `5` internally, so applies `10 - 5`. + + expect(myList2).toMatchObject([1, 1, 1, 2, 5]); + // This also works for functions other than `.map()`, such as `.filter()`. + + /** ** Your Turn ** + * Use `pairwise()` to filter out all values which produce `1` when subtracted + * with their previous value. + */ + myList2 = myList.filter(__YOUR_TURN__); // #QUESTION + myList2 = myList.filter(pairwise((newV, oldV) => newV - oldV === 1, 0)); // #ANSWER + expect(myList2).toMatchObject([1, 2, 3]); + }); + + it('`scan()` on normal arrays', () => { + // As with `pairwise()` in the last test, `scan()` can be used with arrays too. + const myList = [1, 2, 3, 5, 10]; + let myList2: number[]; + + /** + * ** Your Turn ** + * + * Use a `scan()` combined with a `map` on `myList` + * to subtract the previous value from the current. + */ + + let f: (v: number) => number = scan(__YOUR_TURN__); // #QUESTION + let f: (v: number) => number = scan((acc, val) => val - acc, 0); // #ANSWER + myList2 = myList.map(f); + + expect(myList2).toMatchObject([1, 1, 2, 3, 7]); + + // again, it is useful to consider what happens internally. + f(7); // resets the internal `acc` value to 0, as the current `acc` value was 7, and 7-7 = 0. + + myList2 = []; + myList2[0] = f(myList[0]); // 1 :: `f` is newly created with `init = 0`, so applies `1 - 0 = 1`. + myList2[1] = f(myList[1]); // 1 :: `f` has saved the result `1` internally, so applies `2 - 1 = 1`. + myList2[2] = f(myList[2]); // 2 :: `f` has saved the result `1` internally, so applies `3 - 1 = 2`. + myList2[3] = f(myList[3]); // 3 :: `f` has saved the result `2` internally, so applies `5 - 2 = 3`. + myList2[4] = f(myList[4]); // 7 :: `f` has saved the result `3` internally, so applies `10 - 3 = 7`. + + expect(myList2).toMatchObject([1, 1, 2, 3, 7]); + + // This also works for functions other than `map()`, such as `filter()`. + // Use `scan()` to filter out all values from `myList` which produce a value + // of 8 or higher when added with the previous result. In other words, it should + // go through `myList` and add the values producing: (1), (1+2), (1+2+3), (1+2+3+5), + // (1+2+3+5+10), and since this sum only prouces a value higher than 8 when the + // values `5` and `10` are added, the result should be `[5,10]`. + + f = scan(__YOUR_TURN__); // #QUESTION + myList2 = myList.filter(__YOUR_TURN__); // #QUESTION + f = scan((acc, val) => val + acc, 0); // #ANSWER + myList2 = myList.filter(v => f(v) >= 8); // #ANSWER + expect(myList2).toMatchObject([5, 10]); + }); + + it('pairwise - BONUS', () => { + const myCounter$ = atom(1); + let reactSpy = jest.fn(); + + /** + * ** Your Turn ** + * ** BONUS ** + * + * Now, use `pairwise()` directly in `.react()`. Implement the same + * derivation as before: subtract the previous value from the current. + */ + reactSpy = jest.fn(__YOUR_TURN__); // #QUESTION + reactSpy = jest.fn(pairwise((newV, oldV) => newV - oldV, 0)); // #ANSWER + myCounter$.react(reactSpy); + + expect(reactSpy).toHaveLastReturnedWith(1); + + myCounter$.set(3); + + expect(reactSpy).toHaveLastReturnedWith(2); + + myCounter$.set(10); + + expect(reactSpy).toHaveLastReturnedWith(7); + }); + + it('scan - BONUS', () => { + const myCounter$ = atom(1); + let reactSpy = jest.fn(); + + /** + * ** Your Turn ** + * ** BONUS ** + * + * Now, use `scan()` directly in `.react()`. Implement the same + * derivation as before: subtract all the emitted values. + */ + + reactSpy = jest.fn(__YOUR_TURN__); // #QUESTION + reactSpy = jest.fn(scan((acc, val) => val - acc, 0)); // #ANSWER + myCounter$.react(reactSpy); + + expect(reactSpy).toHaveLastReturnedWith(1); + + myCounter$.set(3); + + expect(reactSpy).toHaveLastReturnedWith(2); + + myCounter$.set(10); + + expect(reactSpy).toHaveLastReturnedWith(8); + }); + + /** + * A `struct()` can combine an Object/Array of `Derivable`s into one + * `Derivable`, that contains the values of that `Derivable`. + * + * The Object/Array that is in the output of `struct()` will have the same + * structure as the original Object/Array. + * + * This is best explained in practice. + */ + it('struct', () => { + const allMyAtoms = { + regularProp: 'prop', + string: atom('my string'), + number: atom(1), + sub: { + string: atom('my substring'), + }, + }; + + const myOneAtom$ = struct(allMyAtoms); + + expect(myOneAtom$.get()).toEqual({ + regularProp: 'prop', + string: 'my string', + number: 1, + sub: { + string: 'my substring', + }, + }); + + // Note: we change the original object, not the struct. + allMyAtoms.regularProp = 'new value'; + allMyAtoms.sub.string.set('my new substring'); + + /** + * ** Your Turn ** + * + * Now have a look at the properties of `myOneAtom$`. Is this what you + * expect? + */ + // #QUESTION-BLOCK-START + expect(myOneAtom$.get()).toEqual({ + regularProp: __YOUR_TURN__, + string: __YOUR_TURN__, + number: __YOUR_TURN__, + sub: { + string: __YOUR_TURN__, + }, + }); + // #QUESTION-BLOCK-END + // #ANSWER-BLOCK-START + expect(myOneAtom$.get()).toEqual({ + regularProp: 'new value', + string: 'my string', + number: 1, + sub: { + string: 'my new substring', + }, + }); + // #ANSWER-BLOCK-END + }); + + describe('lift()', () => { + /** + * Derivables can feel like a language build on top of Typescript. Sometimes + * you might want to use normal objects and functions and not have to rewrite + * your code. + * In other words, just like keywords like `atom(V)` lifts a variable V to the higher + * level of Derivables, the `lift(F)` keyword lifts a function `F` to the higher + * level of Derivables. + */ + it('example', () => { + // Example: after years of effort, Bob finally finished his oh-so complicated function: + const isEvenNumber = (v: number) => v % 2 == 0; + + // Rewriting this function to work with derivables would now be a waste of time. + /** + * ** Your Turn ** + * Use the `lift()` function to change `isEvenNumber` to work on Derivables instead. + * In other words: the new function should take a `Derivable` (or more specifically: + * an `Unwrappable`) and return a `Derivable`. + */ + const isEvenDerivable = __YOUR_TURN__; // #QUESTION + const isEvenDerivable = lift(isEvenNumber); // #ANSWER + + expect(isEvenNumber(2)).toBe(true); + expect(isEvenNumber(13)).toBe(false); + expect(isEvenDerivable(atom(2)).get()).toBe(true); + expect(isEvenDerivable(atom(13)).get()).toBe(false); + }); + + it('`lift()` as alternative to `.map()`', () => { + // In tutorial 7, we saw `.map()` used in the following context: + const addOne = jest.fn((v: number) => v + 1); + const myAtom$ = atom(1); + + let myMappedDerivable$ = myAtom$.map(addOne); + + expect(myMappedDerivable$.value).toBe(2); + + /** + * ** Your Turn ** + * Now, use `lift()` as alternative to `.map()`. + */ + myMappedDerivable$ = __YOUR_TURN__; // #QUESTION + myMappedDerivable$ = lift(addOne)(myAtom$); // #ANSWER + + expect(myMappedDerivable$.value).toBe(2); + }); + }); + + /** + * Sometimes you want to use `derive` but still want to keep certain + * variables in it untracked. In such cases, you can use `peek()`. + */ + it('`peek()`', () => { + const myTrackedAtom$ = atom(1); + const myUntrackedAtom$ = atom(2); + + /** + * ** Your Turn ** + * Use `peek()` to get the value of `myUntrackedAtom$` and add it to the + * value of `myTrackedAtom$`, which should be tracked. + */ + const reactor = jest.fn(v => v); + derive(__YOUR_TURN__).react(reactor); // #QUESTION + derive(() => myTrackedAtom$.get() + peek(myUntrackedAtom$)).react(reactor); // #ANSWER + + expect(reactor).toHaveBeenCalledOnce(); + expect(reactor).toHaveLastReturnedWith(3); + + myTrackedAtom$.set(2); + expect(reactor).toHaveBeenCalledTimes(2); + expect(reactor).toHaveLastReturnedWith(4); + + myUntrackedAtom$.set(3); + expect(reactor).toHaveBeenCalledTimes(2); + expect(reactor).toHaveLastReturnedWith(4); + + myTrackedAtom$.set(3); + expect(reactor).toHaveBeenCalledTimes(3); + expect(reactor).toHaveLastReturnedWith(6); + }); +}); diff --git a/generator/9 - expert.test.ts b/generator/9 - expert.test.ts new file mode 100644 index 0000000..28aa4a9 --- /dev/null +++ b/generator/9 - expert.test.ts @@ -0,0 +1,473 @@ +import { DerivableAtom, atom, derive } from '@skunkteam/sherlock'; +import { derivableCache } from '@skunkteam/sherlock-utils'; + +/** + * ** Your Turn ** + * + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +describe('expert', () => { + describe('`.autoCache()`', () => { + /** + * If a `.get()` is called on a `Derivable` all derivations will be + * executed. But what if a `Derivable` is used multiple times in another + * `Derivable`? + */ + it('multiple executions', () => { + const hasDerived = jest.fn(); + + const myAtom$ = atom(true); + const myFirstDerivation$ = myAtom$.derive(hasDerived); + const mySecondDerivation$ = myFirstDerivation$.derive( + () => myFirstDerivation$.get() + myFirstDerivation$.get(), + ); + + /** + * ** Your Turn ** + * + * `hasDerived` is used in the first derivation. But has it been + * called at this point? + */ + + // `.toHaveBeenCalled()` or `.not.toHaveBeenCalled()`? ↴ + expect(hasDerived) /* Your Turn */; // #QUESTION + expect(hasDerived).not.toHaveBeenCalled(); // #ANSWER + + mySecondDerivation$.get(); + + /** + * ** Your Turn ** + * + * Now that we have gotten `mySecondDerivation$`, which calls + * `.get()` on the first multiple times. How many times has the + * first `Derivable` actually executed its derivation? + */ + // how many times? + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(hasDerived).toHaveBeenCalledTimes(3); // #ANSWER + }); + + /** + * So when a `Derivable` is reacting the value is cached and can be + * gotten from cache. But if this `Derivable` is used multiple times in + * a row, even in another derivation it isn't cached. + * + * To fix this issue, `.autoCache()` exists. It will cache the + * `Derivable`s value until the next Event Loop `tick`. + * + * So let's try the example above with this feature + */ + it('autoCaching', async () => { + const firstHasDerived = jest.fn(); + const secondHasDerived = jest.fn(); + + /** + * ** Your Turn ** + * + * Use `.autoCache()` on one of the `Derivable`s below. To make the + * expectations pass. + */ + const myAtom$ = atom(true); + const myFirstDerivation$ = myAtom$.derive(firstHasDerived); // #QUESTION + const myFirstDerivation$ = myAtom$.derive(firstHasDerived).autoCache(); // #ANSWER + const mySecondDerivation$ = myFirstDerivation$.derive(() => + secondHasDerived(myFirstDerivation$.get() + myFirstDerivation$.get()), + ); + + // first before .get() + expect(firstHasDerived).not.toHaveBeenCalled(); + + // second before .get() + expect(secondHasDerived).not.toHaveBeenCalled(); + + mySecondDerivation$.get(); + + // first after first .get() + expect(firstHasDerived).toHaveBeenCalledOnce(); + // second after first .get() + expect(secondHasDerived).toHaveBeenCalledOnce(); + + mySecondDerivation$.get(); + + // first after second .get() + expect(firstHasDerived).toHaveBeenCalledOnce(); + // second after second .get() + expect(secondHasDerived).toHaveBeenCalledTimes(2); + + /** + * Notice that the first `Derivable` has only been executed once, + * even though the second `Derivable` executed twice. + * + * Now we wait a tick for the cache to be invalidated. + */ + + await new Promise(r => setTimeout(r, 1)); + + /** + * ** Your Turn ** + * + * Now what do you expect? + */ + mySecondDerivation$.get(); + + // first after last .get() + expect(firstHasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(firstHasDerived).toHaveBeenCalledTimes(2); // #ANSWER + // second after last .get() + expect(secondHasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(secondHasDerived).toHaveBeenCalledTimes(3); // #ANSWER + }); + }); + + /** + * Some `Derivable`s need an input to be calculated. If this `Derivable` is + * async or has a big setup process, you may still want to create it only + * once, even if the `Derivable` is requested more than once for the same + * resource. + * + * Let's imagine a `stockPrice$(stock: string)` function, which returns a + * `Derivable` with the current price for the given stock. This `Derivable` + * is async, since it will try to retrieve the current price on a distant + * server. + * + * Let's see what can go wrong first, and we will try to fix it after that. + * + * *Note that a `Derivable` without an input is (hopefully) created only + * once, so it does not have this problem* + */ + describe('`derivableCache`', () => { + type Stocks = 'GOOGL' | 'MSFT' | 'APPL'; + + let stockPrice$: jest.Mock, [Stocks], any>; + const reactSpy = jest.fn(); + + beforeEach(() => { + // By default the stock price retriever returns unresolved. + stockPrice$ = jest.fn(_ => atom.unresolved()); + reactSpy.mockClear(); + }); + + function reactor(v: any) { + reactSpy(v); + } + + /** + * If the function to create the `Derivable` is called multiple times, + * the `Derivable` will be created multiple times. Any setup this + * `Derivable` does, will be executed every time. + */ + it('multiple setups', () => { + // To not make things difficult with `unresolved` for this example, + // imagine we get a response synchronously + stockPrice$ = jest.fn(_ => atom(1079.11)); + + const html$ = derive( + () => ` +

Alphabet Price ($${stockPrice$('GOOGL').get().toFixed(2)})

+

Some important text that uses the current price ($${stockPrice$('GOOGL') + .get() + .toFixed()}) as well

+ `, + ); + html$.react(reactor); + + expect(html$.connected).toEqual(true); + expect(reactSpy).toHaveBeenCalledOnce(); + + /** + * ** Your Turn ** + * + * The `Derivable` is connected and has emitted once, but in that + * value the 'GOOGL' stockprice was displayed twice. We know that + * using a `Derivable` twice in a connected `Derivable` will make + * the second `.get()` use a cached value. + * + * But does that apply here? + * How many times has the setup run, for the price `Derivable`. + */ + expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(stockPrice$).toHaveBeenCalledTimes(2); // #ANSWER + + /** Can you explain this behavior? */ + // ANSWER-BLOCK-START + // Yes: it creates a different Derivable every time, so it cannot use any caching. + // This is a similar issue to the `pairwise()` issue from tutorial 7, where, when we + // used lambda functions, we made a new pairwise object every time. + // ANSWER-BLOCK-END + }); + + /** + * An other problem can arise when the setup is done inside a derivation + */ + describe('setup inside a derivation', () => { + /** + * When the setup of a `Derivable` is done inside the same + * derivation as where `.get()` is called. You may be creating some + * problems. + */ + it('unresolveable values', () => { + // First setup an `Atom` with the company we are currently + // interested in + const company$ = atom('GOOGL'); + + // Based on that `Atom` we derive the stockPrice + const price$ = company$.derive(company => stockPrice$(company).get()); + + price$.react(reactor); + + // Because the stockPrice is still `unresolved` the reactor + // should not have emitted anything yet + expect(reactSpy).not.toHaveBeenCalled(); + + // Now let's increase the price + // First we have to get the atom that was given by the + // `stockPrice$` stub + const googlPrice$ = stockPrice$.mock.results[0].value as DerivableAtom; + + // Check if it is the right `Derivable` + expect(googlPrice$.connected).toEqual(true); + expect(googlPrice$.value).toEqual(undefined); + + // Then we set the price + googlPrice$.set(1079.11); + + /** + * ** Your Turn ** + * + * So the value was increased. What do you think happened? + */ + + // How often was the reactor on price$ called? + expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(reactSpy).toHaveBeenCalledTimes(0); // #ANSWER + + // And how many times did the setup run? + expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(stockPrice$).toHaveBeenCalledTimes(2); // #ANSWER + + // What's the value of price$ now? + expect(price$.value).toEqual(__YOUR_TURN__); // #QUESTION + expect(price$.value).toEqual(undefined); // #ANSWER + + // And the value of googlPrice$? + expect(googlPrice$.value).toEqual(__YOUR_TURN__); // #QUESTION + expect(googlPrice$.value).toEqual(1079.11); // #ANSWER + + // Is googlPrice$ still even driving any reactors? + expect(googlPrice$.connected).toEqual(__YOUR_TURN__); // #QUESTION + expect(googlPrice$.connected).toEqual(false); // #ANSWER + + /** + * Can you explain this behavior? + * + * Thought about it? Here is what happened: + * - Initially `stockPrice$('GOOGL')` emits a `Derivable` + * (`googlPrice$`), which is unresolved. + * - Inside the `.derive()` we subscribe to updates on that + * `Derivable`. + * - When `googlPrice$` emits a new value, the `.derive()` step + * is run again. + * - Inside this step, the setup is run again and a new + * `Derivable` (`newGooglPrice$`) is created and subscribed + * to. + * - Unsubscribing from the old `googlPrice$`. + * + * This `newGooglPrice$` is newly created and `unresolved` + * again. So the end result is an `unresolved` `price$` + * `Derivable`. + */ + + /** + * ** BONUS ** + * + * The problem above can be fixed without a `derivableCache`. + * If we split the `.derive()` step into two steps, where the + * first does the setup, and the second unwraps the `Derivable` + * created in the first. This way, a newly emitted value from + * the created `Derivable` will not run the setup again and + * everything should work as expected. + * + * ** Your Turn ** TODO: not in the SOLUTIONS!! + * + * *Hint: there is even an `unwrap` helper function for just + * such an occasion, try it!* + */ + }); + + /** + * But even when you split the setup and the `unwrap`, you may not + * be out of the woods yet! This is actually a problem that most + * libraries have a problem with, if not properly accounted for. + */ + it('uncached Derivables', () => { + // First we setup an `Atom` with the company we are currently + // interested in. This time we support multiple companies though + const companies$ = atom(['GOOGL']); + + // Based on that `Atom` we derive the stockPrices + const prices$ = companies$ + /** + * There is no need derive anything here, so we use `.map()` + * on `companies$`. And since `companies` is an array of + * strings, we `.map()` over that array to create an array + * of `Derivable`s. + */ + .map(companies => companies.map(company => stockPrice$(company))) + // Then we get the prices from the created `Derivable`s in a separate step + .derive(price$s => price$s.map(price$ => price$.value)); + + prices$.react(reactor); + + // Because we use `.value` instead of `.get()` the reactor + // should emit immediately this time. + + // But it should emit `undefined`. + expect(reactSpy).toHaveBeenCalledExactlyOnceWith([undefined]); + + // Now let's increase the price. First we have to get the atom + // that was given by the `stockPrice$` stub: + const googlPrice$ = stockPrice$.mock.results[0].value as DerivableAtom; + // Check if it is the right `Derivable` + expect(googlPrice$.connected).toBe(true); + + // Then we set the price, as before + googlPrice$.set(1079.11); + + /** + * ** Your Turn ** + * + * So the value was increased. What do you think happened now? + */ + expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(reactSpy).toHaveBeenLastCalledWith([__YOUR_TURN__]); // #QUESTION + expect(reactSpy).toHaveBeenCalledTimes(2); // #ANSWER + expect(reactSpy).toHaveBeenLastCalledWith([1079.11]); // #ANSWER + + /** + * So that worked, now let's try and add another company to the + * list. + */ + companies$.swap(current => [...current, 'APPL']); + + expect(companies$.get()).toEqual(['GOOGL', 'APPL']); + + /** + * ** Your Turn ** + * + * With both 'GOOGL' and 'APPL' in the list, what do we expect + * as an output? + * + * We had a price for 'GOOGL', but not for 'APPL'... + */ + expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(reactSpy).toHaveBeenCalledWith([__YOUR_TURN__, __YOUR_TURN__]); // #QUESTION + expect(reactSpy).toHaveBeenCalledTimes(3); // #ANSWER + expect(reactSpy).toHaveBeenCalledWith([undefined, undefined]); // #ANSWER + }); + }); + + /** + * So we know a couple of problems that can arise, but how do we fix + * them. + */ + describe('a solution', () => { + /** + * Let's try putting `stockPrice$` inside a `derivableCache`. + * `derivableCache` requires a `derivableFactory`, this specifies + * the setup for a given key. + * + * We know the key, and what to do with it, so let's try it! + */ + const priceCache$ = derivableCache((company: Stocks) => stockPrice$(company)); + /** + * *Note that from this point forward we use `priceCache$` where we + * used to use `stockPrice$` directly* + */ + + it('should fix everything :-)', () => { + // First setup an `Atom` with the company we are currently + // interested in + const companies$ = atom(['GOOGL']); + + const html$ = companies$.derive(companies => + companies.map( + company => ` +

Alphabet Price ($ ${priceCache$(company).value || 'unknown'})

+

Some important text that uses the current price ($ ${ + priceCache$(company).value || 'unknown' + }) as well

`, + ), + ); + + html$.react(reactor); + + expect(html$.connected).toEqual(true); + expect(reactSpy).toHaveBeenCalledOnce(); + // Convenience function to return the first argument of the last + // call to the reactor + function lastEmittedHTMLs() { + return reactSpy.mock.lastCall[0]; + } + + // The last call, should have the array of HTML's as first + // argument + expect(lastEmittedHTMLs()[0]).toContain('$ unknown'); + + /** + * ** Your Turn ** + * + * The `Derivable` is connected and has emitted once. + * The price for the given company 'GOOGL' is displayed twice, + * just as in the first test. + * + * Has anything changed, by using the `derivableCache`? + */ + expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(stockPrice$).toHaveBeenCalledTimes(1); // #ANSWER + + // Now let's resolve the price + stockPrice$.mock.results[0].value.set(1079.11); + + /** + * ** Your Turn ** + * + * Last time this caused the setup to run again, resolving to + * `unresolved` yet again. + * + * What happens this time? Has the setup run again? + */ + expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(stockPrice$).toHaveBeenCalledTimes(1); // #ANSWER + // Ok, but did it update the HTML? + expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(lastEmittedHTMLs()[0]).toContain(__YOUR_TURN__); // #QUESTION + expect(reactSpy).toHaveBeenCalledTimes(2); // #ANSWER + expect(lastEmittedHTMLs()[0]).toContain('$ 1079.11'); // #ANSWER + + // Last chance, what if we add a company + companies$.swap(current => [...current, 'APPL']); + + /** + * ** Your Turn ** + * + * Now the `stockPrice$` function should have at least run again + * for 'APPL'. + * + * But did it calculate 'GOOGL' again too? + */ + expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(stockPrice$).toHaveBeenCalledTimes(2); // #ANSWER + expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION + expect(reactSpy).toHaveBeenCalledTimes(3); // #ANSWER + // The first should be the generated HTML for 'GOOGL'. + expect(lastEmittedHTMLs()[0]).toContain(__YOUR_TURN__); // #QUESTION + expect(lastEmittedHTMLs()[0]).toContain('$ 1079.11'); // #ANSWER + // The second should be the generated HTML for 'APPL'. + expect(lastEmittedHTMLs()[1]).toContain(__YOUR_TURN__); // #QUESTION + expect(lastEmittedHTMLs()[1]).toContain('$ unknown'); // #ANSWER + }); + }); + }); +}); diff --git a/tutorial/jest.config.ts b/generator/jest.config.ts similarity index 93% rename from tutorial/jest.config.ts rename to generator/jest.config.ts index d27ca0f..2bb780e 100644 --- a/tutorial/jest.config.ts +++ b/generator/jest.config.ts @@ -1,7 +1,7 @@ import type { Config } from 'jest'; export default { - displayName: 'tutorial', + displayName: 'generator', preset: '../jest.preset.js', globals: {}, testEnvironment: 'node', diff --git a/generator/tsconfig.json b/generator/tsconfig.json new file mode 100644 index 0000000..89cc8af --- /dev/null +++ b/generator/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/generator/tsconfig.spec.json b/generator/tsconfig.spec.json new file mode 100644 index 0000000..750d7b4 --- /dev/null +++ b/generator/tsconfig.spec.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "types": ["jest", "jest-extended", "node"] + }, + "include": ["**/*.test.ts", "**/*.tests.ts", "**/*.d.ts", "jest.config.ts"] +} diff --git a/tutorial/6 - errors.test.ts b/tutorial/6 - errors.test.ts deleted file mode 100644 index e8ed4b4..0000000 --- a/tutorial/6 - errors.test.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { - atom, - DerivableAtom, - error, - ErrorWrapper, - final, - FinalWrapper, - MaybeFinalState, - unresolved, -} from '@skunkteam/sherlock'; -import { finalGetter, makeFinalMethod, setFinalMethod } from 'libs/sherlock/src/lib/derivable/mixins'; - -/** - * ** Your Turn ** - * - * If you see this variable, you should do something about it. :-) - */ -export const __YOUR_TURN__ = {} as any; - -// In libs/sherlock/src/lib/interfaces.ts:289, the basic states a Derivable can have are shown. -// > `export type State = V | unresolved | ErrorWrapper;` -// A state can be either any type `V` (`number`, `string`, etc.), `unresolved` as we saw in the -// previous tutorial, or `ErrorWrapper`. This last state is explained here. -describe('errors', () => { - let myDerivable: DerivableAtom; - - beforeEach(() => { - myDerivable = atom(1); - }); - - it('basic errors', () => { - // `errored` shows whether the last statement resulted in an error. - // It does NOT show whether the `Derivable` is in an error state. - expect(myDerivable.errored).toBe(false); - expect(myDerivable.error).toBeUndefined; - expect(myDerivable.getState()).toBe(1); // as explained above, any type can be a state - - // We can set errors using the `setError()` function. - myDerivable.setError('my Error'); - - expect(myDerivable.errored).toBe(true); - expect(myDerivable.error).toBe('my Error'); - // The `ErrorWrapper` state only holds an error string. The `error` function returns - // such an `ErrorWrapper` which we can use to compare. - expect(myDerivable.getState()).toMatchObject(error('my Error')); - - // As expected, calling `get()` on `myDerivable` gives an error. - expect(myDerivable.get).toThrow("Cannot read properties of undefined (reading 'getState')"); // TODO: WHAT - normally this works, but internal JEST just fucks with me....? - expect(() => myDerivable.get()).toThrow('my Error'); - expect(myDerivable.errored).toBe(true); - - // ** __YOUR_TURN__ ** - // What will happen if you try to call `set()` on `myDerivable`? - // `.toThrow()` or `.not.toThrow()`? ↴ - expect(() => myDerivable.set(2)).not.toThrow(); - // expect(() => myDerivable.set(2)) /* __YOUR_TURN__ */ - // expect(myDerivable.errored).toBe(__YOUR_TURN__); - // `.toBe(2)` or `.toMatchObject(error('my Error'))`? ↴ - expect(myDerivable.getState()).toBe(2); - // expect(myDerivable.getState()) /* __YOUR_TURN__ */ - - // Interestingly, calling `set()` does not throw an error. In fact, it removes the error state - // altogether. This means we can call `get()` again. - expect(() => myDerivable.get()).not.toThrow(); - }); - - it('deriving to an error', () => { - const myDerivable2 = myDerivable.derive(v => v + 1); - - // If the original derivable suddenly errors... - myDerivable.setError('division by zero'); - - // ...what happens to `myDerivable2`? - // `.toBe(2)` or `.toMatchObject(error('division by zero'))`? ↴ - expect(myDerivable2.getState()).toMatchObject(error('division by zero')); - // expect(myDerivable2.getState()) /* __YOUR_TURN__ */ - - // EXPLANATION AND MORE TODO: - }); - - it('reacting to an error', () => { - const doNothing: (v: number) => void = _ => {}; - myDerivable.react(doNothing); - - // ** __YOUR_TURN__ ** - // Will an error be thrown when reacting to a Derivable that throws an error? - // `.toThrow()` or `.not.toThrow()`? ↴ - expect(() => myDerivable.setError('my Error')).toThrow('my Error'); - // expect(() => myDerivable.setError('my Error')) - - // Reacting to a Derivable that throws an error will make the reactor throw as well. - // Because the reactor will usually fire when it gets connected, it also throws when - // you try to connect it after the error has already been set. - - myDerivable = atom(1); - myDerivable.setError('my second Error'); - - // ** __YOUR_TURN__ ** - // Will an error be thrown when you use `skipFirst`? - // `.toThrow()` or `.not.toThrow()`? ↴ - expect(() => myDerivable.react(doNothing, { skipFirst: true })).toThrow('my second Error'); - // expect(() => myDerivable.react(doSomething, { skipFirst: true })) - - // And will an error be thrown when `from = false`? - // `.toThrow()` or `.not.toThrow()`? ↴ - expect(() => myDerivable.react(doNothing, { from: false })).not.toThrow(); - // expect(() => myDerivable.react(doNothing, { from: false })) - - // When `from = false`, the reactor is disconnected, preventing the error message from entering. - // `skipFirst`, on the other hand, does allow the value in, but just does not trigger an update. - // This is similar if you change the boolean afterwards. - - // TODO: This is probably redundant. - let b = false; - expect(() => myDerivable.react(doNothing, { from: b })).not.toThrow(); - expect(() => (b = true)).not.toThrow(); - }); - - // always is `stopOnError` used in a DERIVABLE.TAKE, not a DERIVABLE.REACT...? - // libs/sherlock/src/lib/derivable/mixins/take.tests.ts - // 1034, 825... - - it('`mapState` to reason over errors', () => { - const mapping$ = myDerivable.mapState(state => { - if (state === unresolved) { - return atom('unresolved'); - } - if (state instanceof ErrorWrapper) { - return atom('error'); - } - return atom(myDerivable.get().toString()); - }); - - // You can get the mapped value out by using `.get()`. But then, to check the value of that atom, again `.get()`. - expect(mapping$.get().get()).toBe('1'); - - myDerivable.unset(); - expect(mapping$.get().get()).toBe('unresolved'); - - myDerivable.setError('Just a random error.'); - expect(mapping$.get().get()).toBe('error'); - }); - - it('TEMP', () => { - // FINAL - // libs/sherlock/src/lib/utils/final-wrapper.ts - - // TODO: EXPLAIN WHY YOU WOULD WANT THIS - let myAtom$ = atom(1); - - // every atom has a `final` property. - expect(myAtom$.final).toBeFalse(); - - // you can make an atom final using the `makeFinal()` function. - myAtom$.makeFinal(); - expect(myAtom$.final).toBeTrue(); - - // final atoms cannot be set anymore, but can be get. - expect(() => myAtom$.set(2)).toThrow('cannot set a final derivable'); - expect(() => myAtom$.get()).not.toThrow(); - - // alternatively, you can set a last value before setting it to `final`. - // Obviously, if the state is already `final`, this function will also throw an error. - expect(() => myAtom$.setFinal(2)).toThrow('cannot set a final derivable'); - myAtom$ = atom(1); // reset - myAtom$.setFinal(2); // try again - expect(myAtom$.final).toBeTrue(); - - // Every Derivable has a state. We have seen that states (`State`) can be `undefined`, `ErrorWrapper`, - // or any regular type `V`. Other states exist, such as the `MaybeFinalState`. This state can be either - // a normal state `State` or a special `FinalWrapper>` state. Let's see that in action. - myAtom$ = atom(1); - expect(myAtom$.getMaybeFinalState()).toBe(1); // `getMaybeFinalState` can return a normal state, which in turn can be a normal type - myAtom$.makeFinal(); - expect(myAtom$.getMaybeFinalState()).toBeInstanceOf(FinalWrapper); // but `getMaybeFinalState` can also return a `FinalWrapper` type! - - myAtom$ = atom(1); - // But what is the point of this? What can we do with these "states"? - // You can pattern match on the state to find out what the situation is. - // - // - // FIXME: But no seriously, what is the point of this STATE? You already have the boolean to check for final. - // FIXME: and what is the difference between Constants and Finals? Just that you can SET a final whenever you want? - // then isn't a Final just more powerful than a constant? - // FIXME: and when would you use this, in a real scenario? - // - // - // Let's first define a small checking function as we don't know exactly what type we deal with. - function verifyState(state: MaybeFinalState, value: T, final: boolean): void { - if (state instanceof FinalWrapper) { - expect(final).toBeTrue(); - expect(state.value).toBe(value); - } else { - expect(final).toBeFalse(); - expect(state).toBe(value); - } - } - - let myAtomState$ = myAtom$.getMaybeFinalState(); - verifyState(myAtomState$, 1, false); // the state is the same as my value. - - myAtom$.setFinal(2); - myAtomState$ = myAtom$.getMaybeFinalState(); - verifyState(myAtomState$, 2, true); // the final state still contains my value! - - // - // - // - - const final$ = final(1); - // finals cannot be `set`. See for yourself by uncommenting the next line. - // const final$.value = 2; - // finals can be `get` - expect(final$.value).toBe(1); - - finalGetter; - setFinalMethod; - makeFinalMethod; - // markFinal; - - // const myAtomMaybeFinal$ = myAtom$.getMaybeFinalState(); - // myAtomMaybeFinal$ - - // A normal state is called `State; a final state is `FinalWrapper>`, so a - // `MaybeFinalState` can be either! :: - // export type MaybeFinalState = State | FinalWrapper>; - // export type State = V | unresolved | ErrorWrapper; - - let a: MaybeFinalState = 1; - // a FinalWrapper can be made using the `final` function. - a = final(1); // similar to `atom`, but makes a FinalWrapper instead of an Atom. - // This is just syntactic sugar for: - a = FinalWrapper.wrap(1); - a = FinalWrapper.wrap(a); // this does nothing. - expect(a).toBeInstanceOf(FinalWrapper); - - // You can also use other functions. - a = FinalWrapper.unwrap(a); // does the opposite: get the V out of the FinalWrapper. - expect(a).not.toBeInstanceOf(FinalWrapper); // now it is not a FinalWrapper, but a State! - - // also has its own Map function - a = FinalWrapper.map(1, v => v + 1); - expect(a).toBe(2); - a = FinalWrapper.map(final(1), v => v + 1); - expect(a).toMatchObject(final(2)); - }); -}); - -/** - * Final States; (finalGetter, finalMethod, getMaybeFinalState, FinalWrapper, MaybeFinalState, _isFinal, makeFinal, markFinal, .final, .finalized, setFinal...) - * Lens; (libs/sherlock/src/lib/derivable/lens.ts) - ??? - * x Lift; (libs/sherlock-utils/src/lib/lift.ts) - * Peek; (libs/sherlock-utils/src/lib/peek.ts) - ??? - * x Template; (libs/sherlock-utils/src/lib/template.ts) - to make a string using a template literal. (Uses unwrap!!) - * / Factory; (libs/sherlock/src/lib/derivable/factories.ts) - simply contains functions to create objects, namely - * lens; atom; constant; derive. - * Flat-map; (libs/sherlock/src/lib/derivable/mixins/flat-map.ts) - ??? - * Fallback-to; - */ diff --git a/tutorial/7 - utils.test.ts b/tutorial/7 - utils.test.ts deleted file mode 100644 index 695aa53..0000000 --- a/tutorial/7 - utils.test.ts +++ /dev/null @@ -1,414 +0,0 @@ -import { atom, Derivable } from '@skunkteam/sherlock'; -import { lift, pairwise, scan, struct } from '@skunkteam/sherlock-utils'; - -// FIXME: // interne review document, mocht ik iets hebben om te laten zien! In Google Drive, zet het erin! - -/** - * ** Your Turn ** - * - * If you see this variable, you should do something about it. :-) - */ -export const __YOUR_TURN__ = {} as any; - -// Silence TypeScript's import not used errors. -expect(pairwise).toBe(pairwise); -expect(scan).toBe(scan); -expect(struct).toBe(struct); - -/** - * In the `sherlock-utils` lib, there are a couple of functions that can combine - * multiple values of a single `Derivable` or combine multiple `Derivable`s into - * one. We will show a couple of those here. TODO: Hmm, I want to see some others too! - */ -describe('utils', () => { - /** - * As the name suggests, `pairwise()` will call the given function with both - * the current and the previous state. - * - * *Note: functions like `pairwise` and `scan` can be used with any callback, - * so it can be used both in a `.derive()` step and in a `.react()`* - */ - it('pairwise', () => { - const myCounter$ = atom(1); - const reactSpy = jest.fn(); - - /** - * ** Your Turn ** - * - * Now, use `pairwise()` to subtract the previous value from the - * current. - * - * *Hint: check the overloads of pairwise if you're struggling with - * `oldVal`.* - */ - myCounter$.derive(pairwise((newVal, oldVal) => (oldVal ? newVal - oldVal : newVal))).react(reactSpy); - // myCounter$.derive(pairwise((newVal, oldVal) => newVal - oldVal, 0)).react(reactSpy); // OR: alternatively. - - expect(reactSpy).toHaveBeenCalledExactlyOnceWith(1, expect.toBeFunction()); - - myCounter$.set(5); - - expect(reactSpy).toHaveBeenCalledTimes(2); - expect(reactSpy).toHaveBeenCalledWith(4, expect.toBeFunction()); - - myCounter$.set(45); - - expect(reactSpy).toHaveBeenCalledTimes(3); - expect(reactSpy).toHaveBeenCalledWith(40, expect.toBeFunction()); - }); - - /** - * `scan` is the `Derivable` version of `Array.prototype.reduce`. It will be - * called with the current state and the last emitted value. - * - * (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce) - * - * *Note: as with `pairwise()` this is useable in both a `.derive()` and - * `.react()` method* - */ - it('scan', () => { - const myCounter$ = atom(1); - const reactSpy = jest.fn(); - - /** - * ** Your Turn ** - * - * Now, use `scan()` to subtract the previous value from the - * current. TODO: - */ - myCounter$.derive(scan((acc, val) => val + acc, 0)).react(reactSpy); - - expect(reactSpy).toHaveBeenCalledExactlyOnceWith(1, expect.toBeFunction()); - - myCounter$.set(5); - - expect(reactSpy).toHaveBeenCalledTimes(2); - expect(reactSpy).toHaveBeenCalledWith(6, expect.toBeFunction()); - - myCounter$.set(45); - - expect(reactSpy).toHaveBeenCalledTimes(3); - expect(reactSpy).toHaveBeenCalledWith(51, expect.toBeFunction()); - - /** - * *BONUS: Try using `scan()` (or `pairwise()`) directly in the - * `.react()` method.* - */ - }); - - // TODO: dit laat niet mooi het verschil zien. Hier lijkt het net alsof ze hetzelfde doen! - // En `scan` naar `val - acc` veranderen werkt niet. Geeft weird gedrag. - - it('scan2', () => { - // const myList$ = atom([]); - const myInt$ = atom(1); - const reactSpy = jest.fn(); - const f: (n1: number, n2: number) => number[] = (newVal, oldVal) => [newVal + oldVal]; - const d: number = 0; - let stopper: () => void; - - myInt$.derive(pairwise(f, d)); - // this is actually the same as: - myInt$.derive(v => pairwise(f, d)(v)); - // it just uses partial application. Pairwise itself is a function after all, which you apply to some value. - // This value then internally has a `previous state` property somewhere. - - // 1) this is one way or writing a derivable + react. - const myList$: Derivable = myInt$.derive(pairwise(f, d)); - stopper = myList$.react(reactSpy); - stopper(); - - // 2) the value of `myList$` is now directly passed to `reactSpy` without it being an extra variable. - // since we can get the value out of the `reactSpy`, this might be all we need. - stopper = myInt$.derive(pairwise(f, d)).react(reactSpy); - stopper(); - - // Let's try it out for real. - // The previous exercise made it seem like `pairwise` and `scan` do similar things. This is not true. - stopper = myInt$.derive(pairwise(f, d)).react(reactSpy); - - myInt$.set(2); - myInt$.set(3); - myInt$.set(4); - - expect(reactSpy).toHaveBeenCalledWith([3 + 4], expect.toBeFunction()); - stopper(); - - // Now let's to the same with scan. Already, the types don't match. - // The return type must be number (uncomment to see error). - // stopper = myInt$.derive(scan(f, d)).react(reactSpy); - // TODO: why? - - const f2: (n1: number, n2: number) => number = (newVal, oldVal) => newVal + oldVal; - stopper = myInt$.derive(scan(f2, d)).react(reactSpy); // starts at 4 - - myInt$.set(2); // then becomes 6 - myInt$.set(3); // then becomes 9 - myInt$.set(4); // lastly, becomes 13 - - expect(reactSpy).toHaveBeenCalledWith(13, expect.toBeFunction()); - stopper(); - - // ------- - // ------- - // ------- - // ------- - - // expect(reactSpy).toHaveBeenCalledExactlyOnceWith(1, expect.toBeFunction()); - - // myCounter$.set(5); - - // expect(reactSpy).toHaveBeenCalledTimes(2); - // expect(reactSpy).toHaveBeenCalledWith(6, expect.toBeFunction()); - - // myCounter$.set(45); - - // expect(reactSpy).toHaveBeenCalledTimes(3); - // expect(reactSpy).toHaveBeenCalledWith(51, expect.toBeFunction()); - - /** - * *BONUS: Try using `scan()` (or `pairwise()`) directly in the - * `.react()` method.* - */ - }); - - it('`pairwise()` on normal arrays', () => { - // Functions like `pairwise()` and `scan()` work on normal lists too. They are often - // used in combination with `map()` and `filter()`. - const myList = [1, 2, 3, 5, 10]; - let myList2: number[]; - - /** - * ** Your Turn ** - * - * Use a `pairwise()` combined with a `map` on `myList` - * to subtract the previous value from the current. - * - * Hint: do not use a lambda function! - */ - - // let oldValue = init; - libs/sherlock-utils/src/lib/pairwise.ts:24 - // Closures?? TODO: - - // myList2 = myList.map(pairwise((newV, oldV) => newV - oldV, 0)); - myList2 = myList.map(__YOUR_TURN__); - expect(myList2).toMatchObject([1, 1, 1, 2, 5]); - - // However, we should be careful with this, as this does not always behave as intended. - myList2 = myList.map(v => __YOUR_TURN__(v)); // copy the same implementation here - expect(myList2).toMatchObject([1, 2, 3, 5, 10]); - - // Even if we are more clear about what we pass, this unintended behavior does not go away. - myList2 = myList.map((v, _, _2) => __YOUR_TURN__(v)); // copy the same implementation here - expect(myList2).toMatchObject([1, 2, 3, 5, 10]); - - // `pairwise()` keeps track of the previous value under the hood. Using a lambda of - // the form `v => pairwise(...)(v)` would create a new `pairwise` function every call, - // essentially resetting the previous value every call. And resetting the previous value - // to 0 causes the input to stay the same (after all: x - 0 = x). - // We can solve this by saving the `pairwise` in a variable and reusing it for every call. - - // let f = pairwise((newV, oldV) => newV - oldV, 0); - let f: (v: number) => number = __YOUR_TURN__; // copy the same implementation here - myList2 = myList.map(v => f(v)); - expect(myList2).toMatchObject([1, 1, 1, 2, 5]); - - // To get more insight in the `pairwise()` function, you can also just call it - // manually. Here, we show what happens under the hood. - - // f = pairwise((newV, oldV) => newV - oldV, 0); - f = pairwise(__YOUR_TURN__); // copy the same implementation here - - myList2 = []; - myList2[0] = f(myList[0]); // f is newly created with `init = 0`, so applies `1 - 0`. - myList2[1] = f(myList[1]); // f has saved `1` internally, so applies `2 - 1`. - myList2[2] = f(myList[2]); // f has saved `2` internally, so applies `3 - 2`. - myList2[3] = f(myList[3]); // f has saved `3` internally, so applies `5 - 3`. - myList2[4] = f(myList[4]); // f has saved `5` internally, so applies `10 - 5`. - - expect(myList2).toMatchObject([1, 1, 1, 2, 5]); - - // This also works for functions other than `map()`, such as `filter()`. - // Use `pairwise()` to filter out all values which produce `1` when subtracted - // with their previous value. - - // myList2 = myList.filter(pairwise((newV, oldV) => newV - oldV === 1, 0)); - myList2 = myList.filter(__YOUR_TURN__); - expect(myList2).toMatchObject([1, 2, 3]); - }); - - it('`scan()` on normal arrays', () => { - // As with `pairwise()` in the last test, `scan()` can be used with arrays too. - const myList = [1, 2, 3, 5, 10]; - let myList2: number[]; - - /** - * ** Your Turn ** - * - * Use a `scan()` combined with a `map` on `myList` - * to subtract the previous value from the current. - * - * Hint: do not use a lambda function! - * TODO: instead, make them write expectancies rather than the implementation. Is way nicer? - */ - - myList2 = myList.map(scan((acc, val) => val - acc, 0)); - // myList2 = myList.map(__YOUR_TURN__); - expect(myList2).toMatchObject([1, 1, 2, 3, 7]); - - // again, it is useful to consider what happens internally. - let f: (v: number) => number = scan((acc, val) => val - acc, 0); - // let f: (v: number) => number = pairwise(__YOUR_TURN__); // copy the same implementation here - - myList2 = []; - myList2[0] = f(myList[0]); // 1 :: f is newly created with `init = 0`, so applies `1 - 0 = 1`. - myList2[1] = f(myList[1]); // 1 :: f has saved the result `1` internally, so applies `2 - 1 = 1`. - myList2[2] = f(myList[2]); // 2 :: f has saved the result `1` internally, so applies `3 - 1 = 2`. - myList2[3] = f(myList[3]); // 3 :: f has saved the result `2` internally, so applies `5 - 2 = 3`. - myList2[4] = f(myList[4]); // 7 :: f has saved the result `3` internally, so applies `10 - 3 = 7`. - - expect(myList2).toMatchObject([1, 1, 2, 3, 7]); - - // This also works for functions other than `map()`, such as `filter()`. - // Use `scan()` to filter out all values which produce `1` when subtracted - // with the previous result. - // TODO: note (earlier) that `scan()` must return the same type as it gets as input. This is required - // as this returned value is also used for the accumulator value for the next call! - - // f = scan((acc, val) => val - acc, 0); - // myList2 = myList.filter(v => f(v) == 1); - f = scan(__YOUR_TURN__); - myList2 = myList.filter(__YOUR_TURN__); - expect(myList2).toMatchObject([1, 2]); // Only the numbers `1` and `2` from `myList` return `1`. - }); - - it('pairwise - BONUS', () => { - const myCounter$ = atom(1); - let reactSpy = jest.fn(); - - /** - * ** Your Turn ** - * ** BONUS ** - * - * Now, use `pairwise()` directly in `.react()`. Implement the same - * derivation as before: subtract the previous value from the current. - */ - - reactSpy = jest.fn(pairwise((newV, oldV) => newV - oldV, 0)); - // reactSpy = jest.fn(__YOUR_TURN__); - myCounter$.react(reactSpy); - - expect(reactSpy).toHaveLastReturnedWith(1); - - myCounter$.set(3); - - expect(reactSpy).toHaveLastReturnedWith(2); - - myCounter$.set(45); - - expect(reactSpy).toHaveLastReturnedWith(42); // 45 - 3 (last value of `myCounter$`) - }); - - it('scan - BONUS', () => { - const myCounter$ = atom(1); - let reactSpy = jest.fn(); - - /** - * ** Your Turn ** - * ** BONUS ** - * - * Now, use `scan()` directly in `.react()`. Implement the same - * derivation as before: subtract all the emitted values. - */ - - reactSpy = jest.fn(scan((acc, val) => val - acc, 0)); - // reactSpy = jest.fn(__YOUR_TURN__); - // NOTE: acc is the last returned value, not the last value of `myCounter$`!! They are not the same! - myCounter$.react(reactSpy); - // TODO: can I also get all reactors within `myCounter$`? - - expect(reactSpy).toHaveLastReturnedWith(1); - - myCounter$.set(3); - - expect(reactSpy).toHaveLastReturnedWith(2); - - myCounter$.set(45); - - expect(reactSpy).toHaveLastReturnedWith(43); // 45 - 2 (last returned value) = 43 TODO: show this difference better! - }); - - /** - * A `struct()` can combine an Object/Array of `Derivable`s into one - * `Derivable`, that contains the values of that `Derivable`. - * - * The Object/Array that is in the output of `struct()` will have the same - * structure as the original Object/Array. - * - * This is best explained in practice. - */ - it('struct', () => { - const allMyAtoms = { - regularProp: 'prop', - string: atom('my string'), - number: atom(1), - sub: { - string: atom('my substring'), - }, - }; - - const myOneAtom$ = struct(allMyAtoms); - - expect(myOneAtom$.get()).toEqual({ - regularProp: 'prop', - string: 'my string', - number: 1, - sub: { - string: 'my substring', - }, - }); - - allMyAtoms.regularProp = 'new value'; - allMyAtoms.sub.string.set('my new substring'); - - /** - * ** Your Turn ** - * - * Now have a look at the properties of `myOneAtom$`. Is this what you - * expect? - */ - expect(myOneAtom$.get()).toEqual({ - regularProp: 'new value', // it turns everything in a atom, sure - string: 'my string', // but why does changing the original normal string work?? TODO: does it listen to that actual struct (string) now?? - number: 1, - sub: { - string: 'my new substring', - }, - }); - }); - - it('lift', () => { - // Derivables can feel like a language build on top of Typescript. Sometimes - // you might want to use normal objects and functions and not have to rewrite - // your code. - // In other words, just like keywords like `atom(V)` lift the type `V` to the higher - // level of Derivables, the `lift(F)` keyword lifts a function `F` to the higher - // level of Derivables. - - // Example: after years of effort, I finally finished my super-long function: - const isEvenNumber = (v: number) => v % 2 == 0; - - // TODO: - // So rewriting this function to work with derivables would be a waste of time. - // YOUR TURN, use lift to reuse `isEvenNumber` on derivable level. - const isEvenDerivable = lift(isEvenNumber); - - expect(isEvenNumber(2)).toBe(true); - expect(isEvenNumber(13)).toBe(true); - expect(isEvenDerivable(atom(2))).toMatchObject(atom(true)); - expect(isEvenDerivable(atom(13))).toMatchObject(atom(false)); - }); - - // TODO: - it('peek', () => {}); -}); From cf31e4d807c800728bb24cbb183a667b1902330d Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 25 Jul 2024 13:23:54 +0200 Subject: [PATCH 25/30] Added more tests; renamed the folders; improved the generator; README adjustment --- CHANGELOG.md | 7 +- README.md | 4 +- generateTutorialAndSolution.js | 75 ++- generateTutorialAndSolution.ts | 43 +- generated_solution/6 - errors.test.ts | 263 -------- generated_solution/8 - utils.test.ts | 382 ----------- generated_tutorial/6 - errors.test.ts | 263 -------- generated_tutorial/8 - utils.test.ts | 381 ----------- generator/1 - intro.test.ts | 13 +- generator/2 - deriving.test.ts | 11 +- generator/3 - reacting.test.ts | 160 +++-- generator/4 - inner workings.test.ts | 15 +- generator/5 - unresolved.test.ts | 35 +- generator/6 - errors.test.ts | 234 ++----- generator/7 - advanced.test.ts | 60 +- generator/8 - utils.test.ts | 341 +++++++++- generator/9 - expert.test.ts | 23 +- package.json | 3 +- .../1 - intro.test.ts | 16 +- .../2 - deriving.test.ts | 20 +- .../3 - reacting.test.ts | 157 +++-- .../4 - inner workings.test.ts | 22 +- .../5 - unresolved.test.ts | 37 +- solution/6 - errors.test.ts | 114 ++++ .../7 - advanced.test.ts | 69 +- solution/8 - utils.test.ts | 620 +++++++++++++++++ .../9 - expert.test.ts | 24 +- .../jest.config.ts | 2 +- .../tsconfig.json | 0 .../tsconfig.spec.json | 0 .../1 - intro.test.ts | 11 +- .../2 - deriving.test.ts | 7 +- .../3 - reacting.test.ts | 123 ++-- .../4 - inner workings.test.ts | 12 +- .../5 - unresolved.test.ts | 28 +- tutorial/6 - errors.test.ts | 119 ++++ .../7 - advanced.test.ts | 58 +- tutorial/8 - utils.test.ts | 632 ++++++++++++++++++ .../9 - expert.test.ts | 10 +- .../jest.config.ts | 2 +- .../tsconfig.json | 0 .../tsconfig.spec.json | 0 42 files changed, 2469 insertions(+), 1927 deletions(-) delete mode 100644 generated_solution/6 - errors.test.ts delete mode 100644 generated_solution/8 - utils.test.ts delete mode 100644 generated_tutorial/6 - errors.test.ts delete mode 100644 generated_tutorial/8 - utils.test.ts rename {generated_solution => solution}/1 - intro.test.ts (94%) rename {generated_solution => solution}/2 - deriving.test.ts (95%) rename {generated_solution => solution}/3 - reacting.test.ts (72%) rename {generated_solution => solution}/4 - inner workings.test.ts (95%) rename {generated_solution => solution}/5 - unresolved.test.ts (84%) create mode 100644 solution/6 - errors.test.ts rename {generated_solution => solution}/7 - advanced.test.ts (91%) create mode 100644 solution/8 - utils.test.ts rename {generated_solution => solution}/9 - expert.test.ts (96%) rename {generated_solution => solution}/jest.config.ts (91%) rename {generated_solution => solution}/tsconfig.json (100%) rename {generated_solution => solution}/tsconfig.spec.json (100%) rename {generated_tutorial => tutorial}/1 - intro.test.ts (94%) rename {generated_tutorial => tutorial}/2 - deriving.test.ts (97%) rename {generated_tutorial => tutorial}/3 - reacting.test.ts (76%) rename {generated_tutorial => tutorial}/4 - inner workings.test.ts (97%) rename {generated_tutorial => tutorial}/5 - unresolved.test.ts (86%) create mode 100644 tutorial/6 - errors.test.ts rename {generated_tutorial => tutorial}/7 - advanced.test.ts (92%) create mode 100644 tutorial/8 - utils.test.ts rename {generated_tutorial => tutorial}/9 - expert.test.ts (97%) rename {generated_tutorial => tutorial}/jest.config.ts (91%) rename {generated_tutorial => tutorial}/tsconfig.json (100%) rename {generated_tutorial => tutorial}/tsconfig.spec.json (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c3a5e2..7ea79f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,12 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. -TODO: should I put my shit in? +TODO: +Lots of changes! + +- Swapped `7 - advanced` with `8 - util` to be able to use advanced techniques in the utils. +- More tests, including `reactor options order of execution (3)`, `fallback-to (5)`, `errors (6)`, `templates, take, map on arrays, mapState, flatMap, pluck (7)`, and `pairwise/scan on normal arrays, peek, lift, final, promise... (8)`. +- The tutorial and solutions are now both available in the same branch. To prevent making every change twice, all changes should now be put in the `generator` folder rather than directly in the `tutorial` or `solution` folder. ## [8.0.0](https://github.com/skunkteam/sherlock/compare/v7.0.0...v8.0.0) (2023-10-17) diff --git a/README.md b/README.md index 47c5448..b2ddaf6 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ let currentPageNumber$ = atom(0); // We simply use a lambda function to define currentPage$ as a derivation of currentBook$ // and currentFontSize$ using calculatePages. Sherlock automatically records all dependencies. -let currentPages$ = derivation(() => calculatePages(currentBook$.get(), currentFontSize$.get())); +let currentPages$ = derive(() => calculatePages(currentBook$.get(), currentFontSize$.get())); // currentPage$ is always equal to the element in currentPages$ at position currentPageNumber$. let currentPage$ = currentPages$.pluck(currentPageNumber$); @@ -152,7 +152,7 @@ There are three types of Derivables: isBrilliant$.get(); // true ``` - Derivations can also be created with the generic `derivation` function as seen earlier. This function can be used to do an arbitrary calculation on any number of derivables. `@skunkteam/sherlock` automatically records which derivable is dependent on which other derivable to be able to update derived state when needed. + Derivations can also be created with the generic `derive` function as seen earlier. This function can be used to do an arbitrary calculation on any number of derivables. `@skunkteam/sherlock` automatically records which derivable is dependent on which other derivable to be able to update derived state when needed. ## Reactors diff --git a/generateTutorialAndSolution.js b/generateTutorialAndSolution.js index b974d70..1fe14c1 100644 --- a/generateTutorialAndSolution.js +++ b/generateTutorialAndSolution.js @@ -37,50 +37,81 @@ var __generator = (this && this.__generator) || function (thisArg, body) { }; Object.defineProperty(exports, "__esModule", { value: true }); // import * as fs from 'fs'; +var node_console_1 = require("node:console"); var fs = require("node:fs/promises"); // Run with: tsc generateTutorialAndSolution.ts && node generateTutorialAndSolution.js +var generatorFolder = 'generator'; +var tutorialFolder = 'tutorial'; +var solutionFolder = 'solution'; function generateTutorialAndSolutions() { return __awaiter(this, void 0, void 0, function () { - var filenames, _i, filenames_1, filename, originalContent, tutorialContent, solutionContent; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: return [4 /*yield*/, fs.readdir('generator')]; + var filenames, _i, filenames_1, filename, originalContent, tutorialContent, solutionContent, _a, filenames_2, filename, _b, _c, foldername, content; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: return [4 /*yield*/, fs.readdir(generatorFolder)]; case 1: - filenames = (_a.sent()).filter(function (f) { return f.endsWith("test.ts"); }); + filenames = (_d.sent()).filter(function (f) { return f.endsWith("test.ts"); }); _i = 0, filenames_1 = filenames; - _a.label = 2; + _d.label = 2; case 2: if (!(_i < filenames_1.length)) return [3 /*break*/, 7]; filename = filenames_1[_i]; - return [4 /*yield*/, fs.readFile("generator/".concat(filename), 'utf8')]; + return [4 /*yield*/, fs.readFile("".concat(generatorFolder, "/").concat(filename), 'utf8')]; case 3: - originalContent = _a.sent(); + originalContent = _d.sent(); tutorialContent = originalContent .replace(/describe(?!\.skip)/g, "describe.skip") // change `describe` to `describe.skip` - .replace(/\/\/ #QUESTION-BLOCK-(START|END)/g, "") + .replace(/\n.*?\/\/ #QUESTION-BLOCK-(START|END)/g, "") .replace(/\/\/ #QUESTION/g, "") // remove `// #QUESTION` comments - .replace(/\/\/ #ANSWER-BLOCK-START[\s\S]*?\/\/ #ANSWER-BLOCK-END/g, "") // remove // #ANSWER blocks - .replace(/\n.*?\/\/ #ANSWER/g, "") // remove the entire `// #ANSWER` line, including comment - .replace(/\n\s*\n\s*\n/g, "\n\n"); - return [4 /*yield*/, fs.writeFile("generated_tutorial/".concat(filename), tutorialContent)]; + .replace(/\n.*?\/\/ #ANSWER-BLOCK-START[\s\S]*?\/\/ #ANSWER-BLOCK-END/g, "") // remove // #ANSWER blocks + .replace(/\n.*?\/\/ #ANSWER/g, ""); + // .replace(/\n\s*\n\s*\n/g, `\n\n`); // remove excess whitespaces/newlines + return [4 /*yield*/, fs.writeFile("".concat(tutorialFolder, "/").concat(filename), tutorialContent)]; case 4: - _a.sent(); + // .replace(/\n\s*\n\s*\n/g, `\n\n`); // remove excess whitespaces/newlines + _d.sent(); solutionContent = originalContent .replace(/describe\.skip/g, "describe") // change `describe.skip` to `describe` - .replace(/\/\/ #ANSWER-BLOCK-(START|END)/g, "") + .replace(/\n.*?\/\/ #ANSWER-BLOCK-(START|END)/g, "") .replace(/\/\/ #ANSWER/g, "") // remove `// #ANSWER` comments - .replace(/\/\/ #QUESTION-BLOCK-START[\s\S]*?\/\/ #QUESTION-BLOCK-END/g, "") // remove // #QUESTION blocks - .replace(/\n.*?\/\/ #QUESTION/g, "") // remove the entire `// #QUESTION` line, including comment - .replace(/\n\s*\n\s*\n/g, "\n\n"); - return [4 /*yield*/, fs.writeFile("generated_solution/".concat(filename), solutionContent)]; + .replace(/\n.*?\/\/ #QUESTION-BLOCK-START[\s\S]*?\/\/ #QUESTION-BLOCK-END/g, "") // remove // #QUESTION blocks + .replace(/\n.*?\/\/ #QUESTION/g, ""); + // .replace(/\n\s*\n\s*\n/g, `\n\n`); // remove excess whitespaces/newlines + return [4 /*yield*/, fs.writeFile("".concat(solutionFolder, "/").concat(filename), solutionContent)]; case 5: - _a.sent(); + // .replace(/\n\s*\n\s*\n/g, `\n\n`); // remove excess whitespaces/newlines + _d.sent(); console.log("\u001B[33m ".concat(filename, " saved! \u001B[0m")); - _a.label = 6; + _d.label = 6; case 6: _i++; return [3 /*break*/, 2]; - case 7: return [2 /*return*/]; + case 7: return [4 /*yield*/, fs.readdir(generatorFolder)]; + case 8: + filenames = (_d.sent()).filter(function (f) { return f.endsWith("test.ts"); }); // the names are the same in all three folders + _a = 0, filenames_2 = filenames; + _d.label = 9; + case 9: + if (!(_a < filenames_2.length)) return [3 /*break*/, 14]; + filename = filenames_2[_a]; + _b = 0, _c = [tutorialFolder, solutionFolder]; + _d.label = 10; + case 10: + if (!(_b < _c.length)) return [3 /*break*/, 13]; + foldername = _c[_b]; + return [4 /*yield*/, fs.readFile("".concat(foldername, "/").concat(filename), 'utf8')]; + case 11: + content = _d.sent(); + (0, node_console_1.assert)(content.match(/\n\s*\n\s*\n/) === null, "no 2 consecutive empty lines in ".concat(foldername, "/").concat(filename)); + (0, node_console_1.assert)(!content.includes('#QUESTION') && !content.includes('#ANSWER'), "no '#QUESTION' or '#ANSWER' in ".concat(foldername, "/").concat(filename)); + _d.label = 12; + case 12: + _b++; + return [3 /*break*/, 10]; + case 13: + _a++; + return [3 /*break*/, 9]; + case 14: return [2 /*return*/]; } }); }); diff --git a/generateTutorialAndSolution.ts b/generateTutorialAndSolution.ts index a8ec8bd..cc8d129 100644 --- a/generateTutorialAndSolution.ts +++ b/generateTutorialAndSolution.ts @@ -1,39 +1,58 @@ // import * as fs from 'fs'; +import { assert } from 'node:console'; import * as fs from 'node:fs/promises'; // Run with: tsc generateTutorialAndSolution.ts && node generateTutorialAndSolution.js +const generatorFolder = 'generator'; +const tutorialFolder = 'tutorial'; +const solutionFolder = 'solution'; + async function generateTutorialAndSolutions() { // get all filenames in the folder 'tutorial' that end on '.ts' - let filenames: string[] = (await fs.readdir('generator')).filter(f => f.endsWith(`test.ts`)); + let filenames: string[] = (await fs.readdir(generatorFolder)).filter(f => f.endsWith(`test.ts`)); for (let filename of filenames) { // // Read the original text file - const originalContent = await fs.readFile(`generator/${filename}`, 'utf8'); + const originalContent = await fs.readFile(`${generatorFolder}/${filename}`, 'utf8'); // ** TUTORIAL ** let tutorialContent = originalContent .replace(/describe(?!\.skip)/g, `describe.skip`) // change `describe` to `describe.skip` - .replace(/\/\/ #QUESTION-BLOCK-(START|END)/g, ``) + .replace(/\n.*?\/\/ #QUESTION-BLOCK-(START|END)/g, ``) .replace(/\/\/ #QUESTION/g, ``) // remove `// #QUESTION` comments - .replace(/\/\/ #ANSWER-BLOCK-START[\s\S]*?\/\/ #ANSWER-BLOCK-END/g, ``) // remove // #ANSWER blocks - .replace(/\n.*?\/\/ #ANSWER/g, ``) // remove the entire `// #ANSWER` line, including comment - .replace(/\n\s*\n\s*\n/g, `\n\n`); // remove excess whitespaces/newlines + .replace(/\n.*?\/\/ #ANSWER-BLOCK-START[\s\S]*?\/\/ #ANSWER-BLOCK-END/g, ``) // remove // #ANSWER blocks + .replace(/\n.*?\/\/ #ANSWER/g, ``); // remove the entire `// #ANSWER` line, including comment + // .replace(/\n\s*\n\s*\n/g, `\n\n`); // remove excess whitespaces/newlines - await fs.writeFile(`generated_tutorial/${filename}`, tutorialContent); + await fs.writeFile(`${tutorialFolder}/${filename}`, tutorialContent); // ** SOLUTION ** let solutionContent = originalContent .replace(/describe\.skip/g, `describe`) // change `describe.skip` to `describe` - .replace(/\/\/ #ANSWER-BLOCK-(START|END)/g, ``) + .replace(/\n.*?\/\/ #ANSWER-BLOCK-(START|END)/g, ``) .replace(/\/\/ #ANSWER/g, ``) // remove `// #ANSWER` comments - .replace(/\/\/ #QUESTION-BLOCK-START[\s\S]*?\/\/ #QUESTION-BLOCK-END/g, ``) // remove // #QUESTION blocks - .replace(/\n.*?\/\/ #QUESTION/g, ``) // remove the entire `// #QUESTION` line, including comment - .replace(/\n\s*\n\s*\n/g, `\n\n`); // remove excess whitespaces/newlines + .replace(/\n.*?\/\/ #QUESTION-BLOCK-START[\s\S]*?\/\/ #QUESTION-BLOCK-END/g, ``) // remove // #QUESTION blocks + .replace(/\n.*?\/\/ #QUESTION/g, ``); // remove the entire `// #QUESTION` line, including comment + // .replace(/\n\s*\n\s*\n/g, `\n\n`); // remove excess whitespaces/newlines - await fs.writeFile(`generated_solution/${filename}`, solutionContent); + await fs.writeFile(`${solutionFolder}/${filename}`, solutionContent); console.log(`\x1b[33m ${filename} saved! \x1b[0m`); } + + // These tests will not cause any failing, but are just nice to have. + // e.g. instead of removing excess whitespaces/newlines, we now just prevent them altogether. + filenames = (await fs.readdir(generatorFolder)).filter(f => f.endsWith(`test.ts`)); // the names are the same in all three folders + for (let filename of filenames) { + for (let foldername of [tutorialFolder, solutionFolder]) { + let content = await fs.readFile(`${foldername}/${filename}`, 'utf8'); + assert(content.match(/\n\s*\n\s*\n/) === null, `no 2 consecutive empty lines in ${foldername}/${filename}`); + assert( + !content.includes('#QUESTION') && !content.includes('#ANSWER'), + `no '#QUESTION' or '#ANSWER' in ${foldername}/${filename}`, + ); + } + } } generateTutorialAndSolutions(); diff --git a/generated_solution/6 - errors.test.ts b/generated_solution/6 - errors.test.ts deleted file mode 100644 index abf2d3c..0000000 --- a/generated_solution/6 - errors.test.ts +++ /dev/null @@ -1,263 +0,0 @@ -import { atom, DerivableAtom, error, FinalWrapper, unresolved } from '@skunkteam/sherlock'; - -/** - * ** Your Turn ** - * - * If you see this variable, you should do something about it. :-) - */ -export const __YOUR_TURN__ = {} as any; - -// Silence TypeScript's import not used errors. -expect(FinalWrapper).toBe(FinalWrapper); - -// In libs/sherlock/src/lib/interfaces.ts:289, the basic states a Derivable can have are shown. -// > `export type State = V | unresolved | ErrorWrapper;` -// A state can be either any type `V` (`number`, `string`, etc.), `unresolved` as we saw in the -// previous tutorial, or `ErrorWrapper`. This last state is explained here. -describe('errors', () => { - let myAtom$: DerivableAtom; - - beforeEach(() => { - myAtom$ = atom(1); - }); - - it('basic errors', () => { - // The `errored` property shows whether the last statement resulted in an error. - expect(myAtom$.errored).toBe(false); - expect(myAtom$.error).toBeUndefined; // by default, the `error` property is undefined. - expect(myAtom$.getState()).toBe(1); // as explained above, any type can be a state - - // We can set errors using the `setError()` function. - myAtom$.setError('my Error'); - - expect(myAtom$.errored).toBe(true); - expect(myAtom$.error).toBe('my Error'); - - // The `ErrorWrapper` state only holds an error string. The `error()` function returns - // such an `ErrorWrapper` which we can use to compare. - expect(myAtom$.getState()).toMatchObject(error('my Error')); - - // expect(myAtom$.get).toThrow("Cannot read properties of undefined (reading 'getState')"); - // TODO: WHAT - normally this works, but internal JEST just fucks with me....? - - // Calling `get()` on `myAtom$` gives the error. - expect(() => myAtom$.get()).toThrow('my Error'); - expect(myAtom$.errored).toBe(true); - - // ** __YOUR_TURN__ ** - // What will happen if you try to call `set()` on `myAtom$`? - // `.toThrow()` or `.not.toThrow()`? ↴ - expect(() => myAtom$.set(2)).not.toThrow(); - expect(myAtom$.errored).toBe(false); - - // Interestingly, calling `set()` does not throw an error. In fact, it removes the error state - // altogether. This means we can call `get()` again. - expect(() => myAtom$.get()).not.toThrow(); - }); - - it('deriving an error', () => { - const myDerivable$ = myAtom$.derive(v => v + 1); - - // If `myAtom$` suddenly errors... - myAtom$.setError('division by zero'); - - // ...what happens to `myDerivable$`? - expect(myDerivable$.errored).toBe(true); - - // If any Derivable tries to derive from an atom in an error state, - // this Derivable will itself throw an error too. This makes sense, - // given that it cannot obtain the value it needs anymore. - }); - - it('reacting to an error', () => { - // Without a reactor, setting an error to an Atom does not throw an error. - expect(() => myAtom$.setError('my Error')).not.toThrow(); - myAtom$.set(1); - - // Now we set a reactor to `myAtom$`. This reactor does not use the value of `myAtom$`. - const reactor = jest.fn(); - myAtom$.react(reactor); - - // ** __YOUR_TURN__ ** - // Will an error be thrown when `myAtom$` is now set to an error state? - // `.toThrow()` or `.not.toThrow()`? ↴ - expect(() => myAtom$.setError('my Error')).toThrow('my Error'); - - // Reacting to a Derivable that throws an error will make the reactor throw as well. - // Because the reactor will usually fire when it gets connected, it also throws when - // you try to connect it after the error has already been set. - - myAtom$ = atom(1); - myAtom$.setError('my second Error'); - - // ** __YOUR_TURN__ ** - // Will an error be thrown when you use `skipFirst`? - // `.toThrow()` or `.not.toThrow()`? ↴ - expect(() => myAtom$.react(reactor, { skipFirst: true })).toThrow('my second Error'); - - // And will an error be thrown when `from = false`? - // `.toThrow()` or `.not.toThrow()`? ↴ - expect(() => myAtom$.react(reactor, { from: false })).not.toThrow(); - - // When `from = false`, the reactor is disconnected, preventing the error message from entering. - // `skipFirst`, on the other hand, does allow the error in, but does not trigger an update. - }); - - /** - * Similarly to `constants` which we'll explain in tutorial 7, - * you might want to specify that a variable cannot be updated. - * This can be useful for the programmers themselves, to not - * accidentally update the variable, but it can also be useful for - * optimization. You can do this using the `final` concept. - */ - describe('TEMP `final`', () => { - let myAtom$ = atom(1); - - beforeEach(() => { - myAtom$ = atom(1); - }); - - it('`final` basics', () => { - // Every atom has a `final` property. - expect(myAtom$.final).toBeFalse(); - - // You can make an atom final using the `.makeFinal()` function. - myAtom$.makeFinal(); - expect(myAtom$.final).toBeTrue(); - - /** - * ** Your Turn ** - * What do you think will happen when we try to `.get()` or `.set()` this atom? - */ - // .toThrow() or .not.toThrow()? ↴ - expect(() => myAtom$.get()).not.toThrow(); - expect(() => myAtom$.set(2)).toThrow('cannot set a final derivable'); - - // This behavior is consistent with normal variables created using `const`. - // Alternatively, you can set a last value before setting it to `final`. - // .toThrow() or .not.toThrow()? ↴ - expect(() => myAtom$.setFinal(2)).toThrow('cannot set a final derivable'); - - // There is no way to 'unfinalize' a Derivable, so the only solution to reset is to - // create a whole new Derivable. - myAtom$ = atom(1); - myAtom$.setFinal(2); - expect(myAtom$.final).toBeTrue(); - }); - - it('deriving a `final` Derivable', () => { - const myDerivable$ = myAtom$.derive(v => v + 1); - - const hasReacted = jest.fn(); - myDerivable$.react(hasReacted); - - expect(myDerivable$.final).toBeFalse(); - expect(myDerivable$.connected).toBeTrue(); - - myAtom$.makeFinal(); - - /** - * ** Your Turn ** - * - * What will happen to `myDerivable$` when I change `myAtom$` to be `final`? - */ - expect(myDerivable$.final).toBe(true); - expect(myDerivable$.connected).toBe(false); - - /** - * Derivables that are final (or constant) are no longer tracked. This can save - * a lot of memory and time by cleaning up unused data. Also, when all the variables - * that a Derivable depends on become final, that Derivable itself also becomes final. - * Similarly to `unresolved` and `error`, this chains. - */ - }); - - it('`final` State', () => { - /** A property such as `.final`, similar to variables like `.errored` and `.resolved` - * is useful for checking whenever a Derivable is in a certain state, but these properties - * are just a boolean. This means that these properties cannot be derived and we cannot - * have certain functions execute whenever there is a change in the state. For this reason, - * every Derivable holds an internal state, retrievable using `.getState()` which can be - * derived. TODO: Have a clear place where I explain this! Now I have info up top here too. - * - * We have seen that states (`State`) can be `undefined`, `ErrorWrapper`, - * or any regular type `V`. Other states exist, such as the `MaybeFinalState`. This state can be either - * a normal state `State` or a special `FinalWrapper>` state. Let's see that in action. - */ - expect(myAtom$.getMaybeFinalState()).toBe(1); // `getMaybeFinalState` can return a normal state, which in turn can be any normal type. - - myAtom$.makeFinal(); - - expect(myAtom$.getMaybeFinalState()).toBeInstanceOf(FinalWrapper); // but `getMaybeFinalState` can also return a `FinalWrapper` type. - expect(myAtom$.getState()).toBe(1); // the normal type is still the final it contains. - - // TODO: MAAR JE KAN EEN STATE HELEMAAL NIET DERIVEN! - // Dus dat is allemaal onzin lijkt me....??? Bovendien, kan je normale variabelen niet deriven door het gewoon te - // wrappen in een atom ofzo? - }); - }); - - /** - * It is nice to be able to have a backup plan when an error occurs. - * The `.fallbackTo()` function allows you to specify a default value - * whenever your Derivable gets an error state. - */ - it('Fallback-to', () => { - const myAtom$ = atom(0); - - /** - * ** Your Turn ** - * Use the `.fallbackTo()` method to create a `mySafeAtom$` which - * gets the backup value `3` when `myAtom$` gets an error state. - */ - const mySafeAtom$ = myAtom$.fallbackTo(() => 3); - - expect(myAtom$.getState()).toBe(0); - expect(myAtom$.value).toBe(0); - expect(mySafeAtom$.value).toBe(0); - - myAtom$.unset(); - - expect(myAtom$.getState()).toBe(unresolved); - expect(myAtom$.value).toBeUndefined(); - expect(mySafeAtom$.value).toBe(3); - }); - - it('TEMP Flat-map', () => { - // const myAtom$ = atom(0); - // const mapping = (v: any) => atom(v); - // Sometimes you use `map()`, but the result within the `map()` is also a Derivable. - // The result would here be a `Derivable>` (hover over `derive` to see this). - // You would have to use `.get()` to go back to a single Derivable. Similarly how `flatMap` can - // reduce lists of lists to a single list, it can help reduce Derivables of Derivables to a - // single Derivable. If you have something like this: - // let myAtom$$ = myAtom$.map(n => mapping(n)).derive(v => v.get()); - // You can now rewrite it to this: - // myAtom$$ = myAtom$.flatMap(n => mapping(n)); - // It only results in slightly shorter code. - // TODO: right? - }); -}); - -/** - * !! Final States; (finalGetter, finalMethod, getMaybeFinalState, FinalWrapper, MaybeFinalState, _isFinal, makeFinal, markFinal, .final, .finalized, setFinal...) - * ? Lens; (libs/sherlock/src/lib/derivable/lens.ts) - map die twee kanten op kan gaan. Maar een map kan dat al? Maar hier kan - * je dat los definieren! Je kan gewoon `lens` ipv `var.lens`. Zelden dat je dit gebruikt. Output is een Derivable though. - * x Lift; (libs/sherlock-utils/src/lib/lift.ts) - * !! Peek; (libs/sherlock-utils/src/lib/peek.ts) - dan track je niet. In een derivable, deze tracked hij dan niet (ipv .get() waar het wel getracked wordt) - * x Template; (libs/sherlock-utils/src/lib/template.ts) - to make a string using a template literal. (Uses unwrap!!) - * / Factory; (libs/sherlock/src/lib/derivable/factories.ts) - simply contains functions to create objects, namely - * lens; atom; constant; derive. - * !! Flat-map; (libs/sherlock/src/lib/derivable/mixins/flat-map.ts) - ??? - * array: nested arrays naar array - * Derivable: gooit er derive.get() achteraan? - * Derivable (input van inputveld). Flatmap geeft Derivable terug. Derivable.flatmap() returned misschien - * Derivable, returned dan de number. flatMap is een `derive`, maar wat hij returned haalt hij uit de Derivable. - * ofzoiets. Maakt code korter. - * !! Fallback-to; - op een derivable. Als een atom `unresolved` is, dan fallt het back to this value. Ofwel, initial value, maar - * ook als hij later unresolved wordt, dan wordt hij dit (vaak wel initial value). - * !! Take - react options gebruiken buiten react. In een derivable chain, halverwege die options gebruiken. - * e.g. (from)Promise. Zodra die een waarde aanneemt kan hij niet meer veranderen. - * Let FromPromise, FromObservable, FromEventPattern ook uit (in utils?), ToPromise, ToObservable, in praktijk ook handig. - * FromEventPattern (haily mary, als alles niet werkt, dan dit doen). - */ diff --git a/generated_solution/8 - utils.test.ts b/generated_solution/8 - utils.test.ts deleted file mode 100644 index 4fda7ad..0000000 --- a/generated_solution/8 - utils.test.ts +++ /dev/null @@ -1,382 +0,0 @@ -import { atom, derive } from '@skunkteam/sherlock'; -import { lift, pairwise, peek, scan, struct } from '@skunkteam/sherlock-utils'; - -/** - * ** Your Turn ** - * - * If you see this variable, you should do something about it. :-) - */ -export const __YOUR_TURN__ = {} as any; - -// Silence TypeScript's import not used errors. -expect(pairwise).toBe(pairwise); -expect(scan).toBe(scan); -expect(struct).toBe(struct); -expect(peek).toBe(peek); -expect(lift).toBe(lift); - -/** - * In the `sherlock-utils` lib, there are a couple of functions that can combine - * multiple values of a single `Derivable` or combine multiple `Derivable`s into - * one. We will show a couple of those here. - */ -describe('utils', () => { - /** - * As the name suggests, `pairwise()` will call the given function with both - * the current and the previous state. - * - * *Note: functions like `pairwise` and `scan` can be used with any callback, - * so it can be used both in a `.derive()` step and in a `.react()`* - */ - it('pairwise', () => { - const myCounter$ = atom(1); - const reactSpy = jest.fn(); - - /** - * ** Your Turn ** - * - * Now, use `pairwise()` to subtract the previous value from the - * current. - * - * *Hint: check the overloads of pairwise if you're struggling with - * `oldValue`.* - */ - myCounter$.derive(pairwise((newVal, oldVal) => newVal - oldVal, 0)).react(reactSpy); - // myCounter$.derive(pairwise((newVal, oldVal) => (oldVal ? newVal - oldVal : newVal))).react(reactSpy); // OR: alternatively. - - expect(reactSpy).toHaveBeenCalledTimes(1); - expect(reactSpy).toHaveBeenLastCalledWith(1, expect.toBeFunction()); - - myCounter$.set(3); - - expect(reactSpy).toHaveBeenCalledTimes(2); - expect(reactSpy).toHaveBeenLastCalledWith(2, expect.toBeFunction()); // 3 (current value of `myCounter$`) - 1 (previous value of `myCounter$`) - - myCounter$.set(10); - - expect(reactSpy).toHaveBeenCalledTimes(3); - expect(reactSpy).toHaveBeenLastCalledWith(7, expect.toBeFunction()); // 10 (current value of `myCounter$`) - 3 (previous value of `myCounter$`) - }); - - /** - * `scan()` is the `Derivable` version of `Array.prototype.reduce()`. It will be - * called with the current state and the last emitted value. - * - * (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce) - * - * *Note: as with `pairwise()` this is useable in both a `.derive()` and - * `.react()` method* - */ - it('scan', () => { - const myCounter$ = atom(1); - const reactSpy = jest.fn(); - - /** - * ** Your Turn ** - * - * Now, use `scan()` to subtract the previous value from the - * current. - * - * Note that `scan()` must return the same type as it gets as input. This is required - * as this returned value is also used for the accumulator (`acc`) value for the next call. - * This `acc` parameter of `scan()` is the last returned value, not the last value - * of `myCounter$`, as is the case with `pairwise()`. - */ - myCounter$.derive(scan((acc, val) => val - acc, 0)).react(reactSpy); - - expect(reactSpy).toHaveBeenCalledTimes(1); - expect(reactSpy).toHaveBeenLastCalledWith(1, expect.toBeFunction()); - - myCounter$.set(3); - - expect(reactSpy).toHaveBeenCalledTimes(2); - expect(reactSpy).toHaveBeenLastCalledWith(2, expect.toBeFunction()); // 3 (current value of `myCounter$`) - 1 (previous returned value) - - myCounter$.set(10); - - expect(reactSpy).toHaveBeenCalledTimes(3); - expect(reactSpy).toHaveBeenLastCalledWith(8, expect.toBeFunction()); // 10 (current value of `myCounter$`) - 2 (previous returned value) - }); - - it('`pairwise()` on normal arrays', () => { - // Functions like `pairwise()` and `scan()` work on normal lists too. They are often - // used in combination with `map()` and `filter()`. - const myList = [1, 2, 3, 5, 10]; - let myList2: number[]; - - /** - * ** Your Turn ** - * - * Use a `pairwise()` combined with a `.map()` on `myList` - * to subtract the previous value from the current. - * - * Hint: do not use a lambda function! - */ - myList2 = myList.map(pairwise((newV, oldV) => newV - oldV, 0)); - expect(myList2).toMatchObject([1, 1, 1, 2, 5]); - - // However, we should be careful with this, as this does not always behave as intended. - myList2 = myList.map(v => pairwise((newV, oldV) => newV - oldV, 0)(v)); // copy the same implementation here - expect(myList2).toMatchObject([1, 2, 3, 5, 10]); - - // Even if we are more clear about what we pass, this unintended behavior does not go away. - myList2 = myList.map((v, _, _2) => pairwise((newV, oldV) => newV - oldV, 0)(v)); // copy the same implementation here - expect(myList2).toMatchObject([1, 2, 3, 5, 10]); - - // `pairwise()` keeps track of the previous value under the hood. Using a lambda of - // the form `v => pairwise(...)(v)` would create a new `pairwise` function every call, - // essentially resetting the previous value every call. And resetting the previous value - // to 0 causes the input to stay the same (after all: x - 0 = x). - // Other than by not using a lambda function, we can fix this by - // saving the `pairwise` in a variable and reusing it for every call. - - let f = pairwise((newV, oldV) => newV - oldV, 0); - myList2 = myList.map(v => f(v)); - expect(myList2).toMatchObject([1, 1, 1, 2, 5]); - - // To get more insight in the `pairwise()` function, you can call it - // manually. Here, we show what happens under the hood. - - f = pairwise((newV, oldV) => newV - oldV, 0); - - myList2 = []; - myList2[0] = f(myList[0]); // f is newly created with `init = 0`, so applies `1 - 0`. - myList2[1] = f(myList[1]); // f has saved `1` internally, so applies `2 - 1`. - myList2[2] = f(myList[2]); // f has saved `2` internally, so applies `3 - 2`. - myList2[3] = f(myList[3]); // f has saved `3` internally, so applies `5 - 3`. - myList2[4] = f(myList[4]); // f has saved `5` internally, so applies `10 - 5`. - - expect(myList2).toMatchObject([1, 1, 1, 2, 5]); - // This also works for functions other than `.map()`, such as `.filter()`. - - /** ** Your Turn ** - * Use `pairwise()` to filter out all values which produce `1` when subtracted - * with their previous value. - */ - myList2 = myList.filter(pairwise((newV, oldV) => newV - oldV === 1, 0)); - expect(myList2).toMatchObject([1, 2, 3]); - }); - - it('`scan()` on normal arrays', () => { - // As with `pairwise()` in the last test, `scan()` can be used with arrays too. - const myList = [1, 2, 3, 5, 10]; - let myList2: number[]; - - /** - * ** Your Turn ** - * - * Use a `scan()` combined with a `map` on `myList` - * to subtract the previous value from the current. - */ - - let f: (v: number) => number = scan((acc, val) => val - acc, 0); - myList2 = myList.map(f); - - expect(myList2).toMatchObject([1, 1, 2, 3, 7]); - - // again, it is useful to consider what happens internally. - f(7); // resets the internal `acc` value to 0, as the current `acc` value was 7, and 7-7 = 0. - - myList2 = []; - myList2[0] = f(myList[0]); // 1 :: `f` is newly created with `init = 0`, so applies `1 - 0 = 1`. - myList2[1] = f(myList[1]); // 1 :: `f` has saved the result `1` internally, so applies `2 - 1 = 1`. - myList2[2] = f(myList[2]); // 2 :: `f` has saved the result `1` internally, so applies `3 - 1 = 2`. - myList2[3] = f(myList[3]); // 3 :: `f` has saved the result `2` internally, so applies `5 - 2 = 3`. - myList2[4] = f(myList[4]); // 7 :: `f` has saved the result `3` internally, so applies `10 - 3 = 7`. - - expect(myList2).toMatchObject([1, 1, 2, 3, 7]); - - // This also works for functions other than `map()`, such as `filter()`. - // Use `scan()` to filter out all values from `myList` which produce a value - // of 8 or higher when added with the previous result. In other words, it should - // go through `myList` and add the values producing: (1), (1+2), (1+2+3), (1+2+3+5), - // (1+2+3+5+10), and since this sum only prouces a value higher than 8 when the - // values `5` and `10` are added, the result should be `[5,10]`. - - f = scan((acc, val) => val + acc, 0); - myList2 = myList.filter(v => f(v) >= 8); - expect(myList2).toMatchObject([5, 10]); - }); - - it('pairwise - BONUS', () => { - const myCounter$ = atom(1); - let reactSpy = jest.fn(); - - /** - * ** Your Turn ** - * ** BONUS ** - * - * Now, use `pairwise()` directly in `.react()`. Implement the same - * derivation as before: subtract the previous value from the current. - */ - reactSpy = jest.fn(pairwise((newV, oldV) => newV - oldV, 0)); - myCounter$.react(reactSpy); - - expect(reactSpy).toHaveLastReturnedWith(1); - - myCounter$.set(3); - - expect(reactSpy).toHaveLastReturnedWith(2); - - myCounter$.set(10); - - expect(reactSpy).toHaveLastReturnedWith(7); - }); - - it('scan - BONUS', () => { - const myCounter$ = atom(1); - let reactSpy = jest.fn(); - - /** - * ** Your Turn ** - * ** BONUS ** - * - * Now, use `scan()` directly in `.react()`. Implement the same - * derivation as before: subtract all the emitted values. - */ - - reactSpy = jest.fn(scan((acc, val) => val - acc, 0)); - myCounter$.react(reactSpy); - - expect(reactSpy).toHaveLastReturnedWith(1); - - myCounter$.set(3); - - expect(reactSpy).toHaveLastReturnedWith(2); - - myCounter$.set(10); - - expect(reactSpy).toHaveLastReturnedWith(8); - }); - - /** - * A `struct()` can combine an Object/Array of `Derivable`s into one - * `Derivable`, that contains the values of that `Derivable`. - * - * The Object/Array that is in the output of `struct()` will have the same - * structure as the original Object/Array. - * - * This is best explained in practice. - */ - it('struct', () => { - const allMyAtoms = { - regularProp: 'prop', - string: atom('my string'), - number: atom(1), - sub: { - string: atom('my substring'), - }, - }; - - const myOneAtom$ = struct(allMyAtoms); - - expect(myOneAtom$.get()).toEqual({ - regularProp: 'prop', - string: 'my string', - number: 1, - sub: { - string: 'my substring', - }, - }); - - // Note: we change the original object, not the struct. - allMyAtoms.regularProp = 'new value'; - allMyAtoms.sub.string.set('my new substring'); - - /** - * ** Your Turn ** - * - * Now have a look at the properties of `myOneAtom$`. Is this what you - * expect? - */ - - expect(myOneAtom$.get()).toEqual({ - regularProp: 'new value', - string: 'my string', - number: 1, - sub: { - string: 'my new substring', - }, - }); - - }); - - describe('lift()', () => { - /** - * Derivables can feel like a language build on top of Typescript. Sometimes - * you might want to use normal objects and functions and not have to rewrite - * your code. - * In other words, just like keywords like `atom(V)` lifts a variable V to the higher - * level of Derivables, the `lift(F)` keyword lifts a function `F` to the higher - * level of Derivables. - */ - it('example', () => { - // Example: after years of effort, Bob finally finished his oh-so complicated function: - const isEvenNumber = (v: number) => v % 2 == 0; - - // Rewriting this function to work with derivables would now be a waste of time. - /** - * ** Your Turn ** - * Use the `lift()` function to change `isEvenNumber` to work on Derivables instead. - * In other words: the new function should take a `Derivable` (or more specifically: - * an `Unwrappable`) and return a `Derivable`. - */ - const isEvenDerivable = lift(isEvenNumber); - - expect(isEvenNumber(2)).toBe(true); - expect(isEvenNumber(13)).toBe(false); - expect(isEvenDerivable(atom(2)).get()).toBe(true); - expect(isEvenDerivable(atom(13)).get()).toBe(false); - }); - - it('`lift()` as alternative to `.map()`', () => { - // In tutorial 7, we saw `.map()` used in the following context: - const addOne = jest.fn((v: number) => v + 1); - const myAtom$ = atom(1); - - let myMappedDerivable$ = myAtom$.map(addOne); - - expect(myMappedDerivable$.value).toBe(2); - - /** - * ** Your Turn ** - * Now, use `lift()` as alternative to `.map()`. - */ - myMappedDerivable$ = lift(addOne)(myAtom$); - - expect(myMappedDerivable$.value).toBe(2); - }); - }); - - /** - * Sometimes you want to use `derive` but still want to keep certain - * variables in it untracked. In such cases, you can use `peek()`. - */ - it('`peek()`', () => { - const myTrackedAtom$ = atom(1); - const myUntrackedAtom$ = atom(2); - - /** - * ** Your Turn ** - * Use `peek()` to get the value of `myUntrackedAtom$` and add it to the - * value of `myTrackedAtom$`, which should be tracked. - */ - const reactor = jest.fn(v => v); - derive(() => myTrackedAtom$.get() + peek(myUntrackedAtom$)).react(reactor); - - expect(reactor).toHaveBeenCalledOnce(); - expect(reactor).toHaveLastReturnedWith(3); - - myTrackedAtom$.set(2); - expect(reactor).toHaveBeenCalledTimes(2); - expect(reactor).toHaveLastReturnedWith(4); - - myUntrackedAtom$.set(3); - expect(reactor).toHaveBeenCalledTimes(2); - expect(reactor).toHaveLastReturnedWith(4); - - myTrackedAtom$.set(3); - expect(reactor).toHaveBeenCalledTimes(3); - expect(reactor).toHaveLastReturnedWith(6); - }); -}); diff --git a/generated_tutorial/6 - errors.test.ts b/generated_tutorial/6 - errors.test.ts deleted file mode 100644 index a040ed0..0000000 --- a/generated_tutorial/6 - errors.test.ts +++ /dev/null @@ -1,263 +0,0 @@ -import { atom, DerivableAtom, error, FinalWrapper, unresolved } from '@skunkteam/sherlock'; - -/** - * ** Your Turn ** - * - * If you see this variable, you should do something about it. :-) - */ -export const __YOUR_TURN__ = {} as any; - -// Silence TypeScript's import not used errors. -expect(FinalWrapper).toBe(FinalWrapper); - -// In libs/sherlock/src/lib/interfaces.ts:289, the basic states a Derivable can have are shown. -// > `export type State = V | unresolved | ErrorWrapper;` -// A state can be either any type `V` (`number`, `string`, etc.), `unresolved` as we saw in the -// previous tutorial, or `ErrorWrapper`. This last state is explained here. -describe.skip('errors', () => { - let myAtom$: DerivableAtom; - - beforeEach(() => { - myAtom$ = atom(1); - }); - - it('basic errors', () => { - // The `errored` property shows whether the last statement resulted in an error. - expect(myAtom$.errored).toBe(false); - expect(myAtom$.error).toBeUndefined; // by default, the `error` property is undefined. - expect(myAtom$.getState()).toBe(1); // as explained above, any type can be a state - - // We can set errors using the `setError()` function. - myAtom$.setError('my Error'); - - expect(myAtom$.errored).toBe(true); - expect(myAtom$.error).toBe('my Error'); - - // The `ErrorWrapper` state only holds an error string. The `error()` function returns - // such an `ErrorWrapper` which we can use to compare. - expect(myAtom$.getState()).toMatchObject(error('my Error')); - - // expect(myAtom$.get).toThrow("Cannot read properties of undefined (reading 'getState')"); - // TODO: WHAT - normally this works, but internal JEST just fucks with me....? - - // Calling `get()` on `myAtom$` gives the error. - expect(() => myAtom$.get()).toThrow('my Error'); - expect(myAtom$.errored).toBe(true); - - // ** __YOUR_TURN__ ** - // What will happen if you try to call `set()` on `myAtom$`? - // `.toThrow()` or `.not.toThrow()`? ↴ - expect(() => myAtom$.set(2)) /* __YOUR_TURN__ */; - expect(myAtom$.errored).toBe(__YOUR_TURN__); - - // Interestingly, calling `set()` does not throw an error. In fact, it removes the error state - // altogether. This means we can call `get()` again. - expect(() => myAtom$.get()).not.toThrow(); - }); - - it('deriving an error', () => { - const myDerivable$ = myAtom$.derive(v => v + 1); - - // If `myAtom$` suddenly errors... - myAtom$.setError('division by zero'); - - // ...what happens to `myDerivable$`? - expect(myDerivable$.errored).toBe(__YOUR_TURN__); - - // If any Derivable tries to derive from an atom in an error state, - // this Derivable will itself throw an error too. This makes sense, - // given that it cannot obtain the value it needs anymore. - }); - - it('reacting to an error', () => { - // Without a reactor, setting an error to an Atom does not throw an error. - expect(() => myAtom$.setError('my Error')).not.toThrow(); - myAtom$.set(1); - - // Now we set a reactor to `myAtom$`. This reactor does not use the value of `myAtom$`. - const reactor = jest.fn(); - myAtom$.react(reactor); - - // ** __YOUR_TURN__ ** - // Will an error be thrown when `myAtom$` is now set to an error state? - // `.toThrow()` or `.not.toThrow()`? ↴ - expect(() => myAtom$.setError('my Error')) /* __YOUR_TURN__ */; - - // Reacting to a Derivable that throws an error will make the reactor throw as well. - // Because the reactor will usually fire when it gets connected, it also throws when - // you try to connect it after the error has already been set. - - myAtom$ = atom(1); - myAtom$.setError('my second Error'); - - // ** __YOUR_TURN__ ** - // Will an error be thrown when you use `skipFirst`? - // `.toThrow()` or `.not.toThrow()`? ↴ - expect(() => myAtom$.react(reactor, { skipFirst: true })) /* __YOUR_TURN__ */; - - // And will an error be thrown when `from = false`? - // `.toThrow()` or `.not.toThrow()`? ↴ - expect(() => myAtom$.react(reactor, { from: false })) /* __YOUR_TURN__ */; - - // When `from = false`, the reactor is disconnected, preventing the error message from entering. - // `skipFirst`, on the other hand, does allow the error in, but does not trigger an update. - }); - - /** - * Similarly to `constants` which we'll explain in tutorial 7, - * you might want to specify that a variable cannot be updated. - * This can be useful for the programmers themselves, to not - * accidentally update the variable, but it can also be useful for - * optimization. You can do this using the `final` concept. - */ - describe.skip('TEMP `final`', () => { - let myAtom$ = atom(1); - - beforeEach(() => { - myAtom$ = atom(1); - }); - - it('`final` basics', () => { - // Every atom has a `final` property. - expect(myAtom$.final).toBeFalse(); - - // You can make an atom final using the `.makeFinal()` function. - myAtom$.makeFinal(); - expect(myAtom$.final).toBeTrue(); - - /** - * ** Your Turn ** - * What do you think will happen when we try to `.get()` or `.set()` this atom? - */ - // .toThrow() or .not.toThrow()? ↴ - expect(() => myAtom$.get()) /*__YOUR_TURN__*/; - expect(() => myAtom$.set(2)) /*__YOUR_TURN__*/; - - // This behavior is consistent with normal variables created using `const`. - // Alternatively, you can set a last value before setting it to `final`. - // .toThrow() or .not.toThrow()? ↴ - expect(() => myAtom$.setFinal(2)) /*__YOUR_TURN__*/; - - // There is no way to 'unfinalize' a Derivable, so the only solution to reset is to - // create a whole new Derivable. - myAtom$ = atom(1); - myAtom$.setFinal(2); - expect(myAtom$.final).toBeTrue(); - }); - - it('deriving a `final` Derivable', () => { - const myDerivable$ = myAtom$.derive(v => v + 1); - - const hasReacted = jest.fn(); - myDerivable$.react(hasReacted); - - expect(myDerivable$.final).toBeFalse(); - expect(myDerivable$.connected).toBeTrue(); - - myAtom$.makeFinal(); - - /** - * ** Your Turn ** - * - * What will happen to `myDerivable$` when I change `myAtom$` to be `final`? - */ - expect(myDerivable$.final).toBe(__YOUR_TURN__); - expect(myDerivable$.connected).toBe(__YOUR_TURN__); - - /** - * Derivables that are final (or constant) are no longer tracked. This can save - * a lot of memory and time by cleaning up unused data. Also, when all the variables - * that a Derivable depends on become final, that Derivable itself also becomes final. - * Similarly to `unresolved` and `error`, this chains. - */ - }); - - it('`final` State', () => { - /** A property such as `.final`, similar to variables like `.errored` and `.resolved` - * is useful for checking whenever a Derivable is in a certain state, but these properties - * are just a boolean. This means that these properties cannot be derived and we cannot - * have certain functions execute whenever there is a change in the state. For this reason, - * every Derivable holds an internal state, retrievable using `.getState()` which can be - * derived. TODO: Have a clear place where I explain this! Now I have info up top here too. - * - * We have seen that states (`State`) can be `undefined`, `ErrorWrapper`, - * or any regular type `V`. Other states exist, such as the `MaybeFinalState`. This state can be either - * a normal state `State` or a special `FinalWrapper>` state. Let's see that in action. - */ - expect(myAtom$.getMaybeFinalState()).toBe(1); // `getMaybeFinalState` can return a normal state, which in turn can be any normal type. - - myAtom$.makeFinal(); - - expect(myAtom$.getMaybeFinalState()).toBeInstanceOf(FinalWrapper); // but `getMaybeFinalState` can also return a `FinalWrapper` type. - expect(myAtom$.getState()).toBe(1); // the normal type is still the final it contains. - - // TODO: MAAR JE KAN EEN STATE HELEMAAL NIET DERIVEN! - // Dus dat is allemaal onzin lijkt me....??? Bovendien, kan je normale variabelen niet deriven door het gewoon te - // wrappen in een atom ofzo? - }); - }); - - /** - * It is nice to be able to have a backup plan when an error occurs. - * The `.fallbackTo()` function allows you to specify a default value - * whenever your Derivable gets an error state. - */ - it('Fallback-to', () => { - const myAtom$ = atom(0); - - /** - * ** Your Turn ** - * Use the `.fallbackTo()` method to create a `mySafeAtom$` which - * gets the backup value `3` when `myAtom$` gets an error state. - */ - // const mySafeAtom$ = myAtom$.fallbackTo(__YOUR_TURN__); - - expect(myAtom$.getState()).toBe(0); - expect(myAtom$.value).toBe(0); - expect(mySafeAtom$.value).toBe(0); - - myAtom$.unset(); - - expect(myAtom$.getState()).toBe(unresolved); - expect(myAtom$.value).toBeUndefined(); - expect(mySafeAtom$.value).toBe(3); - }); - - it('TEMP Flat-map', () => { - // const myAtom$ = atom(0); - // const mapping = (v: any) => atom(v); - // Sometimes you use `map()`, but the result within the `map()` is also a Derivable. - // The result would here be a `Derivable>` (hover over `derive` to see this). - // You would have to use `.get()` to go back to a single Derivable. Similarly how `flatMap` can - // reduce lists of lists to a single list, it can help reduce Derivables of Derivables to a - // single Derivable. If you have something like this: - // let myAtom$$ = myAtom$.map(n => mapping(n)).derive(v => v.get()); - // You can now rewrite it to this: - // myAtom$$ = myAtom$.flatMap(n => mapping(n)); - // It only results in slightly shorter code. - // TODO: right? - }); -}); - -/** - * !! Final States; (finalGetter, finalMethod, getMaybeFinalState, FinalWrapper, MaybeFinalState, _isFinal, makeFinal, markFinal, .final, .finalized, setFinal...) - * ? Lens; (libs/sherlock/src/lib/derivable/lens.ts) - map die twee kanten op kan gaan. Maar een map kan dat al? Maar hier kan - * je dat los definieren! Je kan gewoon `lens` ipv `var.lens`. Zelden dat je dit gebruikt. Output is een Derivable though. - * x Lift; (libs/sherlock-utils/src/lib/lift.ts) - * !! Peek; (libs/sherlock-utils/src/lib/peek.ts) - dan track je niet. In een derivable, deze tracked hij dan niet (ipv .get() waar het wel getracked wordt) - * x Template; (libs/sherlock-utils/src/lib/template.ts) - to make a string using a template literal. (Uses unwrap!!) - * / Factory; (libs/sherlock/src/lib/derivable/factories.ts) - simply contains functions to create objects, namely - * lens; atom; constant; derive. - * !! Flat-map; (libs/sherlock/src/lib/derivable/mixins/flat-map.ts) - ??? - * array: nested arrays naar array - * Derivable: gooit er derive.get() achteraan? - * Derivable (input van inputveld). Flatmap geeft Derivable terug. Derivable.flatmap() returned misschien - * Derivable, returned dan de number. flatMap is een `derive`, maar wat hij returned haalt hij uit de Derivable. - * ofzoiets. Maakt code korter. - * !! Fallback-to; - op een derivable. Als een atom `unresolved` is, dan fallt het back to this value. Ofwel, initial value, maar - * ook als hij later unresolved wordt, dan wordt hij dit (vaak wel initial value). - * !! Take - react options gebruiken buiten react. In een derivable chain, halverwege die options gebruiken. - * e.g. (from)Promise. Zodra die een waarde aanneemt kan hij niet meer veranderen. - * Let FromPromise, FromObservable, FromEventPattern ook uit (in utils?), ToPromise, ToObservable, in praktijk ook handig. - * FromEventPattern (haily mary, als alles niet werkt, dan dit doen). - */ diff --git a/generated_tutorial/8 - utils.test.ts b/generated_tutorial/8 - utils.test.ts deleted file mode 100644 index 0c926bb..0000000 --- a/generated_tutorial/8 - utils.test.ts +++ /dev/null @@ -1,381 +0,0 @@ -import { atom, derive } from '@skunkteam/sherlock'; -import { lift, pairwise, peek, scan, struct } from '@skunkteam/sherlock-utils'; - -/** - * ** Your Turn ** - * - * If you see this variable, you should do something about it. :-) - */ -export const __YOUR_TURN__ = {} as any; - -// Silence TypeScript's import not used errors. -expect(pairwise).toBe(pairwise); -expect(scan).toBe(scan); -expect(struct).toBe(struct); -expect(peek).toBe(peek); -expect(lift).toBe(lift); - -/** - * In the `sherlock-utils` lib, there are a couple of functions that can combine - * multiple values of a single `Derivable` or combine multiple `Derivable`s into - * one. We will show a couple of those here. - */ -describe.skip('utils', () => { - /** - * As the name suggests, `pairwise()` will call the given function with both - * the current and the previous state. - * - * *Note: functions like `pairwise` and `scan` can be used with any callback, - * so it can be used both in a `.derive()` step and in a `.react()`* - */ - it('pairwise', () => { - const myCounter$ = atom(1); - const reactSpy = jest.fn(); - - /** - * ** Your Turn ** - * - * Now, use `pairwise()` to subtract the previous value from the - * current. - * - * *Hint: check the overloads of pairwise if you're struggling with - * `oldValue`.* - */ - myCounter$.derive(__YOUR_TURN__).react(reactSpy); - - expect(reactSpy).toHaveBeenCalledTimes(1); - expect(reactSpy).toHaveBeenLastCalledWith(1, expect.toBeFunction()); - - myCounter$.set(3); - - expect(reactSpy).toHaveBeenCalledTimes(2); - expect(reactSpy).toHaveBeenLastCalledWith(2, expect.toBeFunction()); // 3 (current value of `myCounter$`) - 1 (previous value of `myCounter$`) - - myCounter$.set(10); - - expect(reactSpy).toHaveBeenCalledTimes(3); - expect(reactSpy).toHaveBeenLastCalledWith(7, expect.toBeFunction()); // 10 (current value of `myCounter$`) - 3 (previous value of `myCounter$`) - }); - - /** - * `scan()` is the `Derivable` version of `Array.prototype.reduce()`. It will be - * called with the current state and the last emitted value. - * - * (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce) - * - * *Note: as with `pairwise()` this is useable in both a `.derive()` and - * `.react()` method* - */ - it('scan', () => { - const myCounter$ = atom(1); - const reactSpy = jest.fn(); - - /** - * ** Your Turn ** - * - * Now, use `scan()` to subtract the previous value from the - * current. - * - * Note that `scan()` must return the same type as it gets as input. This is required - * as this returned value is also used for the accumulator (`acc`) value for the next call. - * This `acc` parameter of `scan()` is the last returned value, not the last value - * of `myCounter$`, as is the case with `pairwise()`. - */ - myCounter$.derive(__YOUR_TURN__).react(reactSpy); - - expect(reactSpy).toHaveBeenCalledTimes(1); - expect(reactSpy).toHaveBeenLastCalledWith(1, expect.toBeFunction()); - - myCounter$.set(3); - - expect(reactSpy).toHaveBeenCalledTimes(2); - expect(reactSpy).toHaveBeenLastCalledWith(2, expect.toBeFunction()); // 3 (current value of `myCounter$`) - 1 (previous returned value) - - myCounter$.set(10); - - expect(reactSpy).toHaveBeenCalledTimes(3); - expect(reactSpy).toHaveBeenLastCalledWith(8, expect.toBeFunction()); // 10 (current value of `myCounter$`) - 2 (previous returned value) - }); - - it('`pairwise()` on normal arrays', () => { - // Functions like `pairwise()` and `scan()` work on normal lists too. They are often - // used in combination with `map()` and `filter()`. - const myList = [1, 2, 3, 5, 10]; - let myList2: number[]; - - /** - * ** Your Turn ** - * - * Use a `pairwise()` combined with a `.map()` on `myList` - * to subtract the previous value from the current. - * - * Hint: do not use a lambda function! - */ - myList2 = myList.map(__YOUR_TURN__); - expect(myList2).toMatchObject([1, 1, 1, 2, 5]); - - // However, we should be careful with this, as this does not always behave as intended. - myList2 = myList.map(v => __YOUR_TURN__(v)); // copy the same implementation here - expect(myList2).toMatchObject([1, 2, 3, 5, 10]); - - // Even if we are more clear about what we pass, this unintended behavior does not go away. - myList2 = myList.map((v, _, _2) => __YOUR_TURN__(v)); // copy the same implementation here - expect(myList2).toMatchObject([1, 2, 3, 5, 10]); - - // `pairwise()` keeps track of the previous value under the hood. Using a lambda of - // the form `v => pairwise(...)(v)` would create a new `pairwise` function every call, - // essentially resetting the previous value every call. And resetting the previous value - // to 0 causes the input to stay the same (after all: x - 0 = x). - // Other than by not using a lambda function, we can fix this by - // saving the `pairwise` in a variable and reusing it for every call. - - let f: (v: number) => number = __YOUR_TURN__; // copy the same implementation here - myList2 = myList.map(v => f(v)); - expect(myList2).toMatchObject([1, 1, 1, 2, 5]); - - // To get more insight in the `pairwise()` function, you can call it - // manually. Here, we show what happens under the hood. - - f = pairwise(__YOUR_TURN__); // copy the same implementation here - - myList2 = []; - myList2[0] = f(myList[0]); // f is newly created with `init = 0`, so applies `1 - 0`. - myList2[1] = f(myList[1]); // f has saved `1` internally, so applies `2 - 1`. - myList2[2] = f(myList[2]); // f has saved `2` internally, so applies `3 - 2`. - myList2[3] = f(myList[3]); // f has saved `3` internally, so applies `5 - 3`. - myList2[4] = f(myList[4]); // f has saved `5` internally, so applies `10 - 5`. - - expect(myList2).toMatchObject([1, 1, 1, 2, 5]); - // This also works for functions other than `.map()`, such as `.filter()`. - - /** ** Your Turn ** - * Use `pairwise()` to filter out all values which produce `1` when subtracted - * with their previous value. - */ - myList2 = myList.filter(__YOUR_TURN__); - expect(myList2).toMatchObject([1, 2, 3]); - }); - - it('`scan()` on normal arrays', () => { - // As with `pairwise()` in the last test, `scan()` can be used with arrays too. - const myList = [1, 2, 3, 5, 10]; - let myList2: number[]; - - /** - * ** Your Turn ** - * - * Use a `scan()` combined with a `map` on `myList` - * to subtract the previous value from the current. - */ - - let f: (v: number) => number = scan(__YOUR_TURN__); - myList2 = myList.map(f); - - expect(myList2).toMatchObject([1, 1, 2, 3, 7]); - - // again, it is useful to consider what happens internally. - f(7); // resets the internal `acc` value to 0, as the current `acc` value was 7, and 7-7 = 0. - - myList2 = []; - myList2[0] = f(myList[0]); // 1 :: `f` is newly created with `init = 0`, so applies `1 - 0 = 1`. - myList2[1] = f(myList[1]); // 1 :: `f` has saved the result `1` internally, so applies `2 - 1 = 1`. - myList2[2] = f(myList[2]); // 2 :: `f` has saved the result `1` internally, so applies `3 - 1 = 2`. - myList2[3] = f(myList[3]); // 3 :: `f` has saved the result `2` internally, so applies `5 - 2 = 3`. - myList2[4] = f(myList[4]); // 7 :: `f` has saved the result `3` internally, so applies `10 - 3 = 7`. - - expect(myList2).toMatchObject([1, 1, 2, 3, 7]); - - // This also works for functions other than `map()`, such as `filter()`. - // Use `scan()` to filter out all values from `myList` which produce a value - // of 8 or higher when added with the previous result. In other words, it should - // go through `myList` and add the values producing: (1), (1+2), (1+2+3), (1+2+3+5), - // (1+2+3+5+10), and since this sum only prouces a value higher than 8 when the - // values `5` and `10` are added, the result should be `[5,10]`. - - f = scan(__YOUR_TURN__); - myList2 = myList.filter(__YOUR_TURN__); - expect(myList2).toMatchObject([5, 10]); - }); - - it('pairwise - BONUS', () => { - const myCounter$ = atom(1); - let reactSpy = jest.fn(); - - /** - * ** Your Turn ** - * ** BONUS ** - * - * Now, use `pairwise()` directly in `.react()`. Implement the same - * derivation as before: subtract the previous value from the current. - */ - reactSpy = jest.fn(__YOUR_TURN__); - myCounter$.react(reactSpy); - - expect(reactSpy).toHaveLastReturnedWith(1); - - myCounter$.set(3); - - expect(reactSpy).toHaveLastReturnedWith(2); - - myCounter$.set(10); - - expect(reactSpy).toHaveLastReturnedWith(7); - }); - - it('scan - BONUS', () => { - const myCounter$ = atom(1); - let reactSpy = jest.fn(); - - /** - * ** Your Turn ** - * ** BONUS ** - * - * Now, use `scan()` directly in `.react()`. Implement the same - * derivation as before: subtract all the emitted values. - */ - - reactSpy = jest.fn(__YOUR_TURN__); - myCounter$.react(reactSpy); - - expect(reactSpy).toHaveLastReturnedWith(1); - - myCounter$.set(3); - - expect(reactSpy).toHaveLastReturnedWith(2); - - myCounter$.set(10); - - expect(reactSpy).toHaveLastReturnedWith(8); - }); - - /** - * A `struct()` can combine an Object/Array of `Derivable`s into one - * `Derivable`, that contains the values of that `Derivable`. - * - * The Object/Array that is in the output of `struct()` will have the same - * structure as the original Object/Array. - * - * This is best explained in practice. - */ - it('struct', () => { - const allMyAtoms = { - regularProp: 'prop', - string: atom('my string'), - number: atom(1), - sub: { - string: atom('my substring'), - }, - }; - - const myOneAtom$ = struct(allMyAtoms); - - expect(myOneAtom$.get()).toEqual({ - regularProp: 'prop', - string: 'my string', - number: 1, - sub: { - string: 'my substring', - }, - }); - - // Note: we change the original object, not the struct. - allMyAtoms.regularProp = 'new value'; - allMyAtoms.sub.string.set('my new substring'); - - /** - * ** Your Turn ** - * - * Now have a look at the properties of `myOneAtom$`. Is this what you - * expect? - */ - - expect(myOneAtom$.get()).toEqual({ - regularProp: __YOUR_TURN__, - string: __YOUR_TURN__, - number: __YOUR_TURN__, - sub: { - string: __YOUR_TURN__, - }, - }); - - }); - - describe.skip('lift()', () => { - /** - * Derivables can feel like a language build on top of Typescript. Sometimes - * you might want to use normal objects and functions and not have to rewrite - * your code. - * In other words, just like keywords like `atom(V)` lifts a variable V to the higher - * level of Derivables, the `lift(F)` keyword lifts a function `F` to the higher - * level of Derivables. - */ - it('example', () => { - // Example: after years of effort, Bob finally finished his oh-so complicated function: - const isEvenNumber = (v: number) => v % 2 == 0; - - // Rewriting this function to work with derivables would now be a waste of time. - /** - * ** Your Turn ** - * Use the `lift()` function to change `isEvenNumber` to work on Derivables instead. - * In other words: the new function should take a `Derivable` (or more specifically: - * an `Unwrappable`) and return a `Derivable`. - */ - const isEvenDerivable = __YOUR_TURN__; - - expect(isEvenNumber(2)).toBe(true); - expect(isEvenNumber(13)).toBe(false); - expect(isEvenDerivable(atom(2)).get()).toBe(true); - expect(isEvenDerivable(atom(13)).get()).toBe(false); - }); - - it('`lift()` as alternative to `.map()`', () => { - // In tutorial 7, we saw `.map()` used in the following context: - const addOne = jest.fn((v: number) => v + 1); - const myAtom$ = atom(1); - - let myMappedDerivable$ = myAtom$.map(addOne); - - expect(myMappedDerivable$.value).toBe(2); - - /** - * ** Your Turn ** - * Now, use `lift()` as alternative to `.map()`. - */ - myMappedDerivable$ = __YOUR_TURN__; - - expect(myMappedDerivable$.value).toBe(2); - }); - }); - - /** - * Sometimes you want to use `derive` but still want to keep certain - * variables in it untracked. In such cases, you can use `peek()`. - */ - it('`peek()`', () => { - const myTrackedAtom$ = atom(1); - const myUntrackedAtom$ = atom(2); - - /** - * ** Your Turn ** - * Use `peek()` to get the value of `myUntrackedAtom$` and add it to the - * value of `myTrackedAtom$`, which should be tracked. - */ - const reactor = jest.fn(v => v); - derive(__YOUR_TURN__).react(reactor); - - expect(reactor).toHaveBeenCalledOnce(); - expect(reactor).toHaveLastReturnedWith(3); - - myTrackedAtom$.set(2); - expect(reactor).toHaveBeenCalledTimes(2); - expect(reactor).toHaveLastReturnedWith(4); - - myUntrackedAtom$.set(3); - expect(reactor).toHaveBeenCalledTimes(2); - expect(reactor).toHaveLastReturnedWith(4); - - myTrackedAtom$.set(3); - expect(reactor).toHaveBeenCalledTimes(3); - expect(reactor).toHaveLastReturnedWith(6); - }); -}); diff --git a/generator/1 - intro.test.ts b/generator/1 - intro.test.ts index 23db949..87a3809 100644 --- a/generator/1 - intro.test.ts +++ b/generator/1 - intro.test.ts @@ -1,11 +1,12 @@ import { atom } from '@skunkteam/sherlock'; +// #QUESTION-BLOCK-START /** * ** Your Turn ** * If you see this variable, you should do something about it. :-) */ export const __YOUR_TURN__ = {} as any; - +// #QUESTION-BLOCK-END /** * Welcome to the `@skunkteam/sherlock` tutorial. * @@ -13,7 +14,7 @@ export const __YOUR_TURN__ = {} as any; * to pass. The `expect()`s and basic setup are there, you just need to get it * to work. * - * All specs except the first one are set to `.skip`. Remove this to start on + * All specs are set to `.skip`. Remove this to start on * that part of the tutorial. * * Start the tutorial by running: @@ -25,7 +26,15 @@ export const __YOUR_TURN__ = {} as any; * * *Hint: most methods and functions are fairly well documented in jsDoc, * which is easily accessed through TypeScript* + * + * If you cannot figure it out or are curious to the intended answers, you can + * read the answers in the `solution` folder. */ + +/** + * ** Your Turn ** + * Your first task: remove this `.skip`. + * */ describe('intro', () => { it(` diff --git a/generator/2 - deriving.test.ts b/generator/2 - deriving.test.ts index 149d05b..aba4a4d 100644 --- a/generator/2 - deriving.test.ts +++ b/generator/2 - deriving.test.ts @@ -1,12 +1,12 @@ import { atom, Derivable, derive } from '@skunkteam/sherlock'; +// #QUESTION-BLOCK-START /** * ** Your Turn ** - * * If you see this variable, you should do something about it. :-) */ export const __YOUR_TURN__ = {} as any; - +// #QUESTION-BLOCK-END /** * Any `Derivable` (including `Atom`s) can be used (and/or combined) to create * a derived state. This derived state is in turn a `Derivable`. @@ -74,15 +74,13 @@ describe('deriving', () => { * method on another `Derivable`. */ - // Should return 'Buzz' when `myCounter$` is a multiple of 5 and '' otherwise. - // Should return 'Fizz' when `myCounter$` is a multiple of 3 and '' otherwise. const fizz$: Derivable = myCounter$.derive(__YOUR_TURN__); // #QUESTION - const fizz$: Derivable = myCounter$.derive(v => (v % 3 ? 'Fizz' : '')); // Shorthand for `v % 3 === 0` // #ANSWER + const fizz$: Derivable = myCounter$.derive(v => (v % 3 ? '' : 'Fizz')); // Shorthand for `v % 3 !== 0` // Should return 'Buzz' when `myCounter$` is a multiple of 5 and '' otherwise. const buzz$: Derivable = myCounter$.derive(__YOUR_TURN__); // #QUESTION - const buzz$: Derivable = myCounter$.derive(v => (v % 5 ? 'Buzz' : '')); // #ANSWER + const buzz$: Derivable = myCounter$.derive(v => (v % 5 ? '' : 'Buzz')); const fizzBuzz$: Derivable = derive(__YOUR_TURN__); // #QUESTION // #ANSWER-BLOCK-START @@ -222,7 +220,6 @@ describe('deriving', () => { .and(__YOUR_TURN__) .or(__YOUR_TURN__) as Derivable; // #QUESTION-BLOCK-END - // #ANSWER-BLOCK-START const fizz$ = myCounter$ .derive(count => count % 3) diff --git a/generator/3 - reacting.test.ts b/generator/3 - reacting.test.ts index 3dc7c14..29f6bc3 100644 --- a/generator/3 - reacting.test.ts +++ b/generator/3 - reacting.test.ts @@ -1,24 +1,48 @@ import { atom } from '@skunkteam/sherlock'; +// #QUESTION-BLOCK-START /** * ** Your Turn ** - * * If you see this variable, you should do something about it. :-) */ export const __YOUR_TURN__ = {} as any; - -// FIXME: check my solutions with the actual solutions -/** - * x 1 - * x 2 - * - */ +// #QUESTION-BLOCK-END +// xxx check my solutions with the actual solutions (https://github.com/skunkteam/sherlock/tree/tutorial-solutions/robin/tutorial) // FIXME: remove all TODO: and FIXME: -// FIXME: check whether the generated tutorials and solutions actually work (e.g. are all solutions correct? No weird shenanigans?) -// FIXME: deze file niet linten / builden (voor automatische test). Tutorial ook niet. Maar solutions juist wel! OP EIND. +// xxx check whether the generated tutorials and solutions actually work (e.g. are all solutions correct? No weird shenanigans?) - +// FIXME: ALSO CHECK "Or, alternatively"! +// FIXME: deze file niet linten / builden (voor automatische test). Tutorial ook niet. Maar solutions juist wel! OP EIND. (mag beide wel linten right?) // FIXME: interne review document, mocht ik iets hebben om te laten zien! In Google Drive, zet het erin! -// FIXME: werkt `npm run tutorial` nog??? - +// xxx werkt `npm run tutorial` nog? > Nu wel. +// xxx PETER: "nu je toch met Sherlock bezig bent; zou je voor mij eens kunnen checken of de code voorbeelden in de README +// nog wel kloppen met de huidige API? Ik heb het gevoel dat dat niet zo is; volgens mij is er geen function "derivation()" +// en heet dat nu "derive()" bijvoorbeeld." +// FIXME: OOOOOOH JA, ik had eroverheen gepushed! Dat moet nog een PR met terugwerkende kracht worden... (of commits squashen, en dat ze dan maar de commit moeten reviewen?) +// FIXME: Add FromEventPattern + FromObservable +// xxx fix the generator for code blocks. +// FIXME: now check whether it did not remove excess lines or kept 2 empty lines where it should not. (I think it is good though.) +/** + * x Final States; (finalGetter, finalMethod, getMaybeFinalState, FinalWrapper, MaybeFinalState, _isFinal, makeFinal, markFinal, .final, .finalized, setFinal...) + * ? Lens; (libs/sherlock/src/lib/derivable/lens.ts) - map die twee kanten op kan gaan. Maar een map kan dat al? Maar hier kan + * je dat los definieren! Je kan gewoon `lens` ipv `var.lens`. Zelden dat je dit gebruikt. Output is een Derivable though. + * x Lift; (libs/sherlock-utils/src/lib/lift.ts) + * x Peek; (libs/sherlock-utils/src/lib/peek.ts) - dan track je niet. In een derivable, deze tracked hij dan niet (ipv .get() waar het wel getracked wordt) + * x Template; (libs/sherlock-utils/src/lib/template.ts) - to make a string using a template literal. (Uses unwrap!!) + * / Factory; (libs/sherlock/src/lib/derivable/factories.ts) - simply contains functions to create objects, namely + * lens; atom; constant; derive. + * !! Flat-map; (libs/sherlock/src/lib/derivable/mixins/flat-map.ts) - ??? + * array: nested arrays naar array + * Derivable: gooit er derive.get() achteraan? + * Derivable (input van inputveld). Flatmap geeft Derivable terug. Derivable.flatmap() returned misschien + * Derivable, returned dan de number. flatMap is een `derive`, maar wat hij returned haalt hij uit de Derivable. + * ofzoiets. Maakt code korter. + * x Fallback-to; - op een derivable. Als een atom `unresolved` is, dan fallt het back to this value. Ofwel, initial value, maar + * ook als hij later unresolved wordt, dan wordt hij dit (vaak wel initial value). + * x Take - react options gebruiken buiten react. In een derivable chain, halverwege die options gebruiken. + * -- e.g. (from)Promise. Zodra die een waarde aanneemt kan hij niet meer veranderen. + * Let FromPromise, FromObservable, FromEventPattern ook uit (in utils?), ToPromise, ToObservable, in praktijk ook handig. + * FromEventPattern (haily mary, als alles niet werkt, dan dit doen). + */ /** * In the intro we have seen a basic usage of the `.react()` method. * Let's dive a bit deeper into the details of this method. @@ -158,7 +182,6 @@ describe('reacting', () => { __YOUR_TURN___; }); // #QUESTION-BLOCK-END - // #ANSWER-BLOCK-START myAtom$.react((val, stopper) => { reactor(val); @@ -266,7 +289,13 @@ describe('reacting', () => { * empty. */ string$.react(reactor, __YOUR_TURN__); // #QUESTION - string$.react(reactor, { until: () => !string$.get() }); // #ANSWER + // #ANSWER-BLOCK-START + const stringEmpty = function () { + return !string$.get(); + }; + string$.react(reactor, { until: stringEmpty }); + // string$.react(reactor, { until: () => !string$.get() }); // Or, alternatively, in a single line: + // #ANSWER-BLOCK-END // It should react as usual: string$.set('New value'); @@ -294,7 +323,7 @@ describe('reacting', () => { * the same as above. */ string$.react(reactor, __YOUR_TURN__); // #QUESTION - string$.react(reactor, { until: s => !s.get() }); // #ANSWER + string$.react(reactor, { until: parent$ => !parent$.get() }); // #ANSWER // It should react as usual. string$.set('New value'); @@ -314,26 +343,30 @@ describe('reacting', () => { * Sometimes, the syntax may leave you confused. */ it('syntax issues', () => { - // It looks this will start reacting until `boolean$`s value is false... - let stopper = boolean$.react(reactor, { until: b => !b }); + boolean$.set(true); + // It looks this will keep reacting until `boolean$`s value is set to false... + let stopper = boolean$.react(reactor, { until: b$ => !b$ }); + + boolean$.set(false); - // ...but does it? (Remember: `boolean$` starts out as `false`) + // ...but does it? Is the reactor still connected? expect(boolean$.connected).toBe(__YOUR_TURN__); // #QUESTION expect(boolean$.connected).toBe(true); // #ANSWER - // The `b` it obtains as argument is a `Derivable`. This is a - // reference value which will evaluate to `true` as it is not `undefined`. - // Thus, the negation will evaluate to `false`, independent of the value of - // the boolean. You can get the boolean value our of the `Derivable` using `.get()`: + // The `b$` it obtains as argument is a `Derivable`. This is a + // reference value. Because we apply a negation to this, `b$` is coerced to a + // boolean value, which will evaluate to `true` as `b$` is not `undefined`. + // Thus, the whole expression will evaluate to `false`, independent of the value of + // `boolean$`. Instead, you can get the value out of the `Derivable` using `.get()`: stopper(); // reset - stopper = boolean$.react(reactor, { until: b => !b.get() }); + stopper = boolean$.react(reactor, { until: b$ => !b$.get() }); expect(boolean$.connected).toBe(__YOUR_TURN__); // #QUESTION expect(boolean$.connected).toBe(false); // #ANSWER // You can also return the `Derivable` after appling the negation - // using the method designed for negating Derivables: + // using the method designed for negating the boolean within a `Derivable`: stopper(); - boolean$.react(reactor, { until: b => b.not() }); + boolean$.react(reactor, { until: b$ => b$.not() }); expect(boolean$.connected).toBe(__YOUR_TURN__); // #QUESTION expect(boolean$.connected).toBe(false); // #ANSWER }); @@ -364,7 +397,7 @@ describe('reacting', () => { * *Hint: remember the `.is()` method from tutorial 2?* */ sherlock$.react(reactor, __YOUR_TURN__); // #QUESTION - sherlock$.react(reactor, { from: sherlock$.is('dear') }); // #ANSWER + sherlock$.react(reactor, { from: parent$ => parent$.is('dear') }); // #ANSWER expectReact(0); ['Elementary,', 'my', 'dear', 'Watson'].forEach(txt => sherlock$.set(txt)); @@ -391,7 +424,8 @@ describe('reacting', () => { * Except 4, we don't want to make it too easy now. */ count$.react(reactor, __YOUR_TURN__); // #QUESTION - count$.react(reactor, { when: v => v.get() % 2 === 0 && v.get() !== 4 }); // #ANSWER + count$.react(reactor, { when: parent$ => parent$.get() % 2 === 0 && parent$.get() !== 4 }); // #ANSWER + // count$.react(reactor, { when: parent$ => parent$.derive(value => value % 2 === 0 && value !== 4) }); // Or, alternatively: // #ANSWER expectReact(1, 0); @@ -419,19 +453,20 @@ describe('reacting', () => { * Say you want to react when `count$` is larger than 3. But not the first time... */ count$.react(reactor, __YOUR_TURN__); // #QUESTION - count$.react(reactor, { when: d => d.get() > 3, skipFirst: true }); // #ANSWER + count$.react(reactor, { when: parent$ => parent$.get() > 3, skipFirst: true }); // #ANSWER + // count$.react(reactor, { when: parent$ => parent$.derive(value => value > 3), skipFirst: true }); // Or, alternatively: // #ANSWER expectReact(0); for (let i = 0; i <= 5; i++) { count$.set(i); } - expectReact(1, 5); // it should have skipped the 4 + expectReact(1, 5); // it should have skipped the 4 and only reacted to the 5 for (let i = 0; i <= 5; i++) { count$.set(i); } - expectReact(3, 5); // now it should not have skipped the 4 + expectReact(3, 5); // now it should have reacted to the 4 and 5 (and the 5 of last time) }); /** @@ -443,29 +478,30 @@ describe('reacting', () => { * can be very useful. */ it('reacting `once`', () => { - const finished$ = atom(false); + const count$ = atom(0); /** * ** Your Turn ** * - * Say you want to react when `finished$` is true. It can not finish - * twice. + * Say you want to react when `count$` is higher than 3. But only the first time... * * *Hint: you will need to combine `once` with another option* */ - // finished$.react(reactor, __YOUR_TURN__); // #QUESTION - finished$.react(reactor, { once: true, when: f => f }); // `f => f.get()` is fine as well // #ANSWER + count$.react(reactor, __YOUR_TURN__); // #QUESTION + count$.react(reactor, { once: true, when: parent$ => parent$.get() > 3 }); // #ANSWER + // count$.react(reactor, { once: true, when: parent$ => parent$.derive(value => value > 3) }); // Or, alternatively: // #ANSWER expectReact(0); - // When finished it should react once. - finished$.set(true); - expectReact(1, true); + for (let i = 0; i <= 5; i++) { + count$.set(i); + } + expectReact(1, 4); // it should have only registered the 4 and not the 5 - // After that it should really be finished. :-) - finished$.set(false); - finished$.set(true); - expectReact(1, true); + for (let i = 0; i <= 5; i++) { + count$.set(i); + } + expectReact(1, 4); // and after that, it should really be finished. :-) }); }); @@ -483,39 +519,38 @@ describe('reacting', () => { * and `when` is true or unset. If e.g. `when` evaluates to false, `skipFirst` cannot trigger. */ it('`from` and `until`', () => { - const myAtom$ = atom(0); - myAtom$.react(reactor, { from: v => v.is(3), until: v => v.is(2) }); + const myAtom$ = atom(0); + myAtom$.react(reactor, { from: parent$ => parent$.is(3), until: parent$ => parent$.is(2) }); for (let i = 1; i <= 5; i++) { myAtom$.set(i); } // The reactor starts reacting when `myAtom` gets the value 3, but stops when it gets the value 2. - // But because `myAtom` obtains the value 2 before it obtains 3... + // But because `myAtom$` obtains the value 2 before it obtains 3... // ...how many times was the reactor called, if any? expectReact(__YOUR_TURN__); // #QUESTION - expectReact(3, 5); // `from` evaluates before `until`. // #ANSWER + expectReact(3, 5); // `from` evaluates before `until`, so it reacted to 3, 4 and 5. // #ANSWER }); it('`when` and `skipFirst`', () => { - const myAtom$ = atom(0); + const myAtom$ = atom(0); myAtom$.react(reactor, { when: v => v.is(1), skipFirst: true }); myAtom$.set(1); - // The reactor reacts when `myAtom` is 1 but skips the first number. - // The first number of `myAtom` is 0, its initial number. - // Does the reactor skip the 0 or the 1? + // The reactor reacts when `myAtom$` is 1 but skips the first number. + // `myAtom$` starts out at 0. Does the reactor skip only the 0 or also the 1? expectReact(__YOUR_TURN__); // #QUESTION - expectReact(0); // `skipFirst` triggers only when `when` evaluates to true. // #ANSWER + expectReact(0); // `skipFirst` triggers only when `when` evaluates to true, so it also skips the 1. // #ANSWER }); it('`from`, `until`, `when`, `skipFirst`, and `once`', () => { - const myAtom$ = atom(0); + const myAtom$ = atom(0); myAtom$.react(reactor, { - from: v => v.is(5), - until: v => v.is(1), - when: v => [2, 3, 4].includes(v.get()), + from: parent$ => parent$.is(5), + until: parent$ => parent$.is(1), + when: parent$ => [2, 3, 4].includes(parent$.get()), skipFirst: true, once: true, }); @@ -524,7 +559,7 @@ describe('reacting', () => { myAtom$.set(v); } - // `from` and `until` allow the reactor to respectively start when `myAtom` has value 5, and stop when it has value 1. + // `from` and `until` allow the reactor to respectively start when `myAtom$` has value 5, and stop when it has value 1. // Meanwhile, `when` allows neither of those values and only allows the values 2, 3, and 4. // `skipFirst` and `once` are also added, just to bring the whole group together. // so, how many times is the reactor called, and what was the last argument (if any)? @@ -532,8 +567,8 @@ describe('reacting', () => { // #ANSWER-BLOCK-START expectReact(1, 3); // `from` makes it start at the first `5`. `when` allows the next `4`,`3`, and `2`, but - // `skipFirst` ensures that the first `4` is skipped. `once` then ensures that only the `3` - // reacted to. Before the `until` can trigger from a `1`, the `once` has already stopped it. + // `skipFirst` ensures that the first `4` is skipped. `once` then ensures that only the `3` is + // reacted to. Before the `until` can trigger from a `1`, the `once` has already stopped the reactor. // #ANSWER-BLOCK-END }); }); @@ -544,18 +579,19 @@ describe('reacting', () => { /** * ** Your Turn ** * - * `connected$` indicates the current connection status: + * `connected$` indicates the current connection status. It is one of: * > 'connected'; * > 'disconnected'; * > 'standby'. * * We want our reactor to trigger once, when the device is not connected, - * which means it is either `standby` or `disconnected` (eg for cleanup). + * (`standby` or `disconnected`), e.g. for cleanup. However, we do not want + * it to trigger right away, even though we start at `disconnected`. * - * This should be possible with three simple ReactorOptions + * This should be possible with three simple ReactorOptions. */ connected$.react(reactor, __YOUR_TURN__); // #QUESTION - connected$.react(reactor, { when: s => s.is('connected').not(), skipFirst: true, once: true }); // #ANSWER + connected$.react(reactor, { when: parent$ => parent$.is('connected').not(), skipFirst: true, once: true }); // #ANSWER // It starts as 'disconnected' expectReact(0); diff --git a/generator/4 - inner workings.test.ts b/generator/4 - inner workings.test.ts index 05188ea..637695f 100644 --- a/generator/4 - inner workings.test.ts +++ b/generator/4 - inner workings.test.ts @@ -1,13 +1,13 @@ import { atom } from '@skunkteam/sherlock'; import { Seq } from 'immutable'; +// #QUESTION-BLOCK-START /** * ** Your Turn ** - * * If you see this variable, you should do something about it. :-) */ export const __YOUR_TURN__ = {} as any; - +// #QUESTION-BLOCK-END /** * Time to dive a bit deeper into the inner workings of `@skunkteam/sherlock`. */ @@ -89,6 +89,7 @@ describe('inner workings', () => { // #ANSWER-BLOCK-START expect(reacted).toHaveBeenCalledTimes(3); expect(reacted).toHaveBeenLastCalledWith('two', expect.toBeFunction()); + // As it got a different value (`two` instead of `2`), it triggered. // #ANSWER-BLOCK-END }); @@ -111,7 +112,7 @@ describe('inner workings', () => { * not called `.get()` on that new `Derivable`. * * How many times do you think the `hasDerived` function has been - * called? 0 is also an option of course. + * called? */ // Well, what do you expect? @@ -173,7 +174,7 @@ describe('inner workings', () => { myDerivation$.get(); expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION - expect(hasDerived).toHaveBeenCalledTimes(1); // no update because someone is reacting, and there has been no update in value. // #ANSWER + expect(hasDerived).toHaveBeenCalledTimes(1); // no update: someone is reacting, and there has been no update in value. // #ANSWER myAtom$.set(false); @@ -290,7 +291,7 @@ describe('inner workings', () => { const hasReacted = jest.fn(); atom$.react(hasReacted, { skipFirst: true }); - expect(hasReacted).toHaveBeenCalledTimes(0); // added for clarity, in case people missed the `skipFirst` or its implication + expect(hasReacted).toHaveBeenCalledTimes(0); atom$.set({}); @@ -301,7 +302,7 @@ describe('inner workings', () => { * `.react()` fire? */ expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION - expect(hasReacted).toHaveBeenCalledTimes(1); // `{} !== {}`, as they have different references // #ANSWER + expect(hasReacted).toHaveBeenCalledTimes(1); // `{} !== {}`, as they are different references // #ANSWER /** * But what if you use an object, that can be easily compared through a @@ -339,7 +340,7 @@ describe('inner workings', () => { * First we check `Object.is()` equality, if that is true, it is the * same, you can't deny that. * - * After that it is pluggable. It can be anything you want. + * After that it is pluggable. It can be anything you want. TODO: what is pluggable? * * By default we try to use `.equals()`, to support libraries like * `ImmutableJS`. diff --git a/generator/5 - unresolved.test.ts b/generator/5 - unresolved.test.ts index f676c5a..94d519e 100644 --- a/generator/5 - unresolved.test.ts +++ b/generator/5 - unresolved.test.ts @@ -1,12 +1,12 @@ import { atom, Derivable, DerivableAtom } from '@skunkteam/sherlock'; +// #QUESTION-BLOCK-START /** * ** Your Turn ** - * * If you see this variable, you should do something about it. :-) */ export const __YOUR_TURN__ = {} as any; - +// #QUESTION-BLOCK-END /** * Sometimes your data isn't available yet. For example if it is still being * fetched from the server. At that point you probably still want your @@ -36,7 +36,7 @@ describe('unresolved', () => { * Resolve the atom, it's pretty easy */ __YOUR_TURN__; // #QUESTION - myAtom$.set(1); // #ANSWER + myAtom$.set(1); // setting it to any value will unresolve it // #ANSWER expect(myAtom$.resolved).toBeTrue(); }); @@ -152,7 +152,7 @@ describe('unresolved', () => { * Combine the two `Atom`s into one `Derivable` */ const myDerivable$: Derivable = __YOUR_TURN__; // #QUESTION - const myDerivable$: Derivable = myString$.derive(s => s + myOtherString$.get()); // #ANSWER + const myDerivable$: Derivable = myString$.derive(parent$ => parent$ + myOtherString$.get()); // #ANSWER /** * ** Your Turn ** @@ -189,4 +189,31 @@ describe('unresolved', () => { expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); // #QUESTION expect(myDerivable$.resolved).toEqual(false); // #ANSWER }); + + /** + * It is nice to be able to have a backup plan when a Derivable gets unresolved. + * The `.fallbackTo()` function allows you to specify a default value + * whenever your Derivable gets unset. + */ + it('Fallback-to', () => { + const myAtom$ = atom(0); + + /** + * ** Your Turn ** + * Use the `.fallbackTo()` method to create a `mySafeAtom$` which + * gets the backup value `3` when `myAtom$` becomes unresolved. + */ + const mySafeAtom$ = myAtom$.fallbackTo(__YOUR_TURN__); // #QUESTION + const mySafeAtom$ = myAtom$.fallbackTo(() => 3); // #ANSWER + + expect(myAtom$.value).toBe(0); + expect(mySafeAtom$.value).toBe(0); + + myAtom$.unset(); + + expect(myAtom$.resolved).toBeFalse(); + expect(mySafeAtom$.resolved).toBeTrue(); + expect(myAtom$.value).toBeUndefined(); + expect(mySafeAtom$.value).toBe(3); + }); }); diff --git a/generator/6 - errors.test.ts b/generator/6 - errors.test.ts index 71a2720..ad59ffe 100644 --- a/generator/6 - errors.test.ts +++ b/generator/6 - errors.test.ts @@ -1,19 +1,16 @@ -import { atom, DerivableAtom, error, FinalWrapper, unresolved } from '@skunkteam/sherlock'; +import { atom, DerivableAtom, error } from '@skunkteam/sherlock'; +// #QUESTION-BLOCK-START /** * ** Your Turn ** - * * If you see this variable, you should do something about it. :-) */ export const __YOUR_TURN__ = {} as any; - -// Silence TypeScript's import not used errors. -expect(FinalWrapper).toBe(FinalWrapper); - -// In libs/sherlock/src/lib/interfaces.ts:289, the basic states a Derivable can have are shown. -// > `export type State = V | unresolved | ErrorWrapper;` -// A state can be either any type `V` (`number`, `string`, etc.), `unresolved` as we saw in the -// previous tutorial, or `ErrorWrapper`. This last state is explained here. +// #QUESTION-BLOCK-END +/** + * Errors are a bit part of any programming language, and Sherlock has its own custom errors + * and ways to deal with them. + */ describe('errors', () => { let myAtom$: DerivableAtom; @@ -22,10 +19,10 @@ describe('errors', () => { }); it('basic errors', () => { - // The `errored` property shows whether the last statement resulted in an error. + // The `errored` property of a Derivable shows whether it is in an error state - meaning that + // the last statement resulted in an error expect(myAtom$.errored).toBe(false); - expect(myAtom$.error).toBeUndefined; // by default, the `error` property is undefined. - expect(myAtom$.getState()).toBe(1); // as explained above, any type can be a state + expect(myAtom$.error).toBeUndefined; // by default, the `error` message is undefined. // We can set errors using the `setError()` function. myAtom$.setError('my Error'); @@ -33,16 +30,13 @@ describe('errors', () => { expect(myAtom$.errored).toBe(true); expect(myAtom$.error).toBe('my Error'); - // The `ErrorWrapper` state only holds an error string. The `error()` function returns - // such an `ErrorWrapper` which we can use to compare. - expect(myAtom$.getState()).toMatchObject(error('my Error')); - // expect(myAtom$.get).toThrow("Cannot read properties of undefined (reading 'getState')"); // TODO: WHAT - normally this works, but internal JEST just fucks with me....? - // Calling `get()` on `myAtom$` gives the error. - expect(() => myAtom$.get()).toThrow('my Error'); - expect(myAtom$.errored).toBe(true); + // What will happen if you try to call `get()` on `myAtom$`? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myAtom$.get()) /* __YOUR_TURN__ */; // #QUESTION + expect(() => myAtom$.get()).toThrow('my Error'); // #ANSWER // ** __YOUR_TURN__ ** // What will happen if you try to call `set()` on `myAtom$`? @@ -53,10 +47,28 @@ describe('errors', () => { expect(myAtom$.errored).toBe(false); // #ANSWER // Interestingly, calling `set()` does not throw an error. In fact, it removes the error state - // altogether. This means we can call `get()` again. + // altogether. This means we can now call `get()` again. expect(() => myAtom$.get()).not.toThrow(); }); + /** + * libs/sherlock/src/lib/interfaces.ts:289 shows the basic states that a Derivable can have. + * > `export type State = V | unresolved | ErrorWrapper;` + * A state can be either any type `V` (`number`, `string`, etc.), `unresolved` as we saw in the + * previous tutorial, or `ErrorWrapper`. This last state is explained here. + */ + it('error states', () => { + expect(myAtom$.getState()).toBe(1); // as explained above, any type can be a state + + myAtom$.setError('my Error'); + + // The `ErrorWrapper` state only holds an error string. The `error()` function returns + // such an `ErrorWrapper` which we can use to compare. + expect(myAtom$.getState()).toMatchObject(error('my Error')); + + // TODO: more! There wasn't a question in here. Maybe combine with Final States? NO, that one should go! + }); + it('deriving an error', () => { const myDerivable$ = myAtom$.derive(v => v + 1); @@ -69,16 +81,17 @@ describe('errors', () => { // If any Derivable tries to derive from an atom in an error state, // this Derivable will itself throw an error too. This makes sense, - // given that it cannot obtain the value it needs anymore. + // given that it cannot obtain the value it needs. }); it('reacting to an error', () => { - // Without a reactor, setting an error to an Atom does not throw an error. + // Setting an error to an Atom generally does not throw an error. expect(() => myAtom$.setError('my Error')).not.toThrow(); + myAtom$.set(1); - // Now we set a reactor to `myAtom$`. This reactor does not use the value of `myAtom$`. - const reactor = jest.fn(); + // Now we set a reactor to `myAtom$`. However, this reactor does not use the value of `myAtom$`. + const reactor = jest.fn(); // empty function body myAtom$.react(reactor); // ** __YOUR_TURN__ ** @@ -87,12 +100,17 @@ describe('errors', () => { expect(() => myAtom$.setError('my Error')) /* __YOUR_TURN__ */; // #QUESTION expect(() => myAtom$.setError('my Error')).toThrow('my Error'); // #ANSWER + // ** __YOUR_TURN__ ** + // Is the reactor still connected now that it errored? + expect(myAtom$.connected).toBe(__YOUR_TURN__); // #QUESTION + expect(myAtom$.connected).toBe(false); // #ANSWER + // Reacting to a Derivable that throws an error will make the reactor throw as well. // Because the reactor will usually fire when it gets connected, it also throws when // you try to connect it after the error has already been set. myAtom$ = atom(1); - myAtom$.setError('my second Error'); + myAtom$.setError('my second Error'); // // ** __YOUR_TURN__ ** // Will an error be thrown when you use `skipFirst`? @@ -108,168 +126,4 @@ describe('errors', () => { // When `from = false`, the reactor is disconnected, preventing the error message from entering. // `skipFirst`, on the other hand, does allow the error in, but does not trigger an update. }); - - /** - * Similarly to `constants` which we'll explain in tutorial 7, - * you might want to specify that a variable cannot be updated. - * This can be useful for the programmers themselves, to not - * accidentally update the variable, but it can also be useful for - * optimization. You can do this using the `final` concept. - */ - describe('TEMP `final`', () => { - let myAtom$ = atom(1); - - beforeEach(() => { - myAtom$ = atom(1); - }); - - it('`final` basics', () => { - // Every atom has a `final` property. - expect(myAtom$.final).toBeFalse(); - - // You can make an atom final using the `.makeFinal()` function. - myAtom$.makeFinal(); - expect(myAtom$.final).toBeTrue(); - - /** - * ** Your Turn ** - * What do you think will happen when we try to `.get()` or `.set()` this atom? - */ - // .toThrow() or .not.toThrow()? ↴ - expect(() => myAtom$.get()) /*__YOUR_TURN__*/; // #QUESTION - expect(() => myAtom$.set(2)) /*__YOUR_TURN__*/; // #QUESTION - expect(() => myAtom$.get()).not.toThrow(); // #ANSWER - expect(() => myAtom$.set(2)).toThrow('cannot set a final derivable'); // #ANSWER - - // This behavior is consistent with normal variables created using `const`. - // Alternatively, you can set a last value before setting it to `final`. - // .toThrow() or .not.toThrow()? ↴ - expect(() => myAtom$.setFinal(2)) /*__YOUR_TURN__*/; // #QUESTION - expect(() => myAtom$.setFinal(2)).toThrow('cannot set a final derivable'); // #ANSWER - - // There is no way to 'unfinalize' a Derivable, so the only solution to reset is to - // create a whole new Derivable. - myAtom$ = atom(1); - myAtom$.setFinal(2); - expect(myAtom$.final).toBeTrue(); - }); - - it('deriving a `final` Derivable', () => { - const myDerivable$ = myAtom$.derive(v => v + 1); - - const hasReacted = jest.fn(); - myDerivable$.react(hasReacted); - - expect(myDerivable$.final).toBeFalse(); - expect(myDerivable$.connected).toBeTrue(); - - myAtom$.makeFinal(); - - /** - * ** Your Turn ** - * - * What will happen to `myDerivable$` when I change `myAtom$` to be `final`? - */ - expect(myDerivable$.final).toBe(__YOUR_TURN__); // #QUESTION - expect(myDerivable$.final).toBe(true); // #ANSWER - expect(myDerivable$.connected).toBe(__YOUR_TURN__); // #QUESTION - expect(myDerivable$.connected).toBe(false); // #ANSWER - - /** - * Derivables that are final (or constant) are no longer tracked. This can save - * a lot of memory and time by cleaning up unused data. Also, when all the variables - * that a Derivable depends on become final, that Derivable itself also becomes final. - * Similarly to `unresolved` and `error`, this chains. - */ - }); - - it('`final` State', () => { - /** A property such as `.final`, similar to variables like `.errored` and `.resolved` - * is useful for checking whenever a Derivable is in a certain state, but these properties - * are just a boolean. This means that these properties cannot be derived and we cannot - * have certain functions execute whenever there is a change in the state. For this reason, - * every Derivable holds an internal state, retrievable using `.getState()` which can be - * derived. TODO: Have a clear place where I explain this! Now I have info up top here too. - * - * We have seen that states (`State`) can be `undefined`, `ErrorWrapper`, - * or any regular type `V`. Other states exist, such as the `MaybeFinalState`. This state can be either - * a normal state `State` or a special `FinalWrapper>` state. Let's see that in action. - */ - expect(myAtom$.getMaybeFinalState()).toBe(1); // `getMaybeFinalState` can return a normal state, which in turn can be any normal type. - - myAtom$.makeFinal(); - - expect(myAtom$.getMaybeFinalState()).toBeInstanceOf(FinalWrapper); // but `getMaybeFinalState` can also return a `FinalWrapper` type. - expect(myAtom$.getState()).toBe(1); // the normal type is still the final it contains. - - // TODO: MAAR JE KAN EEN STATE HELEMAAL NIET DERIVEN! - // Dus dat is allemaal onzin lijkt me....??? Bovendien, kan je normale variabelen niet deriven door het gewoon te - // wrappen in een atom ofzo? - }); - }); - - /** - * It is nice to be able to have a backup plan when an error occurs. - * The `.fallbackTo()` function allows you to specify a default value - * whenever your Derivable gets an error state. - */ - it('Fallback-to', () => { - const myAtom$ = atom(0); - - /** - * ** Your Turn ** - * Use the `.fallbackTo()` method to create a `mySafeAtom$` which - * gets the backup value `3` when `myAtom$` gets an error state. - */ - // const mySafeAtom$ = myAtom$.fallbackTo(__YOUR_TURN__); // #QUESTION - const mySafeAtom$ = myAtom$.fallbackTo(() => 3); // #ANSWER - - expect(myAtom$.getState()).toBe(0); - expect(myAtom$.value).toBe(0); - expect(mySafeAtom$.value).toBe(0); - - myAtom$.unset(); - - expect(myAtom$.getState()).toBe(unresolved); - expect(myAtom$.value).toBeUndefined(); - expect(mySafeAtom$.value).toBe(3); - }); - - it('TEMP Flat-map', () => { - // const myAtom$ = atom(0); - // const mapping = (v: any) => atom(v); - // Sometimes you use `map()`, but the result within the `map()` is also a Derivable. - // The result would here be a `Derivable>` (hover over `derive` to see this). - // You would have to use `.get()` to go back to a single Derivable. Similarly how `flatMap` can - // reduce lists of lists to a single list, it can help reduce Derivables of Derivables to a - // single Derivable. If you have something like this: - // let myAtom$$ = myAtom$.map(n => mapping(n)).derive(v => v.get()); - // You can now rewrite it to this: - // myAtom$$ = myAtom$.flatMap(n => mapping(n)); - // It only results in slightly shorter code. - // TODO: right? - }); }); - -/** - * !! Final States; (finalGetter, finalMethod, getMaybeFinalState, FinalWrapper, MaybeFinalState, _isFinal, makeFinal, markFinal, .final, .finalized, setFinal...) - * ? Lens; (libs/sherlock/src/lib/derivable/lens.ts) - map die twee kanten op kan gaan. Maar een map kan dat al? Maar hier kan - * je dat los definieren! Je kan gewoon `lens` ipv `var.lens`. Zelden dat je dit gebruikt. Output is een Derivable though. - * x Lift; (libs/sherlock-utils/src/lib/lift.ts) - * !! Peek; (libs/sherlock-utils/src/lib/peek.ts) - dan track je niet. In een derivable, deze tracked hij dan niet (ipv .get() waar het wel getracked wordt) - * x Template; (libs/sherlock-utils/src/lib/template.ts) - to make a string using a template literal. (Uses unwrap!!) - * / Factory; (libs/sherlock/src/lib/derivable/factories.ts) - simply contains functions to create objects, namely - * lens; atom; constant; derive. - * !! Flat-map; (libs/sherlock/src/lib/derivable/mixins/flat-map.ts) - ??? - * array: nested arrays naar array - * Derivable: gooit er derive.get() achteraan? - * Derivable (input van inputveld). Flatmap geeft Derivable terug. Derivable.flatmap() returned misschien - * Derivable, returned dan de number. flatMap is een `derive`, maar wat hij returned haalt hij uit de Derivable. - * ofzoiets. Maakt code korter. - * !! Fallback-to; - op een derivable. Als een atom `unresolved` is, dan fallt het back to this value. Ofwel, initial value, maar - * ook als hij later unresolved wordt, dan wordt hij dit (vaak wel initial value). - * !! Take - react options gebruiken buiten react. In een derivable chain, halverwege die options gebruiken. - * e.g. (from)Promise. Zodra die een waarde aanneemt kan hij niet meer veranderen. - * Let FromPromise, FromObservable, FromEventPattern ook uit (in utils?), ToPromise, ToObservable, in praktijk ook handig. - * FromEventPattern (haily mary, als alles niet werkt, dan dit doen). - */ diff --git a/generator/7 - advanced.test.ts b/generator/7 - advanced.test.ts index 37c3114..6b6b1f3 100644 --- a/generator/7 - advanced.test.ts +++ b/generator/7 - advanced.test.ts @@ -1,14 +1,14 @@ import { atom, constant, Derivable, derive, SettableDerivable, unresolved } from '@skunkteam/sherlock'; -import { lift, template } from '@skunkteam/sherlock-utils'; +import { template } from '@skunkteam/sherlock-utils'; import { Map as ImmutableMap } from 'immutable'; +// #QUESTION-BLOCK-START /** * ** Your Turn ** - * * If you see this variable, you should do something about it. :-) */ export const __YOUR_TURN__ = {} as any; - +// #QUESTION-BLOCK-END describe('advanced', () => { /** * In the case a `Derivable` is required, but the value is immutable. @@ -40,7 +40,7 @@ describe('advanced', () => { }); it('`templates`', () => { - // Staying in the theme of redefining normal Typescript code in our Derivable language, + // Staying in the theme of redefining normal Typescript code in our Sherlock language, // we also have a special syntax to copy template literals to a Derivable. const one = 1; const myDerivable = template`I want to go to ${one} party`; @@ -95,7 +95,7 @@ describe('advanced', () => { * when it is `allowed`. */ const myLimitedAtom$ = myAtom$.take(__YOUR_TURN__); // #QUESTION - const myLimitedAtom$ = myAtom$.take({ when: v => v.is('allowed') }); // #ANSWER + const myLimitedAtom$ = myAtom$.take({ when: parent$ => parent$.is('allowed') }); // #ANSWER expect(myLimitedAtom$.resolved).toBe(false); myAtom$.set('allowed'); @@ -198,13 +198,14 @@ describe('advanced', () => { it('triggers when the source changes', () => { const myAtom$ = atom(1); + /** * ** Your Turn ** * * Use the `.map()` method to create the expected output below */ const mappedAtom$: Derivable = __YOUR_TURN__; // #QUESTION - const mappedAtom$: Derivable = myAtom$.map(base => base.toString().repeat(base)); // #ANSWER + const mappedAtom$: Derivable = myAtom$.map(value => value.toString().repeat(value)); // #ANSWER mappedAtom$.react(mapReactSpy); @@ -229,6 +230,7 @@ describe('advanced', () => { expect(deriveReactSpy).toHaveBeenCalledExactlyOnceWith('ho', expect.toBeFunction()); myRepeat$.value = 3; + /** * ** Your Turn ** * @@ -251,6 +253,7 @@ describe('advanced', () => { // #ANSWER-BLOCK-END myString$.value = 'ha'; + /** * ** Your Turn ** * @@ -270,7 +273,6 @@ describe('advanced', () => { expect(mapReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); expect(mapReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); // #QUESTION-BLOCK-END - // #ANSWER-BLOCK-START expect(deriveReactSpy).toHaveBeenCalledTimes(3); expect(deriveReactSpy).toHaveBeenLastCalledWith('hahaha', expect.toBeFunction()); @@ -311,7 +313,7 @@ describe('advanced', () => { n => -n, // ...and this second function is called when setting. __YOUR_TURN__, // #QUESTION - (newV, _) => -newV, // #ANSWER + n => -n, // #ANSWER ); // The original `atom` was set to 1, so we want the inverse to @@ -325,37 +327,33 @@ describe('advanced', () => { expect(myInverse$.get()).toEqual(-2); }); + /** + * The `.map()` used here is similar to the `.map()` used on arrays. + * Both get values out of a container (`Array` or `Derivable`), apply + * some function, and put it back in the container. + */ it('similar to `map()` on arrays', () => { - // If the similarity is not clear yet, here is a comparison between - // the normal `.map()` on arrays and our `Derivable` `.map()`. - // Both get values out of a container (`Array` or `Derivable`), apply - // some function, and put it back in the container. - const addOne = jest.fn((v: number) => v + 1); - const myList = [1, 2, 3]; + const myList = [1]; const myMappedList = myList.map(addOne); - expect(myMappedList).toMatchObject([2, 3, 4]); + expect(myMappedList).toMatchObject([2]); const myAtom$ = atom(1); let myMappedDerivable$ = myAtom$.map(addOne); expect(myMappedDerivable$.value).toBe(2); - // Or, as we have seen before, you can use `lift()` for this. - myMappedDerivable$ = lift(addOne)(myAtom$); - expect(myMappedDerivable$.value).toBe(2); - // You can combine them too. - const myAtom2$ = atom([1, 2, 3]); + const myAtom2$ = atom([1]); const myMappedDerivable2$ = myAtom2$.map(v => v.map(addOne)); - expect(myMappedDerivable2$.value).toMatchObject([2, 3, 4]); + expect(myMappedDerivable2$.value).toMatchObject([2]); }); /** * In order to reason over the state of a Derivable, we can * use `.mapState()`. This will map one state to another, and * can be used to get rid of pesky `unresolved` or `Errorwrapper` - * states (or to introduce them!). + * states. */ it('`.mapState()`', () => { const myAtom$ = atom(1); @@ -412,7 +410,7 @@ describe('advanced', () => { expect(myMappedAtom$.get()).toBe(3); // the state and value are linked, so this is identical to `.getState()` /** * You might think that this change in state would cause `myAtom$` to now also get - * `3` as its state, but this does not happen. Why not? TODO: maximally one cycle? + * `3` as its state, but this does not happen. Why not? TODO: maximally one cycle? ASK! * Since both `2` and `3` are not `unresolved`, it does not matter to our answer. * * The same cannot be said for the fourth case. Setting `myMappedAtom$` to `unresolved` @@ -422,6 +420,22 @@ describe('advanced', () => { * TODO: change this to be for MAP. Then make MAPSTATE a trivial one right after. */ }); + + // FIXME: + it('TEMP Flat-map', () => { + // const myAtom$ = atom(0); + // const mapping = (v: any) => atom(v); + // Sometimes you use `map()`, but the result within the `map()` is also a Derivable. + // The result would here be a `Derivable>` (hover over `derive` to see this). + // You would have to use `.get()` to go back to a single Derivable. Similarly how `flatMap` can + // reduce lists of lists to a single list, it can help reduce Derivables of Derivables to a + // single Derivable. If you have something like this: + // let myAtom$$ = myAtom$.map(n => mapping(n)).derive(v => v.get()); + // You can now rewrite it to this: + // myAtom$$ = myAtom$.flatMap(n => mapping(n)); + // It only results in slightly shorter code. + // TODO: right? + }); }); /** diff --git a/generator/8 - utils.test.ts b/generator/8 - utils.test.ts index c1c1dd6..7a5ff0f 100644 --- a/generator/8 - utils.test.ts +++ b/generator/8 - utils.test.ts @@ -1,9 +1,9 @@ -import { atom, derive } from '@skunkteam/sherlock'; -import { lift, pairwise, peek, scan, struct } from '@skunkteam/sherlock-utils'; +import { atom, constant, derive, FinalWrapper } from '@skunkteam/sherlock'; +import { fromPromise, lift, pairwise, peek, scan, struct } from '@skunkteam/sherlock-utils'; +// #QUESTION-BLOCK-START /** * ** Your Turn ** - * * If you see this variable, you should do something about it. :-) */ export const __YOUR_TURN__ = {} as any; @@ -14,7 +14,8 @@ expect(scan).toBe(scan); expect(struct).toBe(struct); expect(peek).toBe(peek); expect(lift).toBe(lift); - +expect(FinalWrapper).toBe(FinalWrapper); // TODO: not sure whether needed +// #QUESTION-BLOCK-END /** * In the `sherlock-utils` lib, there are a couple of functions that can combine * multiple values of a single `Derivable` or combine multiple `Derivable`s into @@ -40,10 +41,11 @@ describe('utils', () => { * * *Hint: check the overloads of pairwise if you're struggling with * `oldValue`.* + * + * Note: don't call `pairwise()` using a lambda function! */ myCounter$.derive(__YOUR_TURN__).react(reactSpy); // #QUESTION myCounter$.derive(pairwise((newVal, oldVal) => newVal - oldVal, 0)).react(reactSpy); // #ANSWER - // myCounter$.derive(pairwise((newVal, oldVal) => (oldVal ? newVal - oldVal : newVal))).react(reactSpy); // OR: alternatively. // #ANSWER expect(reactSpy).toHaveBeenCalledTimes(1); expect(reactSpy).toHaveBeenLastCalledWith(1, expect.toBeFunction()); @@ -57,6 +59,14 @@ describe('utils', () => { expect(reactSpy).toHaveBeenCalledTimes(3); expect(reactSpy).toHaveBeenLastCalledWith(7, expect.toBeFunction()); // 10 (current value of `myCounter$`) - 3 (previous value of `myCounter$`) + + myCounter$.set(20); + + // ** Your Turn ** + // What will the next output be? + expect(reactSpy).toHaveBeenCalledTimes(4); + expect(reactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); // #QUESTION + expect(reactSpy).toHaveBeenLastCalledWith(10, expect.toBeFunction()); // 20 (current value of `myCounter$`) - 10 (previous value of `myCounter$`) // #ANSWER }); /** @@ -82,6 +92,8 @@ describe('utils', () => { * as this returned value is also used for the accumulator (`acc`) value for the next call. * This `acc` parameter of `scan()` is the last returned value, not the last value * of `myCounter$`, as is the case with `pairwise()`. + * + * Note: don't call `pairwise()` using a lambda function! */ myCounter$.derive(__YOUR_TURN__).react(reactSpy); // #QUESTION myCounter$.derive(scan((acc, val) => val - acc, 0)).react(reactSpy); // #ANSWER @@ -98,11 +110,19 @@ describe('utils', () => { expect(reactSpy).toHaveBeenCalledTimes(3); expect(reactSpy).toHaveBeenLastCalledWith(8, expect.toBeFunction()); // 10 (current value of `myCounter$`) - 2 (previous returned value) + + myCounter$.set(20); + + // ** Your Turn ** + // What will the next output be? + expect(reactSpy).toHaveBeenCalledTimes(4); + expect(reactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); // #QUESTION + expect(reactSpy).toHaveBeenLastCalledWith(12, expect.toBeFunction()); // 20 (current value of `myCounter$`) - 8 (previous returned value) // #ANSWER }); it('`pairwise()` on normal arrays', () => { // Functions like `pairwise()` and `scan()` work on normal lists too. They are often - // used in combination with `map()` and `filter()`. + // used in combination with `.map()` and `.filter()`. const myList = [1, 2, 3, 5, 10]; let myList2: number[]; @@ -112,13 +132,14 @@ describe('utils', () => { * Use a `pairwise()` combined with a `.map()` on `myList` * to subtract the previous value from the current. * - * Hint: do not use a lambda function! + * Note: don't call `pairwise()` using a lambda function! */ myList2 = myList.map(__YOUR_TURN__); // #QUESTION myList2 = myList.map(pairwise((newV, oldV) => newV - oldV, 0)); // #ANSWER expect(myList2).toMatchObject([1, 1, 1, 2, 5]); // However, we should be careful with this, as this does not always behave as intended. + // Particularly, what exactly happens when we do call `pairwise()` using a lambda function? myList2 = myList.map(v => __YOUR_TURN__(v)); // copy the same implementation here // #QUESTION myList2 = myList.map(v => pairwise((newV, oldV) => newV - oldV, 0)(v)); // copy the same implementation here // #ANSWER expect(myList2).toMatchObject([1, 2, 3, 5, 10]); @@ -147,22 +168,28 @@ describe('utils', () => { f = pairwise((newV, oldV) => newV - oldV, 0); // #ANSWER myList2 = []; - myList2[0] = f(myList[0]); // f is newly created with `init = 0`, so applies `1 - 0`. - myList2[1] = f(myList[1]); // f has saved `1` internally, so applies `2 - 1`. - myList2[2] = f(myList[2]); // f has saved `2` internally, so applies `3 - 2`. - myList2[3] = f(myList[3]); // f has saved `3` internally, so applies `5 - 3`. - myList2[4] = f(myList[4]); // f has saved `5` internally, so applies `10 - 5`. + myList2[0] = f(myList[0]); // `f` is newly created with `init = 0`, so applies `1 - 0 = 1`. + myList2[1] = f(myList[1]); // `f` has saved `1` internally, so applies `2 - 1 = 1`. + myList2[2] = f(myList[2]); // `f` has saved `2` internally, so applies `3 - 2 = 1`. + myList2[3] = f(myList[3]); // `f` has saved `3` internally, so applies `5 - 3 = 2`. + myList2[4] = f(myList[4]); // `f` has saved `5` internally, so applies `10 - 5 = 5`. expect(myList2).toMatchObject([1, 1, 1, 2, 5]); + // This also works for functions other than `.map()`, such as `.filter()`. /** ** Your Turn ** * Use `pairwise()` to filter out all values which produce `1` when subtracted * with their previous value. + * Note that the function `f` still requires a number to be the return value. + * Checking for equality therefore cannot be done directly within `f`. */ + f = __YOUR_TURN__; // #QUESTION myList2 = myList.filter(__YOUR_TURN__); // #QUESTION - myList2 = myList.filter(pairwise((newV, oldV) => newV - oldV === 1, 0)); // #ANSWER - expect(myList2).toMatchObject([1, 2, 3]); + f = pairwise((newV, oldV) => newV - oldV, 0); // #ANSWER + myList2 = myList.filter(v => f(v) === 1); // #ANSWER + + expect(myList2).toMatchObject([1, 2, 3]); // only the numbers `1`, `2`, and `3` produce 1 when subtracted with the previous value }); it('`scan()` on normal arrays', () => { @@ -173,10 +200,9 @@ describe('utils', () => { /** * ** Your Turn ** * - * Use a `scan()` combined with a `map` on `myList` + * Use a `scan()` combined with a `.map()` on `myList` * to subtract the previous value from the current. */ - let f: (v: number) => number = scan(__YOUR_TURN__); // #QUESTION let f: (v: number) => number = scan((acc, val) => val - acc, 0); // #ANSWER myList2 = myList.map(f); @@ -184,28 +210,32 @@ describe('utils', () => { expect(myList2).toMatchObject([1, 1, 2, 3, 7]); // again, it is useful to consider what happens internally. - f(7); // resets the internal `acc` value to 0, as the current `acc` value was 7, and 7-7 = 0. + f(7); // reset -- this resets the internal `acc` value to 0, as the current `acc` value was 7, and 7-7 = 0. myList2 = []; - myList2[0] = f(myList[0]); // 1 :: `f` is newly created with `init = 0`, so applies `1 - 0 = 1`. - myList2[1] = f(myList[1]); // 1 :: `f` has saved the result `1` internally, so applies `2 - 1 = 1`. - myList2[2] = f(myList[2]); // 2 :: `f` has saved the result `1` internally, so applies `3 - 1 = 2`. - myList2[3] = f(myList[3]); // 3 :: `f` has saved the result `2` internally, so applies `5 - 2 = 3`. - myList2[4] = f(myList[4]); // 7 :: `f` has saved the result `3` internally, so applies `10 - 3 = 7`. + myList2[0] = f(myList[0]); // `f` is newly created with `init = 0`, so applies `1 - 0 = 1`. + myList2[1] = f(myList[1]); // `f` has saved the result `1` internally, so applies `2 - 1 = 1`. + myList2[2] = f(myList[2]); // `f` has saved the result `1` internally, so applies `3 - 1 = 2`. + myList2[3] = f(myList[3]); // `f` has saved the result `2` internally, so applies `5 - 2 = 3`. + myList2[4] = f(myList[4]); // `f` has saved the result `3` internally, so applies `10 - 3 = 7`. expect(myList2).toMatchObject([1, 1, 2, 3, 7]); // This also works for functions other than `map()`, such as `filter()`. - // Use `scan()` to filter out all values from `myList` which produce a value - // of 8 or higher when added with the previous result. In other words, it should - // go through `myList` and add the values producing: (1), (1+2), (1+2+3), (1+2+3+5), - // (1+2+3+5+10), and since this sum only prouces a value higher than 8 when the - // values `5` and `10` are added, the result should be `[5,10]`. + /** + * ** Your Turn ** + * Use `scan()` to filter out all values from `myList` which produce a value + * of 8 or higher when added with the previous result. In other words, it should + * go through `myList` and add the values producing: (1), (1+2), (1+2+3), (1+2+3+5), + * (1+2+3+5+10), and since this sum only prouces a value higher than 8 when the + * values `5` and `10` are added, the result should be `[5,10]`. + */ f = scan(__YOUR_TURN__); // #QUESTION myList2 = myList.filter(__YOUR_TURN__); // #QUESTION f = scan((acc, val) => val + acc, 0); // #ANSWER myList2 = myList.filter(v => f(v) >= 8); // #ANSWER + expect(myList2).toMatchObject([5, 10]); }); @@ -326,19 +356,22 @@ describe('utils', () => { describe('lift()', () => { /** - * Derivables can feel like a language build on top of Typescript. Sometimes + * Sherlock may feel like a language build on top of Typescript. Sometimes * you might want to use normal objects and functions and not have to rewrite * your code. - * In other words, just like keywords like `atom(V)` lifts a variable V to the higher + * In other words, just as keywords like `atom(V)` lifts a variable V to the higher * level of Derivables, the `lift(F)` keyword lifts a function `F` to the higher * level of Derivables. */ it('example', () => { - // Example: after years of effort, Bob finally finished his oh-so complicated function: + // Say I just finished writing this oh-so-complicated function: const isEvenNumber = (v: number) => v % 2 == 0; - // Rewriting this function to work with derivables would now be a waste of time. /** + * Rewriting this function to work with derivables would now be a waste of time. + * This is especially so if you didn't even write the original function, e.g. when + * you use a library function. + * * ** Your Turn ** * Use the `lift()` function to change `isEvenNumber` to work on Derivables instead. * In other words: the new function should take a `Derivable` (or more specifically: @@ -349,8 +382,8 @@ describe('utils', () => { expect(isEvenNumber(2)).toBe(true); expect(isEvenNumber(13)).toBe(false); - expect(isEvenDerivable(atom(2)).get()).toBe(true); - expect(isEvenDerivable(atom(13)).get()).toBe(false); + expect(isEvenDerivable(atom(2)).value).toBe(true); + expect(isEvenDerivable(atom(13)).value).toBe(false); }); it('`lift()` as alternative to `.map()`', () => { @@ -393,16 +426,258 @@ describe('utils', () => { expect(reactor).toHaveBeenCalledOnce(); expect(reactor).toHaveLastReturnedWith(3); + // there reactor should be called when `myTrackedAtom$` updates myTrackedAtom$.set(2); expect(reactor).toHaveBeenCalledTimes(2); expect(reactor).toHaveLastReturnedWith(4); + // the reactor should not be called when `myUntrackedAtom$` updates myUntrackedAtom$.set(3); expect(reactor).toHaveBeenCalledTimes(2); expect(reactor).toHaveLastReturnedWith(4); + // but when `myTrackedAtom$` updates, the value of `myUntrackedAtom$` did change myTrackedAtom$.set(3); expect(reactor).toHaveBeenCalledTimes(3); expect(reactor).toHaveLastReturnedWith(6); }); + + /** + * Similarly to the `constants` explained in tutorial 7, + * you might want to specify that a variable cannot be updated. + * This can be useful for the programmers themselves, to not + * accidentally update the variable, but it can also be useful for + * optimization. This can be done using the `final` keyword. + */ + describe('`final`', () => { + let myAtom$ = atom(1); + + beforeEach(() => { + myAtom$ = atom(1); + }); + + it('`final` basics', () => { + // Every atom has a `final` property. + expect(myAtom$.final).toBeFalse(); + + // TODO: SHOW THAT CONST ALSO GIVES THE SAME ERROR MESSAGE WHEN SET!! + + // You can make an atom final using the `.makeFinal()` function. + myAtom$.makeFinal(); + expect(myAtom$.final).toBeTrue(); + + /** + * ** Your Turn ** + * What do you think will happen when we try to `.get()` or `.set()` this atom? + */ + // .toThrow() or .not.toThrow()? ↴ + expect(() => myAtom$.get()) /*__YOUR_TURN__*/; // #QUESTION + expect(() => myAtom$.set(2)) /*__YOUR_TURN__*/; // #QUESTION + expect(() => myAtom$.get()).not.toThrow(); // #ANSWER + expect(() => myAtom$.set(2)).toThrow('cannot set a final derivable'); // #ANSWER + + // This behavior is consistent with normal variables created using `const`. + // Alternatively, you can set a last value before setting it to `final`, using `.setFinal()`. + // .toThrow() or .not.toThrow()? ↴ + expect(() => myAtom$.setFinal(2)) /*__YOUR_TURN__*/; // #QUESTION + expect(() => myAtom$.setFinal(2)).toThrow('cannot set a final derivable'); // #ANSWER + // Remember: we try to set an atom that is already final, so we get an error // #ANSWER + + // There is no way to 'unfinalize' a Derivable, so the only solution to reset is to + // create a whole new Derivable. + myAtom$ = atom(1); + myAtom$.setFinal(2); + expect(myAtom$.final).toBeTrue(); + + // Also interesting: a `constant` as introduced in tutorial 7 is actually a Derivable set to + // `final` in disguise. You can verify this by checking the implementation of `constant` at + // libs/sherlock/src/lib/derivable/factories.ts:39 + const myConstantAtom$ = constant(1); + expect(myConstantAtom$.final).toBe(__YOUR_TURN__); // #QUESTION + expect(myConstantAtom$.final).toBe(true); // #ANSWER + }); + + it('deriving a `final` Derivable', () => { + const myDerivable$ = myAtom$.derive(v => v + 1); + + const hasReacted = jest.fn(); + myDerivable$.react(hasReacted); + + expect(myDerivable$.final).toBeFalse(); + expect(myDerivable$.connected).toBeTrue(); + + myAtom$.makeFinal(); + + /** + * ** Your Turn ** + * + * What will happen to `myDerivable$` when I change `myAtom$` to be `final`? + */ + expect(myDerivable$.final).toBe(__YOUR_TURN__); // #QUESTION + expect(myDerivable$.final).toBe(true); // #ANSWER + expect(myDerivable$.connected).toBe(__YOUR_TURN__); // #QUESTION + expect(myDerivable$.connected).toBe(false); // #ANSWER + + /** + * Derivables that are final (or constant) are no longer tracked. This can save + * a lot of memory and time by cleaning up unused data. Also, when all the variables + * that a Derivable depends on become final, that Derivable itself becomes final too. + * This chains similarly to `unresolved` and `error`. + */ + }); + + it('TODO: `final` State', () => { + /** A property such as `.final`, similar to variables like `.errored` and `.resolved` + * is useful for checking whenever a Derivable is in a certain state, but these properties + * are just a boolean. This means that these properties cannot be derived and we cannot + * have certain functions execute whenever there is a change in the state. For this reason, + * every Derivable holds an internal state, retrievable using `.getState()` which can be + * derived. TODO: Have a clear place where I explain this! Now I have info up top here too. + * + * We have seen that states (`State`) can be `undefined`, `ErrorWrapper`, + * or any regular type `V`. Other states exist, such as the `MaybeFinalState`. This state can be either + * a normal state `State` or a special `FinalWrapper>` state. Let's see that in action. + */ + expect(myAtom$.getMaybeFinalState()).toBe(1); // `getMaybeFinalState` can return a normal state, which in turn can be any normal type. + + myAtom$.makeFinal(); + + expect(myAtom$.getMaybeFinalState()).toBeInstanceOf(FinalWrapper); // but `getMaybeFinalState` can also return a `FinalWrapper` type. + expect(myAtom$.getState()).toBe(1); // the normal type is still the final it contains. + + // TODO: MAAR JE KAN EEN STATE HELEMAAL NIET DERIVEN! + // Dus dat is allemaal onzin lijkt me....??? Bovendien, kan je normale variabelen niet deriven door het gewoon te + // wrappen in een atom ofzo? Of door te structen? + }); + }); + + describe('`Promise`, `Observable`, and `EventPattern`', () => { + /** + * Sherlock can also deal with Promises using the `.fromPromise()` and `.toPromise()` functions. + * This translates Promises directly to Sherlock concepts we have discussed already. + */ + it('`fromPromise()`', async () => { + // we initialize a Promise that will resolve, not reject, when handled + let promise = Promise.resolve(15); + let myAtom$ = fromPromise(promise); + + /** + * ** Your Turn ** + * What do you think is the default state of an atom based on a Promise? + */ + expect(myAtom$.resolved).toBe(__YOUR_TURN__); // #QUESTION + expect(myAtom$.final).toBe(__YOUR_TURN__); // #QUESTION + expect(myAtom$.resolved).toBe(false); // #ANSWER + expect(myAtom$.final).toBe(false); // #ANSWER + + // Now we wait for the Promise to be handled (resolved). + await promise; + + /** + * ** Your Turn ** + * So, what will happen to `myAtom$` and `myMappedAtom$`? + */ + expect(myAtom$.get()).toBe(__YOUR_TURN__); // #QUESTION + expect(myAtom$.final).toBe(__YOUR_TURN__); // #QUESTION + expect(myAtom$.get()).toBe(15); // #ANSWER + expect(myAtom$.final).toBe(true); // #ANSWER + + // Now we make a promise that is rejected when called. + promise = Promise.reject('Oh no, I messed up!'); + myAtom$ = fromPromise(promise); + + // We cannot await the Promise itself, as it would immediately throw. + await Promise.resolve(); + + /** + * ** Your Turn ** + * So, what will happen to `myAtom$` now? + */ + expect(myAtom$.errored).toBe(__YOUR_TURN__); // #QUESTION + expect(myAtom$.error).toBe(__YOUR_TURN__); // #QUESTION + expect(myAtom$.final).toBe(__YOUR_TURN__); // #QUESTION + expect(myAtom$.errored).toBe(true); // #ANSWER + expect(myAtom$.error).toBe('Oh no, I messed up!'); // #ANSWER + expect(myAtom$.final).toBe(true); // #ANSWER + }); + + it('`.toPromise()`', async () => { + /** + * `.toPromise()` returns a promise that is linked to the atom it is based on (`myAtom$` here) + * If the atom has a value, the promise is resolved. If the atom errors, the promise is rejected using the same error. + * And it the atom is unresolved, the promise is pending. + */ + let myAtom$ = atom('initial value'); + let promise = myAtom$.toPromise(); + + /** + * ** Your Turn ** + * What do you think will happen when we try to set the atom with a value? + */ + myAtom$.set('second value'); + expect(await promise).toBe(__YOUR_TURN__); // #QUESTION + expect(await promise).toBe('initial value'); // `myAtom$` starts with a value ('initial value'), so the promise is immediately resolved // #ANSWER + + myAtom$.unset(); + promise = myAtom$.toPromise(); + + /** + * ** Your Turn ** + * We set the atom to `unresolved`. What will now happen when we try to set the atom with a value? + */ + myAtom$.set('third value'); + expect(await promise).toBe(__YOUR_TURN__); // #QUESTION + expect(await promise).toBe('third value'); // This is now the first value the atom obtains since the promise was created. // #ANSWER + + // Whenever an atom is in an `unresolved` state, the corresponding Promise is pending. + // This means that the Promise can still become resolved or rejected depending on the atom's actions. + + myAtom$.unset(); + promise = myAtom$.toPromise(); + + myAtom$.setError('Error.'); + + /** + * ** Your Turn ** + * We set the atom to an error state. The promise should now be rejected, hence we wrap it in a `try-catch` block. + * What do you think the error message will be? Remember that `try-catch` is not a custom-defined structure. + */ + try { + await promise; + } catch (error: any) { + // `.toBe('Error.')` or `.not.toBe('Error.')`? ↴ + expect(error.message) /*__YOUR_TURN__*/; // #QUESTION + expect(error.message).not.toBe('Error.'); // #ANSWER + } + + myAtom$.set('no more error'); + const myDerivable$ = myAtom$.derive(() => { + throw new Error('Error.'); + }); + promise = myDerivable$.toPromise(); + + /** + * ** Your Turn ** + * We now let `myDerivable$` derive from `myAtom$`, and it will throw a normal error (not a custom Sherlock error). + * What will the error message be this time? + */ + try { + await promise; + } catch (error: any) { + // `.toBe('Error.')` or `.not.toBe('Error.')`? ↴ + expect(error.message) /*__YOUR_TURN__*/; // #QUESTION + expect(error.message).toBe('Error.'); // #ANSWER + } + }); + + it('`fromObservable()`', () => { + // Has to do with SUBSCRIBING. Hasn't been discussed either... + // TODO: "As all Derivables are now compatible with rxjs's `from` function, + // we no longer need the `toObservable` function from `@skunkteam/sherlock-rxjs`." + }); + + it('`fromEventPattern`', () => { + // TODO: this is kinda complicated shit... Requires explaining a lot of extra stuff (Subjects, Subscribing, Observables...). Leave for now? + }); + }); }); diff --git a/generator/9 - expert.test.ts b/generator/9 - expert.test.ts index 28aa4a9..e1a7184 100644 --- a/generator/9 - expert.test.ts +++ b/generator/9 - expert.test.ts @@ -1,13 +1,13 @@ import { DerivableAtom, atom, derive } from '@skunkteam/sherlock'; import { derivableCache } from '@skunkteam/sherlock-utils'; +// #QUESTION-BLOCK-START /** * ** Your Turn ** - * * If you see this variable, you should do something about it. :-) */ export const __YOUR_TURN__ = {} as any; - +// #QUESTION-BLOCK-END describe('expert', () => { describe('`.autoCache()`', () => { /** @@ -30,7 +30,6 @@ describe('expert', () => { * `hasDerived` is used in the first derivation. But has it been * called at this point? */ - // `.toHaveBeenCalled()` or `.not.toHaveBeenCalled()`? ↴ expect(hasDerived) /* Your Turn */; // #QUESTION expect(hasDerived).not.toHaveBeenCalled(); // #ANSWER @@ -191,15 +190,18 @@ describe('expert', () => { expect(stockPrice$).toHaveBeenCalledTimes(2); // #ANSWER /** Can you explain this behavior? */ - // ANSWER-BLOCK-START - // Yes: it creates a different Derivable every time, so it cannot use any caching. - // This is a similar issue to the `pairwise()` issue from tutorial 7, where, when we - // used lambda functions, we made a new pairwise object every time. - // ANSWER-BLOCK-END + // #ANSWER-BLOCK-START + /** + * Yes: `stockPrices$` is not a derivable itself, just the setup function. + * This function creates a different Derivable every time, so it cannot use any caching. + * This is a similar issue to the `pairwise()` issue from tutorial 7, where, when we + * used lambda functions, we made a new pairwise object every time. + */ + // #ANSWER-BLOCK-END }); /** - * An other problem can arise when the setup is done inside a derivation + * Another problem can arise when the setup is done inside a derivation */ describe('setup inside a derivation', () => { /** @@ -364,7 +366,8 @@ describe('expert', () => { expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); // #QUESTION expect(reactSpy).toHaveBeenCalledWith([__YOUR_TURN__, __YOUR_TURN__]); // #QUESTION expect(reactSpy).toHaveBeenCalledTimes(3); // #ANSWER - expect(reactSpy).toHaveBeenCalledWith([undefined, undefined]); // #ANSWER + expect(reactSpy).toHaveBeenCalledWith([1079.11, undefined]); // #ANSWER + // Note: `[undefined, undefined]` will pass too, but is incorrect. // #ANSWER }); }); diff --git a/package.json b/package.json index 9e3b39d..aef988f 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "workspace-generator": "nx workspace-generator", "dep-graph": "nx dep-graph", "help": "nx help", - "tutorial": "jest tutorial/* -c tutorial/jest.config.ts" + "tutorial": "jest tutorial/* -c tutorial/jest.config.ts", + "solution": "jest solution/* -c solution/jest.config.ts" }, "standard-version": { "bumpFiles": [ diff --git a/generated_solution/1 - intro.test.ts b/solution/1 - intro.test.ts similarity index 94% rename from generated_solution/1 - intro.test.ts rename to solution/1 - intro.test.ts index 6ee2307..5e53e5c 100644 --- a/generated_solution/1 - intro.test.ts +++ b/solution/1 - intro.test.ts @@ -1,11 +1,5 @@ import { atom } from '@skunkteam/sherlock'; -/** - * ** Your Turn ** - * If you see this variable, you should do something about it. :-) - */ -export const __YOUR_TURN__ = {} as any; - /** * Welcome to the `@skunkteam/sherlock` tutorial. * @@ -13,7 +7,7 @@ export const __YOUR_TURN__ = {} as any; * to pass. The `expect()`s and basic setup are there, you just need to get it * to work. * - * All specs except the first one are set to `.skip`. Remove this to start on + * All specs are set to `.skip`. Remove this to start on * that part of the tutorial. * * Start the tutorial by running: @@ -25,7 +19,15 @@ export const __YOUR_TURN__ = {} as any; * * *Hint: most methods and functions are fairly well documented in jsDoc, * which is easily accessed through TypeScript* + * + * If you cannot figure it out or are curious to the intended answers, you can + * read the answers in the `solution` folder. */ + +/** + * ** Your Turn ** + * Your first task: remove this `.skip`. + * */ describe('intro', () => { it(` diff --git a/generated_solution/2 - deriving.test.ts b/solution/2 - deriving.test.ts similarity index 95% rename from generated_solution/2 - deriving.test.ts rename to solution/2 - deriving.test.ts index e88fc49..6eada05 100644 --- a/generated_solution/2 - deriving.test.ts +++ b/solution/2 - deriving.test.ts @@ -1,12 +1,5 @@ import { atom, Derivable, derive } from '@skunkteam/sherlock'; -/** - * ** Your Turn ** - * - * If you see this variable, you should do something about it. :-) - */ -export const __YOUR_TURN__ = {} as any; - /** * Any `Derivable` (including `Atom`s) can be used (and/or combined) to create * a derived state. This derived state is in turn a `Derivable`. @@ -37,7 +30,7 @@ describe('deriving', () => { */ // We can combine txt with `repeat$.get()` here. - const lyric$ = text$.derive(txt => txt.repeat(repeat$.get())); + const lyric$ = text$.derive(txt => txt.repeat(repeat$.get())); expect(lyric$.get()).toEqual(`It won't be long`); @@ -73,13 +66,11 @@ describe('deriving', () => { * method on another `Derivable`. */ - // Should return 'Buzz' when `myCounter$` is a multiple of 5 and '' otherwise. - // Should return 'Fizz' when `myCounter$` is a multiple of 3 and '' otherwise. - const fizz$: Derivable = myCounter$.derive(v => (v % 3 ? 'Fizz' : '')); // Shorthand for `v % 3 === 0` + const fizz$: Derivable = myCounter$.derive(v => (v % 3 ? '' : 'Fizz')); // Shorthand for `v % 3 !== 0` // Should return 'Buzz' when `myCounter$` is a multiple of 5 and '' otherwise. - const buzz$: Derivable = myCounter$.derive(v => (v % 5 ? 'Buzz' : '')); + const buzz$: Derivable = myCounter$.derive(v => (v % 5 ? '' : 'Buzz')); const fizzBuzz$: Derivable = derive(() => fizz$.get() + buzz$.get() || myCounter$.get()); @@ -200,7 +191,6 @@ describe('deriving', () => { * `.and(...)` and `.or(...)`. Make sure the output of those `Derivable`s * is either 'Fizz'/'Buzz' or ''. */ - const fizz$ = myCounter$ .derive(count => count % 3) .is(0) @@ -213,8 +203,8 @@ describe('deriving', () => { .and('Buzz') .or(''); - const fizzBuzz$ = derive(() => fizz$.get() + buzz$.get()).or(myCounter$); - // This will check whether `fizz$.get() + buzz$.get()` is truthy: if so, return it; if not, return `myCounter$` + const fizzBuzz$ = derive(() => fizz$.get() + buzz$.get()).or(myCounter$); + // This will check whether `fizz$.get() + buzz$.get()` is truthy: if so, return it; if not, return `myCounter$` for (let count = 1; count <= 100; count++) { // Set the value of the `Atom`, diff --git a/generated_solution/3 - reacting.test.ts b/solution/3 - reacting.test.ts similarity index 72% rename from generated_solution/3 - reacting.test.ts rename to solution/3 - reacting.test.ts index 2ae2582..0479b20 100644 --- a/generated_solution/3 - reacting.test.ts +++ b/solution/3 - reacting.test.ts @@ -1,19 +1,41 @@ import { atom } from '@skunkteam/sherlock'; -/** - * ** Your Turn ** - * - * If you see this variable, you should do something about it. :-) - */ -export const __YOUR_TURN__ = {} as any; - -// FIXME: check my solutions with the actual solutions +// xxx check my solutions with the actual solutions (https://github.com/skunkteam/sherlock/tree/tutorial-solutions/robin/tutorial) // FIXME: remove all TODO: and FIXME: -// FIXME: check whether the generated tutorials and solutions actually work (e.g. are all solutions correct? No weird shenanigans?) -// FIXME: deze file niet linten / builden (voor automatische test). Tutorial ook niet. Maar solutions juist wel! OP EIND. +// FIXME: check whether the generated tutorials and solutions actually work (e.g. are all solutions correct? No weird shenanigans?) - ALSO CHECK "Or, alternatively"! +// FIXME: deze file niet linten / builden (voor automatische test). Tutorial ook niet. Maar solutions juist wel! OP EIND. (mag beide wel linten right?) // FIXME: interne review document, mocht ik iets hebben om te laten zien! In Google Drive, zet het erin! -// FIXME: werkt `npm run tutorial` nog??? - +// xxx werkt `npm run tutorial` nog? > Nu wel. +// xxx PETER: "nu je toch met Sherlock bezig bent; zou je voor mij eens kunnen checken of de code voorbeelden in de README +// nog wel kloppen met de huidige API? Ik heb het gevoel dat dat niet zo is; volgens mij is er geen function "derivation()" +// en heet dat nu "derive()" bijvoorbeeld." +// FIXME: OOOOOOH JA, ik had eroverheen gepushed! Dat moet nog een PR met terugwerkende kracht worden... (of commits squashen, en dat ze dan maar de commit moeten reviewen?) + +// FIXME: Add FromEventPattern + FromObservable +// xxx fix the generator for code blocks. +// FIXME: now check whether it did not remove excess lines or kept 2 empty lines where it should not. (I think it is good though.) +/** + * x Final States; (finalGetter, finalMethod, getMaybeFinalState, FinalWrapper, MaybeFinalState, _isFinal, makeFinal, markFinal, .final, .finalized, setFinal...) + * ? Lens; (libs/sherlock/src/lib/derivable/lens.ts) - map die twee kanten op kan gaan. Maar een map kan dat al? Maar hier kan + * je dat los definieren! Je kan gewoon `lens` ipv `var.lens`. Zelden dat je dit gebruikt. Output is een Derivable though. + * x Lift; (libs/sherlock-utils/src/lib/lift.ts) + * x Peek; (libs/sherlock-utils/src/lib/peek.ts) - dan track je niet. In een derivable, deze tracked hij dan niet (ipv .get() waar het wel getracked wordt) + * x Template; (libs/sherlock-utils/src/lib/template.ts) - to make a string using a template literal. (Uses unwrap!!) + * / Factory; (libs/sherlock/src/lib/derivable/factories.ts) - simply contains functions to create objects, namely + * lens; atom; constant; derive. + * !! Flat-map; (libs/sherlock/src/lib/derivable/mixins/flat-map.ts) - ??? + * array: nested arrays naar array + * Derivable: gooit er derive.get() achteraan? + * Derivable (input van inputveld). Flatmap geeft Derivable terug. Derivable.flatmap() returned misschien + * Derivable, returned dan de number. flatMap is een `derive`, maar wat hij returned haalt hij uit de Derivable. + * ofzoiets. Maakt code korter. + * x Fallback-to; - op een derivable. Als een atom `unresolved` is, dan fallt het back to this value. Ofwel, initial value, maar + * ook als hij later unresolved wordt, dan wordt hij dit (vaak wel initial value). + * x Take - react options gebruiken buiten react. In een derivable chain, halverwege die options gebruiken. + * -- e.g. (from)Promise. Zodra die een waarde aanneemt kan hij niet meer veranderen. + * Let FromPromise, FromObservable, FromEventPattern ook uit (in utils?), ToPromise, ToObservable, in praktijk ook handig. + * FromEventPattern (haily mary, als alles niet werkt, dan dit doen). + */ /** * In the intro we have seen a basic usage of the `.react()` method. * Let's dive a bit deeper into the details of this method. @@ -76,7 +98,6 @@ describe('reacting', () => { * Time to react to `myAtom$` with the `reactor()` function defined * above. */ - myAtom$.react(reactor); // myAtom$.react(val => reactor(val)); // Alternatively, this would work too. // myAtom$.react((val, _) => reactor(val)); // Or this. @@ -143,7 +164,6 @@ describe('reacting', () => { * In the reaction below, use the stopper callback to stop the * reaction */ - myAtom$.react((val, stopper) => { reactor(val); stopper(); @@ -247,7 +267,11 @@ describe('reacting', () => { * Use `!string$.get()` to return `true` when the `string` is * empty. */ - string$.react(reactor, { until: () => !string$.get() }); + const stringEmpty = function () { + return !string$.get(); + }; + string$.react(reactor, { until: stringEmpty }); + // string$.react(reactor, { until: () => !string$.get() }); // Or, alternatively, in a single line: // It should react as usual: string$.set('New value'); @@ -274,7 +298,7 @@ describe('reacting', () => { * Try using the first parameter of the `until` function to do * the same as above. */ - string$.react(reactor, { until: s => !s.get() }); + string$.react(reactor, { until: parent$ => !parent$.get() }); // It should react as usual. string$.set('New value'); @@ -294,24 +318,28 @@ describe('reacting', () => { * Sometimes, the syntax may leave you confused. */ it('syntax issues', () => { - // It looks this will start reacting until `boolean$`s value is false... - let stopper = boolean$.react(reactor, { until: b => !b }); + boolean$.set(true); + // It looks this will keep reacting until `boolean$`s value is set to false... + let stopper = boolean$.react(reactor, { until: b$ => !b$ }); - // ...but does it? (Remember: `boolean$` starts out as `false`) + boolean$.set(false); + + // ...but does it? Is the reactor still connected? expect(boolean$.connected).toBe(true); - // The `b` it obtains as argument is a `Derivable`. This is a - // reference value which will evaluate to `true` as it is not `undefined`. - // Thus, the negation will evaluate to `false`, independent of the value of - // the boolean. You can get the boolean value our of the `Derivable` using `.get()`: + // The `b$` it obtains as argument is a `Derivable`. This is a + // reference value. Because we apply a negation to this, `b$` is coerced to a + // boolean value, which will evaluate to `true` as `b$` is not `undefined`. + // Thus, the whole expression will evaluate to `false`, independent of the value of + // `boolean$`. Instead, you can get the value out of the `Derivable` using `.get()`: stopper(); // reset - stopper = boolean$.react(reactor, { until: b => !b.get() }); + stopper = boolean$.react(reactor, { until: b$ => !b$.get() }); expect(boolean$.connected).toBe(false); // You can also return the `Derivable` after appling the negation - // using the method designed for negating Derivables: + // using the method designed for negating the boolean within a `Derivable`: stopper(); - boolean$.react(reactor, { until: b => b.not() }); + boolean$.react(reactor, { until: b$ => b$.not() }); expect(boolean$.connected).toBe(false); }); }); @@ -340,7 +368,7 @@ describe('reacting', () => { * * *Hint: remember the `.is()` method from tutorial 2?* */ - sherlock$.react(reactor, { from: sherlock$.is('dear') }); + sherlock$.react(reactor, { from: parent$ => parent$.is('dear') }); expectReact(0); ['Elementary,', 'my', 'dear', 'Watson'].forEach(txt => sherlock$.set(txt)); @@ -366,7 +394,8 @@ describe('reacting', () => { * Now, let's react to all even numbers. * Except 4, we don't want to make it too easy now. */ - count$.react(reactor, { when: v => v.get() % 2 === 0 && v.get() !== 4 }); + count$.react(reactor, { when: parent$ => parent$.get() % 2 === 0 && parent$.get() !== 4 }); + // count$.react(reactor, { when: parent$ => parent$.derive(value => value % 2 === 0 && value !== 4) }); // Or, alternatively: expectReact(1, 0); @@ -393,19 +422,20 @@ describe('reacting', () => { * * Say you want to react when `count$` is larger than 3. But not the first time... */ - count$.react(reactor, { when: d => d.get() > 3, skipFirst: true }); + count$.react(reactor, { when: parent$ => parent$.get() > 3, skipFirst: true }); + // count$.react(reactor, { when: parent$ => parent$.derive(value => value > 3), skipFirst: true }); // Or, alternatively: expectReact(0); for (let i = 0; i <= 5; i++) { count$.set(i); } - expectReact(1, 5); // it should have skipped the 4 + expectReact(1, 5); // it should have skipped the 4 and only reacted to the 5 for (let i = 0; i <= 5; i++) { count$.set(i); } - expectReact(3, 5); // now it should not have skipped the 4 + expectReact(3, 5); // now it should have reacted to the 4 and 5 (and the 5 of last time) }); /** @@ -417,28 +447,29 @@ describe('reacting', () => { * can be very useful. */ it('reacting `once`', () => { - const finished$ = atom(false); + const count$ = atom(0); /** * ** Your Turn ** * - * Say you want to react when `finished$` is true. It can not finish - * twice. + * Say you want to react when `count$` is higher than 3. But only the first time... * * *Hint: you will need to combine `once` with another option* */ - finished$.react(reactor, { once: true, when: f => f }); // `f => f.get()` is fine as well + count$.react(reactor, { once: true, when: parent$ => parent$.get() > 3 }); + // count$.react(reactor, { once: true, when: parent$ => parent$.derive(value => value > 3) }); // Or, alternatively: expectReact(0); - // When finished it should react once. - finished$.set(true); - expectReact(1, true); + for (let i = 0; i <= 5; i++) { + count$.set(i); + } + expectReact(1, 4); // it should have only registered the 4 and not the 5 - // After that it should really be finished. :-) - finished$.set(false); - finished$.set(true); - expectReact(1, true); + for (let i = 0; i <= 5; i++) { + count$.set(i); + } + expectReact(1, 4); // and after that, it should really be finished. :-) }); }); @@ -456,37 +487,36 @@ describe('reacting', () => { * and `when` is true or unset. If e.g. `when` evaluates to false, `skipFirst` cannot trigger. */ it('`from` and `until`', () => { - const myAtom$ = atom(0); - myAtom$.react(reactor, { from: v => v.is(3), until: v => v.is(2) }); + const myAtom$ = atom(0); + myAtom$.react(reactor, { from: parent$ => parent$.is(3), until: parent$ => parent$.is(2) }); for (let i = 1; i <= 5; i++) { myAtom$.set(i); } // The reactor starts reacting when `myAtom` gets the value 3, but stops when it gets the value 2. - // But because `myAtom` obtains the value 2 before it obtains 3... + // But because `myAtom$` obtains the value 2 before it obtains 3... // ...how many times was the reactor called, if any? - expectReact(3, 5); // `from` evaluates before `until`. + expectReact(3, 5); // `from` evaluates before `until`, so it reacted to 3, 4 and 5. }); it('`when` and `skipFirst`', () => { - const myAtom$ = atom(0); + const myAtom$ = atom(0); myAtom$.react(reactor, { when: v => v.is(1), skipFirst: true }); myAtom$.set(1); - // The reactor reacts when `myAtom` is 1 but skips the first number. - // The first number of `myAtom` is 0, its initial number. - // Does the reactor skip the 0 or the 1? - expectReact(0); // `skipFirst` triggers only when `when` evaluates to true. + // The reactor reacts when `myAtom$` is 1 but skips the first number. + // `myAtom$` starts out at 0. Does the reactor skip only the 0 or also the 1? + expectReact(0); // `skipFirst` triggers only when `when` evaluates to true, so it also skips the 1. }); it('`from`, `until`, `when`, `skipFirst`, and `once`', () => { - const myAtom$ = atom(0); + const myAtom$ = atom(0); myAtom$.react(reactor, { - from: v => v.is(5), - until: v => v.is(1), - when: v => [2, 3, 4].includes(v.get()), + from: parent$ => parent$.is(5), + until: parent$ => parent$.is(1), + when: parent$ => [2, 3, 4].includes(parent$.get()), skipFirst: true, once: true, }); @@ -495,16 +525,14 @@ describe('reacting', () => { myAtom$.set(v); } - // `from` and `until` allow the reactor to respectively start when `myAtom` has value 5, and stop when it has value 1. + // `from` and `until` allow the reactor to respectively start when `myAtom$` has value 5, and stop when it has value 1. // Meanwhile, `when` allows neither of those values and only allows the values 2, 3, and 4. // `skipFirst` and `once` are also added, just to bring the whole group together. // so, how many times is the reactor called, and what was the last argument (if any)? - expectReact(1, 3); // `from` makes it start at the first `5`. `when` allows the next `4`,`3`, and `2`, but - // `skipFirst` ensures that the first `4` is skipped. `once` then ensures that only the `3` - // reacted to. Before the `until` can trigger from a `1`, the `once` has already stopped it. - + // `skipFirst` ensures that the first `4` is skipped. `once` then ensures that only the `3` is + // reacted to. Before the `until` can trigger from a `1`, the `once` has already stopped the reactor. }); }); @@ -514,17 +542,18 @@ describe('reacting', () => { /** * ** Your Turn ** * - * `connected$` indicates the current connection status: + * `connected$` indicates the current connection status. It is one of: * > 'connected'; * > 'disconnected'; * > 'standby'. * * We want our reactor to trigger once, when the device is not connected, - * which means it is either `standby` or `disconnected` (eg for cleanup). + * (`standby` or `disconnected`), e.g. for cleanup. However, we do not want + * it to trigger right away, even though we start at `disconnected`. * - * This should be possible with three simple ReactorOptions + * This should be possible with three simple ReactorOptions. */ - connected$.react(reactor, { when: s => s.is('connected').not(), skipFirst: true, once: true }); + connected$.react(reactor, { when: parent$ => parent$.is('connected').not(), skipFirst: true, once: true }); // It starts as 'disconnected' expectReact(0); diff --git a/generated_solution/4 - inner workings.test.ts b/solution/4 - inner workings.test.ts similarity index 95% rename from generated_solution/4 - inner workings.test.ts rename to solution/4 - inner workings.test.ts index 9d838cf..241460e 100644 --- a/generated_solution/4 - inner workings.test.ts +++ b/solution/4 - inner workings.test.ts @@ -1,13 +1,6 @@ import { atom } from '@skunkteam/sherlock'; import { Seq } from 'immutable'; -/** - * ** Your Turn ** - * - * If you see this variable, you should do something about it. :-) - */ -export const __YOUR_TURN__ = {} as any; - /** * Time to dive a bit deeper into the inner workings of `@skunkteam/sherlock`. */ @@ -43,7 +36,6 @@ describe('inner workings', () => { * * What do you expect? */ - expect(reacted).toHaveBeenCalledTimes(1); expect(reacted).toHaveBeenLastCalledWith(1, expect.toBeFunction()); // Note: the reactor doesn't know that changing `string$` will not generate a different @@ -58,7 +50,6 @@ describe('inner workings', () => { * * What do you expect? */ - expect(reacted).toHaveBeenCalledTimes(2); expect(reacted).toHaveBeenLastCalledWith(2, expect.toBeFunction()); // As it got a different value (`2` instead of `1`), it triggered. @@ -72,10 +63,9 @@ describe('inner workings', () => { * * What do you expect now? */ - expect(reacted).toHaveBeenCalledTimes(3); expect(reacted).toHaveBeenLastCalledWith('two', expect.toBeFunction()); - + // As it got a different value (`two` instead of `2`), it triggered. }); /** @@ -97,7 +87,7 @@ describe('inner workings', () => { * not called `.get()` on that new `Derivable`. * * How many times do you think the `hasDerived` function has been - * called? 0 is also an option of course. + * called? */ // Well, what do you expect? @@ -154,7 +144,7 @@ describe('inner workings', () => { myDerivation$.get(); - expect(hasDerived).toHaveBeenCalledTimes(1); // no update because someone is reacting, and there has been no update in value. + expect(hasDerived).toHaveBeenCalledTimes(1); // no update: someone is reacting, and there has been no update in value. myAtom$.set(false); @@ -259,7 +249,7 @@ describe('inner workings', () => { const hasReacted = jest.fn(); atom$.react(hasReacted, { skipFirst: true }); - expect(hasReacted).toHaveBeenCalledTimes(0); // added for clarity, in case people missed the `skipFirst` or its implication + expect(hasReacted).toHaveBeenCalledTimes(0); atom$.set({}); @@ -269,7 +259,7 @@ describe('inner workings', () => { * The `Atom` is set with exactly the same object as before. Will the * `.react()` fire? */ - expect(hasReacted).toHaveBeenCalledTimes(1); // `{} !== {}`, as they have different references + expect(hasReacted).toHaveBeenCalledTimes(1); // `{} !== {}`, as they are different references /** * But what if you use an object, that can be easily compared through a @@ -305,7 +295,7 @@ describe('inner workings', () => { * First we check `Object.is()` equality, if that is true, it is the * same, you can't deny that. * - * After that it is pluggable. It can be anything you want. + * After that it is pluggable. It can be anything you want. TODO: what is pluggable? * * By default we try to use `.equals()`, to support libraries like * `ImmutableJS`. diff --git a/generated_solution/5 - unresolved.test.ts b/solution/5 - unresolved.test.ts similarity index 84% rename from generated_solution/5 - unresolved.test.ts rename to solution/5 - unresolved.test.ts index 71fa8bb..0097345 100644 --- a/generated_solution/5 - unresolved.test.ts +++ b/solution/5 - unresolved.test.ts @@ -1,12 +1,5 @@ import { atom, Derivable, DerivableAtom } from '@skunkteam/sherlock'; -/** - * ** Your Turn ** - * - * If you see this variable, you should do something about it. :-) - */ -export const __YOUR_TURN__ = {} as any; - /** * Sometimes your data isn't available yet. For example if it is still being * fetched from the server. At that point you probably still want your @@ -34,7 +27,7 @@ describe('unresolved', () => { * * Resolve the atom, it's pretty easy */ - myAtom$.set(1); + myAtom$.set(1); // setting it to any value will unresolve it expect(myAtom$.resolved).toBeTrue(); }); @@ -142,7 +135,7 @@ describe('unresolved', () => { * * Combine the two `Atom`s into one `Derivable` */ - const myDerivable$: Derivable = myString$.derive(s => s + myOtherString$.get()); + const myDerivable$: Derivable = myString$.derive(parent$ => parent$ + myOtherString$.get()); /** * ** Your Turn ** @@ -175,4 +168,30 @@ describe('unresolved', () => { myString$.unset(); expect(myDerivable$.resolved).toEqual(false); }); + + /** + * It is nice to be able to have a backup plan when a Derivable gets unresolved. + * The `.fallbackTo()` function allows you to specify a default value + * whenever your Derivable gets unset. + */ + it('Fallback-to', () => { + const myAtom$ = atom(0); + + /** + * ** Your Turn ** + * Use the `.fallbackTo()` method to create a `mySafeAtom$` which + * gets the backup value `3` when `myAtom$` becomes unresolved. + */ + const mySafeAtom$ = myAtom$.fallbackTo(() => 3); + + expect(myAtom$.value).toBe(0); + expect(mySafeAtom$.value).toBe(0); + + myAtom$.unset(); + + expect(myAtom$.resolved).toBeFalse(); + expect(mySafeAtom$.resolved).toBeTrue(); + expect(myAtom$.value).toBeUndefined(); + expect(mySafeAtom$.value).toBe(3); + }); }); diff --git a/solution/6 - errors.test.ts b/solution/6 - errors.test.ts new file mode 100644 index 0000000..7cce76f --- /dev/null +++ b/solution/6 - errors.test.ts @@ -0,0 +1,114 @@ +import { atom, DerivableAtom, error } from '@skunkteam/sherlock'; + +/** + * Errors are a bit part of any programming language, and Sherlock has its own custom errors + * and ways to deal with them. + */ +describe('errors', () => { + let myAtom$: DerivableAtom; + + beforeEach(() => { + myAtom$ = atom(1); + }); + + it('basic errors', () => { + // The `errored` property of a Derivable shows whether it is in an error state - meaning that + // the last statement resulted in an error + expect(myAtom$.errored).toBe(false); + expect(myAtom$.error).toBeUndefined; // by default, the `error` message is undefined. + + // We can set errors using the `setError()` function. + myAtom$.setError('my Error'); + + expect(myAtom$.errored).toBe(true); + expect(myAtom$.error).toBe('my Error'); + + // expect(myAtom$.get).toThrow("Cannot read properties of undefined (reading 'getState')"); + // TODO: WHAT - normally this works, but internal JEST just fucks with me....? + + // What will happen if you try to call `get()` on `myAtom$`? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myAtom$.get()).toThrow('my Error'); + + // ** __YOUR_TURN__ ** + // What will happen if you try to call `set()` on `myAtom$`? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myAtom$.set(2)).not.toThrow(); + expect(myAtom$.errored).toBe(false); + + // Interestingly, calling `set()` does not throw an error. In fact, it removes the error state + // altogether. This means we can now call `get()` again. + expect(() => myAtom$.get()).not.toThrow(); + }); + + /** + * libs/sherlock/src/lib/interfaces.ts:289 shows the basic states that a Derivable can have. + * > `export type State = V | unresolved | ErrorWrapper;` + * A state can be either any type `V` (`number`, `string`, etc.), `unresolved` as we saw in the + * previous tutorial, or `ErrorWrapper`. This last state is explained here. + */ + it('error states', () => { + expect(myAtom$.getState()).toBe(1); // as explained above, any type can be a state + + myAtom$.setError('my Error'); + + // The `ErrorWrapper` state only holds an error string. The `error()` function returns + // such an `ErrorWrapper` which we can use to compare. + expect(myAtom$.getState()).toMatchObject(error('my Error')); + + // TODO: more! There wasn't a question in here. Maybe combine with Final States? NO, that one should go! + }); + + it('deriving an error', () => { + const myDerivable$ = myAtom$.derive(v => v + 1); + + // If `myAtom$` suddenly errors... + myAtom$.setError('division by zero'); + + // ...what happens to `myDerivable$`? + expect(myDerivable$.errored).toBe(true); + + // If any Derivable tries to derive from an atom in an error state, + // this Derivable will itself throw an error too. This makes sense, + // given that it cannot obtain the value it needs. + }); + + it('reacting to an error', () => { + // Setting an error to an Atom generally does not throw an error. + expect(() => myAtom$.setError('my Error')).not.toThrow(); + + myAtom$.set(1); + + // Now we set a reactor to `myAtom$`. However, this reactor does not use the value of `myAtom$`. + const reactor = jest.fn(); // empty function body + myAtom$.react(reactor); + + // ** __YOUR_TURN__ ** + // Will an error be thrown when `myAtom$` is now set to an error state? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myAtom$.setError('my Error')).toThrow('my Error'); + + // ** __YOUR_TURN__ ** + // Is the reactor still connected now that it errored? + expect(myAtom$.connected).toBe(false); + + // Reacting to a Derivable that throws an error will make the reactor throw as well. + // Because the reactor will usually fire when it gets connected, it also throws when + // you try to connect it after the error has already been set. + + myAtom$ = atom(1); + myAtom$.setError('my second Error'); // + + // ** __YOUR_TURN__ ** + // Will an error be thrown when you use `skipFirst`? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myAtom$.react(reactor, { skipFirst: true })).toThrow('my second Error'); + + // And will an error be thrown when `from = false`? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myAtom$.react(reactor, { from: false })).not.toThrow(); + + // When `from = false`, the reactor is disconnected, preventing the error message from entering. + // `skipFirst`, on the other hand, does allow the error in, but does not trigger an update. + }); +}); diff --git a/generated_solution/7 - advanced.test.ts b/solution/7 - advanced.test.ts similarity index 91% rename from generated_solution/7 - advanced.test.ts rename to solution/7 - advanced.test.ts index 18c8b6c..ceff9da 100644 --- a/generated_solution/7 - advanced.test.ts +++ b/solution/7 - advanced.test.ts @@ -1,14 +1,7 @@ import { atom, constant, Derivable, derive, SettableDerivable, unresolved } from '@skunkteam/sherlock'; -import { lift, template } from '@skunkteam/sherlock-utils'; +import { template } from '@skunkteam/sherlock-utils'; import { Map as ImmutableMap } from 'immutable'; -/** - * ** Your Turn ** - * - * If you see this variable, you should do something about it. :-) - */ -export const __YOUR_TURN__ = {} as any; - describe('advanced', () => { /** * In the case a `Derivable` is required, but the value is immutable. @@ -36,7 +29,7 @@ describe('advanced', () => { }); it('`templates`', () => { - // Staying in the theme of redefining normal Typescript code in our Derivable language, + // Staying in the theme of redefining normal Typescript code in our Sherlock language, // we also have a special syntax to copy template literals to a Derivable. const one = 1; const myDerivable = template`I want to go to ${one} party`; @@ -85,7 +78,7 @@ describe('advanced', () => { * Use the `.take()` method on `myAtom$` to only accept the input string * when it is `allowed`. */ - const myLimitedAtom$ = myAtom$.take({ when: v => v.is('allowed') }); + const myLimitedAtom$ = myAtom$.take({ when: parent$ => parent$.is('allowed') }); expect(myLimitedAtom$.resolved).toBe(false); myAtom$.set('allowed'); @@ -154,13 +147,11 @@ describe('advanced', () => { * We just created two `Derivable`s that are almost exactly the same. * But what happens when their source becomes `unresolved`? */ - expect(usingGet$.resolved).toEqual(true); expect(usingVal$.resolved).toEqual(true); myAtom$.unset(); expect(usingGet$.resolved).toEqual(false); expect(usingVal$.resolved).toEqual(true); - }); }); @@ -178,12 +169,13 @@ describe('advanced', () => { it('triggers when the source changes', () => { const myAtom$ = atom(1); + /** * ** Your Turn ** * * Use the `.map()` method to create the expected output below */ - const mappedAtom$: Derivable = myAtom$.map(base => base.toString().repeat(base)); + const mappedAtom$: Derivable = myAtom$.map(value => value.toString().repeat(value)); mappedAtom$.react(mapReactSpy); @@ -208,13 +200,13 @@ describe('advanced', () => { expect(deriveReactSpy).toHaveBeenCalledExactlyOnceWith('ho', expect.toBeFunction()); myRepeat$.value = 3; + /** * ** Your Turn ** * * We changed`myRepeat$` to equal 3. * Do you expect both reactors to have fired? And with what? */ - expect(deriveReactSpy).toHaveBeenCalledTimes(2); expect(deriveReactSpy).toHaveBeenLastCalledWith('hohoho', expect.toBeFunction()); @@ -222,12 +214,12 @@ describe('advanced', () => { expect(mapReactSpy).toHaveBeenLastCalledWith('hohoho', expect.toBeFunction()); myString$.value = 'ha'; + /** * ** Your Turn ** * * And now that we have changed `myString$`? And when `myRepeat$` changed again? */ - expect(deriveReactSpy).toHaveBeenCalledTimes(3); expect(deriveReactSpy).toHaveBeenLastCalledWith('hahaha', expect.toBeFunction()); @@ -240,7 +232,6 @@ describe('advanced', () => { expect(mapReactSpy).toHaveBeenCalledTimes(3); expect(mapReactSpy).toHaveBeenLastCalledWith('haha', expect.toBeFunction()); - /** * As you can see, a change in `myString$` will not trigger an * update. But if an update is triggered, `myString$` will be called @@ -266,7 +257,7 @@ describe('advanced', () => { // This first function is called when getting... n => -n, // ...and this second function is called when setting. - (newV, _) => -newV, + n => -n, ); // The original `atom` was set to 1, so we want the inverse to @@ -280,37 +271,33 @@ describe('advanced', () => { expect(myInverse$.get()).toEqual(-2); }); + /** + * The `.map()` used here is similar to the `.map()` used on arrays. + * Both get values out of a container (`Array` or `Derivable`), apply + * some function, and put it back in the container. + */ it('similar to `map()` on arrays', () => { - // If the similarity is not clear yet, here is a comparison between - // the normal `.map()` on arrays and our `Derivable` `.map()`. - // Both get values out of a container (`Array` or `Derivable`), apply - // some function, and put it back in the container. - const addOne = jest.fn((v: number) => v + 1); - const myList = [1, 2, 3]; + const myList = [1]; const myMappedList = myList.map(addOne); - expect(myMappedList).toMatchObject([2, 3, 4]); + expect(myMappedList).toMatchObject([2]); const myAtom$ = atom(1); let myMappedDerivable$ = myAtom$.map(addOne); expect(myMappedDerivable$.value).toBe(2); - // Or, as we have seen before, you can use `lift()` for this. - myMappedDerivable$ = lift(addOne)(myAtom$); - expect(myMappedDerivable$.value).toBe(2); - // You can combine them too. - const myAtom2$ = atom([1, 2, 3]); + const myAtom2$ = atom([1]); const myMappedDerivable2$ = myAtom2$.map(v => v.map(addOne)); - expect(myMappedDerivable2$.value).toMatchObject([2, 3, 4]); + expect(myMappedDerivable2$.value).toMatchObject([2]); }); /** * In order to reason over the state of a Derivable, we can * use `.mapState()`. This will map one state to another, and * can be used to get rid of pesky `unresolved` or `Errorwrapper` - * states (or to introduce them!). + * states. */ it('`.mapState()`', () => { const myAtom$ = atom(1); @@ -359,7 +346,7 @@ describe('advanced', () => { expect(myMappedAtom$.get()).toBe(3); // the state and value are linked, so this is identical to `.getState()` /** * You might think that this change in state would cause `myAtom$` to now also get - * `3` as its state, but this does not happen. Why not? TODO: maximally one cycle? + * `3` as its state, but this does not happen. Why not? TODO: maximally one cycle? ASK! * Since both `2` and `3` are not `unresolved`, it does not matter to our answer. * * The same cannot be said for the fourth case. Setting `myMappedAtom$` to `unresolved` @@ -369,6 +356,22 @@ describe('advanced', () => { * TODO: change this to be for MAP. Then make MAPSTATE a trivial one right after. */ }); + + // FIXME: + it('TEMP Flat-map', () => { + // const myAtom$ = atom(0); + // const mapping = (v: any) => atom(v); + // Sometimes you use `map()`, but the result within the `map()` is also a Derivable. + // The result would here be a `Derivable>` (hover over `derive` to see this). + // You would have to use `.get()` to go back to a single Derivable. Similarly how `flatMap` can + // reduce lists of lists to a single list, it can help reduce Derivables of Derivables to a + // single Derivable. If you have something like this: + // let myAtom$$ = myAtom$.map(n => mapping(n)).derive(v => v.get()); + // You can now rewrite it to this: + // myAtom$$ = myAtom$.flatMap(n => mapping(n)); + // It only results in slightly shorter code. + // TODO: right? + }); }); /** @@ -492,12 +495,10 @@ describe('advanced', () => { * So what if we set `firstProp$`? Does this propagate to the source * `Derivable`? */ - firstProp$.set('new value'); expect(reactSpy).toHaveBeenCalledTimes(1); expect(myMap$.get().get('firstProp')).toEqual('new value'); expect(myMap$.get().get('secondProp')).toEqual('secondValue'); - }); }); }); diff --git a/solution/8 - utils.test.ts b/solution/8 - utils.test.ts new file mode 100644 index 0000000..a210a0a --- /dev/null +++ b/solution/8 - utils.test.ts @@ -0,0 +1,620 @@ +import { atom, constant, derive, FinalWrapper } from '@skunkteam/sherlock'; +import { fromPromise, lift, pairwise, peek, scan, struct } from '@skunkteam/sherlock-utils'; + +/** + * In the `sherlock-utils` lib, there are a couple of functions that can combine + * multiple values of a single `Derivable` or combine multiple `Derivable`s into + * one. We will show a couple of those here. + */ +describe('utils', () => { + /** + * As the name suggests, `pairwise()` will call the given function with both + * the current and the previous state. + * + * *Note: functions like `pairwise` and `scan` can be used with any callback, + * so it can be used both in a `.derive()` step and in a `.react()`* + */ + it('pairwise', () => { + const myCounter$ = atom(1); + const reactSpy = jest.fn(); + + /** + * ** Your Turn ** + * + * Now, use `pairwise()` to subtract the previous value from the + * current. + * + * *Hint: check the overloads of pairwise if you're struggling with + * `oldValue`.* + * + * Note: don't call `pairwise()` using a lambda function! + */ + myCounter$.derive(pairwise((newVal, oldVal) => newVal - oldVal, 0)).react(reactSpy); + + expect(reactSpy).toHaveBeenCalledTimes(1); + expect(reactSpy).toHaveBeenLastCalledWith(1, expect.toBeFunction()); + + myCounter$.set(3); + + expect(reactSpy).toHaveBeenCalledTimes(2); + expect(reactSpy).toHaveBeenLastCalledWith(2, expect.toBeFunction()); // 3 (current value of `myCounter$`) - 1 (previous value of `myCounter$`) + + myCounter$.set(10); + + expect(reactSpy).toHaveBeenCalledTimes(3); + expect(reactSpy).toHaveBeenLastCalledWith(7, expect.toBeFunction()); // 10 (current value of `myCounter$`) - 3 (previous value of `myCounter$`) + + myCounter$.set(20); + + // ** Your Turn ** + // What will the next output be? + expect(reactSpy).toHaveBeenCalledTimes(4); + expect(reactSpy).toHaveBeenLastCalledWith(10, expect.toBeFunction()); // 20 (current value of `myCounter$`) - 10 (previous value of `myCounter$`) + }); + + /** + * `scan()` is the `Derivable` version of `Array.prototype.reduce()`. It will be + * called with the current state and the last emitted value. + * + * (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce) + * + * *Note: as with `pairwise()` this is useable in both a `.derive()` and + * `.react()` method* + */ + it('scan', () => { + const myCounter$ = atom(1); + const reactSpy = jest.fn(); + + /** + * ** Your Turn ** + * + * Now, use `scan()` to subtract the previous value from the + * current. + * + * Note that `scan()` must return the same type as it gets as input. This is required + * as this returned value is also used for the accumulator (`acc`) value for the next call. + * This `acc` parameter of `scan()` is the last returned value, not the last value + * of `myCounter$`, as is the case with `pairwise()`. + * + * Note: don't call `pairwise()` using a lambda function! + */ + myCounter$.derive(scan((acc, val) => val - acc, 0)).react(reactSpy); + + expect(reactSpy).toHaveBeenCalledTimes(1); + expect(reactSpy).toHaveBeenLastCalledWith(1, expect.toBeFunction()); + + myCounter$.set(3); + + expect(reactSpy).toHaveBeenCalledTimes(2); + expect(reactSpy).toHaveBeenLastCalledWith(2, expect.toBeFunction()); // 3 (current value of `myCounter$`) - 1 (previous returned value) + + myCounter$.set(10); + + expect(reactSpy).toHaveBeenCalledTimes(3); + expect(reactSpy).toHaveBeenLastCalledWith(8, expect.toBeFunction()); // 10 (current value of `myCounter$`) - 2 (previous returned value) + + myCounter$.set(20); + + // ** Your Turn ** + // What will the next output be? + expect(reactSpy).toHaveBeenCalledTimes(4); + expect(reactSpy).toHaveBeenLastCalledWith(12, expect.toBeFunction()); // 20 (current value of `myCounter$`) - 8 (previous returned value) + }); + + it('`pairwise()` on normal arrays', () => { + // Functions like `pairwise()` and `scan()` work on normal lists too. They are often + // used in combination with `.map()` and `.filter()`. + const myList = [1, 2, 3, 5, 10]; + let myList2: number[]; + + /** + * ** Your Turn ** + * + * Use a `pairwise()` combined with a `.map()` on `myList` + * to subtract the previous value from the current. + * + * Note: don't call `pairwise()` using a lambda function! + */ + myList2 = myList.map(pairwise((newV, oldV) => newV - oldV, 0)); + expect(myList2).toMatchObject([1, 1, 1, 2, 5]); + + // However, we should be careful with this, as this does not always behave as intended. + // Particularly, what exactly happens when we do call `pairwise()` using a lambda function? + myList2 = myList.map(v => pairwise((newV, oldV) => newV - oldV, 0)(v)); // copy the same implementation here + expect(myList2).toMatchObject([1, 2, 3, 5, 10]); + + // Even if we are more clear about what we pass, this unintended behavior does not go away. + myList2 = myList.map((v, _, _2) => pairwise((newV, oldV) => newV - oldV, 0)(v)); // copy the same implementation here + expect(myList2).toMatchObject([1, 2, 3, 5, 10]); + + // `pairwise()` keeps track of the previous value under the hood. Using a lambda of + // the form `v => pairwise(...)(v)` would create a new `pairwise` function every call, + // essentially resetting the previous value every call. And resetting the previous value + // to 0 causes the input to stay the same (after all: x - 0 = x). + // Other than by not using a lambda function, we can fix this by + // saving the `pairwise` in a variable and reusing it for every call. + + let f = pairwise((newV, oldV) => newV - oldV, 0); + myList2 = myList.map(v => f(v)); + expect(myList2).toMatchObject([1, 1, 1, 2, 5]); + + // To get more insight in the `pairwise()` function, you can call it + // manually. Here, we show what happens under the hood. + + f = pairwise((newV, oldV) => newV - oldV, 0); + + myList2 = []; + myList2[0] = f(myList[0]); // `f` is newly created with `init = 0`, so applies `1 - 0 = 1`. + myList2[1] = f(myList[1]); // `f` has saved `1` internally, so applies `2 - 1 = 1`. + myList2[2] = f(myList[2]); // `f` has saved `2` internally, so applies `3 - 2 = 1`. + myList2[3] = f(myList[3]); // `f` has saved `3` internally, so applies `5 - 3 = 2`. + myList2[4] = f(myList[4]); // `f` has saved `5` internally, so applies `10 - 5 = 5`. + + expect(myList2).toMatchObject([1, 1, 1, 2, 5]); + + // This also works for functions other than `.map()`, such as `.filter()`. + + /** ** Your Turn ** + * Use `pairwise()` to filter out all values which produce `1` when subtracted + * with their previous value. + * Note that the function `f` still requires a number to be the return value. + * Checking for equality therefore cannot be done directly within `f`. + */ + f = pairwise((newV, oldV) => newV - oldV, 0); + myList2 = myList.filter(v => f(v) === 1); + + expect(myList2).toMatchObject([1, 2, 3]); // only the numbers `1`, `2`, and `3` produce 1 when subtracted with the previous value + }); + + it('`scan()` on normal arrays', () => { + // As with `pairwise()` in the last test, `scan()` can be used with arrays too. + const myList = [1, 2, 3, 5, 10]; + let myList2: number[]; + + /** + * ** Your Turn ** + * + * Use a `scan()` combined with a `.map()` on `myList` + * to subtract the previous value from the current. + */ + let f: (v: number) => number = scan((acc, val) => val - acc, 0); + myList2 = myList.map(f); + + expect(myList2).toMatchObject([1, 1, 2, 3, 7]); + + // again, it is useful to consider what happens internally. + f(7); // reset -- this resets the internal `acc` value to 0, as the current `acc` value was 7, and 7-7 = 0. + + myList2 = []; + myList2[0] = f(myList[0]); // `f` is newly created with `init = 0`, so applies `1 - 0 = 1`. + myList2[1] = f(myList[1]); // `f` has saved the result `1` internally, so applies `2 - 1 = 1`. + myList2[2] = f(myList[2]); // `f` has saved the result `1` internally, so applies `3 - 1 = 2`. + myList2[3] = f(myList[3]); // `f` has saved the result `2` internally, so applies `5 - 2 = 3`. + myList2[4] = f(myList[4]); // `f` has saved the result `3` internally, so applies `10 - 3 = 7`. + + expect(myList2).toMatchObject([1, 1, 2, 3, 7]); + + // This also works for functions other than `map()`, such as `filter()`. + + /** + * ** Your Turn ** + * Use `scan()` to filter out all values from `myList` which produce a value + * of 8 or higher when added with the previous result. In other words, it should + * go through `myList` and add the values producing: (1), (1+2), (1+2+3), (1+2+3+5), + * (1+2+3+5+10), and since this sum only prouces a value higher than 8 when the + * values `5` and `10` are added, the result should be `[5,10]`. + */ + f = scan((acc, val) => val + acc, 0); + myList2 = myList.filter(v => f(v) >= 8); + + expect(myList2).toMatchObject([5, 10]); + }); + + it('pairwise - BONUS', () => { + const myCounter$ = atom(1); + let reactSpy = jest.fn(); + + /** + * ** Your Turn ** + * ** BONUS ** + * + * Now, use `pairwise()` directly in `.react()`. Implement the same + * derivation as before: subtract the previous value from the current. + */ + reactSpy = jest.fn(pairwise((newV, oldV) => newV - oldV, 0)); + myCounter$.react(reactSpy); + + expect(reactSpy).toHaveLastReturnedWith(1); + + myCounter$.set(3); + + expect(reactSpy).toHaveLastReturnedWith(2); + + myCounter$.set(10); + + expect(reactSpy).toHaveLastReturnedWith(7); + }); + + it('scan - BONUS', () => { + const myCounter$ = atom(1); + let reactSpy = jest.fn(); + + /** + * ** Your Turn ** + * ** BONUS ** + * + * Now, use `scan()` directly in `.react()`. Implement the same + * derivation as before: subtract all the emitted values. + */ + + reactSpy = jest.fn(scan((acc, val) => val - acc, 0)); + myCounter$.react(reactSpy); + + expect(reactSpy).toHaveLastReturnedWith(1); + + myCounter$.set(3); + + expect(reactSpy).toHaveLastReturnedWith(2); + + myCounter$.set(10); + + expect(reactSpy).toHaveLastReturnedWith(8); + }); + + /** + * A `struct()` can combine an Object/Array of `Derivable`s into one + * `Derivable`, that contains the values of that `Derivable`. + * + * The Object/Array that is in the output of `struct()` will have the same + * structure as the original Object/Array. + * + * This is best explained in practice. + */ + it('struct', () => { + const allMyAtoms = { + regularProp: 'prop', + string: atom('my string'), + number: atom(1), + sub: { + string: atom('my substring'), + }, + }; + + const myOneAtom$ = struct(allMyAtoms); + + expect(myOneAtom$.get()).toEqual({ + regularProp: 'prop', + string: 'my string', + number: 1, + sub: { + string: 'my substring', + }, + }); + + // Note: we change the original object, not the struct. + allMyAtoms.regularProp = 'new value'; + allMyAtoms.sub.string.set('my new substring'); + + /** + * ** Your Turn ** + * + * Now have a look at the properties of `myOneAtom$`. Is this what you + * expect? + */ + expect(myOneAtom$.get()).toEqual({ + regularProp: 'new value', + string: 'my string', + number: 1, + sub: { + string: 'my new substring', + }, + }); + }); + + describe('lift()', () => { + /** + * Sherlock may feel like a language build on top of Typescript. Sometimes + * you might want to use normal objects and functions and not have to rewrite + * your code. + * In other words, just as keywords like `atom(V)` lifts a variable V to the higher + * level of Derivables, the `lift(F)` keyword lifts a function `F` to the higher + * level of Derivables. + */ + it('example', () => { + // Say I just finished writing this oh-so-complicated function: + const isEvenNumber = (v: number) => v % 2 == 0; + + /** + * Rewriting this function to work with derivables would now be a waste of time. + * This is especially so if you didn't even write the original function, e.g. when + * you use a library function. + * + * ** Your Turn ** + * Use the `lift()` function to change `isEvenNumber` to work on Derivables instead. + * In other words: the new function should take a `Derivable` (or more specifically: + * an `Unwrappable`) and return a `Derivable`. + */ + const isEvenDerivable = lift(isEvenNumber); + + expect(isEvenNumber(2)).toBe(true); + expect(isEvenNumber(13)).toBe(false); + expect(isEvenDerivable(atom(2)).value).toBe(true); + expect(isEvenDerivable(atom(13)).value).toBe(false); + }); + + it('`lift()` as alternative to `.map()`', () => { + // In tutorial 7, we saw `.map()` used in the following context: + const addOne = jest.fn((v: number) => v + 1); + const myAtom$ = atom(1); + + let myMappedDerivable$ = myAtom$.map(addOne); + + expect(myMappedDerivable$.value).toBe(2); + + /** + * ** Your Turn ** + * Now, use `lift()` as alternative to `.map()`. + */ + myMappedDerivable$ = lift(addOne)(myAtom$); + + expect(myMappedDerivable$.value).toBe(2); + }); + }); + + /** + * Sometimes you want to use `derive` but still want to keep certain + * variables in it untracked. In such cases, you can use `peek()`. + */ + it('`peek()`', () => { + const myTrackedAtom$ = atom(1); + const myUntrackedAtom$ = atom(2); + + /** + * ** Your Turn ** + * Use `peek()` to get the value of `myUntrackedAtom$` and add it to the + * value of `myTrackedAtom$`, which should be tracked. + */ + const reactor = jest.fn(v => v); + derive(() => myTrackedAtom$.get() + peek(myUntrackedAtom$)).react(reactor); + + expect(reactor).toHaveBeenCalledOnce(); + expect(reactor).toHaveLastReturnedWith(3); + + // there reactor should be called when `myTrackedAtom$` updates + myTrackedAtom$.set(2); + expect(reactor).toHaveBeenCalledTimes(2); + expect(reactor).toHaveLastReturnedWith(4); + + // the reactor should not be called when `myUntrackedAtom$` updates + myUntrackedAtom$.set(3); + expect(reactor).toHaveBeenCalledTimes(2); + expect(reactor).toHaveLastReturnedWith(4); + + // but when `myTrackedAtom$` updates, the value of `myUntrackedAtom$` did change + myTrackedAtom$.set(3); + expect(reactor).toHaveBeenCalledTimes(3); + expect(reactor).toHaveLastReturnedWith(6); + }); + + /** + * Similarly to the `constants` explained in tutorial 7, + * you might want to specify that a variable cannot be updated. + * This can be useful for the programmers themselves, to not + * accidentally update the variable, but it can also be useful for + * optimization. This can be done using the `final` keyword. + */ + describe('`final`', () => { + let myAtom$ = atom(1); + + beforeEach(() => { + myAtom$ = atom(1); + }); + + it('`final` basics', () => { + // Every atom has a `final` property. + expect(myAtom$.final).toBeFalse(); + + // TODO: SHOW THAT CONST ALSO GIVES THE SAME ERROR MESSAGE WHEN SET!! + + // You can make an atom final using the `.makeFinal()` function. + myAtom$.makeFinal(); + expect(myAtom$.final).toBeTrue(); + + /** + * ** Your Turn ** + * What do you think will happen when we try to `.get()` or `.set()` this atom? + */ + // .toThrow() or .not.toThrow()? ↴ + expect(() => myAtom$.get()).not.toThrow(); + expect(() => myAtom$.set(2)).toThrow('cannot set a final derivable'); + + // This behavior is consistent with normal variables created using `const`. + // Alternatively, you can set a last value before setting it to `final`, using `.setFinal()`. + // .toThrow() or .not.toThrow()? ↴ + expect(() => myAtom$.setFinal(2)).toThrow('cannot set a final derivable'); + // Remember: we try to set an atom that is already final, so we get an error + + // There is no way to 'unfinalize' a Derivable, so the only solution to reset is to + // create a whole new Derivable. + myAtom$ = atom(1); + myAtom$.setFinal(2); + expect(myAtom$.final).toBeTrue(); + + // Also interesting: a `constant` as introduced in tutorial 7 is actually a Derivable set to + // `final` in disguise. You can verify this by checking the implementation of `constant` at + // libs/sherlock/src/lib/derivable/factories.ts:39 + const myConstantAtom$ = constant(1); + expect(myConstantAtom$.final).toBe(true); + }); + + it('deriving a `final` Derivable', () => { + const myDerivable$ = myAtom$.derive(v => v + 1); + + const hasReacted = jest.fn(); + myDerivable$.react(hasReacted); + + expect(myDerivable$.final).toBeFalse(); + expect(myDerivable$.connected).toBeTrue(); + + myAtom$.makeFinal(); + + /** + * ** Your Turn ** + * + * What will happen to `myDerivable$` when I change `myAtom$` to be `final`? + */ + expect(myDerivable$.final).toBe(true); + expect(myDerivable$.connected).toBe(false); + + /** + * Derivables that are final (or constant) are no longer tracked. This can save + * a lot of memory and time by cleaning up unused data. Also, when all the variables + * that a Derivable depends on become final, that Derivable itself becomes final too. + * This chains similarly to `unresolved` and `error`. + */ + }); + + it('TODO: `final` State', () => { + /** A property such as `.final`, similar to variables like `.errored` and `.resolved` + * is useful for checking whenever a Derivable is in a certain state, but these properties + * are just a boolean. This means that these properties cannot be derived and we cannot + * have certain functions execute whenever there is a change in the state. For this reason, + * every Derivable holds an internal state, retrievable using `.getState()` which can be + * derived. TODO: Have a clear place where I explain this! Now I have info up top here too. + * + * We have seen that states (`State`) can be `undefined`, `ErrorWrapper`, + * or any regular type `V`. Other states exist, such as the `MaybeFinalState`. This state can be either + * a normal state `State` or a special `FinalWrapper>` state. Let's see that in action. + */ + expect(myAtom$.getMaybeFinalState()).toBe(1); // `getMaybeFinalState` can return a normal state, which in turn can be any normal type. + + myAtom$.makeFinal(); + + expect(myAtom$.getMaybeFinalState()).toBeInstanceOf(FinalWrapper); // but `getMaybeFinalState` can also return a `FinalWrapper` type. + expect(myAtom$.getState()).toBe(1); // the normal type is still the final it contains. + + // TODO: MAAR JE KAN EEN STATE HELEMAAL NIET DERIVEN! + // Dus dat is allemaal onzin lijkt me....??? Bovendien, kan je normale variabelen niet deriven door het gewoon te + // wrappen in een atom ofzo? Of door te structen? + }); + }); + + describe('`Promise`, `Observable`, and `EventPattern`', () => { + /** + * Sherlock can also deal with Promises using the `.fromPromise()` and `.toPromise()` functions. + * This translates Promises directly to Sherlock concepts we have discussed already. + */ + it('`fromPromise()`', async () => { + // we initialize a Promise that will resolve, not reject, when handled + let promise = Promise.resolve(15); + let myAtom$ = fromPromise(promise); + + /** + * ** Your Turn ** + * What do you think is the default state of an atom based on a Promise? + */ + expect(myAtom$.resolved).toBe(false); + expect(myAtom$.final).toBe(false); + + // Now we wait for the Promise to be handled (resolved). + await promise; + + /** + * ** Your Turn ** + * So, what will happen to `myAtom$` and `myMappedAtom$`? + */ + expect(myAtom$.get()).toBe(15); + expect(myAtom$.final).toBe(true); + + // Now we make a promise that is rejected when called. + promise = Promise.reject('Oh no, I messed up!'); + myAtom$ = fromPromise(promise); + + // We cannot await the Promise itself, as it would immediately throw. + await Promise.resolve(); + + /** + * ** Your Turn ** + * So, what will happen to `myAtom$` now? + */ + expect(myAtom$.errored).toBe(true); + expect(myAtom$.error).toBe('Oh no, I messed up!'); + expect(myAtom$.final).toBe(true); + }); + + it('`.toPromise()`', async () => { + /** + * `.toPromise()` returns a promise that is linked to the atom it is based on (`myAtom$` here) + * If the atom has a value, the promise is resolved. If the atom errors, the promise is rejected using the same error. + * And it the atom is unresolved, the promise is pending. + */ + let myAtom$ = atom('initial value'); + let promise = myAtom$.toPromise(); + + /** + * ** Your Turn ** + * What do you think will happen when we try to set the atom with a value? + */ + myAtom$.set('second value'); + expect(await promise).toBe('initial value'); // `myAtom$` starts with a value ('initial value'), so the promise is immediately resolved + + myAtom$.unset(); + promise = myAtom$.toPromise(); + + /** + * ** Your Turn ** + * We set the atom to `unresolved`. What will now happen when we try to set the atom with a value? + */ + myAtom$.set('third value'); + expect(await promise).toBe('third value'); // This is now the first value the atom obtains since the promise was created. + + // Whenever an atom is in an `unresolved` state, the corresponding Promise is pending. + // This means that the Promise can still become resolved or rejected depending on the atom's actions. + + myAtom$.unset(); + promise = myAtom$.toPromise(); + + myAtom$.setError('Error.'); + + /** + * ** Your Turn ** + * We set the atom to an error state. The promise should now be rejected, hence we wrap it in a `try-catch` block. + * What do you think the error message will be? Remember that `try-catch` is not a custom-defined structure. + */ + try { + await promise; + } catch (error: any) { + // `.toBe('Error.')` or `.not.toBe('Error.')`? ↴ + expect(error.message).not.toBe('Error.'); + } + + myAtom$.set('no more error'); + const myDerivable$ = myAtom$.derive(() => { + throw new Error('Error.'); + }); + promise = myDerivable$.toPromise(); + + /** + * ** Your Turn ** + * We now let `myDerivable$` derive from `myAtom$`, and it will throw a normal error (not a custom Sherlock error). + * What will the error message be this time? + */ + try { + await promise; + } catch (error: any) { + // `.toBe('Error.')` or `.not.toBe('Error.')`? ↴ + expect(error.message).toBe('Error.'); + } + }); + + it('`fromObservable()`', () => { + // Has to do with SUBSCRIBING. Hasn't been discussed either... + // TODO: "As all Derivables are now compatible with rxjs's `from` function, + // we no longer need the `toObservable` function from `@skunkteam/sherlock-rxjs`." + }); + + it('`fromEventPattern`', () => { + // TODO: this is kinda complicated shit... Requires explaining a lot of extra stuff (Subjects, Subscribing, Observables...). Leave for now? + }); + }); +}); diff --git a/generated_solution/9 - expert.test.ts b/solution/9 - expert.test.ts similarity index 96% rename from generated_solution/9 - expert.test.ts rename to solution/9 - expert.test.ts index 49830ed..b1efe4f 100644 --- a/generated_solution/9 - expert.test.ts +++ b/solution/9 - expert.test.ts @@ -1,13 +1,6 @@ import { DerivableAtom, atom, derive } from '@skunkteam/sherlock'; import { derivableCache } from '@skunkteam/sherlock-utils'; -/** - * ** Your Turn ** - * - * If you see this variable, you should do something about it. :-) - */ -export const __YOUR_TURN__ = {} as any; - describe('expert', () => { describe('`.autoCache()`', () => { /** @@ -30,7 +23,6 @@ describe('expert', () => { * `hasDerived` is used in the first derivation. But has it been * called at this point? */ - // `.toHaveBeenCalled()` or `.not.toHaveBeenCalled()`? ↴ expect(hasDerived).not.toHaveBeenCalled(); @@ -185,15 +177,16 @@ describe('expert', () => { expect(stockPrice$).toHaveBeenCalledTimes(2); /** Can you explain this behavior? */ - // ANSWER-BLOCK-START - // Yes: it creates a different Derivable every time, so it cannot use any caching. - // This is a similar issue to the `pairwise()` issue from tutorial 7, where, when we - // used lambda functions, we made a new pairwise object every time. - // ANSWER-BLOCK-END + /** + * Yes: `stockPrices$` is not a derivable itself, just the setup function. + * This function creates a different Derivable every time, so it cannot use any caching. + * This is a similar issue to the `pairwise()` issue from tutorial 7, where, when we + * used lambda functions, we made a new pairwise object every time. + */ }); /** - * An other problem can arise when the setup is done inside a derivation + * Another problem can arise when the setup is done inside a derivation */ describe('setup inside a derivation', () => { /** @@ -349,7 +342,8 @@ describe('expert', () => { * We had a price for 'GOOGL', but not for 'APPL'... */ expect(reactSpy).toHaveBeenCalledTimes(3); - expect(reactSpy).toHaveBeenCalledWith([undefined, undefined]); + expect(reactSpy).toHaveBeenCalledWith([1079.11, undefined]); + // Note: `[undefined, undefined]` will pass too, but is incorrect. }); }); diff --git a/generated_solution/jest.config.ts b/solution/jest.config.ts similarity index 91% rename from generated_solution/jest.config.ts rename to solution/jest.config.ts index 1291563..d22fd58 100644 --- a/generated_solution/jest.config.ts +++ b/solution/jest.config.ts @@ -1,7 +1,7 @@ import type { Config } from 'jest'; export default { - displayName: 'generated_solution', + displayName: 'solution', preset: '../jest.preset.js', globals: {}, testEnvironment: 'node', diff --git a/generated_solution/tsconfig.json b/solution/tsconfig.json similarity index 100% rename from generated_solution/tsconfig.json rename to solution/tsconfig.json diff --git a/generated_solution/tsconfig.spec.json b/solution/tsconfig.spec.json similarity index 100% rename from generated_solution/tsconfig.spec.json rename to solution/tsconfig.spec.json diff --git a/generated_tutorial/1 - intro.test.ts b/tutorial/1 - intro.test.ts similarity index 94% rename from generated_tutorial/1 - intro.test.ts rename to tutorial/1 - intro.test.ts index 7bf0fc6..784e8ba 100644 --- a/generated_tutorial/1 - intro.test.ts +++ b/tutorial/1 - intro.test.ts @@ -5,7 +5,6 @@ import { atom } from '@skunkteam/sherlock'; * If you see this variable, you should do something about it. :-) */ export const __YOUR_TURN__ = {} as any; - /** * Welcome to the `@skunkteam/sherlock` tutorial. * @@ -13,7 +12,7 @@ export const __YOUR_TURN__ = {} as any; * to pass. The `expect()`s and basic setup are there, you just need to get it * to work. * - * All specs except the first one are set to `.skip`. Remove this to start on + * All specs are set to `.skip`. Remove this to start on * that part of the tutorial. * * Start the tutorial by running: @@ -25,7 +24,15 @@ export const __YOUR_TURN__ = {} as any; * * *Hint: most methods and functions are fairly well documented in jsDoc, * which is easily accessed through TypeScript* + * + * If you cannot figure it out or are curious to the intended answers, you can + * read the answers in the `solution` folder. */ + +/** + * ** Your Turn ** + * Your first task: remove this `.skip`. + * */ describe.skip('intro', () => { it(` diff --git a/generated_tutorial/2 - deriving.test.ts b/tutorial/2 - deriving.test.ts similarity index 97% rename from generated_tutorial/2 - deriving.test.ts rename to tutorial/2 - deriving.test.ts index f25b808..c2f2441 100644 --- a/generated_tutorial/2 - deriving.test.ts +++ b/tutorial/2 - deriving.test.ts @@ -2,11 +2,9 @@ import { atom, Derivable, derive } from '@skunkteam/sherlock'; /** * ** Your Turn ** - * * If you see this variable, you should do something about it. :-) */ export const __YOUR_TURN__ = {} as any; - /** * Any `Derivable` (including `Atom`s) can be used (and/or combined) to create * a derived state. This derived state is in turn a `Derivable`. @@ -73,13 +71,13 @@ describe.skip('deriving', () => { * method on another `Derivable`. */ - // Should return 'Buzz' when `myCounter$` is a multiple of 5 and '' otherwise. - // Should return 'Fizz' when `myCounter$` is a multiple of 3 and '' otherwise. const fizz$: Derivable = myCounter$.derive(__YOUR_TURN__); + const fizz$: Derivable = myCounter$.derive(v => (v % 3 ? '' : 'Fizz')); // Shorthand for `v % 3 !== 0` // Should return 'Buzz' when `myCounter$` is a multiple of 5 and '' otherwise. const buzz$: Derivable = myCounter$.derive(__YOUR_TURN__); + const buzz$: Derivable = myCounter$.derive(v => (v % 5 ? '' : 'Buzz')); const fizzBuzz$: Derivable = derive(__YOUR_TURN__); @@ -200,7 +198,6 @@ describe.skip('deriving', () => { * `.and(...)` and `.or(...)`. Make sure the output of those `Derivable`s * is either 'Fizz'/'Buzz' or ''. */ - const fizz$ = myCounter$ .derive(count => count % 3) .is(__YOUR_TURN__) diff --git a/generated_tutorial/3 - reacting.test.ts b/tutorial/3 - reacting.test.ts similarity index 76% rename from generated_tutorial/3 - reacting.test.ts rename to tutorial/3 - reacting.test.ts index cf732b7..ed724d5 100644 --- a/generated_tutorial/3 - reacting.test.ts +++ b/tutorial/3 - reacting.test.ts @@ -2,18 +2,45 @@ import { atom } from '@skunkteam/sherlock'; /** * ** Your Turn ** - * * If you see this variable, you should do something about it. :-) */ export const __YOUR_TURN__ = {} as any; - -// FIXME: check my solutions with the actual solutions +// xxx check my solutions with the actual solutions (https://github.com/skunkteam/sherlock/tree/tutorial-solutions/robin/tutorial) // FIXME: remove all TODO: and FIXME: -// FIXME: check whether the generated tutorials and solutions actually work (e.g. are all solutions correct? No weird shenanigans?) -// FIXME: deze file niet linten / builden (voor automatische test). Tutorial ook niet. Maar solutions juist wel! OP EIND. +// FIXME: check whether the generated tutorials and solutions actually work (e.g. are all solutions correct? No weird shenanigans?) - ALSO CHECK "Or, alternatively"! +// FIXME: deze file niet linten / builden (voor automatische test). Tutorial ook niet. Maar solutions juist wel! OP EIND. (mag beide wel linten right?) // FIXME: interne review document, mocht ik iets hebben om te laten zien! In Google Drive, zet het erin! -// FIXME: werkt `npm run tutorial` nog??? - +// xxx werkt `npm run tutorial` nog? > Nu wel. +// xxx PETER: "nu je toch met Sherlock bezig bent; zou je voor mij eens kunnen checken of de code voorbeelden in de README +// nog wel kloppen met de huidige API? Ik heb het gevoel dat dat niet zo is; volgens mij is er geen function "derivation()" +// en heet dat nu "derive()" bijvoorbeeld." +// FIXME: OOOOOOH JA, ik had eroverheen gepushed! Dat moet nog een PR met terugwerkende kracht worden... (of commits squashen, en dat ze dan maar de commit moeten reviewen?) + +// FIXME: Add FromEventPattern + FromObservable +// xxx fix the generator for code blocks. +// FIXME: now check whether it did not remove excess lines or kept 2 empty lines where it should not. (I think it is good though.) +/** + * x Final States; (finalGetter, finalMethod, getMaybeFinalState, FinalWrapper, MaybeFinalState, _isFinal, makeFinal, markFinal, .final, .finalized, setFinal...) + * ? Lens; (libs/sherlock/src/lib/derivable/lens.ts) - map die twee kanten op kan gaan. Maar een map kan dat al? Maar hier kan + * je dat los definieren! Je kan gewoon `lens` ipv `var.lens`. Zelden dat je dit gebruikt. Output is een Derivable though. + * x Lift; (libs/sherlock-utils/src/lib/lift.ts) + * x Peek; (libs/sherlock-utils/src/lib/peek.ts) - dan track je niet. In een derivable, deze tracked hij dan niet (ipv .get() waar het wel getracked wordt) + * x Template; (libs/sherlock-utils/src/lib/template.ts) - to make a string using a template literal. (Uses unwrap!!) + * / Factory; (libs/sherlock/src/lib/derivable/factories.ts) - simply contains functions to create objects, namely + * lens; atom; constant; derive. + * !! Flat-map; (libs/sherlock/src/lib/derivable/mixins/flat-map.ts) - ??? + * array: nested arrays naar array + * Derivable: gooit er derive.get() achteraan? + * Derivable (input van inputveld). Flatmap geeft Derivable terug. Derivable.flatmap() returned misschien + * Derivable, returned dan de number. flatMap is een `derive`, maar wat hij returned haalt hij uit de Derivable. + * ofzoiets. Maakt code korter. + * x Fallback-to; - op een derivable. Als een atom `unresolved` is, dan fallt het back to this value. Ofwel, initial value, maar + * ook als hij later unresolved wordt, dan wordt hij dit (vaak wel initial value). + * x Take - react options gebruiken buiten react. In een derivable chain, halverwege die options gebruiken. + * -- e.g. (from)Promise. Zodra die een waarde aanneemt kan hij niet meer veranderen. + * Let FromPromise, FromObservable, FromEventPattern ook uit (in utils?), ToPromise, ToObservable, in praktijk ook handig. + * FromEventPattern (haily mary, als alles niet werkt, dan dit doen). + */ /** * In the intro we have seen a basic usage of the `.react()` method. * Let's dive a bit deeper into the details of this method. @@ -140,7 +167,6 @@ describe.skip('reacting', () => { * In the reaction below, use the stopper callback to stop the * reaction */ - myAtom$.react((val, __YOUR_TURN___) => { reactor(val); __YOUR_TURN___; @@ -291,24 +317,28 @@ describe.skip('reacting', () => { * Sometimes, the syntax may leave you confused. */ it('syntax issues', () => { - // It looks this will start reacting until `boolean$`s value is false... - let stopper = boolean$.react(reactor, { until: b => !b }); + boolean$.set(true); + // It looks this will keep reacting until `boolean$`s value is set to false... + let stopper = boolean$.react(reactor, { until: b$ => !b$ }); + + boolean$.set(false); - // ...but does it? (Remember: `boolean$` starts out as `false`) + // ...but does it? Is the reactor still connected? expect(boolean$.connected).toBe(__YOUR_TURN__); - // The `b` it obtains as argument is a `Derivable`. This is a - // reference value which will evaluate to `true` as it is not `undefined`. - // Thus, the negation will evaluate to `false`, independent of the value of - // the boolean. You can get the boolean value our of the `Derivable` using `.get()`: + // The `b$` it obtains as argument is a `Derivable`. This is a + // reference value. Because we apply a negation to this, `b$` is coerced to a + // boolean value, which will evaluate to `true` as `b$` is not `undefined`. + // Thus, the whole expression will evaluate to `false`, independent of the value of + // `boolean$`. Instead, you can get the value out of the `Derivable` using `.get()`: stopper(); // reset - stopper = boolean$.react(reactor, { until: b => !b.get() }); + stopper = boolean$.react(reactor, { until: b$ => !b$.get() }); expect(boolean$.connected).toBe(__YOUR_TURN__); // You can also return the `Derivable` after appling the negation - // using the method designed for negating Derivables: + // using the method designed for negating the boolean within a `Derivable`: stopper(); - boolean$.react(reactor, { until: b => b.not() }); + boolean$.react(reactor, { until: b$ => b$.not() }); expect(boolean$.connected).toBe(__YOUR_TURN__); }); }); @@ -397,12 +427,12 @@ describe.skip('reacting', () => { for (let i = 0; i <= 5; i++) { count$.set(i); } - expectReact(1, 5); // it should have skipped the 4 + expectReact(1, 5); // it should have skipped the 4 and only reacted to the 5 for (let i = 0; i <= 5; i++) { count$.set(i); } - expectReact(3, 5); // now it should not have skipped the 4 + expectReact(3, 5); // now it should have reacted to the 4 and 5 (and the 5 of last time) }); /** @@ -414,28 +444,28 @@ describe.skip('reacting', () => { * can be very useful. */ it('reacting `once`', () => { - const finished$ = atom(false); + const count$ = atom(0); /** * ** Your Turn ** * - * Say you want to react when `finished$` is true. It can not finish - * twice. + * Say you want to react when `count$` is higher than 3. But only the first time... * * *Hint: you will need to combine `once` with another option* */ - // finished$.react(reactor, __YOUR_TURN__); + count$.react(reactor, __YOUR_TURN__); expectReact(0); - // When finished it should react once. - finished$.set(true); - expectReact(1, true); + for (let i = 0; i <= 5; i++) { + count$.set(i); + } + expectReact(1, 4); // it should have only registered the 4 and not the 5 - // After that it should really be finished. :-) - finished$.set(false); - finished$.set(true); - expectReact(1, true); + for (let i = 0; i <= 5; i++) { + count$.set(i); + } + expectReact(1, 4); // and after that, it should really be finished. :-) }); }); @@ -453,37 +483,36 @@ describe.skip('reacting', () => { * and `when` is true or unset. If e.g. `when` evaluates to false, `skipFirst` cannot trigger. */ it('`from` and `until`', () => { - const myAtom$ = atom(0); - myAtom$.react(reactor, { from: v => v.is(3), until: v => v.is(2) }); + const myAtom$ = atom(0); + myAtom$.react(reactor, { from: parent$ => parent$.is(3), until: parent$ => parent$.is(2) }); for (let i = 1; i <= 5; i++) { myAtom$.set(i); } // The reactor starts reacting when `myAtom` gets the value 3, but stops when it gets the value 2. - // But because `myAtom` obtains the value 2 before it obtains 3... + // But because `myAtom$` obtains the value 2 before it obtains 3... // ...how many times was the reactor called, if any? expectReact(__YOUR_TURN__); }); it('`when` and `skipFirst`', () => { - const myAtom$ = atom(0); + const myAtom$ = atom(0); myAtom$.react(reactor, { when: v => v.is(1), skipFirst: true }); myAtom$.set(1); - // The reactor reacts when `myAtom` is 1 but skips the first number. - // The first number of `myAtom` is 0, its initial number. - // Does the reactor skip the 0 or the 1? + // The reactor reacts when `myAtom$` is 1 but skips the first number. + // `myAtom$` starts out at 0. Does the reactor skip only the 0 or also the 1? expectReact(__YOUR_TURN__); }); it('`from`, `until`, `when`, `skipFirst`, and `once`', () => { - const myAtom$ = atom(0); + const myAtom$ = atom(0); myAtom$.react(reactor, { - from: v => v.is(5), - until: v => v.is(1), - when: v => [2, 3, 4].includes(v.get()), + from: parent$ => parent$.is(5), + until: parent$ => parent$.is(1), + when: parent$ => [2, 3, 4].includes(parent$.get()), skipFirst: true, once: true, }); @@ -492,12 +521,11 @@ describe.skip('reacting', () => { myAtom$.set(v); } - // `from` and `until` allow the reactor to respectively start when `myAtom` has value 5, and stop when it has value 1. + // `from` and `until` allow the reactor to respectively start when `myAtom$` has value 5, and stop when it has value 1. // Meanwhile, `when` allows neither of those values and only allows the values 2, 3, and 4. // `skipFirst` and `once` are also added, just to bring the whole group together. // so, how many times is the reactor called, and what was the last argument (if any)? expectReact(__YOUR_TURN__); - }); }); @@ -507,15 +535,16 @@ describe.skip('reacting', () => { /** * ** Your Turn ** * - * `connected$` indicates the current connection status: + * `connected$` indicates the current connection status. It is one of: * > 'connected'; * > 'disconnected'; * > 'standby'. * * We want our reactor to trigger once, when the device is not connected, - * which means it is either `standby` or `disconnected` (eg for cleanup). + * (`standby` or `disconnected`), e.g. for cleanup. However, we do not want + * it to trigger right away, even though we start at `disconnected`. * - * This should be possible with three simple ReactorOptions + * This should be possible with three simple ReactorOptions. */ connected$.react(reactor, __YOUR_TURN__); diff --git a/generated_tutorial/4 - inner workings.test.ts b/tutorial/4 - inner workings.test.ts similarity index 97% rename from generated_tutorial/4 - inner workings.test.ts rename to tutorial/4 - inner workings.test.ts index 4e3223f..c3d5ecf 100644 --- a/generated_tutorial/4 - inner workings.test.ts +++ b/tutorial/4 - inner workings.test.ts @@ -3,11 +3,9 @@ import { Seq } from 'immutable'; /** * ** Your Turn ** - * * If you see this variable, you should do something about it. :-) */ export const __YOUR_TURN__ = {} as any; - /** * Time to dive a bit deeper into the inner workings of `@skunkteam/sherlock`. */ @@ -43,7 +41,6 @@ describe.skip('inner workings', () => { * * What do you expect? */ - expect(reacted).toHaveBeenCalledTimes(__YOUR_TURN__); expect(reacted).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); @@ -55,7 +52,6 @@ describe.skip('inner workings', () => { * * What do you expect? */ - expect(reacted).toHaveBeenCalledTimes(__YOUR_TURN__); expect(reacted).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); @@ -68,10 +64,8 @@ describe.skip('inner workings', () => { * * What do you expect now? */ - expect(reacted).toHaveBeenCalledTimes(__YOUR_TURN__); expect(reacted).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); - }); /** @@ -93,7 +87,7 @@ describe.skip('inner workings', () => { * not called `.get()` on that new `Derivable`. * * How many times do you think the `hasDerived` function has been - * called? 0 is also an option of course. + * called? */ // Well, what do you expect? @@ -255,7 +249,7 @@ describe.skip('inner workings', () => { const hasReacted = jest.fn(); atom$.react(hasReacted, { skipFirst: true }); - expect(hasReacted).toHaveBeenCalledTimes(0); // added for clarity, in case people missed the `skipFirst` or its implication + expect(hasReacted).toHaveBeenCalledTimes(0); atom$.set({}); @@ -301,7 +295,7 @@ describe.skip('inner workings', () => { * First we check `Object.is()` equality, if that is true, it is the * same, you can't deny that. * - * After that it is pluggable. It can be anything you want. + * After that it is pluggable. It can be anything you want. TODO: what is pluggable? * * By default we try to use `.equals()`, to support libraries like * `ImmutableJS`. diff --git a/generated_tutorial/5 - unresolved.test.ts b/tutorial/5 - unresolved.test.ts similarity index 86% rename from generated_tutorial/5 - unresolved.test.ts rename to tutorial/5 - unresolved.test.ts index ace2845..547a272 100644 --- a/generated_tutorial/5 - unresolved.test.ts +++ b/tutorial/5 - unresolved.test.ts @@ -2,11 +2,9 @@ import { atom, Derivable, DerivableAtom } from '@skunkteam/sherlock'; /** * ** Your Turn ** - * * If you see this variable, you should do something about it. :-) */ export const __YOUR_TURN__ = {} as any; - /** * Sometimes your data isn't available yet. For example if it is still being * fetched from the server. At that point you probably still want your @@ -175,4 +173,30 @@ describe.skip('unresolved', () => { myString$.unset(); expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); }); + + /** + * It is nice to be able to have a backup plan when a Derivable gets unresolved. + * The `.fallbackTo()` function allows you to specify a default value + * whenever your Derivable gets unset. + */ + it('Fallback-to', () => { + const myAtom$ = atom(0); + + /** + * ** Your Turn ** + * Use the `.fallbackTo()` method to create a `mySafeAtom$` which + * gets the backup value `3` when `myAtom$` becomes unresolved. + */ + const mySafeAtom$ = myAtom$.fallbackTo(__YOUR_TURN__); + + expect(myAtom$.value).toBe(0); + expect(mySafeAtom$.value).toBe(0); + + myAtom$.unset(); + + expect(myAtom$.resolved).toBeFalse(); + expect(mySafeAtom$.resolved).toBeTrue(); + expect(myAtom$.value).toBeUndefined(); + expect(mySafeAtom$.value).toBe(3); + }); }); diff --git a/tutorial/6 - errors.test.ts b/tutorial/6 - errors.test.ts new file mode 100644 index 0000000..fab5d11 --- /dev/null +++ b/tutorial/6 - errors.test.ts @@ -0,0 +1,119 @@ +import { atom, DerivableAtom, error } from '@skunkteam/sherlock'; + +/** + * ** Your Turn ** + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; +/** + * Errors are a bit part of any programming language, and Sherlock has its own custom errors + * and ways to deal with them. + */ +describe.skip('errors', () => { + let myAtom$: DerivableAtom; + + beforeEach(() => { + myAtom$ = atom(1); + }); + + it('basic errors', () => { + // The `errored` property of a Derivable shows whether it is in an error state - meaning that + // the last statement resulted in an error + expect(myAtom$.errored).toBe(false); + expect(myAtom$.error).toBeUndefined; // by default, the `error` message is undefined. + + // We can set errors using the `setError()` function. + myAtom$.setError('my Error'); + + expect(myAtom$.errored).toBe(true); + expect(myAtom$.error).toBe('my Error'); + + // expect(myAtom$.get).toThrow("Cannot read properties of undefined (reading 'getState')"); + // TODO: WHAT - normally this works, but internal JEST just fucks with me....? + + // What will happen if you try to call `get()` on `myAtom$`? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myAtom$.get()) /* __YOUR_TURN__ */; + + // ** __YOUR_TURN__ ** + // What will happen if you try to call `set()` on `myAtom$`? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myAtom$.set(2)) /* __YOUR_TURN__ */; + expect(myAtom$.errored).toBe(__YOUR_TURN__); + + // Interestingly, calling `set()` does not throw an error. In fact, it removes the error state + // altogether. This means we can now call `get()` again. + expect(() => myAtom$.get()).not.toThrow(); + }); + + /** + * libs/sherlock/src/lib/interfaces.ts:289 shows the basic states that a Derivable can have. + * > `export type State = V | unresolved | ErrorWrapper;` + * A state can be either any type `V` (`number`, `string`, etc.), `unresolved` as we saw in the + * previous tutorial, or `ErrorWrapper`. This last state is explained here. + */ + it('error states', () => { + expect(myAtom$.getState()).toBe(1); // as explained above, any type can be a state + + myAtom$.setError('my Error'); + + // The `ErrorWrapper` state only holds an error string. The `error()` function returns + // such an `ErrorWrapper` which we can use to compare. + expect(myAtom$.getState()).toMatchObject(error('my Error')); + + // TODO: more! There wasn't a question in here. Maybe combine with Final States? NO, that one should go! + }); + + it('deriving an error', () => { + const myDerivable$ = myAtom$.derive(v => v + 1); + + // If `myAtom$` suddenly errors... + myAtom$.setError('division by zero'); + + // ...what happens to `myDerivable$`? + expect(myDerivable$.errored).toBe(__YOUR_TURN__); + + // If any Derivable tries to derive from an atom in an error state, + // this Derivable will itself throw an error too. This makes sense, + // given that it cannot obtain the value it needs. + }); + + it('reacting to an error', () => { + // Setting an error to an Atom generally does not throw an error. + expect(() => myAtom$.setError('my Error')).not.toThrow(); + + myAtom$.set(1); + + // Now we set a reactor to `myAtom$`. However, this reactor does not use the value of `myAtom$`. + const reactor = jest.fn(); // empty function body + myAtom$.react(reactor); + + // ** __YOUR_TURN__ ** + // Will an error be thrown when `myAtom$` is now set to an error state? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myAtom$.setError('my Error')) /* __YOUR_TURN__ */; + + // ** __YOUR_TURN__ ** + // Is the reactor still connected now that it errored? + expect(myAtom$.connected).toBe(__YOUR_TURN__); + + // Reacting to a Derivable that throws an error will make the reactor throw as well. + // Because the reactor will usually fire when it gets connected, it also throws when + // you try to connect it after the error has already been set. + + myAtom$ = atom(1); + myAtom$.setError('my second Error'); // + + // ** __YOUR_TURN__ ** + // Will an error be thrown when you use `skipFirst`? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myAtom$.react(reactor, { skipFirst: true })) /* __YOUR_TURN__ */; + + // And will an error be thrown when `from = false`? + // `.toThrow()` or `.not.toThrow()`? ↴ + expect(() => myAtom$.react(reactor, { from: false })) /* __YOUR_TURN__ */; + + // When `from = false`, the reactor is disconnected, preventing the error message from entering. + // `skipFirst`, on the other hand, does allow the error in, but does not trigger an update. + }); +}); diff --git a/generated_tutorial/7 - advanced.test.ts b/tutorial/7 - advanced.test.ts similarity index 92% rename from generated_tutorial/7 - advanced.test.ts rename to tutorial/7 - advanced.test.ts index bd6b9fb..f76f5ec 100644 --- a/generated_tutorial/7 - advanced.test.ts +++ b/tutorial/7 - advanced.test.ts @@ -1,14 +1,12 @@ import { atom, constant, Derivable, derive, SettableDerivable, unresolved } from '@skunkteam/sherlock'; -import { lift, template } from '@skunkteam/sherlock-utils'; +import { template } from '@skunkteam/sherlock-utils'; import { Map as ImmutableMap } from 'immutable'; /** * ** Your Turn ** - * * If you see this variable, you should do something about it. :-) */ export const __YOUR_TURN__ = {} as any; - describe.skip('advanced', () => { /** * In the case a `Derivable` is required, but the value is immutable. @@ -38,7 +36,7 @@ describe.skip('advanced', () => { }); it('`templates`', () => { - // Staying in the theme of redefining normal Typescript code in our Derivable language, + // Staying in the theme of redefining normal Typescript code in our Sherlock language, // we also have a special syntax to copy template literals to a Derivable. const one = 1; const myDerivable = template`I want to go to ${one} party`; @@ -158,13 +156,11 @@ describe.skip('advanced', () => { * We just created two `Derivable`s that are almost exactly the same. * But what happens when their source becomes `unresolved`? */ - expect(usingGet$.resolved).toEqual(__YOUR_TURN__); expect(usingVal$.resolved).toEqual(__YOUR_TURN__); myAtom$.unset(); expect(usingGet$.resolved).toEqual(__YOUR_TURN__); expect(usingVal$.resolved).toEqual(__YOUR_TURN__); - }); }); @@ -182,6 +178,7 @@ describe.skip('advanced', () => { it('triggers when the source changes', () => { const myAtom$ = atom(1); + /** * ** Your Turn ** * @@ -212,13 +209,13 @@ describe.skip('advanced', () => { expect(deriveReactSpy).toHaveBeenCalledExactlyOnceWith('ho', expect.toBeFunction()); myRepeat$.value = 3; + /** * ** Your Turn ** * * We changed`myRepeat$` to equal 3. * Do you expect both reactors to have fired? And with what? */ - expect(deriveReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); expect(deriveReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); @@ -226,12 +223,12 @@ describe.skip('advanced', () => { expect(mapReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); myString$.value = 'ha'; + /** * ** Your Turn ** * * And now that we have changed `myString$`? And when `myRepeat$` changed again? */ - expect(deriveReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); expect(deriveReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); @@ -244,7 +241,6 @@ describe.skip('advanced', () => { expect(mapReactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); expect(mapReactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); - /** * As you can see, a change in `myString$` will not trigger an * update. But if an update is triggered, `myString$` will be called @@ -284,37 +280,33 @@ describe.skip('advanced', () => { expect(myInverse$.get()).toEqual(-2); }); + /** + * The `.map()` used here is similar to the `.map()` used on arrays. + * Both get values out of a container (`Array` or `Derivable`), apply + * some function, and put it back in the container. + */ it('similar to `map()` on arrays', () => { - // If the similarity is not clear yet, here is a comparison between - // the normal `.map()` on arrays and our `Derivable` `.map()`. - // Both get values out of a container (`Array` or `Derivable`), apply - // some function, and put it back in the container. - const addOne = jest.fn((v: number) => v + 1); - const myList = [1, 2, 3]; + const myList = [1]; const myMappedList = myList.map(addOne); - expect(myMappedList).toMatchObject([2, 3, 4]); + expect(myMappedList).toMatchObject([2]); const myAtom$ = atom(1); let myMappedDerivable$ = myAtom$.map(addOne); expect(myMappedDerivable$.value).toBe(2); - // Or, as we have seen before, you can use `lift()` for this. - myMappedDerivable$ = lift(addOne)(myAtom$); - expect(myMappedDerivable$.value).toBe(2); - // You can combine them too. - const myAtom2$ = atom([1, 2, 3]); + const myAtom2$ = atom([1]); const myMappedDerivable2$ = myAtom2$.map(v => v.map(addOne)); - expect(myMappedDerivable2$.value).toMatchObject([2, 3, 4]); + expect(myMappedDerivable2$.value).toMatchObject([2]); }); /** * In order to reason over the state of a Derivable, we can * use `.mapState()`. This will map one state to another, and * can be used to get rid of pesky `unresolved` or `Errorwrapper` - * states (or to introduce them!). + * states. */ it('`.mapState()`', () => { const myAtom$ = atom(1); @@ -363,7 +355,7 @@ describe.skip('advanced', () => { expect(myMappedAtom$.get()).toBe(3); // the state and value are linked, so this is identical to `.getState()` /** * You might think that this change in state would cause `myAtom$` to now also get - * `3` as its state, but this does not happen. Why not? TODO: maximally one cycle? + * `3` as its state, but this does not happen. Why not? TODO: maximally one cycle? ASK! * Since both `2` and `3` are not `unresolved`, it does not matter to our answer. * * The same cannot be said for the fourth case. Setting `myMappedAtom$` to `unresolved` @@ -373,6 +365,22 @@ describe.skip('advanced', () => { * TODO: change this to be for MAP. Then make MAPSTATE a trivial one right after. */ }); + + // FIXME: + it('TEMP Flat-map', () => { + // const myAtom$ = atom(0); + // const mapping = (v: any) => atom(v); + // Sometimes you use `map()`, but the result within the `map()` is also a Derivable. + // The result would here be a `Derivable>` (hover over `derive` to see this). + // You would have to use `.get()` to go back to a single Derivable. Similarly how `flatMap` can + // reduce lists of lists to a single list, it can help reduce Derivables of Derivables to a + // single Derivable. If you have something like this: + // let myAtom$$ = myAtom$.map(n => mapping(n)).derive(v => v.get()); + // You can now rewrite it to this: + // myAtom$$ = myAtom$.flatMap(n => mapping(n)); + // It only results in slightly shorter code. + // TODO: right? + }); }); /** @@ -496,12 +504,10 @@ describe.skip('advanced', () => { * So what if we set `firstProp$`? Does this propagate to the source * `Derivable`? */ - firstProp$.set(__YOUR_TURN__); expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); expect(myMap$.get().get('firstProp')).toEqual(__YOUR_TURN__); expect(myMap$.get().get('secondProp')).toEqual(__YOUR_TURN__); - }); }); }); diff --git a/tutorial/8 - utils.test.ts b/tutorial/8 - utils.test.ts new file mode 100644 index 0000000..94271b4 --- /dev/null +++ b/tutorial/8 - utils.test.ts @@ -0,0 +1,632 @@ +import { atom, constant, derive, FinalWrapper } from '@skunkteam/sherlock'; +import { fromPromise, lift, pairwise, peek, scan, struct } from '@skunkteam/sherlock-utils'; + +/** + * ** Your Turn ** + * If you see this variable, you should do something about it. :-) + */ +export const __YOUR_TURN__ = {} as any; + +// Silence TypeScript's import not used errors. +expect(pairwise).toBe(pairwise); +expect(scan).toBe(scan); +expect(struct).toBe(struct); +expect(peek).toBe(peek); +expect(lift).toBe(lift); +expect(FinalWrapper).toBe(FinalWrapper); // TODO: not sure whether needed +/** + * In the `sherlock-utils` lib, there are a couple of functions that can combine + * multiple values of a single `Derivable` or combine multiple `Derivable`s into + * one. We will show a couple of those here. + */ +describe.skip('utils', () => { + /** + * As the name suggests, `pairwise()` will call the given function with both + * the current and the previous state. + * + * *Note: functions like `pairwise` and `scan` can be used with any callback, + * so it can be used both in a `.derive()` step and in a `.react()`* + */ + it('pairwise', () => { + const myCounter$ = atom(1); + const reactSpy = jest.fn(); + + /** + * ** Your Turn ** + * + * Now, use `pairwise()` to subtract the previous value from the + * current. + * + * *Hint: check the overloads of pairwise if you're struggling with + * `oldValue`.* + * + * Note: don't call `pairwise()` using a lambda function! + */ + myCounter$.derive(__YOUR_TURN__).react(reactSpy); + + expect(reactSpy).toHaveBeenCalledTimes(1); + expect(reactSpy).toHaveBeenLastCalledWith(1, expect.toBeFunction()); + + myCounter$.set(3); + + expect(reactSpy).toHaveBeenCalledTimes(2); + expect(reactSpy).toHaveBeenLastCalledWith(2, expect.toBeFunction()); // 3 (current value of `myCounter$`) - 1 (previous value of `myCounter$`) + + myCounter$.set(10); + + expect(reactSpy).toHaveBeenCalledTimes(3); + expect(reactSpy).toHaveBeenLastCalledWith(7, expect.toBeFunction()); // 10 (current value of `myCounter$`) - 3 (previous value of `myCounter$`) + + myCounter$.set(20); + + // ** Your Turn ** + // What will the next output be? + expect(reactSpy).toHaveBeenCalledTimes(4); + expect(reactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + }); + + /** + * `scan()` is the `Derivable` version of `Array.prototype.reduce()`. It will be + * called with the current state and the last emitted value. + * + * (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce) + * + * *Note: as with `pairwise()` this is useable in both a `.derive()` and + * `.react()` method* + */ + it('scan', () => { + const myCounter$ = atom(1); + const reactSpy = jest.fn(); + + /** + * ** Your Turn ** + * + * Now, use `scan()` to subtract the previous value from the + * current. + * + * Note that `scan()` must return the same type as it gets as input. This is required + * as this returned value is also used for the accumulator (`acc`) value for the next call. + * This `acc` parameter of `scan()` is the last returned value, not the last value + * of `myCounter$`, as is the case with `pairwise()`. + * + * Note: don't call `pairwise()` using a lambda function! + */ + myCounter$.derive(__YOUR_TURN__).react(reactSpy); + + expect(reactSpy).toHaveBeenCalledTimes(1); + expect(reactSpy).toHaveBeenLastCalledWith(1, expect.toBeFunction()); + + myCounter$.set(3); + + expect(reactSpy).toHaveBeenCalledTimes(2); + expect(reactSpy).toHaveBeenLastCalledWith(2, expect.toBeFunction()); // 3 (current value of `myCounter$`) - 1 (previous returned value) + + myCounter$.set(10); + + expect(reactSpy).toHaveBeenCalledTimes(3); + expect(reactSpy).toHaveBeenLastCalledWith(8, expect.toBeFunction()); // 10 (current value of `myCounter$`) - 2 (previous returned value) + + myCounter$.set(20); + + // ** Your Turn ** + // What will the next output be? + expect(reactSpy).toHaveBeenCalledTimes(4); + expect(reactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + }); + + it('`pairwise()` on normal arrays', () => { + // Functions like `pairwise()` and `scan()` work on normal lists too. They are often + // used in combination with `.map()` and `.filter()`. + const myList = [1, 2, 3, 5, 10]; + let myList2: number[]; + + /** + * ** Your Turn ** + * + * Use a `pairwise()` combined with a `.map()` on `myList` + * to subtract the previous value from the current. + * + * Note: don't call `pairwise()` using a lambda function! + */ + myList2 = myList.map(__YOUR_TURN__); + expect(myList2).toMatchObject([1, 1, 1, 2, 5]); + + // However, we should be careful with this, as this does not always behave as intended. + // Particularly, what exactly happens when we do call `pairwise()` using a lambda function? + myList2 = myList.map(v => __YOUR_TURN__(v)); // copy the same implementation here + expect(myList2).toMatchObject([1, 2, 3, 5, 10]); + + // Even if we are more clear about what we pass, this unintended behavior does not go away. + myList2 = myList.map((v, _, _2) => __YOUR_TURN__(v)); // copy the same implementation here + expect(myList2).toMatchObject([1, 2, 3, 5, 10]); + + // `pairwise()` keeps track of the previous value under the hood. Using a lambda of + // the form `v => pairwise(...)(v)` would create a new `pairwise` function every call, + // essentially resetting the previous value every call. And resetting the previous value + // to 0 causes the input to stay the same (after all: x - 0 = x). + // Other than by not using a lambda function, we can fix this by + // saving the `pairwise` in a variable and reusing it for every call. + + let f: (v: number) => number = __YOUR_TURN__; // copy the same implementation here + myList2 = myList.map(v => f(v)); + expect(myList2).toMatchObject([1, 1, 1, 2, 5]); + + // To get more insight in the `pairwise()` function, you can call it + // manually. Here, we show what happens under the hood. + + f = pairwise(__YOUR_TURN__); // copy the same implementation here + + myList2 = []; + myList2[0] = f(myList[0]); // `f` is newly created with `init = 0`, so applies `1 - 0 = 1`. + myList2[1] = f(myList[1]); // `f` has saved `1` internally, so applies `2 - 1 = 1`. + myList2[2] = f(myList[2]); // `f` has saved `2` internally, so applies `3 - 2 = 1`. + myList2[3] = f(myList[3]); // `f` has saved `3` internally, so applies `5 - 3 = 2`. + myList2[4] = f(myList[4]); // `f` has saved `5` internally, so applies `10 - 5 = 5`. + + expect(myList2).toMatchObject([1, 1, 1, 2, 5]); + + // This also works for functions other than `.map()`, such as `.filter()`. + + /** ** Your Turn ** + * Use `pairwise()` to filter out all values which produce `1` when subtracted + * with their previous value. + * Note that the function `f` still requires a number to be the return value. + * Checking for equality therefore cannot be done directly within `f`. + */ + f = __YOUR_TURN__; + myList2 = myList.filter(__YOUR_TURN__); + + expect(myList2).toMatchObject([1, 2, 3]); // only the numbers `1`, `2`, and `3` produce 1 when subtracted with the previous value + }); + + it('`scan()` on normal arrays', () => { + // As with `pairwise()` in the last test, `scan()` can be used with arrays too. + const myList = [1, 2, 3, 5, 10]; + let myList2: number[]; + + /** + * ** Your Turn ** + * + * Use a `scan()` combined with a `.map()` on `myList` + * to subtract the previous value from the current. + */ + let f: (v: number) => number = scan(__YOUR_TURN__); + myList2 = myList.map(f); + + expect(myList2).toMatchObject([1, 1, 2, 3, 7]); + + // again, it is useful to consider what happens internally. + f(7); // reset -- this resets the internal `acc` value to 0, as the current `acc` value was 7, and 7-7 = 0. + + myList2 = []; + myList2[0] = f(myList[0]); // `f` is newly created with `init = 0`, so applies `1 - 0 = 1`. + myList2[1] = f(myList[1]); // `f` has saved the result `1` internally, so applies `2 - 1 = 1`. + myList2[2] = f(myList[2]); // `f` has saved the result `1` internally, so applies `3 - 1 = 2`. + myList2[3] = f(myList[3]); // `f` has saved the result `2` internally, so applies `5 - 2 = 3`. + myList2[4] = f(myList[4]); // `f` has saved the result `3` internally, so applies `10 - 3 = 7`. + + expect(myList2).toMatchObject([1, 1, 2, 3, 7]); + + // This also works for functions other than `map()`, such as `filter()`. + + /** + * ** Your Turn ** + * Use `scan()` to filter out all values from `myList` which produce a value + * of 8 or higher when added with the previous result. In other words, it should + * go through `myList` and add the values producing: (1), (1+2), (1+2+3), (1+2+3+5), + * (1+2+3+5+10), and since this sum only prouces a value higher than 8 when the + * values `5` and `10` are added, the result should be `[5,10]`. + */ + f = scan(__YOUR_TURN__); + myList2 = myList.filter(__YOUR_TURN__); + + expect(myList2).toMatchObject([5, 10]); + }); + + it('pairwise - BONUS', () => { + const myCounter$ = atom(1); + let reactSpy = jest.fn(); + + /** + * ** Your Turn ** + * ** BONUS ** + * + * Now, use `pairwise()` directly in `.react()`. Implement the same + * derivation as before: subtract the previous value from the current. + */ + reactSpy = jest.fn(__YOUR_TURN__); + myCounter$.react(reactSpy); + + expect(reactSpy).toHaveLastReturnedWith(1); + + myCounter$.set(3); + + expect(reactSpy).toHaveLastReturnedWith(2); + + myCounter$.set(10); + + expect(reactSpy).toHaveLastReturnedWith(7); + }); + + it('scan - BONUS', () => { + const myCounter$ = atom(1); + let reactSpy = jest.fn(); + + /** + * ** Your Turn ** + * ** BONUS ** + * + * Now, use `scan()` directly in `.react()`. Implement the same + * derivation as before: subtract all the emitted values. + */ + + reactSpy = jest.fn(__YOUR_TURN__); + myCounter$.react(reactSpy); + + expect(reactSpy).toHaveLastReturnedWith(1); + + myCounter$.set(3); + + expect(reactSpy).toHaveLastReturnedWith(2); + + myCounter$.set(10); + + expect(reactSpy).toHaveLastReturnedWith(8); + }); + + /** + * A `struct()` can combine an Object/Array of `Derivable`s into one + * `Derivable`, that contains the values of that `Derivable`. + * + * The Object/Array that is in the output of `struct()` will have the same + * structure as the original Object/Array. + * + * This is best explained in practice. + */ + it('struct', () => { + const allMyAtoms = { + regularProp: 'prop', + string: atom('my string'), + number: atom(1), + sub: { + string: atom('my substring'), + }, + }; + + const myOneAtom$ = struct(allMyAtoms); + + expect(myOneAtom$.get()).toEqual({ + regularProp: 'prop', + string: 'my string', + number: 1, + sub: { + string: 'my substring', + }, + }); + + // Note: we change the original object, not the struct. + allMyAtoms.regularProp = 'new value'; + allMyAtoms.sub.string.set('my new substring'); + + /** + * ** Your Turn ** + * + * Now have a look at the properties of `myOneAtom$`. Is this what you + * expect? + */ + expect(myOneAtom$.get()).toEqual({ + regularProp: __YOUR_TURN__, + string: __YOUR_TURN__, + number: __YOUR_TURN__, + sub: { + string: __YOUR_TURN__, + }, + }); + }); + + describe.skip('lift()', () => { + /** + * Sherlock may feel like a language build on top of Typescript. Sometimes + * you might want to use normal objects and functions and not have to rewrite + * your code. + * In other words, just as keywords like `atom(V)` lifts a variable V to the higher + * level of Derivables, the `lift(F)` keyword lifts a function `F` to the higher + * level of Derivables. + */ + it('example', () => { + // Say I just finished writing this oh-so-complicated function: + const isEvenNumber = (v: number) => v % 2 == 0; + + /** + * Rewriting this function to work with derivables would now be a waste of time. + * This is especially so if you didn't even write the original function, e.g. when + * you use a library function. + * + * ** Your Turn ** + * Use the `lift()` function to change `isEvenNumber` to work on Derivables instead. + * In other words: the new function should take a `Derivable` (or more specifically: + * an `Unwrappable`) and return a `Derivable`. + */ + const isEvenDerivable = __YOUR_TURN__; + + expect(isEvenNumber(2)).toBe(true); + expect(isEvenNumber(13)).toBe(false); + expect(isEvenDerivable(atom(2)).value).toBe(true); + expect(isEvenDerivable(atom(13)).value).toBe(false); + }); + + it('`lift()` as alternative to `.map()`', () => { + // In tutorial 7, we saw `.map()` used in the following context: + const addOne = jest.fn((v: number) => v + 1); + const myAtom$ = atom(1); + + let myMappedDerivable$ = myAtom$.map(addOne); + + expect(myMappedDerivable$.value).toBe(2); + + /** + * ** Your Turn ** + * Now, use `lift()` as alternative to `.map()`. + */ + myMappedDerivable$ = __YOUR_TURN__; + + expect(myMappedDerivable$.value).toBe(2); + }); + }); + + /** + * Sometimes you want to use `derive` but still want to keep certain + * variables in it untracked. In such cases, you can use `peek()`. + */ + it('`peek()`', () => { + const myTrackedAtom$ = atom(1); + const myUntrackedAtom$ = atom(2); + + /** + * ** Your Turn ** + * Use `peek()` to get the value of `myUntrackedAtom$` and add it to the + * value of `myTrackedAtom$`, which should be tracked. + */ + const reactor = jest.fn(v => v); + derive(__YOUR_TURN__).react(reactor); + + expect(reactor).toHaveBeenCalledOnce(); + expect(reactor).toHaveLastReturnedWith(3); + + // there reactor should be called when `myTrackedAtom$` updates + myTrackedAtom$.set(2); + expect(reactor).toHaveBeenCalledTimes(2); + expect(reactor).toHaveLastReturnedWith(4); + + // the reactor should not be called when `myUntrackedAtom$` updates + myUntrackedAtom$.set(3); + expect(reactor).toHaveBeenCalledTimes(2); + expect(reactor).toHaveLastReturnedWith(4); + + // but when `myTrackedAtom$` updates, the value of `myUntrackedAtom$` did change + myTrackedAtom$.set(3); + expect(reactor).toHaveBeenCalledTimes(3); + expect(reactor).toHaveLastReturnedWith(6); + }); + + /** + * Similarly to the `constants` explained in tutorial 7, + * you might want to specify that a variable cannot be updated. + * This can be useful for the programmers themselves, to not + * accidentally update the variable, but it can also be useful for + * optimization. This can be done using the `final` keyword. + */ + describe.skip('`final`', () => { + let myAtom$ = atom(1); + + beforeEach(() => { + myAtom$ = atom(1); + }); + + it('`final` basics', () => { + // Every atom has a `final` property. + expect(myAtom$.final).toBeFalse(); + + // TODO: SHOW THAT CONST ALSO GIVES THE SAME ERROR MESSAGE WHEN SET!! + + // You can make an atom final using the `.makeFinal()` function. + myAtom$.makeFinal(); + expect(myAtom$.final).toBeTrue(); + + /** + * ** Your Turn ** + * What do you think will happen when we try to `.get()` or `.set()` this atom? + */ + // .toThrow() or .not.toThrow()? ↴ + expect(() => myAtom$.get()) /*__YOUR_TURN__*/; + expect(() => myAtom$.set(2)) /*__YOUR_TURN__*/; + + // This behavior is consistent with normal variables created using `const`. + // Alternatively, you can set a last value before setting it to `final`, using `.setFinal()`. + // .toThrow() or .not.toThrow()? ↴ + expect(() => myAtom$.setFinal(2)) /*__YOUR_TURN__*/; + + // There is no way to 'unfinalize' a Derivable, so the only solution to reset is to + // create a whole new Derivable. + myAtom$ = atom(1); + myAtom$.setFinal(2); + expect(myAtom$.final).toBeTrue(); + + // Also interesting: a `constant` as introduced in tutorial 7 is actually a Derivable set to + // `final` in disguise. You can verify this by checking the implementation of `constant` at + // libs/sherlock/src/lib/derivable/factories.ts:39 + const myConstantAtom$ = constant(1); + expect(myConstantAtom$.final).toBe(__YOUR_TURN__); + }); + + it('deriving a `final` Derivable', () => { + const myDerivable$ = myAtom$.derive(v => v + 1); + + const hasReacted = jest.fn(); + myDerivable$.react(hasReacted); + + expect(myDerivable$.final).toBeFalse(); + expect(myDerivable$.connected).toBeTrue(); + + myAtom$.makeFinal(); + + /** + * ** Your Turn ** + * + * What will happen to `myDerivable$` when I change `myAtom$` to be `final`? + */ + expect(myDerivable$.final).toBe(__YOUR_TURN__); + expect(myDerivable$.connected).toBe(__YOUR_TURN__); + + /** + * Derivables that are final (or constant) are no longer tracked. This can save + * a lot of memory and time by cleaning up unused data. Also, when all the variables + * that a Derivable depends on become final, that Derivable itself becomes final too. + * This chains similarly to `unresolved` and `error`. + */ + }); + + it('TODO: `final` State', () => { + /** A property such as `.final`, similar to variables like `.errored` and `.resolved` + * is useful for checking whenever a Derivable is in a certain state, but these properties + * are just a boolean. This means that these properties cannot be derived and we cannot + * have certain functions execute whenever there is a change in the state. For this reason, + * every Derivable holds an internal state, retrievable using `.getState()` which can be + * derived. TODO: Have a clear place where I explain this! Now I have info up top here too. + * + * We have seen that states (`State`) can be `undefined`, `ErrorWrapper`, + * or any regular type `V`. Other states exist, such as the `MaybeFinalState`. This state can be either + * a normal state `State` or a special `FinalWrapper>` state. Let's see that in action. + */ + expect(myAtom$.getMaybeFinalState()).toBe(1); // `getMaybeFinalState` can return a normal state, which in turn can be any normal type. + + myAtom$.makeFinal(); + + expect(myAtom$.getMaybeFinalState()).toBeInstanceOf(FinalWrapper); // but `getMaybeFinalState` can also return a `FinalWrapper` type. + expect(myAtom$.getState()).toBe(1); // the normal type is still the final it contains. + + // TODO: MAAR JE KAN EEN STATE HELEMAAL NIET DERIVEN! + // Dus dat is allemaal onzin lijkt me....??? Bovendien, kan je normale variabelen niet deriven door het gewoon te + // wrappen in een atom ofzo? Of door te structen? + }); + }); + + describe.skip('`Promise`, `Observable`, and `EventPattern`', () => { + /** + * Sherlock can also deal with Promises using the `.fromPromise()` and `.toPromise()` functions. + * This translates Promises directly to Sherlock concepts we have discussed already. + */ + it('`fromPromise()`', async () => { + // we initialize a Promise that will resolve, not reject, when handled + let promise = Promise.resolve(15); + let myAtom$ = fromPromise(promise); + + /** + * ** Your Turn ** + * What do you think is the default state of an atom based on a Promise? + */ + expect(myAtom$.resolved).toBe(__YOUR_TURN__); + expect(myAtom$.final).toBe(__YOUR_TURN__); + + // Now we wait for the Promise to be handled (resolved). + await promise; + + /** + * ** Your Turn ** + * So, what will happen to `myAtom$` and `myMappedAtom$`? + */ + expect(myAtom$.get()).toBe(__YOUR_TURN__); + expect(myAtom$.final).toBe(__YOUR_TURN__); + + // Now we make a promise that is rejected when called. + promise = Promise.reject('Oh no, I messed up!'); + myAtom$ = fromPromise(promise); + + // We cannot await the Promise itself, as it would immediately throw. + await Promise.resolve(); + + /** + * ** Your Turn ** + * So, what will happen to `myAtom$` now? + */ + expect(myAtom$.errored).toBe(__YOUR_TURN__); + expect(myAtom$.error).toBe(__YOUR_TURN__); + expect(myAtom$.final).toBe(__YOUR_TURN__); + }); + + it('`.toPromise()`', async () => { + /** + * `.toPromise()` returns a promise that is linked to the atom it is based on (`myAtom$` here) + * If the atom has a value, the promise is resolved. If the atom errors, the promise is rejected using the same error. + * And it the atom is unresolved, the promise is pending. + */ + let myAtom$ = atom('initial value'); + let promise = myAtom$.toPromise(); + + /** + * ** Your Turn ** + * What do you think will happen when we try to set the atom with a value? + */ + myAtom$.set('second value'); + expect(await promise).toBe(__YOUR_TURN__); + + myAtom$.unset(); + promise = myAtom$.toPromise(); + + /** + * ** Your Turn ** + * We set the atom to `unresolved`. What will now happen when we try to set the atom with a value? + */ + myAtom$.set('third value'); + expect(await promise).toBe(__YOUR_TURN__); + + // Whenever an atom is in an `unresolved` state, the corresponding Promise is pending. + // This means that the Promise can still become resolved or rejected depending on the atom's actions. + + myAtom$.unset(); + promise = myAtom$.toPromise(); + + myAtom$.setError('Error.'); + + /** + * ** Your Turn ** + * We set the atom to an error state. The promise should now be rejected, hence we wrap it in a `try-catch` block. + * What do you think the error message will be? Remember that `try-catch` is not a custom-defined structure. + */ + try { + await promise; + } catch (error: any) { + // `.toBe('Error.')` or `.not.toBe('Error.')`? ↴ + expect(error.message) /*__YOUR_TURN__*/; + } + + myAtom$.set('no more error'); + const myDerivable$ = myAtom$.derive(() => { + throw new Error('Error.'); + }); + promise = myDerivable$.toPromise(); + + /** + * ** Your Turn ** + * We now let `myDerivable$` derive from `myAtom$`, and it will throw a normal error (not a custom Sherlock error). + * What will the error message be this time? + */ + try { + await promise; + } catch (error: any) { + // `.toBe('Error.')` or `.not.toBe('Error.')`? ↴ + expect(error.message) /*__YOUR_TURN__*/; + } + }); + + it('`fromObservable()`', () => { + // Has to do with SUBSCRIBING. Hasn't been discussed either... + // TODO: "As all Derivables are now compatible with rxjs's `from` function, + // we no longer need the `toObservable` function from `@skunkteam/sherlock-rxjs`." + }); + + it('`fromEventPattern`', () => { + // TODO: this is kinda complicated shit... Requires explaining a lot of extra stuff (Subjects, Subscribing, Observables...). Leave for now? + }); + }); +}); diff --git a/generated_tutorial/9 - expert.test.ts b/tutorial/9 - expert.test.ts similarity index 97% rename from generated_tutorial/9 - expert.test.ts rename to tutorial/9 - expert.test.ts index cd382f7..7da22f1 100644 --- a/generated_tutorial/9 - expert.test.ts +++ b/tutorial/9 - expert.test.ts @@ -3,11 +3,9 @@ import { derivableCache } from '@skunkteam/sherlock-utils'; /** * ** Your Turn ** - * * If you see this variable, you should do something about it. :-) */ export const __YOUR_TURN__ = {} as any; - describe.skip('expert', () => { describe.skip('`.autoCache()`', () => { /** @@ -30,7 +28,6 @@ describe.skip('expert', () => { * `hasDerived` is used in the first derivation. But has it been * called at this point? */ - // `.toHaveBeenCalled()` or `.not.toHaveBeenCalled()`? ↴ expect(hasDerived) /* Your Turn */; @@ -185,15 +182,10 @@ describe.skip('expert', () => { expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); /** Can you explain this behavior? */ - // ANSWER-BLOCK-START - // Yes: it creates a different Derivable every time, so it cannot use any caching. - // This is a similar issue to the `pairwise()` issue from tutorial 7, where, when we - // used lambda functions, we made a new pairwise object every time. - // ANSWER-BLOCK-END }); /** - * An other problem can arise when the setup is done inside a derivation + * Another problem can arise when the setup is done inside a derivation */ describe.skip('setup inside a derivation', () => { /** diff --git a/generated_tutorial/jest.config.ts b/tutorial/jest.config.ts similarity index 91% rename from generated_tutorial/jest.config.ts rename to tutorial/jest.config.ts index 98e00f2..d27ca0f 100644 --- a/generated_tutorial/jest.config.ts +++ b/tutorial/jest.config.ts @@ -1,7 +1,7 @@ import type { Config } from 'jest'; export default { - displayName: 'generated_tutorial', + displayName: 'tutorial', preset: '../jest.preset.js', globals: {}, testEnvironment: 'node', diff --git a/generated_tutorial/tsconfig.json b/tutorial/tsconfig.json similarity index 100% rename from generated_tutorial/tsconfig.json rename to tutorial/tsconfig.json diff --git a/generated_tutorial/tsconfig.spec.json b/tutorial/tsconfig.spec.json similarity index 100% rename from generated_tutorial/tsconfig.spec.json rename to tutorial/tsconfig.spec.json From 3739e8a1da04442b2564e05c2bed10d09b8db110 Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 29 Jul 2024 10:19:19 +0200 Subject: [PATCH 26/30] Added '.rejects' and '.resolves' to tutorial 8 --- generateTutorialAndSolution.js | 2 ++ generator/8 - utils.test.ts | 42 ++++++++++++++++++++-------------- solution/3 - reacting.test.ts | 4 ++-- solution/8 - utils.test.ts | 32 +++++++++++++++----------- tutorial/3 - reacting.test.ts | 4 ++-- tutorial/8 - utils.test.ts | 34 ++++++++++++++++----------- 6 files changed, 71 insertions(+), 47 deletions(-) diff --git a/generateTutorialAndSolution.js b/generateTutorialAndSolution.js index 1fe14c1..592749e 100644 --- a/generateTutorialAndSolution.js +++ b/generateTutorialAndSolution.js @@ -88,6 +88,8 @@ function generateTutorialAndSolutions() { return [3 /*break*/, 2]; case 7: return [4 /*yield*/, fs.readdir(generatorFolder)]; case 8: + // These tests will not cause any failing, but are just nice to have. + // e.g. instead of removing excess whitespaces/newlines, we now just prevent them altogether. filenames = (_d.sent()).filter(function (f) { return f.endsWith("test.ts"); }); // the names are the same in all three folders _a = 0, filenames_2 = filenames; _d.label = 9; diff --git a/generator/8 - utils.test.ts b/generator/8 - utils.test.ts index 7a5ff0f..acd59e1 100644 --- a/generator/8 - utils.test.ts +++ b/generator/8 - utils.test.ts @@ -557,7 +557,10 @@ describe('utils', () => { * This translates Promises directly to Sherlock concepts we have discussed already. */ it('`fromPromise()`', async () => { - // we initialize a Promise that will resolve, not reject, when handled + /** + * `.fromPromise()` returns an atom that is linked to the Promise it is based on. + * We initialize a Promise that will resolve, not reject, when handled + */ let promise = Promise.resolve(15); let myAtom$ = fromPromise(promise); @@ -565,9 +568,9 @@ describe('utils', () => { * ** Your Turn ** * What do you think is the default state of an atom based on a Promise? */ - expect(myAtom$.resolved).toBe(__YOUR_TURN__); // #QUESTION + expect(myAtom$.value).toBe(__YOUR_TURN__); // #QUESTION expect(myAtom$.final).toBe(__YOUR_TURN__); // #QUESTION - expect(myAtom$.resolved).toBe(false); // #ANSWER + expect(myAtom$.value).toBe(undefined); // #ANSWER expect(myAtom$.final).toBe(false); // #ANSWER // Now we wait for the Promise to be handled (resolved). @@ -575,19 +578,19 @@ describe('utils', () => { /** * ** Your Turn ** - * So, what will happen to `myAtom$` and `myMappedAtom$`? + * So, what will happen to `myAtom$`? */ - expect(myAtom$.get()).toBe(__YOUR_TURN__); // #QUESTION + expect(myAtom$.value).toBe(__YOUR_TURN__); // #QUESTION expect(myAtom$.final).toBe(__YOUR_TURN__); // #QUESTION - expect(myAtom$.get()).toBe(15); // #ANSWER + expect(myAtom$.value).toBe(15); // #ANSWER expect(myAtom$.final).toBe(true); // #ANSWER // Now we make a promise that is rejected when called. promise = Promise.reject('Oh no, I messed up!'); myAtom$ = fromPromise(promise); - // We cannot await the Promise itself, as it would immediately throw. - await Promise.resolve(); + // As expected, the promise gets rejected. + await expect(promise).rejects.toBe('Oh no, I messed up!'); /** * ** Your Turn ** @@ -603,7 +606,7 @@ describe('utils', () => { it('`.toPromise()`', async () => { /** - * `.toPromise()` returns a promise that is linked to the atom it is based on (`myAtom$` here) + * `.toPromise()` returns a promise that is linked to the atom it is based on (`myAtom$` here). Note how this is the reverse of `fromPromise()`. * If the atom has a value, the promise is resolved. If the atom errors, the promise is rejected using the same error. * And it the atom is unresolved, the promise is pending. */ @@ -615,10 +618,13 @@ describe('utils', () => { * What do you think will happen when we try to set the atom with a value? */ myAtom$.set('second value'); - expect(await promise).toBe(__YOUR_TURN__); // #QUESTION - expect(await promise).toBe('initial value'); // `myAtom$` starts with a value ('initial value'), so the promise is immediately resolved // #ANSWER + // `.resolves` or `.rejects`? ↴ + await expect(promise) /*__YOUR_TURN__*/ // #QUESTION + .toBe(__YOUR_TURN__); // #QUESTION + await expect(promise).resolves.toBe('initial value'); // `myAtom$` starts with a value ('initial value'), so the promise is immediately resolved // #ANSWER + + myAtom$.unset(); // reset - myAtom$.unset(); promise = myAtom$.toPromise(); /** @@ -626,15 +632,17 @@ describe('utils', () => { * We set the atom to `unresolved`. What will now happen when we try to set the atom with a value? */ myAtom$.set('third value'); - expect(await promise).toBe(__YOUR_TURN__); // #QUESTION - expect(await promise).toBe('third value'); // This is now the first value the atom obtains since the promise was created. // #ANSWER + // `.resolves` or `.rejects`? ↴ + await expect(promise) /*__YOUR_TURN__*/ // #QUESTION + .toBe(__YOUR_TURN__); // #QUESTION + await expect(promise).resolves.toBe('third value'); // This is now the first value the atom obtains since the promise was created. // #ANSWER // Whenever an atom is in an `unresolved` state, the corresponding Promise is pending. // This means that the Promise can still become resolved or rejected depending on the atom's actions. - myAtom$.unset(); - promise = myAtom$.toPromise(); + myAtom$.unset(); // reset + promise = myAtom$.toPromise(); myAtom$.setError('Error.'); /** @@ -658,7 +666,7 @@ describe('utils', () => { /** * ** Your Turn ** - * We now let `myDerivable$` derive from `myAtom$`, and it will throw a normal error (not a custom Sherlock error). + * We now let `myDerivable$` derive from `myAtom$`, which will throw a normal error (not a custom Sherlock error). * What will the error message be this time? */ try { diff --git a/solution/3 - reacting.test.ts b/solution/3 - reacting.test.ts index 0479b20..82695bf 100644 --- a/solution/3 - reacting.test.ts +++ b/solution/3 - reacting.test.ts @@ -2,7 +2,8 @@ import { atom } from '@skunkteam/sherlock'; // xxx check my solutions with the actual solutions (https://github.com/skunkteam/sherlock/tree/tutorial-solutions/robin/tutorial) // FIXME: remove all TODO: and FIXME: -// FIXME: check whether the generated tutorials and solutions actually work (e.g. are all solutions correct? No weird shenanigans?) - ALSO CHECK "Or, alternatively"! +// xxx check whether the generated tutorials and solutions actually work (e.g. are all solutions correct? No weird shenanigans?) - +// FIXME: ALSO CHECK "Or, alternatively"! // FIXME: deze file niet linten / builden (voor automatische test). Tutorial ook niet. Maar solutions juist wel! OP EIND. (mag beide wel linten right?) // FIXME: interne review document, mocht ik iets hebben om te laten zien! In Google Drive, zet het erin! // xxx werkt `npm run tutorial` nog? > Nu wel. @@ -10,7 +11,6 @@ import { atom } from '@skunkteam/sherlock'; // nog wel kloppen met de huidige API? Ik heb het gevoel dat dat niet zo is; volgens mij is er geen function "derivation()" // en heet dat nu "derive()" bijvoorbeeld." // FIXME: OOOOOOH JA, ik had eroverheen gepushed! Dat moet nog een PR met terugwerkende kracht worden... (of commits squashen, en dat ze dan maar de commit moeten reviewen?) - // FIXME: Add FromEventPattern + FromObservable // xxx fix the generator for code blocks. // FIXME: now check whether it did not remove excess lines or kept 2 empty lines where it should not. (I think it is good though.) diff --git a/solution/8 - utils.test.ts b/solution/8 - utils.test.ts index a210a0a..4527958 100644 --- a/solution/8 - utils.test.ts +++ b/solution/8 - utils.test.ts @@ -505,7 +505,10 @@ describe('utils', () => { * This translates Promises directly to Sherlock concepts we have discussed already. */ it('`fromPromise()`', async () => { - // we initialize a Promise that will resolve, not reject, when handled + /** + * `.fromPromise()` returns an atom that is linked to the Promise it is based on. + * We initialize a Promise that will resolve, not reject, when handled + */ let promise = Promise.resolve(15); let myAtom$ = fromPromise(promise); @@ -513,7 +516,7 @@ describe('utils', () => { * ** Your Turn ** * What do you think is the default state of an atom based on a Promise? */ - expect(myAtom$.resolved).toBe(false); + expect(myAtom$.value).toBe(undefined); expect(myAtom$.final).toBe(false); // Now we wait for the Promise to be handled (resolved). @@ -521,17 +524,17 @@ describe('utils', () => { /** * ** Your Turn ** - * So, what will happen to `myAtom$` and `myMappedAtom$`? + * So, what will happen to `myAtom$`? */ - expect(myAtom$.get()).toBe(15); + expect(myAtom$.value).toBe(15); expect(myAtom$.final).toBe(true); // Now we make a promise that is rejected when called. promise = Promise.reject('Oh no, I messed up!'); myAtom$ = fromPromise(promise); - // We cannot await the Promise itself, as it would immediately throw. - await Promise.resolve(); + // As expected, the promise gets rejected. + await expect(promise).rejects.toBe('Oh no, I messed up!'); /** * ** Your Turn ** @@ -544,7 +547,7 @@ describe('utils', () => { it('`.toPromise()`', async () => { /** - * `.toPromise()` returns a promise that is linked to the atom it is based on (`myAtom$` here) + * `.toPromise()` returns a promise that is linked to the atom it is based on (`myAtom$` here). Note how this is the reverse of `fromPromise()`. * If the atom has a value, the promise is resolved. If the atom errors, the promise is rejected using the same error. * And it the atom is unresolved, the promise is pending. */ @@ -556,9 +559,11 @@ describe('utils', () => { * What do you think will happen when we try to set the atom with a value? */ myAtom$.set('second value'); - expect(await promise).toBe('initial value'); // `myAtom$` starts with a value ('initial value'), so the promise is immediately resolved + // `.resolves` or `.rejects`? ↴ + await expect(promise).resolves.toBe('initial value'); // `myAtom$` starts with a value ('initial value'), so the promise is immediately resolved + + myAtom$.unset(); // reset - myAtom$.unset(); promise = myAtom$.toPromise(); /** @@ -566,14 +571,15 @@ describe('utils', () => { * We set the atom to `unresolved`. What will now happen when we try to set the atom with a value? */ myAtom$.set('third value'); - expect(await promise).toBe('third value'); // This is now the first value the atom obtains since the promise was created. + // `.resolves` or `.rejects`? ↴ + await expect(promise).resolves.toBe('third value'); // This is now the first value the atom obtains since the promise was created. // Whenever an atom is in an `unresolved` state, the corresponding Promise is pending. // This means that the Promise can still become resolved or rejected depending on the atom's actions. - myAtom$.unset(); - promise = myAtom$.toPromise(); + myAtom$.unset(); // reset + promise = myAtom$.toPromise(); myAtom$.setError('Error.'); /** @@ -596,7 +602,7 @@ describe('utils', () => { /** * ** Your Turn ** - * We now let `myDerivable$` derive from `myAtom$`, and it will throw a normal error (not a custom Sherlock error). + * We now let `myDerivable$` derive from `myAtom$`, which will throw a normal error (not a custom Sherlock error). * What will the error message be this time? */ try { diff --git a/tutorial/3 - reacting.test.ts b/tutorial/3 - reacting.test.ts index ed724d5..d1be7e6 100644 --- a/tutorial/3 - reacting.test.ts +++ b/tutorial/3 - reacting.test.ts @@ -7,7 +7,8 @@ import { atom } from '@skunkteam/sherlock'; export const __YOUR_TURN__ = {} as any; // xxx check my solutions with the actual solutions (https://github.com/skunkteam/sherlock/tree/tutorial-solutions/robin/tutorial) // FIXME: remove all TODO: and FIXME: -// FIXME: check whether the generated tutorials and solutions actually work (e.g. are all solutions correct? No weird shenanigans?) - ALSO CHECK "Or, alternatively"! +// xxx check whether the generated tutorials and solutions actually work (e.g. are all solutions correct? No weird shenanigans?) - +// FIXME: ALSO CHECK "Or, alternatively"! // FIXME: deze file niet linten / builden (voor automatische test). Tutorial ook niet. Maar solutions juist wel! OP EIND. (mag beide wel linten right?) // FIXME: interne review document, mocht ik iets hebben om te laten zien! In Google Drive, zet het erin! // xxx werkt `npm run tutorial` nog? > Nu wel. @@ -15,7 +16,6 @@ export const __YOUR_TURN__ = {} as any; // nog wel kloppen met de huidige API? Ik heb het gevoel dat dat niet zo is; volgens mij is er geen function "derivation()" // en heet dat nu "derive()" bijvoorbeeld." // FIXME: OOOOOOH JA, ik had eroverheen gepushed! Dat moet nog een PR met terugwerkende kracht worden... (of commits squashen, en dat ze dan maar de commit moeten reviewen?) - // FIXME: Add FromEventPattern + FromObservable // xxx fix the generator for code blocks. // FIXME: now check whether it did not remove excess lines or kept 2 empty lines where it should not. (I think it is good though.) diff --git a/tutorial/8 - utils.test.ts b/tutorial/8 - utils.test.ts index 94271b4..387e550 100644 --- a/tutorial/8 - utils.test.ts +++ b/tutorial/8 - utils.test.ts @@ -517,7 +517,10 @@ describe.skip('utils', () => { * This translates Promises directly to Sherlock concepts we have discussed already. */ it('`fromPromise()`', async () => { - // we initialize a Promise that will resolve, not reject, when handled + /** + * `.fromPromise()` returns an atom that is linked to the Promise it is based on. + * We initialize a Promise that will resolve, not reject, when handled + */ let promise = Promise.resolve(15); let myAtom$ = fromPromise(promise); @@ -525,7 +528,7 @@ describe.skip('utils', () => { * ** Your Turn ** * What do you think is the default state of an atom based on a Promise? */ - expect(myAtom$.resolved).toBe(__YOUR_TURN__); + expect(myAtom$.value).toBe(__YOUR_TURN__); expect(myAtom$.final).toBe(__YOUR_TURN__); // Now we wait for the Promise to be handled (resolved). @@ -533,17 +536,17 @@ describe.skip('utils', () => { /** * ** Your Turn ** - * So, what will happen to `myAtom$` and `myMappedAtom$`? + * So, what will happen to `myAtom$`? */ - expect(myAtom$.get()).toBe(__YOUR_TURN__); + expect(myAtom$.value).toBe(__YOUR_TURN__); expect(myAtom$.final).toBe(__YOUR_TURN__); // Now we make a promise that is rejected when called. promise = Promise.reject('Oh no, I messed up!'); myAtom$ = fromPromise(promise); - // We cannot await the Promise itself, as it would immediately throw. - await Promise.resolve(); + // As expected, the promise gets rejected. + await expect(promise).rejects.toBe('Oh no, I messed up!'); /** * ** Your Turn ** @@ -556,7 +559,7 @@ describe.skip('utils', () => { it('`.toPromise()`', async () => { /** - * `.toPromise()` returns a promise that is linked to the atom it is based on (`myAtom$` here) + * `.toPromise()` returns a promise that is linked to the atom it is based on (`myAtom$` here). Note how this is the reverse of `fromPromise()`. * If the atom has a value, the promise is resolved. If the atom errors, the promise is rejected using the same error. * And it the atom is unresolved, the promise is pending. */ @@ -568,9 +571,12 @@ describe.skip('utils', () => { * What do you think will happen when we try to set the atom with a value? */ myAtom$.set('second value'); - expect(await promise).toBe(__YOUR_TURN__); + // `.resolves` or `.rejects`? ↴ + await expect(promise) /*__YOUR_TURN__*/ + .toBe(__YOUR_TURN__); + + myAtom$.unset(); // reset - myAtom$.unset(); promise = myAtom$.toPromise(); /** @@ -578,14 +584,16 @@ describe.skip('utils', () => { * We set the atom to `unresolved`. What will now happen when we try to set the atom with a value? */ myAtom$.set('third value'); - expect(await promise).toBe(__YOUR_TURN__); + // `.resolves` or `.rejects`? ↴ + await expect(promise) /*__YOUR_TURN__*/ + .toBe(__YOUR_TURN__); // Whenever an atom is in an `unresolved` state, the corresponding Promise is pending. // This means that the Promise can still become resolved or rejected depending on the atom's actions. - myAtom$.unset(); - promise = myAtom$.toPromise(); + myAtom$.unset(); // reset + promise = myAtom$.toPromise(); myAtom$.setError('Error.'); /** @@ -608,7 +616,7 @@ describe.skip('utils', () => { /** * ** Your Turn ** - * We now let `myDerivable$` derive from `myAtom$`, and it will throw a normal error (not a custom Sherlock error). + * We now let `myDerivable$` derive from `myAtom$`, which will throw a normal error (not a custom Sherlock error). * What will the error message be this time? */ try { From ef9b79a4908a9fd82f9692b3ca125f803dce39d6 Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 26 Sep 2024 12:54:49 +0200 Subject: [PATCH 27/30] All content has now been added. --- generateTutorialAndSolution.js | 4 +- generateTutorialAndSolution.ts | 12 +- generator/2 - deriving.test.ts | 14 +- generator/3 - reacting.test.ts | 36 --- generator/4 - inner workings.test.ts | 5 +- generator/5 - unresolved.test.ts | 8 +- generator/6 - errors.test.ts | 29 +-- generator/7 - advanced.test.ts | 265 ++++++++++++++++------- generator/8 - utils.test.ts | 248 ++++++++++++++++++--- generator/9 - expert.test.ts | 44 +++- package.json | 3 +- solution/1 - intro.test.ts | 8 +- solution/2 - deriving.test.ts | 6 +- solution/3 - reacting.test.ts | 70 ++---- solution/4 - inner workings.test.ts | 45 ++-- solution/5 - unresolved.test.ts | 38 ++-- solution/6 - errors.test.ts | 39 +--- solution/7 - advanced.test.ts | 264 +++++++++++++++------- solution/8 - utils.test.ts | 305 ++++++++++++++++++++------ solution/9 - expert.test.ts | 87 +++++--- tutorial/1 - intro.test.ts | 8 +- tutorial/2 - deriving.test.ts | 14 +- tutorial/3 - reacting.test.ts | 70 ++---- tutorial/4 - inner workings.test.ts | 45 ++-- tutorial/5 - unresolved.test.ts | 38 ++-- tutorial/6 - errors.test.ts | 39 +--- tutorial/7 - advanced.test.ts | 278 ++++++++++++++++-------- tutorial/8 - utils.test.ts | 313 +++++++++++++++++++++------ tutorial/9 - expert.test.ts | 53 ++--- 29 files changed, 1580 insertions(+), 808 deletions(-) diff --git a/generateTutorialAndSolution.js b/generateTutorialAndSolution.js index 592749e..012adc0 100644 --- a/generateTutorialAndSolution.js +++ b/generateTutorialAndSolution.js @@ -62,7 +62,7 @@ function generateTutorialAndSolutions() { tutorialContent = originalContent .replace(/describe(?!\.skip)/g, "describe.skip") // change `describe` to `describe.skip` .replace(/\n.*?\/\/ #QUESTION-BLOCK-(START|END)/g, "") - .replace(/\/\/ #QUESTION/g, "") // remove `// #QUESTION` comments + .replace(/ \/\/ #QUESTION/g, "") // remove `// #QUESTION` comments .replace(/\n.*?\/\/ #ANSWER-BLOCK-START[\s\S]*?\/\/ #ANSWER-BLOCK-END/g, "") // remove // #ANSWER blocks .replace(/\n.*?\/\/ #ANSWER/g, ""); // .replace(/\n\s*\n\s*\n/g, `\n\n`); // remove excess whitespaces/newlines @@ -73,7 +73,7 @@ function generateTutorialAndSolutions() { solutionContent = originalContent .replace(/describe\.skip/g, "describe") // change `describe.skip` to `describe` .replace(/\n.*?\/\/ #ANSWER-BLOCK-(START|END)/g, "") - .replace(/\/\/ #ANSWER/g, "") // remove `// #ANSWER` comments + .replace(/ \/\/ #ANSWER/g, "") // remove `// #ANSWER` comments .replace(/\n.*?\/\/ #QUESTION-BLOCK-START[\s\S]*?\/\/ #QUESTION-BLOCK-END/g, "") // remove // #QUESTION blocks .replace(/\n.*?\/\/ #QUESTION/g, ""); // .replace(/\n\s*\n\s*\n/g, `\n\n`); // remove excess whitespaces/newlines diff --git a/generateTutorialAndSolution.ts b/generateTutorialAndSolution.ts index cc8d129..658dfca 100644 --- a/generateTutorialAndSolution.ts +++ b/generateTutorialAndSolution.ts @@ -1,8 +1,8 @@ -// import * as fs from 'fs'; import { assert } from 'node:console'; import * as fs from 'node:fs/promises'; -// Run with: tsc generateTutorialAndSolution.ts && node generateTutorialAndSolution.js +// Run this file with: `tsc generateTutorialAndSolution.ts && node generateTutorialAndSolution.js` +// Run this automated with: `npm run generate` const generatorFolder = 'generator'; const tutorialFolder = 'tutorial'; @@ -20,7 +20,7 @@ async function generateTutorialAndSolutions() { let tutorialContent = originalContent .replace(/describe(?!\.skip)/g, `describe.skip`) // change `describe` to `describe.skip` .replace(/\n.*?\/\/ #QUESTION-BLOCK-(START|END)/g, ``) - .replace(/\/\/ #QUESTION/g, ``) // remove `// #QUESTION` comments + .replace(/ \/\/ #QUESTION/g, ``) // remove `// #QUESTION` comments .replace(/\n.*?\/\/ #ANSWER-BLOCK-START[\s\S]*?\/\/ #ANSWER-BLOCK-END/g, ``) // remove // #ANSWER blocks .replace(/\n.*?\/\/ #ANSWER/g, ``); // remove the entire `// #ANSWER` line, including comment // .replace(/\n\s*\n\s*\n/g, `\n\n`); // remove excess whitespaces/newlines @@ -31,7 +31,7 @@ async function generateTutorialAndSolutions() { let solutionContent = originalContent .replace(/describe\.skip/g, `describe`) // change `describe.skip` to `describe` .replace(/\n.*?\/\/ #ANSWER-BLOCK-(START|END)/g, ``) - .replace(/\/\/ #ANSWER/g, ``) // remove `// #ANSWER` comments + .replace(/ \/\/ #ANSWER/g, ``) // remove `// #ANSWER` comments .replace(/\n.*?\/\/ #QUESTION-BLOCK-START[\s\S]*?\/\/ #QUESTION-BLOCK-END/g, ``) // remove // #QUESTION blocks .replace(/\n.*?\/\/ #QUESTION/g, ``); // remove the entire `// #QUESTION` line, including comment // .replace(/\n\s*\n\s*\n/g, `\n\n`); // remove excess whitespaces/newlines @@ -43,8 +43,8 @@ async function generateTutorialAndSolutions() { // These tests will not cause any failing, but are just nice to have. // e.g. instead of removing excess whitespaces/newlines, we now just prevent them altogether. filenames = (await fs.readdir(generatorFolder)).filter(f => f.endsWith(`test.ts`)); // the names are the same in all three folders - for (let filename of filenames) { - for (let foldername of [tutorialFolder, solutionFolder]) { + for (let foldername of [tutorialFolder, solutionFolder]) { + for (let filename of filenames) { let content = await fs.readFile(`${foldername}/${filename}`, 'utf8'); assert(content.match(/\n\s*\n\s*\n/) === null, `no 2 consecutive empty lines in ${foldername}/${filename}`); assert( diff --git a/generator/2 - deriving.test.ts b/generator/2 - deriving.test.ts index aba4a4d..255361f 100644 --- a/generator/2 - deriving.test.ts +++ b/generator/2 - deriving.test.ts @@ -76,11 +76,11 @@ describe('deriving', () => { // Should return 'Fizz' when `myCounter$` is a multiple of 3 and '' otherwise. const fizz$: Derivable = myCounter$.derive(__YOUR_TURN__); // #QUESTION - const fizz$: Derivable = myCounter$.derive(v => (v % 3 ? '' : 'Fizz')); // Shorthand for `v % 3 !== 0` + const fizz$: Derivable = myCounter$.derive(v => (v % 3 ? '' : 'Fizz')); // Shorthand for `v % 3 !== 0` // #ANSWER // Should return 'Buzz' when `myCounter$` is a multiple of 5 and '' otherwise. const buzz$: Derivable = myCounter$.derive(__YOUR_TURN__); // #QUESTION - const buzz$: Derivable = myCounter$.derive(v => (v % 5 ? '' : 'Buzz')); + const buzz$: Derivable = myCounter$.derive(v => (v % 5 ? '' : 'Buzz')); // #ANSWER const fizzBuzz$: Derivable = derive(__YOUR_TURN__); // #QUESTION // #ANSWER-BLOCK-START @@ -160,11 +160,11 @@ describe('deriving', () => { const lastTweet = pastTweets[tweetCount - 1]; expect(tweetCount).toEqual(__YOUR_TURN__); // Is there a new tweet? // #QUESTION - expect(tweetCount).toEqual(3); // Is there a new tweet?// #ANSWER - expect(lastTweet).toContain(__YOUR_TURN__); // Who sent it? Donald? Or Barack?// #QUESTION - expect(lastTweet).toContain('Donald'); // Who sent it? Donald? Or Barack?// #ANSWER - expect(lastTweet).toContain(__YOUR_TURN__); // What did he tweet?// #QUESTION - expect(lastTweet).toContain('politics'); // What did he tweet?// #ANSWER + expect(tweetCount).toEqual(3); // Is there a new tweet? // #ANSWER + expect(lastTweet).toContain(__YOUR_TURN__); // Who sent it? Donald? Or Barack? // #QUESTION + expect(lastTweet).toContain('Donald'); // Who sent it? Donald? Or Barack? // #ANSWER + expect(lastTweet).toContain(__YOUR_TURN__); // What did he tweet? // #QUESTION + expect(lastTweet).toContain('politics'); // What did he tweet? // #ANSWER /** * As you can see, this is something to look out for. diff --git a/generator/3 - reacting.test.ts b/generator/3 - reacting.test.ts index 29f6bc3..342025a 100644 --- a/generator/3 - reacting.test.ts +++ b/generator/3 - reacting.test.ts @@ -7,42 +7,6 @@ import { atom } from '@skunkteam/sherlock'; */ export const __YOUR_TURN__ = {} as any; // #QUESTION-BLOCK-END -// xxx check my solutions with the actual solutions (https://github.com/skunkteam/sherlock/tree/tutorial-solutions/robin/tutorial) -// FIXME: remove all TODO: and FIXME: -// xxx check whether the generated tutorials and solutions actually work (e.g. are all solutions correct? No weird shenanigans?) - -// FIXME: ALSO CHECK "Or, alternatively"! -// FIXME: deze file niet linten / builden (voor automatische test). Tutorial ook niet. Maar solutions juist wel! OP EIND. (mag beide wel linten right?) -// FIXME: interne review document, mocht ik iets hebben om te laten zien! In Google Drive, zet het erin! -// xxx werkt `npm run tutorial` nog? > Nu wel. -// xxx PETER: "nu je toch met Sherlock bezig bent; zou je voor mij eens kunnen checken of de code voorbeelden in de README -// nog wel kloppen met de huidige API? Ik heb het gevoel dat dat niet zo is; volgens mij is er geen function "derivation()" -// en heet dat nu "derive()" bijvoorbeeld." -// FIXME: OOOOOOH JA, ik had eroverheen gepushed! Dat moet nog een PR met terugwerkende kracht worden... (of commits squashen, en dat ze dan maar de commit moeten reviewen?) -// FIXME: Add FromEventPattern + FromObservable -// xxx fix the generator for code blocks. -// FIXME: now check whether it did not remove excess lines or kept 2 empty lines where it should not. (I think it is good though.) -/** - * x Final States; (finalGetter, finalMethod, getMaybeFinalState, FinalWrapper, MaybeFinalState, _isFinal, makeFinal, markFinal, .final, .finalized, setFinal...) - * ? Lens; (libs/sherlock/src/lib/derivable/lens.ts) - map die twee kanten op kan gaan. Maar een map kan dat al? Maar hier kan - * je dat los definieren! Je kan gewoon `lens` ipv `var.lens`. Zelden dat je dit gebruikt. Output is een Derivable though. - * x Lift; (libs/sherlock-utils/src/lib/lift.ts) - * x Peek; (libs/sherlock-utils/src/lib/peek.ts) - dan track je niet. In een derivable, deze tracked hij dan niet (ipv .get() waar het wel getracked wordt) - * x Template; (libs/sherlock-utils/src/lib/template.ts) - to make a string using a template literal. (Uses unwrap!!) - * / Factory; (libs/sherlock/src/lib/derivable/factories.ts) - simply contains functions to create objects, namely - * lens; atom; constant; derive. - * !! Flat-map; (libs/sherlock/src/lib/derivable/mixins/flat-map.ts) - ??? - * array: nested arrays naar array - * Derivable: gooit er derive.get() achteraan? - * Derivable (input van inputveld). Flatmap geeft Derivable terug. Derivable.flatmap() returned misschien - * Derivable, returned dan de number. flatMap is een `derive`, maar wat hij returned haalt hij uit de Derivable. - * ofzoiets. Maakt code korter. - * x Fallback-to; - op een derivable. Als een atom `unresolved` is, dan fallt het back to this value. Ofwel, initial value, maar - * ook als hij later unresolved wordt, dan wordt hij dit (vaak wel initial value). - * x Take - react options gebruiken buiten react. In een derivable chain, halverwege die options gebruiken. - * -- e.g. (from)Promise. Zodra die een waarde aanneemt kan hij niet meer veranderen. - * Let FromPromise, FromObservable, FromEventPattern ook uit (in utils?), ToPromise, ToObservable, in praktijk ook handig. - * FromEventPattern (haily mary, als alles niet werkt, dan dit doen). - */ /** * In the intro we have seen a basic usage of the `.react()` method. * Let's dive a bit deeper into the details of this method. diff --git a/generator/4 - inner workings.test.ts b/generator/4 - inner workings.test.ts index 637695f..b5bcdf4 100644 --- a/generator/4 - inner workings.test.ts +++ b/generator/4 - inner workings.test.ts @@ -337,10 +337,11 @@ describe('inner workings', () => { /** * In `@skunkteam/sherlock` equality is a bit complex: * - * First we check `Object.is()` equality, if that is true, it is the + * First we check `Object.is()` equality. If that is true, it is the * same, you can't deny that. * - * After that it is pluggable. It can be anything you want. TODO: what is pluggable? + * After that it is pluggable. This means that you can 'plug in' or 'define' + * the definition for equality yourself. * * By default we try to use `.equals()`, to support libraries like * `ImmutableJS`. diff --git a/generator/5 - unresolved.test.ts b/generator/5 - unresolved.test.ts index 94d519e..3a74024 100644 --- a/generator/5 - unresolved.test.ts +++ b/generator/5 - unresolved.test.ts @@ -206,14 +206,14 @@ describe('unresolved', () => { const mySafeAtom$ = myAtom$.fallbackTo(__YOUR_TURN__); // #QUESTION const mySafeAtom$ = myAtom$.fallbackTo(() => 3); // #ANSWER - expect(myAtom$.value).toBe(0); - expect(mySafeAtom$.value).toBe(0); + expect(myAtom$.get()).toBe(0); + expect(mySafeAtom$.get()).toBe(0); myAtom$.unset(); expect(myAtom$.resolved).toBeFalse(); expect(mySafeAtom$.resolved).toBeTrue(); - expect(myAtom$.value).toBeUndefined(); - expect(mySafeAtom$.value).toBe(3); + expect(() => myAtom$.get()).toThrow(); + expect(mySafeAtom$.get()).toBe(3); }); }); diff --git a/generator/6 - errors.test.ts b/generator/6 - errors.test.ts index ad59ffe..bb9565d 100644 --- a/generator/6 - errors.test.ts +++ b/generator/6 - errors.test.ts @@ -1,4 +1,4 @@ -import { atom, DerivableAtom, error } from '@skunkteam/sherlock'; +import { atom, DerivableAtom } from '@skunkteam/sherlock'; // #QUESTION-BLOCK-START /** @@ -30,20 +30,17 @@ describe('errors', () => { expect(myAtom$.errored).toBe(true); expect(myAtom$.error).toBe('my Error'); - // expect(myAtom$.get).toThrow("Cannot read properties of undefined (reading 'getState')"); - // TODO: WHAT - normally this works, but internal JEST just fucks with me....? - // What will happen if you try to call `get()` on `myAtom$`? // `.toThrow()` or `.not.toThrow()`? ↴ - expect(() => myAtom$.get()) /* __YOUR_TURN__ */; // #QUESTION + // expect(() => myAtom$.get()) /* __YOUR_TURN__ */; // #QUESTION expect(() => myAtom$.get()).toThrow('my Error'); // #ANSWER // ** __YOUR_TURN__ ** // What will happen if you try to call `set()` on `myAtom$`? // `.toThrow()` or `.not.toThrow()`? ↴ - expect(() => myAtom$.set(2)) /* __YOUR_TURN__ */; // #QUESTION + // expect(() => myAtom$.set(2)) /* __YOUR_TURN__ */; // #QUESTION expect(() => myAtom$.set(2)).not.toThrow(); // #ANSWER - expect(myAtom$.errored).toBe(__YOUR_TURN__); // #QUESTION + // expect(myAtom$.errored).toBe(__YOUR_TURN__); // #QUESTION expect(myAtom$.errored).toBe(false); // #ANSWER // Interestingly, calling `set()` does not throw an error. In fact, it removes the error state @@ -51,24 +48,6 @@ describe('errors', () => { expect(() => myAtom$.get()).not.toThrow(); }); - /** - * libs/sherlock/src/lib/interfaces.ts:289 shows the basic states that a Derivable can have. - * > `export type State = V | unresolved | ErrorWrapper;` - * A state can be either any type `V` (`number`, `string`, etc.), `unresolved` as we saw in the - * previous tutorial, or `ErrorWrapper`. This last state is explained here. - */ - it('error states', () => { - expect(myAtom$.getState()).toBe(1); // as explained above, any type can be a state - - myAtom$.setError('my Error'); - - // The `ErrorWrapper` state only holds an error string. The `error()` function returns - // such an `ErrorWrapper` which we can use to compare. - expect(myAtom$.getState()).toMatchObject(error('my Error')); - - // TODO: more! There wasn't a question in here. Maybe combine with Final States? NO, that one should go! - }); - it('deriving an error', () => { const myDerivable$ = myAtom$.derive(v => v + 1); diff --git a/generator/7 - advanced.test.ts b/generator/7 - advanced.test.ts index 6b6b1f3..81b2dc7 100644 --- a/generator/7 - advanced.test.ts +++ b/generator/7 - advanced.test.ts @@ -1,4 +1,13 @@ -import { atom, constant, Derivable, derive, SettableDerivable, unresolved } from '@skunkteam/sherlock'; +import { + atom, + constant, + Derivable, + derive, + ErrorWrapper, + SettableDerivable, + State, + unresolved, +} from '@skunkteam/sherlock'; import { template } from '@skunkteam/sherlock-utils'; import { Map as ImmutableMap } from 'immutable'; @@ -100,7 +109,7 @@ describe('advanced', () => { expect(myLimitedAtom$.resolved).toBe(false); myAtom$.set('allowed'); expect(myLimitedAtom$.resolved).toBe(true); - expect(myLimitedAtom$.value).toBe('allowed'); + expect(myLimitedAtom$.get()).toBe('allowed'); }); /** @@ -337,104 +346,210 @@ describe('advanced', () => { const myList = [1]; const myMappedList = myList.map(addOne); - expect(myMappedList).toMatchObject([2]); + expect(myMappedList).toMatchObject(__YOUR_TURN__); // #QUESTION + expect(myMappedList).toMatchObject([2]); // #ANSWER const myAtom$ = atom(1); let myMappedDerivable$ = myAtom$.map(addOne); - expect(myMappedDerivable$.value).toBe(2); + expect(myMappedDerivable$.value).toBe(__YOUR_TURN__); // #QUESTION + expect(myMappedDerivable$.value).toBe(2); // #ANSWER // You can combine them too. const myAtom2$ = atom([1]); const myMappedDerivable2$ = myAtom2$.map(v => v.map(addOne)); - expect(myMappedDerivable2$.value).toMatchObject([2]); + expect(myMappedDerivable2$.value).toMatchObject(__YOUR_TURN__); // #QUESTION + expect(myMappedDerivable2$.value).toMatchObject([2]); // #ANSWER }); /** - * In order to reason over the state of a Derivable, we can - * use `.mapState()`. This will map one state to another, and - * can be used to get rid of pesky `unresolved` or `Errorwrapper` - * states. + * Although the `.map()` function can be reversed, the intended flow of the + * function is still meant to go the original non-reversed way. This means that, + * if the reverse flow is used, the non-reverse flow is also activated. We will + * show what that means. */ - it('`.mapState()`', () => { + it('one-way flow', () => { const myAtom$ = atom(1); - // like `.map()`, we can specify it both ways. - const myMappedAtom$ = myAtom$.mapState( - state => (state === unresolved ? 3 : state), // `myAtom$` => `myMappedAtom$` - state => (state === 2 ? unresolved : state), // `myMappedAtom$` => `myAtom$` + const myMappedAtom$ = myAtom$.map( + n => n + 1, + n => n * 2, ); - myAtom$.set(2); - expect(myAtom$.resolved).toBe(__YOUR_TURN__); // #QUESTION - expect(myAtom$.resolved).toBe(true); // #ANSWER - expect(myMappedAtom$.resolved).toBe(__YOUR_TURN__); // #QUESTION - expect(myMappedAtom$.resolved).toBe(true); // #ANSWER + // This may seem logical... + myAtom$.set(5); + expect(myAtom$.value).toBe(__YOUR_TURN__); // #QUESTION + expect(myMappedAtom$.value).toBe(__YOUR_TURN__); // #QUESTION + expect(myAtom$.value).toBe(5); // #ANSWER + expect(myMappedAtom$.value).toBe(6); // #ANSWER - myAtom$.unset(); - expect(myAtom$.resolved).toBe(__YOUR_TURN__); // #QUESTION - expect(myAtom$.resolved).toBe(false); // #ANSWER - expect(myMappedAtom$.resolved).toBe(__YOUR_TURN__); // #QUESTION - expect(myMappedAtom$.resolved).toBe(true); // #ANSWER - - myMappedAtom$.set(2); - expect(myAtom$.resolved).toBe(__YOUR_TURN__); // #QUESTION - expect(myAtom$.resolved).toBe(false); // #ANSWER - expect(myMappedAtom$.resolved).toBe(__YOUR_TURN__); // #QUESTION - expect(myMappedAtom$.resolved).toBe(true); // #ANSWER - - // This is a tricky one: - myMappedAtom$.unset(); - expect(myAtom$.resolved).toBe(__YOUR_TURN__); // #QUESTION - expect(myAtom$.resolved).toBe(false); // #ANSWER - expect(myMappedAtom$.resolved).toBe(__YOUR_TURN__); // #QUESTION - expect(myMappedAtom$.resolved).toBe(true); // #ANSWER + // ...but this may seem weird. + myMappedAtom$.set(5); + expect(myAtom$.value).toBe(__YOUR_TURN__); // #QUESTION + expect(myMappedAtom$.value).toBe(__YOUR_TURN__); // #QUESTION + expect(myAtom$.value).toBe(10); // #ANSWER + expect(myMappedAtom$.value).toBe(11); // #ANSWER /** - * The results, especially of the last case, may seem weird. - * In the first exercise, `myAtom$` is set to 2, causing the state to be 2 as well. - * By setting the state of `myAtom$`, the first line of `mapState()` is triggered. - * Since `2` is not equal to `unresolved`, we return the state `2`, causing - * `myMappedAtom$` to also get state 2 (and thus: value 2). Neither are unresolved. + * `.map()` is intended to use one-way, from `myAtom$` to `myMappedAtom$`. + * The reverse direction or 'flow', of setting `myMappedAtom$` and mapping it to `myAtom$` is not + * the intended flow, and is used only as a shortcut to alter `myAtom$`. However, if you do this, + * `myAtom$` will notice that it is changed and thus will trigger another call of `.map()`, now + * from `myAtom$` to `myMappedAtom$`! Thus, `myMappedAtom$` is changed again. * - * In the second case, `myAtom$` is set to `unresolved`, triggering the first line of - * `mapState()`, letting `myMappedAtom$` become 3. `myAtom$` is now `unresolved`, and - * `myMappedAtom$` is not. + * Although this behavior is intended, it may give seemingly weird situations like this where + * you set `myMappedAtom$` to the value 5, yet it "suddenly" has value 11. * - * In the third case, `myMappedAtom$` is set to 2, it triggers the second line of - * `mapState()`, causing `myAtom$` to become `unresolved`. However, what we don't - * notice is that this change in state triggers the first line of `mapState()` again, - * causing `myMappedAtom$` to get state `3`. We can check this: + * Also note that removing the second case of `.map()`, so for the reverse direction, will actually have effects + * on the typing of `myMappedAtom$`: it will become a `Derivable` instead of a `DerivableAtom`, + * which also means it does not have a `.set()` method anymore. Try it out by commenting out the second line of `.map()`! */ + }); + + it('`.flatMap()`', () => { + const myAtom$ = atom(0); + const atomize = jest.fn((n: number) => atom(n)); // turn a number into an atom. + /** + * Sometimes you use `.map()`, but the result of the function within the `.map()` is also a Derivable. + * The result would be a `Derivable>` (like the return type of `.map()` below: hover over it to see) + */ + myAtom$.map(atomize); - myMappedAtom$.set(2); - expect(myMappedAtom$.get()).toBe(3); // the state and value are linked, so this is identical to `.getState()` /** - * You might think that this change in state would cause `myAtom$` to now also get - * `3` as its state, but this does not happen. Why not? TODO: maximally one cycle? ASK! - * Since both `2` and `3` are not `unresolved`, it does not matter to our answer. + * You would have to use `.get()` to go back to a single Derivable. Similarly how `flatMap` can + * reduce lists of lists to a single list, it can help reduce Derivables of Derivables to a + * single Derivable. * - * The same cannot be said for the fourth case. Setting `myMappedAtom$` to `unresolved` - * triggers the second line of `mapState()`, causing `myAtom$` to also become `unresolved`. This, in turn, - * triggers the first line of `mapState()`, causing `myMappedAtom$` to become `3`. - * As such, `myMappedAtom$` is not `unresolved` even though we set it as such. - * TODO: change this to be for MAP. Then make MAPSTATE a trivial one right after. + * ** Your Turn ** + * + * Rewrite the first line using `.flatMap()`. */ + let myMappedAtom$ = myAtom$.map(atomize).derive(v => v.get()); // the `derive()` uses `get()` to remove one layer of `Derivable` + myMappedAtom$ = __YOUR_TURN__ as Derivable; // #QUESTION + myMappedAtom$ = myAtom$.flatMap(atomize) as Derivable; // #ANSWER + + myAtom$.set(1); + expect(myMappedAtom$.get()).toBe(1); + expect(atomize).toHaveBeenCalledTimes(1); + + // `.flatMap()`, like `.map()`, is a common functionality of standard libraries and can be used on e.g. arrays. + const myList = [1, 2, 3]; + const myMappedList = myList.map(v => [v, v + 1]).flat(); + const myFlatMappedList = __YOUR_TURN__; // #QUESTION + const myFlatMappedList = myList.flatMap(v => [v, v + 1]); // #ANSWER + expect(myMappedList).toEqual(myFlatMappedList); }); + }); - // FIXME: - it('TEMP Flat-map', () => { - // const myAtom$ = atom(0); - // const mapping = (v: any) => atom(v); - // Sometimes you use `map()`, but the result within the `map()` is also a Derivable. - // The result would here be a `Derivable>` (hover over `derive` to see this). - // You would have to use `.get()` to go back to a single Derivable. Similarly how `flatMap` can - // reduce lists of lists to a single list, it can help reduce Derivables of Derivables to a - // single Derivable. If you have something like this: - // let myAtom$$ = myAtom$.map(n => mapping(n)).derive(v => v.get()); - // You can now rewrite it to this: - // myAtom$$ = myAtom$.flatMap(n => mapping(n)); - // It only results in slightly shorter code. - // TODO: right? + /** + * Every Derivable also contains a `State`. This state contains all the information of a Derivable in one place, + * such as whether it is a value, unresolved, or an error. + */ + describe('States', () => { + /** + * libs/sherlock/src/lib/interfaces.ts:289 shows all that a State can be. + * ``` + * export type State = V | unresolved | ErrorWrapper; + * ``` + */ + it('value states, unresolved states, and error states', () => { + const myAtom$ = atom(1); + /** + * ** Your Turn ** + * + * What do you expect the state to be? + */ + expect(myAtom$.getState()).toBe(__YOUR_TURN__); // #QUESTION + expect(myAtom$.getState()).toBe(1); // #ANSWER + + /** + * We cannot directly set the state of `myAtom$` as there is no `setState()` function, + * but it will change automatically when we change the value of `myAtom$`. + */ + myAtom$.unset(); + + /** + * ** Your Turn ** + * + * What do you expect the state to be? + */ + expect(myAtom$.getState()).toBe(__YOUR_TURN__); // #QUESTION + expect(myAtom$.getState()).toBe(unresolved); // #ANSWER + + myAtom$.setError('my Error'); + + /** + * ** Your Turn ** + * + * What do you expect the state to be? + */ + expect(myAtom$.getState()).toBeInstanceOf(__YOUR_TURN__); // #QUESTION + expect(myAtom$.getState()).toBeInstanceOf(ErrorWrapper); // #ANSWER + + /** + * Here is an example of when a state can be useful. Using the concept of type 'narrowing', we can check + * on type-level what state the atom is in and we can vary our return value accordingly. + * Study the following function and then fill in the expectations below. + */ + function stateToString(state: State): string { + if (state instanceof ErrorWrapper) { + // We know `state` is of type 'ErrorWrapper', which allows us to grab a property such as 'error' from it. + // Note that our `error` is `ErrorWrapper.error`, not `Derivable.error` such as when using `myAtom$.error`. + return state.error as string; + } else if (typeof state === 'number') { + // We know `state` is of type 'number', so we can apply numerical functions to it. + return String(state + 1); + } else { + // We know `state` must now be of type 'unresolved'. + return state.toString(); + } + } + + myAtom$.set(1); + expect(stateToString(myAtom$.getState())).toBe(__YOUR_TURN__); // #QUESTION + expect(stateToString(myAtom$.getState())).toBe('2'); // #ANSWER + + myAtom$.unset(); + expect(stateToString(myAtom$.getState())).toBe(__YOUR_TURN__); // #QUESTION + expect(stateToString(myAtom$.getState())).toBe('Symbol(unresolved)'); // #ANSWER + + myAtom$.setError('OH NO!'); + expect(stateToString(myAtom$.getState())).toBe(__YOUR_TURN__); // #QUESTION + expect(stateToString(myAtom$.getState())).toBe('OH NO!'); // #ANSWER + }); + + /** + * In order to reason over the state of a Derivable, we can + * use `.mapState()`. This will map one state to another, and + * can be used to get rid of pesky `unresolved` or `Errorwrapper` + * states. + */ + it('`.mapState()`', () => { + const myAtom$ = atom(1); + + const myMappedAtom$ = myAtom$.mapState( + state => (state === unresolved || state instanceof ErrorWrapper ? 0 : state), // `myAtom$` => `myMappedAtom$` + state => state, // `myMappedAtom$` => `myAtom$` + ); + + myAtom$.set(2); + expect(myAtom$.value).toBe(__YOUR_TURN__); // #QUESTION + expect(myAtom$.value).toBe(2); // #ANSWER + expect(myMappedAtom$.value).toBe(__YOUR_TURN__); // #QUESTION + expect(myMappedAtom$.value).toBe(2); // #ANSWER + + myAtom$.unset(); + expect(myAtom$.value).toBe(__YOUR_TURN__); // #QUESTION + expect(myAtom$.value).toBe(undefined); // #ANSWER + expect(myMappedAtom$.value).toBe(__YOUR_TURN__); // #QUESTION + expect(myMappedAtom$.value).toBe(0); // #ANSWER + + // This is a tricky one. Remember the intended flow of the `map()` function, + // as it is the same for the `mapState()` function. + myMappedAtom$.unset(); + expect(myAtom$.value).toBe(__YOUR_TURN__); // #QUESTION + expect(myAtom$.value).toBe(undefined); // #ANSWER + expect(myMappedAtom$.value).toBe(__YOUR_TURN__); // #QUESTION + expect(myMappedAtom$.value).toBe(0); // #ANSWER }); }); @@ -444,7 +559,7 @@ describe('advanced', () => { * `Derivable`, one of those values can be plucked into a new `Derivable`. * This plucked `Derivable` can be settable, if the source supports it. * - * The way properties are plucked is pluggable, but by default both // TODO: no-one here knows what "pluggable" is. Or ImmutableJS. + * The way properties are plucked is 'pluggable' (customizable), but by default both * `.get()` and `[]` are supported to support * basic Objects, Maps and Arrays. * @@ -452,7 +567,7 @@ describe('advanced', () => { * does not. This means that setting a plucked property of a regular * Object/Array/Map will not cause any reaction on that source `Derivable`. * - * ImmutableJS can help fix this problem* + * ImmutableJS can help fix this problem. */ describe('`.pluck()`', () => { const reactSpy = jest.fn(); diff --git a/generator/8 - utils.test.ts b/generator/8 - utils.test.ts index acd59e1..239cdcc 100644 --- a/generator/8 - utils.test.ts +++ b/generator/8 - utils.test.ts @@ -1,5 +1,16 @@ -import { atom, constant, derive, FinalWrapper } from '@skunkteam/sherlock'; -import { fromPromise, lift, pairwise, peek, scan, struct } from '@skunkteam/sherlock-utils'; +import { atom, constant, Derivable, derive, ErrorWrapper, FinalWrapper } from '@skunkteam/sherlock'; +import { + fromEventPattern, + fromObservable, + fromPromise, + lift, + pairwise, + peek, + scan, + struct, +} from '@skunkteam/sherlock-utils'; +import { Atom } from 'libs/sherlock/src/internal'; +import { from, Observable, Subject } from 'rxjs'; // #QUESTION-BLOCK-START /** @@ -14,7 +25,11 @@ expect(scan).toBe(scan); expect(struct).toBe(struct); expect(peek).toBe(peek); expect(lift).toBe(lift); -expect(FinalWrapper).toBe(FinalWrapper); // TODO: not sure whether needed +expect(fromObservable).toBe(fromObservable); +expect(from).toBe(from); +expect(ErrorWrapper).toBe(ErrorWrapper); +expect(Observable).toBe(Observable); +expect(FinalWrapper).toBe(FinalWrapper); // #QUESTION-BLOCK-END /** * In the `sherlock-utils` lib, there are a couple of functions that can combine @@ -460,8 +475,6 @@ describe('utils', () => { // Every atom has a `final` property. expect(myAtom$.final).toBeFalse(); - // TODO: SHOW THAT CONST ALSO GIVES THE SAME ERROR MESSAGE WHEN SET!! - // You can make an atom final using the `.makeFinal()` function. myAtom$.makeFinal(); expect(myAtom$.final).toBeTrue(); @@ -477,6 +490,7 @@ describe('utils', () => { expect(() => myAtom$.set(2)).toThrow('cannot set a final derivable'); // #ANSWER // This behavior is consistent with normal variables created using `const`. + // Alternatively, you can set a last value before setting it to `final`, using `.setFinal()`. // .toThrow() or .not.toThrow()? ↴ expect(() => myAtom$.setFinal(2)) /*__YOUR_TURN__*/; // #QUESTION @@ -526,28 +540,26 @@ describe('utils', () => { */ }); - it('TODO: `final` State', () => { - /** A property such as `.final`, similar to variables like `.errored` and `.resolved` - * is useful for checking whenever a Derivable is in a certain state, but these properties - * are just a boolean. This means that these properties cannot be derived and we cannot - * have certain functions execute whenever there is a change in the state. For this reason, - * every Derivable holds an internal state, retrievable using `.getState()` which can be - * derived. TODO: Have a clear place where I explain this! Now I have info up top here too. - * - * We have seen that states (`State`) can be `undefined`, `ErrorWrapper`, - * or any regular type `V`. Other states exist, such as the `MaybeFinalState`. This state can be either - * a normal state `State` or a special `FinalWrapper>` state. Let's see that in action. + it('`final` State', () => { + /** + * We have seen that states (`State`) can be `unresolved`, `ErrorWrapper`, + * or any regular type `V`. If you want to also show whether a Derivable is `final`, you can + * use the `MaybeFinalState`, which is either any normal `State` or a special + * `FinalWrapper>` state. Let's see that in action. */ - expect(myAtom$.getMaybeFinalState()).toBe(1); // `getMaybeFinalState` can return a normal state, which in turn can be any normal type. + myAtom$.set(2); + expect(myAtom$.getMaybeFinalState()).toBe(__YOUR_TURN__); // #QUESTION + expect(myAtom$.getMaybeFinalState()).toBe(2); // `getMaybeFinalState` can return a normal state, which in turn can be any normal type. // #ANSWER - myAtom$.makeFinal(); + myAtom$.setError('2'); + expect(myAtom$.getMaybeFinalState()).toBeInstanceOf(__YOUR_TURN__); // #QUESTION + expect(myAtom$.getMaybeFinalState()).toBeInstanceOf(ErrorWrapper); // `getMaybeFinalState()` can return a normal state, which in turn can be unresolved. // #ANSWER - expect(myAtom$.getMaybeFinalState()).toBeInstanceOf(FinalWrapper); // but `getMaybeFinalState` can also return a `FinalWrapper` type. - expect(myAtom$.getState()).toBe(1); // the normal type is still the final it contains. - - // TODO: MAAR JE KAN EEN STATE HELEMAAL NIET DERIVEN! - // Dus dat is allemaal onzin lijkt me....??? Bovendien, kan je normale variabelen niet deriven door het gewoon te - // wrappen in een atom ofzo? Of door te structen? + myAtom$.setFinal(2); + expect(myAtom$.getMaybeFinalState()).toBeInstanceOf(__YOUR_TURN__); // #QUESTION + expect(myAtom$.getState()).toBe(__YOUR_TURN__); // #QUESTION + expect(myAtom$.getMaybeFinalState()).toBeInstanceOf(FinalWrapper); // but `getMaybeFinalState)_` can also return a `FinalWrapper` type. // #ANSWER + expect(myAtom$.getState()).toBe(2); // the normal `getState()` function cannot return a FinalWrapper. // #ANSWER }); }); @@ -678,14 +690,192 @@ describe('utils', () => { } }); + /** + * Some reactive libraries already existed, such as RxJS. + * Angular uses RxJS, and since we use Angular, we are forced to use RxJS. + * However, RxJS becomes more and more complicated and user-unfriendly + * as your application becomes bigger. This was the main reason why + * Sherlock was developed. + * As Angular uses RxJS, our Sherlock library needs to be compatible with it. + * The `fromObservable()` and `toObservable()` functions are used for this. + */ it('`fromObservable()`', () => { - // Has to do with SUBSCRIBING. Hasn't been discussed either... - // TODO: "As all Derivables are now compatible with rxjs's `from` function, - // we no longer need the `toObservable` function from `@skunkteam/sherlock-rxjs`." + /** + * RxJS uses `Observables` which are similar to our `Derivables`. + * It also uses the concept of `Subscribing`, which is similar to `Deriving`. + * + * Here's an example of the similarities and differences. + */ + + const dummyObservable = new Subject(); // `Subject` is a form of `Observable` + let subscribedToDummy; + dummyObservable.subscribe({ next: x => (subscribedToDummy = x) }); + dummyObservable.next(2); + expect(subscribedToDummy).toBe(2); + + const dummyDerivable$ = new Atom(1); + const derivedOfDummy = dummyDerivable$.derive(x => x); + dummyDerivable$.set(2); + expect(derivedOfDummy.value).toBe(2); + + /** + * The code for turning an Observable "observable" into a derivable "value$" is + * like this (from libs/sherlock-utils/src/lib/from-observable.ts): + * + * ``` + * observable.subscribe({ + * next: value => value$.set(value), + * error: err => value$.setFinal(error(err)), + * complete: () => value$.makeFinal(), + * }); + * return () => subscription.unsubscribe(); + * ``` + * + * Essentially, + * - we map `next()` (Observable) to `set()` (Derivable); + * - we map `error()` (Observable) to `setFinal(error())` (Derivable); + * NOTE: we don't map it to `setError()` as we would then be able to undo the error state. We can't undo it when it's final. + * - we map`complete()` (Observable) to `makeFinal()` (Derivable); + * - and we return a function we can call to stop (similar to using `react()`), which is mapped to `unsubscribe()` + * NOTE: in fact, `fromEventPattern()` is build using the `react()` function! + * + * Okay, that's enough info for now. Let's get to work. + * The `fromObservable()` function translates an `Observable` to a `Derivable`. + * + * ** Your Turn ** + * + * Use `fromObservable()` to turn this `Observable` into a `Derivable`. + */ + + // A `Subject` is the simplest form of `Observable`: it is comparable to our `Atom`. + const myObservable = new Subject(); + + const myDerivable$: Derivable = __YOUR_TURN__; // #QUESTION + const myDerivable$: Derivable = fromObservable(myObservable); // #ANSWER + const reactor = jest.fn(); + const onError = jest.fn(); + myDerivable$.react(reactor, { onError }); + + myObservable.next(1); + expect(myDerivable$.value).toBe(1); + + myObservable.next(2); + expect(myDerivable$.value).toBe(2); + + myObservable.error('OH NO!'); + expect(myDerivable$.error).toBe('OH NO!'); + + myObservable.next(3); + expect(myDerivable$.value).toBe(undefined); + // It is set to final, so after an error has been thrown, you cannot undo it. + + /** + * The `toObservable()` function has become obsolete as RxJS already contains a function + * called `from()` that can parse `Derivables` to `Observables`. Note that a `from` from + * the side of RxJS is the same as a `to` from the side of Sherlock. + * + * ** Your Turn ** + * + * Use the `from()` function to turn this `Derivable` into an `Observable`. + */ + const myDerivable2$ = atom(1); + + const myObservable2: Observable = __YOUR_TURN__; // #QUESTION + const myObservable2: Observable = from(myDerivable2$); // #ANSWER + + let value = 0; + myObservable2.subscribe({ next: x => (value = x) }); + + expect(value).toBe(1); // immediate call to `next()` + + myDerivable2$.set(2); + expect(value).toBe(2); }); - it('`fromEventPattern`', () => { - // TODO: this is kinda complicated shit... Requires explaining a lot of extra stuff (Subjects, Subscribing, Observables...). Leave for now? + /** + * The `fromObservable()` function can be used to turn `Observables` into `Derivables`. Under the hood, + * this function uses the `fromEventPattern()` function which is capable of turning any abstract pattern + * into a `Derivable`. Let's see how that works. + */ + it('`fromEventPattern`', async () => { + /** + * The basic idea is that you get a stream of inputs, and want to map that stream to a Derivable stream. + * This means that, whenever an update comes from the input-stream, this update is also given to a Derivable, + * which can then be `derive`d and `react`ed to. + * + * For example, you may get a function which, instead of returning an output, passes some output to your own chosen + * `callback` function. For example, this code 'heyifies' your input, adding "hey" in front of it. + * It then passes this heyified output to the callback function. As seen before in `react()` and `fromObservable()`, + * these functions like `heyify()` typically return a stopping function, which can be called to stop the heyification. + */ + function heyify(names: string[], callback: (something: string) => void) { + let i = 0; + // every 100ms, call the callback function + const int = setInterval(() => callback(`Hey ${names[i++]}`), 100); + // when this function is called, `clearInterval()` stops the stream. + return () => clearInterval(int); + } + + /** + * Now, we want to turn this process into a Derivable, where updates that are send to the callback are passed to the Derivable as well. + * This way, we can use our cool Derivable functions like `derive()` or `react()` to process changes to this Derivable + * (= new outputs from the callback). + * + * `fromEventPattern()` looks more complex than it is. This function sets a Derivable in place of the callback and also returns + * a stopping function, which can be reused from the `heyify()` function. + */ + function heyify$(names: string[]): Derivable { + return fromEventPattern(v$ => { + // the callback now sets a derivable. + const stop = heyify(names, something => v$.set(something)); + // and the stopping function is returned. + return stop; + }); + } + + /** + * This Derivable can now be reacted to. + * When this Derivable gets connected (reacted to, in this case), the function within the `fromEventPattern()` triggers. + * Upon connection, a fresh atom is passed to this function, which is then `set()` in the callback function of `heyify()`. + * This atom "lives" in the body of the callback function and is only changed when a new value is passed to the callback function. + * Here, this is a new "Hey {name}" message, every 100ms. + */ + + // To test this, we need to make sure time elapses only when we want it to. So we temporarily stop time, no big deal. + // (Don't worry about what the internet says about the dangers of stopping time: this is perfectly safe.) + jest.useFakeTimers(); + + let value: string = ''; + const stop = heyify$(['Bob', 'Jan', 'Hans', 'Roos']).react(v => (value = v)); + + /** + * ** Your Turn ** + * + * What do you expect `value` to be? + * *Hint: At the start, time has not yet passed, and `setInterval()` only responds after the first 100ms.* + */ + expect(value).toBe(__YOUR_TURN__); // #QUESTION + expect(value).toBe(''); // #ANSWER + + // We manually move time by 100ms, which is exactly the time that the `heyify()` function needs to call the `callback()` again. + jest.advanceTimersByTime(100); + expect(value).toBe(__YOUR_TURN__); // #QUESTION + expect(value).toBe('Hey Bob'); // #ANSWER + + jest.advanceTimersByTime(100); + expect(value).toBe(__YOUR_TURN__); // #QUESTION + expect(value).toBe('Hey Jan'); // #ANSWER + + jest.advanceTimersByTime(100); + expect(value).toBe(__YOUR_TURN__); // #QUESTION + expect(value).toBe('Hey Hans'); // #ANSWER + + stop(); + + // After stopping, the Derivable no longer responds to updates - it is essentially final. + jest.advanceTimersByTime(100); + expect(value).toBe(__YOUR_TURN__); // #QUESTION + expect(value).toBe('Hey Hans'); // #ANSWER }); }); }); diff --git a/generator/9 - expert.test.ts b/generator/9 - expert.test.ts index e1a7184..12d94ff 100644 --- a/generator/9 - expert.test.ts +++ b/generator/9 - expert.test.ts @@ -1,4 +1,4 @@ -import { DerivableAtom, atom, derive } from '@skunkteam/sherlock'; +import { DerivableAtom, atom, derive, unwrap } from '@skunkteam/sherlock'; import { derivableCache } from '@skunkteam/sherlock-utils'; // #QUESTION-BLOCK-START @@ -7,6 +7,9 @@ import { derivableCache } from '@skunkteam/sherlock-utils'; * If you see this variable, you should do something about it. :-) */ export const __YOUR_TURN__ = {} as any; + +// Silence TypeScript's import not used errors. +expect(unwrap).toBe(unwrap); // #QUESTION-BLOCK-END describe('expert', () => { describe('`.autoCache()`', () => { @@ -291,12 +294,49 @@ describe('expert', () => { * the created `Derivable` will not run the setup again and * everything should work as expected. * - * ** Your Turn ** TODO: not in the SOLUTIONS!! + * ** Your Turn ** * * *Hint: there is even an `unwrap` helper function for just * such an occasion, try it!* */ }); + // #ANSWER-BLOCK-START + it('BONUS', () => { + const company$ = atom('GOOGL'); + + // We have now split it into two derivations. + // The `unwrap()` function essentially does the same as `v => v.get()`, unwrapping one layer of derivable. + const price$ = company$.derive(company => stockPrice$(company)).derive(unwrap); + + // The rest of the steps are the same.. + price$.react(reactor); + expect(reactSpy).not.toHaveBeenCalled(); + + const googlPrice$ = stockPrice$.mock.results[0].value as DerivableAtom; + expect(googlPrice$.connected).toEqual(true); + expect(googlPrice$.value).toEqual(undefined); + + googlPrice$.set(1079.11); + + // ..but all these results are now different. + expect(reactSpy).toHaveBeenCalledTimes(1); + expect(stockPrice$).toHaveBeenCalledTimes(1); + expect(price$.value).toEqual(1079.11); + expect(googlPrice$.value).toEqual(1079.11); + expect(googlPrice$.connected).toEqual(true); + + /** + * In the original case, the `derive` used `stockPrice$(company).get()`,. + * This results in a value (`undefined` here) which is stored in `price$`. If you now set the value of the atom, the + * `derive` will run again as it is connected, and this will create a new atom, again with value `atom.unresolved()`. + * As such, `price$` keeps the same value, `undefined`. + * In our new case here, the first `derive` uses `stockPrice$(company)`, and a second `derive` applies the `.get()` + * (namely: `unwrap`). The second `derive` is put directly on the result of the first (namely, the `atom.unresolved()`). + * If you now set the value of the atom, this second derivation will trigger again and will `get()` the new value from the atom. + * As such, `price$` will update to the new value: 1079.11. + */ + }); + // #ANSWER-BLOCK-END /** * But even when you split the setup and the `unwrap`, you may not diff --git a/package.json b/package.json index aef988f..cda3366 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "dep-graph": "nx dep-graph", "help": "nx help", "tutorial": "jest tutorial/* -c tutorial/jest.config.ts", - "solution": "jest solution/* -c solution/jest.config.ts" + "solution": "jest solution/* -c solution/jest.config.ts", + "generate": "tsc generateTutorialAndSolution.ts && node generateTutorialAndSolution.js && npm run tutorial && npm run solution" }, "standard-version": { "bumpFiles": [ diff --git a/solution/1 - intro.test.ts b/solution/1 - intro.test.ts index 5e53e5c..72ef221 100644 --- a/solution/1 - intro.test.ts +++ b/solution/1 - intro.test.ts @@ -45,7 +45,7 @@ describe('intro', () => { * This can also be indicated with the `__YOUR_TURN__` variable. * * It should be clear what to do here... */ - bool = true; + bool = true; expect(bool).toBeTrue(); // We use expectations like this to verify the result. }); @@ -73,7 +73,7 @@ describe('the basics', () => { // the `Atom`. expect(myValue$.get()).toEqual(1); - myValue$.set(2); + myValue$.set(2); // Use the `.set()` method to change the value of the `Atom`. expect(myValue$.get()).toEqual(2); }); @@ -99,7 +99,7 @@ describe('the basics', () => { * negative to a positive number and vice versa) of the original `Atom`. */ // Use `myValue$.derive(val => ...)` to implement `myInverse$`. - const myInverse$ = myValue$.derive(val => -val); + const myInverse$ = myValue$.derive(val => -val); expect(myInverse$.get()).toEqual(-1); // So if we set `myValue$` to -2: myValue$.set(-2); @@ -124,7 +124,7 @@ describe('the basics', () => { * * Now react to `myCounter$`. In every `react()`. * Increase the `reacted` variable by one. */ - myCounter$.react(() => reacted++); + myCounter$.react(() => reacted++); expect(reacted).toEqual(1); // `react()` will react immediately, more on that later. diff --git a/solution/2 - deriving.test.ts b/solution/2 - deriving.test.ts index 6eada05..8ac3085 100644 --- a/solution/2 - deriving.test.ts +++ b/solution/2 - deriving.test.ts @@ -30,7 +30,7 @@ describe('deriving', () => { */ // We can combine txt with `repeat$.get()` here. - const lyric$ = text$.derive(txt => txt.repeat(repeat$.get())); + const lyric$ = text$.derive(txt => txt.repeat(repeat$.get())); expect(lyric$.get()).toEqual(`It won't be long`); @@ -203,8 +203,8 @@ describe('deriving', () => { .and('Buzz') .or(''); - const fizzBuzz$ = derive(() => fizz$.get() + buzz$.get()).or(myCounter$); - // This will check whether `fizz$.get() + buzz$.get()` is truthy: if so, return it; if not, return `myCounter$` + const fizzBuzz$ = derive(() => fizz$.get() + buzz$.get()).or(myCounter$); + // This will check whether `fizz$.get() + buzz$.get()` is truthy: if so, return it; if not, return `myCounter$` for (let count = 1; count <= 100; count++) { // Set the value of the `Atom`, diff --git a/solution/3 - reacting.test.ts b/solution/3 - reacting.test.ts index 82695bf..1998e4d 100644 --- a/solution/3 - reacting.test.ts +++ b/solution/3 - reacting.test.ts @@ -1,41 +1,5 @@ import { atom } from '@skunkteam/sherlock'; -// xxx check my solutions with the actual solutions (https://github.com/skunkteam/sherlock/tree/tutorial-solutions/robin/tutorial) -// FIXME: remove all TODO: and FIXME: -// xxx check whether the generated tutorials and solutions actually work (e.g. are all solutions correct? No weird shenanigans?) - -// FIXME: ALSO CHECK "Or, alternatively"! -// FIXME: deze file niet linten / builden (voor automatische test). Tutorial ook niet. Maar solutions juist wel! OP EIND. (mag beide wel linten right?) -// FIXME: interne review document, mocht ik iets hebben om te laten zien! In Google Drive, zet het erin! -// xxx werkt `npm run tutorial` nog? > Nu wel. -// xxx PETER: "nu je toch met Sherlock bezig bent; zou je voor mij eens kunnen checken of de code voorbeelden in de README -// nog wel kloppen met de huidige API? Ik heb het gevoel dat dat niet zo is; volgens mij is er geen function "derivation()" -// en heet dat nu "derive()" bijvoorbeeld." -// FIXME: OOOOOOH JA, ik had eroverheen gepushed! Dat moet nog een PR met terugwerkende kracht worden... (of commits squashen, en dat ze dan maar de commit moeten reviewen?) -// FIXME: Add FromEventPattern + FromObservable -// xxx fix the generator for code blocks. -// FIXME: now check whether it did not remove excess lines or kept 2 empty lines where it should not. (I think it is good though.) -/** - * x Final States; (finalGetter, finalMethod, getMaybeFinalState, FinalWrapper, MaybeFinalState, _isFinal, makeFinal, markFinal, .final, .finalized, setFinal...) - * ? Lens; (libs/sherlock/src/lib/derivable/lens.ts) - map die twee kanten op kan gaan. Maar een map kan dat al? Maar hier kan - * je dat los definieren! Je kan gewoon `lens` ipv `var.lens`. Zelden dat je dit gebruikt. Output is een Derivable though. - * x Lift; (libs/sherlock-utils/src/lib/lift.ts) - * x Peek; (libs/sherlock-utils/src/lib/peek.ts) - dan track je niet. In een derivable, deze tracked hij dan niet (ipv .get() waar het wel getracked wordt) - * x Template; (libs/sherlock-utils/src/lib/template.ts) - to make a string using a template literal. (Uses unwrap!!) - * / Factory; (libs/sherlock/src/lib/derivable/factories.ts) - simply contains functions to create objects, namely - * lens; atom; constant; derive. - * !! Flat-map; (libs/sherlock/src/lib/derivable/mixins/flat-map.ts) - ??? - * array: nested arrays naar array - * Derivable: gooit er derive.get() achteraan? - * Derivable (input van inputveld). Flatmap geeft Derivable terug. Derivable.flatmap() returned misschien - * Derivable, returned dan de number. flatMap is een `derive`, maar wat hij returned haalt hij uit de Derivable. - * ofzoiets. Maakt code korter. - * x Fallback-to; - op een derivable. Als een atom `unresolved` is, dan fallt het back to this value. Ofwel, initial value, maar - * ook als hij later unresolved wordt, dan wordt hij dit (vaak wel initial value). - * x Take - react options gebruiken buiten react. In een derivable chain, halverwege die options gebruiken. - * -- e.g. (from)Promise. Zodra die een waarde aanneemt kan hij niet meer veranderen. - * Let FromPromise, FromObservable, FromEventPattern ook uit (in utils?), ToPromise, ToObservable, in praktijk ook handig. - * FromEventPattern (haily mary, als alles niet werkt, dan dit doen). - */ /** * In the intro we have seen a basic usage of the `.react()` method. * Let's dive a bit deeper into the details of this method. @@ -132,7 +96,7 @@ describe('reacting', () => { * * catch the returned `stopper` in a variable */ - const stopper = myAtom$.react(reactor); + const stopper = myAtom$.react(reactor); expectReact(1, 'initial value'); @@ -141,7 +105,7 @@ describe('reacting', () => { * * Call the `stopper`. */ - stopper(); + stopper(); myAtom$.set('new value'); @@ -219,7 +183,7 @@ describe('reacting', () => { * * Try giving `boolean$` as `until` option. */ - string$.react(reactor, { until: boolean$ }); + string$.react(reactor, { until: boolean$ }); // It should react directly as usual. expectReact(1, 'Value'); @@ -298,7 +262,7 @@ describe('reacting', () => { * Try using the first parameter of the `until` function to do * the same as above. */ - string$.react(reactor, { until: parent$ => !parent$.get() }); + string$.react(reactor, { until: parent$ => !parent$.get() }); // It should react as usual. string$.set('New value'); @@ -325,7 +289,7 @@ describe('reacting', () => { boolean$.set(false); // ...but does it? Is the reactor still connected? - expect(boolean$.connected).toBe(true); + expect(boolean$.connected).toBe(true); // The `b$` it obtains as argument is a `Derivable`. This is a // reference value. Because we apply a negation to this, `b$` is coerced to a @@ -334,13 +298,13 @@ describe('reacting', () => { // `boolean$`. Instead, you can get the value out of the `Derivable` using `.get()`: stopper(); // reset stopper = boolean$.react(reactor, { until: b$ => !b$.get() }); - expect(boolean$.connected).toBe(false); + expect(boolean$.connected).toBe(false); // You can also return the `Derivable` after appling the negation // using the method designed for negating the boolean within a `Derivable`: stopper(); boolean$.react(reactor, { until: b$ => b$.not() }); - expect(boolean$.connected).toBe(false); + expect(boolean$.connected).toBe(false); }); }); @@ -368,7 +332,7 @@ describe('reacting', () => { * * *Hint: remember the `.is()` method from tutorial 2?* */ - sherlock$.react(reactor, { from: parent$ => parent$.is('dear') }); + sherlock$.react(reactor, { from: parent$ => parent$.is('dear') }); expectReact(0); ['Elementary,', 'my', 'dear', 'Watson'].forEach(txt => sherlock$.set(txt)); @@ -394,8 +358,8 @@ describe('reacting', () => { * Now, let's react to all even numbers. * Except 4, we don't want to make it too easy now. */ - count$.react(reactor, { when: parent$ => parent$.get() % 2 === 0 && parent$.get() !== 4 }); - // count$.react(reactor, { when: parent$ => parent$.derive(value => value % 2 === 0 && value !== 4) }); // Or, alternatively: + count$.react(reactor, { when: parent$ => parent$.get() % 2 === 0 && parent$.get() !== 4 }); + // count$.react(reactor, { when: parent$ => parent$.derive(value => value % 2 === 0 && value !== 4) }); // Or, alternatively: expectReact(1, 0); @@ -422,8 +386,8 @@ describe('reacting', () => { * * Say you want to react when `count$` is larger than 3. But not the first time... */ - count$.react(reactor, { when: parent$ => parent$.get() > 3, skipFirst: true }); - // count$.react(reactor, { when: parent$ => parent$.derive(value => value > 3), skipFirst: true }); // Or, alternatively: + count$.react(reactor, { when: parent$ => parent$.get() > 3, skipFirst: true }); + // count$.react(reactor, { when: parent$ => parent$.derive(value => value > 3), skipFirst: true }); // Or, alternatively: expectReact(0); @@ -456,8 +420,8 @@ describe('reacting', () => { * * *Hint: you will need to combine `once` with another option* */ - count$.react(reactor, { once: true, when: parent$ => parent$.get() > 3 }); - // count$.react(reactor, { once: true, when: parent$ => parent$.derive(value => value > 3) }); // Or, alternatively: + count$.react(reactor, { once: true, when: parent$ => parent$.get() > 3 }); + // count$.react(reactor, { once: true, when: parent$ => parent$.derive(value => value > 3) }); // Or, alternatively: expectReact(0); @@ -497,7 +461,7 @@ describe('reacting', () => { // The reactor starts reacting when `myAtom` gets the value 3, but stops when it gets the value 2. // But because `myAtom$` obtains the value 2 before it obtains 3... // ...how many times was the reactor called, if any? - expectReact(3, 5); // `from` evaluates before `until`, so it reacted to 3, 4 and 5. + expectReact(3, 5); // `from` evaluates before `until`, so it reacted to 3, 4 and 5. }); it('`when` and `skipFirst`', () => { @@ -508,7 +472,7 @@ describe('reacting', () => { // The reactor reacts when `myAtom$` is 1 but skips the first number. // `myAtom$` starts out at 0. Does the reactor skip only the 0 or also the 1? - expectReact(0); // `skipFirst` triggers only when `when` evaluates to true, so it also skips the 1. + expectReact(0); // `skipFirst` triggers only when `when` evaluates to true, so it also skips the 1. }); it('`from`, `until`, `when`, `skipFirst`, and `once`', () => { @@ -553,7 +517,7 @@ describe('reacting', () => { * * This should be possible with three simple ReactorOptions. */ - connected$.react(reactor, { when: parent$ => parent$.is('connected').not(), skipFirst: true, once: true }); + connected$.react(reactor, { when: parent$ => parent$.is('connected').not(), skipFirst: true, once: true }); // It starts as 'disconnected' expectReact(0); diff --git a/solution/4 - inner workings.test.ts b/solution/4 - inner workings.test.ts index 241460e..04248a1 100644 --- a/solution/4 - inner workings.test.ts +++ b/solution/4 - inner workings.test.ts @@ -91,17 +91,17 @@ describe('inner workings', () => { */ // Well, what do you expect? - expect(hasDerived).toHaveBeenCalledTimes(0); + expect(hasDerived).toHaveBeenCalledTimes(0); myDerivation$.get(); // And after a `.get()`? - expect(hasDerived).toHaveBeenCalledTimes(1); + expect(hasDerived).toHaveBeenCalledTimes(1); myDerivation$.get(); // And after the second `.get()`? Is there an extra call? - expect(hasDerived).toHaveBeenCalledTimes(2); + expect(hasDerived).toHaveBeenCalledTimes(2); /** * The state of any `Derivable` can change at any moment. @@ -140,27 +140,27 @@ describe('inner workings', () => { * * Ok, it's your turn to complete the expectations. */ - expect(hasDerived).toHaveBeenCalledTimes(1); // because of the react. + expect(hasDerived).toHaveBeenCalledTimes(1); // because of the react. myDerivation$.get(); - expect(hasDerived).toHaveBeenCalledTimes(1); // no update: someone is reacting, and there has been no update in value. + expect(hasDerived).toHaveBeenCalledTimes(1); // no update: someone is reacting, and there has been no update in value. myAtom$.set(false); - expect(hasDerived).toHaveBeenCalledTimes(2); // `myDerivation$`s value has changed, so update. + expect(hasDerived).toHaveBeenCalledTimes(2); // `myDerivation$`s value has changed, so update. myDerivation$.get(); - expect(hasDerived).toHaveBeenCalledTimes(2); // no update. + expect(hasDerived).toHaveBeenCalledTimes(2); // no update. stopper(); - expect(hasDerived).toHaveBeenCalledTimes(2); // stopping doesn't change the value... + expect(hasDerived).toHaveBeenCalledTimes(2); // stopping doesn't change the value... myDerivation$.get(); - expect(hasDerived).toHaveBeenCalledTimes(3); // ...but now, it is not being reacted to, so it goes back to updating every time `.get()` is called. + expect(hasDerived).toHaveBeenCalledTimes(3); // ...but now, it is not being reacted to, so it goes back to updating every time `.get()` is called. /** * Since the `.react()` already listens to the value-changes, there is @@ -205,23 +205,23 @@ describe('inner workings', () => { // Note that this is the same value as it was initialized with myAtom$.set(1); - expect(first).toHaveBeenCalledTimes(1); // `myAtom$` has the same value (`1`), so no need to be called - expect(second).toHaveBeenCalledTimes(1); // `first$` has the same value (`false`), so no need to be called + expect(first).toHaveBeenCalledTimes(1); // `myAtom$` has the same value (`1`), so no need to be called + expect(second).toHaveBeenCalledTimes(1); // `first$` has the same value (`false`), so no need to be called myAtom$.set(2); - expect(first).toHaveBeenCalledTimes(2); // `myAtom$` has a different value (`2`), so call again - expect(second).toHaveBeenCalledTimes(1); // `first$` has the same value (`false`), so no need to be called + expect(first).toHaveBeenCalledTimes(2); // `myAtom$` has a different value (`2`), so call again + expect(second).toHaveBeenCalledTimes(1); // `first$` has the same value (`false`), so no need to be called myAtom$.set(3); - expect(first).toHaveBeenCalledTimes(3); // `myAtom$` has a different value (`3`), so call again - expect(second).toHaveBeenCalledTimes(2); // `first$` has a different value (`true`), so call again + expect(first).toHaveBeenCalledTimes(3); // `myAtom$` has a different value (`3`), so call again + expect(second).toHaveBeenCalledTimes(2); // `first$` has a different value (`true`), so call again myAtom$.set(4); - expect(first).toHaveBeenCalledTimes(4); // `myAtom$` has a different value (`4`), so call again - expect(second).toHaveBeenCalledTimes(2); // `first$` has the same value (`true`), so no need to be called + expect(first).toHaveBeenCalledTimes(4); // `myAtom$` has a different value (`4`), so call again + expect(second).toHaveBeenCalledTimes(2); // `first$` has the same value (`true`), so no need to be called /** * Can you explain the behavior above? @@ -259,7 +259,7 @@ describe('inner workings', () => { * The `Atom` is set with exactly the same object as before. Will the * `.react()` fire? */ - expect(hasReacted).toHaveBeenCalledTimes(1); // `{} !== {}`, as they are different references + expect(hasReacted).toHaveBeenCalledTimes(1); // `{} !== {}`, as they are different references /** * But what if you use an object, that can be easily compared through a @@ -278,7 +278,7 @@ describe('inner workings', () => { * * Do you think the `.react()` fired with this new value? */ - expect(hasReacted).toHaveBeenCalledTimes(0); + expect(hasReacted).toHaveBeenCalledTimes(0); atom$.set(Seq.Indexed.of(1, 2)); @@ -287,15 +287,16 @@ describe('inner workings', () => { * * And now? */ - expect(hasReacted).toHaveBeenCalledTimes(1); + expect(hasReacted).toHaveBeenCalledTimes(1); /** * In `@skunkteam/sherlock` equality is a bit complex: * - * First we check `Object.is()` equality, if that is true, it is the + * First we check `Object.is()` equality. If that is true, it is the * same, you can't deny that. * - * After that it is pluggable. It can be anything you want. TODO: what is pluggable? + * After that it is pluggable. This means that you can 'plug in' or 'define' + * the definition for equality yourself. * * By default we try to use `.equals()`, to support libraries like * `ImmutableJS`. diff --git a/solution/5 - unresolved.test.ts b/solution/5 - unresolved.test.ts index 0097345..37e842d 100644 --- a/solution/5 - unresolved.test.ts +++ b/solution/5 - unresolved.test.ts @@ -20,14 +20,14 @@ describe('unresolved', () => { // since it can't be inferred by TypeScript this way. const myAtom$ = atom.unresolved(); - expect(myAtom$.resolved).toEqual(false); + expect(myAtom$.resolved).toEqual(false); /** * ** Your Turn ** * * Resolve the atom, it's pretty easy */ - myAtom$.set(1); // setting it to any value will unresolve it + myAtom$.set(1); // setting it to any value will unresolve it expect(myAtom$.resolved).toBeTrue(); }); @@ -42,7 +42,7 @@ describe('unresolved', () => { * * Time to create an `unresolved` Atom.. */ - const myAtom$: DerivableAtom = atom.unresolved(); + const myAtom$: DerivableAtom = atom.unresolved(); expect(myAtom$.resolved).toBeFalse(); @@ -57,10 +57,10 @@ describe('unresolved', () => { * * What do you expect? */ - expect(myAtom$.resolved).toEqual(true); + expect(myAtom$.resolved).toEqual(true); // .toThrow() or .not.toThrow()? ↴ - expect(() => myAtom$.get()).not.toThrow(); + expect(() => myAtom$.get()).not.toThrow(); }); /** @@ -78,14 +78,14 @@ describe('unresolved', () => { * * What do you expect? */ - expect(hasReacted).toHaveBeenCalledTimes(0); + expect(hasReacted).toHaveBeenCalledTimes(0); /** * ** Your Turn ** * * Now make the last expect succeed */ - myAtom$.set(`woohoow, I was called`); + myAtom$.set(`woohoow, I was called`); expect(myAtom$.resolved).toBeTrue(); expect(hasReacted).toHaveBeenCalledExactlyOnceWith(`woohoow, I was called`, expect.toBeFunction()); @@ -105,7 +105,7 @@ describe('unresolved', () => { * * Set the value.. */ - myAtom$.set(`it's alive!`); + myAtom$.set(`it's alive!`); expect(myAtom$.get()).toEqual(`it's alive!`); @@ -114,7 +114,7 @@ describe('unresolved', () => { * * Unset the value.. (*Hint: TypeScript is your friend*) */ - myAtom$.unset(); + myAtom$.unset(); expect(myAtom$.resolved).toBeFalse(); }); @@ -135,14 +135,14 @@ describe('unresolved', () => { * * Combine the two `Atom`s into one `Derivable` */ - const myDerivable$: Derivable = myString$.derive(parent$ => parent$ + myOtherString$.get()); + const myDerivable$: Derivable = myString$.derive(parent$ => parent$ + myOtherString$.get()); /** * ** Your Turn ** * * Is `myDerivable$` expected to be `resolved`? */ - expect(myDerivable$.resolved).toEqual(false); + expect(myDerivable$.resolved).toEqual(false); // Now let's set one of the two source `Atom`s myString$.set('some'); @@ -156,8 +156,8 @@ describe('unresolved', () => { // And what if we set `myOtherString$`? myOtherString$.set('data'); - expect(myDerivable$.resolved).toEqual(true); - expect(myDerivable$.get()).toEqual('somedata'); + expect(myDerivable$.resolved).toEqual(true); + expect(myDerivable$.get()).toEqual('somedata'); /** * ** Your Turn ** @@ -166,7 +166,7 @@ describe('unresolved', () => { * What do you expect `myDerivable$` to be? */ myString$.unset(); - expect(myDerivable$.resolved).toEqual(false); + expect(myDerivable$.resolved).toEqual(false); }); /** @@ -182,16 +182,16 @@ describe('unresolved', () => { * Use the `.fallbackTo()` method to create a `mySafeAtom$` which * gets the backup value `3` when `myAtom$` becomes unresolved. */ - const mySafeAtom$ = myAtom$.fallbackTo(() => 3); + const mySafeAtom$ = myAtom$.fallbackTo(() => 3); - expect(myAtom$.value).toBe(0); - expect(mySafeAtom$.value).toBe(0); + expect(myAtom$.get()).toBe(0); + expect(mySafeAtom$.get()).toBe(0); myAtom$.unset(); expect(myAtom$.resolved).toBeFalse(); expect(mySafeAtom$.resolved).toBeTrue(); - expect(myAtom$.value).toBeUndefined(); - expect(mySafeAtom$.value).toBe(3); + expect(() => myAtom$.get()).toThrow(); + expect(mySafeAtom$.get()).toBe(3); }); }); diff --git a/solution/6 - errors.test.ts b/solution/6 - errors.test.ts index 7cce76f..c6d226b 100644 --- a/solution/6 - errors.test.ts +++ b/solution/6 - errors.test.ts @@ -1,4 +1,4 @@ -import { atom, DerivableAtom, error } from '@skunkteam/sherlock'; +import { atom, DerivableAtom } from '@skunkteam/sherlock'; /** * Errors are a bit part of any programming language, and Sherlock has its own custom errors @@ -23,42 +23,21 @@ describe('errors', () => { expect(myAtom$.errored).toBe(true); expect(myAtom$.error).toBe('my Error'); - // expect(myAtom$.get).toThrow("Cannot read properties of undefined (reading 'getState')"); - // TODO: WHAT - normally this works, but internal JEST just fucks with me....? - // What will happen if you try to call `get()` on `myAtom$`? // `.toThrow()` or `.not.toThrow()`? ↴ - expect(() => myAtom$.get()).toThrow('my Error'); + expect(() => myAtom$.get()).toThrow('my Error'); // ** __YOUR_TURN__ ** // What will happen if you try to call `set()` on `myAtom$`? // `.toThrow()` or `.not.toThrow()`? ↴ - expect(() => myAtom$.set(2)).not.toThrow(); - expect(myAtom$.errored).toBe(false); + expect(() => myAtom$.set(2)).not.toThrow(); + expect(myAtom$.errored).toBe(false); // Interestingly, calling `set()` does not throw an error. In fact, it removes the error state // altogether. This means we can now call `get()` again. expect(() => myAtom$.get()).not.toThrow(); }); - /** - * libs/sherlock/src/lib/interfaces.ts:289 shows the basic states that a Derivable can have. - * > `export type State = V | unresolved | ErrorWrapper;` - * A state can be either any type `V` (`number`, `string`, etc.), `unresolved` as we saw in the - * previous tutorial, or `ErrorWrapper`. This last state is explained here. - */ - it('error states', () => { - expect(myAtom$.getState()).toBe(1); // as explained above, any type can be a state - - myAtom$.setError('my Error'); - - // The `ErrorWrapper` state only holds an error string. The `error()` function returns - // such an `ErrorWrapper` which we can use to compare. - expect(myAtom$.getState()).toMatchObject(error('my Error')); - - // TODO: more! There wasn't a question in here. Maybe combine with Final States? NO, that one should go! - }); - it('deriving an error', () => { const myDerivable$ = myAtom$.derive(v => v + 1); @@ -66,7 +45,7 @@ describe('errors', () => { myAtom$.setError('division by zero'); // ...what happens to `myDerivable$`? - expect(myDerivable$.errored).toBe(true); + expect(myDerivable$.errored).toBe(true); // If any Derivable tries to derive from an atom in an error state, // this Derivable will itself throw an error too. This makes sense, @@ -86,11 +65,11 @@ describe('errors', () => { // ** __YOUR_TURN__ ** // Will an error be thrown when `myAtom$` is now set to an error state? // `.toThrow()` or `.not.toThrow()`? ↴ - expect(() => myAtom$.setError('my Error')).toThrow('my Error'); + expect(() => myAtom$.setError('my Error')).toThrow('my Error'); // ** __YOUR_TURN__ ** // Is the reactor still connected now that it errored? - expect(myAtom$.connected).toBe(false); + expect(myAtom$.connected).toBe(false); // Reacting to a Derivable that throws an error will make the reactor throw as well. // Because the reactor will usually fire when it gets connected, it also throws when @@ -102,11 +81,11 @@ describe('errors', () => { // ** __YOUR_TURN__ ** // Will an error be thrown when you use `skipFirst`? // `.toThrow()` or `.not.toThrow()`? ↴ - expect(() => myAtom$.react(reactor, { skipFirst: true })).toThrow('my second Error'); + expect(() => myAtom$.react(reactor, { skipFirst: true })).toThrow('my second Error'); // And will an error be thrown when `from = false`? // `.toThrow()` or `.not.toThrow()`? ↴ - expect(() => myAtom$.react(reactor, { from: false })).not.toThrow(); + expect(() => myAtom$.react(reactor, { from: false })).not.toThrow(); // When `from = false`, the reactor is disconnected, preventing the error message from entering. // `skipFirst`, on the other hand, does allow the error in, but does not trigger an update. diff --git a/solution/7 - advanced.test.ts b/solution/7 - advanced.test.ts index ceff9da..e7d85f1 100644 --- a/solution/7 - advanced.test.ts +++ b/solution/7 - advanced.test.ts @@ -1,4 +1,13 @@ -import { atom, constant, Derivable, derive, SettableDerivable, unresolved } from '@skunkteam/sherlock'; +import { + atom, + constant, + Derivable, + derive, + ErrorWrapper, + SettableDerivable, + State, + unresolved, +} from '@skunkteam/sherlock'; import { template } from '@skunkteam/sherlock-utils'; import { Map as ImmutableMap } from 'immutable'; @@ -24,8 +33,8 @@ describe('advanced', () => { */ // .toThrow() or .not.toThrow()? ↴ (2x) - expect(() => c.get()).not.toThrow(); /* __YOUR_TURN__ */ - expect(() => c.set('new value')).toThrow() /* __YOUR_TURN__ */; + expect(() => c.get()).not.toThrow(); /* __YOUR_TURN__ */ + expect(() => c.set('new value')).toThrow() /* __YOUR_TURN__ */; }); it('`templates`', () => { @@ -33,7 +42,7 @@ describe('advanced', () => { // we also have a special syntax to copy template literals to a Derivable. const one = 1; const myDerivable = template`I want to go to ${one} party`; - expect(myDerivable.get()).toBe(`I want to go to 1 party`); + expect(myDerivable.get()).toBe(`I want to go to 1 party`); }); /** @@ -58,10 +67,10 @@ describe('advanced', () => { * Rewrite the `.get()`/`.set()` combos below using `.swap()`. */ - myCounter$.swap(plusOne); + myCounter$.swap(plusOne); expect(myCounter$.get()).toEqual(1); - myCounter$.swap(plusOne); + myCounter$.swap(plusOne); expect(myCounter$.get()).toEqual(2); }); @@ -78,12 +87,12 @@ describe('advanced', () => { * Use the `.take()` method on `myAtom$` to only accept the input string * when it is `allowed`. */ - const myLimitedAtom$ = myAtom$.take({ when: parent$ => parent$.is('allowed') }); + const myLimitedAtom$ = myAtom$.take({ when: parent$ => parent$.is('allowed') }); expect(myLimitedAtom$.resolved).toBe(false); myAtom$.set('allowed'); expect(myLimitedAtom$.resolved).toBe(true); - expect(myLimitedAtom$.value).toBe('allowed'); + expect(myLimitedAtom$.get()).toBe('allowed'); }); /** @@ -103,13 +112,13 @@ describe('advanced', () => { * * Use the `.value` accessor to get the current value. */ - expect(myAtom$.value).toEqual('foo'); + expect(myAtom$.value).toEqual('foo'); /** * ** Your Turn ** * * Now use the `.value` accessor to set a 'new value'. */ - myAtom$.value = 'new value'; + myAtom$.value = 'new value'; expect(myAtom$.get()).toEqual('new value'); }); @@ -124,7 +133,7 @@ describe('advanced', () => { /** * ** Your Turn ** */ - expect(myAtom$.value).toEqual(undefined); + expect(myAtom$.value).toEqual(undefined); }); /** @@ -175,7 +184,7 @@ describe('advanced', () => { * * Use the `.map()` method to create the expected output below */ - const mappedAtom$: Derivable = myAtom$.map(value => value.toString().repeat(value)); + const mappedAtom$: Derivable = myAtom$.map(value => value.toString().repeat(value)); mappedAtom$.react(mapReactSpy); @@ -257,7 +266,7 @@ describe('advanced', () => { // This first function is called when getting... n => -n, // ...and this second function is called when setting. - n => -n, + n => -n, ); // The original `atom` was set to 1, so we want the inverse to @@ -294,83 +303,176 @@ describe('advanced', () => { }); /** - * In order to reason over the state of a Derivable, we can - * use `.mapState()`. This will map one state to another, and - * can be used to get rid of pesky `unresolved` or `Errorwrapper` - * states. + * Although the `.map()` function can be reversed, the intended flow of the + * function is still meant to go the original non-reversed way. This means that, + * if the reverse flow is used, the non-reverse flow is also activated. We will + * show what that means. */ - it('`.mapState()`', () => { + it('one-way flow', () => { const myAtom$ = atom(1); - // like `.map()`, we can specify it both ways. - const myMappedAtom$ = myAtom$.mapState( - state => (state === unresolved ? 3 : state), // `myAtom$` => `myMappedAtom$` - state => (state === 2 ? unresolved : state), // `myMappedAtom$` => `myAtom$` + const myMappedAtom$ = myAtom$.map( + n => n + 1, + n => n * 2, ); - myAtom$.set(2); - expect(myAtom$.resolved).toBe(true); - expect(myMappedAtom$.resolved).toBe(true); + // This may seem logical... + myAtom$.set(5); + expect(myAtom$.value).toBe(5); + expect(myMappedAtom$.value).toBe(6); - myAtom$.unset(); - expect(myAtom$.resolved).toBe(false); - expect(myMappedAtom$.resolved).toBe(true); + // ...but this may seem weird. + myMappedAtom$.set(5); + expect(myAtom$.value).toBe(10); + expect(myMappedAtom$.value).toBe(11); - myMappedAtom$.set(2); - expect(myAtom$.resolved).toBe(false); - expect(myMappedAtom$.resolved).toBe(true); + /** + * `.map()` is intended to use one-way, from `myAtom$` to `myMappedAtom$`. + * The reverse direction or 'flow', of setting `myMappedAtom$` and mapping it to `myAtom$` is not + * the intended flow, and is used only as a shortcut to alter `myAtom$`. However, if you do this, + * `myAtom$` will notice that it is changed and thus will trigger another call of `.map()`, now + * from `myAtom$` to `myMappedAtom$`! Thus, `myMappedAtom$` is changed again. + * + * Although this behavior is intended, it may give seemingly weird situations like this where + * you set `myMappedAtom$` to the value 5, yet it "suddenly" has value 11. + * + * Also note that removing the second case of `.map()`, so for the reverse direction, will actually have effects + * on the typing of `myMappedAtom$`: it will become a `Derivable` instead of a `DerivableAtom`, + * which also means it does not have a `.set()` method anymore. Try it out by commenting out the second line of `.map()`! + */ + }); - // This is a tricky one: - myMappedAtom$.unset(); - expect(myAtom$.resolved).toBe(false); - expect(myMappedAtom$.resolved).toBe(true); + it('`.flatMap()`', () => { + const myAtom$ = atom(0); + const atomize = jest.fn((n: number) => atom(n)); // turn a number into an atom. + /** + * Sometimes you use `.map()`, but the result of the function within the `.map()` is also a Derivable. + * The result would be a `Derivable>` (like the return type of `.map()` below: hover over it to see) + */ + myAtom$.map(atomize); + + /** + * You would have to use `.get()` to go back to a single Derivable. Similarly how `flatMap` can + * reduce lists of lists to a single list, it can help reduce Derivables of Derivables to a + * single Derivable. + * + * ** Your Turn ** + * + * Rewrite the first line using `.flatMap()`. + */ + let myMappedAtom$ = myAtom$.map(atomize).derive(v => v.get()); // the `derive()` uses `get()` to remove one layer of `Derivable` + myMappedAtom$ = myAtom$.flatMap(atomize) as Derivable; + + myAtom$.set(1); + expect(myMappedAtom$.get()).toBe(1); + expect(atomize).toHaveBeenCalledTimes(1); + + // `.flatMap()`, like `.map()`, is a common functionality of standard libraries and can be used on e.g. arrays. + const myList = [1, 2, 3]; + const myMappedList = myList.map(v => [v, v + 1]).flat(); + const myFlatMappedList = myList.flatMap(v => [v, v + 1]); + expect(myMappedList).toEqual(myFlatMappedList); + }); + }); + /** + * Every Derivable also contains a `State`. This state contains all the information of a Derivable in one place, + * such as whether it is a value, unresolved, or an error. + */ + describe('States', () => { + /** + * libs/sherlock/src/lib/interfaces.ts:289 shows all that a State can be. + * ``` + * export type State = V | unresolved | ErrorWrapper; + * ``` + */ + it('value states, unresolved states, and error states', () => { + const myAtom$ = atom(1); /** - * The results, especially of the last case, may seem weird. - * In the first exercise, `myAtom$` is set to 2, causing the state to be 2 as well. - * By setting the state of `myAtom$`, the first line of `mapState()` is triggered. - * Since `2` is not equal to `unresolved`, we return the state `2`, causing - * `myMappedAtom$` to also get state 2 (and thus: value 2). Neither are unresolved. + * ** Your Turn ** * - * In the second case, `myAtom$` is set to `unresolved`, triggering the first line of - * `mapState()`, letting `myMappedAtom$` become 3. `myAtom$` is now `unresolved`, and - * `myMappedAtom$` is not. + * What do you expect the state to be? + */ + expect(myAtom$.getState()).toBe(1); + + /** + * We cannot directly set the state of `myAtom$` as there is no `setState()` function, + * but it will change automatically when we change the value of `myAtom$`. + */ + myAtom$.unset(); + + /** + * ** Your Turn ** * - * In the third case, `myMappedAtom$` is set to 2, it triggers the second line of - * `mapState()`, causing `myAtom$` to become `unresolved`. However, what we don't - * notice is that this change in state triggers the first line of `mapState()` again, - * causing `myMappedAtom$` to get state `3`. We can check this: + * What do you expect the state to be? */ + expect(myAtom$.getState()).toBe(unresolved); + + myAtom$.setError('my Error'); - myMappedAtom$.set(2); - expect(myMappedAtom$.get()).toBe(3); // the state and value are linked, so this is identical to `.getState()` /** - * You might think that this change in state would cause `myAtom$` to now also get - * `3` as its state, but this does not happen. Why not? TODO: maximally one cycle? ASK! - * Since both `2` and `3` are not `unresolved`, it does not matter to our answer. + * ** Your Turn ** * - * The same cannot be said for the fourth case. Setting `myMappedAtom$` to `unresolved` - * triggers the second line of `mapState()`, causing `myAtom$` to also become `unresolved`. This, in turn, - * triggers the first line of `mapState()`, causing `myMappedAtom$` to become `3`. - * As such, `myMappedAtom$` is not `unresolved` even though we set it as such. - * TODO: change this to be for MAP. Then make MAPSTATE a trivial one right after. + * What do you expect the state to be? + */ + expect(myAtom$.getState()).toBeInstanceOf(ErrorWrapper); + + /** + * Here is an example of when a state can be useful. Using the concept of type 'narrowing', we can check + * on type-level what state the atom is in and we can vary our return value accordingly. + * Study the following function and then fill in the expectations below. */ + function stateToString(state: State): string { + if (state instanceof ErrorWrapper) { + // We know `state` is of type 'ErrorWrapper', which allows us to grab a property such as 'error' from it. + // Note that our `error` is `ErrorWrapper.error`, not `Derivable.error` such as when using `myAtom$.error`. + return state.error as string; + } else if (typeof state === 'number') { + // We know `state` is of type 'number', so we can apply numerical functions to it. + return String(state + 1); + } else { + // We know `state` must now be of type 'unresolved'. + return state.toString(); + } + } + + myAtom$.set(1); + expect(stateToString(myAtom$.getState())).toBe('2'); + + myAtom$.unset(); + expect(stateToString(myAtom$.getState())).toBe('Symbol(unresolved)'); + + myAtom$.setError('OH NO!'); + expect(stateToString(myAtom$.getState())).toBe('OH NO!'); }); - // FIXME: - it('TEMP Flat-map', () => { - // const myAtom$ = atom(0); - // const mapping = (v: any) => atom(v); - // Sometimes you use `map()`, but the result within the `map()` is also a Derivable. - // The result would here be a `Derivable>` (hover over `derive` to see this). - // You would have to use `.get()` to go back to a single Derivable. Similarly how `flatMap` can - // reduce lists of lists to a single list, it can help reduce Derivables of Derivables to a - // single Derivable. If you have something like this: - // let myAtom$$ = myAtom$.map(n => mapping(n)).derive(v => v.get()); - // You can now rewrite it to this: - // myAtom$$ = myAtom$.flatMap(n => mapping(n)); - // It only results in slightly shorter code. - // TODO: right? + /** + * In order to reason over the state of a Derivable, we can + * use `.mapState()`. This will map one state to another, and + * can be used to get rid of pesky `unresolved` or `Errorwrapper` + * states. + */ + it('`.mapState()`', () => { + const myAtom$ = atom(1); + + const myMappedAtom$ = myAtom$.mapState( + state => (state === unresolved || state instanceof ErrorWrapper ? 0 : state), // `myAtom$` => `myMappedAtom$` + state => state, // `myMappedAtom$` => `myAtom$` + ); + + myAtom$.set(2); + expect(myAtom$.value).toBe(2); + expect(myMappedAtom$.value).toBe(2); + + myAtom$.unset(); + expect(myAtom$.value).toBe(undefined); + expect(myMappedAtom$.value).toBe(0); + + // This is a tricky one. Remember the intended flow of the `map()` function, + // as it is the same for the `mapState()` function. + myMappedAtom$.unset(); + expect(myAtom$.value).toBe(undefined); + expect(myMappedAtom$.value).toBe(0); }); }); @@ -380,7 +482,7 @@ describe('advanced', () => { * `Derivable`, one of those values can be plucked into a new `Derivable`. * This plucked `Derivable` can be settable, if the source supports it. * - * The way properties are plucked is pluggable, but by default both // TODO: no-one here knows what "pluggable" is. Or ImmutableJS. + * The way properties are plucked is 'pluggable' (customizable), but by default both * `.get()` and `[]` are supported to support * basic Objects, Maps and Arrays. * @@ -388,7 +490,7 @@ describe('advanced', () => { * does not. This means that setting a plucked property of a regular * Object/Array/Map will not cause any reaction on that source `Derivable`. * - * ImmutableJS can help fix this problem* + * ImmutableJS can help fix this problem. */ describe('`.pluck()`', () => { const reactSpy = jest.fn(); @@ -413,7 +515,7 @@ describe('advanced', () => { * * * Hint: you'll have to cast the result from `.pluck()`. */ - firstProp$ = myMap$.pluck('firstProp') as SettableDerivable; + firstProp$ = myMap$.pluck('firstProp') as SettableDerivable; }); /** @@ -429,18 +531,18 @@ describe('advanced', () => { * What do you expect the plucked `Derivable` to look like? And what * happens when we `.set()` it? */ - expect(firstProp$.get()).toEqual('firstValue'); + expect(firstProp$.get()).toEqual('firstValue'); // the plucked `Derivable` should be settable firstProp$.set('other value'); // is the `Derivable` value the same as was set? - expect(firstProp$.get()).toEqual('other value'); + expect(firstProp$.get()).toEqual('other value'); // How many times was the spy called? Note the `skipFirst`.. - expect(reactPropSpy).toHaveBeenCalledTimes(1); + expect(reactPropSpy).toHaveBeenCalledTimes(1); // ...and what was the value? - expect(reactPropSpy).toHaveBeenLastCalledWith('other value', expect.toBeFunction()); + expect(reactPropSpy).toHaveBeenLastCalledWith('other value', expect.toBeFunction()); }); /** @@ -462,7 +564,7 @@ describe('advanced', () => { myMap$.swap(map => map.set('secondProp', 'new value')); // How many times was the spy called? Note the `skipFirst`. - expect(reactPropSpy).toHaveBeenCalledTimes(0); + expect(reactPropSpy).toHaveBeenCalledTimes(0); /** * ** Your Turn ** @@ -472,10 +574,10 @@ describe('advanced', () => { myMap$.swap(map => map.set('firstProp', 'new value')); // How many times was the spy called? Note the `skipFirst`.. - expect(reactPropSpy).toHaveBeenCalledTimes(1); + expect(reactPropSpy).toHaveBeenCalledTimes(1); // ...and what was the value? - expect(reactPropSpy).toHaveBeenLastCalledWith('new value', expect.toBeFunction()); + expect(reactPropSpy).toHaveBeenLastCalledWith('new value', expect.toBeFunction()); }); /** diff --git a/solution/8 - utils.test.ts b/solution/8 - utils.test.ts index 4527958..fd260ec 100644 --- a/solution/8 - utils.test.ts +++ b/solution/8 - utils.test.ts @@ -1,5 +1,16 @@ -import { atom, constant, derive, FinalWrapper } from '@skunkteam/sherlock'; -import { fromPromise, lift, pairwise, peek, scan, struct } from '@skunkteam/sherlock-utils'; +import { atom, constant, Derivable, derive, ErrorWrapper, FinalWrapper } from '@skunkteam/sherlock'; +import { + fromEventPattern, + fromObservable, + fromPromise, + lift, + pairwise, + peek, + scan, + struct, +} from '@skunkteam/sherlock-utils'; +import { Atom } from 'libs/sherlock/src/internal'; +import { from, Observable, Subject } from 'rxjs'; /** * In the `sherlock-utils` lib, there are a couple of functions that can combine @@ -29,7 +40,7 @@ describe('utils', () => { * * Note: don't call `pairwise()` using a lambda function! */ - myCounter$.derive(pairwise((newVal, oldVal) => newVal - oldVal, 0)).react(reactSpy); + myCounter$.derive(pairwise((newVal, oldVal) => newVal - oldVal, 0)).react(reactSpy); expect(reactSpy).toHaveBeenCalledTimes(1); expect(reactSpy).toHaveBeenLastCalledWith(1, expect.toBeFunction()); @@ -49,7 +60,7 @@ describe('utils', () => { // ** Your Turn ** // What will the next output be? expect(reactSpy).toHaveBeenCalledTimes(4); - expect(reactSpy).toHaveBeenLastCalledWith(10, expect.toBeFunction()); // 20 (current value of `myCounter$`) - 10 (previous value of `myCounter$`) + expect(reactSpy).toHaveBeenLastCalledWith(10, expect.toBeFunction()); // 20 (current value of `myCounter$`) - 10 (previous value of `myCounter$`) }); /** @@ -78,7 +89,7 @@ describe('utils', () => { * * Note: don't call `pairwise()` using a lambda function! */ - myCounter$.derive(scan((acc, val) => val - acc, 0)).react(reactSpy); + myCounter$.derive(scan((acc, val) => val - acc, 0)).react(reactSpy); expect(reactSpy).toHaveBeenCalledTimes(1); expect(reactSpy).toHaveBeenLastCalledWith(1, expect.toBeFunction()); @@ -98,7 +109,7 @@ describe('utils', () => { // ** Your Turn ** // What will the next output be? expect(reactSpy).toHaveBeenCalledTimes(4); - expect(reactSpy).toHaveBeenLastCalledWith(12, expect.toBeFunction()); // 20 (current value of `myCounter$`) - 8 (previous returned value) + expect(reactSpy).toHaveBeenLastCalledWith(12, expect.toBeFunction()); // 20 (current value of `myCounter$`) - 8 (previous returned value) }); it('`pairwise()` on normal arrays', () => { @@ -115,16 +126,16 @@ describe('utils', () => { * * Note: don't call `pairwise()` using a lambda function! */ - myList2 = myList.map(pairwise((newV, oldV) => newV - oldV, 0)); + myList2 = myList.map(pairwise((newV, oldV) => newV - oldV, 0)); expect(myList2).toMatchObject([1, 1, 1, 2, 5]); // However, we should be careful with this, as this does not always behave as intended. // Particularly, what exactly happens when we do call `pairwise()` using a lambda function? - myList2 = myList.map(v => pairwise((newV, oldV) => newV - oldV, 0)(v)); // copy the same implementation here + myList2 = myList.map(v => pairwise((newV, oldV) => newV - oldV, 0)(v)); // copy the same implementation here expect(myList2).toMatchObject([1, 2, 3, 5, 10]); // Even if we are more clear about what we pass, this unintended behavior does not go away. - myList2 = myList.map((v, _, _2) => pairwise((newV, oldV) => newV - oldV, 0)(v)); // copy the same implementation here + myList2 = myList.map((v, _, _2) => pairwise((newV, oldV) => newV - oldV, 0)(v)); // copy the same implementation here expect(myList2).toMatchObject([1, 2, 3, 5, 10]); // `pairwise()` keeps track of the previous value under the hood. Using a lambda of @@ -134,14 +145,14 @@ describe('utils', () => { // Other than by not using a lambda function, we can fix this by // saving the `pairwise` in a variable and reusing it for every call. - let f = pairwise((newV, oldV) => newV - oldV, 0); + let f = pairwise((newV, oldV) => newV - oldV, 0); myList2 = myList.map(v => f(v)); expect(myList2).toMatchObject([1, 1, 1, 2, 5]); // To get more insight in the `pairwise()` function, you can call it // manually. Here, we show what happens under the hood. - f = pairwise((newV, oldV) => newV - oldV, 0); + f = pairwise((newV, oldV) => newV - oldV, 0); myList2 = []; myList2[0] = f(myList[0]); // `f` is newly created with `init = 0`, so applies `1 - 0 = 1`. @@ -160,8 +171,8 @@ describe('utils', () => { * Note that the function `f` still requires a number to be the return value. * Checking for equality therefore cannot be done directly within `f`. */ - f = pairwise((newV, oldV) => newV - oldV, 0); - myList2 = myList.filter(v => f(v) === 1); + f = pairwise((newV, oldV) => newV - oldV, 0); + myList2 = myList.filter(v => f(v) === 1); expect(myList2).toMatchObject([1, 2, 3]); // only the numbers `1`, `2`, and `3` produce 1 when subtracted with the previous value }); @@ -177,7 +188,7 @@ describe('utils', () => { * Use a `scan()` combined with a `.map()` on `myList` * to subtract the previous value from the current. */ - let f: (v: number) => number = scan((acc, val) => val - acc, 0); + let f: (v: number) => number = scan((acc, val) => val - acc, 0); myList2 = myList.map(f); expect(myList2).toMatchObject([1, 1, 2, 3, 7]); @@ -204,8 +215,8 @@ describe('utils', () => { * (1+2+3+5+10), and since this sum only prouces a value higher than 8 when the * values `5` and `10` are added, the result should be `[5,10]`. */ - f = scan((acc, val) => val + acc, 0); - myList2 = myList.filter(v => f(v) >= 8); + f = scan((acc, val) => val + acc, 0); + myList2 = myList.filter(v => f(v) >= 8); expect(myList2).toMatchObject([5, 10]); }); @@ -221,7 +232,7 @@ describe('utils', () => { * Now, use `pairwise()` directly in `.react()`. Implement the same * derivation as before: subtract the previous value from the current. */ - reactSpy = jest.fn(pairwise((newV, oldV) => newV - oldV, 0)); + reactSpy = jest.fn(pairwise((newV, oldV) => newV - oldV, 0)); myCounter$.react(reactSpy); expect(reactSpy).toHaveLastReturnedWith(1); @@ -247,7 +258,7 @@ describe('utils', () => { * derivation as before: subtract all the emitted values. */ - reactSpy = jest.fn(scan((acc, val) => val - acc, 0)); + reactSpy = jest.fn(scan((acc, val) => val - acc, 0)); myCounter$.react(reactSpy); expect(reactSpy).toHaveLastReturnedWith(1); @@ -334,7 +345,7 @@ describe('utils', () => { * In other words: the new function should take a `Derivable` (or more specifically: * an `Unwrappable`) and return a `Derivable`. */ - const isEvenDerivable = lift(isEvenNumber); + const isEvenDerivable = lift(isEvenNumber); expect(isEvenNumber(2)).toBe(true); expect(isEvenNumber(13)).toBe(false); @@ -355,7 +366,7 @@ describe('utils', () => { * ** Your Turn ** * Now, use `lift()` as alternative to `.map()`. */ - myMappedDerivable$ = lift(addOne)(myAtom$); + myMappedDerivable$ = lift(addOne)(myAtom$); expect(myMappedDerivable$.value).toBe(2); }); @@ -375,7 +386,7 @@ describe('utils', () => { * value of `myTrackedAtom$`, which should be tracked. */ const reactor = jest.fn(v => v); - derive(() => myTrackedAtom$.get() + peek(myUntrackedAtom$)).react(reactor); + derive(() => myTrackedAtom$.get() + peek(myUntrackedAtom$)).react(reactor); expect(reactor).toHaveBeenCalledOnce(); expect(reactor).toHaveLastReturnedWith(3); @@ -414,8 +425,6 @@ describe('utils', () => { // Every atom has a `final` property. expect(myAtom$.final).toBeFalse(); - // TODO: SHOW THAT CONST ALSO GIVES THE SAME ERROR MESSAGE WHEN SET!! - // You can make an atom final using the `.makeFinal()` function. myAtom$.makeFinal(); expect(myAtom$.final).toBeTrue(); @@ -425,14 +434,15 @@ describe('utils', () => { * What do you think will happen when we try to `.get()` or `.set()` this atom? */ // .toThrow() or .not.toThrow()? ↴ - expect(() => myAtom$.get()).not.toThrow(); - expect(() => myAtom$.set(2)).toThrow('cannot set a final derivable'); + expect(() => myAtom$.get()).not.toThrow(); + expect(() => myAtom$.set(2)).toThrow('cannot set a final derivable'); // This behavior is consistent with normal variables created using `const`. + // Alternatively, you can set a last value before setting it to `final`, using `.setFinal()`. // .toThrow() or .not.toThrow()? ↴ - expect(() => myAtom$.setFinal(2)).toThrow('cannot set a final derivable'); - // Remember: we try to set an atom that is already final, so we get an error + expect(() => myAtom$.setFinal(2)).toThrow('cannot set a final derivable'); + // Remember: we try to set an atom that is already final, so we get an error // There is no way to 'unfinalize' a Derivable, so the only solution to reset is to // create a whole new Derivable. @@ -444,7 +454,7 @@ describe('utils', () => { // `final` in disguise. You can verify this by checking the implementation of `constant` at // libs/sherlock/src/lib/derivable/factories.ts:39 const myConstantAtom$ = constant(1); - expect(myConstantAtom$.final).toBe(true); + expect(myConstantAtom$.final).toBe(true); }); it('deriving a `final` Derivable', () => { @@ -463,8 +473,8 @@ describe('utils', () => { * * What will happen to `myDerivable$` when I change `myAtom$` to be `final`? */ - expect(myDerivable$.final).toBe(true); - expect(myDerivable$.connected).toBe(false); + expect(myDerivable$.final).toBe(true); + expect(myDerivable$.connected).toBe(false); /** * Derivables that are final (or constant) are no longer tracked. This can save @@ -474,28 +484,22 @@ describe('utils', () => { */ }); - it('TODO: `final` State', () => { - /** A property such as `.final`, similar to variables like `.errored` and `.resolved` - * is useful for checking whenever a Derivable is in a certain state, but these properties - * are just a boolean. This means that these properties cannot be derived and we cannot - * have certain functions execute whenever there is a change in the state. For this reason, - * every Derivable holds an internal state, retrievable using `.getState()` which can be - * derived. TODO: Have a clear place where I explain this! Now I have info up top here too. - * - * We have seen that states (`State`) can be `undefined`, `ErrorWrapper`, - * or any regular type `V`. Other states exist, such as the `MaybeFinalState`. This state can be either - * a normal state `State` or a special `FinalWrapper>` state. Let's see that in action. + it('`final` State', () => { + /** + * We have seen that states (`State`) can be `unresolved`, `ErrorWrapper`, + * or any regular type `V`. If you want to also show whether a Derivable is `final`, you can + * use the `MaybeFinalState`, which is either any normal `State` or a special + * `FinalWrapper>` state. Let's see that in action. */ - expect(myAtom$.getMaybeFinalState()).toBe(1); // `getMaybeFinalState` can return a normal state, which in turn can be any normal type. + myAtom$.set(2); + expect(myAtom$.getMaybeFinalState()).toBe(2); // `getMaybeFinalState` can return a normal state, which in turn can be any normal type. - myAtom$.makeFinal(); + myAtom$.setError('2'); + expect(myAtom$.getMaybeFinalState()).toBeInstanceOf(ErrorWrapper); // `getMaybeFinalState()` can return a normal state, which in turn can be unresolved. - expect(myAtom$.getMaybeFinalState()).toBeInstanceOf(FinalWrapper); // but `getMaybeFinalState` can also return a `FinalWrapper` type. - expect(myAtom$.getState()).toBe(1); // the normal type is still the final it contains. - - // TODO: MAAR JE KAN EEN STATE HELEMAAL NIET DERIVEN! - // Dus dat is allemaal onzin lijkt me....??? Bovendien, kan je normale variabelen niet deriven door het gewoon te - // wrappen in een atom ofzo? Of door te structen? + myAtom$.setFinal(2); + expect(myAtom$.getMaybeFinalState()).toBeInstanceOf(FinalWrapper); // but `getMaybeFinalState)_` can also return a `FinalWrapper` type. + expect(myAtom$.getState()).toBe(2); // the normal `getState()` function cannot return a FinalWrapper. }); }); @@ -516,8 +520,8 @@ describe('utils', () => { * ** Your Turn ** * What do you think is the default state of an atom based on a Promise? */ - expect(myAtom$.value).toBe(undefined); - expect(myAtom$.final).toBe(false); + expect(myAtom$.value).toBe(undefined); + expect(myAtom$.final).toBe(false); // Now we wait for the Promise to be handled (resolved). await promise; @@ -526,8 +530,8 @@ describe('utils', () => { * ** Your Turn ** * So, what will happen to `myAtom$`? */ - expect(myAtom$.value).toBe(15); - expect(myAtom$.final).toBe(true); + expect(myAtom$.value).toBe(15); + expect(myAtom$.final).toBe(true); // Now we make a promise that is rejected when called. promise = Promise.reject('Oh no, I messed up!'); @@ -540,9 +544,9 @@ describe('utils', () => { * ** Your Turn ** * So, what will happen to `myAtom$` now? */ - expect(myAtom$.errored).toBe(true); - expect(myAtom$.error).toBe('Oh no, I messed up!'); - expect(myAtom$.final).toBe(true); + expect(myAtom$.errored).toBe(true); + expect(myAtom$.error).toBe('Oh no, I messed up!'); + expect(myAtom$.final).toBe(true); }); it('`.toPromise()`', async () => { @@ -560,7 +564,7 @@ describe('utils', () => { */ myAtom$.set('second value'); // `.resolves` or `.rejects`? ↴ - await expect(promise).resolves.toBe('initial value'); // `myAtom$` starts with a value ('initial value'), so the promise is immediately resolved + await expect(promise).resolves.toBe('initial value'); // `myAtom$` starts with a value ('initial value'), so the promise is immediately resolved myAtom$.unset(); // reset @@ -572,7 +576,7 @@ describe('utils', () => { */ myAtom$.set('third value'); // `.resolves` or `.rejects`? ↴ - await expect(promise).resolves.toBe('third value'); // This is now the first value the atom obtains since the promise was created. + await expect(promise).resolves.toBe('third value'); // This is now the first value the atom obtains since the promise was created. // Whenever an atom is in an `unresolved` state, the corresponding Promise is pending. // This means that the Promise can still become resolved or rejected depending on the atom's actions. @@ -591,7 +595,7 @@ describe('utils', () => { await promise; } catch (error: any) { // `.toBe('Error.')` or `.not.toBe('Error.')`? ↴ - expect(error.message).not.toBe('Error.'); + expect(error.message).not.toBe('Error.'); } myAtom$.set('no more error'); @@ -609,18 +613,189 @@ describe('utils', () => { await promise; } catch (error: any) { // `.toBe('Error.')` or `.not.toBe('Error.')`? ↴ - expect(error.message).toBe('Error.'); + expect(error.message).toBe('Error.'); } }); + /** + * Some reactive libraries already existed, such as RxJS. + * Angular uses RxJS, and since we use Angular, we are forced to use RxJS. + * However, RxJS becomes more and more complicated and user-unfriendly + * as your application becomes bigger. This was the main reason why + * Sherlock was developed. + * As Angular uses RxJS, our Sherlock library needs to be compatible with it. + * The `fromObservable()` and `toObservable()` functions are used for this. + */ it('`fromObservable()`', () => { - // Has to do with SUBSCRIBING. Hasn't been discussed either... - // TODO: "As all Derivables are now compatible with rxjs's `from` function, - // we no longer need the `toObservable` function from `@skunkteam/sherlock-rxjs`." + /** + * RxJS uses `Observables` which are similar to our `Derivables`. + * It also uses the concept of `Subscribing`, which is similar to `Deriving`. + * + * Here's an example of the similarities and differences. + */ + + const dummyObservable = new Subject(); // `Subject` is a form of `Observable` + let subscribedToDummy; + dummyObservable.subscribe({ next: x => (subscribedToDummy = x) }); + dummyObservable.next(2); + expect(subscribedToDummy).toBe(2); + + const dummyDerivable$ = new Atom(1); + const derivedOfDummy = dummyDerivable$.derive(x => x); + dummyDerivable$.set(2); + expect(derivedOfDummy.value).toBe(2); + + /** + * The code for turning an Observable "observable" into a derivable "value$" is + * like this (from libs/sherlock-utils/src/lib/from-observable.ts): + * + * ``` + * observable.subscribe({ + * next: value => value$.set(value), + * error: err => value$.setFinal(error(err)), + * complete: () => value$.makeFinal(), + * }); + * return () => subscription.unsubscribe(); + * ``` + * + * Essentially, + * - we map `next()` (Observable) to `set()` (Derivable); + * - we map `error()` (Observable) to `setFinal(error())` (Derivable); + * NOTE: we don't map it to `setError()` as we would then be able to undo the error state. We can't undo it when it's final. + * - we map`complete()` (Observable) to `makeFinal()` (Derivable); + * - and we return a function we can call to stop (similar to using `react()`), which is mapped to `unsubscribe()` + * NOTE: in fact, `fromEventPattern()` is build using the `react()` function! + * + * Okay, that's enough info for now. Let's get to work. + * The `fromObservable()` function translates an `Observable` to a `Derivable`. + * + * ** Your Turn ** + * + * Use `fromObservable()` to turn this `Observable` into a `Derivable`. + */ + + // A `Subject` is the simplest form of `Observable`: it is comparable to our `Atom`. + const myObservable = new Subject(); + + const myDerivable$: Derivable = fromObservable(myObservable); + const reactor = jest.fn(); + const onError = jest.fn(); + myDerivable$.react(reactor, { onError }); + + myObservable.next(1); + expect(myDerivable$.value).toBe(1); + + myObservable.next(2); + expect(myDerivable$.value).toBe(2); + + myObservable.error('OH NO!'); + expect(myDerivable$.error).toBe('OH NO!'); + + myObservable.next(3); + expect(myDerivable$.value).toBe(undefined); + // It is set to final, so after an error has been thrown, you cannot undo it. + + /** + * The `toObservable()` function has become obsolete as RxJS already contains a function + * called `from()` that can parse `Derivables` to `Observables`. Note that a `from` from + * the side of RxJS is the same as a `to` from the side of Sherlock. + * + * ** Your Turn ** + * + * Use the `from()` function to turn this `Derivable` into an `Observable`. + */ + const myDerivable2$ = atom(1); + + const myObservable2: Observable = from(myDerivable2$); + + let value = 0; + myObservable2.subscribe({ next: x => (value = x) }); + + expect(value).toBe(1); // immediate call to `next()` + + myDerivable2$.set(2); + expect(value).toBe(2); }); - it('`fromEventPattern`', () => { - // TODO: this is kinda complicated shit... Requires explaining a lot of extra stuff (Subjects, Subscribing, Observables...). Leave for now? + /** + * The `fromObservable()` function can be used to turn `Observables` into `Derivables`. Under the hood, + * this function uses the `fromEventPattern()` function which is capable of turning any abstract pattern + * into a `Derivable`. Let's see how that works. + */ + it('`fromEventPattern`', async () => { + /** + * The basic idea is that you get a stream of inputs, and want to map that stream to a Derivable stream. + * This means that, whenever an update comes from the input-stream, this update is also given to a Derivable, + * which can then be `derive`d and `react`ed to. + * + * For example, you may get a function which, instead of returning an output, passes some output to your own chosen + * `callback` function. For example, this code 'heyifies' your input, adding "hey" in front of it. + * It then passes this heyified output to the callback function. As seen before in `react()` and `fromObservable()`, + * these functions like `heyify()` typically return a stopping function, which can be called to stop the heyification. + */ + function heyify(names: string[], callback: (something: string) => void) { + let i = 0; + // every 100ms, call the callback function + const int = setInterval(() => callback(`Hey ${names[i++]}`), 100); + // when this function is called, `clearInterval()` stops the stream. + return () => clearInterval(int); + } + + /** + * Now, we want to turn this process into a Derivable, where updates that are send to the callback are passed to the Derivable as well. + * This way, we can use our cool Derivable functions like `derive()` or `react()` to process changes to this Derivable + * (= new outputs from the callback). + * + * `fromEventPattern()` looks more complex than it is. This function sets a Derivable in place of the callback and also returns + * a stopping function, which can be reused from the `heyify()` function. + */ + function heyify$(names: string[]): Derivable { + return fromEventPattern(v$ => { + // the callback now sets a derivable. + const stop = heyify(names, something => v$.set(something)); + // and the stopping function is returned. + return stop; + }); + } + + /** + * This Derivable can now be reacted to. + * When this Derivable gets connected (reacted to, in this case), the function within the `fromEventPattern()` triggers. + * Upon connection, a fresh atom is passed to this function, which is then `set()` in the callback function of `heyify()`. + * This atom "lives" in the body of the callback function and is only changed when a new value is passed to the callback function. + * Here, this is a new "Hey {name}" message, every 100ms. + */ + + // To test this, we need to make sure time elapses only when we want it to. So we temporarily stop time, no big deal. + // (Don't worry about what the internet says about the dangers of stopping time: this is perfectly safe.) + jest.useFakeTimers(); + + let value: string = ''; + const stop = heyify$(['Bob', 'Jan', 'Hans', 'Roos']).react(v => (value = v)); + + /** + * ** Your Turn ** + * + * What do you expect `value` to be? + * *Hint: At the start, time has not yet passed, and `setInterval()` only responds after the first 100ms.* + */ + expect(value).toBe(''); + + // We manually move time by 100ms, which is exactly the time that the `heyify()` function needs to call the `callback()` again. + jest.advanceTimersByTime(100); + expect(value).toBe('Hey Bob'); + + jest.advanceTimersByTime(100); + expect(value).toBe('Hey Jan'); + + jest.advanceTimersByTime(100); + expect(value).toBe('Hey Hans'); + + stop(); + + // After stopping, the Derivable no longer responds to updates - it is essentially final. + jest.advanceTimersByTime(100); + expect(value).toBe('Hey Hans'); }); }); }); diff --git a/solution/9 - expert.test.ts b/solution/9 - expert.test.ts index b1efe4f..9f3a45a 100644 --- a/solution/9 - expert.test.ts +++ b/solution/9 - expert.test.ts @@ -1,4 +1,4 @@ -import { DerivableAtom, atom, derive } from '@skunkteam/sherlock'; +import { DerivableAtom, atom, derive, unwrap } from '@skunkteam/sherlock'; import { derivableCache } from '@skunkteam/sherlock-utils'; describe('expert', () => { @@ -24,7 +24,7 @@ describe('expert', () => { * called at this point? */ // `.toHaveBeenCalled()` or `.not.toHaveBeenCalled()`? ↴ - expect(hasDerived).not.toHaveBeenCalled(); + expect(hasDerived).not.toHaveBeenCalled(); mySecondDerivation$.get(); @@ -36,7 +36,7 @@ describe('expert', () => { * first `Derivable` actually executed its derivation? */ // how many times? - expect(hasDerived).toHaveBeenCalledTimes(3); + expect(hasDerived).toHaveBeenCalledTimes(3); }); /** @@ -60,7 +60,7 @@ describe('expert', () => { * expectations pass. */ const myAtom$ = atom(true); - const myFirstDerivation$ = myAtom$.derive(firstHasDerived).autoCache(); + const myFirstDerivation$ = myAtom$.derive(firstHasDerived).autoCache(); const mySecondDerivation$ = myFirstDerivation$.derive(() => secondHasDerived(myFirstDerivation$.get() + myFirstDerivation$.get()), ); @@ -102,9 +102,9 @@ describe('expert', () => { mySecondDerivation$.get(); // first after last .get() - expect(firstHasDerived).toHaveBeenCalledTimes(2); + expect(firstHasDerived).toHaveBeenCalledTimes(2); // second after last .get() - expect(secondHasDerived).toHaveBeenCalledTimes(3); + expect(secondHasDerived).toHaveBeenCalledTimes(3); }); }); @@ -174,7 +174,7 @@ describe('expert', () => { * But does that apply here? * How many times has the setup run, for the price `Derivable`. */ - expect(stockPrice$).toHaveBeenCalledTimes(2); + expect(stockPrice$).toHaveBeenCalledTimes(2); /** Can you explain this behavior? */ /** @@ -227,19 +227,19 @@ describe('expert', () => { */ // How often was the reactor on price$ called? - expect(reactSpy).toHaveBeenCalledTimes(0); + expect(reactSpy).toHaveBeenCalledTimes(0); // And how many times did the setup run? - expect(stockPrice$).toHaveBeenCalledTimes(2); + expect(stockPrice$).toHaveBeenCalledTimes(2); // What's the value of price$ now? - expect(price$.value).toEqual(undefined); + expect(price$.value).toEqual(undefined); // And the value of googlPrice$? - expect(googlPrice$.value).toEqual(1079.11); + expect(googlPrice$.value).toEqual(1079.11); // Is googlPrice$ still even driving any reactors? - expect(googlPrice$.connected).toEqual(false); + expect(googlPrice$.connected).toEqual(false); /** * Can you explain this behavior? @@ -271,12 +271,47 @@ describe('expert', () => { * the created `Derivable` will not run the setup again and * everything should work as expected. * - * ** Your Turn ** TODO: not in the SOLUTIONS!! + * ** Your Turn ** * * *Hint: there is even an `unwrap` helper function for just * such an occasion, try it!* */ }); + it('BONUS', () => { + const company$ = atom('GOOGL'); + + // We have now split it into two derivations. + // The `unwrap()` function essentially does the same as `v => v.get()`, unwrapping one layer of derivable. + const price$ = company$.derive(company => stockPrice$(company)).derive(unwrap); + + // The rest of the steps are the same.. + price$.react(reactor); + expect(reactSpy).not.toHaveBeenCalled(); + + const googlPrice$ = stockPrice$.mock.results[0].value as DerivableAtom; + expect(googlPrice$.connected).toEqual(true); + expect(googlPrice$.value).toEqual(undefined); + + googlPrice$.set(1079.11); + + // ..but all these results are now different. + expect(reactSpy).toHaveBeenCalledTimes(1); + expect(stockPrice$).toHaveBeenCalledTimes(1); + expect(price$.value).toEqual(1079.11); + expect(googlPrice$.value).toEqual(1079.11); + expect(googlPrice$.connected).toEqual(true); + + /** + * In the original case, the `derive` used `stockPrice$(company).get()`,. + * This results in a value (`undefined` here) which is stored in `price$`. If you now set the value of the atom, the + * `derive` will run again as it is connected, and this will create a new atom, again with value `atom.unresolved()`. + * As such, `price$` keeps the same value, `undefined`. + * In our new case here, the first `derive` uses `stockPrice$(company)`, and a second `derive` applies the `.get()` + * (namely: `unwrap`). The second `derive` is put directly on the result of the first (namely, the `atom.unresolved()`). + * If you now set the value of the atom, this second derivation will trigger again and will `get()` the new value from the atom. + * As such, `price$` will update to the new value: 1079.11. + */ + }); /** * But even when you split the setup and the `unwrap`, you may not @@ -322,8 +357,8 @@ describe('expert', () => { * * So the value was increased. What do you think happened now? */ - expect(reactSpy).toHaveBeenCalledTimes(2); - expect(reactSpy).toHaveBeenLastCalledWith([1079.11]); + expect(reactSpy).toHaveBeenCalledTimes(2); + expect(reactSpy).toHaveBeenLastCalledWith([1079.11]); /** * So that worked, now let's try and add another company to the @@ -341,9 +376,9 @@ describe('expert', () => { * * We had a price for 'GOOGL', but not for 'APPL'... */ - expect(reactSpy).toHaveBeenCalledTimes(3); - expect(reactSpy).toHaveBeenCalledWith([1079.11, undefined]); - // Note: `[undefined, undefined]` will pass too, but is incorrect. + expect(reactSpy).toHaveBeenCalledTimes(3); + expect(reactSpy).toHaveBeenCalledWith([1079.11, undefined]); + // Note: `[undefined, undefined]` will pass too, but is incorrect. }); }); @@ -403,7 +438,7 @@ describe('expert', () => { * * Has anything changed, by using the `derivableCache`? */ - expect(stockPrice$).toHaveBeenCalledTimes(1); + expect(stockPrice$).toHaveBeenCalledTimes(1); // Now let's resolve the price stockPrice$.mock.results[0].value.set(1079.11); @@ -416,10 +451,10 @@ describe('expert', () => { * * What happens this time? Has the setup run again? */ - expect(stockPrice$).toHaveBeenCalledTimes(1); + expect(stockPrice$).toHaveBeenCalledTimes(1); // Ok, but did it update the HTML? - expect(reactSpy).toHaveBeenCalledTimes(2); - expect(lastEmittedHTMLs()[0]).toContain('$ 1079.11'); + expect(reactSpy).toHaveBeenCalledTimes(2); + expect(lastEmittedHTMLs()[0]).toContain('$ 1079.11'); // Last chance, what if we add a company companies$.swap(current => [...current, 'APPL']); @@ -432,12 +467,12 @@ describe('expert', () => { * * But did it calculate 'GOOGL' again too? */ - expect(stockPrice$).toHaveBeenCalledTimes(2); - expect(reactSpy).toHaveBeenCalledTimes(3); + expect(stockPrice$).toHaveBeenCalledTimes(2); + expect(reactSpy).toHaveBeenCalledTimes(3); // The first should be the generated HTML for 'GOOGL'. - expect(lastEmittedHTMLs()[0]).toContain('$ 1079.11'); + expect(lastEmittedHTMLs()[0]).toContain('$ 1079.11'); // The second should be the generated HTML for 'APPL'. - expect(lastEmittedHTMLs()[1]).toContain('$ unknown'); + expect(lastEmittedHTMLs()[1]).toContain('$ unknown'); }); }); }); diff --git a/tutorial/1 - intro.test.ts b/tutorial/1 - intro.test.ts index 784e8ba..198411e 100644 --- a/tutorial/1 - intro.test.ts +++ b/tutorial/1 - intro.test.ts @@ -50,7 +50,7 @@ describe.skip('intro', () => { * This can also be indicated with the `__YOUR_TURN__` variable. * * It should be clear what to do here... */ - bool = __YOUR_TURN__; + bool = __YOUR_TURN__; expect(bool).toBeTrue(); // We use expectations like this to verify the result. }); @@ -78,7 +78,7 @@ describe.skip('the basics', () => { // the `Atom`. expect(myValue$.get()).toEqual(1); - // ** Your Turn ** + // ** Your Turn ** // Use the `.set()` method to change the value of the `Atom`. expect(myValue$.get()).toEqual(2); }); @@ -104,7 +104,7 @@ describe.skip('the basics', () => { * negative to a positive number and vice versa) of the original `Atom`. */ // Use `myValue$.derive(val => ...)` to implement `myInverse$`. - const myInverse$ = myValue$.derive(__YOUR_TURN__ => __YOUR_TURN__); + const myInverse$ = myValue$.derive(__YOUR_TURN__ => __YOUR_TURN__); expect(myInverse$.get()).toEqual(-1); // So if we set `myValue$` to -2: myValue$.set(-2); @@ -129,7 +129,7 @@ describe.skip('the basics', () => { * * Now react to `myCounter$`. In every `react()`. * Increase the `reacted` variable by one. */ - myCounter$.react(() => __YOUR_TURN__); + myCounter$.react(() => __YOUR_TURN__); expect(reacted).toEqual(1); // `react()` will react immediately, more on that later. diff --git a/tutorial/2 - deriving.test.ts b/tutorial/2 - deriving.test.ts index c2f2441..d382aed 100644 --- a/tutorial/2 - deriving.test.ts +++ b/tutorial/2 - deriving.test.ts @@ -35,7 +35,7 @@ describe.skip('deriving', () => { */ // We can combine txt with `repeat$.get()` here. - const lyric$ = text$.derive(txt => txt /* __YOUR_TURN__ */); + const lyric$ = text$.derive(txt => txt /* __YOUR_TURN__ */); expect(lyric$.get()).toEqual(`It won't be long`); @@ -72,14 +72,12 @@ describe.skip('deriving', () => { */ // Should return 'Fizz' when `myCounter$` is a multiple of 3 and '' otherwise. - const fizz$: Derivable = myCounter$.derive(__YOUR_TURN__); - const fizz$: Derivable = myCounter$.derive(v => (v % 3 ? '' : 'Fizz')); // Shorthand for `v % 3 !== 0` + const fizz$: Derivable = myCounter$.derive(__YOUR_TURN__); // Should return 'Buzz' when `myCounter$` is a multiple of 5 and '' otherwise. - const buzz$: Derivable = myCounter$.derive(__YOUR_TURN__); - const buzz$: Derivable = myCounter$.derive(v => (v % 5 ? '' : 'Buzz')); + const buzz$: Derivable = myCounter$.derive(__YOUR_TURN__); - const fizzBuzz$: Derivable = derive(__YOUR_TURN__); + const fizzBuzz$: Derivable = derive(__YOUR_TURN__); expect(fizz$.get()).toEqual(''); expect(buzz$.get()).toEqual(''); @@ -153,7 +151,7 @@ describe.skip('deriving', () => { const tweetCount = pastTweets.length; const lastTweet = pastTweets[tweetCount - 1]; - expect(tweetCount).toEqual(__YOUR_TURN__); // Is there a new tweet? + expect(tweetCount).toEqual(__YOUR_TURN__); // Is there a new tweet? expect(lastTweet).toContain(__YOUR_TURN__); // Who sent it? Donald? Or Barack? expect(lastTweet).toContain(__YOUR_TURN__); // What did he tweet? @@ -210,7 +208,7 @@ describe.skip('deriving', () => { .and(__YOUR_TURN__) .or(__YOUR_TURN__) as Derivable; - const fizzBuzz$ = derive(() => fizz$.get() + buzz$.get()).or(__YOUR_TURN__); + const fizzBuzz$ = derive(() => fizz$.get() + buzz$.get()).or(__YOUR_TURN__); for (let count = 1; count <= 100; count++) { // Set the value of the `Atom`, diff --git a/tutorial/3 - reacting.test.ts b/tutorial/3 - reacting.test.ts index d1be7e6..05a8b7b 100644 --- a/tutorial/3 - reacting.test.ts +++ b/tutorial/3 - reacting.test.ts @@ -5,42 +5,6 @@ import { atom } from '@skunkteam/sherlock'; * If you see this variable, you should do something about it. :-) */ export const __YOUR_TURN__ = {} as any; -// xxx check my solutions with the actual solutions (https://github.com/skunkteam/sherlock/tree/tutorial-solutions/robin/tutorial) -// FIXME: remove all TODO: and FIXME: -// xxx check whether the generated tutorials and solutions actually work (e.g. are all solutions correct? No weird shenanigans?) - -// FIXME: ALSO CHECK "Or, alternatively"! -// FIXME: deze file niet linten / builden (voor automatische test). Tutorial ook niet. Maar solutions juist wel! OP EIND. (mag beide wel linten right?) -// FIXME: interne review document, mocht ik iets hebben om te laten zien! In Google Drive, zet het erin! -// xxx werkt `npm run tutorial` nog? > Nu wel. -// xxx PETER: "nu je toch met Sherlock bezig bent; zou je voor mij eens kunnen checken of de code voorbeelden in de README -// nog wel kloppen met de huidige API? Ik heb het gevoel dat dat niet zo is; volgens mij is er geen function "derivation()" -// en heet dat nu "derive()" bijvoorbeeld." -// FIXME: OOOOOOH JA, ik had eroverheen gepushed! Dat moet nog een PR met terugwerkende kracht worden... (of commits squashen, en dat ze dan maar de commit moeten reviewen?) -// FIXME: Add FromEventPattern + FromObservable -// xxx fix the generator for code blocks. -// FIXME: now check whether it did not remove excess lines or kept 2 empty lines where it should not. (I think it is good though.) -/** - * x Final States; (finalGetter, finalMethod, getMaybeFinalState, FinalWrapper, MaybeFinalState, _isFinal, makeFinal, markFinal, .final, .finalized, setFinal...) - * ? Lens; (libs/sherlock/src/lib/derivable/lens.ts) - map die twee kanten op kan gaan. Maar een map kan dat al? Maar hier kan - * je dat los definieren! Je kan gewoon `lens` ipv `var.lens`. Zelden dat je dit gebruikt. Output is een Derivable though. - * x Lift; (libs/sherlock-utils/src/lib/lift.ts) - * x Peek; (libs/sherlock-utils/src/lib/peek.ts) - dan track je niet. In een derivable, deze tracked hij dan niet (ipv .get() waar het wel getracked wordt) - * x Template; (libs/sherlock-utils/src/lib/template.ts) - to make a string using a template literal. (Uses unwrap!!) - * / Factory; (libs/sherlock/src/lib/derivable/factories.ts) - simply contains functions to create objects, namely - * lens; atom; constant; derive. - * !! Flat-map; (libs/sherlock/src/lib/derivable/mixins/flat-map.ts) - ??? - * array: nested arrays naar array - * Derivable: gooit er derive.get() achteraan? - * Derivable (input van inputveld). Flatmap geeft Derivable terug. Derivable.flatmap() returned misschien - * Derivable, returned dan de number. flatMap is een `derive`, maar wat hij returned haalt hij uit de Derivable. - * ofzoiets. Maakt code korter. - * x Fallback-to; - op een derivable. Als een atom `unresolved` is, dan fallt het back to this value. Ofwel, initial value, maar - * ook als hij later unresolved wordt, dan wordt hij dit (vaak wel initial value). - * x Take - react options gebruiken buiten react. In een derivable chain, halverwege die options gebruiken. - * -- e.g. (from)Promise. Zodra die een waarde aanneemt kan hij niet meer veranderen. - * Let FromPromise, FromObservable, FromEventPattern ook uit (in utils?), ToPromise, ToObservable, in praktijk ook handig. - * FromEventPattern (haily mary, als alles niet werkt, dan dit doen). - */ /** * In the intro we have seen a basic usage of the `.react()` method. * Let's dive a bit deeper into the details of this method. @@ -103,7 +67,7 @@ describe.skip('reacting', () => { * Time to react to `myAtom$` with the `reactor()` function defined * above. */ - __YOUR_TURN__; + __YOUR_TURN__; expectReact(1, 'initial value'); @@ -135,7 +99,7 @@ describe.skip('reacting', () => { * * catch the returned `stopper` in a variable */ - __YOUR_TURN__; + __YOUR_TURN__; expectReact(1, 'initial value'); @@ -144,7 +108,7 @@ describe.skip('reacting', () => { * * Call the `stopper`. */ - __YOUR_TURN__; + __YOUR_TURN__; myAtom$.set('new value'); @@ -222,7 +186,7 @@ describe.skip('reacting', () => { * * Try giving `boolean$` as `until` option. */ - string$.react(reactor, __YOUR_TURN__); + string$.react(reactor, __YOUR_TURN__); // It should react directly as usual. expectReact(1, 'Value'); @@ -270,7 +234,7 @@ describe.skip('reacting', () => { * Use `!string$.get()` to return `true` when the `string` is * empty. */ - string$.react(reactor, __YOUR_TURN__); + string$.react(reactor, __YOUR_TURN__); // It should react as usual: string$.set('New value'); @@ -297,7 +261,7 @@ describe.skip('reacting', () => { * Try using the first parameter of the `until` function to do * the same as above. */ - string$.react(reactor, __YOUR_TURN__); + string$.react(reactor, __YOUR_TURN__); // It should react as usual. string$.set('New value'); @@ -324,7 +288,7 @@ describe.skip('reacting', () => { boolean$.set(false); // ...but does it? Is the reactor still connected? - expect(boolean$.connected).toBe(__YOUR_TURN__); + expect(boolean$.connected).toBe(__YOUR_TURN__); // The `b$` it obtains as argument is a `Derivable`. This is a // reference value. Because we apply a negation to this, `b$` is coerced to a @@ -333,13 +297,13 @@ describe.skip('reacting', () => { // `boolean$`. Instead, you can get the value out of the `Derivable` using `.get()`: stopper(); // reset stopper = boolean$.react(reactor, { until: b$ => !b$.get() }); - expect(boolean$.connected).toBe(__YOUR_TURN__); + expect(boolean$.connected).toBe(__YOUR_TURN__); // You can also return the `Derivable` after appling the negation // using the method designed for negating the boolean within a `Derivable`: stopper(); boolean$.react(reactor, { until: b$ => b$.not() }); - expect(boolean$.connected).toBe(__YOUR_TURN__); + expect(boolean$.connected).toBe(__YOUR_TURN__); }); }); @@ -367,7 +331,7 @@ describe.skip('reacting', () => { * * *Hint: remember the `.is()` method from tutorial 2?* */ - sherlock$.react(reactor, __YOUR_TURN__); + sherlock$.react(reactor, __YOUR_TURN__); expectReact(0); ['Elementary,', 'my', 'dear', 'Watson'].forEach(txt => sherlock$.set(txt)); @@ -393,7 +357,7 @@ describe.skip('reacting', () => { * Now, let's react to all even numbers. * Except 4, we don't want to make it too easy now. */ - count$.react(reactor, __YOUR_TURN__); + count$.react(reactor, __YOUR_TURN__); expectReact(1, 0); @@ -420,7 +384,7 @@ describe.skip('reacting', () => { * * Say you want to react when `count$` is larger than 3. But not the first time... */ - count$.react(reactor, __YOUR_TURN__); + count$.react(reactor, __YOUR_TURN__); expectReact(0); @@ -453,7 +417,7 @@ describe.skip('reacting', () => { * * *Hint: you will need to combine `once` with another option* */ - count$.react(reactor, __YOUR_TURN__); + count$.react(reactor, __YOUR_TURN__); expectReact(0); @@ -493,7 +457,7 @@ describe.skip('reacting', () => { // The reactor starts reacting when `myAtom` gets the value 3, but stops when it gets the value 2. // But because `myAtom$` obtains the value 2 before it obtains 3... // ...how many times was the reactor called, if any? - expectReact(__YOUR_TURN__); + expectReact(__YOUR_TURN__); }); it('`when` and `skipFirst`', () => { @@ -504,7 +468,7 @@ describe.skip('reacting', () => { // The reactor reacts when `myAtom$` is 1 but skips the first number. // `myAtom$` starts out at 0. Does the reactor skip only the 0 or also the 1? - expectReact(__YOUR_TURN__); + expectReact(__YOUR_TURN__); }); it('`from`, `until`, `when`, `skipFirst`, and `once`', () => { @@ -525,7 +489,7 @@ describe.skip('reacting', () => { // Meanwhile, `when` allows neither of those values and only allows the values 2, 3, and 4. // `skipFirst` and `once` are also added, just to bring the whole group together. // so, how many times is the reactor called, and what was the last argument (if any)? - expectReact(__YOUR_TURN__); + expectReact(__YOUR_TURN__); }); }); @@ -546,7 +510,7 @@ describe.skip('reacting', () => { * * This should be possible with three simple ReactorOptions. */ - connected$.react(reactor, __YOUR_TURN__); + connected$.react(reactor, __YOUR_TURN__); // It starts as 'disconnected' expectReact(0); diff --git a/tutorial/4 - inner workings.test.ts b/tutorial/4 - inner workings.test.ts index c3d5ecf..426b725 100644 --- a/tutorial/4 - inner workings.test.ts +++ b/tutorial/4 - inner workings.test.ts @@ -91,17 +91,17 @@ describe.skip('inner workings', () => { */ // Well, what do you expect? - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); myDerivation$.get(); // And after a `.get()`? - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); myDerivation$.get(); // And after the second `.get()`? Is there an extra call? - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); /** * The state of any `Derivable` can change at any moment. @@ -140,27 +140,27 @@ describe.skip('inner workings', () => { * * Ok, it's your turn to complete the expectations. */ - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); myDerivation$.get(); - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); myAtom$.set(false); - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); myDerivation$.get(); - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); stopper(); - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); myDerivation$.get(); - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); /** * Since the `.react()` already listens to the value-changes, there is @@ -205,23 +205,23 @@ describe.skip('inner workings', () => { // Note that this is the same value as it was initialized with myAtom$.set(1); - expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); myAtom$.set(2); - expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); myAtom$.set(3); - expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); myAtom$.set(4); - expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(first).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(second).toHaveBeenCalledTimes(__YOUR_TURN__); /** * Can you explain the behavior above? @@ -259,7 +259,7 @@ describe.skip('inner workings', () => { * The `Atom` is set with exactly the same object as before. Will the * `.react()` fire? */ - expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); /** * But what if you use an object, that can be easily compared through a @@ -278,7 +278,7 @@ describe.skip('inner workings', () => { * * Do you think the `.react()` fired with this new value? */ - expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); atom$.set(Seq.Indexed.of(1, 2)); @@ -287,15 +287,16 @@ describe.skip('inner workings', () => { * * And now? */ - expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); /** * In `@skunkteam/sherlock` equality is a bit complex: * - * First we check `Object.is()` equality, if that is true, it is the + * First we check `Object.is()` equality. If that is true, it is the * same, you can't deny that. * - * After that it is pluggable. It can be anything you want. TODO: what is pluggable? + * After that it is pluggable. This means that you can 'plug in' or 'define' + * the definition for equality yourself. * * By default we try to use `.equals()`, to support libraries like * `ImmutableJS`. diff --git a/tutorial/5 - unresolved.test.ts b/tutorial/5 - unresolved.test.ts index 547a272..650c129 100644 --- a/tutorial/5 - unresolved.test.ts +++ b/tutorial/5 - unresolved.test.ts @@ -25,14 +25,14 @@ describe.skip('unresolved', () => { // since it can't be inferred by TypeScript this way. const myAtom$ = atom.unresolved(); - expect(myAtom$.resolved).toEqual(__YOUR_TURN__); + expect(myAtom$.resolved).toEqual(__YOUR_TURN__); /** * ** Your Turn ** * * Resolve the atom, it's pretty easy */ - __YOUR_TURN__; + __YOUR_TURN__; expect(myAtom$.resolved).toBeTrue(); }); @@ -47,7 +47,7 @@ describe.skip('unresolved', () => { * * Time to create an `unresolved` Atom.. */ - const myAtom$: DerivableAtom = __YOUR_TURN__; + const myAtom$: DerivableAtom = __YOUR_TURN__; expect(myAtom$.resolved).toBeFalse(); @@ -62,10 +62,10 @@ describe.skip('unresolved', () => { * * What do you expect? */ - expect(myAtom$.resolved).toEqual(__YOUR_TURN__); + expect(myAtom$.resolved).toEqual(__YOUR_TURN__); // .toThrow() or .not.toThrow()? ↴ - expect(() => myAtom$.get()) /*__YOUR_TURN__*/; + expect(() => myAtom$.get()) /*__YOUR_TURN__*/; }); /** @@ -83,14 +83,14 @@ describe.skip('unresolved', () => { * * What do you expect? */ - expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasReacted).toHaveBeenCalledTimes(__YOUR_TURN__); /** * ** Your Turn ** * * Now make the last expect succeed */ - __YOUR_TURN__; + __YOUR_TURN__; expect(myAtom$.resolved).toBeTrue(); expect(hasReacted).toHaveBeenCalledExactlyOnceWith(`woohoow, I was called`, expect.toBeFunction()); @@ -110,7 +110,7 @@ describe.skip('unresolved', () => { * * Set the value.. */ - __YOUR_TURN__; + __YOUR_TURN__; expect(myAtom$.get()).toEqual(`it's alive!`); @@ -119,7 +119,7 @@ describe.skip('unresolved', () => { * * Unset the value.. (*Hint: TypeScript is your friend*) */ - __YOUR_TURN__; + __YOUR_TURN__; expect(myAtom$.resolved).toBeFalse(); }); @@ -140,14 +140,14 @@ describe.skip('unresolved', () => { * * Combine the two `Atom`s into one `Derivable` */ - const myDerivable$: Derivable = __YOUR_TURN__; + const myDerivable$: Derivable = __YOUR_TURN__; /** * ** Your Turn ** * * Is `myDerivable$` expected to be `resolved`? */ - expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); + expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); // Now let's set one of the two source `Atom`s myString$.set('some'); @@ -161,8 +161,8 @@ describe.skip('unresolved', () => { // And what if we set `myOtherString$`? myOtherString$.set('data'); - expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); - expect(myDerivable$.get()).toEqual(__YOUR_TURN__); + expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); + expect(myDerivable$.get()).toEqual(__YOUR_TURN__); /** * ** Your Turn ** @@ -171,7 +171,7 @@ describe.skip('unresolved', () => { * What do you expect `myDerivable$` to be? */ myString$.unset(); - expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); + expect(myDerivable$.resolved).toEqual(__YOUR_TURN__); }); /** @@ -187,16 +187,16 @@ describe.skip('unresolved', () => { * Use the `.fallbackTo()` method to create a `mySafeAtom$` which * gets the backup value `3` when `myAtom$` becomes unresolved. */ - const mySafeAtom$ = myAtom$.fallbackTo(__YOUR_TURN__); + const mySafeAtom$ = myAtom$.fallbackTo(__YOUR_TURN__); - expect(myAtom$.value).toBe(0); - expect(mySafeAtom$.value).toBe(0); + expect(myAtom$.get()).toBe(0); + expect(mySafeAtom$.get()).toBe(0); myAtom$.unset(); expect(myAtom$.resolved).toBeFalse(); expect(mySafeAtom$.resolved).toBeTrue(); - expect(myAtom$.value).toBeUndefined(); - expect(mySafeAtom$.value).toBe(3); + expect(() => myAtom$.get()).toThrow(); + expect(mySafeAtom$.get()).toBe(3); }); }); diff --git a/tutorial/6 - errors.test.ts b/tutorial/6 - errors.test.ts index fab5d11..f58ae12 100644 --- a/tutorial/6 - errors.test.ts +++ b/tutorial/6 - errors.test.ts @@ -1,4 +1,4 @@ -import { atom, DerivableAtom, error } from '@skunkteam/sherlock'; +import { atom, DerivableAtom } from '@skunkteam/sherlock'; /** * ** Your Turn ** @@ -28,42 +28,21 @@ describe.skip('errors', () => { expect(myAtom$.errored).toBe(true); expect(myAtom$.error).toBe('my Error'); - // expect(myAtom$.get).toThrow("Cannot read properties of undefined (reading 'getState')"); - // TODO: WHAT - normally this works, but internal JEST just fucks with me....? - // What will happen if you try to call `get()` on `myAtom$`? // `.toThrow()` or `.not.toThrow()`? ↴ - expect(() => myAtom$.get()) /* __YOUR_TURN__ */; + // expect(() => myAtom$.get()) /* __YOUR_TURN__ */; // ** __YOUR_TURN__ ** // What will happen if you try to call `set()` on `myAtom$`? // `.toThrow()` or `.not.toThrow()`? ↴ - expect(() => myAtom$.set(2)) /* __YOUR_TURN__ */; - expect(myAtom$.errored).toBe(__YOUR_TURN__); + // expect(() => myAtom$.set(2)) /* __YOUR_TURN__ */; + // expect(myAtom$.errored).toBe(__YOUR_TURN__); // Interestingly, calling `set()` does not throw an error. In fact, it removes the error state // altogether. This means we can now call `get()` again. expect(() => myAtom$.get()).not.toThrow(); }); - /** - * libs/sherlock/src/lib/interfaces.ts:289 shows the basic states that a Derivable can have. - * > `export type State = V | unresolved | ErrorWrapper;` - * A state can be either any type `V` (`number`, `string`, etc.), `unresolved` as we saw in the - * previous tutorial, or `ErrorWrapper`. This last state is explained here. - */ - it('error states', () => { - expect(myAtom$.getState()).toBe(1); // as explained above, any type can be a state - - myAtom$.setError('my Error'); - - // The `ErrorWrapper` state only holds an error string. The `error()` function returns - // such an `ErrorWrapper` which we can use to compare. - expect(myAtom$.getState()).toMatchObject(error('my Error')); - - // TODO: more! There wasn't a question in here. Maybe combine with Final States? NO, that one should go! - }); - it('deriving an error', () => { const myDerivable$ = myAtom$.derive(v => v + 1); @@ -71,7 +50,7 @@ describe.skip('errors', () => { myAtom$.setError('division by zero'); // ...what happens to `myDerivable$`? - expect(myDerivable$.errored).toBe(__YOUR_TURN__); + expect(myDerivable$.errored).toBe(__YOUR_TURN__); // If any Derivable tries to derive from an atom in an error state, // this Derivable will itself throw an error too. This makes sense, @@ -91,11 +70,11 @@ describe.skip('errors', () => { // ** __YOUR_TURN__ ** // Will an error be thrown when `myAtom$` is now set to an error state? // `.toThrow()` or `.not.toThrow()`? ↴ - expect(() => myAtom$.setError('my Error')) /* __YOUR_TURN__ */; + expect(() => myAtom$.setError('my Error')) /* __YOUR_TURN__ */; // ** __YOUR_TURN__ ** // Is the reactor still connected now that it errored? - expect(myAtom$.connected).toBe(__YOUR_TURN__); + expect(myAtom$.connected).toBe(__YOUR_TURN__); // Reacting to a Derivable that throws an error will make the reactor throw as well. // Because the reactor will usually fire when it gets connected, it also throws when @@ -107,11 +86,11 @@ describe.skip('errors', () => { // ** __YOUR_TURN__ ** // Will an error be thrown when you use `skipFirst`? // `.toThrow()` or `.not.toThrow()`? ↴ - expect(() => myAtom$.react(reactor, { skipFirst: true })) /* __YOUR_TURN__ */; + expect(() => myAtom$.react(reactor, { skipFirst: true })) /* __YOUR_TURN__ */; // And will an error be thrown when `from = false`? // `.toThrow()` or `.not.toThrow()`? ↴ - expect(() => myAtom$.react(reactor, { from: false })) /* __YOUR_TURN__ */; + expect(() => myAtom$.react(reactor, { from: false })) /* __YOUR_TURN__ */; // When `from = false`, the reactor is disconnected, preventing the error message from entering. // `skipFirst`, on the other hand, does allow the error in, but does not trigger an update. diff --git a/tutorial/7 - advanced.test.ts b/tutorial/7 - advanced.test.ts index f76f5ec..f71a0d3 100644 --- a/tutorial/7 - advanced.test.ts +++ b/tutorial/7 - advanced.test.ts @@ -1,4 +1,13 @@ -import { atom, constant, Derivable, derive, SettableDerivable, unresolved } from '@skunkteam/sherlock'; +import { + atom, + constant, + Derivable, + derive, + ErrorWrapper, + SettableDerivable, + State, + unresolved, +} from '@skunkteam/sherlock'; import { template } from '@skunkteam/sherlock-utils'; import { Map as ImmutableMap } from 'immutable'; @@ -28,11 +37,11 @@ describe.skip('advanced', () => { * What do you expect this `Derivable` to do on `.set()`, `.get()` etc? */ - // Remove this after taking your turn below. - expect(false).toBe(true); + // Remove this after taking your turn below. + expect(false).toBe(true); // .toThrow() or .not.toThrow()? ↴ (2x) - expect(() => c.get()) /* __YOUR_TURN__ */; - expect(() => c.set('new value')) /* __YOUR_TURN__ */; + expect(() => c.get()) /* __YOUR_TURN__ */; + expect(() => c.set('new value')) /* __YOUR_TURN__ */; }); it('`templates`', () => { @@ -40,7 +49,7 @@ describe.skip('advanced', () => { // we also have a special syntax to copy template literals to a Derivable. const one = 1; const myDerivable = template`I want to go to ${one} party`; - expect(myDerivable.get()).toBe(__YOUR_TURN__) /* __YOUR_TURN__ */; + expect(myDerivable.get()).toBe(__YOUR_TURN__) /* __YOUR_TURN__ */; }); /** @@ -64,13 +73,13 @@ describe.skip('advanced', () => { * * Rewrite the `.get()`/`.set()` combos below using `.swap()`. */ - // Remove this after taking your turn below. - expect(false).toBe(true); + // Remove this after taking your turn below. + expect(false).toBe(true); - myCounter$.set(plusOne(myCounter$.get())); + myCounter$.set(plusOne(myCounter$.get())); expect(myCounter$.get()).toEqual(1); - myCounter$.set(plusOne(myCounter$.get())); + myCounter$.set(plusOne(myCounter$.get())); expect(myCounter$.get()).toEqual(2); }); @@ -87,12 +96,12 @@ describe.skip('advanced', () => { * Use the `.take()` method on `myAtom$` to only accept the input string * when it is `allowed`. */ - const myLimitedAtom$ = myAtom$.take(__YOUR_TURN__); + const myLimitedAtom$ = myAtom$.take(__YOUR_TURN__); expect(myLimitedAtom$.resolved).toBe(false); myAtom$.set('allowed'); expect(myLimitedAtom$.resolved).toBe(true); - expect(myLimitedAtom$.value).toBe('allowed'); + expect(myLimitedAtom$.get()).toBe('allowed'); }); /** @@ -112,13 +121,13 @@ describe.skip('advanced', () => { * * Use the `.value` accessor to get the current value. */ - expect(__YOUR_TURN__).toEqual('foo'); + expect(__YOUR_TURN__).toEqual('foo'); /** * ** Your Turn ** * * Now use the `.value` accessor to set a 'new value'. */ - myAtom$.value = __YOUR_TURN__; + myAtom$.value = __YOUR_TURN__; expect(myAtom$.get()).toEqual('new value'); }); @@ -133,7 +142,7 @@ describe.skip('advanced', () => { /** * ** Your Turn ** */ - expect(myAtom$.value).toEqual(__YOUR_TURN__); + expect(myAtom$.value).toEqual(__YOUR_TURN__); }); /** @@ -184,7 +193,7 @@ describe.skip('advanced', () => { * * Use the `.map()` method to create the expected output below */ - const mappedAtom$: Derivable = __YOUR_TURN__; + const mappedAtom$: Derivable = __YOUR_TURN__; mappedAtom$.react(mapReactSpy); @@ -266,7 +275,7 @@ describe.skip('advanced', () => { // This first function is called when getting... n => -n, // ...and this second function is called when setting. - __YOUR_TURN__, + __YOUR_TURN__, ); // The original `atom` was set to 1, so we want the inverse to @@ -290,96 +299,189 @@ describe.skip('advanced', () => { const myList = [1]; const myMappedList = myList.map(addOne); - expect(myMappedList).toMatchObject([2]); + expect(myMappedList).toMatchObject(__YOUR_TURN__); const myAtom$ = atom(1); let myMappedDerivable$ = myAtom$.map(addOne); - expect(myMappedDerivable$.value).toBe(2); + expect(myMappedDerivable$.value).toBe(__YOUR_TURN__); // You can combine them too. const myAtom2$ = atom([1]); const myMappedDerivable2$ = myAtom2$.map(v => v.map(addOne)); - expect(myMappedDerivable2$.value).toMatchObject([2]); + expect(myMappedDerivable2$.value).toMatchObject(__YOUR_TURN__); }); /** - * In order to reason over the state of a Derivable, we can - * use `.mapState()`. This will map one state to another, and - * can be used to get rid of pesky `unresolved` or `Errorwrapper` - * states. + * Although the `.map()` function can be reversed, the intended flow of the + * function is still meant to go the original non-reversed way. This means that, + * if the reverse flow is used, the non-reverse flow is also activated. We will + * show what that means. */ - it('`.mapState()`', () => { + it('one-way flow', () => { const myAtom$ = atom(1); - // like `.map()`, we can specify it both ways. - const myMappedAtom$ = myAtom$.mapState( - state => (state === unresolved ? 3 : state), // `myAtom$` => `myMappedAtom$` - state => (state === 2 ? unresolved : state), // `myMappedAtom$` => `myAtom$` + const myMappedAtom$ = myAtom$.map( + n => n + 1, + n => n * 2, ); - myAtom$.set(2); - expect(myAtom$.resolved).toBe(__YOUR_TURN__); - expect(myMappedAtom$.resolved).toBe(__YOUR_TURN__); + // This may seem logical... + myAtom$.set(5); + expect(myAtom$.value).toBe(__YOUR_TURN__); + expect(myMappedAtom$.value).toBe(__YOUR_TURN__); - myAtom$.unset(); - expect(myAtom$.resolved).toBe(__YOUR_TURN__); - expect(myMappedAtom$.resolved).toBe(__YOUR_TURN__); + // ...but this may seem weird. + myMappedAtom$.set(5); + expect(myAtom$.value).toBe(__YOUR_TURN__); + expect(myMappedAtom$.value).toBe(__YOUR_TURN__); - myMappedAtom$.set(2); - expect(myAtom$.resolved).toBe(__YOUR_TURN__); - expect(myMappedAtom$.resolved).toBe(__YOUR_TURN__); + /** + * `.map()` is intended to use one-way, from `myAtom$` to `myMappedAtom$`. + * The reverse direction or 'flow', of setting `myMappedAtom$` and mapping it to `myAtom$` is not + * the intended flow, and is used only as a shortcut to alter `myAtom$`. However, if you do this, + * `myAtom$` will notice that it is changed and thus will trigger another call of `.map()`, now + * from `myAtom$` to `myMappedAtom$`! Thus, `myMappedAtom$` is changed again. + * + * Although this behavior is intended, it may give seemingly weird situations like this where + * you set `myMappedAtom$` to the value 5, yet it "suddenly" has value 11. + * + * Also note that removing the second case of `.map()`, so for the reverse direction, will actually have effects + * on the typing of `myMappedAtom$`: it will become a `Derivable` instead of a `DerivableAtom`, + * which also means it does not have a `.set()` method anymore. Try it out by commenting out the second line of `.map()`! + */ + }); - // This is a tricky one: - myMappedAtom$.unset(); - expect(myAtom$.resolved).toBe(__YOUR_TURN__); - expect(myMappedAtom$.resolved).toBe(__YOUR_TURN__); + it('`.flatMap()`', () => { + const myAtom$ = atom(0); + const atomize = jest.fn((n: number) => atom(n)); // turn a number into an atom. + /** + * Sometimes you use `.map()`, but the result of the function within the `.map()` is also a Derivable. + * The result would be a `Derivable>` (like the return type of `.map()` below: hover over it to see) + */ + myAtom$.map(atomize); /** - * The results, especially of the last case, may seem weird. - * In the first exercise, `myAtom$` is set to 2, causing the state to be 2 as well. - * By setting the state of `myAtom$`, the first line of `mapState()` is triggered. - * Since `2` is not equal to `unresolved`, we return the state `2`, causing - * `myMappedAtom$` to also get state 2 (and thus: value 2). Neither are unresolved. + * You would have to use `.get()` to go back to a single Derivable. Similarly how `flatMap` can + * reduce lists of lists to a single list, it can help reduce Derivables of Derivables to a + * single Derivable. * - * In the second case, `myAtom$` is set to `unresolved`, triggering the first line of - * `mapState()`, letting `myMappedAtom$` become 3. `myAtom$` is now `unresolved`, and - * `myMappedAtom$` is not. + * ** Your Turn ** * - * In the third case, `myMappedAtom$` is set to 2, it triggers the second line of - * `mapState()`, causing `myAtom$` to become `unresolved`. However, what we don't - * notice is that this change in state triggers the first line of `mapState()` again, - * causing `myMappedAtom$` to get state `3`. We can check this: + * Rewrite the first line using `.flatMap()`. */ + let myMappedAtom$ = myAtom$.map(atomize).derive(v => v.get()); // the `derive()` uses `get()` to remove one layer of `Derivable` + myMappedAtom$ = __YOUR_TURN__ as Derivable; + + myAtom$.set(1); + expect(myMappedAtom$.get()).toBe(1); + expect(atomize).toHaveBeenCalledTimes(1); + + // `.flatMap()`, like `.map()`, is a common functionality of standard libraries and can be used on e.g. arrays. + const myList = [1, 2, 3]; + const myMappedList = myList.map(v => [v, v + 1]).flat(); + const myFlatMappedList = __YOUR_TURN__; + expect(myMappedList).toEqual(myFlatMappedList); + }); + }); - myMappedAtom$.set(2); - expect(myMappedAtom$.get()).toBe(3); // the state and value are linked, so this is identical to `.getState()` + /** + * Every Derivable also contains a `State`. This state contains all the information of a Derivable in one place, + * such as whether it is a value, unresolved, or an error. + */ + describe.skip('States', () => { + /** + * libs/sherlock/src/lib/interfaces.ts:289 shows all that a State can be. + * ``` + * export type State = V | unresolved | ErrorWrapper; + * ``` + */ + it('value states, unresolved states, and error states', () => { + const myAtom$ = atom(1); /** - * You might think that this change in state would cause `myAtom$` to now also get - * `3` as its state, but this does not happen. Why not? TODO: maximally one cycle? ASK! - * Since both `2` and `3` are not `unresolved`, it does not matter to our answer. + * ** Your Turn ** * - * The same cannot be said for the fourth case. Setting `myMappedAtom$` to `unresolved` - * triggers the second line of `mapState()`, causing `myAtom$` to also become `unresolved`. This, in turn, - * triggers the first line of `mapState()`, causing `myMappedAtom$` to become `3`. - * As such, `myMappedAtom$` is not `unresolved` even though we set it as such. - * TODO: change this to be for MAP. Then make MAPSTATE a trivial one right after. + * What do you expect the state to be? + */ + expect(myAtom$.getState()).toBe(__YOUR_TURN__); + + /** + * We cannot directly set the state of `myAtom$` as there is no `setState()` function, + * but it will change automatically when we change the value of `myAtom$`. */ + myAtom$.unset(); + + /** + * ** Your Turn ** + * + * What do you expect the state to be? + */ + expect(myAtom$.getState()).toBe(__YOUR_TURN__); + + myAtom$.setError('my Error'); + + /** + * ** Your Turn ** + * + * What do you expect the state to be? + */ + expect(myAtom$.getState()).toBeInstanceOf(__YOUR_TURN__); + + /** + * Here is an example of when a state can be useful. Using the concept of type 'narrowing', we can check + * on type-level what state the atom is in and we can vary our return value accordingly. + * Study the following function and then fill in the expectations below. + */ + function stateToString(state: State): string { + if (state instanceof ErrorWrapper) { + // We know `state` is of type 'ErrorWrapper', which allows us to grab a property such as 'error' from it. + // Note that our `error` is `ErrorWrapper.error`, not `Derivable.error` such as when using `myAtom$.error`. + return state.error as string; + } else if (typeof state === 'number') { + // We know `state` is of type 'number', so we can apply numerical functions to it. + return String(state + 1); + } else { + // We know `state` must now be of type 'unresolved'. + return state.toString(); + } + } + + myAtom$.set(1); + expect(stateToString(myAtom$.getState())).toBe(__YOUR_TURN__); + + myAtom$.unset(); + expect(stateToString(myAtom$.getState())).toBe(__YOUR_TURN__); + + myAtom$.setError('OH NO!'); + expect(stateToString(myAtom$.getState())).toBe(__YOUR_TURN__); }); - // FIXME: - it('TEMP Flat-map', () => { - // const myAtom$ = atom(0); - // const mapping = (v: any) => atom(v); - // Sometimes you use `map()`, but the result within the `map()` is also a Derivable. - // The result would here be a `Derivable>` (hover over `derive` to see this). - // You would have to use `.get()` to go back to a single Derivable. Similarly how `flatMap` can - // reduce lists of lists to a single list, it can help reduce Derivables of Derivables to a - // single Derivable. If you have something like this: - // let myAtom$$ = myAtom$.map(n => mapping(n)).derive(v => v.get()); - // You can now rewrite it to this: - // myAtom$$ = myAtom$.flatMap(n => mapping(n)); - // It only results in slightly shorter code. - // TODO: right? + /** + * In order to reason over the state of a Derivable, we can + * use `.mapState()`. This will map one state to another, and + * can be used to get rid of pesky `unresolved` or `Errorwrapper` + * states. + */ + it('`.mapState()`', () => { + const myAtom$ = atom(1); + + const myMappedAtom$ = myAtom$.mapState( + state => (state === unresolved || state instanceof ErrorWrapper ? 0 : state), // `myAtom$` => `myMappedAtom$` + state => state, // `myMappedAtom$` => `myAtom$` + ); + + myAtom$.set(2); + expect(myAtom$.value).toBe(__YOUR_TURN__); + expect(myMappedAtom$.value).toBe(__YOUR_TURN__); + + myAtom$.unset(); + expect(myAtom$.value).toBe(__YOUR_TURN__); + expect(myMappedAtom$.value).toBe(__YOUR_TURN__); + + // This is a tricky one. Remember the intended flow of the `map()` function, + // as it is the same for the `mapState()` function. + myMappedAtom$.unset(); + expect(myAtom$.value).toBe(__YOUR_TURN__); + expect(myMappedAtom$.value).toBe(__YOUR_TURN__); }); }); @@ -389,7 +491,7 @@ describe.skip('advanced', () => { * `Derivable`, one of those values can be plucked into a new `Derivable`. * This plucked `Derivable` can be settable, if the source supports it. * - * The way properties are plucked is pluggable, but by default both // TODO: no-one here knows what "pluggable" is. Or ImmutableJS. + * The way properties are plucked is 'pluggable' (customizable), but by default both * `.get()` and `[]` are supported to support * basic Objects, Maps and Arrays. * @@ -397,7 +499,7 @@ describe.skip('advanced', () => { * does not. This means that setting a plucked property of a regular * Object/Array/Map will not cause any reaction on that source `Derivable`. * - * ImmutableJS can help fix this problem* + * ImmutableJS can help fix this problem. */ describe.skip('`.pluck()`', () => { const reactSpy = jest.fn(); @@ -422,7 +524,7 @@ describe.skip('advanced', () => { * * * Hint: you'll have to cast the result from `.pluck()`. */ - firstProp$ = __YOUR_TURN__; + firstProp$ = __YOUR_TURN__; }); /** @@ -438,18 +540,18 @@ describe.skip('advanced', () => { * What do you expect the plucked `Derivable` to look like? And what * happens when we `.set()` it? */ - expect(firstProp$.get()).toEqual(__YOUR_TURN__); + expect(firstProp$.get()).toEqual(__YOUR_TURN__); // the plucked `Derivable` should be settable firstProp$.set('other value'); // is the `Derivable` value the same as was set? - expect(firstProp$.get()).toEqual(__YOUR_TURN__); + expect(firstProp$.get()).toEqual(__YOUR_TURN__); // How many times was the spy called? Note the `skipFirst`.. - expect(reactPropSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reactPropSpy).toHaveBeenCalledTimes(__YOUR_TURN__); // ...and what was the value? - expect(reactPropSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + expect(reactPropSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); }); /** @@ -471,7 +573,7 @@ describe.skip('advanced', () => { myMap$.swap(map => map.set('secondProp', 'new value')); // How many times was the spy called? Note the `skipFirst`. - expect(reactPropSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reactPropSpy).toHaveBeenCalledTimes(__YOUR_TURN__); /** * ** Your Turn ** @@ -481,10 +583,10 @@ describe.skip('advanced', () => { myMap$.swap(map => map.set('firstProp', 'new value')); // How many times was the spy called? Note the `skipFirst`.. - expect(reactPropSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reactPropSpy).toHaveBeenCalledTimes(__YOUR_TURN__); // ...and what was the value? - expect(reactPropSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + expect(reactPropSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); }); /** diff --git a/tutorial/8 - utils.test.ts b/tutorial/8 - utils.test.ts index 387e550..c4463bc 100644 --- a/tutorial/8 - utils.test.ts +++ b/tutorial/8 - utils.test.ts @@ -1,5 +1,16 @@ -import { atom, constant, derive, FinalWrapper } from '@skunkteam/sherlock'; -import { fromPromise, lift, pairwise, peek, scan, struct } from '@skunkteam/sherlock-utils'; +import { atom, constant, Derivable, derive, ErrorWrapper, FinalWrapper } from '@skunkteam/sherlock'; +import { + fromEventPattern, + fromObservable, + fromPromise, + lift, + pairwise, + peek, + scan, + struct, +} from '@skunkteam/sherlock-utils'; +import { Atom } from 'libs/sherlock/src/internal'; +import { from, Observable, Subject } from 'rxjs'; /** * ** Your Turn ** @@ -13,7 +24,11 @@ expect(scan).toBe(scan); expect(struct).toBe(struct); expect(peek).toBe(peek); expect(lift).toBe(lift); -expect(FinalWrapper).toBe(FinalWrapper); // TODO: not sure whether needed +expect(fromObservable).toBe(fromObservable); +expect(from).toBe(from); +expect(ErrorWrapper).toBe(ErrorWrapper); +expect(Observable).toBe(Observable); +expect(FinalWrapper).toBe(FinalWrapper); /** * In the `sherlock-utils` lib, there are a couple of functions that can combine * multiple values of a single `Derivable` or combine multiple `Derivable`s into @@ -42,7 +57,7 @@ describe.skip('utils', () => { * * Note: don't call `pairwise()` using a lambda function! */ - myCounter$.derive(__YOUR_TURN__).react(reactSpy); + myCounter$.derive(__YOUR_TURN__).react(reactSpy); expect(reactSpy).toHaveBeenCalledTimes(1); expect(reactSpy).toHaveBeenLastCalledWith(1, expect.toBeFunction()); @@ -62,7 +77,7 @@ describe.skip('utils', () => { // ** Your Turn ** // What will the next output be? expect(reactSpy).toHaveBeenCalledTimes(4); - expect(reactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + expect(reactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); }); /** @@ -91,7 +106,7 @@ describe.skip('utils', () => { * * Note: don't call `pairwise()` using a lambda function! */ - myCounter$.derive(__YOUR_TURN__).react(reactSpy); + myCounter$.derive(__YOUR_TURN__).react(reactSpy); expect(reactSpy).toHaveBeenCalledTimes(1); expect(reactSpy).toHaveBeenLastCalledWith(1, expect.toBeFunction()); @@ -111,7 +126,7 @@ describe.skip('utils', () => { // ** Your Turn ** // What will the next output be? expect(reactSpy).toHaveBeenCalledTimes(4); - expect(reactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); + expect(reactSpy).toHaveBeenLastCalledWith(__YOUR_TURN__, expect.toBeFunction()); }); it('`pairwise()` on normal arrays', () => { @@ -128,16 +143,16 @@ describe.skip('utils', () => { * * Note: don't call `pairwise()` using a lambda function! */ - myList2 = myList.map(__YOUR_TURN__); + myList2 = myList.map(__YOUR_TURN__); expect(myList2).toMatchObject([1, 1, 1, 2, 5]); // However, we should be careful with this, as this does not always behave as intended. // Particularly, what exactly happens when we do call `pairwise()` using a lambda function? - myList2 = myList.map(v => __YOUR_TURN__(v)); // copy the same implementation here + myList2 = myList.map(v => __YOUR_TURN__(v)); // copy the same implementation here expect(myList2).toMatchObject([1, 2, 3, 5, 10]); // Even if we are more clear about what we pass, this unintended behavior does not go away. - myList2 = myList.map((v, _, _2) => __YOUR_TURN__(v)); // copy the same implementation here + myList2 = myList.map((v, _, _2) => __YOUR_TURN__(v)); // copy the same implementation here expect(myList2).toMatchObject([1, 2, 3, 5, 10]); // `pairwise()` keeps track of the previous value under the hood. Using a lambda of @@ -147,14 +162,14 @@ describe.skip('utils', () => { // Other than by not using a lambda function, we can fix this by // saving the `pairwise` in a variable and reusing it for every call. - let f: (v: number) => number = __YOUR_TURN__; // copy the same implementation here + let f: (v: number) => number = __YOUR_TURN__; // copy the same implementation here myList2 = myList.map(v => f(v)); expect(myList2).toMatchObject([1, 1, 1, 2, 5]); // To get more insight in the `pairwise()` function, you can call it // manually. Here, we show what happens under the hood. - f = pairwise(__YOUR_TURN__); // copy the same implementation here + f = pairwise(__YOUR_TURN__); // copy the same implementation here myList2 = []; myList2[0] = f(myList[0]); // `f` is newly created with `init = 0`, so applies `1 - 0 = 1`. @@ -173,8 +188,8 @@ describe.skip('utils', () => { * Note that the function `f` still requires a number to be the return value. * Checking for equality therefore cannot be done directly within `f`. */ - f = __YOUR_TURN__; - myList2 = myList.filter(__YOUR_TURN__); + f = __YOUR_TURN__; + myList2 = myList.filter(__YOUR_TURN__); expect(myList2).toMatchObject([1, 2, 3]); // only the numbers `1`, `2`, and `3` produce 1 when subtracted with the previous value }); @@ -190,7 +205,7 @@ describe.skip('utils', () => { * Use a `scan()` combined with a `.map()` on `myList` * to subtract the previous value from the current. */ - let f: (v: number) => number = scan(__YOUR_TURN__); + let f: (v: number) => number = scan(__YOUR_TURN__); myList2 = myList.map(f); expect(myList2).toMatchObject([1, 1, 2, 3, 7]); @@ -217,8 +232,8 @@ describe.skip('utils', () => { * (1+2+3+5+10), and since this sum only prouces a value higher than 8 when the * values `5` and `10` are added, the result should be `[5,10]`. */ - f = scan(__YOUR_TURN__); - myList2 = myList.filter(__YOUR_TURN__); + f = scan(__YOUR_TURN__); + myList2 = myList.filter(__YOUR_TURN__); expect(myList2).toMatchObject([5, 10]); }); @@ -234,7 +249,7 @@ describe.skip('utils', () => { * Now, use `pairwise()` directly in `.react()`. Implement the same * derivation as before: subtract the previous value from the current. */ - reactSpy = jest.fn(__YOUR_TURN__); + reactSpy = jest.fn(__YOUR_TURN__); myCounter$.react(reactSpy); expect(reactSpy).toHaveLastReturnedWith(1); @@ -260,7 +275,7 @@ describe.skip('utils', () => { * derivation as before: subtract all the emitted values. */ - reactSpy = jest.fn(__YOUR_TURN__); + reactSpy = jest.fn(__YOUR_TURN__); myCounter$.react(reactSpy); expect(reactSpy).toHaveLastReturnedWith(1); @@ -347,7 +362,7 @@ describe.skip('utils', () => { * In other words: the new function should take a `Derivable` (or more specifically: * an `Unwrappable`) and return a `Derivable`. */ - const isEvenDerivable = __YOUR_TURN__; + const isEvenDerivable = __YOUR_TURN__; expect(isEvenNumber(2)).toBe(true); expect(isEvenNumber(13)).toBe(false); @@ -368,7 +383,7 @@ describe.skip('utils', () => { * ** Your Turn ** * Now, use `lift()` as alternative to `.map()`. */ - myMappedDerivable$ = __YOUR_TURN__; + myMappedDerivable$ = __YOUR_TURN__; expect(myMappedDerivable$.value).toBe(2); }); @@ -388,7 +403,7 @@ describe.skip('utils', () => { * value of `myTrackedAtom$`, which should be tracked. */ const reactor = jest.fn(v => v); - derive(__YOUR_TURN__).react(reactor); + derive(__YOUR_TURN__).react(reactor); expect(reactor).toHaveBeenCalledOnce(); expect(reactor).toHaveLastReturnedWith(3); @@ -427,8 +442,6 @@ describe.skip('utils', () => { // Every atom has a `final` property. expect(myAtom$.final).toBeFalse(); - // TODO: SHOW THAT CONST ALSO GIVES THE SAME ERROR MESSAGE WHEN SET!! - // You can make an atom final using the `.makeFinal()` function. myAtom$.makeFinal(); expect(myAtom$.final).toBeTrue(); @@ -438,13 +451,14 @@ describe.skip('utils', () => { * What do you think will happen when we try to `.get()` or `.set()` this atom? */ // .toThrow() or .not.toThrow()? ↴ - expect(() => myAtom$.get()) /*__YOUR_TURN__*/; - expect(() => myAtom$.set(2)) /*__YOUR_TURN__*/; + expect(() => myAtom$.get()) /*__YOUR_TURN__*/; + expect(() => myAtom$.set(2)) /*__YOUR_TURN__*/; // This behavior is consistent with normal variables created using `const`. + // Alternatively, you can set a last value before setting it to `final`, using `.setFinal()`. // .toThrow() or .not.toThrow()? ↴ - expect(() => myAtom$.setFinal(2)) /*__YOUR_TURN__*/; + expect(() => myAtom$.setFinal(2)) /*__YOUR_TURN__*/; // There is no way to 'unfinalize' a Derivable, so the only solution to reset is to // create a whole new Derivable. @@ -456,7 +470,7 @@ describe.skip('utils', () => { // `final` in disguise. You can verify this by checking the implementation of `constant` at // libs/sherlock/src/lib/derivable/factories.ts:39 const myConstantAtom$ = constant(1); - expect(myConstantAtom$.final).toBe(__YOUR_TURN__); + expect(myConstantAtom$.final).toBe(__YOUR_TURN__); }); it('deriving a `final` Derivable', () => { @@ -475,8 +489,8 @@ describe.skip('utils', () => { * * What will happen to `myDerivable$` when I change `myAtom$` to be `final`? */ - expect(myDerivable$.final).toBe(__YOUR_TURN__); - expect(myDerivable$.connected).toBe(__YOUR_TURN__); + expect(myDerivable$.final).toBe(__YOUR_TURN__); + expect(myDerivable$.connected).toBe(__YOUR_TURN__); /** * Derivables that are final (or constant) are no longer tracked. This can save @@ -486,28 +500,22 @@ describe.skip('utils', () => { */ }); - it('TODO: `final` State', () => { - /** A property such as `.final`, similar to variables like `.errored` and `.resolved` - * is useful for checking whenever a Derivable is in a certain state, but these properties - * are just a boolean. This means that these properties cannot be derived and we cannot - * have certain functions execute whenever there is a change in the state. For this reason, - * every Derivable holds an internal state, retrievable using `.getState()` which can be - * derived. TODO: Have a clear place where I explain this! Now I have info up top here too. - * - * We have seen that states (`State`) can be `undefined`, `ErrorWrapper`, - * or any regular type `V`. Other states exist, such as the `MaybeFinalState`. This state can be either - * a normal state `State` or a special `FinalWrapper>` state. Let's see that in action. + it('`final` State', () => { + /** + * We have seen that states (`State`) can be `unresolved`, `ErrorWrapper`, + * or any regular type `V`. If you want to also show whether a Derivable is `final`, you can + * use the `MaybeFinalState`, which is either any normal `State` or a special + * `FinalWrapper>` state. Let's see that in action. */ - expect(myAtom$.getMaybeFinalState()).toBe(1); // `getMaybeFinalState` can return a normal state, which in turn can be any normal type. + myAtom$.set(2); + expect(myAtom$.getMaybeFinalState()).toBe(__YOUR_TURN__); - myAtom$.makeFinal(); + myAtom$.setError('2'); + expect(myAtom$.getMaybeFinalState()).toBeInstanceOf(__YOUR_TURN__); - expect(myAtom$.getMaybeFinalState()).toBeInstanceOf(FinalWrapper); // but `getMaybeFinalState` can also return a `FinalWrapper` type. - expect(myAtom$.getState()).toBe(1); // the normal type is still the final it contains. - - // TODO: MAAR JE KAN EEN STATE HELEMAAL NIET DERIVEN! - // Dus dat is allemaal onzin lijkt me....??? Bovendien, kan je normale variabelen niet deriven door het gewoon te - // wrappen in een atom ofzo? Of door te structen? + myAtom$.setFinal(2); + expect(myAtom$.getMaybeFinalState()).toBeInstanceOf(__YOUR_TURN__); + expect(myAtom$.getState()).toBe(__YOUR_TURN__); }); }); @@ -528,8 +536,8 @@ describe.skip('utils', () => { * ** Your Turn ** * What do you think is the default state of an atom based on a Promise? */ - expect(myAtom$.value).toBe(__YOUR_TURN__); - expect(myAtom$.final).toBe(__YOUR_TURN__); + expect(myAtom$.value).toBe(__YOUR_TURN__); + expect(myAtom$.final).toBe(__YOUR_TURN__); // Now we wait for the Promise to be handled (resolved). await promise; @@ -538,8 +546,8 @@ describe.skip('utils', () => { * ** Your Turn ** * So, what will happen to `myAtom$`? */ - expect(myAtom$.value).toBe(__YOUR_TURN__); - expect(myAtom$.final).toBe(__YOUR_TURN__); + expect(myAtom$.value).toBe(__YOUR_TURN__); + expect(myAtom$.final).toBe(__YOUR_TURN__); // Now we make a promise that is rejected when called. promise = Promise.reject('Oh no, I messed up!'); @@ -552,9 +560,9 @@ describe.skip('utils', () => { * ** Your Turn ** * So, what will happen to `myAtom$` now? */ - expect(myAtom$.errored).toBe(__YOUR_TURN__); - expect(myAtom$.error).toBe(__YOUR_TURN__); - expect(myAtom$.final).toBe(__YOUR_TURN__); + expect(myAtom$.errored).toBe(__YOUR_TURN__); + expect(myAtom$.error).toBe(__YOUR_TURN__); + expect(myAtom$.final).toBe(__YOUR_TURN__); }); it('`.toPromise()`', async () => { @@ -572,8 +580,8 @@ describe.skip('utils', () => { */ myAtom$.set('second value'); // `.resolves` or `.rejects`? ↴ - await expect(promise) /*__YOUR_TURN__*/ - .toBe(__YOUR_TURN__); + await expect(promise) /*__YOUR_TURN__*/ + .toBe(__YOUR_TURN__); myAtom$.unset(); // reset @@ -585,8 +593,8 @@ describe.skip('utils', () => { */ myAtom$.set('third value'); // `.resolves` or `.rejects`? ↴ - await expect(promise) /*__YOUR_TURN__*/ - .toBe(__YOUR_TURN__); + await expect(promise) /*__YOUR_TURN__*/ + .toBe(__YOUR_TURN__); // Whenever an atom is in an `unresolved` state, the corresponding Promise is pending. // This means that the Promise can still become resolved or rejected depending on the atom's actions. @@ -605,7 +613,7 @@ describe.skip('utils', () => { await promise; } catch (error: any) { // `.toBe('Error.')` or `.not.toBe('Error.')`? ↴ - expect(error.message) /*__YOUR_TURN__*/; + expect(error.message) /*__YOUR_TURN__*/; } myAtom$.set('no more error'); @@ -623,18 +631,189 @@ describe.skip('utils', () => { await promise; } catch (error: any) { // `.toBe('Error.')` or `.not.toBe('Error.')`? ↴ - expect(error.message) /*__YOUR_TURN__*/; + expect(error.message) /*__YOUR_TURN__*/; } }); + /** + * Some reactive libraries already existed, such as RxJS. + * Angular uses RxJS, and since we use Angular, we are forced to use RxJS. + * However, RxJS becomes more and more complicated and user-unfriendly + * as your application becomes bigger. This was the main reason why + * Sherlock was developed. + * As Angular uses RxJS, our Sherlock library needs to be compatible with it. + * The `fromObservable()` and `toObservable()` functions are used for this. + */ it('`fromObservable()`', () => { - // Has to do with SUBSCRIBING. Hasn't been discussed either... - // TODO: "As all Derivables are now compatible with rxjs's `from` function, - // we no longer need the `toObservable` function from `@skunkteam/sherlock-rxjs`." + /** + * RxJS uses `Observables` which are similar to our `Derivables`. + * It also uses the concept of `Subscribing`, which is similar to `Deriving`. + * + * Here's an example of the similarities and differences. + */ + + const dummyObservable = new Subject(); // `Subject` is a form of `Observable` + let subscribedToDummy; + dummyObservable.subscribe({ next: x => (subscribedToDummy = x) }); + dummyObservable.next(2); + expect(subscribedToDummy).toBe(2); + + const dummyDerivable$ = new Atom(1); + const derivedOfDummy = dummyDerivable$.derive(x => x); + dummyDerivable$.set(2); + expect(derivedOfDummy.value).toBe(2); + + /** + * The code for turning an Observable "observable" into a derivable "value$" is + * like this (from libs/sherlock-utils/src/lib/from-observable.ts): + * + * ``` + * observable.subscribe({ + * next: value => value$.set(value), + * error: err => value$.setFinal(error(err)), + * complete: () => value$.makeFinal(), + * }); + * return () => subscription.unsubscribe(); + * ``` + * + * Essentially, + * - we map `next()` (Observable) to `set()` (Derivable); + * - we map `error()` (Observable) to `setFinal(error())` (Derivable); + * NOTE: we don't map it to `setError()` as we would then be able to undo the error state. We can't undo it when it's final. + * - we map`complete()` (Observable) to `makeFinal()` (Derivable); + * - and we return a function we can call to stop (similar to using `react()`), which is mapped to `unsubscribe()` + * NOTE: in fact, `fromEventPattern()` is build using the `react()` function! + * + * Okay, that's enough info for now. Let's get to work. + * The `fromObservable()` function translates an `Observable` to a `Derivable`. + * + * ** Your Turn ** + * + * Use `fromObservable()` to turn this `Observable` into a `Derivable`. + */ + + // A `Subject` is the simplest form of `Observable`: it is comparable to our `Atom`. + const myObservable = new Subject(); + + const myDerivable$: Derivable = __YOUR_TURN__; + const reactor = jest.fn(); + const onError = jest.fn(); + myDerivable$.react(reactor, { onError }); + + myObservable.next(1); + expect(myDerivable$.value).toBe(1); + + myObservable.next(2); + expect(myDerivable$.value).toBe(2); + + myObservable.error('OH NO!'); + expect(myDerivable$.error).toBe('OH NO!'); + + myObservable.next(3); + expect(myDerivable$.value).toBe(undefined); + // It is set to final, so after an error has been thrown, you cannot undo it. + + /** + * The `toObservable()` function has become obsolete as RxJS already contains a function + * called `from()` that can parse `Derivables` to `Observables`. Note that a `from` from + * the side of RxJS is the same as a `to` from the side of Sherlock. + * + * ** Your Turn ** + * + * Use the `from()` function to turn this `Derivable` into an `Observable`. + */ + const myDerivable2$ = atom(1); + + const myObservable2: Observable = __YOUR_TURN__; + + let value = 0; + myObservable2.subscribe({ next: x => (value = x) }); + + expect(value).toBe(1); // immediate call to `next()` + + myDerivable2$.set(2); + expect(value).toBe(2); }); - it('`fromEventPattern`', () => { - // TODO: this is kinda complicated shit... Requires explaining a lot of extra stuff (Subjects, Subscribing, Observables...). Leave for now? + /** + * The `fromObservable()` function can be used to turn `Observables` into `Derivables`. Under the hood, + * this function uses the `fromEventPattern()` function which is capable of turning any abstract pattern + * into a `Derivable`. Let's see how that works. + */ + it('`fromEventPattern`', async () => { + /** + * The basic idea is that you get a stream of inputs, and want to map that stream to a Derivable stream. + * This means that, whenever an update comes from the input-stream, this update is also given to a Derivable, + * which can then be `derive`d and `react`ed to. + * + * For example, you may get a function which, instead of returning an output, passes some output to your own chosen + * `callback` function. For example, this code 'heyifies' your input, adding "hey" in front of it. + * It then passes this heyified output to the callback function. As seen before in `react()` and `fromObservable()`, + * these functions like `heyify()` typically return a stopping function, which can be called to stop the heyification. + */ + function heyify(names: string[], callback: (something: string) => void) { + let i = 0; + // every 100ms, call the callback function + const int = setInterval(() => callback(`Hey ${names[i++]}`), 100); + // when this function is called, `clearInterval()` stops the stream. + return () => clearInterval(int); + } + + /** + * Now, we want to turn this process into a Derivable, where updates that are send to the callback are passed to the Derivable as well. + * This way, we can use our cool Derivable functions like `derive()` or `react()` to process changes to this Derivable + * (= new outputs from the callback). + * + * `fromEventPattern()` looks more complex than it is. This function sets a Derivable in place of the callback and also returns + * a stopping function, which can be reused from the `heyify()` function. + */ + function heyify$(names: string[]): Derivable { + return fromEventPattern(v$ => { + // the callback now sets a derivable. + const stop = heyify(names, something => v$.set(something)); + // and the stopping function is returned. + return stop; + }); + } + + /** + * This Derivable can now be reacted to. + * When this Derivable gets connected (reacted to, in this case), the function within the `fromEventPattern()` triggers. + * Upon connection, a fresh atom is passed to this function, which is then `set()` in the callback function of `heyify()`. + * This atom "lives" in the body of the callback function and is only changed when a new value is passed to the callback function. + * Here, this is a new "Hey {name}" message, every 100ms. + */ + + // To test this, we need to make sure time elapses only when we want it to. So we temporarily stop time, no big deal. + // (Don't worry about what the internet says about the dangers of stopping time: this is perfectly safe.) + jest.useFakeTimers(); + + let value: string = ''; + const stop = heyify$(['Bob', 'Jan', 'Hans', 'Roos']).react(v => (value = v)); + + /** + * ** Your Turn ** + * + * What do you expect `value` to be? + * *Hint: At the start, time has not yet passed, and `setInterval()` only responds after the first 100ms.* + */ + expect(value).toBe(__YOUR_TURN__); + + // We manually move time by 100ms, which is exactly the time that the `heyify()` function needs to call the `callback()` again. + jest.advanceTimersByTime(100); + expect(value).toBe(__YOUR_TURN__); + + jest.advanceTimersByTime(100); + expect(value).toBe(__YOUR_TURN__); + + jest.advanceTimersByTime(100); + expect(value).toBe(__YOUR_TURN__); + + stop(); + + // After stopping, the Derivable no longer responds to updates - it is essentially final. + jest.advanceTimersByTime(100); + expect(value).toBe(__YOUR_TURN__); }); }); }); diff --git a/tutorial/9 - expert.test.ts b/tutorial/9 - expert.test.ts index 7da22f1..969b330 100644 --- a/tutorial/9 - expert.test.ts +++ b/tutorial/9 - expert.test.ts @@ -1,4 +1,4 @@ -import { DerivableAtom, atom, derive } from '@skunkteam/sherlock'; +import { DerivableAtom, atom, derive, unwrap } from '@skunkteam/sherlock'; import { derivableCache } from '@skunkteam/sherlock-utils'; /** @@ -6,6 +6,9 @@ import { derivableCache } from '@skunkteam/sherlock-utils'; * If you see this variable, you should do something about it. :-) */ export const __YOUR_TURN__ = {} as any; + +// Silence TypeScript's import not used errors. +expect(unwrap).toBe(unwrap); describe.skip('expert', () => { describe.skip('`.autoCache()`', () => { /** @@ -29,7 +32,7 @@ describe.skip('expert', () => { * called at this point? */ // `.toHaveBeenCalled()` or `.not.toHaveBeenCalled()`? ↴ - expect(hasDerived) /* Your Turn */; + expect(hasDerived) /* Your Turn */; mySecondDerivation$.get(); @@ -41,7 +44,7 @@ describe.skip('expert', () => { * first `Derivable` actually executed its derivation? */ // how many times? - expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(hasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); }); /** @@ -65,7 +68,7 @@ describe.skip('expert', () => { * expectations pass. */ const myAtom$ = atom(true); - const myFirstDerivation$ = myAtom$.derive(firstHasDerived); + const myFirstDerivation$ = myAtom$.derive(firstHasDerived); const mySecondDerivation$ = myFirstDerivation$.derive(() => secondHasDerived(myFirstDerivation$.get() + myFirstDerivation$.get()), ); @@ -107,9 +110,9 @@ describe.skip('expert', () => { mySecondDerivation$.get(); // first after last .get() - expect(firstHasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(firstHasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); // second after last .get() - expect(secondHasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(secondHasDerived).toHaveBeenCalledTimes(__YOUR_TURN__); }); }); @@ -179,7 +182,7 @@ describe.skip('expert', () => { * But does that apply here? * How many times has the setup run, for the price `Derivable`. */ - expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); /** Can you explain this behavior? */ }); @@ -226,19 +229,19 @@ describe.skip('expert', () => { */ // How often was the reactor on price$ called? - expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); // And how many times did the setup run? - expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); // What's the value of price$ now? - expect(price$.value).toEqual(__YOUR_TURN__); + expect(price$.value).toEqual(__YOUR_TURN__); // And the value of googlPrice$? - expect(googlPrice$.value).toEqual(__YOUR_TURN__); + expect(googlPrice$.value).toEqual(__YOUR_TURN__); // Is googlPrice$ still even driving any reactors? - expect(googlPrice$.connected).toEqual(__YOUR_TURN__); + expect(googlPrice$.connected).toEqual(__YOUR_TURN__); /** * Can you explain this behavior? @@ -270,7 +273,7 @@ describe.skip('expert', () => { * the created `Derivable` will not run the setup again and * everything should work as expected. * - * ** Your Turn ** TODO: not in the SOLUTIONS!! + * ** Your Turn ** * * *Hint: there is even an `unwrap` helper function for just * such an occasion, try it!* @@ -321,8 +324,8 @@ describe.skip('expert', () => { * * So the value was increased. What do you think happened now? */ - expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(reactSpy).toHaveBeenLastCalledWith([__YOUR_TURN__]); + expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reactSpy).toHaveBeenLastCalledWith([__YOUR_TURN__]); /** * So that worked, now let's try and add another company to the @@ -340,8 +343,8 @@ describe.skip('expert', () => { * * We had a price for 'GOOGL', but not for 'APPL'... */ - expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(reactSpy).toHaveBeenCalledWith([__YOUR_TURN__, __YOUR_TURN__]); + expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reactSpy).toHaveBeenCalledWith([__YOUR_TURN__, __YOUR_TURN__]); }); }); @@ -401,7 +404,7 @@ describe.skip('expert', () => { * * Has anything changed, by using the `derivableCache`? */ - expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); // Now let's resolve the price stockPrice$.mock.results[0].value.set(1079.11); @@ -414,10 +417,10 @@ describe.skip('expert', () => { * * What happens this time? Has the setup run again? */ - expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); // Ok, but did it update the HTML? - expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(lastEmittedHTMLs()[0]).toContain(__YOUR_TURN__); + expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(lastEmittedHTMLs()[0]).toContain(__YOUR_TURN__); // Last chance, what if we add a company companies$.swap(current => [...current, 'APPL']); @@ -430,12 +433,12 @@ describe.skip('expert', () => { * * But did it calculate 'GOOGL' again too? */ - expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); - expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(stockPrice$).toHaveBeenCalledTimes(__YOUR_TURN__); + expect(reactSpy).toHaveBeenCalledTimes(__YOUR_TURN__); // The first should be the generated HTML for 'GOOGL'. - expect(lastEmittedHTMLs()[0]).toContain(__YOUR_TURN__); + expect(lastEmittedHTMLs()[0]).toContain(__YOUR_TURN__); // The second should be the generated HTML for 'APPL'. - expect(lastEmittedHTMLs()[1]).toContain(__YOUR_TURN__); + expect(lastEmittedHTMLs()[1]).toContain(__YOUR_TURN__); }); }); }); From 571e046fca2151230aa0e0bb574a6d8dcc16d931 Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 26 Sep 2024 13:30:40 +0200 Subject: [PATCH 28/30] Moved generator file --- generateTutorialAndSolution.js | 121 ------------------ .../generateTutorialAndSolution.ts | 20 +-- generator/tsconfig.spec.json | 2 +- package.json | 2 +- 4 files changed, 13 insertions(+), 132 deletions(-) delete mode 100644 generateTutorialAndSolution.js rename generateTutorialAndSolution.ts => generator/generateTutorialAndSolution.ts (73%) diff --git a/generateTutorialAndSolution.js b/generateTutorialAndSolution.js deleted file mode 100644 index 012adc0..0000000 --- a/generateTutorialAndSolution.js +++ /dev/null @@ -1,121 +0,0 @@ -"use strict"; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -var __generator = (this && this.__generator) || function (thisArg, body) { - var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; - return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; - function verb(n) { return function (v) { return step([n, v]); }; } - function step(op) { - if (f) throw new TypeError("Generator is already executing."); - while (g && (g = 0, op[0] && (_ = 0)), _) try { - if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; - if (y = 0, t) op = [op[0] & 2, t.value]; - switch (op[0]) { - case 0: case 1: t = op; break; - case 4: _.label++; return { value: op[1], done: false }; - case 5: _.label++; y = op[1]; op = [0]; continue; - case 7: op = _.ops.pop(); _.trys.pop(); continue; - default: - if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } - if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } - if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } - if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } - if (t[2]) _.ops.pop(); - _.trys.pop(); continue; - } - op = body.call(thisArg, _); - } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } - if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; - } -}; -Object.defineProperty(exports, "__esModule", { value: true }); -// import * as fs from 'fs'; -var node_console_1 = require("node:console"); -var fs = require("node:fs/promises"); -// Run with: tsc generateTutorialAndSolution.ts && node generateTutorialAndSolution.js -var generatorFolder = 'generator'; -var tutorialFolder = 'tutorial'; -var solutionFolder = 'solution'; -function generateTutorialAndSolutions() { - return __awaiter(this, void 0, void 0, function () { - var filenames, _i, filenames_1, filename, originalContent, tutorialContent, solutionContent, _a, filenames_2, filename, _b, _c, foldername, content; - return __generator(this, function (_d) { - switch (_d.label) { - case 0: return [4 /*yield*/, fs.readdir(generatorFolder)]; - case 1: - filenames = (_d.sent()).filter(function (f) { return f.endsWith("test.ts"); }); - _i = 0, filenames_1 = filenames; - _d.label = 2; - case 2: - if (!(_i < filenames_1.length)) return [3 /*break*/, 7]; - filename = filenames_1[_i]; - return [4 /*yield*/, fs.readFile("".concat(generatorFolder, "/").concat(filename), 'utf8')]; - case 3: - originalContent = _d.sent(); - tutorialContent = originalContent - .replace(/describe(?!\.skip)/g, "describe.skip") // change `describe` to `describe.skip` - .replace(/\n.*?\/\/ #QUESTION-BLOCK-(START|END)/g, "") - .replace(/ \/\/ #QUESTION/g, "") // remove `// #QUESTION` comments - .replace(/\n.*?\/\/ #ANSWER-BLOCK-START[\s\S]*?\/\/ #ANSWER-BLOCK-END/g, "") // remove // #ANSWER blocks - .replace(/\n.*?\/\/ #ANSWER/g, ""); - // .replace(/\n\s*\n\s*\n/g, `\n\n`); // remove excess whitespaces/newlines - return [4 /*yield*/, fs.writeFile("".concat(tutorialFolder, "/").concat(filename), tutorialContent)]; - case 4: - // .replace(/\n\s*\n\s*\n/g, `\n\n`); // remove excess whitespaces/newlines - _d.sent(); - solutionContent = originalContent - .replace(/describe\.skip/g, "describe") // change `describe.skip` to `describe` - .replace(/\n.*?\/\/ #ANSWER-BLOCK-(START|END)/g, "") - .replace(/ \/\/ #ANSWER/g, "") // remove `// #ANSWER` comments - .replace(/\n.*?\/\/ #QUESTION-BLOCK-START[\s\S]*?\/\/ #QUESTION-BLOCK-END/g, "") // remove // #QUESTION blocks - .replace(/\n.*?\/\/ #QUESTION/g, ""); - // .replace(/\n\s*\n\s*\n/g, `\n\n`); // remove excess whitespaces/newlines - return [4 /*yield*/, fs.writeFile("".concat(solutionFolder, "/").concat(filename), solutionContent)]; - case 5: - // .replace(/\n\s*\n\s*\n/g, `\n\n`); // remove excess whitespaces/newlines - _d.sent(); - console.log("\u001B[33m ".concat(filename, " saved! \u001B[0m")); - _d.label = 6; - case 6: - _i++; - return [3 /*break*/, 2]; - case 7: return [4 /*yield*/, fs.readdir(generatorFolder)]; - case 8: - // These tests will not cause any failing, but are just nice to have. - // e.g. instead of removing excess whitespaces/newlines, we now just prevent them altogether. - filenames = (_d.sent()).filter(function (f) { return f.endsWith("test.ts"); }); // the names are the same in all three folders - _a = 0, filenames_2 = filenames; - _d.label = 9; - case 9: - if (!(_a < filenames_2.length)) return [3 /*break*/, 14]; - filename = filenames_2[_a]; - _b = 0, _c = [tutorialFolder, solutionFolder]; - _d.label = 10; - case 10: - if (!(_b < _c.length)) return [3 /*break*/, 13]; - foldername = _c[_b]; - return [4 /*yield*/, fs.readFile("".concat(foldername, "/").concat(filename), 'utf8')]; - case 11: - content = _d.sent(); - (0, node_console_1.assert)(content.match(/\n\s*\n\s*\n/) === null, "no 2 consecutive empty lines in ".concat(foldername, "/").concat(filename)); - (0, node_console_1.assert)(!content.includes('#QUESTION') && !content.includes('#ANSWER'), "no '#QUESTION' or '#ANSWER' in ".concat(foldername, "/").concat(filename)); - _d.label = 12; - case 12: - _b++; - return [3 /*break*/, 10]; - case 13: - _a++; - return [3 /*break*/, 9]; - case 14: return [2 /*return*/]; - } - }); - }); -} -generateTutorialAndSolutions(); diff --git a/generateTutorialAndSolution.ts b/generator/generateTutorialAndSolution.ts similarity index 73% rename from generateTutorialAndSolution.ts rename to generator/generateTutorialAndSolution.ts index 658dfca..5e2f01b 100644 --- a/generateTutorialAndSolution.ts +++ b/generator/generateTutorialAndSolution.ts @@ -1,8 +1,10 @@ import { assert } from 'node:console'; -import * as fs from 'node:fs/promises'; +import { readdir, readFile, writeFile } from 'node:fs/promises'; -// Run this file with: `tsc generateTutorialAndSolution.ts && node generateTutorialAndSolution.js` -// Run this automated with: `npm run generate` +// Run this with: `npm run generate` +// NOTE: you should be in the project root directory when you run this command. + +// #!/usr/bin/env ts-node-script -r tsconfig-paths/register --transpile-only --project tsconfig.tools.json -- const generatorFolder = 'generator'; const tutorialFolder = 'tutorial'; @@ -10,11 +12,11 @@ const solutionFolder = 'solution'; async function generateTutorialAndSolutions() { // get all filenames in the folder 'tutorial' that end on '.ts' - let filenames: string[] = (await fs.readdir(generatorFolder)).filter(f => f.endsWith(`test.ts`)); + let filenames: string[] = (await readdir(generatorFolder)).filter(f => f.endsWith(`test.ts`)); for (let filename of filenames) { // // Read the original text file - const originalContent = await fs.readFile(`${generatorFolder}/${filename}`, 'utf8'); + const originalContent = await readFile(`${generatorFolder}/${filename}`, 'utf8'); // ** TUTORIAL ** let tutorialContent = originalContent @@ -25,7 +27,7 @@ async function generateTutorialAndSolutions() { .replace(/\n.*?\/\/ #ANSWER/g, ``); // remove the entire `// #ANSWER` line, including comment // .replace(/\n\s*\n\s*\n/g, `\n\n`); // remove excess whitespaces/newlines - await fs.writeFile(`${tutorialFolder}/${filename}`, tutorialContent); + await writeFile(`${tutorialFolder}/${filename}`, tutorialContent); // ** SOLUTION ** let solutionContent = originalContent @@ -36,16 +38,16 @@ async function generateTutorialAndSolutions() { .replace(/\n.*?\/\/ #QUESTION/g, ``); // remove the entire `// #QUESTION` line, including comment // .replace(/\n\s*\n\s*\n/g, `\n\n`); // remove excess whitespaces/newlines - await fs.writeFile(`${solutionFolder}/${filename}`, solutionContent); + await writeFile(`${solutionFolder}/${filename}`, solutionContent); console.log(`\x1b[33m ${filename} saved! \x1b[0m`); } // These tests will not cause any failing, but are just nice to have. // e.g. instead of removing excess whitespaces/newlines, we now just prevent them altogether. - filenames = (await fs.readdir(generatorFolder)).filter(f => f.endsWith(`test.ts`)); // the names are the same in all three folders + filenames = (await readdir(generatorFolder)).filter(f => f.endsWith(`test.ts`)); // the names are the same in all three folders for (let foldername of [tutorialFolder, solutionFolder]) { for (let filename of filenames) { - let content = await fs.readFile(`${foldername}/${filename}`, 'utf8'); + let content = await readFile(`${foldername}/${filename}`, 'utf8'); assert(content.match(/\n\s*\n\s*\n/) === null, `no 2 consecutive empty lines in ${foldername}/${filename}`); assert( !content.includes('#QUESTION') && !content.includes('#ANSWER'), diff --git a/generator/tsconfig.spec.json b/generator/tsconfig.spec.json index 750d7b4..12a04e6 100644 --- a/generator/tsconfig.spec.json +++ b/generator/tsconfig.spec.json @@ -4,5 +4,5 @@ "module": "commonjs", "types": ["jest", "jest-extended", "node"] }, - "include": ["**/*.test.ts", "**/*.tests.ts", "**/*.d.ts", "jest.config.ts"] + "include": [] } diff --git a/package.json b/package.json index cda3366..6d1ea8c 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "help": "nx help", "tutorial": "jest tutorial/* -c tutorial/jest.config.ts", "solution": "jest solution/* -c solution/jest.config.ts", - "generate": "tsc generateTutorialAndSolution.ts && node generateTutorialAndSolution.js && npm run tutorial && npm run solution" + "generate": "tsc generator/generateTutorialAndSolution.ts && node generator/generateTutorialAndSolution.js && npm run tutorial && npm run solution" }, "standard-version": { "bumpFiles": [ From 6216e85797b1652bb1dfa2c928d9a059c631a88e Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 26 Sep 2024 16:50:35 +0200 Subject: [PATCH 29/30] Small ReadMe update --- CHANGELOG.md | 7 ------- README.md | 27 ++++++++++++++++++--------- generator/8 - utils.test.ts | 2 +- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ea79f0..3c78c0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,6 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. -TODO: -Lots of changes! - -- Swapped `7 - advanced` with `8 - util` to be able to use advanced techniques in the utils. -- More tests, including `reactor options order of execution (3)`, `fallback-to (5)`, `errors (6)`, `templates, take, map on arrays, mapState, flatMap, pluck (7)`, and `pairwise/scan on normal arrays, peek, lift, final, promise... (8)`. -- The tutorial and solutions are now both available in the same branch. To prevent making every change twice, all changes should now be put in the `generator` folder rather than directly in the `tutorial` or `solution` folder. - ## [8.0.0](https://github.com/skunkteam/sherlock/compare/v7.0.0...v8.0.0) (2023-10-17) ### ⚠ BREAKING CHANGES diff --git a/README.md b/README.md index b2ddaf6..e3ab567 100644 --- a/README.md +++ b/README.md @@ -145,28 +145,39 @@ There are three types of Derivables: ```typescript const isBrilliant$ = name$.derive(name => name === 'Sherlock'); - isBrilliant$.get(); // false + isBrilliant$.get(); // => false name$.set('Sherlock'); - isBrilliant$.get(); // true + isBrilliant$.get(); // => true ``` Derivations can also be created with the generic `derive` function as seen earlier. This function can be used to do an arbitrary calculation on any number of derivables. `@skunkteam/sherlock` automatically records which derivable is dependent on which other derivable to be able to update derived state when needed. ## Reactors -To execute side effects, you can react to changes on any derivable as seen in an earlier example. +To execute side effects, you can react to changes on any derivable as seen in an earlier example. This can be done using the `#react` method that is present on all derivables. -_More documentation coming soon_ +```typescript +const normalEffect = atom(''); +let sideEffect = ''; -## Transactions +normalEffect.react(v => { + sideEffect = v.replace('effect', 'side-effect'); +}); -_More documentation coming soon_ +normalEffect.set('Watch this effect'); +sideEffect; // => 'Watch this side-effect' +``` ## Interoperability with RxJS out of the box -_Coming soon_ +RxJS is another popular reactive library. However, RxJS can become quite complicated and user-unfriendly when your application becomes big. This was the main reason why Sherlock was developed. As Angular uses RxJS, and we use Angualr, our Sherlock library needs to be compatible with RxJS. The `fromObservable()` and `toObservable()` functions are used for this. However, the `from()` function in RxJS has become a succesful alternative to `toObservable()`. +`fromObservable()` can be used to map an `Observable` to a `Derivable`, and the `from()` function in RxJS can be used to map a `Derivable` to a `Derivable`. + +## Transactions + +_More documentation coming soon_ ## Proxies using sherlock-proxies @@ -185,5 +196,3 @@ _Coming soon_ ### Cyclic reactors _Coming soon_ - -TODO: FIX THE README! diff --git a/generator/8 - utils.test.ts b/generator/8 - utils.test.ts index 239cdcc..c7ef549 100644 --- a/generator/8 - utils.test.ts +++ b/generator/8 - utils.test.ts @@ -563,7 +563,7 @@ describe('utils', () => { }); }); - describe('`Promise`, `Observable`, and `EventPattern`', () => { + describe('`Promise`, `Observable`, and `fromEventPattern`', () => { /** * Sherlock can also deal with Promises using the `.fromPromise()` and `.toPromise()` functions. * This translates Promises directly to Sherlock concepts we have discussed already. From cd8caee1a0dfe4fd690e9186be2a82e7b52e53d5 Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 26 Sep 2024 16:52:38 +0200 Subject: [PATCH 30/30] Update to `npm run generate` command --- package.json | 2 +- solution/8 - utils.test.ts | 2 +- tutorial/8 - utils.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 6d1ea8c..bfe58c6 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "help": "nx help", "tutorial": "jest tutorial/* -c tutorial/jest.config.ts", "solution": "jest solution/* -c solution/jest.config.ts", - "generate": "tsc generator/generateTutorialAndSolution.ts && node generator/generateTutorialAndSolution.js && npm run tutorial && npm run solution" + "generate": "tsc generator/generateTutorialAndSolution.ts && node generator/generateTutorialAndSolution.js && npm run tutorial && npm run solution && rm generator/generateTutorialAndSolution.js" }, "standard-version": { "bumpFiles": [ diff --git a/solution/8 - utils.test.ts b/solution/8 - utils.test.ts index fd260ec..42ff4a3 100644 --- a/solution/8 - utils.test.ts +++ b/solution/8 - utils.test.ts @@ -503,7 +503,7 @@ describe('utils', () => { }); }); - describe('`Promise`, `Observable`, and `EventPattern`', () => { + describe('`Promise`, `Observable`, and `fromEventPattern`', () => { /** * Sherlock can also deal with Promises using the `.fromPromise()` and `.toPromise()` functions. * This translates Promises directly to Sherlock concepts we have discussed already. diff --git a/tutorial/8 - utils.test.ts b/tutorial/8 - utils.test.ts index c4463bc..0611094 100644 --- a/tutorial/8 - utils.test.ts +++ b/tutorial/8 - utils.test.ts @@ -519,7 +519,7 @@ describe.skip('utils', () => { }); }); - describe.skip('`Promise`, `Observable`, and `EventPattern`', () => { + describe.skip('`Promise`, `Observable`, and `fromEventPattern`', () => { /** * Sherlock can also deal with Promises using the `.fromPromise()` and `.toPromise()` functions. * This translates Promises directly to Sherlock concepts we have discussed already.