@obsidiane/auth-client-js 1.0.4 → 1.0.6
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/README.md +725 -105
- package/fesm2022/obsidiane-auth-client-js.mjs +961 -0
- package/fesm2022/obsidiane-auth-client-js.mjs.map +1 -0
- package/index.d.ts +361 -0
- package/obsidiane-auth-client-js-0.1.0.tgz +0 -0
- package/package.json +15 -40
- package/dist/index.d.ts +0 -9
- package/dist/index.js +0 -10
- package/dist/index.js.map +0 -1
- package/dist/src/lib/bridge/rest/api-platform.adapter.d.ts +0 -18
- package/dist/src/lib/bridge/rest/api-platform.adapter.js +0 -67
- package/dist/src/lib/bridge/rest/api-platform.adapter.js.map +0 -1
- package/dist/src/lib/bridge/rest/http-request.options.d.ts +0 -4
- package/dist/src/lib/bridge/rest/http-request.options.js +0 -17
- package/dist/src/lib/bridge/rest/http-request.options.js.map +0 -1
- package/dist/src/lib/bridge/rest/query-builder.d.ts +0 -3
- package/dist/src/lib/bridge/rest/query-builder.js +0 -41
- package/dist/src/lib/bridge/rest/query-builder.js.map +0 -1
- package/dist/src/lib/bridge/sse/eventsource-wrapper.d.ts +0 -17
- package/dist/src/lib/bridge/sse/eventsource-wrapper.js +0 -57
- package/dist/src/lib/bridge/sse/eventsource-wrapper.js.map +0 -1
- package/dist/src/lib/bridge/sse/mercure-topic.mapper.d.ts +0 -19
- package/dist/src/lib/bridge/sse/mercure-topic.mapper.js +0 -45
- package/dist/src/lib/bridge/sse/mercure-topic.mapper.js.map +0 -1
- package/dist/src/lib/bridge/sse/mercure-url.builder.d.ts +0 -7
- package/dist/src/lib/bridge/sse/mercure-url.builder.js +0 -18
- package/dist/src/lib/bridge/sse/mercure-url.builder.js.map +0 -1
- package/dist/src/lib/bridge/sse/mercure.adapter.d.ts +0 -37
- package/dist/src/lib/bridge/sse/mercure.adapter.js +0 -242
- package/dist/src/lib/bridge/sse/mercure.adapter.js.map +0 -1
- package/dist/src/lib/bridge/sse/ref-count-topic.registry.d.ts +0 -17
- package/dist/src/lib/bridge/sse/ref-count-topic.registry.js +0 -42
- package/dist/src/lib/bridge/sse/ref-count-topic.registry.js.map +0 -1
- package/dist/src/lib/bridge.types.d.ts +0 -27
- package/dist/src/lib/bridge.types.js +0 -8
- package/dist/src/lib/bridge.types.js.map +0 -1
- package/dist/src/lib/facades/bridge.facade.d.ts +0 -27
- package/dist/src/lib/facades/bridge.facade.js +0 -100
- package/dist/src/lib/facades/bridge.facade.js.map +0 -1
- package/dist/src/lib/facades/facade.factory.d.ts +0 -22
- package/dist/src/lib/facades/facade.factory.js +0 -36
- package/dist/src/lib/facades/facade.factory.js.map +0 -1
- package/dist/src/lib/facades/facade.interface.d.ts +0 -13
- package/dist/src/lib/facades/facade.interface.js +0 -2
- package/dist/src/lib/facades/facade.interface.js.map +0 -1
- package/dist/src/lib/facades/resource.facade.d.ts +0 -30
- package/dist/src/lib/facades/resource.facade.js +0 -61
- package/dist/src/lib/facades/resource.facade.js.map +0 -1
- package/dist/src/lib/interceptors/bridge-debug.interceptor.d.ts +0 -6
- package/dist/src/lib/interceptors/bridge-debug.interceptor.js +0 -27
- package/dist/src/lib/interceptors/bridge-debug.interceptor.js.map +0 -1
- package/dist/src/lib/interceptors/bridge-defaults.interceptor.d.ts +0 -5
- package/dist/src/lib/interceptors/bridge-defaults.interceptor.js +0 -43
- package/dist/src/lib/interceptors/bridge-defaults.interceptor.js.map +0 -1
- package/dist/src/lib/interceptors/content-type.interceptor.d.ts +0 -9
- package/dist/src/lib/interceptors/content-type.interceptor.js +0 -34
- package/dist/src/lib/interceptors/content-type.interceptor.js.map +0 -1
- package/dist/src/lib/interceptors/singleflight.interceptor.d.ts +0 -3
- package/dist/src/lib/interceptors/singleflight.interceptor.js +0 -42
- package/dist/src/lib/interceptors/singleflight.interceptor.js.map +0 -1
- package/dist/src/lib/ports/realtime.port.d.ts +0 -28
- package/dist/src/lib/ports/realtime.port.js +0 -2
- package/dist/src/lib/ports/realtime.port.js.map +0 -1
- package/dist/src/lib/ports/resource-repository.port.d.ts +0 -64
- package/dist/src/lib/ports/resource-repository.port.js +0 -2
- package/dist/src/lib/ports/resource-repository.port.js.map +0 -1
- package/dist/src/lib/provide-bridge.d.ts +0 -40
- package/dist/src/lib/provide-bridge.js +0 -81
- package/dist/src/lib/provide-bridge.js.map +0 -1
- package/dist/src/lib/tokens.d.ts +0 -14
- package/dist/src/lib/tokens.js +0 -14
- package/dist/src/lib/tokens.js.map +0 -1
- package/dist/src/lib/utils/url.d.ts +0 -6
- package/dist/src/lib/utils/url.js +0 -17
- package/dist/src/lib/utils/url.js.map +0 -1
- package/dist/src/models/Auth.d.ts +0 -4
- package/dist/src/models/Auth.js +0 -2
- package/dist/src/models/Auth.js.map +0 -1
- package/dist/src/models/AuthInviteCompleteInputInviteComplete.d.ts +0 -6
- package/dist/src/models/AuthInviteCompleteInputInviteComplete.js +0 -2
- package/dist/src/models/AuthInviteCompleteInputInviteComplete.js.map +0 -1
- package/dist/src/models/AuthInviteUserInputInviteSend.d.ts +0 -4
- package/dist/src/models/AuthInviteUserInputInviteSend.js +0 -2
- package/dist/src/models/AuthInviteUserInputInviteSend.js.map +0 -1
- package/dist/src/models/AuthLdJson.d.ts +0 -4
- package/dist/src/models/AuthLdJson.js +0 -2
- package/dist/src/models/AuthLdJson.js.map +0 -1
- package/dist/src/models/AuthPasswordForgotInputPasswordForgot.d.ts +0 -4
- package/dist/src/models/AuthPasswordForgotInputPasswordForgot.js +0 -2
- package/dist/src/models/AuthPasswordForgotInputPasswordForgot.js.map +0 -1
- package/dist/src/models/AuthPasswordResetInputPasswordReset.d.ts +0 -5
- package/dist/src/models/AuthPasswordResetInputPasswordReset.js +0 -2
- package/dist/src/models/AuthPasswordResetInputPasswordReset.js.map +0 -1
- package/dist/src/models/AuthRegisterUserInputUserRegister.d.ts +0 -5
- package/dist/src/models/AuthRegisterUserInputUserRegister.js +0 -2
- package/dist/src/models/AuthRegisterUserInputUserRegister.js.map +0 -1
- package/dist/src/models/FrontendConfig.d.ts +0 -11
- package/dist/src/models/FrontendConfig.js +0 -2
- package/dist/src/models/FrontendConfig.js.map +0 -1
- package/dist/src/models/InvitePreview.d.ts +0 -7
- package/dist/src/models/InvitePreview.js +0 -2
- package/dist/src/models/InvitePreview.js.map +0 -1
- package/dist/src/models/InviteUserInviteRead.d.ts +0 -8
- package/dist/src/models/InviteUserInviteRead.js +0 -2
- package/dist/src/models/InviteUserInviteRead.js.map +0 -1
- package/dist/src/models/Setup.d.ts +0 -4
- package/dist/src/models/Setup.js +0 -2
- package/dist/src/models/Setup.js.map +0 -1
- package/dist/src/models/SetupRegisterUserInputUserRegister.d.ts +0 -5
- package/dist/src/models/SetupRegisterUserInputUserRegister.js +0 -2
- package/dist/src/models/SetupRegisterUserInputUserRegister.js.map +0 -1
- package/dist/src/models/UserUpdateUserRolesInputUserRoles.d.ts +0 -4
- package/dist/src/models/UserUpdateUserRolesInputUserRoles.js +0 -2
- package/dist/src/models/UserUpdateUserRolesInputUserRoles.js.map +0 -1
- package/dist/src/models/UserUserRead.d.ts +0 -8
- package/dist/src/models/UserUserRead.js +0 -2
- package/dist/src/models/UserUserRead.js.map +0 -1
- package/dist/src/models/index.d.ts +0 -14
- package/dist/src/models/index.js +0 -2
- package/dist/src/models/index.js.map +0 -1
- package/dist/src/public-api.d.ts +0 -8
- package/dist/src/public-api.js +0 -9
- package/dist/src/public-api.js.map +0 -1
|
@@ -0,0 +1,961 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, makeEnvironmentProviders, PLATFORM_ID, Inject, Optional, Injectable, EnvironmentInjector, runInInjectionContext } from '@angular/core';
|
|
3
|
+
import * as i1 from '@angular/common/http';
|
|
4
|
+
import { HttpResponse, provideHttpClient, withFetch, withInterceptors, HttpParams, HttpClient } from '@angular/common/http';
|
|
5
|
+
import { timeout, retry, timer, tap, catchError, throwError, finalize, shareReplay, from, switchMap, ReplaySubject, Subject, BehaviorSubject, fromEvent, defer, EMPTY, of, map as map$1, filter as filter$1, share as share$1 } from 'rxjs';
|
|
6
|
+
import { auditTime, concatMap, takeUntil, map, filter, finalize as finalize$1, share } from 'rxjs/operators';
|
|
7
|
+
import { isPlatformBrowser } from '@angular/common';
|
|
8
|
+
import { toSignal } from '@angular/core/rxjs-interop';
|
|
9
|
+
|
|
10
|
+
/** Base URL of the API (e.g. `http://localhost:8000`). */
|
|
11
|
+
const API_BASE_URL = new InjectionToken('API_BASE_URL');
|
|
12
|
+
/** Mercure hub URL (e.g. `http://localhost:8000/.well-known/mercure`). */
|
|
13
|
+
const MERCURE_HUB_URL = new InjectionToken('MERCURE_HUB_URL');
|
|
14
|
+
/**
|
|
15
|
+
* Default credential policy for HTTP requests and Mercure EventSource.
|
|
16
|
+
* When `true`, the bridge sets `withCredentials: true` on HTTP calls and uses cookies for SSE.
|
|
17
|
+
*/
|
|
18
|
+
const BRIDGE_WITH_CREDENTIALS = new InjectionToken('BRIDGE_WITH_CREDENTIALS');
|
|
19
|
+
const MERCURE_TOPIC_MODE = new InjectionToken('MERCURE_TOPIC_MODE');
|
|
20
|
+
const BRIDGE_LOGGER = new InjectionToken('BRIDGE_LOGGER');
|
|
21
|
+
const BRIDGE_DEFAULTS = new InjectionToken('BRIDGE_DEFAULTS');
|
|
22
|
+
|
|
23
|
+
const DEFAULT_MEDIA_TYPES = {
|
|
24
|
+
accept: 'application/ld+json',
|
|
25
|
+
post: 'application/ld+json',
|
|
26
|
+
put: 'application/ld+json',
|
|
27
|
+
patch: 'application/merge-patch+json',
|
|
28
|
+
};
|
|
29
|
+
const contentTypeInterceptor = (req, next) => {
|
|
30
|
+
const mediaTypes = DEFAULT_MEDIA_TYPES;
|
|
31
|
+
let headers = req.headers;
|
|
32
|
+
// Set Accept if missing.
|
|
33
|
+
if (!headers.has('Accept')) {
|
|
34
|
+
headers = headers.set('Accept', mediaTypes.accept);
|
|
35
|
+
}
|
|
36
|
+
// Only methods with a body.
|
|
37
|
+
const isBodyMethod = ['POST', 'PUT', 'PATCH'].includes(req.method);
|
|
38
|
+
// Ignore FormData (browser sets multipart boundary).
|
|
39
|
+
const isFormData = typeof FormData !== 'undefined' && req.body instanceof FormData;
|
|
40
|
+
if (isBodyMethod && !isFormData && !headers.has('Content-Type')) {
|
|
41
|
+
let contentType;
|
|
42
|
+
switch (req.method) {
|
|
43
|
+
case 'PATCH':
|
|
44
|
+
contentType = mediaTypes.patch;
|
|
45
|
+
break;
|
|
46
|
+
case 'PUT':
|
|
47
|
+
contentType = mediaTypes.put;
|
|
48
|
+
break;
|
|
49
|
+
default:
|
|
50
|
+
contentType = mediaTypes.post;
|
|
51
|
+
}
|
|
52
|
+
headers = headers.set('Content-Type', contentType);
|
|
53
|
+
}
|
|
54
|
+
return next(req.clone({ headers }));
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const DEFAULT_RETRY_METHODS = ['GET', 'HEAD', 'OPTIONS'];
|
|
58
|
+
/**
|
|
59
|
+
* Applies default headers, timeout and retry policy configured via `provideBridge({defaults: ...})`.
|
|
60
|
+
*/
|
|
61
|
+
const bridgeDefaultsInterceptor = (req, next) => {
|
|
62
|
+
const defaults = inject(BRIDGE_DEFAULTS, { optional: true }) ?? {};
|
|
63
|
+
const logger = inject(BRIDGE_LOGGER, { optional: true });
|
|
64
|
+
let nextReq = req;
|
|
65
|
+
if (defaults.headers) {
|
|
66
|
+
for (const [k, v] of Object.entries(defaults.headers)) {
|
|
67
|
+
if (!nextReq.headers.has(k)) {
|
|
68
|
+
nextReq = nextReq.clone({ headers: nextReq.headers.set(k, v) });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
let out$ = next(nextReq);
|
|
73
|
+
const timeoutMs = typeof defaults.timeoutMs === 'number' ? defaults.timeoutMs : undefined;
|
|
74
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
75
|
+
out$ = out$.pipe(timeout({ first: timeoutMs }));
|
|
76
|
+
}
|
|
77
|
+
const retryCfg = defaults.retries;
|
|
78
|
+
const retryCount = typeof retryCfg === 'number' ? retryCfg : retryCfg?.count;
|
|
79
|
+
if (retryCount && retryCount > 0) {
|
|
80
|
+
const methods = (typeof retryCfg === 'object' && retryCfg.methods) ? retryCfg.methods : DEFAULT_RETRY_METHODS;
|
|
81
|
+
const normalizedMethod = req.method.toUpperCase();
|
|
82
|
+
const methodAllowList = new Set(methods.map((m) => m.toUpperCase()));
|
|
83
|
+
if (methodAllowList.has(normalizedMethod)) {
|
|
84
|
+
const delayMs = typeof retryCfg === 'object' ? (retryCfg.delayMs ?? 250) : 250;
|
|
85
|
+
out$ = out$.pipe(retry({
|
|
86
|
+
count: retryCount,
|
|
87
|
+
delay: (_err, retryIndex) => {
|
|
88
|
+
logger?.debug?.('[Bridge] retry', { url: req.urlWithParams, method: req.method, retryIndex });
|
|
89
|
+
return timer(delayMs);
|
|
90
|
+
},
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return out$;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Lightweight request/response logging controlled by `provideBridge({debug: true})`.
|
|
99
|
+
* Logs are delegated to the injected `BRIDGE_LOGGER`.
|
|
100
|
+
*/
|
|
101
|
+
const bridgeDebugInterceptor = (req, next) => {
|
|
102
|
+
const logger = inject(BRIDGE_LOGGER, { optional: true });
|
|
103
|
+
if (!logger)
|
|
104
|
+
return next(req);
|
|
105
|
+
const startedAt = Date.now();
|
|
106
|
+
logger.debug('[Bridge] request', { method: req.method, url: req.urlWithParams });
|
|
107
|
+
return next(req).pipe(tap((evt) => {
|
|
108
|
+
if (evt instanceof HttpResponse) {
|
|
109
|
+
logger.debug('[Bridge] response', { method: req.method, url: req.urlWithParams, status: evt.status });
|
|
110
|
+
}
|
|
111
|
+
}), catchError((err) => {
|
|
112
|
+
logger.error('[Bridge] error', { method: req.method, url: req.urlWithParams, err });
|
|
113
|
+
return throwError(() => err);
|
|
114
|
+
}), finalize(() => {
|
|
115
|
+
const durationMs = Date.now() - startedAt;
|
|
116
|
+
logger.debug('[Bridge] done', { method: req.method, url: req.urlWithParams, durationMs });
|
|
117
|
+
}));
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const DEFAULT_MODE = 'safe';
|
|
121
|
+
const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
|
|
122
|
+
function createSingleFlightInterceptor(mode = DEFAULT_MODE) {
|
|
123
|
+
const inflight = new Map();
|
|
124
|
+
return (req, next) => {
|
|
125
|
+
if (mode === 'off') {
|
|
126
|
+
return next(req);
|
|
127
|
+
}
|
|
128
|
+
const method = req.method.toUpperCase();
|
|
129
|
+
if (!SAFE_METHODS.has(method)) {
|
|
130
|
+
return next(req);
|
|
131
|
+
}
|
|
132
|
+
if (req.reportProgress === true) {
|
|
133
|
+
return next(req);
|
|
134
|
+
}
|
|
135
|
+
const key = computeKey(req);
|
|
136
|
+
const existing = inflight.get(key);
|
|
137
|
+
if (existing) {
|
|
138
|
+
return existing;
|
|
139
|
+
}
|
|
140
|
+
const shared$ = next(req).pipe(finalize(() => inflight.delete(key)), shareReplay({ bufferSize: 1, refCount: true }));
|
|
141
|
+
inflight.set(key, shared$);
|
|
142
|
+
return shared$;
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function computeKey(req) {
|
|
146
|
+
const auth = req.headers.get('Authorization') ?? '';
|
|
147
|
+
const accept = req.headers.get('Accept') ?? '';
|
|
148
|
+
const contentType = req.headers.get('Content-Type') ?? '';
|
|
149
|
+
const creds = req.withCredentials ? '1' : '0';
|
|
150
|
+
return [
|
|
151
|
+
req.method.toUpperCase(),
|
|
152
|
+
req.urlWithParams,
|
|
153
|
+
`rt=${req.responseType}`,
|
|
154
|
+
`wc=${creds}`,
|
|
155
|
+
`a=${auth}`,
|
|
156
|
+
`acc=${accept}`,
|
|
157
|
+
`ct=${contentType}`,
|
|
158
|
+
].join('::');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Registers the bridge HTTP client, interceptors, Mercure realtime adapter and configuration tokens. */
|
|
162
|
+
function provideBridge(opts) {
|
|
163
|
+
const { baseUrl, auth, mercure, defaults, singleFlight = true, debug = false, extraInterceptors = [], } = opts;
|
|
164
|
+
if (!baseUrl) {
|
|
165
|
+
throw new Error("provideBridge(): missing 'baseUrl'");
|
|
166
|
+
}
|
|
167
|
+
const resolvedMercureInit = mercure?.init ?? { credentials: 'include' };
|
|
168
|
+
const resolvedMercureHubUrl = mercure?.hubUrl;
|
|
169
|
+
const resolvedMercureTopicMode = mercure?.topicMode ?? 'url';
|
|
170
|
+
const withCredentials = resolveWithCredentials(resolvedMercureInit);
|
|
171
|
+
const loggerProvider = createBridgeLogger(debug);
|
|
172
|
+
const interceptors = [
|
|
173
|
+
contentTypeInterceptor,
|
|
174
|
+
bridgeDefaultsInterceptor,
|
|
175
|
+
...createAuthInterceptors(auth),
|
|
176
|
+
...(singleFlight ? [createSingleFlightInterceptor('safe')] : [createSingleFlightInterceptor('off')]),
|
|
177
|
+
...(debug ? [bridgeDebugInterceptor] : []),
|
|
178
|
+
...extraInterceptors,
|
|
179
|
+
];
|
|
180
|
+
return makeEnvironmentProviders([
|
|
181
|
+
provideHttpClient(withFetch(), withInterceptors(interceptors)),
|
|
182
|
+
{ provide: API_BASE_URL, useValue: baseUrl },
|
|
183
|
+
{ provide: BRIDGE_WITH_CREDENTIALS, useValue: withCredentials },
|
|
184
|
+
...(resolvedMercureHubUrl ? [{ provide: MERCURE_HUB_URL, useValue: resolvedMercureHubUrl }] : []),
|
|
185
|
+
{ provide: MERCURE_TOPIC_MODE, useValue: resolvedMercureTopicMode },
|
|
186
|
+
{ provide: BRIDGE_DEFAULTS, useValue: defaults ?? {} },
|
|
187
|
+
{ provide: BRIDGE_LOGGER, useValue: loggerProvider },
|
|
188
|
+
]);
|
|
189
|
+
}
|
|
190
|
+
function createBridgeLogger(debug) {
|
|
191
|
+
const noop = () => undefined;
|
|
192
|
+
return {
|
|
193
|
+
debug: debug ? console.debug.bind(console) : noop,
|
|
194
|
+
info: debug ? console.info.bind(console) : noop,
|
|
195
|
+
warn: debug ? console.warn.bind(console) : noop,
|
|
196
|
+
error: console.error.bind(console),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function resolveWithCredentials(init) {
|
|
200
|
+
if (!init)
|
|
201
|
+
return false;
|
|
202
|
+
const anyInit = init;
|
|
203
|
+
return anyInit.withCredentials === true || init.credentials === 'include';
|
|
204
|
+
}
|
|
205
|
+
function createAuthInterceptors(auth) {
|
|
206
|
+
if (!auth)
|
|
207
|
+
return [];
|
|
208
|
+
if (typeof auth === 'function') {
|
|
209
|
+
return [auth];
|
|
210
|
+
}
|
|
211
|
+
const bearer = typeof auth === 'string'
|
|
212
|
+
? { type: 'bearer', token: auth }
|
|
213
|
+
: auth;
|
|
214
|
+
if (bearer.type !== 'bearer')
|
|
215
|
+
return [];
|
|
216
|
+
if ('token' in bearer) {
|
|
217
|
+
const token = bearer.token;
|
|
218
|
+
return [createBearerAuthInterceptor(() => token)];
|
|
219
|
+
}
|
|
220
|
+
return [createBearerAuthInterceptor(bearer.getToken)];
|
|
221
|
+
}
|
|
222
|
+
function createBearerAuthInterceptor(getToken) {
|
|
223
|
+
return (req, next) => {
|
|
224
|
+
if (req.headers.has('Authorization'))
|
|
225
|
+
return next(req);
|
|
226
|
+
return from(Promise.resolve(getToken())).pipe(switchMap((token) => {
|
|
227
|
+
if (!token)
|
|
228
|
+
return next(req);
|
|
229
|
+
return next(req.clone({ headers: req.headers.set('Authorization', `Bearer ${token}`) }));
|
|
230
|
+
}));
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Public configuration types for the bridge.
|
|
236
|
+
*
|
|
237
|
+
* Keep this file dependency-free (no Angular imports) so it can be used from both
|
|
238
|
+
* runtime code and type-only contexts.
|
|
239
|
+
*/
|
|
240
|
+
|
|
241
|
+
class EventSourceWrapper {
|
|
242
|
+
url;
|
|
243
|
+
opts;
|
|
244
|
+
logger;
|
|
245
|
+
es;
|
|
246
|
+
statusSub = new ReplaySubject(1);
|
|
247
|
+
eventSub = new Subject();
|
|
248
|
+
status$ = this.statusSub.asObservable();
|
|
249
|
+
events$ = this.eventSub.asObservable();
|
|
250
|
+
constructor(url, opts = {}, logger) {
|
|
251
|
+
this.url = url;
|
|
252
|
+
this.opts = opts;
|
|
253
|
+
this.logger = logger;
|
|
254
|
+
this.setState('closed');
|
|
255
|
+
this.log('[SSE] init', { url, withCredentials: !!opts.withCredentials });
|
|
256
|
+
}
|
|
257
|
+
open() {
|
|
258
|
+
if (this.es) {
|
|
259
|
+
this.log('[SSE] open() ignored: already open');
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
this.setState('connecting');
|
|
263
|
+
this.log('[SSE] open', { url: this.url });
|
|
264
|
+
const es = new EventSource(this.url, {
|
|
265
|
+
withCredentials: !!this.opts.withCredentials,
|
|
266
|
+
});
|
|
267
|
+
this.es = es;
|
|
268
|
+
es.onopen = () => {
|
|
269
|
+
this.setState('connected');
|
|
270
|
+
};
|
|
271
|
+
es.onmessage = (ev) => {
|
|
272
|
+
this.eventSub.next({ type: 'message', data: ev.data, lastEventId: ev.lastEventId || undefined });
|
|
273
|
+
};
|
|
274
|
+
es.onerror = () => {
|
|
275
|
+
// The browser will retry automatically. We stay in "connecting".
|
|
276
|
+
this.log('[SSE] error');
|
|
277
|
+
this.setState('connecting');
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
close() {
|
|
281
|
+
if (this.es) {
|
|
282
|
+
this.es.close();
|
|
283
|
+
this.es = undefined;
|
|
284
|
+
this.log('[SSE] closed');
|
|
285
|
+
}
|
|
286
|
+
this.setState('closed');
|
|
287
|
+
}
|
|
288
|
+
// ──────────────── internals ────────────────
|
|
289
|
+
setState(state) {
|
|
290
|
+
this.statusSub.next(state);
|
|
291
|
+
}
|
|
292
|
+
log(...args) {
|
|
293
|
+
this.logger?.debug?.(...args);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
class MercureUrlBuilder {
|
|
298
|
+
/**
|
|
299
|
+
* Builds the Mercure hub URL with one `topic=` parameter per topic.
|
|
300
|
+
* The adapter is responsible for canonicalising topics beforehand.
|
|
301
|
+
*/
|
|
302
|
+
build(hubUrl, topics, lastEventId) {
|
|
303
|
+
const url = new URL(hubUrl);
|
|
304
|
+
if (lastEventId) {
|
|
305
|
+
url.searchParams.set('lastEventID', lastEventId);
|
|
306
|
+
}
|
|
307
|
+
url.searchParams.delete('topic');
|
|
308
|
+
for (const topic of topics) {
|
|
309
|
+
url.searchParams.append('topic', topic);
|
|
310
|
+
}
|
|
311
|
+
return url.toString();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
class RefCountTopicRegistry {
|
|
316
|
+
topics = new Set();
|
|
317
|
+
refCounts = new Map();
|
|
318
|
+
/**
|
|
319
|
+
* Increments ref-count for each topic.
|
|
320
|
+
* Callers should pass unique topic strings (deduped).
|
|
321
|
+
*/
|
|
322
|
+
addAll(topics) {
|
|
323
|
+
for (const topic of topics) {
|
|
324
|
+
const next = (this.refCounts.get(topic) ?? 0) + 1;
|
|
325
|
+
this.refCounts.set(topic, next);
|
|
326
|
+
this.topics.add(topic);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Decrements ref-count for each topic.
|
|
331
|
+
* Callers should pass unique topic strings (deduped).
|
|
332
|
+
*/
|
|
333
|
+
removeAll(topics) {
|
|
334
|
+
for (const topic of topics) {
|
|
335
|
+
const next = (this.refCounts.get(topic) ?? 0) - 1;
|
|
336
|
+
if (next <= 0) {
|
|
337
|
+
this.refCounts.delete(topic);
|
|
338
|
+
this.topics.delete(topic);
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
this.refCounts.set(topic, next);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
hasAny() {
|
|
346
|
+
return this.topics.size > 0;
|
|
347
|
+
}
|
|
348
|
+
snapshot() {
|
|
349
|
+
return new Set(this.topics);
|
|
350
|
+
}
|
|
351
|
+
computeKey(hubUrl, credentialsOn) {
|
|
352
|
+
const topicsSorted = Array.from(this.topics).sort().join('|');
|
|
353
|
+
return `${hubUrl}::${credentialsOn ? '1' : '0'}::${topicsSorted}`;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
class MercureTopicMapper {
|
|
358
|
+
mode;
|
|
359
|
+
apiBaseUrl;
|
|
360
|
+
constructor(apiBase, mode) {
|
|
361
|
+
this.mode = mode;
|
|
362
|
+
this.apiBaseUrl = new URL(apiBase);
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Canonical value used for ref-counting and as the "topic" query param value.
|
|
366
|
+
* - mode "url": always absolute, same-origin resolved
|
|
367
|
+
* - mode "iri": same-origin path+query+hash ("/api/..."), otherwise keep as-is
|
|
368
|
+
*/
|
|
369
|
+
toTopic(input) {
|
|
370
|
+
if (this.mode === 'url')
|
|
371
|
+
return this.toAbsoluteUrl(input);
|
|
372
|
+
return this.toRelativeIriIfSameOrigin(input);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Canonical value used to compare incoming payload IRIs with subscribed IRIs.
|
|
376
|
+
* We keep payload matching stable by using same-origin relative IRIs ("/api/...").
|
|
377
|
+
*/
|
|
378
|
+
toPayloadIri(input) {
|
|
379
|
+
return this.toRelativeIriIfSameOrigin(input);
|
|
380
|
+
}
|
|
381
|
+
toAbsoluteUrl(input) {
|
|
382
|
+
try {
|
|
383
|
+
return new URL(input, this.apiBaseUrl).toString();
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
return input;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
toRelativeIriIfSameOrigin(input) {
|
|
390
|
+
try {
|
|
391
|
+
const url = new URL(input, this.apiBaseUrl);
|
|
392
|
+
if (url.origin !== this.apiBaseUrl.origin)
|
|
393
|
+
return input;
|
|
394
|
+
return `${url.pathname}${url.search}${url.hash}`;
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
return input;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
class MercureRealtimeAdapter {
|
|
403
|
+
apiBase;
|
|
404
|
+
withCredentialsDefault;
|
|
405
|
+
platformId;
|
|
406
|
+
hubUrl;
|
|
407
|
+
logger;
|
|
408
|
+
lastEventId;
|
|
409
|
+
es;
|
|
410
|
+
currentKey;
|
|
411
|
+
topicsRegistry = new RefCountTopicRegistry();
|
|
412
|
+
urlBuilder;
|
|
413
|
+
topicMapper;
|
|
414
|
+
destroy$ = new Subject();
|
|
415
|
+
connectionStop$ = new Subject();
|
|
416
|
+
rebuild$ = new Subject();
|
|
417
|
+
shuttingDown = false;
|
|
418
|
+
_status$ = new BehaviorSubject('closed');
|
|
419
|
+
incoming$ = new Subject();
|
|
420
|
+
constructor(apiBase, withCredentialsDefault, platformId, hubUrl, topicMode, logger) {
|
|
421
|
+
this.apiBase = apiBase;
|
|
422
|
+
this.withCredentialsDefault = withCredentialsDefault;
|
|
423
|
+
this.platformId = platformId;
|
|
424
|
+
this.hubUrl = hubUrl;
|
|
425
|
+
this.logger = logger;
|
|
426
|
+
this.urlBuilder = new MercureUrlBuilder();
|
|
427
|
+
// `topicMode` affects only the `topic=` query param sent to the hub.
|
|
428
|
+
// Payload IRIs are always matched using same-origin relative IRIs (`/api/...`) when possible.
|
|
429
|
+
this.topicMapper = new MercureTopicMapper(apiBase, topicMode ?? 'url');
|
|
430
|
+
this.rebuild$
|
|
431
|
+
.pipe(auditTime(10), concatMap(() => this.rebuildOnce$()))
|
|
432
|
+
.subscribe();
|
|
433
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
434
|
+
fromEvent(window, 'pagehide')
|
|
435
|
+
.pipe(takeUntil(this.destroy$))
|
|
436
|
+
.subscribe(() => this.shutdownBeforeExit());
|
|
437
|
+
fromEvent(window, 'beforeunload')
|
|
438
|
+
.pipe(takeUntil(this.destroy$))
|
|
439
|
+
.subscribe(() => this.shutdownBeforeExit());
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// ──────────────── API publique ────────────────
|
|
443
|
+
status$() {
|
|
444
|
+
return this._status$.asObservable();
|
|
445
|
+
}
|
|
446
|
+
subscribe$(iris, _filter) {
|
|
447
|
+
return defer(() => {
|
|
448
|
+
const inputIris = iris.filter((v) => typeof v === 'string' && v.length > 0);
|
|
449
|
+
if (inputIris.length === 0)
|
|
450
|
+
return EMPTY;
|
|
451
|
+
if (!this.hubUrl) {
|
|
452
|
+
this.logger?.debug?.('[Mercure] hubUrl not configured → realtime disabled');
|
|
453
|
+
return EMPTY;
|
|
454
|
+
}
|
|
455
|
+
// Canonicalise topics (ref-count + URL) to avoid duplicates like:
|
|
456
|
+
// - "/api/conversations/1" and "http://localhost:8000/api/conversations/1"
|
|
457
|
+
const registeredTopics = Array.from(new Set(inputIris.map((i) => this.topicMapper.toTopic(i))));
|
|
458
|
+
this.topicsRegistry.addAll(registeredTopics);
|
|
459
|
+
this.scheduleRebuild();
|
|
460
|
+
// Matching is done against the payload IRIs (typically "/api/...").
|
|
461
|
+
const subscribed = inputIris.map((i) => normalizeIri(this.topicMapper.toPayloadIri(i)));
|
|
462
|
+
const fieldPath = _filter?.field;
|
|
463
|
+
return this.incoming$.pipe(map((evt) => this.safeParse(evt.data)), filter((raw) => !!raw), filter((raw) => {
|
|
464
|
+
if (fieldPath) {
|
|
465
|
+
const relIris = this.extractRelationIris(raw, fieldPath).map((i) => normalizeIri(this.topicMapper.toPayloadIri(i)));
|
|
466
|
+
return relIris.some((relIri) => matchesAnySubscribed(relIri, subscribed));
|
|
467
|
+
}
|
|
468
|
+
const rawId = raw?.['@id'];
|
|
469
|
+
const id = typeof rawId === 'string' ? normalizeIri(this.topicMapper.toPayloadIri(rawId)) : undefined;
|
|
470
|
+
return typeof id === 'string' && matchesAnySubscribed(id, subscribed);
|
|
471
|
+
}), map((payload) => ({ iri: payload['@id'], data: payload })), finalize$1(() => {
|
|
472
|
+
this.topicsRegistry.removeAll(registeredTopics);
|
|
473
|
+
this.scheduleRebuild();
|
|
474
|
+
}), share());
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
unsubscribe(iris) {
|
|
478
|
+
const inputIris = iris.filter((v) => typeof v === 'string' && v.length > 0);
|
|
479
|
+
if (inputIris.length === 0)
|
|
480
|
+
return;
|
|
481
|
+
const topics = Array.from(new Set(inputIris.map((i) => this.topicMapper.toTopic(i))));
|
|
482
|
+
this.topicsRegistry.removeAll(topics);
|
|
483
|
+
this.scheduleRebuild();
|
|
484
|
+
}
|
|
485
|
+
shutdownBeforeExit() {
|
|
486
|
+
if (this.shuttingDown)
|
|
487
|
+
return;
|
|
488
|
+
this.shuttingDown = true;
|
|
489
|
+
this.teardownConnection();
|
|
490
|
+
}
|
|
491
|
+
ngOnDestroy() {
|
|
492
|
+
this.teardownConnection();
|
|
493
|
+
this._status$.complete();
|
|
494
|
+
this.incoming$.complete();
|
|
495
|
+
this.destroy$.next();
|
|
496
|
+
this.destroy$.complete();
|
|
497
|
+
this.rebuild$.complete();
|
|
498
|
+
}
|
|
499
|
+
// ───────────────── PRIVATE ─────────────────
|
|
500
|
+
scheduleRebuild() {
|
|
501
|
+
if (this.shuttingDown)
|
|
502
|
+
return;
|
|
503
|
+
this.rebuild$.next();
|
|
504
|
+
}
|
|
505
|
+
rebuildOnce$() {
|
|
506
|
+
return defer(() => {
|
|
507
|
+
if (this.shuttingDown)
|
|
508
|
+
return of(void 0);
|
|
509
|
+
try {
|
|
510
|
+
if (!this.hubUrl) {
|
|
511
|
+
this.currentKey = undefined;
|
|
512
|
+
this._status$.next('closed');
|
|
513
|
+
return of(void 0);
|
|
514
|
+
}
|
|
515
|
+
const hasTopics = this.topicsRegistry.hasAny();
|
|
516
|
+
const key = hasTopics ? this.topicsRegistry.computeKey(this.hubUrl, this.withCredentialsDefault) : undefined;
|
|
517
|
+
if (!hasTopics) {
|
|
518
|
+
if (this.es)
|
|
519
|
+
this.teardownConnection();
|
|
520
|
+
this.currentKey = undefined;
|
|
521
|
+
this._status$.next('closed');
|
|
522
|
+
return of(void 0);
|
|
523
|
+
}
|
|
524
|
+
if (key && key === this.currentKey) {
|
|
525
|
+
return of(void 0);
|
|
526
|
+
}
|
|
527
|
+
this.teardownConnection();
|
|
528
|
+
const url = this.urlBuilder.build(this.hubUrl, this.topicsRegistry.snapshot(), this.lastEventId);
|
|
529
|
+
this.logger?.debug?.('[Mercure] connect', { hubUrl: this.hubUrl, topics: this.topicsRegistry.snapshot(), lastEventId: this.lastEventId });
|
|
530
|
+
this.es = new EventSourceWrapper(url, { withCredentials: this.withCredentialsDefault }, this.logger);
|
|
531
|
+
this.connectionStop$ = new Subject();
|
|
532
|
+
this.es.status$
|
|
533
|
+
.pipe(takeUntil(this.connectionStop$), takeUntil(this.destroy$))
|
|
534
|
+
.subscribe((st) => this.updateGlobalStatus(st));
|
|
535
|
+
this.es.events$
|
|
536
|
+
.pipe(takeUntil(this.connectionStop$), takeUntil(this.destroy$))
|
|
537
|
+
.subscribe((e) => {
|
|
538
|
+
this.lastEventId = e.lastEventId ?? this.lastEventId;
|
|
539
|
+
this.incoming$.next(e);
|
|
540
|
+
});
|
|
541
|
+
this._status$.next('connecting');
|
|
542
|
+
this.es.open();
|
|
543
|
+
this.currentKey = key;
|
|
544
|
+
}
|
|
545
|
+
catch (err) {
|
|
546
|
+
this.logger?.error?.('[Mercure] rebuild failed:', err);
|
|
547
|
+
this.currentKey = undefined;
|
|
548
|
+
this._status$.next(this.topicsRegistry.hasAny() ? 'connecting' : 'closed');
|
|
549
|
+
}
|
|
550
|
+
return of(void 0);
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
teardownConnection() {
|
|
554
|
+
this.es?.close();
|
|
555
|
+
this.es = undefined;
|
|
556
|
+
this.connectionStop$.next();
|
|
557
|
+
this.connectionStop$.complete();
|
|
558
|
+
}
|
|
559
|
+
updateGlobalStatus(sse) {
|
|
560
|
+
if (sse === 'connected') {
|
|
561
|
+
this._status$.next('connected');
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (sse === 'connecting') {
|
|
565
|
+
this._status$.next('connecting');
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
this._status$.next(this.topicsRegistry.hasAny() ? 'connecting' : 'closed');
|
|
569
|
+
}
|
|
570
|
+
safeParse(raw) {
|
|
571
|
+
try {
|
|
572
|
+
return JSON.parse(raw);
|
|
573
|
+
}
|
|
574
|
+
catch (err) {
|
|
575
|
+
this.logger?.debug?.('[Mercure] invalid JSON payload ignored', { raw });
|
|
576
|
+
return undefined;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
extractRelationIris(raw, path) {
|
|
580
|
+
const readPath = (obj, dotPath) => {
|
|
581
|
+
return dotPath
|
|
582
|
+
.split('.')
|
|
583
|
+
.filter(Boolean)
|
|
584
|
+
.reduce((acc, key) => acc?.[key], obj);
|
|
585
|
+
};
|
|
586
|
+
const normalize = (value) => {
|
|
587
|
+
if (!value)
|
|
588
|
+
return [];
|
|
589
|
+
if (typeof value === 'string')
|
|
590
|
+
return value.length > 0 ? [value] : [];
|
|
591
|
+
if (typeof value === 'object' && typeof value['@id'] === 'string')
|
|
592
|
+
return [value['@id']];
|
|
593
|
+
if (Array.isArray(value))
|
|
594
|
+
return value.flatMap(normalize);
|
|
595
|
+
return [];
|
|
596
|
+
};
|
|
597
|
+
return normalize(readPath(raw, path));
|
|
598
|
+
}
|
|
599
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: MercureRealtimeAdapter, deps: [{ token: API_BASE_URL }, { token: BRIDGE_WITH_CREDENTIALS }, { token: PLATFORM_ID }, { token: MERCURE_HUB_URL, optional: true }, { token: MERCURE_TOPIC_MODE, optional: true }, { token: BRIDGE_LOGGER, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
600
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: MercureRealtimeAdapter, providedIn: 'root' });
|
|
601
|
+
}
|
|
602
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: MercureRealtimeAdapter, decorators: [{
|
|
603
|
+
type: Injectable,
|
|
604
|
+
args: [{ providedIn: 'root' }]
|
|
605
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
606
|
+
type: Inject,
|
|
607
|
+
args: [API_BASE_URL]
|
|
608
|
+
}] }, { type: undefined, decorators: [{
|
|
609
|
+
type: Inject,
|
|
610
|
+
args: [BRIDGE_WITH_CREDENTIALS]
|
|
611
|
+
}] }, { type: undefined, decorators: [{
|
|
612
|
+
type: Inject,
|
|
613
|
+
args: [PLATFORM_ID]
|
|
614
|
+
}] }, { type: undefined, decorators: [{
|
|
615
|
+
type: Optional
|
|
616
|
+
}, {
|
|
617
|
+
type: Inject,
|
|
618
|
+
args: [MERCURE_HUB_URL]
|
|
619
|
+
}] }, { type: undefined, decorators: [{
|
|
620
|
+
type: Optional
|
|
621
|
+
}, {
|
|
622
|
+
type: Inject,
|
|
623
|
+
args: [MERCURE_TOPIC_MODE]
|
|
624
|
+
}] }, { type: undefined, decorators: [{
|
|
625
|
+
type: Optional
|
|
626
|
+
}, {
|
|
627
|
+
type: Inject,
|
|
628
|
+
args: [BRIDGE_LOGGER]
|
|
629
|
+
}] }] });
|
|
630
|
+
function normalizeIri(iri) {
|
|
631
|
+
return iri.endsWith('/') ? iri.slice(0, -1) : iri;
|
|
632
|
+
}
|
|
633
|
+
function matchesAnySubscribed(candidate, subscribed) {
|
|
634
|
+
for (const iri of subscribed) {
|
|
635
|
+
if (candidate === iri)
|
|
636
|
+
return true;
|
|
637
|
+
if (candidate.startsWith(`${iri}/`))
|
|
638
|
+
return true;
|
|
639
|
+
}
|
|
640
|
+
return false;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function toHttpParams(q) {
|
|
644
|
+
if (!q)
|
|
645
|
+
return new HttpParams();
|
|
646
|
+
if (q instanceof HttpParams)
|
|
647
|
+
return q;
|
|
648
|
+
const fromObject = {};
|
|
649
|
+
const consumed = new Set();
|
|
650
|
+
const maybeQuery = q;
|
|
651
|
+
if (maybeQuery.page != null) {
|
|
652
|
+
fromObject['page'] = String(maybeQuery.page);
|
|
653
|
+
consumed.add('page');
|
|
654
|
+
}
|
|
655
|
+
if (maybeQuery.itemsPerPage != null) {
|
|
656
|
+
fromObject['itemsPerPage'] = String(maybeQuery.itemsPerPage);
|
|
657
|
+
consumed.add('itemsPerPage');
|
|
658
|
+
}
|
|
659
|
+
if (q.filters) {
|
|
660
|
+
consumed.add('filters');
|
|
661
|
+
for (const [k, v] of Object.entries(q.filters)) {
|
|
662
|
+
assign(fromObject, k, v);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
for (const [k, v] of Object.entries(q)) {
|
|
666
|
+
if (consumed.has(k))
|
|
667
|
+
continue;
|
|
668
|
+
assign(fromObject, k, v);
|
|
669
|
+
}
|
|
670
|
+
return new HttpParams({ fromObject });
|
|
671
|
+
}
|
|
672
|
+
function assign(target, key, value) {
|
|
673
|
+
if (value == null)
|
|
674
|
+
return;
|
|
675
|
+
if (Array.isArray(value)) {
|
|
676
|
+
target[key] = value.map(String);
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
target[key] = String(value);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function buildHttpRequestOptions(req, { withCredentialsDefault }) {
|
|
684
|
+
const { query, body, headers, responseType, withCredentials, options = {} } = req;
|
|
685
|
+
const mergedOptions = { ...options };
|
|
686
|
+
if (headers)
|
|
687
|
+
mergedOptions['headers'] = headers;
|
|
688
|
+
if (query)
|
|
689
|
+
mergedOptions['params'] = toHttpParams(query);
|
|
690
|
+
if (body !== undefined)
|
|
691
|
+
mergedOptions['body'] = body;
|
|
692
|
+
mergedOptions['responseType'] = (responseType ?? mergedOptions['responseType'] ?? 'json');
|
|
693
|
+
mergedOptions['withCredentials'] =
|
|
694
|
+
withCredentials ?? mergedOptions['withCredentials'] ?? withCredentialsDefault;
|
|
695
|
+
mergedOptions['observe'] = 'body';
|
|
696
|
+
return mergedOptions;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function joinUrl(base, path) {
|
|
700
|
+
const normalizedBase = base.endsWith('/') ? base.slice(0, -1) : base;
|
|
701
|
+
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
702
|
+
return `${normalizedBase}${normalizedPath}`;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Resolves an API IRI (e.g. `/api/books/1`) to a full URL using the API base.
|
|
706
|
+
* If `path` is already absolute (http/https), it is returned as-is.
|
|
707
|
+
*/
|
|
708
|
+
function resolveUrl(base, path) {
|
|
709
|
+
if (/^https?:\/\//i.test(path))
|
|
710
|
+
return path;
|
|
711
|
+
if (path.startsWith('//'))
|
|
712
|
+
return path;
|
|
713
|
+
return joinUrl(base, path);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
class ApiPlatformRestRepository {
|
|
717
|
+
http;
|
|
718
|
+
apiBase;
|
|
719
|
+
resourcePath;
|
|
720
|
+
withCredentialsDefault;
|
|
721
|
+
constructor(http, apiBase, resourcePath, withCredentialsDefault) {
|
|
722
|
+
this.http = http;
|
|
723
|
+
this.apiBase = apiBase;
|
|
724
|
+
this.resourcePath = resourcePath;
|
|
725
|
+
this.withCredentialsDefault = withCredentialsDefault;
|
|
726
|
+
}
|
|
727
|
+
getCollection$(query, opts) {
|
|
728
|
+
const params = toHttpParams(query);
|
|
729
|
+
return this.http.get(this.resolveUrl(this.resourcePath), {
|
|
730
|
+
params,
|
|
731
|
+
headers: opts?.headers,
|
|
732
|
+
withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
get$(iri, opts) {
|
|
736
|
+
return this.http.get(this.resolveUrl(iri), {
|
|
737
|
+
headers: opts?.headers,
|
|
738
|
+
withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
post$(payload, opts) {
|
|
742
|
+
return this.http.post(this.resolveUrl(this.resourcePath), payload, {
|
|
743
|
+
headers: opts?.headers,
|
|
744
|
+
withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
patch$(iri, changes, opts) {
|
|
748
|
+
return this.http.patch(this.resolveUrl(iri), changes, {
|
|
749
|
+
headers: opts?.headers,
|
|
750
|
+
withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
put$(iri, payload, opts) {
|
|
754
|
+
return this.http.put(this.resolveUrl(iri), payload, {
|
|
755
|
+
headers: opts?.headers,
|
|
756
|
+
withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
delete$(iri, opts) {
|
|
760
|
+
return this.http.delete(this.resolveUrl(iri), {
|
|
761
|
+
headers: opts?.headers,
|
|
762
|
+
withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
request$(req) {
|
|
766
|
+
// Low-level escape hatch for non-standard endpoints (custom controllers, uploads, etc.).
|
|
767
|
+
const { method, url } = req;
|
|
768
|
+
const targetUrl = this.resolveUrl(url ?? this.resourcePath);
|
|
769
|
+
const mergedOptions = buildHttpRequestOptions(req, { withCredentialsDefault: this.withCredentialsDefault });
|
|
770
|
+
return this.http.request(method, targetUrl, mergedOptions);
|
|
771
|
+
}
|
|
772
|
+
resolveUrl(path) {
|
|
773
|
+
const effectivePath = path ?? this.resourcePath;
|
|
774
|
+
if (!effectivePath)
|
|
775
|
+
throw new Error('ApiPlatformRestRepository: missing url and resourcePath');
|
|
776
|
+
return resolveUrl(this.apiBase, effectivePath);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
class ResourceFacade {
|
|
781
|
+
repo;
|
|
782
|
+
realtime;
|
|
783
|
+
connectionStatus;
|
|
784
|
+
constructor(repo, realtime) {
|
|
785
|
+
this.repo = repo;
|
|
786
|
+
this.realtime = realtime;
|
|
787
|
+
this.connectionStatus = toSignal(this.realtime.status$(), { initialValue: 'closed' });
|
|
788
|
+
}
|
|
789
|
+
getCollection$(query, opts) {
|
|
790
|
+
return this.repo.getCollection$(query, opts);
|
|
791
|
+
}
|
|
792
|
+
get$(iri, opts) {
|
|
793
|
+
return this.repo.get$(iri, opts);
|
|
794
|
+
}
|
|
795
|
+
patch$(iri, changes, opts) {
|
|
796
|
+
return this.repo.patch$(iri, changes, opts);
|
|
797
|
+
}
|
|
798
|
+
post$(payload, opts) {
|
|
799
|
+
return this.repo.post$(payload, opts);
|
|
800
|
+
}
|
|
801
|
+
put$(iri, payload, opts) {
|
|
802
|
+
return this.repo.put$(iri, payload, opts);
|
|
803
|
+
}
|
|
804
|
+
delete$(iri, opts) {
|
|
805
|
+
return this.repo.delete$(iri, opts);
|
|
806
|
+
}
|
|
807
|
+
request$(req) {
|
|
808
|
+
return this.repo.request$(req);
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Subscribes to real-time updates for one or many IRIs.
|
|
812
|
+
* Undefined/empty values are ignored.
|
|
813
|
+
*/
|
|
814
|
+
watch$(iri) {
|
|
815
|
+
const iris = (Array.isArray(iri) ? iri : [iri]).filter((v) => typeof v === 'string' && v.length > 0);
|
|
816
|
+
return this.subscribeAndSync(iris);
|
|
817
|
+
}
|
|
818
|
+
unwatch(iri) {
|
|
819
|
+
const iris = (Array.isArray(iri) ? iri : [iri]).filter((v) => typeof v === 'string' && v.length > 0);
|
|
820
|
+
this.realtime.unsubscribe(iris);
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Subscribes to updates of a related sub-resource published on the parent topic.
|
|
824
|
+
* Example: subscribe to Message events on a Conversation topic, filtering by `message.conversation`.
|
|
825
|
+
*/
|
|
826
|
+
watchSubResource$(iri, field) {
|
|
827
|
+
const iris = (Array.isArray(iri) ? iri : [iri]).filter((v) => typeof v === 'string' && v.length > 0);
|
|
828
|
+
return this.realtime
|
|
829
|
+
.subscribe$(iris, { field: field })
|
|
830
|
+
.pipe(map$1(e => e.data), filter$1((d) => d !== undefined), share$1());
|
|
831
|
+
}
|
|
832
|
+
subscribeAndSync(iris) {
|
|
833
|
+
return this.realtime
|
|
834
|
+
.subscribe$(iris)
|
|
835
|
+
.pipe(map$1(event => event.data), filter$1((data) => data !== undefined), share$1());
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
class FacadeFactory {
|
|
840
|
+
env = inject(EnvironmentInjector);
|
|
841
|
+
http = inject(HttpClient);
|
|
842
|
+
baseUrl = inject(API_BASE_URL);
|
|
843
|
+
withCredentials = inject(BRIDGE_WITH_CREDENTIALS);
|
|
844
|
+
mercure = inject(MercureRealtimeAdapter);
|
|
845
|
+
/**
|
|
846
|
+
* Creates a `ResourceFacade<T>`.
|
|
847
|
+
*
|
|
848
|
+
* Important: `ResourceFacade` uses `toSignal()`, which requires an injection context.
|
|
849
|
+
* This factory ensures that by using `runInInjectionContext`.
|
|
850
|
+
*/
|
|
851
|
+
create(config) {
|
|
852
|
+
const url = config.url;
|
|
853
|
+
const repo = config.repo ?? new ApiPlatformRestRepository(this.http, this.baseUrl, url, this.withCredentials);
|
|
854
|
+
const realtime = config.realtime ?? this.mercure;
|
|
855
|
+
return runInInjectionContext(this.env, () => new ResourceFacade(repo, realtime));
|
|
856
|
+
}
|
|
857
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: FacadeFactory, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
858
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: FacadeFactory, providedIn: 'root' });
|
|
859
|
+
}
|
|
860
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: FacadeFactory, decorators: [{
|
|
861
|
+
type: Injectable,
|
|
862
|
+
args: [{ providedIn: 'root' }]
|
|
863
|
+
}] });
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* High-level facade for ad-hoc HTTP calls and Mercure subscriptions.
|
|
867
|
+
*
|
|
868
|
+
* Prefer `FacadeFactory` + `ResourceFacade<T>` when you want a resource-oriented API.
|
|
869
|
+
*/
|
|
870
|
+
class BridgeFacade {
|
|
871
|
+
http;
|
|
872
|
+
realtime;
|
|
873
|
+
apiBase;
|
|
874
|
+
withCredentialsDefault;
|
|
875
|
+
constructor(http, realtime, apiBase, withCredentialsDefault) {
|
|
876
|
+
this.http = http;
|
|
877
|
+
this.realtime = realtime;
|
|
878
|
+
this.apiBase = apiBase;
|
|
879
|
+
this.withCredentialsDefault = withCredentialsDefault;
|
|
880
|
+
}
|
|
881
|
+
// ──────────────── HTTP ────────────────
|
|
882
|
+
get$(url, opts) {
|
|
883
|
+
return this.http.get(this.resolveUrl(url), {
|
|
884
|
+
headers: opts?.headers,
|
|
885
|
+
withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
getCollection$(url, query, opts) {
|
|
889
|
+
const params = toHttpParams(query);
|
|
890
|
+
return this.http.get(this.resolveUrl(url), {
|
|
891
|
+
params,
|
|
892
|
+
headers: opts?.headers,
|
|
893
|
+
withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
post$(url, payload, opts) {
|
|
897
|
+
return this.http.post(this.resolveUrl(url), payload, {
|
|
898
|
+
headers: opts?.headers,
|
|
899
|
+
withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
patch$(url, changes, opts) {
|
|
903
|
+
return this.http.patch(this.resolveUrl(url), changes, {
|
|
904
|
+
headers: opts?.headers,
|
|
905
|
+
withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
put$(url, payload, opts) {
|
|
909
|
+
return this.http.put(this.resolveUrl(url), payload, {
|
|
910
|
+
headers: opts?.headers,
|
|
911
|
+
withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
delete$(url, opts) {
|
|
915
|
+
return this.http.delete(this.resolveUrl(url), {
|
|
916
|
+
headers: opts?.headers,
|
|
917
|
+
withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
request$(req) {
|
|
921
|
+
const { method, url } = req;
|
|
922
|
+
const targetUrl = this.resolveUrl(url);
|
|
923
|
+
const mergedOptions = buildHttpRequestOptions(req, { withCredentialsDefault: this.withCredentialsDefault });
|
|
924
|
+
return this.http.request(method, targetUrl, mergedOptions);
|
|
925
|
+
}
|
|
926
|
+
// ──────────────── SSE / Mercure ────────────────
|
|
927
|
+
watch$(iri, subscribeFilter) {
|
|
928
|
+
const iris = (Array.isArray(iri) ? iri : [iri]).filter((v) => typeof v === 'string' && v.length > 0);
|
|
929
|
+
return this.realtime
|
|
930
|
+
.subscribe$(iris, subscribeFilter)
|
|
931
|
+
.pipe(map((event) => event.data), filter((data) => data !== undefined), share());
|
|
932
|
+
}
|
|
933
|
+
unwatch(iri) {
|
|
934
|
+
const iris = (Array.isArray(iri) ? iri : [iri]).filter((v) => typeof v === 'string' && v.length > 0);
|
|
935
|
+
this.realtime.unsubscribe(iris);
|
|
936
|
+
}
|
|
937
|
+
resolveUrl(path) {
|
|
938
|
+
if (!path)
|
|
939
|
+
throw new Error('BridgeFacade: missing url');
|
|
940
|
+
return resolveUrl(this.apiBase, path);
|
|
941
|
+
}
|
|
942
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: BridgeFacade, deps: [{ token: i1.HttpClient }, { token: MercureRealtimeAdapter }, { token: API_BASE_URL }, { token: BRIDGE_WITH_CREDENTIALS }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
943
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: BridgeFacade, providedIn: 'root' });
|
|
944
|
+
}
|
|
945
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: BridgeFacade, decorators: [{
|
|
946
|
+
type: Injectable,
|
|
947
|
+
args: [{ providedIn: 'root' }]
|
|
948
|
+
}], ctorParameters: () => [{ type: i1.HttpClient }, { type: MercureRealtimeAdapter }, { type: undefined, decorators: [{
|
|
949
|
+
type: Inject,
|
|
950
|
+
args: [API_BASE_URL]
|
|
951
|
+
}] }, { type: undefined, decorators: [{
|
|
952
|
+
type: Inject,
|
|
953
|
+
args: [BRIDGE_WITH_CREDENTIALS]
|
|
954
|
+
}] }] });
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* Generated bundle index. Do not edit.
|
|
958
|
+
*/
|
|
959
|
+
|
|
960
|
+
export { BridgeFacade, FacadeFactory, ResourceFacade, joinUrl, provideBridge, resolveUrl };
|
|
961
|
+
//# sourceMappingURL=obsidiane-auth-client-js.mjs.map
|