├── .gitignore ├── package.json ├── LICENSE ├── index.test.ts ├── index.js ├── .github └── workflows │ └── release.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # This is generated during release 3 | index.d.ts 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@epic-web/remember", 3 | "description": "Simple, type-safe, \"singleton\" implementation.", 4 | "publishConfig": { 5 | "access": "public" 6 | }, 7 | "exports": { 8 | ".": "./index.js" 9 | }, 10 | "files": [ 11 | "index.js", 12 | "index.d.ts" 13 | ], 14 | "module": "index.js", 15 | "type": "module", 16 | "license": "MIT", 17 | "author": "Kent C. Dodds (https://kentcdodds.com/)", 18 | "scripts": { 19 | "test": "bun test" 20 | }, 21 | "prettier": { 22 | "semi": false, 23 | "useTabs": true, 24 | "singleQuote": true, 25 | "proseWrap": "always", 26 | "overrides": [ 27 | { 28 | "files": [ 29 | "**/*.json" 30 | ], 31 | "options": { 32 | "useTabs": false 33 | } 34 | } 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) Copyright (c) 2023 Kent C. Dodds 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /index.test.ts: -------------------------------------------------------------------------------- 1 | import { remember, forget } from './index.js' 2 | import { expect, test, beforeEach } from 'bun:test' 3 | 4 | beforeEach(() => { 5 | // ensure global var empty before each test! 6 | delete globalThis.__remember_epic_web 7 | }) 8 | 9 | // would use mock, but... https://twitter.com/kentcdodds/status/1700718653438931049 10 | test('remember', () => { 11 | const rose = Symbol('rose') 12 | let returnValue = rose 13 | const getValue = () => returnValue 14 | expect(remember('what is in a name', getValue)).toBe(rose) 15 | returnValue = Symbol('bud') 16 | // because the name and getValue did not change, the value is remembered 17 | expect(remember('what is in a name', getValue)).toBe(rose) 18 | }) 19 | 20 | test('forget', () => { 21 | // nothing remembered yet, trying to forget will "fail" 22 | expect(forget('what is in a name')).toBe(false) 23 | const rose = Symbol('rose') 24 | let returnValue = rose 25 | const getValue = () => returnValue 26 | expect(remember('what is in a name', getValue)).toBe(rose) 27 | // remembered value will be found and forgotten 28 | expect(forget('what is in a name')).toBe(true) 29 | const bud = returnValue = Symbol('bud') 30 | // because the name has been forgotten, we should get the "new" value 31 | expect(remember('what is in a name', getValue)).toBe(bud) 32 | }) 33 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Borrowed/modified from https://github.com/jenseng/abuse-the-platform/blob/2993a7e846c95ace693ce61626fa072174c8d9c7/app/utils/singleton.ts 2 | 3 | /** 4 | * Remembers and retrieves a value by a given name, or the value generated by `getValue` if the name doesn't exist. 5 | * The return type is inferred from the return type of `getValue`. 6 | * 7 | * @template Value 8 | * @param {string} name - The name under which to remember the value. 9 | * @param {() => Value} getValue - The function that generates the value to remember. 10 | * @returns {Value} - The remembered value. 11 | */ 12 | export function remember(name, getValue) { 13 | const thusly = globalThis 14 | thusly.__remember_epic_web ??= new Map() 15 | if (!thusly.__remember_epic_web.has(name)) { 16 | thusly.__remember_epic_web.set(name, getValue()) 17 | } 18 | return thusly.__remember_epic_web.get(name) 19 | } 20 | 21 | /** 22 | * Forgets a remembered value by a given name. Does not throw if the name doesn't exist. 23 | * 24 | * @param {string} name - The name under which the value was remembered. 25 | * @return {boolean} - A remembered value existed and has been forgotten. 26 | */ 27 | export function forget(name) { 28 | const thusly = globalThis 29 | thusly.__remember_epic_web ??= new Map() 30 | return thusly.__remember_epic_web.delete(name) 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: [push, pull_request] 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | jobs: 8 | test: 9 | name: 🧪 Test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: ⬇️ Checkout repo 13 | uses: actions/checkout@v5 14 | 15 | - name: 🍔 Setup bun 16 | uses: oven-sh/setup-bun@v1 17 | 18 | - name: 📥 Download deps 19 | run: bun install 20 | 21 | - name: 🧪 Test 22 | run: bun test 23 | 24 | release: 25 | name: 🚀 Release 26 | needs: [test] 27 | runs-on: ubuntu-latest 28 | if: 29 | ${{ github.repository == 'epicweb-dev/remember' && 30 | contains('refs/heads/main,refs/heads/beta,refs/heads/next,refs/heads/alpha', 31 | github.ref) && github.event_name == 'push' }} 32 | permissions: 33 | contents: write # to be able to publish a GitHub release 34 | id-token: write # to enable use of OIDC for npm provenance 35 | issues: write # to be able to comment on released issues 36 | pull-requests: write # to be able to comment on released pull requests 37 | steps: 38 | - name: ⬇️ Checkout repo 39 | uses: actions/checkout@v5 40 | 41 | # I'd prefer to use bun, but I got this error when I tried using bunx instead of npx: 42 | # error TS5042: Option 'project' cannot be mixed with source files on a command line. 43 | # error: "tsc" exited with code 1 (SIGHUP) 44 | # Also, I don't know how to use bun instead of node for semantic-release 🤷‍♂️ 45 | - name: ⎔ Setup node 46 | uses: actions/setup-node@v6 47 | with: 48 | node-version: lts/* 49 | 50 | - name: 📥 Download deps 51 | uses: bahmutov/npm-install@v1 52 | with: 53 | useLockFile: false 54 | 55 | - name: 💪 Generate Types 56 | run: 57 | npx -p typescript tsc --declaration --emitDeclarationOnly --allowJs 58 | --checkJs --downlevelIteration --module nodenext --moduleResolution 59 | nodenext --target es2022 --outDir . index.js 60 | 61 | - name: 🚀 Release 62 | uses: cycjimmy/semantic-release-action@v5.0.2 63 | with: 64 | semantic_version: 25 65 | branches: | 66 | [ 67 | '+([0-9])?(.{+([0-9]),x}).x', 68 | 'main', 69 | 'next', 70 | 'next-major', 71 | {name: 'beta', prerelease: true}, 72 | {name: 'alpha', prerelease: true} 73 | ] 74 | env: 75 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

🤔 @epic-web/remember

3 | 4 | Simple, type-safe, "singleton" implementation. 5 | 6 |

7 | For when your "hot module replacement" involves re-evaluating a module, but 8 | you don't want to actually re-evaluate a portion of it. 9 |

10 |
11 | 12 | ``` 13 | npm install @epic-web/remember 14 | ``` 15 | 16 |
17 | 21 | 25 | 26 |
27 | 28 |
29 | 30 | 31 | [![Build Status][build-badge]][build] 32 | [![GPL 3.0 License][license-badge]][license] 33 | [![Code of Conduct][coc-badge]][coc] 34 | 35 | 36 | ## The problem 37 | 38 | You're using a framework like Remix with 39 | [`--manual` mode](https://remix.run/docs/en/guides/manual-mode) and 40 | re-evaluating your modules on every change. But you have some state that you 41 | don't want to lose between changes. For example: 42 | 43 | - Database connections 44 | - In-memory caches 45 | 46 | ## This solution 47 | 48 | This was copy/paste/modified/tested from 49 | [@jenseng's `abuse-the-platform` demo](https://github.com/jenseng/abuse-the-platform/blob/2993a7e846c95ace693ce61626fa072174c8d9c7/app/utils/singleton.ts) 50 | (ISC). It's basically a type-safe singleton implementation that you can use to 51 | keep state between module re-evaluations. 52 | 53 | ## Usage 54 | 55 | ```tsx 56 | import { remember } from '@epic-web/remember' 57 | 58 | export const prisma = remember('prisma', () => new PrismaClient()) 59 | ``` 60 | 61 | Keep in mind that any changes you make within that callback will not be 62 | reflected when the module is re-evaluated (that's the whole point). So if you 63 | need to change the callback, then you'll need to restart your server. 64 | 65 | #### Forget a value 66 | 67 | It might be required to explicitly forget a value if it gets outdated, a memorized 68 | connection gets lost or memorized instance closes/errors/etc. 69 | 70 | ```tsx 71 | import { remember, forget } from '@epic-web/remember' 72 | 73 | export const server = remember('server', () => 74 | http.createServer().listen('8080') 75 | .on('close', () => forget('server'))) 76 | ``` 77 | 78 | ## License 79 | 80 | MIT 81 | 82 | ## Credit 83 | 84 | The original code was written by [@jenseng](https://github.com/jenseng) and then 85 | I modified it and published it to fit my needs. 86 | 87 | 88 | [build-badge]: https://img.shields.io/github/actions/workflow/status/epicweb-dev/remember/release.yml?branch=main&logo=github&style=flat-square 89 | [build]: https://github.com/epicweb-dev/remember/actions?query=workflow%3Arelease 90 | [license-badge]: https://img.shields.io/badge/license-GPL%203.0%20License-blue.svg?style=flat-square 91 | [license]: https://github.com/epicweb-dev/remember/blob/main/LICENSE 92 | [coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square 93 | [coc]: https://kentcdodds.com/conduct 94 | 95 | --------------------------------------------------------------------------------