@sketchlog/client 1.2.3

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,35 @@
1
+ # `@sketchlog/client`
2
+
3
+ TypeScript HTTP client for SketchLog.
4
+
5
+ ```bash
6
+ npm install @sketchlog/client
7
+ ```
8
+
9
+ ```typescript
10
+ import { SketchLogClient } from "@sketchlog/client";
11
+
12
+ const client = new SketchLogClient({
13
+ endpoint: "https://metrics.example",
14
+ authTokenProvider: async () => obtainRotatedToken(),
15
+ timeoutMs: 5000,
16
+ maxRetries: 3,
17
+ });
18
+
19
+ await client.ingestEvents("api", {
20
+ latencies: [42.5],
21
+ events: { request: 1 },
22
+ });
23
+
24
+ await client.close();
25
+ ```
26
+
27
+ Static credentials can be supplied with `authToken`. Authentication is sent in
28
+ `X-SketchLog-Auth-Token`, never in the URL.
29
+
30
+ GET, PUT, and DELETE requests retry transport failures, HTTP 429, and 5xx
31
+ responses with bounded exponential backoff and jitter. Ingestion POSTs are not
32
+ retried automatically because replay could double-count events.
33
+
34
+ See the [SDK documentation](https://sbalavignesh123.github.io/sketchlog/sdks/)
35
+ for configuration and conformance details.
@@ -0,0 +1,32 @@
1
+ export declare class SketchLogError extends Error {
2
+ status: number;
3
+ constructor(status: number, message: string);
4
+ }
5
+ export interface ClientOptions {
6
+ endpoint: string;
7
+ maxRetries?: number;
8
+ timeoutMs?: number;
9
+ authToken?: string;
10
+ authTokenProvider?: () => string | Promise<string>;
11
+ }
12
+ export interface EventBatch {
13
+ latencies?: number[];
14
+ uniques?: string[];
15
+ events?: Record<string, number>;
16
+ }
17
+ export declare class SketchLogClient {
18
+ private static readonly MAX_RESPONSE_BYTES;
19
+ private endpoint;
20
+ private maxRetries;
21
+ private timeoutMs;
22
+ private agent;
23
+ private authToken?;
24
+ private authTokenProvider?;
25
+ constructor(options: ClientOptions);
26
+ private readResponse;
27
+ private request;
28
+ private delay;
29
+ health(): Promise<any>;
30
+ ingestEvents(streamId: string, batch: EventBatch, signal?: AbortSignal): Promise<void>;
31
+ close(): Promise<void>;
32
+ }
package/dist/index.js ADDED
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SketchLogClient = exports.SketchLogError = void 0;
4
+ const undici_1 = require("undici");
5
+ class SketchLogError extends Error {
6
+ status;
7
+ constructor(status, message) {
8
+ super(message);
9
+ this.status = status;
10
+ this.name = 'SketchLogError';
11
+ }
12
+ }
13
+ exports.SketchLogError = SketchLogError;
14
+ class SketchLogClient {
15
+ static MAX_RESPONSE_BYTES = 1024 * 1024;
16
+ endpoint;
17
+ maxRetries;
18
+ timeoutMs;
19
+ agent;
20
+ authToken;
21
+ authTokenProvider;
22
+ constructor(options) {
23
+ const endpoint = new URL(options.endpoint);
24
+ if (!['http:', 'https:'].includes(endpoint.protocol)
25
+ || endpoint.username || endpoint.password
26
+ || (endpoint.pathname !== '' && endpoint.pathname !== '/')
27
+ || endpoint.search || endpoint.hash) {
28
+ throw new TypeError('endpoint must be an HTTP(S) origin without credentials or a path');
29
+ }
30
+ if (options.maxRetries !== undefined
31
+ && (!Number.isInteger(options.maxRetries)
32
+ || options.maxRetries < 0 || options.maxRetries > 10)) {
33
+ throw new TypeError('maxRetries must be an integer in [0, 10]');
34
+ }
35
+ if (options.timeoutMs !== undefined
36
+ && (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0)) {
37
+ throw new TypeError('timeoutMs must be a positive finite number');
38
+ }
39
+ this.endpoint = endpoint.origin;
40
+ this.maxRetries = options.maxRetries ?? 3;
41
+ this.timeoutMs = options.timeoutMs ?? 5000;
42
+ this.authToken = options.authToken;
43
+ this.authTokenProvider = options.authTokenProvider;
44
+ // Connection pooling
45
+ this.agent = new undici_1.Agent({
46
+ keepAliveTimeout: 10000,
47
+ keepAliveMaxTimeout: 10000,
48
+ });
49
+ }
50
+ async readResponse(res) {
51
+ const contentLength = Number(res.headers.get('content-length'));
52
+ if (Number.isFinite(contentLength)
53
+ && contentLength > SketchLogClient.MAX_RESPONSE_BYTES) {
54
+ await res.body?.cancel();
55
+ throw new SketchLogError(502, 'Response body exceeds 1 MiB');
56
+ }
57
+ if (!res.body)
58
+ return '';
59
+ const reader = res.body.getReader();
60
+ const chunks = [];
61
+ let total = 0;
62
+ while (true) {
63
+ const { done, value } = await reader.read();
64
+ if (done)
65
+ break;
66
+ total += value.byteLength;
67
+ if (total > SketchLogClient.MAX_RESPONSE_BYTES) {
68
+ await reader.cancel();
69
+ throw new SketchLogError(502, 'Response body exceeds 1 MiB');
70
+ }
71
+ chunks.push(value);
72
+ }
73
+ const combined = new Uint8Array(total);
74
+ let offset = 0;
75
+ for (const chunk of chunks) {
76
+ combined.set(chunk, offset);
77
+ offset += chunk.byteLength;
78
+ }
79
+ return new TextDecoder().decode(combined);
80
+ }
81
+ async request(method, path, body, signal) {
82
+ const url = `${this.endpoint}${path}`;
83
+ const isIdempotent = method === 'GET' || method === 'PUT' || method === 'DELETE';
84
+ let attempt = 0;
85
+ while (true) {
86
+ attempt++;
87
+ let timeoutId;
88
+ try {
89
+ const controller = new AbortController();
90
+ timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
91
+ // Combine provided signal with our timeout signal so neither is ignored
92
+ const activeSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
93
+ const token = this.authTokenProvider
94
+ ? await this.authTokenProvider()
95
+ : this.authToken;
96
+ const headers = {};
97
+ if (body)
98
+ headers['Content-Type'] = 'application/json';
99
+ if (token)
100
+ headers['X-SketchLog-Auth-Token'] = token;
101
+ const res = await (0, undici_1.fetch)(url, {
102
+ method,
103
+ headers: Object.keys(headers).length ? headers : undefined,
104
+ body: body ? JSON.stringify(body) : undefined,
105
+ dispatcher: this.agent,
106
+ signal: activeSignal,
107
+ redirect: 'error',
108
+ });
109
+ if (res.status >= 200 && res.status < 300) {
110
+ if (res.status === 204)
111
+ return null;
112
+ const responseText = await this.readResponse(res);
113
+ try {
114
+ return responseText ? JSON.parse(responseText) : null;
115
+ }
116
+ catch {
117
+ throw new SketchLogError(502, 'Server returned invalid JSON');
118
+ }
119
+ }
120
+ // Retry on 5xx or 429
121
+ if ((res.status >= 500 || res.status === 429) && isIdempotent && attempt <= this.maxRetries) {
122
+ await this.readResponse(res); // Consume to free the pooled socket.
123
+ await this.delay(attempt);
124
+ continue;
125
+ }
126
+ throw new SketchLogError(res.status, await this.readResponse(res));
127
+ }
128
+ catch (err) {
129
+ if (err instanceof SketchLogError) {
130
+ throw err;
131
+ }
132
+ if (err.name === 'AbortError') {
133
+ if (signal?.aborted) {
134
+ throw signal.reason || err;
135
+ }
136
+ throw new SketchLogError(408, 'Request Timeout');
137
+ }
138
+ if (isIdempotent && attempt <= this.maxRetries) {
139
+ await this.delay(attempt);
140
+ continue;
141
+ }
142
+ throw new SketchLogError(0, err.message || 'Unknown network error');
143
+ }
144
+ finally {
145
+ if (timeoutId !== undefined)
146
+ clearTimeout(timeoutId);
147
+ }
148
+ }
149
+ }
150
+ delay(attempt) {
151
+ const base = 100 * Math.pow(2, attempt - 1);
152
+ const jitter = Math.random() * 50;
153
+ return new Promise(resolve => setTimeout(resolve, base + jitter));
154
+ }
155
+ async health() {
156
+ return this.request('GET', '/health');
157
+ }
158
+ async ingestEvents(streamId, batch, signal) {
159
+ await this.request('POST', `/v1/streams/${encodeURIComponent(streamId)}/events`, batch, signal);
160
+ }
161
+ async close() {
162
+ await this.agent.close();
163
+ }
164
+ }
165
+ exports.SketchLogClient = SketchLogClient;
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@sketchlog/client",
3
+ "version": "1.2.3",
4
+ "description": "TypeScript SDK for SketchLog",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "require": "./dist/index.js",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist/index.js",
16
+ "dist/index.d.ts"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "test": "npm run build && node --test test/*.test.cjs"
21
+ },
22
+ "keywords": ["sketchlog", "observability", "metrics", "streaming"],
23
+ "author": "Bala Vignesh S",
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/SBALAVIGNESH123/sketchlog.git",
28
+ "directory": "clients/typescript"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/SBALAVIGNESH123/sketchlog/issues"
32
+ },
33
+ "homepage": "https://github.com/SBALAVIGNESH123/sketchlog/tree/main/clients/typescript",
34
+ "publishConfig": {
35
+ "access": "public",
36
+ "provenance": true
37
+ },
38
+ "engines": {
39
+ "node": ">=18.17"
40
+ },
41
+ "type": "commonjs",
42
+ "dependencies": {
43
+ "undici": "^6.20.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^26.0.0",
47
+ "tsx": "^4.22.4",
48
+ "typescript": "^6.0.3"
49
+ }
50
+ }