├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── archived ├── README.md ├── record │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── record.ts │ │ └── tests.ts │ ├── tsconfig.json │ └── tsconfig.rollup.json └── tuple │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── rollup.config.js │ ├── src │ ├── tests.ts │ └── tuple.ts │ ├── tsconfig.json │ └── tsconfig.rollup.json ├── lerna.json ├── nx.json ├── package-lock.json ├── package.json ├── packages ├── caches │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── common.ts │ │ ├── index.ts │ │ ├── strong.ts │ │ ├── tests │ │ │ ├── main.ts │ │ │ ├── strong.ts │ │ │ └── weak.ts │ │ └── weak.ts │ ├── tsconfig.es5.json │ └── tsconfig.json ├── context │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── index.ts │ │ ├── slot.ts │ │ └── tests │ │ │ └── main.ts │ ├── tsconfig.es5.json │ └── tsconfig.json ├── equality │ ├── .gitignore │ ├── .npmignore │ ├── LICENSE │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── index.ts │ │ └── tests │ │ │ └── main.ts │ └── tsconfig.json ├── task │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── index.ts │ │ └── tests │ │ │ └── main.ts │ ├── tsconfig.es5.json │ └── tsconfig.json ├── template │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── index.ts │ │ └── tests │ │ │ └── main.ts │ ├── tsconfig.es5.json │ └── tsconfig.json └── trie │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── rollup.config.js │ ├── src │ ├── index.ts │ └── tests │ │ └── main.ts │ ├── tsconfig.es5.json │ └── tsconfig.json └── shared ├── rollup.config.js ├── test.sh └── tsconfig.json /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "npm" 7 | directory: "/" # Location of package manifests 8 | schedule: 9 | interval: "daily" 10 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | name: Test on node ${{ matrix.node_version }} and ${{ matrix.os }} 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | node_version: ['16', '18', '19', '20', '21'] 16 | os: [ubuntu-latest] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Use Node.js ${{ matrix.node_version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node_version }} 24 | 25 | - name: npm install, build and test 26 | run: | 27 | npm install 28 | npm run build --if-present 29 | npm test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | .idea/ 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (https://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # TypeScript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # next.js build output 62 | .next 63 | 64 | # nx build tool cache 65 | .nx 66 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to Node.js inspector", 6 | "port": 9229, 7 | "request": "attach", 8 | "skipFiles": [ 9 | "/**" 10 | ], 11 | "type": "pwa-node" 12 | }, 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2021 Ben Newman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wryware   ![CI](https://github.com/benjamn/wryware/workflows/CI/badge.svg) 2 | 3 | A collection of packages that are probably a little too clever. Use at your own wrisk. 4 | -------------------------------------------------------------------------------- /archived/README.md: -------------------------------------------------------------------------------- 1 | # Archived `@wry/*` packages 2 | 3 | These packages were ahead of their time in a bad way. Sayonara and/or good riddance. -------------------------------------------------------------------------------- /archived/record/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Ignore generated TypeScript files. 40 | lib 41 | 42 | # Cache for rollup-plugin-typescript2 43 | .rpt2_cache 44 | -------------------------------------------------------------------------------- /archived/record/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src 3 | lib/tests.* 4 | .rpt2_cache 5 | -------------------------------------------------------------------------------- /archived/record/README.md: -------------------------------------------------------------------------------- 1 | # @wry/record 2 | 3 | Immutable record objects with constant-time equality testing (`===`) and 4 | no hidden memory leaks. 5 | 6 | ## Installation & Usage 7 | 8 | First install the package from npm: 9 | 10 | ```sh 11 | npm install @wry/record 12 | ``` 13 | -------------------------------------------------------------------------------- /archived/record/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wry/record", 3 | "version": "0.3.1", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@wry/record", 9 | "version": "0.3.1", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@wry/tuple": "file:../tuple", 13 | "tslib": "^2.3.0" 14 | }, 15 | "engines": { 16 | "node": ">=8" 17 | } 18 | }, 19 | "../tuple": { 20 | "name": "@wry/tuple", 21 | "version": "0.3.1", 22 | "license": "MIT", 23 | "dependencies": { 24 | "@wry/trie": "file:../trie", 25 | "tslib": "^2.3.0" 26 | }, 27 | "engines": { 28 | "node": ">=8" 29 | } 30 | }, 31 | "node_modules/@wry/tuple": { 32 | "resolved": "../tuple", 33 | "link": true 34 | }, 35 | "node_modules/tslib": { 36 | "version": "2.4.0", 37 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", 38 | "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" 39 | } 40 | }, 41 | "dependencies": { 42 | "@wry/tuple": { 43 | "version": "file:../tuple", 44 | "requires": { 45 | "@wry/trie": "file:../trie", 46 | "tslib": "^2.3.0" 47 | } 48 | }, 49 | "tslib": { 50 | "version": "2.4.0", 51 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", 52 | "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /archived/record/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wry/record", 3 | "version": "0.3.1", 4 | "author": "Ben Newman ", 5 | "description": "Immutable record objects with constant-time equality testing (===) and no hidden memory leaks", 6 | "license": "MIT", 7 | "main": "lib/record.js", 8 | "module": "lib/record.esm.js", 9 | "types": "lib/record.d.ts", 10 | "keywords": [], 11 | "homepage": "https://github.com/benjamn/wryware", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/benjamn/wryware.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/benjamn/wryware/issues" 18 | }, 19 | "scripts": { 20 | "clean": "../../node_modules/.bin/rimraf lib", 21 | "tsc": "../../node_modules/.bin/tsc", 22 | "rollup": "../../node_modules/.bin/rollup -c", 23 | "build": "npm run clean && npm run tsc && npm run rollup", 24 | "mocha": "../../scripts/test.sh lib/tests.js", 25 | "prepare": "npm run build", 26 | "test": "npm run build && npm run mocha" 27 | }, 28 | "dependencies": { 29 | "@wry/tuple": "file:../tuple", 30 | "tslib": "^2.3.0" 31 | }, 32 | "engines": { 33 | "node": ">=8" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /archived/record/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescriptPlugin from 'rollup-plugin-typescript2'; 2 | import typescript from 'typescript'; 3 | 4 | const globals = { 5 | __proto__: null, 6 | tslib: "tslib", 7 | "@wry/tuple": "wryTuple", 8 | }; 9 | 10 | function external(id) { 11 | return id in globals; 12 | } 13 | 14 | export default [{ 15 | input: "src/record.ts", 16 | external, 17 | output: { 18 | file: "lib/record.esm.js", 19 | format: "esm", 20 | sourcemap: true, 21 | globals, 22 | }, 23 | plugins: [ 24 | typescriptPlugin({ 25 | typescript, 26 | tsconfig: "./tsconfig.rollup.json", 27 | }), 28 | ], 29 | }, { 30 | input: "lib/record.esm.js", 31 | external, 32 | output: { 33 | // Intentionally overwrite the record.js file written by tsc: 34 | file: "lib/record.js", 35 | format: "cjs", 36 | exports: "named", 37 | sourcemap: true, 38 | name: "record", 39 | globals, 40 | }, 41 | }]; 42 | -------------------------------------------------------------------------------- /archived/record/src/record.ts: -------------------------------------------------------------------------------- 1 | import tuple, { Tuple, WeakTrie } from "@wry/tuple"; 2 | 3 | const recsByTuple = new WeakMap, Record>(); 4 | 5 | export class Record { 6 | private constructor(obj: TObj) { 7 | return Record.from(obj); 8 | } 9 | 10 | static from(obj: TObj): Record { 11 | if (!obj || typeof obj !== "object") return obj; 12 | if (obj instanceof Record) return obj; 13 | const keyValueTuples = sortedKeys(obj).map( 14 | key => tuple(key, (obj as any)[key])); 15 | const tupleOfKeyValueTuples = tuple(...keyValueTuples); 16 | let rec = recsByTuple.get(tupleOfKeyValueTuples); 17 | if (!rec) { 18 | rec = Object.create(Record.prototype) as Record; 19 | recsByTuple.set(tupleOfKeyValueTuples, rec); 20 | keyValueTuples.forEach(([key, value]) => (rec as any)[key] = value); 21 | Object.freeze(rec); 22 | } 23 | return rec; 24 | } 25 | 26 | static isRecord(value: any): value is Record { 27 | return value instanceof Record; 28 | } 29 | } 30 | 31 | export default Record.from; 32 | 33 | const sortingTrie = new WeakTrie<{ 34 | sorted: string[], 35 | }>(); 36 | 37 | function sortedKeys(obj: object): readonly string[] { 38 | const keys = Object.keys(obj); 39 | const node = sortingTrie.lookupArray(keys); 40 | return node.sorted || Object.freeze(node.sorted = keys.sort()); 41 | } 42 | -------------------------------------------------------------------------------- /archived/record/src/tests.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import record, { Record } from "./record"; 3 | 4 | describe("record", function () { 5 | it("should be importable", function () { 6 | assert.strictEqual(typeof record, "function"); 7 | }); 8 | 9 | it("should pass isRecord", function () { 10 | const recXY = record({ x: "y", y: "z" }); 11 | assert.strictEqual(Record.isRecord(recXY), true); 12 | assert.strictEqual(Record.isRecord({ ...recXY }), false); 13 | assert.deepEqual(recXY, { ...recXY }); 14 | assert.strictEqual(recXY, record({ ...recXY })); 15 | }); 16 | 17 | it("should be frozen", function () { 18 | assert.strictEqual(Object.isFrozen(record({ 19 | a: 1, 20 | b: 2, 21 | c: 3, 22 | })), true); 23 | }); 24 | 25 | it("should sort keys", function () { 26 | assert.deepEqual( 27 | Object.keys(record({ 28 | zxcv: "qwer", 29 | asdf: "zxcv", 30 | qwer: "asdf", 31 | })), 32 | ["asdf", "qwer", "zxcv"], 33 | ); 34 | }) 35 | 36 | it("should be === when deeply equal", function () { 37 | assert.strictEqual( 38 | record({ 39 | a: 1, 40 | b: 2, 41 | }), 42 | record({ 43 | b: 2, 44 | a: 1, 45 | }), 46 | ); 47 | 48 | const ab = { 49 | a: "a".charCodeAt(0), 50 | b: "b".charCodeAt(0), 51 | }; 52 | 53 | const abRec = record(ab); 54 | 55 | const xy = { 56 | x: "x".charCodeAt(0), 57 | y: "y".charCodeAt(0), 58 | }; 59 | 60 | const xyRec = record(xy); 61 | 62 | const abxyRec = record({ 63 | ...xy, 64 | ...ab, 65 | }); 66 | 67 | assert.strictEqual(record({ 68 | ...ab, 69 | ...xy, 70 | }), abxyRec); 71 | 72 | assert.strictEqual(record({ 73 | ...ab, 74 | ...xy, 75 | ...ab, 76 | }), abxyRec); 77 | 78 | assert.strictEqual(record({ 79 | ...xy, 80 | ...ab, 81 | ...xy, 82 | }), abxyRec); 83 | 84 | assert.strictEqual(record({ 85 | ...abRec, 86 | ...xyRec, 87 | }), abxyRec); 88 | 89 | assert.strictEqual(record({ 90 | ...xyRec, 91 | ...abRec, 92 | }), abxyRec); 93 | 94 | assert.deepEqual(abxyRec, { 95 | a: 97, 96 | b: 98, 97 | x: 120, 98 | y: 121, 99 | }); 100 | 101 | assert.deepEqual( 102 | Object.keys(abxyRec), 103 | ["a", "b", "x", "y"], 104 | ); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /archived/record/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./lib", 6 | "downlevelIteration": true, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /archived/record/tsconfig.rollup.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "es2015", 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /archived/tuple/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Ignore generated TypeScript files. 40 | lib 41 | 42 | # Cache for rollup-plugin-typescript2 43 | .rpt2_cache 44 | -------------------------------------------------------------------------------- /archived/tuple/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src 3 | lib/tests.* 4 | .rpt2_cache 5 | -------------------------------------------------------------------------------- /archived/tuple/README.md: -------------------------------------------------------------------------------- 1 | # @wry/tuple 2 | 3 | Immutable finite list objects with constant-time equality testing (`===`) 4 | and no hidden memory leaks. 5 | 6 | ## Installation & Usage 7 | 8 | First install the package from npm: 9 | 10 | ```sh 11 | npm install @wry/tuple 12 | ``` 13 | 14 | This package has a default export that can be imported using any name, but 15 | is typically named `tuple`: 16 | 17 | ```js 18 | import assert from "assert"; 19 | import tuple from "@wry/tuple"; 20 | 21 | // Tuples are array-like: 22 | assert(tuple(1, 2, 3).length === 3); 23 | assert(tuple("a", "b")[1] === "b"); 24 | 25 | // Deeply equal tuples are also === equal! 26 | assert.strictEqual( 27 | tuple(1, tuple(2, 3), 4), 28 | tuple(1, tuple(2, 3), 4), 29 | ); 30 | ``` 31 | 32 | In addition to the default export, `@wry/tuple` exports the `Tuple` class, 33 | whose `Tuple.from` function provides the default export; and the 34 | `WeakTrie` class, which you can learn more about by reading its 35 | [code](/packages/tuple/src/weak-trie.ts). 36 | You probably will not need to use these exports directly: 37 | 38 | ``` 39 | import tuple, { Tuple, WeakTrie } from "@wry/tuple"; 40 | 41 | assert(tuple === Tuple.from); 42 | ``` 43 | 44 | ### Constructing tuples 45 | 46 | The `tuple` function takes any number of arguments and returns a unique, 47 | immutable object that inherits from `Tuple.prototype` and is guaranteed to 48 | be `===` any other `Tuple` object created from the same sequence of 49 | arguments: 50 | 51 | ```js 52 | const obj = { asdf: 1234 }; 53 | const t1 = tuple(1, "asdf", obj); 54 | const t2 = tuple(1, "asdf", obj); 55 | 56 | assert.strictEqual(t1 === t2, true); 57 | assert.strictEqual(t1, t2); 58 | ``` 59 | 60 | ### Own properties 61 | 62 | A tuple has a fixed numeric `length` property, and its elements may 63 | be accessed using array index notation: 64 | 65 | ```js 66 | assert.strictEqual(t1.length, 3); 67 | 68 | t1.forEach((x, i) => { 69 | assert.strictEqual(x, t2[i]); 70 | }); 71 | ``` 72 | 73 | ### Nested tuples 74 | 75 | Since `Tuple` objects are just another kind of JavaScript object, 76 | naturally tuples can contain other tuples: 77 | 78 | ```js 79 | assert.strictEqual( 80 | tuple(t1, t2), 81 | tuple(t2, t1) 82 | ); 83 | 84 | assert.strictEqual( 85 | tuple(1, t2, 3)[1][2], 86 | obj 87 | ); 88 | ``` 89 | 90 | However, because tuples are immutable and always distinct from any of 91 | their arguments, it is not possible for a tuple to contain itself, nor to 92 | contain another tuple that contains the original tuple, and so forth. 93 | 94 | ### Constant time `===` equality 95 | 96 | Since `Tuple` objects are identical when (and only when) their elements 97 | are identical, any two tuples can be compared for equality in constant 98 | time, regardless of how many elements they contain. 99 | 100 | This behavior also makes `Tuple` objects useful as keys in a `Map`, or 101 | elements in a `Set`, without any extra hashing or equality logic: 102 | 103 | ```js 104 | const map = new Map; 105 | 106 | map.set(tuple(1, 12, 3), { 107 | author: tuple("Ben", "Newman"), 108 | releaseDate: Date.now() 109 | }); 110 | 111 | const version = "1.12.3"; 112 | const info = map.get(tuple(...version.split(".").map(Number))); 113 | if (info) { 114 | console.log(info.author[1]); // "Newman" 115 | } 116 | ``` 117 | 118 | ### Shallow immutability 119 | 120 | While the identity, number, and order of elements in a `tuple` is fixed, 121 | please note that the contents of the individual elements are not frozen in 122 | any way: 123 | 124 | ```js 125 | const obj = { asdf: 1234 }; 126 | tuple(1, "asdf", obj)[2].asdf = "oyez"; 127 | assert.strictEqual(obj.asdf, "oyez"); 128 | ``` 129 | 130 | ### Iterability 131 | 132 | Every `Tuple` object is array-like and iterable, so `...` spreading and 133 | destructuring work as they should: 134 | 135 | ```js 136 | func(...tuple(a, b)); 137 | func.apply(this, tuple(c, d, e)); 138 | 139 | assert.deepEqual( 140 | [1, ...tuple(2, 3), 4], 141 | [1, 2, 3, 4] 142 | ); 143 | 144 | assert.strictEqual( 145 | tuple(1, ...tuple(2, 3), 4), 146 | tuple(1, 2, 3, 4) 147 | ); 148 | 149 | const [a, [_, b]] = tuple(1, tuple(2, 3), 4); 150 | assert.strictEqual(a, 1); 151 | assert.strictEqual(b, 3); 152 | ``` 153 | 154 | ### Instance pooling (internalization) 155 | 156 | Any data structure that guarantees `===` equality based on structural equality must maintain some sort of internal pool of previously encountered instances. 157 | 158 | Implementing such a pool for `tuple`s is fairly straightforward (though feel free to give it some thought before reading this code, if you like figuring things out for yourself): 159 | 160 | ```js 161 | const pool = new Map; 162 | 163 | function tuple(...items) { 164 | let node = pool; 165 | 166 | items.forEach(item => { 167 | let child = node.get(item); 168 | if (!child) node.set(item, child = new Map); 169 | node = child; 170 | }); 171 | 172 | // If we've created a tuple instance for this sequence of elements before, 173 | // return that instance again. Otherwise create a new immutable tuple instance 174 | // with the same (frozen) elements as the items array. 175 | return node.tuple || (node.tuple = Object.create( 176 | tuple.prototype, 177 | Object.getOwnPropertyDescriptors(Object.freeze(items)) 178 | )); 179 | } 180 | ``` 181 | 182 | This implementation is pretty good, because it requires only linear time (_O_(`items.length`)) to determine if a `tuple` has been created previously for the given `items`, and you can't do better than linear time (asymptotically speaking) because you have to look at all the items. 183 | 184 | This code is also useful as an illustration of exactly how the `tuple` constructor behaves, in case you weren't satisfied by my examples in the previous section. 185 | 186 | ### Garbage collection 187 | 188 | The simple implementation above has a serious problem: in a 189 | garbage-collected language like JavaScript, the `pool` itself will retain 190 | references to all `Tuple` objects ever created, which prevents `Tuple` 191 | objects and their elements (which can be arbitrarily large) from ever 192 | being reclaimed by the garbage collector, even after they become 193 | unreachable by any other means. In other words, storing objects in this 194 | kind of `Tuple` would inevitably cause **memory leaks**. 195 | 196 | To solve this problem, it's tempting to try changing `Map` to 197 | [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) 198 | here: 199 | 200 | ```js 201 | const pool = new WeakMap; 202 | ``` 203 | 204 | and here: 205 | 206 | ```js 207 | if (!child) node.set(item, child = new WeakMap); 208 | ``` 209 | 210 | This approach is appealing because a `WeakMap` should allow its keys to be 211 | reclaimed by the garbage collector. That's the whole point of a `WeakMap`, 212 | after all. Once a `tuple` becomes unreachable because the program has 213 | stopped using it anywhere else, its elements are free to disappear from 214 | the pool of `WeakMap`s whenever they too become unreachable. In other 215 | words, something like a `WeakMap` is exactly what we need here. 216 | 217 | Unfortunately, this strategy stumbles because a `tuple` can contain 218 | primitive values as well as object references, whereas a `WeakMap` only 219 | allows keys that are object references. 220 | 221 | In other words, `node.set(item, ...)` would fail whenever `item` is not an 222 | object, if `node` is a `WeakMap`. To see how the `@wry/tuple` library 223 | cleverly gets around this `WeakMap` limitation, have a look at 224 | [this module](https://github.com/benjamn/wryware/blob/main/packages/tuple/src/weak-trie.ts). 225 | -------------------------------------------------------------------------------- /archived/tuple/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wry/tuple", 3 | "version": "0.3.1", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@wry/tuple", 9 | "version": "0.3.1", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@wry/trie": "file:../trie", 13 | "tslib": "^2.3.0" 14 | }, 15 | "engines": { 16 | "node": ">=8" 17 | } 18 | }, 19 | "../trie": { 20 | "name": "@wry/trie", 21 | "version": "0.3.1", 22 | "license": "MIT", 23 | "dependencies": { 24 | "tslib": "^2.3.0" 25 | }, 26 | "engines": { 27 | "node": ">=8" 28 | } 29 | }, 30 | "../trie/node_modules/tslib": { 31 | "version": "2.3.0", 32 | "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" 33 | }, 34 | "node_modules/@wry/trie": { 35 | "resolved": "../trie", 36 | "link": true 37 | }, 38 | "node_modules/tslib": { 39 | "version": "2.4.0", 40 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", 41 | "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" 42 | } 43 | }, 44 | "dependencies": { 45 | "@wry/trie": { 46 | "version": "file:../trie", 47 | "requires": { 48 | "tslib": "^2.3.0" 49 | }, 50 | "dependencies": { 51 | "tslib": { 52 | "version": "2.3.0", 53 | "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" 54 | } 55 | } 56 | }, 57 | "tslib": { 58 | "version": "2.4.0", 59 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", 60 | "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /archived/tuple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wry/tuple", 3 | "version": "0.3.1", 4 | "author": "Ben Newman ", 5 | "description": "Immutable finite list objects with constant-time equality testing (===) and no hidden memory leaks", 6 | "license": "MIT", 7 | "main": "lib/tuple.js", 8 | "module": "lib/tuple.esm.js", 9 | "types": "lib/tuple.d.ts", 10 | "keywords": [], 11 | "homepage": "https://github.com/benjamn/wryware", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/benjamn/wryware.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/benjamn/wryware/issues" 18 | }, 19 | "scripts": { 20 | "clean": "../../node_modules/.bin/rimraf lib", 21 | "tsc": "../../node_modules/.bin/tsc", 22 | "rollup": "../../node_modules/.bin/rollup -c", 23 | "build": "npm run clean && npm run tsc && npm run rollup", 24 | "mocha": "../../scripts/test.sh lib/tests.js", 25 | "prepare": "npm run build", 26 | "test": "npm run build && npm run mocha" 27 | }, 28 | "dependencies": { 29 | "@wry/trie": "file:../trie", 30 | "tslib": "^2.3.0" 31 | }, 32 | "engines": { 33 | "node": ">=8" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /archived/tuple/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescriptPlugin from 'rollup-plugin-typescript2'; 2 | import typescript from 'typescript'; 3 | 4 | const globals = { 5 | __proto__: null, 6 | tslib: "tslib", 7 | "@wry/trie": "trie", 8 | }; 9 | 10 | function external(id) { 11 | return id in globals; 12 | } 13 | 14 | export default [{ 15 | input: "src/tuple.ts", 16 | external, 17 | output: { 18 | file: "lib/tuple.esm.js", 19 | format: "esm", 20 | sourcemap: true, 21 | globals, 22 | }, 23 | plugins: [ 24 | typescriptPlugin({ 25 | typescript, 26 | tsconfig: "./tsconfig.rollup.json", 27 | }), 28 | ], 29 | }, { 30 | input: "lib/tuple.esm.js", 31 | external, 32 | output: { 33 | // Intentionally overwrite the tuple.js file written by tsc: 34 | file: "lib/tuple.js", 35 | format: "cjs", 36 | exports: "named", 37 | sourcemap: true, 38 | name: "tuple", 39 | globals, 40 | }, 41 | }]; 42 | -------------------------------------------------------------------------------- /archived/tuple/src/tests.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import tuple, { Tuple } from "./tuple"; 3 | 4 | describe("tuple", function () { 5 | it("should be importable", function () { 6 | assert.strictEqual(typeof tuple, "function"); 7 | }); 8 | 9 | it("should produce array-like Tuple instances", function () { 10 | assert.deepEqual(tuple(1, 2, 3), [1, 2, 3]); 11 | assert.strictEqual(tuple("a", "b").length, 2); 12 | assert.strictEqual(tuple() instanceof Tuple, true); 13 | assert.strictEqual(tuple(false, true) instanceof Tuple, true); 14 | assert.strictEqual(Tuple.isTuple(tuple(1, 2, 3)), true); 15 | assert.strictEqual(Tuple.isTuple([1, 2, 3]), false); 16 | }); 17 | 18 | it("should internalize basic tuples", function () { 19 | assert.strictEqual(tuple(), tuple()); 20 | assert.strictEqual(tuple(1, 2, 3), tuple(1, 2, 3)); 21 | }); 22 | 23 | it("can internalize tuples of tuples", function () { 24 | assert.strictEqual( 25 | tuple(1, tuple(2, 3), tuple(), 4), 26 | tuple(1, tuple(2, 3), tuple(), 4), 27 | ); 28 | 29 | assert.notEqual( 30 | tuple(1, tuple(2, 3), tuple(), 4), 31 | tuple(1, tuple(2, 3), tuple(3.5), 4), 32 | ); 33 | }); 34 | 35 | it("can be built with ...spread syntax", function () { 36 | const t1 = tuple(1); 37 | const t111 = tuple(...t1, ...t1, ...t1); 38 | assert.strictEqual( 39 | tuple(...t111, ...t111), 40 | tuple(1, 1, 1, 1, 1, 1), 41 | ); 42 | }) 43 | 44 | it("should be usable as Map keys", function () { 45 | const map = new Map; 46 | 47 | assert.strictEqual(map.has(tuple(1, tuple(2, "buckle"), true)), false); 48 | map.set(tuple(1, tuple(2, "buckle"), true), "oh my"); 49 | assert.strictEqual(map.has(tuple(1, tuple(2, "buckle"), true)), true); 50 | assert.strictEqual(map.get(tuple(1, tuple(2, "buckle"), true)), "oh my"); 51 | 52 | map.forEach(function (value, key) { 53 | assert.strictEqual(key, tuple(1, tuple(2, "buckle"), true)); 54 | assert.strictEqual(value, "oh my"); 55 | }); 56 | 57 | map.delete(tuple(1, tuple(2, "buckle"), true)); 58 | map.forEach(function () { 59 | throw new Error("unreached"); 60 | }); 61 | }); 62 | 63 | it("should be storable in a Set", function () { 64 | const set = new Set([ 65 | tuple(1, 2, tuple(3, 4), 5), 66 | tuple(1, 2, tuple(3, 4), 5), 67 | ]); 68 | 69 | assert.strictEqual(set.size, 1); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /archived/tuple/src/tuple.ts: -------------------------------------------------------------------------------- 1 | import { Trie } from "@wry/trie"; 2 | export { Trie as WeakTrie } 3 | 4 | const pool = new Trie<{ 5 | tuple: Tuple; 6 | }>(true); 7 | 8 | export class Tuple 9 | implements ArrayLike, Iterable 10 | { 11 | // ArrayLike: 12 | [i: number]: T[number]; 13 | public readonly length: number; 14 | 15 | // Iterable: 16 | public [Symbol.iterator]: () => Iterator; 17 | 18 | // Tuple objects created by Tuple.from are guaranteed to be === each 19 | // other if (and only if) they have identical (===) elements, which 20 | // allows checking deep equality in constant time. 21 | public static from(...elements: E): Tuple { 22 | const node = pool.lookupArray(elements); 23 | return node.tuple || (node.tuple = new Tuple(elements)); 24 | } 25 | 26 | public static isTuple(that: any): that is Tuple { 27 | return that instanceof Tuple; 28 | } 29 | 30 | // The constructor must be private to require using Tuple.from(...) 31 | // instead of new Tuple([...]). 32 | private constructor(elements: T) { 33 | this.length = elements.length; 34 | Object.setPrototypeOf(elements, Tuple.prototype); 35 | return Object.freeze(elements); 36 | } 37 | } 38 | 39 | [ // Borrow some reusable properties from Array.prototype. 40 | Symbol.iterator, 41 | ].forEach((borrowed: any) => { 42 | const desc = Object.getOwnPropertyDescriptor(Array.prototype, borrowed); 43 | if (desc) Object.defineProperty(Tuple.prototype, borrowed, desc); 44 | }); 45 | 46 | export default Tuple.from; 47 | -------------------------------------------------------------------------------- /archived/tuple/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./lib", 6 | "downlevelIteration": true, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /archived/tuple/tsconfig.rollup.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "es2015", 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "3.13.1", 3 | "useNx": true, 4 | "packages": ["packages/*"], 5 | "version": "independent", 6 | "hoist": false 7 | } 8 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmScope": "wry", 3 | "tasksRunnerOptions": { 4 | "default": { 5 | "runner": "nx/tasks-runners/default", 6 | "options": { 7 | "cacheableOperations": [ 8 | "tsc", 9 | "rollup", 10 | "build" 11 | ] 12 | } 13 | } 14 | }, 15 | "targetDefaults": { 16 | "build": { 17 | "dependsOn": [ 18 | "^build" 19 | ], 20 | "outputs": [ 21 | "{projectRoot}/lib" 22 | ] 23 | }, 24 | "tsc": { 25 | "outputs": [ 26 | "{projectRoot}/lib" 27 | ] 28 | }, 29 | "rollup": { 30 | "outputs": [ 31 | "{projectRoot}/lib" 32 | ] 33 | }, 34 | "test": { 35 | "dependsOn": [ 36 | "build" 37 | ] 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wryware", 3 | "private": true, 4 | "author": "Ben Newman ", 5 | "description": "A collection of packages that are probably a little too clever", 6 | "license": "MIT", 7 | "type": "module", 8 | "scripts": { 9 | "postinstall": "lerna exec -- npm install --ignore-scripts", 10 | "build": "nx run-many --target=build --all --parallel", 11 | "test": "nx run-many --target=test --all --parallel", 12 | "deploy": "lerna publish --no-push --dist-tag next" 13 | }, 14 | "devDependencies": { 15 | "@nx/js": "^17.1.3", 16 | "@types/mocha": "^10.0.1", 17 | "@types/node": "20.4.6", 18 | "lerna": "^8.0.0", 19 | "mocha": "^10.2.0", 20 | "nx": "^17.1.3", 21 | "rimraf": "5.0.5", 22 | "rollup": "4.5.0", 23 | "source-map-support": "0.5.21", 24 | "typescript": "5.1.6" 25 | }, 26 | "engines": { 27 | "node": ">=8" 28 | }, 29 | "dependencies": { 30 | "tslib": "^2.4.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/caches/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Ignore generated TypeScript files. 40 | lib 41 | 42 | # Cache for rollup-plugin-typescript2 43 | .rpt2_cache 44 | -------------------------------------------------------------------------------- /packages/caches/.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /lib/tests 3 | tsconfig.json 4 | tsconfig.es5.json 5 | -------------------------------------------------------------------------------- /packages/caches/README.md: -------------------------------------------------------------------------------- 1 | # @wry/caches 2 | 3 | Various cache implementations, including but not limited to 4 | 5 | * `StrongCache`: A standard `Map`-like cache with a least-recently-used (LRU) 6 | eviction policy and a callback hook for removed entries. 7 | 8 | * `WeakCache`: Another LRU cache that holds its keys only weakly, so entries can be removed 9 | once no longer retained elsewhere in the application. 10 | -------------------------------------------------------------------------------- /packages/caches/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wry/caches", 3 | "version": "1.0.1", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@wry/caches", 9 | "version": "1.0.1", 10 | "license": "MIT", 11 | "dependencies": { 12 | "tslib": "^2.3.0" 13 | }, 14 | "engines": { 15 | "node": ">=8" 16 | } 17 | }, 18 | "node_modules/tslib": { 19 | "version": "2.6.2", 20 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", 21 | "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" 22 | } 23 | }, 24 | "dependencies": { 25 | "tslib": { 26 | "version": "2.6.2", 27 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", 28 | "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/caches/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wry/caches", 3 | "version": "1.0.1", 4 | "author": "Ben Newman ", 5 | "description": "Various cache implementations", 6 | "license": "MIT", 7 | "type": "module", 8 | "sideEffects": false, 9 | "main": "lib/bundle.cjs", 10 | "module": "lib/index.js", 11 | "types": "lib/index.d.ts", 12 | "keywords": [], 13 | "homepage": "https://github.com/benjamn/wryware", 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/benjamn/wryware.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/benjamn/wryware/issues" 20 | }, 21 | "scripts": { 22 | "build": "npm run clean:before && npm run tsc && npm run rollup && npm run clean:after", 23 | "clean:before": "rimraf lib", 24 | "tsc": "npm run tsc:es5 && npm run tsc:esm", 25 | "tsc:es5": "tsc -p tsconfig.es5.json", 26 | "tsc:esm": "tsc -p tsconfig.json", 27 | "rollup": "rollup -c rollup.config.js", 28 | "clean:after": "rimraf lib/es5", 29 | "prepare": "npm run build", 30 | "test:cjs": "../../shared/test.sh lib/tests/bundle.cjs", 31 | "test:esm": "../../shared/test.sh lib/tests/bundle.js", 32 | "test": "npm run test:esm && npm run test:cjs" 33 | }, 34 | "dependencies": { 35 | "tslib": "^2.3.0" 36 | }, 37 | "engines": { 38 | "node": ">=8" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/caches/rollup.config.js: -------------------------------------------------------------------------------- 1 | export { default } from "../../shared/rollup.config.js"; 2 | -------------------------------------------------------------------------------- /packages/caches/src/common.ts: -------------------------------------------------------------------------------- 1 | export interface CommonCache { 2 | has(key: K): boolean; 3 | get(key: K): V | undefined; 4 | set(key: K, value: V): V; 5 | delete(key: K): boolean; 6 | clean(): void; 7 | readonly size: number; 8 | } 9 | -------------------------------------------------------------------------------- /packages/caches/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { CommonCache } from "./common.js"; 2 | export { StrongCache } from "./strong.js"; 3 | export { WeakCache } from "./weak.js"; 4 | -------------------------------------------------------------------------------- /packages/caches/src/strong.ts: -------------------------------------------------------------------------------- 1 | import type { CommonCache } from "./common"; 2 | 3 | interface Node { 4 | key: K; 5 | value: V; 6 | newer: Node | null; 7 | older: Node | null; 8 | } 9 | 10 | function defaultDispose() {} 11 | 12 | export class StrongCache implements CommonCache { 13 | private map = new Map>(); 14 | private newest: Node | null = null; 15 | private oldest: Node | null = null; 16 | 17 | constructor( 18 | private max = Infinity, 19 | public dispose: (value: V, key: K) => void = defaultDispose, 20 | ) {} 21 | 22 | public has(key: K): boolean { 23 | return this.map.has(key); 24 | } 25 | 26 | public get(key: K): V | undefined { 27 | const node = this.getNode(key); 28 | return node && node.value; 29 | } 30 | 31 | public get size() { 32 | return this.map.size; 33 | } 34 | 35 | private getNode(key: K): Node | undefined { 36 | const node = this.map.get(key); 37 | 38 | if (node && node !== this.newest) { 39 | const { older, newer } = node; 40 | 41 | if (newer) { 42 | newer.older = older; 43 | } 44 | 45 | if (older) { 46 | older.newer = newer; 47 | } 48 | 49 | node.older = this.newest; 50 | node.older!.newer = node; 51 | 52 | node.newer = null; 53 | this.newest = node; 54 | 55 | if (node === this.oldest) { 56 | this.oldest = newer; 57 | } 58 | } 59 | 60 | return node; 61 | } 62 | 63 | public set(key: K, value: V): V { 64 | let node = this.getNode(key); 65 | if (node) { 66 | return node.value = value; 67 | } 68 | 69 | node = { 70 | key, 71 | value, 72 | newer: null, 73 | older: this.newest 74 | }; 75 | 76 | if (this.newest) { 77 | this.newest.newer = node; 78 | } 79 | 80 | this.newest = node; 81 | this.oldest = this.oldest || node; 82 | 83 | this.map.set(key, node); 84 | 85 | return node.value; 86 | } 87 | 88 | public clean() { 89 | while (this.oldest && this.map.size > this.max) { 90 | this.delete(this.oldest.key); 91 | } 92 | } 93 | 94 | public delete(key: K): boolean { 95 | const node = this.map.get(key); 96 | if (node) { 97 | if (node === this.newest) { 98 | this.newest = node.older; 99 | } 100 | 101 | if (node === this.oldest) { 102 | this.oldest = node.newer; 103 | } 104 | 105 | if (node.newer) { 106 | node.newer.older = node.older; 107 | } 108 | 109 | if (node.older) { 110 | node.older.newer = node.newer; 111 | } 112 | 113 | this.map.delete(key); 114 | this.dispose(node.value, key); 115 | 116 | return true; 117 | } 118 | 119 | return false; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /packages/caches/src/tests/main.ts: -------------------------------------------------------------------------------- 1 | import "./strong.js"; 2 | import "./weak.js"; 3 | -------------------------------------------------------------------------------- /packages/caches/src/tests/strong.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { StrongCache } from "../strong.js"; 3 | 4 | describe("least-recently-used cache", function () { 5 | it("can hold lots of elements", function () { 6 | const cache = new StrongCache; 7 | const count = 1000000; 8 | 9 | for (let i = 0; i < count; ++i) { 10 | cache.set(i, String(i)); 11 | } 12 | 13 | cache.clean(); 14 | 15 | assert.strictEqual((cache as any).map.size, count); 16 | assert.ok(cache.has(0)); 17 | assert.ok(cache.has(count - 1)); 18 | assert.strictEqual(cache.get(43), "43"); 19 | }); 20 | 21 | it("evicts excess old elements", function () { 22 | const max = 10; 23 | const evicted = []; 24 | const cache = new StrongCache(max, (value, key) => { 25 | assert.strictEqual(String(key), value); 26 | evicted.push(key); 27 | }); 28 | 29 | const count = 100; 30 | const keys = []; 31 | for (let i = 0; i < count; ++i) { 32 | cache.set(i, String(i)); 33 | keys.push(i); 34 | } 35 | 36 | cache.clean(); 37 | 38 | assert.strictEqual((cache as any).map.size, max); 39 | assert.strictEqual(evicted.length, count - max); 40 | 41 | for (let i = count - max; i < count; ++i) { 42 | assert.ok(cache.has(i)); 43 | } 44 | }); 45 | 46 | it("can cope with small max values", function () { 47 | const cache = new StrongCache(2); 48 | 49 | function check(...sequence: number[]) { 50 | cache.clean(); 51 | 52 | let entry = (cache as any).newest; 53 | const forwards = []; 54 | while (entry) { 55 | forwards.push(entry.key); 56 | entry = entry.older; 57 | } 58 | assert.deepEqual(forwards, sequence); 59 | 60 | const backwards = []; 61 | entry = (cache as any).oldest; 62 | while (entry) { 63 | backwards.push(entry.key); 64 | entry = entry.newer; 65 | } 66 | backwards.reverse(); 67 | assert.deepEqual(backwards, sequence); 68 | 69 | sequence.forEach(function (n) { 70 | assert.strictEqual((cache as any).map.get(n).value, n + 1); 71 | }); 72 | 73 | if (sequence.length > 0) { 74 | assert.strictEqual((cache as any).newest.key, sequence[0]); 75 | assert.strictEqual((cache as any).oldest.key, 76 | sequence[sequence.length - 1]); 77 | } 78 | } 79 | 80 | cache.set(1, 2); 81 | check(1); 82 | 83 | cache.set(2, 3); 84 | check(2, 1); 85 | 86 | cache.set(3, 4); 87 | check(3, 2); 88 | 89 | cache.get(2); 90 | check(2, 3); 91 | 92 | cache.set(4, 5); 93 | check(4, 2); 94 | 95 | assert.strictEqual(cache.has(1), false); 96 | assert.strictEqual(cache.get(2), 3); 97 | assert.strictEqual(cache.has(3), false); 98 | assert.strictEqual(cache.get(4), 5); 99 | 100 | cache.delete(2); 101 | check(4); 102 | cache.delete(4); 103 | check(); 104 | 105 | assert.strictEqual((cache as any).newest, null); 106 | assert.strictEqual((cache as any).oldest, null); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /packages/caches/src/tests/weak.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { WeakCache } from "../weak.js"; 3 | 4 | describe("weak least-recently-used cache", function () { 5 | it("can hold lots of elements", async function () { 6 | this.timeout(10000); 7 | const cache = new WeakCache(); 8 | const count = 1000000; 9 | const keys = []; 10 | 11 | for (let i = 0; i < count; ++i) { 12 | const key = {}; 13 | cache.set(key, String(i)); 14 | keys[i] = key; 15 | } 16 | await waitForCache(cache); 17 | 18 | cache.clean(); 19 | 20 | assert.strictEqual(cache.size, count); 21 | assert.ok(cache.has(keys[0])); 22 | assert.ok(cache.has(keys[count - 1])); 23 | assert.strictEqual(cache.get(keys[43]), "43"); 24 | }); 25 | 26 | it("evicts excess old elements", function () { 27 | const max = 10; 28 | const evicted = []; 29 | const cache = new WeakCache(max, (value, key) => { 30 | assert.strictEqual(key.valueOf(), value.valueOf()); 31 | evicted.push(key); 32 | }); 33 | 34 | const count = 100; 35 | const keys = []; 36 | for (let i = 0; i < count; ++i) { 37 | const key = new String(i); 38 | cache.set(key, String(i)); 39 | keys[i] = key; 40 | } 41 | 42 | cache.clean(); 43 | 44 | assert.strictEqual((cache as any).size, max); 45 | assert.strictEqual(evicted.length, count - max); 46 | 47 | for (let i = count - max; i < count; ++i) { 48 | assert.ok(cache.has(keys[i])); 49 | } 50 | }); 51 | 52 | it("evicts elements that are garbage collected", async function () { 53 | const cache = new WeakCache(); 54 | 55 | const count = 100; 56 | const keys: Array = []; 57 | for (let i = 0; i < count; ++i) { 58 | keys[i] = new String(i); 59 | cache.set(keys[i], String(i)); 60 | } 61 | 62 | assert.strictEqual(cache.size, 100); 63 | await waitForCache(cache); 64 | assert.strictEqual(cache.size, 100); 65 | 66 | for (let i = 0; i < 50; ++i) { 67 | keys[i] = null; 68 | } 69 | 70 | return gcPromise(() => { 71 | return cache.size > 50 ? null : () => { 72 | assert.strictEqual(cache.size, 50); 73 | assert.strictEqual(keys.length, 100); 74 | assert.strictEqual(new Set(keys).size, 51); 75 | }; 76 | }); 77 | }); 78 | 79 | function gcPromise(test: () => null | (() => void)) { 80 | return new Promise(function (resolve, reject) { 81 | function pollGC() { 82 | global.gc!(); 83 | const testCallback = test(); 84 | if (!testCallback) { 85 | setTimeout(pollGC, 20); 86 | } else try { 87 | testCallback(); 88 | resolve(); 89 | } catch (e) { 90 | reject(e); 91 | } 92 | } 93 | pollGC(); 94 | }); 95 | } 96 | 97 | it("can cope with small max values", async function () { 98 | const cache = new WeakCache(2); 99 | const keys = Array(10) 100 | .fill(null) 101 | .map((_, i) => new Number(i)); 102 | 103 | async function check(...sequence: number[]) { 104 | await waitForCache(cache); 105 | cache.clean(); 106 | 107 | let entry = cache["newest"]; 108 | const forwards = []; 109 | while (entry) { 110 | forwards.push(entry.keyRef?.deref()); 111 | entry = entry.older; 112 | } 113 | assert.deepEqual(forwards.map(Number), sequence); 114 | 115 | const backwards = []; 116 | entry = cache["oldest"]; 117 | while (entry) { 118 | backwards.push(entry.keyRef?.deref()); 119 | entry = entry.newer; 120 | } 121 | backwards.reverse(); 122 | assert.deepEqual(backwards.map(Number), sequence); 123 | 124 | sequence.forEach(function (n) { 125 | assert.strictEqual(cache["map"].get(keys[n])?.value, n + 1); 126 | }); 127 | 128 | if (sequence.length > 0) { 129 | assert.strictEqual( 130 | cache["oldest"]?.keyRef?.deref().valueOf(), 131 | sequence[sequence.length - 1] 132 | ); 133 | } 134 | } 135 | 136 | cache.set(keys[1], 2); 137 | await check(1); 138 | 139 | cache.set(keys[2], 3); 140 | await check(2, 1); 141 | 142 | cache.set(keys[3], 4); 143 | await check(3, 2); 144 | 145 | cache.get(keys[2]); 146 | await check(2, 3); 147 | 148 | cache.set(keys[4], 5); 149 | await check(4, 2); 150 | 151 | assert.strictEqual(cache.has(keys[1]), false); 152 | assert.strictEqual(cache.get(keys[2]), 3); 153 | assert.strictEqual(cache.has(keys[3]), false); 154 | assert.strictEqual(cache.get(keys[4]), 5); 155 | 156 | cache.delete(keys[2]); 157 | await check(4); 158 | cache.delete(keys[4]); 159 | await check(); 160 | 161 | assert.strictEqual((cache as any).newest, null); 162 | assert.strictEqual((cache as any).oldest, null); 163 | }); 164 | }); 165 | 166 | async function waitForCache(cache: WeakCache) { 167 | while (cache["finalizationScheduled"]) { 168 | await new Promise(queueMicrotask); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /packages/caches/src/weak.ts: -------------------------------------------------------------------------------- 1 | import type { CommonCache } from "./common"; 2 | 3 | interface PartialNode { 4 | value: V; 5 | newer: Node | null; 6 | older: Node | null; 7 | } 8 | 9 | interface UnfinalizedNode extends PartialNode { 10 | keyRef?: undefined; 11 | key: K; 12 | } 13 | 14 | interface FullNode extends PartialNode { 15 | keyRef: WeakRef; 16 | key?: undefined; 17 | } 18 | 19 | type Node = FullNode | UnfinalizedNode; 20 | 21 | function noop() {} 22 | const defaultDispose = noop; 23 | 24 | const _WeakRef = 25 | typeof WeakRef !== "undefined" 26 | ? WeakRef 27 | : (function (value: T) { 28 | return { deref: () => value } satisfies Omit< 29 | WeakRef, 30 | typeof Symbol.toStringTag 31 | >; 32 | } as any as typeof WeakRef); 33 | const _WeakMap = typeof WeakMap !== "undefined" ? WeakMap : Map; 34 | const _FinalizationRegistry = 35 | typeof FinalizationRegistry !== "undefined" 36 | ? FinalizationRegistry 37 | : (function () { 38 | return { 39 | register: noop, 40 | unregister: noop, 41 | } satisfies Omit, typeof Symbol.toStringTag>; 42 | } as any as typeof FinalizationRegistry); 43 | 44 | const finalizationBatchSize = 10024; 45 | 46 | export class WeakCache 47 | implements CommonCache 48 | { 49 | private map = new _WeakMap>(); 50 | private registry: FinalizationRegistry>; 51 | private newest: Node | null = null; 52 | private oldest: Node | null = null; 53 | private unfinalizedNodes: Set> = new Set(); 54 | private finalizationScheduled = false; 55 | public size = 0; 56 | 57 | constructor( 58 | private max = Infinity, 59 | public dispose: (value: V, key?: K) => void = defaultDispose 60 | ) { 61 | this.registry = new _FinalizationRegistry>( 62 | this.deleteNode.bind(this) 63 | ); 64 | } 65 | 66 | public has(key: K): boolean { 67 | return this.map.has(key); 68 | } 69 | 70 | public get(key: K): V | undefined { 71 | const node = this.getNode(key); 72 | return node && node.value; 73 | } 74 | 75 | private getNode(key: K): Node | undefined { 76 | const node = this.map.get(key); 77 | 78 | if (node && node !== this.newest) { 79 | const { older, newer } = node; 80 | 81 | if (newer) { 82 | newer.older = older; 83 | } 84 | 85 | if (older) { 86 | older.newer = newer; 87 | } 88 | 89 | node.older = this.newest; 90 | node.older!.newer = node; 91 | 92 | node.newer = null; 93 | this.newest = node; 94 | 95 | if (node === this.oldest) { 96 | this.oldest = newer; 97 | } 98 | } 99 | 100 | return node; 101 | } 102 | 103 | public set(key: K, value: V): V { 104 | let node = this.getNode(key); 105 | if (node) { 106 | return (node.value = value); 107 | } 108 | 109 | node = { 110 | key, 111 | value, 112 | newer: null, 113 | older: this.newest, 114 | }; 115 | 116 | if (this.newest) { 117 | this.newest.newer = node; 118 | } 119 | 120 | this.newest = node; 121 | this.oldest = this.oldest || node; 122 | 123 | this.scheduleFinalization(node); 124 | this.map.set(key, node); 125 | this.size++; 126 | 127 | return node.value; 128 | } 129 | 130 | public clean() { 131 | while (this.oldest && this.size > this.max) { 132 | this.deleteNode(this.oldest); 133 | } 134 | } 135 | 136 | private deleteNode(node: Node) { 137 | if (node === this.newest) { 138 | this.newest = node.older; 139 | } 140 | 141 | if (node === this.oldest) { 142 | this.oldest = node.newer; 143 | } 144 | 145 | if (node.newer) { 146 | node.newer.older = node.older; 147 | } 148 | 149 | if (node.older) { 150 | node.older.newer = node.newer; 151 | } 152 | 153 | this.size--; 154 | const key = node.key || (node.keyRef && node.keyRef.deref()); 155 | this.dispose(node.value, key); 156 | if (!node.keyRef) { 157 | this.unfinalizedNodes.delete(node); 158 | } else { 159 | this.registry.unregister(node); 160 | } 161 | if (key) this.map.delete(key); 162 | } 163 | 164 | public delete(key: K): boolean { 165 | const node = this.map.get(key); 166 | if (node) { 167 | this.deleteNode(node); 168 | 169 | return true; 170 | } 171 | 172 | return false; 173 | } 174 | 175 | private scheduleFinalization(node: UnfinalizedNode) { 176 | this.unfinalizedNodes.add(node); 177 | if (!this.finalizationScheduled) { 178 | this.finalizationScheduled = true; 179 | queueMicrotask(this.finalize); 180 | } 181 | } 182 | 183 | private finalize = () => { 184 | const iterator = this.unfinalizedNodes.values(); 185 | for (let i = 0; i < finalizationBatchSize; i++) { 186 | const node = iterator.next().value; 187 | if (!node) break; 188 | this.unfinalizedNodes.delete(node); 189 | const key = node.key; 190 | delete (node as unknown as FullNode).key; 191 | (node as unknown as FullNode).keyRef = new _WeakRef(key); 192 | this.registry.register(key, node, node); 193 | } 194 | if (this.unfinalizedNodes.size > 0) { 195 | queueMicrotask(this.finalize); 196 | } else { 197 | this.finalizationScheduled = false; 198 | } 199 | }; 200 | } 201 | -------------------------------------------------------------------------------- /packages/caches/tsconfig.es5.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ES5", 5 | // We want ES2020 module syntax but everything else ES5, so Rollup can still 6 | // perform tree-shaking using the ESM syntax (ES2020 for dynamic import()). 7 | "module": "ES2020", 8 | "outDir": "lib/es5" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/caches/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../shared/tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["es2015", "ES2021.WeakRef"], 5 | "rootDir": "./src", 6 | "outDir": "./lib" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/context/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Ignore generated TypeScript files. 40 | lib 41 | 42 | # Cache for rollup-plugin-typescript2 43 | .rpt2_cache 44 | -------------------------------------------------------------------------------- /packages/context/.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /lib/tests 3 | tsconfig.json 4 | tsconfig.es5.json 5 | -------------------------------------------------------------------------------- /packages/context/README.md: -------------------------------------------------------------------------------- 1 | # @wry/context 2 | 3 | Manage contextual information needed by synchronous or asynchronous 4 | tasks without explicitly passing objects around. 5 | -------------------------------------------------------------------------------- /packages/context/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wry/context", 3 | "version": "0.7.4", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@wry/context", 9 | "version": "0.7.4", 10 | "license": "MIT", 11 | "dependencies": { 12 | "tslib": "^2.3.0" 13 | }, 14 | "engines": { 15 | "node": ">=8" 16 | } 17 | }, 18 | "node_modules/tslib": { 19 | "version": "2.6.2", 20 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", 21 | "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" 22 | } 23 | }, 24 | "dependencies": { 25 | "tslib": { 26 | "version": "2.6.2", 27 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", 28 | "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/context/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wry/context", 3 | "version": "0.7.4", 4 | "author": "Ben Newman ", 5 | "description": "Manage contextual information needed by (a)synchronous tasks without explicitly passing objects around", 6 | "license": "MIT", 7 | "type": "module", 8 | "main": "lib/bundle.cjs", 9 | "module": "lib/index.js", 10 | "types": "lib/index.d.ts", 11 | "keywords": [], 12 | "homepage": "https://github.com/benjamn/wryware", 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/benjamn/wryware.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/benjamn/wryware/issues" 19 | }, 20 | "scripts": { 21 | "build": "npm run clean:before && npm run tsc && npm run rollup && npm run clean:after", 22 | "clean:before": "rimraf lib", 23 | "tsc": "npm run tsc:es5 && npm run tsc:esm", 24 | "tsc:es5": "tsc -p tsconfig.es5.json", 25 | "tsc:esm": "tsc -p tsconfig.json", 26 | "rollup": "rollup -c rollup.config.js", 27 | "clean:after": "rimraf lib/es5", 28 | "prepare": "npm run build", 29 | "test:cjs": "../../shared/test.sh lib/tests/bundle.cjs", 30 | "test:esm": "../../shared/test.sh lib/tests/bundle.js", 31 | "test": "npm run test:esm && npm run test:cjs" 32 | }, 33 | "dependencies": { 34 | "tslib": "^2.3.0" 35 | }, 36 | "engines": { 37 | "node": ">=8" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/context/rollup.config.js: -------------------------------------------------------------------------------- 1 | export { default } from "../../shared/rollup.config.js"; 2 | -------------------------------------------------------------------------------- /packages/context/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Slot } from "./slot.js"; 2 | export { Slot } 3 | export const { bind, noContext } = Slot; 4 | 5 | // Relying on the @types/node declaration of global.setTimeout can make 6 | // things tricky for dowstream projects (see PR #7). 7 | declare function setTimeout( 8 | callback: (...args: any[]) => any, 9 | ms?: number, 10 | ...args: any[] 11 | ): any; 12 | 13 | // Like global.setTimeout, except the callback runs with captured context. 14 | export { setTimeoutWithContext as setTimeout }; 15 | function setTimeoutWithContext(callback: () => any, delay: number) { 16 | return setTimeout(bind(callback), delay); 17 | } 18 | 19 | // Turn any generator function into an async function (using yield instead 20 | // of await), with context automatically preserved across yields. 21 | export function asyncFromGen< 22 | TArgs extends any[], 23 | TYield = any, 24 | TReturn = any, 25 | TNext = any, 26 | >( 27 | genFn: (...args: TArgs) => Generator 28 | ) { 29 | return function (this: any) { 30 | const gen = genFn.apply(this, arguments as any); 31 | 32 | type Method = ( 33 | this: Generator, 34 | arg: any, 35 | ) => IteratorResult; 36 | 37 | const boundNext: Method = bind(gen.next); 38 | const boundThrow: Method = bind(gen.throw!); 39 | 40 | return new Promise((resolve, reject) => { 41 | function invoke(method: Method, argument: any) { 42 | try { 43 | var result: any = method.call(gen, argument); 44 | } catch (error) { 45 | return reject(error); 46 | } 47 | const next = result.done ? resolve : invokeNext; 48 | if (isPromiseLike(result.value)) { 49 | result.value.then(next, result.done ? reject : invokeThrow); 50 | } else { 51 | next(result.value); 52 | } 53 | } 54 | const invokeNext = (value?: any) => invoke(boundNext, value); 55 | const invokeThrow = (error: any) => invoke(boundThrow, error); 56 | invokeNext(); 57 | }); 58 | } as (...args: TArgs) => Promise; 59 | } 60 | 61 | function isPromiseLike(value: any): value is PromiseLike { 62 | return value && typeof value.then === "function"; 63 | } 64 | 65 | // If you use the fibers npm package to implement coroutines in Node.js, 66 | // you should call this function at least once to ensure context management 67 | // remains coherent across any yields. 68 | const wrappedFibers: Function[] = []; 69 | export function wrapYieldingFiberMethods(Fiber: F): F { 70 | // There can be only one implementation of Fiber per process, so this array 71 | // should never grow longer than one element. 72 | if (wrappedFibers.indexOf(Fiber) < 0) { 73 | const wrap = (obj: any, method: string) => { 74 | const fn = obj[method]; 75 | obj[method] = function () { 76 | return noContext(fn, arguments as any, this); 77 | }; 78 | } 79 | // These methods can yield, according to 80 | // https://github.com/laverdet/node-fibers/blob/ddebed9b8ae3883e57f822e2108e6943e5c8d2a8/fibers.js#L97-L100 81 | wrap(Fiber, "yield"); 82 | wrap(Fiber.prototype, "run"); 83 | wrap(Fiber.prototype, "throwInto"); 84 | wrappedFibers.push(Fiber); 85 | } 86 | return Fiber; 87 | } 88 | -------------------------------------------------------------------------------- /packages/context/src/slot.ts: -------------------------------------------------------------------------------- 1 | type Context = { 2 | parent: Context | null; 3 | slots: { [slotId: string]: any }; 4 | } 5 | 6 | // This currentContext variable will only be used if the makeSlotClass 7 | // function is called, which happens only if this is the first copy of the 8 | // @wry/context package to be imported. 9 | let currentContext: Context | null = null; 10 | 11 | // This unique internal object is used to denote the absence of a value 12 | // for a given Slot, and is never exposed to outside code. 13 | const MISSING_VALUE: any = {}; 14 | 15 | let idCounter = 1; 16 | 17 | // Although we can't do anything about the cost of duplicated code from 18 | // accidentally bundling multiple copies of the @wry/context package, we can 19 | // avoid creating the Slot class more than once using makeSlotClass. 20 | const makeSlotClass = () => class Slot { 21 | // If you have a Slot object, you can find out its slot.id, but you cannot 22 | // guess the slot.id of a Slot you don't have access to, thanks to the 23 | // randomized suffix. 24 | public readonly id = [ 25 | "slot", 26 | idCounter++, 27 | Date.now(), 28 | Math.random().toString(36).slice(2), 29 | ].join(":"); 30 | 31 | public hasValue() { 32 | for (let context = currentContext; context; context = context.parent) { 33 | // We use the Slot object iself as a key to its value, which means the 34 | // value cannot be obtained without a reference to the Slot object. 35 | if (this.id in context.slots) { 36 | const value = context.slots[this.id]; 37 | if (value === MISSING_VALUE) break; 38 | if (context !== currentContext) { 39 | // Cache the value in currentContext.slots so the next lookup will 40 | // be faster. This caching is safe because the tree of contexts and 41 | // the values of the slots are logically immutable. 42 | currentContext!.slots[this.id] = value; 43 | } 44 | return true; 45 | } 46 | } 47 | if (currentContext) { 48 | // If a value was not found for this Slot, it's never going to be found 49 | // no matter how many times we look it up, so we might as well cache 50 | // the absence of the value, too. 51 | currentContext.slots[this.id] = MISSING_VALUE; 52 | } 53 | return false; 54 | } 55 | 56 | public getValue(): TValue | undefined { 57 | if (this.hasValue()) { 58 | return currentContext!.slots[this.id] as TValue; 59 | } 60 | } 61 | 62 | public withValue( 63 | value: TValue, 64 | callback: (this: TThis, ...args: TArgs) => TResult, 65 | // Given the prevalence of arrow functions, specifying arguments is likely 66 | // to be much more common than specifying `this`, hence this ordering: 67 | args?: TArgs, 68 | thisArg?: TThis, 69 | ): TResult { 70 | const slots = { 71 | __proto__: null, 72 | [this.id]: value, 73 | }; 74 | const parent = currentContext; 75 | currentContext = { parent, slots }; 76 | try { 77 | // Function.prototype.apply allows the arguments array argument to be 78 | // omitted or undefined, so args! is fine here. 79 | return callback.apply(thisArg!, args!); 80 | } finally { 81 | currentContext = parent; 82 | } 83 | } 84 | 85 | // Capture the current context and wrap a callback function so that it 86 | // reestablishes the captured context when called. 87 | static bind( 88 | callback: (this: TThis, ...args: TArgs) => TResult, 89 | ) { 90 | const context = currentContext; 91 | return function (this: TThis) { 92 | const saved = currentContext; 93 | try { 94 | currentContext = context; 95 | return callback.apply(this, arguments as any); 96 | } finally { 97 | currentContext = saved; 98 | } 99 | } as typeof callback; 100 | } 101 | 102 | // Immediately run a callback function without any captured context. 103 | static noContext( 104 | callback: (this: TThis, ...args: TArgs) => TResult, 105 | // Given the prevalence of arrow functions, specifying arguments is likely 106 | // to be much more common than specifying `this`, hence this ordering: 107 | args?: TArgs, 108 | thisArg?: TThis, 109 | ) { 110 | if (currentContext) { 111 | const saved = currentContext; 112 | try { 113 | currentContext = null; 114 | // Function.prototype.apply allows the arguments array argument to be 115 | // omitted or undefined, so args! is fine here. 116 | return callback.apply(thisArg!, args!); 117 | } finally { 118 | currentContext = saved; 119 | } 120 | } else { 121 | return callback.apply(thisArg!, args!); 122 | } 123 | } 124 | }; 125 | 126 | function maybe(fn: () => T): T | undefined { 127 | try { 128 | return fn(); 129 | } catch (ignored) {} 130 | } 131 | 132 | // We store a single global implementation of the Slot class as a permanent 133 | // non-enumerable property of the globalThis object. This obfuscation does 134 | // nothing to prevent access to the Slot class, but at least it ensures the 135 | // implementation (i.e. currentContext) cannot be tampered with, and all copies 136 | // of the @wry/context package (hopefully just one) will share the same Slot 137 | // implementation. Since the first copy of the @wry/context package to be 138 | // imported wins, this technique imposes a steep cost for any future breaking 139 | // changes to the Slot class. 140 | const globalKey = "@wry/context:Slot"; 141 | 142 | const host = 143 | // Prefer globalThis when available. 144 | // https://github.com/benjamn/wryware/issues/347 145 | maybe(() => globalThis) || 146 | // Fall back to global, which works in Node.js and may be converted by some 147 | // bundlers to the appropriate identifier (window, self, ...) depending on the 148 | // bundling target. https://github.com/endojs/endo/issues/576#issuecomment-1178515224 149 | maybe(() => global) || 150 | // Otherwise, use a dummy host that's local to this module. We used to fall 151 | // back to using the Array constructor as a namespace, but that was flagged in 152 | // https://github.com/benjamn/wryware/issues/347, and can be avoided. 153 | Object.create(null) as typeof Array; 154 | 155 | // Whichever globalHost we're using, make TypeScript happy about the additional 156 | // globalKey property. 157 | const globalHost: typeof host & { 158 | [globalKey]?: typeof Slot; 159 | } = host; 160 | 161 | export const Slot: ReturnType = 162 | globalHost[globalKey] || 163 | // Earlier versions of this package stored the globalKey property on the Array 164 | // constructor, so we check there as well, to prevent Slot class duplication. 165 | (Array as typeof globalHost)[globalKey] || 166 | (function (Slot) { 167 | try { 168 | Object.defineProperty(globalHost, globalKey, { 169 | value: Slot, 170 | enumerable: false, 171 | writable: false, 172 | // When it was possible for globalHost to be the Array constructor (a 173 | // legacy Slot dedup strategy), it was important for the property to be 174 | // configurable:true so it could be deleted. That does not seem to be as 175 | // important when globalHost is the global object, but I don't want to 176 | // cause similar problems again, and configurable:true seems safest. 177 | // https://github.com/endojs/endo/issues/576#issuecomment-1178274008 178 | configurable: true 179 | }); 180 | } finally { 181 | return Slot; 182 | } 183 | })(makeSlotClass()); 184 | -------------------------------------------------------------------------------- /packages/context/src/tests/main.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { 3 | Slot, 4 | bind, 5 | noContext, 6 | setTimeout, 7 | asyncFromGen 8 | } from "../index.js"; 9 | 10 | function repeat(s: string, times: number) { 11 | let result = ""; 12 | while (times --> 0) result += s; 13 | return result; 14 | } 15 | 16 | describe("Slot", function () { 17 | it("is importable", function () { 18 | assert.strictEqual(typeof Slot, "function"); 19 | }); 20 | 21 | it("has no value initially", function () { 22 | const slot = new Slot; 23 | assert.strictEqual(slot.hasValue(), false); 24 | assert.strictEqual(typeof slot.getValue(), "undefined"); 25 | }); 26 | 27 | it("retains values set by withValue", function () { 28 | const slot = new Slot(); 29 | 30 | const results = slot.withValue(123, () => { 31 | assert.strictEqual(slot.hasValue(), true); 32 | assert.strictEqual(slot.getValue(), 123); 33 | 34 | const results = [ 35 | slot.getValue(), 36 | slot.withValue(456, () => { 37 | assert.strictEqual(slot.hasValue(), true); 38 | return slot.getValue(); 39 | }), 40 | slot.withValue(789, () => { 41 | assert.strictEqual(slot.hasValue(), true); 42 | return slot.getValue(); 43 | }), 44 | ]; 45 | 46 | assert.strictEqual(slot.hasValue(), true); 47 | assert.strictEqual(slot.getValue(), 123); 48 | 49 | return results; 50 | }); 51 | 52 | assert.strictEqual(slot.hasValue(), false); 53 | assert.deepEqual(results, [123, 456, 789]); 54 | }); 55 | 56 | it("is not confused by other slots", function () { 57 | const stringSlot = new Slot(); 58 | const numberSlot = new Slot(); 59 | 60 | function inner() { 61 | return repeat( 62 | stringSlot.getValue()!, 63 | numberSlot.getValue()!, 64 | ); 65 | } 66 | 67 | const oneWay = stringSlot.withValue("oyez", () => { 68 | return numberSlot.withValue(3, inner); 69 | }); 70 | 71 | assert.strictEqual(stringSlot.hasValue(), false); 72 | assert.strictEqual(numberSlot.hasValue(), false); 73 | 74 | const otherWay = numberSlot.withValue(3, () => { 75 | return stringSlot.withValue("oyez", inner); 76 | }); 77 | 78 | assert.strictEqual(stringSlot.hasValue(), false); 79 | assert.strictEqual(numberSlot.hasValue(), false); 80 | 81 | assert.strictEqual(oneWay, otherWay); 82 | assert.strictEqual(oneWay, "oyezoyezoyez"); 83 | }); 84 | 85 | it("is a singleton", async function () { 86 | const cjsSlotModule = await import("../slot.js"); 87 | assert.ok(new Slot() instanceof cjsSlotModule.Slot); 88 | assert.ok(new cjsSlotModule.Slot() instanceof Slot); 89 | assert.strictEqual(cjsSlotModule.Slot, Slot); 90 | const globalKey = "@wry/context:Slot"; 91 | assert.strictEqual((global as any)[globalKey], Slot); 92 | assert.deepEqual(Object.keys(Array), []); 93 | assert.strictEqual( 94 | Object.keys(global).indexOf(globalKey), 95 | -1, 96 | ); 97 | }); 98 | 99 | it("can be subclassed", function () { 100 | class NamedSlot extends Slot { 101 | constructor(public readonly name: string) { 102 | super(); 103 | (this as any).id = name + ":" + this.id; 104 | } 105 | } 106 | 107 | const ageSlot = new NamedSlot("age"); 108 | assert.strictEqual(ageSlot.hasValue(), false); 109 | ageSlot.withValue(87, () => { 110 | assert.strictEqual(ageSlot.hasValue(), true); 111 | const age = ageSlot.getValue()!; 112 | assert.strictEqual(age, 87); 113 | assert.strictEqual(ageSlot.name, "age"); 114 | assert.ok(ageSlot.id.startsWith("age:slot:")); 115 | }); 116 | 117 | class DefaultSlot extends Slot { 118 | constructor(public readonly defaultValue: T) { 119 | super(); 120 | } 121 | 122 | hasValue() { 123 | return true; 124 | } 125 | 126 | getValue() { 127 | return super.hasValue() ? super.getValue() : this.defaultValue; 128 | } 129 | } 130 | 131 | const defaultSlot = new DefaultSlot("default"); 132 | assert.strictEqual(defaultSlot.hasValue(), true); 133 | assert.strictEqual(defaultSlot.getValue(), "default"); 134 | const check = defaultSlot.withValue("real", function () { 135 | assert.strictEqual(defaultSlot.hasValue(), true); 136 | assert.strictEqual(defaultSlot.getValue(), "real"); 137 | return bind(function () { 138 | assert.strictEqual(defaultSlot.hasValue(), true); 139 | assert.strictEqual(defaultSlot.getValue(), "real"); 140 | }); 141 | }); 142 | assert.strictEqual(defaultSlot.hasValue(), true); 143 | assert.strictEqual(defaultSlot.getValue(), "default"); 144 | check(); 145 | }); 146 | }); 147 | 148 | describe("bind", function () { 149 | it("is importable", function () { 150 | assert.strictEqual(typeof bind, "function"); 151 | }); 152 | 153 | it("preserves multiple slots", function () { 154 | const stringSlot = new Slot(); 155 | const numberSlot = new Slot(); 156 | 157 | function neither() { 158 | assert.strictEqual(stringSlot.hasValue(), false); 159 | assert.strictEqual(numberSlot.hasValue(), false); 160 | } 161 | 162 | const checks = [bind(neither)]; 163 | 164 | stringSlot.withValue("asdf", () => { 165 | function justStringAsdf() { 166 | assert.strictEqual(stringSlot.hasValue(), true); 167 | assert.strictEqual(stringSlot.getValue(), "asdf"); 168 | assert.strictEqual(numberSlot.hasValue(), false); 169 | } 170 | 171 | checks.push(bind(justStringAsdf)); 172 | 173 | numberSlot.withValue(54321, () => { 174 | checks.push(bind(function both() { 175 | assert.strictEqual(stringSlot.hasValue(), true); 176 | assert.strictEqual(stringSlot.getValue(), "asdf"); 177 | assert.strictEqual(numberSlot.hasValue(), true); 178 | assert.strictEqual(numberSlot.getValue(), 54321); 179 | })); 180 | }); 181 | 182 | stringSlot.withValue("oyez", () => { 183 | checks.push(bind(function justStringOyez() { 184 | assert.strictEqual(stringSlot.hasValue(), true); 185 | assert.strictEqual(stringSlot.getValue(), "oyez"); 186 | assert.strictEqual(numberSlot.hasValue(), false); 187 | })); 188 | 189 | numberSlot.withValue(12345, () => { 190 | checks.push(bind(function bothAgain() { 191 | assert.strictEqual(stringSlot.hasValue(), true); 192 | assert.strictEqual(stringSlot.getValue(), "oyez"); 193 | assert.strictEqual(numberSlot.hasValue(), true); 194 | assert.strictEqual(numberSlot.getValue(), 12345); 195 | })); 196 | }); 197 | }); 198 | 199 | checks.push(bind(justStringAsdf)); 200 | }); 201 | 202 | checks.push(bind(neither)); 203 | 204 | checks.forEach(check => check()); 205 | }); 206 | 207 | it("does not permit rebinding", function () { 208 | const slot = new Slot(); 209 | const bound = slot.withValue(1, () => bind(function () { 210 | assert.strictEqual(slot.hasValue(), true); 211 | assert.strictEqual(slot.getValue(), 1); 212 | return slot.getValue(); 213 | })); 214 | assert.strictEqual(bound(), 1); 215 | const rebound = slot.withValue(2, () => bind(bound)); 216 | assert.strictEqual(rebound(), 1); 217 | assert.strictEqual(slot.hasValue(), false); 218 | }); 219 | }); 220 | 221 | describe("noContext", function () { 222 | it("is importable", function () { 223 | assert.strictEqual(typeof noContext, "function"); 224 | }); 225 | 226 | it("severs context set by withValue", function () { 227 | const slot = new Slot(); 228 | const result = slot.withValue("asdf", function () { 229 | assert.strictEqual(slot.getValue(), "asdf"); 230 | return noContext(() => { 231 | assert.strictEqual(slot.hasValue(), false); 232 | return "inner"; 233 | }); 234 | }); 235 | assert.strictEqual(result, "inner"); 236 | }); 237 | 238 | it("severs bound context", function () { 239 | const slot = new Slot(); 240 | const bound = slot.withValue("asdf", function () { 241 | assert.strictEqual(slot.getValue(), "asdf"); 242 | return bind(function () { 243 | assert.strictEqual(slot.getValue(), "asdf"); 244 | return noContext(() => { 245 | assert.strictEqual(slot.hasValue(), false); 246 | return "inner"; 247 | }); 248 | }); 249 | }); 250 | assert.strictEqual(slot.hasValue(), false); 251 | assert.strictEqual(bound(), "inner"); 252 | }); 253 | 254 | it("permits reestablishing inner context values", function () { 255 | const slot = new Slot(); 256 | const bound = slot.withValue("asdf", function () { 257 | assert.strictEqual(slot.getValue(), "asdf"); 258 | return bind(function () { 259 | assert.strictEqual(slot.getValue(), "asdf"); 260 | return noContext(() => { 261 | assert.strictEqual(slot.hasValue(), false); 262 | return slot.withValue("oyez", () => { 263 | assert.strictEqual(slot.hasValue(), true); 264 | return slot.getValue(); 265 | }); 266 | }); 267 | }); 268 | }); 269 | assert.strictEqual(slot.hasValue(), false); 270 | assert.strictEqual(bound(), "oyez"); 271 | }); 272 | 273 | it("permits passing arguments and this", function () { 274 | const slot = new Slot(); 275 | const self = {}; 276 | const notSelf = {}; 277 | const result = slot.withValue(1, function (a: number) { 278 | assert.strictEqual(slot.hasValue(), true); 279 | assert.strictEqual(slot.getValue(), 1); 280 | assert.strictEqual(this, self); 281 | return noContext(function (b: number) { 282 | assert.strictEqual(slot.hasValue(), false); 283 | assert.strictEqual(this, notSelf); 284 | return slot.withValue(b, (aArg, bArg) => { 285 | assert.strictEqual(slot.hasValue(), true); 286 | assert.strictEqual(slot.getValue(), b); 287 | assert.strictEqual(this, notSelf); 288 | assert.strictEqual(a, aArg); 289 | assert.strictEqual(b, bArg); 290 | return aArg * bArg; 291 | }, [a, b], self); 292 | }, [3], notSelf); 293 | }, [2], self); 294 | assert.strictEqual(result, 2 * 3); 295 | }); 296 | 297 | it("works with Array-like (arguments) objects", function () { 298 | function multiply(a: number, b: number) { 299 | return noContext(function inner(a, b) { 300 | return a * b; 301 | }, arguments as any); 302 | } 303 | assert.strictEqual(multiply(3, 7) * 2, 42); 304 | }); 305 | }); 306 | 307 | describe("setTimeout", function () { 308 | it("is importable", function () { 309 | assert.strictEqual(typeof setTimeout, "function"); 310 | }); 311 | 312 | it("binds its callback", function () { 313 | const booleanSlot = new Slot(); 314 | const objectSlot = new Slot<{ foo: number }>(); 315 | 316 | return new Promise((resolve, reject) => { 317 | booleanSlot.withValue(true, () => { 318 | assert.strictEqual(booleanSlot.getValue(), true); 319 | objectSlot.withValue({ foo: 42 }, () => { 320 | setTimeout(function () { 321 | try { 322 | assert.strictEqual(booleanSlot.hasValue(), true); 323 | assert.strictEqual(booleanSlot.getValue(), true); 324 | assert.strictEqual(objectSlot.hasValue(), true); 325 | assert.strictEqual(objectSlot.getValue()!.foo, 42); 326 | resolve(); 327 | } catch (error) { 328 | reject(error); 329 | } 330 | }, 10); 331 | }) 332 | }); 333 | }).then(() => { 334 | assert.strictEqual(booleanSlot.hasValue(), false); 335 | assert.strictEqual(objectSlot.hasValue(), false); 336 | }); 337 | }); 338 | }); 339 | 340 | describe("asyncFromGen", function () { 341 | it("is importable", function () { 342 | assert.strictEqual(typeof asyncFromGen, "function"); 343 | }); 344 | 345 | it("works like an async function", asyncFromGen( 346 | function*(): Generator, Promise, number> { 347 | let sum = 0; 348 | const limit = yield new Promise(resolve => { 349 | setTimeout(() => resolve(10), 10); 350 | }); 351 | for (let i = 0; i < limit; ++i) { 352 | sum += yield i + 1; 353 | } 354 | assert.strictEqual(sum, 55); 355 | return Promise.resolve("ok"); 356 | }, 357 | )); 358 | 359 | it("properly handles exceptions", async function () { 360 | const fn = asyncFromGen(function*(throwee?: object) { 361 | const result = yield Promise.resolve("ok"); 362 | if (throwee) { 363 | throw yield throwee; 364 | } 365 | return result; 366 | }); 367 | 368 | const okPromise = fn(); 369 | const expected = {}; 370 | const koPromise = fn(expected); 371 | 372 | assert.strictEqual(await okPromise, "ok"); 373 | 374 | try { 375 | await koPromise; 376 | throw new Error("not reached"); 377 | } catch (error) { 378 | assert.strictEqual(error, expected); 379 | } 380 | 381 | try { 382 | await fn(Promise.resolve("oyez")); 383 | throw new Error("not reached"); 384 | } catch (thrown) { 385 | assert.strictEqual(thrown, "oyez"); 386 | } 387 | }); 388 | 389 | it("propagates contextual slot values across yields", function () { 390 | const stringSlot = new Slot(); 391 | const numberSlot = new Slot(); 392 | 393 | function checkNoValues() { 394 | assert.strictEqual(stringSlot.hasValue(), false); 395 | assert.strictEqual(numberSlot.hasValue(), false); 396 | } 397 | 398 | const inner = asyncFromGen(function*( 399 | stringValue: string, 400 | numberValue: number, 401 | ) { 402 | function checkValues() { 403 | assert.strictEqual(stringSlot.getValue(), stringValue); 404 | assert.strictEqual(numberSlot.getValue(), numberValue); 405 | } 406 | 407 | checkValues(); 408 | 409 | yield new Promise(resolve => setTimeout(function () { 410 | checkValues(); 411 | resolve(); 412 | }, 10)); 413 | 414 | checkValues(); 415 | 416 | yield new Promise(resolve => { 417 | checkValues(); 418 | resolve(); 419 | }); 420 | 421 | checkValues(); 422 | 423 | yield Promise.resolve().then(checkNoValues); 424 | 425 | checkValues(); 426 | 427 | return repeat(stringValue, numberValue); 428 | }); 429 | 430 | const outer = asyncFromGen(function*() { 431 | checkNoValues(); 432 | 433 | const oyezPromise = stringSlot.withValue("oyez", () => { 434 | return numberSlot.withValue(3, () => inner("oyez", 3)); 435 | }); 436 | 437 | checkNoValues(); 438 | 439 | const hahaPromise = numberSlot.withValue(4, () => { 440 | return stringSlot.withValue("ha", () => inner("ha", 4)); 441 | }); 442 | 443 | checkNoValues(); 444 | 445 | assert.strictEqual(yield oyezPromise, "oyezoyezoyez"); 446 | assert.strictEqual(yield hahaPromise, "hahahaha"); 447 | 448 | checkNoValues(); 449 | 450 | return Promise.all([oyezPromise, hahaPromise]); 451 | }); 452 | 453 | return outer().then(results => { 454 | checkNoValues(); 455 | 456 | assert.deepEqual(results, [ 457 | "oyezoyezoyez", 458 | "hahahaha", 459 | ]); 460 | }); 461 | }); 462 | 463 | it("allows Promise rejections to be caught", function () { 464 | const fn = asyncFromGen(function*() { 465 | try { 466 | yield Promise.reject(new Error("expected")); 467 | throw new Error("not reached"); 468 | } catch (error: any) { 469 | assert.strictEqual(error?.message, "expected"); 470 | } 471 | return "ok"; 472 | }); 473 | 474 | return fn().then(result => { 475 | assert.strictEqual(result, "ok"); 476 | }); 477 | }); 478 | }); 479 | -------------------------------------------------------------------------------- /packages/context/tsconfig.es5.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ES5", 5 | // We want ES2020 module syntax but everything else ES5, so Rollup can still 6 | // perform tree-shaking using the ESM syntax (ES2020 for dynamic import()). 7 | "module": "ES2020", 8 | "outDir": "lib/es5" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/context/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../shared/tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ES2020", 5 | "rootDir": "./src", 6 | "outDir": "./lib" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/equality/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Ignore generated TypeScript files. 40 | lib 41 | 42 | # Cache for rollup-plugin-typescript2 43 | .rpt2_cache 44 | -------------------------------------------------------------------------------- /packages/equality/.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /lib/tests 3 | tsconfig.json 4 | tsconfig.es5.json 5 | -------------------------------------------------------------------------------- /packages/equality/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ben Newman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/equality/README.md: -------------------------------------------------------------------------------- 1 | # @wry/equality 2 | 3 | Structural equality checking for JavaScript values, with correct handling 4 | of cyclic references, and minimal bundle size. 5 | -------------------------------------------------------------------------------- /packages/equality/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wry/equality", 3 | "version": "0.5.7", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@wry/equality", 9 | "version": "0.5.7", 10 | "license": "MIT", 11 | "dependencies": { 12 | "tslib": "^2.3.0" 13 | }, 14 | "engines": { 15 | "node": ">=8" 16 | } 17 | }, 18 | "node_modules/tslib": { 19 | "version": "2.6.2", 20 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", 21 | "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" 22 | } 23 | }, 24 | "dependencies": { 25 | "tslib": { 26 | "version": "2.6.2", 27 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", 28 | "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/equality/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wry/equality", 3 | "version": "0.5.7", 4 | "author": "Ben Newman ", 5 | "description": "Structural equality checking for JavaScript values", 6 | "license": "MIT", 7 | "type": "module", 8 | "main": "lib/bundle.cjs", 9 | "module": "lib/index.js", 10 | "types": "lib/index.d.ts", 11 | "keywords": [], 12 | "homepage": "https://github.com/benjamn/wryware", 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/benjamn/wryware.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/benjamn/wryware/issues" 19 | }, 20 | "scripts": { 21 | "build": "npm run clean:before && npm run tsc && npm run rollup", 22 | "clean:before": "rimraf lib", 23 | "tsc": "tsc", 24 | "rollup": "rollup -c rollup.config.js", 25 | "prepare": "npm run build", 26 | "test:cjs": "../../shared/test.sh lib/tests/bundle.cjs", 27 | "test:esm": "../../shared/test.sh lib/tests/bundle.js", 28 | "test": "npm run test:esm && npm run test:cjs" 29 | }, 30 | "dependencies": { 31 | "tslib": "^2.3.0" 32 | }, 33 | "engines": { 34 | "node": ">=8" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/equality/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { build } from "../../shared/rollup.config.js"; 2 | 3 | // This package doesn't use the lib/es5 directory, so we need to override the 4 | // default export from ../../shared/rollup.config.js. 5 | export default [ 6 | build( 7 | "lib/index.js", 8 | "lib/bundle.cjs", 9 | "cjs" 10 | ), 11 | build( 12 | "lib/tests/main.js", 13 | "lib/tests/bundle.js", 14 | "esm" 15 | ), 16 | build( 17 | "lib/tests/main.js", 18 | "lib/tests/bundle.cjs", 19 | "cjs" 20 | ), 21 | ]; 22 | -------------------------------------------------------------------------------- /packages/equality/src/index.ts: -------------------------------------------------------------------------------- 1 | const { toString, hasOwnProperty } = Object.prototype; 2 | const fnToStr = Function.prototype.toString; 3 | const previousComparisons = new Map>(); 4 | 5 | /** 6 | * Performs a deep equality check on two JavaScript values, tolerating cycles. 7 | */ 8 | export function equal(a: any, b: any): boolean { 9 | try { 10 | return check(a, b); 11 | } finally { 12 | previousComparisons.clear(); 13 | } 14 | } 15 | 16 | // Allow default imports as well. 17 | export default equal; 18 | 19 | function check(a: any, b: any): boolean { 20 | // If the two values are strictly equal, our job is easy. 21 | if (a === b) { 22 | return true; 23 | } 24 | 25 | // Object.prototype.toString returns a representation of the runtime type of 26 | // the given value that is considerably more precise than typeof. 27 | const aTag = toString.call(a); 28 | const bTag = toString.call(b); 29 | 30 | // If the runtime types of a and b are different, they could maybe be equal 31 | // under some interpretation of equality, but for simplicity and performance 32 | // we just return false instead. 33 | if (aTag !== bTag) { 34 | return false; 35 | } 36 | 37 | switch (aTag) { 38 | case '[object Array]': 39 | // Arrays are a lot like other objects, but we can cheaply compare their 40 | // lengths as a short-cut before comparing their elements. 41 | if (a.length !== b.length) return false; 42 | // Fall through to object case... 43 | case '[object Object]': { 44 | if (previouslyCompared(a, b)) return true; 45 | 46 | const aKeys = definedKeys(a); 47 | const bKeys = definedKeys(b); 48 | 49 | // If `a` and `b` have a different number of enumerable keys, they 50 | // must be different. 51 | const keyCount = aKeys.length; 52 | if (keyCount !== bKeys.length) return false; 53 | 54 | // Now make sure they have the same keys. 55 | for (let k = 0; k < keyCount; ++k) { 56 | if (!hasOwnProperty.call(b, aKeys[k])) { 57 | return false; 58 | } 59 | } 60 | 61 | // Finally, check deep equality of all child properties. 62 | for (let k = 0; k < keyCount; ++k) { 63 | const key = aKeys[k]; 64 | if (!check(a[key], b[key])) { 65 | return false; 66 | } 67 | } 68 | 69 | return true; 70 | } 71 | 72 | case '[object Error]': 73 | return a.name === b.name && a.message === b.message; 74 | 75 | case '[object Number]': 76 | // Handle NaN, which is !== itself. 77 | if (a !== a) return b !== b; 78 | // Fall through to shared +a === +b case... 79 | case '[object Boolean]': 80 | case '[object Date]': 81 | return +a === +b; 82 | 83 | case '[object RegExp]': 84 | case '[object String]': 85 | return a == `${b}`; 86 | 87 | case '[object Map]': 88 | case '[object Set]': { 89 | if (a.size !== b.size) return false; 90 | if (previouslyCompared(a, b)) return true; 91 | 92 | const aIterator = a.entries(); 93 | const isMap = aTag === '[object Map]'; 94 | 95 | while (true) { 96 | const info = aIterator.next(); 97 | if (info.done) break; 98 | 99 | // If a instanceof Set, aValue === aKey. 100 | const [aKey, aValue] = info.value; 101 | 102 | // So this works the same way for both Set and Map. 103 | if (!b.has(aKey)) { 104 | return false; 105 | } 106 | 107 | // However, we care about deep equality of values only when dealing 108 | // with Map structures. 109 | if (isMap && !check(aValue, b.get(aKey))) { 110 | return false; 111 | } 112 | } 113 | 114 | return true; 115 | } 116 | 117 | case '[object Uint16Array]': 118 | case '[object Uint8Array]': // Buffer, in Node.js. 119 | case '[object Uint32Array]': 120 | case '[object Int32Array]': 121 | case '[object Int8Array]': 122 | case '[object Int16Array]': 123 | case '[object ArrayBuffer]': 124 | // DataView doesn't need these conversions, but the equality check is 125 | // otherwise the same. 126 | a = new Uint8Array(a); 127 | b = new Uint8Array(b); 128 | // Fall through... 129 | case '[object DataView]': { 130 | let len = a.byteLength; 131 | if (len === b.byteLength) { 132 | while (len-- && a[len] === b[len]) { 133 | // Keep looping as long as the bytes are equal. 134 | } 135 | } 136 | return len === -1; 137 | } 138 | 139 | case '[object AsyncFunction]': 140 | case '[object GeneratorFunction]': 141 | case '[object AsyncGeneratorFunction]': 142 | case '[object Function]': { 143 | const aCode = fnToStr.call(a); 144 | if (aCode !== fnToStr.call(b)) { 145 | return false; 146 | } 147 | 148 | // We consider non-native functions equal if they have the same code 149 | // (native functions require === because their code is censored). 150 | // Note that this behavior is not entirely sound, since !== function 151 | // objects with the same code can behave differently depending on 152 | // their closure scope. However, any function can behave differently 153 | // depending on the values of its input arguments (including this) 154 | // and its calling context (including its closure scope), even 155 | // though the function object is === to itself; and it is entirely 156 | // possible for functions that are not === to behave exactly the 157 | // same under all conceivable circumstances. Because none of these 158 | // factors are statically decidable in JavaScript, JS function 159 | // equality is not well-defined. This ambiguity allows us to 160 | // consider the best possible heuristic among various imperfect 161 | // options, and equating non-native functions that have the same 162 | // code has enormous practical benefits, such as when comparing 163 | // functions that are repeatedly passed as fresh function 164 | // expressions within objects that are otherwise deeply equal. Since 165 | // any function created from the same syntactic expression (in the 166 | // same code location) will always stringify to the same code 167 | // according to fnToStr.call, we can reasonably expect these 168 | // repeatedly passed function expressions to have the same code, and 169 | // thus behave "the same" (with all the caveats mentioned above), 170 | // even though the runtime function objects are !== to one another. 171 | return !endsWith(aCode, nativeCodeSuffix); 172 | } 173 | } 174 | 175 | // Otherwise the values are not equal. 176 | return false; 177 | } 178 | 179 | function definedKeys(obj: TObject) { 180 | // Remember that the second argument to Array.prototype.filter will be 181 | // used as `this` within the callback function. 182 | return Object.keys(obj).filter(isDefinedKey, obj); 183 | } 184 | function isDefinedKey( 185 | this: TObject, 186 | key: keyof TObject, 187 | ) { 188 | return this[key] !== void 0; 189 | } 190 | 191 | const nativeCodeSuffix = "{ [native code] }"; 192 | 193 | function endsWith(full: string, suffix: string) { 194 | const fromIndex = full.length - suffix.length; 195 | return fromIndex >= 0 && 196 | full.indexOf(suffix, fromIndex) === fromIndex; 197 | } 198 | 199 | function previouslyCompared(a: object, b: object): boolean { 200 | // Though cyclic references can make an object graph appear infinite from the 201 | // perspective of a depth-first traversal, the graph still contains a finite 202 | // number of distinct object references. We use the previousComparisons cache 203 | // to avoid comparing the same pair of object references more than once, which 204 | // guarantees termination (even if we end up comparing every object in one 205 | // graph to every object in the other graph, which is extremely unlikely), 206 | // while still allowing weird isomorphic structures (like rings with different 207 | // lengths) a chance to pass the equality test. 208 | let bSet = previousComparisons.get(a); 209 | if (bSet) { 210 | // Return true here because we can be sure false will be returned somewhere 211 | // else if the objects are not equivalent. 212 | if (bSet.has(b)) return true; 213 | } else { 214 | previousComparisons.set(a, bSet = new Set); 215 | } 216 | bSet.add(b); 217 | return false; 218 | } 219 | -------------------------------------------------------------------------------- /packages/equality/src/tests/main.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import defaultEqual, { equal } from "../index.js"; 3 | 4 | function toStr(value: any) { 5 | try { 6 | return JSON.stringify(value); 7 | } catch { 8 | return String(value); 9 | } 10 | } 11 | 12 | function assertEqual(a: any, b: any) { 13 | assert.strictEqual(equal(a, b), true, `unexpectedly not equal(${toStr(a)}}, ${toStr(b)})`); 14 | assert.strictEqual(equal(b, a), true, `unexpectedly not equal(${toStr(b)}, ${toStr(a)})`); 15 | } 16 | 17 | function assertNotEqual(a: any, b: any) { 18 | assert.strictEqual(equal(a, b), false, `unexpectedly equal(${toStr(a)}, ${toStr(b)})`); 19 | assert.strictEqual(equal(b, a), false, `unexpectedly equal(${toStr(b)}, ${toStr(a)})`); 20 | } 21 | 22 | describe("equality", function () { 23 | it("should work with named and default imports", function () { 24 | assert.strictEqual(defaultEqual, equal); 25 | }); 26 | 27 | it("should work for primitive types", function () { 28 | assertEqual(2 + 2, 4); 29 | assertNotEqual(2 + 2, 5); 30 | 31 | assertEqual("oyez", "oyez"); 32 | assertNotEqual("oyez", "onoz"); 33 | 34 | assertEqual(null, null); 35 | assertEqual(void 0, void 1); 36 | assertEqual(NaN, NaN); 37 | 38 | assertNotEqual(void 0, null); 39 | assertNotEqual(void 0, false); 40 | assertNotEqual(false, null); 41 | assertNotEqual(0, null); 42 | assertNotEqual(0, false); 43 | assertNotEqual(0, void 0); 44 | 45 | assertEqual(123, new Number(123)); 46 | assertEqual(true, new Boolean(true)); 47 | assertEqual(false, new Boolean(false)); 48 | assertEqual("oyez", new String("oyez")); 49 | }); 50 | 51 | it("should work for arrays", function () { 52 | assertEqual([1, 2, 3], [1, 2, 3]); 53 | assertEqual([1, [2], 3], [1, [2], 3]); 54 | 55 | const a: any[] = [1]; 56 | a.push(a, 2); 57 | const b: any[] = [1]; 58 | b.push(b, 2); 59 | assertEqual(a, b); 60 | 61 | assertEqual( 62 | [1, /*hole*/, 3], 63 | [1, /*hole*/, 3], 64 | ); 65 | 66 | assertEqual( 67 | [1, /*hole*/, 3], 68 | [1, void 0, 3], 69 | ); 70 | 71 | // Not equal because the arrays are a different length. 72 | assertNotEqual( 73 | [1, 2, /*hole*/,], 74 | [1, 2], 75 | ); 76 | }); 77 | 78 | it("should work for objects", function () { 79 | assertEqual({ 80 | a: 1, 81 | b: 2, 82 | }, { 83 | b: 2, 84 | a: 1, 85 | }); 86 | 87 | assertNotEqual({ 88 | a: 1, 89 | b: 2, 90 | c: 3, 91 | }, { 92 | b: 2, 93 | a: 1, 94 | }); 95 | 96 | const a: any = {}; 97 | a.self = a; 98 | const b: any = {}; 99 | b.self = b; 100 | assertEqual(a, b); 101 | 102 | b.foo = 42; 103 | assertNotEqual(a, b); 104 | }); 105 | 106 | it("should consider undefined and missing object properties equivalent", function () { 107 | assertEqual({ 108 | a: 1, 109 | b: void 0, 110 | c: 3, 111 | }, { 112 | a: 1, 113 | c: 3, 114 | }); 115 | 116 | assertEqual({ 117 | a: void 0, 118 | b: void 0, 119 | c: void 0, 120 | }, {}); 121 | }); 122 | 123 | it("should work for Error objects", function () { 124 | assertEqual(new Error("oyez"), new Error("oyez")); 125 | assertNotEqual(new Error("oyez"), new Error("onoz")); 126 | }); 127 | 128 | it("should work for Date objects", function () { 129 | const now = new Date; 130 | const alsoNow = new Date(+now); 131 | assert.notStrictEqual(now, alsoNow); 132 | assertEqual(now, alsoNow); 133 | const later = new Date(+now + 10); 134 | assertNotEqual(now, later); 135 | }); 136 | 137 | it("should work for RegExp objects", function () { 138 | assert.notStrictEqual(/xy/, /xy/); 139 | assertEqual(/xy/img, /xy/mgi); 140 | assertNotEqual(/xy/img, /x.y/img); 141 | }); 142 | 143 | it("should work for Set objects", function () { 144 | assertEqual( 145 | new Set().add(1).add(2).add(3).add(2), 146 | new Set().add(3).add(1).add(2).add(1), 147 | ); 148 | 149 | const obj = {}; 150 | assertEqual( 151 | new Set().add(1).add(obj).add(3).add(2), 152 | new Set().add(3).add(obj).add(2).add(1), 153 | ); 154 | 155 | assertNotEqual( 156 | new Set(), 157 | new Set().add(void 0), 158 | ); 159 | }); 160 | 161 | it("should work for Map objects", function () { 162 | assertEqual( 163 | new Map().set(1, 2).set(2, 3), 164 | new Map().set(2, 3).set(1, 2), 165 | ); 166 | 167 | assertEqual( 168 | new Map().set(1, 2).set(2, 3).set(1, 0), 169 | new Map().set(2, 3).set(1, 2).set(1, 0), 170 | ); 171 | 172 | assertNotEqual( 173 | new Map().set(1, 2).set(2, 3).set(1, 0), 174 | new Map().set(2, 3).set(1, 2).set(3, 4), 175 | ); 176 | 177 | assertEqual( 178 | new Map().set(1, new Set().add(2)), 179 | new Map().set(1, new Set().add(2)), 180 | ); 181 | 182 | assertNotEqual( 183 | new Map().set(1, new Set().add(2)), 184 | new Map().set(1, new Set().add(2).add(3)), 185 | ); 186 | 187 | const a = new Map; 188 | a.set(a, a); 189 | const b = new Map; 190 | b.set(a, b); 191 | assertEqual(a, b); 192 | 193 | a.set(1, 2); 194 | b.set(1, 2); 195 | assertEqual(a, b); 196 | 197 | a.set(3, 4); 198 | assertNotEqual(a, b); 199 | }); 200 | 201 | it("should tolerate cycles", function () { 202 | const a: any[] = []; 203 | a.push(a); 204 | const b: any[] = []; 205 | b.push(b); 206 | assertEqual(a, b); 207 | assertEqual([a], b); 208 | assertEqual(a, [b]); 209 | assertEqual([a], [b]); 210 | 211 | a.push(1); 212 | b.push(1); 213 | assertEqual(a, b); 214 | assertEqual([a, 1], b); 215 | assertEqual(a, [b, 1]); 216 | 217 | const ring1 = { self: { self: { self: {} as any }}}; 218 | ring1.self.self.self.self = ring1; 219 | const ring2 = { self: { self: {} as any }}; 220 | ring2.self.self.self = ring2; 221 | assertEqual(ring1, ring2); 222 | 223 | ring1.self.self.self.self = ring1.self; 224 | assertEqual(ring1, ring2); 225 | }); 226 | 227 | it("should not care about repeated references", function () { 228 | const r = { foo: 42 }; 229 | assertEqual( 230 | [r, r, r], 231 | JSON.parse(JSON.stringify([r, r, r])), 232 | ); 233 | }); 234 | 235 | it("should equate non-native functions with the same code", function () { 236 | const fn = () => 1234; 237 | assertEqual(fn, fn); 238 | assertEqual(fn, () => 1234); 239 | 240 | // These functions are behaviorally the same, but there's no way to 241 | // decide that question statically. 242 | assertNotEqual( 243 | (a: number) => a + 1, 244 | (b: number) => b + 1, 245 | ); 246 | 247 | assertEqual( 248 | { before: 123, fn() { return 4 }, after: 321 }, 249 | { after: 321, before: 123, fn() { return 4 } }, 250 | ); 251 | 252 | assertEqual(Object.assign, Object.assign); 253 | 254 | // Since these slice methods are native functions, they happen to have 255 | // exactly the same (censored) code, but we can test their equality by 256 | // reference, since we can generally assume native functions are pure. 257 | assertNotEqual(String.prototype.slice, Array.prototype.slice); 258 | assertEqual( 259 | Function.prototype.toString.call(String.prototype.slice), 260 | Function.prototype.toString.call(Array.prototype.slice), 261 | ); 262 | }); 263 | 264 | it("should equate async functions with the same code", function () { 265 | const fn = async () => 1234; 266 | assertEqual(fn, fn); 267 | assertEqual(fn, async () => 1234); 268 | 269 | // These functions are behaviorally the same, but there's no way to 270 | // decide that question statically. 271 | assertNotEqual( 272 | async (a: number) => a + 1, 273 | async (b: number) => b + 1, 274 | ); 275 | 276 | assertEqual( 277 | { before: 123, async fn() { return 4 }, after: 321 }, 278 | { after: 321, before: 123, async fn() { return 4 } }, 279 | ); 280 | }); 281 | 282 | it("should equate generator functions with the same code", function () { 283 | const fn = function *(): Generator { return yield 1234 }; 284 | assertEqual(fn, fn); 285 | assertEqual(fn, function *(): Generator { return yield 1234 }); 286 | 287 | // These functions are behaviorally the same, but there's no way to 288 | // decide that question statically. 289 | assertNotEqual( 290 | function *(a: number): Generator { return yield a + 1 }, 291 | function *(b: number): Generator { return yield b + 1 }, 292 | ); 293 | 294 | assertEqual( 295 | { before: 123, *fn() { return 4 }, after: 321 }, 296 | { after: 321, before: 123, *fn() { return 4 } }, 297 | ); 298 | }); 299 | 300 | it("should equate async generator functions with the same code", function () { 301 | const fn = async function *(): AsyncGenerator { return await (yield 1234) }; 302 | assertEqual(fn, fn); 303 | assertEqual(fn, async function *(): AsyncGenerator { return await (yield 1234) }); 304 | 305 | // These functions are behaviorally the same, but there's no way to 306 | // decide that question statically. 307 | assertNotEqual( 308 | async function *(a: number): AsyncGenerator { return yield a + 1 }, 309 | async function *(b: number): AsyncGenerator { return yield b + 1 }, 310 | ); 311 | 312 | assertEqual( 313 | { before: 123, async *fn() { return 4 }, after: 321 }, 314 | { after: 321, before: 123, async *fn() { return 4 } }, 315 | ); 316 | }); 317 | 318 | it('should work for Array Buffers And Typed Arrays', function () { 319 | const hello = new Int8Array([1, 2, 3, 4, 5]); 320 | const world = new Int8Array([1, 2, 3, 4, 5]); 321 | const small = new Int8Array([1, 2, 3, 4]) 322 | assertEqual(new DataView(new ArrayBuffer(4)), new DataView(new ArrayBuffer(4))) 323 | assertEqual(new Int16Array([42]), new Int16Array([42])); 324 | assertEqual(new Int32Array(new ArrayBuffer(4)), new Int32Array(new ArrayBuffer(4))) 325 | assertEqual(new ArrayBuffer(2), new ArrayBuffer(2)) 326 | assertNotEqual(new Int16Array([1, 2, 3]), new Int16Array([1, 2])); 327 | assertNotEqual(new Int16Array([1, 2, 3]), new Uint16Array([1, 2, 3])) 328 | assertNotEqual(new Int16Array([1, 2, 3]), new Int8Array([1, 2, 3])) 329 | assertNotEqual(new Int32Array(8), new Uint32Array(8)); 330 | assertNotEqual(new Int32Array(new ArrayBuffer(8)), new Int32Array(Array.from({ length: 8 }))); 331 | assertNotEqual(new ArrayBuffer(1), new ArrayBuffer(2)); 332 | assertEqual(hello, world); 333 | assertEqual(hello.buffer, world.buffer); 334 | assertEqual(new DataView(hello.buffer), new DataView(world.buffer)); 335 | assertNotEqual(small, world) 336 | assertNotEqual(small.buffer, world.buffer); 337 | assertNotEqual(new DataView(small.buffer), new DataView(world.buffer)); 338 | }); 339 | 340 | it('should work with a kitchen sink', function () { 341 | const foo = { 342 | foo: 'value1', 343 | bar: new Set([1, 2, 3]), 344 | baz: /foo/i, 345 | bat: { 346 | hello: new Map([ ['hello', 'world'] ]), 347 | world: { 348 | aaa: new Map([ 349 | [{ foo: /bar/ }, 'sub sub value1'], 350 | ]), 351 | bbb: [1, 2, { prop2:1, prop:2 }, 4, 5] 352 | } 353 | }, 354 | quz: new Set([{ a:1 , b:2 }]), 355 | qut: new Date(2016, 2, 10), 356 | qar: new Uint8Array([1, 2, 3, 4, 5]), 357 | } 358 | 359 | const bar = { 360 | quz: new Set([{ a:1 , b:2 }]), 361 | baz: /foo/i, 362 | foo: 'value1', 363 | bar: new Set([1, 2, 3]), 364 | qar: new Uint8Array([1, 2, 3, 4, 5]), 365 | qut: new Date('2016/03/10'), 366 | bat: { 367 | world: { 368 | aaa: new Map([ 369 | [{ foo: /bar/ }, 'sub sub value1'], 370 | ]), 371 | bbb: [1, 2, { prop2:1, prop:2 }, 4, 5] 372 | }, 373 | hello: new Map([ ['hello', 'world'] ]) 374 | } 375 | }; 376 | 377 | assertNotEqual(foo, bar) 378 | }); 379 | 380 | describe("performance", function () { 381 | const limit = 1e6; 382 | this.timeout(20000); 383 | 384 | function check(a: any, bEqual: any, bNotEqual: any) { 385 | for (let i = 0; i < limit; ++i) { 386 | assert.strictEqual(equal(a, bEqual), true); 387 | assert.strictEqual(equal(bEqual, a), true); 388 | assert.strictEqual(equal(a, bNotEqual), false); 389 | assert.strictEqual(equal(bNotEqual, a), false); 390 | } 391 | } 392 | 393 | it("should be fast for arrays", function () { 394 | const a = [1, 2, 3]; 395 | check(a, a.slice(0), [1, 2, 4]); 396 | }); 397 | 398 | it("should be fast for objects", function () { 399 | const a = { a: 1, b: 2, c: 3 }; 400 | check(a, { ...a }, { a: 1, b: 3 }); 401 | }); 402 | 403 | it("should be fast for strings", function () { 404 | check('foo', new String('foo'), 'bar'); 405 | }); 406 | 407 | it("should be fast for functions", function () { 408 | check(() => 123, () => 123, () => 321); 409 | }); 410 | }); 411 | }); 412 | -------------------------------------------------------------------------------- /packages/equality/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../shared/tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./lib" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/task/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Ignore generated TypeScript files. 40 | lib 41 | 42 | # Cache for rollup-plugin-typescript2 43 | .rpt2_cache 44 | -------------------------------------------------------------------------------- /packages/task/.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /lib/tests 3 | tsconfig.json 4 | tsconfig.es5.json 5 | -------------------------------------------------------------------------------- /packages/task/README.md: -------------------------------------------------------------------------------- 1 | # @wry/task 2 | 3 | `Promise`-compatible asynchronous computation primitive. 4 | -------------------------------------------------------------------------------- /packages/task/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wry/task", 3 | "version": "0.3.8", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@wry/task", 9 | "version": "0.3.8", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@wry/context": "^0.7.4", 13 | "tslib": "^2.3.0" 14 | }, 15 | "engines": { 16 | "node": ">=8" 17 | } 18 | }, 19 | "../context": { 20 | "name": "@wry/context", 21 | "version": "0.7.0", 22 | "extraneous": true, 23 | "license": "MIT", 24 | "dependencies": { 25 | "tslib": "^2.3.0" 26 | }, 27 | "engines": { 28 | "node": ">=8" 29 | } 30 | }, 31 | "node_modules/@wry/context": { 32 | "version": "0.7.4", 33 | "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.4.tgz", 34 | "integrity": "sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==", 35 | "dependencies": { 36 | "tslib": "^2.3.0" 37 | }, 38 | "engines": { 39 | "node": ">=8" 40 | } 41 | }, 42 | "node_modules/tslib": { 43 | "version": "2.6.2", 44 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", 45 | "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" 46 | } 47 | }, 48 | "dependencies": { 49 | "@wry/context": { 50 | "version": "0.7.4", 51 | "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.4.tgz", 52 | "integrity": "sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==", 53 | "requires": { 54 | "tslib": "^2.3.0" 55 | } 56 | }, 57 | "tslib": { 58 | "version": "2.6.2", 59 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", 60 | "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/task/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wry/task", 3 | "version": "0.3.8", 4 | "author": "Ben Newman ", 5 | "description": "Promise-compatible asynchronous computation primitive", 6 | "license": "MIT", 7 | "type": "module", 8 | "main": "lib/bundle.cjs", 9 | "module": "lib/index.js", 10 | "types": "lib/index.d.ts", 11 | "keywords": [ 12 | "Promise", 13 | "async", 14 | "asynchronous", 15 | "task", 16 | "computation" 17 | ], 18 | "homepage": "https://github.com/benjamn/wryware", 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/benjamn/wryware.git" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/benjamn/wryware/issues" 25 | }, 26 | "scripts": { 27 | "build": "npm run clean:before && npm run tsc && npm run rollup && npm run clean:after", 28 | "clean:before": "rimraf lib", 29 | "tsc": "npm run tsc:es5 && npm run tsc:esm", 30 | "tsc:es5": "tsc -p tsconfig.es5.json", 31 | "tsc:esm": "tsc -p tsconfig.json", 32 | "rollup": "rollup -c rollup.config.js", 33 | "clean:after": "rimraf lib/es5", 34 | "prepare": "npm run build", 35 | "test:cjs": "../../shared/test.sh lib/tests/bundle.cjs", 36 | "test:esm": "../../shared/test.sh lib/tests/bundle.js", 37 | "test": "npm run test:esm && npm run test:cjs" 38 | }, 39 | "dependencies": { 40 | "@wry/context": "^0.7.4", 41 | "tslib": "^2.3.0" 42 | }, 43 | "engines": { 44 | "node": ">=8" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/task/rollup.config.js: -------------------------------------------------------------------------------- 1 | export { default } from "../../shared/rollup.config.js"; 2 | -------------------------------------------------------------------------------- /packages/task/src/index.ts: -------------------------------------------------------------------------------- 1 | import { bind } from '@wry/context'; 2 | 3 | const enum State { 4 | UNSETTLED, 5 | SETTLING, 6 | RESOLVED, 7 | REJECTED, 8 | } 9 | 10 | function isPromiseLike(value: any): value is PromiseLike { 11 | return value && typeof value.then === 'function'; 12 | } 13 | 14 | // A Task is a deliberately stripped-down Promise-compatible abstraction 15 | // with a few notable differences: 16 | // 17 | // 1. Settled Tasks can fire .then callbacks synchronously, as long as the 18 | // result (or rejection) is immediately available, and the Task has not 19 | // delivered its result asynchronously before. 20 | // 21 | // If you've ever tried to extract code containing conditional await 22 | // expressions from an async function, you know that the precise internal 23 | // timing of asynchronous code sometimes requires synchronous delivery of 24 | // results. Don't get me wrong: I'm a huge fan of the always-async 25 | // consistency of the Promise API, but it simply isn't flexible enough to 26 | // support certain patterns, especially when working with Observables, 27 | // which also have the ability to deliver results synchronously. 28 | // 29 | // 2. Tasks expose their .resolve and .reject methods publicly, so you can 30 | // call them easily outside the Task constructor. I am well aware that 31 | // the designers of the Promise API valued separating the concerns of 32 | // the producer from those of consumers, but the extra convenience is 33 | // just too nice to give up. 34 | // 35 | // 3. A Task can be turned into an equivalent Promise via task.toPromise(). 36 | 37 | export class Task implements PromiseLike { 38 | // The task.resolve and task.reject methods are similar to the Promise 39 | // resolve and reject functions, except they are exposed publicly. These 40 | // methods come pre-bound, and they are idempotent, meaning the first call 41 | // always wins, even if the argument is a Task/Promise/thenable that needs 42 | // to be resolved. 43 | public readonly resolve = 44 | (result?: TResult | PromiseLike) => this.settle(State.RESOLVED, result); 45 | public readonly reject = 46 | (reason?: any) => this.settle(State.REJECTED, reason); 47 | 48 | private state: State = State.UNSETTLED; 49 | private resultOrError?: any; 50 | // Becomes false when/if this Task delivers a result asynchronously, so 51 | // all future results can be delivered asynchronously as well. 52 | private sync = true; 53 | 54 | // More Task.WHATEVER constants can be added here as necessary. 55 | static readonly VOID = new Task(task => task.resolve()); 56 | 57 | constructor(exec?: (task: Task) => void) { 58 | // Since Tasks expose their task.resolve and task.reject functions publicly, 59 | // it's not always necessary to pass a function to the Task constructor, 60 | // though it's probably a good idea if you want to catch exceptions thrown 61 | // by the setup code. 62 | if (exec) { 63 | try { 64 | exec(this); 65 | } catch (error) { 66 | this.reject(error); 67 | } 68 | } 69 | } 70 | 71 | static resolve(value: T | PromiseLike): Task { 72 | return new this(task => task.resolve(value)); 73 | } 74 | 75 | static reject(value: T | PromiseLike): Task { 76 | return new this(task => task.reject(value)); 77 | } 78 | 79 | static all(tasks: Array>): Task { 80 | const results: T[] = []; 81 | return tasks.reduce( 82 | (prevTask: Task, nextTask, i) => prevTask.then(prevResult => { 83 | if (i > 0) results.push(prevResult); 84 | return nextTask; 85 | }), 86 | Task.VOID as Task, 87 | ).then(finalResult => { 88 | if (tasks.length > 0) results.push(finalResult); 89 | return results; 90 | }); 91 | } 92 | 93 | public then( 94 | onResolved?: ((value: TResult) => A | PromiseLike) | null, 95 | onRejected?: ((reason: any) => B | PromiseLike) | null, 96 | ): Task { 97 | if (this.sync) { 98 | if (this.state === State.RESOLVED) { 99 | return new Task(task => task.resolve( 100 | onResolved ? onResolved(this.resultOrError) : this.resultOrError, 101 | )); 102 | } 103 | 104 | if (this.state === State.REJECTED) { 105 | return new Task(task => task.resolve( 106 | onRejected ? onRejected(this.resultOrError) : this.resultOrError, 107 | )); 108 | } 109 | } 110 | 111 | // Once this Task has delivered a result asynchronously, all future 112 | // results must also be delivered asynchronously. 113 | this.sync = false; 114 | 115 | return Task.resolve(this.toPromise().then( 116 | onResolved && bind(onResolved), 117 | onRejected && bind(onRejected), 118 | )); 119 | } 120 | 121 | public catch(onRejected: (reason: any) => T | PromiseLike) { 122 | return this.then(null, onRejected); 123 | } 124 | 125 | // Although Task is intended to be lighter-weight than Promise, a Task can be 126 | // easily turned into a Promise by calling task.toPromise(), at which point 127 | // the equivalent Promise will be created. 128 | private promise?: Promise; 129 | 130 | public toPromise(): Promise { 131 | if (this.promise) { 132 | return this.promise; 133 | } 134 | 135 | switch (this.state) { 136 | case State.UNSETTLED: 137 | case State.SETTLING: 138 | return this.promise = new Promise((resolve, reject) => { 139 | const { finalize } = this; 140 | this.finalize = (state, resultOrError) => { 141 | finalize.call(this, state, resultOrError); 142 | if (state === State.RESOLVED) { 143 | resolve(resultOrError); 144 | } else { 145 | reject(resultOrError); 146 | } 147 | }; 148 | }); 149 | case State.RESOLVED: 150 | return this.promise = Promise.resolve(this.resultOrError); 151 | case State.REJECTED: 152 | return this.promise = Promise.reject(this.resultOrError); 153 | } 154 | } 155 | 156 | private settle( 157 | tentativeState: State.RESOLVED | State.REJECTED, 158 | resultOrError: any, 159 | ) { 160 | if (this.state === State.UNSETTLED) { 161 | if (tentativeState === State.RESOLVED && isPromiseLike(resultOrError)) { 162 | this.state = State.SETTLING; 163 | resultOrError.then( 164 | result => this.finalize(State.RESOLVED, result), 165 | error => this.finalize(State.REJECTED, error), 166 | ); 167 | } else { 168 | this.finalize(tentativeState, resultOrError); 169 | } 170 | } 171 | } 172 | 173 | // This method may get wrapped in toPromise so that finalization also calls 174 | // the resolve or reject functions for this.promise. 175 | private finalize( 176 | state: State.RESOLVED | State.REJECTED, 177 | resultOrError: any, 178 | ) { 179 | this.state = state; 180 | this.resultOrError = resultOrError; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /packages/task/src/tests/main.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { Slot, setTimeout } from "@wry/context"; 3 | import { Task } from "../index.js"; 4 | 5 | describe("Task", function () { 6 | it("should be importable", function () { 7 | assert.strictEqual(typeof Task, "function"); 8 | }); 9 | 10 | it("supports .then as well as .catch", function () { 11 | return new Task(task => { 12 | setTimeout(() => task.resolve("oyez"), 10); 13 | }).then(result => { 14 | assert.strictEqual(result, "oyez"); 15 | throw "catch me if you can"; 16 | }).catch(reason => { 17 | assert.strictEqual(reason, "catch me if you can"); 18 | }); 19 | }); 20 | 21 | it("supports Task.resolve and Task.reject", function () { 22 | const resolved = Task.resolve(Promise.resolve(1234)); 23 | assert.ok(resolved instanceof Task); 24 | 25 | const rejected = Task.reject(new Error("oyez")); 26 | assert.ok(rejected instanceof Task); 27 | 28 | return resolved.then(result => { 29 | assert.strictEqual(result, 1234); 30 | return rejected; 31 | }).then(() => { 32 | throw new Error("not reached"); 33 | }, error => { 34 | assert.strictEqual(error.message, "oyez"); 35 | }); 36 | }); 37 | 38 | it("works with @wry/context", function () { 39 | const nameSlot = new Slot(); 40 | function withName(name: string, exec: (task: Task) => void) { 41 | return new Task(task => nameSlot.withValue(name, () => exec(task))); 42 | } 43 | 44 | return withName("parent", task => { 45 | assert.strictEqual(nameSlot.getValue(), "parent"); 46 | task.resolve(123); 47 | }).then(oneTwoThree => withName("child", child => { 48 | assert.strictEqual(nameSlot.getValue(), "child"); 49 | 50 | const sibling = withName("sibling", task => { 51 | task.resolve(nameSlot.getValue()); 52 | }).then(result => { 53 | assert.strictEqual(result, "sibling"); 54 | assert.strictEqual(nameSlot.getValue(), "child"); 55 | }); 56 | 57 | setTimeout(() => { 58 | assert.strictEqual(nameSlot.getValue(), "child"); 59 | child.resolve(withName("grandchild", grandchild => { 60 | assert.strictEqual(nameSlot.getValue(), "grandchild"); 61 | sibling.then(() => { 62 | assert.strictEqual(nameSlot.getValue(), "grandchild"); 63 | grandchild.resolve(oneTwoThree); 64 | }, grandchild.reject); 65 | })); 66 | }, 10); 67 | 68 | })).then(result => { 69 | assert.strictEqual(nameSlot.hasValue(), false); 70 | assert.strictEqual(result, 123); 71 | }); 72 | }); 73 | 74 | it("works with Promise.all", function () { 75 | return Promise.all([ 76 | Task.VOID, 77 | new Task(task => setTimeout(() => task.resolve(123), 10)), 78 | new Task(task => task.resolve("oyez")), 79 | "not a task", 80 | ]).then(([a, b, c, d]) => { 81 | assert.strictEqual(a, void 0); 82 | assert.strictEqual(b * 2, 123 * 2); 83 | assert.strictEqual(c.slice(0, 2), "oy"); 84 | assert.strictEqual(d, "not a task"); 85 | }) 86 | }); 87 | 88 | it("supports Task.all like Promise.all", function () { 89 | // Using Task.all at the outermost layer here would be nice, but that 90 | // would require exploding the type declarations for Task.all in a manner 91 | // similar to Promise.all, which seems like a monumental hassle. 92 | return Promise.all([ 93 | Task.all([1, 2, 3]), 94 | Task.all([ 95 | Task.resolve("a"), 96 | "b", 97 | new Task(task => setTimeout(() => task.resolve("c"), 10)), 98 | Promise.resolve("d"), 99 | ]), 100 | Task.all([]), 101 | ]).then(([primitives, mixed, empty]) => { 102 | assert.deepEqual(primitives, [1, 2, 3]); 103 | assert.deepEqual(mixed, ["a", "b", "c", "d"]); 104 | assert.deepEqual(empty, []); 105 | }); 106 | }); 107 | 108 | it("should deliver synchronous results consistently", function () { 109 | const syncTask = new Task(task => { 110 | return Task.resolve("oyez").then(result => { 111 | task.resolve(result.toUpperCase()); 112 | }); 113 | }); 114 | 115 | let delivered = false; 116 | syncTask.then(result => { 117 | assert.strictEqual(result, "OYEZ"); 118 | assert.strictEqual(delivered, false); 119 | delivered = true; 120 | }); 121 | assert.strictEqual(delivered, true); 122 | 123 | const promise = syncTask.toPromise(); 124 | 125 | let deliveredAgain = false; 126 | syncTask.then(result => { 127 | assert.strictEqual(result, "OYEZ"); 128 | assert.strictEqual(deliveredAgain, false); 129 | deliveredAgain = true; 130 | }); 131 | assert.strictEqual(deliveredAgain, true); 132 | 133 | return promise.then(result => { 134 | assert.strictEqual(result, "OYEZ"); 135 | }); 136 | }); 137 | 138 | it("should deliver asynchronous results consistently", function () { 139 | let delivered = false; 140 | const asyncTask = new Task(task => { 141 | Promise.resolve(1234).then(result => { 142 | task.resolve(result); 143 | }); 144 | }).then(result => { 145 | assert.strictEqual(result, 1234); 146 | assert.strictEqual(delivered, false); 147 | delivered = true; 148 | return result + 1111; 149 | }); 150 | assert.strictEqual(delivered, false); 151 | 152 | return asyncTask.then(() => { 153 | let deliveredAgain = false; 154 | const task2 = asyncTask.then(result => { 155 | assert.strictEqual(result, 2345); 156 | assert.strictEqual(deliveredAgain, false); 157 | deliveredAgain = true; 158 | return result + 1111; 159 | }); 160 | assert.strictEqual(deliveredAgain, false); 161 | 162 | return task2.then(result => { 163 | assert.strictEqual(deliveredAgain, true); 164 | assert.strictEqual(result, 3456); 165 | }); 166 | }); 167 | }); 168 | 169 | it("task.toPromise() always returns the same promise", function () { 170 | const syncTask = Task.resolve("whatever"); 171 | const promise1 = syncTask.toPromise(); 172 | 173 | const asyncTask = Task.resolve(Promise.resolve("whenever")); 174 | const promise2 = asyncTask.toPromise(); 175 | 176 | return promise1.then(() => { 177 | return promise2.then(() => { 178 | assert.strictEqual(promise1, syncTask.toPromise()); 179 | assert.strictEqual(promise2, asyncTask.toPromise()); 180 | }); 181 | }); 182 | }); 183 | }); 184 | -------------------------------------------------------------------------------- /packages/task/tsconfig.es5.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ES5", 5 | // We want ES2020 module syntax but everything else ES5, so Rollup can still 6 | // perform tree-shaking using the ESM syntax (ES2020 for dynamic import()). 7 | "module": "ES2020", 8 | "outDir": "lib/es5" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/task/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../shared/tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./lib" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/template/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Ignore generated TypeScript files. 40 | lib 41 | 42 | # Cache for rollup-plugin-typescript2 43 | .rpt2_cache 44 | -------------------------------------------------------------------------------- /packages/template/.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /lib/tests 3 | tsconfig.json 4 | tsconfig.es5.json 5 | -------------------------------------------------------------------------------- /packages/template/README.md: -------------------------------------------------------------------------------- 1 | # @wry/template 2 | 3 | Template for creating new `@wry/*` packages. Not published to npm. 4 | -------------------------------------------------------------------------------- /packages/template/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wry/template", 3 | "version": "0.0.4", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@wry/template", 9 | "version": "0.0.4", 10 | "license": "MIT", 11 | "dependencies": { 12 | "tslib": "^2.3.0" 13 | }, 14 | "engines": { 15 | "node": ">=8" 16 | } 17 | }, 18 | "node_modules/tslib": { 19 | "version": "2.6.2", 20 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", 21 | "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" 22 | } 23 | }, 24 | "dependencies": { 25 | "tslib": { 26 | "version": "2.6.2", 27 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", 28 | "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wry/template", 3 | "private": true, 4 | "version": "0.0.4", 5 | "author": "Ben Newman ", 6 | "description": "Template for new @wry/* packages", 7 | "license": "MIT", 8 | "type": "module", 9 | "main": "lib/bundle.cjs", 10 | "module": "lib/index.js", 11 | "types": "lib/index.d.ts", 12 | "keywords": [], 13 | "homepage": "https://github.com/benjamn/wryware", 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/benjamn/wryware.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/benjamn/wryware/issues" 20 | }, 21 | "scripts": { 22 | "build": "npm run clean:before && npm run tsc && npm run rollup && npm run clean:after", 23 | "clean:before": "rimraf lib", 24 | "tsc": "npm run tsc:es5 && npm run tsc:esm", 25 | "tsc:es5": "tsc -p tsconfig.es5.json", 26 | "tsc:esm": "tsc -p tsconfig.json", 27 | "rollup": "rollup -c rollup.config.js", 28 | "clean:after": "rimraf lib/es5", 29 | "prepare": "npm run build", 30 | "test:cjs": "../../shared/test.sh lib/tests/bundle.cjs", 31 | "test:esm": "../../shared/test.sh lib/tests/bundle.js", 32 | "test": "npm run test:esm && npm run test:cjs" 33 | }, 34 | "dependencies": { 35 | "tslib": "^2.3.0" 36 | }, 37 | "engines": { 38 | "node": ">=8" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/template/rollup.config.js: -------------------------------------------------------------------------------- 1 | export { default } from "../../shared/rollup.config.js"; 2 | -------------------------------------------------------------------------------- /packages/template/src/index.ts: -------------------------------------------------------------------------------- 1 | export const name = "@wry/template"; 2 | -------------------------------------------------------------------------------- /packages/template/src/tests/main.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { name } from "../index.js"; 3 | 4 | describe("template", function () { 5 | it("should be importable", function () { 6 | assert.strictEqual(name, "@wry/template"); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/template/tsconfig.es5.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ES5", 5 | // We want ES2020 module syntax but everything else ES5, so Rollup can still 6 | // perform tree-shaking using the ESM syntax (ES2020 for dynamic import()). 7 | "module": "ES2020", 8 | "outDir": "lib/es5" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/template/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../shared/tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./lib" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/trie/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Ignore generated TypeScript files. 40 | lib 41 | 42 | # Cache for rollup-plugin-typescript2 43 | .rpt2_cache 44 | -------------------------------------------------------------------------------- /packages/trie/.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /lib/tests 3 | tsconfig.json 4 | tsconfig.es5.json 5 | -------------------------------------------------------------------------------- /packages/trie/README.md: -------------------------------------------------------------------------------- 1 | # @wry/trie 2 | 3 | A [trie](https://en.wikipedia.org/wiki/Trie) data structure that holds 4 | object keys weakly, yet can also hold non-object keys, unlike `WeakMap`. 5 | -------------------------------------------------------------------------------- /packages/trie/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wry/trie", 3 | "version": "0.5.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@wry/trie", 9 | "version": "0.5.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "tslib": "^2.3.0" 13 | }, 14 | "engines": { 15 | "node": ">=8" 16 | } 17 | }, 18 | "node_modules/tslib": { 19 | "version": "2.6.2", 20 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", 21 | "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" 22 | } 23 | }, 24 | "dependencies": { 25 | "tslib": { 26 | "version": "2.6.2", 27 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", 28 | "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/trie/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wry/trie", 3 | "version": "0.5.0", 4 | "author": "Ben Newman ", 5 | "description": "https://en.wikipedia.org/wiki/Trie", 6 | "license": "MIT", 7 | "type": "module", 8 | "main": "lib/bundle.cjs", 9 | "module": "lib/index.js", 10 | "types": "lib/index.d.ts", 11 | "keywords": [ 12 | "trie", 13 | "prefix", 14 | "weak", 15 | "dictionary", 16 | "lexicon" 17 | ], 18 | "homepage": "https://github.com/benjamn/wryware", 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/benjamn/wryware.git" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/benjamn/wryware/issues" 25 | }, 26 | "scripts": { 27 | "build": "npm run clean:before && npm run tsc && npm run rollup && npm run clean:after", 28 | "clean:before": "rimraf lib", 29 | "tsc": "npm run tsc:es5 && npm run tsc:esm", 30 | "tsc:es5": "tsc -p tsconfig.es5.json", 31 | "tsc:esm": "tsc -p tsconfig.json", 32 | "rollup": "rollup -c rollup.config.js", 33 | "clean:after": "rimraf lib/es5", 34 | "prepare": "npm run build", 35 | "test:cjs": "../../shared/test.sh lib/tests/bundle.cjs", 36 | "test:esm": "../../shared/test.sh lib/tests/bundle.js", 37 | "test": "npm run test:esm && npm run test:cjs" 38 | }, 39 | "dependencies": { 40 | "tslib": "^2.3.0" 41 | }, 42 | "engines": { 43 | "node": ">=8" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/trie/rollup.config.js: -------------------------------------------------------------------------------- 1 | export { default } from "../../shared/rollup.config.js"; 2 | -------------------------------------------------------------------------------- /packages/trie/src/index.ts: -------------------------------------------------------------------------------- 1 | // A [trie](https://en.wikipedia.org/wiki/Trie) data structure that holds 2 | // object keys weakly, yet can also hold non-object keys, unlike the 3 | // native `WeakMap`. 4 | 5 | // If no makeData function is supplied, the looked-up data will be an empty, 6 | // null-prototype Object. 7 | const defaultMakeData = () => Object.create(null); 8 | 9 | // Useful for processing arguments objects as well as arrays. 10 | const { forEach, slice } = Array.prototype; 11 | const { hasOwnProperty } = Object.prototype; 12 | 13 | export class Trie { 14 | // Since a `WeakMap` cannot hold primitive values as keys, we need a 15 | // backup `Map` instance to hold primitive keys. Both `this._weakMap` 16 | // and `this._strongMap` are lazily initialized. 17 | private weak?: WeakMap>; 18 | private strong?: Map>; 19 | private data?: Data; 20 | 21 | constructor( 22 | private weakness = true, 23 | private makeData: (array: any[]) => Data = defaultMakeData, 24 | ) {} 25 | 26 | public lookup(...array: T): Data; 27 | public lookup(): Data { 28 | return this.lookupArray(arguments); 29 | } 30 | 31 | public lookupArray(array: T): Data { 32 | let node: Trie = this; 33 | forEach.call(array, key => node = node.getChildTrie(key)); 34 | return hasOwnProperty.call(node, "data") 35 | ? node.data as Data 36 | : node.data = this.makeData(slice.call(array)); 37 | } 38 | 39 | public peek(...array: T): Data | undefined; 40 | public peek(): Data | undefined { 41 | return this.peekArray(arguments); 42 | } 43 | 44 | public peekArray(array: T): Data | undefined { 45 | let node: Trie | undefined = this; 46 | 47 | for (let i = 0, len = array.length; node && i < len; ++i) { 48 | const map = node.mapFor(array[i], false); 49 | node = map && map.get(array[i]); 50 | } 51 | 52 | return node && node.data; 53 | } 54 | 55 | public remove(...array: any[]): Data | undefined; 56 | public remove(): Data | undefined { 57 | return this.removeArray(arguments); 58 | } 59 | 60 | public removeArray(array: T): Data | undefined { 61 | let data: Data | undefined; 62 | 63 | if (array.length) { 64 | const head = array[0]; 65 | const map = this.mapFor(head, false); 66 | const child = map && map.get(head); 67 | if (child) { 68 | data = child.removeArray(slice.call(array, 1)); 69 | if (!child.data && !child.weak && !(child.strong && child.strong.size)) { 70 | map.delete(head); 71 | } 72 | } 73 | } else { 74 | data = this.data; 75 | delete this.data; 76 | } 77 | 78 | return data; 79 | } 80 | 81 | private getChildTrie(key: any) { 82 | const map = this.mapFor(key, true)!; 83 | let child = map.get(key); 84 | if (!child) map.set(key, child = new Trie(this.weakness, this.makeData)); 85 | return child; 86 | } 87 | 88 | private mapFor(key: any, create: boolean): Trie["weak" | "strong"] | undefined { 89 | return this.weakness && isObjRef(key) 90 | ? this.weak || (create ? this.weak = new WeakMap : void 0) 91 | : this.strong || (create ? this.strong = new Map : void 0); 92 | } 93 | } 94 | 95 | function isObjRef(value: any) { 96 | switch (typeof value) { 97 | case "object": 98 | if (value === null) break; 99 | // Fall through to return true... 100 | case "function": 101 | return true; 102 | } 103 | return false; 104 | } 105 | -------------------------------------------------------------------------------- /packages/trie/src/tests/main.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { Trie } from "../index.js"; 3 | 4 | describe("Trie", function () { 5 | it("can be imported", function () { 6 | assert.strictEqual(typeof Trie, "function"); 7 | }); 8 | 9 | it("can hold objects weakly", function () { 10 | const trie = new Trie(true); 11 | assert.strictEqual((trie as any).weakness, true); 12 | const obj1 = {}; 13 | assert.strictEqual( 14 | trie.lookup(obj1, 2, 3), 15 | trie.lookup(obj1, 2, 3), 16 | ); 17 | const obj2 = {}; 18 | assert.notStrictEqual( 19 | trie.lookup(1, obj2), 20 | trie.lookup(1, obj2, 3), 21 | ); 22 | assert.strictEqual((trie as any).weak.has(obj1), true); 23 | assert.strictEqual((trie as any).strong.has(obj1), false); 24 | assert.strictEqual((trie as any).strong.get(1).weak.has(obj2), true); 25 | assert.strictEqual((trie as any).strong.get(1).weak.get(obj2).strong.has(3), true); 26 | }); 27 | 28 | it("can disable WeakMap", function () { 29 | const trie = new Trie(false); 30 | assert.strictEqual((trie as any).weakness, false); 31 | const obj1 = {}; 32 | assert.strictEqual( 33 | trie.lookup(obj1, 2, 3), 34 | trie.lookup(obj1, 2, 3), 35 | ); 36 | const obj2 = {}; 37 | assert.notStrictEqual( 38 | trie.lookup(1, obj2), 39 | trie.lookup(1, obj2, 3), 40 | ); 41 | assert.strictEqual(typeof (trie as any).weak, "undefined"); 42 | assert.strictEqual((trie as any).strong.has(obj1), true); 43 | assert.strictEqual((trie as any).strong.has(1), true); 44 | assert.strictEqual((trie as any).strong.get(1).strong.has(obj2), true); 45 | assert.strictEqual((trie as any).strong.get(1).strong.get(obj2).strong.has(3), true); 46 | }); 47 | 48 | it("can produce data types other than Object", function () { 49 | const symbolTrie = new Trie(true, args => Symbol.for(args.join("."))); 50 | const s123 = symbolTrie.lookup(1, 2, 3); 51 | assert.strictEqual(s123.toString(), "Symbol(1.2.3)"); 52 | assert.strictEqual(s123, symbolTrie.lookup(1, 2, 3)); 53 | assert.strictEqual(s123, symbolTrie.lookupArray([1, 2, 3])); 54 | const sNull = symbolTrie.lookup(); 55 | assert.strictEqual(sNull.toString(), "Symbol()"); 56 | 57 | const regExpTrie = new Trie(true, args => new RegExp("^(" + args.join("|") + ")$")); 58 | const rXYZ = regExpTrie.lookup("x", "y", "z"); 59 | assert.strictEqual(rXYZ.test("w"), false); 60 | assert.strictEqual(rXYZ.test("x"), true); 61 | assert.strictEqual(rXYZ.test("y"), true); 62 | assert.strictEqual(rXYZ.test("z"), true); 63 | assert.strictEqual(String(rXYZ), "/^(x|y|z)$/"); 64 | 65 | class Data { 66 | constructor(public readonly args: any[]) {} 67 | } 68 | const dataTrie = new Trie(true, args => new Data(args)); 69 | function checkData(...args: any[]) { 70 | const data = dataTrie.lookupArray(args); 71 | assert.strictEqual(data instanceof Data, true); 72 | assert.notStrictEqual(data.args, args); 73 | assert.deepStrictEqual(data.args, args); 74 | assert.strictEqual(data, dataTrie.lookup(...args)); 75 | assert.strictEqual(data, dataTrie.lookupArray(arguments)); 76 | return data; 77 | } 78 | const datas = [ 79 | checkData(), 80 | checkData(1), 81 | checkData(1, 2), 82 | checkData(2), 83 | checkData(2, 3), 84 | checkData(true, "a"), 85 | checkData(/asdf/i, "b", function oyez() {}), 86 | ]; 87 | // Verify that all Data objects are distinct. 88 | assert.strictEqual(new Set(datas).size, datas.length); 89 | }); 90 | 91 | it("can peek at values", function () { 92 | const trie = new Trie(true, (args) => args); 93 | 94 | const obj = {}; 95 | assert.strictEqual(trie.peek(1, 2, 'x'), undefined); 96 | assert.strictEqual(trie.peek(1, 2, obj), undefined); 97 | assert.strictEqual(trie.peekArray([1, 2, 'x']), undefined); 98 | assert.strictEqual(trie.peekArray([1, 2, obj]), undefined); 99 | // peek/peekArray should not create anything on its own 100 | assert.strictEqual(trie['weak'], undefined); 101 | assert.strictEqual(trie['strong'], undefined); 102 | assert.strictEqual(trie['data'], undefined); 103 | 104 | const data1 = trie.lookup(1, 2, 'x'); 105 | const data2 = trie.lookup(1, 2, obj); 106 | 107 | assert.strictEqual(trie.peek(1, 2, 'x'), data1); 108 | assert.strictEqual(trie.peek(1, 2, obj), data2); 109 | assert.strictEqual(trie.peekArray([1, 2, 'x']), data1); 110 | assert.strictEqual(trie.peekArray([1, 2, obj]), data2); 111 | }); 112 | 113 | describe("can remove values", function () { 114 | it("will remove values", () => { 115 | const trie = new Trie(true, (args) => args); 116 | 117 | trie.lookup(1, 2, "x"); 118 | trie.remove(1, 2, "x"); 119 | assert.strictEqual(trie.peek(1, 2, "x"), undefined); 120 | }); 121 | 122 | it("removing will return the value", () => { 123 | const trie = new Trie(true, (args) => args); 124 | 125 | const data = trie.lookup(1, 2, "x"); 126 | assert.strictEqual(trie.remove(1, 2, "x"), data); 127 | }); 128 | 129 | it("will remove empty parent nodes", () => { 130 | const trie = new Trie(true, (args) => args); 131 | 132 | const data = trie.lookup(1, 2, "x"); 133 | assert.strictEqual(trie.peek(1, 2, "x"), data); 134 | assert.equal(pathExistsInTrie(trie, 1, 2, "x"), true); 135 | assert.strictEqual(trie.remove(1, 2, "x"), data); 136 | assert.equal(pathExistsInTrie(trie, 1), false); 137 | }); 138 | 139 | it("will not remove parent nodes with other children", () => { 140 | const trie = new Trie(true, (args) => args); 141 | 142 | trie.lookup(1, 2, "x"); 143 | const data = trie.lookup(1, 2); 144 | trie.remove(1, 2, "x"); 145 | assert.strictEqual(trie.peek(1, 2, "x"), undefined); 146 | assert.strictEqual(trie.peek(1, 2), data); 147 | }); 148 | 149 | it("will remove data, not the full node, if a node still has children", () => { 150 | const trie = new Trie(true, (args) => args); 151 | 152 | trie.lookup(1, 2); 153 | const data = trie.lookup(1, 2, "x"); 154 | trie.remove(1, 2); 155 | assert.strictEqual(trie.peek(1, 2), undefined); 156 | assert.strictEqual(trie.peek(1, 2, "x"), data); 157 | }); 158 | 159 | it("will remove direct children", () => { 160 | const trie = new Trie(true, (args) => args); 161 | 162 | trie.lookup(1); 163 | trie.remove(1); 164 | assert.strictEqual(trie.peek(1), undefined); 165 | }); 166 | 167 | it("will remove nodes from WeakMaps", () => { 168 | const trie = new Trie(true, (args) => args); 169 | const obj = {}; 170 | const data = trie.lookup(1, obj, "x"); 171 | assert.equal(pathExistsInTrie(trie, 1), true); 172 | assert.strictEqual(trie.remove(1, obj, "x"), data); 173 | assert.strictEqual(trie.peek(1, obj, "x"), undefined); 174 | assert.equal(pathExistsInTrie(trie, 1, obj), false); 175 | }); 176 | 177 | it("will not remove nodes if they contain an (even empty) WeakMap", () => { 178 | const trie = new Trie(true, (args) => args); 179 | const obj = {}; 180 | 181 | const data = trie.lookup(1, 2, "x"); 182 | trie.lookup(1, obj); 183 | trie.remove(1, obj); 184 | 185 | assert.strictEqual(trie.peek(1, 2, "x"), data); 186 | assert.equal(pathExistsInTrie(trie, 1), true); 187 | assert.equal(pathExistsInTrie(trie, 1, 2), true); 188 | assert.strictEqual(trie.remove(1, 2, "x"), data); 189 | assert.equal(pathExistsInTrie(trie, 1), true); 190 | assert.equal(pathExistsInTrie(trie, 1, 2), false); 191 | }); 192 | }); 193 | 194 | function pathExistsInTrie(trie: Trie, ...path: any[]) { 195 | return ( 196 | path.reduce((node: Trie | undefined, key: any) => { 197 | const map: Trie["weak" | "strong"] = 198 | // not the full implementation but enough for a test 199 | trie["weakness"] && typeof key === "object" 200 | ? node?.["weak"] 201 | : node?.["strong"]; 202 | 203 | return map?.get(key); 204 | }, trie) !== undefined 205 | ); 206 | } 207 | }); 208 | -------------------------------------------------------------------------------- /packages/trie/tsconfig.es5.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ES5", 5 | // We want ES2020 module syntax but everything else ES5, so Rollup can still 6 | // perform tree-shaking using the ESM syntax (ES2020 for dynamic import()). 7 | "module": "ES2020", 8 | "outDir": "lib/es5" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/trie/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../shared/tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./lib" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /shared/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from "fs/promises"; 2 | 3 | export const globals = { 4 | __proto__: null, 5 | tslib: "tslib", 6 | assert: "assert", 7 | // "@wry/context": "wryContext", 8 | // "@wry/trie": "wryTrie", 9 | // "@wry/task": "wryTask", 10 | // "@wry/equality": "wryEquality", 11 | }; 12 | 13 | function external(id) { 14 | return id in globals; 15 | } 16 | 17 | export function build(input, output, format) { 18 | return { 19 | input, 20 | external, 21 | output: { 22 | file: output, 23 | format, 24 | sourcemap: true, 25 | globals 26 | }, 27 | ...(output.endsWith(".cjs") ? { plugins: [ 28 | { // Inspired by https://github.com/apollographql/apollo-client/pull/9716, 29 | // this workaround ensures compatibility with versions of React Native 30 | // that refuse to load .cjs modules as CommonJS (to be fixed in v0.72): 31 | name: "copy *.cjs to *.cjs.native.js", 32 | async writeBundle({ file }) { 33 | const buffer = await readFile(file); 34 | await writeFile(file + ".native.js", buffer); 35 | }, 36 | }, 37 | ]} : null), 38 | }; 39 | } 40 | 41 | export default [ 42 | build( 43 | "lib/es5/index.js", 44 | "lib/bundle.cjs", 45 | "cjs" 46 | ), 47 | build( 48 | "lib/tests/main.js", 49 | "lib/tests/bundle.js", 50 | "esm" 51 | ), 52 | build( 53 | "lib/es5/tests/main.js", 54 | "lib/tests/bundle.cjs", 55 | "cjs" 56 | ), 57 | ]; 58 | -------------------------------------------------------------------------------- /shared/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | node --expose-gc "$(dirname $0)/../node_modules/.bin/mocha" \ 4 | --reporter spec \ 5 | --full-trace \ 6 | --require source-map-support/register \ 7 | $@ 8 | -------------------------------------------------------------------------------- /shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ES2020", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "declaration": true, 8 | "importHelpers": true, 9 | "lib": ["es2015"], 10 | "types": ["node", "mocha"], 11 | "strict": true, 12 | "noImplicitAny": true, 13 | "noUnusedLocals": true, 14 | "esModuleInterop": true 15 | } 16 | } 17 | --------------------------------------------------------------------------------