├── .gitignore ├── .nvmrc ├── biome.json ├── package.json ├── pnpm-lock.yaml ├── readme.md ├── src ├── Computed.ts ├── LazyRef.test.ts ├── LazyRef.ts ├── Provide.ts ├── Versioned.ts ├── index.ts ├── internal.test.ts └── internal.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "organizeImports": { 3 | "enabled": true 4 | }, 5 | "linter": { 6 | "enabled": true, 7 | "rules": { 8 | "recommended": true, 9 | "suspicious": { 10 | "noExplicitAny": "off", 11 | "noAssignInExpressions": "off", 12 | "noShadowRestrictedNames": "off" 13 | }, 14 | "style": { 15 | "noParameterAssign": "off", 16 | "noUselessElse": "off" 17 | }, 18 | "complexity": { 19 | "noBannedTypes": "off" 20 | } 21 | } 22 | }, 23 | "formatter": { 24 | "enabled": true, 25 | "indentStyle": "space", 26 | "indentWidth": 2, 27 | "lineWidth": 100 28 | }, 29 | "javascript": { 30 | "formatter": { 31 | "quoteStyle": "single", 32 | "trailingCommas": "all", 33 | "semicolons": "asNeeded" 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@typed/lazy-ref", 3 | "version": "0.3.3", 4 | "description": "Typed extensions for Effect's LazyRef", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "type": "module", 8 | "scripts": { 9 | "build": "tsc", 10 | "dev": "tsc --watch", 11 | "lint": "biome lint --write", 12 | "test:watch": "vitest --typecheck", 13 | "test": "vitest run --typecheck", 14 | "typecheck": "tsc --noEmit" 15 | }, 16 | "keywords": [ 17 | "effect", 18 | "LazyRef", 19 | "typescript" 20 | ], 21 | "author": "Tylor Steinberger ", 22 | "license": "MIT", 23 | "packageManager": "pnpm@9.12.2+sha512.22721b3a11f81661ae1ec68ce1a7b879425a1ca5b991c975b074ac220b187ce56c708fe5db69f4c962c989452eee76c82877f4ee80f474cebd61ee13461b6228", 24 | "dependencies": { 25 | "effect": "^3.11.9" 26 | }, 27 | "peerDependencies": { 28 | "effect": "^3.11.9" 29 | }, 30 | "devDependencies": { 31 | "@biomejs/biome": "^1.9.4", 32 | "@effect/vitest": "^0.16.0", 33 | "@types/node": "^22.10.2", 34 | "typescript": "^5.4.2", 35 | "vitest": "^2.1.8" 36 | }, 37 | "publishConfig": { 38 | "access": "public" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | effect: 12 | specifier: ^3.11.9 13 | version: 3.12.0 14 | devDependencies: 15 | '@biomejs/biome': 16 | specifier: ^1.9.4 17 | version: 1.9.4 18 | '@effect/vitest': 19 | specifier: ^0.16.0 20 | version: 0.16.0(effect@3.12.0)(vitest@2.1.8(@types/node@22.10.2)) 21 | '@types/node': 22 | specifier: ^22.10.2 23 | version: 22.10.2 24 | typescript: 25 | specifier: ^5.4.2 26 | version: 5.7.2 27 | vitest: 28 | specifier: ^2.1.8 29 | version: 2.1.8(@types/node@22.10.2) 30 | 31 | packages: 32 | 33 | '@biomejs/biome@1.9.4': 34 | resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} 35 | engines: {node: '>=14.21.3'} 36 | hasBin: true 37 | 38 | '@biomejs/cli-darwin-arm64@1.9.4': 39 | resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==} 40 | engines: {node: '>=14.21.3'} 41 | cpu: [arm64] 42 | os: [darwin] 43 | 44 | '@biomejs/cli-darwin-x64@1.9.4': 45 | resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==} 46 | engines: {node: '>=14.21.3'} 47 | cpu: [x64] 48 | os: [darwin] 49 | 50 | '@biomejs/cli-linux-arm64-musl@1.9.4': 51 | resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==} 52 | engines: {node: '>=14.21.3'} 53 | cpu: [arm64] 54 | os: [linux] 55 | 56 | '@biomejs/cli-linux-arm64@1.9.4': 57 | resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} 58 | engines: {node: '>=14.21.3'} 59 | cpu: [arm64] 60 | os: [linux] 61 | 62 | '@biomejs/cli-linux-x64-musl@1.9.4': 63 | resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} 64 | engines: {node: '>=14.21.3'} 65 | cpu: [x64] 66 | os: [linux] 67 | 68 | '@biomejs/cli-linux-x64@1.9.4': 69 | resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} 70 | engines: {node: '>=14.21.3'} 71 | cpu: [x64] 72 | os: [linux] 73 | 74 | '@biomejs/cli-win32-arm64@1.9.4': 75 | resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} 76 | engines: {node: '>=14.21.3'} 77 | cpu: [arm64] 78 | os: [win32] 79 | 80 | '@biomejs/cli-win32-x64@1.9.4': 81 | resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==} 82 | engines: {node: '>=14.21.3'} 83 | cpu: [x64] 84 | os: [win32] 85 | 86 | '@effect/vitest@0.16.0': 87 | resolution: {integrity: sha512-caUXVs8Xsf07XUHRu1pOsHDiww7rE556+0PYZHIg+X6oL6gA9k2+d8ZFmutaW9hJoSQuy+vKyYAFM9/1rVSSAQ==} 88 | peerDependencies: 89 | effect: ^3.12.0 90 | vitest: ^2.0.5 91 | 92 | '@esbuild/aix-ppc64@0.21.5': 93 | resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} 94 | engines: {node: '>=12'} 95 | cpu: [ppc64] 96 | os: [aix] 97 | 98 | '@esbuild/android-arm64@0.21.5': 99 | resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} 100 | engines: {node: '>=12'} 101 | cpu: [arm64] 102 | os: [android] 103 | 104 | '@esbuild/android-arm@0.21.5': 105 | resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} 106 | engines: {node: '>=12'} 107 | cpu: [arm] 108 | os: [android] 109 | 110 | '@esbuild/android-x64@0.21.5': 111 | resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} 112 | engines: {node: '>=12'} 113 | cpu: [x64] 114 | os: [android] 115 | 116 | '@esbuild/darwin-arm64@0.21.5': 117 | resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} 118 | engines: {node: '>=12'} 119 | cpu: [arm64] 120 | os: [darwin] 121 | 122 | '@esbuild/darwin-x64@0.21.5': 123 | resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} 124 | engines: {node: '>=12'} 125 | cpu: [x64] 126 | os: [darwin] 127 | 128 | '@esbuild/freebsd-arm64@0.21.5': 129 | resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} 130 | engines: {node: '>=12'} 131 | cpu: [arm64] 132 | os: [freebsd] 133 | 134 | '@esbuild/freebsd-x64@0.21.5': 135 | resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} 136 | engines: {node: '>=12'} 137 | cpu: [x64] 138 | os: [freebsd] 139 | 140 | '@esbuild/linux-arm64@0.21.5': 141 | resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} 142 | engines: {node: '>=12'} 143 | cpu: [arm64] 144 | os: [linux] 145 | 146 | '@esbuild/linux-arm@0.21.5': 147 | resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} 148 | engines: {node: '>=12'} 149 | cpu: [arm] 150 | os: [linux] 151 | 152 | '@esbuild/linux-ia32@0.21.5': 153 | resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} 154 | engines: {node: '>=12'} 155 | cpu: [ia32] 156 | os: [linux] 157 | 158 | '@esbuild/linux-loong64@0.21.5': 159 | resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} 160 | engines: {node: '>=12'} 161 | cpu: [loong64] 162 | os: [linux] 163 | 164 | '@esbuild/linux-mips64el@0.21.5': 165 | resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} 166 | engines: {node: '>=12'} 167 | cpu: [mips64el] 168 | os: [linux] 169 | 170 | '@esbuild/linux-ppc64@0.21.5': 171 | resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} 172 | engines: {node: '>=12'} 173 | cpu: [ppc64] 174 | os: [linux] 175 | 176 | '@esbuild/linux-riscv64@0.21.5': 177 | resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} 178 | engines: {node: '>=12'} 179 | cpu: [riscv64] 180 | os: [linux] 181 | 182 | '@esbuild/linux-s390x@0.21.5': 183 | resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} 184 | engines: {node: '>=12'} 185 | cpu: [s390x] 186 | os: [linux] 187 | 188 | '@esbuild/linux-x64@0.21.5': 189 | resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} 190 | engines: {node: '>=12'} 191 | cpu: [x64] 192 | os: [linux] 193 | 194 | '@esbuild/netbsd-x64@0.21.5': 195 | resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} 196 | engines: {node: '>=12'} 197 | cpu: [x64] 198 | os: [netbsd] 199 | 200 | '@esbuild/openbsd-x64@0.21.5': 201 | resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} 202 | engines: {node: '>=12'} 203 | cpu: [x64] 204 | os: [openbsd] 205 | 206 | '@esbuild/sunos-x64@0.21.5': 207 | resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} 208 | engines: {node: '>=12'} 209 | cpu: [x64] 210 | os: [sunos] 211 | 212 | '@esbuild/win32-arm64@0.21.5': 213 | resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} 214 | engines: {node: '>=12'} 215 | cpu: [arm64] 216 | os: [win32] 217 | 218 | '@esbuild/win32-ia32@0.21.5': 219 | resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} 220 | engines: {node: '>=12'} 221 | cpu: [ia32] 222 | os: [win32] 223 | 224 | '@esbuild/win32-x64@0.21.5': 225 | resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} 226 | engines: {node: '>=12'} 227 | cpu: [x64] 228 | os: [win32] 229 | 230 | '@jridgewell/sourcemap-codec@1.5.0': 231 | resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 232 | 233 | '@rollup/rollup-android-arm-eabi@4.29.1': 234 | resolution: {integrity: sha512-ssKhA8RNltTZLpG6/QNkCSge+7mBQGUqJRisZ2MDQcEGaK93QESEgWK2iOpIDZ7k9zPVkG5AS3ksvD5ZWxmItw==} 235 | cpu: [arm] 236 | os: [android] 237 | 238 | '@rollup/rollup-android-arm64@4.29.1': 239 | resolution: {integrity: sha512-CaRfrV0cd+NIIcVVN/jx+hVLN+VRqnuzLRmfmlzpOzB87ajixsN/+9L5xNmkaUUvEbI5BmIKS+XTwXsHEb65Ew==} 240 | cpu: [arm64] 241 | os: [android] 242 | 243 | '@rollup/rollup-darwin-arm64@4.29.1': 244 | resolution: {integrity: sha512-2ORr7T31Y0Mnk6qNuwtyNmy14MunTAMx06VAPI6/Ju52W10zk1i7i5U3vlDRWjhOI5quBcrvhkCHyF76bI7kEw==} 245 | cpu: [arm64] 246 | os: [darwin] 247 | 248 | '@rollup/rollup-darwin-x64@4.29.1': 249 | resolution: {integrity: sha512-j/Ej1oanzPjmN0tirRd5K2/nncAhS9W6ICzgxV+9Y5ZsP0hiGhHJXZ2JQ53iSSjj8m6cRY6oB1GMzNn2EUt6Ng==} 250 | cpu: [x64] 251 | os: [darwin] 252 | 253 | '@rollup/rollup-freebsd-arm64@4.29.1': 254 | resolution: {integrity: sha512-91C//G6Dm/cv724tpt7nTyP+JdN12iqeXGFM1SqnljCmi5yTXriH7B1r8AD9dAZByHpKAumqP1Qy2vVNIdLZqw==} 255 | cpu: [arm64] 256 | os: [freebsd] 257 | 258 | '@rollup/rollup-freebsd-x64@4.29.1': 259 | resolution: {integrity: sha512-hEioiEQ9Dec2nIRoeHUP6hr1PSkXzQaCUyqBDQ9I9ik4gCXQZjJMIVzoNLBRGet+hIUb3CISMh9KXuCcWVW/8w==} 260 | cpu: [x64] 261 | os: [freebsd] 262 | 263 | '@rollup/rollup-linux-arm-gnueabihf@4.29.1': 264 | resolution: {integrity: sha512-Py5vFd5HWYN9zxBv3WMrLAXY3yYJ6Q/aVERoeUFwiDGiMOWsMs7FokXihSOaT/PMWUty/Pj60XDQndK3eAfE6A==} 265 | cpu: [arm] 266 | os: [linux] 267 | 268 | '@rollup/rollup-linux-arm-musleabihf@4.29.1': 269 | resolution: {integrity: sha512-RiWpGgbayf7LUcuSNIbahr0ys2YnEERD4gYdISA06wa0i8RALrnzflh9Wxii7zQJEB2/Eh74dX4y/sHKLWp5uQ==} 270 | cpu: [arm] 271 | os: [linux] 272 | 273 | '@rollup/rollup-linux-arm64-gnu@4.29.1': 274 | resolution: {integrity: sha512-Z80O+taYxTQITWMjm/YqNoe9d10OX6kDh8X5/rFCMuPqsKsSyDilvfg+vd3iXIqtfmp+cnfL1UrYirkaF8SBZA==} 275 | cpu: [arm64] 276 | os: [linux] 277 | 278 | '@rollup/rollup-linux-arm64-musl@4.29.1': 279 | resolution: {integrity: sha512-fOHRtF9gahwJk3QVp01a/GqS4hBEZCV1oKglVVq13kcK3NeVlS4BwIFzOHDbmKzt3i0OuHG4zfRP0YoG5OF/rA==} 280 | cpu: [arm64] 281 | os: [linux] 282 | 283 | '@rollup/rollup-linux-loongarch64-gnu@4.29.1': 284 | resolution: {integrity: sha512-5a7q3tnlbcg0OodyxcAdrrCxFi0DgXJSoOuidFUzHZ2GixZXQs6Tc3CHmlvqKAmOs5eRde+JJxeIf9DonkmYkw==} 285 | cpu: [loong64] 286 | os: [linux] 287 | 288 | '@rollup/rollup-linux-powerpc64le-gnu@4.29.1': 289 | resolution: {integrity: sha512-9b4Mg5Yfz6mRnlSPIdROcfw1BU22FQxmfjlp/CShWwO3LilKQuMISMTtAu/bxmmrE6A902W2cZJuzx8+gJ8e9w==} 290 | cpu: [ppc64] 291 | os: [linux] 292 | 293 | '@rollup/rollup-linux-riscv64-gnu@4.29.1': 294 | resolution: {integrity: sha512-G5pn0NChlbRM8OJWpJFMX4/i8OEU538uiSv0P6roZcbpe/WfhEO+AT8SHVKfp8qhDQzaz7Q+1/ixMy7hBRidnQ==} 295 | cpu: [riscv64] 296 | os: [linux] 297 | 298 | '@rollup/rollup-linux-s390x-gnu@4.29.1': 299 | resolution: {integrity: sha512-WM9lIkNdkhVwiArmLxFXpWndFGuOka4oJOZh8EP3Vb8q5lzdSCBuhjavJsw68Q9AKDGeOOIHYzYm4ZFvmWez5g==} 300 | cpu: [s390x] 301 | os: [linux] 302 | 303 | '@rollup/rollup-linux-x64-gnu@4.29.1': 304 | resolution: {integrity: sha512-87xYCwb0cPGZFoGiErT1eDcssByaLX4fc0z2nRM6eMtV9njAfEE6OW3UniAoDhX4Iq5xQVpE6qO9aJbCFumKYQ==} 305 | cpu: [x64] 306 | os: [linux] 307 | 308 | '@rollup/rollup-linux-x64-musl@4.29.1': 309 | resolution: {integrity: sha512-xufkSNppNOdVRCEC4WKvlR1FBDyqCSCpQeMMgv9ZyXqqtKBfkw1yfGMTUTs9Qsl6WQbJnsGboWCp7pJGkeMhKA==} 310 | cpu: [x64] 311 | os: [linux] 312 | 313 | '@rollup/rollup-win32-arm64-msvc@4.29.1': 314 | resolution: {integrity: sha512-F2OiJ42m77lSkizZQLuC+jiZ2cgueWQL5YC9tjo3AgaEw+KJmVxHGSyQfDUoYR9cci0lAywv2Clmckzulcq6ig==} 315 | cpu: [arm64] 316 | os: [win32] 317 | 318 | '@rollup/rollup-win32-ia32-msvc@4.29.1': 319 | resolution: {integrity: sha512-rYRe5S0FcjlOBZQHgbTKNrqxCBUmgDJem/VQTCcTnA2KCabYSWQDrytOzX7avb79cAAweNmMUb/Zw18RNd4mng==} 320 | cpu: [ia32] 321 | os: [win32] 322 | 323 | '@rollup/rollup-win32-x64-msvc@4.29.1': 324 | resolution: {integrity: sha512-+10CMg9vt1MoHj6x1pxyjPSMjHTIlqs8/tBztXvPAx24SKs9jwVnKqHJumlH/IzhaPUaj3T6T6wfZr8okdXaIg==} 325 | cpu: [x64] 326 | os: [win32] 327 | 328 | '@types/estree@1.0.6': 329 | resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} 330 | 331 | '@types/node@22.10.2': 332 | resolution: {integrity: sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==} 333 | 334 | '@vitest/expect@2.1.8': 335 | resolution: {integrity: sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==} 336 | 337 | '@vitest/mocker@2.1.8': 338 | resolution: {integrity: sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==} 339 | peerDependencies: 340 | msw: ^2.4.9 341 | vite: ^5.0.0 342 | peerDependenciesMeta: 343 | msw: 344 | optional: true 345 | vite: 346 | optional: true 347 | 348 | '@vitest/pretty-format@2.1.8': 349 | resolution: {integrity: sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==} 350 | 351 | '@vitest/runner@2.1.8': 352 | resolution: {integrity: sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==} 353 | 354 | '@vitest/snapshot@2.1.8': 355 | resolution: {integrity: sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==} 356 | 357 | '@vitest/spy@2.1.8': 358 | resolution: {integrity: sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==} 359 | 360 | '@vitest/utils@2.1.8': 361 | resolution: {integrity: sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==} 362 | 363 | assertion-error@2.0.1: 364 | resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} 365 | engines: {node: '>=12'} 366 | 367 | cac@6.7.14: 368 | resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} 369 | engines: {node: '>=8'} 370 | 371 | chai@5.1.2: 372 | resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==} 373 | engines: {node: '>=12'} 374 | 375 | check-error@2.1.1: 376 | resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} 377 | engines: {node: '>= 16'} 378 | 379 | debug@4.4.0: 380 | resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} 381 | engines: {node: '>=6.0'} 382 | peerDependencies: 383 | supports-color: '*' 384 | peerDependenciesMeta: 385 | supports-color: 386 | optional: true 387 | 388 | deep-eql@5.0.2: 389 | resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} 390 | engines: {node: '>=6'} 391 | 392 | effect@3.12.0: 393 | resolution: {integrity: sha512-b/u9s3b9HfTo0qygVouegP0hkbiuxRIeaCe1ppf8P88hPyl6lKCbErtn7Az4jG7LuU7f0Wgm4c8WXbMcL2j8+g==} 394 | 395 | es-module-lexer@1.6.0: 396 | resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} 397 | 398 | esbuild@0.21.5: 399 | resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} 400 | engines: {node: '>=12'} 401 | hasBin: true 402 | 403 | estree-walker@3.0.3: 404 | resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} 405 | 406 | expect-type@1.1.0: 407 | resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} 408 | engines: {node: '>=12.0.0'} 409 | 410 | fast-check@3.23.2: 411 | resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} 412 | engines: {node: '>=8.0.0'} 413 | 414 | fsevents@2.3.3: 415 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 416 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 417 | os: [darwin] 418 | 419 | loupe@3.1.2: 420 | resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} 421 | 422 | magic-string@0.30.17: 423 | resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} 424 | 425 | ms@2.1.3: 426 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 427 | 428 | nanoid@3.3.8: 429 | resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} 430 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 431 | hasBin: true 432 | 433 | pathe@1.1.2: 434 | resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} 435 | 436 | pathval@2.0.0: 437 | resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} 438 | engines: {node: '>= 14.16'} 439 | 440 | picocolors@1.1.1: 441 | resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 442 | 443 | postcss@8.4.49: 444 | resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} 445 | engines: {node: ^10 || ^12 || >=14} 446 | 447 | pure-rand@6.1.0: 448 | resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} 449 | 450 | rollup@4.29.1: 451 | resolution: {integrity: sha512-RaJ45M/kmJUzSWDs1Nnd5DdV4eerC98idtUOVr6FfKcgxqvjwHmxc5upLF9qZU9EpsVzzhleFahrT3shLuJzIw==} 452 | engines: {node: '>=18.0.0', npm: '>=8.0.0'} 453 | hasBin: true 454 | 455 | siginfo@2.0.0: 456 | resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} 457 | 458 | source-map-js@1.2.1: 459 | resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 460 | engines: {node: '>=0.10.0'} 461 | 462 | stackback@0.0.2: 463 | resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} 464 | 465 | std-env@3.8.0: 466 | resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} 467 | 468 | tinybench@2.9.0: 469 | resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} 470 | 471 | tinyexec@0.3.1: 472 | resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} 473 | 474 | tinypool@1.0.2: 475 | resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} 476 | engines: {node: ^18.0.0 || >=20.0.0} 477 | 478 | tinyrainbow@1.2.0: 479 | resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} 480 | engines: {node: '>=14.0.0'} 481 | 482 | tinyspy@3.0.2: 483 | resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} 484 | engines: {node: '>=14.0.0'} 485 | 486 | typescript@5.7.2: 487 | resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} 488 | engines: {node: '>=14.17'} 489 | hasBin: true 490 | 491 | undici-types@6.20.0: 492 | resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} 493 | 494 | vite-node@2.1.8: 495 | resolution: {integrity: sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==} 496 | engines: {node: ^18.0.0 || >=20.0.0} 497 | hasBin: true 498 | 499 | vite@5.4.11: 500 | resolution: {integrity: sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==} 501 | engines: {node: ^18.0.0 || >=20.0.0} 502 | hasBin: true 503 | peerDependencies: 504 | '@types/node': ^18.0.0 || >=20.0.0 505 | less: '*' 506 | lightningcss: ^1.21.0 507 | sass: '*' 508 | sass-embedded: '*' 509 | stylus: '*' 510 | sugarss: '*' 511 | terser: ^5.4.0 512 | peerDependenciesMeta: 513 | '@types/node': 514 | optional: true 515 | less: 516 | optional: true 517 | lightningcss: 518 | optional: true 519 | sass: 520 | optional: true 521 | sass-embedded: 522 | optional: true 523 | stylus: 524 | optional: true 525 | sugarss: 526 | optional: true 527 | terser: 528 | optional: true 529 | 530 | vitest@2.1.8: 531 | resolution: {integrity: sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==} 532 | engines: {node: ^18.0.0 || >=20.0.0} 533 | hasBin: true 534 | peerDependencies: 535 | '@edge-runtime/vm': '*' 536 | '@types/node': ^18.0.0 || >=20.0.0 537 | '@vitest/browser': 2.1.8 538 | '@vitest/ui': 2.1.8 539 | happy-dom: '*' 540 | jsdom: '*' 541 | peerDependenciesMeta: 542 | '@edge-runtime/vm': 543 | optional: true 544 | '@types/node': 545 | optional: true 546 | '@vitest/browser': 547 | optional: true 548 | '@vitest/ui': 549 | optional: true 550 | happy-dom: 551 | optional: true 552 | jsdom: 553 | optional: true 554 | 555 | why-is-node-running@2.3.0: 556 | resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} 557 | engines: {node: '>=8'} 558 | hasBin: true 559 | 560 | snapshots: 561 | 562 | '@biomejs/biome@1.9.4': 563 | optionalDependencies: 564 | '@biomejs/cli-darwin-arm64': 1.9.4 565 | '@biomejs/cli-darwin-x64': 1.9.4 566 | '@biomejs/cli-linux-arm64': 1.9.4 567 | '@biomejs/cli-linux-arm64-musl': 1.9.4 568 | '@biomejs/cli-linux-x64': 1.9.4 569 | '@biomejs/cli-linux-x64-musl': 1.9.4 570 | '@biomejs/cli-win32-arm64': 1.9.4 571 | '@biomejs/cli-win32-x64': 1.9.4 572 | 573 | '@biomejs/cli-darwin-arm64@1.9.4': 574 | optional: true 575 | 576 | '@biomejs/cli-darwin-x64@1.9.4': 577 | optional: true 578 | 579 | '@biomejs/cli-linux-arm64-musl@1.9.4': 580 | optional: true 581 | 582 | '@biomejs/cli-linux-arm64@1.9.4': 583 | optional: true 584 | 585 | '@biomejs/cli-linux-x64-musl@1.9.4': 586 | optional: true 587 | 588 | '@biomejs/cli-linux-x64@1.9.4': 589 | optional: true 590 | 591 | '@biomejs/cli-win32-arm64@1.9.4': 592 | optional: true 593 | 594 | '@biomejs/cli-win32-x64@1.9.4': 595 | optional: true 596 | 597 | '@effect/vitest@0.16.0(effect@3.12.0)(vitest@2.1.8(@types/node@22.10.2))': 598 | dependencies: 599 | effect: 3.12.0 600 | vitest: 2.1.8(@types/node@22.10.2) 601 | 602 | '@esbuild/aix-ppc64@0.21.5': 603 | optional: true 604 | 605 | '@esbuild/android-arm64@0.21.5': 606 | optional: true 607 | 608 | '@esbuild/android-arm@0.21.5': 609 | optional: true 610 | 611 | '@esbuild/android-x64@0.21.5': 612 | optional: true 613 | 614 | '@esbuild/darwin-arm64@0.21.5': 615 | optional: true 616 | 617 | '@esbuild/darwin-x64@0.21.5': 618 | optional: true 619 | 620 | '@esbuild/freebsd-arm64@0.21.5': 621 | optional: true 622 | 623 | '@esbuild/freebsd-x64@0.21.5': 624 | optional: true 625 | 626 | '@esbuild/linux-arm64@0.21.5': 627 | optional: true 628 | 629 | '@esbuild/linux-arm@0.21.5': 630 | optional: true 631 | 632 | '@esbuild/linux-ia32@0.21.5': 633 | optional: true 634 | 635 | '@esbuild/linux-loong64@0.21.5': 636 | optional: true 637 | 638 | '@esbuild/linux-mips64el@0.21.5': 639 | optional: true 640 | 641 | '@esbuild/linux-ppc64@0.21.5': 642 | optional: true 643 | 644 | '@esbuild/linux-riscv64@0.21.5': 645 | optional: true 646 | 647 | '@esbuild/linux-s390x@0.21.5': 648 | optional: true 649 | 650 | '@esbuild/linux-x64@0.21.5': 651 | optional: true 652 | 653 | '@esbuild/netbsd-x64@0.21.5': 654 | optional: true 655 | 656 | '@esbuild/openbsd-x64@0.21.5': 657 | optional: true 658 | 659 | '@esbuild/sunos-x64@0.21.5': 660 | optional: true 661 | 662 | '@esbuild/win32-arm64@0.21.5': 663 | optional: true 664 | 665 | '@esbuild/win32-ia32@0.21.5': 666 | optional: true 667 | 668 | '@esbuild/win32-x64@0.21.5': 669 | optional: true 670 | 671 | '@jridgewell/sourcemap-codec@1.5.0': {} 672 | 673 | '@rollup/rollup-android-arm-eabi@4.29.1': 674 | optional: true 675 | 676 | '@rollup/rollup-android-arm64@4.29.1': 677 | optional: true 678 | 679 | '@rollup/rollup-darwin-arm64@4.29.1': 680 | optional: true 681 | 682 | '@rollup/rollup-darwin-x64@4.29.1': 683 | optional: true 684 | 685 | '@rollup/rollup-freebsd-arm64@4.29.1': 686 | optional: true 687 | 688 | '@rollup/rollup-freebsd-x64@4.29.1': 689 | optional: true 690 | 691 | '@rollup/rollup-linux-arm-gnueabihf@4.29.1': 692 | optional: true 693 | 694 | '@rollup/rollup-linux-arm-musleabihf@4.29.1': 695 | optional: true 696 | 697 | '@rollup/rollup-linux-arm64-gnu@4.29.1': 698 | optional: true 699 | 700 | '@rollup/rollup-linux-arm64-musl@4.29.1': 701 | optional: true 702 | 703 | '@rollup/rollup-linux-loongarch64-gnu@4.29.1': 704 | optional: true 705 | 706 | '@rollup/rollup-linux-powerpc64le-gnu@4.29.1': 707 | optional: true 708 | 709 | '@rollup/rollup-linux-riscv64-gnu@4.29.1': 710 | optional: true 711 | 712 | '@rollup/rollup-linux-s390x-gnu@4.29.1': 713 | optional: true 714 | 715 | '@rollup/rollup-linux-x64-gnu@4.29.1': 716 | optional: true 717 | 718 | '@rollup/rollup-linux-x64-musl@4.29.1': 719 | optional: true 720 | 721 | '@rollup/rollup-win32-arm64-msvc@4.29.1': 722 | optional: true 723 | 724 | '@rollup/rollup-win32-ia32-msvc@4.29.1': 725 | optional: true 726 | 727 | '@rollup/rollup-win32-x64-msvc@4.29.1': 728 | optional: true 729 | 730 | '@types/estree@1.0.6': {} 731 | 732 | '@types/node@22.10.2': 733 | dependencies: 734 | undici-types: 6.20.0 735 | 736 | '@vitest/expect@2.1.8': 737 | dependencies: 738 | '@vitest/spy': 2.1.8 739 | '@vitest/utils': 2.1.8 740 | chai: 5.1.2 741 | tinyrainbow: 1.2.0 742 | 743 | '@vitest/mocker@2.1.8(vite@5.4.11(@types/node@22.10.2))': 744 | dependencies: 745 | '@vitest/spy': 2.1.8 746 | estree-walker: 3.0.3 747 | magic-string: 0.30.17 748 | optionalDependencies: 749 | vite: 5.4.11(@types/node@22.10.2) 750 | 751 | '@vitest/pretty-format@2.1.8': 752 | dependencies: 753 | tinyrainbow: 1.2.0 754 | 755 | '@vitest/runner@2.1.8': 756 | dependencies: 757 | '@vitest/utils': 2.1.8 758 | pathe: 1.1.2 759 | 760 | '@vitest/snapshot@2.1.8': 761 | dependencies: 762 | '@vitest/pretty-format': 2.1.8 763 | magic-string: 0.30.17 764 | pathe: 1.1.2 765 | 766 | '@vitest/spy@2.1.8': 767 | dependencies: 768 | tinyspy: 3.0.2 769 | 770 | '@vitest/utils@2.1.8': 771 | dependencies: 772 | '@vitest/pretty-format': 2.1.8 773 | loupe: 3.1.2 774 | tinyrainbow: 1.2.0 775 | 776 | assertion-error@2.0.1: {} 777 | 778 | cac@6.7.14: {} 779 | 780 | chai@5.1.2: 781 | dependencies: 782 | assertion-error: 2.0.1 783 | check-error: 2.1.1 784 | deep-eql: 5.0.2 785 | loupe: 3.1.2 786 | pathval: 2.0.0 787 | 788 | check-error@2.1.1: {} 789 | 790 | debug@4.4.0: 791 | dependencies: 792 | ms: 2.1.3 793 | 794 | deep-eql@5.0.2: {} 795 | 796 | effect@3.12.0: 797 | dependencies: 798 | fast-check: 3.23.2 799 | 800 | es-module-lexer@1.6.0: {} 801 | 802 | esbuild@0.21.5: 803 | optionalDependencies: 804 | '@esbuild/aix-ppc64': 0.21.5 805 | '@esbuild/android-arm': 0.21.5 806 | '@esbuild/android-arm64': 0.21.5 807 | '@esbuild/android-x64': 0.21.5 808 | '@esbuild/darwin-arm64': 0.21.5 809 | '@esbuild/darwin-x64': 0.21.5 810 | '@esbuild/freebsd-arm64': 0.21.5 811 | '@esbuild/freebsd-x64': 0.21.5 812 | '@esbuild/linux-arm': 0.21.5 813 | '@esbuild/linux-arm64': 0.21.5 814 | '@esbuild/linux-ia32': 0.21.5 815 | '@esbuild/linux-loong64': 0.21.5 816 | '@esbuild/linux-mips64el': 0.21.5 817 | '@esbuild/linux-ppc64': 0.21.5 818 | '@esbuild/linux-riscv64': 0.21.5 819 | '@esbuild/linux-s390x': 0.21.5 820 | '@esbuild/linux-x64': 0.21.5 821 | '@esbuild/netbsd-x64': 0.21.5 822 | '@esbuild/openbsd-x64': 0.21.5 823 | '@esbuild/sunos-x64': 0.21.5 824 | '@esbuild/win32-arm64': 0.21.5 825 | '@esbuild/win32-ia32': 0.21.5 826 | '@esbuild/win32-x64': 0.21.5 827 | 828 | estree-walker@3.0.3: 829 | dependencies: 830 | '@types/estree': 1.0.6 831 | 832 | expect-type@1.1.0: {} 833 | 834 | fast-check@3.23.2: 835 | dependencies: 836 | pure-rand: 6.1.0 837 | 838 | fsevents@2.3.3: 839 | optional: true 840 | 841 | loupe@3.1.2: {} 842 | 843 | magic-string@0.30.17: 844 | dependencies: 845 | '@jridgewell/sourcemap-codec': 1.5.0 846 | 847 | ms@2.1.3: {} 848 | 849 | nanoid@3.3.8: {} 850 | 851 | pathe@1.1.2: {} 852 | 853 | pathval@2.0.0: {} 854 | 855 | picocolors@1.1.1: {} 856 | 857 | postcss@8.4.49: 858 | dependencies: 859 | nanoid: 3.3.8 860 | picocolors: 1.1.1 861 | source-map-js: 1.2.1 862 | 863 | pure-rand@6.1.0: {} 864 | 865 | rollup@4.29.1: 866 | dependencies: 867 | '@types/estree': 1.0.6 868 | optionalDependencies: 869 | '@rollup/rollup-android-arm-eabi': 4.29.1 870 | '@rollup/rollup-android-arm64': 4.29.1 871 | '@rollup/rollup-darwin-arm64': 4.29.1 872 | '@rollup/rollup-darwin-x64': 4.29.1 873 | '@rollup/rollup-freebsd-arm64': 4.29.1 874 | '@rollup/rollup-freebsd-x64': 4.29.1 875 | '@rollup/rollup-linux-arm-gnueabihf': 4.29.1 876 | '@rollup/rollup-linux-arm-musleabihf': 4.29.1 877 | '@rollup/rollup-linux-arm64-gnu': 4.29.1 878 | '@rollup/rollup-linux-arm64-musl': 4.29.1 879 | '@rollup/rollup-linux-loongarch64-gnu': 4.29.1 880 | '@rollup/rollup-linux-powerpc64le-gnu': 4.29.1 881 | '@rollup/rollup-linux-riscv64-gnu': 4.29.1 882 | '@rollup/rollup-linux-s390x-gnu': 4.29.1 883 | '@rollup/rollup-linux-x64-gnu': 4.29.1 884 | '@rollup/rollup-linux-x64-musl': 4.29.1 885 | '@rollup/rollup-win32-arm64-msvc': 4.29.1 886 | '@rollup/rollup-win32-ia32-msvc': 4.29.1 887 | '@rollup/rollup-win32-x64-msvc': 4.29.1 888 | fsevents: 2.3.3 889 | 890 | siginfo@2.0.0: {} 891 | 892 | source-map-js@1.2.1: {} 893 | 894 | stackback@0.0.2: {} 895 | 896 | std-env@3.8.0: {} 897 | 898 | tinybench@2.9.0: {} 899 | 900 | tinyexec@0.3.1: {} 901 | 902 | tinypool@1.0.2: {} 903 | 904 | tinyrainbow@1.2.0: {} 905 | 906 | tinyspy@3.0.2: {} 907 | 908 | typescript@5.7.2: {} 909 | 910 | undici-types@6.20.0: {} 911 | 912 | vite-node@2.1.8(@types/node@22.10.2): 913 | dependencies: 914 | cac: 6.7.14 915 | debug: 4.4.0 916 | es-module-lexer: 1.6.0 917 | pathe: 1.1.2 918 | vite: 5.4.11(@types/node@22.10.2) 919 | transitivePeerDependencies: 920 | - '@types/node' 921 | - less 922 | - lightningcss 923 | - sass 924 | - sass-embedded 925 | - stylus 926 | - sugarss 927 | - supports-color 928 | - terser 929 | 930 | vite@5.4.11(@types/node@22.10.2): 931 | dependencies: 932 | esbuild: 0.21.5 933 | postcss: 8.4.49 934 | rollup: 4.29.1 935 | optionalDependencies: 936 | '@types/node': 22.10.2 937 | fsevents: 2.3.3 938 | 939 | vitest@2.1.8(@types/node@22.10.2): 940 | dependencies: 941 | '@vitest/expect': 2.1.8 942 | '@vitest/mocker': 2.1.8(vite@5.4.11(@types/node@22.10.2)) 943 | '@vitest/pretty-format': 2.1.8 944 | '@vitest/runner': 2.1.8 945 | '@vitest/snapshot': 2.1.8 946 | '@vitest/spy': 2.1.8 947 | '@vitest/utils': 2.1.8 948 | chai: 5.1.2 949 | debug: 4.4.0 950 | expect-type: 1.1.0 951 | magic-string: 0.30.17 952 | pathe: 1.1.2 953 | std-env: 3.8.0 954 | tinybench: 2.9.0 955 | tinyexec: 0.3.1 956 | tinypool: 1.0.2 957 | tinyrainbow: 1.2.0 958 | vite: 5.4.11(@types/node@22.10.2) 959 | vite-node: 2.1.8(@types/node@22.10.2) 960 | why-is-node-running: 2.3.0 961 | optionalDependencies: 962 | '@types/node': 22.10.2 963 | transitivePeerDependencies: 964 | - less 965 | - lightningcss 966 | - msw 967 | - sass 968 | - sass-embedded 969 | - stylus 970 | - sugarss 971 | - supports-color 972 | - terser 973 | 974 | why-is-node-running@2.3.0: 975 | dependencies: 976 | siginfo: 2.0.0 977 | stackback: 0.0.2 978 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # @typed/lazy-ref 2 | 3 | A powerful, type-safe reactive state management library for Effect, combining the best of lazy evaluation and reactive programming. This library provides a way to manage state that is both efficient (computed only when needed) and reactive (automatically updates dependents when changes occur). 4 | 5 | ## Features 6 | 7 | - 🔄 **Lazy Evaluation**: Values are computed only when needed, saving resources and improving performance 8 | - 🎯 **Type-safe**: Full TypeScript support with precise type inference, catching errors at compile time 9 | - 🔗 **Composable**: Combine refs using `struct`, `tuple`, and other combinators to build complex state structures 10 | - 🎭 **Reactive**: Stream-based updates with automatic value propagation to all dependents 11 | - 🎨 **Flexible**: Works with both `Effect` and `Stream` based computations for different use cases 12 | - 🏷️ **Tagged**: First-class support for Effect's tagged services pattern for dependency injection 13 | - 🔄 **Concurrency-safe Updates**: Thread-safe state updates with `runUpdates` preventing race conditions 14 | 15 | ## Installation 16 | 17 | ```bash 18 | npm install @typed/lazy-ref 19 | # or 20 | yarn add @typed/lazy-ref 21 | # or 22 | pnpm add @typed/lazy-ref 23 | ``` 24 | 25 | ## Quick Start 26 | 27 | Here's a simple example demonstrating the basic usage of LazyRef. This example shows how to create, read, update, and reset a reference: 28 | 29 | ```typescript 30 | import * as LazyRef from '@typed/lazy-ref' 31 | import { Effect } from 'effect' 32 | 33 | // Create a simple counter 34 | const program = Effect.gen(function* () { 35 | // Initialize with 0 36 | const counter = yield* LazyRef.of(0) 37 | 38 | // Get current value 39 | const initial = yield* counter 40 | console.log(initial) // 0 41 | 42 | // Update value 43 | yield* LazyRef.update(counter, x => x + 1) 44 | console.log(yield* counter) // 1 45 | 46 | // Reset to initial state 47 | yield* LazyRef.delete(counter) 48 | console.log(yield* counter) // 0 49 | }) 50 | 51 | // Run the program with proper resource management 52 | Effect.runPromise(Effect.scoped(program)) 53 | ``` 54 | 55 | ## Core Concepts 56 | 57 | ### LazyRef 58 | 59 | A `LazyRef` is a reference to a value that can be lazily computed and reactively updated. Think of it as a smart container for your values that provides: 60 | 61 | - **Lazy evaluation**: Values are only computed when someone actually needs them 62 | - **Reactive updates**: When a value changes, all dependent computations are automatically updated 63 | - **Resource management**: Proper cleanup of resources when they're no longer needed 64 | - **Concurrency safety**: Thread-safe updates preventing race conditions 65 | 66 | The interface shows the core capabilities: 67 | 68 | ```typescript 69 | // An Effect which can retrieve the current value 70 | interface LazyRef extends Effect { 71 | // Stream of value changes 72 | readonly changes: Stream 73 | // Current version number (increments with updates) 74 | readonly version: Effect 75 | // Safe concurrent updates 76 | readonly runUpdates: ( 77 | f: (ref: GetSetDelete) => Effect 78 | ) => Effect 79 | } 80 | ``` 81 | 82 | ### Creating LazyRefs 83 | 84 | There are several ways to create a LazyRef, each suited for different use cases: 85 | 86 | ```typescript 87 | // From a simple value - good for initial states 88 | const ref = LazyRef.of(initialValue) 89 | 90 | // From an Effect - good for async computations 91 | const ref = LazyRef.make(Effect.succeed(value)) 92 | 93 | // From a Stream - good for continuous updates 94 | const ref = LazyRef.make(Stream.succeed(value)) 95 | 96 | // From a failing computation - good for error handling 97 | const ref = LazyRef.fail(error) 98 | ``` 99 | 100 | ### Composing LazyRefs 101 | 102 | One of the most powerful features of LazyRef is its ability to compose multiple refs together. This allows you to build complex state structures while maintaining reactivity. 103 | 104 | #### Struct Composition 105 | 106 | Use struct composition when you want to combine multiple refs into an object structure: 107 | 108 | ```typescript 109 | const program = Effect.gen(function* () { 110 | const a = yield* LazyRef.of(0) 111 | const b = yield* LazyRef.of(1) 112 | const c = yield* LazyRef.of(2) 113 | 114 | // Combine into an object structure 115 | const struct = LazyRef.struct({ a, b, c }) 116 | 117 | console.log(yield* struct) // { a: 0, b: 1, c: 2 } 118 | 119 | // Updates to individual refs automatically propagate 120 | yield* LazyRef.update(a, x => x + 1) 121 | console.log(yield* struct) // { a: 1, b: 1, c: 2 } 122 | }) 123 | ``` 124 | 125 | #### Tuple Composition 126 | 127 | Use tuple composition when you want to combine refs into an array-like structure: 128 | 129 | ```typescript 130 | const program = Effect.gen(function* () { 131 | const a = yield* LazyRef.of(0) 132 | const b = yield* LazyRef.of(1) 133 | 134 | // Combine into a tuple 135 | const tuple = LazyRef.tuple(a, b) 136 | 137 | console.log(yield* tuple) // [0, 1] 138 | 139 | // Update the entire tuple atomically 140 | yield* LazyRef.update(tuple, ([x, y]) => [x + 1, y + 1]) 141 | console.log(yield* tuple) // [1, 2] 142 | }) 143 | ``` 144 | 145 | ### Computed Values 146 | 147 | Computed values are derived state that automatically update when their dependencies change. This is perfect for maintaining derived state that should stay in sync with source data: 148 | 149 | ```typescript 150 | const program = Effect.gen(function* () { 151 | const count = yield* LazyRef.of(0) 152 | // Create a computed value that's always double the count 153 | const doubled = LazyRef.map(count, x => x * 2) 154 | 155 | console.log(yield* doubled) // 0 156 | yield* LazyRef.update(count, x => x + 1) 157 | console.log(yield* doubled) // 2 - automatically updated! 158 | }) 159 | ``` 160 | 161 | ### Tagged Services 162 | 163 | Tagged services allow you to integrate LazyRef with Effect's dependency injection system. This is great for managing global state and dependencies and provides the same LazyRef interface just provided from Effect's Context: 164 | 165 | ```typescript 166 | class Counter extends LazyRef.Tag('Counter')() { 167 | // Define operations as static members for clean usage 168 | static increment = LazyRef.update(Counter, (x) => x + 1) 169 | static decrement = LazyRef.update(Counter, (x) => x - 1) 170 | } 171 | 172 | const program = Effect.gen(function* () { 173 | console.log(yield* Counter) // 0 174 | yield* Counter.increment 175 | yield* Counter.increment 176 | yield* Counter.decrement 177 | console.log(yield* Counter) // 1 178 | }).pipe( 179 | Effect.provide(Counter.of(0)), 180 | // Alternative ways to provide the service: 181 | // Effect.provide(Counter.make(Effect.succeed(0))) 182 | // Effect.provide(Counter.make(Stream.succeed(0))) 183 | ) 184 | ``` 185 | 186 | ### Updates 187 | 188 | LazyRef provides concurrency-safe updates through the `runUpdates` method. This ensures that updates are atomic and prevent race conditions: 189 | 190 | ```typescript 191 | const program = Effect.gen(function* () { 192 | const ref = yield* LazyRef.of(0) 193 | 194 | yield* ref.runUpdates(({ get, set }) => 195 | // Runs within a Semaphore of 1 (Lock) 196 | Effect.gen(function* () { 197 | const current = yield* get 198 | yield* set(current + 1) 199 | return "Updated!" 200 | }) 201 | ) 202 | }) 203 | ``` 204 | 205 | ## Advanced Features 206 | 207 | ### Streams 208 | 209 | Every LazyRef comes with built-in support for reactive programming through its `changes` stream. This allows you to react to changes over time: 210 | 211 | ```typescript 212 | const program = Effect.gen(function* () { 213 | const ref = yield* LazyRef.of(0) 214 | // Subscribe to the next 3 values 215 | const fiber = yield* ref.changes.pipe( 216 | Stream.take(3), 217 | Stream.runCollect, 218 | Effect.fork 219 | ) 220 | 221 | // We need to allow time for changes fiber to start subscribing 222 | yield* Effect.sleep(0) 223 | 224 | // Simulate another process updating 225 | yield* LazyRef.update(ref, x => x + 1) 226 | yield* LazyRef.update(ref, x => x + 1) 227 | const values = yield* Effect.fromFiber(fiber) 228 | console.log(Array.from(values)) // [0, 1, 2] 229 | }) 230 | ``` 231 | 232 | ### Resource Management 233 | 234 | LazyRef integrates with Effect's resource management system. Resources are automatically cleaned up when their scope ends, but you can also manage them manually: 235 | 236 | ```typescript 237 | const program = Effect.gen(function* () { 238 | const ref = yield* LazyRef.of(0) 239 | 240 | // Initiate cleanup in a background Fiber 241 | yield* ref.shutdown 242 | 243 | // Wait for cleanup to complete if needed 244 | yield* ref.awaitShutdown 245 | }) 246 | ``` 247 | 248 | ## License 249 | 250 | MIT 251 | 252 | -------------------------------------------------------------------------------- /src/Computed.ts: -------------------------------------------------------------------------------- 1 | import type * as Context from 'effect/Context' 2 | import * as Effect from 'effect/Effect' 3 | import { hasProperty } from 'effect/Predicate' 4 | import * as Stream from 'effect/Stream' 5 | import type * as Types from 'effect/Types' 6 | import { VersionedImpl, type Versioned } from './Versioned.js' 7 | 8 | export const ComputedTypeId = Symbol.for('@typed/LazyRef/Computed') 9 | export type ComputedTypeId = typeof ComputedTypeId 10 | 11 | export interface Computed extends Versioned { 12 | readonly [ComputedTypeId]: Computed.Variance 13 | } 14 | 15 | export namespace Computed { 16 | export type Variance = { 17 | readonly _A: Types.Covariant 18 | readonly _E: Types.Covariant 19 | readonly _R: Types.Covariant 20 | } 21 | 22 | export type Success = [T] extends [ 23 | { readonly [ComputedTypeId]: Variance }, 24 | ] 25 | ? A 26 | : never 27 | export type Error = [T] extends [ 28 | { readonly [ComputedTypeId]: Variance }, 29 | ] 30 | ? E 31 | : never 32 | export type Context = [T] extends [ 33 | { readonly [ComputedTypeId]: Variance }, 34 | ] 35 | ? R 36 | : never 37 | } 38 | 39 | /** 40 | * @internal 41 | */ 42 | export function makeComputed( 43 | get: Effect.Effect, 44 | changes: Stream.Stream, 45 | version: Effect.Effect, 46 | ): Computed { 47 | return new ComputedImpl(get, changes, version) 48 | } 49 | 50 | /** 51 | * @internal 52 | */ 53 | export function fromVersioned(versioned: Versioned): Computed { 54 | return new ComputedImpl(versioned.get, versioned.changes, versioned.version) 55 | } 56 | 57 | const variance: Computed.Variance = { 58 | _A: (_) => _, 59 | _E: (_) => _, 60 | _R: (_) => _, 61 | } 62 | 63 | class ComputedImpl extends VersionedImpl implements Computed { 64 | readonly [ComputedTypeId]: Computed.Variance = variance 65 | } 66 | 67 | export function isComputed( 68 | value: unknown, 69 | ): value is Computed { 70 | return hasProperty(value, ComputedTypeId) 71 | } 72 | 73 | export function computedFromTag( 74 | tag: Context.Tag, 75 | f: (s: S) => Computed, 76 | ): Computed { 77 | return new ComputedImpl( 78 | Effect.flatMap(tag, f), 79 | Stream.unwrap(Effect.map(tag, (s) => f(s).changes)), 80 | Effect.flatMap(tag, (s) => f(s).version), 81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /src/LazyRef.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@effect/vitest' 2 | import { Effect, Option, Stream } from 'effect' 3 | import { deepStrictEqual } from 'node:assert' 4 | import * as LazyRef from './LazyRef' 5 | import { increment } from 'effect/Number' 6 | 7 | describe('LazyRef', () => { 8 | it.scoped('allows keeping state with Effect', () => 9 | Effect.gen(function* () { 10 | const ref = yield* LazyRef.make(Effect.succeed(0)) 11 | 12 | yield* ref 13 | yield* LazyRef.update(ref, (x) => x + 1) 14 | yield* LazyRef.delete(ref) 15 | yield* ref 16 | 17 | deepStrictEqual(yield* ref, 0) 18 | deepStrictEqual(yield* LazyRef.update(ref, (x) => x + 1), 1) 19 | deepStrictEqual(yield* LazyRef.delete(ref), Option.some(1)) 20 | deepStrictEqual(yield* ref, 0) 21 | }), 22 | ) 23 | 24 | it.scoped('allows keeping state with Stream', () => 25 | Effect.gen(function* () { 26 | const ref = yield* LazyRef.make(Stream.succeed(0)) 27 | 28 | yield* ref 29 | yield* LazyRef.update(ref, (x) => x + 1) 30 | yield* LazyRef.delete(ref) 31 | yield* ref 32 | 33 | deepStrictEqual(yield* ref, 0) 34 | deepStrictEqual(yield* LazyRef.update(ref, (x) => x + 1), 1) 35 | deepStrictEqual(yield* LazyRef.delete(ref), Option.some(1)) 36 | deepStrictEqual(yield* ref, 0) 37 | }), 38 | ) 39 | 40 | it.scoped('allows keeping state with sync functions', () => 41 | Effect.gen(function* () { 42 | const ref = yield* LazyRef.sync(() => 0) 43 | yield* LazyRef.update(ref, (x) => x + 1) 44 | deepStrictEqual(yield* ref, 1) 45 | }), 46 | ) 47 | 48 | it.scopedLive('runUpdates', () => 49 | Effect.gen(function* () { 50 | const ref = yield* LazyRef.of(0) 51 | const fiber = yield* ref.changes.pipe(Stream.take(10), Stream.runCollect, Effect.fork) 52 | // Allow fiber to start 53 | yield* Effect.sleep(0) 54 | 55 | yield* Effect.fork( 56 | ref.runUpdates(({ get, set }) => 57 | Effect.gen(function* () { 58 | // Preserves ordering of asynchonous updates 59 | yield* Effect.sleep(100) 60 | expect(yield* get).toEqual(0) 61 | expect(yield* set(1)).toEqual(1) 62 | expect(yield* set(1)).toEqual(1) // prevents duplicates 63 | expect(yield* set(2)).toEqual(2) 64 | expect(yield* set(3)).toEqual(3) 65 | expect(yield* set(4)).toEqual(4) 66 | 67 | return 42 68 | }), 69 | ), 70 | ) 71 | 72 | yield* Effect.fork( 73 | ref.runUpdates(({ get, set }) => 74 | Effect.gen(function* (_) { 75 | expect(yield* _(get)).toEqual(4) 76 | expect(yield* _(set(5))).toEqual(5) 77 | expect(yield* _(set(6))).toEqual(6) 78 | expect(yield* _(set(7))).toEqual(7) 79 | expect(yield* _(set(8))).toEqual(8) 80 | expect(yield* _(set(9))).toEqual(9) 81 | 82 | return 99 83 | }), 84 | ), 85 | ) 86 | 87 | expect(Array.from(yield* fiber)).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) 88 | }), 89 | ) 90 | 91 | it.scoped('Computed', () => 92 | Effect.gen(function* () { 93 | const ref = yield* LazyRef.of(0) 94 | const computed = LazyRef.map(ref, (x) => x + 1) 95 | expect(yield* computed).toEqual(1) 96 | 97 | yield* LazyRef.update(ref, (x) => x + 1) 98 | expect(yield* computed).toEqual(2) 99 | }), 100 | ) 101 | 102 | it.scoped('struct', () => 103 | Effect.gen(function* () { 104 | const a = yield* LazyRef.of(0) 105 | const b = yield* LazyRef.of(1) 106 | const c = yield* LazyRef.of(2) 107 | const struct = LazyRef.struct({ a, b, c }) 108 | 109 | expect(yield* struct).toEqual({ a: 0, b: 1, c: 2 }) 110 | 111 | yield* LazyRef.update(a, (x) => x + 1) 112 | expect(yield* struct).toEqual({ a: 1, b: 1, c: 2 }) 113 | 114 | yield* LazyRef.update(b, (x) => x + 1) 115 | expect(yield* struct).toEqual({ a: 1, b: 2, c: 2 }) 116 | 117 | yield* LazyRef.update(c, (x) => x + 1) 118 | expect(yield* struct).toEqual({ a: 1, b: 2, c: 3 }) 119 | 120 | yield* LazyRef.update(struct, (x) => ({ ...x, a: x.a + 1 })) 121 | expect(yield* struct).toEqual({ a: 2, b: 2, c: 3 }) 122 | }), 123 | ) 124 | 125 | it.scoped('struct with computed', () => 126 | Effect.gen(function* () { 127 | const a = yield* LazyRef.of(0) 128 | const b = yield* LazyRef.of(1) 129 | const c = LazyRef.map(yield* LazyRef.of(2), (x) => x + 1) 130 | const struct = LazyRef.struct({ a, b, c }) 131 | expect(yield* struct).toEqual({ a: 0, b: 1, c: 3 }) 132 | }), 133 | ) 134 | 135 | it.scoped('tuple', () => 136 | Effect.gen(function* () { 137 | const a = yield* LazyRef.of(0) 138 | const b = yield* LazyRef.of(1) 139 | const c = yield* LazyRef.of(2) 140 | const tuple = LazyRef.tuple(a, b, c) 141 | expect(yield* tuple).toEqual([0, 1, 2]) 142 | 143 | yield* LazyRef.update(a, (x) => x + 1) 144 | expect(yield* tuple).toEqual([1, 1, 2]) 145 | 146 | yield* LazyRef.update(b, (x) => x + 1) 147 | expect(yield* tuple).toEqual([1, 2, 2]) 148 | 149 | yield* LazyRef.update(c, (x) => x + 1) 150 | expect(yield* tuple).toEqual([1, 2, 3]) 151 | 152 | yield* LazyRef.update(tuple, (x) => [x[0] + 1, x[1] + 1, x[2] + 1]) 153 | expect(yield* tuple).toEqual([2, 3, 4]) 154 | }), 155 | ) 156 | 157 | it.scoped('tuple with computed', () => 158 | Effect.gen(function* () { 159 | const a = yield* LazyRef.of(0) 160 | const b = yield* LazyRef.of(1) 161 | const c = LazyRef.map(yield* LazyRef.of(2), (x) => x + 1) 162 | const computed = LazyRef.tuple(a, b, c) 163 | expect(yield* computed).toEqual([0, 1, 3]) 164 | }), 165 | ) 166 | 167 | it.scoped('tagged', () => { 168 | class Foo extends LazyRef.Tag('foo')() {} 169 | class Bar extends LazyRef.Tag('bar')() {} 170 | const FooBar = LazyRef.struct({ foo: Foo, bar: Bar }) 171 | 172 | return Effect.gen(function* () { 173 | expect(yield* FooBar).toEqual({ foo: 0, bar: 0 }) 174 | yield* LazyRef.update(FooBar, (x) => ({ ...x, foo: x.foo + 1 })) 175 | yield* LazyRef.update(Bar, (x) => x + 1) 176 | expect(yield* FooBar).toEqual({ foo: 1, bar: 1 }) 177 | yield* LazyRef.delete(FooBar) 178 | expect(yield* FooBar).toEqual({ foo: 0, bar: 0 }) 179 | }).pipe(Effect.provide([Foo.of(0), Bar.of(0)])) 180 | }) 181 | 182 | it.scopedLive('replays the latest value to late subscribers', () => 183 | Effect.gen(function* () { 184 | const ref = yield* LazyRef.of(0) 185 | const fiber1 = yield* ref.changes.pipe(Stream.runCollect, Effect.fork) 186 | const fiber2 = yield* ref.changes.pipe(Stream.runCollect, Effect.delay(250), Effect.fork) 187 | const fiber3 = yield* ref.changes.pipe(Stream.runCollect, Effect.delay(500), Effect.fork) 188 | 189 | // Update ref 10 times 190 | yield* Effect.iterate(0, { 191 | while: (x) => x < 10, 192 | body: () => LazyRef.update(ref, (y) => y + 1).pipe(Effect.delay(100)), 193 | }).pipe( 194 | Effect.onExit(() => 195 | Effect.gen(function* () { 196 | expect(yield* ref.version).toEqual(10) 197 | yield* ref.awaitShutdown 198 | }), 199 | ), 200 | Effect.fork, 201 | ) 202 | 203 | expect(Array.from(yield* fiber1)).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) 204 | expect(Array.from(yield* fiber2)).toEqual([2, 3, 4, 5, 6, 7, 8, 9, 10]) 205 | expect(Array.from(yield* fiber3)).toEqual([4, 5, 6, 7, 8, 9, 10]) 206 | }), 207 | ) 208 | 209 | it.scopedLive('emits updates to computed streams', () => 210 | Effect.gen(function* () { 211 | const ref = yield* LazyRef.of(0) 212 | const computed = LazyRef.map(ref, (x) => x + 1) 213 | const computed2 = LazyRef.map(computed, (x) => x + 1) 214 | const fiber = yield* computed.changes.pipe(Stream.runCollect, Effect.fork) 215 | const fiber2 = yield* computed2.changes.pipe(Stream.runCollect, Effect.fork) 216 | 217 | yield* Effect.sleep(0) 218 | yield* LazyRef.update(ref, (x) => x + 1) 219 | yield* ref.awaitShutdown 220 | 221 | expect(Array.from(yield* fiber)).toEqual([1, 2]) 222 | expect(Array.from(yield* fiber2)).toEqual([2, 3]) 223 | }), 224 | ) 225 | 226 | it.scopedLive('emits updates to computed streams of Options', () => 227 | Effect.gen(function* () { 228 | const ref = yield* LazyRef.of(0) 229 | const computed = LazyRef.map(ref, (x) => Option.some(x + 1)) 230 | const computed2 = LazyRef.map( 231 | computed, 232 | Option.map((x) => x + 1), 233 | ) 234 | const fiber = yield* computed.changes.pipe(Stream.runCollect, Effect.fork) 235 | const fiber2 = yield* computed2.changes.pipe(Stream.runCollect, Effect.fork) 236 | 237 | yield* Effect.sleep(0) 238 | yield* LazyRef.update(ref, (x) => x + 1) 239 | yield* ref.awaitShutdown 240 | 241 | expect(Array.from(yield* fiber)).toEqual([Option.some(1), Option.some(2)]) 242 | expect(Array.from(yield* fiber2)).toEqual([Option.some(2), Option.some(3)]) 243 | }), 244 | ) 245 | 246 | it.scopedLive('verifying LazyRef works as expected', () => 247 | Effect.gen(function* () { 248 | const ref = yield* LazyRef.make<{ 249 | entries: never[] 250 | index: number 251 | transition: Option.Option<{ from: URL; to: URL }> 252 | }>( 253 | Effect.succeed({ 254 | entries: [], 255 | index: -1, 256 | transition: Option.none(), 257 | }), 258 | ) 259 | const transition = LazyRef.map(ref, (x) => x.transition) 260 | const fiber = yield* transition.pipe( 261 | Stream.runCollect, 262 | Effect.map((_) => Array.from(_)), 263 | Effect.fork, 264 | ) 265 | 266 | yield* Effect.sleep(10) 267 | 268 | yield* LazyRef.set(ref, { 269 | entries: [], 270 | index: 0, 271 | transition: Option.some({ 272 | from: new URL('https://example.com/foo/1'), 273 | to: new URL('https://example.com/foo/2'), 274 | }), 275 | }) 276 | 277 | yield* ref.awaitShutdown 278 | 279 | const events = yield* fiber 280 | 281 | expect(events).toEqual([ 282 | Option.none(), 283 | Option.some({ 284 | from: new URL('https://example.com/foo/1'), 285 | to: new URL('https://example.com/foo/2'), 286 | }), 287 | ]) 288 | }), 289 | ) 290 | 291 | it.scopedLive('it can be read while being updated', () => 292 | Effect.gen(function* () { 293 | const ref = yield* LazyRef.of(0) 294 | 295 | yield* Effect.forkScoped( 296 | ref.runUpdates(({ set }) => 297 | Effect.iterate(0, { 298 | while: (x) => x < 10, 299 | body: (x) => set(x + 1).pipe(Effect.delay(100)), 300 | }), 301 | ), 302 | ) 303 | 304 | const sleepAndAssert = (expected: number) => 305 | Effect.gen(function* () { 306 | yield* Effect.sleep(100) 307 | expect(yield* ref).toEqual(expected) 308 | expect(yield* ref.version).toEqual(expected) 309 | }) 310 | 311 | // Let fiber start 312 | yield* Effect.sleep(0) 313 | for (let i = 0; i < 10; i++) { 314 | yield* sleepAndAssert(i + 1) 315 | } 316 | }), 317 | ) 318 | 319 | it.scopedLive('github issue #2', () => 320 | Effect.gen(function* () { 321 | const ref = yield* LazyRef.of(0) 322 | // Subscribe to the next 3 values 323 | const fiber = yield* ref.changes.pipe( 324 | Stream.take(3), 325 | Stream.runCollect, 326 | Effect.fork 327 | ) 328 | 329 | // We need to allow time for changes fiber to start subscribing 330 | yield* Effect.sleep(0) 331 | 332 | // Simulate another process updating 333 | // Updates are propagated to subscribers 334 | yield* LazyRef.update(ref, increment) 335 | yield* LazyRef.update(ref, increment) 336 | 337 | const values = yield* Effect.fromFiber(fiber) 338 | 339 | expect(Array.from(values)).toEqual([0, 1, 2]) 340 | }), 341 | ) 342 | }) -------------------------------------------------------------------------------- /src/LazyRef.ts: -------------------------------------------------------------------------------- 1 | import * as Boolean from 'effect/Boolean' 2 | import type * as Cause from 'effect/Cause' 3 | import type { Channel } from 'effect/Channel' 4 | import type { Chunk } from 'effect/Chunk' 5 | import * as Context from 'effect/Context' 6 | import * as Effect from 'effect/Effect' 7 | import type * as Equivalence from 'effect/Equivalence' 8 | import * as Exit from 'effect/Exit' 9 | import { dual, identity } from 'effect/Function' 10 | import * as Layer from 'effect/Layer' 11 | import * as MutableRef from 'effect/MutableRef' 12 | import * as Option from 'effect/Option' 13 | import { hasProperty } from 'effect/Predicate' 14 | import * as Readable from 'effect/Readable' 15 | import * as Record from 'effect/Record' 16 | import type * as Runtime from 'effect/Runtime' 17 | import type * as Scope from 'effect/Scope' 18 | import * as Stream from 'effect/Stream' 19 | import * as Subscribable from 'effect/Subscribable' 20 | import type * as Tracer from 'effect/Tracer' 21 | import type * as Types from 'effect/Types' 22 | import * as Computed from './Computed.js' 23 | import { 24 | deepEquals, 25 | EffectBase, 26 | getExitEquivalence, 27 | getOrInitializeCore, 28 | getSetDelete, 29 | interruptCore, 30 | interruptCoreAndWait, 31 | makeCore, 32 | makeDeferredRef, 33 | sendEvent, 34 | streamExit, 35 | type SubscriptionRefCore, 36 | } from './internal.js' 37 | import * as Provide from './Provide.js' 38 | import * as Versioned from './Versioned.js' 39 | 40 | export const TypeId = Symbol.for('@typed/LazyRef') 41 | export type TypeId = typeof TypeId 42 | 43 | export interface LazyRef 44 | extends Computed.Computed { 45 | readonly [TypeId]: LazyRef.Variance 46 | readonly shutdown: Effect.Effect 47 | readonly awaitShutdown: Effect.Effect 48 | readonly subscriberCount: Effect.Effect 49 | readonly runUpdates: ( 50 | f: (getSetDelete: GetSetDelete) => Effect.Effect, 51 | ) => Effect.Effect 52 | } 53 | 54 | export declare namespace LazyRef { 55 | export interface Variance { 56 | readonly _A: Types.Invariant 57 | readonly _E: Types.Covariant 58 | readonly _R: Types.Covariant 59 | } 60 | 61 | export type Success = [T] extends [{ readonly [TypeId]: Variance }] 62 | ? A 63 | : never 64 | export type Error = [T] extends [{ readonly [TypeId]: Variance }] 65 | ? E 66 | : never 67 | export type Context = [T] extends [{ readonly [TypeId]: Variance }] 68 | ? R 69 | : never 70 | } 71 | 72 | export interface LazyRefOptions { 73 | readonly eq?: Equivalence.Equivalence 74 | } 75 | 76 | export interface GetSetDelete { 77 | readonly get: Effect.Effect 78 | readonly set: (a: A) => Effect.Effect 79 | readonly delete: Effect.Effect, E, R> 80 | readonly version: Effect.Effect 81 | } 82 | 83 | export namespace GetSetDelete { 84 | export type Success = [T] extends [GetSetDelete] ? A : never 85 | export type Error = [T] extends [GetSetDelete] ? E : never 86 | export type Context = [T] extends [GetSetDelete] ? R : never 87 | } 88 | 89 | function tupleGetSetDelete>>( 90 | gsds: GSDS, 91 | ): GetSetDelete< 92 | { readonly [K in keyof GSDS]: GetSetDelete.Success }, 93 | GetSetDelete.Error, 94 | GetSetDelete.Context 95 | > { 96 | return { 97 | get: Effect.all( 98 | gsds.map((gsd) => gsd.get), 99 | { concurrency: 'unbounded' }, 100 | ), 101 | set: (values: { readonly [K in keyof GSDS]: GetSetDelete.Success }) => 102 | Effect.all( 103 | gsds.map((gsd, i) => gsd.set(values[i])), 104 | { concurrency: 'unbounded' }, 105 | ), 106 | delete: Effect.map( 107 | Effect.all( 108 | gsds.map((gsd) => gsd.delete), 109 | { concurrency: 'unbounded' }, 110 | ), 111 | Option.all, 112 | ), 113 | version: Effect.all( 114 | gsds.map((gsd) => gsd.version), 115 | { concurrency: 'unbounded' }, 116 | ).pipe(Effect.map((versions) => versions.reduce(sum, 0))), 117 | } as any 118 | } 119 | 120 | function structGetSetDelete>>( 121 | gsds: GSDS, 122 | ): GetSetDelete< 123 | { readonly [K in keyof GSDS]: GetSetDelete.Success }, 124 | GetSetDelete.Error, 125 | GetSetDelete.Context 126 | > { 127 | return { 128 | get: Effect.all( 129 | Record.map(gsds, (gsd) => gsd.get), 130 | { concurrency: 'unbounded' }, 131 | ), 132 | set: (values: { readonly [K in keyof GSDS]: GetSetDelete.Success }) => 133 | Effect.all( 134 | Record.map(gsds, (gsd, i) => gsd.set(values[i])), 135 | { concurrency: 'unbounded' }, 136 | ), 137 | delete: Effect.map( 138 | Effect.all( 139 | Record.map(gsds, (gsd) => gsd.delete), 140 | { concurrency: 'unbounded' }, 141 | ), 142 | Option.all, 143 | ), 144 | version: Effect.all( 145 | Object.values(gsds).map((gsd) => gsd.version), 146 | { concurrency: 'unbounded' }, 147 | ).pipe(Effect.map((versions) => versions.reduce(sum, 0))), 148 | } as any 149 | } 150 | 151 | const variance: LazyRef.Variance = { 152 | _A: (_) => _, 153 | _E: (_) => _, 154 | _R: (_) => _, 155 | } 156 | 157 | abstract class VariancesImpl extends EffectBase { 158 | readonly [TypeId]: LazyRef.Variance = variance 159 | readonly [Computed.ComputedTypeId]: Computed.Computed.Variance = variance 160 | readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId 161 | readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId 162 | } 163 | 164 | class LazyRefImpl 165 | extends VariancesImpl> 166 | implements LazyRef> 167 | { 168 | readonly version: Effect.Effect 169 | readonly shutdown: Effect.Effect> 170 | readonly awaitShutdown: Effect.Effect> 171 | readonly subscriberCount: Effect.Effect> 172 | readonly getSetDelete: GetSetDelete> 173 | readonly get: Effect.Effect> 174 | readonly changes: Stream.Stream> 175 | readonly channel: Channel, unknown, E, unknown, unknown, unknown, Exclude> 176 | 177 | constructor(readonly core: SubscriptionRefCore) { 178 | super() 179 | 180 | this.version = Effect.sync(() => core.deferredRef.version) 181 | this.shutdown = Effect.provide(interruptCore(core), core.runtime.context) 182 | this.awaitShutdown = Effect.provide(interruptCoreAndWait(core), core.runtime.context) 183 | this.subscriberCount = Effect.sync(() => MutableRef.get(core.pubsub.subscriberCount)) 184 | this.get = this.toEffect() 185 | this.getSetDelete = getSetDelete(core) 186 | this.changes = Stream.flatten(Stream.fromPubSub(core.pubsub)) 187 | this.channel = Stream.toChannel(this.changes) 188 | } 189 | 190 | toEffect(): Effect.Effect> { 191 | return getOrInitializeCore(this.core, true) 192 | } 193 | 194 | runUpdates( 195 | f: (ref: GetSetDelete>) => Effect.Effect, 196 | ): Effect.Effect | R3> { 197 | return this.core.semaphore.withPermits(1)(f(this.getSetDelete)) 198 | } 199 | } 200 | 201 | export const fromEffect = ( 202 | effect: Effect.Effect, 203 | options?: LazyRefOptions, 204 | ): Effect.Effect, never, R | Scope.Scope> => 205 | Effect.map(makeCore(effect, options), (core) => new LazyRefImpl(core)) 206 | 207 | export const fromStream = ( 208 | stream: Stream.Stream, 209 | options?: LazyRefOptions, 210 | ): Effect.Effect, never, R | Scope.Scope> => 211 | makeDeferredRef(getExitEquivalence(options?.eq ?? deepEquals)).pipe( 212 | Effect.bindTo('deferredRef'), 213 | Effect.bind('core', ({ deferredRef }) => makeCore(deferredRef, options)), 214 | Effect.tap(({ core, deferredRef }) => 215 | Effect.forkIn( 216 | stream.pipe( 217 | streamExit, 218 | Stream.runForEach((exit) => 219 | deferredRef.done(exit) ? Effect.sync(() => sendEvent(core, exit)) : Effect.void, 220 | ), 221 | ), 222 | core.scope, 223 | ), 224 | ), 225 | Effect.map(({ core }) => new LazyRefImpl(core)), 226 | ) 227 | 228 | export function make( 229 | input: Effect.Effect | Stream.Stream, 230 | options?: LazyRefOptions, 231 | ): Effect.Effect, never, R | Scope.Scope> { 232 | return Effect.isEffect(input) ? fromEffect(input, options) : fromStream(input, options) 233 | } 234 | 235 | export function of( 236 | value: A, 237 | options?: LazyRefOptions, 238 | ): Effect.Effect, never, Scope.Scope> { 239 | return make(Effect.succeed(value), options) 240 | } 241 | 242 | export function failCause( 243 | cause: Cause.Cause, 244 | options?: LazyRefOptions, 245 | ): Effect.Effect, never, Scope.Scope> { 246 | return make(Exit.failCause(cause), options) 247 | } 248 | 249 | export function fail( 250 | error: E, 251 | options?: LazyRefOptions, 252 | ): Effect.Effect, never, Scope.Scope> { 253 | return make(Exit.fail(error), options) 254 | } 255 | 256 | export function sync( 257 | f: () => A, 258 | options?: LazyRefOptions, 259 | ): Effect.Effect, never, Scope.Scope> { 260 | return make(Effect.sync(f), options) 261 | } 262 | 263 | export function get(ref: LazyRef): Effect.Effect { 264 | return ref.get 265 | } 266 | 267 | export const set: { 268 | (value: A): (ref: LazyRef) => Effect.Effect 269 | (ref: LazyRef, a: A): Effect.Effect 270 | } = dual(2, function set(ref: LazyRef, a: A): Effect.Effect { 271 | return ref.runUpdates(({ set }) => set(a)) 272 | }) 273 | 274 | export function reset(ref: LazyRef): Effect.Effect, E, R> { 275 | return ref.runUpdates((ref) => ref.delete) 276 | } 277 | 278 | export { reset as delete } 279 | 280 | export const modify: { 281 | ( 282 | f: (a: A) => readonly [B, A], 283 | ): (ref: LazyRef) => Effect.Effect 284 | (ref: LazyRef, f: (a: A) => readonly [B, A]): Effect.Effect 285 | } = dual(2, function modify< 286 | A, 287 | B, 288 | E, 289 | R, 290 | >(ref: LazyRef, f: (a: A) => readonly [B, A]): Effect.Effect { 291 | return ref.runUpdates(({ set, get }) => 292 | get.pipe( 293 | Effect.map(f), 294 | Effect.flatMap(([b, a]) => set(a).pipe(Effect.as(b))), 295 | ), 296 | ) 297 | }) 298 | 299 | export const modifyEffect: { 300 | ( 301 | f: (a: A) => Effect.Effect, 302 | ): (ref: LazyRef) => Effect.Effect 303 | ( 304 | ref: LazyRef, 305 | f: (a: A) => Effect.Effect, 306 | ): Effect.Effect 307 | } = dual(2, function modifyEffect< 308 | A, 309 | E, 310 | R, 311 | B, 312 | E2, 313 | R2, 314 | >(ref: LazyRef, f: (a: A) => Effect.Effect): Effect.Effect< 315 | B, 316 | E | E2, 317 | R | R2 318 | > { 319 | return ref.runUpdates(({ set, get }) => 320 | get.pipe( 321 | Effect.flatMap(f), 322 | Effect.flatMap(([b, a]) => set(a).pipe(Effect.as(b))), 323 | ), 324 | ) 325 | }) 326 | 327 | export const update: { 328 | (f: (a: A) => A): (ref: LazyRef) => Effect.Effect 329 | (ref: LazyRef, f: (a: A) => A): Effect.Effect 330 | } = dual(2, function update(ref: LazyRef, f: (a: A) => A): Effect.Effect< 331 | A, 332 | E, 333 | R 334 | > { 335 | return modify(ref, (a) => { 336 | const a2 = f(a) 337 | return [a2, a2] 338 | }) 339 | }) 340 | 341 | export const getAndUpdate: { 342 | (f: (a: A) => A): (ref: LazyRef) => Effect.Effect 343 | (ref: LazyRef, f: (a: A) => A): Effect.Effect 344 | } = dual(2, function getAndUpdate(ref: LazyRef, f: (a: A) => A): Effect.Effect< 345 | A, 346 | E, 347 | R 348 | > { 349 | return modify(ref, (a) => { 350 | const a2 = f(a) 351 | return [a, a2] 352 | }) 353 | }) 354 | 355 | export const updateEffect: { 356 | ( 357 | f: (a: A) => Effect.Effect, 358 | ): (ref: LazyRef) => Effect.Effect 359 | ( 360 | ref: LazyRef, 361 | f: (a: A) => Effect.Effect, 362 | ): Effect.Effect 363 | } = dual(2, function updateEffect< 364 | A, 365 | E, 366 | R, 367 | E2, 368 | R2, 369 | >(ref: LazyRef, f: (a: A) => Effect.Effect): Effect.Effect { 370 | return modifyEffect(ref, (a) => Effect.map(f(a), (a2) => [a2, a2])) 371 | }) 372 | 373 | export const getAndUpdateEffect: { 374 | ( 375 | f: (a: A) => Effect.Effect, 376 | ): (ref: LazyRef) => Effect.Effect 377 | ( 378 | ref: LazyRef, 379 | f: (a: A) => Effect.Effect, 380 | ): Effect.Effect 381 | } = dual(2, function getAndUpdateEffect< 382 | A, 383 | E, 384 | R, 385 | E2, 386 | R2, 387 | >(ref: LazyRef, f: (a: A) => Effect.Effect): Effect.Effect { 388 | return modifyEffect(ref, (a) => Effect.map(f(a), (a2) => [a, a2])) 389 | }) 390 | 391 | export const getAndSet: { 392 | (a: A): (ref: LazyRef) => Effect.Effect 393 | (ref: LazyRef, a: A): Effect.Effect 394 | } = dual(2, function getAndSet(ref: LazyRef, a: A): Effect.Effect { 395 | return getAndUpdate(ref, () => a) 396 | }) 397 | 398 | export const mapEffect: { 399 | ( 400 | f: (a: A) => Effect.Effect, 401 | options?: Parameters[2], 402 | ): (ref: Computed.Computed) => Computed.Computed 403 | 404 | ( 405 | ref: Computed.Computed, 406 | f: (a: A) => Effect.Effect, 407 | options?: Parameters[2], 408 | ): Computed.Computed 409 | } = dual( 410 | (args) => Computed.isComputed(args[0]), 411 | function mapEffect( 412 | ref: Computed.Computed, 413 | f: (a: A) => Effect.Effect, 414 | options?: Parameters[2], 415 | ): Computed.Computed { 416 | return Computed.makeComputed( 417 | Effect.flatMap(ref, f), 418 | Stream.flatMap(ref.changes, f, options), 419 | ref.version, 420 | ) 421 | }, 422 | ) 423 | 424 | export const map: { 425 | (f: (a: A) => B): (ref: Computed.Computed) => Computed.Computed 426 | (ref: Computed.Computed, f: (a: A) => B): Computed.Computed 427 | } = dual(2, function map< 428 | A, 429 | E, 430 | R, 431 | B, 432 | >(ref: Computed.Computed, f: (a: A) => B): Computed.Computed { 433 | return Computed.makeComputed(Effect.map(ref, f), Stream.map(ref.changes, f), ref.version) 434 | }) 435 | 436 | export const proxy = > | ReadonlyArray, E, R>( 437 | source: Computed.Computed, 438 | ): { 439 | readonly [K in keyof A]: Computed.Computed 440 | } => { 441 | const target: any = {} 442 | return new Proxy(target, { 443 | get(self, prop) { 444 | return (self[prop] ??= map(source, (a) => a[prop as keyof A])) 445 | }, 446 | }) 447 | } 448 | 449 | export function isSubscriptionRef( 450 | value: unknown, 451 | ): value is LazyRef { 452 | return hasProperty(value, TypeId) 453 | } 454 | 455 | const sum = (a: number, b: number) => a + b 456 | 457 | class TupleImpl>> 458 | extends VariancesImpl< 459 | { [K in keyof Refs]: LazyRef.Success }, 460 | LazyRef.Error, 461 | LazyRef.Context 462 | > 463 | implements 464 | LazyRef< 465 | { [K in keyof Refs]: LazyRef.Success }, 466 | LazyRef.Error, 467 | LazyRef.Context 468 | > 469 | { 470 | readonly [TypeId]: LazyRef.Variance< 471 | { [K in keyof Refs]: LazyRef.Success }, 472 | LazyRef.Error, 473 | LazyRef.Context 474 | > = variance 475 | readonly [Computed.ComputedTypeId]: Computed.Computed.Variance< 476 | { [K in keyof Refs]: LazyRef.Success }, 477 | LazyRef.Error, 478 | LazyRef.Context 479 | > = variance 480 | 481 | readonly shutdown: Effect.Effect> 482 | readonly awaitShutdown: Effect.Effect> 483 | readonly subscriberCount: Effect.Effect> 484 | readonly version: Effect.Effect> 485 | readonly changes: Stream.Stream< 486 | { readonly [K in keyof Refs]: LazyRef.Success }, 487 | LazyRef.Error, 488 | LazyRef.Context 489 | > 490 | 491 | constructor(readonly refs: Refs) { 492 | super() 493 | 494 | this.subscriberCount = Effect.all( 495 | refs.map((ref) => ref.subscriberCount), 496 | { concurrency: 'unbounded' }, 497 | ).pipe(Effect.map((counts) => counts.reduce(sum, 0))) 498 | this.shutdown = Effect.all( 499 | refs.map((ref) => ref.shutdown), 500 | { concurrency: 'unbounded' }, 501 | ) 502 | this.awaitShutdown = Effect.all( 503 | refs.map((ref) => ref.awaitShutdown), 504 | { concurrency: 'unbounded' }, 505 | ) 506 | 507 | this.version = Effect.all( 508 | refs.map((ref) => ref.version), 509 | { concurrency: 'unbounded' }, 510 | ).pipe(Effect.map((versions) => versions.reduce(sum, 0))) 511 | 512 | this.changes = Stream.zipLatestAll(...refs.map((ref) => ref.changes)) as any 513 | } 514 | 515 | get get(): Effect.Effect< 516 | { readonly [K in keyof Refs]: LazyRef.Success }, 517 | LazyRef.Error, 518 | LazyRef.Context 519 | > { 520 | return this 521 | } 522 | 523 | toEffect(): Effect.Effect< 524 | { readonly [K in keyof Refs]: LazyRef.Success }, 525 | LazyRef.Error, 526 | LazyRef.Context 527 | > { 528 | return Effect.all(this.refs, { concurrency: 'unbounded' }) as Effect.Effect< 529 | { readonly [K in keyof Refs]: LazyRef.Success }, 530 | LazyRef.Error, 531 | LazyRef.Context 532 | > 533 | } 534 | 535 | runUpdates( 536 | f: ( 537 | getSetDelete: GetSetDelete< 538 | { readonly [K in keyof Refs]: LazyRef.Success }, 539 | LazyRef.Error, 540 | LazyRef.Context 541 | >, 542 | ) => Effect.Effect, 543 | ): Effect.Effect { 544 | return tupleRunUpdates(this.refs, (gsds) => f(tupleGetSetDelete(gsds) as any)) 545 | } 546 | } 547 | 548 | export function tuple>>( 549 | ...refs: Refs 550 | ): LazyRef< 551 | { readonly [K in keyof Refs]: LazyRef.Success }, 552 | LazyRef.Error, 553 | LazyRef.Context 554 | > 555 | 556 | export function tuple< 557 | const Refs extends ReadonlyArray | Computed.Computed>, 558 | >( 559 | ...refs: Refs 560 | ): Computed.Computed< 561 | { readonly [K in keyof Refs]: Computed.Computed.Success }, 562 | LazyRef.Error, 563 | LazyRef.Context 564 | > 565 | 566 | export function tuple>>(...refs: Refs) { 567 | const hasNonSubscriptionRefs = refs.some((ref) => !isSubscriptionRef(ref)) 568 | if (hasNonSubscriptionRefs) { 569 | return Computed.fromVersioned(Versioned.tuple(...refs)) 570 | } 571 | 572 | return new TupleImpl(refs) 573 | } 574 | 575 | export function struct>>>( 576 | refs: Refs, 577 | ): LazyRef< 578 | { readonly [K in keyof Refs]: LazyRef.Success }, 579 | LazyRef.Error, 580 | LazyRef.Context 581 | > 582 | 583 | export function struct< 584 | const Refs extends Readonly< 585 | Record | Computed.Computed> 586 | >, 587 | >( 588 | refs: Refs, 589 | ): Computed.Computed< 590 | { readonly [K in keyof Refs]: Computed.Computed.Success }, 591 | LazyRef.Error, 592 | LazyRef.Context 593 | > 594 | 595 | export function struct>>>( 596 | refs: Refs, 597 | ) { 598 | const hasNonSubscriptionRefs = Object.values(refs).some((ref) => !isSubscriptionRef(ref)) 599 | if (hasNonSubscriptionRefs) { 600 | return Computed.fromVersioned(Versioned.struct(refs)) 601 | } 602 | 603 | return new StructImpl(refs) 604 | } 605 | 606 | class StructImpl>>> 607 | extends VariancesImpl< 608 | { readonly [K in keyof Refs]: LazyRef.Success }, 609 | LazyRef.Error, 610 | LazyRef.Context 611 | > 612 | implements 613 | LazyRef< 614 | { readonly [K in keyof Refs]: LazyRef.Success }, 615 | LazyRef.Error, 616 | LazyRef.Context 617 | > 618 | { 619 | readonly shutdown: Effect.Effect> 620 | readonly awaitShutdown: Effect.Effect> 621 | readonly subscriberCount: Effect.Effect> 622 | readonly version: Effect.Effect> 623 | readonly changes: Stream.Stream< 624 | { readonly [K in keyof Refs]: LazyRef.Success }, 625 | LazyRef.Error, 626 | LazyRef.Context 627 | > 628 | 629 | constructor(readonly refs: Refs) { 630 | super() 631 | 632 | const refValues = Object.values(refs) 633 | 634 | this.shutdown = Effect.all( 635 | refValues.map((ref) => ref.shutdown), 636 | { concurrency: 'unbounded' }, 637 | ) 638 | 639 | this.awaitShutdown = Effect.all( 640 | refValues.map((ref) => ref.awaitShutdown), 641 | { concurrency: 'unbounded' }, 642 | ) 643 | 644 | this.subscriberCount = Effect.all( 645 | refValues.map((ref) => ref.subscriberCount), 646 | { concurrency: 'unbounded' }, 647 | ).pipe(Effect.map((counts) => counts.reduce(sum, 0))) 648 | 649 | this.version = Effect.all( 650 | refValues.map((ref) => ref.version), 651 | { concurrency: 'unbounded' }, 652 | ).pipe(Effect.map((versions) => versions.reduce(sum, 0))) 653 | 654 | this.changes = Stream.zipLatestAll(...refValues.map((ref) => ref.changes)) as any 655 | } 656 | 657 | get get(): Effect.Effect< 658 | { readonly [K in keyof Refs]: LazyRef.Success }, 659 | LazyRef.Error, 660 | LazyRef.Context 661 | > { 662 | return this 663 | } 664 | 665 | toEffect(): Effect.Effect< 666 | { readonly [K in keyof Refs]: LazyRef.Success }, 667 | LazyRef.Error, 668 | LazyRef.Context 669 | > { 670 | return Effect.all(this.refs, { concurrency: 'unbounded' }) as any 671 | } 672 | 673 | runUpdates( 674 | f: ( 675 | getSetDelete: GetSetDelete< 676 | { readonly [K in keyof Refs]: LazyRef.Success }, 677 | LazyRef.Error, 678 | LazyRef.Context 679 | >, 680 | ) => Effect.Effect, 681 | ): Effect.Effect { 682 | return structRunUpdates(this.refs, (gsds) => f(structGetSetDelete(gsds) as any)) 683 | } 684 | } 685 | 686 | function tupleRunUpdates>, B, E2, R2>( 687 | refs: Refs, 688 | accumulated: ( 689 | gsds: { 690 | readonly [K in keyof Refs]: GetSetDelete< 691 | Refs[K], 692 | LazyRef.Error, 693 | LazyRef.Context 694 | > 695 | }, 696 | ) => Effect.Effect, 697 | ): Effect.Effect { 698 | if (refs.length === 0) { 699 | return accumulated([] as any) 700 | } 701 | 702 | const [first, ...rest] = refs 703 | return first.runUpdates((firstGsd) => 704 | tupleRunUpdates(rest, (restGsds) => accumulated([firstGsd, ...restGsds] as any)), 705 | ) 706 | } 707 | 708 | function structRunUpdates>>, B, E2, R2>( 709 | refs: Refs, 710 | accumulated: ( 711 | gsds: { 712 | readonly [K in keyof Refs]: GetSetDelete< 713 | Refs[K], 714 | LazyRef.Error, 715 | LazyRef.Context 716 | > 717 | }, 718 | ) => Effect.Effect, 719 | ) { 720 | const entries = Object.entries(refs) 721 | return tupleRunUpdates( 722 | entries.map(([_, ref]) => ref), 723 | (gsds) => accumulated(Object.fromEntries(entries.map(([key], i) => [key, gsds[i]])) as any), 724 | ) 725 | } 726 | 727 | export interface Tagged extends LazyRef { 728 | readonly id: Id 729 | readonly tag: Context.Tag> 730 | 731 | readonly of: (value: A) => Layer.Layer 732 | 733 | readonly make: ( 734 | fxOrEffect: Stream.Stream | Effect.Effect, 735 | options?: LazyRefOptions & { readonly drop?: number; readonly take?: number }, 736 | ) => Layer.Layer 737 | 738 | readonly layer: ( 739 | make: Effect.Effect, E2, R2 | Scope.Scope>, 740 | ) => Layer.Layer 741 | } 742 | 743 | export interface TaggedClass extends Tagged { 744 | new (): Tagged 745 | } 746 | 747 | class TaggedImpl 748 | extends VariancesImpl 749 | implements Tagged 750 | { 751 | readonly tag: Context.Tag> 752 | readonly shutdown: Effect.Effect 753 | readonly awaitShutdown: Effect.Effect 754 | readonly subscriberCount: Effect.Effect 755 | readonly version: Effect.Effect 756 | readonly changes: Stream.Stream 757 | readonly runUpdates: ( 758 | f: (getSetDelete: GetSetDelete) => Effect.Effect, 759 | ) => Effect.Effect 760 | 761 | constructor(readonly id: Id) { 762 | super() 763 | 764 | this.id = id 765 | this.tag = Context.GenericTag>(id) 766 | this.shutdown = Effect.flatMap(this.tag, (ref) => ref.shutdown) 767 | this.awaitShutdown = Effect.flatMap(this.tag, (ref) => ref.awaitShutdown) 768 | this.subscriberCount = Effect.flatMap(this.tag, (ref) => ref.subscriberCount) 769 | this.version = Effect.flatMap(this.tag, (ref) => ref.version) 770 | this.changes = Stream.unwrap(Effect.map(this.tag, (ref) => ref.changes)) 771 | this.runUpdates = ( 772 | f: (getSetDelete: GetSetDelete) => Effect.Effect, 773 | ) => Effect.flatMap(this.tag, (ref) => ref.runUpdates(f)) 774 | } 775 | 776 | get get(): Effect.Effect { 777 | return this 778 | } 779 | 780 | toEffect(): Effect.Effect { 781 | return Effect.flatten(this.tag) 782 | } 783 | 784 | of(value: A): Layer.Layer { 785 | return this.make(Effect.succeed(value)) 786 | } 787 | 788 | make( 789 | fxOrEffect: Stream.Stream | Effect.Effect, 790 | options?: LazyRefOptions & { readonly drop?: number; readonly take?: number }, 791 | ) { 792 | return Layer.scoped( 793 | this.tag, 794 | make(fxOrEffect, options).pipe( 795 | Effect.map((x) => (options?.drop ? drop(x, options.drop) : x)), 796 | Effect.map((x) => (options?.take ? take(x, options.take) : x)), 797 | ), 798 | ) 799 | } 800 | 801 | layer(make: Effect.Effect, E2, R2 | Scope.Scope>) { 802 | return Layer.scoped(this.tag, make) 803 | } 804 | } 805 | 806 | export function Tag(id: Id) { 807 | return (): TaggedClass => { 808 | const base = new TaggedImpl(id) 809 | 810 | function klass() { 811 | return base 812 | } 813 | 814 | Object.setPrototypeOf(klass, base) 815 | 816 | return klass as any 817 | } 818 | } 819 | 820 | class FromTag extends VariancesImpl implements LazyRef { 821 | readonly shutdown: Effect.Effect 822 | readonly awaitShutdown: Effect.Effect 823 | readonly subscriberCount: Effect.Effect 824 | readonly version: Effect.Effect 825 | readonly changes: Stream.Stream 826 | readonly runUpdates: ( 827 | f: (getSetDelete: GetSetDelete) => Effect.Effect, 828 | ) => Effect.Effect 829 | readonly channel: Channel, unknown, E, unknown, unknown, unknown, Id | R> 830 | 831 | constructor( 832 | readonly tag: Context.Tag, 833 | readonly f: (s: S) => LazyRef, 834 | ) { 835 | super() 836 | 837 | this.shutdown = Effect.flatMap(this.tag, (s) => this.f(s).shutdown) 838 | this.awaitShutdown = Effect.flatMap(this.tag, (s) => this.f(s).awaitShutdown) 839 | this.subscriberCount = Effect.flatMap(this.tag, (s) => this.f(s).subscriberCount) 840 | this.version = Effect.flatMap(this.tag, (s) => this.f(s).version) 841 | this.changes = Stream.unwrap(Effect.map(this.tag, (s) => this.f(s).changes)) 842 | this.runUpdates = ( 843 | f: (getSetDelete: GetSetDelete) => Effect.Effect, 844 | ) => Effect.flatMap(this.tag, (s) => this.f(s).runUpdates(f)) 845 | this.channel = Stream.toChannel(this.changes) 846 | } 847 | 848 | toEffect(): Effect.Effect { 849 | return Effect.flatMap(this.tag, this.f) 850 | } 851 | 852 | get get(): Effect.Effect { 853 | return this 854 | } 855 | } 856 | 857 | export function fromTag( 858 | tag: Context.Tag, 859 | f: (s: S) => LazyRef, 860 | ): LazyRef { 861 | return new FromTag(tag, f) 862 | } 863 | 864 | export const when: { 865 | (matchers: { 866 | onFalse: () => B 867 | onTrue: () => C 868 | }): (ref: Computed.Computed) => Computed.Computed 869 | 870 | ( 871 | ref: Computed.Computed, 872 | matchers: { 873 | onFalse: () => B 874 | onTrue: () => C 875 | }, 876 | ): Computed.Computed 877 | } = dual( 878 | 2, 879 | function when( 880 | ref: Computed.Computed, 881 | matchers: { 882 | onFalse: () => B 883 | onTrue: () => C 884 | }, 885 | ) { 886 | return Computed.makeComputed( 887 | Effect.map(ref, Boolean.match(matchers)), 888 | Stream.map(ref.changes, Boolean.match(matchers)), 889 | ref.version, 890 | ) 891 | }, 892 | ) 893 | 894 | class SubscriptionRefProvide 895 | extends VariancesImpl | R2> 896 | implements LazyRef | R2> 897 | { 898 | readonly shutdown: Effect.Effect | R2> 899 | readonly awaitShutdown: Effect.Effect | R2> 900 | readonly subscriberCount: Effect.Effect | R2> 901 | readonly version: Effect.Effect | R2> 902 | readonly changes: Stream.Stream | R2> 903 | readonly runUpdates: ( 904 | f: (getSetDelete: GetSetDelete | R2>) => Effect.Effect, 905 | ) => Effect.Effect | R2 | R3> 906 | readonly channel: Channel, unknown, E, unknown, unknown, unknown, Exclude | R2> 907 | 908 | constructor( 909 | readonly ref: LazyRef, 910 | readonly provide: Provide.Provide, 911 | ) { 912 | super() 913 | 914 | this.shutdown = Provide.provideToEffect(ref.shutdown, provide) 915 | this.awaitShutdown = Provide.provideToEffect(ref.awaitShutdown, provide) 916 | this.subscriberCount = Provide.provideToEffect(ref.subscriberCount, provide) 917 | this.version = Provide.provideToEffect(ref.version, provide) 918 | this.changes = Provide.provideToStream(ref.changes, provide) 919 | this.runUpdates = ( 920 | f: (getSetDelete: GetSetDelete | R2>) => Effect.Effect, 921 | ) => 922 | Provide.provideToEffect( 923 | ref.runUpdates((gsd) => f(gsd as any)), 924 | provide, 925 | ) as any 926 | this.channel = Stream.toChannel(this.changes) 927 | } 928 | 929 | toEffect(): Effect.Effect | R2> { 930 | return Provide.provideToEffect(this.ref, this.provide) 931 | } 932 | 933 | get get(): Effect.Effect | R2> { 934 | return this 935 | } 936 | } 937 | 938 | function provide_( 939 | ref: LazyRef, 940 | provide: Provide.Provide, 941 | ): LazyRef | R2> { 942 | if (ref instanceof SubscriptionRefProvide) { 943 | return new SubscriptionRefProvide(ref.ref, Provide.merge(ref.provide, provide)) 944 | } 945 | 946 | return new SubscriptionRefProvide(ref, provide) 947 | } 948 | 949 | export const provide: { 950 | ( 951 | contextOrLayerOrRuntime: Context.Context | Layer.Layer | Runtime.Runtime, 952 | ): { 953 | (ref: LazyRef): LazyRef | R2> 954 | (ref: Computed.Computed): Computed.Computed | R2> 955 | } 956 | 957 | ( 958 | ref: LazyRef, 959 | contextOrLayerOrRuntime: Context.Context | Layer.Layer | Runtime.Runtime, 960 | ): LazyRef | R2> 961 | 962 | ( 963 | ref: Computed.Computed, 964 | contextOrLayerOrRuntime: Context.Context | Layer.Layer | Runtime.Runtime, 965 | ): LazyRef | R2> 966 | } = dual(2, function provide< 967 | A, 968 | E, 969 | R, 970 | S, 971 | R2 = never, 972 | >(ref: LazyRef, contextOrLayerOrRuntime: Context.Context | Layer.Layer | Runtime.Runtime) { 973 | const provide: Provide.Provide = Context.isContext(contextOrLayerOrRuntime) 974 | ? Provide.ProvideContext(contextOrLayerOrRuntime) 975 | : Layer.isLayer(contextOrLayerOrRuntime) 976 | ? Provide.ProvideLayer(contextOrLayerOrRuntime as Layer.Layer) 977 | : Provide.ProvideRuntime(contextOrLayerOrRuntime as Runtime.Runtime) 978 | 979 | if (isSubscriptionRef(ref)) { 980 | return provide_(ref, provide) 981 | } 982 | 983 | return Computed.makeComputed( 984 | Provide.provideToEffect(ref, provide), 985 | Provide.provideToStream(ref, provide), 986 | Provide.provideToEffect(ref.version, provide), 987 | ) 988 | }) 989 | 990 | export const provideService: { 991 | ( 992 | tag: Context.Tag, 993 | services: S, 994 | ): { 995 | (ref: LazyRef): LazyRef> 996 | (ref: Computed.Computed): Computed.Computed> 997 | } 998 | 999 | ( 1000 | ref: LazyRef, 1001 | tag: Context.Tag, 1002 | services: S, 1003 | ): LazyRef> 1004 | 1005 | ( 1006 | ref: Computed.Computed, 1007 | tag: Context.Tag, 1008 | services: S, 1009 | ): Computed.Computed> 1010 | } = dual(3, function provideService< 1011 | A, 1012 | E, 1013 | R, 1014 | Id, 1015 | S, 1016 | >(ref: LazyRef, tag: Context.Tag, services: S): LazyRef> { 1017 | return provide_(ref, Provide.ProvideService(tag, services)) 1018 | }) 1019 | 1020 | export const provideServiceEffect: { 1021 | ( 1022 | tag: Context.Tag, 1023 | service: Effect.Effect, 1024 | ): { 1025 | (ref: LazyRef): LazyRef | R2> 1026 | (ref: Computed.Computed): Computed.Computed | R2> 1027 | } 1028 | 1029 | ( 1030 | ref: LazyRef, 1031 | tag: Context.Tag, 1032 | service: Effect.Effect, 1033 | ): LazyRef | R2> 1034 | 1035 | ( 1036 | ref: Computed.Computed, 1037 | tag: Context.Tag, 1038 | service: Effect.Effect, 1039 | ): Computed.Computed | R2> 1040 | } = dual(3, function provideServiceEffect< 1041 | A, 1042 | E, 1043 | R, 1044 | Id, 1045 | S, 1046 | R2, 1047 | >(ref: LazyRef, tag: Context.Tag, service: Effect.Effect): LazyRef< 1048 | A, 1049 | E, 1050 | Exclude | R2 1051 | > { 1052 | return provide_(ref, Provide.ProvideServiceEffect(tag, service)) 1053 | }) 1054 | 1055 | class SimpleTransform 1056 | extends VariancesImpl 1057 | implements LazyRef 1058 | { 1059 | readonly shutdown: Effect.Effect 1060 | readonly awaitShutdown: Effect.Effect 1061 | readonly subscriberCount: Effect.Effect 1062 | readonly version: Effect.Effect 1063 | readonly changes: Stream.Stream 1064 | readonly runUpdates: ( 1065 | f: (getSetDelete: GetSetDelete) => Effect.Effect, 1066 | ) => Effect.Effect 1067 | readonly channel: Channel, unknown, E, unknown, unknown, unknown, R | R2> 1068 | 1069 | constructor( 1070 | readonly ref: LazyRef, 1071 | readonly transformEffect: (effect: Effect.Effect) => Effect.Effect, 1072 | readonly transformStream: (stream: Stream.Stream) => Stream.Stream, 1073 | ) { 1074 | super() 1075 | 1076 | this.shutdown = ref.shutdown 1077 | this.awaitShutdown = ref.awaitShutdown 1078 | this.subscriberCount = ref.subscriberCount 1079 | this.version = ref.version 1080 | this.changes = transformStream(ref.changes) 1081 | this.runUpdates = ref.runUpdates 1082 | this.channel = Stream.toChannel(this.changes) 1083 | } 1084 | 1085 | toEffect(): Effect.Effect { 1086 | return this.transformEffect(this.ref) 1087 | } 1088 | 1089 | get get(): Effect.Effect { 1090 | return this 1091 | } 1092 | } 1093 | 1094 | export const withSpan: { 1095 | ( 1096 | name: string, 1097 | options?: { 1098 | readonly attributes?: Record 1099 | readonly links?: ReadonlyArray 1100 | readonly parent?: Tracer.ParentSpan 1101 | readonly root?: boolean 1102 | readonly context?: Context.Context 1103 | }, 1104 | ): (self: LazyRef) => LazyRef 1105 | 1106 | ( 1107 | self: LazyRef, 1108 | name: string, 1109 | options?: { 1110 | readonly attributes?: Record 1111 | readonly links?: ReadonlyArray 1112 | readonly parent?: Tracer.ParentSpan 1113 | readonly root?: boolean 1114 | readonly context?: Context.Context 1115 | }, 1116 | ): LazyRef 1117 | } = dual( 1118 | (args) => isSubscriptionRef(args[0]), 1119 | (ref, name, options) => 1120 | new SimpleTransform(ref, Effect.withSpan(name, options), Stream.withSpan(name, options)), 1121 | ) 1122 | 1123 | export const drop: { 1124 | ( 1125 | n: number, 1126 | ): { 1127 | (ref: LazyRef): LazyRef 1128 | (ref: Computed.Computed): Computed.Computed 1129 | } 1130 | 1131 | (ref: LazyRef, n: number): LazyRef 1132 | (ref: Computed.Computed, n: number): Computed.Computed 1133 | } = dual(2, function drop(ref: LazyRef, n: number) { 1134 | return isSubscriptionRef(ref) 1135 | ? new SimpleTransform(ref, identity, Stream.drop(n)) 1136 | : Computed.makeComputed(ref.get, Stream.drop(ref.changes, n), ref.version) 1137 | }) 1138 | 1139 | export const take: { 1140 | ( 1141 | n: number, 1142 | ): { 1143 | (ref: LazyRef): LazyRef 1144 | (ref: Computed.Computed): Computed.Computed 1145 | } 1146 | 1147 | (ref: LazyRef, n: number): LazyRef 1148 | (ref: Computed.Computed, n: number): Computed.Computed 1149 | } = dual(2, function take(ref: LazyRef | Computed.Computed, n: number) { 1150 | return isSubscriptionRef(ref) 1151 | ? new SimpleTransform(ref, identity, Stream.take(n)) 1152 | : Computed.makeComputed(ref.get, Stream.take(ref.changes, n), ref.version) 1153 | }) 1154 | 1155 | export const transformOrFail: { 1156 | ( 1157 | f: (a: A) => Effect.Effect, 1158 | g: (b: B) => Effect.Effect, 1159 | ): (ref: LazyRef) => LazyRef 1160 | 1161 | ( 1162 | ref: LazyRef, 1163 | f: (a: A) => Effect.Effect, 1164 | g: (b: B) => Effect.Effect, 1165 | ): LazyRef 1166 | } = dual(3, function transformOrFail< 1167 | A, 1168 | E, 1169 | R, 1170 | B, 1171 | E2, 1172 | R2, 1173 | E3, 1174 | R3, 1175 | >(ref: LazyRef, f: (a: A) => Effect.Effect, g: (b: B) => Effect.Effect): LazyRef< 1176 | B, 1177 | E | E2 | E3, 1178 | R | R2 | R3 1179 | > { 1180 | return new TransformOrFail(ref, f, g) 1181 | }) 1182 | 1183 | class TransformOrFail 1184 | extends VariancesImpl 1185 | implements LazyRef 1186 | { 1187 | readonly shutdown: Effect.Effect 1188 | readonly awaitShutdown: Effect.Effect 1189 | readonly subscriberCount: Effect.Effect 1190 | readonly version: Effect.Effect 1191 | readonly changes: Stream.Stream 1192 | readonly runUpdates: ( 1193 | f: (getSetDelete: GetSetDelete) => Effect.Effect, 1194 | ) => Effect.Effect 1195 | readonly channel: Channel, unknown, E | E2 | E3, unknown, unknown, unknown, R | R2 | R3> 1196 | 1197 | constructor( 1198 | readonly ref: LazyRef, 1199 | readonly f: (a: A) => Effect.Effect, 1200 | readonly g: (b: B) => Effect.Effect, 1201 | ) { 1202 | super() 1203 | 1204 | this.shutdown = ref.shutdown 1205 | this.awaitShutdown = ref.awaitShutdown 1206 | this.subscriberCount = ref.subscriberCount 1207 | this.version = ref.version 1208 | this.changes = Stream.mapEffect(ref.changes, f) 1209 | this.runUpdates = (run) => 1210 | ref.runUpdates((gsd) => 1211 | run({ 1212 | get: Effect.flatMap(gsd.get, f), 1213 | set: (b) => g(b).pipe(Effect.flatMap(gsd.set), Effect.flatMap(f), Effect.orDie), 1214 | delete: Effect.flatMap( 1215 | gsd.delete, 1216 | Option.match({ 1217 | onNone: () => Effect.succeedNone, 1218 | onSome: (a) => Effect.asSome(f(a)), 1219 | }), 1220 | ), 1221 | version: gsd.version, 1222 | }), 1223 | ) 1224 | 1225 | this.channel = Stream.toChannel(this.changes) 1226 | } 1227 | 1228 | toEffect() { 1229 | return Effect.flatMap(this.ref, this.f) 1230 | } 1231 | 1232 | get get() { 1233 | return this 1234 | } 1235 | } 1236 | 1237 | export const transform: { 1238 | (f: (a: A) => B, g: (b: B) => A): (ref: LazyRef) => LazyRef 1239 | (ref: Computed.Computed, f: (a: A) => B, g: (b: B) => A): LazyRef 1240 | } = dual(3, function transform< 1241 | A, 1242 | E, 1243 | R, 1244 | B, 1245 | >(ref: LazyRef, f: (a: A) => B, g: (b: B) => A): LazyRef { 1246 | return transformOrFail( 1247 | ref, 1248 | (a) => Effect.sync(() => f(a)), 1249 | (b) => Effect.sync(() => g(b)), 1250 | ) 1251 | }) 1252 | -------------------------------------------------------------------------------- /src/Provide.ts: -------------------------------------------------------------------------------- 1 | import type * as Context from 'effect/Context' 2 | import * as Effect from 'effect/Effect' 3 | import * as FiberRefsPatch from 'effect/FiberRefsPatch' 4 | import * as Layer from 'effect/Layer' 5 | import * as Runtime from 'effect/Runtime' 6 | import * as RuntimeFlags from 'effect/RuntimeFlags' 7 | import type * as Scope from 'effect/Scope' 8 | import * as Stream from 'effect/Stream' 9 | 10 | export type Provide = 11 | | ProvideContext 12 | | ProvideLayer 13 | | ProvideRuntime 14 | | ProvideService 15 | | ProvideServiceEffect 16 | 17 | export interface ProvideContext { 18 | readonly _tag: 'ProvideContext' 19 | readonly i0: Context.Context 20 | } 21 | 22 | export const ProvideContext = (i0: Context.Context): ProvideContext => ({ 23 | _tag: 'ProvideContext', 24 | i0, 25 | }) 26 | 27 | export interface ProvideLayer { 28 | readonly _tag: 'ProvideLayer' 29 | readonly i0: Layer.Layer 30 | } 31 | 32 | export const ProvideLayer = (i0: Layer.Layer): ProvideLayer => ({ 33 | _tag: 'ProvideLayer', 34 | i0, 35 | }) 36 | 37 | export interface ProvideService { 38 | readonly _tag: 'ProvideService' 39 | readonly i0: Context.Tag 40 | readonly i1: S 41 | } 42 | 43 | export const ProvideService = (i0: Context.Tag, i1: S): ProvideService => ({ 44 | _tag: 'ProvideService', 45 | i0, 46 | i1, 47 | }) 48 | 49 | export interface ProvideServiceEffect { 50 | readonly _tag: 'ProvideServiceEffect' 51 | readonly i0: Context.Tag 52 | readonly i1: Effect.Effect 53 | } 54 | 55 | export const ProvideServiceEffect = ( 56 | i0: Context.Tag, 57 | i1: Effect.Effect, 58 | ): ProvideServiceEffect => ({ 59 | _tag: 'ProvideServiceEffect', 60 | i0, 61 | i1, 62 | }) 63 | 64 | export interface ProvideRuntime { 65 | readonly _tag: 'ProvideRuntime' 66 | readonly i0: Runtime.Runtime 67 | } 68 | 69 | export const ProvideRuntime = (i0: Runtime.Runtime): ProvideRuntime => ({ 70 | _tag: 'ProvideRuntime', 71 | i0, 72 | }) 73 | 74 | export function matchProvide( 75 | self: Provide, 76 | matchers: { 77 | readonly ProvideContext: (i0: Context.Context) => B 78 | readonly ProvideRuntime: (i0: Runtime.Runtime) => B 79 | readonly ProvideLayer: (i0: Layer.Layer) => B 80 | readonly ProvideService: (tag: Context.Tag, service: any) => B 81 | readonly ProvideServiceEffect: ( 82 | tag: Context.Tag, 83 | service: Effect.Effect, 84 | ) => B 85 | }, 86 | ): B { 87 | return matchers[self._tag](self.i0 as any, (self as any).i1) 88 | } 89 | 90 | export function merge( 91 | self: Provide, 92 | that: Provide, 93 | ): Provide | R2> { 94 | return ProvideLayer(Layer.provideMerge(toLayer(self), toLayer(that))) 95 | } 96 | 97 | export function buildWithScope(provide: Provide, scope: Scope.Scope) { 98 | return Layer.buildWithScope(toLayer(provide), scope) 99 | } 100 | 101 | export function toLayer(provide: Provide): Layer.Layer { 102 | switch (provide._tag) { 103 | case 'ProvideContext': 104 | return Layer.succeedContext(provide.i0) 105 | case 'ProvideLayer': 106 | return provide.i0 107 | case 'ProvideRuntime': 108 | return runtimeToLayer(provide.i0) 109 | case 'ProvideService': 110 | return Layer.succeed(provide.i0, provide.i1) 111 | case 'ProvideServiceEffect': 112 | return Layer.effect(provide.i0, provide.i1) 113 | } 114 | } 115 | 116 | export function provideToEffect( 117 | effect: Effect.Effect, 118 | provide: Provide, 119 | ): Effect.Effect | R2> { 120 | return Effect.provide(effect, toLayer(provide)) 121 | } 122 | 123 | export function provideToStream( 124 | stream: Stream.Stream, 125 | provide: Provide, 126 | ): Stream.Stream | R2> { 127 | return Stream.provideSomeLayer(stream, toLayer(provide)) 128 | } 129 | 130 | export function runtimeToLayer(runtime: Runtime.Runtime): Layer.Layer { 131 | // Calculate patch 132 | const patchRefs = FiberRefsPatch.diff(Runtime.defaultRuntime.fiberRefs, runtime.fiberRefs) 133 | const patchFlags = RuntimeFlags.diff(Runtime.defaultRuntime.runtimeFlags, runtime.runtimeFlags) 134 | 135 | return Layer.scopedContext( 136 | Effect.Do.pipe( 137 | // Get Current Refs + Flags 138 | Effect.bind('oldRefs', () => Effect.getFiberRefs), 139 | Effect.bind('oldFlags', () => Effect.getRuntimeFlags), 140 | // Patch Refs + Flags 141 | Effect.tap(() => Effect.patchFiberRefs(patchRefs)), 142 | Effect.tap(() => Effect.patchRuntimeFlags(patchFlags)), 143 | // Get the new Refs + Flags 144 | Effect.bind('newRefs', () => Effect.getFiberRefs), 145 | Effect.bind('newFlags', () => Effect.getRuntimeFlags), 146 | // Calculate rollback patch 147 | Effect.let('rollbackRefs', ({ newRefs, oldRefs }) => FiberRefsPatch.diff(newRefs, oldRefs)), 148 | Effect.let('rollbackFlags', ({ newFlags, oldFlags }) => 149 | RuntimeFlags.diff(newFlags, oldFlags), 150 | ), 151 | // Apply the rollbacks when the current scope is closed 152 | Effect.tap(({ rollbackFlags, rollbackRefs }) => 153 | Effect.addFinalizer(() => 154 | Effect.zipRight( 155 | Effect.patchFiberRefs(rollbackRefs), 156 | Effect.patchRuntimeFlags(rollbackFlags), 157 | ), 158 | ), 159 | ), 160 | // Provide the runtime's context 161 | Effect.map(() => runtime.context), 162 | ), 163 | ) 164 | } 165 | -------------------------------------------------------------------------------- /src/Versioned.ts: -------------------------------------------------------------------------------- 1 | import type { Channel } from 'effect/Channel' 2 | import type { Chunk } from 'effect/Chunk' 3 | import * as Effect from 'effect/Effect' 4 | import * as Readable from 'effect/Readable' 5 | import * as Record from 'effect/Record' 6 | import * as Stream from 'effect/Stream' 7 | import * as Subscribable from 'effect/Subscribable' 8 | import { EffectBase, MulticastEffect } from './internal.js' 9 | 10 | export interface Versioned 11 | extends Effect.Effect, 12 | Subscribable.Subscribable { 13 | readonly version: Effect.Effect 14 | } 15 | 16 | export declare namespace Versioned { 17 | export type Success = T extends Versioned ? A : never 18 | export type Error = T extends Versioned ? E : never 19 | export type Context = T extends Versioned ? R : never 20 | } 21 | 22 | export function make( 23 | get: Effect.Effect, 24 | changes: Stream.Stream, 25 | version: Effect.Effect, 26 | ): Versioned { 27 | return new VersionedImpl(get, changes, version) 28 | } 29 | 30 | /** 31 | * @internal 32 | */ 33 | export class VersionedImpl extends EffectBase implements Versioned { 34 | readonly [Subscribable.TypeId]: typeof Subscribable.TypeId = Subscribable.TypeId 35 | readonly [Readable.TypeId]: typeof Readable.TypeId = Readable.TypeId 36 | 37 | readonly get: Effect.Effect 38 | readonly channel: Channel, unknown, E, unknown, unknown, unknown, R> 39 | 40 | constructor( 41 | get: Effect.Effect, 42 | readonly changes: Stream.Stream, 43 | readonly version: Effect.Effect, 44 | ) { 45 | super() 46 | this.get = new MulticastEffect(get) 47 | this.channel = Stream.toChannel(this.changes) 48 | } 49 | 50 | toEffect(): Effect.Effect { 51 | return this.get 52 | } 53 | } 54 | 55 | export function tuple>>( 56 | ...versioneds: Versioneds 57 | ): Versioned< 58 | { readonly [K in keyof Versioneds]: Versioned.Success }, 59 | Versioned.Error, 60 | Versioned.Context 61 | > { 62 | return new TupleImpl(versioneds) 63 | } 64 | 65 | class TupleImpl>> 66 | extends EffectBase< 67 | { readonly [K in keyof Versioneds]: Versioned.Success }, 68 | Versioned.Error, 69 | Versioned.Context 70 | > 71 | implements 72 | Versioned< 73 | { readonly [K in keyof Versioneds]: Versioned.Success }, 74 | Versioned.Error, 75 | Versioned.Context 76 | > 77 | { 78 | readonly [Subscribable.TypeId]: typeof Subscribable.TypeId = Subscribable.TypeId 79 | readonly [Readable.TypeId]: typeof Readable.TypeId = Readable.TypeId 80 | 81 | readonly version: Effect.Effect> 82 | readonly changes: Stream.Stream< 83 | { readonly [K in keyof Versioneds]: Versioned.Success }, 84 | Versioned.Error, 85 | Versioned.Context 86 | > 87 | 88 | constructor(readonly versioneds: Versioneds) { 89 | super() 90 | 91 | this.version = Effect.all(versioneds, { concurrency: 'unbounded' }).pipe( 92 | (_) => _ as Effect.Effect>, 93 | Effect.map((versions) => versions.reduce((acc, version) => acc + version, 0)), 94 | ) 95 | 96 | this.changes = Stream.zipLatestAll(...versioneds.map((v) => v.changes)) as Stream.Stream< 97 | { readonly [K in keyof Versioneds]: Versioned.Success }, 98 | Versioned.Error, 99 | Versioned.Context 100 | > 101 | } 102 | 103 | get get(): Effect.Effect< 104 | { readonly [K in keyof Versioneds]: Versioned.Success }, 105 | Versioned.Error, 106 | Versioned.Context 107 | > { 108 | return this 109 | } 110 | 111 | toEffect(): Effect.Effect< 112 | { readonly [K in keyof Versioneds]: Versioned.Success }, 113 | Versioned.Error, 114 | Versioned.Context 115 | > { 116 | return Effect.all(this.versioneds, { 117 | concurrency: 'unbounded', 118 | }) as Effect.Effect< 119 | { readonly [K in keyof Versioneds]: Versioned.Success }, 120 | Versioned.Error, 121 | Versioned.Context 122 | > 123 | } 124 | } 125 | 126 | export function struct>>>( 127 | refs: Versioneds, 128 | ): Versioned< 129 | { readonly [K in keyof Versioneds]: Versioned.Success }, 130 | Versioned.Error, 131 | Versioned.Context 132 | > { 133 | return new StructImpl(refs) 134 | } 135 | 136 | class StructImpl>>> 137 | extends EffectBase< 138 | { readonly [K in keyof Versioneds]: Versioned.Success }, 139 | Versioned.Error, 140 | Versioned.Context 141 | > 142 | implements 143 | Versioned< 144 | { readonly [K in keyof Versioneds]: Versioned.Success }, 145 | Versioned.Error, 146 | Versioned.Context 147 | > 148 | { 149 | readonly [Subscribable.TypeId]: typeof Subscribable.TypeId = Subscribable.TypeId 150 | readonly [Readable.TypeId]: typeof Readable.TypeId = Readable.TypeId 151 | 152 | readonly version: Effect.Effect> 153 | readonly changes: Stream.Stream< 154 | { readonly [K in keyof Versioneds]: Versioned.Success }, 155 | Versioned.Error, 156 | Versioned.Context 157 | > 158 | 159 | constructor(readonly versioneds: Versioneds) { 160 | super() 161 | 162 | this.version = Effect.all(Object.values(versioneds), { concurrency: 'unbounded' }).pipe( 163 | (_) => _ as Effect.Effect>, 164 | Effect.map((versions) => versions.reduce((acc, version) => acc + version, 0)), 165 | ) 166 | 167 | this.changes = Stream.map( 168 | Stream.zipLatestAll( 169 | ...Object.entries(versioneds).map(([key, { changes }]) => 170 | changes.pipe(Stream.map((value) => [key, value] as const)), 171 | ), 172 | ), 173 | Record.fromEntries, 174 | ) as Stream.Stream< 175 | { readonly [K in keyof Versioneds]: Versioned.Success }, 176 | Versioned.Error, 177 | Versioned.Context 178 | > 179 | } 180 | 181 | get get(): Effect.Effect< 182 | { readonly [K in keyof Versioneds]: Versioned.Success }, 183 | Versioned.Error, 184 | Versioned.Context 185 | > { 186 | return this 187 | } 188 | 189 | toEffect(): Effect.Effect< 190 | { readonly [K in keyof Versioneds]: Versioned.Success }, 191 | Versioned.Error, 192 | Versioned.Context 193 | > { 194 | return Effect.all(this.versioneds, { concurrency: 'unbounded' }) as Effect.Effect< 195 | { readonly [K in keyof Versioneds]: Versioned.Success }, 196 | Versioned.Error, 197 | Versioned.Context 198 | > 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Computed.js' 2 | export * from './LazyRef.js' 3 | export * as Versioned from './Versioned.js' 4 | -------------------------------------------------------------------------------- /src/internal.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@effect/vitest' 2 | import { Effect } from 'effect' 3 | import { MulticastEffect } from './internal.js' 4 | 5 | describe('MulticastEffect', () => { 6 | it.effect('ensures only one Effect is running at a time', () => 7 | Effect.gen(function* () { 8 | let count = 0 9 | const effect = Effect.sync(() => { 10 | count++ 11 | return count 12 | }) 13 | 14 | const multicast = new MulticastEffect(effect) 15 | yield* Effect.all( 16 | Array.from({ length: 100 }, () => multicast), 17 | { concurrency: 'unbounded' }, 18 | ) 19 | expect(count).toBe(1) 20 | yield* Effect.all( 21 | Array.from({ length: 500 }, () => multicast), 22 | { concurrency: 'unbounded' }, 23 | ) 24 | expect(count).toBe(2) 25 | }).pipe(Effect.scoped), 26 | ) 27 | }) 28 | -------------------------------------------------------------------------------- /src/internal.ts: -------------------------------------------------------------------------------- 1 | import * as Cause from 'effect/Cause' 2 | import * as Chunk from 'effect/Chunk' 3 | import * as Context from 'effect/Context' 4 | import * as Data from 'effect/Data' 5 | import * as Deferred from 'effect/Deferred' 6 | import * as Effect from 'effect/Effect' 7 | import * as Effectable from 'effect/Effectable' 8 | import * as Equal from 'effect/Equal' 9 | import * as Equivalence from 'effect/Equivalence' 10 | import * as ExecutionStrategy from 'effect/ExecutionStrategy' 11 | import * as Exit from 'effect/Exit' 12 | import * as Fiber from 'effect/Fiber' 13 | import type * as FiberId from 'effect/FiberId' 14 | import { constVoid } from 'effect/Function' 15 | import * as MutableRef from 'effect/MutableRef' 16 | import * as Option from 'effect/Option' 17 | import { pipeArguments } from 'effect/Pipeable' 18 | import * as Predicate from 'effect/Predicate' 19 | import * as PubSub from 'effect/PubSub' 20 | import * as Queue from 'effect/Queue' 21 | import * as Record from 'effect/Record' 22 | import type * as Runtime from 'effect/Runtime' 23 | import * as Scope from 'effect/Scope' 24 | import * as Stream from 'effect/Stream' 25 | import type * as Types from 'effect/Types' 26 | import type { GetSetDelete, LazyRefOptions } from './LazyRef.js' 27 | 28 | const toDeepEquals = (u: unknown): unknown => { 29 | switch (typeof u) { 30 | case 'object': { 31 | if (Predicate.isNullable(u)) { 32 | return u 33 | } else if (Equal.symbol in u) { 34 | return u 35 | } else if (Array.isArray(u)) { 36 | return Data.tuple(u.map(toDeepEquals)) 37 | } else if (u instanceof Set) { 38 | return Data.tuple(Array.from(u, toDeepEquals)) 39 | } else if (u instanceof Map) { 40 | return Data.tuple(Array.from(u, ([k, v]) => Data.tuple([toDeepEquals(k), toDeepEquals(v)]))) 41 | } else if (u instanceof URLSearchParams) { 42 | return Data.tuple( 43 | Array.from(u.keys()).map((key) => Data.tuple([key, toDeepEquals(u.getAll(key))])), 44 | ) 45 | } else if (Symbol.iterator in u) { 46 | return Data.tuple(Array.from(u as any, toDeepEquals)) 47 | } else { 48 | return Data.struct(Record.map(u, toDeepEquals)) 49 | } 50 | } 51 | default: 52 | return u 53 | } 54 | } 55 | 56 | /** 57 | * @internal 58 | */ 59 | export const deepEquals = (a: unknown, b: unknown) => { 60 | // Attempt reference equality first for performance 61 | if (Object.is(a, b)) return true 62 | return Equal.equals(toDeepEquals(a), toDeepEquals(b)) 63 | } 64 | 65 | export abstract class EffectBase 66 | extends Effectable.StructuralClass 67 | implements Effect.Effect 68 | { 69 | abstract toEffect(): Effect.Effect 70 | 71 | private _effect: Effect.Effect | undefined 72 | 73 | commit(): Effect.Effect { 74 | return (this._effect ??= this.toEffect()) 75 | } 76 | } 77 | 78 | export class SubscriptionRefCore { 79 | constructor( 80 | readonly initial: Effect.Effect, 81 | readonly pubsub: PubsubWithReplay, 82 | readonly runtime: Runtime.Runtime, 83 | readonly scope: Scope.CloseableScope, 84 | readonly deferredRef: DeferredRef, 85 | readonly semaphore: Effect.Semaphore, 86 | ) {} 87 | 88 | public _fiber: Fiber.Fiber | undefined = undefined 89 | } 90 | 91 | export function makeCore(initial: Effect.Effect, options?: LazyRefOptions) { 92 | return Effect.runtime().pipe( 93 | Effect.bindTo('runtime'), 94 | Effect.bind('scope', ({ runtime }) => 95 | Scope.fork(Context.get(runtime.context, Scope.Scope), ExecutionStrategy.parallel), 96 | ), 97 | Effect.bind('id', () => Effect.fiberId), 98 | Effect.map(({ id, runtime, scope }) => unsafeMakeCore(initial, id, runtime, scope, options)), 99 | Effect.tap((core) => 100 | Scope.addFinalizer(core.scope, Effect.provide(interruptCore(core), core.runtime)), 101 | ), 102 | ) 103 | } 104 | 105 | export function unsafeMakeCore( 106 | initial: Effect.Effect, 107 | id: FiberId.FiberId, 108 | runtime: Runtime.Runtime, 109 | scope: Scope.CloseableScope, 110 | options?: LazyRefOptions, 111 | ): SubscriptionRefCore { 112 | const pubsub = pubsubWithReplay() 113 | const core = new SubscriptionRefCore( 114 | initial, 115 | pubsub, 116 | runtime, 117 | scope, 118 | unsafeMakeDeferredRef(id, getExitEquivalence(options?.eq ?? deepEquals), pubsub.lastValue), 119 | Effect.unsafeMakeSemaphore(1), 120 | ) 121 | 122 | // Initialize the deferred ref with the initial value if it's already available 123 | matchEffectPrimitive(initial, { 124 | Success: (a) => core.deferredRef.done(Exit.succeed(a)), 125 | Failure: (cause) => core.deferredRef.done(Exit.failCause(cause)), 126 | Left: (e) => core.deferredRef.done(Exit.fail(e)), 127 | Right: (a) => core.deferredRef.done(Exit.succeed(a)), 128 | Some: (a) => core.deferredRef.done(Exit.succeed(a)), 129 | Sync: (f) => core.deferredRef.done(Exit.succeed(f())), 130 | None: (e) => core.deferredRef.done(Exit.fail(e)), 131 | Otherwise: constVoid, 132 | }) 133 | 134 | return core 135 | } 136 | 137 | export function matchEffectPrimitive( 138 | effect: Effect.Effect, 139 | matchers: { 140 | Success: (a: A) => Z 141 | Failure: (e: Cause.Cause) => Z 142 | Sync: (f: () => A) => Z 143 | Left: (e: E) => Z 144 | Right: (a: A) => Z 145 | Some: (a: A) => Z 146 | None: (e: E) => Z 147 | Otherwise: (effect: Effect.Effect) => Z 148 | }, 149 | ): Z { 150 | const eff = effect as any 151 | 152 | switch (eff._op) { 153 | case 'Success': 154 | return matchers.Success(eff.value) 155 | case 'Failure': 156 | return matchers.Failure(eff.cause) 157 | case 'Sync': 158 | return matchers.Sync(eff.effect_instruction_i0) 159 | case 'Left': 160 | return matchers.Left(eff.left) 161 | case 'Right': 162 | return matchers.Right(eff.right) 163 | case 'Some': 164 | return matchers.Some(eff.value) 165 | case 'None': 166 | return matchers.None(new Cause.NoSuchElementException() as E) 167 | case 'Commit': 168 | return matchEffectPrimitive(eff.commit(), matchers) 169 | default: 170 | return matchers.Otherwise(effect) 171 | } 172 | } 173 | 174 | export const getExitEquivalence = (A: Equivalence.Equivalence) => 175 | Equivalence.make>((a, b) => { 176 | if (a._tag === 'Failure') { 177 | return b._tag === 'Failure' && Equal.equals(a.cause, b.cause) 178 | } else { 179 | return b._tag === 'Success' && A(a.value, b.value) 180 | } 181 | }) 182 | 183 | // Here to wrap the pubsub with a last value ref which can be shared with the DeferredRef 184 | class PubsubWithReplay implements PubSub.PubSub> { 185 | [Queue.EnqueueTypeId]: { 186 | _In: Types.Contravariant> 187 | } = { 188 | _In: (_) => _, 189 | } 190 | 191 | readonly subscriberCount: MutableRef.MutableRef = MutableRef.make(0) 192 | 193 | constructor( 194 | readonly pubsub: PubSub.PubSub>, 195 | readonly lastValue: MutableRef.MutableRef>>, 196 | ) {} 197 | 198 | publish(value: Exit.Exit): Effect.Effect { 199 | return this.pubsub.publish(value) 200 | } 201 | 202 | publishAll(elements: Iterable>): Effect.Effect { 203 | return this.pubsub.publishAll(elements) 204 | } 205 | 206 | subscribe: Effect.Effect>, never, Scope.Scope> = Effect.suspend( 207 | () => { 208 | MutableRef.increment(this.subscriberCount) 209 | 210 | return this.pubsub.subscribe.pipe( 211 | Effect.map((dequeue) => 212 | Option.match(MutableRef.get(this.lastValue), { 213 | onNone: () => dequeue, 214 | onSome: (previous) => dequeuePrepend(dequeue, previous), 215 | }), 216 | ), 217 | Effect.tap( 218 | Effect.addFinalizer(() => Effect.sync(() => MutableRef.decrement(this.subscriberCount))), 219 | ), 220 | ) 221 | }, 222 | ) 223 | 224 | offer(value: Exit.Exit): Effect.Effect { 225 | return this.pubsub.offer(value) 226 | } 227 | 228 | unsafeOffer(value: Exit.Exit) { 229 | return this.pubsub.unsafeOffer(value) 230 | } 231 | 232 | offerAll(elements: Iterable>): Effect.Effect { 233 | return this.pubsub.offerAll(elements) 234 | } 235 | 236 | capacity(): number { 237 | return this.pubsub.capacity() 238 | } 239 | 240 | isActive(): boolean { 241 | return this.pubsub.isActive() 242 | } 243 | 244 | get size(): Effect.Effect { 245 | return this.pubsub.size 246 | } 247 | 248 | unsafeSize(): Option.Option { 249 | return this.pubsub.unsafeSize() 250 | } 251 | 252 | get isFull(): Effect.Effect { 253 | return this.pubsub.isFull 254 | } 255 | 256 | get isEmpty(): Effect.Effect { 257 | return this.pubsub.isEmpty 258 | } 259 | 260 | get isShutdown(): Effect.Effect { 261 | return this.pubsub.isShutdown 262 | } 263 | 264 | get shutdown(): Effect.Effect { 265 | return this.pubsub.shutdown 266 | } 267 | 268 | get awaitShutdown(): Effect.Effect { 269 | return this.pubsub.awaitShutdown 270 | } 271 | 272 | pipe() { 273 | // biome-ignore lint/style/noArguments: This is a pipeable 274 | return pipeArguments(this, arguments) 275 | } 276 | } 277 | 278 | class DequeueWithPrepend extends EffectBase implements Queue.Dequeue { 279 | private previousUtilized = false; 280 | 281 | [Queue.DequeueTypeId]: { 282 | _Out: Types.Covariant 283 | } = { 284 | _Out: (_) => _, 285 | } 286 | 287 | constructor( 288 | readonly dequeue: Queue.Dequeue, 289 | readonly previous: A, 290 | ) { 291 | super() 292 | } 293 | 294 | toEffect(): Effect.Effect { 295 | return this.take 296 | } 297 | 298 | take: Effect.Effect = Effect.suspend(() => { 299 | if (this.previousUtilized) { 300 | return this.dequeue.take 301 | } else { 302 | this.previousUtilized = true 303 | return Effect.succeed(this.previous) 304 | } 305 | }) 306 | 307 | takeAll: Effect.Effect> = Effect.suspend(() => { 308 | if (this.previousUtilized) { 309 | return this.dequeue.takeAll 310 | } else { 311 | this.previousUtilized = true 312 | return this.dequeue.takeAll.pipe(Effect.map((a) => Chunk.prepend(a, this.previous))) 313 | } 314 | }) 315 | 316 | takeUpTo(max: number): Effect.Effect, never, never> { 317 | return Effect.suspend(() => { 318 | if (this.previousUtilized) { 319 | return this.dequeue.takeUpTo(max) 320 | } else { 321 | this.previousUtilized = true 322 | return this.dequeue 323 | .takeUpTo(max - 1) 324 | .pipe(Effect.map((a) => Chunk.prepend(a, this.previous))) 325 | } 326 | }) 327 | } 328 | 329 | takeBetween(min: number, max: number): Effect.Effect, never, never> { 330 | return Effect.suspend(() => { 331 | if (this.previousUtilized) { 332 | return this.dequeue.takeBetween(min, max) 333 | } else { 334 | this.previousUtilized = true 335 | 336 | return this.dequeue 337 | .takeBetween(min - 1, max - 1) 338 | .pipe(Effect.map((a) => Chunk.prepend(a, this.previous))) 339 | } 340 | }) 341 | } 342 | 343 | capacity(): number { 344 | return this.dequeue.capacity() 345 | } 346 | 347 | isActive(): boolean { 348 | return this.dequeue.isActive() 349 | } 350 | 351 | get size(): Effect.Effect { 352 | return Effect.suspend(() => { 353 | if (this.previousUtilized) { 354 | return this.dequeue.size 355 | } else { 356 | return this.dequeue.size.pipe(Effect.map((size) => size + 1)) 357 | } 358 | }) 359 | } 360 | 361 | unsafeSize(): Option.Option { 362 | if (this.previousUtilized) { 363 | return this.dequeue.unsafeSize() 364 | } else { 365 | return this.dequeue.unsafeSize().pipe(Option.map((size) => size + 1)) 366 | } 367 | } 368 | 369 | get isFull(): Effect.Effect { 370 | return this.dequeue.isFull 371 | } 372 | 373 | get isEmpty(): Effect.Effect { 374 | return Effect.suspend(() => { 375 | if (this.previousUtilized) { 376 | return this.dequeue.isEmpty 377 | } else { 378 | return Effect.succeed(false) 379 | } 380 | }) 381 | } 382 | 383 | get isShutdown(): Effect.Effect { 384 | return this.dequeue.isShutdown 385 | } 386 | 387 | get shutdown(): Effect.Effect { 388 | return this.dequeue.shutdown 389 | } 390 | 391 | get awaitShutdown(): Effect.Effect { 392 | return this.dequeue.awaitShutdown 393 | } 394 | 395 | pipe() { 396 | // biome-ignore lint/style/noArguments: This is a pipeable 397 | return pipeArguments(this, arguments) 398 | } 399 | } 400 | 401 | export function dequeuePrepend(dequeue: Queue.Dequeue, previous: A): Queue.Dequeue { 402 | return new DequeueWithPrepend(dequeue, previous) 403 | } 404 | 405 | export function pubsubWithReplay() { 406 | const lastValue = MutableRef.make(Option.none>()) 407 | const base = Effect.runSync(PubSub.unbounded>()) 408 | return new PubsubWithReplay(base, lastValue) 409 | } 410 | 411 | export function streamExit( 412 | stream: Stream.Stream, 413 | ): Stream.Stream, never, R> { 414 | return stream.pipe( 415 | Stream.map(Exit.succeed), 416 | Stream.catchAllCause((cause) => Stream.succeed(Exit.failCause(cause))), 417 | ) 418 | } 419 | 420 | export class DeferredRef extends EffectBase { 421 | // Keep track of the latest value emitted by the stream 422 | public version!: number 423 | public deferred!: Deferred.Deferred 424 | 425 | constructor( 426 | private id: FiberId.FiberId, 427 | private eq: Equivalence.Equivalence>, 428 | readonly current: MutableRef.MutableRef>>, 429 | ) { 430 | super() 431 | this.reset() 432 | } 433 | 434 | toEffect() { 435 | return Effect.suspend(() => { 436 | const current = MutableRef.get(this.current) 437 | if (Option.isNone(current)) { 438 | return Deferred.await(this.deferred) 439 | } else { 440 | return current.value 441 | } 442 | }) 443 | } 444 | 445 | done(exit: Exit.Exit) { 446 | const current = MutableRef.get(this.current) 447 | 448 | MutableRef.set(this.current, Option.some(exit)) 449 | 450 | if (Option.isSome(current) && this.eq(current.value, exit)) { 451 | return false 452 | } 453 | 454 | Deferred.unsafeDone(this.deferred, exit) 455 | this.version += 1 456 | 457 | return true 458 | } 459 | 460 | reset() { 461 | MutableRef.set(this.current, Option.none()) 462 | this.version = -1 463 | 464 | if (this.deferred) { 465 | Deferred.unsafeDone(this.deferred, Exit.interrupt(this.id)) 466 | } 467 | 468 | this.deferred = Deferred.unsafeMake(this.id) 469 | } 470 | } 471 | 472 | export function makeDeferredRef(eq: Equivalence.Equivalence>) { 473 | return Effect.map(Effect.fiberId, (id) => new DeferredRef(id, eq, MutableRef.make(Option.none()))) 474 | } 475 | 476 | export function unsafeMakeDeferredRef( 477 | id: FiberId.FiberId, 478 | eq: Equivalence.Equivalence>, 479 | current: MutableRef.MutableRef>>, 480 | ) { 481 | return new DeferredRef(id, eq, current) 482 | } 483 | 484 | export class MulticastEffect extends EffectBase { 485 | private _fiber: Fiber.Fiber | null = null 486 | private _refCount = 0 487 | 488 | constructor(readonly effect: Effect.Effect) { 489 | super() 490 | } 491 | 492 | toEffect(): Effect.Effect { 493 | return Effect.suspend(() => { 494 | if (++this._refCount === 1) { 495 | return this.effect.pipe( 496 | Effect.forkDaemon, 497 | Effect.tap((fiber) => { 498 | this._fiber = fiber 499 | }), 500 | Effect.flatMap(Fiber.join), 501 | Effect.onExit(() => this.interrupt), 502 | ) 503 | } 504 | 505 | // biome-ignore lint/style/noNonNullAssertion: fiber is set by forkDaemon 506 | return Effect.fromFiber(this._fiber!).pipe(Effect.onExit(() => this.interrupt)) 507 | }) 508 | } 509 | 510 | interrupt = Effect.suspend(() => { 511 | if (--this._refCount === 0 && this._fiber) { 512 | const interrupt = Fiber.interrupt(this._fiber) 513 | this._fiber = null 514 | return interrupt 515 | } 516 | 517 | return Effect.void 518 | }) 519 | } 520 | 521 | export function getSetDelete( 522 | core: SubscriptionRefCore, 523 | ): GetSetDelete> { 524 | return { 525 | get: getOrInitializeCore(core, false), 526 | set: (a) => setCore(core, a), 527 | delete: deleteCore(core), 528 | version: Effect.sync(() => core.deferredRef.version), 529 | } 530 | } 531 | 532 | export function getOrInitializeCore( 533 | core: SubscriptionRefCore, 534 | lockInitialize: boolean, 535 | ): Effect.Effect> { 536 | return Effect.suspend(() => { 537 | if (core._fiber === undefined && Option.isNone(MutableRef.get(core.deferredRef.current))) { 538 | return initializeCoreAndTap(core, lockInitialize) 539 | } else { 540 | return core.deferredRef 541 | } 542 | }) 543 | } 544 | 545 | function initializeCoreEffect( 546 | core: SubscriptionRefCore, 547 | lock: boolean, 548 | ): Effect.Effect, never, Exclude> { 549 | const initialize = Effect.onExit(Effect.provide(core.initial, core.runtime.context), (exit) => 550 | Effect.sync(() => { 551 | core._fiber = undefined 552 | core.deferredRef.done(exit) 553 | }), 554 | ) 555 | 556 | return Effect.flatMap( 557 | Effect.forkIn(lock ? core.semaphore.withPermits(1)(initialize) : initialize, core.scope), 558 | (fiber) => Effect.sync(() => (core._fiber = fiber)), 559 | ) 560 | } 561 | 562 | function initializeCore( 563 | core: SubscriptionRefCore, 564 | lock: boolean, 565 | ): Effect.Effect, never, Exclude> { 566 | type Z = Effect.Effect, never, Exclude> 567 | 568 | const onSuccess = (a: A): Z => { 569 | core.deferredRef.done(Exit.succeed(a)) 570 | return Effect.succeed(Fiber.succeed(a)) 571 | } 572 | 573 | const onCause = (cause: Cause.Cause): Z => { 574 | core.deferredRef.done(Exit.failCause(cause)) 575 | return Effect.succeed(Fiber.failCause(cause)) 576 | } 577 | 578 | const onError = (e: E): Z => onCause(Cause.fail(e)) 579 | 580 | return matchEffectPrimitive(core.initial, { 581 | Success: onSuccess, 582 | Failure: onCause, 583 | Some: onSuccess, 584 | None: onError, 585 | Left: onError, 586 | Right: onSuccess, 587 | Sync: (f) => onSuccess(f()), 588 | Otherwise: () => initializeCoreEffect(core, lock), 589 | }) 590 | } 591 | 592 | function initializeCoreAndTap( 593 | core: SubscriptionRefCore, 594 | lock: boolean, 595 | ): Effect.Effect> { 596 | return Effect.zipRight(initializeCore(core, lock), tapEventCore(core, core.deferredRef), { 597 | concurrent: true, 598 | }) 599 | } 600 | 601 | function setCore( 602 | core: SubscriptionRefCore, 603 | a: A, 604 | ): Effect.Effect> { 605 | const exit = Exit.succeed(a) 606 | 607 | return Effect.sync(() => { 608 | // If the value changed, send an event 609 | if (core.deferredRef.done(exit)) { 610 | sendEvent(core, exit) 611 | } 612 | return a 613 | }) 614 | } 615 | 616 | export function interruptCore( 617 | core: SubscriptionRefCore, 618 | ): Effect.Effect { 619 | return Effect.fiberIdWith((id) => { 620 | core.deferredRef.reset() 621 | 622 | const closeScope = Effect.forkDaemon(Scope.close(core.scope, Exit.interrupt(id))) 623 | const interruptFiber = core._fiber ? Fiber.interruptFork(core._fiber) : Effect.void 624 | const interruptSubject = core.pubsub.shutdown 625 | 626 | return Effect.all([closeScope, interruptFiber, interruptSubject], { 627 | discard: true, 628 | concurrency: 'unbounded', 629 | }) 630 | }) 631 | } 632 | 633 | export function interruptCoreAndWait( 634 | core: SubscriptionRefCore, 635 | ): Effect.Effect { 636 | return Effect.fiberIdWith((id) => { 637 | core.deferredRef.reset() 638 | 639 | const closeScope = Scope.close(core.scope, Exit.interrupt(id)) 640 | const interruptFiber = core._fiber ? Fiber.interrupt(core._fiber) : Effect.void 641 | const interruptSubject = core.pubsub.awaitShutdown 642 | 643 | return Effect.all([closeScope, interruptFiber, interruptSubject], { 644 | discard: true, 645 | concurrency: 'unbounded', 646 | }) 647 | }) 648 | } 649 | 650 | function deleteCore( 651 | core: SubscriptionRefCore, 652 | ): Effect.Effect, E, Exclude> { 653 | return Effect.suspend(() => { 654 | const current = MutableRef.get(core.deferredRef.current) 655 | core.deferredRef.reset() 656 | 657 | if (Option.isNone(current)) { 658 | return Effect.succeed(Option.none()) 659 | } 660 | 661 | return Effect.sync(() => MutableRef.get(core.pubsub.subscriberCount)).pipe( 662 | Effect.flatMap((count: number) => 663 | count > 0 && !core._fiber ? initializeCore(core, false) : Effect.void, 664 | ), 665 | Effect.zipRight(Effect.asSome(current.value)), 666 | ) 667 | }) 668 | } 669 | 670 | function tapEventCore( 671 | core: SubscriptionRefCore, 672 | effect: Effect.Effect, 673 | ) { 674 | return effect.pipe( 675 | Effect.exit, 676 | Effect.tap((exit) => sendEvent(core, exit)), 677 | Effect.flatten, 678 | ) 679 | } 680 | 681 | export function sendEvent( 682 | core: SubscriptionRefCore, 683 | exit: Exit.Exit, 684 | ): void { 685 | core.pubsub.unsafeOffer(exit) 686 | } 687 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "lib": [ 7 | "ES2022" 8 | ], 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "declaration": true, 14 | "sourceMap": true, 15 | "outDir": "dist", 16 | "rootDir": "src" 17 | }, 18 | "include": [ 19 | "src/**/*" 20 | ], 21 | "exclude": [ 22 | "node_modules", 23 | "dist", 24 | "**/*.test.ts" 25 | ] 26 | } --------------------------------------------------------------------------------