├── scripts ├── imported │ ├── config.js │ ├── package.json │ └── index.js ├── example.js ├── log-version.js ├── nop │ ├── package.json │ └── index.js ├── correct-import-extensions.js └── post-build.js ├── .npmignore ├── .gitignore ├── bun.lockb ├── src ├── index.ts ├── tests │ ├── index.tsx │ ├── big-tree.tsx │ └── memo.tsx ├── types │ └── jsx.d.ts ├── is.ts └── memo.ts ├── .nycrc ├── CONTRIBUTING.md ├── tsconfig.json ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github └── workflows │ ├── test-actions.yml │ ├── release-actions.yml │ └── codeql-analysis.yml ├── LICENSE.md ├── README.md ├── package.json ├── import-map-deno.json └── CODE-OF-CONDUCT.md /scripts/imported/config.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | node_modules 4 | coverage -------------------------------------------------------------------------------- /scripts/example.js: -------------------------------------------------------------------------------- 1 | import "../esnext/example/index.js"; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | node_modules 4 | esnext 5 | coverage -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualstate/memo/main/bun.lockb -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "@virtualstate/composite-key"; 2 | export * from "./memo"; 3 | -------------------------------------------------------------------------------- /src/tests/index.tsx: -------------------------------------------------------------------------------- 1 | export default 1; 2 | 3 | await import("./memo"); 4 | await import("./big-tree"); -------------------------------------------------------------------------------- /src/types/jsx.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace JSX { 2 | interface IntrinsicElements 3 | extends Record> {} 4 | } 5 | -------------------------------------------------------------------------------- /scripts/log-version.js: -------------------------------------------------------------------------------- 1 | import("fs") 2 | .then(({ promises }) => promises.readFile("package.json")) 3 | .then(JSON.parse) 4 | .then(({ version }) => console.log(version)); 5 | -------------------------------------------------------------------------------- /scripts/nop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "main": "./index.js", 4 | "type": "module", 5 | "exports": { 6 | ".": "./index.js", 7 | "./": "./index.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /scripts/imported/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "main": "./index.js", 4 | "type": "module", 5 | "exports": { 6 | ".": "./index.js", 7 | "./": "./index.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | ], 4 | "reporter": [ 5 | "clover", 6 | "json-summary", 7 | "html", 8 | "text-summary" 9 | ], 10 | "branches": 80, 11 | "lines": 80, 12 | "functions": 80, 13 | "statements": 80 14 | } 15 | -------------------------------------------------------------------------------- /scripts/nop/index.js: -------------------------------------------------------------------------------- 1 | export const listenAndServe = undefined; 2 | export const createServer = undefined; 3 | 4 | /** 5 | * @type {any} 6 | */ 7 | const on = () => {}; 8 | /** 9 | * @type {any} 10 | */ 11 | const env = {}; 12 | 13 | /*** 14 | * @type {any} 15 | */ 16 | export default { 17 | env, 18 | on, 19 | exit(arg) {}, 20 | }; 21 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make by way of an issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | We are open to all ideas big or small, and are greatly appreciative of any and all contributions. 7 | 8 | Please note we have a code of conduct, please follow it in all your interactions with the project. 9 | -------------------------------------------------------------------------------- /scripts/imported/index.js: -------------------------------------------------------------------------------- 1 | /* c8 ignore start */ 2 | 3 | const initialImportPath = 4 | getConfig()["@virtualstate/app-history/test/imported/path"] ?? 5 | "@virtualstate/app-history"; 6 | 7 | if (typeof initialImportPath !== "string") 8 | throw new Error("Expected string import path"); 9 | 10 | export const { AppHistory } = await import(initialImportPath); 11 | 12 | export function getConfig() { 13 | return { 14 | ...getNodeConfig(), 15 | }; 16 | } 17 | 18 | function getNodeConfig() { 19 | if (typeof process === "undefined") return {}; 20 | return JSON.parse(process.env.TEST_CONFIG ?? "{}"); 21 | } 22 | /* c8 ignore end */ 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "lib": ["es2018", "esnext", "dom"], 5 | "types": ["jest", "node"], 6 | "esModuleInterop": true, 7 | "target": "esnext", 8 | "noImplicitAny": true, 9 | "downlevelIteration": true, 10 | "moduleResolution": "node", 11 | "declaration": true, 12 | "sourceMap": true, 13 | "outDir": "esnext", 14 | "baseUrl": "./src", 15 | "jsx": "react", 16 | "jsxFactory": "h", 17 | "jsxFragmentFactory": "createFragment", 18 | "allowJs": true, 19 | "paths": {} 20 | }, 21 | "include": ["src/**/*"], 22 | "typeRoots": ["./node_modules/@types", "src/types"], 23 | "exclude": ["node_modules/@opennetwork/vdom"] 24 | } 25 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.192.0/containers/typescript-node/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] Node.js version: 16, 14, 12 4 | ARG VARIANT="16-buster" 5 | FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT} 6 | 7 | # [Optional] Uncomment this section to install additional OS packages. 8 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 9 | # && apt-get -y install --no-install-recommends 10 | 11 | # [Optional] Uncomment if you want to install an additional version of node using nvm 12 | # ARG EXTRA_NODE_VERSION=10 13 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 14 | 15 | # [Optional] Uncomment if you want to install more global node packages 16 | # RUN su node -c "npm install -g " 17 | -------------------------------------------------------------------------------- /.github/workflows/test-actions.yml: -------------------------------------------------------------------------------- 1 | name: test-actions 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | env: 8 | NO_COVERAGE_BADGE_UPDATE: 1 9 | FLAGS: FETCH_SERVICE_DISABLE,POST_CONFIGURE_TEST,PLAYWRIGHT,CONTINUE_ON_ERROR 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 0 14 | - uses: actions/setup-node@v2 15 | with: 16 | node-version: "16.x" 17 | registry-url: "https://registry.npmjs.org" 18 | - uses: denoland/setup-deno@v1 19 | with: 20 | deno-version: "v1.x" 21 | - uses: antongolub/action-setup-bun@v1 22 | - run: | 23 | yarn install 24 | bun install 25 | npx playwright install-deps 26 | - run: yarn build 27 | # yarn coverage === c8 + yarn test 28 | - run: yarn coverage 29 | - run: yarn test:deno 30 | - run: yarn test:bun 31 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.192.0/containers/typescript-node 3 | { 4 | "name": "Node.js & TypeScript", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | // Update 'VARIANT' to pick a Node version: 12, 14, 16 8 | "args": { 9 | "VARIANT": "16" 10 | } 11 | }, 12 | 13 | // Set *default* container specific settings.json values on container create. 14 | "settings": {}, 15 | 16 | // Add the IDs of extensions you want installed when the container is created. 17 | "extensions": ["dbaeumer.vscode-eslint"], 18 | 19 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 20 | // "forwardPorts": [], 21 | 22 | // Use 'postCreateCommand' to run commands after the container is created. 23 | // "postCreateCommand": "yarn install", 24 | 25 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 26 | "remoteUser": "node" 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Axiom Applied Technologies and Development Limited 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 | # `@virtualstate/memo` 2 | 3 | [memo](https://github.com/tc39/proposal-function-memo) for JSX values. 4 | 5 | [//]: # (badges) 6 | 7 | ### Support 8 | 9 | ![Node.js supported](https://img.shields.io/badge/node-%3E%3D16.0.0-blue) ![Deno supported](https://img.shields.io/badge/deno-%3E%3D1.17.0-blue) ![Bun supported](https://img.shields.io/badge/bun-%3E%3D0.1.8-blue) 10 | 11 | ### Test Coverage 12 | 13 | ![100%25 lines covered](https://img.shields.io/badge/lines-100%25-brightgreen) ![100%25 statements covered](https://img.shields.io/badge/statements-100%25-brightgreen) ![100%25 functions covered](https://img.shields.io/badge/functions-100%25-brightgreen) ![100%25 branches covered](https://img.shields.io/badge/branches-100%25-brightgreen) 14 | 15 | [//]: # (badges) 16 | 17 | ## Usage 18 | 19 | ```typescript jsx 20 | import { memo } from "@virtualstate/memo"; 21 | import { descendants } from "@virtualstate/focus"; 22 | import { Component, Body } from "./somewhere"; 23 | 24 | const tree = memo( 25 | <> 26 | 27 |
28 | 29 |
30 | 31 | ); 32 | 33 | // On the first usage, the tree will be memo'd as it is read 34 | console.log(await descendants(tree)); 35 | 36 | // Uses the memo'd version, Component & Body aren't called again 37 | console.log(await descendants(tree)); 38 | ``` -------------------------------------------------------------------------------- /src/is.ts: -------------------------------------------------------------------------------- 1 | /* c8 ignore start */ 2 | 3 | import { isLike } from "@virtualstate/focus"; 4 | 5 | export function isAsyncIterable(value: unknown): value is AsyncIterable { 6 | return !!( 7 | isLike>(value) && 8 | typeof value[Symbol.asyncIterator] === "function" 9 | ); 10 | } 11 | 12 | export function isIterable(value: unknown): value is Iterable { 13 | return !!( 14 | isLike>(value) && 15 | typeof value[Symbol.iterator] === "function" 16 | ); 17 | } 18 | 19 | export function isIteratorYieldResult( 20 | value: unknown 21 | ): value is IteratorYieldResult { 22 | return !!( 23 | isLike>>(value) && 24 | typeof value.done === "boolean" && 25 | !value.done 26 | ); 27 | } 28 | 29 | export function isIteratorResult( 30 | value: unknown 31 | ): value is IteratorYieldResult { 32 | return !!( 33 | isLike>>(value) && typeof value.done === "boolean" 34 | ); 35 | } 36 | 37 | export function isRejected( 38 | value: PromiseSettledResult 39 | ): value is PromiseRejectedResult; 40 | export function isRejected( 41 | value: PromiseSettledResult 42 | ): value is R; 43 | export function isRejected( 44 | value: PromiseSettledResult 45 | ): value is R { 46 | return value?.status === "rejected"; 47 | } 48 | 49 | export function isFulfilled( 50 | value: PromiseSettledResult 51 | ): value is PromiseFulfilledResult { 52 | return value?.status === "fulfilled"; 53 | } 54 | 55 | export function isPromise(value: unknown): value is Promise { 56 | return isLike>(value) && typeof value.then === "function"; 57 | } 58 | 59 | export function isArray(value: unknown): value is T[] { 60 | return Array.isArray(value); 61 | } 62 | 63 | export function isDefined(value?: T): value is T { 64 | return typeof value !== "undefined"; 65 | } 66 | -------------------------------------------------------------------------------- /.github/workflows/release-actions.yml: -------------------------------------------------------------------------------- 1 | name: release-actions 2 | on: 3 | push: 4 | branches: 5 | - main 6 | release: 7 | types: 8 | - created 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | env: 13 | NO_COVERAGE_BADGE_UPDATE: 1 14 | steps: 15 | - uses: actions/checkout@v2 16 | with: 17 | fetch-depth: 0 18 | - uses: actions/setup-node@v2 19 | with: 20 | node-version: "16.x" 21 | registry-url: "https://registry.npmjs.org" 22 | - uses: denoland/setup-deno@v1 23 | with: 24 | deno-version: "v1.x" 25 | - uses: antongolub/action-setup-bun@v1 26 | - run: | 27 | yarn install 28 | bun install 29 | - run: yarn build 30 | # yarn coverage === c8 + yarn test 31 | - run: yarn coverage 32 | - run: yarn test:deno 33 | - run: yarn test:bun 34 | - name: Package Registry Publish - npm 35 | run: | 36 | git config user.name "${{ github.actor }}" 37 | git config user.email "${{ github.actor}}@users.noreply.github.com" 38 | npm set "registry=https://registry.npmjs.org/" 39 | npm set "@virtualstate:registry=https://registry.npmjs.org/" 40 | npm set "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" 41 | npm publish --access=public 42 | continue-on-error: true 43 | env: 44 | YARN_TOKEN: ${{ secrets.YARN_TOKEN }} 45 | NPM_TOKEN: ${{ secrets.YARN_TOKEN }} 46 | NODE_AUTH_TOKEN: ${{ secrets.YARN_TOKEN }} 47 | - uses: actions/setup-node@v2 48 | with: 49 | node-version: "16.x" 50 | registry-url: "https://npm.pkg.github.com" 51 | - name: Package Registry Publish - GitHub 52 | run: | 53 | git config user.name "${{ github.actor }}" 54 | git config user.email "${{ github.actor}}@users.noreply.github.com" 55 | npm set "registry=https://npm.pkg.github.com/" 56 | npm set "@virtualstate:registry=https://npm.pkg.github.com/virtualstate" 57 | npm set "//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}" 58 | npm publish --access=public 59 | env: 60 | YARN_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | NPM_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | continue-on-error: true 64 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '15 10 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@virtualstate/memo", 3 | "version": "1.8.0", 4 | "main": "./esnext/index.js", 5 | "module": "./esnext/index.js", 6 | "types": "./esnext/index.d.ts", 7 | "type": "module", 8 | "sideEffects": false, 9 | "keywords": [], 10 | "exports": { 11 | ".": "./esnext/index.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/virtualstate/memo.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/virtualstate/memo/issues" 19 | }, 20 | "homepage": "https://github.com/virtualstate/memo#readme", 21 | "author": "Fabian Cook ", 22 | "license": "MIT", 23 | "dependencies": { 24 | "@virtualstate/composite-key": "^1.0.0", 25 | "@virtualstate/focus": "^1.1.1", 26 | "@virtualstate/promise": "^1.1.6", 27 | "abort-controller": "^3.0.0", 28 | "uuid": "^8.3.2", 29 | "whatwg-url": "^9.1.0" 30 | }, 31 | "devDependencies": { 32 | "@opennetwork/http-representation": "^3.0.0", 33 | "@types/chance": "^1.1.3", 34 | "@types/jest": "^27.0.1", 35 | "@types/mkdirp": "^1.0.2", 36 | "@types/node": "^17.0.1", 37 | "@types/rimraf": "^3.0.2", 38 | "@types/uuid": "^8.3.3", 39 | "@types/whatwg-url": "^8.2.1", 40 | "@virtualstate/dom": "^2.46.0", 41 | "@virtualstate/examples": "^2.46.0", 42 | "@virtualstate/fringe": "^2.46.1", 43 | "@virtualstate/hooks": "^2.46.0", 44 | "@virtualstate/hooks-extended": "^2.46.0", 45 | "@virtualstate/union": "^2.46.0", 46 | "@virtualstate/x": "^2.46.0", 47 | "c8": "^7.12.0", 48 | "chance": "^1.1.8", 49 | "cheerio": "^1.0.0-rc.10", 50 | "core-js": "^3.17.2", 51 | "dom-lite": "^20.2.0", 52 | "filehound": "^1.17.4", 53 | "jest": "^27.1.0", 54 | "jest-playwright-preset": "^1.7.0", 55 | "mkdirp": "^1.0.4", 56 | "playwright": "^1.17.1", 57 | "rimraf": "^3.0.2", 58 | "ts-jest": "^27.0.5", 59 | "ts-node": "^10.2.1", 60 | "typescript": "^4.7.4", 61 | "urlpattern-polyfill": "^1.0.0-rc2", 62 | "v8-to-istanbul": "^8.1.0" 63 | }, 64 | "scripts": { 65 | "build": "rm -rf esnext && tsc", 66 | "postbuild": "mkdir -p coverage && node scripts/post-build.js", 67 | "prepublishOnly": "npm run build", 68 | "test": "yarn build && node --enable-source-maps esnext/tests/index.js", 69 | "test:deno": "yarn build && deno run --allow-read --allow-net --import-map=import-map-deno.json esnext/tests/index.js", 70 | "test:bun": "yarn build && bun esnext/tests/index.js", 71 | "test:deno:r": "yarn build && deno run -r --allow-read --allow-net --import-map=import-map-deno.json esnext/tests/index.js", 72 | "test:inspect": "yarn build && node --enable-source-maps --inspect-brk esnext/tests/index.js", 73 | "coverage": "yarn build && c8 node esnext/tests/index.js && yarn postbuild" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /import-map-deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "@opennetwork/http-representation": "https://cdn.skypack.dev/@opennetwork/http-representation", 4 | "@virtualstate/astro-renderer": "https://cdn.skypack.dev/@virtualstate/astro-renderer", 5 | "@virtualstate/dom": "https://cdn.skypack.dev/@virtualstate/dom", 6 | "@virtualstate/examples": "https://cdn.skypack.dev/@virtualstate/examples", 7 | "@virtualstate/fringe": "https://cdn.skypack.dev/@virtualstate/fringe", 8 | "@virtualstate/focus": "https://cdn.skypack.dev/@virtualstate/focus", 9 | "@virtualstate/focus/static-h": "https://cdn.skypack.dev/@virtualstate/focus/static-h", 10 | "@virtualstate/hooks": "https://cdn.skypack.dev/@virtualstate/hooks", 11 | "@virtualstate/hooks-extended": "https://cdn.skypack.dev/@virtualstate/hooks-extended", 12 | "@virtualstate/union": "https://cdn.skypack.dev/@virtualstate/union", 13 | "@virtualstate/x": "https://cdn.skypack.dev/@virtualstate/x", 14 | "@virtualstate/promise": "https://cdn.skypack.dev/@virtualstate/promise", 15 | "@virtualstate/composite-key": "https://cdn.skypack.dev/@virtualstate/composite-key", 16 | "chevrotain": "https://cdn.skypack.dev/chevrotain", 17 | "@virtualstate/promise/the-thing": "https://cdn.skypack.dev/@virtualstate/promise/the-thing", 18 | "@virtualstate/app-history": "./src/app-history.ts", 19 | "@virtualstate/app-history/polyfill": "./src/polyfill.ts", 20 | "@virtualstate/app-history/event-target/sync": "./src/event-target/sync-event-target.ts", 21 | "@virtualstate/app-history/event-target/async": "./src/event-target/async-event-target.ts", 22 | "@virtualstate/app-history/event-target": "./src/event-target/sync-event-target.ts", 23 | "@virtualstate/app-history-imported": "./src/app-history.ts", 24 | "@virtualstate/app-history-imported/polyfill": "./src/polyfill.ts", 25 | "@virtualstate/app-history-imported/event-target/sync": "./src/event-target/sync-event-target.ts", 26 | "@virtualstate/app-history-imported/event-target/async": "./src/event-target/async-event-target.ts", 27 | "@virtualstate/app-history-imported/event-target": "./src/event-target/sync-event-target.ts", 28 | "dom-lite": "https://cdn.skypack.dev/dom-lite", 29 | "iterable": "https://cdn.skypack.dev/iterable@6.0.1-beta.5", 30 | "https://cdn.skypack.dev/-/iterable@v5.7.0-CNtyuMJo9f2zFu6CuB1D/dist=es2019,mode=imports/optimized/iterable.js": "https://cdn.skypack.dev/iterable@6.0.1-beta.5", 31 | "uuid": "./src/util/deno-uuid.ts", 32 | "whatwg-url": "https://cdn.skypack.dev/whatwg-url", 33 | "abort-controller": "https://cdn.skypack.dev/abort-controller", 34 | "deno:std/uuid/mod": "https://deno.land/std@0.113.0/uuid/mod.ts", 35 | "deno:std/uuid/v4": "https://deno.land/std@0.113.0/uuid/v4.ts", 36 | "deno:deno_dom/deno-dom-wasm.ts": "https://deno.land/x/deno_dom/deno-dom-wasm.ts", 37 | "urlpattern-polyfill": "https://cdn.skypack.dev/urlpattern-polyfill", 38 | "cheerio": "./scripts/nop/index.js" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /scripts/correct-import-extensions.js: -------------------------------------------------------------------------------- 1 | import FileHound from "filehound"; 2 | import { promises as fs } from "fs"; 3 | import path from "path"; 4 | import { promisify } from "util"; 5 | // 6 | // const packages = await FileHound.create() 7 | // .paths(`packages`) 8 | // .directory() 9 | // .depth(1) 10 | // .find(); 11 | 12 | const buildPaths = ["esnext"]; 13 | 14 | for (const buildPath of buildPaths) { 15 | const filePaths = await FileHound.create() 16 | .paths(buildPath) 17 | .discard("node_modules") 18 | .ext("js") 19 | .find(); 20 | 21 | await Promise.all( 22 | filePaths.map(async (filePath) => { 23 | const initialContents = await fs.readFile(filePath, "utf-8"); 24 | 25 | const statements = initialContents.match( 26 | /(?:(?:import|export)(?: .+ from)? ".+";|(?:import\(".+"\)))/g 27 | ); 28 | 29 | if (!statements) { 30 | return; 31 | } 32 | 33 | const importMap = process.env.IMPORT_MAP 34 | ? JSON.parse(await fs.readFile(process.env.IMPORT_MAP, "utf-8")) 35 | : undefined; 36 | const contents = await statements.reduce( 37 | async (contentsPromise, statement) => { 38 | const contents = await contentsPromise; 39 | const url = statement.match(/"(.+)"/)[1]; 40 | if (importMap?.imports?.[url]) { 41 | const replacement = importMap.imports[url]; 42 | if (!replacement.includes("./src")) { 43 | return contents.replace( 44 | statement, 45 | statement.replace(url, replacement) 46 | ); 47 | } 48 | const shift = filePath 49 | .split("/") 50 | // Skip top folder + file 51 | .slice(2) 52 | // Replace with shift up directory 53 | .map(() => "..") 54 | .join("/"); 55 | return contents.replace( 56 | statement, 57 | statement.replace( 58 | url, 59 | replacement.replace("./src", shift).replace(/\.tsx?$/, ".js") 60 | ) 61 | ); 62 | } else { 63 | return contents.replace(statement, await getReplacement(url)); 64 | } 65 | 66 | async function getReplacement(url) { 67 | const [stat, indexStat] = await Promise.all([ 68 | fs 69 | .stat(path.resolve(path.dirname(filePath), url + ".js")) 70 | .catch(() => {}), 71 | fs 72 | .stat(path.resolve(path.dirname(filePath), url + "/index.js")) 73 | .catch(() => {}), 74 | ]); 75 | 76 | if (stat && stat.isFile()) { 77 | return statement.replace(url, url + ".js"); 78 | } else if (indexStat && indexStat.isFile()) { 79 | return statement.replace(url, url + "/index.js"); 80 | } 81 | return statement; 82 | } 83 | }, 84 | Promise.resolve(initialContents) 85 | ); 86 | 87 | await fs.writeFile(filePath, contents, "utf-8"); 88 | }) 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /src/tests/big-tree.tsx: -------------------------------------------------------------------------------- 1 | import {children, descendants, h, ok} from "@virtualstate/focus"; 2 | import {memo} from "../memo"; 3 | 4 | const base = Array.from({ length: 5 }, () => { 5 | return async function *Base() { 6 | yield 1; 7 | yield 2; 8 | yield 3; 9 | // yield 1; 10 | // yield 2; 11 | // yield 3; 12 | // yield 1; 13 | // yield 2; 14 | // yield 3; 15 | // yield 1; 16 | // yield 2; 17 | // yield 3; 18 | } 19 | }).map(Node => ); 20 | const next1 = Array.from({ length: 5 }, () => { 21 | return async function *Next1(options: unknown, input?: unknown) { 22 | for await (const snapshot of children(input)) { 23 | yield snapshot.map(value => ({value})) 24 | } 25 | } 26 | }).map(Node => {...base}); 27 | const next2 = Array.from({ length: 15 }, () => { 28 | return async function *Next2(options: unknown, input?: unknown) { 29 | for await (const snapshot of children(input)) { 30 | yield snapshot.map(value => ({value})) 31 | } 32 | } 33 | }).map(Node => {...next1}); 34 | const top = Array.from({ length: 5 }, () => { 35 | return async function *Top(options: unknown, input?: unknown) { 36 | for await (const snapshot of children(input)) { 37 | yield snapshot.map(value => ({value})) 38 | } 39 | } 40 | }).map(Node => {...next2}); 41 | 42 | async function time(fn: () => Promise): Promise { 43 | const start = Date.now(); 44 | try { 45 | return await fn(); 46 | } finally { 47 | const end = Date.now(); 48 | console.log(`Took ${(end - start) / 1000} seconds`); 49 | } 50 | } 51 | let snapshot = await time(() => descendants(top)); 52 | const expectedLength = snapshot.length; 53 | console.log({ expectedLength }); 54 | ok(snapshot.length); 55 | const cached = memo(top); 56 | snapshot = await time(() => descendants(cached)); 57 | ok(snapshot.length === expectedLength) 58 | await new Promise(queueMicrotask); 59 | snapshot = await time(() => descendants(cached)); 60 | ok(snapshot.length === expectedLength) 61 | await new Promise(queueMicrotask); 62 | snapshot = await time(() => descendants(cached)); 63 | ok(snapshot.length === expectedLength); 64 | 65 | { 66 | 67 | let seen = new Set(); 68 | let total = 0; 69 | for await (const snapshot of descendants(top)) { 70 | // console.log(snapshot); 71 | total += snapshot.length; 72 | for (const value of snapshot) { 73 | seen.add(value); 74 | } 75 | } 76 | console.log(seen.size, total); 77 | } 78 | 79 | { 80 | 81 | let seen = new Set(); 82 | let total = 0; 83 | for await (const snapshot of descendants(cached)) { 84 | // console.log(snapshot); 85 | total += snapshot.length; 86 | for (const value of snapshot) { 87 | seen.add(value); 88 | } 89 | } 90 | console.log(seen.size, total); 91 | } 92 | // for (let i = 0; i < 100; i += 1) { 93 | // await new Promise(queueMicrotask); 94 | // await new Promise(resolve => setTimeout(resolve, 100)); 95 | // await time(() => descendants(cached)); 96 | // } -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [conduct+axiom@fabiancook.dev](mailto:conduct+axiom@fabiancook.dev). All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /scripts/post-build.js: -------------------------------------------------------------------------------- 1 | import "./correct-import-extensions.js"; 2 | import { promises as fs } from "fs"; 3 | import { dirname, resolve } from "path"; 4 | 5 | const { pathname } = new URL(import.meta.url); 6 | const cwd = resolve(dirname(pathname), ".."); 7 | 8 | { 9 | // /Volumes/Extreme/Users/fabian/src/virtualstate/esnext/tests/app-history.playwright.wpt.js 10 | // /Volumes/Extreme/Users/fabian/src/virtualstate/app-history/esnext/tests/app-history.playwright.wpt.js 11 | 12 | // console.log({ 13 | // cwd, 14 | // path: 15 | // `/Volumes/Extreme/Users/fabian/src/virtualstate/app-history/esnext/tests/app-history.playwright.wpt.js` === 16 | // `${cwd}/esnext/tests/app-history.playwright.wpt.js`, 17 | // p: `${cwd}/esnext/tests/app-history.playwright.wpt.js`, 18 | // }); 19 | 20 | // const bundle = await rollup({ 21 | // input: "./esnext/tests/index.js", 22 | // plugins: [ 23 | // ignore([ 24 | // "playwright", 25 | // "fs", 26 | // "path", 27 | // "uuid", 28 | // "cheerio", 29 | // "@virtualstate/app-history", 30 | // "@virtualstate/app-history-imported", 31 | // `${cwd}/esnext/tests/app-history.playwright.js`, 32 | // `${cwd}/esnext/tests/app-history.playwright.wpt.js`, 33 | // `${cwd}/esnext/tests/dependencies-input.js`, 34 | // `${cwd}/esnext/tests/dependencies.js`, 35 | // "./app-history.playwright.js", 36 | // "./app-history.playwright.wpt.js", 37 | // ]), 38 | // nodeResolve(), 39 | // ], 40 | // inlineDynamicImports: true, 41 | // treeshake: { 42 | // preset: "smallest", 43 | // moduleSideEffects: "no-external", 44 | // }, 45 | // }); 46 | // await bundle.write({ 47 | // sourcemap: true, 48 | // output: { 49 | // file: "./esnext/tests/rollup.js", 50 | // }, 51 | // inlineDynamicImports: true, 52 | // format: "cjs", 53 | // interop: "auto", 54 | // globals: { 55 | // "esnext/tests/app-history.playwright.js": "globalThis", 56 | // }, 57 | // }); 58 | } 59 | 60 | if (!process.env.NO_COVERAGE_BADGE_UPDATE) { 61 | const badges = []; 62 | 63 | const { name } = await fs.readFile("package.json", "utf-8").then(JSON.parse); 64 | 65 | badges.push( 66 | "### Support\n\n", 67 | "![Node.js supported](https://img.shields.io/badge/node-%3E%3D16.0.0-blue)", 68 | "![Deno supported](https://img.shields.io/badge/deno-%3E%3D1.17.0-blue)", 69 | "![Bun supported](https://img.shields.io/badge/bun-%3E%3D0.1.8-blue)", 70 | // "![Chromium supported](https://img.shields.io/badge/chromium-%3E%3D98.0.4695.0-blue)", 71 | // "![Webkit supported](https://img.shields.io/badge/webkit-%3E%3D15.4-blue)", 72 | // "![Firefox supported](https://img.shields.io/badge/firefox-%3E%3D94.0.1-blue)" 73 | ); 74 | 75 | badges.push( 76 | "\n\n### Test Coverage\n\n" 77 | // `![nycrc config on GitHub](https://img.shields.io/nycrc/${name.replace(/^@/, "")})` 78 | ); 79 | 80 | // const wptResults = await fs 81 | // .readFile("coverage/wpt.results.json", "utf8") 82 | // .then(JSON.parse) 83 | // .catch(() => ({})); 84 | // if (wptResults?.total) { 85 | // const message = `${wptResults.pass}/${wptResults.total}`; 86 | // const name = "Web Platform Tests"; 87 | // badges.push( 88 | // `![${name} ${message}](https://img.shields.io/badge/${encodeURIComponent( 89 | // name 90 | // )}-${encodeURIComponent(message)}-brightgreen)` 91 | // ); 92 | // } 93 | 94 | const coverage = await fs 95 | .readFile("coverage/coverage-summary.json", "utf8") 96 | .then(JSON.parse) 97 | .catch(() => ({})); 98 | const coverageConfig = await fs.readFile(".nycrc", "utf8").then(JSON.parse); 99 | for (const [name, { pct }] of Object.entries(coverage?.total ?? {})) { 100 | const good = coverageConfig[name]; 101 | if (!good) continue; // not configured 102 | const color = pct >= good ? "brightgreen" : "yellow"; 103 | const message = `${pct}%25`; 104 | badges.push( 105 | `![${message} ${name} covered](https://img.shields.io/badge/${name}-${message}-${color})` 106 | ); 107 | } 108 | 109 | const tag = "[//]: # (badges)"; 110 | 111 | const readMe = await fs.readFile("README.md", "utf8"); 112 | const badgeStart = readMe.indexOf(tag); 113 | const badgeStartAfter = badgeStart + tag.length; 114 | if (badgeStart === -1) { 115 | throw new Error(`Expected to find "${tag}" in README.md`); 116 | } 117 | const badgeEnd = badgeStartAfter + readMe.slice(badgeStartAfter).indexOf(tag); 118 | const badgeEndAfter = badgeEnd + tag.length; 119 | const readMeBefore = readMe.slice(0, badgeStart); 120 | const readMeAfter = readMe.slice(badgeEndAfter); 121 | 122 | const readMeNext = `${readMeBefore}${tag}\n\n${badges.join( 123 | " " 124 | )}\n\n${tag}${readMeAfter}`; 125 | await fs.writeFile("README.md", readMeNext); 126 | console.log("Wrote coverage badges!"); 127 | } 128 | -------------------------------------------------------------------------------- /src/memo.ts: -------------------------------------------------------------------------------- 1 | import { 2 | children, 3 | getChildrenFromRawNode, getNameKey, h, isComponentFn, isFragment, 4 | isUnknownJSXNode, 5 | name, ok, 6 | properties, 7 | raw, 8 | } from "@virtualstate/focus"; 9 | import { Push } from "@virtualstate/promise"; 10 | import {isAsyncIterable} from "./is"; 11 | import {createCompositeKey} from "@virtualstate/composite-key"; 12 | 13 | function createMemoFn(source: (...args: A) => T): (...args: A) => T { 14 | const cache = new WeakMap(); 15 | const compositeKey = createCompositeKey(); 16 | 17 | return function (this: unknown, ...args: A): T { 18 | const key = compositeKey(this, source.call, ...args); 19 | const existing = cache.get(key); 20 | if (existing) return existing; 21 | const result = source.call(this, ...args); 22 | if (isAsyncIterable(result)) { 23 | const returning = createAsyncIterableMemo(result); 24 | ok(returning); 25 | cache.set(key, returning); 26 | return returning; 27 | } else { 28 | cache.set(key, result); 29 | return result; 30 | } 31 | } 32 | } 33 | 34 | function createAsyncIterableMemo(result: AsyncIterable) { 35 | let target: Push | undefined = undefined; 36 | async function *asyncIterable() { 37 | if (target) { 38 | return yield * target; 39 | } 40 | target = new Push({ 41 | keep: true // memo all pushed values 42 | }); 43 | try { 44 | for await (const snapshot of result) { 45 | target.push(snapshot); 46 | yield snapshot; 47 | } 48 | } catch (error) { 49 | target.throw(error); 50 | throw await Promise.reject(error); 51 | } 52 | target.close(); 53 | } 54 | 55 | ok(isUnknownJSXNode(result)); 56 | 57 | const fnCache = new WeakMap(); 58 | 59 | return new Proxy(result, { 60 | get(target, p) { 61 | if (p === Symbol.asyncIterator) { 62 | const iterable = asyncIterable() 63 | return iterable[Symbol.asyncIterator].bind(iterable); 64 | } 65 | const value = result[p]; 66 | if (typeof value !== "function") return value; 67 | const existing = fnCache.get(value); 68 | if (existing) return existing; 69 | const fn = createMemoFn(value.bind(result)); 70 | fnCache.set(value, fn); 71 | return fn; 72 | } 73 | }); 74 | } 75 | 76 | function createMemoContextFn( 77 | input: (context: C, ...keys: unknown[]) => T 78 | ): (context: C, ...keys: unknown[]) => T { 79 | function point(...args: unknown[]) { 80 | void args; 81 | let value: T | undefined = undefined, 82 | has = false; 83 | function is(given: unknown): given is T { 84 | return has; 85 | } 86 | return function (context: C) { 87 | if (is(value)) { 88 | return value; 89 | } 90 | const returning = input(context); 91 | value = returning; 92 | has = true; 93 | return returning; 94 | } 95 | } 96 | const fn = createMemoFn(point); 97 | return (context, ...args): T => fn(...args)(context); 98 | } 99 | 100 | function createNameMemo( 101 | input: (context: C, ...keys: unknown[]) => T 102 | ): (context: C, ...keys: unknown[]) => T { 103 | 104 | const nameCache = new Map T>(); 105 | 106 | function getCache(node: unknown) { 107 | const key = getNameCacheKey(node); 108 | const existing = nameCache.get(key); 109 | if (existing) return existing; 110 | const cache = createMemoContextFn(input); 111 | nameCache.set(key, cache); 112 | return cache; 113 | } 114 | 115 | return (node, ...keys) => { 116 | // Split the cache by node name to create quick partitions 117 | return getCache(node)(node, ...getCacheKey(node), ...keys); 118 | }; 119 | } 120 | 121 | function getPropertiesCacheKey(node: unknown) { 122 | return Object.entries(properties(node)) 123 | .sort(([a], [b]) => (a < b ? -1 : 1)) 124 | .flat(); 125 | } 126 | 127 | const CacheSeparator = Symbol.for("@virtualstate/memo/cache/separator"); 128 | const CacheChildSeparator = Symbol.for("@virtualstate/memo/cache/separator/child"); 129 | 130 | function getNameCacheKey(input: unknown) { 131 | const node = raw(input); 132 | const value = node[getNameKey(node)]; 133 | if (!isComponentFn(value)) return name(input); 134 | return value; 135 | } 136 | 137 | const getChildrenCacheKey = createMemoFn(function getChildrenCacheKey(node: unknown): unknown[] { 138 | const array = getChildrenFromRawNode(node); 139 | if (!(Array.isArray(array) && array.length)) return []; 140 | return array.flatMap( 141 | node => { 142 | if (!isUnknownJSXNode(node)) return [node, CacheChildSeparator]; 143 | return [getNameCacheKey(node), ...getCacheKey(node), CacheChildSeparator]; 144 | } 145 | ) 146 | }) 147 | 148 | const getCacheKey = createMemoFn(function getCacheKey(source: unknown, input: unknown = source) { 149 | return [ 150 | ...getPropertiesCacheKey(source), 151 | CacheSeparator, 152 | ...getChildrenCacheKey(input) 153 | ] 154 | }) 155 | 156 | function createMemoComponentFunction(source: unknown) { 157 | const cache = createMemoContextFn((node) => memo(node)); 158 | 159 | return function *(options: Record, input?: unknown) { 160 | const snapshotOptions = { 161 | ...properties(input), 162 | ...options 163 | }; 164 | const key = getCacheKey({ options }, input); 165 | yield cache( 166 | h(source, snapshotOptions, input), 167 | ...key 168 | ); 169 | } 170 | } 171 | 172 | type OptionsRecord = Record 173 | type PartialOptions = Partial; 174 | 175 | interface MemoComponentFn { 176 | (options: O, input?: I): R 177 | } 178 | 179 | const memoFn = createMemoFn((input?: unknown) => { 180 | const internalMemo = createMemoFn(map); 181 | const internalMemoNamed = createMemoFn( 182 | createNameMemo(map) 183 | ); 184 | 185 | function map(input: unknown): unknown { 186 | if (!isUnknownJSXNode(input)) return input; 187 | 188 | if (typeof input === "function") { 189 | return createMemoComponentFunction(input); 190 | } 191 | 192 | if (isAsyncIterable(input)) { 193 | return createAsyncIterableMemo(input); 194 | } 195 | 196 | if (isFragment(input)) { 197 | return { 198 | async *[Symbol.asyncIterator]() { 199 | for await (const snapshot of children(input)) { 200 | yield snapshot.map((node) => internalMemoNamed(node)); 201 | } 202 | } 203 | } 204 | } 205 | 206 | const node = raw(input); 207 | const array = getChildrenFromRawNode(node); 208 | // Must have a sync indexable tree, if not, ignore 209 | if (!(Array.isArray(array) && array.length)) return input; 210 | return h(name(node), properties(node), ...array.map(node => internalMemo(node))); 211 | } 212 | return internalMemo(input); 213 | }) 214 | 215 | export function memo(input: F): F; 216 | export function memo>(input: A): A; 217 | export function memo(input?: unknown): unknown 218 | export function memo(input?: unknown): unknown { 219 | return memoFn(input); 220 | } 221 | -------------------------------------------------------------------------------- /src/tests/memo.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | children, 4 | createFragment, 5 | descendants, 6 | ok, name, 7 | } from "@virtualstate/focus"; 8 | import { memo} from "../memo"; 9 | import { anAsyncThing } from "@virtualstate/promise/the-thing"; 10 | 11 | export default 1; 12 | 13 | { 14 | let called = 0; 15 | let otherCalled = 0; 16 | 17 | async function* Other() { 18 | otherCalled += 1; 19 | yield 1; 20 | } 21 | 22 | async function* Component() { 23 | called += 1; 24 | yield 1; 25 | yield 2; 26 | yield [1, 2]; 27 | yield 3; 28 | yield ; 29 | yield 1; 30 | } 31 | 32 | const node = memo( 33 | <> 34 | 35 | 36 | 37 | ); 38 | 39 | console.log({ called, otherCalled }); 40 | console.log(await children(node)); 41 | console.log({ called, otherCalled }); 42 | console.log(await children(node)); 43 | console.log({ called, otherCalled }); 44 | ok(called === 2); 45 | ok(otherCalled === 2); 46 | } 47 | 48 | { 49 | let called = 0; 50 | async function* Component() { 51 | called += 1; 52 | yield 1; 53 | } 54 | const node = memo( 55 | <> 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | ); 66 | 67 | console.log({ called }); 68 | console.log(await descendants(node)); 69 | console.log({ called }); 70 | console.log(await descendants(node)); 71 | console.log({ called }); 72 | ok(called === 1); 73 | } 74 | { 75 | let called = 0; 76 | async function* Component() { 77 | called += 1; 78 | yield 1; 79 | } 80 | const node = memo( 81 | <> 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | ); 92 | 93 | console.log({ called }); 94 | console.log(await descendants(node)); 95 | console.log({ called }); 96 | console.log(await descendants(node)); 97 | console.log({ called }); 98 | ok(called === 2); 99 | } 100 | 101 | { 102 | let called = 0; 103 | async function* Component() { 104 | called += 1; 105 | yield 1; 106 | } 107 | const node = memo( 108 | <> 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | ); 119 | 120 | console.log({ called }); 121 | console.log(await descendants(node)); 122 | console.log({ called }); 123 | console.log(await descendants(node)); 124 | console.log({ called }); 125 | ok(called === 1); 126 | } 127 | 128 | { 129 | const value = memo("value"); 130 | ok(value === "value"); 131 | } 132 | 133 | { 134 | let called = 0; 135 | async function* Component() { 136 | called += 1; 137 | yield 1; 138 | } 139 | const component = memo(); 140 | 141 | const node = memo( 142 | <> 143 | 144 | 145 | {component} 146 | 147 | 148 | 149 | {component} 150 | 151 | 152 | ); 153 | 154 | console.log({ called }); 155 | console.log(await descendants(node)); 156 | console.log({ called }); 157 | console.log(await descendants(node)); 158 | console.log({ called }); 159 | ok(called === 1); 160 | } 161 | { 162 | const node = memo( 163 | <> 164 | 165 | 166 | 167 | 168 | 169 | ); 170 | 171 | console.log(await descendants(node)); 172 | console.log(await descendants(node)); 173 | } 174 | { 175 | const node = memo( 176 | <> 177 | 178 | {{ 179 | name: "b", 180 | children: 1, 181 | }} 182 | 183 | ); 184 | 185 | console.log(await descendants(node)); 186 | console.log(await descendants(node)); 187 | } 188 | 189 | { 190 | let called = 0; 191 | async function* Component() { 192 | called += 1; 193 | yield 1; 194 | } 195 | const node = memo( 196 | <> 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | ); 207 | 208 | let anotherCalled = 0; 209 | 210 | async function *Another(options?: Record, input?: unknown) { 211 | anotherCalled += 1; 212 | yield {input} 213 | } 214 | 215 | const another = ( 216 | <> 217 | {node} 218 | {node} 219 | 220 | ) 221 | 222 | console.log({ called, anotherCalled }); 223 | console.log(await descendants(another)); 224 | console.log({ called, anotherCalled }); 225 | ok(called === 1); 226 | ok(anotherCalled === 2); 227 | console.log(await descendants(another)); 228 | console.log({ called, anotherCalled }); 229 | ok(called === 1); 230 | ok(anotherCalled === 4); 231 | } 232 | 233 | { 234 | let called = 0; 235 | const Component = memo( 236 | async function *(options, input) { 237 | called += 1; 238 | yield {input} 239 | } 240 | ); 241 | 242 | const node = ( 243 | <> 244 | 245 | 246 | 247 | ); 248 | 249 | console.log({ called }); 250 | console.log(await descendants(node)); 251 | console.log({ called }); 252 | console.log(await descendants(node)); 253 | console.log({ called }); 254 | ok(called === 1); 255 | 256 | } 257 | { 258 | let called = 0; 259 | const Component = memo( 260 | async function *(options, input) { 261 | called += 1; 262 | yield {input} 263 | } 264 | ); 265 | 266 | const node = ( 267 | <> 268 | 269 | 270 | 271 | 272 | 273 | ); 274 | 275 | console.log({ called }); 276 | console.log(await descendants(node)); 277 | console.log({ called }); 278 | console.log(await descendants(node)); 279 | console.log({ called }); 280 | ok(called === 2); 281 | 282 | } 283 | { 284 | let called = 0; 285 | const Component = memo( 286 | async function *(options, input) { 287 | called += 1; 288 | yield {input} 289 | } 290 | ); 291 | 292 | const node = ( 293 | <> 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | ); 302 | 303 | console.log({ called }); 304 | console.log(await descendants(node)); 305 | console.log({ called }); 306 | console.log(await descendants(node)); 307 | console.log({ called }); 308 | ok(called === 2); 309 | 310 | } 311 | { 312 | let called = 0; 313 | const Component = memo( 314 | async function *(options, input) { 315 | called += 1; 316 | yield {input} 317 | } 318 | ); 319 | 320 | const node = ( 321 | <> 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | ); 330 | 331 | console.log({ called }); 332 | console.log(await descendants(node)); 333 | console.log({ called }); 334 | console.log(await descendants(node)); 335 | console.log({ called }); 336 | ok(called === 2); 337 | 338 | } 339 | 340 | 341 | { 342 | let called = 0; 343 | const Component = memo( 344 | async function *(options, input) { 345 | called += 1; 346 | yield {input} 347 | } 348 | ); 349 | 350 | const node = ( 351 | <> 352 | 353 | 354 | 355 | 356 | 357 |
358 | 359 | ); 360 | 361 | console.log({ called }); 362 | console.log(await descendants(node)); 363 | console.log({ called }); 364 | console.log(await descendants(node)); 365 | console.log({ called }); 366 | ok(called === 1); 367 | 368 | } 369 | 370 | { 371 | let called = 0; 372 | const Component = memo( 373 | async function *(options, input) { 374 | called += 1; 375 | yield {input} 376 | } 377 | ); 378 | 379 | const node = ( 380 | <> 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | ); 389 | 390 | console.log({ called }); 391 | console.log(await descendants(node)); 392 | console.log({ called }); 393 | console.log(await descendants(node)); 394 | console.log({ called }); 395 | ok(called === 2); 396 | 397 | } 398 | 399 | 400 | { 401 | let called = 0; 402 | const Component = memo( 403 | async function *(options, input) { 404 | called += 1; 405 | yield {input} 406 | } 407 | ); 408 | 409 | const node = ( 410 | <> 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | ); 419 | 420 | console.log({ called }); 421 | console.log(await descendants(node)); 422 | console.log({ called }); 423 | console.log(await descendants(node)); 424 | console.log({ called }); 425 | ok(called === 2); 426 | 427 | } 428 | { 429 | let called = 0; 430 | const Component = memo( 431 | async function *(options, input) { 432 | called += 1; 433 | yield {input} 434 | } 435 | ); 436 | 437 | const node = ( 438 | <> 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | ); 447 | 448 | console.log({ called }); 449 | console.log(await descendants(node)); 450 | console.log({ called }); 451 | console.log(await descendants(node)); 452 | console.log({ called }); 453 | ok(called === 1); 454 | 455 | } 456 | 457 | 458 | { 459 | let called = 0; 460 | const Component = memo( 461 | async function *(options, input) { 462 | called += 1; 463 | yield {input} 464 | } 465 | ); 466 | 467 | const node = ( 468 | <> 469 | 470 | {1} 471 | 472 | 473 | {1} 474 | 475 | 476 | ); 477 | 478 | console.log({ called }); 479 | console.log(await descendants(node)); 480 | console.log({ called }); 481 | console.log(await descendants(node)); 482 | console.log({ called }); 483 | ok(called === 1); 484 | 485 | } 486 | { 487 | let called = 0; 488 | const Component = memo( 489 | async function *(options, input) { 490 | called += 1; 491 | yield {input} 492 | } 493 | ); 494 | 495 | const node = ( 496 | <> 497 | 498 | {1} 499 | 500 | 501 | {2} 502 | 503 | 504 | ); 505 | 506 | console.log({ called }); 507 | console.log(await descendants(node)); 508 | console.log({ called }); 509 | console.log(await descendants(node)); 510 | console.log({ called }); 511 | ok(called === 2); 512 | 513 | } 514 | 515 | { 516 | 517 | let count = 0, 518 | another = 0; 519 | const original = { 520 | async *[Symbol.asyncIterator]() { 521 | count += 1; 522 | yield 1; 523 | yield 2; 524 | }, 525 | another() { 526 | another += 1; 527 | return this; 528 | }, 529 | value: 1 530 | }; 531 | const result = memo(original); 532 | ok(result.another !== original.another); 533 | ok(result.value === 1); 534 | await anAsyncThing(result.another()); 535 | await anAsyncThing(result.another()); 536 | console.log({ count, another }); 537 | ok(count === 1); 538 | ok(another === 1); 539 | } 540 | 541 | { 542 | let count = 0; 543 | const result = memo({ 544 | async *[Symbol.asyncIterator]() { 545 | count += 1; 546 | yield 1; 547 | throw new Error() 548 | } 549 | }); 550 | let error = await anAsyncThing(result).catch(error => error); 551 | ok(error instanceof Error); 552 | error = await anAsyncThing(result).catch(error => error); 553 | ok(error instanceof Error); 554 | ok(count === 1); 555 | } 556 | 557 | { 558 | let component = 0, 559 | body = 0; 560 | async function Component() { 561 | component += 1; 562 | return
563 | } 564 | 565 | async function Body() { 566 | body += 1; 567 | return
568 | } 569 | 570 | { 571 | const tree = ( 572 | <> 573 | 574 |
575 | 576 |
577 | 578 | ); 579 | 580 | console.log(await descendants(tree)); 581 | console.log({ component, body }); 582 | ok(component === 1); 583 | ok(body === 1); 584 | 585 | console.log(await descendants(tree)); 586 | ok(component === 2); 587 | ok(body === 2); 588 | 589 | component = 0; 590 | body = 0; 591 | } 592 | 593 | { 594 | const tree = memo( 595 | <> 596 | 597 |
598 | 599 |
600 | 601 | ); 602 | 603 | console.log(await descendants(tree)); 604 | console.log({ component, body }); 605 | ok(component === 1); 606 | ok(body === 1); 607 | 608 | console.log(await descendants(tree)); 609 | ok(component === 1); 610 | ok(body === 1); 611 | 612 | component = 0; 613 | body = 0; 614 | } 615 | } 616 | 617 | { 618 | function Component() { 619 | return 1; 620 | } 621 | const node = memo( 622 | 623 | 624 | 625 | ); 626 | const result = await descendants(<>{node}); 627 | console.log(result); 628 | ok(result.length === 2); 629 | ok(name(result[0]) === "inner"); 630 | ok(result[1] === 1); 631 | } --------------------------------------------------------------------------------