From eaa6da005b1fc9b268d18dfd7e29794a01e2801b Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Wed, 17 Sep 2025 08:31:44 -0700 Subject: [PATCH] sdk/client: add optional dedicated connections and lifetime to connectRPCObject --- packages/client/package-lock.json | 228 +++++++++--------- packages/client/package.json | 2 +- packages/client/src/index.ts | 143 ++++++----- sdk/package-lock.json | 4 +- sdk/package.json | 2 +- sdk/types/package-lock.json | 4 +- sdk/types/package.json | 2 +- .../scrypted_python/scrypted_sdk/types.py | 2 +- sdk/types/src/types.input.ts | 9 +- 9 files changed, 220 insertions(+), 176 deletions(-) diff --git a/packages/client/package-lock.json b/packages/client/package-lock.json index 4ed8a659c..0c9e2fb17 100644 --- a/packages/client/package-lock.json +++ b/packages/client/package-lock.json @@ -21,7 +21,7 @@ "typescript": "^5.8.3" }, "peerDependencies": { - "@scrypted/types": "^0.5.37" + "@scrypted/types": "^0.5.41" } }, "node_modules/@cspotcode/source-map-support": { @@ -37,6 +37,27 @@ "node": ">=12" } }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -65,9 +86,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, @@ -83,60 +104,15 @@ } }, "node_modules/@scrypted/types": { - "version": "0.5.37", - "resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.5.37.tgz", - "integrity": "sha512-RksclqyAN4c5Mm1hndeGIXVm1p9o80VPoYspGatkSGznoG6I2TMklm8uyiZCvdTYAbeH9GWffEmf5u631xcN8g==", + "version": "0.5.41", + "resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.5.41.tgz", + "integrity": "sha512-XaTDV8vYMCC7pflG2a/G0vaxyegWm9+ov3w1f/TvWvZI+EwDKxwdvUSG4u3F9B5wlgOjKkirwqd+KgeyE88yCQ==", "license": "ISC", "peer": true, "dependencies": { "openai": "^5.3.0" } }, - "node_modules/@scrypted/types/node_modules/openai": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-5.3.0.tgz", - "integrity": "sha512-VIKmoF7y4oJCDOwP/oHXGzM69+x0dpGFmN9QmYO+uPbLFOmmnwO+x1GbsgUtI+6oraxomGZ566Y421oYVu191w==", - "license": "Apache-2.0", - "peer": true, - "bin": { - "openai": "bin/cli" - }, - "peerDependencies": { - "ws": "^8.18.0", - "zod": "^3.23.8" - }, - "peerDependenciesMeta": { - "ws": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/@scrypted/types/node_modules/ws": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", - "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", @@ -182,13 +158,13 @@ } }, "node_modules/@types/node": { - "version": "24.0.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz", - "integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==", + "version": "24.5.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.1.tgz", + "integrity": "sha512-/SQdmUP2xa+1rdx7VwB9yPq8PaKej8TD5cQ+XfKDPWWC+VDJU4rvVVagXqKUzhKjtFoNA8rXDJAkCxQPAe00+Q==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.8.0" + "undici-types": "~7.12.0" } }, "node_modules/@types/ws": { @@ -202,9 +178,9 @@ } }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -228,9 +204,9 @@ } }, "node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", "engines": { "node": ">=12" @@ -240,9 +216,9 @@ } }, "node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", "engines": { "node": ">=12" @@ -258,21 +234,6 @@ "dev": true, "license": "MIT" }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -364,6 +325,27 @@ "xmlhttprequest-ssl": "~2.1.1" } }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/engine.io-parser": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", @@ -374,9 +356,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", @@ -410,14 +392,14 @@ } }, "node_modules/glob": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", - "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", "license": "ISC", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" @@ -448,9 +430,9 @@ "license": "ISC" }, "node_modules/jackspeak": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", - "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -463,9 +445,9 @@ } }, "node_modules/lru-cache": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", - "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.1.tgz", + "integrity": "sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==", "license": "ISC", "engines": { "node": "20 || >=22" @@ -479,12 +461,12 @@ "license": "ISC" }, "node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { "node": "20 || >=22" @@ -508,6 +490,28 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/openai": { + "version": "5.20.3", + "resolved": "https://registry.npmjs.org/openai/-/openai-5.20.3.tgz", + "integrity": "sha512-8V0KgAcPFppDIP8uMBOkhRrhDBuxNQYQxb9IovP4NN4VyaYGISAzYexyYYuAwVul2HB75Wpib0xDboYJqRMNow==", + "license": "Apache-2.0", + "peer": true, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -651,9 +655,9 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -732,9 +736,9 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", "bin": { @@ -746,9 +750,9 @@ } }, "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", + "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", "dev": true, "license": "MIT" }, @@ -866,10 +870,12 @@ } }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=10.0.0" }, diff --git a/packages/client/package.json b/packages/client/package.json index 8949e514c..2e9552233 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -19,7 +19,7 @@ "typescript": "^5.8.3" }, "peerDependencies": { - "@scrypted/types": "^0.5.37" + "@scrypted/types": "^0.5.41" }, "dependencies": { "engine.io-client": "^6.6.3", diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 5e090a136..d4e653abc 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -1,4 +1,4 @@ -import { MediaObjectCreateOptions, ScryptedStatic } from "@scrypted/types"; +import { MediaObjectCreateOptions, ScryptedStatic, ConnectRPCObjectOptions } from "@scrypted/types"; import * as eio from 'engine.io-client'; import { SocketOptions } from 'engine.io-client'; import { timeoutPromise } from "../../../common/src/promise-utils"; @@ -573,62 +573,77 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro .find(device => device.pluginId === '@scrypted/core' && device.nativeId === `user:${username}`); const clusterPeers = new Map>(); - const ensureClusterPeer = (clusterObject: ClusterObject) => { - let clusterPeerPromise = clusterPeers.get(clusterObject.port); - if (!clusterPeerPromise) { - clusterPeerPromise = (async () => { - const eioPath = 'engine.io/connectRPCObject'; - const eioEndpoint = baseUrl ? new URL(eioPath, baseUrl).pathname : '/' + eioPath; - const clusterPeerOptions = { - path: eioEndpoint, - query: { - cacehBust, - clusterObject: JSON.stringify(clusterObject), - }, - withCredentials: true, - extraHeaders, - rejectUnauthorized: false, - transports: options?.transports, - }; + const finalizationRegistry = new FinalizationRegistry((clusterPeer: RpcPeer) => { + clusterPeer.kill('object finalized'); + }); + const ensureClusterPeer = (clusterObject: ClusterObject, connectRPCObjectOptions?: ConnectRPCObjectOptions) => { + // If dedicatedTransport is true, don't reuse existing cluster peers + if (!connectRPCObjectOptions?.dedicatedTransport) { + let clusterPeerPromise = clusterPeers.get(clusterObject.port); + if (clusterPeerPromise) + return clusterPeerPromise; + } - const clusterPeerSocket = new eio.Socket(explicitBaseUrl, clusterPeerOptions); - let peerReady = false; - clusterPeerSocket.on('close', () => { + const clusterPeerPromise = (async () => { + const eioPath = 'engine.io/connectRPCObject'; + const eioEndpoint = baseUrl ? new URL(eioPath, baseUrl).pathname : '/' + eioPath; + const clusterPeerOptions = { + path: eioEndpoint, + query: { + cacehBust, + clusterObject: JSON.stringify(clusterObject), + }, + withCredentials: true, + extraHeaders, + rejectUnauthorized: false, + transports: options?.transports, + }; + + const clusterPeerSocket = new eio.Socket(explicitBaseUrl, clusterPeerOptions); + let peerReady = false; + clusterPeerSocket.on('close', () => { + // Only remove from clusterPeers if it's not a dedicated transport + if (!connectRPCObjectOptions?.dedicatedTransport) { clusterPeers.delete(clusterObject.port); - if (!peerReady) { - throw new Error("peer disconnected before setup completed"); + } + if (!peerReady) { + throw new Error("peer disconnected before setup completed"); + } + }); + + try { + await once(clusterPeerSocket, 'open'); + + const serializer = createRpcDuplexSerializer({ + write: data => clusterPeerSocket.send(data), + }); + clusterPeerSocket.on('message', data => serializer.onData(Buffer.from(data))); + + const clusterPeer = new RpcPeer(clientName || 'engine.io-client', "cluster-proxy", (message, reject, serializationContext) => { + try { + serializer.sendMessage(message, reject, serializationContext); + } + catch (e) { + reject?.(e as Error); } }); + serializer.setupRpcPeer(clusterPeer); + clusterPeer.tags.localPort = sourcePeerId; + peerReady = true; + return clusterPeer; + } + catch (e) { + console.error('failure ipc connect', e); + clusterPeerSocket.close(); + throw e; + } + })(); - try { - await once(clusterPeerSocket, 'open'); - - const serializer = createRpcDuplexSerializer({ - write: data => clusterPeerSocket.send(data), - }); - clusterPeerSocket.on('message', data => serializer.onData(Buffer.from(data))); - - const clusterPeer = new RpcPeer(clientName || 'engine.io-client', "cluster-proxy", (message, reject, serializationContext) => { - try { - serializer.sendMessage(message, reject, serializationContext); - } - catch (e) { - reject?.(e as Error); - } - }); - serializer.setupRpcPeer(clusterPeer); - clusterPeer.tags.localPort = sourcePeerId; - peerReady = true; - return clusterPeer; - } - catch (e) { - console.error('failure ipc connect', e); - clusterPeerSocket.close(); - throw e; - } - })(); + // Only store in clusterPeers if it's not a dedicated transport + if (!connectRPCObjectOptions?.dedicatedTransport) { clusterPeers.set(clusterObject.port, clusterPeerPromise); } + return clusterPeerPromise; }; @@ -642,7 +657,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro return null; } - const connectRPCObject = async (value: any) => { + const connectRPCObject = async (value: any, options?: ConnectRPCObjectOptions) => { const clusterObject: ClusterObject = value?.__cluster; if (!clusterObject) { return value; @@ -657,13 +672,29 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro } try { - const clusterPeerPromise = ensureClusterPeer(clusterObject); + const clusterPeerPromise = ensureClusterPeer(clusterObject, options); const clusterPeer = await clusterPeerPromise; const connectRPCObject: ConnectRPCObject = await clusterPeer.getParam('connectRPCObject'); - const newValue = await connectRPCObject(clusterObject); - if (!newValue) - throw new Error('ipc object not found?'); - return newValue; + try { + const newValue = await connectRPCObject(clusterObject); + if (!newValue) + throw new Error('ipc object not found?'); + + // If dedicatedTransport is true, register the object for cleanup + if (options?.dedicatedTransport) { + finalizationRegistry.register(newValue, clusterPeer); + } + + return newValue; + } + catch (e) { + // If we have a clusterPeer and this is a dedicated transport, kill the connection + // to prevent resource leaks when connectRPCObject fails + if (options?.dedicatedTransport) { + clusterPeer.kill('connectRPCObject failed'); + } + throw e; + } } catch (e) { console.error('failure ipc', e); diff --git a/sdk/package-lock.json b/sdk/package-lock.json index f420f8fab..a1ad54d5e 100644 --- a/sdk/package-lock.json +++ b/sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/sdk", - "version": "0.5.40", + "version": "0.5.44", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@scrypted/sdk", - "version": "0.5.40", + "version": "0.5.44", "license": "ISC", "dependencies": { "@babel/preset-typescript": "^7.27.1", diff --git a/sdk/package.json b/sdk/package.json index be90bdede..48eefdfed 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/sdk", - "version": "0.5.40", + "version": "0.5.44", "description": "", "main": "dist/src/index.js", "exports": { diff --git a/sdk/types/package-lock.json b/sdk/types/package-lock.json index c31dbc09c..c6179f4d3 100644 --- a/sdk/types/package-lock.json +++ b/sdk/types/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/types", - "version": "0.5.38", + "version": "0.5.41", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/types", - "version": "0.5.38", + "version": "0.5.41", "license": "ISC", "dependencies": { "openai": "^5.3.0" diff --git a/sdk/types/package.json b/sdk/types/package.json index 34e1b9070..4b2a21e2f 100644 --- a/sdk/types/package.json +++ b/sdk/types/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/types", - "version": "0.5.38", + "version": "0.5.41", "description": "", "main": "dist/index.js", "author": "", diff --git a/sdk/types/scrypted_python/scrypted_sdk/types.py b/sdk/types/scrypted_python/scrypted_sdk/types.py index c89e149f2..8ed1e1bd0 100644 --- a/sdk/types/scrypted_python/scrypted_sdk/types.py +++ b/sdk/types/scrypted_python/scrypted_sdk/types.py @@ -1088,7 +1088,7 @@ class TamperState(TypedDict): pass -TYPES_VERSION = "0.5.38" +TYPES_VERSION = "0.5.41" class AirPurifier: diff --git a/sdk/types/src/types.input.ts b/sdk/types/src/types.input.ts index dcbeabfb6..8c2c5cfff 100644 --- a/sdk/types/src/types.input.ts +++ b/sdk/types/src/types.input.ts @@ -2837,6 +2837,13 @@ export interface ClusterManager { getClusterWorkers(): Promise>; } +export interface ConnectRPCObjectOptions { + dedicatedTransport?: { + receiveTimeout?: number; + sendTimeout?: number; + }; +} + export interface ScryptedStatic { /** * @deprecated @@ -2871,5 +2878,5 @@ export interface ScryptedStatic { * through the Scrypted Server which typically manages plugin communication. * This is ideal for sending large amounts of data. */ - connectRPCObject(value: T): Promise; + connectRPCObject(value: T, options?: ConnectRPCObjectOptions): Promise; }