@redocly/client-generator 0.4.25 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/eject-assets/AGENTS.md +24 -23
- package/eject-assets/generators/cli/docs.ts +1 -1
- package/eject-assets/generators/cli/engine-source.ts +1 -1
- package/eject-assets/generators/cli/index.ts +2 -2
- package/eject-assets/generators/cli/render.ts +1 -1
- package/eject-assets/generators/go/client.ts +1 -1
- package/eject-assets/generators/go/descriptor.ts +1 -1
- package/eject-assets/generators/go/index.ts +2 -2
- package/eject-assets/generators/go/models.ts +1 -1
- package/eject-assets/generators/go/naming.ts +1 -1
- package/eject-assets/generators/go/operations.ts +35 -3
- package/eject-assets/generators/go/pagination.ts +1 -1
- package/eject-assets/generators/go/types.ts +1 -1
- package/eject-assets/generators/mock/faker.ts +1 -1
- package/eject-assets/generators/mock/index.ts +2 -2
- package/eject-assets/generators/mock/render.ts +1 -1
- package/eject-assets/generators/mock/sample.ts +1 -1
- package/eject-assets/generators/mock/values.ts +1 -1
- package/eject-assets/generators/php/client.ts +1 -1
- package/eject-assets/generators/php/descriptor.ts +1 -1
- package/eject-assets/generators/php/index.ts +2 -2
- package/eject-assets/generators/php/models.ts +1 -1
- package/eject-assets/generators/php/naming.ts +1 -1
- package/eject-assets/generators/php/operations.ts +21 -3
- package/eject-assets/generators/php/pagination.ts +1 -1
- package/eject-assets/generators/php/types.ts +1 -1
- package/eject-assets/generators/python/client.ts +1 -1
- package/eject-assets/generators/python/descriptor.ts +1 -1
- package/eject-assets/generators/python/index.ts +2 -2
- package/eject-assets/generators/python/models.ts +1 -1
- package/eject-assets/generators/python/naming.ts +1 -1
- package/eject-assets/generators/python/operations.ts +19 -6
- package/eject-assets/generators/python/pagination.ts +1 -1
- package/eject-assets/generators/python/types.ts +1 -1
- package/eject-assets/generators/swr/index.ts +2 -2
- package/eject-assets/generators/swr/render.ts +1 -1
- package/eject-assets/generators/tanstack-query/index.ts +2 -2
- package/eject-assets/generators/tanstack-query/render.ts +1 -1
- package/eject-assets/generators/transformers/index.ts +2 -2
- package/eject-assets/generators/transformers/render.ts +1 -1
- package/eject-assets/generators/typescript/banner.ts +1 -1
- package/eject-assets/generators/typescript/client.ts +1 -1
- package/eject-assets/generators/typescript/descriptor.ts +1 -1
- package/eject-assets/generators/typescript/index.ts +2 -2
- package/eject-assets/generators/typescript/inline-runtime.ts +1 -1
- package/eject-assets/generators/typescript/operation-signature.ts +1 -1
- package/eject-assets/generators/typescript/operation-types.ts +1 -1
- package/eject-assets/generators/typescript/operations.ts +3 -4
- package/eject-assets/generators/typescript/response-headers.ts +1 -1
- package/eject-assets/generators/typescript/type-guards.ts +1 -1
- package/eject-assets/generators/typescript/types.ts +1 -1
- package/eject-assets/generators/zod/index.ts +2 -2
- package/eject-assets/generators/zod/schemas.ts +1 -1
- package/eject-assets/skills/client-generators/SKILL.md +24 -23
- package/eject-assets/skills/go-generator/SKILL.md +11 -0
- package/eject-assets/skills/php-generator/SKILL.md +16 -0
- package/eject-assets/skills/python-generator/SKILL.md +13 -0
- package/eject-assets/skills/typescript-generator/SKILL.md +16 -0
- package/lib/authoring/index.d.ts +2 -2
- package/lib/authoring/index.d.ts.map +1 -1
- package/lib/authoring/index.js +2 -1
- package/lib/authoring/index.js.map +1 -1
- package/lib/authoring/operation.d.ts +2 -0
- package/lib/authoring/operation.d.ts.map +1 -1
- package/lib/authoring/operation.js +5 -0
- package/lib/authoring/operation.js.map +1 -1
- package/lib/generators/go/operations.d.ts.map +1 -1
- package/lib/generators/go/operations.js +22 -3
- package/lib/generators/go/operations.js.map +1 -1
- package/lib/generators/php/operations.d.ts.map +1 -1
- package/lib/generators/php/operations.js +15 -3
- package/lib/generators/php/operations.js.map +1 -1
- package/lib/generators/python/operations.d.ts.map +1 -1
- package/lib/generators/python/operations.js +15 -6
- package/lib/generators/python/operations.js.map +1 -1
- package/lib/generators/typescript/operations.d.ts.map +1 -1
- package/lib/generators/typescript/operations.js +3 -3
- package/lib/generators/typescript/operations.js.map +1 -1
- package/lib/generators/typescript/runtime/send.d.ts +2 -1
- package/lib/generators/typescript/runtime/send.d.ts.map +1 -1
- package/lib/generators/typescript/runtime/send.js +25 -4
- package/lib/generators/typescript/runtime/send.js.map +1 -1
- package/lib/reserved-names.d.ts.map +1 -1
- package/lib/reserved-names.js +1 -0
- package/lib/reserved-names.js.map +1 -1
- package/lib/runtime-sources/go.d.ts +1 -1
- package/lib/runtime-sources/go.d.ts.map +1 -1
- package/lib/runtime-sources/go.js +1 -1
- package/lib/runtime-sources/go.js.map +1 -1
- package/lib/runtime-sources/php.d.ts +1 -1
- package/lib/runtime-sources/php.d.ts.map +1 -1
- package/lib/runtime-sources/php.js +1 -1
- package/lib/runtime-sources/php.js.map +1 -1
- package/lib/runtime-sources/python.d.ts +2 -2
- package/lib/runtime-sources/python.js +2 -2
- package/lib/runtime-sources/python.js.map +1 -1
- package/lib/runtime-sources/typescript.d.ts +2 -2
- package/lib/runtime-sources/typescript.js +2 -2
- package/lib/runtime-sources/typescript.js.map +1 -1
- package/package.json +2 -2
|
@@ -4,9 +4,9 @@ export const PYTHON_RUNTIME_SOURCES = {
|
|
|
4
4
|
'_auth.py': '# Auth resolution for generated Python clients — mirror of the TypeScript\n# runtime\'s auth.ts: the first OR-alternative whose schemes are all configured\n# is applied, so "bearer OR apiKey" works with either credential and never\n# sends both. Cookie-borne api keys fold into a single Cookie header.\nfrom __future__ import annotations\n\nimport base64\nfrom typing import Any, Callable, Dict, List, Tuple, Union\nfrom urllib.parse import quote\n\nTokenProvider = Union[str, Callable[[], str]]\n\n\ndef _api_keys(auth: Dict[str, Any]) -> Dict[str, Any]:\n """The apiKey credentials. `apiKey` is the documented key (it matches the scheme\n kind and the other language SDKs); `api_key` is accepted too, so a snake_case\n config keeps working."""\n return {**(auth.get("api_key") or {}), **(auth.get("apiKey") or {})}\n\ndef _resolve_token(provider: TokenProvider) -> str:\n return provider() if callable(provider) else provider\n\n\ndef _is_configured(scheme: Dict[str, Any], auth: Dict[str, Any]) -> bool:\n kind = scheme["kind"]\n if kind == "apiKey":\n return scheme["scheme"] in _api_keys(auth)\n if kind == "bearer":\n return auth.get("bearer") is not None\n return auth.get("basic") is not None\n\n\ndef resolve_auth(\n security: List[List[Dict[str, Any]]], auth: Dict[str, Any]\n) -> Tuple[Dict[str, str], Dict[str, str]]:\n """Build (headers, query) for one operation\'s security OR-alternatives from\n the client credentials. When no alternative is fully configured, the first\n alternative\'s configured schemes are still sent (the server rejects the\n request — same behavior as the TypeScript runtime)."""\n alternative = next(\n (schemes for schemes in security if all(_is_configured(s, auth) for s in schemes)),\n security[0] if security else [],\n )\n headers: Dict[str, str] = {}\n query: Dict[str, str] = {}\n cookies: List[str] = []\n for scheme in alternative:\n kind = scheme["kind"]\n if kind == "apiKey":\n provider = _api_keys(auth).get(scheme["scheme"])\n if provider is None:\n continue\n value = _resolve_token(provider)\n location = scheme.get("in", "header")\n if location == "header":\n headers[scheme["name"]] = value\n elif location == "query":\n query[scheme["name"]] = value\n else:\n # Reserved characters (`;`, `=`, space) must not break Cookie syntax.\n cookies.append(f"{scheme[\'name\']}={quote(value, safe=\'\')}")\n elif kind == "bearer":\n provider = auth.get("bearer")\n if provider is not None:\n headers["Authorization"] = f"Bearer {_resolve_token(provider)}"\n else:\n basic = auth.get("basic")\n if basic is not None:\n username, password = basic["username"], basic["password"]\n token = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("ascii")\n headers["Authorization"] = f"Basic {token}"\n if cookies:\n headers["Cookie"] = "; ".join(cookies)\n return headers, query\n',
|
|
5
5
|
'_url.py': '# URL assembly for generated Python clients — path-parameter substitution with\n# percent-encoding, mirroring the TypeScript runtime\'s url.ts semantics.\nfrom __future__ import annotations\n\nfrom typing import Any, Dict\nfrom urllib.parse import quote\n\n\ndef build_url(server_url: str, path: str, path_params: Dict[str, Any]) -> str:\n filled = path\n for name, value in path_params.items():\n filled = filled.replace("{" + name + "}", quote(str(value), safe=""))\n return server_url.rstrip("/") + filled\n',
|
|
6
6
|
'_decode.py': '# Reflective JSON <-> model conversion for generated Python clients. Models are\n# plain dataclasses by default, or pydantic BaseModels under `models: pydantic`;\n# one decoder serves both. For a dataclass it hydrates parsed JSON reflectively,\n# honoring each class\'s `_field_map` (python name -> wire name) and the typing\n# constructs the generator emits: Optional/Union, List, Dict, Enum, Literal, Any.\n# For a pydantic model it defers to pydantic, which already knows the aliases.\n# encode() mirrors whichever it was given back to wire shape.\nfrom __future__ import annotations\n\nimport dataclasses\nimport typing\nfrom datetime import date, datetime\nfrom enum import Enum\nfrom typing import Any, Dict, Tuple, get_args, get_origin, get_type_hints\n\n# Discriminated unions: resolved Union annotation -> (wire property, {value: class}).\n# The generated module registers its unions here; decode() dispatches through it\n# before falling back to trying members in order.\nDISCRIMINATORS: Dict[Any, Tuple[str, Dict[str, Any]]] = {}\n\n\ndef decode(type_: Any, data: Any):\n """Best-effort hydration: wire data -> the annotated Python shape. Unknown or\n mismatched shapes pass through unchanged (the server is the source of truth)."""\n if data is None or type_ is Any or type_ is None:\n return data\n # `Annotated[Union[...], Field(discriminator=...)]`: pydantic reads that annotation on a\n # model\'s own field, so here only the union underneath matters.\n if hasattr(type_, "__metadata__"):\n type_ = get_args(type_)[0]\n origin = get_origin(type_)\n if origin is typing.Union:\n discriminator = DISCRIMINATORS.get(type_)\n if discriminator is not None and isinstance(data, dict):\n wire_property, mapping = discriminator\n target = mapping.get(data.get(wire_property))\n if target is not None:\n try:\n return decode(target, data)\n except (TypeError, ValueError, KeyError):\n pass\n for member in get_args(type_):\n if member is type(None):\n continue\n try:\n return decode(member, data)\n except (TypeError, ValueError, KeyError):\n continue\n return data\n if origin is list:\n (item_type,) = get_args(type_) or (Any,)\n return [decode(item_type, item) for item in data]\n if origin is dict:\n args = get_args(type_)\n value_type = args[1] if len(args) == 2 else Any\n return {key: decode(value_type, value) for key, value in data.items()}\n if origin is typing.Literal:\n return data\n if isinstance(type_, type) and issubclass(type_, Enum):\n return type_(data)\n # `dateType: Date` annotates date/date-time fields as datetime objects; a value that\n # doesn\'t parse passes through unchanged (the server is the source of truth).\n if type_ is datetime or type_ is date:\n if not isinstance(data, str):\n return data\n try:\n # `datetime` accepts a bare date too; `date` rejects a timestamp, so trim it.\n return (\n datetime.fromisoformat(data)\n if type_ is datetime\n else date.fromisoformat(data[:10])\n )\n except ValueError:\n return data\n # A pydantic model validates itself, aliases included. `ValidationError`\n # subclasses `ValueError`, so union member probing above still works.\n if isinstance(type_, type) and hasattr(type_, "model_validate"):\n return type_.model_validate(data)\n if dataclasses.is_dataclass(type_):\n hints = get_type_hints(type_)\n field_map = getattr(type_, "_field_map", {})\n kwargs = {}\n for field in dataclasses.fields(type_):\n wire = field_map.get(field.name, field.name)\n if isinstance(data, dict) and wire in data:\n kwargs[field.name] = decode(hints.get(field.name, Any), data[wire])\n return type_(**kwargs)\n return data\n\n\ndef encode(value: Any):\n """Python shape -> wire (JSON) shape; inverse of decode for request bodies."""\n # `mode="json"` resolves datetimes and enums the same way the branches below do,\n # and `exclude_none` matches the dataclass path: an unset optional is not sent.\n if hasattr(value, "model_dump") and not isinstance(value, type):\n return value.model_dump(by_alias=True, exclude_none=True, mode="json")\n if dataclasses.is_dataclass(value) and not isinstance(value, type):\n field_map = getattr(type(value), "_field_map", {})\n out = {}\n for field in dataclasses.fields(value):\n item = getattr(value, field.name)\n if item is None:\n continue\n out[field_map.get(field.name, field.name)] = encode(item)\n return out\n if isinstance(value, Enum):\n return value.value\n # A date-only value must not gain a time component on the way out.\n if isinstance(value, datetime):\n return value.isoformat()\n if isinstance(value, date):\n return value.isoformat()\n if isinstance(value, list):\n return [encode(item) for item in value]\n if isinstance(value, dict):\n return {key: encode(item) for key, item in value.items()}\n return value\n',
|
|
7
|
-
'_send.py': '# The request core for generated Python clients — mirror of the TypeScript\n# runtime\'s send.ts: default + config + per-call headers, on_request middleware\n# BEFORE serialization (mutations are sent), the retry loop (idempotent-methods\n# default, Idempotency-Key opt-in makes POST/PATCH safe, Retry-After honored,\n# exponential backoff with full jitter, a fresh timeout budget per attempt), and\n# the reverse on_response onion.\nfrom __future__ import annotations\n\nimport asyncio\nimport random\nimport time\nimport uuid\nfrom dataclasses import dataclass\nfrom typing import Any, Dict, Generic, List, Optional, Tuple, TypeVar\n\nimport httpx\n\nfrom ._errors import ApiTimeoutError\n\nT = TypeVar("T")\n\n\n@dataclass\nclass Envelope(Generic[T]):\n """A *_with_headers() result: decoded body + coerced declared headers + raw response."""\n\n data: T\n headers: Dict[str, Any]\n response: httpx.Response\n\n\ndef read_envelope_headers(\n response: httpx.Response, specs: List[Tuple[str, str, str]]\n) -> Dict[str, Any]:\n """Coerce declared response headers per (name, key, type) specs; absent/unparsable omitted."""\n headers: Dict[str, Any] = {}\n for name, key, type_ in specs:\n raw = response.headers.get(name)\n if raw is None:\n continue\n if type_ in ("integer", "number"):\n try:\n headers[key] = int(raw) if type_ == "integer" else float(raw)\n except ValueError:\n pass\n elif type_ == "boolean":\n lower = raw.strip().lower()\n if lower in ("true", "false"):\n headers[key] = lower == "true"\n else:\n headers[key] = raw\n return headers\n\n\n_IDEMPOTENT_METHODS = {"GET", "HEAD", "PUT", "DELETE", "OPTIONS"}\n_TRANSIENT_STATUS = {408, 429, 500, 502, 503, 504}\n\n\ndef _default_retry_on(method: str, headers: Dict[str, str], response: Optional[httpx.Response]) -> bool:\n safe = method.upper() in _IDEMPOTENT_METHODS or "Idempotency-Key" in headers\n if not safe:\n return False\n return response is None or response.status_code in _TRANSIENT_STATUS\n\n\ndef _retry_delay(retry: Dict[str, Any], attempt: int, retry_after: Optional[str]) -> float:\n if retry_after:\n try:\n return float(retry_after)\n except ValueError:\n pass # HTTP-date form: fall through to backoff\n base = float(retry.get("retry_delay", 1.0))\n raw = base if retry.get("retry_strategy") == "fixed" else base * (2 ** (attempt - 1))\n return random.uniform(0, raw) if retry.get("jitter", True) is not False else raw\n\n\ndef send(\n client: httpx.Client,\n config: Dict[str, Any],\n op: Dict[str, Any],\n url: str,\n *,\n method: str,\n headers: Optional[Dict[str, str]] = None,\n params: Optional[Dict[str, Any]] = None,\n json_body: Any = None,\n content: Any = None,\n data: Any = None,\n files: Any = None,\n timeout: Optional[float] = None,\n idempotency_key: Any = None,\n retry: Optional[Dict[str, Any]] = None,\n) -> httpx.Response:\n merged_retry: Dict[str, Any] = {**(config.get("retry") or {}), **(retry or {})}\n effective_timeout = timeout if timeout is not None else config.get("timeout")\n merged_headers: Dict[str, str] = {**(config.get("headers") or {}), **(headers or {})}\n\n # One stable key per LOGICAL call — set before the retry loop so every\n # attempt re-sends the same key; a caller-provided header always wins.\n key = idempotency_key if idempotency_key is not None else config.get("idempotency_key")\n if (\n key not in (None, False)\n and method.upper() in ("POST", "PATCH")\n and "Idempotency-Key" not in merged_headers\n ):\n merged_headers["Idempotency-Key"] = (\n key if isinstance(key, str) else key() if callable(key) else str(uuid.uuid4())\n )\n\n context = {\n "url": url,\n "method": method.upper(),\n "headers": merged_headers,\n "body": json_body,\n "operation": op,\n }\n middleware: List[Any] = config.get("middleware") or []\n for mw in middleware:\n on_request = getattr(mw, "on_request", None) or (mw.get("on_request") if isinstance(mw, dict) else None)\n if on_request:\n on_request(context)\n\n max_attempts = 1 + int(merged_retry.get("retries", 0))\n retry_on = merged_retry.get("retry_on") or (\n lambda ctx: _default_retry_on(context["method"], context["headers"], ctx.get("response"))\n )\n\n attempt = 0\n while True:\n attempt += 1\n try:\n response = client.request(\n context["method"],\n context["url"],\n headers=context["headers"],\n params=params,\n json=context["body"] if content is None and files is None and data is None else None,\n content=content,\n data=data,\n files=files,\n timeout=effective_timeout if effective_timeout is not None else httpx.USE_CLIENT_DEFAULT,\n )\n except httpx.TimeoutException:\n if attempt < max_attempts and retry_on({"attempt": attempt, "response": None}):\n time.sleep(_retry_delay(merged_retry, attempt, None))\n continue\n raise ApiTimeoutError(op.get("id", "?"), float(effective_timeout or 0), attempt) from None\n except httpx.TransportError:\n if attempt < max_attempts and retry_on({"attempt": attempt, "response": None}):\n time.sleep(_retry_delay(merged_retry, attempt, None))\n continue\n raise\n\n for mw in reversed(middleware):\n on_response = getattr(mw, "on_response", None) or (mw.get("on_response") if isinstance(mw, dict) else None)\n if on_response:\n replaced = on_response(response, context)\n if replaced is not None:\n response = replaced\n\n if (\n not response.is_success\n and attempt < max_attempts\n and retry_on({"attempt": attempt, "response": response})\n ):\n time.sleep(_retry_delay(merged_retry, attempt, response.headers.get("retry-after")))\n continue\n return response\n\n\nasync def send_async(\n client: httpx.AsyncClient,\n config: Dict[str, Any],\n op: Dict[str, Any],\n url: str,\n *,\n method: str,\n headers: Optional[Dict[str, str]] = None,\n params: Optional[Dict[str, Any]] = None,\n json_body: Any = None,\n content: Any = None,\n data: Any = None,\n files: Any = None,\n timeout: Optional[float] = None,\n idempotency_key: Any = None,\n retry: Optional[Dict[str, Any]] = None,\n) -> httpx.Response:\n """The async mirror of send() — same retry/timeout/idempotency semantics."""\n merged_retry: Dict[str, Any] = {**(config.get("retry") or {}), **(retry or {})}\n effective_timeout = timeout if timeout is not None else config.get("timeout")\n merged_headers: Dict[str, str] = {**(config.get("headers") or {}), **(headers or {})}\n key = idempotency_key if idempotency_key is not None else config.get("idempotency_key")\n if (\n key not in (None, False)\n and method.upper() in ("POST", "PATCH")\n and "Idempotency-Key" not in merged_headers\n ):\n merged_headers["Idempotency-Key"] = (\n key if isinstance(key, str) else key() if callable(key) else str(uuid.uuid4())\n )\n context = {\n "url": url,\n "method": method.upper(),\n "headers": merged_headers,\n "body": json_body,\n "operation": op,\n }\n middleware: List[Any] = config.get("middleware") or []\n for mw in middleware:\n on_request = getattr(mw, "on_request", None) or (mw.get("on_request") if isinstance(mw, dict) else None)\n if on_request:\n on_request(context)\n max_attempts = 1 + int(merged_retry.get("retries", 0))\n retry_on = merged_retry.get("retry_on") or (\n lambda ctx: _default_retry_on(context["method"], context["headers"], ctx.get("response"))\n )\n attempt = 0\n while True:\n attempt += 1\n try:\n response = await client.request(\n context["method"],\n context["url"],\n headers=context["headers"],\n params=params,\n json=context["body"] if content is None and files is None and data is None else None,\n content=content,\n data=data,\n files=files,\n timeout=effective_timeout if effective_timeout is not None else httpx.USE_CLIENT_DEFAULT,\n )\n except httpx.TimeoutException:\n if attempt < max_attempts and retry_on({"attempt": attempt, "response": None}):\n await asyncio.sleep(_retry_delay(merged_retry, attempt, None))\n continue\n raise ApiTimeoutError(op.get("id", "?"), float(effective_timeout or 0), attempt) from None\n except httpx.TransportError:\n if attempt < max_attempts and retry_on({"attempt": attempt, "response": None}):\n await asyncio.sleep(_retry_delay(merged_retry, attempt, None))\n continue\n raise\n for mw in reversed(middleware):\n on_response = getattr(mw, "on_response", None) or (mw.get("on_response") if isinstance(mw, dict) else None)\n if on_response:\n replaced = on_response(response, context)\n if replaced is not None:\n response = replaced\n if (\n not response.is_success\n and attempt < max_attempts\n and retry_on({"attempt": attempt, "response": response})\n ):\n await asyncio.sleep(_retry_delay(merged_retry, attempt, response.headers.get("retry-after")))\n continue\n return response\n',
|
|
7
|
+
'_send.py': '# The request core for generated Python clients — mirror of the TypeScript\n# runtime\'s send.ts: default + config + per-call headers, on_request middleware\n# BEFORE serialization (mutations are sent), the retry loop (idempotent-methods\n# default, Idempotency-Key opt-in makes POST/PATCH safe, Retry-After honored,\n# exponential backoff with full jitter, a fresh timeout budget per attempt), and\n# the reverse on_response onion.\nfrom __future__ import annotations\n\nimport asyncio\nimport random\nimport time\nimport uuid\nfrom dataclasses import dataclass\nfrom typing import Any, Dict, Generic, List, Optional, Tuple, TypeVar\n\nimport httpx\n\nfrom ._errors import ApiTimeoutError\n\nT = TypeVar("T")\n\n\n@dataclass\nclass Envelope(Generic[T]):\n """A *_with_headers() result: decoded body + coerced declared headers + raw response."""\n\n data: T\n headers: Dict[str, Any]\n response: httpx.Response\n\n\ndef read_envelope_headers(\n response: httpx.Response, specs: List[Tuple[str, str, str]]\n) -> Dict[str, Any]:\n """Coerce declared response headers per (name, key, type) specs; absent/unparsable omitted."""\n headers: Dict[str, Any] = {}\n for name, key, type_ in specs:\n raw = response.headers.get(name)\n if raw is None:\n continue\n if type_ in ("integer", "number"):\n try:\n headers[key] = int(raw) if type_ == "integer" else float(raw)\n except ValueError:\n pass\n elif type_ == "boolean":\n lower = raw.strip().lower()\n if lower in ("true", "false"):\n headers[key] = lower == "true"\n else:\n headers[key] = raw\n return headers\n\n\n_IDEMPOTENT_METHODS = {"GET", "HEAD", "PUT", "DELETE", "OPTIONS"}\n_TRANSIENT_STATUS = {408, 429, 500, 502, 503, 504}\n\n\ndef _default_retry_on(method: str, headers: Dict[str, str], response: Optional[httpx.Response]) -> bool:\n safe = method.upper() in _IDEMPOTENT_METHODS or "Idempotency-Key" in headers\n if not safe:\n return False\n return response is None or response.status_code in _TRANSIENT_STATUS\n\n\ndef _retry_delay(retry: Dict[str, Any], attempt: int, retry_after: Optional[str]) -> float:\n if retry_after:\n try:\n return float(retry_after)\n except ValueError:\n pass # HTTP-date form: fall through to backoff\n base = float(retry.get("retry_delay", 1.0))\n raw = base if retry.get("retry_strategy") == "fixed" else base * (2 ** (attempt - 1))\n return random.uniform(0, raw) if retry.get("jitter", True) is not False else raw\n\n\ndef send(\n client: httpx.Client,\n config: Dict[str, Any],\n op: Dict[str, Any],\n url: str,\n *,\n method: str,\n headers: Optional[Dict[str, str]] = None,\n params: Optional[Dict[str, Any]] = None,\n json_body: Any = None,\n content: Any = None,\n content_type: Optional[str] = None,\n data: Any = None,\n files: Any = None,\n timeout: Optional[float] = None,\n idempotency_key: Any = None,\n retry: Optional[Dict[str, Any]] = None,\n) -> httpx.Response:\n merged_retry: Dict[str, Any] = {**(config.get("retry") or {}), **(retry or {})}\n effective_timeout = timeout if timeout is not None else config.get("timeout")\n merged_headers: Dict[str, str] = {**(config.get("headers") or {}), **(headers or {})}\n\n # One stable key per LOGICAL call — set before the retry loop so every\n # attempt re-sends the same key; a caller-provided header always wins.\n key = idempotency_key if idempotency_key is not None else config.get("idempotency_key")\n if (\n key not in (None, False)\n and method.upper() in ("POST", "PATCH")\n and "Idempotency-Key" not in merged_headers\n ):\n merged_headers["Idempotency-Key"] = (\n key if isinstance(key, str) else key() if callable(key) else str(uuid.uuid4())\n )\n # The declared content type fills the gap; a caller-provided header always wins.\n if content_type is not None and not any(name.lower() == "content-type" for name in merged_headers):\n merged_headers["Content-Type"] = content_type\n\n context = {\n "url": url,\n "method": method.upper(),\n "headers": merged_headers,\n "body": json_body,\n "operation": op,\n }\n middleware: List[Any] = config.get("middleware") or []\n for mw in middleware:\n on_request = getattr(mw, "on_request", None) or (mw.get("on_request") if isinstance(mw, dict) else None)\n if on_request:\n on_request(context)\n\n # A file-like object or iterator is consumed by the first attempt and cannot be replayed, so it never retries.\n replayable = content is None or isinstance(content, (bytes, bytearray, str))\n max_attempts = 1 + int(merged_retry.get("retries", 0)) if replayable else 1\n retry_on = merged_retry.get("retry_on") or (\n lambda ctx: _default_retry_on(context["method"], context["headers"], ctx.get("response"))\n )\n\n attempt = 0\n while True:\n attempt += 1\n try:\n response = client.request(\n context["method"],\n context["url"],\n headers=context["headers"],\n params=params,\n json=context["body"] if content is None and files is None and data is None else None,\n content=content,\n data=data,\n files=files,\n timeout=effective_timeout if effective_timeout is not None else httpx.USE_CLIENT_DEFAULT,\n )\n except httpx.TimeoutException:\n if attempt < max_attempts and retry_on({"attempt": attempt, "response": None}):\n time.sleep(_retry_delay(merged_retry, attempt, None))\n continue\n raise ApiTimeoutError(op.get("id", "?"), float(effective_timeout or 0), attempt) from None\n except httpx.TransportError:\n if attempt < max_attempts and retry_on({"attempt": attempt, "response": None}):\n time.sleep(_retry_delay(merged_retry, attempt, None))\n continue\n raise\n\n for mw in reversed(middleware):\n on_response = getattr(mw, "on_response", None) or (mw.get("on_response") if isinstance(mw, dict) else None)\n if on_response:\n replaced = on_response(response, context)\n if replaced is not None:\n response = replaced\n\n if (\n not response.is_success\n and attempt < max_attempts\n and retry_on({"attempt": attempt, "response": response})\n ):\n time.sleep(_retry_delay(merged_retry, attempt, response.headers.get("retry-after")))\n continue\n return response\n\n\nasync def send_async(\n client: httpx.AsyncClient,\n config: Dict[str, Any],\n op: Dict[str, Any],\n url: str,\n *,\n method: str,\n headers: Optional[Dict[str, str]] = None,\n params: Optional[Dict[str, Any]] = None,\n json_body: Any = None,\n content: Any = None,\n content_type: Optional[str] = None,\n data: Any = None,\n files: Any = None,\n timeout: Optional[float] = None,\n idempotency_key: Any = None,\n retry: Optional[Dict[str, Any]] = None,\n) -> httpx.Response:\n """The async mirror of send() — same retry/timeout/idempotency semantics."""\n merged_retry: Dict[str, Any] = {**(config.get("retry") or {}), **(retry or {})}\n effective_timeout = timeout if timeout is not None else config.get("timeout")\n merged_headers: Dict[str, str] = {**(config.get("headers") or {}), **(headers or {})}\n key = idempotency_key if idempotency_key is not None else config.get("idempotency_key")\n if (\n key not in (None, False)\n and method.upper() in ("POST", "PATCH")\n and "Idempotency-Key" not in merged_headers\n ):\n merged_headers["Idempotency-Key"] = (\n key if isinstance(key, str) else key() if callable(key) else str(uuid.uuid4())\n )\n if content_type is not None and not any(name.lower() == "content-type" for name in merged_headers):\n merged_headers["Content-Type"] = content_type\n context = {\n "url": url,\n "method": method.upper(),\n "headers": merged_headers,\n "body": json_body,\n "operation": op,\n }\n middleware: List[Any] = config.get("middleware") or []\n for mw in middleware:\n on_request = getattr(mw, "on_request", None) or (mw.get("on_request") if isinstance(mw, dict) else None)\n if on_request:\n on_request(context)\n # A file-like object or iterator is consumed by the first attempt and cannot be replayed, so it never retries.\n replayable = content is None or isinstance(content, (bytes, bytearray, str))\n max_attempts = 1 + int(merged_retry.get("retries", 0)) if replayable else 1\n retry_on = merged_retry.get("retry_on") or (\n lambda ctx: _default_retry_on(context["method"], context["headers"], ctx.get("response"))\n )\n attempt = 0\n while True:\n attempt += 1\n try:\n response = await client.request(\n context["method"],\n context["url"],\n headers=context["headers"],\n params=params,\n json=context["body"] if content is None and files is None and data is None else None,\n content=content,\n data=data,\n files=files,\n timeout=effective_timeout if effective_timeout is not None else httpx.USE_CLIENT_DEFAULT,\n )\n except httpx.TimeoutException:\n if attempt < max_attempts and retry_on({"attempt": attempt, "response": None}):\n await asyncio.sleep(_retry_delay(merged_retry, attempt, None))\n continue\n raise ApiTimeoutError(op.get("id", "?"), float(effective_timeout or 0), attempt) from None\n except httpx.TransportError:\n if attempt < max_attempts and retry_on({"attempt": attempt, "response": None}):\n await asyncio.sleep(_retry_delay(merged_retry, attempt, None))\n continue\n raise\n for mw in reversed(middleware):\n on_response = getattr(mw, "on_response", None) or (mw.get("on_response") if isinstance(mw, dict) else None)\n if on_response:\n replaced = on_response(response, context)\n if replaced is not None:\n response = replaced\n if (\n not response.is_success\n and attempt < max_attempts\n and retry_on({"attempt": attempt, "response": response})\n ):\n await asyncio.sleep(_retry_delay(merged_retry, attempt, response.headers.get("retry-after")))\n continue\n return response\n',
|
|
8
8
|
'_paginate.py': '# Auto-pagination iterators for generated Python clients — the TypeScript\n# runtime\'s paginate.ts semantics ported: cursor (next-cursor pointer, optional\n# has-more flag, repeated-cursor guard), offset/page (advance by count/one,\n# repeated-page guard, null start treated as absent), and link (RFC 8288\n# `Link: rel="next"` following with relative resolution and a loop guard).\nfrom __future__ import annotations\n\nimport re\nfrom typing import Any, AsyncIterator, Awaitable, Callable, Dict, Iterator, Optional, Tuple\nfrom urllib.parse import parse_qsl, urljoin, urlparse\n\n# call(params) -> (parsed_json, httpx.Response)\nPageCall = Callable[[Dict[str, Any]], Tuple[Any, Any]]\n\n\ndef resolve_pointer(data: Any, pointer: str) -> Any:\n """RFC 6901 JSON pointer over parsed JSON; None on any miss."""\n if pointer == "":\n return data\n if not pointer.startswith("/"):\n return None\n current = data\n for token in pointer[1:].split("/"):\n key = token.replace("~1", "/").replace("~0", "~")\n if isinstance(current, dict):\n current = current.get(key)\n elif isinstance(current, list) and key.isdigit():\n index = int(key)\n current = current[index] if index < len(current) else None\n else:\n return None\n if current is None:\n return None\n return current\n\n\ndef iter_pages(call: PageCall, spec: Dict[str, Any], params: Optional[Dict[str, Any]] = None) -> Iterator[Any]:\n """Yield raw page JSON per the pagination spec; every page is yielded before\n the stop condition is evaluated, so the last page always arrives."""\n style = spec["style"]\n base = dict(params or {})\n if style == "cursor":\n cursor = base.get(spec["param"])\n while True:\n page_params = dict(base)\n if cursor is not None:\n page_params[spec["param"]] = cursor\n page, _response = call(page_params)\n yield page\n if spec.get("has_more") is not None and resolve_pointer(page, spec["has_more"]) is False:\n return\n nxt = resolve_pointer(page, spec.get("next_cursor", ""))\n if nxt is None or nxt == "":\n return\n if not isinstance(nxt, (str, int, float)):\n raise ValueError(f"Pagination cursor at {spec[\'next_cursor\']} is not a string or number")\n if nxt == cursor:\n raise ValueError("Pagination did not advance: the operation returned the same cursor twice")\n cursor = nxt\n elif style == "link":\n yield from _iter_pages_by_link(call, base)\n else: # offset / page\n start = base.get(spec["param"])\n fallback = 1 if style == "page" else 0\n try:\n position = fallback if start in (None, "") else int(start)\n except (TypeError, ValueError):\n position = fallback\n previous_items = None\n while True:\n page, _response = call({**base, spec["param"]: position})\n items = resolve_pointer(page, spec.get("items", ""))\n serialized = repr(items) if isinstance(items, list) else None\n if serialized is not None and serialized == previous_items:\n raise ValueError("Pagination did not advance: the operation returned the same page twice")\n yield page\n if not isinstance(items, list) or len(items) == 0:\n return\n previous_items = serialized\n position += 1 if style == "page" else len(items)\n\n\ndef _link_next(header: Optional[str]) -> Optional[str]:\n if not header:\n return None\n for entry in re.split(r",\\s*(?=<)", header):\n match = re.match(r"^\\s*<([^>]*)>(.*)$", entry)\n if not match:\n continue\n rel = re.search(r\';\\s*rel\\s*=\\s*"?([^";]+)"?\', match.group(2), re.IGNORECASE)\n if rel and "next" in rel.group(1).split():\n return match.group(1)\n return None\n\n\ndef _iter_pages_by_link(call: PageCall, base: Dict[str, Any]) -> Iterator[Any]:\n params = dict(base)\n previous = None\n while True:\n page, response = call(params)\n yield page\n target = _link_next(response.headers.get("link"))\n if target is None:\n return\n page_url = str(response.request.url) if response.request is not None else ""\n nxt = urljoin(page_url or "http://relative.invalid", target)\n if nxt in (previous, page_url):\n raise ValueError(\'Pagination did not advance: the Link rel="next" target repeats\')\n previous = nxt\n link_params: Dict[str, Any] = {}\n for key, value in parse_qsl(urlparse(nxt).query):\n if key in link_params:\n existing = link_params[key]\n link_params[key] = [*existing, value] if isinstance(existing, list) else [existing, value]\n else:\n link_params[key] = value\n params = {**base, **link_params}\n\n\ndef iter_items(call: PageCall, spec: Dict[str, Any], params: Optional[Dict[str, Any]] = None) -> Iterator[Any]:\n """Each page\'s `items` pointer, flattened."""\n for page in iter_pages(call, spec, params):\n items = resolve_pointer(page, spec.get("items", ""))\n if isinstance(items, list):\n yield from items\n\n\n# call(params) -> awaitable of (parsed_json, httpx.Response)\nAsyncPageCall = Callable[[Dict[str, Any]], Awaitable[Tuple[Any, Any]]]\n\n\nasync def aiter_pages(\n call: AsyncPageCall, spec: Dict[str, Any], params: Optional[Dict[str, Any]] = None\n) -> AsyncIterator[Any]:\n """Async mirror of iter_pages — same stop conditions and guards."""\n style = spec["style"]\n base = dict(params or {})\n if style == "cursor":\n cursor = base.get(spec["param"])\n while True:\n page_params = dict(base)\n if cursor is not None:\n page_params[spec["param"]] = cursor\n page, _response = await call(page_params)\n yield page\n if spec.get("has_more") is not None and resolve_pointer(page, spec["has_more"]) is False:\n return\n nxt = resolve_pointer(page, spec.get("next_cursor", ""))\n if nxt is None or nxt == "":\n return\n if not isinstance(nxt, (str, int, float)):\n raise ValueError(f"Pagination cursor at {spec[\'next_cursor\']} is not a string or number")\n if nxt == cursor:\n raise ValueError("Pagination did not advance: the operation returned the same cursor twice")\n cursor = nxt\n elif style == "link":\n previous = None\n link_params: Dict[str, Any] = dict(base)\n while True:\n page, response = await call(link_params)\n yield page\n target = _link_next(response.headers.get("link"))\n if target is None:\n return\n page_url = str(response.request.url) if response.request is not None else ""\n nxt = urljoin(page_url or "http://relative.invalid", target)\n if nxt in (previous, page_url):\n raise ValueError(\'Pagination did not advance: the Link rel="next" target repeats\')\n previous = nxt\n merged: Dict[str, Any] = {}\n for key, value in parse_qsl(urlparse(nxt).query):\n if key in merged:\n existing = merged[key]\n merged[key] = [*existing, value] if isinstance(existing, list) else [existing, value]\n else:\n merged[key] = value\n link_params = {**base, **merged}\n else:\n start = base.get(spec["param"])\n fallback = 1 if style == "page" else 0\n try:\n position = fallback if start in (None, "") else int(start)\n except (TypeError, ValueError):\n position = fallback\n previous_items = None\n while True:\n page, _response = await call({**base, spec["param"]: position})\n items = resolve_pointer(page, spec.get("items", ""))\n serialized = repr(items) if isinstance(items, list) else None\n if serialized is not None and serialized == previous_items:\n raise ValueError("Pagination did not advance: the operation returned the same page twice")\n yield page\n if not isinstance(items, list) or len(items) == 0:\n return\n previous_items = serialized\n position += 1 if style == "page" else len(items)\n\n\nasync def aiter_items(\n call: AsyncPageCall, spec: Dict[str, Any], params: Optional[Dict[str, Any]] = None\n) -> AsyncIterator[Any]:\n async for page in aiter_pages(call, spec, params):\n items = resolve_pointer(page, spec.get("items", ""))\n if isinstance(items, list):\n for item in items:\n yield item\n',
|
|
9
9
|
'_sse.py': '# Server-Sent Events for generated Python clients — the TypeScript runtime\'s\n# sse.ts semantics ported: frame parsing per the EventSource spec (retry must be\n# ASCII digits; comment-only frames skipped; multi-line data joined with \\n) and\n# auto-reconnect resuming from the last event id via Last-Event-ID, with\n# exponential backoff capped at 30s. JSON payloads are parsed when the operation\n# declares a JSON event stream.\nfrom __future__ import annotations\n\nimport asyncio\nimport json\nimport random\nimport time\nfrom dataclasses import dataclass\nfrom typing import Any, AsyncIterator, Callable, Dict, Iterator, Optional\n\nimport httpx\n\n_FRAME_DELIMITER = "\\n\\n"\n\n\n@dataclass\nclass ServerSentEvent:\n data: Any\n event: Optional[str] = None\n id: Optional[str] = None\n retry: Optional[int] = None\n\n\ndef parse_sse_frame(raw: str, data_kind: str = "text") -> Optional[ServerSentEvent]:\n event = None\n data_lines = []\n event_id = None\n retry = None\n saw_field = False\n for line in raw.replace("\\r\\n", "\\n").replace("\\r", "\\n").split("\\n"):\n if line == "" or line.startswith(":"):\n continue\n field, _, value = line.partition(":")\n if value.startswith(" "):\n value = value[1:]\n saw_field = True\n if field == "event":\n event = value\n elif field == "data":\n data_lines.append(value)\n elif field == "id":\n event_id = value\n elif field == "retry" and value.isdigit():\n retry = int(value)\n if not saw_field:\n return None\n text = "\\n".join(data_lines)\n data: Any = text\n if data_kind == "json" and text != "":\n data = json.loads(text)\n return ServerSentEvent(data=data, event=event, id=event_id, retry=retry)\n\n\ndef iter_sse(\n open_stream: Callable[[Dict[str, str]], Any],\n data_kind: str = "text",\n reconnect: bool = True,\n reconnect_delay: float = 1.0,\n) -> Iterator[ServerSentEvent]:\n """Iterate an event stream. `open_stream(extra_headers)` must return an\n httpx streaming-response context manager; it is reopened on dropped\n connections with Last-Event-ID set (fresh call = fresh auth)."""\n last_event_id: Optional[str] = None\n server_retry: Optional[float] = None\n failures = 0\n while True:\n headers = {"Accept": "text/event-stream"}\n if last_event_id is not None:\n headers["Last-Event-ID"] = last_event_id\n try:\n with open_stream(headers) as response:\n if response.status_code >= 400:\n response.read()\n raise httpx.HTTPStatusError(\n f"SSE request failed with status {response.status_code}",\n request=response.request,\n response=response,\n )\n failures = 0\n buffer = ""\n for chunk in response.iter_text():\n buffer += chunk\n while _FRAME_DELIMITER in buffer:\n raw, buffer = buffer.split(_FRAME_DELIMITER, 1)\n parsed = parse_sse_frame(raw, data_kind)\n if parsed is not None:\n if parsed.id is not None:\n last_event_id = parsed.id\n if parsed.retry is not None:\n server_retry = parsed.retry / 1000\n yield parsed\n # Clean end: flush a trailing frame, then finish (no reconnect).\n if buffer.strip():\n parsed = parse_sse_frame(buffer, data_kind)\n if parsed is not None:\n yield parsed\n return\n except httpx.HTTPStatusError:\n raise # a 4xx/5xx is definitive, not a dropped connection\n except (httpx.TransportError, httpx.TimeoutException):\n if not reconnect:\n raise\n failures += 1\n base = server_retry if server_retry is not None else reconnect_delay\n time.sleep(random.uniform(0, min(base * (2 ** (failures - 1)), 30.0)))\n\n\nasync def aiter_sse(\n open_stream: Callable[[Dict[str, str]], Any],\n data_kind: str = "text",\n reconnect: bool = True,\n reconnect_delay: float = 1.0,\n) -> AsyncIterator[ServerSentEvent]:\n """Async mirror of iter_sse; `open_stream` returns an async context manager."""\n last_event_id: Optional[str] = None\n server_retry: Optional[float] = None\n failures = 0\n while True:\n headers = {"Accept": "text/event-stream"}\n if last_event_id is not None:\n headers["Last-Event-ID"] = last_event_id\n try:\n async with open_stream(headers) as response:\n if response.status_code >= 400:\n await response.aread()\n raise httpx.HTTPStatusError(\n f"SSE request failed with status {response.status_code}",\n request=response.request,\n response=response,\n )\n failures = 0\n buffer = ""\n async for chunk in response.aiter_text():\n buffer += chunk\n while _FRAME_DELIMITER in buffer:\n raw, buffer = buffer.split(_FRAME_DELIMITER, 1)\n parsed = parse_sse_frame(raw, data_kind)\n if parsed is not None:\n if parsed.id is not None:\n last_event_id = parsed.id\n if parsed.retry is not None:\n server_retry = parsed.retry / 1000\n yield parsed\n if buffer.strip():\n parsed = parse_sse_frame(buffer, data_kind)\n if parsed is not None:\n yield parsed\n return\n except httpx.HTTPStatusError:\n raise\n except (httpx.TransportError, httpx.TimeoutException):\n if not reconnect:\n raise\n failures += 1\n base = server_retry if server_retry is not None else reconnect_delay\n await asyncio.sleep(random.uniform(0, min(base * (2 ** (failures - 1)), 30.0)))\n',
|
|
10
|
-
'_multipart.py': '# Multipart bodies for generated Python clients — a typed dict/dataclass body is\n# split into httpx\'s (data, files): bytes and file-like values upload as parts,\n# everything else is form data (nested values JSON-encoded, mirroring the\n# TypeScript runtime\'s FormData serialization).\nfrom __future__ import annotations\n\nimport json\nfrom typing import Any, Dict, Tuple\n\nfrom ._decode import encode\n\n\ndef to_multipart(body: Any) -> Tuple[Dict[str, Any], Dict[str, Any]]:\n wire = encode(body)\n data: Dict[str, Any] = {}\n files: Dict[str, Any] = {}\n for key, value in (wire or {}).items():\n if isinstance(value, (bytes, bytearray)) or hasattr(value, "read"):\n files[key] = value\n elif isinstance(value, (dict, list)):\n data[key] = json.dumps(value)\n else:\n data[key] = value\n return data, files\n',
|
|
10
|
+
'_multipart.py': '# Multipart bodies for generated Python clients — a typed dict/dataclass body is\n# split into httpx\'s (data, files): bytes and file-like values upload as parts,\n# everything else is form data (nested values JSON-encoded, mirroring the\n# TypeScript runtime\'s FormData serialization). A body that is already bytes, a\n# `str`, a file-like object, or an (async) iterator of bytes passes through as httpx\n# `content=` untouched; the caller sets its Content-Type.\nfrom __future__ import annotations\n\nimport json\nfrom typing import Any, Dict, Tuple\n\nfrom ._decode import encode\n\n\ndef to_multipart(body: Any) -> Tuple[Dict[str, Any], Dict[str, Any]]:\n wire = encode(body)\n data: Dict[str, Any] = {}\n files: Dict[str, Any] = {}\n for key, value in (wire or {}).items():\n if isinstance(value, (bytes, bytearray)) or hasattr(value, "read"):\n files[key] = value\n elif isinstance(value, (dict, list)):\n data[key] = json.dumps(value)\n else:\n data[key] = value\n return data, files\n\n\ndef is_stream_body(body: Any) -> bool:\n return (\n isinstance(body, (bytes, bytearray, str))\n or callable(getattr(body, "read", None))\n or hasattr(body, "__next__")\n or hasattr(body, "__aiter__")\n )\n\n\ndef multipart_arguments(body: Any) -> Dict[str, Any]:\n if is_stream_body(body):\n return {"content": body}\n data, files = to_multipart(body)\n return {"data": data, "files": files}\n',
|
|
11
11
|
};
|
|
12
12
|
//# sourceMappingURL=python.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"python.js","sourceRoot":"","sources":["../../src/runtime-sources/python.ts"],"names":[],"mappings":"AAAA,iKAAiK;AACjK,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,YAAY,EACV,+lDAA+lD;IACjmD,UAAU,EACR,spGAAspG;IACxpG,SAAS,EACP,ghBAAghB;IAClhB,YAAY,EACV,+yKAA+yK;IACjzK,UAAU,EACR,
|
|
1
|
+
{"version":3,"file":"python.js","sourceRoot":"","sources":["../../src/runtime-sources/python.ts"],"names":[],"mappings":"AAAA,iKAAiK;AACjK,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,YAAY,EACV,+lDAA+lD;IACjmD,UAAU,EACR,spGAAspG;IACxpG,SAAS,EACP,ghBAAghB;IAClhB,YAAY,EACV,+yKAA+yK;IACjzK,UAAU,EACR,4wVAA4wV;IAC9wV,cAAc,EACZ,s4RAAs4R;IACx4R,SAAS,EACP,03MAA03M;IAC53M,eAAe,EACb,u+CAAu+C;CACj+C,CAAC"}
|
|
@@ -7,7 +7,7 @@ export declare const RUNTIME_SOURCES: {
|
|
|
7
7
|
readonly 'multipart.ts': "/**\n * Serialize a plain object into `FormData` for a typed `multipart/form-data` body\n * (capability module — wired into `createClient`, never imported by the send core).\n * `Blob`/`File` and strings pass through; `Date`s become ISO strings; arrays append\n * one field per item; other objects are JSON-encoded; everything else is stringified.\n * `undefined`/`null` entries are skipped.\n */\nexport function toFormData(body: Record<string, unknown>): FormData {\n const fd = new FormData();\n const append = (key: string, value: unknown): void => {\n if (value === undefined || value === null) return;\n if (value instanceof Blob || typeof value === 'string') fd.append(key, value);\n else if (value instanceof Date) fd.append(key, value.toISOString());\n else if (Object(value) === value) fd.append(key, JSON.stringify(value));\n else fd.append(key, String(value));\n };\n for (const [key, value] of Object.entries(body)) {\n if (Array.isArray(value)) for (const item of value) append(key, item);\n else append(key, value);\n }\n return fd;\n}\n";
|
|
8
8
|
readonly 'auth.ts': "import type { ClientConfig, SecuritySpec, TokenProvider } from './types.js';\n\n/** Resolve a credential: a literal passes through; a function is awaited per request. */\nasync function resolveToken(provider: TokenProvider): Promise<string> {\n return typeof provider === 'function' ? await provider() : provider;\n}\n\n/** UTF-8-safe base64: bare `btoa` throws on non-Latin-1 credentials (RFC 7617 allows UTF-8). */\nfunction encodeBase64(text: string): string {\n let binary = '';\n for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\n/** Whether a credential for this scheme is configured on the instance. */\nfunction isConfigured(scheme: SecuritySpec, config: ClientConfig): boolean {\n if (scheme.kind === 'apiKey') return config.auth?.apiKey?.[scheme.scheme] !== undefined;\n if (scheme.kind === 'bearer') return config.auth?.bearer !== undefined;\n return config.auth?.basic !== undefined;\n}\n\n/**\n * Build the auth headers/query for one operation's `security` OR-alternatives from the\n * instance credentials (`config.auth`) — capability module, wired into `createClient`.\n * The first alternative whose schemes (an AND-set) are all configured is applied, so\n * \"bearer OR apiKey\" works with either credential and never sends both. When none is\n * fully configured, the first alternative's configured schemes are still sent (the\n * server rejects the request, mirroring the previous behavior).\n * Cookie-borne apiKeys fold into a single `Cookie` header joined with `; `.\n */\nexport async function resolveAuth(\n security: readonly (readonly SecuritySpec[])[],\n config: ClientConfig\n): Promise<{ headers: Record<string, string>; query: Record<string, string> }> {\n const alternative =\n security.find((schemes) => schemes.every((scheme) => isConfigured(scheme, config))) ??\n security[0] ??\n [];\n const headers: Record<string, string> = {};\n const query: Record<string, string> = {};\n const cookies: string[] = [];\n for (const scheme of alternative) {\n if (scheme.kind === 'apiKey') {\n const provider = config.auth?.apiKey?.[scheme.scheme];\n if (provider === undefined) continue;\n const value = await resolveToken(provider);\n if (scheme.in === 'header') headers[scheme.name] = value;\n else if (scheme.in === 'query') query[scheme.name] = value;\n // Cookie values may contain reserved characters (`;`, `=`, space, …); percent-encode\n // so the credential can't break the `Cookie` header syntax.\n else cookies.push(`${scheme.name}=${encodeURIComponent(value)}`);\n } else if (scheme.kind === 'bearer') {\n const provider = config.auth?.bearer;\n if (provider !== undefined) headers.Authorization = `Bearer ${await resolveToken(provider)}`;\n } else {\n const basic = config.auth?.basic;\n if (basic !== undefined) {\n headers.Authorization = `Basic ${encodeBase64(`${basic.username}:${basic.password}`)}`;\n }\n }\n }\n if (cookies.length > 0) headers.Cookie = cookies.join('; ');\n return { headers, query };\n}\n";
|
|
9
9
|
readonly 'setup.ts': "import type { ClientConfig, Middleware } from './types.js';\n\n/**\n * Merge a publisher's baked setup (`defineClientSetup({...})`) with the app's config:\n * app config fields win per-field over baked defaults, while middleware composes —\n * baked middleware runs first, then the app's.\n */\nexport function mergeSetup(\n setup: { config?: ClientConfig; middleware?: Middleware[] } | undefined,\n config: ClientConfig = {}\n): ClientConfig {\n return {\n ...setup?.config,\n ...config,\n middleware: [...(setup?.middleware ?? []), ...(config.middleware ?? [])],\n };\n}\n";
|
|
10
|
-
readonly 'send.ts': "import { abortError, TimeoutError } from './errors.js';\nimport { defaultRetryOn, retryDelay, sleep } from './retry.js';\nimport type {\n ClientConfig,\n Middleware,\n OperationContext,\n RequestContext,\n RequestOptions,\n RetryConfig,\n} from './types.js';\n\n/**\n * Optional behaviors the send core can use but never statically imports — wired by\n * `createClient` (the same seam the future inline-mode assembler relies on).\n */\nexport type SendCapabilities = {\n /** Serialize a typed multipart body (a plain object) to FormData. */\n serializeMultipart?: (body: Record<string, unknown>) => FormData;\n};\n\n/**\n * Normalize a caller's `HeadersInit` (plain record, `Headers` instance, or entry pairs)\n * to a plain record — spreading a `Headers` or an array contributes no entries.\n */\nexport function toHeaderRecord(headers: HeadersInit | undefined): Record<string, string> {\n if (headers === undefined) return {};\n if (headers instanceof Headers) {\n const record: Record<string, string> = {};\n headers.forEach((value, key) => {\n record[key] = value;\n });\n return record;\n }\n if (Array.isArray(headers)) return Object.fromEntries(headers);\n return headers;\n}\n\n/**\n * The effective middleware chain for a request: the single `onRequest`/`onResponse`/\n * `onError` config hooks as one implicit first middleware, then `config.middleware`.\n */\nexport function middlewareChain(config: ClientConfig): Middleware[] {\n const single =\n config.onRequest || config.onResponse || config.onError\n ? [{ onRequest: config.onRequest, onResponse: config.onResponse, onError: config.onError }]\n : [];\n return [...single, ...(config.middleware ?? [])];\n}\n\n/**\n * The fetch core shared by every operation: default + config + per-call headers, the\n * `onRequest` chain (BEFORE body serialization, so mutations are sent), body\n * serialization (JSON, or FormData via the multipart capability), the retry loop\n * (idempotent-only defaults, `Retry-After`, abandoned-body drain), and the reverse\n * `onResponse` onion. Returns the final response plus the request context.\n */\nexport async function send(\n config: ClientConfig,\n op: OperationContext,\n url: string,\n init: RequestOptions,\n body: unknown | undefined,\n bodySpec: { contentType: string; multipart?: boolean } | undefined,\n caps: SendCapabilities,\n accept = 'application/json'\n): Promise<{ response: Response; context: RequestContext }> {\n const { retry: callRetry, timeout: callTimeout, idempotencyKey: callKey, ...fetchInit } = init;\n const retry: RetryConfig = { ...config.retry, ...callRetry };\n const timeout = callTimeout ?? config.timeout;\n const idempotency = callKey ?? config.idempotencyKey;\n const extra = typeof config.headers === 'function' ? await config.headers() : config.headers;\n const headers: Record<string, string> = {\n Accept: accept,\n ...extra,\n ...toHeaderRecord(fetchInit.headers),\n };\n const method = (fetchInit.method ?? 'GET').toUpperCase();\n // One stable key per LOGICAL call — set before the retry loop so every attempt\n // re-sends the same key; a caller-provided header always wins.\n if (\n idempotency !== undefined &&\n idempotency !== false &&\n (method === 'POST' || method === 'PATCH') &&\n !('Idempotency-Key' in headers) &&\n !('idempotency-key' in headers)\n ) {\n headers['Idempotency-Key'] =\n typeof idempotency === 'string'\n ? idempotency\n : typeof idempotency === 'function'\n ? idempotency()\n : crypto.randomUUID();\n }\n // Client identification for the API owner's telemetry — never in browsers, where a\n // custom header would force a CORS preflight the API may not allow.\n if (\n typeof config.clientHeader === 'string' &&\n typeof document === 'undefined' &&\n !('X-Redocly-Client' in headers) &&\n !('x-redocly-client' in headers)\n ) {\n headers['X-Redocly-Client'] = config.clientHeader;\n }\n const context: RequestContext = {\n url,\n method: fetchInit.method ?? 'GET',\n headers,\n body,\n operation: op,\n };\n const middleware = middlewareChain(config);\n for (const mw of middleware) if (mw.onRequest) await mw.onRequest(context);\n // Serialize AFTER onRequest so body mutations (case conversion, enveloping, signing) take effect.\n let payload: BodyInit | undefined;\n if (context.body !== undefined) {\n const value = context.body;\n const isBinary =\n value instanceof Blob ||\n value instanceof ArrayBuffer ||\n ArrayBuffer.isView(value as ArrayBufferView);\n const isFormData = typeof FormData !== 'undefined' && value instanceof FormData;\n const isURLSearchParams = value instanceof URLSearchParams;\n if (isFormData || isURLSearchParams || isBinary || typeof value === 'string') {\n payload = value as BodyInit;\n } else if (bodySpec?.multipart === true) {\n if (!caps.serializeMultipart) {\n throw new Error('Multipart capability not wired: cannot serialize the request body');\n }\n payload = caps.serializeMultipart(value as Record<string, unknown>);\n } else {\n payload = JSON.stringify(value);\n if (!
|
|
10
|
+
readonly 'send.ts': "import { abortError, TimeoutError } from './errors.js';\nimport { defaultRetryOn, retryDelay, sleep } from './retry.js';\nimport type {\n ClientConfig,\n Middleware,\n OperationContext,\n RequestContext,\n RequestOptions,\n RetryConfig,\n} from './types.js';\n\n/**\n * Optional behaviors the send core can use but never statically imports — wired by\n * `createClient` (the same seam the future inline-mode assembler relies on).\n */\nexport type SendCapabilities = {\n /** Serialize a typed multipart body (a plain object) to FormData. */\n serializeMultipart?: (body: Record<string, unknown>) => FormData;\n};\n\n/**\n * Normalize a caller's `HeadersInit` (plain record, `Headers` instance, or entry pairs)\n * to a plain record — spreading a `Headers` or an array contributes no entries.\n */\nexport function toHeaderRecord(headers: HeadersInit | undefined): Record<string, string> {\n if (headers === undefined) return {};\n if (headers instanceof Headers) {\n const record: Record<string, string> = {};\n headers.forEach((value, key) => {\n record[key] = value;\n });\n return record;\n }\n if (Array.isArray(headers)) return Object.fromEntries(headers);\n return headers;\n}\n\n/**\n * The effective middleware chain for a request: the single `onRequest`/`onResponse`/\n * `onError` config hooks as one implicit first middleware, then `config.middleware`.\n */\nexport function middlewareChain(config: ClientConfig): Middleware[] {\n const single =\n config.onRequest || config.onResponse || config.onError\n ? [{ onRequest: config.onRequest, onResponse: config.onResponse, onError: config.onError }]\n : [];\n return [...single, ...(config.middleware ?? [])];\n}\n\n/**\n * The fetch core shared by every operation: default + config + per-call headers, the\n * `onRequest` chain (BEFORE body serialization, so mutations are sent), body\n * serialization (JSON, or FormData via the multipart capability; a stream passes\n * through in one attempt), the retry loop\n * (idempotent-only defaults, `Retry-After`, abandoned-body drain), and the reverse\n * `onResponse` onion. Returns the final response plus the request context.\n */\nexport async function send(\n config: ClientConfig,\n op: OperationContext,\n url: string,\n init: RequestOptions,\n body: unknown | undefined,\n bodySpec: { contentType: string; multipart?: boolean } | undefined,\n caps: SendCapabilities,\n accept = 'application/json'\n): Promise<{ response: Response; context: RequestContext }> {\n const { retry: callRetry, timeout: callTimeout, idempotencyKey: callKey, ...fetchInit } = init;\n const retry: RetryConfig = { ...config.retry, ...callRetry };\n const timeout = callTimeout ?? config.timeout;\n const idempotency = callKey ?? config.idempotencyKey;\n const extra = typeof config.headers === 'function' ? await config.headers() : config.headers;\n const headers: Record<string, string> = {\n Accept: accept,\n ...extra,\n ...toHeaderRecord(fetchInit.headers),\n };\n const method = (fetchInit.method ?? 'GET').toUpperCase();\n // One stable key per LOGICAL call — set before the retry loop so every attempt\n // re-sends the same key; a caller-provided header always wins.\n if (\n idempotency !== undefined &&\n idempotency !== false &&\n (method === 'POST' || method === 'PATCH') &&\n !('Idempotency-Key' in headers) &&\n !('idempotency-key' in headers)\n ) {\n headers['Idempotency-Key'] =\n typeof idempotency === 'string'\n ? idempotency\n : typeof idempotency === 'function'\n ? idempotency()\n : crypto.randomUUID();\n }\n // Client identification for the API owner's telemetry — never in browsers, where a\n // custom header would force a CORS preflight the API may not allow.\n if (\n typeof config.clientHeader === 'string' &&\n typeof document === 'undefined' &&\n !('X-Redocly-Client' in headers) &&\n !('x-redocly-client' in headers)\n ) {\n headers['X-Redocly-Client'] = config.clientHeader;\n }\n const context: RequestContext = {\n url,\n method: fetchInit.method ?? 'GET',\n headers,\n body,\n operation: op,\n };\n const middleware = middlewareChain(config);\n for (const mw of middleware) if (mw.onRequest) await mw.onRequest(context);\n // Serialize AFTER onRequest so body mutations (case conversion, enveloping, signing) take effect.\n let payload: BodyInit | undefined;\n let isStream = false;\n if (context.body !== undefined) {\n const value = context.body;\n const isBinary =\n value instanceof Blob ||\n value instanceof ArrayBuffer ||\n ArrayBuffer.isView(value as ArrayBufferView);\n const isFormData = typeof FormData !== 'undefined' && value instanceof FormData;\n const isURLSearchParams = value instanceof URLSearchParams;\n isStream =\n (typeof ReadableStream !== 'undefined' && value instanceof ReadableStream) ||\n (value !== null &&\n typeof (value as Partial<AsyncIterable<unknown>>)[Symbol.asyncIterator] === 'function');\n const hasContentType = 'Content-Type' in context.headers || 'content-type' in context.headers;\n if (isFormData || isURLSearchParams || isBinary || isStream || typeof value === 'string') {\n payload = value as BodyInit;\n // `fetch` derives the type of FormData and URLSearchParams, boundary included, and a\n // multipart boundary is only the caller's to set. Any other pass-through body gets\n // the declared type when the caller set none.\n const declared = bodySpec?.contentType;\n if (\n !isFormData &&\n !isURLSearchParams &&\n !hasContentType &&\n declared !== undefined &&\n !declared.toLowerCase().includes('multipart')\n ) {\n context.headers['Content-Type'] = declared;\n }\n } else if (bodySpec?.multipart === true) {\n if (!caps.serializeMultipart) {\n throw new Error('Multipart capability not wired: cannot serialize the request body');\n }\n payload = caps.serializeMultipart(value as Record<string, unknown>);\n } else {\n payload = JSON.stringify(value);\n if (!hasContentType) {\n // The spec's declared request content type (e.g. application/merge-patch+json).\n context.headers['Content-Type'] = bodySpec?.contentType ?? 'application/json';\n }\n }\n }\n const doFetch = config.fetch ?? fetch;\n // A stream is consumed by the first attempt and cannot be replayed, so it never retries.\n const maxAttempts = isStream ? 1 : 1 + (retry.retries ?? 0);\n const retryOn = retry.retryOn ?? defaultRetryOn;\n const signal = fetchInit.signal ?? undefined;\n\n let attempt = 0;\n while (true) {\n attempt++;\n if (signal?.aborted) throw abortError(signal);\n // A fresh timeout budget per attempt; the caller's signal still wins the race.\n // The composed signal also governs reading the response body.\n const attemptSignal = timeout\n ? signal\n ? AbortSignal.any([signal, AbortSignal.timeout(timeout)])\n : AbortSignal.timeout(timeout)\n : signal;\n let response: Response;\n try {\n response = await doFetch(context.url, {\n ...fetchInit,\n // `fetch` requires it for a stream body.\n ...(isStream ? { duplex: 'half' } : {}),\n signal: attemptSignal,\n method: context.method,\n headers: context.headers,\n body: payload,\n });\n } catch (error) {\n if (\n attempt < maxAttempts &&\n !signal?.aborted &&\n (await retryOn({ attempt, request: context, error }))\n ) {\n await sleep(retryDelay(retry, attempt, null), signal);\n continue;\n }\n // Our timeout fired (never the caller's own abort — that rethrows untouched):\n // wrap the bare DOMException with the context a log line needs.\n if (\n timeout &&\n !signal?.aborted &&\n error instanceof DOMException &&\n error.name === 'TimeoutError'\n ) {\n throw new TimeoutError(op.id, timeout, attempt);\n }\n throw error;\n }\n // Reverse order: the last-registered middleware wraps closest to the network (onion).\n for (let i = middleware.length - 1; i >= 0; i--) {\n const onResponse = middleware[i].onResponse;\n if (onResponse) {\n const replaced = await onResponse(response, context);\n if (replaced && replaced !== response) {\n // Cancel the abandoned original's body — like the retry path, an unread body\n // keeps its connection checked out under Node/undici.\n await response.body?.cancel().catch(() => undefined);\n response = replaced;\n }\n }\n }\n if (\n !response.ok &&\n attempt < maxAttempts &&\n !signal?.aborted &&\n (await retryOn({ attempt, request: context, response }))\n ) {\n const retryAfter = response.headers.get('retry-after');\n // Drain the abandoned response body before the next attempt: an unread body\n // keeps the connection checked out (and can stall the pool) under Node/undici\n // and other strict HTTP clients. Ignore errors (e.g. a middleware already read it).\n await response.body?.cancel().catch(() => undefined);\n await sleep(retryDelay(retry, attempt, retryAfter), signal);\n continue;\n }\n return { response, context };\n }\n}\n";
|
|
11
11
|
readonly 'sse.ts': "import { ApiError } from './errors.js';\nimport { readError } from './parse.js';\nimport { sleep } from './retry.js';\nimport { send, toHeaderRecord } from './send.js';\nimport type { ClientConfig, OperationContext, ServerSentEvent, SseOptions } from './types.js';\n\n/**\n * A frame delimiter: two consecutive line terminators (each CR, LF, or CRLF, per the SSE\n * spec — so mixed endings like `\\n\\r\\n` are valid boundaries, not just matching pairs).\n */\nconst FRAME_DELIMITER = /(?:\\r\\n|\\r|\\n){2}/;\n\n/**\n * A terminally malformed event stream — unparseable JSON `data` or an unbounded frame.\n * A stable bad payload, not a dropped connection, so the stream never reconnects on it.\n */\nexport class SseParseError extends Error {}\n\n/**\n * Consume a `text/event-stream` operation as typed events (capability module — wired\n * into `createClient`). Auto-reconnects on dropped connections, resuming from the last\n * seen event id via `Last-Event-ID` (backoff: the server's `retry:` value, then\n * `reconnectDelay`, then 1s — exponential with jitter, capped at 30s). A clean stream\n * end flushes a trailing frame and finishes; `break`/abort end the iterator cleanly.\n */\nexport async function* sse<T>(\n config: ClientConfig,\n op: OperationContext,\n prepare: () => Promise<{ url: string; init: SseOptions; body?: unknown }>,\n dataKind: 'json' | 'text' = 'text'\n): AsyncGenerator<ServerSentEvent<T>> {\n let lastEventId: string | undefined;\n let serverRetry: number | undefined;\n let failures = 0;\n while (true) {\n // Re-prepare each attempt so a refresh-style TokenProvider yields a fresh credential\n // on reconnect (the auth is baked into `url` query + `init.headers`). `reconnect`,\n // `reconnectDelay`, and `signal` come from the caller's original options unchanged.\n const { url, init, body: requestBody } = await prepare();\n const { reconnect = true, reconnectDelay, ...rest } = init;\n const signal = rest.signal ?? undefined;\n if (signal?.aborted) return;\n const headers: Record<string, string> = {\n Accept: 'text/event-stream',\n ...toHeaderRecord(rest.headers),\n };\n const sendHeaders =\n lastEventId === undefined ? headers : { ...headers, 'Last-Event-ID': lastEventId };\n try {\n const { response } = await send(\n config,\n op,\n url,\n // `timeout: 0` opts the stream out of a config-level timeout — an event stream\n // is long-lived by design and must not be severed after N milliseconds.\n { ...rest, method: rest.method ?? 'GET', headers: sendHeaders, timeout: 0 },\n requestBody,\n undefined,\n {}\n );\n if (!response.ok) {\n const errorBody = await readError(response);\n throw new ApiError(url, response.status, response.statusText, errorBody);\n }\n failures = 0;\n const body = response.body;\n if (!body) return;\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n try {\n while (true) {\n const { done, value } = await reader.read();\n buffer += done ? decoder.decode() : decoder.decode(value, { stream: true });\n let match: RegExpExecArray | null;\n while ((match = FRAME_DELIMITER.exec(buffer)) !== null) {\n const raw = buffer.slice(0, match.index);\n buffer = buffer.slice(match.index + match[0].length);\n const event = parseSseFrame(raw, dataKind);\n if (event) {\n if (event.id !== undefined) lastEventId = event.id;\n if (event.retry !== undefined) serverRetry = event.retry;\n yield event as ServerSentEvent<T>;\n }\n }\n if (done) {\n // Stream closed cleanly. Flush a final event that arrived without a trailing\n // delimiter, then finish — a clean end is not a dropped connection, so do not reconnect.\n const event = buffer.length > 0 ? parseSseFrame(buffer, dataKind) : undefined;\n if (event) {\n if (event.id !== undefined) lastEventId = event.id;\n if (event.retry !== undefined) serverRetry = event.retry;\n yield event as ServerSentEvent<T>;\n }\n return;\n }\n // Bound memory: a server that never sends a frame delimiter would otherwise\n // grow `buffer` without limit. 1 MiB is far above any real SSE frame.\n if (buffer.length > 1048576) {\n throw new SseParseError('SSE frame exceeded 1048576 characters without a delimiter');\n }\n }\n } finally {\n await reader.cancel().catch(() => undefined);\n }\n } catch (error) {\n if (signal?.aborted) return;\n // A non-OK HTTP response (4xx/5xx) or an unparseable JSON payload is a definitive\n // error, not a transient drop — surface it instead of reconnecting in a loop (a\n // stable bad payload would otherwise reconnect forever).\n if (error instanceof ApiError || error instanceof SseParseError) throw error;\n // A transport failure (connect/DNS/reset) when opening the request, or a mid-stream\n // read error, is a dropped connection: fall through to backoff/reconnect when enabled.\n if (!reconnect) throw error;\n }\n // Only the swallowed-drop path reaches here: reconnect is on and the signal not aborted.\n failures++;\n const base = serverRetry ?? reconnectDelay ?? 1000;\n const delay = Math.min(base * Math.pow(2, failures - 1), 30_000);\n try {\n await sleep(Math.random() * delay, signal);\n } catch {\n return; // sleep rejects only on abort — end the iterator cleanly\n }\n }\n}\n\n/** Parse one raw SSE frame (its lines) into an event; returns undefined for comment-only frames. */\nexport function parseSseFrame(\n raw: string,\n dataKind: 'json' | 'text'\n): ServerSentEvent<unknown> | undefined {\n let event: string | undefined;\n const dataLines: string[] = [];\n let id: string | undefined;\n let retry: number | undefined;\n let sawField = false;\n for (const line of raw.split(/\\r\\n|\\n|\\r/)) {\n if (line === '' || line.startsWith(':')) continue;\n const colon = line.indexOf(':');\n const field = colon === -1 ? line : line.slice(0, colon);\n let val = colon === -1 ? '' : line.slice(colon + 1);\n if (val.startsWith(' ')) val = val.slice(1);\n sawField = true;\n if (field === 'event') event = val;\n else if (field === 'data') dataLines.push(val);\n else if (field === 'id') id = val;\n else if (field === 'retry') {\n // ASCII digits only, per the EventSource spec — anything else is ignored\n // (`Number('')` is 0 and would zero the reconnect backoff).\n if (/^\\d+$/.test(val)) retry = Number(val);\n }\n }\n if (!sawField) return undefined;\n const dataText = dataLines.join('\\n');\n let data: unknown = dataText;\n if (dataKind === 'json' && dataText !== '') {\n try {\n data = JSON.parse(dataText);\n } catch (error) {\n throw new SseParseError(\n `Failed to parse SSE event data as JSON: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n }\n return { event, data, id, retry };\n}\n";
|
|
12
12
|
readonly 'create-client.ts': "import { ApiError } from './errors.js';\nimport { parse, readError } from './parse.js';\nimport { middlewareChain, send, toHeaderRecord, type SendCapabilities } from './send.js';\nimport type {\n ApiErrorLike,\n Client,\n ClientConfig,\n Middleware,\n OperationContext,\n OperationDescriptor,\n OpsShape,\n PaginationSpec,\n ParseAs,\n QueryValue,\n RequestOptions,\n ResponseHeaderSpec,\n SecuritySpec,\n ServerSentEvent,\n SseOptions,\n TokenProvider,\n} from './types.js';\nimport { buildUrl, substitutePath, type QueryStyle } from './url.js';\n\n/**\n * The optional behaviors `createClientCore` can dispatch to but never statically\n * imports. The package's public `createClient` wires the full set; the future\n * inline-mode assembler wires only the capabilities a spec needs.\n */\nexport type Capabilities = SendCapabilities & {\n resolveAuth?: (\n security: readonly (readonly SecuritySpec[])[],\n config: ClientConfig\n ) => Promise<{ headers: Record<string, string>; query: Record<string, string> }>;\n sse?: (\n config: ClientConfig,\n op: OperationContext,\n // Re-preparing per (re)connect (not a frozen url/init) lets a refresh-style\n // TokenProvider issue a fresh credential after a dropped stream reconnects.\n prepare: () => Promise<{ url: string; init: SseOptions }>,\n dataKind: 'json' | 'text'\n ) => AsyncGenerator<ServerSentEvent<unknown>>;\n paginate?: {\n pages: (\n call: (args?: OperationArgs, init?: RequestOptions) => Promise<unknown>,\n spec: PaginationSpec,\n args?: OperationArgs,\n init?: RequestOptions\n ) => AsyncGenerator<unknown>;\n items: (\n call: (args?: OperationArgs, init?: RequestOptions) => Promise<unknown>,\n spec: PaginationSpec,\n args?: OperationArgs,\n init?: RequestOptions\n ) => AsyncGenerator<unknown>;\n // The `link`-style iterators need the raw `Link` header + page URL, which the\n // parsed-page call above cannot carry (the shape mirrors paginate's `LinkPageCall`).\n pagesByLink: (\n call: (\n args?: OperationArgs,\n init?: RequestOptions\n ) => Promise<{ page: unknown; linkHeader: string | null; url: string }>,\n args?: OperationArgs,\n init?: RequestOptions\n ) => AsyncGenerator<unknown>;\n itemsByLink: (\n call: (\n args?: OperationArgs,\n init?: RequestOptions\n ) => Promise<{ page: unknown; linkHeader: string | null; url: string }>,\n spec: PaginationSpec,\n args?: OperationArgs,\n init?: RequestOptions\n ) => AsyncGenerator<unknown>;\n };\n};\n\n/**\n * One call's inputs, namespaced by transport layer. `argsStyle: 'flat'` clients accept the\n * merged form instead (every parameter and body property at one level) — `namespaceArgs`\n * converts it to this shape before anything downstream reads it.\n */\nexport type OperationArgs = {\n path?: Record<string, unknown>;\n query?: Record<string, QueryValue>;\n body?: unknown;\n headers?: Record<string, unknown>;\n cookies?: Record<string, unknown>;\n} & Record<string, unknown>;\n\n/** The five layer keys, and the only top-level keys a namespaced call may carry. */\nconst LAYERS: readonly string[] = ['path', 'query', 'body', 'headers', 'cookies'];\n\n/** Where a declared parameter's `in` value puts it. */\nconst LAYER_OF: Record<string, 'path' | 'query' | 'headers' | 'cookies'> = {\n path: 'path',\n query: 'query',\n header: 'headers',\n cookie: 'cookies',\n};\n\n/**\n * Merged (`argsStyle: 'flat'`) args → the namespaced shape. A key that names a declared\n * parameter goes to that parameter's layer; anything else is a property of the request\n * body, which is how a flat call spells an object body. `body` stays reserved for the\n * operations a flat call cannot merge (an array, a scalar, or a binary body).\n */\nfunction namespaceArgs(op: OperationDescriptor, args: OperationArgs): OperationArgs {\n const layers: Record<string, Record<string, unknown>> = {};\n let body: unknown;\n let properties: Record<string, unknown> | undefined;\n const layerOfParam = new Map((op.params ?? []).map((param) => [param.name, param.in]));\n for (const [key, value] of Object.entries(args)) {\n const layer = LAYER_OF[layerOfParam.get(key) ?? ''];\n if (layer !== undefined) {\n (layers[layer] ??= {})[key] = value;\n } else if (key === 'body' && op.body !== undefined) {\n body = value;\n } else if (op.body !== undefined) {\n (properties ??= {})[key] = value;\n } else {\n throw new TypeError(\n `Unknown argument \"${key}\" for operation \"${op.id}\": it names no declared parameter, and the operation takes no request body.`\n );\n }\n }\n const namespaced: OperationArgs = {};\n if (layers.path) namespaced.path = layers.path;\n // The flat surface types every query value, so the collected bag is one by construction.\n if (layers.query) namespaced.query = layers.query as Record<string, QueryValue>;\n if (layers.headers) namespaced.headers = layers.headers;\n if (layers.cookies) namespaced.cookies = layers.cookies;\n if (properties !== undefined) namespaced.body = properties;\n else if (body !== undefined) namespaced.body = body;\n return namespaced;\n}\n\n/** The response reader implied by the descriptor (before any per-call `parseAs` override). */\n/**\n * The `Accept` header matching how the response will be read — a blob/text operation\n * must not ask for `application/json` (a content-negotiating server would 406 or\n * answer with a JSON error body instead of the payload). Caller `init.headers` and\n * `config.headers` still override.\n */\nfunction acceptFor(kind: ParseAs | 'void'): string {\n if (kind === 'text') return 'text/*';\n if (kind === 'blob' || kind === 'arrayBuffer' || kind === 'stream' || kind === 'formData') {\n return '*/*';\n }\n return 'application/json'; // json | auto | void\n}\n\nfunction kindFor(op: OperationDescriptor): ParseAs | 'void' {\n if (op.responseKind === 'void' || op.responseKind === 'blob' || op.responseKind === 'text') {\n return op.responseKind;\n }\n return 'auto';\n}\n\n/**\n * The call's inputs in namespaced form, converting first on a flat-style client. An\n * operation the generator marked `argsStyle: 'grouped'` is already namespaced — its names\n * could not be merged, so its input type never offered the flat shape.\n */\nfunction inputOf(\n op: OperationDescriptor,\n args: OperationArgs,\n config: ClientConfig\n): OperationArgs {\n const merged = config.argsStyle === 'flat' && op.argsStyle !== 'grouped';\n return merged ? namespaceArgs(op, args) : args;\n}\n\n/** Route the namespaced args to the request pieces. */\nfunction splitArgs(op: OperationDescriptor, args: OperationArgs) {\n // An unknown layer key can only be a bug (usually flat-style args on a namespaced\n // client). TypeScript catches it, but a transpiler that skips type-checking would\n // otherwise ship a request that silently drops the value — fail the call loudly.\n for (const key of Object.keys(args)) {\n if (!LAYERS.includes(key)) {\n throw new TypeError(\n `Unknown argument \"${key}\" for operation \"${op.id}\". Inputs are grouped by layer: ${LAYERS.join(', ')}.`\n );\n }\n }\n return {\n path: args.path ?? {},\n query: args.query,\n body: args.body,\n headers: args.headers,\n cookies: args.cookies,\n };\n}\n\n/**\n * The query-serialization hints for the descriptor's query params. A spec is built only\n * when the param deviates from the OpenAPI defaults (`form` + `explode: true`, encoded),\n * and always fully resolved — so `explode: false` or `allowReserved` alone (no `style`)\n * are honored, and an omitted `explode` keeps the exploded default.\n */\nfunction queryStyles(op: OperationDescriptor): Record<string, QueryStyle> | undefined {\n let styles: Record<string, QueryStyle> | undefined;\n for (const param of op.params ?? []) {\n if (param.in !== 'query') continue;\n const deviates =\n (param.style !== undefined && param.style !== 'form') ||\n param.explode === false ||\n param.allowReserved === true;\n if (!deviates) continue;\n styles ??= {};\n styles[param.name] = {\n style: param.style ?? 'form',\n explode: param.explode ?? true,\n allowReserved: param.allowReserved,\n };\n }\n return styles;\n}\n\n/** Stringify caller-supplied extra headers, skipping empty entries. */\nfunction stringHeaders(headers: Record<string, unknown> | undefined): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers ?? {})) {\n if (value !== undefined && value !== null) out[key] = String(value);\n }\n return out;\n}\n\n/** Build the request pieces an attempt needs: the final URL and the merged per-call init. */\nasync function prepareRequest(\n config: ClientConfig,\n op: OperationDescriptor,\n args: OperationArgs,\n init: RequestOptions | SseOptions,\n caps: Capabilities\n): Promise<{ url: string; init: RequestOptions; body: unknown }> {\n const { path, query, body, headers, cookies } = splitArgs(op, args);\n const authed: { headers: Record<string, string>; query: Record<string, string> } =\n op.security?.length && caps.resolveAuth\n ? await caps.resolveAuth(op.security, config)\n : { headers: {}, query: {} };\n // Cookie params join the auth-injected cookies in one `Cookie` header (values\n // percent-encoded, like auth cookies). Server-side only — browsers own the header.\n const cookiePairs = Object.entries(cookies ?? {})\n .filter(([, value]) => value !== undefined && value !== null)\n .map(([cookieName, value]) => `${cookieName}=${encodeURIComponent(String(value))}`);\n if (cookiePairs.length > 0) {\n authed.headers.Cookie = [authed.headers.Cookie, ...cookiePairs].filter(Boolean).join('; ');\n }\n const fullQuery: Record<string, QueryValue> = { ...query, ...authed.query };\n const url = buildUrl(\n config.serverUrl ?? '',\n substitutePath(op.path, path),\n Object.keys(fullQuery).length > 0 ? fullQuery : undefined,\n queryStyles(op)\n );\n const mergedInit: RequestOptions = {\n ...init,\n method: op.method.toUpperCase(),\n // Precedence, lowest → highest (later spreads win): injected auth → explicit\n // header params → caller `init.headers` — the caller always overrides both.\n headers: {\n ...authed.headers,\n ...stringHeaders(headers),\n ...toHeaderRecord(init.headers),\n },\n };\n return { url, init: mergedInit, body };\n}\n\n/** Coerce a single declared response header value; omit when absent or unparsable. */\nfunction coerceResponseHeader(\n raw: string | null,\n type: ResponseHeaderSpec['type']\n): string | number | boolean | undefined {\n if (raw === null) return undefined;\n if (type === 'number') {\n if (raw.trim() === '') return undefined;\n const value = Number(raw);\n return Number.isFinite(value) ? value : undefined;\n }\n if (type === 'boolean') {\n const value = raw.trim().toLowerCase();\n if (value === 'true') return true;\n if (value === 'false') return false;\n return undefined;\n }\n return raw;\n}\n\n/** Build the camelCase declared-header bag for a throw-mode envelope. */\nfunction readEnvelopeHeaders(\n response: Response,\n specs: readonly ResponseHeaderSpec[] | undefined\n): Record<string, string | number | boolean> {\n const headers: Record<string, string | number | boolean> = {};\n for (const spec of specs ?? []) {\n const value = coerceResponseHeader(response.headers.get(spec.name), spec.type);\n if (value !== undefined) headers[spec.key] = value;\n }\n return headers;\n}\n\n/** One non-SSE call: send, then branch on the configured error mode. */\nasync function execute(\n config: ClientConfig,\n op: OperationDescriptor,\n args: OperationArgs,\n init: RequestOptions,\n caps: Capabilities\n): Promise<unknown> {\n const prepared = await prepareRequest(config, op, args, init, caps);\n const opCtx: OperationContext = { id: op.id, path: op.path, tags: [...(op.tags ?? [])] };\n // `parseAs` / `envelope` are client options, not fetch RequestInit fields.\n const { parseAs, envelope, ...sendInit } = prepared.init;\n const readKind = parseAs ?? kindFor(op);\n const { response, context } = await send(\n config,\n opCtx,\n prepared.url,\n sendInit,\n prepared.body,\n op.body,\n caps,\n acceptFor(readKind)\n );\n if (config.errorMode === 'result') {\n if (!response.ok) {\n return { data: undefined, error: await readError(response), response };\n }\n return { data: await parse(response, readKind), error: undefined, response };\n }\n if (!response.ok) {\n let error: globalThis.Error = new ApiError(\n context.url,\n response.status,\n response.statusText,\n await readError(response)\n );\n // Thread the error through each middleware's onError in turn (each may replace it).\n for (const mw of middlewareChain(config)) {\n if (mw.onError) error = await mw.onError(error as ApiErrorLike, context);\n }\n throw error;\n }\n const data = await parse(response, readKind);\n if (envelope === true) {\n return {\n data,\n headers: readEnvelopeHeaders(response, op.responseHeaders),\n response,\n };\n }\n return data;\n}\n\n/** The paginate capability, or a descriptive throw when a paginated op is iterated unwired. */\nfunction paginateCapability(caps: Capabilities, op: OperationDescriptor) {\n if (!caps.paginate) {\n throw new Error(`Pagination capability not wired: cannot iterate operation \"${op.id}\"`);\n }\n return caps.paginate;\n}\n\n/**\n * The per-page call the iterators drive: the method itself in throw mode; in result\n * mode a wrapper that unwraps the `{ data, error, response }` envelope — the page\n * pointers are data-rooted — rethrowing a failed page as `ApiError` (iteration is\n * error-mode-agnostic; the throw-mode-only `onError` middleware hook is not invoked).\n */\nfunction pageCall(\n method: (args?: OperationArgs, init?: RequestOptions) => Promise<unknown>,\n config: ClientConfig\n) {\n const callWithoutEnvelope = (args?: OperationArgs, init?: RequestOptions) => {\n if (!init || init.envelope === undefined) return method(args, init);\n const { envelope: _envelope, ...pageInit } = init;\n return method(args, pageInit);\n };\n if (config.errorMode !== 'result') return callWithoutEnvelope;\n return async (args?: OperationArgs, init?: RequestOptions) => {\n const envelope = (await callWithoutEnvelope(args, init)) as {\n data: unknown;\n error: unknown;\n response: Response;\n };\n // Failure is `!response.ok` — NOT `data === undefined`: a successful bodyless page\n // (204/void) also parses to undefined data, and a failed page's `error` can be\n // undefined too (unreadable body). The pointers then miss on the undefined data\n // and iteration stops cleanly, which is the correct semantics for an empty page.\n if (!envelope.response.ok) {\n const { response } = envelope;\n throw new ApiError(response.url, response.status, response.statusText, envelope.error);\n }\n return envelope.data;\n };\n}\n\n/**\n * The per-page call the `link`-style iterators drive: like `execute`, but returning the\n * parsed page together with the raw `Link` header and the page's own URL (for resolving\n * a relative `rel=\"next\"` target). Error-mode-agnostic like all iteration: a failed\n * page throws `ApiError` even on result-mode clients.\n */\nfunction linkPageCall(config: ClientConfig, op: OperationDescriptor, caps: Capabilities) {\n return async (args: OperationArgs = {}, init: RequestOptions = {}) => {\n const prepared = await prepareRequest(config, op, args, init, caps);\n const { parseAs, envelope: _envelope, ...sendInit } = prepared.init;\n const readKind = parseAs ?? kindFor(op);\n const opCtx: OperationContext = { id: op.id, path: op.path, tags: [...(op.tags ?? [])] };\n const { response } = await send(\n config,\n opCtx,\n prepared.url,\n sendInit,\n prepared.body,\n op.body,\n caps,\n acceptFor(readKind)\n );\n if (!response.ok) {\n throw new ApiError(\n prepared.url,\n response.status,\n response.statusText,\n await readError(response)\n );\n }\n return {\n page: await parse(response, readKind),\n linkHeader: response.headers.get('link'),\n // Some `Response` implementations leave `url` empty (mocks, constructed responses).\n url: response.url === '' ? prepared.url : response.url,\n };\n };\n}\n\n/**\n * Build a typed instance client over operation descriptors: one real bound method per\n * operation (attached by a construction-time loop — no Proxy), plus the core members\n * (`configure`/`use`/`auth`), which are assigned AFTER the loop so they win any name\n * collision with an operation. All behavior dispatches through the capability seam.\n */\nexport function createClientCore<\n Ops extends OpsShape,\n Id extends string = string,\n Path extends string = string,\n Tag extends string = string,\n>(\n operations: Record<string, OperationDescriptor>,\n initial: ClientConfig<OperationContext<Id, Path, Tag>> = {},\n caps: Capabilities = {}\n): Client<Ops, OperationContext<Id, Path, Tag>> {\n // The literal-union narrowing is a compile-time DX contract only; internally the\n // runtime works with the base (string-typed) context. One cast at this boundary —\n // `ClientConfig<Narrow>` is not assignable to `ClientConfig` (middleware ctx\n // params are contravariant).\n const given = initial as ClientConfig;\n // Private mutable config; the middleware array is copied so `use()` never mutates the caller's.\n const config: ClientConfig = { ...given, middleware: [...(given.middleware ?? [])] };\n const client = {} as Record<string, unknown>;\n\n for (const [name, op] of Object.entries(operations)) {\n if (op.responseKind === 'sse') {\n const method = (given: OperationArgs = {}, init: SseOptions = {}) => {\n const args = inputOf(op, given, config);\n if (!caps.sse) {\n throw new Error(`SSE capability not wired: cannot stream operation \"${op.id}\"`);\n }\n const stream = caps.sse;\n return (async function* () {\n const opCtx: OperationContext = { id: op.id, path: op.path, tags: [...(op.tags ?? [])] };\n // A thunk the stream re-runs on every (re)connect, so auth (which `prepareRequest`\n // resolves) is refreshed per attempt rather than frozen at the first connect.\n const prepare = async () => {\n const prepared = await prepareRequest(config, op, args, init, caps);\n return { url: prepared.url, init: prepared.init as SseOptions, body: prepared.body };\n };\n yield* stream(config, opCtx, prepare, op.sseDataKind ?? 'text');\n })();\n };\n // Consumers key off the function reference (cache keys, `OPERATIONS[fn.name]`), so\n // each closure carries its operationId instead of an inferred binding name.\n // `operationId` is the explicit, minification-proof form of the same identity\n // (the SPEC operationId — `name` is the emitted key, which a collision may rename).\n Object.defineProperty(method, 'name', { value: name });\n Object.defineProperty(method, 'operationId', { value: op.id });\n client[name] = method;\n } else {\n // `raw` takes namespaced args; `method` is the public entry that accepts whichever\n // style the client was generated with. The iterators namespace once and then drive\n // `raw`, so a flat call is never converted twice.\n const raw = (args: OperationArgs = {}, init: RequestOptions = {}) =>\n execute(config, op, args, init, caps);\n const method = (args: OperationArgs = {}, init: RequestOptions = {}) =>\n raw(inputOf(op, args, config), init);\n Object.defineProperty(method, 'name', { value: name });\n Object.defineProperty(method, 'operationId', { value: op.id });\n const spec = op.pagination;\n // Paginated ops keep their one-shot call and gain `.pages`/`.items`, dispatching\n // through the capability seam (like SSE: absent capability throws descriptively).\n // Iteration is error-mode-agnostic: the iterators' pointers are data-rooted, so on\n // a result-mode client (`errorMode` is fixed at construction — `configure()`\n // ignores it) each page's envelope is unwrapped before it reaches the capability.\n // A failed page aborts iteration by throwing ApiError, even on result-mode\n // clients; the `onError` middleware hook (throw-mode-only) is not invoked.\n client[name] =\n spec === undefined\n ? method\n : spec.style === 'link'\n ? Object.assign(method, {\n pages: (args?: OperationArgs, init?: RequestOptions) =>\n paginateCapability(caps, op).pagesByLink(\n linkPageCall(config, op, caps),\n inputOf(op, args ?? {}, config),\n init\n ),\n items: (args?: OperationArgs, init?: RequestOptions) =>\n paginateCapability(caps, op).itemsByLink(\n linkPageCall(config, op, caps),\n spec,\n inputOf(op, args ?? {}, config),\n init\n ),\n })\n : Object.assign(method, {\n pages: (args?: OperationArgs, init?: RequestOptions) =>\n paginateCapability(caps, op).pages(\n pageCall(raw, config),\n spec,\n inputOf(op, args ?? {}, config),\n init\n ),\n items: (args?: OperationArgs, init?: RequestOptions) =>\n paginateCapability(caps, op).items(\n pageCall(raw, config),\n spec,\n inputOf(op, args ?? {}, config),\n init\n ),\n });\n }\n }\n\n // Core members are assigned AFTER the operation loop — they win over colliding op names.\n client.configure = (next: ClientConfig): void => {\n // `errorMode` and `argsStyle` are fixed at generate time (they shape the static types);\n // flipping either at runtime would silently desync the calls from `Client<Ops>`, so both\n // are ignored here.\n const { errorMode: _fixedMode, argsStyle: _fixedStyle, auth, ...rest } = next;\n Object.assign(config, rest);\n // `auth` merges into existing credentials (like the `auth.*` setters) rather than\n // replacing wholesale — so `configure({ auth: { bearer } })` keeps a previously set\n // basic/apiKey. `apiKey` merges per scheme.\n if (auth) {\n config.auth = {\n ...config.auth,\n ...auth,\n ...(auth.apiKey ? { apiKey: { ...config.auth?.apiKey, ...auth.apiKey } } : {}),\n };\n }\n };\n client.use = (...middleware: Middleware[]): void => {\n // Reassign (don't push) so a caller-provided `middleware` array isn't mutated.\n config.middleware = [...(config.middleware ?? []), ...middleware];\n };\n client.auth = {\n bearer(token: TokenProvider): void {\n config.auth = { ...config.auth, bearer: token };\n },\n basic(username: string, password: string): void {\n config.auth = { ...config.auth, basic: { username, password } };\n },\n apiKey(scheme: string, value: TokenProvider): void {\n config.auth = { ...config.auth, apiKey: { ...config.auth?.apiKey, [scheme]: value } };\n },\n };\n\n return client as Client<Ops, OperationContext<Id, Path, Tag>>;\n}\n";
|
|
13
13
|
readonly 'paginate.ts': "import type { OperationArgs } from './create-client.js';\nimport type { PaginationSpec, QueryValue, RequestOptions } from './types.js';\n\n/**\n * Auto-pagination (capability module — wired into `createClient`, dispatched by the\n * method's `.pages()`/`.items()`): walk an operation's pages by advancing the descriptor's\n * `param` query parameter, per its `style`. The caller's args are never mutated — each\n * request gets a fresh `query` clone — and `init` is forwarded to every call.\n *\n * Iteration is error-mode-agnostic: `call` always resolves to the RAW page (on a\n * result-mode client the attachment unwraps the envelope first), so a failed page\n * aborts iteration by throwing `ApiError`, even on result-mode clients; the `onError`\n * middleware hook (throw-mode-only) is not invoked.\n */\n\n/**\n * Resolve an RFC 6901 JSON pointer (`~1` → `/`, `~0` → `~`) against a value.\n * The empty pointer is the whole document; anything else must start with `/`.\n * Returns `undefined` on any miss (bad token, absent key, non-object step) — never throws.\n */\nexport function resolvePointer(value: unknown, pointer: string): unknown {\n if (pointer === '') return value;\n if (!pointer.startsWith('/')) return undefined;\n let current = value;\n for (const token of pointer.slice(1).split('/')) {\n const key = token.replaceAll('~1', '/').replaceAll('~0', '~');\n if (Array.isArray(current)) {\n if (!/^(0|[1-9]\\d*)$/.test(key)) return undefined;\n current = current[Number(key)];\n } else if (Object(current) === current && key in (current as object)) {\n current = (current as Record<string, unknown>)[key];\n } else {\n return undefined;\n }\n }\n return current;\n}\n\n/**\n * Iterate an operation's full page results. Every page is yielded before the stop\n * condition is evaluated, so the last page always arrives. Cursor style resumes from a\n * caller-provided `query[spec.param]`, stops when the optional `hasMore` pointer\n * resolves to `false` or when `nextCursor` resolves to `undefined`/`null`/`''`, and\n * throws if the next cursor is not a string or number, or\n * if the same cursor comes back twice in a row (infinite-loop guards). Offset/page\n * styles advance by item count / by one and stop when\n * the `items` pointer misses or the array is empty.\n */\nexport async function* pages<TPage>(\n call: (args?: OperationArgs, init?: RequestOptions) => Promise<TPage>,\n spec: PaginationSpec,\n args: OperationArgs = {},\n init?: RequestOptions\n): AsyncGenerator<TPage> {\n if (spec.style === 'cursor') {\n let cursor: unknown = args.query?.[spec.param];\n while (true) {\n const query = { ...args.query };\n if (cursor !== undefined) query[spec.param] = cursor as QueryValue;\n const page = await call({ ...args, query }, init);\n yield page;\n // Connection-style APIs keep a non-null cursor on the last page and signal the\n // end via a boolean flag — honor it before the cursor check to skip the\n // follow-up empty request. Strictly `false`: a missing pointer falls through.\n if (spec.hasMore !== undefined && resolvePointer(page, spec.hasMore) === false) return;\n const next = resolvePointer(page, spec.nextCursor);\n if (next === undefined || next === null || next === '') return;\n if (typeof next !== 'string' && typeof next !== 'number') {\n // A fresh non-scalar cursor never compares equal, so without this guard a lying\n // server would slip past the did-not-advance check into an infinite loop.\n throw new Error(`Pagination cursor at ${spec.nextCursor} is not a string or number`);\n }\n if (next === cursor) {\n throw new Error('Pagination did not advance: operation returned the same cursor twice');\n }\n cursor = next;\n }\n } else if (spec.style === 'link') {\n // `link` iteration needs the response's `Link` header, which the parsed-page `call`\n // cannot carry — the client wires those operations to `pagesByLink` instead.\n throw new Error('link-style pagination iterates via pagesByLink');\n } else {\n // Coerce the starting position to a number: a caller may pass `query[spec.param]` as a\n // string (common from URL/form input), and `+=` on a string would concatenate. `null`\n // and `''` count as absent — `Number` would turn them into 0, but a one-shot call\n // omits the param for those values, so the iterator must not start at position 0.\n const start = args.query?.[spec.param];\n const fallback = spec.style === 'page' ? 1 : 0;\n const absent = start === undefined || start === null || start === '';\n let position = absent || Number.isNaN(Number(start)) ? fallback : Number(start);\n let previousItems: string | undefined;\n while (true) {\n const page = await call({ ...args, query: { ...args.query, [spec.param]: position } }, init);\n const pageItems = resolvePointer(page, spec.items);\n // Some APIs clamp a past-the-end offset/page to the last non-empty page instead\n // of returning an empty one — the repeated page would otherwise loop forever\n // (the position always \"advances\", so a cursor-style token check can't catch it).\n const serialized = Array.isArray(pageItems) ? JSON.stringify(pageItems) : undefined;\n if (serialized !== undefined && serialized === previousItems) {\n throw new Error('Pagination did not advance: the operation returned the same page twice');\n }\n yield page;\n if (!Array.isArray(pageItems) || pageItems.length === 0) return;\n previousItems = serialized;\n position += spec.style === 'page' ? 1 : pageItems.length;\n }\n }\n}\n\n/**\n * Iterate the operation's individual items: each page's `items` pointer, flattened.\n * A cursor-style page whose pointer misses yields nothing but pagination continues;\n * for offset/page styles a miss has already stopped `pages`.\n */\nexport async function* items<TItem>(\n call: (args?: OperationArgs, init?: RequestOptions) => Promise<unknown>,\n spec: PaginationSpec,\n args?: OperationArgs,\n init?: RequestOptions\n): AsyncGenerator<TItem> {\n for await (const page of pages(call, spec, args, init)) {\n const pageItems = resolvePointer(page, spec.items);\n if (Array.isArray(pageItems)) yield* pageItems as TItem[];\n }\n}\n\n/**\n * The per-page call the `link`-style iterators drive: the parsed page plus the raw\n * `Link` header and the page's own URL (for resolving a relative `next` target).\n */\nexport type LinkPageCall = (\n args?: OperationArgs,\n init?: RequestOptions\n) => Promise<{ page: unknown; linkHeader: string | null; url: string }>;\n\n/** The `rel=\"next\"` target of an RFC 8288 `Link` header, or `undefined` when absent. */\nexport function linkNext(header: string | null): string | undefined {\n if (header === null) return undefined;\n // Entries are `<url>; rel=\"next\"`, comma-separated; the URL is inside `<>`, so split\n // on commas that precede a `<` and a comma inside a target cannot break an entry.\n for (const entry of header.split(/,\\s*(?=<)/)) {\n const target = /^\\s*<([^>]*)>(.*)$/.exec(entry);\n if (!target) continue;\n const rel = /;\\s*rel\\s*=\\s*\"?([^\";]+)\"?/i.exec(target[2]);\n // `rel` may carry several space-separated relation types (RFC 8288 §3.3).\n if (rel && rel[1].split(/\\s+/).includes('next')) return target[1];\n }\n return undefined;\n}\n\n/**\n * Iterate a `link`-style operation's pages: follow the `Link` header's `rel=\"next\"`\n * target by merging ITS query params into the next call — every page goes through the\n * same declared endpoint, so auth, middleware, and `serverUrl` handling apply\n * unchanged, and credentials can never be handed to a cross-origin `next` URL.\n * Stops when no `rel=\"next\"` is present; throws when the link does not advance\n * (the same target twice, or a self-link — an infinite-loop guard).\n */\nexport async function* pagesByLink<TPage>(\n call: LinkPageCall,\n args: OperationArgs = {},\n init?: RequestOptions\n): AsyncGenerator<TPage> {\n let query = args.query;\n let previous: string | undefined;\n while (true) {\n const { page, linkHeader, url } = await call({ ...args, query }, init);\n yield page as TPage;\n const target = linkNext(linkHeader);\n if (target === undefined) return;\n // A relative target resolves against the page's own URL (RFC 8288 §3.1) — which may\n // itself be relative (relative `serverUrl`, mocked fetch), so both resolve against a\n // placeholder origin. It never reaches the wire: only the target's query params\n // carry into the next call.\n const pageUrl = new URL(url, 'http://relative.invalid');\n const next = new URL(target, pageUrl).toString();\n if (next === previous || next === pageUrl.toString()) {\n throw new Error('Pagination did not advance: the Link rel=\"next\" target repeats');\n }\n previous = next;\n // A repeated key (`?tag=a&tag=b`) folds into an array — the query serializer\n // expands arrays back into repeated pairs.\n const linkParams: Record<string, string | string[]> = {};\n for (const [key, value] of new URL(next).searchParams) {\n const seen = linkParams[key];\n if (seen === undefined) linkParams[key] = value;\n else if (Array.isArray(seen)) seen.push(value);\n else linkParams[key] = [seen, value];\n }\n query = { ...args.query, ...linkParams };\n }\n}\n\n/** Iterate a `link`-style operation's individual items: each page's `items` pointer, flattened. */\nexport async function* itemsByLink<TItem>(\n call: LinkPageCall,\n spec: PaginationSpec,\n args?: OperationArgs,\n init?: RequestOptions\n): AsyncGenerator<TItem> {\n for await (const page of pagesByLink(call, args, init)) {\n const pageItems = resolvePointer(page, spec.items);\n if (Array.isArray(pageItems)) yield* pageItems as TItem[];\n }\n}\n";
|
|
@@ -23,7 +23,7 @@ export declare const RUNTIME_SOURCES_STRIPPED: {
|
|
|
23
23
|
readonly 'multipart.ts': "/**\n * Serialize a plain object into `FormData` for a typed `multipart/form-data` body\n * (capability module — wired into `createClient`, never imported by the send core).\n * `Blob`/`File` and strings pass through; `Date`s become ISO strings; arrays append\n * one field per item; other objects are JSON-encoded; everything else is stringified.\n * `undefined`/`null` entries are skipped.\n */\nfunction toFormData(body: Record<string, unknown>): FormData {\n const fd = new FormData();\n const append = (key: string, value: unknown): void => {\n if (value === undefined || value === null) return;\n if (value instanceof Blob || typeof value === 'string') fd.append(key, value);\n else if (value instanceof Date) fd.append(key, value.toISOString());\n else if (Object(value) === value) fd.append(key, JSON.stringify(value));\n else fd.append(key, String(value));\n };\n for (const [key, value] of Object.entries(body)) {\n if (Array.isArray(value)) for (const item of value) append(key, item);\n else append(key, value);\n }\n return fd;\n}";
|
|
24
24
|
readonly 'auth.ts': "/** Resolve a credential: a literal passes through; a function is awaited per request. */\nasync function resolveToken(provider: TokenProvider): Promise<string> {\n return typeof provider === 'function' ? await provider() : provider;\n}\n\n/** UTF-8-safe base64: bare `btoa` throws on non-Latin-1 credentials (RFC 7617 allows UTF-8). */\nfunction encodeBase64(text: string): string {\n let binary = '';\n for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\n/** Whether a credential for this scheme is configured on the instance. */\nfunction isConfigured(scheme: SecuritySpec, config: ClientConfig): boolean {\n if (scheme.kind === 'apiKey') return config.auth?.apiKey?.[scheme.scheme] !== undefined;\n if (scheme.kind === 'bearer') return config.auth?.bearer !== undefined;\n return config.auth?.basic !== undefined;\n}\n\n/**\n * Build the auth headers/query for one operation's `security` OR-alternatives from the\n * instance credentials (`config.auth`) — capability module, wired into `createClient`.\n * The first alternative whose schemes (an AND-set) are all configured is applied, so\n * \"bearer OR apiKey\" works with either credential and never sends both. When none is\n * fully configured, the first alternative's configured schemes are still sent (the\n * server rejects the request, mirroring the previous behavior).\n * Cookie-borne apiKeys fold into a single `Cookie` header joined with `; `.\n */\nasync function resolveAuth(\n security: readonly (readonly SecuritySpec[])[],\n config: ClientConfig\n): Promise<{ headers: Record<string, string>; query: Record<string, string> }> {\n const alternative =\n security.find((schemes) => schemes.every((scheme) => isConfigured(scheme, config))) ??\n security[0] ??\n [];\n const headers: Record<string, string> = {};\n const query: Record<string, string> = {};\n const cookies: string[] = [];\n for (const scheme of alternative) {\n if (scheme.kind === 'apiKey') {\n const provider = config.auth?.apiKey?.[scheme.scheme];\n if (provider === undefined) continue;\n const value = await resolveToken(provider);\n if (scheme.in === 'header') headers[scheme.name] = value;\n else if (scheme.in === 'query') query[scheme.name] = value;\n // Cookie values may contain reserved characters (`;`, `=`, space, …); percent-encode\n // so the credential can't break the `Cookie` header syntax.\n else cookies.push(`${scheme.name}=${encodeURIComponent(value)}`);\n } else if (scheme.kind === 'bearer') {\n const provider = config.auth?.bearer;\n if (provider !== undefined) headers.Authorization = `Bearer ${await resolveToken(provider)}`;\n } else {\n const basic = config.auth?.basic;\n if (basic !== undefined) {\n headers.Authorization = `Basic ${encodeBase64(`${basic.username}:${basic.password}`)}`;\n }\n }\n }\n if (cookies.length > 0) headers.Cookie = cookies.join('; ');\n return { headers, query };\n}";
|
|
25
25
|
readonly 'setup.ts': "/**\n * Merge a publisher's baked setup (`defineClientSetup({...})`) with the app's config:\n * app config fields win per-field over baked defaults, while middleware composes —\n * baked middleware runs first, then the app's.\n */\nexport function mergeSetup(\n setup: { config?: ClientConfig; middleware?: Middleware[] } | undefined,\n config: ClientConfig = {}\n): ClientConfig {\n return {\n ...setup?.config,\n ...config,\n middleware: [...(setup?.middleware ?? []), ...(config.middleware ?? [])],\n };\n}";
|
|
26
|
-
readonly 'send.ts': "/**\n * Optional behaviors the send core can use but never statically imports — wired by\n * `createClient` (the same seam the future inline-mode assembler relies on).\n */\ntype SendCapabilities = {\n /** Serialize a typed multipart body (a plain object) to FormData. */\n serializeMultipart?: (body: Record<string, unknown>) => FormData;\n};\n\n/**\n * Normalize a caller's `HeadersInit` (plain record, `Headers` instance, or entry pairs)\n * to a plain record — spreading a `Headers` or an array contributes no entries.\n */\nfunction toHeaderRecord(headers: HeadersInit | undefined): Record<string, string> {\n if (headers === undefined) return {};\n if (headers instanceof Headers) {\n const record: Record<string, string> = {};\n headers.forEach((value, key) => {\n record[key] = value;\n });\n return record;\n }\n if (Array.isArray(headers)) return Object.fromEntries(headers);\n return headers;\n}\n\n/**\n * The effective middleware chain for a request: the single `onRequest`/`onResponse`/\n * `onError` config hooks as one implicit first middleware, then `config.middleware`.\n */\nfunction middlewareChain(config: ClientConfig): Middleware[] {\n const single =\n config.onRequest || config.onResponse || config.onError\n ? [{ onRequest: config.onRequest, onResponse: config.onResponse, onError: config.onError }]\n : [];\n return [...single, ...(config.middleware ?? [])];\n}\n\n/**\n * The fetch core shared by every operation: default + config + per-call headers, the\n * `onRequest` chain (BEFORE body serialization, so mutations are sent), body\n * serialization (JSON, or FormData via the multipart capability), the retry loop\n * (idempotent-only defaults, `Retry-After`, abandoned-body drain), and the reverse\n * `onResponse` onion. Returns the final response plus the request context.\n */\nasync function send(\n config: ClientConfig,\n op: OperationContext,\n url: string,\n init: RequestOptions,\n body: unknown | undefined,\n bodySpec: { contentType: string; multipart?: boolean } | undefined,\n caps: SendCapabilities,\n accept = 'application/json'\n): Promise<{ response: Response; context: RequestContext }> {\n const { retry: callRetry, timeout: callTimeout, idempotencyKey: callKey, ...fetchInit } = init;\n const retry: RetryConfig = { ...config.retry, ...callRetry };\n const timeout = callTimeout ?? config.timeout;\n const idempotency = callKey ?? config.idempotencyKey;\n const extra = typeof config.headers === 'function' ? await config.headers() : config.headers;\n const headers: Record<string, string> = {\n Accept: accept,\n ...extra,\n ...toHeaderRecord(fetchInit.headers),\n };\n const method = (fetchInit.method ?? 'GET').toUpperCase();\n // One stable key per LOGICAL call — set before the retry loop so every attempt\n // re-sends the same key; a caller-provided header always wins.\n if (\n idempotency !== undefined &&\n idempotency !== false &&\n (method === 'POST' || method === 'PATCH') &&\n !('Idempotency-Key' in headers) &&\n !('idempotency-key' in headers)\n ) {\n headers['Idempotency-Key'] =\n typeof idempotency === 'string'\n ? idempotency\n : typeof idempotency === 'function'\n ? idempotency()\n : crypto.randomUUID();\n }\n // Client identification for the API owner's telemetry — never in browsers, where a\n // custom header would force a CORS preflight the API may not allow.\n if (\n typeof config.clientHeader === 'string' &&\n typeof document === 'undefined' &&\n !('X-Redocly-Client' in headers) &&\n !('x-redocly-client' in headers)\n ) {\n headers['X-Redocly-Client'] = config.clientHeader;\n }\n const context: RequestContext = {\n url,\n method: fetchInit.method ?? 'GET',\n headers,\n body,\n operation: op,\n };\n const middleware = middlewareChain(config);\n for (const mw of middleware) if (mw.onRequest) await mw.onRequest(context);\n // Serialize AFTER onRequest so body mutations (case conversion, enveloping, signing) take effect.\n let payload: BodyInit | undefined;\n if (context.body !== undefined) {\n const value = context.body;\n const isBinary =\n value instanceof Blob ||\n value instanceof ArrayBuffer ||\n ArrayBuffer.isView(value as ArrayBufferView);\n const isFormData = typeof FormData !== 'undefined' && value instanceof FormData;\n const isURLSearchParams = value instanceof URLSearchParams;\n if (isFormData || isURLSearchParams || isBinary || typeof value === 'string') {\n payload = value as BodyInit;\n } else if (bodySpec?.multipart === true) {\n if (!caps.serializeMultipart) {\n throw new Error('Multipart capability not wired: cannot serialize the request body');\n }\n payload = caps.serializeMultipart(value as Record<string, unknown>);\n } else {\n payload = JSON.stringify(value);\n if (!
|
|
26
|
+
readonly 'send.ts': "/**\n * Optional behaviors the send core can use but never statically imports — wired by\n * `createClient` (the same seam the future inline-mode assembler relies on).\n */\ntype SendCapabilities = {\n /** Serialize a typed multipart body (a plain object) to FormData. */\n serializeMultipart?: (body: Record<string, unknown>) => FormData;\n};\n\n/**\n * Normalize a caller's `HeadersInit` (plain record, `Headers` instance, or entry pairs)\n * to a plain record — spreading a `Headers` or an array contributes no entries.\n */\nfunction toHeaderRecord(headers: HeadersInit | undefined): Record<string, string> {\n if (headers === undefined) return {};\n if (headers instanceof Headers) {\n const record: Record<string, string> = {};\n headers.forEach((value, key) => {\n record[key] = value;\n });\n return record;\n }\n if (Array.isArray(headers)) return Object.fromEntries(headers);\n return headers;\n}\n\n/**\n * The effective middleware chain for a request: the single `onRequest`/`onResponse`/\n * `onError` config hooks as one implicit first middleware, then `config.middleware`.\n */\nfunction middlewareChain(config: ClientConfig): Middleware[] {\n const single =\n config.onRequest || config.onResponse || config.onError\n ? [{ onRequest: config.onRequest, onResponse: config.onResponse, onError: config.onError }]\n : [];\n return [...single, ...(config.middleware ?? [])];\n}\n\n/**\n * The fetch core shared by every operation: default + config + per-call headers, the\n * `onRequest` chain (BEFORE body serialization, so mutations are sent), body\n * serialization (JSON, or FormData via the multipart capability; a stream passes\n * through in one attempt), the retry loop\n * (idempotent-only defaults, `Retry-After`, abandoned-body drain), and the reverse\n * `onResponse` onion. Returns the final response plus the request context.\n */\nasync function send(\n config: ClientConfig,\n op: OperationContext,\n url: string,\n init: RequestOptions,\n body: unknown | undefined,\n bodySpec: { contentType: string; multipart?: boolean } | undefined,\n caps: SendCapabilities,\n accept = 'application/json'\n): Promise<{ response: Response; context: RequestContext }> {\n const { retry: callRetry, timeout: callTimeout, idempotencyKey: callKey, ...fetchInit } = init;\n const retry: RetryConfig = { ...config.retry, ...callRetry };\n const timeout = callTimeout ?? config.timeout;\n const idempotency = callKey ?? config.idempotencyKey;\n const extra = typeof config.headers === 'function' ? await config.headers() : config.headers;\n const headers: Record<string, string> = {\n Accept: accept,\n ...extra,\n ...toHeaderRecord(fetchInit.headers),\n };\n const method = (fetchInit.method ?? 'GET').toUpperCase();\n // One stable key per LOGICAL call — set before the retry loop so every attempt\n // re-sends the same key; a caller-provided header always wins.\n if (\n idempotency !== undefined &&\n idempotency !== false &&\n (method === 'POST' || method === 'PATCH') &&\n !('Idempotency-Key' in headers) &&\n !('idempotency-key' in headers)\n ) {\n headers['Idempotency-Key'] =\n typeof idempotency === 'string'\n ? idempotency\n : typeof idempotency === 'function'\n ? idempotency()\n : crypto.randomUUID();\n }\n // Client identification for the API owner's telemetry — never in browsers, where a\n // custom header would force a CORS preflight the API may not allow.\n if (\n typeof config.clientHeader === 'string' &&\n typeof document === 'undefined' &&\n !('X-Redocly-Client' in headers) &&\n !('x-redocly-client' in headers)\n ) {\n headers['X-Redocly-Client'] = config.clientHeader;\n }\n const context: RequestContext = {\n url,\n method: fetchInit.method ?? 'GET',\n headers,\n body,\n operation: op,\n };\n const middleware = middlewareChain(config);\n for (const mw of middleware) if (mw.onRequest) await mw.onRequest(context);\n // Serialize AFTER onRequest so body mutations (case conversion, enveloping, signing) take effect.\n let payload: BodyInit | undefined;\n let isStream = false;\n if (context.body !== undefined) {\n const value = context.body;\n const isBinary =\n value instanceof Blob ||\n value instanceof ArrayBuffer ||\n ArrayBuffer.isView(value as ArrayBufferView);\n const isFormData = typeof FormData !== 'undefined' && value instanceof FormData;\n const isURLSearchParams = value instanceof URLSearchParams;\n isStream =\n (typeof ReadableStream !== 'undefined' && value instanceof ReadableStream) ||\n (value !== null &&\n typeof (value as Partial<AsyncIterable<unknown>>)[Symbol.asyncIterator] === 'function');\n const hasContentType = 'Content-Type' in context.headers || 'content-type' in context.headers;\n if (isFormData || isURLSearchParams || isBinary || isStream || typeof value === 'string') {\n payload = value as BodyInit;\n // `fetch` derives the type of FormData and URLSearchParams, boundary included, and a\n // multipart boundary is only the caller's to set. Any other pass-through body gets\n // the declared type when the caller set none.\n const declared = bodySpec?.contentType;\n if (\n !isFormData &&\n !isURLSearchParams &&\n !hasContentType &&\n declared !== undefined &&\n !declared.toLowerCase().includes('multipart')\n ) {\n context.headers['Content-Type'] = declared;\n }\n } else if (bodySpec?.multipart === true) {\n if (!caps.serializeMultipart) {\n throw new Error('Multipart capability not wired: cannot serialize the request body');\n }\n payload = caps.serializeMultipart(value as Record<string, unknown>);\n } else {\n payload = JSON.stringify(value);\n if (!hasContentType) {\n // The spec's declared request content type (e.g. application/merge-patch+json).\n context.headers['Content-Type'] = bodySpec?.contentType ?? 'application/json';\n }\n }\n }\n const doFetch = config.fetch ?? fetch;\n // A stream is consumed by the first attempt and cannot be replayed, so it never retries.\n const maxAttempts = isStream ? 1 : 1 + (retry.retries ?? 0);\n const retryOn = retry.retryOn ?? defaultRetryOn;\n const signal = fetchInit.signal ?? undefined;\n\n let attempt = 0;\n while (true) {\n attempt++;\n if (signal?.aborted) throw abortError(signal);\n // A fresh timeout budget per attempt; the caller's signal still wins the race.\n // The composed signal also governs reading the response body.\n const attemptSignal = timeout\n ? signal\n ? AbortSignal.any([signal, AbortSignal.timeout(timeout)])\n : AbortSignal.timeout(timeout)\n : signal;\n let response: Response;\n try {\n response = await doFetch(context.url, {\n ...fetchInit,\n // `fetch` requires it for a stream body.\n ...(isStream ? { duplex: 'half' } : {}),\n signal: attemptSignal,\n method: context.method,\n headers: context.headers,\n body: payload,\n });\n } catch (error) {\n if (\n attempt < maxAttempts &&\n !signal?.aborted &&\n (await retryOn({ attempt, request: context, error }))\n ) {\n await sleep(retryDelay(retry, attempt, null), signal);\n continue;\n }\n // Our timeout fired (never the caller's own abort — that rethrows untouched):\n // wrap the bare DOMException with the context a log line needs.\n if (\n timeout &&\n !signal?.aborted &&\n error instanceof DOMException &&\n error.name === 'TimeoutError'\n ) {\n throw new TimeoutError(op.id, timeout, attempt);\n }\n throw error;\n }\n // Reverse order: the last-registered middleware wraps closest to the network (onion).\n for (let i = middleware.length - 1; i >= 0; i--) {\n const onResponse = middleware[i].onResponse;\n if (onResponse) {\n const replaced = await onResponse(response, context);\n if (replaced && replaced !== response) {\n // Cancel the abandoned original's body — like the retry path, an unread body\n // keeps its connection checked out under Node/undici.\n await response.body?.cancel().catch(() => undefined);\n response = replaced;\n }\n }\n }\n if (\n !response.ok &&\n attempt < maxAttempts &&\n !signal?.aborted &&\n (await retryOn({ attempt, request: context, response }))\n ) {\n const retryAfter = response.headers.get('retry-after');\n // Drain the abandoned response body before the next attempt: an unread body\n // keeps the connection checked out (and can stall the pool) under Node/undici\n // and other strict HTTP clients. Ignore errors (e.g. a middleware already read it).\n await response.body?.cancel().catch(() => undefined);\n await sleep(retryDelay(retry, attempt, retryAfter), signal);\n continue;\n }\n return { response, context };\n }\n}";
|
|
27
27
|
readonly 'sse.ts': "/**\n * A frame delimiter: two consecutive line terminators (each CR, LF, or CRLF, per the SSE\n * spec — so mixed endings like `\\n\\r\\n` are valid boundaries, not just matching pairs).\n */\nconst FRAME_DELIMITER = /(?:\\r\\n|\\r|\\n){2}/;\n\n/**\n * A terminally malformed event stream — unparseable JSON `data` or an unbounded frame.\n * A stable bad payload, not a dropped connection, so the stream never reconnects on it.\n */\nclass SseParseError extends Error {}\n\n/**\n * Consume a `text/event-stream` operation as typed events (capability module — wired\n * into `createClient`). Auto-reconnects on dropped connections, resuming from the last\n * seen event id via `Last-Event-ID` (backoff: the server's `retry:` value, then\n * `reconnectDelay`, then 1s — exponential with jitter, capped at 30s). A clean stream\n * end flushes a trailing frame and finishes; `break`/abort end the iterator cleanly.\n */\nasync function* sse<T>(\n config: ClientConfig,\n op: OperationContext,\n prepare: () => Promise<{ url: string; init: SseOptions; body?: unknown }>,\n dataKind: 'json' | 'text' = 'text'\n): AsyncGenerator<ServerSentEvent<T>> {\n let lastEventId: string | undefined;\n let serverRetry: number | undefined;\n let failures = 0;\n while (true) {\n // Re-prepare each attempt so a refresh-style TokenProvider yields a fresh credential\n // on reconnect (the auth is baked into `url` query + `init.headers`). `reconnect`,\n // `reconnectDelay`, and `signal` come from the caller's original options unchanged.\n const { url, init, body: requestBody } = await prepare();\n const { reconnect = true, reconnectDelay, ...rest } = init;\n const signal = rest.signal ?? undefined;\n if (signal?.aborted) return;\n const headers: Record<string, string> = {\n Accept: 'text/event-stream',\n ...toHeaderRecord(rest.headers),\n };\n const sendHeaders =\n lastEventId === undefined ? headers : { ...headers, 'Last-Event-ID': lastEventId };\n try {\n const { response } = await send(\n config,\n op,\n url,\n // `timeout: 0` opts the stream out of a config-level timeout — an event stream\n // is long-lived by design and must not be severed after N milliseconds.\n { ...rest, method: rest.method ?? 'GET', headers: sendHeaders, timeout: 0 },\n requestBody,\n undefined,\n {}\n );\n if (!response.ok) {\n const errorBody = await readError(response);\n throw new ApiError(url, response.status, response.statusText, errorBody);\n }\n failures = 0;\n const body = response.body;\n if (!body) return;\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n try {\n while (true) {\n const { done, value } = await reader.read();\n buffer += done ? decoder.decode() : decoder.decode(value, { stream: true });\n let match: RegExpExecArray | null;\n while ((match = FRAME_DELIMITER.exec(buffer)) !== null) {\n const raw = buffer.slice(0, match.index);\n buffer = buffer.slice(match.index + match[0].length);\n const event = parseSseFrame(raw, dataKind);\n if (event) {\n if (event.id !== undefined) lastEventId = event.id;\n if (event.retry !== undefined) serverRetry = event.retry;\n yield event as ServerSentEvent<T>;\n }\n }\n if (done) {\n // Stream closed cleanly. Flush a final event that arrived without a trailing\n // delimiter, then finish — a clean end is not a dropped connection, so do not reconnect.\n const event = buffer.length > 0 ? parseSseFrame(buffer, dataKind) : undefined;\n if (event) {\n if (event.id !== undefined) lastEventId = event.id;\n if (event.retry !== undefined) serverRetry = event.retry;\n yield event as ServerSentEvent<T>;\n }\n return;\n }\n // Bound memory: a server that never sends a frame delimiter would otherwise\n // grow `buffer` without limit. 1 MiB is far above any real SSE frame.\n if (buffer.length > 1048576) {\n throw new SseParseError('SSE frame exceeded 1048576 characters without a delimiter');\n }\n }\n } finally {\n await reader.cancel().catch(() => undefined);\n }\n } catch (error) {\n if (signal?.aborted) return;\n // A non-OK HTTP response (4xx/5xx) or an unparseable JSON payload is a definitive\n // error, not a transient drop — surface it instead of reconnecting in a loop (a\n // stable bad payload would otherwise reconnect forever).\n if (error instanceof ApiError || error instanceof SseParseError) throw error;\n // A transport failure (connect/DNS/reset) when opening the request, or a mid-stream\n // read error, is a dropped connection: fall through to backoff/reconnect when enabled.\n if (!reconnect) throw error;\n }\n // Only the swallowed-drop path reaches here: reconnect is on and the signal not aborted.\n failures++;\n const base = serverRetry ?? reconnectDelay ?? 1000;\n const delay = Math.min(base * Math.pow(2, failures - 1), 30_000);\n try {\n await sleep(Math.random() * delay, signal);\n } catch {\n return; // sleep rejects only on abort — end the iterator cleanly\n }\n }\n}\n\n/** Parse one raw SSE frame (its lines) into an event; returns undefined for comment-only frames. */\nfunction parseSseFrame(\n raw: string,\n dataKind: 'json' | 'text'\n): ServerSentEvent<unknown> | undefined {\n let event: string | undefined;\n const dataLines: string[] = [];\n let id: string | undefined;\n let retry: number | undefined;\n let sawField = false;\n for (const line of raw.split(/\\r\\n|\\n|\\r/)) {\n if (line === '' || line.startsWith(':')) continue;\n const colon = line.indexOf(':');\n const field = colon === -1 ? line : line.slice(0, colon);\n let val = colon === -1 ? '' : line.slice(colon + 1);\n if (val.startsWith(' ')) val = val.slice(1);\n sawField = true;\n if (field === 'event') event = val;\n else if (field === 'data') dataLines.push(val);\n else if (field === 'id') id = val;\n else if (field === 'retry') {\n // ASCII digits only, per the EventSource spec — anything else is ignored\n // (`Number('')` is 0 and would zero the reconnect backoff).\n if (/^\\d+$/.test(val)) retry = Number(val);\n }\n }\n if (!sawField) return undefined;\n const dataText = dataLines.join('\\n');\n let data: unknown = dataText;\n if (dataKind === 'json' && dataText !== '') {\n try {\n data = JSON.parse(dataText);\n } catch (error) {\n throw new SseParseError(\n `Failed to parse SSE event data as JSON: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n }\n return { event, data, id, retry };\n}";
|
|
28
28
|
readonly 'create-client.ts': "/**\n * The optional behaviors `createClientCore` can dispatch to but never statically\n * imports. The package's public `createClient` wires the full set; the future\n * inline-mode assembler wires only the capabilities a spec needs.\n */\ntype Capabilities = SendCapabilities & {\n resolveAuth?: (\n security: readonly (readonly SecuritySpec[])[],\n config: ClientConfig\n ) => Promise<{ headers: Record<string, string>; query: Record<string, string> }>;\n sse?: (\n config: ClientConfig,\n op: OperationContext,\n // Re-preparing per (re)connect (not a frozen url/init) lets a refresh-style\n // TokenProvider issue a fresh credential after a dropped stream reconnects.\n prepare: () => Promise<{ url: string; init: SseOptions }>,\n dataKind: 'json' | 'text'\n ) => AsyncGenerator<ServerSentEvent<unknown>>;\n paginate?: {\n pages: (\n call: (args?: OperationArgs, init?: RequestOptions) => Promise<unknown>,\n spec: PaginationSpec,\n args?: OperationArgs,\n init?: RequestOptions\n ) => AsyncGenerator<unknown>;\n items: (\n call: (args?: OperationArgs, init?: RequestOptions) => Promise<unknown>,\n spec: PaginationSpec,\n args?: OperationArgs,\n init?: RequestOptions\n ) => AsyncGenerator<unknown>;\n // The `link`-style iterators need the raw `Link` header + page URL, which the\n // parsed-page call above cannot carry (the shape mirrors paginate's `LinkPageCall`).\n pagesByLink: (\n call: (\n args?: OperationArgs,\n init?: RequestOptions\n ) => Promise<{ page: unknown; linkHeader: string | null; url: string }>,\n args?: OperationArgs,\n init?: RequestOptions\n ) => AsyncGenerator<unknown>;\n itemsByLink: (\n call: (\n args?: OperationArgs,\n init?: RequestOptions\n ) => Promise<{ page: unknown; linkHeader: string | null; url: string }>,\n spec: PaginationSpec,\n args?: OperationArgs,\n init?: RequestOptions\n ) => AsyncGenerator<unknown>;\n };\n};\n\n/**\n * One call's inputs, namespaced by transport layer. `argsStyle: 'flat'` clients accept the\n * merged form instead (every parameter and body property at one level) — `namespaceArgs`\n * converts it to this shape before anything downstream reads it.\n */\ntype OperationArgs = {\n path?: Record<string, unknown>;\n query?: Record<string, QueryValue>;\n body?: unknown;\n headers?: Record<string, unknown>;\n cookies?: Record<string, unknown>;\n} & Record<string, unknown>;\n\n/** The five layer keys, and the only top-level keys a namespaced call may carry. */\nconst LAYERS: readonly string[] = ['path', 'query', 'body', 'headers', 'cookies'];\n\n/** Where a declared parameter's `in` value puts it. */\nconst LAYER_OF: Record<string, 'path' | 'query' | 'headers' | 'cookies'> = {\n path: 'path',\n query: 'query',\n header: 'headers',\n cookie: 'cookies',\n};\n\n/**\n * Merged (`argsStyle: 'flat'`) args → the namespaced shape. A key that names a declared\n * parameter goes to that parameter's layer; anything else is a property of the request\n * body, which is how a flat call spells an object body. `body` stays reserved for the\n * operations a flat call cannot merge (an array, a scalar, or a binary body).\n */\nfunction namespaceArgs(op: OperationDescriptor, args: OperationArgs): OperationArgs {\n const layers: Record<string, Record<string, unknown>> = {};\n let body: unknown;\n let properties: Record<string, unknown> | undefined;\n const layerOfParam = new Map((op.params ?? []).map((param) => [param.name, param.in]));\n for (const [key, value] of Object.entries(args)) {\n const layer = LAYER_OF[layerOfParam.get(key) ?? ''];\n if (layer !== undefined) {\n (layers[layer] ??= {})[key] = value;\n } else if (key === 'body' && op.body !== undefined) {\n body = value;\n } else if (op.body !== undefined) {\n (properties ??= {})[key] = value;\n } else {\n throw new TypeError(\n `Unknown argument \"${key}\" for operation \"${op.id}\": it names no declared parameter, and the operation takes no request body.`\n );\n }\n }\n const namespaced: OperationArgs = {};\n if (layers.path) namespaced.path = layers.path;\n // The flat surface types every query value, so the collected bag is one by construction.\n if (layers.query) namespaced.query = layers.query as Record<string, QueryValue>;\n if (layers.headers) namespaced.headers = layers.headers;\n if (layers.cookies) namespaced.cookies = layers.cookies;\n if (properties !== undefined) namespaced.body = properties;\n else if (body !== undefined) namespaced.body = body;\n return namespaced;\n}\n\n/** The response reader implied by the descriptor (before any per-call `parseAs` override). */\n/**\n * The `Accept` header matching how the response will be read — a blob/text operation\n * must not ask for `application/json` (a content-negotiating server would 406 or\n * answer with a JSON error body instead of the payload). Caller `init.headers` and\n * `config.headers` still override.\n */\nfunction acceptFor(kind: ParseAs | 'void'): string {\n if (kind === 'text') return 'text/*';\n if (kind === 'blob' || kind === 'arrayBuffer' || kind === 'stream' || kind === 'formData') {\n return '*/*';\n }\n return 'application/json'; // json | auto | void\n}\n\nfunction kindFor(op: OperationDescriptor): ParseAs | 'void' {\n if (op.responseKind === 'void' || op.responseKind === 'blob' || op.responseKind === 'text') {\n return op.responseKind;\n }\n return 'auto';\n}\n\n/**\n * The call's inputs in namespaced form, converting first on a flat-style client. An\n * operation the generator marked `argsStyle: 'grouped'` is already namespaced — its names\n * could not be merged, so its input type never offered the flat shape.\n */\nfunction inputOf(\n op: OperationDescriptor,\n args: OperationArgs,\n config: ClientConfig\n): OperationArgs {\n const merged = config.argsStyle === 'flat' && op.argsStyle !== 'grouped';\n return merged ? namespaceArgs(op, args) : args;\n}\n\n/** Route the namespaced args to the request pieces. */\nfunction splitArgs(op: OperationDescriptor, args: OperationArgs) {\n // An unknown layer key can only be a bug (usually flat-style args on a namespaced\n // client). TypeScript catches it, but a transpiler that skips type-checking would\n // otherwise ship a request that silently drops the value — fail the call loudly.\n for (const key of Object.keys(args)) {\n if (!LAYERS.includes(key)) {\n throw new TypeError(\n `Unknown argument \"${key}\" for operation \"${op.id}\". Inputs are grouped by layer: ${LAYERS.join(', ')}.`\n );\n }\n }\n return {\n path: args.path ?? {},\n query: args.query,\n body: args.body,\n headers: args.headers,\n cookies: args.cookies,\n };\n}\n\n/**\n * The query-serialization hints for the descriptor's query params. A spec is built only\n * when the param deviates from the OpenAPI defaults (`form` + `explode: true`, encoded),\n * and always fully resolved — so `explode: false` or `allowReserved` alone (no `style`)\n * are honored, and an omitted `explode` keeps the exploded default.\n */\nfunction queryStyles(op: OperationDescriptor): Record<string, QueryStyle> | undefined {\n let styles: Record<string, QueryStyle> | undefined;\n for (const param of op.params ?? []) {\n if (param.in !== 'query') continue;\n const deviates =\n (param.style !== undefined && param.style !== 'form') ||\n param.explode === false ||\n param.allowReserved === true;\n if (!deviates) continue;\n styles ??= {};\n styles[param.name] = {\n style: param.style ?? 'form',\n explode: param.explode ?? true,\n allowReserved: param.allowReserved,\n };\n }\n return styles;\n}\n\n/** Stringify caller-supplied extra headers, skipping empty entries. */\nfunction stringHeaders(headers: Record<string, unknown> | undefined): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers ?? {})) {\n if (value !== undefined && value !== null) out[key] = String(value);\n }\n return out;\n}\n\n/** Build the request pieces an attempt needs: the final URL and the merged per-call init. */\nasync function prepareRequest(\n config: ClientConfig,\n op: OperationDescriptor,\n args: OperationArgs,\n init: RequestOptions | SseOptions,\n caps: Capabilities\n): Promise<{ url: string; init: RequestOptions; body: unknown }> {\n const { path, query, body, headers, cookies } = splitArgs(op, args);\n const authed: { headers: Record<string, string>; query: Record<string, string> } =\n op.security?.length && caps.resolveAuth\n ? await caps.resolveAuth(op.security, config)\n : { headers: {}, query: {} };\n // Cookie params join the auth-injected cookies in one `Cookie` header (values\n // percent-encoded, like auth cookies). Server-side only — browsers own the header.\n const cookiePairs = Object.entries(cookies ?? {})\n .filter(([, value]) => value !== undefined && value !== null)\n .map(([cookieName, value]) => `${cookieName}=${encodeURIComponent(String(value))}`);\n if (cookiePairs.length > 0) {\n authed.headers.Cookie = [authed.headers.Cookie, ...cookiePairs].filter(Boolean).join('; ');\n }\n const fullQuery: Record<string, QueryValue> = { ...query, ...authed.query };\n const url = buildUrl(\n config.serverUrl ?? '',\n substitutePath(op.path, path),\n Object.keys(fullQuery).length > 0 ? fullQuery : undefined,\n queryStyles(op)\n );\n const mergedInit: RequestOptions = {\n ...init,\n method: op.method.toUpperCase(),\n // Precedence, lowest → highest (later spreads win): injected auth → explicit\n // header params → caller `init.headers` — the caller always overrides both.\n headers: {\n ...authed.headers,\n ...stringHeaders(headers),\n ...toHeaderRecord(init.headers),\n },\n };\n return { url, init: mergedInit, body };\n}\n\n/** Coerce a single declared response header value; omit when absent or unparsable. */\nfunction coerceResponseHeader(\n raw: string | null,\n type: ResponseHeaderSpec['type']\n): string | number | boolean | undefined {\n if (raw === null) return undefined;\n if (type === 'number') {\n if (raw.trim() === '') return undefined;\n const value = Number(raw);\n return Number.isFinite(value) ? value : undefined;\n }\n if (type === 'boolean') {\n const value = raw.trim().toLowerCase();\n if (value === 'true') return true;\n if (value === 'false') return false;\n return undefined;\n }\n return raw;\n}\n\n/** Build the camelCase declared-header bag for a throw-mode envelope. */\nfunction readEnvelopeHeaders(\n response: Response,\n specs: readonly ResponseHeaderSpec[] | undefined\n): Record<string, string | number | boolean> {\n const headers: Record<string, string | number | boolean> = {};\n for (const spec of specs ?? []) {\n const value = coerceResponseHeader(response.headers.get(spec.name), spec.type);\n if (value !== undefined) headers[spec.key] = value;\n }\n return headers;\n}\n\n/** One non-SSE call: send, then branch on the configured error mode. */\nasync function execute(\n config: ClientConfig,\n op: OperationDescriptor,\n args: OperationArgs,\n init: RequestOptions,\n caps: Capabilities\n): Promise<unknown> {\n const prepared = await prepareRequest(config, op, args, init, caps);\n const opCtx: OperationContext = { id: op.id, path: op.path, tags: [...(op.tags ?? [])] };\n // `parseAs` / `envelope` are client options, not fetch RequestInit fields.\n const { parseAs, envelope, ...sendInit } = prepared.init;\n const readKind = parseAs ?? kindFor(op);\n const { response, context } = await send(\n config,\n opCtx,\n prepared.url,\n sendInit,\n prepared.body,\n op.body,\n caps,\n acceptFor(readKind)\n );\n if (config.errorMode === 'result') {\n if (!response.ok) {\n return { data: undefined, error: await readError(response), response };\n }\n return { data: await parse(response, readKind), error: undefined, response };\n }\n if (!response.ok) {\n let error: globalThis.Error = new ApiError(\n context.url,\n response.status,\n response.statusText,\n await readError(response)\n );\n // Thread the error through each middleware's onError in turn (each may replace it).\n for (const mw of middlewareChain(config)) {\n if (mw.onError) error = await mw.onError(error as ApiErrorLike, context);\n }\n throw error;\n }\n const data = await parse(response, readKind);\n if (envelope === true) {\n return {\n data,\n headers: readEnvelopeHeaders(response, op.responseHeaders),\n response,\n };\n }\n return data;\n}\n\n/** The paginate capability, or a descriptive throw when a paginated op is iterated unwired. */\nfunction paginateCapability(caps: Capabilities, op: OperationDescriptor) {\n if (!caps.paginate) {\n throw new Error(`Pagination capability not wired: cannot iterate operation \"${op.id}\"`);\n }\n return caps.paginate;\n}\n\n/**\n * The per-page call the iterators drive: the method itself in throw mode; in result\n * mode a wrapper that unwraps the `{ data, error, response }` envelope — the page\n * pointers are data-rooted — rethrowing a failed page as `ApiError` (iteration is\n * error-mode-agnostic; the throw-mode-only `onError` middleware hook is not invoked).\n */\nfunction pageCall(\n method: (args?: OperationArgs, init?: RequestOptions) => Promise<unknown>,\n config: ClientConfig\n) {\n const callWithoutEnvelope = (args?: OperationArgs, init?: RequestOptions) => {\n if (!init || init.envelope === undefined) return method(args, init);\n const { envelope: _envelope, ...pageInit } = init;\n return method(args, pageInit);\n };\n if (config.errorMode !== 'result') return callWithoutEnvelope;\n return async (args?: OperationArgs, init?: RequestOptions) => {\n const envelope = (await callWithoutEnvelope(args, init)) as {\n data: unknown;\n error: unknown;\n response: Response;\n };\n // Failure is `!response.ok` — NOT `data === undefined`: a successful bodyless page\n // (204/void) also parses to undefined data, and a failed page's `error` can be\n // undefined too (unreadable body). The pointers then miss on the undefined data\n // and iteration stops cleanly, which is the correct semantics for an empty page.\n if (!envelope.response.ok) {\n const { response } = envelope;\n throw new ApiError(response.url, response.status, response.statusText, envelope.error);\n }\n return envelope.data;\n };\n}\n\n/**\n * The per-page call the `link`-style iterators drive: like `execute`, but returning the\n * parsed page together with the raw `Link` header and the page's own URL (for resolving\n * a relative `rel=\"next\"` target). Error-mode-agnostic like all iteration: a failed\n * page throws `ApiError` even on result-mode clients.\n */\nfunction linkPageCall(config: ClientConfig, op: OperationDescriptor, caps: Capabilities) {\n return async (args: OperationArgs = {}, init: RequestOptions = {}) => {\n const prepared = await prepareRequest(config, op, args, init, caps);\n const { parseAs, envelope: _envelope, ...sendInit } = prepared.init;\n const readKind = parseAs ?? kindFor(op);\n const opCtx: OperationContext = { id: op.id, path: op.path, tags: [...(op.tags ?? [])] };\n const { response } = await send(\n config,\n opCtx,\n prepared.url,\n sendInit,\n prepared.body,\n op.body,\n caps,\n acceptFor(readKind)\n );\n if (!response.ok) {\n throw new ApiError(\n prepared.url,\n response.status,\n response.statusText,\n await readError(response)\n );\n }\n return {\n page: await parse(response, readKind),\n linkHeader: response.headers.get('link'),\n // Some `Response` implementations leave `url` empty (mocks, constructed responses).\n url: response.url === '' ? prepared.url : response.url,\n };\n };\n}\n\n/**\n * Build a typed instance client over operation descriptors: one real bound method per\n * operation (attached by a construction-time loop — no Proxy), plus the core members\n * (`configure`/`use`/`auth`), which are assigned AFTER the loop so they win any name\n * collision with an operation. All behavior dispatches through the capability seam.\n */\nfunction createClientCore<\n Ops extends OpsShape,\n Id extends string = string,\n Path extends string = string,\n Tag extends string = string,\n>(\n operations: Record<string, OperationDescriptor>,\n initial: ClientConfig<OperationContext<Id, Path, Tag>> = {},\n caps: Capabilities = {}\n): Client<Ops, OperationContext<Id, Path, Tag>> {\n // The literal-union narrowing is a compile-time DX contract only; internally the\n // runtime works with the base (string-typed) context. One cast at this boundary —\n // `ClientConfig<Narrow>` is not assignable to `ClientConfig` (middleware ctx\n // params are contravariant).\n const given = initial as ClientConfig;\n // Private mutable config; the middleware array is copied so `use()` never mutates the caller's.\n const config: ClientConfig = { ...given, middleware: [...(given.middleware ?? [])] };\n const client = {} as Record<string, unknown>;\n\n for (const [name, op] of Object.entries(operations)) {\n if (op.responseKind === 'sse') {\n const method = (given: OperationArgs = {}, init: SseOptions = {}) => {\n const args = inputOf(op, given, config);\n if (!caps.sse) {\n throw new Error(`SSE capability not wired: cannot stream operation \"${op.id}\"`);\n }\n const stream = caps.sse;\n return (async function* () {\n const opCtx: OperationContext = { id: op.id, path: op.path, tags: [...(op.tags ?? [])] };\n // A thunk the stream re-runs on every (re)connect, so auth (which `prepareRequest`\n // resolves) is refreshed per attempt rather than frozen at the first connect.\n const prepare = async () => {\n const prepared = await prepareRequest(config, op, args, init, caps);\n return { url: prepared.url, init: prepared.init as SseOptions, body: prepared.body };\n };\n yield* stream(config, opCtx, prepare, op.sseDataKind ?? 'text');\n })();\n };\n // Consumers key off the function reference (cache keys, `OPERATIONS[fn.name]`), so\n // each closure carries its operationId instead of an inferred binding name.\n // `operationId` is the explicit, minification-proof form of the same identity\n // (the SPEC operationId — `name` is the emitted key, which a collision may rename).\n Object.defineProperty(method, 'name', { value: name });\n Object.defineProperty(method, 'operationId', { value: op.id });\n client[name] = method;\n } else {\n // `raw` takes namespaced args; `method` is the public entry that accepts whichever\n // style the client was generated with. The iterators namespace once and then drive\n // `raw`, so a flat call is never converted twice.\n const raw = (args: OperationArgs = {}, init: RequestOptions = {}) =>\n execute(config, op, args, init, caps);\n const method = (args: OperationArgs = {}, init: RequestOptions = {}) =>\n raw(inputOf(op, args, config), init);\n Object.defineProperty(method, 'name', { value: name });\n Object.defineProperty(method, 'operationId', { value: op.id });\n const spec = op.pagination;\n // Paginated ops keep their one-shot call and gain `.pages`/`.items`, dispatching\n // through the capability seam (like SSE: absent capability throws descriptively).\n // Iteration is error-mode-agnostic: the iterators' pointers are data-rooted, so on\n // a result-mode client (`errorMode` is fixed at construction — `configure()`\n // ignores it) each page's envelope is unwrapped before it reaches the capability.\n // A failed page aborts iteration by throwing ApiError, even on result-mode\n // clients; the `onError` middleware hook (throw-mode-only) is not invoked.\n client[name] =\n spec === undefined\n ? method\n : spec.style === 'link'\n ? Object.assign(method, {\n pages: (args?: OperationArgs, init?: RequestOptions) =>\n paginateCapability(caps, op).pagesByLink(\n linkPageCall(config, op, caps),\n inputOf(op, args ?? {}, config),\n init\n ),\n items: (args?: OperationArgs, init?: RequestOptions) =>\n paginateCapability(caps, op).itemsByLink(\n linkPageCall(config, op, caps),\n spec,\n inputOf(op, args ?? {}, config),\n init\n ),\n })\n : Object.assign(method, {\n pages: (args?: OperationArgs, init?: RequestOptions) =>\n paginateCapability(caps, op).pages(\n pageCall(raw, config),\n spec,\n inputOf(op, args ?? {}, config),\n init\n ),\n items: (args?: OperationArgs, init?: RequestOptions) =>\n paginateCapability(caps, op).items(\n pageCall(raw, config),\n spec,\n inputOf(op, args ?? {}, config),\n init\n ),\n });\n }\n }\n\n // Core members are assigned AFTER the operation loop — they win over colliding op names.\n client.configure = (next: ClientConfig): void => {\n // `errorMode` and `argsStyle` are fixed at generate time (they shape the static types);\n // flipping either at runtime would silently desync the calls from `Client<Ops>`, so both\n // are ignored here.\n const { errorMode: _fixedMode, argsStyle: _fixedStyle, auth, ...rest } = next;\n Object.assign(config, rest);\n // `auth` merges into existing credentials (like the `auth.*` setters) rather than\n // replacing wholesale — so `configure({ auth: { bearer } })` keeps a previously set\n // basic/apiKey. `apiKey` merges per scheme.\n if (auth) {\n config.auth = {\n ...config.auth,\n ...auth,\n ...(auth.apiKey ? { apiKey: { ...config.auth?.apiKey, ...auth.apiKey } } : {}),\n };\n }\n };\n client.use = (...middleware: Middleware[]): void => {\n // Reassign (don't push) so a caller-provided `middleware` array isn't mutated.\n config.middleware = [...(config.middleware ?? []), ...middleware];\n };\n client.auth = {\n bearer(token: TokenProvider): void {\n config.auth = { ...config.auth, bearer: token };\n },\n basic(username: string, password: string): void {\n config.auth = { ...config.auth, basic: { username, password } };\n },\n apiKey(scheme: string, value: TokenProvider): void {\n config.auth = { ...config.auth, apiKey: { ...config.auth?.apiKey, [scheme]: value } };\n },\n };\n\n return client as Client<Ops, OperationContext<Id, Path, Tag>>;\n}";
|
|
29
29
|
readonly 'paginate.ts': "/**\n * Auto-pagination (capability module — wired into `createClient`, dispatched by the\n * method's `.pages()`/`.items()`): walk an operation's pages by advancing the descriptor's\n * `param` query parameter, per its `style`. The caller's args are never mutated — each\n * request gets a fresh `query` clone — and `init` is forwarded to every call.\n *\n * Iteration is error-mode-agnostic: `call` always resolves to the RAW page (on a\n * result-mode client the attachment unwraps the envelope first), so a failed page\n * aborts iteration by throwing `ApiError`, even on result-mode clients; the `onError`\n * middleware hook (throw-mode-only) is not invoked.\n */\n\n/**\n * Resolve an RFC 6901 JSON pointer (`~1` → `/`, `~0` → `~`) against a value.\n * The empty pointer is the whole document; anything else must start with `/`.\n * Returns `undefined` on any miss (bad token, absent key, non-object step) — never throws.\n */\nfunction resolvePointer(value: unknown, pointer: string): unknown {\n if (pointer === '') return value;\n if (!pointer.startsWith('/')) return undefined;\n let current = value;\n for (const token of pointer.slice(1).split('/')) {\n const key = token.replaceAll('~1', '/').replaceAll('~0', '~');\n if (Array.isArray(current)) {\n if (!/^(0|[1-9]\\d*)$/.test(key)) return undefined;\n current = current[Number(key)];\n } else if (Object(current) === current && key in (current as object)) {\n current = (current as Record<string, unknown>)[key];\n } else {\n return undefined;\n }\n }\n return current;\n}\n\n/**\n * Iterate an operation's full page results. Every page is yielded before the stop\n * condition is evaluated, so the last page always arrives. Cursor style resumes from a\n * caller-provided `query[spec.param]`, stops when the optional `hasMore` pointer\n * resolves to `false` or when `nextCursor` resolves to `undefined`/`null`/`''`, and\n * throws if the next cursor is not a string or number, or\n * if the same cursor comes back twice in a row (infinite-loop guards). Offset/page\n * styles advance by item count / by one and stop when\n * the `items` pointer misses or the array is empty.\n */\nasync function* pages<TPage>(\n call: (args?: OperationArgs, init?: RequestOptions) => Promise<TPage>,\n spec: PaginationSpec,\n args: OperationArgs = {},\n init?: RequestOptions\n): AsyncGenerator<TPage> {\n if (spec.style === 'cursor') {\n let cursor: unknown = args.query?.[spec.param];\n while (true) {\n const query = { ...args.query };\n if (cursor !== undefined) query[spec.param] = cursor as QueryValue;\n const page = await call({ ...args, query }, init);\n yield page;\n // Connection-style APIs keep a non-null cursor on the last page and signal the\n // end via a boolean flag — honor it before the cursor check to skip the\n // follow-up empty request. Strictly `false`: a missing pointer falls through.\n if (spec.hasMore !== undefined && resolvePointer(page, spec.hasMore) === false) return;\n const next = resolvePointer(page, spec.nextCursor);\n if (next === undefined || next === null || next === '') return;\n if (typeof next !== 'string' && typeof next !== 'number') {\n // A fresh non-scalar cursor never compares equal, so without this guard a lying\n // server would slip past the did-not-advance check into an infinite loop.\n throw new Error(`Pagination cursor at ${spec.nextCursor} is not a string or number`);\n }\n if (next === cursor) {\n throw new Error('Pagination did not advance: operation returned the same cursor twice');\n }\n cursor = next;\n }\n } else if (spec.style === 'link') {\n // `link` iteration needs the response's `Link` header, which the parsed-page `call`\n // cannot carry — the client wires those operations to `pagesByLink` instead.\n throw new Error('link-style pagination iterates via pagesByLink');\n } else {\n // Coerce the starting position to a number: a caller may pass `query[spec.param]` as a\n // string (common from URL/form input), and `+=` on a string would concatenate. `null`\n // and `''` count as absent — `Number` would turn them into 0, but a one-shot call\n // omits the param for those values, so the iterator must not start at position 0.\n const start = args.query?.[spec.param];\n const fallback = spec.style === 'page' ? 1 : 0;\n const absent = start === undefined || start === null || start === '';\n let position = absent || Number.isNaN(Number(start)) ? fallback : Number(start);\n let previousItems: string | undefined;\n while (true) {\n const page = await call({ ...args, query: { ...args.query, [spec.param]: position } }, init);\n const pageItems = resolvePointer(page, spec.items);\n // Some APIs clamp a past-the-end offset/page to the last non-empty page instead\n // of returning an empty one — the repeated page would otherwise loop forever\n // (the position always \"advances\", so a cursor-style token check can't catch it).\n const serialized = Array.isArray(pageItems) ? JSON.stringify(pageItems) : undefined;\n if (serialized !== undefined && serialized === previousItems) {\n throw new Error('Pagination did not advance: the operation returned the same page twice');\n }\n yield page;\n if (!Array.isArray(pageItems) || pageItems.length === 0) return;\n previousItems = serialized;\n position += spec.style === 'page' ? 1 : pageItems.length;\n }\n }\n}\n\n/**\n * Iterate the operation's individual items: each page's `items` pointer, flattened.\n * A cursor-style page whose pointer misses yields nothing but pagination continues;\n * for offset/page styles a miss has already stopped `pages`.\n */\nasync function* items<TItem>(\n call: (args?: OperationArgs, init?: RequestOptions) => Promise<unknown>,\n spec: PaginationSpec,\n args?: OperationArgs,\n init?: RequestOptions\n): AsyncGenerator<TItem> {\n for await (const page of pages(call, spec, args, init)) {\n const pageItems = resolvePointer(page, spec.items);\n if (Array.isArray(pageItems)) yield* pageItems as TItem[];\n }\n}\n\n/**\n * The per-page call the `link`-style iterators drive: the parsed page plus the raw\n * `Link` header and the page's own URL (for resolving a relative `next` target).\n */\ntype LinkPageCall = (\n args?: OperationArgs,\n init?: RequestOptions\n) => Promise<{ page: unknown; linkHeader: string | null; url: string }>;\n\n/** The `rel=\"next\"` target of an RFC 8288 `Link` header, or `undefined` when absent. */\nfunction linkNext(header: string | null): string | undefined {\n if (header === null) return undefined;\n // Entries are `<url>; rel=\"next\"`, comma-separated; the URL is inside `<>`, so split\n // on commas that precede a `<` and a comma inside a target cannot break an entry.\n for (const entry of header.split(/,\\s*(?=<)/)) {\n const target = /^\\s*<([^>]*)>(.*)$/.exec(entry);\n if (!target) continue;\n const rel = /;\\s*rel\\s*=\\s*\"?([^\";]+)\"?/i.exec(target[2]);\n // `rel` may carry several space-separated relation types (RFC 8288 §3.3).\n if (rel && rel[1].split(/\\s+/).includes('next')) return target[1];\n }\n return undefined;\n}\n\n/**\n * Iterate a `link`-style operation's pages: follow the `Link` header's `rel=\"next\"`\n * target by merging ITS query params into the next call — every page goes through the\n * same declared endpoint, so auth, middleware, and `serverUrl` handling apply\n * unchanged, and credentials can never be handed to a cross-origin `next` URL.\n * Stops when no `rel=\"next\"` is present; throws when the link does not advance\n * (the same target twice, or a self-link — an infinite-loop guard).\n */\nasync function* pagesByLink<TPage>(\n call: LinkPageCall,\n args: OperationArgs = {},\n init?: RequestOptions\n): AsyncGenerator<TPage> {\n let query = args.query;\n let previous: string | undefined;\n while (true) {\n const { page, linkHeader, url } = await call({ ...args, query }, init);\n yield page as TPage;\n const target = linkNext(linkHeader);\n if (target === undefined) return;\n // A relative target resolves against the page's own URL (RFC 8288 §3.1) — which may\n // itself be relative (relative `serverUrl`, mocked fetch), so both resolve against a\n // placeholder origin. It never reaches the wire: only the target's query params\n // carry into the next call.\n const pageUrl = new URL(url, 'http://relative.invalid');\n const next = new URL(target, pageUrl).toString();\n if (next === previous || next === pageUrl.toString()) {\n throw new Error('Pagination did not advance: the Link rel=\"next\" target repeats');\n }\n previous = next;\n // A repeated key (`?tag=a&tag=b`) folds into an array — the query serializer\n // expands arrays back into repeated pairs.\n const linkParams: Record<string, string | string[]> = {};\n for (const [key, value] of new URL(next).searchParams) {\n const seen = linkParams[key];\n if (seen === undefined) linkParams[key] = value;\n else if (Array.isArray(seen)) seen.push(value);\n else linkParams[key] = [seen, value];\n }\n query = { ...args.query, ...linkParams };\n }\n}\n\n/** Iterate a `link`-style operation's individual items: each page's `items` pointer, flattened. */\nasync function* itemsByLink<TItem>(\n call: LinkPageCall,\n spec: PaginationSpec,\n args?: OperationArgs,\n init?: RequestOptions\n): AsyncGenerator<TItem> {\n for await (const page of pagesByLink(call, args, init)) {\n const pageItems = resolvePointer(page, spec.items);\n if (Array.isArray(pageItems)) yield* pageItems as TItem[];\n }\n}";
|