@constl/mandataire
Version:
Un mandataire très léger pour le client Constellation.
395 lines (354 loc) • 10.7 kB
text/typescript
import { TypedEmitter } from "tiny-typed-emitter";
import type { types } from "@constl/ipa";
import { v4 as uuidv4 } from "uuid";
import {
MessageActionDIpa,
MessageActionPourIpa,
MessageConfirmationRéceptionRetourDIpa,
MessageDIpa,
MessageErreurDIpa,
MessagePourIpa,
MessageRetourPourIpa,
MessageSuivrePourIpa,
MessageSuivrePrêtDIpa,
} from "./messages.js";
import {
ERREUR_EXÉCUTION_IPA,
ERREUR_FONCTION_MANQUANTE,
ERREUR_FORMAT_ARGUMENTS,
ERREUR_INIT_IPA,
ERREUR_INIT_IPA_DÉJÀ_LANCÉ,
ERREUR_MESSAGE_INCONNU,
ERREUR_MULTIPLES_FONCTIONS,
ERREUR_PAS_UNE_FONCTION,
} from "./codes.js";
import { lorsque } from "./utils.js";
interface Tâche {
idRequête: string;
fSuivre: types.schémaFonctionSuivi<unknown>;
fRetour: (fonction: string, args?: unknown[]) => Promise<void>;
}
class Callable extends Function {
// Code obtenu de https://hackernoon.com/creating-callable-objects-in-javascript-d21l3te1
//@ts-expect-error On ne peut pas appeller super() d'une fonction dans un contexte navigateur sécuritaire
constructor() {
const closure = function () {
// Rien faire. Je ne comprends pas tout ça mais ça fonctionne.
};
return Object.setPrototypeOf(closure, new.target.prototype);
}
}
export type ErreurMandataire = {
code: string;
erreur: string;
idRequête?: string;
};
type ÉvénementsMandataire = {
erreur: (e: ErreurMandataire) => void;
};
export abstract class Mandatairifiable extends Callable {
dernièreErreur?: ErreurMandataire;
événements: TypedEmitter<ÉvénementsMandataire>;
événementsInternes: TypedEmitter<{
[id: string]: (
x:
| MessageActionDIpa
| MessageSuivrePrêtDIpa
| MessageErreurDIpa
| MessageConfirmationRéceptionRetourDIpa,
) => void;
}>;
tâches: { [key: string]: Tâche };
constructor() {
super();
this.événements = new TypedEmitter<ÉvénementsMandataire>();
this.événementsInternes = new TypedEmitter<{
[id: string]: (
x:
| MessageActionDIpa
| MessageSuivrePrêtDIpa
| MessageErreurDIpa
| MessageConfirmationRéceptionRetourDIpa,
) => void;
}>();
this.tâches = {};
}
__call__(
fonction: string[],
args: { [key: string]: unknown } = {},
): Promise<unknown> {
if (typeof args !== "object")
this.erreur({
code: ERREUR_FORMAT_ARGUMENTS,
erreur: `La fonction ${fonction.join(
".",
)} fut appelée avec arguments ${args}.
Toute fonction mandataire Constellation doit être appelée avec un seul argument en format d'objet (dictionnaire).`,
});
const idRequête = uuidv4();
const nomArgFonction = Object.entries(args).find(
(x) => typeof x[1] === "function",
)?.[0];
if (nomArgFonction) {
return this.appelerFonctionSuivre(
idRequête,
fonction,
args,
nomArgFonction,
);
} else {
return this.appelerFonctionAction(idRequête, fonction, args);
}
}
async appelerFonctionSuivre(
idRequête: string,
fonction: string[],
args: { [key: string]: unknown },
nomArgFonction: string,
): Promise<
| types.schémaFonctionOublier
| { [key: string]: (...args: unknown[]) => void }
> {
const f = args[nomArgFonction] as types.schémaFonctionSuivi<unknown>;
const argsSansF = Object.fromEntries(
Object.entries(args).filter((x) => typeof x[1] !== "function"),
);
// Vérifier format paramètres
if (f === undefined) {
this.erreur({
code: ERREUR_FONCTION_MANQUANTE,
erreur:
"Aucun argument de nom " +
nomArgFonction +
" n'a été donnée pour " +
fonction.join("."),
idRequête,
});
}
if (Object.keys(args).length > Object.keys(argsSansF).length + 1) {
this.erreur({
code: ERREUR_MULTIPLES_FONCTIONS,
erreur:
"Plus d'un argument pour " +
fonction.join(".") +
" est une fonction : " +
JSON.stringify(args),
idRequête,
});
} else if (typeof f !== "function") {
this.erreur({
code: ERREUR_PAS_UNE_FONCTION,
erreur: "Argument " + nomArgFonction + "n'est pas une fonction : ",
idRequête,
});
}
const message: MessageSuivrePourIpa = {
type: "suivre",
idRequête,
fonction,
args: argsSansF,
nomArgFonction,
};
const fRetour = async (fonction: string, args?: unknown[]) => {
const idRetour = uuidv4();
const messageRetour: MessageRetourPourIpa = {
type: "retour",
idRequête,
fonction,
idRetour,
args,
};
const lorsqueRetour = lorsque(this.événementsInternes, idRetour);
this.envoyerMessageÀIpa(messageRetour);
const retour = await lorsqueRetour;
if (retour.type === "erreur") {
this.erreur({
erreur: retour.erreur,
idRequête,
code: retour.codeErreur || ERREUR_EXÉCUTION_IPA,
});
}
if (retour.type !== "confirmation") {
this.erreur({
erreur: `Type de retour ${JSON.stringify(retour)} non reconnu.`,
idRequête,
code: ERREUR_EXÉCUTION_IPA,
});
}
};
const tâche: Tâche = {
idRequête,
fSuivre: f,
fRetour,
};
this.tâches[idRequête] = tâche;
const fOublierTâche = async () => {
await this.oublierTâche(idRequête);
};
const lorsqueRetour = lorsque(this.événementsInternes, idRequête);
this.envoyerMessageÀIpa(message);
const retour = await lorsqueRetour;
if (retour.type === "erreur") {
this.erreur({
erreur: retour.erreur,
idRequête,
code: retour.codeErreur || ERREUR_EXÉCUTION_IPA,
});
}
if (retour.type === "suivrePrêt") {
const { fonctions } = retour;
if (fonctions && fonctions[0]) {
const retour: { [key: string]: (...args: unknown[]) => Promise<void> } =
{
fOublier: fOublierTâche,
};
for (const f of fonctions) {
retour[f] = async (...args: unknown[]) => {
await this.tâches[idRequête]?.fRetour(f, args);
};
}
return retour;
}
}
return fOublierTâche;
}
async appelerFonctionAction<T>(
idRequête: string,
fonction: string[],
args: { [key: string]: unknown },
): Promise<T> {
const message: MessageActionPourIpa = {
type: "action",
idRequête,
fonction,
args: args,
};
const lorsqueRetour = lorsque(this.événementsInternes, idRequête);
this.envoyerMessageÀIpa(message);
const retour = await lorsqueRetour;
if (retour.type === "action") {
return retour.résultat as T;
} else if (retour.type === "erreur") {
this.erreur({
erreur: retour.erreur,
idRequête,
code: retour.codeErreur || ERREUR_EXÉCUTION_IPA,
});
} else {
this.erreur({
erreur: `Type de retour ${retour} non reconnu.`,
idRequête,
code: ERREUR_EXÉCUTION_IPA,
});
}
throw new Error("On ne devrait jamais arriver ici.");
}
erreur({
erreur,
code,
idRequête,
}: {
erreur: string;
code: string;
idRequête?: string;
}): void {
// Si l'IPA n'a pas bien été initialisée, toutes les autres erreurs sont pas très importantes
if (
this.dernièreErreur?.code !== ERREUR_INIT_IPA &&
this.dernièreErreur?.code !== ERREUR_INIT_IPA_DÉJÀ_LANCÉ
) {
this.dernièreErreur = { erreur, idRequête, code };
}
this.événements.emit("erreur", { idRequête, ...this.dernièreErreur });
throw new Error(JSON.stringify({ idRequête, ...this.dernièreErreur }));
}
async oublierTâche(idRequête: string): Promise<void> {
const tâche = this.tâches[idRequête];
if (tâche) await tâche.fRetour("fOublier");
delete this.tâches[idRequête];
}
abstract envoyerMessageÀIpa(message: MessagePourIpa): void;
async recevoirMessageDIpa(message: MessageDIpa): Promise<void> {
const { type } = message;
switch (type) {
case "suivre": {
const { idRequête, données } = message;
if (!this.tâches[idRequête]) return;
const { fSuivre } = this.tâches[idRequête];
await fSuivre(données);
break;
}
case "action":
case "suivrePrêt":
case "erreur": {
if (message.type === "erreur" && !message.idRequête) {
this.erreur({
erreur: message.erreur,
code: message.erreur || ERREUR_EXÉCUTION_IPA,
});
break;
}
this.événementsInternes.emit(message.idRequête!, message);
break;
}
case "confirmation":
// Important - on utiliser l'id du message de retour
this.événementsInternes.emit(message.idRetour, message);
break;
default: {
this.erreur({
code: ERREUR_MESSAGE_INCONNU,
erreur: `Type inconnu ${type} du message ${message}.`,
idRequête: (message as MessageDIpa).idRequête,
});
}
}
}
// Fonctions publiques
suivreErreurs({
f,
}: {
f: (x: ErreurMandataire | undefined) => void;
}): types.schémaFonctionOublier {
this.événements.on("erreur", f);
f(this.dernièreErreur);
return async () => {
this.événements.off("erreur", f);
};
}
}
class Handler {
listeAtributs: string[];
constructor(listeAtributs?: string[]) {
this.listeAtributs = listeAtributs || [];
}
get(obj: Mandatairifiable, prop: string): unknown {
// Inscrire ici les fonctions publiques du mandataire qui ne
// doivent pas être envoyées à Constellation
const directes = ["suivreErreurs"];
if (directes.includes(prop)) {
return obj[prop as keyof Mandatairifiable].bind(obj);
} else {
const listeAtributs = [...this.listeAtributs, prop];
const h = new Handler(listeAtributs);
return new Proxy(obj, h);
}
}
apply(
target: Mandatairifiable,
_thisArg: Handler,
args: [{ [key: string]: unknown }],
) {
return target.__call__(this.listeAtributs, args[0]);
}
}
export type MandataireConstellation<T> = Required<T> & Mandatairifiable;
export const générerMandataire = <T>(
mandataireClient: Mandatairifiable,
): MandataireConstellation<T> => {
const handler = new Handler();
return new Proxy<Mandatairifiable>(
mandataireClient,
handler,
) as MandataireConstellation<T>;
};