From 632045edd911071be885d8d77bff37613cbd9f47 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Thu, 4 Dec 2025 00:00:38 +0000 Subject: [PATCH] feat(typedsocket): Add SmartServe integration, tagging and improved client reconnect/backoff; update deps and tests --- changelog.md | 12 + package.json | 18 +- pnpm-lock.yaml | 325 ++++++----- test/test.ts | 85 ++- ts/00_commitinfo_data.ts | 2 +- ts/typedsocket.classes.typedsocket.ts | 770 ++++++++++++++++---------- ts/typedsocket.plugins.ts | 7 +- 7 files changed, 739 insertions(+), 480 deletions(-) diff --git a/changelog.md b/changelog.md index 1ea08f4..a436783 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,17 @@ # Changelog +## 2025-12-04 - 4.1.0 - feat(typedsocket) +Add SmartServe integration, tagging and improved client reconnect/backoff; update deps and tests + +- Add SmartServe integration: new TypedSocket.fromSmartServe(smartServe, typedRouter) with SmartServe connection wrappers and tag compatibility +- Implement server-side tag handlers and storage (__typedsocket_setTag / __typedsocket_removeTag) with payloads stored under internal TAG_PREFIX +- Add client improvements: TypedSocket.createClient(...) with autoReconnect, configurable exponential backoff (initialBackoffMs, maxBackoffMs, maxRetries) and connection status observable (statusSubject) +- Expose client tag API: setTag(name, payload) and removeTag(name) to manage server-side tags +- Server-side APIs for connection discovery: findAllTargetConnections, findTargetConnection, findAllTargetConnectionsByTag, findTargetConnectionByTag +- Plugins and internal tooling updated: replace smartsocket exports with smartdelay and smartpromise; export smartpromise and smartdelay for internal use +- Tests updated to use SmartServe for end-to-end scenarios (server/client message flow, tagging and connection discovery); test script changed to run in verbose mode +- Bumped dependency versions and adjusted peer dependency for @push.rocks/smartserve to >=1.1.0 + ## 2025-12-03 - 4.0.0 - BREAKING CHANGE(TypedSocket.createServer) Remove SmartExpress attachment support from createServer and upgrade smartsocket to ^3.0.0 diff --git a/package.json b/package.json index 2c9727e..36781e5 100644 --- a/package.json +++ b/package.json @@ -9,37 +9,33 @@ "author": "Lossless GmbH", "license": "MIT", "scripts": { - "test": "(tstest test/ --web)", + "test": "(tstest test/ --verbose)", "build": "(tsbuild --web --allowimplicitany --skiplibcheck)", "buildDocs": "tsdoc" }, "devDependencies": { "@git.zone/tsbuild": "^3.1.2", - "@git.zone/tsbundle": "^2.6.2", + "@git.zone/tsbundle": "^2.6.3", "@git.zone/tsrun": "^2.0.0", "@git.zone/tstest": "^3.1.3", "@push.rocks/smartenv": "^6.0.0", - "@push.rocks/smartserve": "^1.1.0", + "@push.rocks/smartserve": "^1.1.2", "@push.rocks/tapbundle": "^6.0.3", "@types/node": "^24.10.1" }, "dependencies": { - "@api.global/typedrequest": "^3.1.10", + "@api.global/typedrequest": "^3.1.11", "@api.global/typedrequest-interfaces": "^3.0.19", "@push.rocks/isohash": "^2.0.1", + "@push.rocks/smartdelay": "^3.0.5", "@push.rocks/smartjson": "^5.2.0", + "@push.rocks/smartpromise": "^4.2.3", "@push.rocks/smartrx": "^3.0.10", - "@push.rocks/smartsocket": "^3.0.0", "@push.rocks/smartstring": "^4.1.0", "@push.rocks/smarturl": "^3.1.0" }, "peerDependencies": { - "@push.rocks/smartserve": ">=1.0.0" - }, - "peerDependenciesMeta": { - "@push.rocks/smartserve": { - "optional": true - } + "@push.rocks/smartserve": ">=1.1.0" }, "browserslist": [ "last 1 chrome versions" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e782159..3ae073d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,23 +9,26 @@ importers: .: dependencies: '@api.global/typedrequest': - specifier: ^3.1.10 - version: 3.1.10 + specifier: ^3.1.11 + version: 3.1.11 '@api.global/typedrequest-interfaces': specifier: ^3.0.19 version: 3.0.19 '@push.rocks/isohash': specifier: ^2.0.1 version: 2.0.1 + '@push.rocks/smartdelay': + specifier: ^3.0.5 + version: 3.0.5 '@push.rocks/smartjson': specifier: ^5.2.0 version: 5.2.0 + '@push.rocks/smartpromise': + specifier: ^4.2.3 + version: 4.2.3 '@push.rocks/smartrx': specifier: ^3.0.10 version: 3.0.10 - '@push.rocks/smartsocket': - specifier: ^3.0.0 - version: 3.0.0 '@push.rocks/smartstring': specifier: ^4.1.0 version: 4.1.0 @@ -37,8 +40,8 @@ importers: specifier: ^3.1.2 version: 3.1.2 '@git.zone/tsbundle': - specifier: ^2.6.2 - version: 2.6.2 + specifier: ^2.6.3 + version: 2.6.3 '@git.zone/tsrun': specifier: ^2.0.0 version: 2.0.0 @@ -49,8 +52,8 @@ importers: specifier: ^6.0.0 version: 6.0.0 '@push.rocks/smartserve': - specifier: ^1.1.0 - version: 1.1.0 + specifier: ^1.1.2 + version: 1.1.2 '@push.rocks/tapbundle': specifier: ^6.0.3 version: 6.0.3(@aws-sdk/credential-providers@3.758.0)(socks@2.8.4) @@ -66,8 +69,8 @@ packages: '@api.global/typedrequest-interfaces@3.0.19': resolution: {integrity: sha512-uuHUXJeOy/inWSDrwD0Cwax2rovpxYllDhM2RWh+6mVpQuNmZ3uw6IVg6dA2G1rOe24Ebs+Y9SzEogo+jYN7vw==} - '@api.global/typedrequest@3.1.10': - resolution: {integrity: sha512-EiCp44XVcMjBvEs4oM1nMUaeY4ySU0Pzt3+mDwVG5DNP6EV87Nwancbr2jKScvaFNel9eeDgGtgEnFBKjOnApA==} + '@api.global/typedrequest@3.1.11': + resolution: {integrity: sha512-j8EO3na0WMw8pFkAfEaEui2a4TaAL1G/dv1CYl8LEPXckSKkl1BCAS1kFOW2xuI9pwZkmSqlo3xpQ3KmkmHaGQ==} '@api.global/typedserver@3.0.68': resolution: {integrity: sha512-7o6fkz60ed8q2lmEe44hsu/6kNqG4j5WVgWwmY+a1MmSOUtuu5+VTYYNyc8KrSgtbRBzx4+2A2N31l4wDjcy3w==} @@ -434,8 +437,8 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.27.0': - resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==} + '@esbuild/aix-ppc64@0.27.1': + resolution: {integrity: sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] @@ -446,8 +449,8 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.27.0': - resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==} + '@esbuild/android-arm64@0.27.1': + resolution: {integrity: sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==} engines: {node: '>=18'} cpu: [arm64] os: [android] @@ -458,8 +461,8 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.27.0': - resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==} + '@esbuild/android-arm@0.27.1': + resolution: {integrity: sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==} engines: {node: '>=18'} cpu: [arm] os: [android] @@ -470,8 +473,8 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.27.0': - resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==} + '@esbuild/android-x64@0.27.1': + resolution: {integrity: sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==} engines: {node: '>=18'} cpu: [x64] os: [android] @@ -482,8 +485,8 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.27.0': - resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==} + '@esbuild/darwin-arm64@0.27.1': + resolution: {integrity: sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] @@ -494,8 +497,8 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.27.0': - resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==} + '@esbuild/darwin-x64@0.27.1': + resolution: {integrity: sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] @@ -506,8 +509,8 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.27.0': - resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==} + '@esbuild/freebsd-arm64@0.27.1': + resolution: {integrity: sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] @@ -518,8 +521,8 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.0': - resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==} + '@esbuild/freebsd-x64@0.27.1': + resolution: {integrity: sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] @@ -530,8 +533,8 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.27.0': - resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==} + '@esbuild/linux-arm64@0.27.1': + resolution: {integrity: sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==} engines: {node: '>=18'} cpu: [arm64] os: [linux] @@ -542,8 +545,8 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.27.0': - resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==} + '@esbuild/linux-arm@0.27.1': + resolution: {integrity: sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==} engines: {node: '>=18'} cpu: [arm] os: [linux] @@ -554,8 +557,8 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.27.0': - resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==} + '@esbuild/linux-ia32@0.27.1': + resolution: {integrity: sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==} engines: {node: '>=18'} cpu: [ia32] os: [linux] @@ -566,8 +569,8 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.27.0': - resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==} + '@esbuild/linux-loong64@0.27.1': + resolution: {integrity: sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] @@ -578,8 +581,8 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.27.0': - resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==} + '@esbuild/linux-mips64el@0.27.1': + resolution: {integrity: sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] @@ -590,8 +593,8 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.27.0': - resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==} + '@esbuild/linux-ppc64@0.27.1': + resolution: {integrity: sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] @@ -602,8 +605,8 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.27.0': - resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==} + '@esbuild/linux-riscv64@0.27.1': + resolution: {integrity: sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] @@ -614,8 +617,8 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.27.0': - resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==} + '@esbuild/linux-s390x@0.27.1': + resolution: {integrity: sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] @@ -626,8 +629,8 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.27.0': - resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==} + '@esbuild/linux-x64@0.27.1': + resolution: {integrity: sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==} engines: {node: '>=18'} cpu: [x64] os: [linux] @@ -638,8 +641,8 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-arm64@0.27.0': - resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==} + '@esbuild/netbsd-arm64@0.27.1': + resolution: {integrity: sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] @@ -650,8 +653,8 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.0': - resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==} + '@esbuild/netbsd-x64@0.27.1': + resolution: {integrity: sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] @@ -662,8 +665,8 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.27.0': - resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==} + '@esbuild/openbsd-arm64@0.27.1': + resolution: {integrity: sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] @@ -674,14 +677,14 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.0': - resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==} + '@esbuild/openbsd-x64@0.27.1': + resolution: {integrity: sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.0': - resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==} + '@esbuild/openharmony-arm64@0.27.1': + resolution: {integrity: sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] @@ -692,8 +695,8 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.27.0': - resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==} + '@esbuild/sunos-x64@0.27.1': + resolution: {integrity: sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] @@ -704,8 +707,8 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.27.0': - resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==} + '@esbuild/win32-arm64@0.27.1': + resolution: {integrity: sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] @@ -716,8 +719,8 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.27.0': - resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==} + '@esbuild/win32-ia32@0.27.1': + resolution: {integrity: sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] @@ -728,8 +731,8 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.27.0': - resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==} + '@esbuild/win32-x64@0.27.1': + resolution: {integrity: sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -741,8 +744,8 @@ packages: resolution: {integrity: sha512-K0u840Qo0WEhvcpAtktvdBX6KEXjelU32o820WzcK7dMA7dd2YV+mPOEYfbmWLcdtFJkrjkigQq5fpLhTN4oKQ==} hasBin: true - '@git.zone/tsbundle@2.6.2': - resolution: {integrity: sha512-wj32zHpvbDUdStEjJ9RCqffmafqlopWSROSRBQDgpJ8hnMAO3ftWkTWWfGdTGh2p2pALfPqgSFzwxCd4RJG8aQ==} + '@git.zone/tsbundle@2.6.3': + resolution: {integrity: sha512-YD1qMYA/4eOuF57V0ccR+xo6ww1+QOYFA2K5gBPFBDNh9VdfvWxxDhOUybja8lT9PVMoli8PHG5WA5tKJkdXIQ==} hasBin: true '@git.zone/tspublish@1.10.3': @@ -988,6 +991,9 @@ packages: '@push.rocks/smartbuffer@3.0.4': resolution: {integrity: sha512-TLfhx/JD61YC8XGO9TI6Ux6US38R14HaIM84QT8hZZod8axfXrg+h8xA8tMUBpSV8PXsQy9LzxmOq0Il1fmDXw==} + '@push.rocks/smartbuffer@3.0.5': + resolution: {integrity: sha512-pWYF08Mn8s/KF/9nHRk7pZPzuMjmYVQay2c5gGexdayxn1W4eCSYYhWH73vR2JBfGeGq/izbRNuUuEaIEeTIKA==} + '@push.rocks/smartcache@1.0.16': resolution: {integrity: sha512-UAXf74eDuH4/RebJhydIbHlYVR3ACYJjniEY/9ZePblu7bIPgwFZqLBE9g1lcKVogbH9yY62dk3rSpgBzenyfQ==} @@ -1158,8 +1164,8 @@ packages: '@push.rocks/smarts3@3.0.3': resolution: {integrity: sha512-Y9nXMwurthJ9Z7yi0RwjhPFUC58aY8Mhia8kFo6Xj1tBM4LE8Oxg/ydejF7otHqQGr3QyqV5C4YrDEG17rUuzg==} - '@push.rocks/smartserve@1.1.0': - resolution: {integrity: sha512-w9cSRw+ia5oWXcuK1QCBCEUUPsAJ2zDeOv1gHDytL+2e1oLlRAEp35uhWFHGPsTCZiwgVZhy3ZORWemJTODQlQ==} + '@push.rocks/smartserve@1.1.2': + resolution: {integrity: sha512-NkJNgdDt/rfsd9AMheCxtFd5X+ubzffvxOxjb0Aw1A5JR3xmiWeRifqEV1oN7mMTGL9jyQVvIME6Yrdxr244dA==} '@push.rocks/smartshell@3.2.3': resolution: {integrity: sha512-BWA/DH1H9lG7Er23d4uYgirfYaya5dX4g/WpWm2la7mOzuL9o2FnPIhel52DQUKIh7ty3Ql305ApV8YaAb4+/w==} @@ -1176,9 +1182,6 @@ packages: '@push.rocks/smartsocket@2.1.0': resolution: {integrity: sha512-etOGyfiDFQz/1WJnD3jFL2N7ykujTjiudAz6qZTz82xE5oabKuKX+Cn8SdM9dOwzyWmBUKbUdll8QhovAXjn+g==} - '@push.rocks/smartsocket@3.0.0': - resolution: {integrity: sha512-8pUbOybNBwXF+D9VsuC+3LjD+r9uRzcgcwFRcpic6WGVqW0c5hI8D8YwKDHkL11XJJD7yNRJnsoIc618EVUvuQ==} - '@push.rocks/smartspawn@3.0.3': resolution: {integrity: sha512-DyrGPV69wwOiJgKkyruk5hS3UEGZ99xFAqBE9O2nM8VXCRLbbty3xt1Ug5Z092ZZmJYaaGMSnMw3ijyZJFCT0Q==} @@ -2897,8 +2900,8 @@ packages: engines: {node: '>=18'} hasBin: true - esbuild@0.27.0: - resolution: {integrity: sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==} + esbuild@0.27.1: + resolution: {integrity: sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==} engines: {node: '>=18'} hasBin: true @@ -4858,6 +4861,10 @@ packages: resolution: {integrity: sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==} engines: {node: '>=18'} + uint8array-extras@1.5.0: + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + engines: {node: '>=18'} + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -5071,12 +5078,12 @@ snapshots: '@api.global/typedrequest-interfaces@3.0.19': {} - '@api.global/typedrequest@3.1.10': + '@api.global/typedrequest@3.1.11': dependencies: '@api.global/typedrequest-interfaces': 3.0.19 '@push.rocks/isounique': 1.0.5 - '@push.rocks/lik': 6.1.0 - '@push.rocks/smartbuffer': 3.0.4 + '@push.rocks/lik': 6.2.2 + '@push.rocks/smartbuffer': 3.0.5 '@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartguard': 3.1.0 '@push.rocks/smartpromise': 4.2.3 @@ -5085,7 +5092,7 @@ snapshots: '@api.global/typedserver@3.0.68': dependencies: - '@api.global/typedrequest': 3.1.10 + '@api.global/typedrequest': 3.1.11 '@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedsocket': 3.0.1 '@cloudflare/workers-types': 4.20250310.0 @@ -5130,7 +5137,7 @@ snapshots: '@api.global/typedserver@3.0.80': dependencies: - '@api.global/typedrequest': 3.1.10 + '@api.global/typedrequest': 3.1.11 '@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedsocket': 3.0.1 '@cloudflare/workers-types': 4.20251202.0 @@ -5177,7 +5184,7 @@ snapshots: '@api.global/typedsocket@3.0.1': dependencies: - '@api.global/typedrequest': 3.1.10 + '@api.global/typedrequest': 3.1.11 '@api.global/typedrequest-interfaces': 3.0.19 '@push.rocks/isohash': 2.0.1 '@push.rocks/smartjson': 5.2.0 @@ -6190,14 +6197,14 @@ snapshots: '@design.estate/dees-comms@1.0.27': dependencies: - '@api.global/typedrequest': 3.1.10 + '@api.global/typedrequest': 3.1.11 '@api.global/typedrequest-interfaces': 3.0.19 '@push.rocks/smartdelay': 3.0.5 broadcast-channel: 7.0.0 '@design.estate/dees-domtools@2.3.2': dependencies: - '@api.global/typedrequest': 3.1.10 + '@api.global/typedrequest': 3.1.11 '@design.estate/dees-comms': 1.0.27 '@push.rocks/lik': 6.2.2 '@push.rocks/smartdelay': 3.0.5 @@ -6223,7 +6230,7 @@ snapshots: '@design.estate/dees-domtools@2.3.6': dependencies: - '@api.global/typedrequest': 3.1.10 + '@api.global/typedrequest': 3.1.11 '@design.estate/dees-comms': 1.0.27 '@push.rocks/lik': 6.2.2 '@push.rocks/smartdelay': 3.0.5 @@ -6290,154 +6297,154 @@ snapshots: '@esbuild/aix-ppc64@0.25.1': optional: true - '@esbuild/aix-ppc64@0.27.0': + '@esbuild/aix-ppc64@0.27.1': optional: true '@esbuild/android-arm64@0.25.1': optional: true - '@esbuild/android-arm64@0.27.0': + '@esbuild/android-arm64@0.27.1': optional: true '@esbuild/android-arm@0.25.1': optional: true - '@esbuild/android-arm@0.27.0': + '@esbuild/android-arm@0.27.1': optional: true '@esbuild/android-x64@0.25.1': optional: true - '@esbuild/android-x64@0.27.0': + '@esbuild/android-x64@0.27.1': optional: true '@esbuild/darwin-arm64@0.25.1': optional: true - '@esbuild/darwin-arm64@0.27.0': + '@esbuild/darwin-arm64@0.27.1': optional: true '@esbuild/darwin-x64@0.25.1': optional: true - '@esbuild/darwin-x64@0.27.0': + '@esbuild/darwin-x64@0.27.1': optional: true '@esbuild/freebsd-arm64@0.25.1': optional: true - '@esbuild/freebsd-arm64@0.27.0': + '@esbuild/freebsd-arm64@0.27.1': optional: true '@esbuild/freebsd-x64@0.25.1': optional: true - '@esbuild/freebsd-x64@0.27.0': + '@esbuild/freebsd-x64@0.27.1': optional: true '@esbuild/linux-arm64@0.25.1': optional: true - '@esbuild/linux-arm64@0.27.0': + '@esbuild/linux-arm64@0.27.1': optional: true '@esbuild/linux-arm@0.25.1': optional: true - '@esbuild/linux-arm@0.27.0': + '@esbuild/linux-arm@0.27.1': optional: true '@esbuild/linux-ia32@0.25.1': optional: true - '@esbuild/linux-ia32@0.27.0': + '@esbuild/linux-ia32@0.27.1': optional: true '@esbuild/linux-loong64@0.25.1': optional: true - '@esbuild/linux-loong64@0.27.0': + '@esbuild/linux-loong64@0.27.1': optional: true '@esbuild/linux-mips64el@0.25.1': optional: true - '@esbuild/linux-mips64el@0.27.0': + '@esbuild/linux-mips64el@0.27.1': optional: true '@esbuild/linux-ppc64@0.25.1': optional: true - '@esbuild/linux-ppc64@0.27.0': + '@esbuild/linux-ppc64@0.27.1': optional: true '@esbuild/linux-riscv64@0.25.1': optional: true - '@esbuild/linux-riscv64@0.27.0': + '@esbuild/linux-riscv64@0.27.1': optional: true '@esbuild/linux-s390x@0.25.1': optional: true - '@esbuild/linux-s390x@0.27.0': + '@esbuild/linux-s390x@0.27.1': optional: true '@esbuild/linux-x64@0.25.1': optional: true - '@esbuild/linux-x64@0.27.0': + '@esbuild/linux-x64@0.27.1': optional: true '@esbuild/netbsd-arm64@0.25.1': optional: true - '@esbuild/netbsd-arm64@0.27.0': + '@esbuild/netbsd-arm64@0.27.1': optional: true '@esbuild/netbsd-x64@0.25.1': optional: true - '@esbuild/netbsd-x64@0.27.0': + '@esbuild/netbsd-x64@0.27.1': optional: true '@esbuild/openbsd-arm64@0.25.1': optional: true - '@esbuild/openbsd-arm64@0.27.0': + '@esbuild/openbsd-arm64@0.27.1': optional: true '@esbuild/openbsd-x64@0.25.1': optional: true - '@esbuild/openbsd-x64@0.27.0': + '@esbuild/openbsd-x64@0.27.1': optional: true - '@esbuild/openharmony-arm64@0.27.0': + '@esbuild/openharmony-arm64@0.27.1': optional: true '@esbuild/sunos-x64@0.25.1': optional: true - '@esbuild/sunos-x64@0.27.0': + '@esbuild/sunos-x64@0.27.1': optional: true '@esbuild/win32-arm64@0.25.1': optional: true - '@esbuild/win32-arm64@0.27.0': + '@esbuild/win32-arm64@0.27.1': optional: true '@esbuild/win32-ia32@0.25.1': optional: true - '@esbuild/win32-ia32@0.27.0': + '@esbuild/win32-ia32@0.27.1': optional: true '@esbuild/win32-x64@0.25.1': optional: true - '@esbuild/win32-x64@0.27.0': + '@esbuild/win32-x64@0.27.1': optional: true '@esm-bundle/chai@4.3.4-fix.0': @@ -6459,7 +6466,7 @@ snapshots: - aws-crt - supports-color - '@git.zone/tsbundle@2.6.2': + '@git.zone/tsbundle@2.6.3': dependencies: '@push.rocks/early': 4.0.4 '@push.rocks/smartcli': 4.0.19 @@ -6472,7 +6479,7 @@ snapshots: '@push.rocks/smartspawn': 3.0.3 '@rspack/core': 1.6.6 '@types/html-minifier': 4.0.6 - esbuild: 0.27.0 + esbuild: 0.27.1 html-minifier: 4.0.0 rolldown: 1.0.0-beta.52 typescript: 5.9.3 @@ -6504,7 +6511,7 @@ snapshots: '@git.zone/tstest@3.1.3(@aws-sdk/credential-providers@3.758.0)(socks@2.8.4)(typescript@5.9.3)': dependencies: '@api.global/typedserver': 3.0.80 - '@git.zone/tsbundle': 2.6.2 + '@git.zone/tsbundle': 2.6.3 '@git.zone/tsrun': 2.0.0 '@push.rocks/consolecolor': 2.0.3 '@push.rocks/qenv': 6.1.3 @@ -6917,7 +6924,7 @@ snapshots: '@push.rocks/qenv@6.1.0': dependencies: - '@api.global/typedrequest': 3.1.10 + '@api.global/typedrequest': 3.1.11 '@configvault.io/interfaces': 1.0.17 '@push.rocks/smartfile': 11.2.0 '@push.rocks/smartlog': 3.0.7 @@ -6925,7 +6932,7 @@ snapshots: '@push.rocks/qenv@6.1.3': dependencies: - '@api.global/typedrequest': 3.1.10 + '@api.global/typedrequest': 3.1.11 '@configvault.io/interfaces': 1.0.17 '@push.rocks/smartfile': 11.2.7 '@push.rocks/smartlog': 3.1.10 @@ -6995,6 +7002,10 @@ snapshots: dependencies: uint8array-extras: 1.4.0 + '@push.rocks/smartbuffer@3.0.5': + dependencies: + uint8array-extras: 1.5.0 + '@push.rocks/smartcache@1.0.16': dependencies: '@pushrocks/smartdelay': 2.0.13 @@ -7186,7 +7197,7 @@ snapshots: '@push.rocks/smartguard@3.1.0': dependencies: '@push.rocks/smartpromise': 4.2.3 - '@push.rocks/smartrequest': 2.0.23 + '@push.rocks/smartrequest': 2.1.0 '@push.rocks/smarthash@3.0.4': dependencies: @@ -7216,7 +7227,7 @@ snapshots: '@push.rocks/smartlog-destination-local@9.0.2': dependencies: - '@push.rocks/consolecolor': 2.0.2 + '@push.rocks/consolecolor': 2.0.3 '@push.rocks/smartlog-interfaces': 3.0.2 '@push.rocks/smartpromise': 4.2.3 @@ -7463,13 +7474,17 @@ snapshots: transitivePeerDependencies: - aws-crt - '@push.rocks/smartserve@1.1.0': + '@push.rocks/smartserve@1.1.2': dependencies: - '@api.global/typedrequest': 3.1.10 + '@api.global/typedrequest': 3.1.11 '@push.rocks/lik': 6.2.2 '@push.rocks/smartenv': 6.0.0 '@push.rocks/smartlog': 3.1.10 '@push.rocks/smartpath': 6.0.0 + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate '@push.rocks/smartshell@3.2.3': dependencies: @@ -7532,24 +7547,6 @@ snapshots: - utf-8-validate - vue - '@push.rocks/smartsocket@3.0.0': - dependencies: - '@api.global/typedrequest-interfaces': 3.0.19 - '@push.rocks/isohash': 2.0.1 - '@push.rocks/isounique': 1.0.5 - '@push.rocks/lik': 6.2.2 - '@push.rocks/smartdelay': 3.0.5 - '@push.rocks/smartenv': 6.0.0 - '@push.rocks/smartjson': 5.2.0 - '@push.rocks/smartlog': 3.1.10 - '@push.rocks/smartpromise': 4.2.3 - '@push.rocks/smartrx': 3.0.10 - '@push.rocks/smarttime': 4.1.1 - ws: 8.18.3 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - '@push.rocks/smartspawn@3.0.3': dependencies: '@push.rocks/smartpromise': 4.2.3 @@ -7723,7 +7720,7 @@ snapshots: '@push.rocks/webstream@1.0.10': dependencies: - '@push.rocks/smartenv': 5.0.12 + '@push.rocks/smartenv': 5.0.13 '@pushrocks/isounique@1.0.5': {} @@ -9731,34 +9728,34 @@ snapshots: '@esbuild/win32-ia32': 0.25.1 '@esbuild/win32-x64': 0.25.1 - esbuild@0.27.0: + esbuild@0.27.1: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.0 - '@esbuild/android-arm': 0.27.0 - '@esbuild/android-arm64': 0.27.0 - '@esbuild/android-x64': 0.27.0 - '@esbuild/darwin-arm64': 0.27.0 - '@esbuild/darwin-x64': 0.27.0 - '@esbuild/freebsd-arm64': 0.27.0 - '@esbuild/freebsd-x64': 0.27.0 - '@esbuild/linux-arm': 0.27.0 - '@esbuild/linux-arm64': 0.27.0 - '@esbuild/linux-ia32': 0.27.0 - '@esbuild/linux-loong64': 0.27.0 - '@esbuild/linux-mips64el': 0.27.0 - '@esbuild/linux-ppc64': 0.27.0 - '@esbuild/linux-riscv64': 0.27.0 - '@esbuild/linux-s390x': 0.27.0 - '@esbuild/linux-x64': 0.27.0 - '@esbuild/netbsd-arm64': 0.27.0 - '@esbuild/netbsd-x64': 0.27.0 - '@esbuild/openbsd-arm64': 0.27.0 - '@esbuild/openbsd-x64': 0.27.0 - '@esbuild/openharmony-arm64': 0.27.0 - '@esbuild/sunos-x64': 0.27.0 - '@esbuild/win32-arm64': 0.27.0 - '@esbuild/win32-ia32': 0.27.0 - '@esbuild/win32-x64': 0.27.0 + '@esbuild/aix-ppc64': 0.27.1 + '@esbuild/android-arm': 0.27.1 + '@esbuild/android-arm64': 0.27.1 + '@esbuild/android-x64': 0.27.1 + '@esbuild/darwin-arm64': 0.27.1 + '@esbuild/darwin-x64': 0.27.1 + '@esbuild/freebsd-arm64': 0.27.1 + '@esbuild/freebsd-x64': 0.27.1 + '@esbuild/linux-arm': 0.27.1 + '@esbuild/linux-arm64': 0.27.1 + '@esbuild/linux-ia32': 0.27.1 + '@esbuild/linux-loong64': 0.27.1 + '@esbuild/linux-mips64el': 0.27.1 + '@esbuild/linux-ppc64': 0.27.1 + '@esbuild/linux-riscv64': 0.27.1 + '@esbuild/linux-s390x': 0.27.1 + '@esbuild/linux-x64': 0.27.1 + '@esbuild/netbsd-arm64': 0.27.1 + '@esbuild/netbsd-x64': 0.27.1 + '@esbuild/openbsd-arm64': 0.27.1 + '@esbuild/openbsd-x64': 0.27.1 + '@esbuild/openharmony-arm64': 0.27.1 + '@esbuild/sunos-x64': 0.27.1 + '@esbuild/win32-arm64': 0.27.1 + '@esbuild/win32-ia32': 0.27.1 + '@esbuild/win32-x64': 0.27.1 escalade@3.2.0: {} @@ -12037,7 +12034,7 @@ snapshots: threads@1.7.0: dependencies: callsites: 3.1.0 - debug: 4.4.0 + debug: 4.4.3 is-observable: 2.1.0 observable-fns: 0.6.1 optionalDependencies: @@ -12136,6 +12133,8 @@ snapshots: uint8array-extras@1.4.0: {} + uint8array-extras@1.5.0: {} + undici-types@7.16.0: {} unified@11.0.5: diff --git a/test/test.ts b/test/test.ts index 3e0a08e..5c47ba5 100644 --- a/test/test.ts +++ b/test/test.ts @@ -1,6 +1,7 @@ import { expect, tap } from '@push.rocks/tapbundle'; import * as typedrequest from '@api.global/typedrequest'; import * as typedrequestInterfaces from '@api.global/typedrequest-interfaces'; +import { SmartServe } from '@push.rocks/smartserve'; import * as typedsocket from '../ts/index.js'; @@ -18,12 +19,15 @@ interface IRequest_Client_Server }; } +let testSmartServe: SmartServe; let testTypedSocketServer: typedsocket.TypedSocket; let testTypedSocketClient: typedsocket.TypedSocket; const testTypedRouter = new typedrequest.TypedRouter(); +const clientTypedRouter = new typedrequest.TypedRouter(); tap.test('should add some handlers', async () => { + // Server-side handler testTypedRouter.addTypedHandler( new typedrequest.TypedHandler('sayhi', async (requestData) => { return { @@ -31,45 +35,86 @@ tap.test('should add some handlers', async () => { }; }) ); + + // Client-side handler (for server-to-client messages) + clientTypedRouter.addTypedHandler( + new typedrequest.TypedHandler('sayhi', async (requestData) => { + return { + answer: `client got: ${requestData.greeting}`, + }; + }) + ); }); tap.test('should create Server and Client', async (tools) => { - testTypedSocketServer = await typedsocket.TypedSocket.createServer(testTypedRouter); + // Create SmartServe with TypedRouter for WebSocket + testSmartServe = new SmartServe({ + port: 3000, + websocket: { + typedRouter: testTypedRouter, + }, + }); + await testSmartServe.start(); + console.log('SmartServe started on port 3000'); + + // Create TypedSocket server from SmartServe + testTypedSocketServer = typedsocket.TypedSocket.fromSmartServe(testSmartServe, testTypedRouter); + console.log('TypedSocket server created'); + + // Create client testTypedSocketClient = await typedsocket.TypedSocket.createClient( - testTypedRouter, + clientTypedRouter, 'http://localhost:3000' ); - console.log('test: waiting 5 seconds'); - await tools.delayFor(5000); - await testTypedSocketServer.stop(); + console.log('TypedSocket client connected'); - // lets create another server - testTypedSocketServer = await typedsocket.TypedSocket.createServer(testTypedRouter); - - // lets see if auto reconnect works - console.log('test: waiting 21 seconds for reconnect'); - await tools.delayFor(21000); + console.log('test: waiting 1 second for connection to stabilize'); + await tools.delayFor(1000); }); -tap.test('should process messages from both sides', async () => { - const myServerSideTypedRequest = - testTypedSocketServer.createTypedRequest('sayhi'); +tap.test('should set tags via TypedRequest', async () => { + console.log('Setting tag...'); + await testTypedSocketClient.setTag('testTag', { userId: 123 }); + console.log('Tag set successfully'); +}); + +tap.test('should process messages from client to server', async () => { + console.log('Testing client to server...'); const myClientSideTypedRequest = testTypedSocketClient.createTypedRequest('sayhi'); const response = await myClientSideTypedRequest.fire({ greeting: 'that is a greeting from the client', }); - console.log(response); - const response2 = await myServerSideTypedRequest.fire({ + console.log('Client got response:', response); + expect(response.answer).toContain('ok, got it'); +}); + +tap.test('should find connections by tag', async () => { + console.log('Finding connections by tag...'); + const connections = await testTypedSocketServer.findAllTargetConnectionsByTag('testTag'); + console.log(`Found ${connections.length} connections with tag`); + expect(connections.length).toEqual(1); +}); + +tap.test('should process messages from server to client', async () => { + console.log('Testing server to client...'); + const connections = await testTypedSocketServer.findAllTargetConnectionsByTag('testTag'); + const myServerSideTypedRequest = + testTypedSocketServer.createTypedRequest('sayhi', connections[0]); + const response = await myServerSideTypedRequest.fire({ greeting: 'that is a greeting from the server', }); - console.log(response2); + console.log('Server got response:', response); + expect(response.answer).toContain('client got'); }); tap.test('should disconnect', async (tools) => { + console.log('Stopping client...'); await testTypedSocketClient.stop(); - await testTypedSocketServer.stop(); - tools.delayFor(1000).then(() => process.exit(0)); + console.log('Stopping server...'); + await testSmartServe.stop(); + console.log('All stopped'); + tools.delayFor(500).then(() => process.exit(0)); }); -tap.start(); +export default tap.start(); diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 4aa59a7..88e507b 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@api.global/typedsocket', - version: '4.0.0', + version: '4.1.0', description: 'A library for creating typed WebSocket connections, supporting bi-directional communication with type safety.' } diff --git a/ts/typedsocket.classes.typedsocket.ts b/ts/typedsocket.classes.typedsocket.ts index 23cafc8..9312214 100644 --- a/ts/typedsocket.classes.typedsocket.ts +++ b/ts/typedsocket.classes.typedsocket.ts @@ -1,13 +1,37 @@ import * as plugins from './typedsocket.plugins.js'; -const publicRoleName = 'publicRoleName'; -const publicRolePass = 'publicRolePass'; - export type TTypedSocketSide = 'server' | 'client'; +export type TConnectionStatus = 'new' | 'connecting' | 'connected' | 'disconnected' | 'reconnecting'; + +const TAG_PREFIX = '__typedsocket_tag__'; + +/** + * Internal TypedRequest interfaces for tag management + */ +interface IReq_SetClientTag extends plugins.typedrequestInterfaces.ITypedRequest { + method: '__typedsocket_setTag'; + request: { name: string; payload: any }; + response: { success: boolean }; +} + +interface IReq_RemoveClientTag extends plugins.typedrequestInterfaces.ITypedRequest { + method: '__typedsocket_removeTag'; + request: { name: string }; + response: { success: boolean }; +} + +/** + * Options for creating a TypedSocket client + */ +export interface ITypedSocketClientOptions { + autoReconnect?: boolean; + maxRetries?: number; + initialBackoffMs?: number; + maxBackoffMs?: number; +} /** * Wrapper for SmartServe's IWebSocketPeer to provide tag compatibility - * SmartServe uses Set for tags, while TypedSocket uses {id, payload} format */ export interface ISmartServeConnectionWrapper { peer: plugins.IWebSocketPeer; @@ -18,7 +42,6 @@ export interface ISmartServeConnectionWrapper { * Creates a wrapper around IWebSocketPeer for tag compatibility */ function wrapSmartServePeer(peer: plugins.IWebSocketPeer): ISmartServeConnectionWrapper { - const TAG_PREFIX = '__typedsocket_tag__'; return { peer, async getTagById(tagId: string): Promise<{ id: string; payload: any } | undefined> { @@ -32,109 +55,62 @@ function wrapSmartServePeer(peer: plugins.IWebSocketPeer): ISmartServeConnection } export class TypedSocket { - // STATIC + // ============================================================================ + // STATIC METHODS + // ============================================================================ + /** - * creates a typedsocket server - * note: this will fail in browser environments as server libs are not bundled. + * Creates a TypedSocket client using native WebSocket. + * Works in both browser and Node.js environments. + * + * @param typedrouterArg - TypedRouter for handling server-initiated requests + * @param serverUrlArg - Server URL (e.g., 'http://localhost:3000' or 'wss://example.com') + * @param options - Connection options + * + * @example + * ```typescript + * const typedRouter = new TypedRouter(); + * const client = await TypedSocket.createClient( + * typedRouter, + * 'http://localhost:3000', + * { autoReconnect: true } + * ); + * ``` */ - public static async createServer( - typedrouterArg: plugins.typedrequest.TypedRouter - ): Promise { - const smartsocketServer = new plugins.smartsocket.Smartsocket({ - alias: 'typedsocketServer', - port: 3000, - }); - - smartsocketServer.socketFunctions.add( - new plugins.smartsocket.SocketFunction({ - funcName: 'processMessage', - funcDef: async (dataArg, socketConnectionArg) => { - return typedrouterArg.routeAndAddResponse(dataArg); - }, - }) - ); - const typedsocket = new TypedSocket( - 'server', - typedrouterArg, - async ( - dataArg: T, - targetConnectionArg?: plugins.smartsocket.SocketConnection - ): Promise => { - if (!targetConnectionArg) { - if ((smartsocketServer.socketConnections.getArray().length = 1)) { - console.log( - 'Since no targetConnection was supplied and there is only one active one present, choosing that one automatically' - ); - targetConnectionArg = smartsocketServer.socketConnections.getArray()[0]; - } else { - throw new Error( - 'you need to specify the wanted targetConnection. Currently no target is selectable automatically.' - ); - } - } - const response: T = (await smartsocketServer.clientCall( - 'processMessage', - dataArg, - targetConnectionArg - )) as any; - return response; - }, - smartsocketServer - ); - await smartsocketServer.start(); - - return typedsocket; - } - public static async createClient( typedrouterArg: plugins.typedrequest.TypedRouter, serverUrlArg: string, - aliasArg = 'clientArg' + options: ITypedSocketClientOptions = {} ): Promise { - const domain = new plugins.smartstring.Domain(serverUrlArg); - - const socketOptions: plugins.smartsocket.ISmartsocketClientOptions = { - alias: aliasArg, - port: domain.port || 3000, - url: `${domain.nodeParsedUrl.protocol}//${domain.nodeParsedUrl.hostname}`, + const defaultOptions: Required = { autoReconnect: true, + maxRetries: 100, + initialBackoffMs: 1000, + maxBackoffMs: 60000, }; - console.log(`starting typedsocket with the following settings:`); - console.log(socketOptions); - const smartsocketClient = new plugins.smartsocket.SmartsocketClient(socketOptions); - smartsocketClient.addSocketFunction( - new plugins.smartsocket.SocketFunction({ - funcName: 'processMessage', - funcDef: async (dataArg, socketConnectionArg) => { - return typedrouterArg.routeAndAddResponse(dataArg); - }, - }) - ); - const typedsocket = new TypedSocket( - 'client', - typedrouterArg, - async (dataArg: T): Promise => { - const response: T = smartsocketClient.serverCall('processMessage', dataArg) as any as T; - return response; - }, - smartsocketClient - ); - console.log(`typedsocket triggering smartsocket to connect...`); - const before = Date.now(); - await smartsocketClient.connect(); - console.log(`typedsocket triggered smartsocket connected in ${Date.now() - before}ms!!!`) + const opts = { ...defaultOptions, ...options }; - return typedsocket; - } + const typedSocket = new TypedSocket('client', typedrouterArg); + typedSocket.clientOptions = opts; + typedSocket.serverUrl = serverUrlArg; + typedSocket.currentBackoff = opts.initialBackoffMs; - public static useWindowLocationOriginUrl = () => { - const windowLocationResult = plugins.smarturl.Smarturl.createFromUrl(globalThis.location.origin).toString(); - return windowLocationResult; + await typedSocket.connect(); + + return typedSocket; } + /** + * Returns the current window location origin URL. + * Useful in browser environments for connecting to the same origin. + */ + public static useWindowLocationOriginUrl = (): string => { + return plugins.smarturl.Smarturl.createFromUrl(globalThis.location.origin).toString(); + }; + /** * Creates a TypedSocket server from an existing SmartServe instance. - * Use this when you want TypedSocket to work with SmartServe's WebSocket handling. + * This is the only way to create a server-side TypedSocket. * * @param smartServeArg - SmartServe instance with typedRouter configured in websocket options * @param typedRouterArg - TypedRouter for handling requests (must match SmartServe's typedRouter) @@ -159,249 +135,479 @@ export class TypedSocket { ): TypedSocket { const connectionWrappers = new Map(); - // Create the postMethod for server-initiated requests - const postMethod = async ( - dataArg: T, - targetConnectionArg?: ISmartServeConnectionWrapper - ): Promise => { - if (!targetConnectionArg) { - const allConnections = smartServeArg.getWebSocketConnections(); - if (allConnections.length === 1) { - console.log( - 'Since no targetConnection was supplied and there is only one active one present, choosing that one automatically' - ); - const peer = allConnections[0]; - let wrapper = connectionWrappers.get(peer.id); - if (!wrapper) { - wrapper = wrapSmartServePeer(peer); - connectionWrappers.set(peer.id, wrapper); - } - targetConnectionArg = wrapper; - } else if (allConnections.length === 0) { - throw new Error('No WebSocket connections available'); - } else { - throw new Error( - 'you need to specify the wanted targetConnection. Currently no target is selectable automatically.' - ); - } - } - - // Register interest for the response using correlation ID - const interest = await typedRouterArg.fireEventInterestMap.addInterest( - dataArg.correlation.id, - dataArg - ); - - // Send the request to the client - targetConnectionArg.peer.send(plugins.smartjson.stringify(dataArg)); - - // Wait for the response (TypedRouter will fulfill via routeAndAddResponse when response arrives) - const response = await interest.interestFullfilled as T; - return response; - }; - - const typedSocket = new TypedSocket( - 'server', - typedRouterArg, - postMethod as any, - null as any // No smartsocket server/client when using SmartServe - ); + // Register built-in tag handlers + TypedSocket.registerTagHandlers(typedRouterArg); + const typedSocket = new TypedSocket('server', typedRouterArg); typedSocket.smartServeRef = smartServeArg; typedSocket.smartServeConnectionWrappers = connectionWrappers; return typedSocket; } - // INSTANCE - public side: TTypedSocketSide; - public typedrouter: plugins.typedrequest.TypedRouter; + /** + * Registers built-in TypedHandlers for tag management + */ + private static registerTagHandlers(typedRouter: plugins.typedrequest.TypedRouter): void { + // Set tag handler + typedRouter.addTypedHandler( + new plugins.typedrequest.TypedHandler('__typedsocket_setTag', async (data, meta) => { + const peer = meta?.localData?.peer as plugins.IWebSocketPeer; + if (!peer) { + console.warn('setTag: No peer found in request context'); + return { success: false }; + } - // SmartServe mode properties - private smartServeRef?: plugins.SmartServe; - private smartServeConnectionWrappers: Map = new Map(); + peer.tags.add(data.name); + peer.data.set(`${TAG_PREFIX}${data.name}`, data.payload); - public get eventSubject(): plugins.smartrx.rxjs.Subject { - if (this.smartServeRef) { - // SmartServe doesn't provide an eventSubject, return a new Subject - // In SmartServe mode, connection events are handled via onConnectionOpen/onConnectionClose hooks - console.warn('eventSubject is not fully supported in SmartServe mode. Use SmartServe hooks instead.'); - return new plugins.smartrx.rxjs.Subject(); - } - return this.socketServerOrClient.eventSubject; + return { success: true }; + }) + ); + + // Remove tag handler + typedRouter.addTypedHandler( + new plugins.typedrequest.TypedHandler('__typedsocket_removeTag', async (data, meta) => { + const peer = meta?.localData?.peer as plugins.IWebSocketPeer; + if (!peer) { + console.warn('removeTag: No peer found in request context'); + return { success: false }; + } + + peer.tags.delete(data.name); + peer.data.delete(`${TAG_PREFIX}${data.name}`); + + return { success: true }; + }) + ); } - private postMethod: plugins.typedrequest.IPostMethod & - (( - typedRequestPostObject: plugins.typedrequestInterfaces.ITypedRequest, - socketConnectionArg?: plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper - ) => Promise); - private socketServerOrClient: - | plugins.smartsocket.Smartsocket - | plugins.smartsocket.SmartsocketClient; - constructor( + + // ============================================================================ + // INSTANCE PROPERTIES + // ============================================================================ + + public readonly side: TTypedSocketSide; + public readonly typedrouter: plugins.typedrequest.TypedRouter; + + // Connection status observable + public statusSubject = new plugins.smartrx.rxjs.Subject(); + private connectionStatus: TConnectionStatus = 'new'; + + // Client-specific properties + private websocket: WebSocket | null = null; + private clientOptions: Required | null = null; + private serverUrl: string = ''; + private retryCount = 0; + private currentBackoff = 1000; + private pendingRequests = new Map void; + reject: (error: Error) => void; + }>(); + + // Server-specific properties (SmartServe mode) + private smartServeRef: plugins.SmartServe | null = null; + private smartServeConnectionWrappers = new Map(); + + // ============================================================================ + // CONSTRUCTOR + // ============================================================================ + + private constructor( sideArg: TTypedSocketSide, - typedrouterArg: plugins.typedrequest.TypedRouter, - postMethodArg: plugins.typedrequest.IPostMethod, - socketServerOrClientArg: plugins.smartsocket.Smartsocket | plugins.smartsocket.SmartsocketClient + typedrouterArg: plugins.typedrequest.TypedRouter ) { this.side = sideArg; this.typedrouter = typedrouterArg; - this.postMethod = postMethodArg; - this.socketServerOrClient = socketServerOrClientArg; } - public addTag( - nameArg: T['name'], - payloadArg: T['payload'] - ) { - if ( - this.side === 'client' && - this.socketServerOrClient instanceof plugins.smartsocket.SmartsocketClient - ) { - this.socketServerOrClient.socketConnection.addTag({ - id: nameArg, - payload: payloadArg, - }); - } else { - throw new Error('tagging is only supported on clients'); + // ============================================================================ + // CLIENT METHODS + // ============================================================================ + + /** + * Connects the client to the server using native WebSocket + */ + private async connect(): Promise { + const done = plugins.smartpromise.defer(); + + this.updateStatus('connecting'); + + // Convert HTTP URL to WebSocket URL + const wsUrl = this.toWebSocketUrl(this.serverUrl); + console.log(`TypedSocket connecting to ${wsUrl}...`); + + this.websocket = new WebSocket(wsUrl); + + const connectionTimeout = setTimeout(() => { + if (this.connectionStatus !== 'connected') { + console.warn('TypedSocket connection timeout'); + this.websocket?.close(); + done.reject(new Error('Connection timeout')); + } + }, 10000); + + this.websocket.onopen = () => { + clearTimeout(connectionTimeout); + console.log('TypedSocket connected!'); + this.updateStatus('connected'); + this.retryCount = 0; + this.currentBackoff = this.clientOptions?.initialBackoffMs ?? 1000; + done.resolve(); + }; + + this.websocket.onmessage = async (event) => { + await this.handleMessage(event.data); + }; + + this.websocket.onclose = () => { + clearTimeout(connectionTimeout); + this.handleDisconnect(); + }; + + this.websocket.onerror = (error) => { + console.error('TypedSocket WebSocket error:', error); + }; + + try { + await done.promise; + } catch (err) { + clearTimeout(connectionTimeout); + if (this.clientOptions?.autoReconnect) { + await this.scheduleReconnect(); + } else { + throw err; + } } } + /** + * Converts an HTTP(S) URL to a WebSocket URL + */ + private toWebSocketUrl(url: string): string { + const parsed = new URL(url); + const wsProtocol = parsed.protocol === 'https:' ? 'wss:' : 'ws:'; + return `${wsProtocol}//${parsed.host}${parsed.pathname}`; + } + + /** + * Handles incoming WebSocket messages + */ + private async handleMessage(data: string | ArrayBuffer): Promise { + try { + const messageText = typeof data === 'string' ? data : new TextDecoder().decode(data); + const message = plugins.smartjson.parse(messageText) as plugins.typedrequestInterfaces.ITypedRequest; + + // Check if this is a response to a pending request + if (message.correlation?.id && this.pendingRequests.has(message.correlation.id)) { + const pending = this.pendingRequests.get(message.correlation.id)!; + this.pendingRequests.delete(message.correlation.id); + pending.resolve(message); + return; + } + + // Server-initiated request - route through TypedRouter + const response = await this.typedrouter.routeAndAddResponse(message); + if (response && this.websocket?.readyState === WebSocket.OPEN) { + this.websocket.send(plugins.smartjson.stringify(response)); + } + } catch (err) { + console.error('TypedSocket failed to process message:', err); + } + } + + /** + * Handles WebSocket disconnection + */ + private handleDisconnect(): void { + if (this.connectionStatus === 'disconnected') { + return; // Already handled + } + + this.updateStatus('disconnected'); + + if (this.clientOptions?.autoReconnect && this.retryCount < this.clientOptions.maxRetries) { + this.scheduleReconnect(); + } + } + + /** + * Schedules a reconnection attempt with exponential backoff + */ + private async scheduleReconnect(): Promise { + if (!this.clientOptions) return; + + this.updateStatus('reconnecting'); + this.retryCount++; + + // Exponential backoff with jitter + const jitter = this.currentBackoff * 0.2 * (Math.random() * 2 - 1); + const delay = Math.min(this.currentBackoff + jitter, this.clientOptions.maxBackoffMs); + + console.log(`TypedSocket reconnecting in ${Math.round(delay)}ms (attempt ${this.retryCount}/${this.clientOptions.maxRetries})`); + + await plugins.smartdelay.delayFor(delay); + + // Increase backoff for next time + this.currentBackoff = Math.min(this.currentBackoff * 2, this.clientOptions.maxBackoffMs); + + try { + await this.connect(); + } catch (err) { + console.error('TypedSocket reconnection failed:', err); + } + } + + /** + * Updates connection status and notifies subscribers + */ + private updateStatus(status: TConnectionStatus): void { + if (this.connectionStatus !== status) { + this.connectionStatus = status; + this.statusSubject.next(status); + } + } + + /** + * Sends a request to the server and waits for response (client-side) + */ + private async sendRequest( + request: T + ): Promise { + if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) { + throw new Error('WebSocket not connected'); + } + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + this.pendingRequests.delete(request.correlation.id); + reject(new Error('Request timeout')); + }, 30000); + + this.pendingRequests.set(request.correlation.id, { + resolve: (response) => { + clearTimeout(timeout); + resolve(response); + }, + reject: (error) => { + clearTimeout(timeout); + reject(error); + }, + }); + + this.websocket!.send(plugins.smartjson.stringify(request)); + }); + } + + // ============================================================================ + // PUBLIC API - SHARED + // ============================================================================ + + /** + * Creates a TypedRequest for the specified method. + * On clients, sends to the server. + * On servers, sends to the specified target connection. + */ public createTypedRequest( methodName: T['method'], - targetConnection?: plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper + targetConnection?: ISmartServeConnectionWrapper ): plugins.typedrequest.TypedRequest { - const typedrequest = new plugins.typedrequest.TypedRequest( - new plugins.typedrequest.TypedTarget({ - postMethod: async (requestDataArg) => { - const result = await this.postMethod(requestDataArg, targetConnection as any); - return result; - }, - }), + const postMethod = async (requestDataArg: T): Promise => { + if (this.side === 'client') { + return this.sendRequest(requestDataArg); + } + + // Server-side: send to target connection + if (!this.smartServeRef) { + throw new Error('Server not initialized'); + } + + let target = targetConnection; + if (!target) { + const allConnections = this.smartServeRef.getWebSocketConnections(); + if (allConnections.length === 1) { + const peer = allConnections[0]; + target = this.getOrCreateWrapper(peer); + } else if (allConnections.length === 0) { + throw new Error('No WebSocket connections available'); + } else { + throw new Error('Multiple connections available - specify targetConnection'); + } + } + + // Register interest for response + const interest = await this.typedrouter.fireEventInterestMap.addInterest( + requestDataArg.correlation.id, + requestDataArg + ); + + // Send request + target.peer.send(plugins.smartjson.stringify(requestDataArg)); + + // Wait for response + return await interest.interestFullfilled as T; + }; + + return new plugins.typedrequest.TypedRequest( + new plugins.typedrequest.TypedTarget({ postMethod }), methodName ); - return typedrequest; } /** - * returns all matching target connections - * Works with both Smartsocket and SmartServe backends - * @param asyncFindFuncArg - async filter function - * @returns array of matching connections + * Gets the current connection status + */ + public getStatus(): TConnectionStatus { + return this.connectionStatus; + } + + /** + * Stops the TypedSocket client or cleans up server state + */ + public async stop(): Promise { + if (this.side === 'client') { + if (this.clientOptions) { + this.clientOptions.autoReconnect = false; + } + if (this.websocket) { + this.websocket.close(); + this.websocket = null; + } + this.pendingRequests.clear(); + } else { + // Server mode - just clear wrappers (SmartServe manages its own lifecycle) + this.smartServeConnectionWrappers.clear(); + } + } + + // ============================================================================ + // CLIENT-ONLY METHODS + // ============================================================================ + + /** + * Sets a tag on this client connection. + * Tags are stored on the server and can be used for filtering. + * @client-only + */ + public async setTag( + name: T['name'], + payload: T['payload'] + ): Promise { + if (this.side !== 'client') { + throw new Error('setTag is only available on clients'); + } + + const request = this.createTypedRequest('__typedsocket_setTag'); + const response = await request.fire({ name, payload }); + + if (!response.success) { + throw new Error('Failed to set tag on server'); + } + } + + /** + * Removes a tag from this client connection. + * @client-only + */ + public async removeTag(name: string): Promise { + if (this.side !== 'client') { + throw new Error('removeTag is only available on clients'); + } + + const request = this.createTypedRequest('__typedsocket_removeTag'); + const response = await request.fire({ name }); + + if (!response.success) { + throw new Error('Failed to remove tag on server'); + } + } + + // ============================================================================ + // SERVER-ONLY METHODS + // ============================================================================ + + /** + * Gets or creates a connection wrapper for a peer + */ + private getOrCreateWrapper(peer: plugins.IWebSocketPeer): ISmartServeConnectionWrapper { + let wrapper = this.smartServeConnectionWrappers.get(peer.id); + if (!wrapper) { + wrapper = wrapSmartServePeer(peer); + this.smartServeConnectionWrappers.set(peer.id, wrapper); + } + return wrapper; + } + + /** + * Finds all connections matching the filter function. + * @server-only */ public async findAllTargetConnections( - asyncFindFuncArg: (connectionArg: plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper) => Promise - ): Promise<(plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper)[]> { - // SmartServe mode - if (this.smartServeRef) { - const matchingConnections: ISmartServeConnectionWrapper[] = []; - for (const peer of this.smartServeRef.getWebSocketConnections()) { - let wrapper = this.smartServeConnectionWrappers.get(peer.id); - if (!wrapper) { - wrapper = wrapSmartServePeer(peer); - this.smartServeConnectionWrappers.set(peer.id, wrapper); - } - if (await asyncFindFuncArg(wrapper)) { - matchingConnections.push(wrapper); - } - } - return matchingConnections; + asyncFindFuncArg: (connectionArg: ISmartServeConnectionWrapper) => Promise + ): Promise { + if (this.side !== 'server' || !this.smartServeRef) { + throw new Error('findAllTargetConnections is only available on servers'); } - // Smartsocket mode - if (this.socketServerOrClient instanceof plugins.smartsocket.Smartsocket) { - const matchingSockets: plugins.smartsocket.SocketConnection[] = []; - for (const socketConnection of this.socketServerOrClient.socketConnections.getArray()) { - if (await asyncFindFuncArg(socketConnection)) { - matchingSockets.push(socketConnection); - } + const matchingConnections: ISmartServeConnectionWrapper[] = []; + for (const peer of this.smartServeRef.getWebSocketConnections()) { + const wrapper = this.getOrCreateWrapper(peer); + if (await asyncFindFuncArg(wrapper)) { + matchingConnections.push(wrapper); } - return matchingSockets; } - - throw new Error('this method >>findTargetConnection<< is only available from the server'); + return matchingConnections; } /** - * returns a single target connection by returning the first one of all matching ones - * @param asyncFindFuncArg - * @returns + * Finds the first connection matching the filter function. + * @server-only */ public async findTargetConnection( - asyncFindFuncArg: (connectionArg: plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper) => Promise - ): Promise { + asyncFindFuncArg: (connectionArg: ISmartServeConnectionWrapper) => Promise + ): Promise { const allMatching = await this.findAllTargetConnections(asyncFindFuncArg); return allMatching[0]; } /** - * Find all connections that have a specific tag - * Works with both Smartsocket and SmartServe backends + * Finds all connections with the specified tag. + * @server-only */ - public async findAllTargetConnectionsByTag< - TTag extends plugins.typedrequestInterfaces.ITag = any - >(keyArg: TTag['name'], payloadArg?: TTag['payload']): Promise<(plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper)[]> { - // SmartServe mode - use native filtering for better performance - if (this.smartServeRef) { - const peers = this.smartServeRef.getWebSocketConnectionsByTag(keyArg); - const results: ISmartServeConnectionWrapper[] = []; - - for (const peer of peers) { - let wrapper = this.smartServeConnectionWrappers.get(peer.id); - if (!wrapper) { - wrapper = wrapSmartServePeer(peer); - this.smartServeConnectionWrappers.set(peer.id, wrapper); - } - - // If payload specified, also filter by payload stored in peer.data - if (payloadArg !== undefined) { - const tag = await wrapper.getTagById(keyArg); - if (plugins.smartjson.stringify(tag?.payload) !== plugins.smartjson.stringify(payloadArg)) { - continue; - } - } - results.push(wrapper); - } - return results; + public async findAllTargetConnectionsByTag( + keyArg: TTag['name'], + payloadArg?: TTag['payload'] + ): Promise { + if (this.side !== 'server' || !this.smartServeRef) { + throw new Error('findAllTargetConnectionsByTag is only available on servers'); } - // Smartsocket mode - use existing logic - return this.findAllTargetConnections(async (socketConnectionArg) => { - let result: boolean; - if (!payloadArg) { - result = !!(await (socketConnectionArg as plugins.smartsocket.SocketConnection).getTagById(keyArg)); - } else { - result = !!( - plugins.smartjson.stringify((await (socketConnectionArg as plugins.smartsocket.SocketConnection).getTagById(keyArg))?.payload) === - plugins.smartjson.stringify(payloadArg) - ); + const peers = this.smartServeRef.getWebSocketConnectionsByTag(keyArg); + const results: ISmartServeConnectionWrapper[] = []; + + for (const peer of peers) { + const wrapper = this.getOrCreateWrapper(peer); + + // If payload specified, also filter by payload + if (payloadArg !== undefined) { + const tag = await wrapper.getTagById(keyArg); + if (plugins.smartjson.stringify(tag?.payload) !== plugins.smartjson.stringify(payloadArg)) { + continue; + } } - return result; - }); + results.push(wrapper); + } + return results; } /** - * Find a single connection by tag + * Finds the first connection with the specified tag. + * @server-only */ public async findTargetConnectionByTag( keyArg: TTag['name'], payloadArg?: TTag['payload'] - ): Promise { + ): Promise { const allResults = await this.findAllTargetConnectionsByTag(keyArg, payloadArg); return allResults[0]; } - - /** - * Stop the TypedSocket server/client - * Note: In SmartServe mode, SmartServe manages its own lifecycle - */ - public async stop() { - if (this.smartServeRef) { - // SmartServe manages its own lifecycle - // Clear our connection wrappers - this.smartServeConnectionWrappers.clear(); - return; - } - await this.socketServerOrClient.stop(); - } } diff --git a/ts/typedsocket.plugins.ts b/ts/typedsocket.plugins.ts index e5bd5da..9518e04 100644 --- a/ts/typedsocket.plugins.ts +++ b/ts/typedsocket.plugins.ts @@ -6,13 +6,14 @@ export { typedrequest, typedrequestInterfaces }; // @pushrocks scope import * as isohash from '@push.rocks/isohash'; +import * as smartdelay from '@push.rocks/smartdelay'; import * as smartjson from '@push.rocks/smartjson'; +import * as smartpromise from '@push.rocks/smartpromise'; import * as smartrx from '@push.rocks/smartrx'; -import * as smartsocket from '@push.rocks/smartsocket'; import * as smartstring from '@push.rocks/smartstring'; import * as smarturl from '@push.rocks/smarturl'; -export { isohash, smartjson, smartrx, smartsocket, smartstring, smarturl }; +export { isohash, smartdelay, smartjson, smartpromise, smartrx, smartstring, smarturl }; -// Optional SmartServe support (type-only imports for optional peer dependency) +// SmartServe - required for server-side WebSocket support export type { SmartServe, IWebSocketPeer } from '@push.rocks/smartserve';