From d2835de118b0754018805ac0ee08e1888f74ac5e Mon Sep 17 00:00:00 2001 From: Hoenhaim <> Date: Fri, 26 Dec 2025 11:43:29 +0200 Subject: [PATCH 1/3] nodes cache / getWsUrls --- package.json | 4 +- src/AccessToken.browser.ts | 6 ++- src/AccessToken.ts | 107 +++++++++++++++++++++++++++++++++---- 3 files changed, 105 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index b5c7e258..483ff57e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dtelecom/server-sdk-js", - "version": "2.0.12", + "version": "2.0.13", "description": "Server-side SDK", "main": "./dist/index.js", "module": "./dist/index.js", @@ -75,4 +75,4 @@ "typedoc": "^0.28.2", "typescript": "5.7.x" } -} \ No newline at end of file +} diff --git a/src/AccessToken.browser.ts b/src/AccessToken.browser.ts index 1b0e1293..d1e9ccee 100644 --- a/src/AccessToken.browser.ts +++ b/src/AccessToken.browser.ts @@ -44,6 +44,10 @@ export class AccessToken { async getWsUrl(clientIp?: string): Promise { throw new Error('AccessToken can only be used on the server side'); } + + async getWsUrls(clientIp?: string): Promise { + throw new Error('AccessToken can only be used on the server side'); + } } export class TokenVerifier { @@ -54,4 +58,4 @@ export class TokenVerifier { verify(token: string): ClaimGrants { throw new Error('TokenVerifier can only be used on the server side'); } -} \ No newline at end of file +} diff --git a/src/AccessToken.ts b/src/AccessToken.ts index c32780bb..7c82a41a 100644 --- a/src/AccessToken.ts +++ b/src/AccessToken.ts @@ -14,6 +14,23 @@ const isNode = typeof process !== 'undefined' && process.versions != null && pro // 6 hours const defaultTTL = 6 * 60 * 60; +// Default cache TTL +const defaultCacheTTL = 5 * 60 * 1000; + +// Response from /relevants endpoint +interface RelevantResponse { + domain: string; + ip: string; +} + +interface NodesCache { + nodes: IAllNodeResponseItem[]; + timestamp: number; +} + +// Global cache for available servers +let nodesCache: NodesCache | null = null; + export interface AccessTokenOptions { /** * amount of time before expiration @@ -165,15 +182,36 @@ export class AccessToken { * @returns wss url */ async getWsUrl(clientIp?: string): Promise { - let nodes = await getAllNode(); + const nodes = await this.getCachedNodes(); - nodes = nodes.sort(() => 0.5 - Math.random()); + if (nodes.length === 0) { + throw new Error('No available nodes found'); + } const address = await this.requestAddressForClient(nodes, clientIp); return address; } + /** + * @returns array of wss urls + */ + async getWsUrls(clientIp?: string): Promise { + const nodes = await this.getCachedNodes(); + + if (nodes.length === 0) { + throw new Error('No available nodes found'); + } + + if (!clientIp) { + return nodes.map((node: IAllNodeResponseItem) => `wss://${node.domain}`); + } + + const relevantNodes = await this.requestRelevantsForClient(nodes, clientIp); + + return relevantNodes.map((node: RelevantResponse) => `wss://${node.domain}`); + } + async requestAddressForClient(nodes: IAllNodeResponseItem[], clientIp?: string) { let address = ""; @@ -189,19 +227,70 @@ export class AccessToken { } for (const node of nodes) { - const response = await axios.get(`https://${node.domain}/relevant`, { - data: {ip: clientIp}, - timeout: 3000 - }).catch(() => null); - - if (response?.data?.domain) { - address = `wss://${response.data.domain}`; + const response = await axios.post( + `https://${node.domain}/relevants`, + { ip: clientIp }, + { + headers: { 'Content-Type': 'application/json' }, + timeout: 1000 + } + ).catch(() => null); + + if (response?.data && Array.isArray(response.data) && response.data.length > 0) { + address = `wss://${response.data[0].domain}`; break; } } return address; } + + /** + * Request relevant servers for client IP from /relevants endpoint + * @returns array of relevant servers sorted by proximity + */ + async requestRelevantsForClient(nodes: IAllNodeResponseItem[], clientIp: string): Promise { + for (const node of nodes) { + const response = await axios.post( + `https://${node.domain}/relevants`, + { ip: clientIp }, + { + headers: { 'Content-Type': 'application/json' }, + timeout: 1000 + } + ).catch(() => null); + + if (response?.data && Array.isArray(response.data) && response.data.length > 0) { + return response.data as RelevantResponse[]; + } + } + + return nodes.map((node: IAllNodeResponseItem) => ({ + domain: node.domain, + ip: '' + })); + } + + /** + * Get cached nodes or fetch fresh ones if cache is expired + * @returns array of available nodes + */ + private async getCachedNodes(): Promise { + const now = Date.now(); + + if (nodesCache && (now - nodesCache.timestamp) < defaultCacheTTL) { + return nodesCache.nodes; + } + + const nodes = await getAllNode(); + + nodesCache = { + nodes, + timestamp: now + }; + + return nodes; + } } export class TokenVerifier { From 71cd1ae7a5594076ddb500a8b690aa1abb8e61bf Mon Sep 17 00:00:00 2001 From: Hoenhaim <> Date: Fri, 26 Dec 2025 11:49:08 +0200 Subject: [PATCH 2/3] revert requestAddressForClient --- src/AccessToken.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/AccessToken.ts b/src/AccessToken.ts index 7c82a41a..697f5c2a 100644 --- a/src/AccessToken.ts +++ b/src/AccessToken.ts @@ -227,17 +227,13 @@ export class AccessToken { } for (const node of nodes) { - const response = await axios.post( - `https://${node.domain}/relevants`, - { ip: clientIp }, - { - headers: { 'Content-Type': 'application/json' }, - timeout: 1000 - } - ).catch(() => null); + const response = await axios.get(`https://${node.domain}/relevant`, { + data: { ip: clientIp }, + timeout: 3000 + }).catch(() => null); - if (response?.data && Array.isArray(response.data) && response.data.length > 0) { - address = `wss://${response.data[0].domain}`; + if (response?.data?.domain) { + address = `wss://${response.data.domain}`; break; } } From f532756c2f9abb2dc39a7281c01e7a344688c0a2 Mon Sep 17 00:00:00 2001 From: Hoenhaim <> Date: Fri, 26 Dec 2025 14:41:58 +0200 Subject: [PATCH 3/3] add ordered to server requests --- src/AccessToken.ts | 168 ++++++++++++++++++++++++++------------------- 1 file changed, 97 insertions(+), 71 deletions(-) diff --git a/src/AccessToken.ts b/src/AccessToken.ts index 697f5c2a..29228e36 100644 --- a/src/AccessToken.ts +++ b/src/AccessToken.ts @@ -14,18 +14,24 @@ const isNode = typeof process !== 'undefined' && process.versions != null && pro // 6 hours const defaultTTL = 6 * 60 * 60; -// Default cache TTL -const defaultCacheTTL = 5 * 60 * 1000; +const nodeRefreshInterval = 60 * 1000; // Response from /relevants endpoint interface RelevantResponse { domain: string; ip: string; + id?: string; + participants?: number; + country?: string; + city?: string; + latitude?: number; + longitude?: number; } interface NodesCache { nodes: IAllNodeResponseItem[]; - timestamp: number; + nodesOrdered: RelevantResponse[]; + lastRefreshTime: number; } // Global cache for available servers @@ -182,110 +188,130 @@ export class AccessToken { * @returns wss url */ async getWsUrl(clientIp?: string): Promise { - const nodes = await this.getCachedNodes(); + const addresses = await this.requestRelevantsForIp(clientIp); + const address = addresses[0]; - if (nodes.length === 0) { - throw new Error('No available nodes found'); + if (!address) { + throw new Error('Not found'); } - const address = await this.requestAddressForClient(nodes, clientIp); - - return address; + return `wss://${address}`; } /** * @returns array of wss urls */ async getWsUrls(clientIp?: string): Promise { - const nodes = await this.getCachedNodes(); - - if (nodes.length === 0) { - throw new Error('No available nodes found'); - } - if (!clientIp) { + const nodes = await this.listNodes(); return nodes.map((node: IAllNodeResponseItem) => `wss://${node.domain}`); } - const relevantNodes = await this.requestRelevantsForClient(nodes, clientIp); + const relevantNodes = await this.requestRelevantsForIp(clientIp); + return relevantNodes.map((domain) => `wss://${domain}`); + } - return relevantNodes.map((node: RelevantResponse) => `wss://${node.domain}`); + private async listNodes(): Promise { + await this.ensureCacheInitialized(); + return nodesCache?.nodes || []; } - async requestAddressForClient(nodes: IAllNodeResponseItem[], clientIp?: string) { - let address = ""; + private async getOrderedNodes(): Promise { + await this.ensureCacheInitialized(); - if (nodes.length < 1) { - console.error('Error requestAddressForClient nodes empty'); - return address; - } else { - address = `wss://${nodes[0].domain}`; + if (nodesCache && nodesCache.nodesOrdered.length > 0) { + return nodesCache.nodesOrdered.map(n => n.domain); } - if (!clientIp) { - return address; - } + const nodes = await this.listNodes(); + return nodes.map(n => n.domain); + } - for (const node of nodes) { - const response = await axios.get(`https://${node.domain}/relevant`, { - data: { ip: clientIp }, - timeout: 3000 - }).catch(() => null); + private async ensureCacheInitialized(): Promise { + if (!nodesCache) { + nodesCache = { + nodes: [], + nodesOrdered: [], + lastRefreshTime: 0 + }; - if (response?.data?.domain) { - address = `wss://${response.data.domain}`; - break; - } + await this.refresh(); + return; } - return address; + const now = Date.now(); + if (now - nodesCache.lastRefreshTime >= nodeRefreshInterval) { + await this.refresh() + } } - /** - * Request relevant servers for client IP from /relevants endpoint - * @returns array of relevant servers sorted by proximity - */ - async requestRelevantsForClient(nodes: IAllNodeResponseItem[], clientIp: string): Promise { - for (const node of nodes) { - const response = await axios.post( - `https://${node.domain}/relevants`, - { ip: clientIp }, - { - headers: { 'Content-Type': 'application/json' }, - timeout: 1000 + async refresh(): Promise { + try { + const nodes = await getAllNode(); + const now = Date.now(); + + if (!nodesCache) { + nodesCache = { + nodes: [], + nodesOrdered: [], + lastRefreshTime: now + }; + } + + nodesCache.nodes = nodes; + nodesCache.lastRefreshTime = now; + + const serverIp = process.env.SERVER_IP; + if (serverIp) { + const orderedNodes = await this.getOrderedNodes(); + const relevants = await this.fetchRelevants(orderedNodes, serverIp); + if (relevants.length > 0) { + nodesCache.nodesOrdered = relevants; } - ).catch(() => null); + } + } catch (error) { + console.error('Error refreshing nodes cache:', error); + } + } - if (response?.data && Array.isArray(response.data) && response.data.length > 0) { - return response.data as RelevantResponse[]; + private async fetchRelevants(domains: string[], ip: string): Promise { + for (const domain of domains) { + try { + const response = await axios.post( + `https://${domain}/relevants`, + { ip }, + { + headers: { 'Content-Type': 'application/json' }, + timeout: 1000 + } + ); + if (response?.data && Array.isArray(response.data) && response.data.length > 0) { + return response.data as RelevantResponse[]; + } + } catch (error) { + console.error(`Failed to fetch relevants from ${domain}:`, error); } } + return []; + } - return nodes.map((node: IAllNodeResponseItem) => ({ - domain: node.domain, - ip: '' - })); + static clearCache(): void { + nodesCache = null; } - /** - * Get cached nodes or fetch fresh ones if cache is expired - * @returns array of available nodes - */ - private async getCachedNodes(): Promise { - const now = Date.now(); + async requestRelevantsForIp(ip?: string): Promise { + const orderedNodes = await this.getOrderedNodes(); - if (nodesCache && (now - nodesCache.timestamp) < defaultCacheTTL) { - return nodesCache.nodes; + if (!ip) { + return orderedNodes; } - const nodes = await getAllNode(); - - nodesCache = { - nodes, - timestamp: now - }; + const relevants = await this.fetchRelevants(orderedNodes, ip); + if (relevants.length > 0) { + return relevants.map(node => node.domain); + } - return nodes; + return orderedNodes; } }