nozomi-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,163 @@
1
+ # Nozomi SDK
2
+
3
+ Find the fastest Nozomi endpoints for optimal Solana transaction submission.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install nozomi-sdk
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Basic Usage
14
+
15
+ ```typescript
16
+ import { findFastestEndpoints } from 'nozomi-sdk';
17
+
18
+ // Find the 2 fastest regional endpoints + auto-routed fallback
19
+ const endpoints = await findFastestEndpoints();
20
+
21
+ console.log(endpoints);
22
+ // [
23
+ // { url: 'https://pit1.nozomi.temporal.xyz', region: 'pittsburgh', minTime: 12.5, ... },
24
+ // { url: 'https://ewr1.nozomi.temporal.xyz', region: 'newark', minTime: 15.2, ... },
25
+ // { url: 'https://nozomi.temporal.xyz', region: 'auto', minTime: 18.0, ... }
26
+ // ]
27
+ ```
28
+
29
+ ### Find Single Fastest
30
+
31
+ ```typescript
32
+ const [fastest] = await findFastestEndpoints({ topCount: 1 });
33
+ console.log(`Fastest: ${fastest.url} (${fastest.minTime.toFixed(2)}ms)`);
34
+ ```
35
+
36
+ ### With Solana Web3.js
37
+
38
+ ```typescript
39
+ import { Connection, Keypair, Transaction } from '@solana/web3.js';
40
+ import { findFastestEndpoints } from 'nozomi-sdk';
41
+
42
+ const [fastest] = await findFastestEndpoints({ topCount: 1 });
43
+
44
+ const API_KEY = process.env.NOZOMI_API_KEY;
45
+ const connection = new Connection(`${fastest.url}/?c=${API_KEY}`, 'confirmed');
46
+
47
+ // Send transaction via Nozomi
48
+ const signature = await connection.sendRawTransaction(signedTx, {
49
+ skipPreflight: true,
50
+ maxRetries: 0
51
+ });
52
+ ```
53
+
54
+ ### Configuration Options
55
+
56
+ ```typescript
57
+ const results = await findFastestEndpoints({
58
+ // Number of measurement pings per endpoint (default: 5, max: 20)
59
+ pingCount: 10,
60
+
61
+ // Number of warmup pings before measurement (default: 2, max: 5)
62
+ warmupCount: 2,
63
+
64
+ // Number of top endpoints to return (default: 2, max: 10)
65
+ topCount: 3,
66
+
67
+ // Timeout per ping in ms (default: 5000, min: 1000, max: 30000)
68
+ timeout: 3000,
69
+
70
+ // Include auto-routed endpoint in results (default: true)
71
+ includeAutoRouted: true,
72
+
73
+ // Custom ping endpoint path (default: '/ping')
74
+ endpoint: '/ping',
75
+
76
+ // Custom endpoints URL (default: GitHub raw URL)
77
+ endpointsUrl: 'https://example.com/endpoints.json',
78
+
79
+ // Custom endpoint configs (skips remote fetch)
80
+ endpoints: [
81
+ { url: 'https://custom.xyz', region: 'custom', type: 'direct' }
82
+ ],
83
+
84
+ // Callback for each endpoint result (useful for progress)
85
+ onResult: (result) => {
86
+ console.log(`${result.url}: ${result.minTime}ms`);
87
+ }
88
+ });
89
+ ```
90
+
91
+ ### Fallback Strategy
92
+
93
+ ```typescript
94
+ const endpoints = await findFastestEndpoints({ topCount: 3 });
95
+
96
+ for (const endpoint of endpoints) {
97
+ try {
98
+ const connection = new Connection(`${endpoint.url}/?c=${API_KEY}`);
99
+ const sig = await connection.sendRawTransaction(tx);
100
+ console.log(`Success via ${endpoint.url}`);
101
+ break;
102
+ } catch (err) {
103
+ console.warn(`Failed on ${endpoint.url}, trying next...`);
104
+ }
105
+ }
106
+ ```
107
+
108
+ ## API Reference
109
+
110
+ ### `findFastestEndpoints(options?)`
111
+
112
+ Returns a promise that resolves to an array of `EndpointResult` objects.
113
+
114
+ **Never throws** - always returns at least one endpoint (the auto-routed fallback).
115
+
116
+ #### Options
117
+
118
+ | Option | Type | Default | Description |
119
+ |--------|------|---------|-------------|
120
+ | `pingCount` | number | 5 | Number of measurement pings (1-20) |
121
+ | `warmupCount` | number | 2 | Number of warmup pings (0-5) |
122
+ | `topCount` | number | 2 | Number of top results to return (1-10) |
123
+ | `timeout` | number | 5000 | Timeout per ping in ms (1000-30000) |
124
+ | `includeAutoRouted` | boolean | true | Include auto-routed endpoint |
125
+ | `endpoint` | string | '/ping' | Ping endpoint path |
126
+ | `endpointsUrl` | string | GitHub URL | URL to fetch endpoint configs |
127
+ | `endpoints` | EndpointConfig[] | - | Custom endpoint configs |
128
+ | `onResult` | function | - | Callback for each result |
129
+
130
+ #### Result Type
131
+
132
+ ```typescript
133
+ interface EndpointResult {
134
+ url: string; // Endpoint URL
135
+ region: string; // Region identifier
136
+ minTime: number; // Minimum ping time (ms)
137
+ times?: number[]; // All measurement times
138
+ warmupTimes?: number[]; // Warmup times
139
+ }
140
+ ```
141
+
142
+ ### Constants
143
+
144
+ ```typescript
145
+ import {
146
+ NOZOMI_ENDPOINTS, // Hardcoded fallback endpoints
147
+ NOZOMI_AUTO_ENDPOINT, // Auto-routed endpoint URL
148
+ NOZOMI_ENDPOINTS_URL // Default endpoints JSON URL
149
+ } from 'nozomi-sdk';
150
+ ```
151
+
152
+ ## Features
153
+
154
+ - **Zero dependencies** - works in Node.js and browsers
155
+ - **Never throws** - always returns valid results with fallbacks
156
+ - **Region deduplication** - returns only the fastest endpoint per region
157
+ - **Warmup pings** - accounts for TLS/TCP connection setup
158
+ - **Remote config** - fetches latest endpoints from GitHub with fallback
159
+ - **Fully typed** - complete TypeScript definitions
160
+
161
+ ## License
162
+
163
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ NOZOMI_AUTO_ENDPOINT: () => NOZOMI_AUTO_ENDPOINT,
24
+ NOZOMI_ENDPOINTS: () => NOZOMI_ENDPOINTS,
25
+ NOZOMI_ENDPOINTS_URL: () => NOZOMI_ENDPOINTS_URL,
26
+ findFastestEndpoints: () => findFastestEndpoints
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+ var NOZOMI_ENDPOINTS_URL = "https://raw.githubusercontent.com/temporalxyz/nozomi-sdk/main/endpoints.json";
30
+ var NOZOMI_AUTO_ENDPOINT = "https://nozomi.temporal.xyz";
31
+ var NOZOMI_ENDPOINTS = [
32
+ { url: NOZOMI_AUTO_ENDPOINT, region: "auto", type: "auto" },
33
+ { url: "https://pit1.nozomi.temporal.xyz", region: "pittsburgh", type: "direct" },
34
+ { url: "https://tyo1.nozomi.temporal.xyz", region: "tokyo", type: "direct" },
35
+ { url: "https://sgp1.nozomi.temporal.xyz", region: "singapore", type: "direct" },
36
+ { url: "https://ewr1.nozomi.temporal.xyz", region: "newark", type: "direct" },
37
+ { url: "https://ams1.nozomi.temporal.xyz", region: "amsterdam", type: "direct" },
38
+ { url: "https://fra2.nozomi.temporal.xyz", region: "frankfurt", type: "direct" },
39
+ { url: "https://ash1.nozomi.temporal.xyz", region: "ashburn", type: "direct" },
40
+ { url: "https://lax1.nozomi.temporal.xyz", region: "los-angeles", type: "direct" },
41
+ { url: "https://lon1.nozomi.temporal.xyz", region: "london", type: "direct" },
42
+ { url: "https://pit.nozomi.temporal.xyz", region: "pittsburgh", type: "cloudflare" },
43
+ { url: "https://tyo.nozomi.temporal.xyz", region: "tokyo", type: "cloudflare" },
44
+ { url: "https://sgp.nozomi.temporal.xyz", region: "singapore", type: "cloudflare" },
45
+ { url: "https://ewr.nozomi.temporal.xyz", region: "newark", type: "cloudflare" },
46
+ { url: "https://ams.nozomi.temporal.xyz", region: "amsterdam", type: "cloudflare" },
47
+ { url: "https://fra.nozomi.temporal.xyz", region: "frankfurt", type: "cloudflare" },
48
+ { url: "https://ash.nozomi.temporal.xyz", region: "ashburn", type: "cloudflare" },
49
+ { url: "https://lax.nozomi.temporal.xyz", region: "los-angeles", type: "cloudflare" },
50
+ { url: "https://lon.nozomi.temporal.xyz", region: "london", type: "cloudflare" }
51
+ ];
52
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
53
+ async function fetchEndpointsFromUrl(url, timeout, retries) {
54
+ for (let attempt = 0; attempt <= retries; attempt++) {
55
+ if (attempt > 0) await sleep(attempt * 500);
56
+ const controller = new AbortController();
57
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
58
+ try {
59
+ const response = await fetch(url, { signal: controller.signal, cache: "no-store" });
60
+ clearTimeout(timeoutId);
61
+ if (!response.ok) continue;
62
+ const manifest = await response.json();
63
+ if (manifest?.endpoints && Array.isArray(manifest.endpoints)) {
64
+ const valid = manifest.endpoints.filter(
65
+ (e) => e?.url && typeof e.url === "string" && e.url.startsWith("https://") && e.region
66
+ );
67
+ if (valid.length > 0) return valid;
68
+ }
69
+ } catch {
70
+ clearTimeout(timeoutId);
71
+ }
72
+ }
73
+ return null;
74
+ }
75
+ async function getEndpointConfigs(endpointsUrl) {
76
+ const url = endpointsUrl || NOZOMI_ENDPOINTS_URL;
77
+ const remote = await fetchEndpointsFromUrl(url, 3e3, 2);
78
+ return remote && remote.length > 0 ? remote : [...NOZOMI_ENDPOINTS];
79
+ }
80
+ async function measurePing(url, endpoint, timeout) {
81
+ const pingUrl = url.replace(/\/+$/, "") + endpoint;
82
+ const controller = new AbortController();
83
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
84
+ const start = performance.now();
85
+ try {
86
+ const response = await fetch(pingUrl, { method: "GET", signal: controller.signal, cache: "no-store" });
87
+ clearTimeout(timeoutId);
88
+ return response.ok ? performance.now() - start : Infinity;
89
+ } catch {
90
+ clearTimeout(timeoutId);
91
+ return Infinity;
92
+ }
93
+ }
94
+ async function pingEndpoint(config, pingCount, warmupCount, endpoint, timeout) {
95
+ const warmupTimes = [];
96
+ for (let i = 0; i < warmupCount; i++) {
97
+ warmupTimes.push(await measurePing(config.url, endpoint, timeout));
98
+ }
99
+ const times = [];
100
+ for (let i = 0; i < pingCount; i++) {
101
+ times.push(await measurePing(config.url, endpoint, timeout));
102
+ }
103
+ const minTime = times.length > 0 ? Math.min(...times) : Infinity;
104
+ return { url: config.url, region: config.region, minTime, times, warmupTimes };
105
+ }
106
+ async function findFastestEndpoints(options = {}) {
107
+ try {
108
+ let configs = options.endpoints || await getEndpointConfigs(options.endpointsUrl);
109
+ if (!Array.isArray(configs) || configs.length === 0) configs = [...NOZOMI_ENDPOINTS];
110
+ const pingCount = Math.max(1, Math.min(20, options.pingCount ?? 5));
111
+ const topCount = Math.max(1, Math.min(10, options.topCount ?? 2));
112
+ const timeout = Math.max(1e3, Math.min(3e4, options.timeout ?? 5e3));
113
+ const warmupCount = Math.max(0, Math.min(5, options.warmupCount ?? 2));
114
+ const endpoint = options.endpoint ?? "/ping";
115
+ const includeAutoRouted = options.includeAutoRouted ?? true;
116
+ const results = await Promise.all(
117
+ configs.map(async (config) => {
118
+ const result = await pingEndpoint(config, pingCount, warmupCount, endpoint, timeout);
119
+ try {
120
+ options.onResult?.(result);
121
+ } catch {
122
+ }
123
+ return result;
124
+ })
125
+ );
126
+ const validResults = results.filter((r) => r.minTime !== Infinity && isFinite(r.minTime)).sort((a, b) => a.minTime - b.minTime);
127
+ let topResults;
128
+ if (includeAutoRouted) {
129
+ const nonAutoResults = validResults.filter((r) => r.region !== "auto");
130
+ const seenRegions = /* @__PURE__ */ new Set();
131
+ const deduped = [];
132
+ for (const result of nonAutoResults) {
133
+ if (!seenRegions.has(result.region)) {
134
+ seenRegions.add(result.region);
135
+ deduped.push(result);
136
+ }
137
+ }
138
+ topResults = deduped.slice(0, topCount);
139
+ const autoResult = validResults.find((r) => r.region === "auto");
140
+ topResults.push(autoResult ?? { url: NOZOMI_AUTO_ENDPOINT, region: "auto", minTime: Infinity, times: [], warmupTimes: [] });
141
+ } else {
142
+ const seenRegions = /* @__PURE__ */ new Set();
143
+ const deduped = [];
144
+ for (const result of validResults) {
145
+ if (!seenRegions.has(result.region)) {
146
+ seenRegions.add(result.region);
147
+ deduped.push(result);
148
+ }
149
+ }
150
+ topResults = deduped.slice(0, topCount);
151
+ }
152
+ if (topResults.length === 0) {
153
+ topResults = [{ url: NOZOMI_AUTO_ENDPOINT, region: "auto", minTime: Infinity, times: [], warmupTimes: [] }];
154
+ }
155
+ return topResults;
156
+ } catch {
157
+ return [{ url: NOZOMI_AUTO_ENDPOINT, region: "auto", minTime: Infinity, times: [], warmupTimes: [] }];
158
+ }
159
+ }
160
+ // Annotate the CommonJS export names for ESM import in node:
161
+ 0 && (module.exports = {
162
+ NOZOMI_AUTO_ENDPOINT,
163
+ NOZOMI_ENDPOINTS,
164
+ NOZOMI_ENDPOINTS_URL,
165
+ findFastestEndpoints
166
+ });
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Nozomi SDK - Endpoint Discovery
3
+ *
4
+ * Find the fastest Nozomi endpoints for optimal transaction submission.
5
+ */
6
+ /** Remote endpoints JSON structure */
7
+ interface EndpointConfig {
8
+ url: string;
9
+ region: string;
10
+ type: 'auto' | 'direct' | 'cloudflare';
11
+ }
12
+ interface EndpointsManifest {
13
+ version: number;
14
+ updated: string;
15
+ endpoints: EndpointConfig[];
16
+ }
17
+ /** Default GitHub raw URL for remote endpoints */
18
+ declare const NOZOMI_ENDPOINTS_URL = "https://raw.githubusercontent.com/temporalxyz/nozomi-sdk/main/endpoints.json";
19
+ /** Auto-routed endpoint (always included as fallback by default) */
20
+ declare const NOZOMI_AUTO_ENDPOINT = "https://nozomi.temporal.xyz";
21
+ /** Hardcoded fallback endpoints with regions */
22
+ declare const NOZOMI_ENDPOINTS: EndpointConfig[];
23
+ interface EndpointResult {
24
+ url: string;
25
+ region: string;
26
+ minTime: number;
27
+ times?: number[];
28
+ warmupTimes?: number[];
29
+ }
30
+ interface FindFastestOptions {
31
+ endpoints?: EndpointConfig[];
32
+ endpointsUrl?: string;
33
+ pingCount?: number;
34
+ topCount?: number;
35
+ timeout?: number;
36
+ endpoint?: string;
37
+ warmupCount?: number;
38
+ includeAutoRouted?: boolean;
39
+ onResult?: (result: EndpointResult) => void;
40
+ }
41
+ /**
42
+ * Find the fastest Nozomi endpoints.
43
+ *
44
+ * NEVER THROWS - always returns at least the auto-routed endpoint.
45
+ *
46
+ * By default returns [2 fastest regional endpoints, auto-routed endpoint].
47
+ */
48
+ declare function findFastestEndpoints(options?: FindFastestOptions): Promise<EndpointResult[]>;
49
+
50
+ export { type EndpointConfig, type EndpointResult, type EndpointsManifest, type FindFastestOptions, NOZOMI_AUTO_ENDPOINT, NOZOMI_ENDPOINTS, NOZOMI_ENDPOINTS_URL, findFastestEndpoints };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Nozomi SDK - Endpoint Discovery
3
+ *
4
+ * Find the fastest Nozomi endpoints for optimal transaction submission.
5
+ */
6
+ /** Remote endpoints JSON structure */
7
+ interface EndpointConfig {
8
+ url: string;
9
+ region: string;
10
+ type: 'auto' | 'direct' | 'cloudflare';
11
+ }
12
+ interface EndpointsManifest {
13
+ version: number;
14
+ updated: string;
15
+ endpoints: EndpointConfig[];
16
+ }
17
+ /** Default GitHub raw URL for remote endpoints */
18
+ declare const NOZOMI_ENDPOINTS_URL = "https://raw.githubusercontent.com/temporalxyz/nozomi-sdk/main/endpoints.json";
19
+ /** Auto-routed endpoint (always included as fallback by default) */
20
+ declare const NOZOMI_AUTO_ENDPOINT = "https://nozomi.temporal.xyz";
21
+ /** Hardcoded fallback endpoints with regions */
22
+ declare const NOZOMI_ENDPOINTS: EndpointConfig[];
23
+ interface EndpointResult {
24
+ url: string;
25
+ region: string;
26
+ minTime: number;
27
+ times?: number[];
28
+ warmupTimes?: number[];
29
+ }
30
+ interface FindFastestOptions {
31
+ endpoints?: EndpointConfig[];
32
+ endpointsUrl?: string;
33
+ pingCount?: number;
34
+ topCount?: number;
35
+ timeout?: number;
36
+ endpoint?: string;
37
+ warmupCount?: number;
38
+ includeAutoRouted?: boolean;
39
+ onResult?: (result: EndpointResult) => void;
40
+ }
41
+ /**
42
+ * Find the fastest Nozomi endpoints.
43
+ *
44
+ * NEVER THROWS - always returns at least the auto-routed endpoint.
45
+ *
46
+ * By default returns [2 fastest regional endpoints, auto-routed endpoint].
47
+ */
48
+ declare function findFastestEndpoints(options?: FindFastestOptions): Promise<EndpointResult[]>;
49
+
50
+ export { type EndpointConfig, type EndpointResult, type EndpointsManifest, type FindFastestOptions, NOZOMI_AUTO_ENDPOINT, NOZOMI_ENDPOINTS, NOZOMI_ENDPOINTS_URL, findFastestEndpoints };
package/dist/index.js ADDED
@@ -0,0 +1,138 @@
1
+ // src/index.ts
2
+ var NOZOMI_ENDPOINTS_URL = "https://raw.githubusercontent.com/temporalxyz/nozomi-sdk/main/endpoints.json";
3
+ var NOZOMI_AUTO_ENDPOINT = "https://nozomi.temporal.xyz";
4
+ var NOZOMI_ENDPOINTS = [
5
+ { url: NOZOMI_AUTO_ENDPOINT, region: "auto", type: "auto" },
6
+ { url: "https://pit1.nozomi.temporal.xyz", region: "pittsburgh", type: "direct" },
7
+ { url: "https://tyo1.nozomi.temporal.xyz", region: "tokyo", type: "direct" },
8
+ { url: "https://sgp1.nozomi.temporal.xyz", region: "singapore", type: "direct" },
9
+ { url: "https://ewr1.nozomi.temporal.xyz", region: "newark", type: "direct" },
10
+ { url: "https://ams1.nozomi.temporal.xyz", region: "amsterdam", type: "direct" },
11
+ { url: "https://fra2.nozomi.temporal.xyz", region: "frankfurt", type: "direct" },
12
+ { url: "https://ash1.nozomi.temporal.xyz", region: "ashburn", type: "direct" },
13
+ { url: "https://lax1.nozomi.temporal.xyz", region: "los-angeles", type: "direct" },
14
+ { url: "https://lon1.nozomi.temporal.xyz", region: "london", type: "direct" },
15
+ { url: "https://pit.nozomi.temporal.xyz", region: "pittsburgh", type: "cloudflare" },
16
+ { url: "https://tyo.nozomi.temporal.xyz", region: "tokyo", type: "cloudflare" },
17
+ { url: "https://sgp.nozomi.temporal.xyz", region: "singapore", type: "cloudflare" },
18
+ { url: "https://ewr.nozomi.temporal.xyz", region: "newark", type: "cloudflare" },
19
+ { url: "https://ams.nozomi.temporal.xyz", region: "amsterdam", type: "cloudflare" },
20
+ { url: "https://fra.nozomi.temporal.xyz", region: "frankfurt", type: "cloudflare" },
21
+ { url: "https://ash.nozomi.temporal.xyz", region: "ashburn", type: "cloudflare" },
22
+ { url: "https://lax.nozomi.temporal.xyz", region: "los-angeles", type: "cloudflare" },
23
+ { url: "https://lon.nozomi.temporal.xyz", region: "london", type: "cloudflare" }
24
+ ];
25
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
26
+ async function fetchEndpointsFromUrl(url, timeout, retries) {
27
+ for (let attempt = 0; attempt <= retries; attempt++) {
28
+ if (attempt > 0) await sleep(attempt * 500);
29
+ const controller = new AbortController();
30
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
31
+ try {
32
+ const response = await fetch(url, { signal: controller.signal, cache: "no-store" });
33
+ clearTimeout(timeoutId);
34
+ if (!response.ok) continue;
35
+ const manifest = await response.json();
36
+ if (manifest?.endpoints && Array.isArray(manifest.endpoints)) {
37
+ const valid = manifest.endpoints.filter(
38
+ (e) => e?.url && typeof e.url === "string" && e.url.startsWith("https://") && e.region
39
+ );
40
+ if (valid.length > 0) return valid;
41
+ }
42
+ } catch {
43
+ clearTimeout(timeoutId);
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+ async function getEndpointConfigs(endpointsUrl) {
49
+ const url = endpointsUrl || NOZOMI_ENDPOINTS_URL;
50
+ const remote = await fetchEndpointsFromUrl(url, 3e3, 2);
51
+ return remote && remote.length > 0 ? remote : [...NOZOMI_ENDPOINTS];
52
+ }
53
+ async function measurePing(url, endpoint, timeout) {
54
+ const pingUrl = url.replace(/\/+$/, "") + endpoint;
55
+ const controller = new AbortController();
56
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
57
+ const start = performance.now();
58
+ try {
59
+ const response = await fetch(pingUrl, { method: "GET", signal: controller.signal, cache: "no-store" });
60
+ clearTimeout(timeoutId);
61
+ return response.ok ? performance.now() - start : Infinity;
62
+ } catch {
63
+ clearTimeout(timeoutId);
64
+ return Infinity;
65
+ }
66
+ }
67
+ async function pingEndpoint(config, pingCount, warmupCount, endpoint, timeout) {
68
+ const warmupTimes = [];
69
+ for (let i = 0; i < warmupCount; i++) {
70
+ warmupTimes.push(await measurePing(config.url, endpoint, timeout));
71
+ }
72
+ const times = [];
73
+ for (let i = 0; i < pingCount; i++) {
74
+ times.push(await measurePing(config.url, endpoint, timeout));
75
+ }
76
+ const minTime = times.length > 0 ? Math.min(...times) : Infinity;
77
+ return { url: config.url, region: config.region, minTime, times, warmupTimes };
78
+ }
79
+ async function findFastestEndpoints(options = {}) {
80
+ try {
81
+ let configs = options.endpoints || await getEndpointConfigs(options.endpointsUrl);
82
+ if (!Array.isArray(configs) || configs.length === 0) configs = [...NOZOMI_ENDPOINTS];
83
+ const pingCount = Math.max(1, Math.min(20, options.pingCount ?? 5));
84
+ const topCount = Math.max(1, Math.min(10, options.topCount ?? 2));
85
+ const timeout = Math.max(1e3, Math.min(3e4, options.timeout ?? 5e3));
86
+ const warmupCount = Math.max(0, Math.min(5, options.warmupCount ?? 2));
87
+ const endpoint = options.endpoint ?? "/ping";
88
+ const includeAutoRouted = options.includeAutoRouted ?? true;
89
+ const results = await Promise.all(
90
+ configs.map(async (config) => {
91
+ const result = await pingEndpoint(config, pingCount, warmupCount, endpoint, timeout);
92
+ try {
93
+ options.onResult?.(result);
94
+ } catch {
95
+ }
96
+ return result;
97
+ })
98
+ );
99
+ const validResults = results.filter((r) => r.minTime !== Infinity && isFinite(r.minTime)).sort((a, b) => a.minTime - b.minTime);
100
+ let topResults;
101
+ if (includeAutoRouted) {
102
+ const nonAutoResults = validResults.filter((r) => r.region !== "auto");
103
+ const seenRegions = /* @__PURE__ */ new Set();
104
+ const deduped = [];
105
+ for (const result of nonAutoResults) {
106
+ if (!seenRegions.has(result.region)) {
107
+ seenRegions.add(result.region);
108
+ deduped.push(result);
109
+ }
110
+ }
111
+ topResults = deduped.slice(0, topCount);
112
+ const autoResult = validResults.find((r) => r.region === "auto");
113
+ topResults.push(autoResult ?? { url: NOZOMI_AUTO_ENDPOINT, region: "auto", minTime: Infinity, times: [], warmupTimes: [] });
114
+ } else {
115
+ const seenRegions = /* @__PURE__ */ new Set();
116
+ const deduped = [];
117
+ for (const result of validResults) {
118
+ if (!seenRegions.has(result.region)) {
119
+ seenRegions.add(result.region);
120
+ deduped.push(result);
121
+ }
122
+ }
123
+ topResults = deduped.slice(0, topCount);
124
+ }
125
+ if (topResults.length === 0) {
126
+ topResults = [{ url: NOZOMI_AUTO_ENDPOINT, region: "auto", minTime: Infinity, times: [], warmupTimes: [] }];
127
+ }
128
+ return topResults;
129
+ } catch {
130
+ return [{ url: NOZOMI_AUTO_ENDPOINT, region: "auto", minTime: Infinity, times: [], warmupTimes: [] }];
131
+ }
132
+ }
133
+ export {
134
+ NOZOMI_AUTO_ENDPOINT,
135
+ NOZOMI_ENDPOINTS,
136
+ NOZOMI_ENDPOINTS_URL,
137
+ findFastestEndpoints
138
+ };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "nozomi-sdk",
3
+ "version": "1.0.0",
4
+ "description": "Find the fastest Nozomi endpoints for optimal transaction submission",
5
+ "type": "module",
6
+ "main": "dist/index.cjs",
7
+ "module": "dist/index.js",
8
+ "browser": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsup src/index.ts --format cjs,esm --dts",
22
+ "test": "vitest run",
23
+ "test:watch": "vitest",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "keywords": [
27
+ "nozomi",
28
+ "temporal",
29
+ "solana",
30
+ "blockchain",
31
+ "transactions",
32
+ "latency"
33
+ ],
34
+ "license": "MIT",
35
+ "devDependencies": {
36
+ "@types/node": "^25.0.3",
37
+ "tsup": "^8.0.0",
38
+ "typescript": "^5.0.0",
39
+ "vitest": "^2.0.0"
40
+ }
41
+ }