@obsidiane/meridiane 2.0.13 → 3.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 (59) hide show
  1. package/README.md +33 -66
  2. package/cli.js +1 -10
  3. package/package.json +1 -1
  4. package/src/application/bridge-workflow.js +23 -66
  5. package/src/application/generate-bridge.js +32 -36
  6. package/src/application/run-build.js +8 -30
  7. package/src/application/run-dev.js +8 -63
  8. package/src/application/run-generate.js +8 -47
  9. package/src/domain/bridge-options.js +10 -63
  10. package/src/generators/bridge/write-bridge-files.js +11 -29
  11. package/src/generators/models/interface-schema.js +3 -20
  12. package/src/generators/models/model-selection.js +67 -106
  13. package/src/generators/models/oas-contract.js +1 -108
  14. package/src/generators/models/oas-schema-name.js +1 -19
  15. package/src/generators/models/openapi-to-models.js +51 -42
  16. package/src/generators/models/schema-utils.js +0 -34
  17. package/src/generators/models/type-resolver.js +2 -20
  18. package/src/generators/models/utils.js +0 -11
  19. package/src/generators/models/write-model-files.js +5 -15
  20. package/src/infrastructure/dev-project.js +0 -33
  21. package/src/infrastructure/files.js +15 -0
  22. package/src/infrastructure/local-package-install.js +2 -2
  23. package/src/infrastructure/logger.js +0 -2
  24. package/templates/angular-bridge/README.md +29 -281
  25. package/templates/angular-bridge/package.json +2 -2
  26. package/templates/angular-bridge/src/lib/bridge/rest/http-request.options.ts +10 -19
  27. package/templates/angular-bridge/src/lib/bridge/sse/mercure-hub.ts +449 -0
  28. package/templates/angular-bridge/src/lib/bridge/sse/mercure-url.ts +28 -0
  29. package/templates/angular-bridge/src/lib/bridge/sse/resource-topic.ts +83 -0
  30. package/templates/angular-bridge/src/lib/bridge-options.ts +21 -17
  31. package/templates/angular-bridge/src/lib/bridge.types.ts +22 -33
  32. package/templates/angular-bridge/src/lib/facades/bridge.facade.ts +58 -134
  33. package/templates/angular-bridge/src/lib/facades/facade.factory.ts +25 -25
  34. package/templates/angular-bridge/src/lib/facades/facade.interface.ts +4 -9
  35. package/templates/angular-bridge/src/lib/facades/resource.facade.ts +38 -61
  36. package/templates/angular-bridge/src/lib/interceptors/bearer-auth.interceptor.ts +8 -12
  37. package/templates/angular-bridge/src/lib/interceptors/bridge-defaults.interceptor.ts +19 -11
  38. package/templates/angular-bridge/src/lib/interceptors/content-type.interceptor.ts +5 -17
  39. package/templates/angular-bridge/src/lib/interceptors/singleflight.interceptor.ts +28 -24
  40. package/templates/angular-bridge/src/lib/ports/realtime.port.ts +34 -53
  41. package/templates/angular-bridge/src/lib/ports/resource-repository.port.ts +31 -21
  42. package/templates/angular-bridge/src/lib/provide-bridge.ts +14 -38
  43. package/templates/angular-bridge/src/lib/tokens.ts +4 -15
  44. package/templates/angular-bridge/src/lib/utils/request-url.ts +4 -6
  45. package/templates/angular-bridge/src/lib/utils/url.ts +32 -9
  46. package/templates/angular-bridge/src/public-api.ts +0 -1
  47. package/templates/angular-bridge/tsconfig.lib.json +0 -3
  48. package/src/README.md +0 -9
  49. package/src/infrastructure/json.js +0 -71
  50. package/templates/angular-bridge/src/lib/bridge/rest/api-platform.adapter.ts +0 -82
  51. package/templates/angular-bridge/src/lib/bridge/sse/eventsource-wrapper.ts +0 -70
  52. package/templates/angular-bridge/src/lib/bridge/sse/mercure-connections.ts +0 -255
  53. package/templates/angular-bridge/src/lib/bridge/sse/mercure-payload.ts +0 -82
  54. package/templates/angular-bridge/src/lib/bridge/sse/mercure-topic.mapper.ts +0 -48
  55. package/templates/angular-bridge/src/lib/bridge/sse/mercure-url.builder.ts +0 -39
  56. package/templates/angular-bridge/src/lib/bridge/sse/mercure.adapter.ts +0 -358
  57. package/templates/angular-bridge/src/lib/bridge/sse/ref-count-topic.registry.ts +0 -36
  58. package/templates/angular-bridge/src/lib/bridge/sse/subscription-claims.ts +0 -76
  59. package/templates/angular-bridge/src/lib/utils/iri.ts +0 -6
@@ -1,82 +0,0 @@
1
- import {HttpClient} from '@angular/common/http';
2
- import {Observable} from 'rxjs';
3
- import {toHttpParams} from './query-builder';
4
- import {buildHttpRequestOptions, resolveHttpCallOptions} 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 httpWithCredentialsDefault: boolean,
23
- ) {
24
- }
25
-
26
- getCollection$(query?: AnyQuery, options?: HttpCallOptions): Observable<Collection<T>> {
27
- return this.http.get<Collection<T>>(this.resolveUrl(this.resourcePath), {
28
- ...resolveHttpCallOptions(options, this.httpWithCredentialsDefault),
29
- params: toHttpParams(query),
30
- });
31
- }
32
-
33
- get$(iri: IriRequired, options?: HttpCallOptions): Observable<T> {
34
- return this.http.get<T>(this.resolveUrl(iri), resolveHttpCallOptions(options, this.httpWithCredentialsDefault));
35
- }
36
-
37
- post$(payload: Partial<T>, options?: HttpCallOptions): Observable<T> {
38
- return this.http.post<T>(
39
- this.resolveUrl(this.resourcePath),
40
- payload,
41
- resolveHttpCallOptions(options, this.httpWithCredentialsDefault)
42
- );
43
- }
44
-
45
- patch$(iri: IriRequired, changes: Partial<T>, options?: HttpCallOptions): Observable<T> {
46
- return this.http.patch<T>(
47
- this.resolveUrl(iri),
48
- changes,
49
- resolveHttpCallOptions(options, this.httpWithCredentialsDefault)
50
- );
51
- }
52
-
53
- put$(iri: IriRequired, payload: Partial<T>, options?: HttpCallOptions): Observable<T> {
54
- return this.http.put<T>(
55
- this.resolveUrl(iri),
56
- payload,
57
- resolveHttpCallOptions(options, this.httpWithCredentialsDefault)
58
- );
59
- }
60
-
61
- delete$(iri: IriRequired, options?: HttpCallOptions): Observable<void> {
62
- return this.http.delete<void>(
63
- this.resolveUrl(iri),
64
- resolveHttpCallOptions(options, this.httpWithCredentialsDefault)
65
- );
66
- }
67
-
68
- request$<R = unknown, B = unknown>(request: HttpRequestConfig<B>): Observable<R> {
69
- // Low-level escape hatch for non-standard endpoints (custom controllers, uploads, etc.).
70
- const {method, url} = request;
71
-
72
- const targetUrl = this.resolveUrl(url ?? this.resourcePath);
73
- const mergedOptions = buildHttpRequestOptions(request, {withCredentialsDefault: this.httpWithCredentialsDefault});
74
- return this.http.request<R>(method, targetUrl, mergedOptions as {observe: 'body'});
75
- }
76
-
77
- private resolveUrl(path?: Iri): string {
78
- const effectivePath = path ?? this.resourcePath;
79
- if (!effectivePath) throw new Error('ApiPlatformRestRepository: missing url and resourcePath');
80
- return resolveUrl(this.apiBase, effectivePath);
81
- }
82
- }
@@ -1,70 +0,0 @@
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
- }
@@ -1,255 +0,0 @@
1
- import {BehaviorSubject, Observable, Subject} from 'rxjs';
2
- import {takeUntil} from 'rxjs/operators';
3
- import {
4
- BridgeLogger,
5
- MercureConnectionMode,
6
- RealtimeDiagnostics,
7
- } from '../../bridge.types';
8
- import {RealtimeStatus} from '../../ports/realtime.port';
9
- import {EventSourceWrapper} from './eventsource-wrapper';
10
- import {MercureUrlBuilder} from './mercure-url.builder';
11
-
12
- export type IncomingMessage = {type: string; data: string; lastEventId?: string};
13
-
14
- type ConnectionScope = 'shared' | 'dedicated';
15
-
16
- type ManagedConnection = {
17
- id: string;
18
- key: string;
19
- scope: ConnectionScope;
20
- groupId?: string;
21
- topics: ReadonlySet<string>;
22
- urlLength: number;
23
- wrapper: EventSourceWrapper;
24
- stop$: Subject<void>;
25
- status: RealtimeStatus;
26
- };
27
-
28
- type ConnectionTarget = Subject<IncomingMessage>;
29
-
30
- type ConnectionManagerOptions = {
31
- hubUrl?: string;
32
- withCredentials: boolean;
33
- mode: MercureConnectionMode;
34
- maxUrlLength: number;
35
- logger?: BridgeLogger;
36
- destroy$: Observable<void>;
37
- nextId: (prefix: string) => string;
38
- };
39
-
40
- export class MercureConnectionManager {
41
- private readonly urlBuilder = new MercureUrlBuilder();
42
- private readonly connections = new Map<string, ManagedConnection>();
43
- private readonly sharedConnections = new Map<string, ManagedConnection>();
44
- private readonly dedicatedGroups = new Map<string, Set<string>>();
45
- private readonly lastEventIdByKey = new Map<string, string>();
46
-
47
- private readonly statusSubject = new BehaviorSubject<RealtimeStatus>('closed');
48
- private readonly diagnosticsSubject: BehaviorSubject<RealtimeDiagnostics>;
49
-
50
- readonly status$ = this.statusSubject.asObservable();
51
- readonly diagnostics$: Observable<RealtimeDiagnostics>;
52
-
53
- constructor(private readonly options: ConnectionManagerOptions) {
54
- this.diagnosticsSubject = new BehaviorSubject<RealtimeDiagnostics>(this.createDiagnostics());
55
- this.diagnostics$ = this.diagnosticsSubject.asObservable();
56
- }
57
-
58
- reconcileShared(topics: Iterable<string>, target: ConnectionTarget): void {
59
- if (!this.options.hubUrl) {
60
- this.closeShared();
61
- return;
62
- }
63
-
64
- const desiredConnections = new Map<string, ReadonlySet<string>>();
65
- for (const partition of this.partitionTopics(topics)) {
66
- const partitionTopics = new Set(partition);
67
- desiredConnections.set(this.connectionKey(partitionTopics, 'shared'), partitionTopics);
68
- }
69
-
70
- for (const [key, connection] of [...this.sharedConnections.entries()]) {
71
- if (!desiredConnections.has(key)) this.close(connection.id);
72
- }
73
-
74
- for (const [key, partitionTopics] of desiredConnections) {
75
- if (this.sharedConnections.has(key)) continue;
76
- const connection = this.open({key, scope: 'shared', topics: partitionTopics, target});
77
- this.sharedConnections.set(key, connection);
78
- }
79
- }
80
-
81
- openDedicated(groupId: string, topics: Iterable<string>, target: ConnectionTarget): void {
82
- this.dedicatedGroups.set(groupId, new Set());
83
-
84
- for (const partition of this.partitionTopics(topics)) {
85
- const partitionTopics = new Set(partition);
86
- const key = this.connectionKey(partitionTopics, 'dedicated', groupId);
87
- const connection = this.open({key, scope: 'dedicated', groupId, topics: partitionTopics, target});
88
- this.dedicatedGroups.get(groupId)?.add(connection.id);
89
- }
90
- }
91
-
92
- closeShared(): void {
93
- for (const connection of [...this.sharedConnections.values()]) this.close(connection.id);
94
- }
95
-
96
- closeDedicated(groupId: string): void {
97
- const connectionIds = this.dedicatedGroups.get(groupId);
98
- if (!connectionIds) return;
99
-
100
- for (const connectionId of [...connectionIds]) this.close(connectionId);
101
- this.dedicatedGroups.delete(groupId);
102
- }
103
-
104
- closeAll(): void {
105
- for (const connection of [...this.connections.values()]) this.close(connection.id);
106
- this.sharedConnections.clear();
107
- this.dedicatedGroups.clear();
108
- this.updateStatus();
109
- }
110
-
111
- complete(): void {
112
- this.statusSubject.complete();
113
- this.diagnosticsSubject.complete();
114
- }
115
-
116
- private partitionTopics(topics: Iterable<string>): string[][] {
117
- if (!this.options.hubUrl) return [];
118
- return this.urlBuilder.partitionTopics(
119
- this.options.hubUrl,
120
- topics,
121
- this.options.maxUrlLength,
122
- this.options.mode === 'auto'
123
- );
124
- }
125
-
126
- private open(params: {
127
- key: string;
128
- scope: ConnectionScope;
129
- topics: ReadonlySet<string>;
130
- target: ConnectionTarget;
131
- groupId?: string;
132
- }): ManagedConnection {
133
- const {hubUrl, withCredentials, logger, destroy$, nextId} = this.options;
134
- if (!hubUrl) throw new Error('Mercure hub URL is required to open a realtime connection.');
135
-
136
- const lastEventId = this.lastEventIdByKey.get(params.key);
137
- const url = this.urlBuilder.build(hubUrl, params.topics, lastEventId);
138
-
139
- logger?.debug?.('[Mercure] connect', {
140
- mode: this.options.mode,
141
- scope: params.scope,
142
- key: params.key,
143
- urlLength: url.length,
144
- topics: [...params.topics],
145
- lastEventId,
146
- });
147
-
148
- const wrapper = new EventSourceWrapper(url, {withCredentials}, logger);
149
- const stop$ = new Subject<void>();
150
- const connection: ManagedConnection = {
151
- id: nextId('conn'),
152
- key: params.key,
153
- scope: params.scope,
154
- groupId: params.groupId,
155
- topics: params.topics,
156
- urlLength: url.length,
157
- wrapper,
158
- stop$,
159
- status: 'connecting',
160
- };
161
-
162
- this.connections.set(connection.id, connection);
163
-
164
- wrapper.status$
165
- .pipe(takeUntil(stop$), takeUntil(destroy$))
166
- .subscribe((status) => {
167
- connection.status = status;
168
- this.updateStatus();
169
- });
170
-
171
- wrapper.events$
172
- .pipe(takeUntil(stop$), takeUntil(destroy$))
173
- .subscribe((event) => {
174
- if (event.lastEventId) this.lastEventIdByKey.set(params.key, event.lastEventId);
175
- params.target.next({type: event.type, data: event.data, lastEventId: event.lastEventId});
176
- });
177
-
178
- try {
179
- wrapper.open();
180
- this.updateStatus();
181
- return connection;
182
- } catch (error) {
183
- this.close(connection.id);
184
- throw error;
185
- }
186
- }
187
-
188
- private close(connectionId: string): void {
189
- const connection = this.connections.get(connectionId);
190
- if (!connection) return;
191
-
192
- connection.status = 'closed';
193
- connection.wrapper.close();
194
- connection.stop$.next();
195
- connection.stop$.complete();
196
- this.connections.delete(connectionId);
197
-
198
- if (connection.scope === 'shared') this.sharedConnections.delete(connection.key);
199
- if (connection.scope === 'dedicated' && connection.groupId) {
200
- const group = this.dedicatedGroups.get(connection.groupId);
201
- group?.delete(connection.id);
202
- if (group?.size === 0) this.dedicatedGroups.delete(connection.groupId);
203
- this.lastEventIdByKey.delete(connection.key);
204
- }
205
-
206
- this.updateStatus();
207
- }
208
-
209
- private connectionKey(topics: ReadonlySet<string>, scope: ConnectionScope, groupId = ''): string {
210
- const {hubUrl = '', withCredentials, mode} = this.options;
211
- return [
212
- scope,
213
- groupId,
214
- hubUrl,
215
- withCredentials ? '1' : '0',
216
- mode,
217
- [...topics].sort().join('|'),
218
- ].join('::');
219
- }
220
-
221
- private updateStatus(): void {
222
- const statuses = [...this.connections.values()].map((connection) => connection.status);
223
- this.statusSubject.next(aggregateStatus(statuses));
224
- this.diagnosticsSubject.next(this.createDiagnostics());
225
- }
226
-
227
- private createDiagnostics(): RealtimeDiagnostics {
228
- const connections = [...this.connections.values()]
229
- .map((connection) => ({
230
- id: connection.id,
231
- scope: connection.scope,
232
- status: connection.status,
233
- topicCount: connection.topics.size,
234
- topics: [...connection.topics].sort(),
235
- urlLength: connection.urlLength,
236
- }))
237
- .sort((left, right) => left.scope.localeCompare(right.scope) || left.id.localeCompare(right.id));
238
- const sharedConnections = connections.filter((connection) => connection.scope === 'shared').length;
239
-
240
- return {
241
- mode: this.options.mode,
242
- maxUrlLength: this.options.maxUrlLength,
243
- totalConnections: connections.length,
244
- sharedConnections,
245
- dedicatedConnections: connections.length - sharedConnections,
246
- connections,
247
- };
248
- }
249
- }
250
-
251
- function aggregateStatus(statuses: RealtimeStatus[]): RealtimeStatus {
252
- if (statuses.includes('connected')) return 'connected';
253
- if (statuses.includes('connecting')) return 'connecting';
254
- return 'closed';
255
- }
@@ -1,82 +0,0 @@
1
- import {BridgeLogger} from '../../bridge.types';
2
- import {SubscribeFilter} from '../../ports/realtime.port';
3
- import {Item} from '../../ports/resource-repository.port';
4
- import {MercureTopicMapper} from './mercure-topic.mapper';
5
-
6
- export function parseMercurePayload(raw: string, logger?: BridgeLogger): Item | undefined {
7
- try {
8
- const value: unknown = JSON.parse(raw);
9
- return isRecord(value) ? value as Item : undefined;
10
- } catch {
11
- logger?.debug?.('[Mercure] invalid JSON payload ignored', {raw});
12
- return undefined;
13
- }
14
- }
15
-
16
- export function createPayloadTopicMatcher(
17
- iris: string[],
18
- filter: SubscribeFilter | undefined,
19
- topicMapper: MercureTopicMapper
20
- ): (payload: Item) => string | undefined {
21
- const subscribedTopics = iris.map((iri) => normalizeIri(topicMapper.toPayloadIri(iri)));
22
- const fieldPaths = [
23
- ...(filter?.fields?.filter((field): field is string => typeof field === 'string' && field.length > 0) ?? []),
24
- ...(typeof filter?.field === 'string' && filter.field.length > 0 ? [filter.field] : []),
25
- ];
26
- const includeSelf = filter?.includeSelf ?? fieldPaths.length === 0;
27
-
28
- return (payload) => {
29
- for (const fieldPath of fieldPaths) {
30
- const relationIris = readRelationIris(payload, fieldPath)
31
- .map((iri) => normalizeIri(topicMapper.toPayloadIri(iri)));
32
- for (const relationIri of relationIris) {
33
- const match = findClosestTopic(relationIri, subscribedTopics);
34
- if (match) return match;
35
- }
36
- }
37
-
38
- if (!includeSelf) return undefined;
39
-
40
- const payloadIri = payload['@id'];
41
- if (typeof payloadIri !== 'string') return undefined;
42
- return findClosestTopic(normalizeIri(topicMapper.toPayloadIri(payloadIri)), subscribedTopics);
43
- };
44
- }
45
-
46
- function readRelationIris(payload: Item, path: string): string[] {
47
- const value = path
48
- .split('.')
49
- .filter(Boolean)
50
- .reduce<unknown>((current, key) => isIndexable(current) ? current[key] : undefined, payload);
51
-
52
- return extractIris(value);
53
- }
54
-
55
- function extractIris(value: unknown): string[] {
56
- if (!value) return [];
57
- if (typeof value === 'string') return value.length > 0 ? [value] : [];
58
- if (isIndexable(value) && typeof value['@id'] === 'string') return [value['@id']];
59
- if (Array.isArray(value)) return value.flatMap(extractIris);
60
- return [];
61
- }
62
-
63
- function findClosestTopic(candidate: string, subscribedTopics: string[]): string | undefined {
64
- let closest: string | undefined;
65
- for (const topic of subscribedTopics) {
66
- if (candidate !== topic && !candidate.startsWith(`${topic}/`)) continue;
67
- if (!closest || topic.length > closest.length) closest = topic;
68
- }
69
- return closest;
70
- }
71
-
72
- function normalizeIri(iri: string): string {
73
- return iri.endsWith('/') ? iri.slice(0, -1) : iri;
74
- }
75
-
76
- function isRecord(value: unknown): value is Record<string, unknown> {
77
- return !!value && typeof value === 'object' && !Array.isArray(value);
78
- }
79
-
80
- function isIndexable(value: unknown): value is Record<string, unknown> {
81
- return !!value && typeof value === 'object';
82
- }
@@ -1,48 +0,0 @@
1
- import {MercureTopicMode} from '../../bridge.types';
2
-
3
- export class MercureTopicMapper {
4
- private readonly apiBaseUrl: URL;
5
-
6
- constructor(
7
- apiBase: string,
8
- private readonly mode: MercureTopicMode,
9
- ) {
10
- this.apiBaseUrl = new URL(apiBase);
11
- }
12
-
13
- /**
14
- * Canonical value used for ref-counting and as the "topic" query param value.
15
- * - mode "url": always absolute, same-origin resolved
16
- * - mode "iri": same-origin path+query+hash ("/api/..."), otherwise keep as-is
17
- */
18
- toTopic(input: string): string {
19
- if (this.mode === 'url') return this.toAbsoluteUrl(input);
20
- return this.toRelativeIriIfSameOrigin(input);
21
- }
22
-
23
- /**
24
- * Canonical value used to compare incoming payload IRIs with subscribed IRIs.
25
- * We keep payload matching stable by using same-origin relative IRIs ("/api/...").
26
- */
27
- toPayloadIri(input: string): string {
28
- return this.toRelativeIriIfSameOrigin(input);
29
- }
30
-
31
- private toAbsoluteUrl(input: string): string {
32
- try {
33
- return new URL(input, this.apiBaseUrl).toString();
34
- } catch {
35
- return input;
36
- }
37
- }
38
-
39
- private toRelativeIriIfSameOrigin(input: string): string {
40
- try {
41
- const url = new URL(input, this.apiBaseUrl);
42
- if (url.origin !== this.apiBaseUrl.origin) return input;
43
- return `${url.pathname}${url.search}${url.hash}`;
44
- } catch {
45
- return input;
46
- }
47
- }
48
- }
@@ -1,39 +0,0 @@
1
- export class MercureUrlBuilder {
2
- /**
3
- * Builds the Mercure hub URL with one `topic=` parameter per topic.
4
- * The adapter is responsible for canonicalising topics beforehand.
5
- */
6
- build(hubUrl: string, topics: ReadonlySet<string>, lastEventId?: string): string {
7
- const url = new URL(hubUrl);
8
- if (lastEventId) {
9
- url.searchParams.set('lastEventID', lastEventId);
10
- }
11
- url.searchParams.delete('topic');
12
- for (const topic of topics) {
13
- url.searchParams.append('topic', topic);
14
- }
15
- return url.toString();
16
- }
17
-
18
- partitionTopics(hubUrl: string, topics: Iterable<string>, maxUrlLength: number, allowSplit: boolean): string[][] {
19
- const sortedTopics = [...new Set(topics)].sort();
20
- if (sortedTopics.length === 0) return [];
21
- if (!allowSplit) return [sortedTopics];
22
-
23
- const partitions: string[][] = [];
24
- let current: string[] = [];
25
-
26
- for (const topic of sortedTopics) {
27
- const candidate = [...current, topic];
28
- if (current.length > 0 && this.build(hubUrl, new Set(candidate)).length > maxUrlLength) {
29
- partitions.push(current);
30
- current = [topic];
31
- } else {
32
- current = candidate;
33
- }
34
- }
35
-
36
- if (current.length > 0) partitions.push(current);
37
- return partitions;
38
- }
39
- }