@reblu/site-client 5.12.0 → 5.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _reblu_site_contracts from '@reblu/site-contracts';
2
- import { AcademyCourseSort, PaginationMeta, PageType, PagesListSort, ShopSort, TrustBadgeType, ContactFormInput } from '@reblu/site-contracts';
2
+ import { AcademyCourseListQuery, PaginationMeta, PageType, PagesListSort, ShopSort, TrustBadgeType, ContactFormInput } from '@reblu/site-contracts';
3
3
 
4
4
  /**
5
5
  * Cache adapter — the seam that severs the SDK from any framework's caching
@@ -91,28 +91,21 @@ interface ShopProductsParams {
91
91
  /**
92
92
  * Query params for `academy.courses` (REB-507), grounded on
93
93
  * `GET /api/site/academy/courses` — see `parseCourseListParams` in base-api.
94
+ * Since REB-547 this IS the documented query contract (`AcademyCourseListQuery`,
95
+ * the same keys the OpenAPI document lists), so the SDK cannot drift from it.
94
96
  *
95
97
  * Server-side clamping the caller should know about: `limit` is clamped to 1..50
96
98
  * and `page` to >= 1, so an out-of-range value is silently corrected rather than
97
- * rejected. `q`/`instructor`/`category` are truncated (200/64/120 chars).
98
- * `free: true` means "price is 0" and OVERRIDES any priceMin/priceMax range.
99
- * `sort` is a union because an unrecognized key is ignored server-side — typing
100
- * it makes a typo a compile error instead of a silent fall-back to default order.
99
+ * rejected. `q`/`category` are truncated (200/120 chars).
100
+ * - `category`: a `CourseCategory` slug. Only ACTIVE categories ever match.
101
+ * - `instructor`: a user id from `academy.instructors()`. A value that is not a
102
+ * uuid matches no course.
103
+ * - `priceMin`/`priceMax`: inclusive, in integer cents.
104
+ * - `free: true` means "price is 0" and OVERRIDES any priceMin/priceMax range.
105
+ * - `sort` is a union because an unrecognized key is ignored server-side — typing
106
+ * it makes a typo a compile error instead of a silent fall-back to default order.
101
107
  */
102
- interface AcademyCourseListParams {
103
- page?: number;
104
- limit?: number;
105
- q?: string;
106
- sort?: AcademyCourseSort;
107
- /** A `CourseCategory` slug. Only ACTIVE categories ever match. */
108
- category?: string;
109
- /** A user id — `Course.instructorIds` is a denormalized user-uuid array. */
110
- instructor?: string;
111
- /** Restrict to free courses (priceCents = 0); overrides priceMin/priceMax. */
112
- free?: boolean;
113
- priceMin?: number;
114
- priceMax?: number;
115
- }
108
+ type AcademyCourseListParams = AcademyCourseListQuery;
116
109
  /**
117
110
  * Body of `academy.enrollmentRequest`, grounded on
118
111
  * `POST /api/site/academy/enrollment-requests`.
@@ -146,6 +139,10 @@ interface AcademyEnrollmentRequestResult {
146
139
  message: string;
147
140
  }
148
141
 
142
+ /** Memo store of one server request, keyed by read identity (REB-1214). */
143
+ type RequestScope = () => Map<string, Promise<unknown>> | undefined;
144
+ /** Upper bound for a `Retry-After` wait before the single retry (REB-1214). */
145
+ declare const RETRY_AFTER_CAP_MS = 2000;
149
146
  /**
150
147
  * Header carrying the visitor IP from a SITE server to base-api (REB-1005).
151
148
  * base-api keys its per-visitor limit on public writes on it, and trusts it
@@ -385,6 +382,13 @@ interface RebluClientConfig {
385
382
  * a Stripe CLI mock host) — never to widen production trust.
386
383
  */
387
384
  checkoutRedirectHosts?: string[];
385
+ /**
386
+ * REB-1214 — memo store of the current server request, so identical GET
387
+ * reads within it are fetched once. In Next.js pass `rebluRequestScope`
388
+ * from `@reblu/site-next` (React `cache()`); see the README. Without it,
389
+ * identical GETs share one fetch only while in flight.
390
+ */
391
+ requestScope?: RequestScope;
388
392
  }
389
393
  /**
390
394
  * Build a Reblu SITE API client. Each domain is a small namespace of bound
@@ -440,6 +444,7 @@ declare function createRebluClient(config: RebluClientConfig): {
440
444
  courses: (params?: AcademyCourseListParams, cache?: RebluCachePolicy) => Promise<ListResult<_reblu_site_contracts.AcademyCourseSummary>>;
441
445
  bySlug: (slug: string, cache?: RebluCachePolicy) => Promise<_reblu_site_contracts.AcademyCourseDetail>;
442
446
  lessonPreview: (courseSlug: string, lessonSlug: string) => Promise<_reblu_site_contracts.AcademyLessonPreview>;
447
+ instructors: (cache?: RebluCachePolicy) => Promise<ListResult<_reblu_site_contracts.AcademyInstructorPublic>>;
443
448
  categories: (cache?: RebluCachePolicy) => Promise<ListResult<_reblu_site_contracts.AcademyCategoryPublic>>;
444
449
  enrollmentRequest: (body: AcademyEnrollmentRequestInput, options?: RebluMutationOptions) => Promise<AcademyEnrollmentRequestResult>;
445
450
  };
@@ -456,4 +461,4 @@ declare function createRebluClient(config: RebluClientConfig): {
456
461
  /** The assembled client returned by {@link createRebluClient}. */
457
462
  type RebluClient = ReturnType<typeof createRebluClient>;
458
463
 
459
- export { type AcademyCourseListParams, type AcademyEnrollmentRequestInput, type AcademyEnrollmentRequestResult, AmbiguousPageSlugError, ApiError, type ApiErrorBody, type ApiErrorDetails, type BlogListParams, type ContactFormMetadata, type ContactFormMetadataOptions, DEFAULT_CHECKOUT_REDIRECT_HOSTS, type FeaturedPagesOptions, type ListResult, type PageBySlugOptions, type PageLocation, type PageSlugCandidate, type PagesListParams, PlainCacheAdapter, type RebluCacheAdapter, type RebluCachePolicy, type RebluClient, type RebluClientConfig, type RebluMutationOptions, type ShopProductsParams, type TrustBadgeListParams, UnsafeCheckoutRedirectError, VISITOR_IP_HEADER, buildContactFormMetadata, createRebluClient, currentPagePath, isSafeCheckoutRedirectUrl, visitorIpFromHeaders };
464
+ export { type AcademyCourseListParams, type AcademyEnrollmentRequestInput, type AcademyEnrollmentRequestResult, AmbiguousPageSlugError, ApiError, type ApiErrorBody, type ApiErrorDetails, type BlogListParams, type ContactFormMetadata, type ContactFormMetadataOptions, DEFAULT_CHECKOUT_REDIRECT_HOSTS, type FeaturedPagesOptions, type ListResult, type PageBySlugOptions, type PageLocation, type PageSlugCandidate, type PagesListParams, PlainCacheAdapter, RETRY_AFTER_CAP_MS, type RebluCacheAdapter, type RebluCachePolicy, type RebluClient, type RebluClientConfig, type RebluMutationOptions, type RequestScope, type ShopProductsParams, type TrustBadgeListParams, UnsafeCheckoutRedirectError, VISITOR_IP_HEADER, buildContactFormMetadata, createRebluClient, currentPagePath, isSafeCheckoutRedirectUrl, visitorIpFromHeaders };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _reblu_site_contracts from '@reblu/site-contracts';
2
- import { AcademyCourseSort, PaginationMeta, PageType, PagesListSort, ShopSort, TrustBadgeType, ContactFormInput } from '@reblu/site-contracts';
2
+ import { AcademyCourseListQuery, PaginationMeta, PageType, PagesListSort, ShopSort, TrustBadgeType, ContactFormInput } from '@reblu/site-contracts';
3
3
 
4
4
  /**
5
5
  * Cache adapter — the seam that severs the SDK from any framework's caching
@@ -91,28 +91,21 @@ interface ShopProductsParams {
91
91
  /**
92
92
  * Query params for `academy.courses` (REB-507), grounded on
93
93
  * `GET /api/site/academy/courses` — see `parseCourseListParams` in base-api.
94
+ * Since REB-547 this IS the documented query contract (`AcademyCourseListQuery`,
95
+ * the same keys the OpenAPI document lists), so the SDK cannot drift from it.
94
96
  *
95
97
  * Server-side clamping the caller should know about: `limit` is clamped to 1..50
96
98
  * and `page` to >= 1, so an out-of-range value is silently corrected rather than
97
- * rejected. `q`/`instructor`/`category` are truncated (200/64/120 chars).
98
- * `free: true` means "price is 0" and OVERRIDES any priceMin/priceMax range.
99
- * `sort` is a union because an unrecognized key is ignored server-side — typing
100
- * it makes a typo a compile error instead of a silent fall-back to default order.
99
+ * rejected. `q`/`category` are truncated (200/120 chars).
100
+ * - `category`: a `CourseCategory` slug. Only ACTIVE categories ever match.
101
+ * - `instructor`: a user id from `academy.instructors()`. A value that is not a
102
+ * uuid matches no course.
103
+ * - `priceMin`/`priceMax`: inclusive, in integer cents.
104
+ * - `free: true` means "price is 0" and OVERRIDES any priceMin/priceMax range.
105
+ * - `sort` is a union because an unrecognized key is ignored server-side — typing
106
+ * it makes a typo a compile error instead of a silent fall-back to default order.
101
107
  */
102
- interface AcademyCourseListParams {
103
- page?: number;
104
- limit?: number;
105
- q?: string;
106
- sort?: AcademyCourseSort;
107
- /** A `CourseCategory` slug. Only ACTIVE categories ever match. */
108
- category?: string;
109
- /** A user id — `Course.instructorIds` is a denormalized user-uuid array. */
110
- instructor?: string;
111
- /** Restrict to free courses (priceCents = 0); overrides priceMin/priceMax. */
112
- free?: boolean;
113
- priceMin?: number;
114
- priceMax?: number;
115
- }
108
+ type AcademyCourseListParams = AcademyCourseListQuery;
116
109
  /**
117
110
  * Body of `academy.enrollmentRequest`, grounded on
118
111
  * `POST /api/site/academy/enrollment-requests`.
@@ -146,6 +139,10 @@ interface AcademyEnrollmentRequestResult {
146
139
  message: string;
147
140
  }
148
141
 
142
+ /** Memo store of one server request, keyed by read identity (REB-1214). */
143
+ type RequestScope = () => Map<string, Promise<unknown>> | undefined;
144
+ /** Upper bound for a `Retry-After` wait before the single retry (REB-1214). */
145
+ declare const RETRY_AFTER_CAP_MS = 2000;
149
146
  /**
150
147
  * Header carrying the visitor IP from a SITE server to base-api (REB-1005).
151
148
  * base-api keys its per-visitor limit on public writes on it, and trusts it
@@ -385,6 +382,13 @@ interface RebluClientConfig {
385
382
  * a Stripe CLI mock host) — never to widen production trust.
386
383
  */
387
384
  checkoutRedirectHosts?: string[];
385
+ /**
386
+ * REB-1214 — memo store of the current server request, so identical GET
387
+ * reads within it are fetched once. In Next.js pass `rebluRequestScope`
388
+ * from `@reblu/site-next` (React `cache()`); see the README. Without it,
389
+ * identical GETs share one fetch only while in flight.
390
+ */
391
+ requestScope?: RequestScope;
388
392
  }
389
393
  /**
390
394
  * Build a Reblu SITE API client. Each domain is a small namespace of bound
@@ -440,6 +444,7 @@ declare function createRebluClient(config: RebluClientConfig): {
440
444
  courses: (params?: AcademyCourseListParams, cache?: RebluCachePolicy) => Promise<ListResult<_reblu_site_contracts.AcademyCourseSummary>>;
441
445
  bySlug: (slug: string, cache?: RebluCachePolicy) => Promise<_reblu_site_contracts.AcademyCourseDetail>;
442
446
  lessonPreview: (courseSlug: string, lessonSlug: string) => Promise<_reblu_site_contracts.AcademyLessonPreview>;
447
+ instructors: (cache?: RebluCachePolicy) => Promise<ListResult<_reblu_site_contracts.AcademyInstructorPublic>>;
443
448
  categories: (cache?: RebluCachePolicy) => Promise<ListResult<_reblu_site_contracts.AcademyCategoryPublic>>;
444
449
  enrollmentRequest: (body: AcademyEnrollmentRequestInput, options?: RebluMutationOptions) => Promise<AcademyEnrollmentRequestResult>;
445
450
  };
@@ -456,4 +461,4 @@ declare function createRebluClient(config: RebluClientConfig): {
456
461
  /** The assembled client returned by {@link createRebluClient}. */
457
462
  type RebluClient = ReturnType<typeof createRebluClient>;
458
463
 
459
- export { type AcademyCourseListParams, type AcademyEnrollmentRequestInput, type AcademyEnrollmentRequestResult, AmbiguousPageSlugError, ApiError, type ApiErrorBody, type ApiErrorDetails, type BlogListParams, type ContactFormMetadata, type ContactFormMetadataOptions, DEFAULT_CHECKOUT_REDIRECT_HOSTS, type FeaturedPagesOptions, type ListResult, type PageBySlugOptions, type PageLocation, type PageSlugCandidate, type PagesListParams, PlainCacheAdapter, type RebluCacheAdapter, type RebluCachePolicy, type RebluClient, type RebluClientConfig, type RebluMutationOptions, type ShopProductsParams, type TrustBadgeListParams, UnsafeCheckoutRedirectError, VISITOR_IP_HEADER, buildContactFormMetadata, createRebluClient, currentPagePath, isSafeCheckoutRedirectUrl, visitorIpFromHeaders };
464
+ export { type AcademyCourseListParams, type AcademyEnrollmentRequestInput, type AcademyEnrollmentRequestResult, AmbiguousPageSlugError, ApiError, type ApiErrorBody, type ApiErrorDetails, type BlogListParams, type ContactFormMetadata, type ContactFormMetadataOptions, DEFAULT_CHECKOUT_REDIRECT_HOSTS, type FeaturedPagesOptions, type ListResult, type PageBySlugOptions, type PageLocation, type PageSlugCandidate, type PagesListParams, PlainCacheAdapter, RETRY_AFTER_CAP_MS, type RebluCacheAdapter, type RebluCachePolicy, type RebluClient, type RebluClientConfig, type RebluMutationOptions, type RequestScope, type ShopProductsParams, type TrustBadgeListParams, UnsafeCheckoutRedirectError, VISITOR_IP_HEADER, buildContactFormMetadata, createRebluClient, currentPagePath, isSafeCheckoutRedirectUrl, visitorIpFromHeaders };
package/dist/index.js CHANGED
@@ -131,25 +131,87 @@ function apiErrorRawBody(error) {
131
131
  }
132
132
 
133
133
  // src/client.ts
134
+ var RETRY_AFTER_CAP_MS = 2e3;
135
+ var DEFAULT_RETRY_DELAY_MS = 250;
136
+ var RETRYABLE_READ_STATUSES = /* @__PURE__ */ new Set([429, 503]);
137
+ function retryDelayMs(header) {
138
+ if (header !== null && header.trim() !== "") {
139
+ const value = header.trim();
140
+ const seconds = /^\d+$/.test(value) ? Number(value) * 1e3 : NaN;
141
+ const ms = Number.isFinite(seconds) ? seconds : Date.parse(value) - Date.now();
142
+ if (Number.isFinite(ms)) return Math.min(Math.max(0, ms), RETRY_AFTER_CAP_MS);
143
+ }
144
+ return DEFAULT_RETRY_DELAY_MS;
145
+ }
146
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
134
147
  async function request(ctx, path, opts) {
135
- const url = ctx.baseUrl + path;
148
+ const method = opts.method ?? "GET";
149
+ const init = {
150
+ method,
151
+ body: opts.body,
152
+ headers: {
153
+ "X-Reblu-Api-Key": ctx.apiKey,
154
+ "X-Reblu-Api-Version": ctx.apiVersion,
155
+ "Content-Type": "application/json",
156
+ ...opts.headers
157
+ },
158
+ ...ctx.cache.resolveRequestInit(opts.cachePolicy ?? {})
159
+ };
160
+ if (method !== "GET" || opts.body !== void 0) {
161
+ return attempt(ctx, path, init);
162
+ }
163
+ const key = JSON.stringify([ctx.baseUrl + path, init]);
164
+ const scope = ctx.requestScope?.();
165
+ const store = scope ?? ctx.inFlight;
166
+ const existing = store?.get(key);
167
+ if (existing) return existing;
168
+ const pending = readWithRetry(ctx, path, init);
169
+ if (store) {
170
+ store.set(key, pending);
171
+ pending.then(
172
+ () => {
173
+ if (!scope) store.delete(key);
174
+ },
175
+ () => {
176
+ if (store.get(key) === pending) store.delete(key);
177
+ }
178
+ );
179
+ }
180
+ return pending;
181
+ }
182
+ async function readWithRetry(ctx, path, init) {
183
+ try {
184
+ return await attempt(ctx, path, init);
185
+ } catch (error) {
186
+ if (!(error instanceof RetryableReadError)) throw error;
187
+ await sleep(retryDelayMs(error.retryAfter));
188
+ try {
189
+ return await attempt(ctx, path, init);
190
+ } catch (retryError) {
191
+ throw retryError instanceof RetryableReadError ? retryError.apiError : retryError;
192
+ }
193
+ }
194
+ }
195
+ var RetryableReadError = class extends Error {
196
+ constructor(apiError, retryAfter) {
197
+ super(apiError.message);
198
+ this.apiError = apiError;
199
+ this.retryAfter = retryAfter;
200
+ }
201
+ apiError;
202
+ retryAfter;
203
+ };
204
+ async function attempt(ctx, path, init) {
136
205
  const controller = new AbortController();
137
206
  const timer = setTimeout(() => controller.abort(), ctx.timeoutMs);
138
207
  try {
139
- const res = await ctx.fetch(url, {
140
- method: opts.method ?? "GET",
141
- body: opts.body,
142
- signal: controller.signal,
143
- headers: {
144
- "X-Reblu-Api-Key": ctx.apiKey,
145
- "X-Reblu-Api-Version": ctx.apiVersion,
146
- "Content-Type": "application/json",
147
- ...opts.headers
148
- },
149
- ...ctx.cache.resolveRequestInit(opts.cachePolicy ?? {})
150
- });
208
+ const res = await ctx.fetch(ctx.baseUrl + path, { ...init, signal: controller.signal });
151
209
  if (!res.ok) {
152
- throw new ApiError(res.status, path, await res.text());
210
+ const error = new ApiError(res.status, path, await res.text());
211
+ if (init.method === "GET" && RETRYABLE_READ_STATUSES.has(res.status)) {
212
+ throw new RetryableReadError(error, res.headers.get("retry-after"));
213
+ }
214
+ throw error;
153
215
  }
154
216
  return await res.json();
155
217
  } finally {
@@ -220,6 +282,14 @@ function makeAcademyNamespace(ctx) {
220
282
  `/api/site/academy/courses/${encodeURIComponent(courseSlug)}/lessons/${encodeURIComponent(lessonSlug)}`,
221
283
  { cachePolicy: { revalidate: false } }
222
284
  ),
285
+ /**
286
+ * Instructors of published courses, sorted by name, backing the catalog
287
+ * instructor filter (REB-547). Pass an `id` as `courses({ instructor })`.
288
+ * Not paginated.
289
+ */
290
+ instructors: (cache) => requestList(ctx, "/api/site/academy/instructors", {
291
+ cachePolicy: cache
292
+ }),
223
293
  /** Active course categories backing the catalog filter. Not paginated. */
224
294
  categories: (cache) => requestList(ctx, "/api/site/academy/categories", {
225
295
  cachePolicy: cache
@@ -529,7 +599,9 @@ function createRebluClient(config) {
529
599
  cache: config.cache ?? new PlainCacheAdapter(),
530
600
  timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,
531
601
  fetch: boundFetch,
532
- checkoutRedirectHosts: config.checkoutRedirectHosts ?? DEFAULT_CHECKOUT_REDIRECT_HOSTS
602
+ checkoutRedirectHosts: config.checkoutRedirectHosts ?? DEFAULT_CHECKOUT_REDIRECT_HOSTS,
603
+ requestScope: config.requestScope,
604
+ inFlight: /* @__PURE__ */ new Map()
533
605
  };
534
606
  return {
535
607
  config: makeConfigNamespace(ctx),
@@ -547,6 +619,6 @@ function createRebluClient(config) {
547
619
  };
548
620
  }
549
621
 
550
- export { AmbiguousPageSlugError, ApiError, DEFAULT_CHECKOUT_REDIRECT_HOSTS, PlainCacheAdapter, UnsafeCheckoutRedirectError, VISITOR_IP_HEADER, buildContactFormMetadata, createRebluClient, currentPagePath, isSafeCheckoutRedirectUrl, visitorIpFromHeaders };
622
+ export { AmbiguousPageSlugError, ApiError, DEFAULT_CHECKOUT_REDIRECT_HOSTS, PlainCacheAdapter, RETRY_AFTER_CAP_MS, UnsafeCheckoutRedirectError, VISITOR_IP_HEADER, buildContactFormMetadata, createRebluClient, currentPagePath, isSafeCheckoutRedirectUrl, visitorIpFromHeaders };
551
623
  //# sourceMappingURL=index.js.map
552
624
  //# sourceMappingURL=index.js.map