feat(typedsocket): Add SmartServe integration, tagging and improved client reconnect/backoff; update deps and tests

This commit is contained in:
2025-12-04 00:00:38 +00:00
parent cace6ca85a
commit 632045edd9
7 changed files with 739 additions and 480 deletions

View File

@@ -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

View File

@@ -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"

325
pnpm-lock.yaml generated
View File

@@ -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:

View File

@@ -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<IRequest_Client_Server>(
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<IRequest_Client_Server>(
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<IRequest_Client_Server>('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<IRequest_Client_Server>('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<IRequest_Client_Server>('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();

View File

@@ -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.'
}

View File

@@ -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<string> 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<TypedSocket> {
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 <T extends plugins.typedrequestInterfaces.ITypedRequest>(
dataArg: T,
targetConnectionArg?: plugins.smartsocket.SocketConnection
): Promise<T> => {
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<TypedSocket> {
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<ITypedSocketClientOptions> = {
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 <T extends plugins.typedrequestInterfaces.ITypedRequest>(dataArg: T): Promise<T> => {
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<string, ISmartServeConnectionWrapper>();
// Create the postMethod for server-initiated requests
const postMethod = async <T extends plugins.typedrequestInterfaces.ITypedRequest>(
dataArg: T,
targetConnectionArg?: ISmartServeConnectionWrapper
): Promise<T> => {
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<IReq_SetClientTag>(
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<string, ISmartServeConnectionWrapper> = new Map();
peer.tags.add(data.name);
peer.data.set(`${TAG_PREFIX}${data.name}`, data.payload);
public get eventSubject(): plugins.smartrx.rxjs.Subject<plugins.smartsocket.TConnectionStatus> {
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<IReq_RemoveClientTag>(
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<plugins.typedrequestInterfaces.ITypedRequest>);
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<TConnectionStatus>();
private connectionStatus: TConnectionStatus = 'new';
// Client-specific properties
private websocket: WebSocket | null = null;
private clientOptions: Required<ITypedSocketClientOptions> | null = null;
private serverUrl: string = '';
private retryCount = 0;
private currentBackoff = 1000;
private pendingRequests = new Map<string, {
resolve: (response: any) => void;
reject: (error: Error) => void;
}>();
// Server-specific properties (SmartServe mode)
private smartServeRef: plugins.SmartServe | null = null;
private smartServeConnectionWrappers = new Map<string, ISmartServeConnectionWrapper>();
// ============================================================================
// 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<T extends plugins.typedrequestInterfaces.ITag = any>(
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<void> {
const done = plugins.smartpromise.defer<void>();
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<void> {
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<void> {
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<T extends plugins.typedrequestInterfaces.ITypedRequest>(
request: T
): Promise<T> {
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<T extends plugins.typedrequestInterfaces.ITypedRequest>(
methodName: T['method'],
targetConnection?: plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper
targetConnection?: ISmartServeConnectionWrapper
): plugins.typedrequest.TypedRequest<T> {
const typedrequest = new plugins.typedrequest.TypedRequest<T>(
new plugins.typedrequest.TypedTarget({
postMethod: async (requestDataArg) => {
const result = await this.postMethod(requestDataArg, targetConnection as any);
return result;
},
}),
const postMethod = async (requestDataArg: T): Promise<T> => {
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<T>(
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<void> {
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<T extends plugins.typedrequestInterfaces.ITag>(
name: T['name'],
payload: T['payload']
): Promise<void> {
if (this.side !== 'client') {
throw new Error('setTag is only available on clients');
}
const request = this.createTypedRequest<IReq_SetClientTag>('__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<void> {
if (this.side !== 'client') {
throw new Error('removeTag is only available on clients');
}
const request = this.createTypedRequest<IReq_RemoveClientTag>('__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<boolean>
): 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<boolean>
): Promise<ISmartServeConnectionWrapper[]> {
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<boolean>
): Promise<plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper | undefined> {
asyncFindFuncArg: (connectionArg: ISmartServeConnectionWrapper) => Promise<boolean>
): Promise<ISmartServeConnectionWrapper | undefined> {
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<TTag extends plugins.typedrequestInterfaces.ITag = any>(
keyArg: TTag['name'],
payloadArg?: TTag['payload']
): Promise<ISmartServeConnectionWrapper[]> {
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<TTag extends plugins.typedrequestInterfaces.ITag = any>(
keyArg: TTag['name'],
payloadArg?: TTag['payload']
): Promise<plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper | undefined> {
): Promise<ISmartServeConnectionWrapper | undefined> {
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();
}
}

View File

@@ -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';