2026-04-22
This commit is contained in:
parent
47b66eeb34
commit
e7c3dda8af
8 changed files with 167 additions and 138 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { Cause, Console, Context, Effect, Layer, pipe, Schedule, Schema, SchemaIssue } from "effect";
|
||||
import { Console, Context, Effect, Layer, pipe, Schedule, Schema, SchemaIssue } from "effect";
|
||||
import {
|
||||
FetchHttpClient,
|
||||
HttpClient,
|
||||
|
|
@ -8,36 +8,44 @@ import {
|
|||
import type { CartProduct } from "../schemas/api.ts";
|
||||
import { WooCommerceCart } from "../schemas/cart.ts";
|
||||
|
||||
const MAX_RETRIES = 3;
|
||||
const RETRY_TIME = "1 seconds";
|
||||
|
||||
class BodyParsingError extends Schema.TaggedErrorClass<BodyParsingError>()("BodyParsingError", {
|
||||
cause: Schema.String,
|
||||
}) {}
|
||||
class FetchError extends Schema.TaggedErrorClass<FetchError>()("FetchError", {
|
||||
cause: Schema.Defect,
|
||||
}) {}
|
||||
class BadRequestError extends Schema.TaggedErrorClass<BadRequestError>()("BadRequestError", {
|
||||
cause: Schema.Defect,
|
||||
}) {}
|
||||
class ForbiddenError extends Schema.TaggedErrorClass<ForbiddenError>()("ForbiddenError", {
|
||||
cause: Schema.Defect,
|
||||
}) {}
|
||||
class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("NotFoundError", {
|
||||
cause: Schema.Defect,
|
||||
}) {}
|
||||
class ServerError extends Schema.TaggedErrorClass<ServerError>()("ServerError", {
|
||||
cause: Schema.Defect,
|
||||
}) {}
|
||||
class UnauthorizedError extends Schema.TaggedErrorClass<UnauthorizedError>()("UnauthorizedError", {
|
||||
cause: Schema.Defect,
|
||||
}) {}
|
||||
class BadStatusCodeError extends Schema.TaggedErrorClass<BadStatusCodeError>()("BadStatusCodeError", {
|
||||
reason: Schema.Union([BadRequestError, ForbiddenError, NotFoundError, ServerError, UnauthorizedError])
|
||||
}){}
|
||||
|
||||
|
||||
/** Décrit une Erreur survenue au traitement d'une `Request`. */
|
||||
class APIRequestError extends Schema.TaggedErrorClass<APIRequestError>()("APIRequestError", {
|
||||
reason: Schema.Union([BodyParsingError, FetchError]),
|
||||
}) {}
|
||||
|
||||
class BadRequestError extends Schema.TaggedErrorClass<BadRequestError>()("BadRequestError", {
|
||||
cause: Schema.Defect,
|
||||
}){}
|
||||
class UnauthorizedError extends Schema.TaggedErrorClass<UnauthorizedError>()("UnauthorizedError", {
|
||||
cause: Schema.Defect,
|
||||
}){}
|
||||
class ForbiddenError extends Schema.TaggedErrorClass<ForbiddenError>()("ForbiddenError", {
|
||||
cause: Schema.Defect,
|
||||
}){}
|
||||
class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("NotFoundError", {
|
||||
cause: Schema.Defect,
|
||||
}){}
|
||||
class ServerError extends Schema.TaggedErrorClass<ServerError>()("ServerError", {
|
||||
cause: Schema.Defect,
|
||||
}){}
|
||||
|
||||
/** Décrit une Erreur survenue au traitement d'une `Response`. */
|
||||
class APIResponseError extends Schema.TaggedErrorClass<APIResponseError>()("APIResponseError", {
|
||||
reason: Schema.Union([BadRequestError,UnauthorizedError,ForbiddenError,NotFoundError,ServerError]),
|
||||
}){}
|
||||
reason: Schema.Union([BadStatusCodeError, BodyParsingError]),
|
||||
}) {}
|
||||
|
||||
|
||||
/** Client `fetch` contenant les options et en-têtes de Requêtes pré-renseignées. */
|
||||
const APIFetchClient = FetchHttpClient.layer.pipe(
|
||||
|
|
@ -61,19 +69,15 @@ class APIClient extends Context.Service<APIClient>()("haikuatelier.fr/APIClient"
|
|||
// Créé un client HTTP où chaque Requête est imprimée dans la console.
|
||||
const haikuHTTPClient = pipe(
|
||||
yield* HttpClient.HttpClient,
|
||||
// Journalise toutes les Requêtes et Réponses.
|
||||
// Journalise toutes les Requêtes.
|
||||
HttpClient.tapRequest(request => Console.debug("APIClient", "Request", request)),
|
||||
HttpClient.tap(response =>
|
||||
Effect.gen(function*() {
|
||||
const json = yield* response.json;
|
||||
yield* Console.debug("APIClient", "Response", response.status, json);
|
||||
})
|
||||
),
|
||||
// En cas de code HTTP indiquant un échec, générer une erreur.
|
||||
HttpClient.filterStatusOk,
|
||||
// Définis une politique d'essai.
|
||||
HttpClient.retryTransient({
|
||||
retryOn: "errors-only",
|
||||
schedule: Schedule.exponential("1 seconds"),
|
||||
times: 3,
|
||||
schedule: Schedule.exponential(RETRY_TIME),
|
||||
times: MAX_RETRIES,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@ import { Console, Layer, ManagedRuntime, pipe } from "effect";
|
|||
import { APIClient } from "../../scripts-effect/lib/api.ts";
|
||||
import ProductPageDOM from "./service-dom.ts";
|
||||
import ProductPageElements from "./service-elements.ts";
|
||||
import ProductPageMessages from "./service-messages.ts";
|
||||
|
||||
const ProductPageRuntime = ManagedRuntime.make(
|
||||
pipe(
|
||||
ProductPageDOM.layer,
|
||||
ProductPageDOM.Live,
|
||||
Layer.provideMerge(ProductPageMessages.Live),
|
||||
Layer.provide(ProductPageElements.Live),
|
||||
Layer.provide(APIClient.Live),
|
||||
Layer.tapError(error => Console.error("ProductPageRuntime", "Impossible de créer le Layer :", error)),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
// oxlint-disable typescript/dot-notation
|
||||
import {
|
||||
Array as FxArray,
|
||||
Cause,
|
||||
Console,
|
||||
Context,
|
||||
Effect,
|
||||
|
|
@ -13,8 +12,8 @@ import {
|
|||
Schema,
|
||||
SchemaIssue,
|
||||
Stream,
|
||||
SubscriptionRef,
|
||||
} from "effect";
|
||||
import type { NoSuchElementError } from "effect/Cause";
|
||||
import type { SchemaError } from "effect/Schema";
|
||||
import { APIClient } from "../../scripts-effect/lib/api.ts";
|
||||
import type { APIRequestError } from "../../scripts-effect/lib/api.ts";
|
||||
|
|
@ -32,6 +31,7 @@ import { lanceAnimationCycleLoading } from "../lib/animations.ts";
|
|||
import { emetMessageMajBoutonPanier } from "../lib/messages.ts";
|
||||
import { IncoherentDOMError } from "./errors.ts";
|
||||
import ProductPageElements from "./service-elements.ts";
|
||||
import ProductPageMessages from "./service-messages.ts";
|
||||
import type { DetailEnsemble } from "./types.d.ts";
|
||||
|
||||
const PageStatesSchema = Schema.Struct({
|
||||
|
|
@ -48,36 +48,10 @@ class InvalidPageStateError extends Schema.TaggedErrorClass<InvalidPageStateErro
|
|||
});
|
||||
}
|
||||
|
||||
class ProductPageDOM extends Context.Service<
|
||||
ProductPageDOM,
|
||||
class ProductPageDOM extends Context.Service<ProductPageDOM>()(
|
||||
"haikuatelier.fr/Produit/ProductPageDOM",
|
||||
{
|
||||
/**
|
||||
* Initialise l'état initial du Bouton d'ajout au Panier.
|
||||
*/
|
||||
initAddToCartButtonInitialState: () => Effect.Effect<void>;
|
||||
/**
|
||||
* Initialise les mises à jour du Bouton d'ajout au Panier en fonction des interactions de l'Utilisateur.
|
||||
*/
|
||||
initAddToCartButtonUpdates: () => Effect.Effect<void>;
|
||||
/**
|
||||
* Initialise les interactions des Sections de la Description du Produit.
|
||||
*/
|
||||
initDetailInteractions: () => Effect.Effect<void, NoSuchElementError>;
|
||||
/*
|
||||
* Initialise la mise à jour du Prix affiché en fonction du choix de la Varation de Produit.
|
||||
*/
|
||||
initPriceUpdatesOnVariationChange: () => Effect.Effect<void, Error>;
|
||||
/**
|
||||
* Replie toutes les sections de la description du Produit.
|
||||
*/
|
||||
initAddToCartButtonClicks: () => unknown;
|
||||
PageStates: typeof PageStatesSchema.Type;
|
||||
CurrentVariation: Ref.Ref<Option.Option<ProductVariation>>;
|
||||
}
|
||||
>()("haikuatelier.fr/Produit/ProductPageDOM") {
|
||||
static readonly layer = Layer.effect(
|
||||
ProductPageDOM,
|
||||
Effect.gen(function*() {
|
||||
make: Effect.gen(function*() {
|
||||
const {
|
||||
AddToCartButton,
|
||||
Details,
|
||||
|
|
@ -87,6 +61,7 @@ class ProductPageDOM extends Context.Service<
|
|||
VariationSelectors,
|
||||
PageStatesRawJson,
|
||||
} = yield* ProductPageElements;
|
||||
const { AddToCartButtonText } = yield* ProductPageMessages;
|
||||
const API = yield* APIClient;
|
||||
|
||||
const PageStates = yield* pipe(
|
||||
|
|
@ -208,7 +183,10 @@ class ProductPageDOM extends Context.Service<
|
|||
|
||||
// TODO: Faire une véritable gestion des erreurs.
|
||||
const recoverFromBackendFailure = Effect.fn("recoverFromBackendFailure")(function*(_error: APIRequestError) {
|
||||
AddToCartButton.textContent = "Error while adding the Product to the Cart...";
|
||||
AddToCartButton.toggleAttribute(ATTRIBUT_DESACTIVE, false);
|
||||
AddToCartButton.toggleAttribute(ATTRIBUT_CHARGEMENT, false);
|
||||
yield* SubscriptionRef.set(AddToCartButtonText, "Error!");
|
||||
|
||||
return yield* Effect.void;
|
||||
});
|
||||
|
||||
|
|
@ -241,7 +219,7 @@ class ProductPageDOM extends Context.Service<
|
|||
// Désactive les interactions le temps de la requête.
|
||||
AddToCartButton.toggleAttribute(ATTRIBUT_DESACTIVE, true);
|
||||
AddToCartButton.toggleAttribute(ATTRIBUT_CHARGEMENT, true);
|
||||
lanceAnimationCycleLoading(AddToCartButton, 500);
|
||||
// lanceAnimationCycleLoading(AddToCartButton, 500);
|
||||
|
||||
// Exécute la Requête auprès du backend.
|
||||
const newCart = yield* API.AddProductToCart(PageStates.nonce, requestBody);
|
||||
|
|
@ -252,7 +230,7 @@ class ProductPageDOM extends Context.Service<
|
|||
|
||||
AddToCartButton.toggleAttribute(ATTRIBUT_DESACTIVE, false);
|
||||
AddToCartButton.toggleAttribute(ATTRIBUT_CHARGEMENT, false);
|
||||
AddToCartButton.textContent = "Add to cart";
|
||||
yield* SubscriptionRef.set(AddToCartButtonText, "Add to cart");
|
||||
},
|
||||
Effect.catchTag("APIRequestError", recoverFromBackendFailure),
|
||||
);
|
||||
|
|
@ -313,7 +291,7 @@ class ProductPageDOM extends Context.Service<
|
|||
);
|
||||
});
|
||||
|
||||
return ProductPageDOM.of({
|
||||
return {
|
||||
CurrentVariation: CurrentProduct,
|
||||
initAddToCartButtonClicks,
|
||||
initAddToCartButtonInitialState,
|
||||
|
|
@ -321,9 +299,11 @@ class ProductPageDOM extends Context.Service<
|
|||
initDetailInteractions,
|
||||
initPriceUpdatesOnVariationChange,
|
||||
PageStates,
|
||||
});
|
||||
};
|
||||
}),
|
||||
);
|
||||
},
|
||||
) {
|
||||
static readonly Live = Layer.effect(this, this.make);
|
||||
}
|
||||
|
||||
export default ProductPageDOM;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
import { Context, Effect, Layer, pipe, Stream, SubscriptionRef } from "effect";
|
||||
import ProductPageElements from "./service-elements.ts";
|
||||
|
||||
class ProductPageMessages extends Context.Service<ProductPageMessages>()("haikuatelier.fr/Product/Messages", {
|
||||
make: Effect.gen(function*() {
|
||||
const { AddToCartButton } = yield* ProductPageElements;
|
||||
|
||||
const AddToCartButtonText = yield* SubscriptionRef.make("Add to cart");
|
||||
// const AddToCartErrorText = yield* SubscriptionRef.make<Option.Option<string>>(Option.none());
|
||||
|
||||
const initAddToCartButtonUpdates = Effect.fn("initAddToCartButtonUpdates")(function*() {
|
||||
return yield* pipe(
|
||||
SubscriptionRef.changes(AddToCartButtonText),
|
||||
Stream.tap(newText => {
|
||||
AddToCartButton.textContent = newText;
|
||||
return Effect.succeed(newText);
|
||||
}),
|
||||
Stream.runDrain,
|
||||
);
|
||||
});
|
||||
|
||||
return { AddToCartButtonText, initAddToCartButtonUpdates };
|
||||
}),
|
||||
}) {
|
||||
static readonly Live = Layer.effect(this, this.make);
|
||||
}
|
||||
|
||||
export default ProductPageMessages;
|
||||
|
|
@ -3,10 +3,12 @@
|
|||
import { Console, Effect } from "effect";
|
||||
import ProductPageRuntime from "./page-produit/runtime.ts";
|
||||
import ProductPageDOM from "./page-produit/service-dom.ts";
|
||||
import ProductPageMessages from "./page-produit/service-messages.ts";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", (): void => {
|
||||
Effect.gen(function*() {
|
||||
const DOM = yield* ProductPageDOM;
|
||||
const Messages = yield* ProductPageMessages;
|
||||
|
||||
const effects = Effect.all(
|
||||
[
|
||||
|
|
@ -15,6 +17,7 @@ document.addEventListener("DOMContentLoaded", (): void => {
|
|||
DOM.initAddToCartButtonClicks(),
|
||||
DOM.initDetailInteractions(),
|
||||
DOM.initPriceUpdatesOnVariationChange(),
|
||||
Messages.initAddToCartButtonUpdates(),
|
||||
],
|
||||
{
|
||||
concurrency: "unbounded",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue