@bedrock-rbx/ocale 0.1.0-beta.9 → 0.1.1
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 +182 -0
- package/dist/badges.d.mts +33 -2
- package/dist/badges.d.mts.map +1 -1
- package/dist/badges.mjs +16 -4
- package/dist/badges.mjs.map +1 -1
- package/dist/{data.generated-Cb6g6asv.d.mts → data.generated-B2ceLfSn.d.mts} +9 -1
- package/dist/data.generated-B2ceLfSn.d.mts.map +1 -0
- package/dist/developer-products.d.mts +38 -2
- package/dist/developer-products.d.mts.map +1 -1
- package/dist/developer-products.mjs +32 -8
- package/dist/developer-products.mjs.map +1 -1
- package/dist/game-passes.d.mts +40 -2
- package/dist/game-passes.d.mts.map +1 -1
- package/dist/game-passes.mjs +30 -6
- package/dist/game-passes.mjs.map +1 -1
- package/dist/index.d.mts +21 -12
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +5 -5
- package/dist/locales.d.mts +1 -1
- package/dist/locales.mjs +2 -0
- package/dist/locales.mjs.map +1 -1
- package/dist/luau-execution.d.mts +22 -3
- package/dist/luau-execution.d.mts.map +1 -1
- package/dist/luau-execution.mjs +29 -13
- package/dist/luau-execution.mjs.map +1 -1
- package/dist/places.d.mts +20 -2
- package/dist/places.d.mts.map +1 -1
- package/dist/places.mjs +34 -14
- package/dist/places.mjs.map +1 -1
- package/dist/{poll-timeout-Dg_QFEqi.mjs → poll-timeout-BfxUWWCZ.mjs} +6 -2
- package/dist/poll-timeout-BfxUWWCZ.mjs.map +1 -0
- package/dist/{polling-helpers-CS00apdU.mjs → polling-helpers-DCw9LyJM.mjs} +225 -78
- package/dist/polling-helpers-DCw9LyJM.mjs.map +1 -0
- package/dist/{polling-BMrYajok.d.mts → polling-rj0dGREg.d.mts} +73 -14
- package/dist/polling-rj0dGREg.d.mts.map +1 -0
- package/dist/{price-information-DIrvwCmd.mjs → price-information-C4gC2CMZ.mjs} +2 -2
- package/dist/{price-information-DIrvwCmd.mjs.map → price-information-C4gC2CMZ.mjs.map} +1 -1
- package/dist/{rate-limit-D1q2Js-z.mjs → rate-limit-Dh2leqaB.mjs} +49 -3
- package/dist/rate-limit-Dh2leqaB.mjs.map +1 -0
- package/dist/resource-client-CIAkS2xQ.mjs +960 -0
- package/dist/resource-client-CIAkS2xQ.mjs.map +1 -0
- package/dist/retry-Bh2nNjBV.d.mts +234 -0
- package/dist/retry-Bh2nNjBV.d.mts.map +1 -0
- package/dist/retry-BvZRZDXs.mjs +380 -0
- package/dist/retry-BvZRZDXs.mjs.map +1 -0
- package/dist/{signatures-B5Fojgn0.mjs → signatures-JF-7Psce.mjs} +1 -1
- package/dist/{signatures-B5Fojgn0.mjs.map → signatures-JF-7Psce.mjs.map} +1 -1
- package/dist/storage.d.mts +30 -27
- package/dist/storage.d.mts.map +1 -1
- package/dist/storage.mjs +46 -31
- package/dist/storage.mjs.map +1 -1
- package/dist/testing.d.mts +24 -9
- package/dist/testing.d.mts.map +1 -1
- package/dist/testing.mjs +2 -2
- package/dist/testing.mjs.map +1 -1
- package/dist/{to-blob-B27VhoRp.mjs → to-blob-RPqMBuft.mjs} +1 -1
- package/dist/{to-blob-B27VhoRp.mjs.map → to-blob-RPqMBuft.mjs.map} +1 -1
- package/dist/{types-CwtZT1ek.d.mts → types-CRiJg5EQ.d.mts} +46 -4
- package/dist/types-CRiJg5EQ.d.mts.map +1 -0
- package/dist/universes.d.mts +47 -5
- package/dist/universes.d.mts.map +1 -1
- package/dist/universes.mjs +31 -15
- package/dist/universes.mjs.map +1 -1
- package/dist/{validation-DkL5KQqz.mjs → validation-xJZRa8tX.mjs} +4 -2
- package/dist/validation-xJZRa8tX.mjs.map +1 -0
- package/dist/{wire-CeL6W_V7.d.mts → wire-D3K-a-UP.d.mts} +3 -1
- package/dist/wire-D3K-a-UP.d.mts.map +1 -0
- package/package.json +6 -5
- package/dist/data.generated-Cb6g6asv.d.mts.map +0 -1
- package/dist/permission-error-DOVtNq3A.mjs +0 -46
- package/dist/permission-error-DOVtNq3A.mjs.map +0 -1
- package/dist/poll-timeout-Dg_QFEqi.mjs.map +0 -1
- package/dist/polling-BMrYajok.d.mts.map +0 -1
- package/dist/polling-helpers-CS00apdU.mjs.map +0 -1
- package/dist/rate-limit-BYuizHoD.d.mts +0 -92
- package/dist/rate-limit-BYuizHoD.d.mts.map +0 -1
- package/dist/rate-limit-D1q2Js-z.mjs.map +0 -1
- package/dist/resource-client-D6Efj9fU.mjs +0 -732
- package/dist/resource-client-D6Efj9fU.mjs.map +0 -1
- package/dist/types-CwtZT1ek.d.mts.map +0 -1
- package/dist/validation-DkL5KQqz.mjs.map +0 -1
- package/dist/wire-CeL6W_V7.d.mts.map +0 -1
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { n as NetworkError, r as ApiError, t as RateLimitError } from "./rate-limit-Dh2leqaB.mjs";
|
|
2
|
+
//#region src/errors/permission-error.ts
|
|
3
|
+
/**
|
|
4
|
+
* Thrown when the Roblox Open Cloud API returns a 401 or 403 for an operation
|
|
5
|
+
* whose required scopes are known. Subclass of {@link ApiError} carrying the
|
|
6
|
+
* scope strings the caller's credential is missing plus the operation key, so
|
|
7
|
+
* a CLI consumer can tell the user exactly which scope to grant on their API
|
|
8
|
+
* key.
|
|
9
|
+
*
|
|
10
|
+
* @since 0.1.0
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { PermissionError } from "@bedrock-rbx/ocale";
|
|
16
|
+
*
|
|
17
|
+
* const error = new PermissionError("HTTP 403", {
|
|
18
|
+
* operationKey: "developer-products.create",
|
|
19
|
+
* requiredScopes: ["creator-store-product:write"],
|
|
20
|
+
* statusCode: 403,
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* expect(error).toBeInstanceOf(PermissionError);
|
|
24
|
+
* expect(error.requiredScopes).toStrictEqual(["creator-store-product:write"]);
|
|
25
|
+
* expect(error.operationKey).toBe("developer-products.create");
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
var PermissionError = class extends ApiError {
|
|
29
|
+
name = "PermissionError";
|
|
30
|
+
operationKey;
|
|
31
|
+
requiredScopes;
|
|
32
|
+
/**
|
|
33
|
+
* Creates a new PermissionError.
|
|
34
|
+
*
|
|
35
|
+
* @param message - Human-readable error description.
|
|
36
|
+
* @param options - Error options including status code, the operation key,
|
|
37
|
+
* and the scopes the caller's credential must carry.
|
|
38
|
+
*/
|
|
39
|
+
constructor(message, options) {
|
|
40
|
+
super(message, options);
|
|
41
|
+
this.operationKey = options.operationKey;
|
|
42
|
+
this.requiredScopes = options.requiredScopes;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/internal/utils/find-error-code.ts
|
|
47
|
+
/**
|
|
48
|
+
* Maximum cause-chain depth walked by {@link findErrorCode}. Caps pathological
|
|
49
|
+
* self-referential or deeply nested chains; transport failures surface as
|
|
50
|
+
* `NetworkError → TypeError("fetch failed") → OS Error{code}`, so three
|
|
51
|
+
* levels is the expected shape and five leaves headroom.
|
|
52
|
+
*/
|
|
53
|
+
const MAX_DEPTH = 5;
|
|
54
|
+
/**
|
|
55
|
+
* Walks an error's `cause` chain and returns the first node-style string
|
|
56
|
+
* `code` it finds (for example `"ECONNRESET"`, `"ETIMEDOUT"`). Native `fetch`
|
|
57
|
+
* surfaces a transport reset as a `NetworkError` wrapping a
|
|
58
|
+
* `TypeError("fetch failed")` whose own cause carries the OS-level `code`, so
|
|
59
|
+
* the code lives several links down the chain.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
*
|
|
63
|
+
* ```ts
|
|
64
|
+
* import { findErrorCode } from "./find-error-code";
|
|
65
|
+
*
|
|
66
|
+
* const root = Object.assign(new Error("read ECONNRESET"), { code: "ECONNRESET" });
|
|
67
|
+
* const outer = new Error("Network request failed", {
|
|
68
|
+
* cause: new TypeError("fetch failed", { cause: root }),
|
|
69
|
+
* });
|
|
70
|
+
*
|
|
71
|
+
* expect(findErrorCode(outer)).toBe("ECONNRESET");
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* @param error - The error to inspect; typically a `NetworkError`.
|
|
75
|
+
* @returns The first string `code` in the chain, or `undefined` if none.
|
|
76
|
+
*/
|
|
77
|
+
function findErrorCode(error) {
|
|
78
|
+
let current = error;
|
|
79
|
+
for (let depth = 0; depth < MAX_DEPTH && current instanceof Error; depth += 1) {
|
|
80
|
+
const code = readCode(current);
|
|
81
|
+
if (code !== void 0) return code;
|
|
82
|
+
current = current.cause;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function readCode(error) {
|
|
86
|
+
const code = Reflect.get(error, "code");
|
|
87
|
+
return typeof code === "string" ? code : void 0;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* `DOMException.name` produced when an `AbortSignal.timeout` fires. This is the
|
|
91
|
+
* web-standard discriminator (stable across Node and Bun, unlike the
|
|
92
|
+
* runtime-specific message) and distinguishes the SDK's own request timeout
|
|
93
|
+
* from a caller-supplied cancellation, which surfaces as `"AbortError"`.
|
|
94
|
+
*/
|
|
95
|
+
const TIMEOUT_ABORT_NAME = "TimeoutError";
|
|
96
|
+
/**
|
|
97
|
+
* Reports whether an error chain was produced by the SDK's own
|
|
98
|
+
* `AbortSignal.timeout` self-abort. Such a `DOMException` carries a numeric
|
|
99
|
+
* `code` (23), so {@link findErrorCode} (which only reads string codes)
|
|
100
|
+
* cannot classify it; this walk keys on `name` instead. A caller-supplied
|
|
101
|
+
* abort (`"AbortError"`) is deliberately not matched.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
*
|
|
105
|
+
* ```ts
|
|
106
|
+
* import { isTimeoutAbort } from "./find-error-code";
|
|
107
|
+
*
|
|
108
|
+
* const error = new Error("Network request failed", {
|
|
109
|
+
* cause: new DOMException("timed out", "TimeoutError"),
|
|
110
|
+
* });
|
|
111
|
+
*
|
|
112
|
+
* expect(isTimeoutAbort(error)).toBe(true);
|
|
113
|
+
* expect(isTimeoutAbort(new DOMException("cancelled", "AbortError"))).toBe(false);
|
|
114
|
+
* ```
|
|
115
|
+
*
|
|
116
|
+
* @param error - The error to inspect; typically a `NetworkError`.
|
|
117
|
+
* @returns `true` when a `TimeoutError` abort sits within the cause chain.
|
|
118
|
+
*/
|
|
119
|
+
function isTimeoutAbort(error) {
|
|
120
|
+
let current = error;
|
|
121
|
+
for (let depth = 0; depth < MAX_DEPTH && current instanceof Error; depth += 1) {
|
|
122
|
+
if (Reflect.get(current, "name") === TIMEOUT_ABORT_NAME) return true;
|
|
123
|
+
current = current.cause;
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/internal/http/retry.ts
|
|
129
|
+
/**
|
|
130
|
+
* Transient transport error codes that are safe to retry for idempotent
|
|
131
|
+
* operations. Connection resets, timeouts, and DNS hiccups are recoverable on
|
|
132
|
+
* a retry. A self-aborted request timeout carries no OS-level `code`, so
|
|
133
|
+
* {@link shouldRetry} folds it into this set as `ETIMEDOUT` (via
|
|
134
|
+
* {@link isTimeoutAbort}) for idempotent methods; create methods retry no
|
|
135
|
+
* transport codes and so still never re-issue a timed-out write.
|
|
136
|
+
*
|
|
137
|
+
* @since 0.1.0
|
|
138
|
+
*/
|
|
139
|
+
const TRANSIENT_TRANSPORT_CODES = Object.freeze([
|
|
140
|
+
"ECONNRESET",
|
|
141
|
+
"ECONNREFUSED",
|
|
142
|
+
"ETIMEDOUT",
|
|
143
|
+
"EPIPE",
|
|
144
|
+
"ENETUNREACH",
|
|
145
|
+
"EHOSTDOWN",
|
|
146
|
+
"EAI_AGAIN",
|
|
147
|
+
"UND_ERR_SOCKET"
|
|
148
|
+
]);
|
|
149
|
+
/**
|
|
150
|
+
* Default retry policy for idempotent operations (read, list, update,
|
|
151
|
+
* delete). Safe to retry on rate limits, transient server errors, and
|
|
152
|
+
* transient transport failures.
|
|
153
|
+
*/
|
|
154
|
+
const IDEMPOTENT_METHOD_DEFAULTS = Object.freeze({
|
|
155
|
+
retryableStatuses: Object.freeze([
|
|
156
|
+
429,
|
|
157
|
+
500,
|
|
158
|
+
502,
|
|
159
|
+
503,
|
|
160
|
+
504
|
|
161
|
+
]),
|
|
162
|
+
retryableTransportCodes: TRANSIENT_TRANSPORT_CODES
|
|
163
|
+
});
|
|
164
|
+
/**
|
|
165
|
+
* Default retry policy for create operations. Retries rate limits only (no
|
|
166
|
+
* 5xx and no transport-error retries) to prevent duplicate resources, since
|
|
167
|
+
* Roblox Open Cloud has no idempotency-key support. Consumers who can tolerate
|
|
168
|
+
* a duplicate opt in per request.
|
|
169
|
+
*/
|
|
170
|
+
const CREATE_METHOD_DEFAULTS = Object.freeze({
|
|
171
|
+
retryableStatuses: Object.freeze([429]),
|
|
172
|
+
retryableTransportCodes: Object.freeze([])
|
|
173
|
+
});
|
|
174
|
+
/**
|
|
175
|
+
* Default exponential backoff: 1s → 2s → 4s → 8s → 16s → 30s (capped).
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
*
|
|
179
|
+
* ```ts
|
|
180
|
+
* import { defaultRetryDelay } from "./retry";
|
|
181
|
+
*
|
|
182
|
+
* expect(defaultRetryDelay(0)).toBe(1000);
|
|
183
|
+
* expect(defaultRetryDelay(4)).toBe(16_000);
|
|
184
|
+
* expect(defaultRetryDelay(10)).toBe(30_000);
|
|
185
|
+
* ```
|
|
186
|
+
*
|
|
187
|
+
* @param attempt - Zero-indexed retry attempt number.
|
|
188
|
+
* @returns Wait duration in milliseconds.
|
|
189
|
+
*/
|
|
190
|
+
function defaultRetryDelay(attempt) {
|
|
191
|
+
return Math.min(1e3 * 2 ** attempt, 3e4);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Computes how long to wait before the next retry. Prefers the server's
|
|
195
|
+
* suggested delay when the error is a {@link RateLimitError} with a positive
|
|
196
|
+
* `retryAfterSeconds`; otherwise falls through to `retryDelay(attempt)`.
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
*
|
|
200
|
+
* ```ts
|
|
201
|
+
* import { RateLimitError } from "../../errors/rate-limit.ts";
|
|
202
|
+
* import { computeRetryWaitMs, defaultRetryDelay } from "./retry";
|
|
203
|
+
*
|
|
204
|
+
* const error = new RateLimitError("slow down", { retryAfterSeconds: 3 });
|
|
205
|
+
*
|
|
206
|
+
* expect(computeRetryWaitMs(error, { attempt: 0, retryDelay: defaultRetryDelay })).toBe(
|
|
207
|
+
* 3000,
|
|
208
|
+
* );
|
|
209
|
+
* ```
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
*
|
|
213
|
+
* ```ts
|
|
214
|
+
* import { ApiError } from "../../errors/api-error.ts";
|
|
215
|
+
* import { computeRetryWaitMs, defaultRetryDelay } from "./retry";
|
|
216
|
+
*
|
|
217
|
+
* const error = new ApiError("server error", { statusCode: 503 });
|
|
218
|
+
*
|
|
219
|
+
* expect(computeRetryWaitMs(error, { attempt: 2, retryDelay: defaultRetryDelay })).toBe(
|
|
220
|
+
* 4000,
|
|
221
|
+
* );
|
|
222
|
+
* ```
|
|
223
|
+
*
|
|
224
|
+
* @param error - The error returned by the failing request.
|
|
225
|
+
* @param options - Retry attempt index and fallback delay function.
|
|
226
|
+
* @returns Wait duration in milliseconds before the next attempt.
|
|
227
|
+
*/
|
|
228
|
+
function computeRetryWaitMs(error, options) {
|
|
229
|
+
if (error instanceof RateLimitError && error.retryAfterSeconds > 0) return error.retryAfterSeconds * 1e3;
|
|
230
|
+
return options.retryDelay(options.attempt);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Decides whether a failed request is eligible for retry. {@link RateLimitError}
|
|
234
|
+
* (checked against 429) and {@link ApiError} (checked against its `statusCode`)
|
|
235
|
+
* are retryable when their status is in `retryableStatuses`. A
|
|
236
|
+
* {@link NetworkError} is retryable when its transport code
|
|
237
|
+
* ({@link findErrorCode}) is in `retryableTransportCodes`. This is how
|
|
238
|
+
* transient connection resets recover. A self-aborted request timeout
|
|
239
|
+
* ({@link isTimeoutAbort}) carries no transport code, so it is classified as
|
|
240
|
+
* `ETIMEDOUT`: recovered for idempotent methods, never for creates (whose
|
|
241
|
+
* list is empty). All other failures return `false`.
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
*
|
|
245
|
+
* ```ts
|
|
246
|
+
* import { RateLimitError } from "../../errors/rate-limit.ts";
|
|
247
|
+
* import { shouldRetry } from "./retry";
|
|
248
|
+
*
|
|
249
|
+
* const error = new RateLimitError("", { retryAfterSeconds: 1 });
|
|
250
|
+
*
|
|
251
|
+
* expect(shouldRetry(error, { retryableStatuses: [429], retryableTransportCodes: [] })).toBe(
|
|
252
|
+
* true,
|
|
253
|
+
* );
|
|
254
|
+
* ```
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
*
|
|
258
|
+
* ```ts
|
|
259
|
+
* import { NetworkError } from "../../errors/network-error.ts";
|
|
260
|
+
* import { shouldRetry } from "./retry";
|
|
261
|
+
*
|
|
262
|
+
* const reset = Object.assign(new Error("read ECONNRESET"), { code: "ECONNRESET" });
|
|
263
|
+
* const error = new NetworkError("Network request failed", { cause: reset });
|
|
264
|
+
*
|
|
265
|
+
* expect(
|
|
266
|
+
* shouldRetry(error, { retryableStatuses: [], retryableTransportCodes: ["ECONNRESET"] }),
|
|
267
|
+
* ).toBe(true);
|
|
268
|
+
* ```
|
|
269
|
+
*
|
|
270
|
+
* @param error - The error returned by the failing request.
|
|
271
|
+
* @param config - Object carrying the retry-eligible status and transport-code lists.
|
|
272
|
+
* @returns `true` if the error should be retried, `false` otherwise.
|
|
273
|
+
*/
|
|
274
|
+
function shouldRetry(error, config) {
|
|
275
|
+
if (error instanceof RateLimitError) return config.retryableStatuses.includes(429);
|
|
276
|
+
if (error instanceof ApiError) return config.retryableStatuses.includes(error.statusCode);
|
|
277
|
+
if (error instanceof NetworkError) {
|
|
278
|
+
const code = findErrorCode(error) ?? (isTimeoutAbort(error) ? "ETIMEDOUT" : void 0);
|
|
279
|
+
return code !== void 0 && config.retryableTransportCodes.includes(code);
|
|
280
|
+
}
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Resolves the effective config for a single request by shallow-merging the
|
|
285
|
+
* client config, method defaults, and per-request options. Precedence depends
|
|
286
|
+
* on `methodKind`:
|
|
287
|
+
*
|
|
288
|
+
* - `"create"`: method defaults override client config, so client-level
|
|
289
|
+
* settings cannot silently relax create-method safety. Only explicit
|
|
290
|
+
* per-request `requestOptions` can.
|
|
291
|
+
* - `"idempotent"`: client config overrides method defaults, so consumers
|
|
292
|
+
* can loosen or tighten retry policy globally. `requestOptions` still wins
|
|
293
|
+
* when provided.
|
|
294
|
+
*
|
|
295
|
+
* Array-valued fields like `retryableStatuses` are *replaced*, not extended.
|
|
296
|
+
*
|
|
297
|
+
* @template T - Concrete `RetryResolvable` subtype being merged.
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
*
|
|
301
|
+
* ```ts
|
|
302
|
+
* import {
|
|
303
|
+
* CREATE_METHOD_DEFAULTS,
|
|
304
|
+
* defaultRetryDelay,
|
|
305
|
+
* mergeConfig,
|
|
306
|
+
* type RetryResolvable,
|
|
307
|
+
* } from "./retry";
|
|
308
|
+
*
|
|
309
|
+
* const clientConfig: RetryResolvable = {
|
|
310
|
+
* apiKey: "k",
|
|
311
|
+
* baseUrl: "https://apis.roblox.com",
|
|
312
|
+
* maxRetries: 3,
|
|
313
|
+
* retryableStatuses: [429, 500],
|
|
314
|
+
* retryableTransportCodes: [],
|
|
315
|
+
* retryDelay: defaultRetryDelay,
|
|
316
|
+
* timeout: 30_000,
|
|
317
|
+
* };
|
|
318
|
+
*
|
|
319
|
+
* const merged = mergeConfig(clientConfig, {
|
|
320
|
+
* methodDefaults: CREATE_METHOD_DEFAULTS,
|
|
321
|
+
* methodKind: "create",
|
|
322
|
+
* });
|
|
323
|
+
*
|
|
324
|
+
* expect(merged.retryableStatuses).toStrictEqual([429]);
|
|
325
|
+
* ```
|
|
326
|
+
*
|
|
327
|
+
* @example
|
|
328
|
+
*
|
|
329
|
+
* ```ts
|
|
330
|
+
* import {
|
|
331
|
+
* defaultRetryDelay,
|
|
332
|
+
* IDEMPOTENT_METHOD_DEFAULTS,
|
|
333
|
+
* mergeConfig,
|
|
334
|
+
* type RetryResolvable,
|
|
335
|
+
* } from "./retry";
|
|
336
|
+
*
|
|
337
|
+
* const clientConfig: RetryResolvable = {
|
|
338
|
+
* apiKey: "k",
|
|
339
|
+
* baseUrl: "https://apis.roblox.com",
|
|
340
|
+
* maxRetries: 3,
|
|
341
|
+
* retryableStatuses: [429],
|
|
342
|
+
* retryableTransportCodes: [],
|
|
343
|
+
* retryDelay: defaultRetryDelay,
|
|
344
|
+
* timeout: 30_000,
|
|
345
|
+
* };
|
|
346
|
+
*
|
|
347
|
+
* const merged = mergeConfig(clientConfig, {
|
|
348
|
+
* methodDefaults: IDEMPOTENT_METHOD_DEFAULTS,
|
|
349
|
+
* methodKind: "idempotent",
|
|
350
|
+
* requestOptions: { timeout: 10_000 },
|
|
351
|
+
* });
|
|
352
|
+
*
|
|
353
|
+
* expect(merged.retryableStatuses).toStrictEqual([429]);
|
|
354
|
+
* expect(merged.timeout).toBe(10_000);
|
|
355
|
+
* ```
|
|
356
|
+
*
|
|
357
|
+
* @param clientConfig - Config frozen at client construction.
|
|
358
|
+
* @param options - Method defaults, method kind, and optional per-request overrides.
|
|
359
|
+
* @returns A new merged config object. Inputs are not mutated.
|
|
360
|
+
*/
|
|
361
|
+
function mergeConfig(clientConfig, options) {
|
|
362
|
+
const { methodDefaults, methodKind, requestOptions } = options;
|
|
363
|
+
switch (methodKind) {
|
|
364
|
+
case "create": return {
|
|
365
|
+
...clientConfig,
|
|
366
|
+
...methodDefaults,
|
|
367
|
+
...requestOptions
|
|
368
|
+
};
|
|
369
|
+
case "idempotent": return {
|
|
370
|
+
...methodDefaults,
|
|
371
|
+
...clientConfig,
|
|
372
|
+
...requestOptions
|
|
373
|
+
};
|
|
374
|
+
default: throw new Error(`Unexpected methodKind: ${String(methodKind)}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
//#endregion
|
|
378
|
+
export { defaultRetryDelay as a, findErrorCode as c, computeRetryWaitMs as i, PermissionError as l, IDEMPOTENT_METHOD_DEFAULTS as n, mergeConfig as o, TRANSIENT_TRANSPORT_CODES as r, shouldRetry as s, CREATE_METHOD_DEFAULTS as t };
|
|
379
|
+
|
|
380
|
+
//# sourceMappingURL=retry-BvZRZDXs.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry-BvZRZDXs.mjs","names":["exhaustive"],"sources":["../src/errors/permission-error.ts","../src/internal/utils/find-error-code.ts","../src/internal/http/retry.ts"],"sourcesContent":["import { ApiError, type ApiErrorOptions } from \"./api-error.ts\";\n\n/**\n * Options for constructing a {@link PermissionError}.\n *\n * @since 0.1.0\n */\nexport interface PermissionErrorOptions extends ApiErrorOptions {\n\t/**\n\t * Stable identifier of the Open Cloud operation that returned the\n\t * permission failure (matches `OperationLimit.operationKey`, e.g.\n\t * `\"developer-products.create\"`).\n\t */\n\toperationKey: string;\n\t/**\n\t * Scope strings the API key or OAuth token must carry for the failing\n\t * operation, sourced from the vendored OpenAPI schema's `x-roblox-scopes`\n\t * for that operationId.\n\t */\n\trequiredScopes: ReadonlyArray<string>;\n}\n\n/**\n * Thrown when the Roblox Open Cloud API returns a 401 or 403 for an operation\n * whose required scopes are known. Subclass of {@link ApiError} carrying the\n * scope strings the caller's credential is missing plus the operation key, so\n * a CLI consumer can tell the user exactly which scope to grant on their API\n * key.\n *\n * @since 0.1.0\n *\n * @example\n *\n * ```ts\n * import { PermissionError } from \"@bedrock-rbx/ocale\";\n *\n * const error = new PermissionError(\"HTTP 403\", {\n * operationKey: \"developer-products.create\",\n * requiredScopes: [\"creator-store-product:write\"],\n * statusCode: 403,\n * });\n *\n * expect(error).toBeInstanceOf(PermissionError);\n * expect(error.requiredScopes).toStrictEqual([\"creator-store-product:write\"]);\n * expect(error.operationKey).toBe(\"developer-products.create\");\n * ```\n */\nexport class PermissionError extends ApiError {\n\tpublic override readonly name: string = \"PermissionError\";\n\tpublic readonly operationKey: string;\n\tpublic readonly requiredScopes: ReadonlyArray<string>;\n\n\t/**\n\t * Creates a new PermissionError.\n\t *\n\t * @param message - Human-readable error description.\n\t * @param options - Error options including status code, the operation key,\n\t * and the scopes the caller's credential must carry.\n\t */\n\tconstructor(message: string, options: PermissionErrorOptions) {\n\t\tsuper(message, options);\n\t\tthis.operationKey = options.operationKey;\n\t\tthis.requiredScopes = options.requiredScopes;\n\t}\n}\n","/**\n * Maximum cause-chain depth walked by {@link findErrorCode}. Caps pathological\n * self-referential or deeply nested chains; transport failures surface as\n * `NetworkError → TypeError(\"fetch failed\") → OS Error{code}`, so three\n * levels is the expected shape and five leaves headroom.\n */\nconst MAX_DEPTH = 5;\n\n/**\n * Walks an error's `cause` chain and returns the first node-style string\n * `code` it finds (for example `\"ECONNRESET\"`, `\"ETIMEDOUT\"`). Native `fetch`\n * surfaces a transport reset as a `NetworkError` wrapping a\n * `TypeError(\"fetch failed\")` whose own cause carries the OS-level `code`, so\n * the code lives several links down the chain.\n *\n * @example\n *\n * ```ts\n * import { findErrorCode } from \"./find-error-code\";\n *\n * const root = Object.assign(new Error(\"read ECONNRESET\"), { code: \"ECONNRESET\" });\n * const outer = new Error(\"Network request failed\", {\n * cause: new TypeError(\"fetch failed\", { cause: root }),\n * });\n *\n * expect(findErrorCode(outer)).toBe(\"ECONNRESET\");\n * ```\n *\n * @param error - The error to inspect; typically a `NetworkError`.\n * @returns The first string `code` in the chain, or `undefined` if none.\n */\nexport function findErrorCode(error: unknown): string | undefined {\n\tlet current: unknown = error;\n\tfor (let depth = 0; depth < MAX_DEPTH && current instanceof Error; depth += 1) {\n\t\tconst code = readCode(current);\n\t\tif (code !== undefined) {\n\t\t\treturn code;\n\t\t}\n\n\t\tcurrent = current.cause;\n\t}\n\n\treturn undefined;\n}\n\nfunction readCode(error: Error): string | undefined {\n\tconst code = Reflect.get(error, \"code\");\n\treturn typeof code === \"string\" ? code : undefined;\n}\n\n/**\n * `DOMException.name` produced when an `AbortSignal.timeout` fires. This is the\n * web-standard discriminator (stable across Node and Bun, unlike the\n * runtime-specific message) and distinguishes the SDK's own request timeout\n * from a caller-supplied cancellation, which surfaces as `\"AbortError\"`.\n */\nconst TIMEOUT_ABORT_NAME = \"TimeoutError\";\n\n/**\n * Reports whether an error chain was produced by the SDK's own\n * `AbortSignal.timeout` self-abort. Such a `DOMException` carries a numeric\n * `code` (23), so {@link findErrorCode} (which only reads string codes)\n * cannot classify it; this walk keys on `name` instead. A caller-supplied\n * abort (`\"AbortError\"`) is deliberately not matched.\n *\n * @example\n *\n * ```ts\n * import { isTimeoutAbort } from \"./find-error-code\";\n *\n * const error = new Error(\"Network request failed\", {\n * cause: new DOMException(\"timed out\", \"TimeoutError\"),\n * });\n *\n * expect(isTimeoutAbort(error)).toBe(true);\n * expect(isTimeoutAbort(new DOMException(\"cancelled\", \"AbortError\"))).toBe(false);\n * ```\n *\n * @param error - The error to inspect; typically a `NetworkError`.\n * @returns `true` when a `TimeoutError` abort sits within the cause chain.\n */\nexport function isTimeoutAbort(error: unknown): boolean {\n\tlet current: unknown = error;\n\tfor (let depth = 0; depth < MAX_DEPTH && current instanceof Error; depth += 1) {\n\t\tif (Reflect.get(current, \"name\") === TIMEOUT_ABORT_NAME) {\n\t\t\treturn true;\n\t\t}\n\n\t\tcurrent = current.cause;\n\t}\n\n\treturn false;\n}\n","import { ApiError } from \"../../errors/api-error.ts\";\nimport { NetworkError } from \"../../errors/network-error.ts\";\nimport { RateLimitError } from \"../../errors/rate-limit.ts\";\nimport { findErrorCode, isTimeoutAbort } from \"../utils/find-error-code.ts\";\n\n/**\n * Fully-resolved retry config shape that {@link mergeConfig} and\n * {@link shouldRetry} operate on. Fields are required because this represents\n * the post-defaulting, internal view; callers should supply every field (or\n * resolve them via a test factory / client constructor). The partial,\n * user-facing type lives on client construction options; method defaults and\n * per-request overrides use `Partial<RetryResolvable>`.\n */\nexport interface RetryResolvable {\n\t/** Roblox Open Cloud API key. */\n\treadonly apiKey: string;\n\t/** Base URL for the Open Cloud API. */\n\treadonly baseUrl: string;\n\t/** Maximum retry attempts before giving up. */\n\treadonly maxRetries: number;\n\t/** Status codes that are eligible for retry. */\n\treadonly retryableStatuses: ReadonlyArray<number>;\n\t/**\n\t * Node-style transport error codes ({@link findErrorCode}) eligible for\n\t * retry when surfaced as a {@link NetworkError}. Empty for create\n\t * operations by default; consumers opt a create in via a per-request\n\t * override.\n\t */\n\treadonly retryableTransportCodes: ReadonlyArray<string>;\n\t/** Fallback delay function when no server hint is available. */\n\treadonly retryDelay: (attempt: number) => number;\n\t/** Per-request timeout in milliseconds. */\n\treadonly timeout: number;\n}\n\n/**\n * Transient transport error codes that are safe to retry for idempotent\n * operations. Connection resets, timeouts, and DNS hiccups are recoverable on\n * a retry. A self-aborted request timeout carries no OS-level `code`, so\n * {@link shouldRetry} folds it into this set as `ETIMEDOUT` (via\n * {@link isTimeoutAbort}) for idempotent methods; create methods retry no\n * transport codes and so still never re-issue a timed-out write.\n *\n * @since 0.1.0\n */\nexport const TRANSIENT_TRANSPORT_CODES: ReadonlyArray<string> = Object.freeze([\n\t\"ECONNRESET\",\n\t\"ECONNREFUSED\",\n\t\"ETIMEDOUT\",\n\t\"EPIPE\",\n\t\"ENETUNREACH\",\n\t\"EHOSTDOWN\",\n\t\"EAI_AGAIN\",\n\t\"UND_ERR_SOCKET\",\n]);\n\n/** Method-level retry defaults, keyed by {@link MethodKind}. */\ntype MethodDefaults = Readonly<\n\tPick<RetryResolvable, \"retryableStatuses\" | \"retryableTransportCodes\">\n>;\n\n/**\n * Default retry policy for idempotent operations (read, list, update,\n * delete). Safe to retry on rate limits, transient server errors, and\n * transient transport failures.\n */\nexport const IDEMPOTENT_METHOD_DEFAULTS: MethodDefaults = Object.freeze({\n\tretryableStatuses: Object.freeze([429, 500, 502, 503, 504] as const),\n\tretryableTransportCodes: TRANSIENT_TRANSPORT_CODES,\n});\n\n/**\n * Default retry policy for create operations. Retries rate limits only (no\n * 5xx and no transport-error retries) to prevent duplicate resources, since\n * Roblox Open Cloud has no idempotency-key support. Consumers who can tolerate\n * a duplicate opt in per request.\n */\nexport const CREATE_METHOD_DEFAULTS: MethodDefaults = Object.freeze({\n\tretryableStatuses: Object.freeze([429] as const),\n\tretryableTransportCodes: Object.freeze([] as const),\n});\n\n/** Kind of HTTP method the merge is being performed for. */\nexport type MethodKind = \"create\" | \"idempotent\";\n\n/**\n * Options for {@link mergeConfig}.\n *\n * @template T - Concrete `RetryResolvable` subtype being merged.\n */\ninterface MergeConfigOptions<T> {\n\t/** Method-level defaults (e.g. {@link CREATE_METHOD_DEFAULTS}). */\n\treadonly methodDefaults: Partial<T>;\n\t/** Whether the method is a create or idempotent operation. */\n\treadonly methodKind: MethodKind;\n\t/** Optional per-request overrides; always win when provided. */\n\treadonly requestOptions?: Partial<T>;\n}\n\n/**\n * Options for {@link computeRetryWaitMs}.\n */\ninterface ComputeRetryWaitMsOptions {\n\t/** Zero-indexed retry attempt number. */\n\treadonly attempt: number;\n\t/** Fallback delay function when no server hint is available. */\n\treadonly retryDelay: (attempt: number) => number;\n}\n\n/**\n * Default exponential backoff: 1s → 2s → 4s → 8s → 16s → 30s (capped).\n *\n * @example\n *\n * ```ts\n * import { defaultRetryDelay } from \"./retry\";\n *\n * expect(defaultRetryDelay(0)).toBe(1000);\n * expect(defaultRetryDelay(4)).toBe(16_000);\n * expect(defaultRetryDelay(10)).toBe(30_000);\n * ```\n *\n * @param attempt - Zero-indexed retry attempt number.\n * @returns Wait duration in milliseconds.\n */\nexport function defaultRetryDelay(attempt: number): number {\n\treturn Math.min(1000 * 2 ** attempt, 30_000);\n}\n\n/**\n * Computes how long to wait before the next retry. Prefers the server's\n * suggested delay when the error is a {@link RateLimitError} with a positive\n * `retryAfterSeconds`; otherwise falls through to `retryDelay(attempt)`.\n *\n * @example\n *\n * ```ts\n * import { RateLimitError } from \"../../errors/rate-limit.ts\";\n * import { computeRetryWaitMs, defaultRetryDelay } from \"./retry\";\n *\n * const error = new RateLimitError(\"slow down\", { retryAfterSeconds: 3 });\n *\n * expect(computeRetryWaitMs(error, { attempt: 0, retryDelay: defaultRetryDelay })).toBe(\n * 3000,\n * );\n * ```\n *\n * @example\n *\n * ```ts\n * import { ApiError } from \"../../errors/api-error.ts\";\n * import { computeRetryWaitMs, defaultRetryDelay } from \"./retry\";\n *\n * const error = new ApiError(\"server error\", { statusCode: 503 });\n *\n * expect(computeRetryWaitMs(error, { attempt: 2, retryDelay: defaultRetryDelay })).toBe(\n * 4000,\n * );\n * ```\n *\n * @param error - The error returned by the failing request.\n * @param options - Retry attempt index and fallback delay function.\n * @returns Wait duration in milliseconds before the next attempt.\n */\nexport function computeRetryWaitMs(\n\terror: ApiError | NetworkError | RateLimitError,\n\toptions: ComputeRetryWaitMsOptions,\n): number {\n\tif (error instanceof RateLimitError && error.retryAfterSeconds > 0) {\n\t\treturn error.retryAfterSeconds * 1000;\n\t}\n\n\treturn options.retryDelay(options.attempt);\n}\n\n/**\n * Decides whether a failed request is eligible for retry. {@link RateLimitError}\n * (checked against 429) and {@link ApiError} (checked against its `statusCode`)\n * are retryable when their status is in `retryableStatuses`. A\n * {@link NetworkError} is retryable when its transport code\n * ({@link findErrorCode}) is in `retryableTransportCodes`. This is how\n * transient connection resets recover. A self-aborted request timeout\n * ({@link isTimeoutAbort}) carries no transport code, so it is classified as\n * `ETIMEDOUT`: recovered for idempotent methods, never for creates (whose\n * list is empty). All other failures return `false`.\n *\n * @example\n *\n * ```ts\n * import { RateLimitError } from \"../../errors/rate-limit.ts\";\n * import { shouldRetry } from \"./retry\";\n *\n * const error = new RateLimitError(\"\", { retryAfterSeconds: 1 });\n *\n * expect(shouldRetry(error, { retryableStatuses: [429], retryableTransportCodes: [] })).toBe(\n * true,\n * );\n * ```\n *\n * @example\n *\n * ```ts\n * import { NetworkError } from \"../../errors/network-error.ts\";\n * import { shouldRetry } from \"./retry\";\n *\n * const reset = Object.assign(new Error(\"read ECONNRESET\"), { code: \"ECONNRESET\" });\n * const error = new NetworkError(\"Network request failed\", { cause: reset });\n *\n * expect(\n * shouldRetry(error, { retryableStatuses: [], retryableTransportCodes: [\"ECONNRESET\"] }),\n * ).toBe(true);\n * ```\n *\n * @param error - The error returned by the failing request.\n * @param config - Object carrying the retry-eligible status and transport-code lists.\n * @returns `true` if the error should be retried, `false` otherwise.\n */\nexport function shouldRetry(\n\terror: unknown,\n\tconfig: {\n\t\treadonly retryableStatuses: ReadonlyArray<number>;\n\t\treadonly retryableTransportCodes: ReadonlyArray<string>;\n\t},\n): error is ApiError | NetworkError | RateLimitError {\n\tif (error instanceof RateLimitError) {\n\t\treturn config.retryableStatuses.includes(429);\n\t}\n\n\tif (error instanceof ApiError) {\n\t\treturn config.retryableStatuses.includes(error.statusCode);\n\t}\n\n\tif (error instanceof NetworkError) {\n\t\tconst code = findErrorCode(error) ?? (isTimeoutAbort(error) ? \"ETIMEDOUT\" : undefined);\n\t\treturn code !== undefined && config.retryableTransportCodes.includes(code);\n\t}\n\n\treturn false;\n}\n\n/**\n * Resolves the effective config for a single request by shallow-merging the\n * client config, method defaults, and per-request options. Precedence depends\n * on `methodKind`:\n *\n * - `\"create\"`: method defaults override client config, so client-level\n * settings cannot silently relax create-method safety. Only explicit\n * per-request `requestOptions` can.\n * - `\"idempotent\"`: client config overrides method defaults, so consumers\n * can loosen or tighten retry policy globally. `requestOptions` still wins\n * when provided.\n *\n * Array-valued fields like `retryableStatuses` are *replaced*, not extended.\n *\n * @template T - Concrete `RetryResolvable` subtype being merged.\n *\n * @example\n *\n * ```ts\n * import {\n * CREATE_METHOD_DEFAULTS,\n * defaultRetryDelay,\n * mergeConfig,\n * type RetryResolvable,\n * } from \"./retry\";\n *\n * const clientConfig: RetryResolvable = {\n * apiKey: \"k\",\n * baseUrl: \"https://apis.roblox.com\",\n * maxRetries: 3,\n * retryableStatuses: [429, 500],\n * retryableTransportCodes: [],\n * retryDelay: defaultRetryDelay,\n * timeout: 30_000,\n * };\n *\n * const merged = mergeConfig(clientConfig, {\n * methodDefaults: CREATE_METHOD_DEFAULTS,\n * methodKind: \"create\",\n * });\n *\n * expect(merged.retryableStatuses).toStrictEqual([429]);\n * ```\n *\n * @example\n *\n * ```ts\n * import {\n * defaultRetryDelay,\n * IDEMPOTENT_METHOD_DEFAULTS,\n * mergeConfig,\n * type RetryResolvable,\n * } from \"./retry\";\n *\n * const clientConfig: RetryResolvable = {\n * apiKey: \"k\",\n * baseUrl: \"https://apis.roblox.com\",\n * maxRetries: 3,\n * retryableStatuses: [429],\n * retryableTransportCodes: [],\n * retryDelay: defaultRetryDelay,\n * timeout: 30_000,\n * };\n *\n * const merged = mergeConfig(clientConfig, {\n * methodDefaults: IDEMPOTENT_METHOD_DEFAULTS,\n * methodKind: \"idempotent\",\n * requestOptions: { timeout: 10_000 },\n * });\n *\n * expect(merged.retryableStatuses).toStrictEqual([429]);\n * expect(merged.timeout).toBe(10_000);\n * ```\n *\n * @param clientConfig - Config frozen at client construction.\n * @param options - Method defaults, method kind, and optional per-request overrides.\n * @returns A new merged config object. Inputs are not mutated.\n */\nexport function mergeConfig<T extends RetryResolvable>(\n\tclientConfig: T,\n\toptions: MergeConfigOptions<T>,\n): T {\n\tconst { methodDefaults, methodKind, requestOptions } = options;\n\n\tswitch (methodKind) {\n\t\tcase \"create\": {\n\t\t\treturn { ...clientConfig, ...methodDefaults, ...requestOptions };\n\t\t}\n\t\tcase \"idempotent\": {\n\t\t\treturn { ...methodDefaults, ...clientConfig, ...requestOptions };\n\t\t}\n\t\tdefault: {\n\t\t\tconst exhaustive: never = methodKind;\n\t\t\tthrow new Error(`Unexpected methodKind: ${String(exhaustive)}`);\n\t\t}\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA+CA,IAAa,kBAAb,cAAqC,SAAS;CAC7C,OAAwC;CACxC;CACA;;;;;;;;CASA,YAAY,SAAiB,SAAiC;EAC7D,MAAM,SAAS,OAAO;EACtB,KAAK,eAAe,QAAQ;EAC5B,KAAK,iBAAiB,QAAQ;CAC/B;AACD;;;;;;;;;AC1DA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;AAyBlB,SAAgB,cAAc,OAAoC;CACjE,IAAI,UAAmB;CACvB,KAAK,IAAI,QAAQ,GAAG,QAAQ,aAAa,mBAAmB,OAAO,SAAS,GAAG;EAC9E,MAAM,OAAO,SAAS,OAAO;EAC7B,IAAI,SAAS,KAAA,GACZ,OAAO;EAGR,UAAU,QAAQ;CACnB;AAGD;AAEA,SAAS,SAAS,OAAkC;CACnD,MAAM,OAAO,QAAQ,IAAI,OAAO,MAAM;CACtC,OAAO,OAAO,SAAS,WAAW,OAAO,KAAA;AAC1C;;;;;;;AAQA,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;AAyB3B,SAAgB,eAAe,OAAyB;CACvD,IAAI,UAAmB;CACvB,KAAK,IAAI,QAAQ,GAAG,QAAQ,aAAa,mBAAmB,OAAO,SAAS,GAAG;EAC9E,IAAI,QAAQ,IAAI,SAAS,MAAM,MAAM,oBACpC,OAAO;EAGR,UAAU,QAAQ;CACnB;CAEA,OAAO;AACR;;;;;;;;;;;;;AC/CA,MAAa,4BAAmD,OAAO,OAAO;CAC7E;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACD,CAAC;;;;;;AAYD,MAAa,6BAA6C,OAAO,OAAO;CACvE,mBAAmB,OAAO,OAAO;EAAC;EAAK;EAAK;EAAK;EAAK;CAAG,CAAU;CACnE,yBAAyB;AAC1B,CAAC;;;;;;;AAQD,MAAa,yBAAyC,OAAO,OAAO;CACnE,mBAAmB,OAAO,OAAO,CAAC,GAAG,CAAU;CAC/C,yBAAyB,OAAO,OAAO,CAAC,CAAU;AACnD,CAAC;;;;;;;;;;;;;;;;;AA6CD,SAAgB,kBAAkB,SAAyB;CAC1D,OAAO,KAAK,IAAI,MAAO,KAAK,SAAS,GAAM;AAC5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCA,SAAgB,mBACf,OACA,SACS;CACT,IAAI,iBAAiB,kBAAkB,MAAM,oBAAoB,GAChE,OAAO,MAAM,oBAAoB;CAGlC,OAAO,QAAQ,WAAW,QAAQ,OAAO;AAC1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA,SAAgB,YACf,OACA,QAIoD;CACpD,IAAI,iBAAiB,gBACpB,OAAO,OAAO,kBAAkB,SAAS,GAAG;CAG7C,IAAI,iBAAiB,UACpB,OAAO,OAAO,kBAAkB,SAAS,MAAM,UAAU;CAG1D,IAAI,iBAAiB,cAAc;EAClC,MAAM,OAAO,cAAc,KAAK,MAAM,eAAe,KAAK,IAAI,cAAc,KAAA;EAC5E,OAAO,SAAS,KAAA,KAAa,OAAO,wBAAwB,SAAS,IAAI;CAC1E;CAEA,OAAO;AACR;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgFA,SAAgB,YACf,cACA,SACI;CACJ,MAAM,EAAE,gBAAgB,YAAY,mBAAmB;CAEvD,QAAQ,YAAR;EACC,KAAK,UACJ,OAAO;GAAE,GAAG;GAAc,GAAG;GAAgB,GAAG;EAAe;EAEhE,KAAK,cACJ,OAAO;GAAE,GAAG;GAAgB,GAAG;GAAc,GAAG;EAAe;EAEhE,SAEC,MAAM,IAAI,MAAM,0BAA0B,OAAOA,UAAU,GAAG;CAEhE;AACD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signatures-
|
|
1
|
+
{"version":3,"file":"signatures-JF-7Psce.mjs","names":[],"sources":["../src/domains/universes/places/signatures.ts"],"sourcesContent":["/**\n * Magic-byte prefix every Roblox binary place file (`.rbxl`) starts with.\n * The first 8 bytes spell out `<roblox!` in ASCII; the remaining 6 bytes\n * (`\\x89\\xff\\r\\n\\x1a\\n`) are the binary-format marker that distinguishes a\n * binary place file from the XML form (`.rbxlx`), whose ASCII-only header\n * begins with `<roblox `.\n */\nexport const RBXL_SIGNATURE: Readonly<Uint8Array<ArrayBuffer>> = new Uint8Array([\n\t0x3c, 0x72, 0x6f, 0x62, 0x6c, 0x6f, 0x78, 0x21, 0x89, 0xff, 0x0d, 0x0a, 0x1a, 0x0a,\n]);\n\n/**\n * Magic-byte prefix every Roblox XML place file (`.rbxlx`) starts with.\n * Equivalent to the ASCII string `<roblox ` (note the trailing space): a\n * well-formed rbxlx file opens with `<roblox` followed by attributes, while\n * an rbxl file uses `<roblox!` (exclamation mark) as the eighth byte. The\n * trailing space is what proves the file is the XML variant rather than\n * the binary one.\n */\nexport const RBXLX_SIGNATURE: Readonly<Uint8Array<ArrayBuffer>> = new Uint8Array([\n\t0x3c, 0x72, 0x6f, 0x62, 0x6c, 0x6f, 0x78, 0x20,\n]);\n\n/**\n * Reports whether `body` begins with `signature`. A pure byte-prefix check\n * with no allocation; used by the place builder to disambiguate `.rbxl` and\n * `.rbxlx` payloads against their declared `format`.\n *\n * @param body - The caller-supplied place file bytes.\n * @param signature - One of the frozen signature constants from this module.\n * @returns `true` if every byte of `signature` matches `body[0..signature.length]`.\n */\nexport function matchesSignature(\n\tbody: Uint8Array,\n\tsignature: Readonly<Uint8Array<ArrayBuffer>>,\n): boolean {\n\tfor (const [index, expected] of signature.entries()) {\n\t\tif (body[index] !== expected) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\treturn true;\n}\n"],"mappings":";;;;;;;;AAOA,MAAa,iBAAoD,IAAI,WAAW;CAC/E;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;AAC/E,CAAC;;;;;;;;;AAUD,MAAa,kBAAqD,IAAI,WAAW;CAChF;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;AAC3C,CAAC;;;;;;;;;;AAWD,SAAgB,iBACf,MACA,WACU;CACV,KAAK,MAAM,CAAC,OAAO,aAAa,UAAU,QAAQ,GACjD,IAAI,KAAK,WAAW,UACnB,OAAO;CAIT,OAAO;AACR"}
|
package/dist/storage.d.mts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { d as OpenCloudError, i as OpenCloudClientOptions, l as Result, n as HttpRequest, r as HttpResponse, s as RequestOptions, u as SleepFunc } from "./types-
|
|
1
|
+
import { d as OpenCloudError, i as OpenCloudClientOptions, l as Result, n as HttpRequest, r as HttpResponse, s as RequestOptions, u as SleepFunc } from "./types-CRiJg5EQ.mjs";
|
|
2
|
+
import { n as RetryResolvable, t as MethodKind } from "./retry-Bh2nNjBV.mjs";
|
|
2
3
|
|
|
3
4
|
//#region src/domains/cloud-v2/memory-store-queues/types.d.ts
|
|
4
5
|
/**
|
|
5
6
|
* Caller-supplied input for the `enqueue` method on `StorageClient.queues`.
|
|
6
7
|
* Mirrors `Cloud_CreateMemoryStoreQueueItem` on the Open Cloud API.
|
|
8
|
+
*
|
|
9
|
+
* @since 0.1.0
|
|
7
10
|
*/
|
|
8
11
|
interface EnqueueQueueItemParameters {
|
|
9
12
|
/**
|
|
@@ -34,6 +37,8 @@ interface EnqueueQueueItemParameters {
|
|
|
34
37
|
* Parsed representation of a memory-store queue item, as returned by the
|
|
35
38
|
* Open Cloud `Cloud_CreateMemoryStoreQueueItem` and (inside the array)
|
|
36
39
|
* `Cloud_ReadMemoryStoreQueueItems` endpoints.
|
|
40
|
+
*
|
|
41
|
+
* @since 0.1.0
|
|
37
42
|
*/
|
|
38
43
|
interface QueueItem {
|
|
39
44
|
/** Server-generated item identifier, parsed from the wire `path`. */
|
|
@@ -55,6 +60,8 @@ interface QueueItem {
|
|
|
55
60
|
/**
|
|
56
61
|
* Caller-supplied input for the `dequeue` method on
|
|
57
62
|
* `StorageClient.queues`. Mirrors `Cloud_ReadMemoryStoreQueueItems`.
|
|
63
|
+
*
|
|
64
|
+
* @since 0.1.0
|
|
58
65
|
*/
|
|
59
66
|
interface DequeueQueueItemsParameters {
|
|
60
67
|
/**
|
|
@@ -84,6 +91,8 @@ interface DequeueQueueItemsParameters {
|
|
|
84
91
|
* response. The `readId` value must be passed to a subsequent
|
|
85
92
|
* `discard` call to acknowledge the items; otherwise the items reappear
|
|
86
93
|
* once the invisibility window elapses.
|
|
94
|
+
*
|
|
95
|
+
* @since 0.1.0
|
|
87
96
|
*/
|
|
88
97
|
interface DequeueResult {
|
|
89
98
|
/** Items dequeued from the front of the queue, in dequeue order. */
|
|
@@ -100,6 +109,8 @@ interface DequeueResult {
|
|
|
100
109
|
* Acknowledging a `readId` removes the dequeued batch from the queue
|
|
101
110
|
* permanently; without `discard`, the items reappear once their
|
|
102
111
|
* invisibility window elapses.
|
|
112
|
+
*
|
|
113
|
+
* @since 0.1.0
|
|
103
114
|
*/
|
|
104
115
|
interface DiscardQueueItemsParameters {
|
|
105
116
|
/** Stringified queue identifier. */
|
|
@@ -116,6 +127,8 @@ interface DiscardQueueItemsParameters {
|
|
|
116
127
|
* server contract requires at most one of `stringSortKey` or
|
|
117
128
|
* `numericSortKey`; the union surfaces that constraint at the type
|
|
118
129
|
* level so callers cannot accidentally set both.
|
|
130
|
+
*
|
|
131
|
+
* @since 0.1.0
|
|
119
132
|
*/
|
|
120
133
|
type SortKey = {
|
|
121
134
|
readonly kind: "numeric";
|
|
@@ -128,6 +141,8 @@ type SortKey = {
|
|
|
128
141
|
* Caller-supplied input for the `create` method on
|
|
129
142
|
* `StorageClient.sortedMaps`. Mirrors
|
|
130
143
|
* `Cloud_CreateMemoryStoreSortedMapItem` on the Open Cloud API.
|
|
144
|
+
*
|
|
145
|
+
* @since 0.1.0
|
|
131
146
|
*/
|
|
132
147
|
interface CreateSortedMapItemParameters {
|
|
133
148
|
/**
|
|
@@ -159,6 +174,8 @@ interface CreateSortedMapItemParameters {
|
|
|
159
174
|
* `Cloud_ListMemoryStoreSortedMapItems` on the Open Cloud API. All
|
|
160
175
|
* paging and filtering parameters are optional; omitting them returns
|
|
161
176
|
* up to one item server-side (`maxPageSize` defaults to `1`).
|
|
177
|
+
*
|
|
178
|
+
* @since 0.1.0
|
|
162
179
|
*/
|
|
163
180
|
interface ListSortedMapItemsParameters {
|
|
164
181
|
/**
|
|
@@ -190,6 +207,8 @@ interface ListSortedMapItemsParameters {
|
|
|
190
207
|
/**
|
|
191
208
|
* Parsed representation of a sorted-map item, as returned by every
|
|
192
209
|
* sorted-map operation that yields a single item.
|
|
210
|
+
*
|
|
211
|
+
* @since 0.1.0
|
|
193
212
|
*/
|
|
194
213
|
interface SortedMapItem {
|
|
195
214
|
/** Item identifier, parsed from the wire `path`. */
|
|
@@ -221,6 +240,8 @@ interface SortedMapItem {
|
|
|
221
240
|
/**
|
|
222
241
|
* Parsed result of a successful `Cloud_ListMemoryStoreSortedMapItems`
|
|
223
242
|
* response.
|
|
243
|
+
*
|
|
244
|
+
* @since 0.1.0
|
|
224
245
|
*/
|
|
225
246
|
interface ListSortedMapItemsResult {
|
|
226
247
|
/** Items returned in the current page, ordered per `orderBy`. */
|
|
@@ -235,6 +256,8 @@ interface ListSortedMapItemsResult {
|
|
|
235
256
|
* Caller-supplied input for the `delete` method on
|
|
236
257
|
* `StorageClient.sortedMaps`. Mirrors
|
|
237
258
|
* `Cloud_DeleteMemoryStoreSortedMapItem` on the Open Cloud API.
|
|
259
|
+
*
|
|
260
|
+
* @since 0.1.0
|
|
238
261
|
*/
|
|
239
262
|
interface DeleteSortedMapItemParameters {
|
|
240
263
|
/** Caller-supplied item identifier. URL-encoded by the builder. */
|
|
@@ -248,6 +271,8 @@ interface DeleteSortedMapItemParameters {
|
|
|
248
271
|
* Caller-supplied input for the `get` method on
|
|
249
272
|
* `StorageClient.sortedMaps`. Mirrors
|
|
250
273
|
* `Cloud_GetMemoryStoreSortedMapItem` on the Open Cloud API.
|
|
274
|
+
*
|
|
275
|
+
* @since 0.1.0
|
|
251
276
|
*/
|
|
252
277
|
interface GetSortedMapItemParameters {
|
|
253
278
|
/** Caller-supplied item identifier. URL-encoded by the builder. */
|
|
@@ -263,6 +288,8 @@ interface GetSortedMapItemParameters {
|
|
|
263
288
|
* `Cloud_UpdateMemoryStoreSortedMapItem` on the Open Cloud API. Body
|
|
264
289
|
* fields (`value`, `ttl`, `sortKey`) are optional under PATCH
|
|
265
290
|
* semantics; omitted fields are left unchanged on the server.
|
|
291
|
+
*
|
|
292
|
+
* @since 0.1.0
|
|
266
293
|
*/
|
|
267
294
|
interface UpdateSortedMapItemParameters {
|
|
268
295
|
/**
|
|
@@ -308,32 +335,6 @@ interface OperationLimit {
|
|
|
308
335
|
readonly operationKey: string;
|
|
309
336
|
}
|
|
310
337
|
//#endregion
|
|
311
|
-
//#region src/internal/http/retry.d.ts
|
|
312
|
-
/**
|
|
313
|
-
* Fully-resolved retry config shape that {@link mergeConfig} and
|
|
314
|
-
* {@link shouldRetry} operate on. Fields are required because this represents
|
|
315
|
-
* the post-defaulting, internal view — callers should supply every field (or
|
|
316
|
-
* resolve them via a test factory / client constructor). The partial,
|
|
317
|
-
* user-facing type lives on client construction options; method defaults and
|
|
318
|
-
* per-request overrides use `Partial<RetryResolvable>`.
|
|
319
|
-
*/
|
|
320
|
-
interface RetryResolvable {
|
|
321
|
-
/** Roblox Open Cloud API key. */
|
|
322
|
-
readonly apiKey: string;
|
|
323
|
-
/** Base URL for the Open Cloud API. */
|
|
324
|
-
readonly baseUrl: string;
|
|
325
|
-
/** Maximum retry attempts before giving up. */
|
|
326
|
-
readonly maxRetries: number;
|
|
327
|
-
/** Status codes that are eligible for retry. */
|
|
328
|
-
readonly retryableStatuses: ReadonlyArray<number>;
|
|
329
|
-
/** Fallback delay function when no server hint is available. */
|
|
330
|
-
readonly retryDelay: (attempt: number) => number;
|
|
331
|
-
/** Per-request timeout in milliseconds. */
|
|
332
|
-
readonly timeout: number;
|
|
333
|
-
}
|
|
334
|
-
/** Kind of HTTP method the merge is being performed for. */
|
|
335
|
-
type MethodKind = "create" | "idempotent";
|
|
336
|
-
//#endregion
|
|
337
338
|
//#region src/internal/resource-client.d.ts
|
|
338
339
|
/**
|
|
339
340
|
* Describes a single resource method's shape for dispatch through
|
|
@@ -635,6 +636,8 @@ declare class MemoryStoreSortedMapsGroup {
|
|
|
635
636
|
* Every method returns a `Result` so callers handle failure
|
|
636
637
|
* explicitly; no thrown error ever escapes the client.
|
|
637
638
|
*
|
|
639
|
+
* @since 0.1.0
|
|
640
|
+
*
|
|
638
641
|
* @example
|
|
639
642
|
*
|
|
640
643
|
* ```ts
|
package/dist/storage.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.mts","names":[],"sources":["../src/domains/cloud-v2/memory-store-queues/types.ts","../src/domains/cloud-v2/memory-store-sorted-maps/types.ts","../src/internal/http/rate-limit-queue.ts","../src/internal/
|
|
1
|
+
{"version":3,"file":"storage.d.mts","names":[],"sources":["../src/domains/cloud-v2/memory-store-queues/types.ts","../src/domains/cloud-v2/memory-store-sorted-maps/types.ts","../src/internal/http/rate-limit-queue.ts","../src/internal/resource-client.ts","../src/resources/storage/queues-group.ts","../src/resources/storage/sorted-maps-group.ts","../src/resources/storage/client.ts"],"mappings":"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"}
|