@sia-ui/api 0.7.1 → 0.8.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/CHANGELOG.md +37 -0
- package/README.md +40 -0
- package/dist/index.cjs +118 -0
- package/dist/index.d.cts +86 -1
- package/dist/index.d.ts +86 -1
- package/dist/index.js +115 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# @sia-ui/api
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Français et anglais livrés, temps réel, calendrier réécrit, suivi d'opérations.
|
|
8
|
+
- **Deux langues** — `FRENCH` et `ENGLISH` (`@sia-ui/headless`), au format
|
|
9
|
+
`SiaLocale` : la langue de formatage (`language`), le vocabulaire commun et
|
|
10
|
+
un groupe typé par composant. Plus aucun texte en dur dans les composants,
|
|
11
|
+
`aria-label` compris ; dates, nombres et pluriels suivent `language`. Une
|
|
12
|
+
application qui a sa propre i18n remplit le même format. `formatMessage` et
|
|
13
|
+
`plural` (`{count}`, clés `_one` / `_other`). `ENGLISH_STATUS_MESSAGES`
|
|
14
|
+
(`@sia-ui/api`) à côté de `FRENCH_STATUS_MESSAGES`.
|
|
15
|
+
Seul changement de forme : `EventCalendarLabels.more`, qui était une
|
|
16
|
+
fonction, devient `more_one` / `more_other` avec `{count}`.
|
|
17
|
+
- **`SiaProvider` seul fournisseur** — il prend `locale` et `defaults`, et
|
|
18
|
+
s'imbrique pour une section. `SiaConfigProvider` est obsolète.
|
|
19
|
+
- **Temps réel** — `createRealtimeBinding`, `socketIoSource`,
|
|
20
|
+
`eventSourceSource` (`@sia-ui/api`), `useRealtimeStatus`, `LiveIndicator`.
|
|
21
|
+
- **`EventCalendar` réécrit** — vues mois, semaine, jour, agenda ; variantes ;
|
|
22
|
+
mode étroit. **`EventManager`** : créer, consulter, modifier, supprimer.
|
|
23
|
+
- **Nouveaux composants** — `AccountMenu`, `ColorModeToggle`, `LogStream`,
|
|
24
|
+
`JsonView`, `JobProgress`, `BrandIcon` ; `SunIcon`, `MoonIcon`.
|
|
25
|
+
- **Formulaires** — `validateAsync` par champ, `submitProps.disabled`
|
|
26
|
+
respecté ; `controlProps` sur une propriété de ressource.
|
|
27
|
+
- **Tons lisibles en mode sombre** — `--sia-<ton>-foreground`, calculés pour
|
|
28
|
+
le contraste ; noms de variables CSS en kebab-case.
|
|
29
|
+
- **Couches en portail** — menus, calendriers, boîtes et toasts, rendus hors
|
|
30
|
+
de `[data-sia-root]`, reprennent la police et la couleur de texte du
|
|
31
|
+
système au lieu de celles du navigateur.
|
|
32
|
+
- **`Drawer` et `Modal`** — la fermeture est signalée tout de suite ; un
|
|
33
|
+
double clic à l'ouverture ne referme plus.
|
|
34
|
+
- **Documentation** — la présentation décrit chaque brique métier (session,
|
|
35
|
+
greffons d'API, ressources, formulaires, temps réel, langues, utilitaires)
|
|
36
|
+
avec son scénario et un exemple.
|
|
37
|
+
- **Props des sous-composants** — chaque composant composé expose celles de
|
|
38
|
+
ce qu'il utilise (`…Props`) ; `pnpm check:slot-props` le vérifie.
|
|
39
|
+
|
|
3
40
|
## 0.7.1
|
|
4
41
|
|
|
5
42
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -37,6 +37,9 @@ le projet utilise déjà axios. Le paquet ne dépend d'aucun des deux.
|
|
|
37
37
|
| `HttpError` | statut, corps, et `fields` — trois formes d'erreurs par champ |
|
|
38
38
|
| Greffons | auth, rafraîchissement de jeton, 401, journal, idempotence, lecture seule, signal d'en-tête |
|
|
39
39
|
| `TokenStorage` | mémoire ou navigateur, chaque accès protégé |
|
|
40
|
+
| Cache | `createQueryResource` : clés et options TanStack Query, invalidation après écriture |
|
|
41
|
+
| Temps réel | `createRealtimeBinding` : Socket.IO ou SSE qui tient le cache à jour, rattrapage à la reconnexion |
|
|
42
|
+
| Messages | `FRENCH_STATUS_MESSAGES`, `ENGLISH_STATUS_MESSAGES` : une phrase lisible par statut |
|
|
40
43
|
| Filtres | `$and` / `$or` portés par un en-tête plutôt que par l'URL |
|
|
41
44
|
|
|
42
45
|
Voir [docs/fondations/module-api.md](https://beatjo.github.io/sia-ui-site/fondations/module-api.html).
|
|
@@ -57,6 +60,43 @@ Le contenu ci-dessous reprend intégralement `CHANGELOG.md` pour rester visible
|
|
|
57
60
|
<!-- sia:changelog:start -->
|
|
58
61
|
# @sia-ui/api
|
|
59
62
|
|
|
63
|
+
## 0.8.0
|
|
64
|
+
|
|
65
|
+
### Minor Changes
|
|
66
|
+
|
|
67
|
+
- Français et anglais livrés, temps réel, calendrier réécrit, suivi d'opérations.
|
|
68
|
+
- **Deux langues** — `FRENCH` et `ENGLISH` (`@sia-ui/headless`), au format
|
|
69
|
+
`SiaLocale` : la langue de formatage (`language`), le vocabulaire commun et
|
|
70
|
+
un groupe typé par composant. Plus aucun texte en dur dans les composants,
|
|
71
|
+
`aria-label` compris ; dates, nombres et pluriels suivent `language`. Une
|
|
72
|
+
application qui a sa propre i18n remplit le même format. `formatMessage` et
|
|
73
|
+
`plural` (`{count}`, clés `_one` / `_other`). `ENGLISH_STATUS_MESSAGES`
|
|
74
|
+
(`@sia-ui/api`) à côté de `FRENCH_STATUS_MESSAGES`.
|
|
75
|
+
Seul changement de forme : `EventCalendarLabels.more`, qui était une
|
|
76
|
+
fonction, devient `more_one` / `more_other` avec `{count}`.
|
|
77
|
+
- **`SiaProvider` seul fournisseur** — il prend `locale` et `defaults`, et
|
|
78
|
+
s'imbrique pour une section. `SiaConfigProvider` est obsolète.
|
|
79
|
+
- **Temps réel** — `createRealtimeBinding`, `socketIoSource`,
|
|
80
|
+
`eventSourceSource` (`@sia-ui/api`), `useRealtimeStatus`, `LiveIndicator`.
|
|
81
|
+
- **`EventCalendar` réécrit** — vues mois, semaine, jour, agenda ; variantes ;
|
|
82
|
+
mode étroit. **`EventManager`** : créer, consulter, modifier, supprimer.
|
|
83
|
+
- **Nouveaux composants** — `AccountMenu`, `ColorModeToggle`, `LogStream`,
|
|
84
|
+
`JsonView`, `JobProgress`, `BrandIcon` ; `SunIcon`, `MoonIcon`.
|
|
85
|
+
- **Formulaires** — `validateAsync` par champ, `submitProps.disabled`
|
|
86
|
+
respecté ; `controlProps` sur une propriété de ressource.
|
|
87
|
+
- **Tons lisibles en mode sombre** — `--sia-<ton>-foreground`, calculés pour
|
|
88
|
+
le contraste ; noms de variables CSS en kebab-case.
|
|
89
|
+
- **Couches en portail** — menus, calendriers, boîtes et toasts, rendus hors
|
|
90
|
+
de `[data-sia-root]`, reprennent la police et la couleur de texte du
|
|
91
|
+
système au lieu de celles du navigateur.
|
|
92
|
+
- **`Drawer` et `Modal`** — la fermeture est signalée tout de suite ; un
|
|
93
|
+
double clic à l'ouverture ne referme plus.
|
|
94
|
+
- **Documentation** — la présentation décrit chaque brique métier (session,
|
|
95
|
+
greffons d'API, ressources, formulaires, temps réel, langues, utilitaires)
|
|
96
|
+
avec son scénario et un exemple.
|
|
97
|
+
- **Props des sous-composants** — chaque composant composé expose celles de
|
|
98
|
+
ce qu'il utilise (`…Props`) ; `pnpm check:slot-props` le vérifie.
|
|
99
|
+
|
|
60
100
|
## 0.7.1
|
|
61
101
|
|
|
62
102
|
### Patch Changes
|
package/dist/index.cjs
CHANGED
|
@@ -156,6 +156,22 @@ var FRENCH_STATUS_MESSAGES = {
|
|
|
156
156
|
503: "Le service est momentan\xE9ment indisponible.",
|
|
157
157
|
504: "Le serveur a mis trop de temps \xE0 r\xE9pondre."
|
|
158
158
|
};
|
|
159
|
+
var ENGLISH_STATUS_MESSAGES = {
|
|
160
|
+
0: "The server can't be reached. Check your connection.",
|
|
161
|
+
400: "The request is invalid.",
|
|
162
|
+
401: "Your session has expired. Please sign in again.",
|
|
163
|
+
403: "You don't have permission to do this.",
|
|
164
|
+
404: "The requested item can't be found.",
|
|
165
|
+
408: "The server took too long to respond.",
|
|
166
|
+
409: "This action conflicts with the current state. Reload and try again.",
|
|
167
|
+
413: "The content sent is too large.",
|
|
168
|
+
422: "Some values are invalid.",
|
|
169
|
+
429: "Too many attempts. Try again in a moment.",
|
|
170
|
+
500: "Something went wrong on the server.",
|
|
171
|
+
502: "The service is temporarily unavailable.",
|
|
172
|
+
503: "The service is temporarily unavailable.",
|
|
173
|
+
504: "The server took too long to respond."
|
|
174
|
+
};
|
|
159
175
|
var ECHEC_RESEAU = /failed to fetch|networkerror|load failed/i;
|
|
160
176
|
var PHRASES = {
|
|
161
177
|
400: "bad request",
|
|
@@ -1054,9 +1070,108 @@ function createQueryResource(service, options = {}) {
|
|
|
1054
1070
|
};
|
|
1055
1071
|
}
|
|
1056
1072
|
|
|
1073
|
+
// src/realtime.ts
|
|
1074
|
+
function createRealtimeBinding(options) {
|
|
1075
|
+
const { source, cache, on, refetchOnReconnect = [] } = options;
|
|
1076
|
+
const contexte = {
|
|
1077
|
+
cache,
|
|
1078
|
+
patch: (queryKey, updater) => {
|
|
1079
|
+
cache.setQueryData(queryKey, updater);
|
|
1080
|
+
},
|
|
1081
|
+
invalidate: (...queryKeys) => {
|
|
1082
|
+
for (const queryKey of queryKeys) void cache.invalidateQueries({ queryKey });
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
return {
|
|
1086
|
+
source,
|
|
1087
|
+
start() {
|
|
1088
|
+
const desabonnements = Object.entries(on).map(
|
|
1089
|
+
([topic, gestionnaire]) => source.subscribe(topic, (event) => {
|
|
1090
|
+
try {
|
|
1091
|
+
gestionnaire(event, contexte);
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
console.error(`Temps r\xE9el : le gestionnaire de \xAB ${topic} \xBB a \xE9chou\xE9.`, error);
|
|
1094
|
+
}
|
|
1095
|
+
})
|
|
1096
|
+
);
|
|
1097
|
+
let precedent = source.status();
|
|
1098
|
+
const statut = source.onStatusChange((suivant) => {
|
|
1099
|
+
if (suivant === "open" && precedent !== "open" && refetchOnReconnect.length > 0) {
|
|
1100
|
+
contexte.invalidate(...refetchOnReconnect);
|
|
1101
|
+
}
|
|
1102
|
+
precedent = suivant;
|
|
1103
|
+
});
|
|
1104
|
+
return () => {
|
|
1105
|
+
for (const desabonner of desabonnements) desabonner();
|
|
1106
|
+
statut();
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
function etatObservable(initial) {
|
|
1112
|
+
let courant = initial;
|
|
1113
|
+
const ecouteurs = /* @__PURE__ */ new Set();
|
|
1114
|
+
return {
|
|
1115
|
+
lire: () => courant,
|
|
1116
|
+
poser: (suivant) => {
|
|
1117
|
+
if (suivant === courant) return;
|
|
1118
|
+
courant = suivant;
|
|
1119
|
+
for (const ecouteur of ecouteurs) ecouteur(suivant);
|
|
1120
|
+
},
|
|
1121
|
+
ecouter: (ecouteur) => {
|
|
1122
|
+
ecouteurs.add(ecouteur);
|
|
1123
|
+
return () => {
|
|
1124
|
+
ecouteurs.delete(ecouteur);
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
function socketIoSource(socket) {
|
|
1130
|
+
const etat = etatObservable(socket.connected ? "open" : "connecting");
|
|
1131
|
+
const ouvert = () => etat.poser("open");
|
|
1132
|
+
const ferme = () => etat.poser("closed");
|
|
1133
|
+
const reconnexion = () => etat.poser("connecting");
|
|
1134
|
+
socket.on("connect", ouvert);
|
|
1135
|
+
socket.on("disconnect", ferme);
|
|
1136
|
+
socket.on("connect_error", reconnexion);
|
|
1137
|
+
return {
|
|
1138
|
+
subscribe(topic, handler) {
|
|
1139
|
+
const listener = (event) => handler(event);
|
|
1140
|
+
socket.on(topic, listener);
|
|
1141
|
+
return () => {
|
|
1142
|
+
socket.off(topic, listener);
|
|
1143
|
+
};
|
|
1144
|
+
},
|
|
1145
|
+
status: etat.lire,
|
|
1146
|
+
onStatusChange: etat.ecouter
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
function eventSourceSource(stream) {
|
|
1150
|
+
const etat = etatObservable(stream.readyState === 1 ? "open" : stream.readyState === 2 ? "closed" : "connecting");
|
|
1151
|
+
stream.onopen = () => etat.poser("open");
|
|
1152
|
+
stream.onerror = () => etat.poser(stream.readyState === 2 ? "closed" : "connecting");
|
|
1153
|
+
return {
|
|
1154
|
+
subscribe(topic, handler) {
|
|
1155
|
+
const listener = (event) => {
|
|
1156
|
+
let contenu = event.data;
|
|
1157
|
+
try {
|
|
1158
|
+
contenu = JSON.parse(event.data);
|
|
1159
|
+
} catch {
|
|
1160
|
+
}
|
|
1161
|
+
handler(contenu);
|
|
1162
|
+
};
|
|
1163
|
+
stream.addEventListener(topic, listener);
|
|
1164
|
+
return () => stream.removeEventListener(topic, listener);
|
|
1165
|
+
},
|
|
1166
|
+
status: etat.lire,
|
|
1167
|
+
onStatusChange: etat.ecouter
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1057
1171
|
exports.ACCESS_TOKEN_KEY = ACCESS_TOKEN_KEY;
|
|
1058
1172
|
exports.ApiClient = ApiClient;
|
|
1059
1173
|
exports.BaseService = BaseService;
|
|
1174
|
+
exports.ENGLISH_STATUS_MESSAGES = ENGLISH_STATUS_MESSAGES;
|
|
1060
1175
|
exports.FILTERS_HEADER = FILTERS_HEADER;
|
|
1061
1176
|
exports.FRENCH_STATUS_MESSAGES = FRENCH_STATUS_MESSAGES;
|
|
1062
1177
|
exports.HttpError = HttpError;
|
|
@@ -1074,11 +1189,14 @@ exports.createLoggerPlugin = createLoggerPlugin;
|
|
|
1074
1189
|
exports.createMemoryTokenStorage = createMemoryTokenStorage;
|
|
1075
1190
|
exports.createQueryResource = createQueryResource;
|
|
1076
1191
|
exports.createReadOnlyPlugin = createReadOnlyPlugin;
|
|
1192
|
+
exports.createRealtimeBinding = createRealtimeBinding;
|
|
1077
1193
|
exports.createRefreshTokenPlugin = createRefreshTokenPlugin;
|
|
1078
1194
|
exports.createResourceService = createResourceService;
|
|
1079
1195
|
exports.createUnauthorizedPlugin = createUnauthorizedPlugin;
|
|
1196
|
+
exports.eventSourceSource = eventSourceSource;
|
|
1080
1197
|
exports.filtersHeader = filtersHeader;
|
|
1081
1198
|
exports.isGenericMessage = isGenericMessage;
|
|
1082
1199
|
exports.localizeError = localizeError;
|
|
1083
1200
|
exports.nestFieldErrors = nestFieldErrors;
|
|
1084
1201
|
exports.normalizeFilters = normalizeFilters;
|
|
1202
|
+
exports.socketIoSource = socketIoSource;
|
package/dist/index.d.cts
CHANGED
|
@@ -250,6 +250,8 @@ declare function createApiClient(options: ApiClientOptions): ApiClient;
|
|
|
250
250
|
* `0` couvre l'échec réseau : le serveur injoignable, la connexion coupée.
|
|
251
251
|
*/
|
|
252
252
|
declare const FRENCH_STATUS_MESSAGES: Readonly<Record<number, string>>;
|
|
253
|
+
/** Les mêmes, en anglais. */
|
|
254
|
+
declare const ENGLISH_STATUS_MESSAGES: Readonly<Record<number, string>>;
|
|
253
255
|
/**
|
|
254
256
|
* Vrai quand le message du serveur est générique.
|
|
255
257
|
*
|
|
@@ -560,6 +562,89 @@ declare function createQueryResource<TEntity, TCreate = Partial<TEntity>, TUpdat
|
|
|
560
562
|
};
|
|
561
563
|
type QueryResource<TEntity> = ReturnType<typeof createQueryResource<TEntity>>;
|
|
562
564
|
|
|
565
|
+
/** L'état d'une connexion temps réel. */
|
|
566
|
+
type RealtimeStatus = "connecting" | "open" | "closed";
|
|
567
|
+
/**
|
|
568
|
+
* Un flux d'événements, quel qu'il soit : Socket.IO, SSE, WebSocket nu.
|
|
569
|
+
*
|
|
570
|
+
* La forme minimale qu'une liaison demande — s'abonner à un sujet, et suivre
|
|
571
|
+
* l'état de la connexion. `socketIoSource` et `eventSourceSource` en
|
|
572
|
+
* fabriquent une ; un autre transport n'a qu'à fournir ces trois membres.
|
|
573
|
+
*/
|
|
574
|
+
interface RealtimeSource {
|
|
575
|
+
subscribe(topic: string, handler: (event: unknown) => void): () => void;
|
|
576
|
+
status(): RealtimeStatus;
|
|
577
|
+
onStatusChange(listener: (status: RealtimeStatus) => void): () => void;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Ce que la liaison demande du cache de requêtes : invalider, et patcher.
|
|
581
|
+
* Le `QueryClient` de TanStack Query le satisfait tel quel.
|
|
582
|
+
*/
|
|
583
|
+
interface QueryCacheClient extends QueryInvalidator {
|
|
584
|
+
setQueryData: (queryKey: readonly unknown[], updater: (old: unknown) => unknown) => unknown;
|
|
585
|
+
}
|
|
586
|
+
/** Les outils remis à chaque gestionnaire d'événement. */
|
|
587
|
+
interface RealtimeContext {
|
|
588
|
+
/** Remplace une entrée du cache, sans requête : le détail d'une ligne. */
|
|
589
|
+
patch: <T>(queryKey: readonly unknown[], updater: (old: T | undefined) => T | undefined) => void;
|
|
590
|
+
/** Rend des requêtes périmées : les listes, un compteur. */
|
|
591
|
+
invalidate: (...queryKeys: Array<readonly unknown[]>) => void;
|
|
592
|
+
cache: QueryCacheClient;
|
|
593
|
+
}
|
|
594
|
+
interface RealtimeBindingOptions {
|
|
595
|
+
source: RealtimeSource;
|
|
596
|
+
cache: QueryCacheClient;
|
|
597
|
+
/** Un gestionnaire par sujet : \`"operation.updated": (event, { patch, invalidate }) => …\`. */
|
|
598
|
+
on: Record<string, (event: never, context: RealtimeContext) => void>;
|
|
599
|
+
/**
|
|
600
|
+
* À la reconnexion, ce qui a pu être manqué pendant la coupure est
|
|
601
|
+
* invalidé : ces clés, typiquement les listes suivies.
|
|
602
|
+
*/
|
|
603
|
+
refetchOnReconnect?: Array<readonly unknown[]>;
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Relie un flux d'événements au cache de requêtes.
|
|
607
|
+
*
|
|
608
|
+
* Chaque projet réécrivait la même chose : s'abonner, patcher le détail,
|
|
609
|
+
* invalider les listes, se désabonner au bon moment, et rattraper ce qui
|
|
610
|
+
* s'est passé pendant une coupure. \`start()\` branche tout et rend la
|
|
611
|
+
* fonction qui débranche tout — la forme exacte d'un nettoyage d'effet :
|
|
612
|
+
*
|
|
613
|
+
* \`\`\`ts
|
|
614
|
+
* useEffect(() => liaison.start(), [liaison]);
|
|
615
|
+
* \`\`\`
|
|
616
|
+
*/
|
|
617
|
+
declare function createRealtimeBinding(options: RealtimeBindingOptions): {
|
|
618
|
+
source: RealtimeSource;
|
|
619
|
+
start(): () => void;
|
|
620
|
+
};
|
|
621
|
+
type RealtimeBinding = ReturnType<typeof createRealtimeBinding>;
|
|
622
|
+
/** La forme d'un socket Socket.IO, sans en dépendre. */
|
|
623
|
+
interface SocketLike {
|
|
624
|
+
connected: boolean;
|
|
625
|
+
on(event: string, listener: (...args: never[]) => void): unknown;
|
|
626
|
+
off(event: string, listener: (...args: never[]) => void): unknown;
|
|
627
|
+
}
|
|
628
|
+
/** Un socket Socket.IO en source temps réel. L'authentification reste au socket. */
|
|
629
|
+
declare function socketIoSource(socket: SocketLike): RealtimeSource;
|
|
630
|
+
/** La forme d'un \`EventSource\`, sans en dépendre (et testable sans navigateur). */
|
|
631
|
+
interface EventSourceLike {
|
|
632
|
+
readyState: number;
|
|
633
|
+
onopen: ((event: unknown) => void) | null;
|
|
634
|
+
onerror: ((event: unknown) => void) | null;
|
|
635
|
+
addEventListener(type: string, listener: (event: {
|
|
636
|
+
data: string;
|
|
637
|
+
}) => void): void;
|
|
638
|
+
removeEventListener(type: string, listener: (event: {
|
|
639
|
+
data: string;
|
|
640
|
+
}) => void): void;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Un flux SSE en source temps réel. Chaque événement nommé devient un sujet ;
|
|
644
|
+
* son \`data\` est lu en JSON, ou passé tel quel s'il n'en est pas.
|
|
645
|
+
*/
|
|
646
|
+
declare function eventSourceSource(stream: EventSourceLike): RealtimeSource;
|
|
647
|
+
|
|
563
648
|
interface AxiosLikeInstance {
|
|
564
649
|
request<T = unknown>(config: {
|
|
565
650
|
url: string;
|
|
@@ -582,4 +667,4 @@ interface AxiosLikeInstance {
|
|
|
582
667
|
declare function createFetchTransport(fetcher?: typeof fetch): ApiTransport;
|
|
583
668
|
declare function createAxiosTransport(axios: AxiosLikeInstance): ApiTransport;
|
|
584
669
|
|
|
585
|
-
export { ACCESS_TOKEN_KEY, ApiClient, type ApiClientOptions, type ApiFilters, type ApiMethod, type ApiPlugin, type ApiPluginHook, type ApiRequestConfig, type ApiRequestContext, type ApiRequestInput, type ApiTransport, type ApiTransportRequest, type ApiTransportResponse, type AuthPluginOptions, BaseService, type BrowserTokenStorageOptions, type CursorMeta, FILTERS_HEADER, FRENCH_STATUS_MESSAGES, type FieldError, type FieldErrorExtractor, type FilterCondition, type FilterGroup, type FilterOperator, type FilterValue, type HeaderSignalPluginOptions, HttpError, type IdempotencyPluginOptions, type LoggerPluginOptions, type PageMeta, type PaginatedResponse, type QueryFnContext, type QueryInvalidator, type QueryParamObject, type QueryParams, type QueryResource, type QueryResourceOptions, REFRESH_TOKEN_KEY, type RefreshTokenConfig, ResponseHandler, type ResponseHandlerConfig, type RtkBaseQueryResult, type SaveVariables, type TokenStorage, type UnauthorizedPluginOptions, buildQueryString, createApiClient, createAuthPlugin, createAxiosTransport, createBrowserTokenStorage, createFetchTransport, createHeaderSignalPlugin, createIdempotencyPlugin, createLoggerPlugin, createMemoryTokenStorage, createQueryResource, createReadOnlyPlugin, createRefreshTokenPlugin, createResourceService, createUnauthorizedPlugin, filtersHeader, isGenericMessage, localizeError, nestFieldErrors, normalizeFilters };
|
|
670
|
+
export { ACCESS_TOKEN_KEY, ApiClient, type ApiClientOptions, type ApiFilters, type ApiMethod, type ApiPlugin, type ApiPluginHook, type ApiRequestConfig, type ApiRequestContext, type ApiRequestInput, type ApiTransport, type ApiTransportRequest, type ApiTransportResponse, type AuthPluginOptions, BaseService, type BrowserTokenStorageOptions, type CursorMeta, ENGLISH_STATUS_MESSAGES, type EventSourceLike, FILTERS_HEADER, FRENCH_STATUS_MESSAGES, type FieldError, type FieldErrorExtractor, type FilterCondition, type FilterGroup, type FilterOperator, type FilterValue, type HeaderSignalPluginOptions, HttpError, type IdempotencyPluginOptions, type LoggerPluginOptions, type PageMeta, type PaginatedResponse, type QueryCacheClient, type QueryFnContext, type QueryInvalidator, type QueryParamObject, type QueryParams, type QueryResource, type QueryResourceOptions, REFRESH_TOKEN_KEY, type RealtimeBinding, type RealtimeBindingOptions, type RealtimeContext, type RealtimeSource, type RealtimeStatus, type RefreshTokenConfig, ResponseHandler, type ResponseHandlerConfig, type RtkBaseQueryResult, type SaveVariables, type SocketLike, type TokenStorage, type UnauthorizedPluginOptions, buildQueryString, createApiClient, createAuthPlugin, createAxiosTransport, createBrowserTokenStorage, createFetchTransport, createHeaderSignalPlugin, createIdempotencyPlugin, createLoggerPlugin, createMemoryTokenStorage, createQueryResource, createReadOnlyPlugin, createRealtimeBinding, createRefreshTokenPlugin, createResourceService, createUnauthorizedPlugin, eventSourceSource, filtersHeader, isGenericMessage, localizeError, nestFieldErrors, normalizeFilters, socketIoSource };
|
package/dist/index.d.ts
CHANGED
|
@@ -250,6 +250,8 @@ declare function createApiClient(options: ApiClientOptions): ApiClient;
|
|
|
250
250
|
* `0` couvre l'échec réseau : le serveur injoignable, la connexion coupée.
|
|
251
251
|
*/
|
|
252
252
|
declare const FRENCH_STATUS_MESSAGES: Readonly<Record<number, string>>;
|
|
253
|
+
/** Les mêmes, en anglais. */
|
|
254
|
+
declare const ENGLISH_STATUS_MESSAGES: Readonly<Record<number, string>>;
|
|
253
255
|
/**
|
|
254
256
|
* Vrai quand le message du serveur est générique.
|
|
255
257
|
*
|
|
@@ -560,6 +562,89 @@ declare function createQueryResource<TEntity, TCreate = Partial<TEntity>, TUpdat
|
|
|
560
562
|
};
|
|
561
563
|
type QueryResource<TEntity> = ReturnType<typeof createQueryResource<TEntity>>;
|
|
562
564
|
|
|
565
|
+
/** L'état d'une connexion temps réel. */
|
|
566
|
+
type RealtimeStatus = "connecting" | "open" | "closed";
|
|
567
|
+
/**
|
|
568
|
+
* Un flux d'événements, quel qu'il soit : Socket.IO, SSE, WebSocket nu.
|
|
569
|
+
*
|
|
570
|
+
* La forme minimale qu'une liaison demande — s'abonner à un sujet, et suivre
|
|
571
|
+
* l'état de la connexion. `socketIoSource` et `eventSourceSource` en
|
|
572
|
+
* fabriquent une ; un autre transport n'a qu'à fournir ces trois membres.
|
|
573
|
+
*/
|
|
574
|
+
interface RealtimeSource {
|
|
575
|
+
subscribe(topic: string, handler: (event: unknown) => void): () => void;
|
|
576
|
+
status(): RealtimeStatus;
|
|
577
|
+
onStatusChange(listener: (status: RealtimeStatus) => void): () => void;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Ce que la liaison demande du cache de requêtes : invalider, et patcher.
|
|
581
|
+
* Le `QueryClient` de TanStack Query le satisfait tel quel.
|
|
582
|
+
*/
|
|
583
|
+
interface QueryCacheClient extends QueryInvalidator {
|
|
584
|
+
setQueryData: (queryKey: readonly unknown[], updater: (old: unknown) => unknown) => unknown;
|
|
585
|
+
}
|
|
586
|
+
/** Les outils remis à chaque gestionnaire d'événement. */
|
|
587
|
+
interface RealtimeContext {
|
|
588
|
+
/** Remplace une entrée du cache, sans requête : le détail d'une ligne. */
|
|
589
|
+
patch: <T>(queryKey: readonly unknown[], updater: (old: T | undefined) => T | undefined) => void;
|
|
590
|
+
/** Rend des requêtes périmées : les listes, un compteur. */
|
|
591
|
+
invalidate: (...queryKeys: Array<readonly unknown[]>) => void;
|
|
592
|
+
cache: QueryCacheClient;
|
|
593
|
+
}
|
|
594
|
+
interface RealtimeBindingOptions {
|
|
595
|
+
source: RealtimeSource;
|
|
596
|
+
cache: QueryCacheClient;
|
|
597
|
+
/** Un gestionnaire par sujet : \`"operation.updated": (event, { patch, invalidate }) => …\`. */
|
|
598
|
+
on: Record<string, (event: never, context: RealtimeContext) => void>;
|
|
599
|
+
/**
|
|
600
|
+
* À la reconnexion, ce qui a pu être manqué pendant la coupure est
|
|
601
|
+
* invalidé : ces clés, typiquement les listes suivies.
|
|
602
|
+
*/
|
|
603
|
+
refetchOnReconnect?: Array<readonly unknown[]>;
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Relie un flux d'événements au cache de requêtes.
|
|
607
|
+
*
|
|
608
|
+
* Chaque projet réécrivait la même chose : s'abonner, patcher le détail,
|
|
609
|
+
* invalider les listes, se désabonner au bon moment, et rattraper ce qui
|
|
610
|
+
* s'est passé pendant une coupure. \`start()\` branche tout et rend la
|
|
611
|
+
* fonction qui débranche tout — la forme exacte d'un nettoyage d'effet :
|
|
612
|
+
*
|
|
613
|
+
* \`\`\`ts
|
|
614
|
+
* useEffect(() => liaison.start(), [liaison]);
|
|
615
|
+
* \`\`\`
|
|
616
|
+
*/
|
|
617
|
+
declare function createRealtimeBinding(options: RealtimeBindingOptions): {
|
|
618
|
+
source: RealtimeSource;
|
|
619
|
+
start(): () => void;
|
|
620
|
+
};
|
|
621
|
+
type RealtimeBinding = ReturnType<typeof createRealtimeBinding>;
|
|
622
|
+
/** La forme d'un socket Socket.IO, sans en dépendre. */
|
|
623
|
+
interface SocketLike {
|
|
624
|
+
connected: boolean;
|
|
625
|
+
on(event: string, listener: (...args: never[]) => void): unknown;
|
|
626
|
+
off(event: string, listener: (...args: never[]) => void): unknown;
|
|
627
|
+
}
|
|
628
|
+
/** Un socket Socket.IO en source temps réel. L'authentification reste au socket. */
|
|
629
|
+
declare function socketIoSource(socket: SocketLike): RealtimeSource;
|
|
630
|
+
/** La forme d'un \`EventSource\`, sans en dépendre (et testable sans navigateur). */
|
|
631
|
+
interface EventSourceLike {
|
|
632
|
+
readyState: number;
|
|
633
|
+
onopen: ((event: unknown) => void) | null;
|
|
634
|
+
onerror: ((event: unknown) => void) | null;
|
|
635
|
+
addEventListener(type: string, listener: (event: {
|
|
636
|
+
data: string;
|
|
637
|
+
}) => void): void;
|
|
638
|
+
removeEventListener(type: string, listener: (event: {
|
|
639
|
+
data: string;
|
|
640
|
+
}) => void): void;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Un flux SSE en source temps réel. Chaque événement nommé devient un sujet ;
|
|
644
|
+
* son \`data\` est lu en JSON, ou passé tel quel s'il n'en est pas.
|
|
645
|
+
*/
|
|
646
|
+
declare function eventSourceSource(stream: EventSourceLike): RealtimeSource;
|
|
647
|
+
|
|
563
648
|
interface AxiosLikeInstance {
|
|
564
649
|
request<T = unknown>(config: {
|
|
565
650
|
url: string;
|
|
@@ -582,4 +667,4 @@ interface AxiosLikeInstance {
|
|
|
582
667
|
declare function createFetchTransport(fetcher?: typeof fetch): ApiTransport;
|
|
583
668
|
declare function createAxiosTransport(axios: AxiosLikeInstance): ApiTransport;
|
|
584
669
|
|
|
585
|
-
export { ACCESS_TOKEN_KEY, ApiClient, type ApiClientOptions, type ApiFilters, type ApiMethod, type ApiPlugin, type ApiPluginHook, type ApiRequestConfig, type ApiRequestContext, type ApiRequestInput, type ApiTransport, type ApiTransportRequest, type ApiTransportResponse, type AuthPluginOptions, BaseService, type BrowserTokenStorageOptions, type CursorMeta, FILTERS_HEADER, FRENCH_STATUS_MESSAGES, type FieldError, type FieldErrorExtractor, type FilterCondition, type FilterGroup, type FilterOperator, type FilterValue, type HeaderSignalPluginOptions, HttpError, type IdempotencyPluginOptions, type LoggerPluginOptions, type PageMeta, type PaginatedResponse, type QueryFnContext, type QueryInvalidator, type QueryParamObject, type QueryParams, type QueryResource, type QueryResourceOptions, REFRESH_TOKEN_KEY, type RefreshTokenConfig, ResponseHandler, type ResponseHandlerConfig, type RtkBaseQueryResult, type SaveVariables, type TokenStorage, type UnauthorizedPluginOptions, buildQueryString, createApiClient, createAuthPlugin, createAxiosTransport, createBrowserTokenStorage, createFetchTransport, createHeaderSignalPlugin, createIdempotencyPlugin, createLoggerPlugin, createMemoryTokenStorage, createQueryResource, createReadOnlyPlugin, createRefreshTokenPlugin, createResourceService, createUnauthorizedPlugin, filtersHeader, isGenericMessage, localizeError, nestFieldErrors, normalizeFilters };
|
|
670
|
+
export { ACCESS_TOKEN_KEY, ApiClient, type ApiClientOptions, type ApiFilters, type ApiMethod, type ApiPlugin, type ApiPluginHook, type ApiRequestConfig, type ApiRequestContext, type ApiRequestInput, type ApiTransport, type ApiTransportRequest, type ApiTransportResponse, type AuthPluginOptions, BaseService, type BrowserTokenStorageOptions, type CursorMeta, ENGLISH_STATUS_MESSAGES, type EventSourceLike, FILTERS_HEADER, FRENCH_STATUS_MESSAGES, type FieldError, type FieldErrorExtractor, type FilterCondition, type FilterGroup, type FilterOperator, type FilterValue, type HeaderSignalPluginOptions, HttpError, type IdempotencyPluginOptions, type LoggerPluginOptions, type PageMeta, type PaginatedResponse, type QueryCacheClient, type QueryFnContext, type QueryInvalidator, type QueryParamObject, type QueryParams, type QueryResource, type QueryResourceOptions, REFRESH_TOKEN_KEY, type RealtimeBinding, type RealtimeBindingOptions, type RealtimeContext, type RealtimeSource, type RealtimeStatus, type RefreshTokenConfig, ResponseHandler, type ResponseHandlerConfig, type RtkBaseQueryResult, type SaveVariables, type SocketLike, type TokenStorage, type UnauthorizedPluginOptions, buildQueryString, createApiClient, createAuthPlugin, createAxiosTransport, createBrowserTokenStorage, createFetchTransport, createHeaderSignalPlugin, createIdempotencyPlugin, createLoggerPlugin, createMemoryTokenStorage, createQueryResource, createReadOnlyPlugin, createRealtimeBinding, createRefreshTokenPlugin, createResourceService, createUnauthorizedPlugin, eventSourceSource, filtersHeader, isGenericMessage, localizeError, nestFieldErrors, normalizeFilters, socketIoSource };
|
package/dist/index.js
CHANGED
|
@@ -154,6 +154,22 @@ var FRENCH_STATUS_MESSAGES = {
|
|
|
154
154
|
503: "Le service est momentan\xE9ment indisponible.",
|
|
155
155
|
504: "Le serveur a mis trop de temps \xE0 r\xE9pondre."
|
|
156
156
|
};
|
|
157
|
+
var ENGLISH_STATUS_MESSAGES = {
|
|
158
|
+
0: "The server can't be reached. Check your connection.",
|
|
159
|
+
400: "The request is invalid.",
|
|
160
|
+
401: "Your session has expired. Please sign in again.",
|
|
161
|
+
403: "You don't have permission to do this.",
|
|
162
|
+
404: "The requested item can't be found.",
|
|
163
|
+
408: "The server took too long to respond.",
|
|
164
|
+
409: "This action conflicts with the current state. Reload and try again.",
|
|
165
|
+
413: "The content sent is too large.",
|
|
166
|
+
422: "Some values are invalid.",
|
|
167
|
+
429: "Too many attempts. Try again in a moment.",
|
|
168
|
+
500: "Something went wrong on the server.",
|
|
169
|
+
502: "The service is temporarily unavailable.",
|
|
170
|
+
503: "The service is temporarily unavailable.",
|
|
171
|
+
504: "The server took too long to respond."
|
|
172
|
+
};
|
|
157
173
|
var ECHEC_RESEAU = /failed to fetch|networkerror|load failed/i;
|
|
158
174
|
var PHRASES = {
|
|
159
175
|
400: "bad request",
|
|
@@ -1052,4 +1068,102 @@ function createQueryResource(service, options = {}) {
|
|
|
1052
1068
|
};
|
|
1053
1069
|
}
|
|
1054
1070
|
|
|
1055
|
-
|
|
1071
|
+
// src/realtime.ts
|
|
1072
|
+
function createRealtimeBinding(options) {
|
|
1073
|
+
const { source, cache, on, refetchOnReconnect = [] } = options;
|
|
1074
|
+
const contexte = {
|
|
1075
|
+
cache,
|
|
1076
|
+
patch: (queryKey, updater) => {
|
|
1077
|
+
cache.setQueryData(queryKey, updater);
|
|
1078
|
+
},
|
|
1079
|
+
invalidate: (...queryKeys) => {
|
|
1080
|
+
for (const queryKey of queryKeys) void cache.invalidateQueries({ queryKey });
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
return {
|
|
1084
|
+
source,
|
|
1085
|
+
start() {
|
|
1086
|
+
const desabonnements = Object.entries(on).map(
|
|
1087
|
+
([topic, gestionnaire]) => source.subscribe(topic, (event) => {
|
|
1088
|
+
try {
|
|
1089
|
+
gestionnaire(event, contexte);
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
console.error(`Temps r\xE9el : le gestionnaire de \xAB ${topic} \xBB a \xE9chou\xE9.`, error);
|
|
1092
|
+
}
|
|
1093
|
+
})
|
|
1094
|
+
);
|
|
1095
|
+
let precedent = source.status();
|
|
1096
|
+
const statut = source.onStatusChange((suivant) => {
|
|
1097
|
+
if (suivant === "open" && precedent !== "open" && refetchOnReconnect.length > 0) {
|
|
1098
|
+
contexte.invalidate(...refetchOnReconnect);
|
|
1099
|
+
}
|
|
1100
|
+
precedent = suivant;
|
|
1101
|
+
});
|
|
1102
|
+
return () => {
|
|
1103
|
+
for (const desabonner of desabonnements) desabonner();
|
|
1104
|
+
statut();
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
function etatObservable(initial) {
|
|
1110
|
+
let courant = initial;
|
|
1111
|
+
const ecouteurs = /* @__PURE__ */ new Set();
|
|
1112
|
+
return {
|
|
1113
|
+
lire: () => courant,
|
|
1114
|
+
poser: (suivant) => {
|
|
1115
|
+
if (suivant === courant) return;
|
|
1116
|
+
courant = suivant;
|
|
1117
|
+
for (const ecouteur of ecouteurs) ecouteur(suivant);
|
|
1118
|
+
},
|
|
1119
|
+
ecouter: (ecouteur) => {
|
|
1120
|
+
ecouteurs.add(ecouteur);
|
|
1121
|
+
return () => {
|
|
1122
|
+
ecouteurs.delete(ecouteur);
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
function socketIoSource(socket) {
|
|
1128
|
+
const etat = etatObservable(socket.connected ? "open" : "connecting");
|
|
1129
|
+
const ouvert = () => etat.poser("open");
|
|
1130
|
+
const ferme = () => etat.poser("closed");
|
|
1131
|
+
const reconnexion = () => etat.poser("connecting");
|
|
1132
|
+
socket.on("connect", ouvert);
|
|
1133
|
+
socket.on("disconnect", ferme);
|
|
1134
|
+
socket.on("connect_error", reconnexion);
|
|
1135
|
+
return {
|
|
1136
|
+
subscribe(topic, handler) {
|
|
1137
|
+
const listener = (event) => handler(event);
|
|
1138
|
+
socket.on(topic, listener);
|
|
1139
|
+
return () => {
|
|
1140
|
+
socket.off(topic, listener);
|
|
1141
|
+
};
|
|
1142
|
+
},
|
|
1143
|
+
status: etat.lire,
|
|
1144
|
+
onStatusChange: etat.ecouter
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
function eventSourceSource(stream) {
|
|
1148
|
+
const etat = etatObservable(stream.readyState === 1 ? "open" : stream.readyState === 2 ? "closed" : "connecting");
|
|
1149
|
+
stream.onopen = () => etat.poser("open");
|
|
1150
|
+
stream.onerror = () => etat.poser(stream.readyState === 2 ? "closed" : "connecting");
|
|
1151
|
+
return {
|
|
1152
|
+
subscribe(topic, handler) {
|
|
1153
|
+
const listener = (event) => {
|
|
1154
|
+
let contenu = event.data;
|
|
1155
|
+
try {
|
|
1156
|
+
contenu = JSON.parse(event.data);
|
|
1157
|
+
} catch {
|
|
1158
|
+
}
|
|
1159
|
+
handler(contenu);
|
|
1160
|
+
};
|
|
1161
|
+
stream.addEventListener(topic, listener);
|
|
1162
|
+
return () => stream.removeEventListener(topic, listener);
|
|
1163
|
+
},
|
|
1164
|
+
status: etat.lire,
|
|
1165
|
+
onStatusChange: etat.ecouter
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
export { ACCESS_TOKEN_KEY, ApiClient, BaseService, ENGLISH_STATUS_MESSAGES, FILTERS_HEADER, FRENCH_STATUS_MESSAGES, HttpError, REFRESH_TOKEN_KEY, ResponseHandler, buildQueryString, createApiClient, createAuthPlugin, createAxiosTransport, createBrowserTokenStorage, createFetchTransport, createHeaderSignalPlugin, createIdempotencyPlugin, createLoggerPlugin, createMemoryTokenStorage, createQueryResource, createReadOnlyPlugin, createRealtimeBinding, createRefreshTokenPlugin, createResourceService, createUnauthorizedPlugin, eventSourceSource, filtersHeader, isGenericMessage, localizeError, nestFieldErrors, normalizeFilters, socketIoSource };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sia-ui/api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Typed HTTP client with CRUD services, plugins and injectable transports.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sia-ui",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"access": "public"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@sia-ui/utils": "0.
|
|
45
|
+
"@sia-ui/utils": "0.8.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"tsup": "^8.5.1",
|