@asstio-dev/storefront 0.7.0 → 0.11.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.
@@ -0,0 +1,68 @@
1
+ import { localProblem, retryAfterSeconds } from "./types.js";
2
+ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3
+ /** `POST /v1/checkout`: creates the cart's attempt or converges on the live one (spec §5.3). Call it from a Server Action or route handler, never during render. */
4
+ export async function startCheckout(client, cartToken, opts = {}) {
5
+ const { data, error, response } = await client.POST("/v1/checkout", {
6
+ body: { option: opts.option ?? null, returnOrigin: opts.returnOrigin ?? null },
7
+ headers: { "X-Cart-Token": cartToken },
8
+ });
9
+ const retryAfter = retryAfterSeconds(response);
10
+ if (error || !data)
11
+ return { ok: false, status: response.status, problem: (error ?? localProblem("SDK_EMPTY_ANSWER", "no body")), retryAfter };
12
+ return { ok: true, data, retryAfter };
13
+ }
14
+ /** The same `POST /v1/checkout`, retrying `CHECKOUT_IN_PROGRESS` per `Retry-After`, twice (spec §10.3) — or
15
+ * `retries` times; `{ retries: 0 }` is the single-attempt call to put behind `mountCheckout` *(plan review 3 #5)*.
16
+ * Never repeats a cart mutation: it only syncs. */
17
+ export async function syncCheckout(client, cartToken, opts = {}, retry = {}) {
18
+ const sleep = retry.sleep ?? defaultSleep;
19
+ const retries = retry.retries ?? 2; // *(plan review 3 #5)*
20
+ let result = await startCheckout(client, cartToken, opts);
21
+ for (let attempt = 0; attempt < retries && !result.ok && result.problem.code === "CHECKOUT_IN_PROGRESS"; attempt++) {
22
+ await sleep((result.retryAfter ?? 1) * 1000);
23
+ result = await startCheckout(client, cartToken, opts);
24
+ }
25
+ return result;
26
+ }
27
+ /** `POST /v1/checkout/{attemptId}/complete`: the confirmation page's call; with the cart token the answer carries the order's access token (spec §8.3). */
28
+ export async function completeCheckout(client, attemptId, cartToken) {
29
+ const { data, error, response } = await client.POST("/v1/checkout/{attemptId}/complete", {
30
+ params: { path: { attemptId } },
31
+ headers: cartToken ? { "X-Cart-Token": cartToken } : {},
32
+ });
33
+ if (error || !data)
34
+ return { ok: false, status: response.status, problem: (error ?? localProblem("SDK_EMPTY_ANSWER", "no body")), retryAfter: retryAfterSeconds(response) };
35
+ return { ok: true, data };
36
+ }
37
+ /** `GET /v1/checkout/{attemptId}`: the non-mutating status read the browser module polls with. */
38
+ export async function getCheckoutStatus(client, attemptId, cartToken) {
39
+ const { data, error, response } = await client.GET("/v1/checkout/{attemptId}", { params: { path: { attemptId } }, headers: { "X-Cart-Token": cartToken } });
40
+ if (error || !data)
41
+ return { ok: false, status: response.status, problem: (error ?? localProblem("SDK_EMPTY_ANSWER", "no body")), retryAfter: retryAfterSeconds(response) };
42
+ return { ok: true, data };
43
+ }
44
+ /** `GET /v1/orders/{orderId}` with `X-Order-Token` — a header only, never a query string (spec §8.3). */
45
+ export async function getOrder(client, orderId, token) {
46
+ const { data, error, response } = await client.GET("/v1/orders/{orderId}", { params: { path: { orderId } }, headers: { "X-Order-Token": token } });
47
+ if (error || !data)
48
+ return { ok: false, status: response.status, problem: (error ?? localProblem("SDK_EMPTY_ANSWER", "no body")) };
49
+ return { ok: true, data };
50
+ }
51
+ const FINAL = new Set(["completed", "pending", "declined", "expired"]);
52
+ /** Polls `complete` with doubling backoff until the attempt has an order or is closed, or the timeout passes (the last answer is returned either way). */
53
+ export async function awaitCompletion(complete, opts = {}) {
54
+ const first = opts.firstDelayMs ?? 1000;
55
+ const max = opts.maxDelayMs ?? 10_000;
56
+ const timeout = opts.timeoutMs ?? 120_000;
57
+ const sleep = opts.sleep ?? defaultSleep;
58
+ let delay = first;
59
+ let waited = 0;
60
+ let last = await complete();
61
+ while (last.ok && !FINAL.has(last.data.status) && waited + delay <= timeout) {
62
+ await sleep(delay);
63
+ waited += delay;
64
+ delay = Math.min(delay * 2, max);
65
+ last = await complete();
66
+ }
67
+ return last;
68
+ }
@@ -0,0 +1,42 @@
1
+ import type { components } from "../schema";
2
+ export type CheckoutDto = components["schemas"]["CheckoutDto"];
3
+ export type CheckoutStatusDto = components["schemas"]["CheckoutStatusDto"];
4
+ export type CheckoutOrderRefDto = components["schemas"]["CheckoutOrderRefDto"];
5
+ export type CheckoutWidgetDto = components["schemas"]["CheckoutWidgetDto"];
6
+ export type OrderDto = components["schemas"]["OrderDto"];
7
+ export type StartCheckoutRequest = components["schemas"]["StartCheckoutRequest"];
8
+ /** The problem body; `cart` is the corrected cart on 409 CHECKOUT_CART_CHANGED (an extension, so it is typed loosely here). */
9
+ export type StorefrontProblem = components["schemas"]["StorefrontProblem"] & {
10
+ cart?: unknown;
11
+ };
12
+ export type CheckoutResult = {
13
+ ok: true;
14
+ data: CheckoutDto;
15
+ retryAfter?: number;
16
+ } | {
17
+ ok: false;
18
+ status: number;
19
+ problem: StorefrontProblem;
20
+ retryAfter?: number;
21
+ };
22
+ export type StatusResult = {
23
+ ok: true;
24
+ data: CheckoutStatusDto;
25
+ } | {
26
+ ok: false;
27
+ status: number;
28
+ problem: StorefrontProblem;
29
+ retryAfter?: number;
30
+ };
31
+ export type OrderResult = {
32
+ ok: true;
33
+ data: OrderDto;
34
+ } | {
35
+ ok: false;
36
+ status: number;
37
+ problem: StorefrontProblem;
38
+ };
39
+ /** `Retry-After` in seconds: a delay, or an HTTP-date turned into one; garbage is 1 s, absence is undefined. */
40
+ export declare function retryAfterSeconds(response: Response, now?: () => number): number | undefined;
41
+ /** A problem shaped like the API's, for failures that never reached it (a thrown fetch, a timeout). */
42
+ export declare function localProblem(code: string, detail: string): StorefrontProblem;
@@ -0,0 +1,17 @@
1
+ /** `Retry-After` in seconds: a delay, or an HTTP-date turned into one; garbage is 1 s, absence is undefined. */
2
+ export function retryAfterSeconds(response, now = Date.now) {
3
+ const raw = response.headers.get("Retry-After");
4
+ if (raw === null || raw === "")
5
+ return undefined;
6
+ const seconds = Number(raw);
7
+ if (Number.isFinite(seconds))
8
+ return Math.max(0, Math.ceil(seconds));
9
+ const date = Date.parse(raw);
10
+ if (Number.isFinite(date))
11
+ return Math.max(0, Math.ceil((date - now()) / 1000));
12
+ return 1;
13
+ }
14
+ /** A problem shaped like the API's, for failures that never reached it (a thrown fetch, a timeout). */
15
+ export function localProblem(code, detail) {
16
+ return { type: "https://docs.asstio.com/storefront/errors#" + code, title: code, status: 0, detail, code };
17
+ }
package/dist/index.d.ts CHANGED
@@ -47,3 +47,12 @@ export type FilterSelection = NonNullable<NonNullable<paths["/v1/products"]["get
47
47
  export declare function filterQuery(selection: Record<string, string | string[] | undefined>): FilterSelection;
48
48
  export { decodeFilterValues, encodeFilterValues } from "./filters.js";
49
49
  export { coverOffset, fileUrl, fillFactor, formatFactor, formatImage, imageRect, mediaHash, mediaUrl, MediaUrlError, pickWidth, pictureSources, posterUrl, scaledSizes, srcSet, styleToCss, videoSources, type FormatFit, type FormattedImage, type MediaConfig, type MediaDescriptor, type MediaFormat, type PictureSources, type SlotSize, type VideoSource, } from "./media.js";
50
+ export { awaitCompletion, completeCheckout, getCheckoutStatus, getOrder, startCheckout, syncCheckout, type CheckoutClient, type StartCheckoutOptions, type SyncRetryOptions, type AwaitCompletionOptions } from "./checkout/server.js";
51
+ export { retryAfterSeconds, type CheckoutDto, type CheckoutOrderRefDto, type CheckoutResult, type CheckoutStatusDto, type CheckoutWidgetDto, type OrderDto, type OrderResult, type StatusResult, type StorefrontProblem } from "./checkout/types.js";
52
+ export { mountCheckout, type CheckoutHandle, type MountOptions, type MinimalDocument, type MinimalElement, type MinimalScript } from "./checkout/browser.js";
53
+ export { controllers, type CheckoutController, type ProviderWindow } from "./checkout/controllers.js";
54
+ export { breadcrumbJsonLd } from "./seo/breadcrumb.js";
55
+ export { brandHead, collectionHead, productHead, toNextMetadata } from "./seo/head.js";
56
+ export { productJsonLd } from "./seo/product.js";
57
+ export { jsonLdScript } from "./seo/serialize.js";
58
+ export { SeoError, type BreadcrumbLink, type CollectionHeadOptions, type HeadOptions, type JsonLd, type NextMetadataLike, type ProductJsonLdOptions, type SchemaProperty, type SeoContext, type SeoHead, type SeoMeta, } from "./seo/types.js";
package/dist/index.js CHANGED
@@ -36,3 +36,12 @@ export function filterQuery(selection) {
36
36
  }
37
37
  export { decodeFilterValues, encodeFilterValues } from "./filters.js";
38
38
  export { coverOffset, fileUrl, fillFactor, formatFactor, formatImage, imageRect, mediaHash, mediaUrl, MediaUrlError, pickWidth, pictureSources, posterUrl, scaledSizes, srcSet, styleToCss, videoSources, } from "./media.js";
39
+ export { awaitCompletion, completeCheckout, getCheckoutStatus, getOrder, startCheckout, syncCheckout } from "./checkout/server.js"; // SyncRetryOptions *(plan review 3 #5)*
40
+ export { retryAfterSeconds } from "./checkout/types.js";
41
+ export { mountCheckout } from "./checkout/browser.js";
42
+ export { controllers } from "./checkout/controllers.js";
43
+ export { breadcrumbJsonLd } from "./seo/breadcrumb.js";
44
+ export { brandHead, collectionHead, productHead, toNextMetadata } from "./seo/head.js";
45
+ export { productJsonLd } from "./seo/product.js";
46
+ export { jsonLdScript } from "./seo/serialize.js";
47
+ export { SeoError, } from "./seo/types.js";