@obsidiane/auth-client-js 1.0.3 → 1.0.5

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.
Files changed (48) hide show
  1. package/README.md +725 -105
  2. package/fesm2022/obsidiane-auth-client-js.mjs +961 -0
  3. package/fesm2022/obsidiane-auth-client-js.mjs.map +1 -0
  4. package/index.d.ts +361 -0
  5. package/obsidiane-auth-client-js-0.1.0.tgz +0 -0
  6. package/package.json +15 -36
  7. package/DEVELOPMENT.md +0 -226
  8. package/dist/index.js +0 -10
  9. package/index.ts +0 -10
  10. package/src/lib/bridge/rest/api-platform.adapter.ts +0 -84
  11. package/src/lib/bridge/rest/http-request.options.ts +0 -21
  12. package/src/lib/bridge/rest/query-builder.ts +0 -43
  13. package/src/lib/bridge/sse/eventsource-wrapper.ts +0 -70
  14. package/src/lib/bridge/sse/mercure-topic.mapper.ts +0 -48
  15. package/src/lib/bridge/sse/mercure-url.builder.ts +0 -17
  16. package/src/lib/bridge/sse/mercure.adapter.ts +0 -261
  17. package/src/lib/bridge/sse/ref-count-topic.registry.ts +0 -45
  18. package/src/lib/bridge.types.ts +0 -33
  19. package/src/lib/facades/bridge.facade.ts +0 -108
  20. package/src/lib/facades/facade.factory.ts +0 -38
  21. package/src/lib/facades/facade.interface.ts +0 -30
  22. package/src/lib/facades/resource.facade.ts +0 -101
  23. package/src/lib/interceptors/bridge-debug.interceptor.ts +0 -32
  24. package/src/lib/interceptors/bridge-defaults.interceptor.ts +0 -53
  25. package/src/lib/interceptors/content-type.interceptor.ts +0 -49
  26. package/src/lib/interceptors/singleflight.interceptor.ts +0 -55
  27. package/src/lib/ports/realtime.port.ts +0 -36
  28. package/src/lib/ports/resource-repository.port.ts +0 -78
  29. package/src/lib/provide-bridge.ts +0 -148
  30. package/src/lib/tokens.ts +0 -20
  31. package/src/lib/utils/url.ts +0 -15
  32. package/src/models/Auth.ts +0 -5
  33. package/src/models/AuthInviteCompleteInputInviteComplete.ts +0 -7
  34. package/src/models/AuthInviteUserInputInviteSend.ts +0 -5
  35. package/src/models/AuthLdJson.ts +0 -5
  36. package/src/models/AuthPasswordForgotInputPasswordForgot.ts +0 -5
  37. package/src/models/AuthPasswordResetInputPasswordReset.ts +0 -6
  38. package/src/models/AuthRegisterUserInputUserRegister.ts +0 -6
  39. package/src/models/FrontendConfig.ts +0 -12
  40. package/src/models/InvitePreview.ts +0 -8
  41. package/src/models/InviteUserInviteRead.ts +0 -9
  42. package/src/models/Setup.ts +0 -5
  43. package/src/models/SetupRegisterUserInputUserRegister.ts +0 -6
  44. package/src/models/UserUpdateUserRolesInputUserRoles.ts +0 -5
  45. package/src/models/UserUserRead.ts +0 -9
  46. package/src/models/index.ts +0 -14
  47. package/src/public-api.ts +0 -9
  48. package/tsconfig.json +0 -23
@@ -1,261 +0,0 @@
1
- import {Inject, Injectable, OnDestroy, Optional, PLATFORM_ID} from '@angular/core';
2
- import {BehaviorSubject, defer, EMPTY, fromEvent, Observable, of, Subject} from 'rxjs';
3
- import {auditTime, concatMap, filter, finalize, map, share, takeUntil} from 'rxjs/operators';
4
- import {API_BASE_URL, BRIDGE_LOGGER, BRIDGE_WITH_CREDENTIALS, MERCURE_HUB_URL, MERCURE_TOPIC_MODE} from '../../tokens';
5
- import {BridgeLogger, MercureTopicMode} from '../../bridge.types';
6
- import {RealtimeEvent, RealtimePort, RealtimeStatus} from '../../ports/realtime.port';
7
- import {EventSourceWrapper} from './eventsource-wrapper';
8
- import {Item} from '../../ports/resource-repository.port';
9
- import {isPlatformBrowser} from '@angular/common';
10
- import {MercureUrlBuilder} from './mercure-url.builder';
11
- import {RefCountTopicRegistry} from './ref-count-topic.registry';
12
- import {MercureTopicMapper} from './mercure-topic.mapper';
13
-
14
-
15
- @Injectable({providedIn: 'root'})
16
- export class MercureRealtimeAdapter implements RealtimePort, OnDestroy {
17
- private lastEventId?: string;
18
- private es?: EventSourceWrapper;
19
- private currentKey?: string;
20
-
21
- private readonly topicsRegistry = new RefCountTopicRegistry();
22
- private readonly urlBuilder: MercureUrlBuilder;
23
- private readonly topicMapper: MercureTopicMapper;
24
-
25
- private readonly destroy$ = new Subject<void>();
26
- private connectionStop$ = new Subject<void>();
27
- private readonly rebuild$ = new Subject<void>();
28
- private shuttingDown = false;
29
-
30
- private readonly _status$ = new BehaviorSubject<RealtimeStatus>('closed');
31
- private readonly incoming$ = new Subject<{ type: string; data: string }>();
32
-
33
- constructor(
34
- @Inject(API_BASE_URL) private readonly apiBase: string,
35
- @Inject(BRIDGE_WITH_CREDENTIALS) private readonly withCredentialsDefault: boolean,
36
- @Inject(PLATFORM_ID) private readonly platformId: object,
37
- @Optional() @Inject(MERCURE_HUB_URL) private readonly hubUrl?: string,
38
- @Optional() @Inject(MERCURE_TOPIC_MODE) topicMode?: MercureTopicMode,
39
- @Optional() @Inject(BRIDGE_LOGGER) private readonly logger?: BridgeLogger,
40
- ) {
41
- this.urlBuilder = new MercureUrlBuilder();
42
- // `topicMode` affects only the `topic=` query param sent to the hub.
43
- // Payload IRIs are always matched using same-origin relative IRIs (`/api/...`) when possible.
44
- this.topicMapper = new MercureTopicMapper(apiBase, topicMode ?? 'url');
45
-
46
- this.rebuild$
47
- .pipe(
48
- auditTime(10),
49
- concatMap(() => this.rebuildOnce$()),
50
- )
51
- .subscribe();
52
-
53
- if (isPlatformBrowser(this.platformId)) {
54
- fromEvent<PageTransitionEvent>(window, 'pagehide')
55
- .pipe(takeUntil(this.destroy$))
56
- .subscribe(() => this.shutdownBeforeExit());
57
-
58
- fromEvent<BeforeUnloadEvent>(window, 'beforeunload')
59
- .pipe(takeUntil(this.destroy$))
60
- .subscribe(() => this.shutdownBeforeExit());
61
- }
62
- }
63
-
64
- // ──────────────── API publique ────────────────
65
-
66
- status$(): Observable<RealtimeStatus> {
67
- return this._status$.asObservable();
68
- }
69
-
70
-
71
- subscribe$<T>(iris: string[], _filter?: { field?: string }): Observable<RealtimeEvent<T>> {
72
- return defer(() => {
73
- const inputIris = iris.filter((v): v is string => typeof v === 'string' && v.length > 0);
74
- if (inputIris.length === 0) return EMPTY;
75
-
76
- if (!this.hubUrl) {
77
- this.logger?.debug?.('[Mercure] hubUrl not configured → realtime disabled');
78
- return EMPTY;
79
- }
80
-
81
- // Canonicalise topics (ref-count + URL) to avoid duplicates like:
82
- // - "/api/conversations/1" and "http://localhost:8000/api/conversations/1"
83
- const registeredTopics = Array.from(new Set(inputIris.map((i) => this.topicMapper.toTopic(i))));
84
-
85
- this.topicsRegistry.addAll(registeredTopics);
86
- this.scheduleRebuild();
87
-
88
- // Matching is done against the payload IRIs (typically "/api/...").
89
- const subscribed = inputIris.map((i) => normalizeIri(this.topicMapper.toPayloadIri(i)));
90
- const fieldPath = _filter?.field;
91
-
92
- return this.incoming$.pipe(
93
- map((evt) => this.safeParse(evt.data)),
94
- filter((raw): raw is Item => !!raw),
95
-
96
- filter((raw: any) => {
97
- if (fieldPath) {
98
- const relIris = this.extractRelationIris(raw, fieldPath).map((i) => normalizeIri(this.topicMapper.toPayloadIri(i)));
99
- return relIris.some((relIri) => matchesAnySubscribed(relIri, subscribed));
100
- }
101
- const rawId = raw?.['@id'];
102
- const id = typeof rawId === 'string' ? normalizeIri(this.topicMapper.toPayloadIri(rawId)) : undefined;
103
- return typeof id === 'string' && matchesAnySubscribed(id, subscribed);
104
- }),
105
- map((payload) => ({iri: payload['@id'] as string, data: payload as T})),
106
- finalize(() => {
107
- this.topicsRegistry.removeAll(registeredTopics);
108
- this.scheduleRebuild();
109
- }),
110
- share()
111
- );
112
- });
113
- }
114
-
115
- unsubscribe(iris: string[]): void {
116
- const inputIris = iris.filter((v): v is string => typeof v === 'string' && v.length > 0);
117
- if (inputIris.length === 0) return;
118
- const topics = Array.from(new Set(inputIris.map((i) => this.topicMapper.toTopic(i))));
119
- this.topicsRegistry.removeAll(topics);
120
- this.scheduleRebuild();
121
- }
122
-
123
- public shutdownBeforeExit(): void {
124
- if (this.shuttingDown) return;
125
- this.shuttingDown = true;
126
- this.teardownConnection();
127
- }
128
-
129
- ngOnDestroy(): void {
130
- this.teardownConnection();
131
- this._status$.complete();
132
- this.incoming$.complete();
133
- this.destroy$.next();
134
- this.destroy$.complete();
135
- this.rebuild$.complete();
136
- }
137
-
138
- // ───────────────── PRIVATE ─────────────────
139
-
140
- private scheduleRebuild(): void {
141
- if (this.shuttingDown) return;
142
- this.rebuild$.next();
143
- }
144
-
145
- private rebuildOnce$() {
146
- return defer(() => {
147
- if (this.shuttingDown) return of(void 0);
148
-
149
- try {
150
- if (!this.hubUrl) {
151
- this.currentKey = undefined;
152
- this._status$.next('closed');
153
- return of(void 0);
154
- }
155
-
156
- const hasTopics = this.topicsRegistry.hasAny();
157
- const key = hasTopics ? this.topicsRegistry.computeKey(this.hubUrl, this.withCredentialsDefault) : undefined;
158
-
159
- if (!hasTopics) {
160
- if (this.es) this.teardownConnection();
161
- this.currentKey = undefined;
162
- this._status$.next('closed');
163
- return of(void 0);
164
- }
165
-
166
- if (key && key === this.currentKey) {
167
- return of(void 0);
168
- }
169
-
170
- this.teardownConnection();
171
-
172
- const url = this.urlBuilder.build(this.hubUrl, this.topicsRegistry.snapshot(), this.lastEventId);
173
- this.logger?.debug?.('[Mercure] connect', {hubUrl: this.hubUrl, topics: this.topicsRegistry.snapshot(), lastEventId: this.lastEventId});
174
- this.es = new EventSourceWrapper(url, {withCredentials: this.withCredentialsDefault}, this.logger);
175
-
176
- this.connectionStop$ = new Subject<void>();
177
-
178
- this.es.status$
179
- .pipe(takeUntil(this.connectionStop$), takeUntil(this.destroy$))
180
- .subscribe((st) => this.updateGlobalStatus(st));
181
-
182
- this.es.events$
183
- .pipe(takeUntil(this.connectionStop$), takeUntil(this.destroy$))
184
- .subscribe((e) => {
185
- this.lastEventId = e.lastEventId ?? this.lastEventId;
186
- this.incoming$.next(e);
187
- });
188
-
189
- this._status$.next('connecting');
190
- this.es.open();
191
- this.currentKey = key;
192
- } catch (err) {
193
- this.logger?.error?.('[Mercure] rebuild failed:', err);
194
- this.currentKey = undefined;
195
- this._status$.next(this.topicsRegistry.hasAny() ? 'connecting' : 'closed');
196
- }
197
-
198
- return of(void 0);
199
- });
200
- }
201
-
202
- private teardownConnection(): void {
203
- this.es?.close();
204
- this.es = undefined;
205
- this.connectionStop$.next();
206
- this.connectionStop$.complete();
207
- }
208
-
209
- private updateGlobalStatus(sse: RealtimeStatus): void {
210
- if (sse === 'connected') {
211
- this._status$.next('connected');
212
- return;
213
- }
214
- if (sse === 'connecting') {
215
- this._status$.next('connecting');
216
- return;
217
- }
218
- this._status$.next(this.topicsRegistry.hasAny() ? 'connecting' : 'closed');
219
- }
220
-
221
- private safeParse(raw: string): Item | undefined {
222
- try {
223
- return JSON.parse(raw) as Item;
224
- } catch (err) {
225
- this.logger?.debug?.('[Mercure] invalid JSON payload ignored', {raw});
226
- return undefined;
227
- }
228
- }
229
-
230
- private extractRelationIris(raw: any, path: string): string[] {
231
- const readPath = (obj: any, dotPath: string): any => {
232
- return dotPath
233
- .split('.')
234
- .filter(Boolean)
235
- .reduce((acc, key) => acc?.[key], obj);
236
- };
237
-
238
- const normalize = (value: any): string[] => {
239
- if (!value) return [];
240
- if (typeof value === 'string') return value.length > 0 ? [value] : [];
241
- if (typeof value === 'object' && typeof value['@id'] === 'string') return [value['@id']];
242
- if (Array.isArray(value)) return value.flatMap(normalize);
243
- return [];
244
- };
245
-
246
- return normalize(readPath(raw, path));
247
- }
248
-
249
- }
250
-
251
- function normalizeIri(iri: string): string {
252
- return iri.endsWith('/') ? iri.slice(0, -1) : iri;
253
- }
254
-
255
- function matchesAnySubscribed(candidate: string, subscribed: string[]): boolean {
256
- for (const iri of subscribed) {
257
- if (candidate === iri) return true;
258
- if (candidate.startsWith(`${iri}/`)) return true;
259
- }
260
- return false;
261
- }
@@ -1,45 +0,0 @@
1
- export class RefCountTopicRegistry {
2
- private readonly topics = new Set<string>();
3
- private readonly refCounts = new Map<string, number>();
4
-
5
- /**
6
- * Increments ref-count for each topic.
7
- * Callers should pass unique topic strings (deduped).
8
- */
9
- addAll(topics: Iterable<string>): void {
10
- for (const topic of topics) {
11
- const next = (this.refCounts.get(topic) ?? 0) + 1;
12
- this.refCounts.set(topic, next);
13
- this.topics.add(topic);
14
- }
15
- }
16
-
17
- /**
18
- * Decrements ref-count for each topic.
19
- * Callers should pass unique topic strings (deduped).
20
- */
21
- removeAll(topics: Iterable<string>): void {
22
- for (const topic of topics) {
23
- const next = (this.refCounts.get(topic) ?? 0) - 1;
24
- if (next <= 0) {
25
- this.refCounts.delete(topic);
26
- this.topics.delete(topic);
27
- } else {
28
- this.refCounts.set(topic, next);
29
- }
30
- }
31
- }
32
-
33
- hasAny(): boolean {
34
- return this.topics.size > 0;
35
- }
36
-
37
- snapshot(): ReadonlySet<string> {
38
- return new Set(this.topics);
39
- }
40
-
41
- computeKey(hubUrl: string, credentialsOn: boolean): string {
42
- const topicsSorted = Array.from(this.topics).sort().join('|');
43
- return `${hubUrl}::${credentialsOn ? '1' : '0'}::${topicsSorted}`;
44
- }
45
- }
@@ -1,33 +0,0 @@
1
- /**
2
- * Public configuration types for the bridge.
3
- *
4
- * Keep this file dependency-free (no Angular imports) so it can be used from both
5
- * runtime code and type-only contexts.
6
- */
7
-
8
- export interface BridgeLogger {
9
- debug: (...args: unknown[]) => void;
10
- info: (...args: unknown[]) => void;
11
- warn: (...args: unknown[]) => void;
12
- error: (...args: unknown[]) => void;
13
- }
14
-
15
- export interface BridgeDefaults {
16
- headers?: Record<string, string>;
17
- timeoutMs?: number;
18
- retries?:
19
- | number
20
- | {
21
- count: number;
22
- delayMs?: number;
23
- methods?: string[];
24
- };
25
- }
26
-
27
- /**
28
- * Controls how Mercure topics are sent in the hub URL.
29
- * - `url`: topics are absolute URLs (recommended default)
30
- * - `iri`: topics are same-origin relative IRIs (e.g. `/api/...`)
31
- */
32
- export type MercureTopicMode = 'url' | 'iri';
33
-
@@ -1,108 +0,0 @@
1
- import {Inject, Injectable} from '@angular/core';
2
- import {HttpClient} from '@angular/common/http';
3
- import {Observable} from 'rxjs';
4
- import {filter, map, share} from 'rxjs/operators';
5
- import {toHttpParams} from '../bridge/rest/query-builder';
6
- import {buildHttpRequestOptions} from '../bridge/rest/http-request.options';
7
- import {MercureRealtimeAdapter} from '../bridge/sse/mercure.adapter';
8
- import {API_BASE_URL, BRIDGE_WITH_CREDENTIALS} from '../tokens';
9
- import {AnyQuery, Collection, HttpCallOptions, HttpRequestConfig, Iri, IriRequired, Item} from '../ports/resource-repository.port';
10
- import {SubscribeFilter} from '../ports/realtime.port';
11
- import {resolveUrl} from '../utils/url';
12
-
13
- /**
14
- * High-level facade for ad-hoc HTTP calls and Mercure subscriptions.
15
- *
16
- * Prefer `FacadeFactory` + `ResourceFacade<T>` when you want a resource-oriented API.
17
- */
18
- @Injectable({providedIn: 'root'})
19
- export class BridgeFacade {
20
- constructor(
21
- private readonly http: HttpClient,
22
- private readonly realtime: MercureRealtimeAdapter,
23
- @Inject(API_BASE_URL) private readonly apiBase: string,
24
- @Inject(BRIDGE_WITH_CREDENTIALS) private readonly withCredentialsDefault: boolean,
25
- ) {
26
- }
27
-
28
- // ──────────────── HTTP ────────────────
29
-
30
- get$<R = unknown>(url: IriRequired, opts?: HttpCallOptions): Observable<R> {
31
- return this.http.get<R>(this.resolveUrl(url), {
32
- headers: opts?.headers,
33
- withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
34
- });
35
- }
36
-
37
- getCollection$<T extends Item = Item>(
38
- url: IriRequired,
39
- query?: AnyQuery,
40
- opts?: HttpCallOptions
41
- ): Observable<Collection<T>> {
42
- const params = toHttpParams(query);
43
- return this.http.get<Collection<T>>(this.resolveUrl(url), {
44
- params,
45
- headers: opts?.headers,
46
- withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
47
- });
48
- }
49
-
50
- post$<R = unknown, B = unknown>(url: IriRequired, payload: B, opts?: HttpCallOptions): Observable<R> {
51
- return this.http.post<R>(this.resolveUrl(url), payload, {
52
- headers: opts?.headers,
53
- withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
54
- });
55
- }
56
-
57
- patch$<R = unknown, B = unknown>(url: IriRequired, changes: B, opts?: HttpCallOptions): Observable<R> {
58
- return this.http.patch<R>(this.resolveUrl(url), changes, {
59
- headers: opts?.headers,
60
- withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
61
- });
62
- }
63
-
64
- put$<R = unknown, B = unknown>(url: IriRequired, payload: B, opts?: HttpCallOptions): Observable<R> {
65
- return this.http.put<R>(this.resolveUrl(url), payload, {
66
- headers: opts?.headers,
67
- withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
68
- });
69
- }
70
-
71
- delete$(url: IriRequired, opts?: HttpCallOptions): Observable<void> {
72
- return this.http.delete<void>(this.resolveUrl(url), {
73
- headers: opts?.headers,
74
- withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
75
- });
76
- }
77
-
78
- request$<R = unknown, B = unknown>(req: HttpRequestConfig<B>): Observable<R> {
79
- const {method, url} = req;
80
- const targetUrl = this.resolveUrl(url);
81
-
82
- const mergedOptions = buildHttpRequestOptions(req, {withCredentialsDefault: this.withCredentialsDefault});
83
- return this.http.request<R>(method, targetUrl, mergedOptions as {observe: 'body'});
84
- }
85
-
86
- // ──────────────── SSE / Mercure ────────────────
87
-
88
- watch$<T = Item>(iri: Iri | Iri[], subscribeFilter?: SubscribeFilter): Observable<T> {
89
- const iris = (Array.isArray(iri) ? iri : [iri]).filter((v): v is string => typeof v === 'string' && v.length > 0);
90
- return this.realtime
91
- .subscribe$<T>(iris, subscribeFilter)
92
- .pipe(
93
- map((event) => event.data),
94
- filter((data): data is T => data !== undefined),
95
- share()
96
- );
97
- }
98
-
99
- unwatch(iri: Iri | Iri[]): void {
100
- const iris = (Array.isArray(iri) ? iri : [iri]).filter((v): v is string => typeof v === 'string' && v.length > 0);
101
- this.realtime.unsubscribe(iris);
102
- }
103
-
104
- private resolveUrl(path?: Iri): string {
105
- if (!path) throw new Error('BridgeFacade: missing url');
106
- return resolveUrl(this.apiBase, path);
107
- }
108
- }
@@ -1,38 +0,0 @@
1
- import {EnvironmentInjector, inject, Injectable, runInInjectionContext} from '@angular/core';
2
- import {HttpClient} from '@angular/common/http';
3
- import {RealtimePort} from '../ports/realtime.port';
4
- import {Item, ResourceRepository} from '../ports/resource-repository.port';
5
- import {API_BASE_URL, BRIDGE_WITH_CREDENTIALS} from '../tokens';
6
- import {MercureRealtimeAdapter} from '../bridge/sse/mercure.adapter';
7
- import {ApiPlatformRestRepository} from '../bridge/rest/api-platform.adapter';
8
- import {ResourceFacade} from './resource.facade';
9
-
10
- export type FacadeConfig<T> = {
11
- url: string;
12
- repo?: ResourceRepository<T>;
13
- realtime?: RealtimePort;
14
- };
15
-
16
-
17
- @Injectable({providedIn: 'root'})
18
- export class FacadeFactory {
19
- private readonly env = inject(EnvironmentInjector);
20
- private readonly http = inject(HttpClient);
21
- private readonly baseUrl = inject(API_BASE_URL);
22
- private readonly withCredentials = inject(BRIDGE_WITH_CREDENTIALS);
23
- private readonly mercure = inject(MercureRealtimeAdapter);
24
-
25
- /**
26
- * Creates a `ResourceFacade<T>`.
27
- *
28
- * Important: `ResourceFacade` uses `toSignal()`, which requires an injection context.
29
- * This factory ensures that by using `runInInjectionContext`.
30
- */
31
- create<T extends Item>(config: FacadeConfig<T>): ResourceFacade<T> {
32
- const url = config.url;
33
- const repo = config.repo ?? new ApiPlatformRestRepository<T>(this.http, this.baseUrl, url, this.withCredentials);
34
- const realtime = config.realtime ?? this.mercure;
35
-
36
- return runInInjectionContext(this.env, () => new ResourceFacade<T>(repo, realtime));
37
- }
38
- }
@@ -1,30 +0,0 @@
1
- import {
2
- Collection,
3
- HttpRequestConfig,
4
- Iri,
5
- IriRequired,
6
- Item,
7
- HttpCallOptions,
8
- AnyQuery,
9
- } from '../ports/resource-repository.port';
10
- import {Observable} from 'rxjs';
11
-
12
- export interface Facade<T extends Item> {
13
- getCollection$(query?: AnyQuery, opts?: HttpCallOptions): Observable<Collection<T>>;
14
-
15
- get$(iri: IriRequired, opts?: HttpCallOptions): Observable<T>;
16
-
17
- post$(payload: Partial<T>, opts?: HttpCallOptions): Observable<T>;
18
-
19
- patch$(iri: IriRequired, changes: Partial<T>, opts?: HttpCallOptions): Observable<T>;
20
-
21
- put$(iri: IriRequired, payload: Partial<T>, opts?: HttpCallOptions): Observable<T>;
22
-
23
- delete$(iri: IriRequired, opts?: HttpCallOptions): Observable<void>;
24
-
25
- request$<R = unknown, B = unknown>(req: HttpRequestConfig<B>): Observable<R>;
26
-
27
- watch$(iri: Iri | Iri[]): Observable<T>;
28
-
29
- unwatch(iri: Iri | Iri[]): void;
30
- }
@@ -1,101 +0,0 @@
1
- import {Signal} from '@angular/core';
2
- import {
3
- ResourceRepository,
4
- AnyQuery,
5
- HttpCallOptions,
6
- Iri,
7
- IriRequired,
8
- Collection,
9
- Item,
10
- HttpRequestConfig
11
- } from '../ports/resource-repository.port';
12
- import {RealtimePort, RealtimeStatus} from '../ports/realtime.port';
13
- import {toSignal} from '@angular/core/rxjs-interop';
14
- import {filter, map, Observable, share} from 'rxjs';
15
- import {Facade} from './facade.interface';
16
-
17
- export class ResourceFacade<T extends Item> implements Facade<T> {
18
-
19
- readonly connectionStatus: Signal<RealtimeStatus>;
20
-
21
- constructor(
22
- protected readonly repo: ResourceRepository<T>,
23
- protected readonly realtime: RealtimePort
24
- ) {
25
- this.connectionStatus = toSignal(
26
- this.realtime.status$(),
27
- {initialValue: 'closed'}
28
- );
29
- }
30
-
31
- getCollection$(query?: AnyQuery, opts?: HttpCallOptions): Observable<Collection<T>> {
32
- return this.repo.getCollection$(query, opts);
33
- }
34
-
35
- get$(iri: IriRequired, opts?: HttpCallOptions): Observable<T> {
36
- return this.repo.get$(iri, opts);
37
- }
38
-
39
- patch$(iri: IriRequired, changes: Partial<T>, opts?: HttpCallOptions): Observable<T> {
40
- return this.repo.patch$(iri, changes, opts);
41
- }
42
-
43
- post$(payload: Partial<T>, opts?: HttpCallOptions): Observable<T> {
44
- return this.repo.post$(payload, opts);
45
- }
46
-
47
- put$(iri: IriRequired, payload: Partial<T>, opts?: HttpCallOptions): Observable<T> {
48
- return this.repo.put$(iri, payload, opts);
49
- }
50
-
51
- delete$(iri: IriRequired, opts?: HttpCallOptions): Observable<void> {
52
- return this.repo.delete$(iri, opts);
53
- }
54
-
55
- request$<R = unknown, B = unknown>(req: HttpRequestConfig<B>): Observable<R> {
56
- return this.repo.request$<R, B>(req);
57
- }
58
-
59
- /**
60
- * Subscribes to real-time updates for one or many IRIs.
61
- * Undefined/empty values are ignored.
62
- */
63
- watch$(iri: Iri | Iri[]): Observable<T> {
64
- const iris = (Array.isArray(iri) ? iri : [iri]).filter((v): v is string => typeof v === 'string' && v.length > 0);
65
- return this.subscribeAndSync(iris);
66
- }
67
-
68
-
69
- unwatch(iri: Iri | Iri[]): void {
70
- const iris = (Array.isArray(iri) ? iri : [iri]).filter((v): v is string => typeof v === 'string' && v.length > 0);
71
- this.realtime.unsubscribe(iris);
72
- }
73
-
74
- /**
75
- * Subscribes to updates of a related sub-resource published on the parent topic.
76
- * Example: subscribe to Message events on a Conversation topic, filtering by `message.conversation`.
77
- */
78
- watchSubResource$<R>(
79
- iri: Iri | Iri[],
80
- field: string
81
- ): Observable<R> {
82
- const iris = (Array.isArray(iri) ? iri : [iri]).filter((v): v is string => typeof v === 'string' && v.length > 0);
83
- return this.realtime
84
- .subscribe$<R>(iris, {field: field})
85
- .pipe(
86
- map(e => e.data),
87
- filter((d): d is R => d !== undefined),
88
- share()
89
- );
90
- }
91
-
92
- protected subscribeAndSync(iris: string[]): Observable<T> {
93
- return this.realtime
94
- .subscribe$<T>(iris)
95
- .pipe(
96
- map(event => event.data),
97
- filter((data): data is T => data !== undefined),
98
- share()
99
- );
100
- }
101
- }
@@ -1,32 +0,0 @@
1
- import {HttpInterceptorFn, HttpResponse} from '@angular/common/http';
2
- import {inject} from '@angular/core';
3
- import {BRIDGE_LOGGER} from '../tokens';
4
- import {catchError, finalize, tap, throwError} from 'rxjs';
5
-
6
- /**
7
- * Lightweight request/response logging controlled by `provideBridge({debug: true})`.
8
- * Logs are delegated to the injected `BRIDGE_LOGGER`.
9
- */
10
- export const bridgeDebugInterceptor: HttpInterceptorFn = (req, next) => {
11
- const logger = inject(BRIDGE_LOGGER, {optional: true});
12
- if (!logger) return next(req);
13
-
14
- const startedAt = Date.now();
15
- logger.debug('[Bridge] request', {method: req.method, url: req.urlWithParams});
16
-
17
- return next(req).pipe(
18
- tap((evt) => {
19
- if (evt instanceof HttpResponse) {
20
- logger.debug('[Bridge] response', {method: req.method, url: req.urlWithParams, status: evt.status});
21
- }
22
- }),
23
- catchError((err) => {
24
- logger.error('[Bridge] error', {method: req.method, url: req.urlWithParams, err});
25
- return throwError(() => err);
26
- }),
27
- finalize(() => {
28
- const durationMs = Date.now() - startedAt;
29
- logger.debug('[Bridge] done', {method: req.method, url: req.urlWithParams, durationMs});
30
- })
31
- );
32
- };