@bedrock-rbx/ocale 0.1.0 → 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/dist/badges.mjs +3 -3
- package/dist/developer-products.mjs +4 -4
- package/dist/game-passes.mjs +4 -4
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +4 -4
- package/dist/luau-execution.mjs +4 -4
- package/dist/places.mjs +5 -5
- package/dist/{poll-timeout-C0nmJzOd.mjs → poll-timeout-BfxUWWCZ.mjs} +2 -2
- package/dist/{poll-timeout-C0nmJzOd.mjs.map → poll-timeout-BfxUWWCZ.mjs.map} +1 -1
- package/dist/{polling-helpers-BcycDhDm.mjs → polling-helpers-DCw9LyJM.mjs} +6 -6
- package/dist/{polling-helpers-BcycDhDm.mjs.map → polling-helpers-DCw9LyJM.mjs.map} +1 -1
- package/dist/{price-information-DK83Wul1.mjs → price-information-C4gC2CMZ.mjs} +2 -2
- package/dist/{price-information-DK83Wul1.mjs.map → price-information-C4gC2CMZ.mjs.map} +1 -1
- package/dist/{rate-limit-Co9i28qi.mjs → rate-limit-Dh2leqaB.mjs} +20 -2
- package/dist/rate-limit-Dh2leqaB.mjs.map +1 -0
- package/dist/{resource-client-C_D--PYX.mjs → resource-client-CIAkS2xQ.mjs} +182 -39
- package/dist/resource-client-CIAkS2xQ.mjs.map +1 -0
- package/dist/{retry-r1TXe5Zd.d.mts → retry-Bh2nNjBV.d.mts} +47 -2
- package/dist/retry-Bh2nNjBV.d.mts.map +1 -0
- package/dist/{retry-CbHBw60o.mjs → retry-BvZRZDXs.mjs} +2 -2
- package/dist/{retry-CbHBw60o.mjs.map → retry-BvZRZDXs.mjs.map} +1 -1
- package/dist/storage.d.mts +1 -1
- package/dist/storage.mjs +3 -3
- package/dist/testing.mjs +1 -1
- package/dist/universes.mjs +4 -4
- package/dist/{validation-9oU6qNNQ.mjs → validation-xJZRa8tX.mjs} +2 -2
- package/dist/{validation-9oU6qNNQ.mjs.map → validation-xJZRa8tX.mjs.map} +1 -1
- package/package.json +2 -2
- package/dist/rate-limit-Co9i28qi.mjs.map +0 -1
- package/dist/resource-client-C_D--PYX.mjs.map +0 -1
- package/dist/retry-r1TXe5Zd.d.mts.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retry-CbHBw60o.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
|
+
{"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"}
|
package/dist/storage.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
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-
|
|
2
|
+
import { n as RetryResolvable, t as MethodKind } from "./retry-Bh2nNjBV.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/domains/cloud-v2/memory-store-queues/types.d.ts
|
|
5
5
|
/**
|
package/dist/storage.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { r as ApiError } from "./rate-limit-
|
|
2
|
-
import { n as IDEMPOTENT_METHOD_DEFAULTS, t as CREATE_METHOD_DEFAULTS } from "./retry-
|
|
3
|
-
import { a as isDateTimeString, i as isRecord, n as okRequest, r as parseEmptyResponse, t as ResourceClient } from "./resource-client-
|
|
1
|
+
import { r as ApiError } from "./rate-limit-Dh2leqaB.mjs";
|
|
2
|
+
import { n as IDEMPOTENT_METHOD_DEFAULTS, t as CREATE_METHOD_DEFAULTS } from "./retry-BvZRZDXs.mjs";
|
|
3
|
+
import { a as isDateTimeString, i as isRecord, n as okRequest, r as parseEmptyResponse, t as ResourceClient } from "./resource-client-CIAkS2xQ.mjs";
|
|
4
4
|
//#region src/domains/cloud-v2/memory-store-queues/builders.ts
|
|
5
5
|
/**
|
|
6
6
|
* Builds a `POST` request for the Open Cloud
|
package/dist/testing.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as NetworkError, r as ApiError, t as RateLimitError } from "./rate-limit-
|
|
1
|
+
import { n as NetworkError, r as ApiError, t as RateLimitError } from "./rate-limit-Dh2leqaB.mjs";
|
|
2
2
|
import { n as RBXL_SIGNATURE, t as RBXLX_SIGNATURE } from "./signatures-JF-7Psce.mjs";
|
|
3
3
|
//#region tests/helpers/badges.ts
|
|
4
4
|
/**
|
package/dist/universes.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { r as ApiError } from "./rate-limit-
|
|
2
|
-
import { n as IDEMPOTENT_METHOD_DEFAULTS, t as CREATE_METHOD_DEFAULTS } from "./retry-
|
|
3
|
-
import { t as ValidationError } from "./validation-
|
|
1
|
+
import { r as ApiError } from "./rate-limit-Dh2leqaB.mjs";
|
|
2
|
+
import { n as IDEMPOTENT_METHOD_DEFAULTS, t as CREATE_METHOD_DEFAULTS } from "./retry-BvZRZDXs.mjs";
|
|
3
|
+
import { t as ValidationError } from "./validation-xJZRa8tX.mjs";
|
|
4
4
|
import { t as toBlob } from "./to-blob-RPqMBuft.mjs";
|
|
5
|
-
import { a as isDateTimeString, i as isRecord, n as okRequest, r as parseEmptyResponse, t as ResourceClient } from "./resource-client-
|
|
5
|
+
import { a as isDateTimeString, i as isRecord, n as okRequest, r as parseEmptyResponse, t as ResourceClient } from "./resource-client-CIAkS2xQ.mjs";
|
|
6
6
|
//#region src/domains/cloud-v2/universes/builders.ts
|
|
7
7
|
/**
|
|
8
8
|
* Dodges `unicorn/no-null` while still emitting a literal `null` onto
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as OpenCloudError } from "./rate-limit-
|
|
1
|
+
import { i as OpenCloudError } from "./rate-limit-Dh2leqaB.mjs";
|
|
2
2
|
//#region src/errors/validation.ts
|
|
3
3
|
/**
|
|
4
4
|
* Thrown locally when caller-supplied input is rejected before any HTTP
|
|
@@ -37,4 +37,4 @@ var ValidationError = class extends OpenCloudError {
|
|
|
37
37
|
//#endregion
|
|
38
38
|
export { ValidationError as t };
|
|
39
39
|
|
|
40
|
-
//# sourceMappingURL=validation-
|
|
40
|
+
//# sourceMappingURL=validation-xJZRa8tX.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation-
|
|
1
|
+
{"version":3,"file":"validation-xJZRa8tX.mjs","names":[],"sources":["../src/errors/validation.ts"],"sourcesContent":["import { OpenCloudError } from \"./base.ts\";\n\n/**\n * Closed discriminator for a {@link ValidationError}. Consumers can\n * exhaustively `switch` over this union so TypeScript will refuse to compile\n * if a new variant is added without a handler.\n *\n * @since 0.1.0\n */\nexport type ValidationErrorCode =\n\t| \"empty_body\"\n\t| \"empty_image_ids\"\n\t| \"empty_update\"\n\t| \"format_mismatch\"\n\t| \"incomplete_ref\"\n\t| \"invalid_image_id\";\n\n/**\n * Options for constructing a {@link ValidationError}.\n *\n * @since 0.1.0\n */\nexport interface ValidationErrorOptions extends ErrorOptions {\n\t/** Machine-readable discriminator identifying the validation failure. */\n\tcode: ValidationErrorCode;\n}\n\n/**\n * Thrown locally when caller-supplied input is rejected before any HTTP\n * round-trip. The `code` discriminator lets consumers branch on local-input\n * errors separately from server-side errors.\n *\n * @since 0.1.0\n *\n * @example\n *\n * ```ts\n * import { ValidationError } from \"@bedrock-rbx/ocale\";\n *\n * const error = new ValidationError(\"Place body is empty\", {\n * code: \"empty_body\",\n * });\n *\n * expect(error).toBeInstanceOf(ValidationError);\n * expect(error.code).toBe(\"empty_body\");\n * ```\n */\nexport class ValidationError extends OpenCloudError {\n\tpublic readonly code: ValidationErrorCode;\n\tpublic override readonly name: string = \"ValidationError\";\n\n\t/**\n\t * Creates a new ValidationError.\n\t *\n\t * @param message - Human-readable error description.\n\t * @param options - Error options including the validation failure code.\n\t */\n\tconstructor(message: string, options: ValidationErrorOptions) {\n\t\tsuper(message, options);\n\t\tthis.code = options.code;\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA+CA,IAAa,kBAAb,cAAqC,eAAe;CACnD;CACA,OAAwC;;;;;;;CAQxC,YAAY,SAAiB,SAAiC;EAC7D,MAAM,SAAS,OAAO;EACtB,KAAK,OAAO,QAAQ;CACrB;AACD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bedrock-rbx/ocale",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Roblox Open Cloud API client",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"api",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"type-fest": "5.6.0",
|
|
55
55
|
"typescript": "npm:@typescript/typescript6@6.0.1",
|
|
56
56
|
"vitest": "4.1.9",
|
|
57
|
-
"@bedrock-rbx/typescript-config": "0.0.0",
|
|
58
57
|
"@bedrock-rbx/testing": "0.0.0",
|
|
58
|
+
"@bedrock-rbx/typescript-config": "0.0.0",
|
|
59
59
|
"@bedrock-rbx/vite-config": "0.0.0"
|
|
60
60
|
},
|
|
61
61
|
"engines": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit-Co9i28qi.mjs","names":[],"sources":["../src/errors/base.ts","../src/errors/api-error.ts","../src/errors/network-error.ts","../src/errors/rate-limit.ts"],"sourcesContent":["/**\n * Base error class for all Open Cloud SDK errors.\n *\n * All specific error types (RateLimitError, ApiError, NetworkError)\n * extend this class, enabling `instanceof OpenCloudError` checks.\n *\n * @since 0.1.0\n */\nexport class OpenCloudError extends Error {\n\tpublic override readonly name: string = \"OpenCloudError\";\n}\n","import { OpenCloudError } from \"./base.ts\";\n\n/**\n * Options for constructing an {@link ApiError}.\n *\n * @since 0.1.0\n */\nexport interface ApiErrorOptions extends ErrorOptions {\n\t/** Optional machine-readable error code from the API. */\n\tcode?: string | undefined;\n\t/** Parsed response body, when present. */\n\tdetails?: JSONValue | undefined;\n\t/** HTTP status code from the API response. */\n\tstatusCode: number;\n}\n\n/**\n * Thrown when the Roblox Open Cloud API returns a non-2xx response\n * that is not a rate limit (429).\n *\n * @since 0.1.0\n *\n * @example\n *\n * ```ts\n * import { ApiError } from \"@bedrock-rbx/ocale\";\n *\n * const error = new ApiError(\"HTTP 404: Pass not found (code NotFound)\", {\n * code: \"NotFound\",\n * details: { errorCode: \"NotFound\", message: \"Pass not found\" },\n * statusCode: 404,\n * });\n *\n * expect(error).toBeInstanceOf(ApiError);\n * expect(error.statusCode).toBe(404);\n * expect(error.code).toBe(\"NotFound\");\n * expect(error.details).toEqual({\n * errorCode: \"NotFound\",\n * message: \"Pass not found\",\n * });\n * ```\n */\nexport class ApiError extends OpenCloudError {\n\tpublic readonly code: string | undefined;\n\tpublic readonly details: JSONValue | undefined;\n\tpublic override readonly name: string = \"ApiError\";\n\tpublic readonly statusCode: number;\n\n\t/**\n\t * Creates a new ApiError.\n\t *\n\t * @param message - Human-readable error description.\n\t * @param options - Error options including status code, optional error\n\t * code, and the parsed response body when present.\n\t */\n\tconstructor(message: string, options: ApiErrorOptions) {\n\t\tsuper(message, options);\n\t\tthis.statusCode = options.statusCode;\n\t\tthis.code = options.code;\n\t\tthis.details = options.details;\n\t}\n}\n","import { OpenCloudError } from \"./base.ts\";\n\n/**\n * Options for constructing a {@link NetworkError}.\n *\n * @since 0.1.0\n */\nexport interface NetworkErrorOptions extends ErrorOptions {\n\t/** HTTP method of the request that failed. */\n\tmethod?: string | undefined;\n\t/** Fully-qualified URL of the request that failed. */\n\turl?: string | undefined;\n}\n\n/**\n * Thrown when a network-level failure prevents the request from reaching\n * the Roblox Open Cloud API (e.g., DNS resolution failure, connection reset).\n * The `method` and `url` name the failing call so a transport failure that\n * survives every retry can be diagnosed; the underlying transport error is\n * carried on `cause`.\n *\n * @since 0.1.0\n */\nexport class NetworkError extends OpenCloudError {\n\tpublic readonly method: string | undefined;\n\tpublic override readonly name: string = \"NetworkError\";\n\tpublic readonly url: string | undefined;\n\n\t/**\n\t * Creates a new NetworkError.\n\t *\n\t * @param message - Human-readable error description.\n\t * @param options - Error options including the optional `cause` and the\n\t * `method` / `url` of the request that failed.\n\t */\n\tconstructor(message: string, options?: NetworkErrorOptions) {\n\t\tsuper(message, options);\n\t\tthis.method = options?.method;\n\t\tthis.url = options?.url;\n\t}\n}\n","import { OpenCloudError } from \"./base.ts\";\n\n/**\n * Options for constructing a {@link RateLimitError}.\n *\n * @since 0.1.0\n */\nexport interface RateLimitErrorOptions extends ErrorOptions {\n\t/**\n\t * Requests still allowed in the throttled window, read from\n\t * `x-ratelimit-remaining` (the most-constrained window). `undefined` when\n\t * the header is absent or carries no finite numeric token; parsed\n\t * independently of `x-ratelimit-reset`, so a valid value survives a\n\t * non-numeric reset. Typically `0` on a genuine 429.\n\t */\n\tremaining?: number | undefined;\n\t/** Seconds to wait before retrying the request. */\n\tretryAfterSeconds: number;\n}\n\n/**\n * Thrown when the Roblox Open Cloud API returns a 429 Too Many Requests response.\n * Contains the server-suggested retry delay.\n *\n * @since 0.1.0\n *\n * @example\n *\n * ```ts\n * import { RateLimitError } from \"@bedrock-rbx/ocale\";\n *\n * const error = new RateLimitError(\"Too many requests\", {\n * retryAfterSeconds: 30,\n * });\n *\n * expect(error).toBeInstanceOf(RateLimitError);\n * expect(error.retryAfterSeconds).toBe(30);\n * ```\n */\nexport class RateLimitError extends OpenCloudError {\n\tpublic override readonly name = \"RateLimitError\";\n\t/** Requests left in the throttled window, or `undefined` if not reported. */\n\tpublic readonly remaining: number | undefined;\n\tpublic readonly retryAfterSeconds: number;\n\n\t/**\n\t * Creates a new RateLimitError.\n\t *\n\t * @param message - Human-readable error description.\n\t * @param options - Error options including the retry delay.\n\t */\n\tconstructor(message: string, options: RateLimitErrorOptions) {\n\t\tsuper(message, options);\n\t\tthis.retryAfterSeconds = options.retryAfterSeconds;\n\t\tthis.remaining = options.remaining;\n\t}\n}\n"],"mappings":";;;;;;;;;AAQA,IAAa,iBAAb,cAAoC,MAAM;CACzC,OAAwC;AACzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACgCA,IAAa,WAAb,cAA8B,eAAe;CAC5C;CACA;CACA,OAAwC;CACxC;;;;;;;;CASA,YAAY,SAAiB,SAA0B;EACtD,MAAM,SAAS,OAAO;EACtB,KAAK,aAAa,QAAQ;EAC1B,KAAK,OAAO,QAAQ;EACpB,KAAK,UAAU,QAAQ;CACxB;AACD;;;;;;;;;;;;ACtCA,IAAa,eAAb,cAAkC,eAAe;CAChD;CACA,OAAwC;CACxC;;;;;;;;CASA,YAAY,SAAiB,SAA+B;EAC3D,MAAM,SAAS,OAAO;EACtB,KAAK,SAAS,SAAS;EACvB,KAAK,MAAM,SAAS;CACrB;AACD;;;;;;;;;;;;;;;;;;;;;;ACDA,IAAa,iBAAb,cAAoC,eAAe;CAClD,OAAgC;;CAEhC;CACA;;;;;;;CAQA,YAAY,SAAiB,SAAgC;EAC5D,MAAM,SAAS,OAAO;EACtB,KAAK,oBAAoB,QAAQ;EACjC,KAAK,YAAY,QAAQ;CAC1B;AACD"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"resource-client-C_D--PYX.mjs","names":["#window","#lastAllowedAt","#chains","#sleep","#trackers","#gateOnce","#tracker","#hooks","#intervalMs","#maxBucketLevel","#sleep","#chain","#waitForToken","#lastCheck","#bucketLevel","#budgets","#config","#hooks","#httpClient","#queues","#sleep","#getQueue","#gatedSend"],"sources":["../src/internal/utils/is-date-time-string.ts","../src/internal/utils/is-record.ts","../src/internal/http/budget-tracker.ts","../src/internal/http/budget-gate.ts","../src/internal/http/execute.ts","../src/internal/http/rate-limit-sample.ts","../src/internal/http/rate-limit-observation.ts","../src/internal/http/rate-limit-queue.ts","../src/internal/utils/try-catch.ts","../src/internal/http/fetch-client.ts","../src/internal/http/resolve-dependencies.ts","../src/internal/http/upload-request.ts","../src/internal/resource-client.ts"],"sourcesContent":["/**\n * Narrows `value` to a string that parses to a real {@link Date} via the\n * `Date(string)` constructor. Used by resource parsers to gate\n * `format: date-time` wire fields before handing them to `new Date(...)`,\n * which silently produces an `Invalid Date` for invalid input.\n *\n * @param value - The unknown wire value to validate.\n * @returns `true` when `value` is a string and `new Date(value).getTime()`\n * is not `NaN`.\n */\nexport function isDateTimeString(value: unknown): value is string {\n\tif (typeof value !== \"string\") {\n\t\treturn false;\n\t}\n\n\tconst parsed = new Date(value);\n\treturn !Number.isNaN(parsed.getTime());\n}\n","/**\n * Narrows `value` to a plain JSON-style record. Excludes arrays, class\n * instances, primitives, and `null`/`undefined`. Used by resource\n * parsers to gate property access on wire bodies whose shape isn't\n * known at compile time.\n *\n * @param value - The unknown value to narrow.\n * @returns `true` when `value` is a plain `[object Object]`.\n */\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n\treturn Object.prototype.toString.call(value) === \"[object Object]\";\n}\n","import type { RateLimitSample } from \"./rate-limit-sample.ts\";\n\nconst MS_PER_SECOND = 1000;\n\n/** Live window state for one scope: budget left and when it resets. */\ninterface WindowState {\n\t/** Best estimate of requests still allowed before the window resets. */\n\treadonly predictedRemaining: number;\n\t/** Absolute time (ms) the window resets to full. */\n\treadonly resetAt: number;\n}\n\n/**\n * Tracks the live rate-limit budget for a single scope. Primed by `observe`\n * from response headers and drawn down by `reserve` as requests leave, so\n * `waitMs` can pace requests across the window.\n *\n * Pacing has two regimes. While budget remains, requests are spread evenly over\n * the time left in the window (`timeLeft / remaining`), so a burst does not\n * spend the whole window's budget up front and then stall. Once the budget is\n * spent, requests hold until the window resets. Budget and reset time move\n * together as one window, so the tracker is either unprimed or fully primed,\n * never half-known.\n */\nexport class BudgetTracker {\n\t/** Time (ms) the most recent request was allowed out, for spacing. */\n\t#lastAllowedAt: number | undefined = undefined;\n\t#window: undefined | WindowState = undefined;\n\n\t/**\n\t * Folds a fresh server reading in, replacing any prior window. The latest\n\t * reading wins: observe time is monotonic, so the most recently resolved\n\t * response is the best current estimate. The spacing reference is left\n\t * untouched so a window refresh does not reset pacing mid-stream.\n\t *\n\t * @param sample - Parsed `remaining`/`resetSeconds` from a response.\n\t * @param now - The current time in ms.\n\t */\n\tpublic observe(sample: RateLimitSample, now: number): void {\n\t\tthis.#window = {\n\t\t\tpredictedRemaining: sample.remaining,\n\t\t\tresetAt: now + sample.resetSeconds * MS_PER_SECOND,\n\t\t};\n\t}\n\n\t/**\n\t * Accounts for one request leaving at `now`: records the spacing reference\n\t * and decrements the prediction. A no-op on the prediction while unprimed.\n\t *\n\t * @param now - The time the request was allowed out, in ms.\n\t */\n\tpublic reserve(now: number): void {\n\t\tthis.#lastAllowedAt = now;\n\t\tif (this.#window !== undefined) {\n\t\t\tthis.#window = {\n\t\t\t\t...this.#window,\n\t\t\t\tpredictedRemaining: this.#window.predictedRemaining - 1,\n\t\t\t};\n\t\t}\n\t}\n\n\t/**\n\t * Milliseconds to wait before the next request is allowed.\n\t *\n\t * @param now - The current time in ms.\n\t * @returns `0` when a request may go now (unprimed, or the first paced send);\n\t * the time until reset when the budget is spent; otherwise the time until\n\t * this request's evenly-spaced slot.\n\t */\n\tpublic waitMs(now: number): number {\n\t\tif (this.#window === undefined) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tconst { predictedRemaining, resetAt } = this.#window;\n\t\tif (predictedRemaining <= 0) {\n\t\t\treturn Math.max(0, resetAt - now);\n\t\t}\n\n\t\tif (this.#lastAllowedAt === undefined) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tconst interval = (resetAt - now) / predictedRemaining;\n\t\treturn Math.max(0, this.#lastAllowedAt + interval - now);\n\t}\n}\n","import type { SleepFunc } from \"../utils/sleep.ts\";\nimport { BudgetTracker } from \"./budget-tracker.ts\";\nimport type { RateLimitSample } from \"./rate-limit-sample.ts\";\n\n/**\n * Header-primed rate-limit gate shared across a client. Holds one\n * {@link BudgetTracker} per API key, since the tightest Roblox window is the\n * per-key one shared across every operation. Before each request the caller\n * gates on the request's key (sleeping if its budget is spent), and after each\n * response folds the parsed sample back in, so a sibling operation on the same\n * key can head off a 429 the static per-operation token bucket cannot foresee.\n * A per-operation tracker is deliberately not kept: every operation reports the\n * same most-constrained `remaining`, so a per-key tracker (drawn down by all\n * operations) is always the binding constraint.\n *\n * Gating is serialized per scope through a promise chain so concurrent\n * requests on one key cannot read the same budget and reserve the same slot;\n * each waits for the prior gate's reserve before computing its own.\n */\nexport class BudgetGate {\n\treadonly #chains = new Map<string, Promise<void>>();\n\treadonly #sleep: SleepFunc;\n\treadonly #trackers = new Map<string, BudgetTracker>();\n\n\t/**\n\t * Creates a gate bound to an injectable sleep.\n\t *\n\t * @param sleep - Injectable sleep (tests pass a fake clock).\n\t */\n\tconstructor(sleep: SleepFunc) {\n\t\tthis.#sleep = sleep;\n\t}\n\n\t/**\n\t * Holds until the scope's budget permits a send, then reserves one slot.\n\t * Runs after the prior gate on the same scope settles, whether it resolved\n\t * or rejected, so one failed attempt cannot poison later gates on the key.\n\t *\n\t * @param scope - The scope key to gate on (the effective API key).\n\t */\n\tpublic async gate(scope: string): Promise<void> {\n\t\tconst previous = this.#chains.get(scope) ?? Promise.resolve();\n\t\tconst runGate = async (): Promise<void> => this.#gateOnce(scope);\n\t\tconst mine = previous.then(runGate, runGate);\n\t\tthis.#chains.set(scope, mine);\n\t\tawait mine;\n\t}\n\n\t/**\n\t * Folds a response's parsed budget back onto the scope. A `undefined`\n\t * sample (headers absent or non-numeric) is ignored, leaving the scope on\n\t * static pacing.\n\t *\n\t * @param scope - The same scope key passed to {@link gate}.\n\t * @param sample - Parsed sample, or `undefined` when none was reported.\n\t */\n\tpublic observe(scope: string, sample: RateLimitSample | undefined): void {\n\t\tif (sample === undefined) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#tracker(scope).observe(sample, Date.now());\n\t}\n\n\tasync #gateOnce(scope: string): Promise<void> {\n\t\tconst tracker = this.#tracker(scope);\n\t\tconst waitMs = tracker.waitMs(Date.now());\n\t\tif (waitMs > 0) {\n\t\t\tawait this.#sleep(waitMs);\n\t\t}\n\n\t\ttracker.reserve(Date.now());\n\t}\n\n\t#tracker(scope: string): BudgetTracker {\n\t\tconst existing = this.#trackers.get(scope);\n\t\tif (existing !== undefined) {\n\t\t\treturn existing;\n\t\t}\n\n\t\tconst tracker = new BudgetTracker();\n\t\tthis.#trackers.set(scope, tracker);\n\t\treturn tracker;\n\t}\n}\n","import type { OpenCloudError } from \"../../errors/base.ts\";\nimport type { Result } from \"../../types.ts\";\nimport type { SleepFunc } from \"../utils/sleep.ts\";\nimport { computeRetryWaitMs, type RetryResolvable, shouldRetry } from \"./retry.ts\";\nimport type { HttpRequest, HttpResponse, OpenCloudHooks } from \"./types.ts\";\n\n/** A transport callback: takes a request, returns a classified Result. */\ntype SendFunc = (request: HttpRequest) => Promise<Result<HttpResponse, OpenCloudError>>;\n\n/**\n * Inputs to {@link executeWithRetry} bundled as an options object to keep the\n * function signature narrow.\n */\ninterface ExecuteOptions {\n\t/** Fully-resolved retry config (post-merge). */\n\treadonly config: RetryResolvable;\n\t/** Client-level observability hooks. */\n\treadonly hooks: OpenCloudHooks;\n\t/** Transport callback. May be pre-wrapped by a rate-limit queue. */\n\treadonly send: SendFunc;\n\t/** Injectable sleep (tests pass a fake). */\n\treadonly sleep: SleepFunc;\n}\n\n/**\n * Retry-aware orchestration loop. Coordinates a single logical request,\n * looping over `options.send` until it succeeds, the error is non-retryable,\n * or `options.config.maxRetries` is exhausted. Fires observability hooks\n * at each transition. Domain- and queue-agnostic: `send` may be any\n * callback, including one wrapped by a rate-limit queue.\n *\n * @param request - The immutable request to send.\n * @param options - The transport callback, resolved config, hooks, and sleep.\n * @returns The first success, or the final error after retries are exhausted.\n */\nexport async function executeWithRetry(\n\trequest: HttpRequest,\n\toptions: ExecuteOptions,\n): Promise<Result<HttpResponse, OpenCloudError>> {\n\tconst { config, hooks, send, sleep } = options;\n\n\tasync function attempt(): Promise<Result<HttpResponse, OpenCloudError>> {\n\t\thooks.onRequest?.(request);\n\t\treturn send(request);\n\t}\n\n\tlet result = await attempt();\n\n\tfor (let retry = 0; retry < config.maxRetries; retry++) {\n\t\tif (result.success || !shouldRetry(result.err, config)) {\n\t\t\treturn result;\n\t\t}\n\n\t\tconst { err } = result;\n\t\thooks.onRetry?.(retry + 1, err);\n\t\tconst waitMs = computeRetryWaitMs(err, { attempt: retry, retryDelay: config.retryDelay });\n\t\thooks.onRateLimit?.(waitMs);\n\t\tawait sleep(waitMs);\n\n\t\tresult = await attempt();\n\t}\n\n\treturn result;\n}\n","/**\n * A point-in-time rate-limit budget reading parsed from Roblox Open Cloud\n * response headers. Both fields are non-negative integers.\n */\nexport interface RateLimitSample {\n\t/** Requests still allowed in the current window (the most-constrained one). */\n\treadonly remaining: number;\n\t/** Seconds until the most-constrained window resets to full. */\n\treadonly resetSeconds: number;\n}\n\n/**\n * Reduces a comma-separated rate-limit header value (e.g. `\"0, 70000\"`) to a\n * single non-negative integer via `combine`. Tokens are trimmed; blank and\n * non-finite tokens (`\"\"`, `\"Infinity\"`, `\"abc\"`) are dropped so a stray value\n * cannot corrupt the result. Returns `undefined` when the header is absent or\n * has no finite tokens.\n *\n * @param headerValue - The raw header value, or `undefined` if missing.\n * @param combine - Pairwise reducer, `Math.min` for remaining, `Math.max` for reset.\n * @returns The reduced, floored, clamped value, or `undefined`.\n */\nexport function reduceRateLimitTokens(\n\theaderValue: string | undefined,\n\tcombine: (a: number, b: number) => number,\n): number | undefined {\n\tif (headerValue === undefined) {\n\t\treturn undefined;\n\t}\n\n\tconst tokens = headerValue\n\t\t.split(\",\")\n\t\t.map((part) => part.trim())\n\t\t.filter((part) => part !== \"\")\n\t\t.map((part) => Number(part))\n\t\t.filter((value) => Number.isFinite(value));\n\tif (tokens.length === 0) {\n\t\treturn undefined;\n\t}\n\n\treturn Math.max(0, Math.floor(tokens.reduce(combine)));\n}\n\n/**\n * Parses the `x-ratelimit-remaining` and `x-ratelimit-reset` response headers\n * into a {@link RateLimitSample}. Each header may carry a comma-separated list\n * of per-window values; `remaining` takes the smallest (most constrained) and\n * `resetSeconds` takes the largest (longest wait), symmetric to how a 429's\n * retry delay is reduced. Returns `undefined` when either header is missing or\n * has no finite numeric tokens, so a caller can fall back to static pacing.\n *\n * @param headers - Response headers with lowercased keys.\n * @returns The parsed sample, or `undefined` when the budget cannot be read.\n */\nexport function parseRateLimitHeaders(\n\theaders: Readonly<Record<string, string>>,\n): RateLimitSample | undefined {\n\tconst remaining = reduceRateLimitTokens(headers[\"x-ratelimit-remaining\"], (a, b) =>\n\t\tMath.min(a, b),\n\t);\n\tconst resetSeconds = reduceRateLimitTokens(headers[\"x-ratelimit-reset\"], (a, b) =>\n\t\tMath.max(a, b),\n\t);\n\tif (remaining === undefined || resetSeconds === undefined) {\n\t\treturn undefined;\n\t}\n\n\treturn { remaining, resetSeconds };\n}\n","import type { OpenCloudError } from \"../../errors/base.ts\";\nimport { RateLimitError } from \"../../errors/rate-limit.ts\";\nimport type { Result } from \"../../types.ts\";\nimport type { RateLimitSample } from \"./rate-limit-sample.ts\";\nimport { parseRateLimitHeaders } from \"./rate-limit-sample.ts\";\nimport type { HttpResponse } from \"./types.ts\";\n\n/**\n * Extracts a {@link RateLimitSample} from a transport result so the budget gate\n * can be fed from every attempt. A 2xx carries the budget in its headers; a 429\n * carries it on the {@link RateLimitError} (the raw headers are dropped before\n * this point). Any other error, or a response that reported no budget, yields\n * `undefined` and leaves the gate on static pacing.\n *\n * @param result - The classified transport result for one attempt.\n * @returns The parsed sample, or `undefined` when none was reported.\n */\nexport function rateLimitSampleFromResult(\n\tresult: Result<HttpResponse, OpenCloudError>,\n): RateLimitSample | undefined {\n\tif (result.success) {\n\t\treturn parseRateLimitHeaders(result.data.headers);\n\t}\n\n\tconst { err } = result;\n\tif (err instanceof RateLimitError && err.remaining !== undefined) {\n\t\treturn { remaining: err.remaining, resetSeconds: err.retryAfterSeconds };\n\t}\n\n\treturn undefined;\n}\n","import type { SleepFunc } from \"../utils/sleep.ts\";\nimport type { OpenCloudHooks } from \"./types.ts\";\n\n/**\n * Identifies and bounds a single Roblox Open Cloud operation for rate\n * limiting, e.g. `{ operationKey: \"game-passes.create\", maxPerSecond: 5 }`.\n */\nexport interface OperationLimit {\n\t/** Maximum sustained request rate in requests per second. */\n\treadonly maxPerSecond: number;\n\t/**\n\t * Stable identifier for the operation (e.g. \"game-passes.create\"). Not\n\t * consumed by the queue itself; callers use it to key per-operation\n\t * queues in a registry (see GamePassesClient).\n\t */\n\treadonly operationKey: string;\n}\n\n/**\n * Token-bucket rate limiter for a single `(apiKey, operation)` pair. Every\n * call to `acquire` consumes one token; when the bucket is empty the call\n * waits until a token regenerates before invoking the task. Burst capacity\n * equals `maxPerSecond`, refilling at `maxPerSecond` tokens per second.\n *\n * Implemented as a leaky bucket tracking drain debt in ms. `#lastCheck`\n * advances by `waitMs` after every sleep so the algorithm stays correct\n * whether or not the injected sleep moves `Date.now()` forward.\n */\nexport class RateLimitQueue {\n\treadonly #hooks: OpenCloudHooks;\n\treadonly #intervalMs: number;\n\treadonly #maxBucketLevel: number;\n\treadonly #sleep: SleepFunc;\n\n\t#bucketLevel = 0;\n\t#chain: Promise<void> = Promise.resolve();\n\t#lastCheck: number = Date.now();\n\n\t/**\n\t * Creates a rate-limit queue bound to a single operation.\n\t *\n\t * @param limit - The operation key and its per-second request ceiling.\n\t * @param hooks - Observability callbacks; `onRateLimit` fires when the\n\t * bucket is empty and a sleep is about to start.\n\t * @param sleep - Injectable sleep (tests pass a fake).\n\t */\n\tconstructor(limit: OperationLimit, hooks: OpenCloudHooks, sleep: SleepFunc) {\n\t\tthis.#intervalMs = 1000 / limit.maxPerSecond;\n\t\tthis.#maxBucketLevel = limit.maxPerSecond * this.#intervalMs;\n\t\tthis.#hooks = hooks;\n\t\tthis.#sleep = sleep;\n\t}\n\n\t/**\n\t * Waits for a token — sleeping and firing `hooks.onRateLimit` if the\n\t * bucket is empty — then executes `task`. Concurrent callers are\n\t * serialized at token acquisition; tasks themselves run independently\n\t * once their token is secured.\n\t *\n\t * @param task - The request to run once a token is available.\n\t * @returns The value produced by `task`.\n\t */\n\tpublic async acquire<T>(task: () => Promise<T>): Promise<T> {\n\t\tconst myTurn = this.#chain.then(async () => this.#waitForToken());\n\t\tthis.#chain = myTurn;\n\t\tawait myTurn;\n\t\treturn task();\n\t}\n\n\tasync #waitForToken(): Promise<void> {\n\t\tconst now = Math.max(Date.now(), this.#lastCheck);\n\t\tconst drained = Math.max(0, this.#bucketLevel - (now - this.#lastCheck));\n\t\tthis.#lastCheck = now;\n\n\t\tif (drained + this.#intervalMs <= this.#maxBucketLevel) {\n\t\t\tthis.#bucketLevel = drained + this.#intervalMs;\n\t\t\treturn;\n\t\t}\n\n\t\tconst waitMs = drained + this.#intervalMs - this.#maxBucketLevel;\n\t\tthis.#hooks.onRateLimit?.(waitMs);\n\t\tawait this.#sleep(waitMs);\n\t\tthis.#bucketLevel = this.#maxBucketLevel;\n\t\tthis.#lastCheck = now + waitMs;\n\t}\n}\n","import type { Result } from \"../../types.ts\";\n\n/**\n * Wraps a promise into a {@link Result}, catching rejections.\n *\n * @template T - The resolved value type.\n * @param promise - The promise to wrap.\n * @returns A Result containing the resolved value or the rejection error.\n */\nexport async function tryCatch<T>(promise: Promise<T>): Promise<Result<T>> {\n\ttry {\n\t\tconst data = await promise;\n\t\treturn { data, success: true };\n\t} catch (err) {\n\t\treturn { err: err instanceof Error ? err : new Error(String(err)), success: false };\n\t}\n}\n","import { ApiError } from \"../../errors/api-error.ts\";\nimport type { OpenCloudError } from \"../../errors/base.ts\";\nimport { NetworkError } from \"../../errors/network-error.ts\";\nimport { RateLimitError } from \"../../errors/rate-limit.ts\";\nimport type { Result } from \"../../types.ts\";\nimport { tryCatch } from \"../utils/try-catch.ts\";\nimport { reduceRateLimitTokens } from \"./rate-limit-sample.ts\";\nimport type { HttpClient, HttpRequest, HttpResponse, RequestConfig } from \"./types.ts\";\n\n// Caps the raw body retained when a response cannot be parsed, so a multi-KB\n// HTML error page is not surfaced or logged whole.\nconst MAX_DETAIL_LENGTH = 500;\n\nconst CONTENT_TYPE_HEADER = \"content-type\";\n\ninterface ParseFailureArgs {\n\treadonly cause: Error;\n\treadonly response: Response;\n\treadonly text: string;\n}\n\ninterface ApiErrorMessageParts {\n\treadonly code: string | undefined;\n\treadonly message: string | undefined;\n\treadonly status: number;\n}\n\n/**\n * Converts a `Headers` object to a plain record with lowercased keys.\n *\n * @param headers - The `Headers` instance to convert.\n * @returns A record mapping lowercased header names to their values.\n */\nexport function headersToRecord(headers: Headers): Record<string, string> {\n\treturn Object.fromEntries(headers);\n}\n\n/**\n * Permissively extracts a machine-readable error code from a response body.\n *\n * Modern Open Cloud responses use `{ errorCode: string, message: string }`;\n * the legacy game-internationalization endpoints use\n * `{ errors: [{ code: number, message: string }, ...] }`. Both shapes are\n * checked; numeric legacy codes are returned as strings so callers see one\n * consistent type.\n *\n * @param body - The parsed response body (unknown shape).\n * @returns The error code if present, otherwise `undefined`.\n */\nexport function extractErrorCode(body: unknown): string | undefined {\n\tif (body === null || typeof body !== \"object\") {\n\t\treturn undefined;\n\t}\n\n\tconst errorCode = Reflect.get(body, \"errorCode\");\n\tif (typeof errorCode === \"string\") {\n\t\treturn errorCode;\n\t}\n\n\treturn extractLegacyCode(body);\n}\n\n/**\n * Permissively extracts a human-readable error message from a response body.\n *\n * Modern Open Cloud responses expose `message` at the top level; the legacy\n * game-internationalization endpoints nest it under `errors[0].message`.\n *\n * @param body - The parsed response body (unknown shape).\n * @returns The message if present, otherwise `undefined`.\n */\nexport function extractErrorMessage(body: unknown): string | undefined {\n\tif (body === null || typeof body !== \"object\") {\n\t\treturn undefined;\n\t}\n\n\tconst message = Reflect.get(body, \"message\");\n\tif (typeof message === \"string\") {\n\t\treturn message;\n\t}\n\n\treturn extractLegacyMessage(body);\n}\n\n/**\n * Parses the `x-ratelimit-reset` header value into seconds. On a 429 the header\n * is a comma-separated list of per-window reset times (e.g. `\"22, 0\"`, one entry\n * per rate-limit window); the largest value is the longest-resetting window and\n * the only safe wait that won't retry into a still-exhausted window. A single\n * value is treated as a one-element list.\n *\n * @param headerValue - The raw header value, or `undefined` if missing.\n * @returns The number of seconds to wait, or 0 if missing/invalid.\n */\nexport function parseRetryAfterSeconds(headerValue: string | undefined): number {\n\treturn reduceRateLimitTokens(headerValue, (a, b) => Math.max(a, b)) ?? 0;\n}\n\n/**\n * Joins the base URL from config with the relative path from the request.\n *\n * @param request - The HTTP request containing the relative URL.\n * @param config - The request config containing the base URL.\n * @returns The fully-qualified URL string.\n */\nexport function buildUrl(request: HttpRequest, config: RequestConfig): string {\n\tconst base = config.baseUrl.endsWith(\"/\") ? config.baseUrl.slice(0, -1) : config.baseUrl;\n\treturn `${base}${request.url}`;\n}\n\n/**\n * Constructs the `RequestInit` options for a `fetch` call.\n *\n * @param request - The HTTP request to build options for.\n * @param config - The request config containing API key and timeout.\n * @returns A `RequestInit` object ready for `fetch`.\n */\nexport function buildFetchOptions(request: HttpRequest, config: RequestConfig): RequestInit {\n\tconst headers = new Headers({\n\t\t\"x-api-key\": config.apiKey,\n\t});\n\n\tconst options: RequestInit = {\n\t\theaders,\n\t\tmethod: request.method,\n\t};\n\n\tif (request.body instanceof FormData) {\n\t\toptions.body = request.body;\n\t} else if (request.body instanceof Uint8Array) {\n\t\theaders.set(CONTENT_TYPE_HEADER, \"application/octet-stream\");\n\t\toptions.body = request.body;\n\t} else if (request.body !== undefined) {\n\t\theaders.set(CONTENT_TYPE_HEADER, \"application/json\");\n\t\toptions.body = JSON.stringify(request.body);\n\t}\n\n\tif (request.headers !== undefined) {\n\t\tfor (const [name, value] of Object.entries(request.headers)) {\n\t\t\tif (name.toLowerCase() === \"x-api-key\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\theaders.set(name, value);\n\t\t}\n\t}\n\n\tif (config.timeout !== undefined) {\n\t\toptions.signal = AbortSignal.timeout(config.timeout);\n\t}\n\n\treturn options;\n}\n\n/**\n * Creates an {@link HttpClient} backed by the Fetch API.\n *\n * @param fetchFunc - The fetch implementation to use. Defaults to `globalThis.fetch`.\n * @returns An HttpClient that classifies responses into typed Results.\n */\nexport function createFetchHttpClient(\n\tfetchFunc: (url: string, init: RequestInit) => Promise<Response> = globalThis.fetch,\n): HttpClient {\n\treturn {\n\t\tasync request(\n\t\t\thttpRequest: HttpRequest,\n\t\t\tconfig: RequestConfig,\n\t\t): Promise<Result<HttpResponse, OpenCloudError>> {\n\t\t\tconst url = buildUrl(httpRequest, config);\n\t\t\tconst options = buildFetchOptions(httpRequest, config);\n\n\t\t\tconst fetchResult = await tryCatch(fetchFunc(url, options));\n\t\t\tif (!fetchResult.success) {\n\t\t\t\treturn {\n\t\t\t\t\terr: new NetworkError(\"Network request failed\", {\n\t\t\t\t\t\tcause: fetchResult.err,\n\t\t\t\t\t\tmethod: httpRequest.method,\n\t\t\t\t\t\turl,\n\t\t\t\t\t}),\n\t\t\t\t\tsuccess: false,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn classifyResponse(fetchResult.data);\n\t\t},\n\t};\n}\n\nfunction readLegacyErrorEntry(body: object): object | undefined {\n\tconst errors = Reflect.get(body, \"errors\");\n\tif (!Array.isArray(errors)) {\n\t\treturn undefined;\n\t}\n\n\tconst [first] = errors;\n\tif (typeof first !== \"object\" || first === null) {\n\t\treturn undefined;\n\t}\n\n\treturn first;\n}\n\nfunction extractLegacyCode(body: object): string | undefined {\n\tconst first = readLegacyErrorEntry(body);\n\tif (first === undefined) {\n\t\treturn undefined;\n\t}\n\n\tconst code = Reflect.get(first, \"code\");\n\tif (typeof code === \"string\") {\n\t\treturn code;\n\t}\n\n\treturn typeof code === \"number\" ? String(code) : undefined;\n}\n\nfunction extractLegacyMessage(body: object): string | undefined {\n\tconst first = readLegacyErrorEntry(body);\n\tif (first === undefined) {\n\t\treturn undefined;\n\t}\n\n\tconst message = Reflect.get(first, \"message\");\n\treturn typeof message === \"string\" ? message : undefined;\n}\n\nfunction formatApiErrorMessage(parts: ApiErrorMessageParts): string {\n\tconst { code, message, status } = parts;\n\tconst base = `HTTP ${status}`;\n\tif (message === undefined && code === undefined) {\n\t\treturn base;\n\t}\n\n\tif (message === undefined) {\n\t\treturn `${base} (code ${code})`;\n\t}\n\n\tif (code === undefined) {\n\t\treturn `${base}: ${message}`;\n\t}\n\n\treturn `${base}: ${message} (code ${code})`;\n}\n\nfunction createApiError(status: number, body: JSONValue | undefined): ApiError {\n\tconst code = extractErrorCode(body);\n\tconst message = extractErrorMessage(body);\n\treturn new ApiError(formatApiErrorMessage({ code, message, status }), {\n\t\tcode,\n\t\tdetails: body,\n\t\tstatusCode: status,\n\t});\n}\n\nfunction createRateLimitError(response: Response): RateLimitError {\n\tconst headers = headersToRecord(response.headers);\n\treturn new RateLimitError(\"Rate limited\", {\n\t\tremaining: reduceRateLimitTokens(headers[\"x-ratelimit-remaining\"], (a, b) =>\n\t\t\tMath.min(a, b),\n\t\t),\n\t\tretryAfterSeconds: parseRetryAfterSeconds(headers[\"x-ratelimit-reset\"]),\n\t});\n}\n\n/**\n * Parses response text as JSON, returning the underlying `SyntaxError` on\n * failure rather than throwing. The synchronous sibling of {@link tryCatch}.\n *\n * @param text - The raw response body text.\n * @returns A Result wrapping the parsed value, or the parse error.\n */\nfunction parseJson(text: string): Result<JSONValue> {\n\ttry {\n\t\treturn { data: JSON.parse(text), success: true };\n\t} catch (err) {\n\t\treturn { err: err instanceof Error ? err : new Error(String(err)), success: false };\n\t}\n}\n\n/**\n * Builds the error for a 2xx response whose body could not be parsed as JSON,\n * preserving the parse `cause`, the (truncated) raw body, and the declared\n * content-type so the failure can be diagnosed after the fact.\n *\n * @param args - The Response, raw body text, and underlying parse error.\n * @returns An ApiError carrying the diagnostic context.\n */\nfunction parseFailureError({ cause, response, text }: ParseFailureArgs): ApiError {\n\tconst contentType = response.headers.get(CONTENT_TYPE_HEADER) ?? \"unknown\";\n\treturn new ApiError(`Failed to parse response body (content-type: ${contentType})`, {\n\t\tcause,\n\t\tdetails: text.slice(0, MAX_DETAIL_LENGTH),\n\t\tstatusCode: response.status,\n\t});\n}\n\n/**\n * Classifies a fetch `Response` into a typed `Result`.\n *\n * The body is read once and parsed best-effort. Error responses (status >= 300)\n * never require valid JSON: an error body that is not valid JSON (for example\n * an HTML gateway page) degrades to a status-based {@link ApiError} carrying\n * the raw text. A parse failure is only fatal on a 2xx, where a parseable body is part\n * of the contract.\n *\n * @param response - The raw fetch Response to classify.\n * @returns A Result containing an HttpResponse on success or an OpenCloudError on failure.\n */\nasync function classifyResponse(response: Response): Promise<Result<HttpResponse, OpenCloudError>> {\n\tif (response.status === 429) {\n\t\treturn { err: createRateLimitError(response), success: false };\n\t}\n\n\tconst text = await response.text();\n\tconst parsed: Result<JSONValue | undefined> =\n\t\ttext === \"\" ? { data: undefined, success: true } : parseJson(text);\n\n\tif (response.status >= 300) {\n\t\tconst body = parsed.success ? parsed.data : text.slice(0, MAX_DETAIL_LENGTH);\n\t\treturn { err: createApiError(response.status, body), success: false };\n\t}\n\n\tif (!parsed.success) {\n\t\treturn { err: parseFailureError({ cause: parsed.err, response, text }), success: false };\n\t}\n\n\treturn {\n\t\tdata: {\n\t\t\tbody: parsed.data,\n\t\t\theaders: headersToRecord(response.headers),\n\t\t\tstatus: response.status,\n\t\t},\n\t\tsuccess: true,\n\t};\n}\n","import { setTimeout } from \"node:timers/promises\";\n\nimport type { HttpClient, SleepFunc } from \"../../client/types.ts\";\nimport { createFetchHttpClient } from \"./fetch-client.ts\";\n\n/**\n * Options accepted by {@link resolveDependencies}. Mirrors the test-seam\n * subset of the public client options.\n */\ninterface ResolveDependenciesOptions {\n\t/** Test seam: custom {@link HttpClient}. Defaults to a fetch-backed client. */\n\treadonly httpClient?: HttpClient | undefined;\n\t/** Test seam: custom {@link SleepFunc}. Defaults to a `setTimeout`-backed sleep. */\n\treadonly sleep?: SleepFunc | undefined;\n}\n\n/**\n * Fully-populated dependency set consumed by resource client constructors.\n */\ninterface ResolvedDependencies {\n\t/** Concrete {@link HttpClient} implementation. */\n\treadonly httpClient: HttpClient;\n\t/** Concrete {@link SleepFunc} implementation. */\n\treadonly sleep: SleepFunc;\n}\n\n/**\n * Resolves the concrete HTTP client and sleep implementation a resource\n * client should use. Falls back to the fetch-backed HTTP client and the\n * default `setTimeout`-based sleep when the caller omits the test seams.\n *\n * Extracted so resource client constructors can keep their dependency\n * resolution logic in a single, unit-testable place; this makes the\n * default branches easy to cover without stubbing globals like `fetch`.\n *\n * @param options - Optional {@link HttpClient} and {@link SleepFunc} test seams.\n * @returns A {@link ResolvedDependencies} with defaults applied.\n */\nexport function resolveDependencies(options: ResolveDependenciesOptions): ResolvedDependencies {\n\treturn {\n\t\thttpClient: options.httpClient ?? createFetchHttpClient(),\n\t\tsleep: options.sleep ?? setTimeout,\n\t};\n}\n","import type { HttpRequest } from \"../../client/types.ts\";\n\n/**\n * Reports whether a request is an upload: its body is `FormData`\n * (multipart) or `Uint8Array` (raw binary). Upload latency is\n * bandwidth-bound rather than compute-bound, so the SDK applies no default\n * request timeout to these requests; a sensible wall-clock budget depends on\n * payload size and link quality the SDK cannot know.\n *\n * @param request - The built request to classify.\n * @returns `true` when the body is `FormData` or `Uint8Array`.\n */\nexport function isUploadRequest(request: HttpRequest): boolean {\n\treturn request.body instanceof FormData || request.body instanceof Uint8Array;\n}\n","import type { Except } from \"type-fest\";\n\nimport type {\n\tHttpClient,\n\tHttpRequest,\n\tHttpResponse,\n\tOpenCloudClientOptions,\n\tOpenCloudHooks,\n\tRequestConfig,\n\tRequestOptions,\n\tSleepFunc,\n} from \"../client/types.ts\";\nimport { ApiError } from \"../errors/api-error.ts\";\nimport type { OpenCloudError } from \"../errors/base.ts\";\nimport { PermissionError } from \"../errors/permission-error.ts\";\nimport type { Result } from \"../types.ts\";\nimport { BudgetGate } from \"./http/budget-gate.ts\";\nimport { executeWithRetry } from \"./http/execute.ts\";\nimport { rateLimitSampleFromResult } from \"./http/rate-limit-observation.ts\";\nimport { type OperationLimit, RateLimitQueue } from \"./http/rate-limit-queue.ts\";\nimport { resolveDependencies } from \"./http/resolve-dependencies.ts\";\nimport {\n\tdefaultRetryDelay,\n\tIDEMPOTENT_METHOD_DEFAULTS,\n\tmergeConfig,\n\ttype MethodKind,\n\ttype RetryResolvable,\n} from \"./http/retry.ts\";\nimport { isUploadRequest } from \"./http/upload-request.ts\";\n\n/**\n * Describes a single resource method's shape for dispatch through\n * `ResourceClient.execute`. Each resource client declares one module-level\n * constant per public method; that constant binds the four resource-specific\n * values (request builder, response parser, retry-policy method kind,\n * operation-level rate limit) and flows through `execute` uniformly.\n *\n * @template P - The resource-specific parameter shape the builder\n * accepts.\n * @template T - The resource-specific parsed success type the parser\n * produces.\n */\nexport interface ResourceMethodSpec<P, T> {\n\t/**\n\t * Builds the pure {@link HttpRequest} for a single call. Returns a\n\t * {@link Result} so a builder can short-circuit with a local error\n\t * (typically a {@link OpenCloudError} subclass such as `ValidationError`)\n\t * before any HTTP, queue, or retry work happens. Builders that cannot\n\t * fail wrap their return as `{ data: request, success: true }`.\n\t */\n\treadonly buildRequest: (parameters: P) => Result<HttpRequest, OpenCloudError>;\n\t/** Method-level retry defaults merged into the resolved config. */\n\treadonly methodDefaults: Partial<RetryResolvable>;\n\t/**\n\t * Method kind, controlling merge precedence: `\"create\"` lets method\n\t * defaults win over client config so create safety cannot be relaxed\n\t * silently; `\"idempotent\"` lets client config win over method defaults\n\t * so consumers can loosen retry globally.\n\t */\n\treadonly methodKind: MethodKind;\n\t/** Operation-level rate limit, keyed into the client's per-key queue map. */\n\treadonly operationLimit: OperationLimit;\n\t/**\n\t * Converts the full {@link HttpResponse} into the resource-specific\n\t * parsed shape. Takes the whole response (body, status, headers) so\n\t * future parsers can read headers without widening the signature.\n\t */\n\treadonly parse: (response: HttpResponse) => Result<T, OpenCloudError>;\n\t/**\n\t * Open Cloud scopes the API key or OAuth token must carry for this\n\t * method, sourced from the vendored OpenAPI schema's `x-roblox-scopes`.\n\t * When set, a 401 or 403 ApiError from the upstream call is upgraded to\n\t * a {@link PermissionError} carrying these scopes alongside\n\t * {@link OperationLimit.operationKey}, so callers can name the missing\n\t * scope instead of just the HTTP status. Optional so test specs and\n\t * not-yet-wired resources can opt out.\n\t */\n\treadonly requiredScopes?: ReadonlyArray<string>;\n}\n\n/**\n * Single-argument bundle consumed by `ResourceClient.execute`: the per-method\n * spec, the resource-specific parameters, and optional per-request config\n * overrides.\n *\n * @template P - The resource-specific parameter shape the builder accepts.\n * @template T - The resource-specific parsed success type the parser produces.\n */\ninterface ExecuteCall<P, T> {\n\t/** Optional per-request config overrides. */\n\treadonly options?: RequestOptions | undefined;\n\t/** Resource-specific request parameters. */\n\treadonly parameters: P;\n\t/** Per-method binding of builder, parser, method kind, and operation limit. */\n\treadonly spec: ResourceMethodSpec<P, T>;\n}\n\n/**\n * Wraps an infallible request build as a {@link Result}-returning\n * `buildRequest` callback compatible with {@link ResourceMethodSpec}.\n * Use from a resource client whose builder cannot fail; resource clients\n * with local validation should construct the {@link Result} directly.\n *\n * @param request - The pre-built {@link HttpRequest}.\n * @returns A success Result wrapping the request.\n */\nexport function okRequest(request: HttpRequest): Result<HttpRequest, OpenCloudError> {\n\treturn { data: request, success: true };\n}\n\n/**\n * A {@link ResourceMethodSpec.parse} implementation for endpoints that return\n * no business payload on success (such as `DELETE` and reorder operations).\n * Surfaces `undefined` data and never inspects the response body.\n *\n * @returns A success Result with `undefined` data.\n */\nexport function parseEmptyResponse(): Result<undefined, OpenCloudError> {\n\treturn { data: undefined, success: true };\n}\n\nconst CLIENT_DEFAULTS = Object.freeze({\n\tbaseUrl: \"https://apis.roblox.com\",\n\tmaxRetries: 3,\n\tretryableStatuses: IDEMPOTENT_METHOD_DEFAULTS.retryableStatuses,\n\tretryableTransportCodes: IDEMPOTENT_METHOD_DEFAULTS.retryableTransportCodes,\n\tretryDelay: defaultRetryDelay,\n\ttimeout: 30_000,\n} satisfies Except<RetryResolvable, \"apiKey\">);\n\n/**\n * Inputs to {@link buildRequestConfig}, bundled to keep the signature narrow.\n */\ninterface RequestConfigInputs {\n\t/** The resolved config for this call. */\n\treadonly merged: RetryResolvable;\n\t/** The caller's per-request overrides, if any. */\n\treadonly options: RequestOptions | undefined;\n\t/** The built request, inspected for an upload body. */\n\treadonly request: HttpRequest;\n}\n\n/**\n * Internal orchestrator shared by every Open Cloud resource client. Holds\n * the frozen client config, observability hooks, injected HTTP client and\n * sleep, and the per-effective-key rate-limit queue registry. Resource\n * classes compose one instance and dispatch every public method through\n * {@link ResourceClient.execute} with a per-method {@link ResourceMethodSpec}.\n * Not exported from any package subpath; reachable only via sibling\n * `src/resources/**` modules in this package.\n */\nexport class ResourceClient {\n\treadonly #budgets: BudgetGate;\n\treadonly #config: Readonly<RetryResolvable>;\n\treadonly #hooks: OpenCloudHooks;\n\treadonly #httpClient: HttpClient;\n\treadonly #queues = new Map<string, RateLimitQueue>();\n\treadonly #sleep: SleepFunc;\n\n\t/**\n\t * Creates a new {@link ResourceClient}. Resolves the injected HTTP\n\t * client and sleep (defaulting to fetch + `setTimeout`) and freezes the\n\t * merged client config so subsequent calls cannot mutate it.\n\t *\n\t * @param options - Client-level configuration including the API key\n\t * and optional construction-time test seams.\n\t */\n\tconstructor(options: OpenCloudClientOptions) {\n\t\tconst { apiKey, hooks, httpClient, sleep, ...overrides } = options;\n\t\tconst resolved = resolveDependencies({ httpClient, sleep });\n\t\tthis.#httpClient = resolved.httpClient;\n\t\tthis.#sleep = resolved.sleep;\n\t\tthis.#budgets = new BudgetGate(this.#sleep);\n\t\tthis.#hooks = hooks ?? {};\n\t\tthis.#config = Object.freeze({\n\t\t\t...CLIENT_DEFAULTS,\n\t\t\tapiKey,\n\t\t\t...overrides,\n\t\t});\n\t}\n\n\t/**\n\t * Dispatches a single resource-method call. Merges the frozen client\n\t * config with the method's `methodDefaults` and the caller's optional\n\t * per-request `options`, routes through the effective-apiKey rate-limit\n\t * queue, runs the retry loop, and finally parses the response with the\n\t * spec's parser.\n\t *\n\t * @param call - The per-method spec, resource-specific parameters, and\n\t * optional per-request overrides.\n\t * @returns The parsed success payload or the {@link OpenCloudError} that\n\t * caused the request to fail. Never throws.\n\t */\n\tpublic async execute<P, T>(call: ExecuteCall<P, T>): Promise<Result<T, OpenCloudError>> {\n\t\tconst { options, parameters, spec } = call;\n\t\tconst merged = mergeConfig(this.#config, {\n\t\t\tmethodDefaults: spec.methodDefaults,\n\t\t\tmethodKind: spec.methodKind,\n\t\t\trequestOptions: options ?? {},\n\t\t});\n\t\tconst requestResult = spec.buildRequest(parameters);\n\t\tif (!requestResult.success) {\n\t\t\treturn requestResult;\n\t\t}\n\n\t\tconst requestConfig = buildRequestConfig({ merged, options, request: requestResult.data });\n\t\tconst queue = this.#getQueue(merged.apiKey, spec.operationLimit);\n\t\tconst httpResult = await queue.acquire(async () => {\n\t\t\treturn executeWithRetry(requestResult.data, {\n\t\t\t\tconfig: merged,\n\t\t\t\thooks: this.#hooks,\n\t\t\t\tsend: this.#gatedSend(merged.apiKey, requestConfig),\n\t\t\t\tsleep: this.#sleep,\n\t\t\t});\n\t\t});\n\t\tif (!httpResult.success) {\n\t\t\treturn { err: enrichPermissionError(httpResult.err, spec), success: false };\n\t\t}\n\n\t\treturn spec.parse(httpResult.data);\n\t}\n\n\t/**\n\t * Returns the sleep function used by this client instance.\n\t *\n\t * @returns The sleep function injected at construction time.\n\t */\n\tpublic get sleep(): SleepFunc {\n\t\treturn this.#sleep;\n\t}\n\n\t/**\n\t * Builds the transport callback for one logical call, wrapping the HTTP\n\t * client with the budget gate: each attempt waits on the API key's budget\n\t * before sending, then folds the response's reported budget back in so the\n\t * next attempt (or a sibling operation on the same key) can head off a 429.\n\t *\n\t * @param apiKey - The effective API key to gate on.\n\t * @param requestConfig - The resolved per-request transport config.\n\t * @returns A send callback for {@link executeWithRetry}.\n\t */\n\t#gatedSend(\n\t\tapiKey: string,\n\t\trequestConfig: RequestConfig,\n\t): (request: HttpRequest) => Promise<Result<HttpResponse, OpenCloudError>> {\n\t\treturn async (toSend) => {\n\t\t\tawait this.#budgets.gate(apiKey);\n\t\t\tconst sendResult = await this.#httpClient.request(toSend, requestConfig);\n\t\t\tthis.#budgets.observe(apiKey, rateLimitSampleFromResult(sendResult));\n\t\t\treturn sendResult;\n\t\t};\n\t}\n\n\t#getQueue(apiKey: string, limit: OperationLimit): RateLimitQueue {\n\t\tconst key = `${apiKey}::${limit.operationKey}`;\n\t\tconst existing = this.#queues.get(key);\n\t\tif (existing !== undefined) {\n\t\t\treturn existing;\n\t\t}\n\n\t\tconst queue = new RateLimitQueue(limit, this.#hooks, this.#sleep);\n\t\tthis.#queues.set(key, queue);\n\t\treturn queue;\n\t}\n}\n\n/**\n * Resolves the per-request {@link RequestConfig}. Upload requests\n * ({@link isUploadRequest}) carry no default timeout: a multi-megabyte place\n * file over a slow link is bandwidth-bound, so a client-side deadline only\n * fires spuriously. An explicit `options.timeout` still applies to any\n * request; every non-upload request keeps the merged default.\n *\n * @param inputs - The merged config, the built request, and per-request overrides.\n * @returns The config to hand to the transport, with `timeout` omitted when\n * no client-side deadline should apply.\n */\nfunction buildRequestConfig(inputs: RequestConfigInputs): RequestConfig {\n\tconst { merged, options, request } = inputs;\n\tconst shouldOmitDefaultTimeout = options?.timeout === undefined && isUploadRequest(request);\n\treturn {\n\t\tapiKey: merged.apiKey,\n\t\tbaseUrl: merged.baseUrl,\n\t\t...(shouldOmitDefaultTimeout ? {} : { timeout: merged.timeout }),\n\t};\n}\n\nfunction enrichPermissionError<P, T>(\n\terr: OpenCloudError,\n\tspec: ResourceMethodSpec<P, T>,\n): OpenCloudError {\n\tif (spec.requiredScopes === undefined) {\n\t\treturn err;\n\t}\n\n\tif (err instanceof PermissionError) {\n\t\treturn err;\n\t}\n\n\tif (!(err instanceof ApiError)) {\n\t\treturn err;\n\t}\n\n\tif (err.statusCode !== 401 && err.statusCode !== 403) {\n\t\treturn err;\n\t}\n\n\treturn new PermissionError(err.message, {\n\t\tcause: err.cause,\n\t\tcode: err.code,\n\t\tdetails: err.details,\n\t\toperationKey: spec.operationLimit.operationKey,\n\t\trequiredScopes: spec.requiredScopes,\n\t\tstatusCode: err.statusCode,\n\t});\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"retry-r1TXe5Zd.d.mts","names":[],"sources":["../src/errors/api-error.ts","../src/errors/network-error.ts","../src/errors/rate-limit.ts","../src/internal/http/retry.ts"],"mappings":";;;;;AAOA;;;UAAiB,eAAA,SAAwB,YAAA;;EAExC,IAAA;;EAEA,OAAA,GAAU,SAAA;;EAEV,UAAA;AAAA;AA6BD;;;;;;;;;;;;;;;;;;;AAauC;;;;AChDvC;;;ADmCA,cAAa,QAAA,SAAiB,cAAA;EAAA,SACb,IAAA;EAAA,SACA,OAAA,EAAS,SAAA;EAAA,kBACA,IAAA;EAAA,SACT,UAAA;ECnChB;AAYD;;;;;;EDgCC,WAAA,CAAY,OAAA,UAAiB,OAAA,EAAS,eAAA;AAAA;;;;;AAhDvC;;;UCAiB,mBAAA,SAA4B,YAAA;;EAE5C,MAAA;;EAEA,GAAA;AAAA;;ADEA;AA6BD;;;;;;;cCnBa,YAAA,SAAqB,cAAA;EAAA,SACjB,MAAA;EAAA,kBACS,IAAA;EAAA,SACT,GAAA;;;;;;;;EAShB,WAAA,CAAY,OAAA,UAAiB,OAAA,GAAU,mBAAA;AAAA;;;;;AD5BxC;;;UEAiB,qBAAA,SAA8B,YAAA;;;;;;;AFM9C;EEEA,SAAA;EF2BY;EEzBZ,iBAAA;AAAA;;;;;;;;;;;;;;;;AFsCsC;;;;cEhB1B,cAAA,SAAuB,cAAA;EAAA,kBACV,IAAA;;WAET,SAAA;EAAA,SACA,iBAAA;;;;ADhChB;AAYD;;EC4BC,WAAA,CAAY,OAAA,UAAiB,OAAA,EAAS,qBAAA;AAAA;;;AF5CvC;;;;;;;;AAAA,UGMiB,eAAA;EHAhB;EAAA,SGES,MAAA;EH2BV;EAAA,SGzBU,OAAA;;WAEA,UAAA;;WAEA,iBAAA,EAAmB,aAAA;EHqBC;;;;;;EAAA,SGdpB,uBAAA,EAAyB,aAAA;;WAEzB,UAAA,GAAa,OAAA;;WAEb,OAAA;AAAA;;AHuB6B;;;;AChDvC;;;;;cEsCa,yBAAA,EAA2B,aAAA;;KAsC5B,UAAA"}
|