@storyblok/api-client 0.2.4 → 1.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/client.cjs +176 -0
  2. package/dist/client.cjs.map +1 -0
  3. package/dist/client.d.cts +333 -0
  4. package/dist/client.d.mts +333 -0
  5. package/dist/client.mjs +175 -0
  6. package/dist/client.mjs.map +1 -0
  7. package/dist/error.cjs.map +1 -1
  8. package/dist/error.d.cts +13 -2
  9. package/dist/error.d.mts +13 -2
  10. package/dist/error.mjs.map +1 -1
  11. package/dist/generated/datasource_entries/types.gen.d.cts +1 -16
  12. package/dist/generated/datasource_entries/types.gen.d.mts +1 -16
  13. package/dist/generated/datasources/types.gen.d.cts +1 -28
  14. package/dist/generated/datasources/types.gen.d.mts +1 -28
  15. package/dist/generated/links/types.gen.d.cts +1 -18
  16. package/dist/generated/links/types.gen.d.mts +1 -18
  17. package/dist/generated/shared/client/index.d.mts +1 -1
  18. package/dist/generated/shared/client/types.gen.d.cts +39 -3
  19. package/dist/generated/shared/client/types.gen.d.mts +39 -3
  20. package/dist/generated/shared/core/serverSentEvents.gen.d.cts +4 -1
  21. package/dist/generated/shared/core/serverSentEvents.gen.d.mts +4 -1
  22. package/dist/generated/shared/core/types.gen.d.cts +14 -1
  23. package/dist/generated/shared/core/types.gen.d.mts +14 -1
  24. package/dist/generated/spaces/types.gen.d.cts +1 -9
  25. package/dist/generated/spaces/types.gen.d.mts +1 -9
  26. package/dist/generated/stories/index.d.mts +1 -1
  27. package/dist/generated/stories/types.gen.d.cts +72 -9
  28. package/dist/generated/stories/types.gen.d.mts +72 -9
  29. package/dist/generated/tags/types.gen.d.cts +1 -16
  30. package/dist/generated/tags/types.gen.d.mts +1 -16
  31. package/dist/index.cjs +2 -153
  32. package/dist/index.d.cts +4 -172
  33. package/dist/index.d.mts +4 -172
  34. package/dist/index.mjs +2 -150
  35. package/dist/resources/datasource-entries.cjs.map +1 -1
  36. package/dist/resources/datasource-entries.mjs.map +1 -1
  37. package/dist/resources/datasources.cjs.map +1 -1
  38. package/dist/resources/datasources.mjs.map +1 -1
  39. package/dist/resources/links.cjs.map +1 -1
  40. package/dist/resources/links.mjs.map +1 -1
  41. package/dist/resources/spaces.cjs.map +1 -1
  42. package/dist/resources/spaces.mjs.map +1 -1
  43. package/dist/resources/stories.cjs +6 -4
  44. package/dist/resources/stories.cjs.map +1 -1
  45. package/dist/resources/stories.d.cts +78 -3
  46. package/dist/resources/stories.d.mts +79 -3
  47. package/dist/resources/stories.mjs +6 -4
  48. package/dist/resources/stories.mjs.map +1 -1
  49. package/dist/resources/tags.cjs.map +1 -1
  50. package/dist/resources/tags.mjs.map +1 -1
  51. package/dist/utils/rate-limit.cjs +9 -5
  52. package/dist/utils/rate-limit.cjs.map +1 -1
  53. package/dist/utils/rate-limit.d.cts +4 -18
  54. package/dist/utils/rate-limit.d.mts +4 -18
  55. package/dist/utils/rate-limit.mjs +10 -4
  56. package/dist/utils/rate-limit.mjs.map +1 -1
  57. package/package.json +18 -8
  58. package/dist/index.cjs.map +0 -1
  59. package/dist/index.mjs.map +0 -1
  60. package/dist/types.d.cts +0 -37
  61. package/dist/types.d.mts +0 -37
@@ -1 +1 @@
1
- {"version":3,"file":"stories.mjs","names":[],"sources":["../../src/resources/stories.ts"],"sourcesContent":["import { get, list } from '../generated/stories/sdk.gen';\nimport type { GetData, GetResponses, ListData, ListResponses } from '../generated/stories/types.gen';\nimport type {\n AssetField,\n MultilinkField,\n PluginField,\n RichtextField,\n StoryCapi,\n StoryContent,\n TableField,\n} from '../generated/stories';\nimport { inlineStoriesContent, inlineStoryContent, resolveRelationMap } from '../utils/inline-relations';\nimport type { ApiResponse, FetchOptions, ResourceDeps } from '../types';\n\ntype InlinedStoryContentField =\n | string\n | number\n | boolean\n | Array<string | AssetField | StoryContent | StoryWithInlinedRelations>\n | AssetField\n | MultilinkField\n | TableField\n | RichtextField\n | PluginField\n | StoryWithInlinedRelations\n | undefined;\n\ninterface InlinedStoryContent {\n _uid: string;\n component: string;\n _editable?: string;\n [key: string]: InlinedStoryContentField;\n}\n\nexport type StoryWithInlinedRelations = Omit<StoryCapi, 'content'> & {\n content: InlinedStoryContent;\n};\n\ntype StoryResult<InlineRelations extends boolean> = InlineRelations extends true\n ? StoryWithInlinedRelations\n : StoryCapi;\n\ntype GetResponse<InlineRelations extends boolean> = Omit<GetResponses[200], 'story'> & {\n story: StoryResult<InlineRelations>;\n};\ntype ListResponse<InlineRelations extends boolean> = Omit<ListResponses[200], 'stories'> & {\n stories: Array<StoryResult<InlineRelations>>;\n};\n\n/** Pre-resolved to avoid TypeScript emitting deep indexed-access chains that trip up DTS bundlers. */\ntype StoryIdentifier = GetData['path']['identifier'];\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\nexport interface StoriesResourceDeps extends ResourceDeps {\n inlineRelations: boolean;\n}\n\nexport function createStoriesResource<InlineRelations extends boolean>(\n deps: StoriesResourceDeps,\n) {\n const { client, requestWithCache, asApiResponse, inlineRelations, throttleManager } = deps;\n\n return {\n get: async <ThrowOnError extends boolean = false>(\n identifier: StoryIdentifier,\n options: { query?: GetData['query']; signal?: AbortSignal; throwOnError?: ThrowOnError; fetchOptions?: FetchOptions } = {},\n ): Promise<ApiResponse<GetResponse<InlineRelations>, ThrowOnError>> => {\n const { query = {}, signal, throwOnError, fetchOptions } = options;\n const resolvedQuery = typeof identifier === 'string' && UUID_RE.test(identifier) && !query.find_by\n ? { ...query, find_by: 'uuid' as const }\n : query;\n const requestPath = `/v2/cdn/stories/${identifier}`;\n type Res = ApiResponse<GetResponse<InlineRelations>, ThrowOnError>;\n return requestWithCache<GetResponse<InlineRelations>, ThrowOnError>('GET', requestPath, resolvedQuery, async (requestQuery: Record<string, unknown>): Promise<Res> => {\n const response = await throttleManager.execute(requestPath, requestQuery, () =>\n asApiResponse<GetResponse<InlineRelations>, ThrowOnError>(get({\n client,\n path: { identifier },\n query: requestQuery,\n signal,\n ...(throwOnError === undefined ? {} : { throwOnError }),\n ...(fetchOptions ? { kyOptions: { ...client.getConfig().kyOptions, ...fetchOptions } } : {}),\n })));\n\n if (!inlineRelations || response.data === undefined) {\n return response;\n }\n\n const resolved = await resolveRelationMap(response.data, requestQuery, { client, throttleManager });\n if (!resolved) {\n return response;\n }\n\n return {\n ...response,\n data: {\n ...response.data,\n story: inlineStoryContent(response.data.story, resolved.relationPaths, resolved.relationMap),\n },\n };\n }, inlineRelations ? { cacheKeyPrefix: 'inline' } : undefined);\n },\n\n list: async <ThrowOnError extends boolean = false>(\n options: { query?: ListData['query']; signal?: AbortSignal; throwOnError?: ThrowOnError; fetchOptions?: FetchOptions } = {},\n ): Promise<ApiResponse<ListResponse<InlineRelations>, ThrowOnError>> => {\n const { query = {}, signal, throwOnError, fetchOptions } = options;\n const requestPath = '/v2/cdn/stories';\n type ResAll = ApiResponse<ListResponse<InlineRelations>, ThrowOnError>;\n return requestWithCache<ListResponse<InlineRelations>, ThrowOnError>('GET', requestPath, query, async (requestQuery: Record<string, unknown>): Promise<ResAll> => {\n const response = await throttleManager.execute(requestPath, requestQuery, () =>\n asApiResponse<ListResponse<InlineRelations>, ThrowOnError>(list({\n client,\n query: requestQuery,\n signal,\n ...(throwOnError === undefined ? {} : { throwOnError }),\n ...(fetchOptions ? { kyOptions: { ...client.getConfig().kyOptions, ...fetchOptions } } : {}),\n })));\n\n if (!inlineRelations || response.data === undefined) {\n return response;\n }\n\n const resolved = await resolveRelationMap(response.data, requestQuery, { client, throttleManager });\n if (!resolved) {\n return response;\n }\n\n return {\n ...response,\n data: {\n ...response.data,\n stories: inlineStoriesContent(response.data.stories, resolved.relationPaths, resolved.relationMap),\n },\n };\n }, inlineRelations ? { cacheKeyPrefix: 'inline' } : undefined);\n },\n };\n}\n"],"mappings":";;;;AAoDA,MAAM,UAAU;AAMhB,SAAgB,sBACd,MACA;CACA,MAAM,EAAE,QAAQ,kBAAkB,eAAe,iBAAiB,oBAAoB;AAEtF,QAAO;EACL,KAAK,OACH,YACA,UAAwH,EAAE,KACrD;GACrE,MAAM,EAAE,QAAQ,EAAE,EAAE,QAAQ,cAAc,iBAAiB;GAC3D,MAAM,gBAAgB,OAAO,eAAe,YAAY,QAAQ,KAAK,WAAW,IAAI,CAAC,MAAM,UACvF;IAAE,GAAG;IAAO,SAAS;IAAiB,GACtC;GACJ,MAAM,cAAc,mBAAmB;AAEvC,UAAO,iBAA6D,OAAO,aAAa,eAAe,OAAO,iBAAwD;IACpK,MAAM,WAAW,MAAM,gBAAgB,QAAQ,aAAa,oBAC1D,cAA0D,IAAI;KAC5D;KACA,MAAM,EAAE,YAAY;KACpB,OAAO;KACP;KACA,GAAI,iBAAiB,SAAY,EAAE,GAAG,EAAE,cAAc;KACtD,GAAI,eAAe,EAAE,WAAW;MAAE,GAAG,OAAO,WAAW,CAAC;MAAW,GAAG;MAAc,EAAE,GAAG,EAAE;KAC5F,CAAC,CAAC,CAAC;AAEN,QAAI,CAAC,mBAAmB,SAAS,SAAS,OACxC,QAAO;IAGT,MAAM,WAAW,MAAM,mBAAmB,SAAS,MAAM,cAAc;KAAE;KAAQ;KAAiB,CAAC;AACnG,QAAI,CAAC,SACH,QAAO;AAGT,WAAO;KACL,GAAG;KACH,MAAM;MACJ,GAAG,SAAS;MACZ,OAAO,mBAAmB,SAAS,KAAK,OAAO,SAAS,eAAe,SAAS,YAAY;MAC7F;KACF;MACA,kBAAkB,EAAE,gBAAgB,UAAU,GAAG,OAAU;;EAGhE,MAAM,OACJ,UAAyH,EAAE,KACrD;GACtE,MAAM,EAAE,QAAQ,EAAE,EAAE,QAAQ,cAAc,iBAAiB;GAC3D,MAAM,cAAc;AAEpB,UAAO,iBAA8D,OAAO,aAAa,OAAO,OAAO,iBAA2D;IAChK,MAAM,WAAW,MAAM,gBAAgB,QAAQ,aAAa,oBAC1D,cAA2D,KAAK;KAC9D;KACA,OAAO;KACP;KACA,GAAI,iBAAiB,SAAY,EAAE,GAAG,EAAE,cAAc;KACtD,GAAI,eAAe,EAAE,WAAW;MAAE,GAAG,OAAO,WAAW,CAAC;MAAW,GAAG;MAAc,EAAE,GAAG,EAAE;KAC5F,CAAC,CAAC,CAAC;AAEN,QAAI,CAAC,mBAAmB,SAAS,SAAS,OACxC,QAAO;IAGT,MAAM,WAAW,MAAM,mBAAmB,SAAS,MAAM,cAAc;KAAE;KAAQ;KAAiB,CAAC;AACnG,QAAI,CAAC,SACH,QAAO;AAGT,WAAO;KACL,GAAG;KACH,MAAM;MACJ,GAAG,SAAS;MACZ,SAAS,qBAAqB,SAAS,KAAK,SAAS,SAAS,eAAe,SAAS,YAAY;MACnG;KACF;MACA,kBAAkB,EAAE,gBAAgB,UAAU,GAAG,OAAU;;EAEjE"}
1
+ {"version":3,"file":"stories.mjs","names":[],"sources":["../../src/resources/stories.ts"],"sourcesContent":["import { get, list } from '../generated/stories/sdk.gen';\nimport type { GetData, GetResponses, ListData, ListResponses } from '../generated/stories/types.gen';\nimport type {\n AssetFieldValue,\n BlokContent,\n MultilinkFieldValue,\n PluginFieldValue,\n RichtextFieldValue,\n StoryCapi,\n TableFieldValue,\n} from '../generated/stories';\nimport { inlineStoriesContent, inlineStoryContent, resolveRelationMap } from '../utils/inline-relations';\nimport type { ApiResponse, FetchOptions, ResourceDeps } from '../client';\nimport type { Story as CapiStory, Block as Component, RootBlocks as RootComponents } from '@storyblok/schema';\n\ntype InlinedStoryContentField =\n | string\n | number\n | boolean\n | Array<string | AssetFieldValue | BlokContent | StoryWithInlinedRelations>\n | AssetFieldValue\n | MultilinkFieldValue\n | TableFieldValue\n | RichtextFieldValue\n | PluginFieldValue\n | StoryWithInlinedRelations\n | undefined;\n\ninterface InlinedStoryContent {\n _uid: string;\n component: string;\n _editable?: string;\n [key: string]: InlinedStoryContentField;\n}\n\nexport type StoryWithInlinedRelations = Omit<StoryCapi, 'content'> & {\n content: InlinedStoryContent;\n};\n\n/** Splits `\"comp.field,comp2.field2\"` into a union of `{ component, field }`. */\ntype ParseRelations<T extends string> =\n T extends `${infer Comp}.${infer Field},${infer Rest}`\n ? { component: Comp; field: Field } | ParseRelations<Rest>\n : T extends `${infer Comp}.${infer Field}`\n ? { component: Comp; field: Field }\n : never;\n\n/** Extracts resolved field names for a given component name. */\ntype ResolvedFieldsFor<R extends string, ComponentName extends string> =\n Extract<ParseRelations<R>, { component: ComponentName }>['field'];\n\n/** A resolved relation: a full story typed to the component union. */\ntype ResolvedRelation<TComponents extends Component> =\n { [K in TComponents as K['name']]: CapiStory<K, TComponents> }[TComponents['name']];\n\n/**\n * Given a story type and a set of resolved field names, replaces\n * those fields with `ResolvedRelation<TComponents>` (a full story object).\n */\ntype WithResolvedRelations<\n TStory,\n TComponents extends Component,\n Fields extends string,\n> = TStory extends { content: infer C } ? Omit<TStory, 'content'> & {\n content: {\n [K in keyof C]: K extends Fields ? ResolvedRelation<TComponents> : C[K]\n };\n}\n : TStory;\n\n/**\n * Resolves to a narrowed component-derived story type when `TComponents` is a specific\n * Component union, or falls back to the generated StoryCapi / StoryWithInlinedRelations\n * when `TComponents` is the default Component base type (no type argument provided).\n *\n * When `ResolveRelations` is a string literal (e.g. `\"article.author\"`),\n * matched fields are widened from their schema type to `ResolvedRelation<TComponents>`\n * — a full story object typed to the component union.\n *\n * Uses a mapped-type approach instead of a distributive conditional with a\n * separate full-components parameter. This ensures the full `TComponents` union is\n * preserved even when DTS bundlers (like tsdown) inline the type alias —\n * a distributive conditional + default-parameter pattern would collapse\n * both parameters to the distributed single member after inlining.\n *\n * The mapped type `{ [K in TComponents as K[\"name\"]]: CapiStory<K, TComponents> }`\n * iterates each union member as `K` while keeping `TComponents` as the full union\n * for nested blok field resolution. The final indexed access\n * `[TComponents[\"name\"]]` produces the discriminated union of all story types.\n */\ntype StoryResult<\n TComponents extends Component,\n InlineRelations extends boolean,\n ResolveRelationsRaw extends string | undefined = undefined,\n> =\n Component extends TComponents\n ? InlineRelations extends true ? StoryWithInlinedRelations : StoryCapi // fallback\n : ResolveRelationsRaw extends string\n ? {\n [K in RootComponents<TComponents> as K['name']]: WithResolvedRelations<\n CapiStory<K, TComponents>,\n TComponents,\n ResolvedFieldsFor<ResolveRelationsRaw, K['name']>\n >\n }[RootComponents<TComponents>['name']]\n : CapiStory<TComponents>;\n\ntype GetResponse<\n TComponents extends Component,\n InlineRelations extends boolean,\n ResolveRelationsRaw extends string | undefined = undefined,\n> = Omit<GetResponses[200], 'story'> & {\n story: StoryResult<TComponents, InlineRelations, ResolveRelationsRaw>;\n};\ntype ListResponse<\n TComponents extends Component,\n InlineRelations extends boolean,\n ResolveRelationsRaw extends string | undefined = undefined,\n> = Omit<ListResponses[200], 'stories'> & {\n stories: Array<StoryResult<TComponents, InlineRelations, ResolveRelationsRaw>>;\n};\n\n/** Pre-resolved to avoid TypeScript emitting deep indexed-access chains that trip up DTS bundlers. */\ntype StoryIdentifier = GetData['path']['identifier'];\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\nexport interface StoriesResourceDeps<DefaultThrowOnError extends boolean = false> extends ResourceDeps<DefaultThrowOnError> {\n inlineRelations: boolean;\n}\n\nexport function createStoriesResource<\n TComponents extends Component = Component,\n InlineRelations extends boolean = false,\n DefaultThrowOnError extends boolean = false,\n>(\n deps: StoriesResourceDeps<DefaultThrowOnError>,\n) {\n const { client, requestWithCache, asApiResponse, inlineRelations, throttleManager } = deps;\n\n return {\n get: async <\n ThrowOnError extends boolean = DefaultThrowOnError,\n const ResolveRelationsStr extends string | undefined = undefined,\n >(\n identifier: StoryIdentifier,\n options: { query?: Omit<NonNullable<GetData['query']>, 'resolve_relations'> & { resolve_relations?: ResolveRelationsStr }; signal?: AbortSignal; throwOnError?: ThrowOnError; fetchOptions?: FetchOptions } = {},\n ): Promise<ApiResponse<GetResponse<TComponents, InlineRelations, ResolveRelationsStr>, ThrowOnError>> => {\n const { query = {}, signal, throwOnError, fetchOptions } = options;\n const typedQuery = (query ?? {}) as NonNullable<GetData['query']>;\n const resolvedQuery = typeof identifier === 'string' && UUID_RE.test(identifier) && !typedQuery.find_by\n ? { ...typedQuery, find_by: 'uuid' }\n : typedQuery;\n const requestPath = `/v2/cdn/stories/${identifier}`;\n return requestWithCache('GET', requestPath, resolvedQuery, async (requestQuery: Record<string, unknown>) => {\n const response = await throttleManager.execute(requestPath, requestQuery, () =>\n asApiResponse(get({\n client,\n path: { identifier },\n query: requestQuery,\n signal,\n ...(throwOnError === undefined ? {} : { throwOnError }),\n ...(fetchOptions ? { kyOptions: { ...client.getConfig().kyOptions, ...fetchOptions } } : {}),\n }))) satisfies ApiResponse<GetResponse<TComponents, InlineRelations, ResolveRelationsStr>, ThrowOnError>;\n\n if (!inlineRelations || response.data === undefined) {\n return response;\n }\n\n const resolved = await resolveRelationMap(response.data, requestQuery, { client, throttleManager });\n if (!resolved) {\n return response;\n }\n\n return {\n ...response,\n data: {\n ...response.data,\n // `inlineStoryContent` operates on raw `StoryCapi` shapes and mutates relation fields\n // from UUID strings to full story objects. We cast to satisfy its parameter type.\n story: inlineStoryContent(response.data.story as StoryCapi, resolved.relationPaths, resolved.relationMap),\n },\n };\n }, inlineRelations ? { cacheKeyPrefix: 'inline' } : undefined);\n },\n\n list: async <\n ThrowOnError extends boolean = DefaultThrowOnError,\n const ResolveRelationsStr extends string | undefined = undefined,\n >(\n options: { query?: Omit<NonNullable<ListData['query']>, 'resolve_relations'> & { resolve_relations?: ResolveRelationsStr }; signal?: AbortSignal; throwOnError?: ThrowOnError; fetchOptions?: FetchOptions } = {},\n ): Promise<ApiResponse<ListResponse<TComponents, InlineRelations, ResolveRelationsStr>, ThrowOnError>> => {\n const { query = {}, signal, throwOnError, fetchOptions } = options;\n const typedQuery = (query ?? {}) as NonNullable<ListData['query']>;\n const requestPath = '/v2/cdn/stories';\n return requestWithCache('GET', requestPath, typedQuery, async (requestQuery: Record<string, unknown>) => {\n const response = await throttleManager.execute(requestPath, requestQuery, () =>\n asApiResponse(list({\n client,\n query: requestQuery,\n signal,\n ...(throwOnError === undefined ? {} : { throwOnError }),\n ...(fetchOptions ? { kyOptions: { ...client.getConfig().kyOptions, ...fetchOptions } } : {}),\n }))) satisfies ApiResponse<ListResponse<TComponents, InlineRelations, ResolveRelationsStr>, ThrowOnError>;\n\n if (!inlineRelations || response.data === undefined) {\n return response;\n }\n\n const resolved = await resolveRelationMap(response.data, requestQuery, { client, throttleManager });\n if (!resolved) {\n return response;\n }\n\n return {\n ...response,\n data: {\n ...response.data,\n // `inlineStoriesContent` operates on raw `StoryCapi` shapes and mutates relation fields\n // from UUID strings to full story objects. We cast to satisfy its parameter type.\n stories: inlineStoriesContent(response.data.stories as StoryCapi[], resolved.relationPaths, resolved.relationMap),\n },\n };\n }, inlineRelations ? { cacheKeyPrefix: 'inline' } : undefined);\n },\n };\n}\n"],"mappings":";;;;AA6HA,MAAM,UAAU;AAMhB,SAAgB,sBAKd,MACA;CACA,MAAM,EAAE,QAAQ,kBAAkB,eAAe,iBAAiB,oBAAoB;AAEtF,QAAO;EACL,KAAK,OAIH,YACA,UAA8M,EAAE,KACzG;GACvG,MAAM,EAAE,QAAQ,EAAE,EAAE,QAAQ,cAAc,iBAAiB;GAC3D,MAAM,aAAc,SAAS,EAAE;GAC/B,MAAM,gBAAgB,OAAO,eAAe,YAAY,QAAQ,KAAK,WAAW,IAAI,CAAC,WAAW,UAC5F;IAAE,GAAG;IAAY,SAAS;IAAQ,GAClC;GACJ,MAAM,cAAc,mBAAmB;AACvC,UAAO,iBAAiB,OAAO,aAAa,eAAe,OAAO,iBAA0C;IAC1G,MAAM,WAAW,MAAM,gBAAgB,QAAQ,aAAa,oBAC1D,cAAc,IAAI;KAChB;KACA,MAAM,EAAE,YAAY;KACpB,OAAO;KACP;KACA,GAAI,iBAAiB,SAAY,EAAE,GAAG,EAAE,cAAc;KACtD,GAAI,eAAe,EAAE,WAAW;MAAE,GAAG,OAAO,WAAW,CAAC;MAAW,GAAG;MAAc,EAAE,GAAG,EAAE;KAC5F,CAAC,CAAC,CAAC;AAEN,QAAI,CAAC,mBAAmB,SAAS,SAAS,OACxC,QAAO;IAGT,MAAM,WAAW,MAAM,mBAAmB,SAAS,MAAM,cAAc;KAAE;KAAQ;KAAiB,CAAC;AACnG,QAAI,CAAC,SACH,QAAO;AAGT,WAAO;KACL,GAAG;KACH,MAAM;MACJ,GAAG,SAAS;MAGZ,OAAO,mBAAmB,SAAS,KAAK,OAAoB,SAAS,eAAe,SAAS,YAAY;MAC1G;KACF;MACA,kBAAkB,EAAE,gBAAgB,UAAU,GAAG,OAAU;;EAGhE,MAAM,OAIJ,UAA+M,EAAE,KACzG;GACxG,MAAM,EAAE,QAAQ,EAAE,EAAE,QAAQ,cAAc,iBAAiB;GAC3D,MAAM,aAAc,SAAS,EAAE;GAC/B,MAAM,cAAc;AACpB,UAAO,iBAAiB,OAAO,aAAa,YAAY,OAAO,iBAA0C;IACvG,MAAM,WAAW,MAAM,gBAAgB,QAAQ,aAAa,oBAC1D,cAAc,KAAK;KACjB;KACA,OAAO;KACP;KACA,GAAI,iBAAiB,SAAY,EAAE,GAAG,EAAE,cAAc;KACtD,GAAI,eAAe,EAAE,WAAW;MAAE,GAAG,OAAO,WAAW,CAAC;MAAW,GAAG;MAAc,EAAE,GAAG,EAAE;KAC5F,CAAC,CAAC,CAAC;AAEN,QAAI,CAAC,mBAAmB,SAAS,SAAS,OACxC,QAAO;IAGT,MAAM,WAAW,MAAM,mBAAmB,SAAS,MAAM,cAAc;KAAE;KAAQ;KAAiB,CAAC;AACnG,QAAI,CAAC,SACH,QAAO;AAGT,WAAO;KACL,GAAG;KACH,MAAM;MACJ,GAAG,SAAS;MAGZ,SAAS,qBAAqB,SAAS,KAAK,SAAwB,SAAS,eAAe,SAAS,YAAY;MAClH;KACF;MACA,kBAAkB,EAAE,gBAAgB,UAAU,GAAG,OAAU;;EAEjE"}
@@ -1 +1 @@
1
- {"version":3,"file":"tags.cjs","names":["listTagsApi"],"sources":["../../src/resources/tags.ts"],"sourcesContent":["import { list as listTagsApi } from '../generated/tags/sdk.gen';\nimport type { ListData as TagsListData, ListResponses as TagsListResponses } from '../generated/tags/types.gen';\nimport type { ApiResponse, FetchOptions, ResourceDeps } from '../types';\n\nexport function createTagsResource(deps: ResourceDeps) {\n const { client, requestWithCache, asApiResponse, throttleManager } = deps;\n\n return {\n list: async <ThrowOnError extends boolean = false>(\n options: { query?: TagsListData['query']; signal?: AbortSignal; throwOnError?: ThrowOnError; fetchOptions?: FetchOptions } = {},\n ): Promise<ApiResponse<TagsListResponses[200], ThrowOnError>> => {\n const { query = {}, signal, throwOnError, fetchOptions } = options;\n const requestPath = '/v2/cdn/tags';\n return requestWithCache<TagsListResponses[200], ThrowOnError>('GET', requestPath, query, (requestQuery: Record<string, unknown>) => {\n return throttleManager.execute(requestPath, requestQuery, () =>\n asApiResponse<TagsListResponses[200], ThrowOnError>(listTagsApi({\n client,\n query: requestQuery,\n signal,\n ...(throwOnError === undefined ? {} : { throwOnError }),\n ...(fetchOptions ? { kyOptions: { ...client.getConfig().kyOptions, ...fetchOptions } } : {}),\n })));\n });\n },\n };\n}\n"],"mappings":";;;AAIA,SAAgB,mBAAmB,MAAoB;CACrD,MAAM,EAAE,QAAQ,kBAAkB,eAAe,oBAAoB;AAErE,QAAO,EACL,MAAM,OACJ,UAA6H,EAAE,KAChE;EAC/D,MAAM,EAAE,QAAQ,EAAE,EAAE,QAAQ,cAAc,iBAAiB;EAC3D,MAAM,cAAc;AACpB,SAAO,iBAAuD,OAAO,aAAa,QAAQ,iBAA0C;AAClI,UAAO,gBAAgB,QAAQ,aAAa,oBAC1C,cAAoDA,qBAAY;IAC9D;IACA,OAAO;IACP;IACA,GAAI,iBAAiB,SAAY,EAAE,GAAG,EAAE,cAAc;IACtD,GAAI,eAAe,EAAE,WAAW;KAAE,GAAG,OAAO,WAAW,CAAC;KAAW,GAAG;KAAc,EAAE,GAAG,EAAE;IAC5F,CAAC,CAAC,CAAC;IACN;IAEL"}
1
+ {"version":3,"file":"tags.cjs","names":["listTagsApi"],"sources":["../../src/resources/tags.ts"],"sourcesContent":["import { list as listTagsApi } from '../generated/tags/sdk.gen';\nimport type { ListData as TagsListData, ListResponses as TagsListResponses } from '../generated/tags/types.gen';\nimport type { ApiResponse, FetchOptions, ResourceDeps } from '../client';\n\nexport function createTagsResource<DefaultThrowOnError extends boolean = false>(deps: ResourceDeps<DefaultThrowOnError>) {\n const { client, requestWithCache, asApiResponse, throttleManager } = deps;\n\n return {\n list: async <ThrowOnError extends boolean = DefaultThrowOnError>(\n options: { query?: TagsListData['query']; signal?: AbortSignal; throwOnError?: ThrowOnError; fetchOptions?: FetchOptions } = {},\n ): Promise<ApiResponse<TagsListResponses[200], ThrowOnError>> => {\n const { query = {}, signal, throwOnError, fetchOptions } = options;\n const requestPath = '/v2/cdn/tags';\n return requestWithCache<TagsListResponses[200], ThrowOnError>('GET', requestPath, query, (requestQuery: Record<string, unknown>) => {\n return throttleManager.execute(requestPath, requestQuery, () =>\n asApiResponse<TagsListResponses[200], ThrowOnError>(listTagsApi({\n client,\n query: requestQuery,\n signal,\n ...(throwOnError === undefined ? {} : { throwOnError }),\n ...(fetchOptions ? { kyOptions: { ...client.getConfig().kyOptions, ...fetchOptions } } : {}),\n })));\n });\n },\n };\n}\n"],"mappings":";;;AAIA,SAAgB,mBAAgE,MAAyC;CACvH,MAAM,EAAE,QAAQ,kBAAkB,eAAe,oBAAoB;AAErE,QAAO,EACL,MAAM,OACJ,UAA6H,EAAE,KAChE;EAC/D,MAAM,EAAE,QAAQ,EAAE,EAAE,QAAQ,cAAc,iBAAiB;EAC3D,MAAM,cAAc;AACpB,SAAO,iBAAuD,OAAO,aAAa,QAAQ,iBAA0C;AAClI,UAAO,gBAAgB,QAAQ,aAAa,oBAC1C,cAAoDA,qBAAY;IAC9D;IACA,OAAO;IACP;IACA,GAAI,iBAAiB,SAAY,EAAE,GAAG,EAAE,cAAc;IACtD,GAAI,eAAe,EAAE,WAAW;KAAE,GAAG,OAAO,WAAW,CAAC;KAAW,GAAG;KAAc,EAAE,GAAG,EAAE;IAC5F,CAAC,CAAC,CAAC;IACN;IAEL"}
@@ -1 +1 @@
1
- {"version":3,"file":"tags.mjs","names":["listTagsApi"],"sources":["../../src/resources/tags.ts"],"sourcesContent":["import { list as listTagsApi } from '../generated/tags/sdk.gen';\nimport type { ListData as TagsListData, ListResponses as TagsListResponses } from '../generated/tags/types.gen';\nimport type { ApiResponse, FetchOptions, ResourceDeps } from '../types';\n\nexport function createTagsResource(deps: ResourceDeps) {\n const { client, requestWithCache, asApiResponse, throttleManager } = deps;\n\n return {\n list: async <ThrowOnError extends boolean = false>(\n options: { query?: TagsListData['query']; signal?: AbortSignal; throwOnError?: ThrowOnError; fetchOptions?: FetchOptions } = {},\n ): Promise<ApiResponse<TagsListResponses[200], ThrowOnError>> => {\n const { query = {}, signal, throwOnError, fetchOptions } = options;\n const requestPath = '/v2/cdn/tags';\n return requestWithCache<TagsListResponses[200], ThrowOnError>('GET', requestPath, query, (requestQuery: Record<string, unknown>) => {\n return throttleManager.execute(requestPath, requestQuery, () =>\n asApiResponse<TagsListResponses[200], ThrowOnError>(listTagsApi({\n client,\n query: requestQuery,\n signal,\n ...(throwOnError === undefined ? {} : { throwOnError }),\n ...(fetchOptions ? { kyOptions: { ...client.getConfig().kyOptions, ...fetchOptions } } : {}),\n })));\n });\n },\n };\n}\n"],"mappings":";;;AAIA,SAAgB,mBAAmB,MAAoB;CACrD,MAAM,EAAE,QAAQ,kBAAkB,eAAe,oBAAoB;AAErE,QAAO,EACL,MAAM,OACJ,UAA6H,EAAE,KAChE;EAC/D,MAAM,EAAE,QAAQ,EAAE,EAAE,QAAQ,cAAc,iBAAiB;EAC3D,MAAM,cAAc;AACpB,SAAO,iBAAuD,OAAO,aAAa,QAAQ,iBAA0C;AAClI,UAAO,gBAAgB,QAAQ,aAAa,oBAC1C,cAAoDA,KAAY;IAC9D;IACA,OAAO;IACP;IACA,GAAI,iBAAiB,SAAY,EAAE,GAAG,EAAE,cAAc;IACtD,GAAI,eAAe,EAAE,WAAW;KAAE,GAAG,OAAO,WAAW,CAAC;KAAW,GAAG;KAAc,EAAE,GAAG,EAAE;IAC5F,CAAC,CAAC,CAAC;IACN;IAEL"}
1
+ {"version":3,"file":"tags.mjs","names":["listTagsApi"],"sources":["../../src/resources/tags.ts"],"sourcesContent":["import { list as listTagsApi } from '../generated/tags/sdk.gen';\nimport type { ListData as TagsListData, ListResponses as TagsListResponses } from '../generated/tags/types.gen';\nimport type { ApiResponse, FetchOptions, ResourceDeps } from '../client';\n\nexport function createTagsResource<DefaultThrowOnError extends boolean = false>(deps: ResourceDeps<DefaultThrowOnError>) {\n const { client, requestWithCache, asApiResponse, throttleManager } = deps;\n\n return {\n list: async <ThrowOnError extends boolean = DefaultThrowOnError>(\n options: { query?: TagsListData['query']; signal?: AbortSignal; throwOnError?: ThrowOnError; fetchOptions?: FetchOptions } = {},\n ): Promise<ApiResponse<TagsListResponses[200], ThrowOnError>> => {\n const { query = {}, signal, throwOnError, fetchOptions } = options;\n const requestPath = '/v2/cdn/tags';\n return requestWithCache<TagsListResponses[200], ThrowOnError>('GET', requestPath, query, (requestQuery: Record<string, unknown>) => {\n return throttleManager.execute(requestPath, requestQuery, () =>\n asApiResponse<TagsListResponses[200], ThrowOnError>(listTagsApi({\n client,\n query: requestQuery,\n signal,\n ...(throwOnError === undefined ? {} : { throwOnError }),\n ...(fetchOptions ? { kyOptions: { ...client.getConfig().kyOptions, ...fetchOptions } } : {}),\n })));\n });\n },\n };\n}\n"],"mappings":";;;AAIA,SAAgB,mBAAgE,MAAyC;CACvH,MAAM,EAAE,QAAQ,kBAAkB,eAAe,oBAAoB;AAErE,QAAO,EACL,MAAM,OACJ,UAA6H,EAAE,KAChE;EAC/D,MAAM,EAAE,QAAQ,EAAE,EAAE,QAAQ,cAAc,iBAAiB;EAC3D,MAAM,cAAc;AACpB,SAAO,iBAAuD,OAAO,aAAa,QAAQ,iBAA0C;AAClI,UAAO,gBAAgB,QAAQ,aAAa,oBAC1C,cAAoDA,KAAY;IAC9D;IACA,OAAO;IACP;IACA,GAAI,iBAAiB,SAAY,EAAE,GAAG,EAAE,cAAc;IACtD,GAAI,eAAe,EAAE,WAAW;KAAE,GAAG,OAAO,WAAW,CAAC;KAAW,GAAG;KAAc,EAAE,GAAG,EAAE;IAC5F,CAAC,CAAC,CAAC;IACN;IAEL"}
@@ -77,14 +77,20 @@ function determineTier(path, query) {
77
77
  return "VERY_LARGE";
78
78
  }
79
79
  /**
80
- * Extracts the quota (`q=`) value from the `X-RateLimit-Policy` response header.
81
- * Returns `undefined` if the header is absent or unparseable.
80
+ * Extracts the quota (`q=`) value from the `X-RateLimit-Policy` response header,
81
+ * but only when the policy describes a rate limit — not a concurrency limit.
82
82
  *
83
- * Example header: `"concurrent-requests";q=30`
83
+ * The Storyblok CDN currently returns `"concurrent-requests";q=30` which is a
84
+ * concurrency limit (always ~30), not a rate limit. Applying that value to the
85
+ * tier-based rate limiter would incorrectly cap throughput below what the API
86
+ * allows. This function therefore ignores concurrency policies and only returns
87
+ * a value for rate-limit policies (e.g. `"rate-limit";q=50`), which the API
88
+ * does not send yet but may in the future.
84
89
  */
85
90
  function parseRateLimitPolicyHeader(response) {
86
91
  const policy = response.headers.get("x-ratelimit-policy");
87
92
  if (!policy) return;
93
+ if (policy.includes("\"concurrent-requests\"")) return;
88
94
  const match = policy.match(/q=(\d+)/);
89
95
  if (!match) return;
90
96
  return Math.min(Number.parseInt(match[1], 10), MAX_RATE_LIMIT);
@@ -134,7 +140,5 @@ function createThrottleManager(config) {
134
140
  }
135
141
 
136
142
  //#endregion
137
- exports.createThrottle = createThrottle;
138
143
  exports.createThrottleManager = createThrottleManager;
139
- exports.parseRateLimitPolicyHeader = parseRateLimitPolicyHeader;
140
144
  //# sourceMappingURL=rate-limit.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"rate-limit.cjs","names":[],"sources":["../../src/utils/rate-limit.ts"],"sourcesContent":["/**\n * Rate limiting for the Content API client.\n *\n * Provides both a simple token-bucket throttle and a tier-aware manager\n * that automatically selects the right concurrency limit based on request\n * type (single story vs. listing) and the per_page query parameter — mirroring\n * the tiers enforced server-side by the Storyblok CDN.\n */\n\nconst TIER_LIMITS = {\n SINGLE_OR_SMALL: 50, // single story fetch or per_page ≤ 25\n MEDIUM: 15, // per_page 26–50\n LARGE: 10, // per_page 51–75\n VERY_LARGE: 6, // per_page 76–100\n} as const;\n\ntype TierName = keyof typeof TIER_LIMITS;\n\nconst PER_PAGE_THRESHOLDS = {\n SMALL: 25,\n MEDIUM: 50,\n LARGE: 75,\n} as const;\n\nconst DEFAULT_PER_PAGE = 25;\nconst MAX_RATE_LIMIT = 1_000;\n\nexport interface RateLimitConfig {\n /**\n * Fixed maximum number of concurrent requests per second.\n * When set, disables automatic per_page tier detection and all requests\n * share a single queue at this limit. Capped at 1000.\n */\n maxConcurrency?: number;\n /**\n * Dynamically adjust the rate limit based on the `X-RateLimit-Policy`\n * response header returned by the Storyblok API.\n * @default true\n */\n adaptToServerHeaders?: boolean;\n}\n\nexport interface ThrottleManager {\n execute: <T>(path: string, query: Record<string, unknown>, fn: () => Promise<T>) => Promise<T>;\n adaptToResponse: (response: Response | undefined) => void;\n}\n\ninterface Throttle {\n execute: <T>(fn: () => Promise<T>) => Promise<T>;\n setLimit: (n: number) => void;\n}\n\n/**\n * Concurrency limiter: allows up to `initialLimit` requests to be in-flight\n * at the same time. A slot is freed as soon as the request's promise settles\n * (resolves or rejects), so throughput scales with how quickly requests\n * complete rather than being artificially capped at N per second.\n */\nexport function createThrottle(initialLimit: number): Throttle {\n let limit = initialLimit;\n let activeCount = 0;\n const queue: Array<() => void> = [];\n\n const tryNext = () => {\n while (queue.length > 0 && activeCount < limit) {\n activeCount++;\n const run = queue.shift()!;\n run();\n }\n };\n\n const execute = <T>(fn: () => Promise<T>): Promise<T> => {\n return new Promise<T>((resolve, reject) => {\n queue.push(() => {\n fn().then(\n (value) => {\n activeCount--;\n tryNext();\n resolve(value);\n },\n (error) => {\n activeCount--;\n tryNext();\n reject(error);\n },\n );\n });\n tryNext();\n });\n };\n\n const setLimit = (n: number) => {\n limit = n;\n // If the limit increased, unblock any waiting requests.\n tryNext();\n };\n\n return { execute, setLimit };\n}\n\n// Matches /v2/cdn/stories/<identifier> — a single story fetch (including nested slugs).\nconst SINGLE_STORY_PATH_RE = /\\/v2\\/cdn\\/stories\\/.+$/;\n\n/**\n * Maps a request path + query to one of the four rate-limit tiers.\n * Used by the auto-detection mode of `createThrottleManager`.\n */\nexport function determineTier(path: string, query: Record<string, unknown>): TierName {\n if (SINGLE_STORY_PATH_RE.test(path)) {\n return 'SINGLE_OR_SMALL';\n }\n\n const rawPerPage = query.per_page;\n const perPage\n = typeof rawPerPage === 'number'\n ? rawPerPage\n : typeof rawPerPage === 'string'\n ? Number.parseInt(rawPerPage, 10) || DEFAULT_PER_PAGE\n : DEFAULT_PER_PAGE;\n\n if (perPage <= PER_PAGE_THRESHOLDS.SMALL) {\n return 'SINGLE_OR_SMALL';\n }\n if (perPage <= PER_PAGE_THRESHOLDS.MEDIUM) {\n return 'MEDIUM';\n }\n if (perPage <= PER_PAGE_THRESHOLDS.LARGE) {\n return 'LARGE';\n }\n return 'VERY_LARGE';\n}\n\n/**\n * Extracts the quota (`q=`) value from the `X-RateLimit-Policy` response header.\n * Returns `undefined` if the header is absent or unparseable.\n *\n * Example header: `\"concurrent-requests\";q=30`\n */\nexport function parseRateLimitPolicyHeader(response: Response): number | undefined {\n const policy = response.headers.get('x-ratelimit-policy');\n if (!policy) {\n return undefined;\n }\n const match = policy.match(/q=(\\d+)/);\n if (!match) {\n return undefined;\n }\n return Math.min(Number.parseInt(match[1], 10), MAX_RATE_LIMIT);\n}\n\n/**\n * Creates a `ThrottleManager` from the user-supplied `rateLimit` config.\n *\n * - `false` → no throttling (passthrough)\n * - `number` → fixed single queue at that limit\n * - `{ maxConcurrency: n }` → fixed single queue at n req/s\n * - `{}` / `undefined` (default)→ auto-detect tier from path + per_page\n */\nexport function createThrottleManager(config: RateLimitConfig | number | false): ThrottleManager {\n // Disabled — every request goes straight through.\n if (config === false) {\n return {\n execute: (_path, _query, fn) => fn(),\n adaptToResponse: () => {},\n };\n }\n\n const resolvedConfig: RateLimitConfig = typeof config === 'number' ? { maxConcurrency: config } : config;\n const { maxConcurrency, adaptToServerHeaders = true } = resolvedConfig;\n\n // Fixed-limit mode — single queue, optional server-header adaptation.\n if (maxConcurrency !== undefined) {\n const cappedLimit = Math.min(maxConcurrency, MAX_RATE_LIMIT);\n const throttle = createThrottle(cappedLimit);\n\n return {\n execute: (_path, _query, fn) => throttle.execute(fn),\n adaptToResponse: (response) => {\n if (!adaptToServerHeaders || response === undefined) {\n return;\n }\n const serverLimit = parseRateLimitPolicyHeader(response);\n if (serverLimit !== undefined) {\n // Never exceed the user-configured ceiling.\n throttle.setLimit(Math.min(cappedLimit, serverLimit));\n }\n },\n };\n }\n\n // Auto-detect mode — one throttle per tier, tier chosen per request.\n const throttles: Record<TierName, Throttle> = {\n SINGLE_OR_SMALL: createThrottle(TIER_LIMITS.SINGLE_OR_SMALL),\n MEDIUM: createThrottle(TIER_LIMITS.MEDIUM),\n LARGE: createThrottle(TIER_LIMITS.LARGE),\n VERY_LARGE: createThrottle(TIER_LIMITS.VERY_LARGE),\n };\n\n return {\n execute: (path, query, fn) => {\n const tier = determineTier(path, query);\n return throttles[tier].execute(fn);\n },\n adaptToResponse: (response) => {\n if (!adaptToServerHeaders || response === undefined) {\n return;\n }\n const serverLimit = parseRateLimitPolicyHeader(response);\n if (serverLimit !== undefined) {\n // The SINGLE_OR_SMALL tier is the most common; adapting it covers the\n // majority of requests. Other tiers are already conservatively limited.\n throttles.SINGLE_OR_SMALL.setLimit(Math.min(TIER_LIMITS.SINGLE_OR_SMALL, serverLimit));\n }\n },\n };\n}\n"],"mappings":";;;;;;;;;;AASA,MAAM,cAAc;CAClB,iBAAiB;CACjB,QAAQ;CACR,OAAO;CACP,YAAY;CACb;AAID,MAAM,sBAAsB;CAC1B,OAAO;CACP,QAAQ;CACR,OAAO;CACR;AAED,MAAM,mBAAmB;AACzB,MAAM,iBAAiB;;;;;;;AAiCvB,SAAgB,eAAe,cAAgC;CAC7D,IAAI,QAAQ;CACZ,IAAI,cAAc;CAClB,MAAM,QAA2B,EAAE;CAEnC,MAAM,gBAAgB;AACpB,SAAO,MAAM,SAAS,KAAK,cAAc,OAAO;AAC9C;AAEA,GADY,MAAM,OAAO,EACpB;;;CAIT,MAAM,WAAc,OAAqC;AACvD,SAAO,IAAI,SAAY,SAAS,WAAW;AACzC,SAAM,WAAW;AACf,QAAI,CAAC,MACF,UAAU;AACT;AACA,cAAS;AACT,aAAQ,MAAM;QAEf,UAAU;AACT;AACA,cAAS;AACT,YAAO,MAAM;MAEhB;KACD;AACF,YAAS;IACT;;CAGJ,MAAM,YAAY,MAAc;AAC9B,UAAQ;AAER,WAAS;;AAGX,QAAO;EAAE;EAAS;EAAU;;AAI9B,MAAM,uBAAuB;;;;;AAM7B,SAAgB,cAAc,MAAc,OAA0C;AACpF,KAAI,qBAAqB,KAAK,KAAK,CACjC,QAAO;CAGT,MAAM,aAAa,MAAM;CACzB,MAAM,UACF,OAAO,eAAe,WACpB,aACA,OAAO,eAAe,WACpB,OAAO,SAAS,YAAY,GAAG,IAAI,mBACnC;AAER,KAAI,WAAW,oBAAoB,MACjC,QAAO;AAET,KAAI,WAAW,oBAAoB,OACjC,QAAO;AAET,KAAI,WAAW,oBAAoB,MACjC,QAAO;AAET,QAAO;;;;;;;;AAST,SAAgB,2BAA2B,UAAwC;CACjF,MAAM,SAAS,SAAS,QAAQ,IAAI,qBAAqB;AACzD,KAAI,CAAC,OACH;CAEF,MAAM,QAAQ,OAAO,MAAM,UAAU;AACrC,KAAI,CAAC,MACH;AAEF,QAAO,KAAK,IAAI,OAAO,SAAS,MAAM,IAAI,GAAG,EAAE,eAAe;;;;;;;;;;AAWhE,SAAgB,sBAAsB,QAA2D;AAE/F,KAAI,WAAW,MACb,QAAO;EACL,UAAU,OAAO,QAAQ,OAAO,IAAI;EACpC,uBAAuB;EACxB;CAIH,MAAM,EAAE,gBAAgB,uBAAuB,SADP,OAAO,WAAW,WAAW,EAAE,gBAAgB,QAAQ,GAAG;AAIlG,KAAI,mBAAmB,QAAW;EAChC,MAAM,cAAc,KAAK,IAAI,gBAAgB,eAAe;EAC5D,MAAM,WAAW,eAAe,YAAY;AAE5C,SAAO;GACL,UAAU,OAAO,QAAQ,OAAO,SAAS,QAAQ,GAAG;GACpD,kBAAkB,aAAa;AAC7B,QAAI,CAAC,wBAAwB,aAAa,OACxC;IAEF,MAAM,cAAc,2BAA2B,SAAS;AACxD,QAAI,gBAAgB,OAElB,UAAS,SAAS,KAAK,IAAI,aAAa,YAAY,CAAC;;GAG1D;;CAIH,MAAM,YAAwC;EAC5C,iBAAiB,eAAe,YAAY,gBAAgB;EAC5D,QAAQ,eAAe,YAAY,OAAO;EAC1C,OAAO,eAAe,YAAY,MAAM;EACxC,YAAY,eAAe,YAAY,WAAW;EACnD;AAED,QAAO;EACL,UAAU,MAAM,OAAO,OAAO;AAE5B,UAAO,UADM,cAAc,MAAM,MAAM,EAChB,QAAQ,GAAG;;EAEpC,kBAAkB,aAAa;AAC7B,OAAI,CAAC,wBAAwB,aAAa,OACxC;GAEF,MAAM,cAAc,2BAA2B,SAAS;AACxD,OAAI,gBAAgB,OAGlB,WAAU,gBAAgB,SAAS,KAAK,IAAI,YAAY,iBAAiB,YAAY,CAAC;;EAG3F"}
1
+ {"version":3,"file":"rate-limit.cjs","names":[],"sources":["../../src/utils/rate-limit.ts"],"sourcesContent":["/**\n * Rate limiting for the Content API client.\n *\n * Provides both a simple token-bucket throttle and a tier-aware manager\n * that automatically selects the right concurrency limit based on request\n * type (single story vs. listing) and the per_page query parameter — mirroring\n * the tiers enforced server-side by the Storyblok CDN.\n */\n\nconst TIER_LIMITS = {\n SINGLE_OR_SMALL: 50, // single story fetch or per_page ≤ 25\n MEDIUM: 15, // per_page 26–50\n LARGE: 10, // per_page 51–75\n VERY_LARGE: 6, // per_page 76–100\n} as const;\n\ntype TierName = keyof typeof TIER_LIMITS;\n\nconst PER_PAGE_THRESHOLDS = {\n SMALL: 25,\n MEDIUM: 50,\n LARGE: 75,\n} as const;\n\nconst DEFAULT_PER_PAGE = 25;\nconst MAX_RATE_LIMIT = 1_000;\n\nexport interface RateLimitConfig {\n /**\n * Fixed maximum number of concurrent requests per second.\n * When set, disables automatic per_page tier detection and all requests\n * share a single queue at this limit. Capped at 1000.\n */\n maxConcurrency?: number;\n /**\n * Dynamically adjust the rate limit based on the `X-RateLimit-Policy`\n * response header returned by the Storyblok API.\n * @default true\n */\n adaptToServerHeaders?: boolean;\n}\n\nexport interface ThrottleManager {\n execute: <T>(path: string, query: Record<string, unknown>, fn: () => Promise<T>) => Promise<T>;\n adaptToResponse: (response: Response | undefined) => void;\n}\n\ninterface Throttle {\n execute: <T>(fn: () => Promise<T>) => Promise<T>;\n setLimit: (n: number) => void;\n}\n\n/**\n * Concurrency limiter: allows up to `initialLimit` requests to be in-flight\n * at the same time. A slot is freed as soon as the request's promise settles\n * (resolves or rejects), so throughput scales with how quickly requests\n * complete rather than being artificially capped at N per second.\n */\nexport function createThrottle(initialLimit: number): Throttle {\n let limit = initialLimit;\n let activeCount = 0;\n const queue: Array<() => void> = [];\n\n const tryNext = () => {\n while (queue.length > 0 && activeCount < limit) {\n activeCount++;\n const run = queue.shift()!;\n run();\n }\n };\n\n const execute = <T>(fn: () => Promise<T>): Promise<T> => {\n return new Promise<T>((resolve, reject) => {\n queue.push(() => {\n fn().then(\n (value) => {\n activeCount--;\n tryNext();\n resolve(value);\n },\n (error) => {\n activeCount--;\n tryNext();\n reject(error);\n },\n );\n });\n tryNext();\n });\n };\n\n const setLimit = (n: number) => {\n limit = n;\n // If the limit increased, unblock any waiting requests.\n tryNext();\n };\n\n return { execute, setLimit };\n}\n\n// Matches /v2/cdn/stories/<identifier> — a single story fetch (including nested slugs).\nconst SINGLE_STORY_PATH_RE = /\\/v2\\/cdn\\/stories\\/.+$/;\n\n/**\n * Maps a request path + query to one of the four rate-limit tiers.\n * Used by the auto-detection mode of `createThrottleManager`.\n */\nexport function determineTier(path: string, query: Record<string, unknown>): TierName {\n if (SINGLE_STORY_PATH_RE.test(path)) {\n return 'SINGLE_OR_SMALL';\n }\n\n const rawPerPage = query.per_page;\n const perPage\n = typeof rawPerPage === 'number'\n ? rawPerPage\n : typeof rawPerPage === 'string'\n ? Number.parseInt(rawPerPage, 10) || DEFAULT_PER_PAGE\n : DEFAULT_PER_PAGE;\n\n if (perPage <= PER_PAGE_THRESHOLDS.SMALL) {\n return 'SINGLE_OR_SMALL';\n }\n if (perPage <= PER_PAGE_THRESHOLDS.MEDIUM) {\n return 'MEDIUM';\n }\n if (perPage <= PER_PAGE_THRESHOLDS.LARGE) {\n return 'LARGE';\n }\n return 'VERY_LARGE';\n}\n\n/**\n * Extracts the quota (`q=`) value from the `X-RateLimit-Policy` response header,\n * but only when the policy describes a rate limit — not a concurrency limit.\n *\n * The Storyblok CDN currently returns `\"concurrent-requests\";q=30` which is a\n * concurrency limit (always ~30), not a rate limit. Applying that value to the\n * tier-based rate limiter would incorrectly cap throughput below what the API\n * allows. This function therefore ignores concurrency policies and only returns\n * a value for rate-limit policies (e.g. `\"rate-limit\";q=50`), which the API\n * does not send yet but may in the future.\n */\nexport function parseRateLimitPolicyHeader(response: Response): number | undefined {\n const policy = response.headers.get('x-ratelimit-policy');\n if (!policy) {\n return undefined;\n }\n // Only act on rate-limit policies — skip concurrency limits like\n // \"concurrent-requests\";q=30 which represent a different constraint.\n if (policy.includes('\"concurrent-requests\"')) {\n return undefined;\n }\n const match = policy.match(/q=(\\d+)/);\n if (!match) {\n return undefined;\n }\n return Math.min(Number.parseInt(match[1], 10), MAX_RATE_LIMIT);\n}\n\n/**\n * Creates a `ThrottleManager` from the user-supplied `rateLimit` config.\n *\n * - `false` → no throttling (passthrough)\n * - `number` → fixed single queue at that limit\n * - `{ maxConcurrency: n }` → fixed single queue at n req/s\n * - `{}` / `undefined` (default)→ auto-detect tier from path + per_page\n */\nexport function createThrottleManager(config: RateLimitConfig | number | false): ThrottleManager {\n // Disabled — every request goes straight through.\n if (config === false) {\n return {\n execute: (_path, _query, fn) => fn(),\n adaptToResponse: () => {},\n };\n }\n\n const resolvedConfig: RateLimitConfig = typeof config === 'number' ? { maxConcurrency: config } : config;\n const { maxConcurrency, adaptToServerHeaders = true } = resolvedConfig;\n\n // Fixed-limit mode — single queue, optional server-header adaptation.\n if (maxConcurrency !== undefined) {\n const cappedLimit = Math.min(maxConcurrency, MAX_RATE_LIMIT);\n const throttle = createThrottle(cappedLimit);\n\n return {\n execute: (_path, _query, fn) => throttle.execute(fn),\n adaptToResponse: (response) => {\n if (!adaptToServerHeaders || response === undefined) {\n return;\n }\n const serverLimit = parseRateLimitPolicyHeader(response);\n if (serverLimit !== undefined) {\n // Never exceed the user-configured ceiling.\n throttle.setLimit(Math.min(cappedLimit, serverLimit));\n }\n },\n };\n }\n\n // Auto-detect mode — one throttle per tier, tier chosen per request.\n const throttles: Record<TierName, Throttle> = {\n SINGLE_OR_SMALL: createThrottle(TIER_LIMITS.SINGLE_OR_SMALL),\n MEDIUM: createThrottle(TIER_LIMITS.MEDIUM),\n LARGE: createThrottle(TIER_LIMITS.LARGE),\n VERY_LARGE: createThrottle(TIER_LIMITS.VERY_LARGE),\n };\n\n return {\n execute: (path, query, fn) => {\n const tier = determineTier(path, query);\n return throttles[tier].execute(fn);\n },\n adaptToResponse: (response) => {\n if (!adaptToServerHeaders || response === undefined) {\n return;\n }\n const serverLimit = parseRateLimitPolicyHeader(response);\n if (serverLimit !== undefined) {\n // The SINGLE_OR_SMALL tier is the most common; adapting it covers the\n // majority of requests. Other tiers are already conservatively limited.\n throttles.SINGLE_OR_SMALL.setLimit(Math.min(TIER_LIMITS.SINGLE_OR_SMALL, serverLimit));\n }\n },\n };\n}\n"],"mappings":";;;;;;;;;;AASA,MAAM,cAAc;CAClB,iBAAiB;CACjB,QAAQ;CACR,OAAO;CACP,YAAY;CACb;AAID,MAAM,sBAAsB;CAC1B,OAAO;CACP,QAAQ;CACR,OAAO;CACR;AAED,MAAM,mBAAmB;AACzB,MAAM,iBAAiB;;;;;;;AAiCvB,SAAgB,eAAe,cAAgC;CAC7D,IAAI,QAAQ;CACZ,IAAI,cAAc;CAClB,MAAM,QAA2B,EAAE;CAEnC,MAAM,gBAAgB;AACpB,SAAO,MAAM,SAAS,KAAK,cAAc,OAAO;AAC9C;AAEA,GADY,MAAM,OAAO,EACpB;;;CAIT,MAAM,WAAc,OAAqC;AACvD,SAAO,IAAI,SAAY,SAAS,WAAW;AACzC,SAAM,WAAW;AACf,QAAI,CAAC,MACF,UAAU;AACT;AACA,cAAS;AACT,aAAQ,MAAM;QAEf,UAAU;AACT;AACA,cAAS;AACT,YAAO,MAAM;MAEhB;KACD;AACF,YAAS;IACT;;CAGJ,MAAM,YAAY,MAAc;AAC9B,UAAQ;AAER,WAAS;;AAGX,QAAO;EAAE;EAAS;EAAU;;AAI9B,MAAM,uBAAuB;;;;;AAM7B,SAAgB,cAAc,MAAc,OAA0C;AACpF,KAAI,qBAAqB,KAAK,KAAK,CACjC,QAAO;CAGT,MAAM,aAAa,MAAM;CACzB,MAAM,UACF,OAAO,eAAe,WACpB,aACA,OAAO,eAAe,WACpB,OAAO,SAAS,YAAY,GAAG,IAAI,mBACnC;AAER,KAAI,WAAW,oBAAoB,MACjC,QAAO;AAET,KAAI,WAAW,oBAAoB,OACjC,QAAO;AAET,KAAI,WAAW,oBAAoB,MACjC,QAAO;AAET,QAAO;;;;;;;;;;;;;AAcT,SAAgB,2BAA2B,UAAwC;CACjF,MAAM,SAAS,SAAS,QAAQ,IAAI,qBAAqB;AACzD,KAAI,CAAC,OACH;AAIF,KAAI,OAAO,SAAS,0BAAwB,CAC1C;CAEF,MAAM,QAAQ,OAAO,MAAM,UAAU;AACrC,KAAI,CAAC,MACH;AAEF,QAAO,KAAK,IAAI,OAAO,SAAS,MAAM,IAAI,GAAG,EAAE,eAAe;;;;;;;;;;AAWhE,SAAgB,sBAAsB,QAA2D;AAE/F,KAAI,WAAW,MACb,QAAO;EACL,UAAU,OAAO,QAAQ,OAAO,IAAI;EACpC,uBAAuB;EACxB;CAIH,MAAM,EAAE,gBAAgB,uBAAuB,SADP,OAAO,WAAW,WAAW,EAAE,gBAAgB,QAAQ,GAAG;AAIlG,KAAI,mBAAmB,QAAW;EAChC,MAAM,cAAc,KAAK,IAAI,gBAAgB,eAAe;EAC5D,MAAM,WAAW,eAAe,YAAY;AAE5C,SAAO;GACL,UAAU,OAAO,QAAQ,OAAO,SAAS,QAAQ,GAAG;GACpD,kBAAkB,aAAa;AAC7B,QAAI,CAAC,wBAAwB,aAAa,OACxC;IAEF,MAAM,cAAc,2BAA2B,SAAS;AACxD,QAAI,gBAAgB,OAElB,UAAS,SAAS,KAAK,IAAI,aAAa,YAAY,CAAC;;GAG1D;;CAIH,MAAM,YAAwC;EAC5C,iBAAiB,eAAe,YAAY,gBAAgB;EAC5D,QAAQ,eAAe,YAAY,OAAO;EAC1C,OAAO,eAAe,YAAY,MAAM;EACxC,YAAY,eAAe,YAAY,WAAW;EACnD;AAED,QAAO;EACL,UAAU,MAAM,OAAO,OAAO;AAE5B,UAAO,UADM,cAAc,MAAM,MAAM,EAChB,QAAQ,GAAG;;EAEpC,kBAAkB,aAAa;AAC7B,OAAI,CAAC,wBAAwB,aAAa,OACxC;GAEF,MAAM,cAAc,2BAA2B,SAAS;AACxD,OAAI,gBAAgB,OAGlB,WAAU,gBAAgB,SAAS,KAAK,IAAI,YAAY,iBAAiB,YAAY,CAAC;;EAG3F"}
@@ -13,24 +13,10 @@ interface RateLimitConfig {
13
13
  */
14
14
  adaptToServerHeaders?: boolean;
15
15
  }
16
- interface Throttle {
17
- execute: <T>(fn: () => Promise<T>) => Promise<T>;
18
- setLimit: (n: number) => void;
16
+ interface ThrottleManager {
17
+ execute: <T>(path: string, query: Record<string, unknown>, fn: () => Promise<T>) => Promise<T>;
18
+ adaptToResponse: (response: Response | undefined) => void;
19
19
  }
20
- /**
21
- * Concurrency limiter: allows up to `initialLimit` requests to be in-flight
22
- * at the same time. A slot is freed as soon as the request's promise settles
23
- * (resolves or rejects), so throughput scales with how quickly requests
24
- * complete rather than being artificially capped at N per second.
25
- */
26
- declare function createThrottle(initialLimit: number): Throttle;
27
- /**
28
- * Extracts the quota (`q=`) value from the `X-RateLimit-Policy` response header.
29
- * Returns `undefined` if the header is absent or unparseable.
30
- *
31
- * Example header: `"concurrent-requests";q=30`
32
- */
33
- declare function parseRateLimitPolicyHeader(response: Response): number | undefined;
34
20
  //#endregion
35
- export { RateLimitConfig, createThrottle, parseRateLimitPolicyHeader };
21
+ export { RateLimitConfig, ThrottleManager };
36
22
  //# sourceMappingURL=rate-limit.d.cts.map
@@ -13,24 +13,10 @@ interface RateLimitConfig {
13
13
  */
14
14
  adaptToServerHeaders?: boolean;
15
15
  }
16
- interface Throttle {
17
- execute: <T>(fn: () => Promise<T>) => Promise<T>;
18
- setLimit: (n: number) => void;
16
+ interface ThrottleManager {
17
+ execute: <T>(path: string, query: Record<string, unknown>, fn: () => Promise<T>) => Promise<T>;
18
+ adaptToResponse: (response: Response | undefined) => void;
19
19
  }
20
- /**
21
- * Concurrency limiter: allows up to `initialLimit` requests to be in-flight
22
- * at the same time. A slot is freed as soon as the request's promise settles
23
- * (resolves or rejects), so throughput scales with how quickly requests
24
- * complete rather than being artificially capped at N per second.
25
- */
26
- declare function createThrottle(initialLimit: number): Throttle;
27
- /**
28
- * Extracts the quota (`q=`) value from the `X-RateLimit-Policy` response header.
29
- * Returns `undefined` if the header is absent or unparseable.
30
- *
31
- * Example header: `"concurrent-requests";q=30`
32
- */
33
- declare function parseRateLimitPolicyHeader(response: Response): number | undefined;
34
20
  //#endregion
35
- export { RateLimitConfig, createThrottle, parseRateLimitPolicyHeader };
21
+ export { RateLimitConfig, ThrottleManager };
36
22
  //# sourceMappingURL=rate-limit.d.mts.map
@@ -76,14 +76,20 @@ function determineTier(path, query) {
76
76
  return "VERY_LARGE";
77
77
  }
78
78
  /**
79
- * Extracts the quota (`q=`) value from the `X-RateLimit-Policy` response header.
80
- * Returns `undefined` if the header is absent or unparseable.
79
+ * Extracts the quota (`q=`) value from the `X-RateLimit-Policy` response header,
80
+ * but only when the policy describes a rate limit — not a concurrency limit.
81
81
  *
82
- * Example header: `"concurrent-requests";q=30`
82
+ * The Storyblok CDN currently returns `"concurrent-requests";q=30` which is a
83
+ * concurrency limit (always ~30), not a rate limit. Applying that value to the
84
+ * tier-based rate limiter would incorrectly cap throughput below what the API
85
+ * allows. This function therefore ignores concurrency policies and only returns
86
+ * a value for rate-limit policies (e.g. `"rate-limit";q=50`), which the API
87
+ * does not send yet but may in the future.
83
88
  */
84
89
  function parseRateLimitPolicyHeader(response) {
85
90
  const policy = response.headers.get("x-ratelimit-policy");
86
91
  if (!policy) return;
92
+ if (policy.includes("\"concurrent-requests\"")) return;
87
93
  const match = policy.match(/q=(\d+)/);
88
94
  if (!match) return;
89
95
  return Math.min(Number.parseInt(match[1], 10), MAX_RATE_LIMIT);
@@ -133,5 +139,5 @@ function createThrottleManager(config) {
133
139
  }
134
140
 
135
141
  //#endregion
136
- export { createThrottle, createThrottleManager, parseRateLimitPolicyHeader };
142
+ export { createThrottleManager };
137
143
  //# sourceMappingURL=rate-limit.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"rate-limit.mjs","names":[],"sources":["../../src/utils/rate-limit.ts"],"sourcesContent":["/**\n * Rate limiting for the Content API client.\n *\n * Provides both a simple token-bucket throttle and a tier-aware manager\n * that automatically selects the right concurrency limit based on request\n * type (single story vs. listing) and the per_page query parameter — mirroring\n * the tiers enforced server-side by the Storyblok CDN.\n */\n\nconst TIER_LIMITS = {\n SINGLE_OR_SMALL: 50, // single story fetch or per_page ≤ 25\n MEDIUM: 15, // per_page 26–50\n LARGE: 10, // per_page 51–75\n VERY_LARGE: 6, // per_page 76–100\n} as const;\n\ntype TierName = keyof typeof TIER_LIMITS;\n\nconst PER_PAGE_THRESHOLDS = {\n SMALL: 25,\n MEDIUM: 50,\n LARGE: 75,\n} as const;\n\nconst DEFAULT_PER_PAGE = 25;\nconst MAX_RATE_LIMIT = 1_000;\n\nexport interface RateLimitConfig {\n /**\n * Fixed maximum number of concurrent requests per second.\n * When set, disables automatic per_page tier detection and all requests\n * share a single queue at this limit. Capped at 1000.\n */\n maxConcurrency?: number;\n /**\n * Dynamically adjust the rate limit based on the `X-RateLimit-Policy`\n * response header returned by the Storyblok API.\n * @default true\n */\n adaptToServerHeaders?: boolean;\n}\n\nexport interface ThrottleManager {\n execute: <T>(path: string, query: Record<string, unknown>, fn: () => Promise<T>) => Promise<T>;\n adaptToResponse: (response: Response | undefined) => void;\n}\n\ninterface Throttle {\n execute: <T>(fn: () => Promise<T>) => Promise<T>;\n setLimit: (n: number) => void;\n}\n\n/**\n * Concurrency limiter: allows up to `initialLimit` requests to be in-flight\n * at the same time. A slot is freed as soon as the request's promise settles\n * (resolves or rejects), so throughput scales with how quickly requests\n * complete rather than being artificially capped at N per second.\n */\nexport function createThrottle(initialLimit: number): Throttle {\n let limit = initialLimit;\n let activeCount = 0;\n const queue: Array<() => void> = [];\n\n const tryNext = () => {\n while (queue.length > 0 && activeCount < limit) {\n activeCount++;\n const run = queue.shift()!;\n run();\n }\n };\n\n const execute = <T>(fn: () => Promise<T>): Promise<T> => {\n return new Promise<T>((resolve, reject) => {\n queue.push(() => {\n fn().then(\n (value) => {\n activeCount--;\n tryNext();\n resolve(value);\n },\n (error) => {\n activeCount--;\n tryNext();\n reject(error);\n },\n );\n });\n tryNext();\n });\n };\n\n const setLimit = (n: number) => {\n limit = n;\n // If the limit increased, unblock any waiting requests.\n tryNext();\n };\n\n return { execute, setLimit };\n}\n\n// Matches /v2/cdn/stories/<identifier> — a single story fetch (including nested slugs).\nconst SINGLE_STORY_PATH_RE = /\\/v2\\/cdn\\/stories\\/.+$/;\n\n/**\n * Maps a request path + query to one of the four rate-limit tiers.\n * Used by the auto-detection mode of `createThrottleManager`.\n */\nexport function determineTier(path: string, query: Record<string, unknown>): TierName {\n if (SINGLE_STORY_PATH_RE.test(path)) {\n return 'SINGLE_OR_SMALL';\n }\n\n const rawPerPage = query.per_page;\n const perPage\n = typeof rawPerPage === 'number'\n ? rawPerPage\n : typeof rawPerPage === 'string'\n ? Number.parseInt(rawPerPage, 10) || DEFAULT_PER_PAGE\n : DEFAULT_PER_PAGE;\n\n if (perPage <= PER_PAGE_THRESHOLDS.SMALL) {\n return 'SINGLE_OR_SMALL';\n }\n if (perPage <= PER_PAGE_THRESHOLDS.MEDIUM) {\n return 'MEDIUM';\n }\n if (perPage <= PER_PAGE_THRESHOLDS.LARGE) {\n return 'LARGE';\n }\n return 'VERY_LARGE';\n}\n\n/**\n * Extracts the quota (`q=`) value from the `X-RateLimit-Policy` response header.\n * Returns `undefined` if the header is absent or unparseable.\n *\n * Example header: `\"concurrent-requests\";q=30`\n */\nexport function parseRateLimitPolicyHeader(response: Response): number | undefined {\n const policy = response.headers.get('x-ratelimit-policy');\n if (!policy) {\n return undefined;\n }\n const match = policy.match(/q=(\\d+)/);\n if (!match) {\n return undefined;\n }\n return Math.min(Number.parseInt(match[1], 10), MAX_RATE_LIMIT);\n}\n\n/**\n * Creates a `ThrottleManager` from the user-supplied `rateLimit` config.\n *\n * - `false` → no throttling (passthrough)\n * - `number` → fixed single queue at that limit\n * - `{ maxConcurrency: n }` → fixed single queue at n req/s\n * - `{}` / `undefined` (default)→ auto-detect tier from path + per_page\n */\nexport function createThrottleManager(config: RateLimitConfig | number | false): ThrottleManager {\n // Disabled — every request goes straight through.\n if (config === false) {\n return {\n execute: (_path, _query, fn) => fn(),\n adaptToResponse: () => {},\n };\n }\n\n const resolvedConfig: RateLimitConfig = typeof config === 'number' ? { maxConcurrency: config } : config;\n const { maxConcurrency, adaptToServerHeaders = true } = resolvedConfig;\n\n // Fixed-limit mode — single queue, optional server-header adaptation.\n if (maxConcurrency !== undefined) {\n const cappedLimit = Math.min(maxConcurrency, MAX_RATE_LIMIT);\n const throttle = createThrottle(cappedLimit);\n\n return {\n execute: (_path, _query, fn) => throttle.execute(fn),\n adaptToResponse: (response) => {\n if (!adaptToServerHeaders || response === undefined) {\n return;\n }\n const serverLimit = parseRateLimitPolicyHeader(response);\n if (serverLimit !== undefined) {\n // Never exceed the user-configured ceiling.\n throttle.setLimit(Math.min(cappedLimit, serverLimit));\n }\n },\n };\n }\n\n // Auto-detect mode — one throttle per tier, tier chosen per request.\n const throttles: Record<TierName, Throttle> = {\n SINGLE_OR_SMALL: createThrottle(TIER_LIMITS.SINGLE_OR_SMALL),\n MEDIUM: createThrottle(TIER_LIMITS.MEDIUM),\n LARGE: createThrottle(TIER_LIMITS.LARGE),\n VERY_LARGE: createThrottle(TIER_LIMITS.VERY_LARGE),\n };\n\n return {\n execute: (path, query, fn) => {\n const tier = determineTier(path, query);\n return throttles[tier].execute(fn);\n },\n adaptToResponse: (response) => {\n if (!adaptToServerHeaders || response === undefined) {\n return;\n }\n const serverLimit = parseRateLimitPolicyHeader(response);\n if (serverLimit !== undefined) {\n // The SINGLE_OR_SMALL tier is the most common; adapting it covers the\n // majority of requests. Other tiers are already conservatively limited.\n throttles.SINGLE_OR_SMALL.setLimit(Math.min(TIER_LIMITS.SINGLE_OR_SMALL, serverLimit));\n }\n },\n };\n}\n"],"mappings":";;;;;;;;;AASA,MAAM,cAAc;CAClB,iBAAiB;CACjB,QAAQ;CACR,OAAO;CACP,YAAY;CACb;AAID,MAAM,sBAAsB;CAC1B,OAAO;CACP,QAAQ;CACR,OAAO;CACR;AAED,MAAM,mBAAmB;AACzB,MAAM,iBAAiB;;;;;;;AAiCvB,SAAgB,eAAe,cAAgC;CAC7D,IAAI,QAAQ;CACZ,IAAI,cAAc;CAClB,MAAM,QAA2B,EAAE;CAEnC,MAAM,gBAAgB;AACpB,SAAO,MAAM,SAAS,KAAK,cAAc,OAAO;AAC9C;AAEA,GADY,MAAM,OAAO,EACpB;;;CAIT,MAAM,WAAc,OAAqC;AACvD,SAAO,IAAI,SAAY,SAAS,WAAW;AACzC,SAAM,WAAW;AACf,QAAI,CAAC,MACF,UAAU;AACT;AACA,cAAS;AACT,aAAQ,MAAM;QAEf,UAAU;AACT;AACA,cAAS;AACT,YAAO,MAAM;MAEhB;KACD;AACF,YAAS;IACT;;CAGJ,MAAM,YAAY,MAAc;AAC9B,UAAQ;AAER,WAAS;;AAGX,QAAO;EAAE;EAAS;EAAU;;AAI9B,MAAM,uBAAuB;;;;;AAM7B,SAAgB,cAAc,MAAc,OAA0C;AACpF,KAAI,qBAAqB,KAAK,KAAK,CACjC,QAAO;CAGT,MAAM,aAAa,MAAM;CACzB,MAAM,UACF,OAAO,eAAe,WACpB,aACA,OAAO,eAAe,WACpB,OAAO,SAAS,YAAY,GAAG,IAAI,mBACnC;AAER,KAAI,WAAW,oBAAoB,MACjC,QAAO;AAET,KAAI,WAAW,oBAAoB,OACjC,QAAO;AAET,KAAI,WAAW,oBAAoB,MACjC,QAAO;AAET,QAAO;;;;;;;;AAST,SAAgB,2BAA2B,UAAwC;CACjF,MAAM,SAAS,SAAS,QAAQ,IAAI,qBAAqB;AACzD,KAAI,CAAC,OACH;CAEF,MAAM,QAAQ,OAAO,MAAM,UAAU;AACrC,KAAI,CAAC,MACH;AAEF,QAAO,KAAK,IAAI,OAAO,SAAS,MAAM,IAAI,GAAG,EAAE,eAAe;;;;;;;;;;AAWhE,SAAgB,sBAAsB,QAA2D;AAE/F,KAAI,WAAW,MACb,QAAO;EACL,UAAU,OAAO,QAAQ,OAAO,IAAI;EACpC,uBAAuB;EACxB;CAIH,MAAM,EAAE,gBAAgB,uBAAuB,SADP,OAAO,WAAW,WAAW,EAAE,gBAAgB,QAAQ,GAAG;AAIlG,KAAI,mBAAmB,QAAW;EAChC,MAAM,cAAc,KAAK,IAAI,gBAAgB,eAAe;EAC5D,MAAM,WAAW,eAAe,YAAY;AAE5C,SAAO;GACL,UAAU,OAAO,QAAQ,OAAO,SAAS,QAAQ,GAAG;GACpD,kBAAkB,aAAa;AAC7B,QAAI,CAAC,wBAAwB,aAAa,OACxC;IAEF,MAAM,cAAc,2BAA2B,SAAS;AACxD,QAAI,gBAAgB,OAElB,UAAS,SAAS,KAAK,IAAI,aAAa,YAAY,CAAC;;GAG1D;;CAIH,MAAM,YAAwC;EAC5C,iBAAiB,eAAe,YAAY,gBAAgB;EAC5D,QAAQ,eAAe,YAAY,OAAO;EAC1C,OAAO,eAAe,YAAY,MAAM;EACxC,YAAY,eAAe,YAAY,WAAW;EACnD;AAED,QAAO;EACL,UAAU,MAAM,OAAO,OAAO;AAE5B,UAAO,UADM,cAAc,MAAM,MAAM,EAChB,QAAQ,GAAG;;EAEpC,kBAAkB,aAAa;AAC7B,OAAI,CAAC,wBAAwB,aAAa,OACxC;GAEF,MAAM,cAAc,2BAA2B,SAAS;AACxD,OAAI,gBAAgB,OAGlB,WAAU,gBAAgB,SAAS,KAAK,IAAI,YAAY,iBAAiB,YAAY,CAAC;;EAG3F"}
1
+ {"version":3,"file":"rate-limit.mjs","names":[],"sources":["../../src/utils/rate-limit.ts"],"sourcesContent":["/**\n * Rate limiting for the Content API client.\n *\n * Provides both a simple token-bucket throttle and a tier-aware manager\n * that automatically selects the right concurrency limit based on request\n * type (single story vs. listing) and the per_page query parameter — mirroring\n * the tiers enforced server-side by the Storyblok CDN.\n */\n\nconst TIER_LIMITS = {\n SINGLE_OR_SMALL: 50, // single story fetch or per_page ≤ 25\n MEDIUM: 15, // per_page 26–50\n LARGE: 10, // per_page 51–75\n VERY_LARGE: 6, // per_page 76–100\n} as const;\n\ntype TierName = keyof typeof TIER_LIMITS;\n\nconst PER_PAGE_THRESHOLDS = {\n SMALL: 25,\n MEDIUM: 50,\n LARGE: 75,\n} as const;\n\nconst DEFAULT_PER_PAGE = 25;\nconst MAX_RATE_LIMIT = 1_000;\n\nexport interface RateLimitConfig {\n /**\n * Fixed maximum number of concurrent requests per second.\n * When set, disables automatic per_page tier detection and all requests\n * share a single queue at this limit. Capped at 1000.\n */\n maxConcurrency?: number;\n /**\n * Dynamically adjust the rate limit based on the `X-RateLimit-Policy`\n * response header returned by the Storyblok API.\n * @default true\n */\n adaptToServerHeaders?: boolean;\n}\n\nexport interface ThrottleManager {\n execute: <T>(path: string, query: Record<string, unknown>, fn: () => Promise<T>) => Promise<T>;\n adaptToResponse: (response: Response | undefined) => void;\n}\n\ninterface Throttle {\n execute: <T>(fn: () => Promise<T>) => Promise<T>;\n setLimit: (n: number) => void;\n}\n\n/**\n * Concurrency limiter: allows up to `initialLimit` requests to be in-flight\n * at the same time. A slot is freed as soon as the request's promise settles\n * (resolves or rejects), so throughput scales with how quickly requests\n * complete rather than being artificially capped at N per second.\n */\nexport function createThrottle(initialLimit: number): Throttle {\n let limit = initialLimit;\n let activeCount = 0;\n const queue: Array<() => void> = [];\n\n const tryNext = () => {\n while (queue.length > 0 && activeCount < limit) {\n activeCount++;\n const run = queue.shift()!;\n run();\n }\n };\n\n const execute = <T>(fn: () => Promise<T>): Promise<T> => {\n return new Promise<T>((resolve, reject) => {\n queue.push(() => {\n fn().then(\n (value) => {\n activeCount--;\n tryNext();\n resolve(value);\n },\n (error) => {\n activeCount--;\n tryNext();\n reject(error);\n },\n );\n });\n tryNext();\n });\n };\n\n const setLimit = (n: number) => {\n limit = n;\n // If the limit increased, unblock any waiting requests.\n tryNext();\n };\n\n return { execute, setLimit };\n}\n\n// Matches /v2/cdn/stories/<identifier> — a single story fetch (including nested slugs).\nconst SINGLE_STORY_PATH_RE = /\\/v2\\/cdn\\/stories\\/.+$/;\n\n/**\n * Maps a request path + query to one of the four rate-limit tiers.\n * Used by the auto-detection mode of `createThrottleManager`.\n */\nexport function determineTier(path: string, query: Record<string, unknown>): TierName {\n if (SINGLE_STORY_PATH_RE.test(path)) {\n return 'SINGLE_OR_SMALL';\n }\n\n const rawPerPage = query.per_page;\n const perPage\n = typeof rawPerPage === 'number'\n ? rawPerPage\n : typeof rawPerPage === 'string'\n ? Number.parseInt(rawPerPage, 10) || DEFAULT_PER_PAGE\n : DEFAULT_PER_PAGE;\n\n if (perPage <= PER_PAGE_THRESHOLDS.SMALL) {\n return 'SINGLE_OR_SMALL';\n }\n if (perPage <= PER_PAGE_THRESHOLDS.MEDIUM) {\n return 'MEDIUM';\n }\n if (perPage <= PER_PAGE_THRESHOLDS.LARGE) {\n return 'LARGE';\n }\n return 'VERY_LARGE';\n}\n\n/**\n * Extracts the quota (`q=`) value from the `X-RateLimit-Policy` response header,\n * but only when the policy describes a rate limit — not a concurrency limit.\n *\n * The Storyblok CDN currently returns `\"concurrent-requests\";q=30` which is a\n * concurrency limit (always ~30), not a rate limit. Applying that value to the\n * tier-based rate limiter would incorrectly cap throughput below what the API\n * allows. This function therefore ignores concurrency policies and only returns\n * a value for rate-limit policies (e.g. `\"rate-limit\";q=50`), which the API\n * does not send yet but may in the future.\n */\nexport function parseRateLimitPolicyHeader(response: Response): number | undefined {\n const policy = response.headers.get('x-ratelimit-policy');\n if (!policy) {\n return undefined;\n }\n // Only act on rate-limit policies — skip concurrency limits like\n // \"concurrent-requests\";q=30 which represent a different constraint.\n if (policy.includes('\"concurrent-requests\"')) {\n return undefined;\n }\n const match = policy.match(/q=(\\d+)/);\n if (!match) {\n return undefined;\n }\n return Math.min(Number.parseInt(match[1], 10), MAX_RATE_LIMIT);\n}\n\n/**\n * Creates a `ThrottleManager` from the user-supplied `rateLimit` config.\n *\n * - `false` → no throttling (passthrough)\n * - `number` → fixed single queue at that limit\n * - `{ maxConcurrency: n }` → fixed single queue at n req/s\n * - `{}` / `undefined` (default)→ auto-detect tier from path + per_page\n */\nexport function createThrottleManager(config: RateLimitConfig | number | false): ThrottleManager {\n // Disabled — every request goes straight through.\n if (config === false) {\n return {\n execute: (_path, _query, fn) => fn(),\n adaptToResponse: () => {},\n };\n }\n\n const resolvedConfig: RateLimitConfig = typeof config === 'number' ? { maxConcurrency: config } : config;\n const { maxConcurrency, adaptToServerHeaders = true } = resolvedConfig;\n\n // Fixed-limit mode — single queue, optional server-header adaptation.\n if (maxConcurrency !== undefined) {\n const cappedLimit = Math.min(maxConcurrency, MAX_RATE_LIMIT);\n const throttle = createThrottle(cappedLimit);\n\n return {\n execute: (_path, _query, fn) => throttle.execute(fn),\n adaptToResponse: (response) => {\n if (!adaptToServerHeaders || response === undefined) {\n return;\n }\n const serverLimit = parseRateLimitPolicyHeader(response);\n if (serverLimit !== undefined) {\n // Never exceed the user-configured ceiling.\n throttle.setLimit(Math.min(cappedLimit, serverLimit));\n }\n },\n };\n }\n\n // Auto-detect mode — one throttle per tier, tier chosen per request.\n const throttles: Record<TierName, Throttle> = {\n SINGLE_OR_SMALL: createThrottle(TIER_LIMITS.SINGLE_OR_SMALL),\n MEDIUM: createThrottle(TIER_LIMITS.MEDIUM),\n LARGE: createThrottle(TIER_LIMITS.LARGE),\n VERY_LARGE: createThrottle(TIER_LIMITS.VERY_LARGE),\n };\n\n return {\n execute: (path, query, fn) => {\n const tier = determineTier(path, query);\n return throttles[tier].execute(fn);\n },\n adaptToResponse: (response) => {\n if (!adaptToServerHeaders || response === undefined) {\n return;\n }\n const serverLimit = parseRateLimitPolicyHeader(response);\n if (serverLimit !== undefined) {\n // The SINGLE_OR_SMALL tier is the most common; adapting it covers the\n // majority of requests. Other tiers are already conservatively limited.\n throttles.SINGLE_OR_SMALL.setLimit(Math.min(TIER_LIMITS.SINGLE_OR_SMALL, serverLimit));\n }\n },\n };\n}\n"],"mappings":";;;;;;;;;AASA,MAAM,cAAc;CAClB,iBAAiB;CACjB,QAAQ;CACR,OAAO;CACP,YAAY;CACb;AAID,MAAM,sBAAsB;CAC1B,OAAO;CACP,QAAQ;CACR,OAAO;CACR;AAED,MAAM,mBAAmB;AACzB,MAAM,iBAAiB;;;;;;;AAiCvB,SAAgB,eAAe,cAAgC;CAC7D,IAAI,QAAQ;CACZ,IAAI,cAAc;CAClB,MAAM,QAA2B,EAAE;CAEnC,MAAM,gBAAgB;AACpB,SAAO,MAAM,SAAS,KAAK,cAAc,OAAO;AAC9C;AAEA,GADY,MAAM,OAAO,EACpB;;;CAIT,MAAM,WAAc,OAAqC;AACvD,SAAO,IAAI,SAAY,SAAS,WAAW;AACzC,SAAM,WAAW;AACf,QAAI,CAAC,MACF,UAAU;AACT;AACA,cAAS;AACT,aAAQ,MAAM;QAEf,UAAU;AACT;AACA,cAAS;AACT,YAAO,MAAM;MAEhB;KACD;AACF,YAAS;IACT;;CAGJ,MAAM,YAAY,MAAc;AAC9B,UAAQ;AAER,WAAS;;AAGX,QAAO;EAAE;EAAS;EAAU;;AAI9B,MAAM,uBAAuB;;;;;AAM7B,SAAgB,cAAc,MAAc,OAA0C;AACpF,KAAI,qBAAqB,KAAK,KAAK,CACjC,QAAO;CAGT,MAAM,aAAa,MAAM;CACzB,MAAM,UACF,OAAO,eAAe,WACpB,aACA,OAAO,eAAe,WACpB,OAAO,SAAS,YAAY,GAAG,IAAI,mBACnC;AAER,KAAI,WAAW,oBAAoB,MACjC,QAAO;AAET,KAAI,WAAW,oBAAoB,OACjC,QAAO;AAET,KAAI,WAAW,oBAAoB,MACjC,QAAO;AAET,QAAO;;;;;;;;;;;;;AAcT,SAAgB,2BAA2B,UAAwC;CACjF,MAAM,SAAS,SAAS,QAAQ,IAAI,qBAAqB;AACzD,KAAI,CAAC,OACH;AAIF,KAAI,OAAO,SAAS,0BAAwB,CAC1C;CAEF,MAAM,QAAQ,OAAO,MAAM,UAAU;AACrC,KAAI,CAAC,MACH;AAEF,QAAO,KAAK,IAAI,OAAO,SAAS,MAAM,IAAI,GAAG,EAAE,eAAe;;;;;;;;;;AAWhE,SAAgB,sBAAsB,QAA2D;AAE/F,KAAI,WAAW,MACb,QAAO;EACL,UAAU,OAAO,QAAQ,OAAO,IAAI;EACpC,uBAAuB;EACxB;CAIH,MAAM,EAAE,gBAAgB,uBAAuB,SADP,OAAO,WAAW,WAAW,EAAE,gBAAgB,QAAQ,GAAG;AAIlG,KAAI,mBAAmB,QAAW;EAChC,MAAM,cAAc,KAAK,IAAI,gBAAgB,eAAe;EAC5D,MAAM,WAAW,eAAe,YAAY;AAE5C,SAAO;GACL,UAAU,OAAO,QAAQ,OAAO,SAAS,QAAQ,GAAG;GACpD,kBAAkB,aAAa;AAC7B,QAAI,CAAC,wBAAwB,aAAa,OACxC;IAEF,MAAM,cAAc,2BAA2B,SAAS;AACxD,QAAI,gBAAgB,OAElB,UAAS,SAAS,KAAK,IAAI,aAAa,YAAY,CAAC;;GAG1D;;CAIH,MAAM,YAAwC;EAC5C,iBAAiB,eAAe,YAAY,gBAAgB;EAC5D,QAAQ,eAAe,YAAY,OAAO;EAC1C,OAAO,eAAe,YAAY,MAAM;EACxC,YAAY,eAAe,YAAY,WAAW;EACnD;AAED,QAAO;EACL,UAAU,MAAM,OAAO,OAAO;AAE5B,UAAO,UADM,cAAc,MAAM,MAAM,EAChB,QAAQ,GAAG;;EAEpC,kBAAkB,aAAa;AAC7B,OAAI,CAAC,wBAAwB,aAAa,OACxC;GAEF,MAAM,cAAc,2BAA2B,SAAS;AACxD,OAAI,gBAAgB,OAGlB,WAAU,gBAAgB,SAAS,KAAK,IAAI,YAAY,iBAAiB,YAAY,CAAC;;EAG3F"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@storyblok/api-client",
3
3
  "type": "module",
4
- "version": "0.2.4",
4
+ "version": "1.0.0-alpha.1",
5
5
  "private": false,
6
6
  "description": "Storyblok Content Delivery API Client",
7
7
  "author": "",
@@ -9,7 +9,7 @@
9
9
  "homepage": "https://github.com/storyblok/monoblok/tree/main/packages/capi-client#readme",
10
10
  "repository": {
11
11
  "type": "git",
12
- "url": "https://github.com/storyblok/monoblok.git",
12
+ "url": "git+https://github.com/storyblok/monoblok.git",
13
13
  "directory": "packages/capi-client"
14
14
  },
15
15
  "bugs": {
@@ -31,27 +31,35 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "ky": "^1.14.3",
34
- "@storyblok/region-helper": "1.4.0"
34
+ "@storyblok/region-helper": "1.4.0",
35
+ "@storyblok/schema": "1.0.0-alpha.0"
35
36
  },
36
37
  "devDependencies": {
37
38
  "@hey-api/openapi-ts": "^0.92.3",
38
39
  "@msw/source": "^0.6.1",
39
40
  "@types/node": "^24.11.0",
41
+ "dotenv": "^17.3.1",
40
42
  "eslint": "^9.39.2",
41
43
  "glob": "^13.0.6",
42
44
  "msw": "^2.12.9",
43
45
  "pathe": "^2.0.3",
44
46
  "tsdown": "^0.20.3",
45
47
  "tsx": "^4.21.0",
46
- "vitest": "^4.0.18",
48
+ "vitest": "^4.1.3",
47
49
  "@storyblok/eslint-config": "0.5.0",
48
- "@storyblok/openapi": "2.0.0"
50
+ "@storyblok/management-api-client": "1.0.0-alpha.1",
51
+ "@storyblok/openapi": "2.1.0-alpha.0"
49
52
  },
50
53
  "nx": {
51
54
  "targets": {
52
55
  "generate": {
53
56
  "dependsOn": [
54
- "^build"
57
+ {
58
+ "projects": [
59
+ "@storyblok/openapi"
60
+ ],
61
+ "target": "build"
62
+ }
55
63
  ],
56
64
  "outputs": [
57
65
  "{projectRoot}/src/generated/**"
@@ -72,8 +80,10 @@
72
80
  "generate": "tsx scripts/generate.ts",
73
81
  "build": "tsdown",
74
82
  "test": "vitest run",
83
+ "test:types": "tsc --noEmit --skipLibCheck",
84
+ "test:e2e": "vitest run -c vitest.config.e2e.ts",
85
+ "coverage": "vitest run --coverage",
75
86
  "lint": "eslint .",
76
- "lint:fix": "eslint . --fix",
77
- "typecheck": "tsc --noEmit"
87
+ "lint:fix": "eslint . --fix"
78
88
  }
79
89
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.cjs","names":["createThrottleManager","createMemoryCacheProvider","createStrategy","createClient","createConfig","ClientError","extractCv","applyCvToQuery","shouldUseCache","createCacheKey","createStoriesResource","createDatasourceEntriesResource","createDatasourcesResource","createLinksResource","createSpacesResource","createTagsResource"],"sources":["../src/index.ts"],"sourcesContent":["import { createClient, createConfig } from './generated/shared/client';\nimport type { StoryCapi } from './generated/stories';\nimport type { CacheProvider, CacheStrategy, CacheStrategyHandler } from './utils/cache';\nimport { createMemoryCacheProvider, createStrategy } from './utils/cache';\nimport { ClientError } from './error';\nimport type { RateLimitConfig } from './utils/rate-limit';\nimport { createThrottleManager } from './utils/rate-limit';\nimport { applyCvToQuery, extractCv } from './utils/cv';\nimport { createCacheKey, shouldUseCache } from './utils/request';\nimport { getRegionBaseUrl, type Region } from '@storyblok/region-helper';\nimport type { RetryOptions } from 'ky';\nimport type { Client } from './generated/shared/client';\nimport type { ApiResponse, FetchOptions, HttpRequestMethod, HttpRequestOptions, RequestWithCacheOptions, ResourceDeps } from './types';\nimport { createStoriesResource } from './resources/stories';\nimport { createLinksResource } from './resources/links';\nimport { createTagsResource } from './resources/tags';\nimport { createDatasourcesResource } from './resources/datasources';\nimport { createDatasourceEntriesResource } from './resources/datasource-entries';\nimport { createSpacesResource } from './resources/spaces';\n\ntype Prettify<T> = {\n [K in keyof T]: T[K];\n} & {};\n\nexport type Story = Prettify<StoryCapi>;\nexport { ClientError } from './error';\nexport type { DatasourceEntryCapi as DatasourceEntry } from './generated/datasource_entries/types.gen';\nexport type { DatasourceCapi as Datasource } from './generated/datasources/types.gen';\nexport type { LinkCapi as Link } from './generated/links/types.gen';\nexport type { Middleware } from './generated/shared/client/utils.gen';\nexport type { SpaceCapi as Space } from './generated/spaces/types.gen';\nexport type { ApiResponse, FetchOptions, HttpRequestMethod, HttpRequestOptions };\nexport type { CacheProvider, CacheStrategy, CacheStrategyHandler };\nexport type { RateLimitConfig };\nexport type { TagCapi as Tag } from './generated/tags/types.gen';\nexport type { StoryWithInlinedRelations } from './resources/stories';\n\nexport { createThrottle, parseRateLimitPolicyHeader } from './utils/rate-limit';\n\n/**\n * Cache configuration.\n *\n * **Note:** Requests with `version: 'draft'` always bypass the cache regardless\n * of the configured strategy. Only published content is cached.\n */\ninterface CacheConfig {\n /** Custom cache provider. Defaults to an in-memory LRU cache (1 000 entries). */\n provider?: CacheProvider;\n /** Cache strategy for published requests. @default 'cache-first' */\n strategy?: CacheStrategy | CacheStrategyHandler;\n /** Time-to-live in milliseconds for cached entries. @default 60_000 */\n ttlMs?: number;\n /**\n * Controls when the cache is flushed on cv change.\n *\n * - `'auto'` (default): automatically flush the cache whenever the API returns a new cv value.\n * - `'manual'`: never auto-flush; call `client.flushCache()` explicitly (e.g. on webhook trigger).\n */\n flush?: 'auto' | 'manual';\n /**\n * Called when SWR background revalidation fails.\n * Only relevant when `strategy` is `'swr'`.\n * @default console.warn\n */\n onRevalidationError?: (error: unknown) => void;\n}\n\nexport interface ContentApiClientConfig<\n ThrowOnError extends boolean = false,\n InlineRelations extends boolean = false,\n> {\n accessToken: string;\n region?: Region;\n baseUrl?: string;\n headers?: Record<string, string>;\n throwOnError?: ThrowOnError;\n cache?: CacheConfig;\n inlineRelations?: InlineRelations;\n retry?: RetryOptions;\n /**\n * Request timeout in milliseconds.\n * @default 30_000\n */\n timeout?: number;\n /**\n * Preventive rate limiting to avoid hitting the Storyblok CDN rate limits.\n *\n * - `undefined` (default): auto-detect tier from path + `per_page` query param.\n * - `number`: fixed max concurrent requests per second (single queue).\n * - `{ maxConcurrency?: number; adaptToServerHeaders?: boolean }`: full config.\n * - `false`: disable rate limiting entirely.\n */\n rateLimit?: RateLimitConfig | number | false;\n /**\n * Custom `fetch` function to use for all requests.\n * Must be fully compatible with the Fetch API standard.\n *\n * Use cases:\n * - SSR framework fetch wrappers (e.g., Next.js `fetch` with caching)\n * - Custom instrumentation or logging around requests\n *\n * @default globalThis.fetch\n */\n fetch?: typeof globalThis.fetch;\n}\n\nexport const createApiClient = <\n ThrowOnError extends boolean = false,\n InlineRelations extends boolean = false,\n>(\n config: ContentApiClientConfig<ThrowOnError, InlineRelations>,\n) => {\n const {\n accessToken,\n region = 'eu',\n baseUrl,\n headers = {},\n throwOnError = false,\n cache = {},\n inlineRelations = false,\n retry,\n timeout = 30_000,\n rateLimit,\n fetch: customFetch,\n } = config;\n const retryOptions: RetryOptions = { limit: 3, backoffLimit: 20_000, jitter: true, ...retry };\n // `rateLimit` defaults to `{}` (auto-detect mode) when not supplied.\n const throttleManager = createThrottleManager(rateLimit ?? {});\n const cacheProvider = cache.provider ?? createMemoryCacheProvider();\n const swrOptions = cache.onRevalidationError ? { onRevalidationError: cache.onRevalidationError } : undefined;\n const strategy = cache.strategy\n ? typeof cache.strategy === 'string'\n ? createStrategy(cache.strategy, swrOptions)\n : cache.strategy\n : createStrategy('cache-first');\n const cacheTtlMs = cache.ttlMs ?? 60_000;\n const cacheFlush = cache.flush ?? 'auto';\n let currentCv: number | undefined;\n\n const client: Client = createClient(\n createConfig({\n auth: accessToken,\n baseUrl: baseUrl || getRegionBaseUrl(region),\n headers,\n throwOnError,\n kyOptions: {\n // Enable `throwHttpErrors` to make retry work, even if `throwOnError`\n // is `false`. The client's error handling will still work because it\n // catches `HTTPError`.\n throwHttpErrors: true,\n timeout,\n retry: retryOptions,\n ...(customFetch && { fetch: customFetch }),\n },\n }),\n );\n\n client.interceptors.error.use(\n (error: unknown, response: Response) =>\n new ClientError(response?.statusText || 'API request failed', {\n status: response?.status ?? 0,\n statusText: response?.statusText ?? '',\n data: error,\n }),\n );\n\n const security = [\n {\n in: 'query' as const,\n name: 'token',\n type: 'apiKey' as const,\n },\n ];\n\n const updateCv = async (result: ApiResponse): Promise<boolean> => {\n const nextCv = extractCv(result.data);\n if (nextCv === undefined) {\n return true;\n }\n\n // Guard against cv regression: SWR background revalidation may carry a\n // stale cv from a prior request; never move cv backward.\n if (currentCv !== undefined && nextCv < currentCv) {\n return false;\n }\n\n if (cacheFlush === 'auto' && currentCv !== undefined && currentCv !== nextCv) {\n await cacheProvider.flush();\n }\n\n currentCv = nextCv;\n return true;\n };\n\n const cacheSuccessResult = async <TResponse extends ApiResponse>(key: string, result: TResponse) => {\n const shouldCacheResult = await updateCv(result);\n if (result.error === undefined && shouldCacheResult) {\n await cacheProvider.set(key, {\n value: result,\n ttlMs: cacheTtlMs,\n });\n }\n return result;\n };\n\n const requestNetwork = async (\n method: 'GET',\n path: string,\n query: Record<string, unknown>,\n options: HttpRequestOptions,\n ): Promise<ApiResponse> => {\n return client.request<unknown, ClientError, boolean>({\n ...options,\n method,\n query,\n security,\n url: path,\n });\n };\n\n /**\n * Wraps a raw SDK call to cast the `error: unknown` type returned by\n * generated code to `ClientError` — the error interceptor ensures the\n * runtime value IS a ClientError.\n */\n const asApiResponse = <TData, ThrowOnError extends boolean = false>(\n p: Promise<unknown>,\n ): Promise<ApiResponse<TData, ThrowOnError>> => p as unknown as Promise<ApiResponse<TData, ThrowOnError>>;\n\n const requestWithCache = async <TData = unknown, ThrowOnError extends boolean = false>(\n method: 'GET',\n path: string,\n rawQuery: Record<string, unknown>,\n fetchFn: (query: Record<string, unknown>) => Promise<ApiResponse<TData, ThrowOnError>>,\n cacheOptions?: RequestWithCacheOptions,\n ): Promise<ApiResponse<TData, ThrowOnError>> => {\n const query = currentCv !== undefined ? applyCvToQuery(rawQuery, currentCv) : rawQuery;\n const cacheEnabled = shouldUseCache(method, path, rawQuery);\n\n if (!cacheEnabled) {\n const networkResult = await fetchFn(query);\n throttleManager.adaptToResponse(networkResult.response);\n await updateCv(networkResult);\n return networkResult;\n }\n\n const baseKey = createCacheKey(method, path, rawQuery);\n const key = cacheOptions?.cacheKeyPrefix ? `${cacheOptions.cacheKeyPrefix}:${baseKey}` : baseKey;\n const cachedEntry = await cacheProvider.get<ApiResponse<TData, ThrowOnError>>(key);\n const cachedResult = cachedEntry?.value;\n\n const loadNetwork = async () => {\n const result = await fetchFn(query);\n throttleManager.adaptToResponse(result.response);\n return cacheSuccessResult(key, result);\n };\n\n return strategy({\n key,\n cachedResult,\n loadNetwork,\n });\n };\n\n const request = async (\n method: 'GET',\n path: string,\n options: HttpRequestOptions = {},\n ): Promise<ApiResponse> => {\n const rawQuery = options.query || {};\n\n return requestWithCache(method, path, rawQuery, (query) => {\n return throttleManager.execute(path, rawQuery, () => requestNetwork(method, path, query, options));\n });\n };\n\n const getRequest = (\n path: string,\n options: HttpRequestOptions = {},\n ) => {\n return request('GET', path, options);\n };\n\n const resourceDeps: ResourceDeps = {\n client,\n requestWithCache,\n asApiResponse,\n throttleManager,\n };\n\n const stories = createStoriesResource<InlineRelations>({\n ...resourceDeps,\n inlineRelations,\n });\n\n /**\n * Flush the in-memory cache and reset the tracked cv.\n *\n * Call this explicitly when `cache.flush` is set to `'manual'`, e.g. after\n * receiving a Storyblok webhook event that signals content has changed.\n */\n const flushCache = async (): Promise<void> => {\n await cacheProvider.flush();\n currentCv = undefined;\n };\n\n return {\n datasourceEntries: createDatasourceEntriesResource(resourceDeps),\n datasources: createDatasourcesResource(resourceDeps),\n flushCache,\n get: getRequest,\n interceptors: client.interceptors,\n links: createLinksResource(resourceDeps),\n spaces: createSpacesResource(resourceDeps),\n stories,\n tags: createTagsResource(resourceDeps),\n };\n};\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { createClient, createConfig } from './generated/shared/client';\nimport type { StoryCapi } from './generated/stories';\nimport type { CacheProvider, CacheStrategy, CacheStrategyHandler } from './utils/cache';\nimport { createMemoryCacheProvider, createStrategy } from './utils/cache';\nimport { ClientError } from './error';\nimport type { RateLimitConfig } from './utils/rate-limit';\nimport { createThrottleManager } from './utils/rate-limit';\nimport { applyCvToQuery, extractCv } from './utils/cv';\nimport { createCacheKey, shouldUseCache } from './utils/request';\nimport { getRegionBaseUrl, type Region } from '@storyblok/region-helper';\nimport type { RetryOptions } from 'ky';\nimport type { Client } from './generated/shared/client';\nimport type { ApiResponse, FetchOptions, HttpRequestMethod, HttpRequestOptions, RequestWithCacheOptions, ResourceDeps } from './types';\nimport { createStoriesResource } from './resources/stories';\nimport { createLinksResource } from './resources/links';\nimport { createTagsResource } from './resources/tags';\nimport { createDatasourcesResource } from './resources/datasources';\nimport { createDatasourceEntriesResource } from './resources/datasource-entries';\nimport { createSpacesResource } from './resources/spaces';\n\ntype Prettify<T> = {\n [K in keyof T]: T[K];\n} & {};\n\nexport type Story = Prettify<StoryCapi>;\nexport { ClientError } from './error';\nexport type { DatasourceEntryCapi as DatasourceEntry } from './generated/datasource_entries/types.gen';\nexport type { DatasourceCapi as Datasource } from './generated/datasources/types.gen';\nexport type { LinkCapi as Link } from './generated/links/types.gen';\nexport type { Middleware } from './generated/shared/client/utils.gen';\nexport type { SpaceCapi as Space } from './generated/spaces/types.gen';\nexport type { ApiResponse, FetchOptions, HttpRequestMethod, HttpRequestOptions };\nexport type { CacheProvider, CacheStrategy, CacheStrategyHandler };\nexport type { RateLimitConfig };\nexport type { TagCapi as Tag } from './generated/tags/types.gen';\nexport type { StoryWithInlinedRelations } from './resources/stories';\n\nexport { createThrottle, parseRateLimitPolicyHeader } from './utils/rate-limit';\n\n/**\n * Cache configuration.\n *\n * **Note:** Requests with `version: 'draft'` always bypass the cache regardless\n * of the configured strategy. Only published content is cached.\n */\ninterface CacheConfig {\n /** Custom cache provider. Defaults to an in-memory LRU cache (1 000 entries). */\n provider?: CacheProvider;\n /** Cache strategy for published requests. @default 'cache-first' */\n strategy?: CacheStrategy | CacheStrategyHandler;\n /** Time-to-live in milliseconds for cached entries. @default 60_000 */\n ttlMs?: number;\n /**\n * Controls when the cache is flushed on cv change.\n *\n * - `'auto'` (default): automatically flush the cache whenever the API returns a new cv value.\n * - `'manual'`: never auto-flush; call `client.flushCache()` explicitly (e.g. on webhook trigger).\n */\n flush?: 'auto' | 'manual';\n /**\n * Called when SWR background revalidation fails.\n * Only relevant when `strategy` is `'swr'`.\n * @default console.warn\n */\n onRevalidationError?: (error: unknown) => void;\n}\n\nexport interface ContentApiClientConfig<\n ThrowOnError extends boolean = false,\n InlineRelations extends boolean = false,\n> {\n accessToken: string;\n region?: Region;\n baseUrl?: string;\n headers?: Record<string, string>;\n throwOnError?: ThrowOnError;\n cache?: CacheConfig;\n inlineRelations?: InlineRelations;\n retry?: RetryOptions;\n /**\n * Request timeout in milliseconds.\n * @default 30_000\n */\n timeout?: number;\n /**\n * Preventive rate limiting to avoid hitting the Storyblok CDN rate limits.\n *\n * - `undefined` (default): auto-detect tier from path + `per_page` query param.\n * - `number`: fixed max concurrent requests per second (single queue).\n * - `{ maxConcurrency?: number; adaptToServerHeaders?: boolean }`: full config.\n * - `false`: disable rate limiting entirely.\n */\n rateLimit?: RateLimitConfig | number | false;\n /**\n * Custom `fetch` function to use for all requests.\n * Must be fully compatible with the Fetch API standard.\n *\n * Use cases:\n * - SSR framework fetch wrappers (e.g., Next.js `fetch` with caching)\n * - Custom instrumentation or logging around requests\n *\n * @default globalThis.fetch\n */\n fetch?: typeof globalThis.fetch;\n}\n\nexport const createApiClient = <\n ThrowOnError extends boolean = false,\n InlineRelations extends boolean = false,\n>(\n config: ContentApiClientConfig<ThrowOnError, InlineRelations>,\n) => {\n const {\n accessToken,\n region = 'eu',\n baseUrl,\n headers = {},\n throwOnError = false,\n cache = {},\n inlineRelations = false,\n retry,\n timeout = 30_000,\n rateLimit,\n fetch: customFetch,\n } = config;\n const retryOptions: RetryOptions = { limit: 3, backoffLimit: 20_000, jitter: true, ...retry };\n // `rateLimit` defaults to `{}` (auto-detect mode) when not supplied.\n const throttleManager = createThrottleManager(rateLimit ?? {});\n const cacheProvider = cache.provider ?? createMemoryCacheProvider();\n const swrOptions = cache.onRevalidationError ? { onRevalidationError: cache.onRevalidationError } : undefined;\n const strategy = cache.strategy\n ? typeof cache.strategy === 'string'\n ? createStrategy(cache.strategy, swrOptions)\n : cache.strategy\n : createStrategy('cache-first');\n const cacheTtlMs = cache.ttlMs ?? 60_000;\n const cacheFlush = cache.flush ?? 'auto';\n let currentCv: number | undefined;\n\n const client: Client = createClient(\n createConfig({\n auth: accessToken,\n baseUrl: baseUrl || getRegionBaseUrl(region),\n headers,\n throwOnError,\n kyOptions: {\n // Enable `throwHttpErrors` to make retry work, even if `throwOnError`\n // is `false`. The client's error handling will still work because it\n // catches `HTTPError`.\n throwHttpErrors: true,\n timeout,\n retry: retryOptions,\n ...(customFetch && { fetch: customFetch }),\n },\n }),\n );\n\n client.interceptors.error.use(\n (error: unknown, response: Response) =>\n new ClientError(response?.statusText || 'API request failed', {\n status: response?.status ?? 0,\n statusText: response?.statusText ?? '',\n data: error,\n }),\n );\n\n const security = [\n {\n in: 'query' as const,\n name: 'token',\n type: 'apiKey' as const,\n },\n ];\n\n const updateCv = async (result: ApiResponse): Promise<boolean> => {\n const nextCv = extractCv(result.data);\n if (nextCv === undefined) {\n return true;\n }\n\n // Guard against cv regression: SWR background revalidation may carry a\n // stale cv from a prior request; never move cv backward.\n if (currentCv !== undefined && nextCv < currentCv) {\n return false;\n }\n\n if (cacheFlush === 'auto' && currentCv !== undefined && currentCv !== nextCv) {\n await cacheProvider.flush();\n }\n\n currentCv = nextCv;\n return true;\n };\n\n const cacheSuccessResult = async <TResponse extends ApiResponse>(key: string, result: TResponse) => {\n const shouldCacheResult = await updateCv(result);\n if (result.error === undefined && shouldCacheResult) {\n await cacheProvider.set(key, {\n value: result,\n ttlMs: cacheTtlMs,\n });\n }\n return result;\n };\n\n const requestNetwork = async (\n method: 'GET',\n path: string,\n query: Record<string, unknown>,\n options: HttpRequestOptions,\n ): Promise<ApiResponse> => {\n return client.request<unknown, ClientError, boolean>({\n ...options,\n method,\n query,\n security,\n url: path,\n });\n };\n\n /**\n * Wraps a raw SDK call to cast the `error: unknown` type returned by\n * generated code to `ClientError` — the error interceptor ensures the\n * runtime value IS a ClientError.\n */\n const asApiResponse = <TData, ThrowOnError extends boolean = false>(\n p: Promise<unknown>,\n ): Promise<ApiResponse<TData, ThrowOnError>> => p as unknown as Promise<ApiResponse<TData, ThrowOnError>>;\n\n const requestWithCache = async <TData = unknown, ThrowOnError extends boolean = false>(\n method: 'GET',\n path: string,\n rawQuery: Record<string, unknown>,\n fetchFn: (query: Record<string, unknown>) => Promise<ApiResponse<TData, ThrowOnError>>,\n cacheOptions?: RequestWithCacheOptions,\n ): Promise<ApiResponse<TData, ThrowOnError>> => {\n const query = currentCv !== undefined ? applyCvToQuery(rawQuery, currentCv) : rawQuery;\n const cacheEnabled = shouldUseCache(method, path, rawQuery);\n\n if (!cacheEnabled) {\n const networkResult = await fetchFn(query);\n throttleManager.adaptToResponse(networkResult.response);\n await updateCv(networkResult);\n return networkResult;\n }\n\n const baseKey = createCacheKey(method, path, rawQuery);\n const key = cacheOptions?.cacheKeyPrefix ? `${cacheOptions.cacheKeyPrefix}:${baseKey}` : baseKey;\n const cachedEntry = await cacheProvider.get<ApiResponse<TData, ThrowOnError>>(key);\n const cachedResult = cachedEntry?.value;\n\n const loadNetwork = async () => {\n const result = await fetchFn(query);\n throttleManager.adaptToResponse(result.response);\n return cacheSuccessResult(key, result);\n };\n\n return strategy({\n key,\n cachedResult,\n loadNetwork,\n });\n };\n\n const request = async (\n method: 'GET',\n path: string,\n options: HttpRequestOptions = {},\n ): Promise<ApiResponse> => {\n const rawQuery = options.query || {};\n\n return requestWithCache(method, path, rawQuery, (query) => {\n return throttleManager.execute(path, rawQuery, () => requestNetwork(method, path, query, options));\n });\n };\n\n const getRequest = (\n path: string,\n options: HttpRequestOptions = {},\n ) => {\n return request('GET', path, options);\n };\n\n const resourceDeps: ResourceDeps = {\n client,\n requestWithCache,\n asApiResponse,\n throttleManager,\n };\n\n const stories = createStoriesResource<InlineRelations>({\n ...resourceDeps,\n inlineRelations,\n });\n\n /**\n * Flush the in-memory cache and reset the tracked cv.\n *\n * Call this explicitly when `cache.flush` is set to `'manual'`, e.g. after\n * receiving a Storyblok webhook event that signals content has changed.\n */\n const flushCache = async (): Promise<void> => {\n await cacheProvider.flush();\n currentCv = undefined;\n };\n\n return {\n datasourceEntries: createDatasourceEntriesResource(resourceDeps),\n datasources: createDatasourcesResource(resourceDeps),\n flushCache,\n get: getRequest,\n interceptors: client.interceptors,\n links: createLinksResource(resourceDeps),\n spaces: createSpacesResource(resourceDeps),\n stories,\n tags: createTagsResource(resourceDeps),\n };\n};\n"],"mappings":"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"}
package/dist/types.d.cts DELETED
@@ -1,37 +0,0 @@
1
- import { RequestOptions } from "./generated/shared/client/types.gen.cjs";
2
- import { ClientError } from "./error.cjs";
3
-
4
- //#region src/types.d.ts
5
- type ApiResponse<Data = unknown, ThrowOnError extends boolean = false> = ThrowOnError extends true ? {
6
- data: Data;
7
- response: Response;
8
- request: Request;
9
- } : {
10
- data?: Data;
11
- error?: ClientError;
12
- response: Response;
13
- request: Request;
14
- };
15
- type HttpRequestOptions = Omit<RequestOptions, 'method' | 'security' | 'url'>;
16
- type HttpRequestMethod = <TData = unknown>(path: string, options?: HttpRequestOptions) => Promise<ApiResponse<TData>>;
17
- /**
18
- * Arbitrary options forwarded to the underlying `fetch()` call.
19
- *
20
- * Standard `RequestInit` properties (`cache`, `credentials`, `mode`, …) and
21
- * non-standard, vendor-specific properties (Next.js `next`, Cloudflare `cf`, …)
22
- * are both supported.
23
- *
24
- * @example
25
- * ```ts
26
- * client.stories.get('home', {
27
- * fetchOptions: {
28
- * cache: 'no-store',
29
- * next: { revalidate: 60, tags: ['home'] },
30
- * },
31
- * })
32
- * ```
33
- */
34
- type FetchOptions = Record<string, unknown>;
35
- //#endregion
36
- export { ApiResponse, FetchOptions, HttpRequestMethod, HttpRequestOptions };
37
- //# sourceMappingURL=types.d.cts.map
package/dist/types.d.mts DELETED
@@ -1,37 +0,0 @@
1
- import { RequestOptions } from "./generated/shared/client/types.gen.mjs";
2
- import { ClientError } from "./error.mjs";
3
-
4
- //#region src/types.d.ts
5
- type ApiResponse<Data = unknown, ThrowOnError extends boolean = false> = ThrowOnError extends true ? {
6
- data: Data;
7
- response: Response;
8
- request: Request;
9
- } : {
10
- data?: Data;
11
- error?: ClientError;
12
- response: Response;
13
- request: Request;
14
- };
15
- type HttpRequestOptions = Omit<RequestOptions, 'method' | 'security' | 'url'>;
16
- type HttpRequestMethod = <TData = unknown>(path: string, options?: HttpRequestOptions) => Promise<ApiResponse<TData>>;
17
- /**
18
- * Arbitrary options forwarded to the underlying `fetch()` call.
19
- *
20
- * Standard `RequestInit` properties (`cache`, `credentials`, `mode`, …) and
21
- * non-standard, vendor-specific properties (Next.js `next`, Cloudflare `cf`, …)
22
- * are both supported.
23
- *
24
- * @example
25
- * ```ts
26
- * client.stories.get('home', {
27
- * fetchOptions: {
28
- * cache: 'no-store',
29
- * next: { revalidate: 60, tags: ['home'] },
30
- * },
31
- * })
32
- * ```
33
- */
34
- type FetchOptions = Record<string, unknown>;
35
- //#endregion
36
- export { ApiResponse, FetchOptions, HttpRequestMethod, HttpRequestOptions };
37
- //# sourceMappingURL=types.d.mts.map