@cowliss/sdk 0.1.1 → 0.3.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 +10 -2
- package/dist/index.d.ts +31 -1
- package/dist/index.js +63 -15
- package/dist/version.autogenerated.d.ts +1 -1
- package/dist/version.autogenerated.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,8 +48,16 @@ per call when a single process writes into more than one app.
|
|
|
48
48
|
Retries are handled for you. A call that fails to reach Cowliss, or that
|
|
49
49
|
Cowliss could not complete, backs off and tries twice more; a generated
|
|
50
50
|
`messageId` travels with every track, so a retry after a lost response never
|
|
51
|
-
writes the event twice.
|
|
52
|
-
|
|
51
|
+
writes the event twice. Each attempt has a deadline of its own (`timeoutMs`,
|
|
52
|
+
ten seconds by default, thirty for a batch), so a stalled Cowliss cannot
|
|
53
|
+
hold up your code. A
|
|
54
|
+
call Cowliss rejected is not retried: it throws a `CowError` carrying the
|
|
55
|
+
reason.
|
|
56
|
+
|
|
57
|
+
You do not have to wait for a call. `await cow.track(...)` throws on failure,
|
|
58
|
+
as it always has; `cow.track(...)` on its own returns immediately and can
|
|
59
|
+
never take your process down with an unhandled rejection. Pass `onError` to
|
|
60
|
+
see the failures a call you did not await would otherwise swallow.
|
|
53
61
|
|
|
54
62
|
## Docs
|
|
55
63
|
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,15 @@ export type * from "./types.js";
|
|
|
12
12
|
* response dedupes instead of double-writing); network errors and 5xx retry with exponential backoff;
|
|
13
13
|
* 4xx surface as typed CowErrors with the API's error-code enum, never
|
|
14
14
|
* retried.
|
|
15
|
+
*
|
|
16
|
+
* Calling Cowliss must never take the caller's app with it, so the client is
|
|
17
|
+
* safe to use without awaiting. Every attempt carries a `timeoutMs` deadline
|
|
18
|
+
* (10s by default, 30s for a batch) so a stalled request cannot hang a
|
|
19
|
+
* caller for undici's
|
|
20
|
+
* multi-minute default, and every call marks its own rejection handled, so a
|
|
21
|
+
* deliberate fire-and-forget `cow.track(...)` cannot crash the process with
|
|
22
|
+
* an unhandled rejection. Awaiting still throws the CowError, and `onError`
|
|
23
|
+
* gives the fire-and-forget caller somewhere to see failures.
|
|
15
24
|
*/
|
|
16
25
|
export declare class CowError extends Error {
|
|
17
26
|
readonly code: ErrorCode | "network_error";
|
|
@@ -35,6 +44,27 @@ export type CowOptions = {
|
|
|
35
44
|
baseUrl?: string;
|
|
36
45
|
/** Retries after the initial attempt on network errors / 5xx. Default 2. */
|
|
37
46
|
maxRetries?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Deadline for one identify or track attempt, in milliseconds. Default
|
|
49
|
+
* 10000, so a call that exhausts the default retries gives up after ~30s
|
|
50
|
+
* instead of inheriting the runtime's multi-minute default. A timed-out
|
|
51
|
+
* attempt retries and finally surfaces as a `network_error` CowError.
|
|
52
|
+
*/
|
|
53
|
+
timeoutMs?: number;
|
|
54
|
+
/**
|
|
55
|
+
* The same, for one `batch` attempt. Default 30000: a batch carries up to
|
|
56
|
+
* `MAX_BATCH_ITEMS` rows, so it is a slower request by design and holding
|
|
57
|
+
* it to the single-write deadline would abort imports that are finishing
|
|
58
|
+
* fine. Raise it for a large or slow import.
|
|
59
|
+
*/
|
|
60
|
+
batchTimeoutMs?: number;
|
|
61
|
+
/**
|
|
62
|
+
* Called with the CowError of any failed call. Only useful to a caller who
|
|
63
|
+
* does not await, since an awaited call throws the same error; this is
|
|
64
|
+
* where a fire-and-forget `cow.track(...)` gets logged. Anything it throws
|
|
65
|
+
* is swallowed.
|
|
66
|
+
*/
|
|
67
|
+
onError?: (error: CowError) => void;
|
|
38
68
|
/** Injectable for tests and non-fetch runtimes. */
|
|
39
69
|
fetch?: typeof globalThis.fetch;
|
|
40
70
|
};
|
|
@@ -55,7 +85,7 @@ export declare class Cow {
|
|
|
55
85
|
/**
|
|
56
86
|
* Tracks one event. A messageId is generated when the caller does not
|
|
57
87
|
* supply one, and sent both in the body and as the Idempotency-Key, so a
|
|
58
|
-
* retried request (network error, 5xx, or lost response) dedupes
|
|
88
|
+
* retried request (network error, timeout, 5xx, or lost response) dedupes
|
|
59
89
|
* server-side. Historical timestamps pass through for backfill scripts.
|
|
60
90
|
*/
|
|
61
91
|
track(input: WithDefaultSource<TrackInput>): Promise<EventDto>;
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,15 @@ const CLIENT_ID = `@cowliss/sdk/${SDK_VERSION}`;
|
|
|
13
13
|
* response dedupes instead of double-writing); network errors and 5xx retry with exponential backoff;
|
|
14
14
|
* 4xx surface as typed CowErrors with the API's error-code enum, never
|
|
15
15
|
* retried.
|
|
16
|
+
*
|
|
17
|
+
* Calling Cowliss must never take the caller's app with it, so the client is
|
|
18
|
+
* safe to use without awaiting. Every attempt carries a `timeoutMs` deadline
|
|
19
|
+
* (10s by default, 30s for a batch) so a stalled request cannot hang a
|
|
20
|
+
* caller for undici's
|
|
21
|
+
* multi-minute default, and every call marks its own rejection handled, so a
|
|
22
|
+
* deliberate fire-and-forget `cow.track(...)` cannot crash the process with
|
|
23
|
+
* an unhandled rejection. Awaiting still throws the CowError, and `onError`
|
|
24
|
+
* gives the fire-and-forget caller somewhere to see failures.
|
|
16
25
|
*/
|
|
17
26
|
export class CowError extends Error {
|
|
18
27
|
code;
|
|
@@ -25,18 +34,28 @@ export class CowError extends Error {
|
|
|
25
34
|
}
|
|
26
35
|
}
|
|
27
36
|
const RETRY_BASE_DELAY_MS = 100;
|
|
37
|
+
/** See {@link CowOptions.timeoutMs}. */
|
|
38
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
39
|
+
/** See {@link CowOptions.batchTimeoutMs}. */
|
|
40
|
+
const DEFAULT_BATCH_TIMEOUT_MS = 30_000;
|
|
28
41
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
29
42
|
export class Cow {
|
|
30
43
|
#apiKey;
|
|
31
44
|
#sourceId;
|
|
32
45
|
#baseUrl;
|
|
33
46
|
#maxRetries;
|
|
47
|
+
#timeoutMs;
|
|
48
|
+
#batchTimeoutMs;
|
|
49
|
+
#onError;
|
|
34
50
|
#fetch;
|
|
35
51
|
constructor(options) {
|
|
36
52
|
this.#apiKey = options.apiKey;
|
|
37
53
|
this.#sourceId = options.sourceId;
|
|
38
54
|
this.#baseUrl = options.baseUrl ?? "http://localhost:3400";
|
|
39
55
|
this.#maxRetries = options.maxRetries ?? 2;
|
|
56
|
+
this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
57
|
+
this.#batchTimeoutMs = options.batchTimeoutMs ?? DEFAULT_BATCH_TIMEOUT_MS;
|
|
58
|
+
this.#onError = options.onError;
|
|
40
59
|
this.#fetch = options.fetch ?? globalThis.fetch;
|
|
41
60
|
}
|
|
42
61
|
/**
|
|
@@ -51,20 +70,46 @@ export class Cow {
|
|
|
51
70
|
}
|
|
52
71
|
return sourceId;
|
|
53
72
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Runs one call and marks its rejection handled, so a caller who
|
|
75
|
+
* deliberately does not await (`cow.track(...)` on a request path) can
|
|
76
|
+
* never take the process down with an unhandled rejection. The promise
|
|
77
|
+
* handed back is the original one, so awaiting still throws the CowError.
|
|
78
|
+
*
|
|
79
|
+
* This wraps at the public method, not inside `#post`, because
|
|
80
|
+
* `#sourceFor` throws before a request is ever made. The public methods
|
|
81
|
+
* are deliberately not `async` for the same reason: an `async` wrapper
|
|
82
|
+
* would hand the caller a second, unwatched promise and void the
|
|
83
|
+
* guarantee.
|
|
84
|
+
*/
|
|
85
|
+
#watch(run) {
|
|
86
|
+
const promise = run();
|
|
87
|
+
promise.catch((error) => {
|
|
88
|
+
try {
|
|
89
|
+
this.#onError?.(error);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// An onError that throws must not become the crash we just prevented.
|
|
93
|
+
}
|
|
57
94
|
});
|
|
95
|
+
return promise;
|
|
96
|
+
}
|
|
97
|
+
identify(input) {
|
|
98
|
+
return this.#watch(async () => this.#post("/v1/identify", {
|
|
99
|
+
data: { ...input, sourceId: this.#sourceFor(input) },
|
|
100
|
+
}));
|
|
58
101
|
}
|
|
59
102
|
/**
|
|
60
103
|
* Tracks one event. A messageId is generated when the caller does not
|
|
61
104
|
* supply one, and sent both in the body and as the Idempotency-Key, so a
|
|
62
|
-
* retried request (network error, 5xx, or lost response) dedupes
|
|
105
|
+
* retried request (network error, timeout, 5xx, or lost response) dedupes
|
|
63
106
|
* server-side. Historical timestamps pass through for backfill scripts.
|
|
64
107
|
*/
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
108
|
+
track(input) {
|
|
109
|
+
return this.#watch(async () => {
|
|
110
|
+
const messageId = input.messageId ?? crypto.randomUUID();
|
|
111
|
+
return this.#post("/v1/track", { data: { ...input, sourceId: this.#sourceFor(input), messageId } }, messageId);
|
|
112
|
+
});
|
|
68
113
|
}
|
|
69
114
|
/**
|
|
70
115
|
* Batch import: one source's identify and track calls in one request.
|
|
@@ -72,12 +117,10 @@ export class Cow {
|
|
|
72
117
|
* the SDK does not generate one here, so re-running the same backfill
|
|
73
118
|
* (same messageIds) dedupes instead of double-writing.
|
|
74
119
|
*/
|
|
75
|
-
|
|
76
|
-
return this.#post("/v1/batch", {
|
|
77
|
-
data: { ...input, sourceId: this.#sourceFor(input) },
|
|
78
|
-
});
|
|
120
|
+
batch(input) {
|
|
121
|
+
return this.#watch(async () => this.#post("/v1/batch", { data: { ...input, sourceId: this.#sourceFor(input) } }, undefined, this.#batchTimeoutMs));
|
|
79
122
|
}
|
|
80
|
-
async #post(path, body, idempotencyKey) {
|
|
123
|
+
async #post(path, body, idempotencyKey, timeoutMs = this.#timeoutMs) {
|
|
81
124
|
for (let attempt = 0;; attempt++) {
|
|
82
125
|
let res;
|
|
83
126
|
try {
|
|
@@ -90,7 +133,15 @@ export class Cow {
|
|
|
90
133
|
...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
|
|
91
134
|
},
|
|
92
135
|
body: JSON.stringify(body),
|
|
136
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
93
137
|
});
|
|
138
|
+
// Reading the body is part of the attempt: an abort or a truncated
|
|
139
|
+
// response mid-read is a transport failure like any other, and
|
|
140
|
+
// retrying it is safe (track carries its Idempotency-Key, identify
|
|
141
|
+
// upserts on identifiers).
|
|
142
|
+
if (res.ok) {
|
|
143
|
+
return (await res.json()).data;
|
|
144
|
+
}
|
|
94
145
|
}
|
|
95
146
|
catch (cause) {
|
|
96
147
|
if (attempt < this.#maxRetries) {
|
|
@@ -99,9 +150,6 @@ export class Cow {
|
|
|
99
150
|
}
|
|
100
151
|
throw new CowError("network_error", "Network error", 0, { cause });
|
|
101
152
|
}
|
|
102
|
-
if (res.ok) {
|
|
103
|
-
return (await res.json()).data;
|
|
104
|
-
}
|
|
105
153
|
if (res.status >= 500 && attempt < this.#maxRetries) {
|
|
106
154
|
await sleep(RETRY_BASE_DELAY_MS * 2 ** attempt);
|
|
107
155
|
continue;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "0.
|
|
1
|
+
export declare const SDK_VERSION = "0.3.0";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/version.ts from package.json. Do not edit.
|
|
2
|
-
export const SDK_VERSION = "0.
|
|
2
|
+
export const SDK_VERSION = "0.3.0";
|