scorezilla 0.1.0-next.3 → 0.2.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.js CHANGED
@@ -31,6 +31,7 @@ function validateConfig(cfg) {
31
31
  timeoutMs: cfg.timeoutMs,
32
32
  maxRetries: cfg.maxRetries,
33
33
  sleepImpl: cfg.sleepImpl,
34
+ warn: cfg.warn,
34
35
  userAgent: cfg.userAgent,
35
36
  auth
36
37
  };
@@ -117,7 +118,9 @@ var ScorezillaError = class _ScorezillaError extends Error {
117
118
  * {@link ScorezillaErrorCode}. For network errors, this is `'network_error'`;
118
119
  * for aborts, `'aborted'`; for timeouts, `'timeout'`. */
119
120
  code;
120
- /** Sub-classifier — present on `out_of_bounds` (`'below_min' | 'above_max'`)
121
+ /** Sub-classifier — present on:
122
+ * - `out_of_bounds`: `'below_min' | 'above_max'`
123
+ * - `usage_cap_exceeded`: `'over_cap' | 'suspended'`
121
124
  * and possibly other codes in future minor releases. */
122
125
  reason;
123
126
  /** Seconds — present on `rate_limited`. Honored by the transport's retry
@@ -130,6 +133,20 @@ var ScorezillaError = class _ScorezillaError extends Error {
130
133
  bound;
131
134
  /** Which rate-limit layer fired on `rate_limited`. */
132
135
  layer;
136
+ /** Tenant's billing tier — present on `usage_cap_exceeded`. */
137
+ tier;
138
+ /** The cap value crossed on `usage_cap_exceeded`. `0` indicates a
139
+ * suspended tenant. `undefined` on all other error codes. */
140
+ cap;
141
+ /** The post-increment submit count on `usage_cap_exceeded`. Always
142
+ * `> cap` when `reason === 'over_cap'`. */
143
+ count;
144
+ /** The period the count belongs to on `usage_cap_exceeded`, in `YYYY-MM`
145
+ * UTC form. */
146
+ period;
147
+ /** ISO-8601 timestamp of midnight UTC on the 1st of the next month —
148
+ * the counter's natural reset point on `usage_cap_exceeded`. */
149
+ resetsAt;
133
150
  /** The underlying cause (e.g., a `TypeError: fetch failed`) for
134
151
  * network/abort/timeout paths. `undefined` when the error came from a
135
152
  * successfully-parsed API error body. */
@@ -144,6 +161,11 @@ var ScorezillaError = class _ScorezillaError extends Error {
144
161
  this.requestId = truncateField(init.requestId);
145
162
  this.bound = init.bound;
146
163
  this.layer = truncateField(init.layer);
164
+ this.tier = truncateField(init.tier);
165
+ this.cap = init.cap;
166
+ this.count = init.count;
167
+ this.period = truncateField(init.period);
168
+ this.resetsAt = truncateField(init.resetsAt);
147
169
  this.cause = init.cause;
148
170
  Object.setPrototypeOf(this, _ScorezillaError.prototype);
149
171
  if (typeof Error.captureStackTrace === "function") {
@@ -157,6 +179,22 @@ var ScorezillaError = class _ScorezillaError extends Error {
157
179
  isRateLimited() {
158
180
  return this.code === "rate_limited";
159
181
  }
182
+ /**
183
+ * `true` when this error is a 402 / `usage_cap_exceeded`. The tenant
184
+ * has either hit their tier's monthly submit cap (`reason ===
185
+ * 'over_cap'`) or is suspended (`reason === 'suspended'`).
186
+ *
187
+ * Consumers SHOULD NOT auto-retry on this error — the cap doesn't lift
188
+ * until `resetsAt`. Surface to the developer with an upgrade prompt
189
+ * (over_cap) or contact-support message (suspended).
190
+ */
191
+ isUsageCapExceeded() {
192
+ return this.code === "usage_cap_exceeded";
193
+ }
194
+ /** `true` when this error is a 402 + reason 'suspended' (vs over-cap). */
195
+ isSuspended() {
196
+ return this.code === "usage_cap_exceeded" && this.reason === "suspended";
197
+ }
160
198
  /** `true` when this error is a 401 / `unauthorized` (or 403 / `forbidden`). */
161
199
  isAuth() {
162
200
  return this.code === "unauthorized" || this.code === "forbidden";
@@ -169,13 +207,21 @@ var ScorezillaError = class _ScorezillaError extends Error {
169
207
  isOutOfBounds() {
170
208
  return this.code === "out_of_bounds";
171
209
  }
172
- /** `true` for transient / retryable conditions: network errors, timeouts,
173
- * 5xx, and 429. The transport layer relies on this for its retry policy. */
210
+ /** `true` for the SDK's retryable conditions: pure network errors, 5xx, and
211
+ * 429. Deliberately excludes `timeout` and `aborted` those are caller-
212
+ * observable terminal states, not transient. Aligned with `shouldRetryError`
213
+ * in `retry.ts` so a consumer mirroring the SDK's retry policy gets the
214
+ * same answer the transport does. */
174
215
  isTransient() {
175
- if (this.status === STATUS_NETWORK_ERROR) return true;
216
+ if (this.code === "network_error") return true;
176
217
  if (this.status >= 500 && this.status < 600) return true;
177
218
  return this.isRateLimited();
178
219
  }
220
+ /** `true` when this error is a 409 / `conflict` (idempotency-key conflict
221
+ * on retry). */
222
+ isConflict() {
223
+ return this.code === "conflict";
224
+ }
179
225
  // ─── Factory ─────────────────────────────────────────────────────────
180
226
  /**
181
227
  * Build a `ScorezillaError` from a fetch round-trip outcome.
@@ -196,6 +242,13 @@ var ScorezillaError = class _ScorezillaError extends Error {
196
242
  retryAfter: body.retryAfter,
197
243
  bound: body.bound,
198
244
  layer: body.layer,
245
+ // Usage-cap fields from `ApiError` (populated by the server on
246
+ // 402 responses; undefined on other errors).
247
+ tier: body.tier,
248
+ cap: body.cap,
249
+ count: body.count,
250
+ period: body.period,
251
+ resetsAt: body.resetsAt,
199
252
  requestId,
200
253
  cause
201
254
  });
@@ -237,8 +290,10 @@ var ScorezillaError = class _ScorezillaError extends Error {
237
290
  };
238
291
  function codeForStatus(status) {
239
292
  if (status === 401) return "unauthorized";
293
+ if (status === 402) return "usage_cap_exceeded";
240
294
  if (status === 403) return "forbidden";
241
295
  if (status === 404) return "not_found";
296
+ if (status === 409) return "conflict";
242
297
  if (status === 422) return "out_of_bounds";
243
298
  if (status === 429) return "rate_limited";
244
299
  if (status >= 500) return "internal_error";
@@ -349,6 +404,7 @@ async function request(opts) {
349
404
  }
350
405
  const response = await fetchImpl(url, init);
351
406
  if (response.ok) {
407
+ warnOnDeprecationOnce(response, opts.warnImpl);
352
408
  return await parseJson(response);
353
409
  }
354
410
  const body = await safelyParseErrorBody(response);
@@ -460,6 +516,29 @@ function readRetryAfter(response) {
460
516
  const n = Number(raw);
461
517
  return Number.isFinite(n) && n >= 0 ? n : void 0;
462
518
  }
519
+ var seenDeprecations = /* @__PURE__ */ new Set();
520
+ function warnOnDeprecationOnce(response, warnImpl) {
521
+ const deprecation = response.headers.get("Deprecation");
522
+ const sunset = response.headers.get("Sunset");
523
+ if (!deprecation && !sunset) return;
524
+ const link = response.headers.get("Link") ?? "";
525
+ const key = `${deprecation ?? ""}|${sunset ?? ""}|${link}`;
526
+ if (seenDeprecations.has(key)) return;
527
+ seenDeprecations.add(key);
528
+ const detail = [];
529
+ if (deprecation === "true" || deprecation) detail.push(`Deprecation: ${deprecation}`);
530
+ if (sunset) detail.push(`Sunset: ${sunset}`);
531
+ if (link) {
532
+ const m = link.match(/<([^>]+)>/);
533
+ if (m) detail.push(`Docs: ${m[1]}`);
534
+ }
535
+ const message = `[scorezilla-sdk] API responded with deprecation signal: ${detail.join(" \xB7 ")}. Upgrade your SDK before the sunset date.`;
536
+ if (warnImpl) {
537
+ warnImpl(message);
538
+ } else {
539
+ console.warn(message);
540
+ }
541
+ }
463
542
  function combineSignalsWithTimeout(caller, timeoutMs) {
464
543
  const ctrl = new AbortController();
465
544
  let didTimeOut = false;
@@ -548,10 +627,11 @@ function validateMetadata(metadata) {
548
627
  `scorezilla: metadata exceeds ${METADATA_MAX_BYTES} bytes (got ${byteLength} bytes when JSON-stringified)`
549
628
  );
550
629
  }
630
+ return serialized;
551
631
  }
552
632
  var Scorezilla = class _Scorezilla {
553
633
  /** The package version, injected at build time from `package.json`. */
554
- static version = "0.1.0-next.3";
634
+ static version = "0.2.0";
555
635
  #config;
556
636
  #userAgent;
557
637
  #authHeader;
@@ -609,7 +689,8 @@ var Scorezilla = class _Scorezilla {
609
689
  return this.#request({
610
690
  path: submitScorePath(input.boardId),
611
691
  method: "POST",
612
- body
692
+ body,
693
+ signal: input.signal
613
694
  });
614
695
  }
615
696
  /**
@@ -633,7 +714,8 @@ var Scorezilla = class _Scorezilla {
633
714
  if (input.offset !== void 0) q.offset = input.offset;
634
715
  return this.#request({
635
716
  path: getLeaderboardPath(input.boardId, q),
636
- method: "GET"
717
+ method: "GET",
718
+ signal: input.signal
637
719
  });
638
720
  }
639
721
  /**
@@ -662,7 +744,8 @@ var Scorezilla = class _Scorezilla {
662
744
  async getPlayerRank(input) {
663
745
  return this.#request({
664
746
  path: getPlayerRankPath(input.boardId, input.playerId),
665
- method: "GET"
747
+ method: "GET",
748
+ signal: input.signal
666
749
  });
667
750
  }
668
751
  /**
@@ -687,7 +770,8 @@ var Scorezilla = class _Scorezilla {
687
770
  if (input.after !== void 0) q.after = input.after;
688
771
  return this.#request({
689
772
  path: getWindowAroundPath(input.boardId, input.playerId, q),
690
- method: "GET"
773
+ method: "GET",
774
+ signal: input.signal
691
775
  });
692
776
  }
693
777
  // ─── Internal ────────────────────────────────────────────────────────
@@ -714,7 +798,9 @@ var Scorezilla = class _Scorezilla {
714
798
  headers
715
799
  };
716
800
  if (opts.body !== void 0) requestOpts.body = opts.body;
801
+ if (opts.signal !== void 0) requestOpts.signal = opts.signal;
717
802
  if (this.#config.fetch !== void 0) requestOpts.fetchImpl = this.#config.fetch;
803
+ if (this.#config.warn !== void 0) requestOpts.warnImpl = this.#config.warn;
718
804
  if (this.#config.timeoutMs !== void 0) requestOpts.timeoutMs = this.#config.timeoutMs;
719
805
  if (this.#config.maxRetries !== void 0 || this.#config.sleepImpl !== void 0) {
720
806
  requestOpts.retry = {
@@ -730,7 +816,7 @@ function createClient(config) {
730
816
  }
731
817
 
732
818
  // src/index.ts
733
- var SDK_VERSION = "0.1.0-next.3";
819
+ var SDK_VERSION = "0.2.0";
734
820
 
735
821
  export { SDK_VERSION, Scorezilla, ScorezillaError, createClient, detectRuntime };
736
822
  //# sourceMappingURL=index.js.map