@redpennon/node-sdk 1.0.0 → 1.1.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/dist/index.d.ts CHANGED
@@ -79,9 +79,20 @@ export type EventPayload = {
79
79
  occurred_at?: string;
80
80
  /** Opaque trace token from a prior {@link VariableResult.evaluation_trace}. */
81
81
  evaluation_trace?: string;
82
+ /**
83
+ * De-duplication key, unique per environment, at most 200 characters.
84
+ *
85
+ * Send one and a retry after a timeout is free: the platform skips
86
+ * keys it has already stored instead of counting the event twice.
87
+ * Omit it and repeats are stored — which is correct for genuinely
88
+ * repeated events, since nothing else can tell them apart.
89
+ */
90
+ event_id?: string;
82
91
  };
83
92
  export type TrackEventsResult = {
84
93
  accepted: number;
94
+ /** Rows skipped because their `event_id` was already stored. */
95
+ duplicates?: number;
85
96
  };
86
97
  export declare class APIError extends Error {
87
98
  readonly statusCode: number;
@@ -96,12 +107,28 @@ export declare class APIError extends Error {
96
107
  readonly code: string | null;
97
108
  constructor(statusCode: number, message: string, body: string, code?: string | null);
98
109
  }
110
+ /**
111
+ * Default per-request timeout, in milliseconds.
112
+ *
113
+ * A flag lookup that has not answered within a second is not going to
114
+ * be useful to the render that asked for it. Without a bound, a hung
115
+ * API hangs the calling application indefinitely — the exact failure
116
+ * the fail-open design exists to prevent, and one `variableValue`
117
+ * cannot catch because the request never returns.
118
+ */
119
+ export declare const DEFAULT_TIMEOUT_MS = 1000;
99
120
  export type ClientOptions = {
100
121
  apiKey: string;
101
122
  /** Override the API origin. Defaults to {@link DEFAULT_API_BASE_URL}. */
102
123
  baseUrl?: string;
103
124
  /** Override the fetch implementation (handy for tests). */
104
125
  fetchImpl?: typeof fetch;
126
+ /**
127
+ * Per-request timeout in milliseconds. Defaults to
128
+ * {@link DEFAULT_TIMEOUT_MS}. Pass 0 to disable the bound entirely —
129
+ * only sensible when the caller imposes its own.
130
+ */
131
+ timeoutMs?: number;
105
132
  };
106
133
  export type EvalOptions = {
107
134
  user?: UserContext;
@@ -115,7 +142,10 @@ export declare class RedPennonClient {
115
142
  readonly origin: string;
116
143
  readonly apiKey: string;
117
144
  readonly fetchImpl: typeof fetch;
145
+ readonly timeoutMs: number;
118
146
  constructor(options: ClientOptions);
147
+ /** Request init shared by every call, including the timeout signal. */
148
+ private requestInit;
119
149
  /**
120
150
  * Resolve a single variable to its full result object.
121
151
  *
@@ -148,5 +178,16 @@ export declare class RedPennonClient {
148
178
  * `variable_not_found` inline.
149
179
  */
150
180
  variables(keys: string[], options?: EvalOptions): Promise<BatchResults>;
181
+ /**
182
+ * Batch counterpart of {@link variableValue}: resolve many keys and
183
+ * return a value for each, falling back to the matching entry in
184
+ * `defaults` whenever the platform served no value or the call failed
185
+ * outright.
186
+ *
187
+ * Without this, only the single-key path actually failed open — a
188
+ * caller batching for efficiency had to hand-roll the try/catch that
189
+ * makes the SDK's central promise true.
190
+ */
191
+ variableValues<T extends Record<string, unknown>>(defaults: T, options?: EvalOptions): Promise<T>;
151
192
  }
152
193
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -52,6 +52,16 @@ function parseErrorCode(body) {
52
52
  }
53
53
  return null;
54
54
  }
55
+ /**
56
+ * Default per-request timeout, in milliseconds.
57
+ *
58
+ * A flag lookup that has not answered within a second is not going to
59
+ * be useful to the render that asked for it. Without a bound, a hung
60
+ * API hangs the calling application indefinitely — the exact failure
61
+ * the fail-open design exists to prevent, and one `variableValue`
62
+ * cannot catch because the request never returns.
63
+ */
64
+ export const DEFAULT_TIMEOUT_MS = 1000;
55
65
  /**
56
66
  * Thin client around the variable-key evaluation endpoints.
57
67
  * Stateless — one instance can serve any number of concurrent
@@ -61,10 +71,27 @@ export class RedPennonClient {
61
71
  origin;
62
72
  apiKey;
63
73
  fetchImpl;
74
+ timeoutMs;
64
75
  constructor(options) {
65
76
  this.origin = (options.baseUrl ?? DEFAULT_API_BASE_URL).replace(/\/+$/, "");
66
77
  this.apiKey = options.apiKey;
67
78
  this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
79
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
80
+ }
81
+ /** Request init shared by every call, including the timeout signal. */
82
+ requestInit(body) {
83
+ const init = {
84
+ method: "POST",
85
+ headers: {
86
+ "X-API-Key": this.apiKey,
87
+ "Content-Type": "application/json",
88
+ },
89
+ body: JSON.stringify(body),
90
+ };
91
+ if (this.timeoutMs > 0) {
92
+ init.signal = AbortSignal.timeout(this.timeoutMs);
93
+ }
94
+ return init;
68
95
  }
69
96
  /**
70
97
  * Resolve a single variable to its full result object.
@@ -77,14 +104,7 @@ export class RedPennonClient {
77
104
  const body = {};
78
105
  if (options.user)
79
106
  body.user = options.user;
80
- const response = await this.fetchImpl(`${this.origin}/v1/variables/${encodeURIComponent(key)}`, {
81
- method: "POST",
82
- headers: {
83
- "X-API-Key": this.apiKey,
84
- "Content-Type": "application/json",
85
- },
86
- body: JSON.stringify(body),
87
- });
107
+ const response = await this.fetchImpl(`${this.origin}/v1/variables/${encodeURIComponent(key)}`, this.requestInit(body));
88
108
  if (!response.ok) {
89
109
  const text = await response.text();
90
110
  throw new APIError(response.status, response.statusText, text, parseErrorCode(text));
@@ -120,14 +140,7 @@ export class RedPennonClient {
120
140
  * errors (`events_batch_too_large`, `rate_limit_exceeded`, etc.).
121
141
  */
122
142
  async trackEvents(events) {
123
- const response = await this.fetchImpl(`${this.origin}/v1/events`, {
124
- method: "POST",
125
- headers: {
126
- "X-API-Key": this.apiKey,
127
- "Content-Type": "application/json",
128
- },
129
- body: JSON.stringify({ events }),
130
- });
143
+ const response = await this.fetchImpl(`${this.origin}/v1/events`, this.requestInit({ events }));
131
144
  if (!response.ok) {
132
145
  const text = await response.text();
133
146
  throw new APIError(response.status, response.statusText, text, parseErrorCode(text));
@@ -143,14 +156,7 @@ export class RedPennonClient {
143
156
  const body = { keys };
144
157
  if (options.user)
145
158
  body.user = options.user;
146
- const response = await this.fetchImpl(`${this.origin}/v1/variables`, {
147
- method: "POST",
148
- headers: {
149
- "X-API-Key": this.apiKey,
150
- "Content-Type": "application/json",
151
- },
152
- body: JSON.stringify(body),
153
- });
159
+ const response = await this.fetchImpl(`${this.origin}/v1/variables`, this.requestInit(body));
154
160
  if (!response.ok) {
155
161
  const text = await response.text();
156
162
  throw new APIError(response.status, response.statusText, text, parseErrorCode(text));
@@ -158,4 +164,31 @@ export class RedPennonClient {
158
164
  const parsed = (await response.json());
159
165
  return parsed.results;
160
166
  }
167
+ /**
168
+ * Batch counterpart of {@link variableValue}: resolve many keys and
169
+ * return a value for each, falling back to the matching entry in
170
+ * `defaults` whenever the platform served no value or the call failed
171
+ * outright.
172
+ *
173
+ * Without this, only the single-key path actually failed open — a
174
+ * caller batching for efficiency had to hand-roll the try/catch that
175
+ * makes the SDK's central promise true.
176
+ */
177
+ async variableValues(defaults, options = {}) {
178
+ const keys = Object.keys(defaults);
179
+ try {
180
+ const results = await this.variables(keys, options);
181
+ const out = { ...defaults };
182
+ for (const key of keys) {
183
+ const value = results[key]?.value;
184
+ if (value !== null && value !== undefined) {
185
+ out[key] = value;
186
+ }
187
+ }
188
+ return out;
189
+ }
190
+ catch {
191
+ return { ...defaults };
192
+ }
193
+ }
161
194
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpennon/node-sdk",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "RedPennon feature flag evaluation API client for Node.js",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",