@obsidiane/auth-client-js 1.0.1

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 (44) hide show
  1. package/DEVELOPMENT.md +226 -0
  2. package/README.md +181 -0
  3. package/dist/index.js +10 -0
  4. package/index.ts +10 -0
  5. package/package.json +45 -0
  6. package/src/lib/bridge/rest/api-platform.adapter.ts +84 -0
  7. package/src/lib/bridge/rest/http-request.options.ts +21 -0
  8. package/src/lib/bridge/rest/query-builder.ts +43 -0
  9. package/src/lib/bridge/sse/eventsource-wrapper.ts +70 -0
  10. package/src/lib/bridge/sse/mercure-topic.mapper.ts +48 -0
  11. package/src/lib/bridge/sse/mercure-url.builder.ts +17 -0
  12. package/src/lib/bridge/sse/mercure.adapter.ts +261 -0
  13. package/src/lib/bridge/sse/ref-count-topic.registry.ts +45 -0
  14. package/src/lib/bridge.types.ts +33 -0
  15. package/src/lib/facades/bridge.facade.ts +108 -0
  16. package/src/lib/facades/facade.factory.ts +38 -0
  17. package/src/lib/facades/facade.interface.ts +30 -0
  18. package/src/lib/facades/resource.facade.ts +101 -0
  19. package/src/lib/interceptors/bridge-debug.interceptor.ts +32 -0
  20. package/src/lib/interceptors/bridge-defaults.interceptor.ts +53 -0
  21. package/src/lib/interceptors/content-type.interceptor.ts +49 -0
  22. package/src/lib/interceptors/singleflight.interceptor.ts +55 -0
  23. package/src/lib/ports/realtime.port.ts +36 -0
  24. package/src/lib/ports/resource-repository.port.ts +78 -0
  25. package/src/lib/provide-bridge.ts +148 -0
  26. package/src/lib/tokens.ts +20 -0
  27. package/src/lib/utils/url.ts +15 -0
  28. package/src/models/Auth.ts +5 -0
  29. package/src/models/AuthInviteCompleteInputInviteComplete.ts +7 -0
  30. package/src/models/AuthInviteUserInputInviteSend.ts +5 -0
  31. package/src/models/AuthLdJson.ts +5 -0
  32. package/src/models/AuthPasswordForgotInputPasswordForgot.ts +5 -0
  33. package/src/models/AuthPasswordResetInputPasswordReset.ts +6 -0
  34. package/src/models/AuthRegisterUserInputUserRegister.ts +6 -0
  35. package/src/models/FrontendConfig.ts +12 -0
  36. package/src/models/InvitePreview.ts +8 -0
  37. package/src/models/InviteUserInviteRead.ts +9 -0
  38. package/src/models/Setup.ts +5 -0
  39. package/src/models/SetupRegisterUserInputUserRegister.ts +6 -0
  40. package/src/models/UserUpdateUserRolesInputUserRoles.ts +5 -0
  41. package/src/models/UserUserRead.ts +9 -0
  42. package/src/models/index.ts +14 -0
  43. package/src/public-api.ts +9 -0
  44. package/tsconfig.json +23 -0
package/DEVELOPMENT.md ADDED
@@ -0,0 +1,226 @@
1
+ # Development Guide
2
+
3
+ ## Architecture Overview
4
+
5
+ ```
6
+ @obsidiane/auth-client-js
7
+ ├── Bridge (Meridiane-generated) ← Auto-generated from OpenAPI
8
+ │ ├── src/lib/ ← HTTP client, facades, interceptors
9
+ │ ├── src/models/ ← TypeScript types
10
+ │ └── src/public-api.ts ← Main exports
11
+
12
+ └── Config
13
+ ├── package.json ← NPM metadata
14
+ ├── tsconfig.json ← TypeScript config
15
+ └── .gitignore ← Ignore generated /src/
16
+ ```
17
+
18
+ ## File Structure
19
+
20
+ ### Generated Files (auto-ignored)
21
+
22
+ ```
23
+ src/ ← GENERATED by Meridiane, ignored in git
24
+ ├── lib/
25
+ │ ├── bridge/
26
+ │ │ ├── rest/ ← HTTP adapters
27
+ │ │ └── sse/ ← Realtime (optional)
28
+ │ ├── facades/ ← Resource facades
29
+ │ ├── interceptors/ ← HTTP interceptors
30
+ │ ├── ports/ ← Interfaces
31
+ │ ├── utils/ ← Helpers
32
+ │ ├── provide-bridge.ts ← Angular provider
33
+ │ ├── bridge.types.ts ← Core types
34
+ │ └── tokens.ts ← DI tokens
35
+ ├── models/ ← Generated from OpenAPI schemas
36
+ └── public-api.ts ← Main exports
37
+ ```
38
+
39
+ ### Manual Files (version-controlled)
40
+
41
+ ```
42
+ package.json
43
+ tsconfig.json
44
+ README.md
45
+ DEVELOPMENT.md (this file)
46
+ .gitignore
47
+ ```
48
+
49
+ ## Regeneration Workflow
50
+
51
+ ### When Backend API Changes
52
+
53
+ 1. **Backend deploys** new endpoints to `/api/docs.json`
54
+
55
+ 2. **Run generation**:
56
+ ```bash
57
+ make sdks
58
+ ```
59
+
60
+ 3. **What happens**:
61
+ - Downloads OpenAPI spec from `http://localhost:9000/api/docs.json`
62
+ - Runs Meridiane to generate the bridge
63
+ - Copies `src/` into `packages/auth-client-js/src/`
64
+
65
+ 4. **Commit**:
66
+ ```bash
67
+ git add packages/auth-client-js/
68
+ git commit -m "chore: regenerate SDKs from OpenAPI spec"
69
+ ```
70
+
71
+ 5. **Publish** (if needed):
72
+ ```bash
73
+ cd packages/auth-client-js
74
+ npm run build
75
+ npm publish
76
+ ```
77
+
78
+ ## Adding New Features
79
+
80
+ ### Scenario 1: New HTTP Endpoint
81
+
82
+ **The endpoint is added in the backend API** → automatically generated by Meridiane.
83
+
84
+ ```bash
85
+ make sdk-npm
86
+ # Done! New endpoint is in src/ automatically
87
+ ```
88
+
89
+ ## Building for Publication
90
+
91
+ ```bash
92
+ # Install dependencies (if needed for TypeScript compilation)
93
+ npm install
94
+
95
+ # Build TypeScript → JavaScript
96
+ npm run build
97
+
98
+ # Output in ./dist/
99
+ ls -la dist/
100
+
101
+ # Publish to npm
102
+ npm publish
103
+ ```
104
+
105
+ ## Using in Applications
106
+
107
+ ### In Angular App
108
+
109
+ ```typescript
110
+ // app.config.ts
111
+ import { provideBridge } from '@obsidiane/auth-client-js';
112
+
113
+ export const appConfig: ApplicationConfig = {
114
+ providers: [
115
+ provideBridge({
116
+ baseUrl: 'http://localhost:9000',
117
+ }),
118
+ ],
119
+ };
120
+ ```
121
+
122
+ ### In a Service
123
+
124
+ ```typescript
125
+ import { FacadeFactory } from '@obsidiane/auth-client-js';
126
+ import type { UserUserRead } from '@obsidiane/auth-client-js';
127
+
128
+ @Injectable({ providedIn: 'root' })
129
+ export class UsersService {
130
+ private readonly factory = inject(FacadeFactory);
131
+ private readonly facade = this.factory.create<UserUserRead>({
132
+ url: '/api/users',
133
+ });
134
+
135
+ getAll() {
136
+ return this.facade.getCollection$();
137
+ }
138
+
139
+ getOne(id: string) {
140
+ return this.facade.get$(id);
141
+ }
142
+ }
143
+ ```
144
+
145
+ ## Testing
146
+
147
+ The bridge (Meridiane) is tested separately in its own project.
148
+
149
+ ## Troubleshooting
150
+
151
+ ### Build Fails
152
+
153
+ ```bash
154
+ npm run build
155
+
156
+ # If tsc not found
157
+ npm install
158
+ npm run build
159
+ ```
160
+
161
+ ### "Cannot find module 'src/public-api'"
162
+
163
+ The `src/` folder doesn't exist. You need to generate it:
164
+
165
+ ```bash
166
+ make sdk-npm
167
+ ```
168
+
169
+ ## CI/CD Integration
170
+
171
+ Example GitHub Actions workflow:
172
+
173
+ ```yaml
174
+ name: Publish SDK
175
+
176
+ on:
177
+ push:
178
+ branches: [main]
179
+
180
+ jobs:
181
+ publish:
182
+ runs-on: ubuntu-latest
183
+ steps:
184
+ - uses: actions/checkout@v3
185
+
186
+ # Regenerate from OpenAPI (ensures freshness)
187
+ - run: make sdk-npm
188
+
189
+ # Build
190
+ - run: npm ci
191
+ working-directory: packages/auth-client-js
192
+ - run: npm run build
193
+ working-directory: packages/auth-client-js
194
+
195
+ # Publish to npm
196
+ - run: npm publish
197
+ working-directory: packages/auth-client-js
198
+ env:
199
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
200
+ ```
201
+
202
+ ## Synchronization with webfront/bridge
203
+
204
+ Both the SDK and the webfront bridge are generated from the same OpenAPI spec:
205
+
206
+ - **webfront/bridge**: Direct use in the Angular app
207
+ - **packages/auth-client-js**: Packaged for reuse in other projects
208
+
209
+ They should be **identical in functionality** but:
210
+ - Bridge: used directly via imports (`import { provideBridge }`)
211
+ - SDK: packaged as npm module (`npm install @obsidiane/auth-client-js`)
212
+
213
+ Both can coexist in your monorepo.
214
+
215
+ ## Maintenance Checklist
216
+
217
+ - [ ] Run `make sdks` after API changes
218
+ - [ ] Version bump before publishing (in `package.json`)
219
+ - [ ] Run `npm run build` before release
220
+ - [ ] Commit `package.json` + `package-lock.json` changes
221
+
222
+ ## References
223
+
224
+ - [Meridiane Documentation](https://github.com/obsidiane-lab/meridiane)
225
+ - [Angular HTTP Client](https://angular.io/guide/http)
226
+ - [Angular Dependency Injection](https://angular.io/guide/dependency-injection)
package/README.md ADDED
@@ -0,0 +1,181 @@
1
+ # @obsidiane/auth-client-js
2
+
3
+ Obsidiane Auth API Client for Angular/TypeScript.
4
+
5
+ A lightweight SDK for interacting with the Obsidiane Auth service, featuring:
6
+ - **Bridge from Meridiane**: Auto-generated HTTP client, facades, and TypeScript models from OpenAPI spec
7
+ - **Zero runtime dependencies**: Uses native browser APIs and Angular's built-in HTTP client
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install @obsidiane/auth-client-js
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ### 1. Setup in your Angular app
18
+
19
+ ```typescript
20
+ // app.config.ts
21
+ import { provideBridge } from '@obsidiane/auth-client-js';
22
+
23
+ export const appConfig: ApplicationConfig = {
24
+ providers: [
25
+ // Provide the Meridiane bridge (HTTP client + facades)
26
+ provideBridge({
27
+ baseUrl: 'http://localhost:9000',
28
+ }),
29
+
30
+ ],
31
+ };
32
+ ```
33
+
34
+ ### 2. Use in your services
35
+
36
+ ```typescript
37
+ import { Injectable, inject } from '@angular/core';
38
+ import { FacadeFactory } from '@obsidiane/auth-client-js';
39
+ import type { UserUserRead } from '@obsidiane/auth-client-js';
40
+
41
+ @Injectable({ providedIn: 'root' })
42
+ export class AuthService {
43
+ private readonly factory = inject(FacadeFactory);
44
+ private readonly users = this.factory.create<UserUserRead>({
45
+ url: '/api/users',
46
+ });
47
+
48
+ getUsers() {
49
+ return this.users.getCollection$();
50
+ }
51
+
52
+ getUser(id: string) {
53
+ return this.users.get$(id);
54
+ }
55
+ }
56
+ ```
57
+
58
+ ## API Reference
59
+
60
+ ### Bridge Exports (from Meridiane)
61
+
62
+ The bridge provides:
63
+
64
+ #### **FacadeFactory**
65
+ Create resource facades for CRUD operations:
66
+
67
+ ```typescript
68
+ const facade = factory.create<MyResource>({
69
+ url: '/api/resources',
70
+ });
71
+
72
+ // Collection operations
73
+ facade.getCollection$({ page: 1, itemsPerPage: 20 })
74
+
75
+ // Item operations
76
+ facade.get$(id)
77
+ facade.post$(item)
78
+ facade.patch$(id, partialItem)
79
+ facade.delete$(id)
80
+ ```
81
+
82
+ #### **BridgeFacade**
83
+ Low-level HTTP client for custom endpoints:
84
+
85
+ ```typescript
86
+ const bridge = inject(BridgeFacade);
87
+
88
+ // Custom GET
89
+ bridge.get$<MyResponse>('/api/custom-endpoint')
90
+
91
+ // Custom POST
92
+ bridge.post$<MyResponse>('/api/custom-endpoint', { payload })
93
+ ```
94
+
95
+ #### **Models**
96
+ TypeScript types for all API resources:
97
+
98
+ ```typescript
99
+ import type {
100
+ UserUserRead,
101
+ Auth,
102
+ InviteUserInviteRead,
103
+ // ... all other models
104
+ } from '@obsidiane/auth-client-js';
105
+ ```
106
+
107
+ ## How It Works
108
+
109
+ ### Bridge Layer (Meridiane)
110
+
111
+ The bridge is auto-generated from the OpenAPI spec (`/api/docs.json`) using Meridiane. It includes:
112
+
113
+ - **HTTP Client**: Optimized fetch wrapper with automatic retries and deduplication
114
+ - **Facades**: Resource-based API for common CRUD operations
115
+ - **Models**: TypeScript interfaces for all API request/response types
116
+ - **Interceptors**: Built-in handling for headers, errors, and single-flight requests
117
+
118
+ ## Development
119
+
120
+ ### Regenerate from OpenAPI
121
+
122
+ The bridge is regenerated from the backend OpenAPI spec:
123
+
124
+ ```bash
125
+ # From project root
126
+ make sdk-npm
127
+
128
+ # Or manually
129
+ npx meridiane generate "@obsidiane/auth-client-js" \
130
+ --spec http://localhost:9000/api/docs.json \
131
+ --formats "application/ld+json"
132
+ ```
133
+
134
+ ### Build
135
+
136
+ ```bash
137
+ npm run build
138
+ # Output: ./dist/
139
+ ```
140
+
141
+ ### Publish
142
+
143
+ ```bash
144
+ npm publish
145
+ ```
146
+
147
+ ## Migration from Old SDK
148
+
149
+ The old SDK (`packages/auth-client-js.backup/`) had 758 lines of custom HTTP client code. This new version:
150
+
151
+ - **Removes**: Custom HTTP client, request builders, response decoders
152
+ - **Adds**: Meridiane bridge for consistency with the main app
153
+
154
+ The API remains similar but cleaner:
155
+
156
+ ```typescript
157
+ // Old way (custom client)
158
+ import { AuthClient } from '@obsidiane/auth-client-js';
159
+ const client = new AuthClient({ baseUrl: 'http://localhost:9000' });
160
+ client.auth.login(email, password);
161
+
162
+ // New way (Meridiane bridge)
163
+ import { BridgeFacade } from '@obsidiane/auth-client-js';
164
+ const bridge = inject(BridgeFacade);
165
+ bridge.post$('/api/auth/login', { email, password });
166
+ ```
167
+
168
+ ## Architecture
169
+
170
+ ```
171
+ @obsidiane/auth-client-js
172
+ ├── Bridge (Meridiane-generated)
173
+ │ ├── HTTP client (fetch wrapper)
174
+ │ ├── Facades (FacadeFactory, BridgeFacade)
175
+ │ ├── Models (TypeScript interfaces)
176
+ │ └── Interceptors
177
+ ```
178
+
179
+ ## License
180
+
181
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Obsidiane Auth Client for Angular/TypeScript
3
+ *
4
+ * Main entry point that exports the Meridiane-generated bridge:
5
+ * - HTTP client (BridgeFacade)
6
+ * - Facades (resource-based API)
7
+ * - Models (auto-generated types from OpenAPI)
8
+ */
9
+ export * from './src/public-api';
10
+ //# sourceMappingURL=index.js.map
package/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Obsidiane Auth Client for Angular/TypeScript
3
+ *
4
+ * Main entry point that exports the Meridiane-generated bridge:
5
+ * - HTTP client (BridgeFacade)
6
+ * - Facades (resource-based API)
7
+ * - Models (auto-generated types from OpenAPI)
8
+ */
9
+
10
+ export * from './src/public-api';
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@obsidiane/auth-client-js",
3
+ "version": "1.0.1",
4
+ "description": "Obsidiane Auth API Client - Angular/TypeScript SDK",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "prepublish": "npm run build"
17
+ },
18
+ "peerDependencies": {
19
+ "@angular/common": "^20.0.0",
20
+ "@angular/core": "^20.0.0",
21
+ "rxjs": "^7.8.0"
22
+ },
23
+ "dependencies": {
24
+ "tslib": "^2.8.1"
25
+ },
26
+ "devDependencies": {
27
+ "@angular/common": "^20.0.2",
28
+ "@angular/core": "^20.0.2",
29
+ "rxjs": "^7.8.1",
30
+ "typescript": "^5.8.3"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/obsidiane-lab/obsidiane-auth.git",
35
+ "directory": "packages/auth-client-js"
36
+ },
37
+ "license": "MIT",
38
+ "keywords": [
39
+ "auth",
40
+ "authentication",
41
+ "obsidiane",
42
+ "api-client",
43
+ "angular"
44
+ ]
45
+ }
@@ -0,0 +1,84 @@
1
+ import {HttpClient} from '@angular/common/http';
2
+ import {Observable} from 'rxjs';
3
+ import {toHttpParams} from './query-builder';
4
+ import {buildHttpRequestOptions} from './http-request.options';
5
+ import {
6
+ Collection,
7
+ HttpCallOptions,
8
+ HttpRequestConfig,
9
+ Iri,
10
+ IriRequired,
11
+ Item,
12
+ AnyQuery,
13
+ ResourceRepository,
14
+ } from '../../ports/resource-repository.port';
15
+ import {resolveUrl} from '../../utils/url';
16
+
17
+ export class ApiPlatformRestRepository<T extends Item> implements ResourceRepository<T> {
18
+ constructor(
19
+ private readonly http: HttpClient,
20
+ private readonly apiBase: string,
21
+ private readonly resourcePath: Iri,
22
+ private readonly withCredentialsDefault: boolean,
23
+ ) {
24
+ }
25
+
26
+ getCollection$(query?: AnyQuery, opts?: HttpCallOptions): Observable<Collection<T>> {
27
+ const params = toHttpParams(query);
28
+ return this.http.get<Collection<T>>(this.resolveUrl(this.resourcePath), {
29
+ params,
30
+ headers: opts?.headers,
31
+ withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
32
+ });
33
+ }
34
+
35
+ get$(iri: IriRequired, opts?: HttpCallOptions): Observable<T> {
36
+ return this.http.get<T>(this.resolveUrl(iri), {
37
+ headers: opts?.headers,
38
+ withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
39
+ });
40
+ }
41
+
42
+ post$(payload: Partial<T>, opts?: HttpCallOptions): Observable<T> {
43
+ return this.http.post<T>(this.resolveUrl(this.resourcePath), payload, {
44
+ headers: opts?.headers,
45
+ withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
46
+ });
47
+ }
48
+
49
+ patch$(iri: IriRequired, changes: Partial<T>, opts?: HttpCallOptions): Observable<T> {
50
+ return this.http.patch<T>(this.resolveUrl(iri), changes, {
51
+ headers: opts?.headers,
52
+ withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
53
+ });
54
+ }
55
+
56
+ put$(iri: IriRequired, payload: Partial<T>, opts?: HttpCallOptions): Observable<T> {
57
+ return this.http.put<T>(this.resolveUrl(iri), payload, {
58
+ headers: opts?.headers,
59
+ withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
60
+ });
61
+ }
62
+
63
+ delete$(iri: IriRequired, opts?: HttpCallOptions): Observable<void> {
64
+ return this.http.delete<void>(this.resolveUrl(iri), {
65
+ headers: opts?.headers,
66
+ withCredentials: opts?.withCredentials ?? this.withCredentialsDefault,
67
+ });
68
+ }
69
+
70
+ request$<R = unknown, B = unknown>(req: HttpRequestConfig<B>): Observable<R> {
71
+ // Low-level escape hatch for non-standard endpoints (custom controllers, uploads, etc.).
72
+ const {method, url} = req;
73
+
74
+ const targetUrl = this.resolveUrl(url ?? this.resourcePath);
75
+ const mergedOptions = buildHttpRequestOptions(req, {withCredentialsDefault: this.withCredentialsDefault});
76
+ return this.http.request<R>(method, targetUrl, mergedOptions as {observe: 'body'});
77
+ }
78
+
79
+ private resolveUrl(path?: Iri): string {
80
+ const effectivePath = path ?? this.resourcePath;
81
+ if (!effectivePath) throw new Error('ApiPlatformRestRepository: missing url and resourcePath');
82
+ return resolveUrl(this.apiBase, effectivePath);
83
+ }
84
+ }
@@ -0,0 +1,21 @@
1
+ import {HttpRequestConfig} from '../../ports/resource-repository.port';
2
+ import {toHttpParams} from './query-builder';
3
+
4
+ export function buildHttpRequestOptions(
5
+ req: HttpRequestConfig,
6
+ {withCredentialsDefault}: {withCredentialsDefault: boolean}
7
+ ): Record<string, unknown> {
8
+ const {query, body, headers, responseType, withCredentials, options = {}} = req;
9
+ const mergedOptions: Record<string, unknown> = {...options};
10
+
11
+ if (headers) mergedOptions['headers'] = headers;
12
+ if (query) mergedOptions['params'] = toHttpParams(query);
13
+ if (body !== undefined) mergedOptions['body'] = body;
14
+
15
+ mergedOptions['responseType'] = (responseType ?? (mergedOptions['responseType'] as any) ?? 'json') as any;
16
+ mergedOptions['withCredentials'] =
17
+ withCredentials ?? (mergedOptions['withCredentials'] as any) ?? withCredentialsDefault;
18
+ mergedOptions['observe'] = 'body';
19
+
20
+ return mergedOptions;
21
+ }
@@ -0,0 +1,43 @@
1
+ import {HttpParams} from '@angular/common/http';
2
+ import {AnyQuery, Query, QueryParamValue} from '../../ports/resource-repository.port';
3
+
4
+ export function toHttpParams(q: AnyQuery | undefined): HttpParams {
5
+ if (!q) return new HttpParams();
6
+ if (q instanceof HttpParams) return q;
7
+
8
+ const fromObject: Record<string, string | string[]> = {};
9
+
10
+ const consumed = new Set<string>();
11
+ const maybeQuery = q as Query;
12
+ if (maybeQuery.page != null) {
13
+ fromObject['page'] = String(maybeQuery.page);
14
+ consumed.add('page');
15
+ }
16
+ if (maybeQuery.itemsPerPage != null) {
17
+ fromObject['itemsPerPage'] = String(maybeQuery.itemsPerPage);
18
+ consumed.add('itemsPerPage');
19
+ }
20
+
21
+ if (q.filters) {
22
+ consumed.add('filters');
23
+ for (const [k, v] of Object.entries(q.filters)) {
24
+ assign(fromObject, k, v);
25
+ }
26
+ }
27
+
28
+ for (const [k, v] of Object.entries(q as Record<string, QueryParamValue>)) {
29
+ if (consumed.has(k)) continue;
30
+ assign(fromObject, k, v);
31
+ }
32
+
33
+ return new HttpParams({fromObject});
34
+ }
35
+
36
+ function assign(target: Record<string, string | string[]>, key: string, value: QueryParamValue | undefined) {
37
+ if (value == null) return;
38
+ if (Array.isArray(value)) {
39
+ target[key] = value.map(String);
40
+ } else {
41
+ target[key] = String(value);
42
+ }
43
+ }
@@ -0,0 +1,70 @@
1
+ import {ReplaySubject, Subject} from 'rxjs';
2
+ import {SseEvent, SseOptions, RealtimeStatus} from '../../ports/realtime.port';
3
+ import {BridgeLogger} from '../../bridge.types';
4
+
5
+ export class EventSourceWrapper {
6
+ private es?: EventSource;
7
+
8
+ private readonly statusSub = new ReplaySubject<RealtimeStatus>(1);
9
+ private readonly eventSub = new Subject<SseEvent>();
10
+
11
+ readonly status$ = this.statusSub.asObservable();
12
+ readonly events$ = this.eventSub.asObservable();
13
+
14
+ constructor(
15
+ private readonly url: string,
16
+ private readonly opts: SseOptions = {},
17
+ private readonly logger?: BridgeLogger,
18
+ ) {
19
+ this.setState('closed');
20
+ this.log('[SSE] init', {url, withCredentials: !!opts.withCredentials});
21
+ }
22
+
23
+ open(): void {
24
+ if (this.es) {
25
+ this.log('[SSE] open() ignored: already open');
26
+ return;
27
+ }
28
+
29
+ this.setState('connecting');
30
+ this.log('[SSE] open', {url: this.url});
31
+
32
+ const es = new EventSource(this.url, {
33
+ withCredentials: !!this.opts.withCredentials,
34
+ });
35
+ this.es = es;
36
+
37
+ es.onopen = () => {
38
+ this.setState('connected');
39
+ };
40
+
41
+ es.onmessage = (ev) => {
42
+ this.eventSub.next({type: 'message', data: ev.data, lastEventId: ev.lastEventId || undefined});
43
+ };
44
+
45
+ es.onerror = () => {
46
+ // The browser will retry automatically. We stay in "connecting".
47
+ this.log('[SSE] error');
48
+ this.setState('connecting');
49
+ };
50
+ }
51
+
52
+ close(): void {
53
+ if (this.es) {
54
+ this.es.close();
55
+ this.es = undefined;
56
+ this.log('[SSE] closed');
57
+ }
58
+ this.setState('closed');
59
+ }
60
+
61
+ // ──────────────── internals ────────────────
62
+
63
+ private setState(state: RealtimeStatus): void {
64
+ this.statusSub.next(state);
65
+ }
66
+
67
+ private log(...args: unknown[]): void {
68
+ this.logger?.debug?.(...args);
69
+ }
70
+ }