@obsidiane/meridiane 2.0.1 → 2.0.2
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/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Bridge Angular (runtime + models TypeScript) pour une API Platform (Hydra/JSON-LD), avec support Mercure/SSE optionnel.
|
|
4
4
|
|
|
5
|
-
Le package expose une API Angular minimaliste :
|
|
5
|
+
Le package expose une API Angular volontairement minimaliste :
|
|
6
6
|
- `provideBridge()` pour configurer le bridge (HTTP + tokens + Mercure) ;
|
|
7
7
|
- `FacadeFactory` / `ResourceFacade<T>` pour une API orientée ressource ;
|
|
8
8
|
- `BridgeFacade` pour des appels ad-hoc.
|
|
@@ -22,7 +22,7 @@ Le bridge est conçu pour être installé dans une application Angular. Les dép
|
|
|
22
22
|
|
|
23
23
|
## Démarrage rapide
|
|
24
24
|
|
|
25
|
-
Configurez le bridge au démarrage de l’application avec `provideBridge()
|
|
25
|
+
Configurez le bridge au démarrage de l’application avec `provideBridge()` (requis).
|
|
26
26
|
|
|
27
27
|
### Application standalone (recommandé)
|
|
28
28
|
|
|
@@ -57,6 +57,15 @@ export class AppModule {}
|
|
|
57
57
|
|
|
58
58
|
## Configuration
|
|
59
59
|
|
|
60
|
+
`provideBridge({ ... })` accepte notamment :
|
|
61
|
+
- `baseUrl` (requis) ;
|
|
62
|
+
- `auth` (Bearer ou interceptor custom) ;
|
|
63
|
+
- `mercure` (hubUrl + options SSE) ;
|
|
64
|
+
- `defaults` (headers/timeout/retries) ;
|
|
65
|
+
- `singleFlight` (déduplication “in-flight” des requêtes HTTP identiques `GET/HEAD/OPTIONS`) ;
|
|
66
|
+
- `debug` (logs runtime) ;
|
|
67
|
+
- `extraInterceptors` (interceptors Angular additionnels).
|
|
68
|
+
|
|
60
69
|
### Cookies / `withCredentials`
|
|
61
70
|
|
|
62
71
|
Le bridge peut envoyer des cookies (sessions) côté HTTP et SSE.
|
|
@@ -175,6 +184,24 @@ provideBridge({
|
|
|
175
184
|
});
|
|
176
185
|
```
|
|
177
186
|
|
|
187
|
+
### Concurrence
|
|
188
|
+
|
|
189
|
+
Le bridge maintient une seule connexion SSE (une seule `EventSource`) et mutualise les topics :
|
|
190
|
+
- regarder plusieurs fois la même ressource ne crée pas plusieurs connexions ;
|
|
191
|
+
- un même topic est dédoublonné et géré par ref-count (unsubscribe effectif quand plus personne n’écoute).
|
|
192
|
+
|
|
193
|
+
Côté HTTP, le bridge déduplique les requêtes identiques tant qu’elles sont en cours (single-flight, activé par défaut) :
|
|
194
|
+
- `GET` / `HEAD` / `OPTIONS` : deux appels identiques partagent le même appel réseau et reçoivent la même réponse ;
|
|
195
|
+
- les méthodes avec body (`POST`/`PUT`/`PATCH`/`DELETE`) ne sont pas dédupliquées.
|
|
196
|
+
|
|
197
|
+
Ce n’est pas un cache : une fois la requête terminée, un nouvel appel identique relance un nouvel appel réseau.
|
|
198
|
+
|
|
199
|
+
Pour désactiver :
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
provideBridge({baseUrl: 'https://api.example.com', singleFlight: false});
|
|
203
|
+
```
|
|
204
|
+
|
|
178
205
|
API :
|
|
179
206
|
- `ResourceFacade<T>.watch$(iri | iri[])` / `unwatch(iri | iri[])`
|
|
180
207
|
- `ResourceFacade<T>.watchSubResource$(iri | iri[], 'field.path')`
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {HttpEvent, HttpInterceptorFn, HttpRequest} from '@angular/common/http';
|
|
2
|
+
import {finalize, Observable, shareReplay} from 'rxjs';
|
|
3
|
+
|
|
4
|
+
export type SingleFlightMode = 'off' | 'safe';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_MODE: SingleFlightMode = 'safe';
|
|
7
|
+
const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
|
|
8
|
+
|
|
9
|
+
export function createSingleFlightInterceptor(mode: SingleFlightMode = DEFAULT_MODE): HttpInterceptorFn {
|
|
10
|
+
const inflight = new Map<string, Observable<HttpEvent<unknown>>>();
|
|
11
|
+
|
|
12
|
+
return (req, next) => {
|
|
13
|
+
if (mode === 'off') {
|
|
14
|
+
return next(req);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const method = req.method.toUpperCase();
|
|
18
|
+
if (!SAFE_METHODS.has(method)) {
|
|
19
|
+
return next(req);
|
|
20
|
+
}
|
|
21
|
+
if (req.reportProgress === true) {
|
|
22
|
+
return next(req);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const key = computeKey(req);
|
|
26
|
+
const existing = inflight.get(key);
|
|
27
|
+
if (existing) {
|
|
28
|
+
return existing;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const shared$ = next(req).pipe(
|
|
32
|
+
finalize(() => inflight.delete(key)),
|
|
33
|
+
shareReplay({bufferSize: 1, refCount: true})
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
inflight.set(key, shared$);
|
|
37
|
+
return shared$;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function computeKey(req: HttpRequest<unknown>): string {
|
|
42
|
+
const auth = req.headers.get('Authorization') ?? '';
|
|
43
|
+
const accept = req.headers.get('Accept') ?? '';
|
|
44
|
+
const contentType = req.headers.get('Content-Type') ?? '';
|
|
45
|
+
const creds = req.withCredentials ? '1' : '0';
|
|
46
|
+
return [
|
|
47
|
+
req.method.toUpperCase(),
|
|
48
|
+
req.urlWithParams,
|
|
49
|
+
`rt=${req.responseType}`,
|
|
50
|
+
`wc=${creds}`,
|
|
51
|
+
`a=${auth}`,
|
|
52
|
+
`acc=${accept}`,
|
|
53
|
+
`ct=${contentType}`,
|
|
54
|
+
].join('::');
|
|
55
|
+
}
|
|
@@ -13,6 +13,7 @@ import {contentTypeInterceptor} from './interceptors/content-type.interceptor';
|
|
|
13
13
|
import {bridgeDefaultsInterceptor} from './interceptors/bridge-defaults.interceptor';
|
|
14
14
|
import {bridgeDebugInterceptor} from './interceptors/bridge-debug.interceptor';
|
|
15
15
|
import {from, switchMap} from 'rxjs';
|
|
16
|
+
import {createSingleFlightInterceptor} from './interceptors/singleflight.interceptor';
|
|
16
17
|
|
|
17
18
|
export type BridgeAuth =
|
|
18
19
|
| string
|
|
@@ -37,6 +38,13 @@ export interface BridgeOptions {
|
|
|
37
38
|
mercure?: BridgeMercureOptions;
|
|
38
39
|
/** Default HTTP behaviour (headers, timeout, retries). */
|
|
39
40
|
defaults?: BridgeDefaults;
|
|
41
|
+
/**
|
|
42
|
+
* De-duplicates in-flight HTTP requests.
|
|
43
|
+
*
|
|
44
|
+
* - `true` (default): single-flight for safe methods (`GET/HEAD/OPTIONS`)
|
|
45
|
+
* - `false`: disabled (each call triggers a new request)
|
|
46
|
+
*/
|
|
47
|
+
singleFlight?: boolean;
|
|
40
48
|
/** Enables debug logging via the debug interceptor and console logger. */
|
|
41
49
|
debug?: boolean;
|
|
42
50
|
/** Extra `HttpInterceptorFn` applied after bridge interceptors. */
|
|
@@ -50,6 +58,7 @@ export function provideBridge(opts: BridgeOptions): EnvironmentProviders {
|
|
|
50
58
|
auth,
|
|
51
59
|
mercure,
|
|
52
60
|
defaults,
|
|
61
|
+
singleFlight = true,
|
|
53
62
|
debug = false,
|
|
54
63
|
extraInterceptors = [],
|
|
55
64
|
} = opts;
|
|
@@ -69,6 +78,7 @@ export function provideBridge(opts: BridgeOptions): EnvironmentProviders {
|
|
|
69
78
|
contentTypeInterceptor,
|
|
70
79
|
bridgeDefaultsInterceptor,
|
|
71
80
|
...createAuthInterceptors(auth),
|
|
81
|
+
...(singleFlight ? [createSingleFlightInterceptor('safe')] : [createSingleFlightInterceptor('off')]),
|
|
72
82
|
...(debug ? [bridgeDebugInterceptor] : []),
|
|
73
83
|
...extraInterceptors,
|
|
74
84
|
];
|