A generic websocket connection with Zod schema validation and on message execution.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add cross-platform support for Node.js and browser environments

Replace hard dependencies on Node.js APIs (ws, Buffer, EventEmitter)
with a platform adapter pattern that auto-detects the runtime and uses
the appropriate WebSocket implementation. Move ws to optional
peerDependencies so browser consumers never pull it in.

vinerima 4a12e779 82075c1b

+1625 -62
+20 -3
package.json
··· 10 10 "files": [ 11 11 "dist" 12 12 ], 13 + "exports": { 14 + ".": { 15 + "types": "./dist/index.d.ts", 16 + "import": "./dist/index.mjs", 17 + "require": "./dist/index.js" 18 + } 19 + }, 13 20 "scripts": { 14 21 "build": "tsup", 15 22 "lint": "eslint src", 16 23 "lint:fix": "eslint src --fix", 17 24 "format": "prettier --write \"src/**/*.ts\"", 18 25 "format:check": "prettier --check \"src/**/*.ts\"", 19 - "typecheck": "tsc --noEmit" 26 + "typecheck": "tsc --noEmit", 27 + "test": "vitest run", 28 + "test:watch": "vitest" 20 29 }, 21 30 "dependencies": { 22 - "ws": "^8.19.0", 23 31 "zod": "^3.24.0" 24 32 }, 33 + "peerDependencies": { 34 + "ws": "^8.19.0" 35 + }, 36 + "peerDependenciesMeta": { 37 + "ws": { 38 + "optional": true 39 + } 40 + }, 25 41 "devDependencies": { 26 42 "@types/node": "^22.19.0", 27 43 "@types/ws": "^8.5.13", ··· 31 47 "eslint-config-prettier": "^9.1.0", 32 48 "prettier": "^3.8.0", 33 49 "tsup": "^8.5.0", 34 - "typescript": "^5.9.0" 50 + "typescript": "^5.9.0", 51 + "vitest": "^4.1.0" 35 52 }, 36 53 "engines": { 37 54 "node": ">=18.0.0"
+666 -4
pnpm-lock.yaml
··· 38 38 version: 3.8.1 39 39 tsup: 40 40 specifier: ^8.5.0 41 - version: 8.5.1(typescript@5.9.3) 41 + version: 8.5.1(postcss@8.5.8)(typescript@5.9.3) 42 42 typescript: 43 43 specifier: ^5.9.0 44 44 version: 5.9.3 45 + vitest: 46 + specifier: ^4.1.0 47 + version: 4.1.0(@types/node@22.19.11)(vite@8.0.1(@types/node@22.19.11)(esbuild@0.27.3)) 45 48 46 49 packages: 50 + 51 + '@emnapi/core@1.9.1': 52 + resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} 53 + 54 + '@emnapi/runtime@1.9.1': 55 + resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} 56 + 57 + '@emnapi/wasi-threads@1.2.0': 58 + resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} 47 59 48 60 '@esbuild/aix-ppc64@0.27.3': 49 61 resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} ··· 268 280 '@jridgewell/trace-mapping@0.3.31': 269 281 resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} 270 282 283 + '@napi-rs/wasm-runtime@1.1.1': 284 + resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} 285 + 286 + '@oxc-project/types@0.120.0': 287 + resolution: {integrity: sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==} 288 + 289 + '@rolldown/binding-android-arm64@1.0.0-rc.10': 290 + resolution: {integrity: sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==} 291 + engines: {node: ^20.19.0 || >=22.12.0} 292 + cpu: [arm64] 293 + os: [android] 294 + 295 + '@rolldown/binding-darwin-arm64@1.0.0-rc.10': 296 + resolution: {integrity: sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==} 297 + engines: {node: ^20.19.0 || >=22.12.0} 298 + cpu: [arm64] 299 + os: [darwin] 300 + 301 + '@rolldown/binding-darwin-x64@1.0.0-rc.10': 302 + resolution: {integrity: sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==} 303 + engines: {node: ^20.19.0 || >=22.12.0} 304 + cpu: [x64] 305 + os: [darwin] 306 + 307 + '@rolldown/binding-freebsd-x64@1.0.0-rc.10': 308 + resolution: {integrity: sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==} 309 + engines: {node: ^20.19.0 || >=22.12.0} 310 + cpu: [x64] 311 + os: [freebsd] 312 + 313 + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10': 314 + resolution: {integrity: sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==} 315 + engines: {node: ^20.19.0 || >=22.12.0} 316 + cpu: [arm] 317 + os: [linux] 318 + 319 + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10': 320 + resolution: {integrity: sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==} 321 + engines: {node: ^20.19.0 || >=22.12.0} 322 + cpu: [arm64] 323 + os: [linux] 324 + 325 + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.10': 326 + resolution: {integrity: sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==} 327 + engines: {node: ^20.19.0 || >=22.12.0} 328 + cpu: [arm64] 329 + os: [linux] 330 + 331 + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10': 332 + resolution: {integrity: sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==} 333 + engines: {node: ^20.19.0 || >=22.12.0} 334 + cpu: [ppc64] 335 + os: [linux] 336 + 337 + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10': 338 + resolution: {integrity: sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==} 339 + engines: {node: ^20.19.0 || >=22.12.0} 340 + cpu: [s390x] 341 + os: [linux] 342 + 343 + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.10': 344 + resolution: {integrity: sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==} 345 + engines: {node: ^20.19.0 || >=22.12.0} 346 + cpu: [x64] 347 + os: [linux] 348 + 349 + '@rolldown/binding-linux-x64-musl@1.0.0-rc.10': 350 + resolution: {integrity: sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==} 351 + engines: {node: ^20.19.0 || >=22.12.0} 352 + cpu: [x64] 353 + os: [linux] 354 + 355 + '@rolldown/binding-openharmony-arm64@1.0.0-rc.10': 356 + resolution: {integrity: sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==} 357 + engines: {node: ^20.19.0 || >=22.12.0} 358 + cpu: [arm64] 359 + os: [openharmony] 360 + 361 + '@rolldown/binding-wasm32-wasi@1.0.0-rc.10': 362 + resolution: {integrity: sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==} 363 + engines: {node: '>=14.0.0'} 364 + cpu: [wasm32] 365 + 366 + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10': 367 + resolution: {integrity: sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==} 368 + engines: {node: ^20.19.0 || >=22.12.0} 369 + cpu: [arm64] 370 + os: [win32] 371 + 372 + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.10': 373 + resolution: {integrity: sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==} 374 + engines: {node: ^20.19.0 || >=22.12.0} 375 + cpu: [x64] 376 + os: [win32] 377 + 378 + '@rolldown/pluginutils@1.0.0-rc.10': 379 + resolution: {integrity: sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==} 380 + 271 381 '@rollup/rollup-android-arm-eabi@4.57.1': 272 382 resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} 273 383 cpu: [arm] ··· 393 503 cpu: [x64] 394 504 os: [win32] 395 505 506 + '@standard-schema/spec@1.1.0': 507 + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} 508 + 509 + '@tybys/wasm-util@0.10.1': 510 + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} 511 + 512 + '@types/chai@5.2.3': 513 + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} 514 + 515 + '@types/deep-eql@4.0.2': 516 + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} 517 + 396 518 '@types/estree@1.0.8': 397 519 resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} 398 520 ··· 464 586 resolution: {integrity: sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==} 465 587 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 466 588 589 + '@vitest/expect@4.1.0': 590 + resolution: {integrity: sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==} 591 + 592 + '@vitest/mocker@4.1.0': 593 + resolution: {integrity: sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==} 594 + peerDependencies: 595 + msw: ^2.4.9 596 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 597 + peerDependenciesMeta: 598 + msw: 599 + optional: true 600 + vite: 601 + optional: true 602 + 603 + '@vitest/pretty-format@4.1.0': 604 + resolution: {integrity: sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==} 605 + 606 + '@vitest/runner@4.1.0': 607 + resolution: {integrity: sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==} 608 + 609 + '@vitest/snapshot@4.1.0': 610 + resolution: {integrity: sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==} 611 + 612 + '@vitest/spy@4.1.0': 613 + resolution: {integrity: sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==} 614 + 615 + '@vitest/utils@4.1.0': 616 + resolution: {integrity: sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==} 617 + 467 618 acorn-jsx@5.3.2: 468 619 resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} 469 620 peerDependencies: ··· 487 638 argparse@2.0.1: 488 639 resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} 489 640 641 + assertion-error@2.0.1: 642 + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} 643 + engines: {node: '>=12'} 644 + 490 645 balanced-match@1.0.2: 491 646 resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 492 647 ··· 510 665 resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} 511 666 engines: {node: '>=6'} 512 667 668 + chai@6.2.2: 669 + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} 670 + engines: {node: '>=18'} 671 + 513 672 chalk@4.1.2: 514 673 resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 515 674 engines: {node: '>=10'} ··· 539 698 resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} 540 699 engines: {node: ^14.18.0 || >=16.10.0} 541 700 701 + convert-source-map@2.0.0: 702 + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} 703 + 542 704 cross-spawn@7.0.6: 543 705 resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} 544 706 engines: {node: '>= 8'} ··· 555 717 deep-is@0.1.4: 556 718 resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} 557 719 720 + detect-libc@2.1.2: 721 + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} 722 + engines: {node: '>=8'} 723 + 724 + es-module-lexer@2.0.0: 725 + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} 726 + 558 727 esbuild@0.27.3: 559 728 resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} 560 729 engines: {node: '>=18'} ··· 608 777 resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} 609 778 engines: {node: '>=4.0'} 610 779 780 + estree-walker@3.0.3: 781 + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} 782 + 611 783 esutils@2.0.3: 612 784 resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} 613 785 engines: {node: '>=0.10.0'} 786 + 787 + expect-type@1.3.0: 788 + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} 789 + engines: {node: '>=12.0.0'} 614 790 615 791 fast-deep-equal@3.1.3: 616 792 resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} ··· 716 892 resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} 717 893 engines: {node: '>= 0.8.0'} 718 894 895 + lightningcss-android-arm64@1.32.0: 896 + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} 897 + engines: {node: '>= 12.0.0'} 898 + cpu: [arm64] 899 + os: [android] 900 + 901 + lightningcss-darwin-arm64@1.32.0: 902 + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} 903 + engines: {node: '>= 12.0.0'} 904 + cpu: [arm64] 905 + os: [darwin] 906 + 907 + lightningcss-darwin-x64@1.32.0: 908 + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} 909 + engines: {node: '>= 12.0.0'} 910 + cpu: [x64] 911 + os: [darwin] 912 + 913 + lightningcss-freebsd-x64@1.32.0: 914 + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} 915 + engines: {node: '>= 12.0.0'} 916 + cpu: [x64] 917 + os: [freebsd] 918 + 919 + lightningcss-linux-arm-gnueabihf@1.32.0: 920 + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} 921 + engines: {node: '>= 12.0.0'} 922 + cpu: [arm] 923 + os: [linux] 924 + 925 + lightningcss-linux-arm64-gnu@1.32.0: 926 + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} 927 + engines: {node: '>= 12.0.0'} 928 + cpu: [arm64] 929 + os: [linux] 930 + 931 + lightningcss-linux-arm64-musl@1.32.0: 932 + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} 933 + engines: {node: '>= 12.0.0'} 934 + cpu: [arm64] 935 + os: [linux] 936 + 937 + lightningcss-linux-x64-gnu@1.32.0: 938 + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} 939 + engines: {node: '>= 12.0.0'} 940 + cpu: [x64] 941 + os: [linux] 942 + 943 + lightningcss-linux-x64-musl@1.32.0: 944 + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} 945 + engines: {node: '>= 12.0.0'} 946 + cpu: [x64] 947 + os: [linux] 948 + 949 + lightningcss-win32-arm64-msvc@1.32.0: 950 + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} 951 + engines: {node: '>= 12.0.0'} 952 + cpu: [arm64] 953 + os: [win32] 954 + 955 + lightningcss-win32-x64-msvc@1.32.0: 956 + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} 957 + engines: {node: '>= 12.0.0'} 958 + cpu: [x64] 959 + os: [win32] 960 + 961 + lightningcss@1.32.0: 962 + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} 963 + engines: {node: '>= 12.0.0'} 964 + 719 965 lilconfig@3.1.3: 720 966 resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} 721 967 engines: {node: '>=14'} ··· 753 999 mz@2.7.0: 754 1000 resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} 755 1001 1002 + nanoid@3.3.11: 1003 + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} 1004 + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 1005 + hasBin: true 1006 + 756 1007 natural-compare@1.4.0: 757 1008 resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} 758 1009 759 1010 object-assign@4.1.1: 760 1011 resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 761 1012 engines: {node: '>=0.10.0'} 1013 + 1014 + obug@2.1.1: 1015 + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} 762 1016 763 1017 optionator@0.9.4: 764 1018 resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} ··· 819 1073 yaml: 820 1074 optional: true 821 1075 1076 + postcss@8.5.8: 1077 + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} 1078 + engines: {node: ^10 || ^12 || >=14} 1079 + 822 1080 prelude-ls@1.2.1: 823 1081 resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} 824 1082 engines: {node: '>= 0.8.0'} ··· 844 1102 resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} 845 1103 engines: {node: '>=8'} 846 1104 1105 + rolldown@1.0.0-rc.10: 1106 + resolution: {integrity: sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==} 1107 + engines: {node: ^20.19.0 || >=22.12.0} 1108 + hasBin: true 1109 + 847 1110 rollup@4.57.1: 848 1111 resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==} 849 1112 engines: {node: '>=18.0.0', npm: '>=8.0.0'} ··· 862 1125 resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 863 1126 engines: {node: '>=8'} 864 1127 1128 + siginfo@2.0.0: 1129 + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} 1130 + 1131 + source-map-js@1.2.1: 1132 + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 1133 + engines: {node: '>=0.10.0'} 1134 + 865 1135 source-map@0.7.6: 866 1136 resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} 867 1137 engines: {node: '>= 12'} 868 1138 1139 + stackback@0.0.2: 1140 + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} 1141 + 1142 + std-env@4.0.0: 1143 + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} 1144 + 869 1145 strip-json-comments@3.1.1: 870 1146 resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} 871 1147 engines: {node: '>=8'} ··· 886 1162 thenify@3.3.1: 887 1163 resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} 888 1164 1165 + tinybench@2.9.0: 1166 + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} 1167 + 889 1168 tinyexec@0.3.2: 890 1169 resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} 891 1170 1171 + tinyexec@1.0.4: 1172 + resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} 1173 + engines: {node: '>=18'} 1174 + 892 1175 tinyglobby@0.2.15: 893 1176 resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} 894 1177 engines: {node: '>=12.0.0'} 1178 + 1179 + tinyrainbow@3.1.0: 1180 + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} 1181 + engines: {node: '>=14.0.0'} 895 1182 896 1183 tree-kill@1.2.2: 897 1184 resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} ··· 905 1192 906 1193 ts-interface-checker@0.1.13: 907 1194 resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} 1195 + 1196 + tslib@2.8.1: 1197 + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 908 1198 909 1199 tsup@8.5.1: 910 1200 resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} ··· 943 1233 uri-js@4.4.1: 944 1234 resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 945 1235 1236 + vite@8.0.1: 1237 + resolution: {integrity: sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==} 1238 + engines: {node: ^20.19.0 || >=22.12.0} 1239 + hasBin: true 1240 + peerDependencies: 1241 + '@types/node': ^20.19.0 || >=22.12.0 1242 + '@vitejs/devtools': ^0.1.0 1243 + esbuild: ^0.27.0 1244 + jiti: '>=1.21.0' 1245 + less: ^4.0.0 1246 + sass: ^1.70.0 1247 + sass-embedded: ^1.70.0 1248 + stylus: '>=0.54.8' 1249 + sugarss: ^5.0.0 1250 + terser: ^5.16.0 1251 + tsx: ^4.8.1 1252 + yaml: ^2.4.2 1253 + peerDependenciesMeta: 1254 + '@types/node': 1255 + optional: true 1256 + '@vitejs/devtools': 1257 + optional: true 1258 + esbuild: 1259 + optional: true 1260 + jiti: 1261 + optional: true 1262 + less: 1263 + optional: true 1264 + sass: 1265 + optional: true 1266 + sass-embedded: 1267 + optional: true 1268 + stylus: 1269 + optional: true 1270 + sugarss: 1271 + optional: true 1272 + terser: 1273 + optional: true 1274 + tsx: 1275 + optional: true 1276 + yaml: 1277 + optional: true 1278 + 1279 + vitest@4.1.0: 1280 + resolution: {integrity: sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==} 1281 + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} 1282 + hasBin: true 1283 + peerDependencies: 1284 + '@edge-runtime/vm': '*' 1285 + '@opentelemetry/api': ^1.9.0 1286 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 1287 + '@vitest/browser-playwright': 4.1.0 1288 + '@vitest/browser-preview': 4.1.0 1289 + '@vitest/browser-webdriverio': 4.1.0 1290 + '@vitest/ui': 4.1.0 1291 + happy-dom: '*' 1292 + jsdom: '*' 1293 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 1294 + peerDependenciesMeta: 1295 + '@edge-runtime/vm': 1296 + optional: true 1297 + '@opentelemetry/api': 1298 + optional: true 1299 + '@types/node': 1300 + optional: true 1301 + '@vitest/browser-playwright': 1302 + optional: true 1303 + '@vitest/browser-preview': 1304 + optional: true 1305 + '@vitest/browser-webdriverio': 1306 + optional: true 1307 + '@vitest/ui': 1308 + optional: true 1309 + happy-dom: 1310 + optional: true 1311 + jsdom: 1312 + optional: true 1313 + 946 1314 which@2.0.2: 947 1315 resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 948 1316 engines: {node: '>= 8'} 1317 + hasBin: true 1318 + 1319 + why-is-node-running@2.3.0: 1320 + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} 1321 + engines: {node: '>=8'} 949 1322 hasBin: true 950 1323 951 1324 word-wrap@1.2.5: ··· 972 1345 resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} 973 1346 974 1347 snapshots: 1348 + 1349 + '@emnapi/core@1.9.1': 1350 + dependencies: 1351 + '@emnapi/wasi-threads': 1.2.0 1352 + tslib: 2.8.1 1353 + optional: true 1354 + 1355 + '@emnapi/runtime@1.9.1': 1356 + dependencies: 1357 + tslib: 2.8.1 1358 + optional: true 1359 + 1360 + '@emnapi/wasi-threads@1.2.0': 1361 + dependencies: 1362 + tslib: 2.8.1 1363 + optional: true 975 1364 976 1365 '@esbuild/aix-ppc64@0.27.3': 977 1366 optional: true ··· 1122 1511 '@jridgewell/resolve-uri': 3.1.2 1123 1512 '@jridgewell/sourcemap-codec': 1.5.5 1124 1513 1514 + '@napi-rs/wasm-runtime@1.1.1': 1515 + dependencies: 1516 + '@emnapi/core': 1.9.1 1517 + '@emnapi/runtime': 1.9.1 1518 + '@tybys/wasm-util': 0.10.1 1519 + optional: true 1520 + 1521 + '@oxc-project/types@0.120.0': {} 1522 + 1523 + '@rolldown/binding-android-arm64@1.0.0-rc.10': 1524 + optional: true 1525 + 1526 + '@rolldown/binding-darwin-arm64@1.0.0-rc.10': 1527 + optional: true 1528 + 1529 + '@rolldown/binding-darwin-x64@1.0.0-rc.10': 1530 + optional: true 1531 + 1532 + '@rolldown/binding-freebsd-x64@1.0.0-rc.10': 1533 + optional: true 1534 + 1535 + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10': 1536 + optional: true 1537 + 1538 + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10': 1539 + optional: true 1540 + 1541 + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.10': 1542 + optional: true 1543 + 1544 + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10': 1545 + optional: true 1546 + 1547 + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10': 1548 + optional: true 1549 + 1550 + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.10': 1551 + optional: true 1552 + 1553 + '@rolldown/binding-linux-x64-musl@1.0.0-rc.10': 1554 + optional: true 1555 + 1556 + '@rolldown/binding-openharmony-arm64@1.0.0-rc.10': 1557 + optional: true 1558 + 1559 + '@rolldown/binding-wasm32-wasi@1.0.0-rc.10': 1560 + dependencies: 1561 + '@napi-rs/wasm-runtime': 1.1.1 1562 + optional: true 1563 + 1564 + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10': 1565 + optional: true 1566 + 1567 + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.10': 1568 + optional: true 1569 + 1570 + '@rolldown/pluginutils@1.0.0-rc.10': {} 1571 + 1125 1572 '@rollup/rollup-android-arm-eabi@4.57.1': 1126 1573 optional: true 1127 1574 ··· 1197 1644 '@rollup/rollup-win32-x64-msvc@4.57.1': 1198 1645 optional: true 1199 1646 1647 + '@standard-schema/spec@1.1.0': {} 1648 + 1649 + '@tybys/wasm-util@0.10.1': 1650 + dependencies: 1651 + tslib: 2.8.1 1652 + optional: true 1653 + 1654 + '@types/chai@5.2.3': 1655 + dependencies: 1656 + '@types/deep-eql': 4.0.2 1657 + assertion-error: 2.0.1 1658 + 1659 + '@types/deep-eql@4.0.2': {} 1660 + 1200 1661 '@types/estree@1.0.8': {} 1201 1662 1202 1663 '@types/json-schema@7.0.15': {} ··· 1300 1761 '@typescript-eslint/types': 8.55.0 1301 1762 eslint-visitor-keys: 4.2.1 1302 1763 1764 + '@vitest/expect@4.1.0': 1765 + dependencies: 1766 + '@standard-schema/spec': 1.1.0 1767 + '@types/chai': 5.2.3 1768 + '@vitest/spy': 4.1.0 1769 + '@vitest/utils': 4.1.0 1770 + chai: 6.2.2 1771 + tinyrainbow: 3.1.0 1772 + 1773 + '@vitest/mocker@4.1.0(vite@8.0.1(@types/node@22.19.11)(esbuild@0.27.3))': 1774 + dependencies: 1775 + '@vitest/spy': 4.1.0 1776 + estree-walker: 3.0.3 1777 + magic-string: 0.30.21 1778 + optionalDependencies: 1779 + vite: 8.0.1(@types/node@22.19.11)(esbuild@0.27.3) 1780 + 1781 + '@vitest/pretty-format@4.1.0': 1782 + dependencies: 1783 + tinyrainbow: 3.1.0 1784 + 1785 + '@vitest/runner@4.1.0': 1786 + dependencies: 1787 + '@vitest/utils': 4.1.0 1788 + pathe: 2.0.3 1789 + 1790 + '@vitest/snapshot@4.1.0': 1791 + dependencies: 1792 + '@vitest/pretty-format': 4.1.0 1793 + '@vitest/utils': 4.1.0 1794 + magic-string: 0.30.21 1795 + pathe: 2.0.3 1796 + 1797 + '@vitest/spy@4.1.0': {} 1798 + 1799 + '@vitest/utils@4.1.0': 1800 + dependencies: 1801 + '@vitest/pretty-format': 4.1.0 1802 + convert-source-map: 2.0.0 1803 + tinyrainbow: 3.1.0 1804 + 1303 1805 acorn-jsx@5.3.2(acorn@8.15.0): 1304 1806 dependencies: 1305 1807 acorn: 8.15.0 ··· 1320 1822 any-promise@1.3.0: {} 1321 1823 1322 1824 argparse@2.0.1: {} 1825 + 1826 + assertion-error@2.0.1: {} 1323 1827 1324 1828 balanced-match@1.0.2: {} 1325 1829 ··· 1341 1845 1342 1846 callsites@3.1.0: {} 1343 1847 1848 + chai@6.2.2: {} 1849 + 1344 1850 chalk@4.1.2: 1345 1851 dependencies: 1346 1852 ansi-styles: 4.3.0 ··· 1364 1870 1365 1871 consola@3.4.2: {} 1366 1872 1873 + convert-source-map@2.0.0: {} 1874 + 1367 1875 cross-spawn@7.0.6: 1368 1876 dependencies: 1369 1877 path-key: 3.1.1 ··· 1375 1883 ms: 2.1.3 1376 1884 1377 1885 deep-is@0.1.4: {} 1886 + 1887 + detect-libc@2.1.2: {} 1888 + 1889 + es-module-lexer@2.0.0: {} 1378 1890 1379 1891 esbuild@0.27.3: 1380 1892 optionalDependencies: ··· 1475 1987 1476 1988 estraverse@5.3.0: {} 1477 1989 1990 + estree-walker@3.0.3: 1991 + dependencies: 1992 + '@types/estree': 1.0.8 1993 + 1478 1994 esutils@2.0.3: {} 1995 + 1996 + expect-type@1.3.0: {} 1479 1997 1480 1998 fast-deep-equal@3.1.3: {} 1481 1999 ··· 1560 2078 prelude-ls: 1.2.1 1561 2079 type-check: 0.4.0 1562 2080 2081 + lightningcss-android-arm64@1.32.0: 2082 + optional: true 2083 + 2084 + lightningcss-darwin-arm64@1.32.0: 2085 + optional: true 2086 + 2087 + lightningcss-darwin-x64@1.32.0: 2088 + optional: true 2089 + 2090 + lightningcss-freebsd-x64@1.32.0: 2091 + optional: true 2092 + 2093 + lightningcss-linux-arm-gnueabihf@1.32.0: 2094 + optional: true 2095 + 2096 + lightningcss-linux-arm64-gnu@1.32.0: 2097 + optional: true 2098 + 2099 + lightningcss-linux-arm64-musl@1.32.0: 2100 + optional: true 2101 + 2102 + lightningcss-linux-x64-gnu@1.32.0: 2103 + optional: true 2104 + 2105 + lightningcss-linux-x64-musl@1.32.0: 2106 + optional: true 2107 + 2108 + lightningcss-win32-arm64-msvc@1.32.0: 2109 + optional: true 2110 + 2111 + lightningcss-win32-x64-msvc@1.32.0: 2112 + optional: true 2113 + 2114 + lightningcss@1.32.0: 2115 + dependencies: 2116 + detect-libc: 2.1.2 2117 + optionalDependencies: 2118 + lightningcss-android-arm64: 1.32.0 2119 + lightningcss-darwin-arm64: 1.32.0 2120 + lightningcss-darwin-x64: 1.32.0 2121 + lightningcss-freebsd-x64: 1.32.0 2122 + lightningcss-linux-arm-gnueabihf: 1.32.0 2123 + lightningcss-linux-arm64-gnu: 1.32.0 2124 + lightningcss-linux-arm64-musl: 1.32.0 2125 + lightningcss-linux-x64-gnu: 1.32.0 2126 + lightningcss-linux-x64-musl: 1.32.0 2127 + lightningcss-win32-arm64-msvc: 1.32.0 2128 + lightningcss-win32-x64-msvc: 1.32.0 2129 + 1563 2130 lilconfig@3.1.3: {} 1564 2131 1565 2132 lines-and-columns@1.2.4: {} ··· 1599 2166 object-assign: 4.1.1 1600 2167 thenify-all: 1.6.0 1601 2168 2169 + nanoid@3.3.11: {} 2170 + 1602 2171 natural-compare@1.4.0: {} 1603 2172 1604 2173 object-assign@4.1.1: {} 1605 2174 2175 + obug@2.1.1: {} 2176 + 1606 2177 optionator@0.9.4: 1607 2178 dependencies: 1608 2179 deep-is: 0.1.4 ··· 1642 2213 mlly: 1.8.0 1643 2214 pathe: 2.0.3 1644 2215 1645 - postcss-load-config@6.0.1: 2216 + postcss-load-config@6.0.1(postcss@8.5.8): 1646 2217 dependencies: 1647 2218 lilconfig: 3.1.3 2219 + optionalDependencies: 2220 + postcss: 8.5.8 2221 + 2222 + postcss@8.5.8: 2223 + dependencies: 2224 + nanoid: 3.3.11 2225 + picocolors: 1.1.1 2226 + source-map-js: 1.2.1 1648 2227 1649 2228 prelude-ls@1.2.1: {} 1650 2229 ··· 1658 2237 1659 2238 resolve-from@5.0.0: {} 1660 2239 2240 + rolldown@1.0.0-rc.10: 2241 + dependencies: 2242 + '@oxc-project/types': 0.120.0 2243 + '@rolldown/pluginutils': 1.0.0-rc.10 2244 + optionalDependencies: 2245 + '@rolldown/binding-android-arm64': 1.0.0-rc.10 2246 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.10 2247 + '@rolldown/binding-darwin-x64': 1.0.0-rc.10 2248 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.10 2249 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.10 2250 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.10 2251 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.10 2252 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.10 2253 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.10 2254 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.10 2255 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.10 2256 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.10 2257 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.10 2258 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.10 2259 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.10 2260 + 1661 2261 rollup@4.57.1: 1662 2262 dependencies: 1663 2263 '@types/estree': 1.0.8 ··· 1696 2296 shebang-regex: 3.0.0 1697 2297 1698 2298 shebang-regex@3.0.0: {} 2299 + 2300 + siginfo@2.0.0: {} 2301 + 2302 + source-map-js@1.2.1: {} 1699 2303 1700 2304 source-map@0.7.6: {} 1701 2305 2306 + stackback@0.0.2: {} 2307 + 2308 + std-env@4.0.0: {} 2309 + 1702 2310 strip-json-comments@3.1.1: {} 1703 2311 1704 2312 sucrase@3.35.1: ··· 1722 2330 thenify@3.3.1: 1723 2331 dependencies: 1724 2332 any-promise: 1.3.0 2333 + 2334 + tinybench@2.9.0: {} 1725 2335 1726 2336 tinyexec@0.3.2: {} 1727 2337 2338 + tinyexec@1.0.4: {} 2339 + 1728 2340 tinyglobby@0.2.15: 1729 2341 dependencies: 1730 2342 fdir: 6.5.0(picomatch@4.0.3) 1731 2343 picomatch: 4.0.3 2344 + 2345 + tinyrainbow@3.1.0: {} 1732 2346 1733 2347 tree-kill@1.2.2: {} 1734 2348 ··· 1738 2352 1739 2353 ts-interface-checker@0.1.13: {} 1740 2354 1741 - tsup@8.5.1(typescript@5.9.3): 2355 + tslib@2.8.1: 2356 + optional: true 2357 + 2358 + tsup@8.5.1(postcss@8.5.8)(typescript@5.9.3): 1742 2359 dependencies: 1743 2360 bundle-require: 5.1.0(esbuild@0.27.3) 1744 2361 cac: 6.7.14 ··· 1749 2366 fix-dts-default-cjs-exports: 1.0.1 1750 2367 joycon: 3.1.1 1751 2368 picocolors: 1.1.1 1752 - postcss-load-config: 6.0.1 2369 + postcss-load-config: 6.0.1(postcss@8.5.8) 1753 2370 resolve-from: 5.0.0 1754 2371 rollup: 4.57.1 1755 2372 source-map: 0.7.6 ··· 1758 2375 tinyglobby: 0.2.15 1759 2376 tree-kill: 1.2.2 1760 2377 optionalDependencies: 2378 + postcss: 8.5.8 1761 2379 typescript: 5.9.3 1762 2380 transitivePeerDependencies: 1763 2381 - jiti ··· 1779 2397 dependencies: 1780 2398 punycode: 2.3.1 1781 2399 2400 + vite@8.0.1(@types/node@22.19.11)(esbuild@0.27.3): 2401 + dependencies: 2402 + lightningcss: 1.32.0 2403 + picomatch: 4.0.3 2404 + postcss: 8.5.8 2405 + rolldown: 1.0.0-rc.10 2406 + tinyglobby: 0.2.15 2407 + optionalDependencies: 2408 + '@types/node': 22.19.11 2409 + esbuild: 0.27.3 2410 + fsevents: 2.3.3 2411 + 2412 + vitest@4.1.0(@types/node@22.19.11)(vite@8.0.1(@types/node@22.19.11)(esbuild@0.27.3)): 2413 + dependencies: 2414 + '@vitest/expect': 4.1.0 2415 + '@vitest/mocker': 4.1.0(vite@8.0.1(@types/node@22.19.11)(esbuild@0.27.3)) 2416 + '@vitest/pretty-format': 4.1.0 2417 + '@vitest/runner': 4.1.0 2418 + '@vitest/snapshot': 4.1.0 2419 + '@vitest/spy': 4.1.0 2420 + '@vitest/utils': 4.1.0 2421 + es-module-lexer: 2.0.0 2422 + expect-type: 1.3.0 2423 + magic-string: 0.30.21 2424 + obug: 2.1.1 2425 + pathe: 2.0.3 2426 + picomatch: 4.0.3 2427 + std-env: 4.0.0 2428 + tinybench: 2.9.0 2429 + tinyexec: 1.0.4 2430 + tinyglobby: 0.2.15 2431 + tinyrainbow: 3.1.0 2432 + vite: 8.0.1(@types/node@22.19.11)(esbuild@0.27.3) 2433 + why-is-node-running: 2.3.0 2434 + optionalDependencies: 2435 + '@types/node': 22.19.11 2436 + transitivePeerDependencies: 2437 + - msw 2438 + 1782 2439 which@2.0.2: 1783 2440 dependencies: 1784 2441 isexe: 2.0.0 2442 + 2443 + why-is-node-running@2.3.0: 2444 + dependencies: 2445 + siginfo: 2.0.0 2446 + stackback: 0.0.2 1785 2447 1786 2448 word-wrap@1.2.5: {} 1787 2449
+13 -24
src/WebSocketClient.ts
··· 1 - import { EventEmitter } from "events"; 2 - import WebSocket from "ws"; 3 1 import { z } from "zod"; 2 + import { Emitter } from "./platform/Emitter"; 3 + import { getPlatformAdapter } from "./platform/index"; 4 + import type { PlatformAdapter } from "./platform/types"; 4 5 import { WebSocketConnection } from "./connection/WebSocketConnection"; 5 6 import { WebSocketRouter } from "./router/WebSocketRouter"; 6 7 import { Logger } from "./logger/Logger"; ··· 41 42 * client.connect(); 42 43 * ``` 43 44 */ 44 - export class WebSocketClient extends EventEmitter { 45 + export class WebSocketClient extends Emitter { 45 46 private connection: WebSocketConnection; 46 47 private router: WebSocketRouter; 47 48 private logger: Logger; 49 + private adapter: PlatformAdapter; 48 50 49 51 constructor(options: WebSocketClientOptions) { 50 52 super(); 53 + this.adapter = getPlatformAdapter(); 51 54 this.logger = new Logger(options.logger); 52 55 this.connection = new WebSocketConnection(options, this.logger); 53 56 this.router = new WebSocketRouter(this.logger); ··· 99 102 /** 100 103 * Sends data through the WebSocket. Objects are JSON-serialized automatically. 101 104 * 102 - * @param data - Data to send. Objects/arrays are JSON.stringified; strings and Buffers are sent as-is. 105 + * @param data - Data to send. Objects/arrays are JSON.stringified; strings and binary data are sent as-is. 103 106 * @returns `true` if sent, `false` if the connection is not open. 104 107 */ 105 108 send(data: unknown): boolean { 106 109 const serialized = 107 - typeof data === "string" || Buffer.isBuffer(data) ? data : JSON.stringify(data); 108 - return this.connection.send(serialized); 110 + typeof data === "string" || data instanceof Uint8Array || data instanceof ArrayBuffer 111 + ? data 112 + : JSON.stringify(data); 113 + return this.connection.send(serialized as string | ArrayBuffer | Uint8Array); 109 114 } 110 115 111 116 /** ··· 148 153 this.router.on("error", (handlerError: HandlerError) => this.emit("error", handlerError)); 149 154 150 155 // Connection messages → router 151 - this.connection.on("message", (data: WebSocket.Data) => { 152 - const raw = this.toRawString(data); 156 + this.connection.on("message", (data: unknown) => { 157 + const raw = this.adapter.dataToString(data); 153 158 if (raw !== null) { 154 159 const sendFn = (payload: unknown): boolean => this.send(payload); 155 160 const info = this.connection.getConnectionInfo(); ··· 159 164 }); 160 165 } 161 166 }); 162 - } 163 - 164 - private toRawString(data: WebSocket.Data): string | null { 165 - if (typeof data === "string") { 166 - return data; 167 - } 168 - if (Buffer.isBuffer(data)) { 169 - return data.toString("utf-8"); 170 - } 171 - if (data instanceof ArrayBuffer) { 172 - return Buffer.from(data).toString("utf-8"); 173 - } 174 - if (Array.isArray(data)) { 175 - return Buffer.concat(data).toString("utf-8"); 176 - } 177 - return null; 178 167 } 179 168 }
+48 -28
src/connection/WebSocketConnection.ts
··· 1 - import { EventEmitter } from "events"; 2 - import WebSocket from "ws"; 1 + import { Emitter } from "../platform/Emitter"; 2 + import { getPlatformAdapter, WS_READY_STATE } from "../platform/index"; 3 + import type { PlatformAdapter, UniversalWebSocket } from "../platform/types"; 3 4 import { ConnectionOptions, ConnectionInfo, ConnectionState, ReconnectOptions } from "./types"; 4 5 import { Logger } from "../logger/Logger"; 5 6 ··· 12 13 * - `"open"` — connection established 13 14 * - `"close"` — connection closed (emits `{ code: number, reason: string }`) 14 15 * - `"error"` — connection error (emits the `Error` object) 15 - * - `"message"` — message received (emits the raw `WebSocket.Data`) 16 + * - `"message"` — message received (emits the raw message data) 16 17 * - `"reconnecting"` — about to attempt reconnection (emits `{ attempt, maxAttempts, delay, service }`) 17 18 * - `"serviceSwitched"` — failed over to a different service URL (emits `{ from, to, cycle }`) 18 19 */ 19 - export class WebSocketConnection extends EventEmitter { 20 + export class WebSocketConnection extends Emitter { 20 21 private services: string[]; 21 22 private queryParams: Record<string, string | number | boolean>; 22 23 private reconnectConfig: Required<ReconnectOptions>; 23 24 private pingInterval: number; 24 25 25 - private ws: WebSocket | null = null; 26 - private pingTimer: NodeJS.Timeout | null = null; 27 - private reconnectTimer: NodeJS.Timeout | null = null; 26 + private adapter: PlatformAdapter; 27 + private ws: UniversalWebSocket | null = null; 28 + private pingTimer: ReturnType<typeof setInterval> | null = null; 29 + private reconnectTimer: ReturnType<typeof setTimeout> | null = null; 28 30 29 31 private serviceIndex = 0; 30 32 private reconnectAttempts = 0; ··· 38 40 39 41 constructor(options: ConnectionOptions, logger: Logger) { 40 42 super(); 43 + this.adapter = getPlatformAdapter(); 41 44 this.services = Array.isArray(options.service) ? options.service : [options.service]; 42 45 this.queryParams = { ...options.queryParams }; 43 46 this.pingInterval = options.pingInterval ?? 10000; ··· 86 89 * Sends raw data through the WebSocket. 87 90 * @returns `true` if the data was sent, `false` if the connection is not open. 88 91 */ 89 - send(data: string | Buffer): boolean { 90 - if (this.ws && this.ws.readyState === WebSocket.OPEN) { 92 + send(data: string | ArrayBuffer | Uint8Array): boolean { 93 + if (this.ws && this.ws.readyState === WS_READY_STATE.OPEN) { 91 94 try { 92 95 this.ws.send(data); 93 96 return true; ··· 138 141 private getState(): ConnectionState { 139 142 if (!this.ws) return "closed"; 140 143 switch (this.ws.readyState) { 141 - case WebSocket.CONNECTING: 144 + case WS_READY_STATE.CONNECTING: 142 145 return "connecting"; 143 - case WebSocket.OPEN: 146 + case WS_READY_STATE.OPEN: 144 147 return "connected"; 145 - case WebSocket.CLOSING: 148 + case WS_READY_STATE.CLOSING: 146 149 return "closing"; 147 150 default: 148 151 return "closed"; ··· 175 178 176 179 try { 177 180 this.logger.info("Connecting to WebSocket", { service: serviceUrl }); 178 - this.ws = new WebSocket(serviceUrl); 181 + const ws = this.adapter.createWebSocket(serviceUrl); 182 + this.ws = ws; 179 183 180 - this.ws.on("open", () => { 184 + ws.onopen = () => { 181 185 this.isConnecting = false; 182 186 this.reconnectAttempts = 0; 183 187 this.serviceCycles = 0; 184 188 this.startHeartbeat(); 185 189 this.logger.info("WebSocket connected", { service: serviceUrl }); 186 190 this.emit("open"); 187 - }); 191 + }; 188 192 189 - this.ws.on("message", (data: WebSocket.Data) => { 193 + ws.onmessage = (event: unknown) => { 190 194 this.messageCount++; 191 195 this.lastMessageTime = Date.now(); 196 + // Node ws passes the data directly; browser wraps in MessageEvent 197 + const data = event && typeof event === "object" && "data" in event 198 + ? (event as { data: unknown }).data 199 + : event; 192 200 this.emit("message", data); 193 - }); 201 + }; 194 202 195 - this.ws.on("error", (error: Error) => { 203 + ws.onerror = (event: unknown) => { 204 + const error = event instanceof Error 205 + ? event 206 + : new Error("WebSocket error"); 196 207 this.logger.error("WebSocket error", error); 197 208 this.isConnecting = false; 198 209 this.emit("error", error); 199 - }); 210 + }; 200 211 201 - this.ws.on("close", (code: number, reason: Buffer) => { 202 - const reasonStr = reason.toString(); 203 - this.logger.info("WebSocket disconnected", { code, reason: reasonStr }); 212 + ws.onclose = (event: unknown) => { 213 + const code = event && typeof event === "object" && "code" in event 214 + ? (event as { code: number }).code 215 + : 1006; 216 + const reason = event && typeof event === "object" && "reason" in event 217 + ? String((event as { reason: unknown }).reason) 218 + : ""; 219 + this.logger.info("WebSocket disconnected", { code, reason }); 204 220 this.isConnecting = false; 205 221 this.stopHeartbeat(); 206 - this.emit("close", { code, reason: reasonStr }); 222 + this.emit("close", { code, reason }); 207 223 208 224 if (this.shouldReconnect) { 209 225 this.scheduleReconnect(); 210 226 } 211 - }); 227 + }; 212 228 } catch (error) { 213 229 this.logger.error("Error creating WebSocket", error); 214 230 this.isConnecting = false; ··· 286 302 287 303 private cleanup(): void { 288 304 if (this.ws) { 289 - this.ws.removeAllListeners(); 290 - if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) { 305 + this.adapter.removeAllListeners(this.ws); 306 + if ( 307 + this.ws.readyState === WS_READY_STATE.OPEN || 308 + this.ws.readyState === WS_READY_STATE.CONNECTING 309 + ) { 291 310 try { 292 311 this.ws.close(); 293 312 } catch { ··· 300 319 } 301 320 302 321 private startHeartbeat(): void { 322 + if (!this.adapter.supportsPing) return; 303 323 this.stopHeartbeat(); 304 324 this.pingTimer = setInterval(() => { 305 - if (this.ws && this.ws.readyState === WebSocket.OPEN) { 325 + if (this.ws && this.ws.readyState === WS_READY_STATE.OPEN) { 306 326 try { 307 - this.ws.ping(); 327 + this.adapter.ping(this.ws); 308 328 } catch (error) { 309 329 this.logger.error("Error sending ping", error); 310 330 }
+5
src/index.ts
··· 17 17 HandlerRegistration, 18 18 HandlerError, 19 19 } from "./router/types"; 20 + 21 + export { Emitter } from "./platform/Emitter"; 22 + export { getPlatformAdapter } from "./platform/index"; 23 + export type { PlatformAdapter, UniversalWebSocket } from "./platform/types"; 24 + export { WS_READY_STATE } from "./platform/types";
+43
src/platform/Emitter.ts
··· 1 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 2 + type Listener = (...args: any[]) => void; 3 + 4 + /** 5 + * Minimal cross-platform event emitter. 6 + * Replaces Node's `EventEmitter` so the library works in browsers. 7 + */ 8 + export class Emitter { 9 + private listeners = new Map<string, Set<Listener>>(); 10 + 11 + on(event: string, fn: Listener): this { 12 + let set = this.listeners.get(event); 13 + if (!set) { 14 + set = new Set(); 15 + this.listeners.set(event, set); 16 + } 17 + set.add(fn); 18 + return this; 19 + } 20 + 21 + off(event: string, fn: Listener): this { 22 + this.listeners.get(event)?.delete(fn); 23 + return this; 24 + } 25 + 26 + emit(event: string, ...args: unknown[]): boolean { 27 + const set = this.listeners.get(event); 28 + if (!set || set.size === 0) return false; 29 + for (const fn of set) { 30 + fn(...args); 31 + } 32 + return true; 33 + } 34 + 35 + removeAllListeners(event?: string): this { 36 + if (event) { 37 + this.listeners.delete(event); 38 + } else { 39 + this.listeners.clear(); 40 + } 41 + return this; 42 + } 43 + }
+35
src/platform/browser.ts
··· 1 + import type { PlatformAdapter, UniversalWebSocket } from "./types"; 2 + 3 + const decoder = new TextDecoder(); 4 + 5 + /** 6 + * Browser platform adapter. 7 + * Uses the native `WebSocket` and `TextDecoder` for binary conversion. 8 + */ 9 + export function createBrowserAdapter(): PlatformAdapter { 10 + return { 11 + createWebSocket(url: string): UniversalWebSocket { 12 + return new WebSocket(url); 13 + }, 14 + 15 + dataToString(data: unknown): string | null { 16 + if (typeof data === "string") return data; 17 + if (data instanceof ArrayBuffer) return decoder.decode(data); 18 + if (data instanceof Uint8Array) return decoder.decode(data); 19 + return null; 20 + }, 21 + 22 + supportsPing: false, 23 + 24 + ping(): void { 25 + // Browser engines handle WebSocket keepalive at the protocol level. 26 + }, 27 + 28 + removeAllListeners(ws: UniversalWebSocket): void { 29 + ws.onopen = null; 30 + ws.onclose = null; 31 + ws.onerror = null; 32 + ws.onmessage = null; 33 + }, 34 + }; 35 + }
+27
src/platform/index.ts
··· 1 + import type { PlatformAdapter } from "./types"; 2 + import { createNodeAdapter } from "./node"; 3 + import { createBrowserAdapter } from "./browser"; 4 + 5 + export type { PlatformAdapter, UniversalWebSocket } from "./types"; 6 + export { WS_READY_STATE } from "./types"; 7 + export { Emitter } from "./Emitter"; 8 + 9 + let cached: PlatformAdapter | null = null; 10 + 11 + function isNode(): boolean { 12 + return ( 13 + typeof process !== "undefined" && 14 + typeof process.versions !== "undefined" && 15 + typeof process.versions.node !== "undefined" 16 + ); 17 + } 18 + 19 + /** 20 + * Returns a platform adapter for the current runtime. 21 + * The result is cached — subsequent calls return the same instance. 22 + */ 23 + export function getPlatformAdapter(): PlatformAdapter { 24 + if (cached) return cached; 25 + cached = isNode() ? createNodeAdapter() : createBrowserAdapter(); 26 + return cached; 27 + }
+43
src/platform/node.ts
··· 1 + import type { PlatformAdapter, UniversalWebSocket } from "./types"; 2 + 3 + interface WsWebSocket extends UniversalWebSocket { 4 + ping(): void; 5 + removeAllListeners(): void; 6 + } 7 + 8 + interface WsConstructor { 9 + new (url: string): WsWebSocket; 10 + } 11 + 12 + /** 13 + * Node.js platform adapter. 14 + * Uses the `ws` package for WebSocket and `Buffer` for binary conversion. 15 + */ 16 + export function createNodeAdapter(): PlatformAdapter { 17 + // eslint-disable-next-line @typescript-eslint/no-require-imports 18 + const WS = require("ws") as WsConstructor; 19 + 20 + return { 21 + createWebSocket(url: string): UniversalWebSocket { 22 + return new WS(url); 23 + }, 24 + 25 + dataToString(data: unknown): string | null { 26 + if (typeof data === "string") return data; 27 + if (Buffer.isBuffer(data)) return data.toString("utf-8"); 28 + if (data instanceof ArrayBuffer) return Buffer.from(data).toString("utf-8"); 29 + if (Array.isArray(data)) return Buffer.concat(data).toString("utf-8"); 30 + return null; 31 + }, 32 + 33 + supportsPing: true, 34 + 35 + ping(ws: UniversalWebSocket): void { 36 + (ws as WsWebSocket).ping(); 37 + }, 38 + 39 + removeAllListeners(ws: UniversalWebSocket): void { 40 + (ws as WsWebSocket).removeAllListeners(); 41 + }, 42 + }; 43 + }
+46
src/platform/types.ts
··· 1 + /** 2 + * Minimal WebSocket interface that both Node `ws` and the browser 3 + * native `WebSocket` satisfy. 4 + */ 5 + export interface UniversalWebSocket { 6 + readonly readyState: number; 7 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 8 + onopen: ((event: any) => void) | null; 9 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 10 + onclose: ((event: any) => void) | null; 11 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 + onerror: ((event: any) => void) | null; 13 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 14 + onmessage: ((event: any) => void) | null; 15 + close(code?: number, reason?: string): void; 16 + send(data: string | ArrayBuffer | Uint8Array): void; 17 + } 18 + 19 + /** Ready-state constants shared by both Node ws and browser WebSocket. */ 20 + export const WS_READY_STATE = { 21 + CONNECTING: 0, 22 + OPEN: 1, 23 + CLOSING: 2, 24 + CLOSED: 3, 25 + } as const; 26 + 27 + /** 28 + * Abstracts the three platform-specific concerns: 29 + * WebSocket construction, binary-to-string conversion, and ping support. 30 + */ 31 + export interface PlatformAdapter { 32 + /** Creates a WebSocket connection to the given URL. */ 33 + createWebSocket(url: string): UniversalWebSocket; 34 + 35 + /** Converts incoming message data to a UTF-8 string. */ 36 + dataToString(data: unknown): string | null; 37 + 38 + /** Whether the platform supports sending ping frames. */ 39 + readonly supportsPing: boolean; 40 + 41 + /** Sends a ping frame. No-op when `supportsPing` is false. */ 42 + ping(ws: UniversalWebSocket): void; 43 + 44 + /** Removes all event handlers from a WebSocket instance. */ 45 + removeAllListeners(ws: UniversalWebSocket): void; 46 + }
+3 -2
src/router/WebSocketRouter.ts
··· 1 - import { EventEmitter } from "events"; 2 1 import { z } from "zod"; 2 + import { Emitter } from "../platform/Emitter"; 3 3 import { ConnectionInfo } from "../connection/types"; 4 4 import { HandlerRegistration, HandlerError, MessageHandler } from "./types"; 5 5 import { Logger } from "../logger/Logger"; ··· 15 15 * Events emitted: 16 16 * - `"error"` — a handler threw or JSON parsing failed (emits {@link HandlerError}) 17 17 */ 18 - export class WebSocketRouter extends EventEmitter { 18 + export class WebSocketRouter extends Emitter { 19 19 private handlers: HandlerRegistration[] = []; 20 20 private logger: Logger; 21 21 ··· 23 23 super(); 24 24 this.logger = logger; 25 25 } 26 + 26 27 27 28 /** 28 29 * Registers a handler that will be invoked for every message matching the given schema.
+225
tests/client.test.ts
··· 1 + import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; 2 + import { z } from "zod"; 3 + import type { PlatformAdapter, UniversalWebSocket } from "../src/platform/types"; 4 + import { WS_READY_STATE } from "../src/platform/types"; 5 + 6 + interface MockWs extends UniversalWebSocket { 7 + url: string; 8 + } 9 + 10 + const mockWsInstances: MockWs[] = []; 11 + 12 + function createMockWs(url: string): MockWs { 13 + const ws: MockWs = { 14 + readyState: WS_READY_STATE.CONNECTING, 15 + onopen: null, 16 + onclose: null, 17 + onerror: null, 18 + onmessage: null, 19 + close: vi.fn(), 20 + send: vi.fn(), 21 + url, 22 + }; 23 + mockWsInstances.push(ws); 24 + return ws; 25 + } 26 + 27 + const mockAdapter: PlatformAdapter = { 28 + createWebSocket: (url: string) => createMockWs(url), 29 + dataToString: (data: unknown) => { 30 + if (typeof data === "string") return data; 31 + if (data instanceof ArrayBuffer) return new TextDecoder().decode(data); 32 + if (data instanceof Uint8Array) return new TextDecoder().decode(data); 33 + if (Buffer.isBuffer(data)) return data.toString("utf-8"); 34 + return null; 35 + }, 36 + supportsPing: false, 37 + ping: vi.fn(), 38 + removeAllListeners: (ws: UniversalWebSocket) => { 39 + ws.onopen = null; 40 + ws.onclose = null; 41 + ws.onerror = null; 42 + ws.onmessage = null; 43 + }, 44 + }; 45 + 46 + vi.mock("../src/platform/index", async (importOriginal) => { 47 + const actual = await importOriginal<typeof import("../src/platform/index")>(); 48 + return { 49 + ...actual, 50 + getPlatformAdapter: () => mockAdapter, 51 + }; 52 + }); 53 + 54 + import { WebSocketClient } from "../src/WebSocketClient"; 55 + 56 + describe("WebSocketClient", () => { 57 + beforeEach(() => { 58 + mockWsInstances.length = 0; 59 + }); 60 + 61 + afterEach(() => { 62 + vi.restoreAllMocks(); 63 + }); 64 + 65 + function createClient( 66 + options?: Partial<ConstructorParameters<typeof WebSocketClient>[0]> 67 + ): WebSocketClient { 68 + return new WebSocketClient({ 69 + service: "wss://test.example.com", 70 + logger: { enabled: false }, 71 + ...options, 72 + }); 73 + } 74 + 75 + function lastWs(): MockWs { 76 + return mockWsInstances[mockWsInstances.length - 1]; 77 + } 78 + 79 + it("connects and emits open event", () => { 80 + const client = createClient(); 81 + const onOpen = vi.fn(); 82 + client.on("open", onOpen); 83 + 84 + client.connect(); 85 + const ws = lastWs(); 86 + ws.readyState = WS_READY_STATE.OPEN; 87 + ws.onopen!({}); 88 + 89 + expect(onOpen).toHaveBeenCalled(); 90 + }); 91 + 92 + it("emits close event with code and reason", () => { 93 + const client = createClient(); 94 + const onClose = vi.fn(); 95 + client.on("close", onClose); 96 + 97 + client.connect(); 98 + const ws = lastWs(); 99 + ws.readyState = WS_READY_STATE.OPEN; 100 + ws.onopen!({}); 101 + ws.readyState = WS_READY_STATE.CLOSED; 102 + ws.onclose!({ code: 1000, reason: "normal" }); 103 + 104 + expect(onClose).toHaveBeenCalledWith({ code: 1000, reason: "normal" }); 105 + }); 106 + 107 + it("emits error event on connection error", () => { 108 + const client = createClient(); 109 + const onError = vi.fn(); 110 + client.on("error", onError); 111 + 112 + client.connect(); 113 + const ws = lastWs(); 114 + ws.onerror!(new Error("connection failed")); 115 + 116 + expect(onError).toHaveBeenCalledWith(expect.any(Error)); 117 + }); 118 + 119 + it("routes messages through handlers", async () => { 120 + const client = createClient(); 121 + const schema = z.object({ type: z.literal("trade"), price: z.number() }); 122 + const handler = vi.fn(); 123 + 124 + client.handle(schema, handler); 125 + client.connect(); 126 + 127 + const ws = lastWs(); 128 + ws.readyState = WS_READY_STATE.OPEN; 129 + ws.onopen!({}); 130 + 131 + // Simulate incoming message — adapter extracts data from MessageEvent-like object 132 + ws.onmessage!({ data: JSON.stringify({ type: "trade", price: 42.5 }) }); 133 + 134 + await vi.waitFor(() => { 135 + expect(handler).toHaveBeenCalledWith( 136 + expect.objectContaining({ 137 + data: { type: "trade", price: 42.5 }, 138 + }) 139 + ); 140 + }); 141 + }); 142 + 143 + it("send() serializes objects as JSON", () => { 144 + const client = createClient(); 145 + client.connect(); 146 + 147 + const ws = lastWs(); 148 + ws.readyState = WS_READY_STATE.OPEN; 149 + ws.onopen!({}); 150 + 151 + const result = client.send({ action: "subscribe", channel: "trades" }); 152 + 153 + expect(result).toBe(true); 154 + expect(ws.send).toHaveBeenCalledWith( 155 + JSON.stringify({ action: "subscribe", channel: "trades" }) 156 + ); 157 + }); 158 + 159 + it("send() passes strings through unchanged", () => { 160 + const client = createClient(); 161 + client.connect(); 162 + 163 + const ws = lastWs(); 164 + ws.readyState = WS_READY_STATE.OPEN; 165 + ws.onopen!({}); 166 + 167 + client.send("raw string"); 168 + expect(ws.send).toHaveBeenCalledWith("raw string"); 169 + }); 170 + 171 + it("send() returns false when not connected", () => { 172 + const client = createClient(); 173 + const result = client.send("data"); 174 + expect(result).toBe(false); 175 + }); 176 + 177 + it("close() stops the connection", () => { 178 + const client = createClient(); 179 + client.connect(); 180 + 181 + const ws = lastWs(); 182 + ws.readyState = WS_READY_STATE.OPEN; 183 + ws.onopen!({}); 184 + 185 + client.close(); 186 + expect(ws.close).toHaveBeenCalled(); 187 + }); 188 + 189 + it("getConnectionInfo() returns current state", () => { 190 + const client = createClient(); 191 + const info = client.getConnectionInfo(); 192 + expect(info.state).toBe("closed"); 193 + expect(info.currentService).toBe("wss://test.example.com"); 194 + }); 195 + 196 + it("handle() returns this for chaining", () => { 197 + const client = createClient(); 198 + const schema = z.object({ type: z.string() }); 199 + const result = client.handle(schema, () => {}); 200 + expect(result).toBe(client); 201 + }); 202 + 203 + it("emits router errors as error events", async () => { 204 + const client = createClient(); 205 + const onError = vi.fn(); 206 + client.on("error", onError); 207 + 208 + client.connect(); 209 + const ws = lastWs(); 210 + ws.readyState = WS_READY_STATE.OPEN; 211 + ws.onopen!({}); 212 + 213 + // Send invalid JSON to trigger router error 214 + ws.onmessage!({ data: "not json!" }); 215 + 216 + await vi.waitFor(() => { 217 + expect(onError).toHaveBeenCalledWith( 218 + expect.objectContaining({ 219 + error: expect.any(Error), 220 + rawData: "not json!", 221 + }) 222 + ); 223 + }); 224 + }); 225 + });
+85
tests/emitter.test.ts
··· 1 + import { describe, it, expect, vi } from "vitest"; 2 + import { Emitter } from "../src/platform/Emitter"; 3 + 4 + describe("Emitter", () => { 5 + it("calls listeners when event is emitted", () => { 6 + const emitter = new Emitter(); 7 + const fn = vi.fn(); 8 + emitter.on("test", fn); 9 + emitter.emit("test", "a", 2); 10 + expect(fn).toHaveBeenCalledWith("a", 2); 11 + }); 12 + 13 + it("returns false when no listeners exist", () => { 14 + const emitter = new Emitter(); 15 + expect(emitter.emit("nope")).toBe(false); 16 + }); 17 + 18 + it("returns true when listeners exist", () => { 19 + const emitter = new Emitter(); 20 + emitter.on("x", () => {}); 21 + expect(emitter.emit("x")).toBe(true); 22 + }); 23 + 24 + it("supports multiple listeners on the same event", () => { 25 + const emitter = new Emitter(); 26 + const fn1 = vi.fn(); 27 + const fn2 = vi.fn(); 28 + emitter.on("e", fn1); 29 + emitter.on("e", fn2); 30 + emitter.emit("e", "data"); 31 + expect(fn1).toHaveBeenCalledWith("data"); 32 + expect(fn2).toHaveBeenCalledWith("data"); 33 + }); 34 + 35 + it("removes a specific listener with off()", () => { 36 + const emitter = new Emitter(); 37 + const fn = vi.fn(); 38 + emitter.on("e", fn); 39 + emitter.off("e", fn); 40 + emitter.emit("e"); 41 + expect(fn).not.toHaveBeenCalled(); 42 + }); 43 + 44 + it("removeAllListeners() with event name clears only that event", () => { 45 + const emitter = new Emitter(); 46 + const fn1 = vi.fn(); 47 + const fn2 = vi.fn(); 48 + emitter.on("a", fn1); 49 + emitter.on("b", fn2); 50 + emitter.removeAllListeners("a"); 51 + emitter.emit("a"); 52 + emitter.emit("b"); 53 + expect(fn1).not.toHaveBeenCalled(); 54 + expect(fn2).toHaveBeenCalled(); 55 + }); 56 + 57 + it("removeAllListeners() without args clears all events", () => { 58 + const emitter = new Emitter(); 59 + const fn1 = vi.fn(); 60 + const fn2 = vi.fn(); 61 + emitter.on("a", fn1); 62 + emitter.on("b", fn2); 63 + emitter.removeAllListeners(); 64 + emitter.emit("a"); 65 + emitter.emit("b"); 66 + expect(fn1).not.toHaveBeenCalled(); 67 + expect(fn2).not.toHaveBeenCalled(); 68 + }); 69 + 70 + it("on() returns this for chaining", () => { 71 + const emitter = new Emitter(); 72 + const result = emitter.on("e", () => {}); 73 + expect(result).toBe(emitter); 74 + }); 75 + 76 + it("does not add duplicate references of the same function", () => { 77 + const emitter = new Emitter(); 78 + const fn = vi.fn(); 79 + emitter.on("e", fn); 80 + emitter.on("e", fn); 81 + emitter.emit("e"); 82 + // Set prevents duplicates 83 + expect(fn).toHaveBeenCalledTimes(1); 84 + }); 85 + });
+81
tests/logger.test.ts
··· 1 + import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; 2 + import { Logger, LogLevel } from "../src/logger/Logger"; 3 + 4 + describe("Logger", () => { 5 + const originalConsole = { ...console }; 6 + 7 + beforeEach(() => { 8 + console.log = vi.fn(); 9 + console.debug = vi.fn(); 10 + console.warn = vi.fn(); 11 + console.error = vi.fn(); 12 + }); 13 + 14 + afterEach(() => { 15 + console.log = originalConsole.log; 16 + console.debug = originalConsole.debug; 17 + console.warn = originalConsole.warn; 18 + console.error = originalConsole.error; 19 + }); 20 + 21 + it("logs info by default", () => { 22 + const logger = new Logger(); 23 + logger.info("test message"); 24 + expect(console.log).toHaveBeenCalledTimes(1); 25 + }); 26 + 27 + it("suppresses debug when level is INFO", () => { 28 + const logger = new Logger({ level: LogLevel.INFO }); 29 + logger.debug("hidden"); 30 + expect(console.debug).not.toHaveBeenCalled(); 31 + }); 32 + 33 + it("logs debug when level is DEBUG", () => { 34 + const logger = new Logger({ level: LogLevel.DEBUG }); 35 + logger.debug("visible"); 36 + expect(console.debug).toHaveBeenCalledTimes(1); 37 + }); 38 + 39 + it("logs nothing when disabled", () => { 40 + const logger = new Logger({ enabled: false }); 41 + logger.info("nope"); 42 + logger.error("nope"); 43 + expect(console.log).not.toHaveBeenCalled(); 44 + expect(console.error).not.toHaveBeenCalled(); 45 + }); 46 + 47 + it("uses console.error for ERROR level", () => { 48 + const logger = new Logger({ level: LogLevel.DEBUG }); 49 + logger.error("fail"); 50 + expect(console.error).toHaveBeenCalledTimes(1); 51 + }); 52 + 53 + it("uses console.warn for WARN level", () => { 54 + const logger = new Logger({ level: LogLevel.DEBUG }); 55 + logger.warn("warning"); 56 + expect(console.warn).toHaveBeenCalledTimes(1); 57 + }); 58 + 59 + it("delegates to custom logger when provided", () => { 60 + const custom = { 61 + debug: vi.fn(), 62 + info: vi.fn(), 63 + warn: vi.fn(), 64 + error: vi.fn(), 65 + }; 66 + const logger = new Logger({ custom }); 67 + logger.info("test", { key: "value" }); 68 + expect(custom.info).toHaveBeenCalledWith("test", { key: "value" }); 69 + expect(console.log).not.toHaveBeenCalled(); 70 + }); 71 + 72 + it("includes context when provided", () => { 73 + const logger = new Logger({ level: LogLevel.DEBUG }); 74 + logger.debug("msg", { foo: 1 }); 75 + expect(console.debug).toHaveBeenCalledWith( 76 + expect.stringContaining("[WAH DEBUG]"), 77 + "msg", 78 + { foo: 1 } 79 + ); 80 + }); 81 + });
+154
tests/platform.test.ts
··· 1 + import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; 2 + import { createNodeAdapter } from "../src/platform/node"; 3 + import { createBrowserAdapter } from "../src/platform/browser"; 4 + 5 + describe("Node adapter", () => { 6 + it("has supportsPing = true", () => { 7 + const adapter = createNodeAdapter(); 8 + expect(adapter.supportsPing).toBe(true); 9 + }); 10 + 11 + it("dataToString handles string input", () => { 12 + const adapter = createNodeAdapter(); 13 + expect(adapter.dataToString("hello")).toBe("hello"); 14 + }); 15 + 16 + it("dataToString handles Buffer input", () => { 17 + const adapter = createNodeAdapter(); 18 + const buf = Buffer.from("test", "utf-8"); 19 + expect(adapter.dataToString(buf)).toBe("test"); 20 + }); 21 + 22 + it("dataToString handles ArrayBuffer input", () => { 23 + const adapter = createNodeAdapter(); 24 + const ab = new TextEncoder().encode("ab").buffer; 25 + expect(adapter.dataToString(ab)).toBe("ab"); 26 + }); 27 + 28 + it("dataToString handles Buffer[] input", () => { 29 + const adapter = createNodeAdapter(); 30 + const bufs = [Buffer.from("a"), Buffer.from("b")]; 31 + expect(adapter.dataToString(bufs)).toBe("ab"); 32 + }); 33 + 34 + it("dataToString returns null for unsupported types", () => { 35 + const adapter = createNodeAdapter(); 36 + expect(adapter.dataToString(123)).toBeNull(); 37 + }); 38 + 39 + it("ping calls ws.ping()", () => { 40 + const adapter = createNodeAdapter(); 41 + const mockWs = { ping: vi.fn() } as unknown as Parameters<typeof adapter.ping>[0]; 42 + adapter.ping(mockWs); 43 + expect((mockWs as unknown as { ping: ReturnType<typeof vi.fn> }).ping).toHaveBeenCalled(); 44 + }); 45 + 46 + it("removeAllListeners calls ws.removeAllListeners()", () => { 47 + const adapter = createNodeAdapter(); 48 + const mockWs = { removeAllListeners: vi.fn() } as unknown as Parameters< 49 + typeof adapter.removeAllListeners 50 + >[0]; 51 + adapter.removeAllListeners(mockWs); 52 + expect( 53 + (mockWs as unknown as { removeAllListeners: ReturnType<typeof vi.fn> }).removeAllListeners 54 + ).toHaveBeenCalled(); 55 + }); 56 + }); 57 + 58 + describe("Browser adapter", () => { 59 + it("has supportsPing = false", () => { 60 + const adapter = createBrowserAdapter(); 61 + expect(adapter.supportsPing).toBe(false); 62 + }); 63 + 64 + it("dataToString handles string input", () => { 65 + const adapter = createBrowserAdapter(); 66 + expect(adapter.dataToString("hello")).toBe("hello"); 67 + }); 68 + 69 + it("dataToString handles ArrayBuffer input", () => { 70 + const adapter = createBrowserAdapter(); 71 + const ab = new TextEncoder().encode("test").buffer; 72 + expect(adapter.dataToString(ab)).toBe("test"); 73 + }); 74 + 75 + it("dataToString handles Uint8Array input", () => { 76 + const adapter = createBrowserAdapter(); 77 + const u8 = new TextEncoder().encode("bytes"); 78 + expect(adapter.dataToString(u8)).toBe("bytes"); 79 + }); 80 + 81 + it("dataToString returns null for unsupported types", () => { 82 + const adapter = createBrowserAdapter(); 83 + expect(adapter.dataToString(42)).toBeNull(); 84 + }); 85 + 86 + it("ping is a no-op", () => { 87 + const adapter = createBrowserAdapter(); 88 + // Should not throw 89 + adapter.ping({} as Parameters<typeof adapter.ping>[0]); 90 + }); 91 + 92 + it("removeAllListeners nulls out property handlers", () => { 93 + const adapter = createBrowserAdapter(); 94 + const mockWs = { 95 + onopen: () => {}, 96 + onclose: () => {}, 97 + onerror: () => {}, 98 + onmessage: () => {}, 99 + } as unknown as Parameters<typeof adapter.removeAllListeners>[0]; 100 + 101 + adapter.removeAllListeners(mockWs); 102 + expect(mockWs.onopen).toBeNull(); 103 + expect(mockWs.onclose).toBeNull(); 104 + expect(mockWs.onerror).toBeNull(); 105 + expect(mockWs.onmessage).toBeNull(); 106 + }); 107 + 108 + describe("createWebSocket", () => { 109 + const originalWebSocket = globalThis.WebSocket; 110 + 111 + beforeEach(() => { 112 + class MockWebSocket { 113 + readyState = 0; 114 + onopen: ((event: unknown) => void) | null = null; 115 + onclose: ((event: unknown) => void) | null = null; 116 + onerror: ((event: unknown) => void) | null = null; 117 + onmessage: ((event: unknown) => void) | null = null; 118 + close = vi.fn(); 119 + send = vi.fn(); 120 + constructor(public url: string) {} 121 + } 122 + (globalThis as unknown as Record<string, unknown>).WebSocket = MockWebSocket; 123 + }); 124 + 125 + afterEach(() => { 126 + (globalThis as unknown as Record<string, unknown>).WebSocket = originalWebSocket; 127 + }); 128 + 129 + it("creates a WebSocket via globalThis.WebSocket", () => { 130 + const adapter = createBrowserAdapter(); 131 + const ws = adapter.createWebSocket("wss://example.com"); 132 + expect(ws).toBeDefined(); 133 + expect(ws.readyState).toBe(0); 134 + }); 135 + }); 136 + }); 137 + 138 + describe("getPlatformAdapter", () => { 139 + it("returns a Node adapter in Node.js", async () => { 140 + // Reset the cache by re-importing 141 + vi.resetModules(); 142 + const { getPlatformAdapter } = await import("../src/platform/index"); 143 + const adapter = getPlatformAdapter(); 144 + expect(adapter.supportsPing).toBe(true); 145 + }); 146 + 147 + it("caches the adapter", async () => { 148 + vi.resetModules(); 149 + const { getPlatformAdapter } = await import("../src/platform/index"); 150 + const a = getPlatformAdapter(); 151 + const b = getPlatformAdapter(); 152 + expect(a).toBe(b); 153 + }); 154 + });
+120
tests/router.test.ts
··· 1 + import { describe, it, expect, vi } from "vitest"; 2 + import { z } from "zod"; 3 + import { WebSocketRouter } from "../src/router/WebSocketRouter"; 4 + import { Logger } from "../src/logger/Logger"; 5 + import type { ConnectionInfo } from "../src/connection/types"; 6 + 7 + function makeInfo(): ConnectionInfo { 8 + return { 9 + state: "connected", 10 + currentService: "wss://test.example.com", 11 + serviceIndex: 0, 12 + allServices: ["wss://test.example.com"], 13 + reconnectAttempts: 0, 14 + serviceCycles: 0, 15 + messageCount: 0, 16 + lastMessageTime: 0, 17 + }; 18 + } 19 + 20 + describe("WebSocketRouter", () => { 21 + const logger = new Logger({ enabled: false }); 22 + 23 + it("dispatches to a matching handler", async () => { 24 + const router = new WebSocketRouter(logger); 25 + const schema = z.object({ type: z.literal("ping") }); 26 + const handler = vi.fn(); 27 + router.register(schema, handler); 28 + 29 + const sendFn = vi.fn(() => true); 30 + await router.route(JSON.stringify({ type: "ping" }), sendFn, makeInfo()); 31 + 32 + expect(handler).toHaveBeenCalledWith( 33 + expect.objectContaining({ 34 + data: { type: "ping" }, 35 + send: sendFn, 36 + }) 37 + ); 38 + }); 39 + 40 + it("dispatches to multiple matching handlers", async () => { 41 + const router = new WebSocketRouter(logger); 42 + const schema1 = z.object({ type: z.string() }); 43 + const schema2 = z.object({ type: z.literal("ping") }); 44 + const handler1 = vi.fn(); 45 + const handler2 = vi.fn(); 46 + router.register(schema1, handler1); 47 + router.register(schema2, handler2); 48 + 49 + await router.route(JSON.stringify({ type: "ping" }), vi.fn(() => true), makeInfo()); 50 + 51 + expect(handler1).toHaveBeenCalled(); 52 + expect(handler2).toHaveBeenCalled(); 53 + }); 54 + 55 + it("does not dispatch when no schema matches", async () => { 56 + const router = new WebSocketRouter(logger); 57 + const schema = z.object({ type: z.literal("ping") }); 58 + const handler = vi.fn(); 59 + router.register(schema, handler); 60 + 61 + await router.route(JSON.stringify({ type: "pong" }), vi.fn(() => true), makeInfo()); 62 + 63 + expect(handler).not.toHaveBeenCalled(); 64 + }); 65 + 66 + it("emits error on invalid JSON", async () => { 67 + const router = new WebSocketRouter(logger); 68 + const errorHandler = vi.fn(); 69 + router.on("error", errorHandler); 70 + 71 + await router.route("not json!", vi.fn(() => true), makeInfo()); 72 + 73 + expect(errorHandler).toHaveBeenCalledWith( 74 + expect.objectContaining({ 75 + error: expect.any(Error), 76 + rawData: "not json!", 77 + }) 78 + ); 79 + }); 80 + 81 + it("emits error when a handler throws", async () => { 82 + const router = new WebSocketRouter(logger); 83 + const schema = z.object({ type: z.literal("boom") }); 84 + const handlerError = new Error("handler failed"); 85 + router.register(schema, () => { 86 + throw handlerError; 87 + }); 88 + 89 + const errorHandler = vi.fn(); 90 + router.on("error", errorHandler); 91 + 92 + await router.route(JSON.stringify({ type: "boom" }), vi.fn(() => true), makeInfo()); 93 + 94 + expect(errorHandler).toHaveBeenCalledWith( 95 + expect.objectContaining({ 96 + error: handlerError, 97 + rawData: JSON.stringify({ type: "boom" }), 98 + }) 99 + ); 100 + }); 101 + 102 + it("provides rawData and connection info in handler context", async () => { 103 + const router = new WebSocketRouter(logger); 104 + const schema = z.object({ v: z.number() }); 105 + const handler = vi.fn(); 106 + router.register(schema, handler); 107 + 108 + const info = makeInfo(); 109 + const raw = JSON.stringify({ v: 42 }); 110 + await router.route(raw, vi.fn(() => true), info); 111 + 112 + expect(handler).toHaveBeenCalledWith( 113 + expect.objectContaining({ 114 + data: { v: 42 }, 115 + rawData: raw, 116 + connection: info, 117 + }) 118 + ); 119 + }); 120 + });
+1 -1
tsconfig.json
··· 1 1 { 2 2 "compilerOptions": { 3 - "lib": ["ESNext"], 3 + "lib": ["ESNext", "DOM"], 4 4 "outDir": "dist", 5 5 "module": "CommonJS", 6 6 "target": "ES2020",
+2
tsup.config.ts
··· 9 9 clean: true, 10 10 target: "es2020", 11 11 outDir: "dist", 12 + external: ["ws"], 13 + shims: true, 12 14 });
+8
vitest.config.ts
··· 1 + import { defineConfig } from "vitest/config"; 2 + 3 + export default defineConfig({ 4 + test: { 5 + globals: true, 6 + environment: "node", 7 + }, 8 + });