@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.
- package/README.md +33 -66
- package/cli.js +1 -10
- package/package.json +1 -1
- package/src/application/bridge-workflow.js +23 -66
- package/src/application/generate-bridge.js +32 -36
- package/src/application/run-build.js +8 -30
- package/src/application/run-dev.js +8 -63
- package/src/application/run-generate.js +8 -47
- package/src/domain/bridge-options.js +10 -63
- package/src/generators/bridge/write-bridge-files.js +11 -29
- package/src/generators/models/interface-schema.js +3 -20
- package/src/generators/models/model-selection.js +67 -106
- package/src/generators/models/oas-contract.js +1 -108
- package/src/generators/models/oas-schema-name.js +1 -19
- package/src/generators/models/openapi-to-models.js +51 -42
- package/src/generators/models/schema-utils.js +0 -34
- package/src/generators/models/type-resolver.js +2 -20
- package/src/generators/models/utils.js +0 -11
- package/src/generators/models/write-model-files.js +5 -15
- package/src/infrastructure/dev-project.js +0 -33
- package/src/infrastructure/files.js +15 -0
- package/src/infrastructure/local-package-install.js +2 -2
- package/src/infrastructure/logger.js +0 -2
- package/templates/angular-bridge/README.md +29 -281
- package/templates/angular-bridge/package.json +2 -2
- package/templates/angular-bridge/src/lib/bridge/rest/http-request.options.ts +10 -19
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-hub.ts +449 -0
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-url.ts +28 -0
- package/templates/angular-bridge/src/lib/bridge/sse/resource-topic.ts +83 -0
- package/templates/angular-bridge/src/lib/bridge-options.ts +21 -17
- package/templates/angular-bridge/src/lib/bridge.types.ts +22 -33
- package/templates/angular-bridge/src/lib/facades/bridge.facade.ts +58 -134
- package/templates/angular-bridge/src/lib/facades/facade.factory.ts +25 -25
- package/templates/angular-bridge/src/lib/facades/facade.interface.ts +4 -9
- package/templates/angular-bridge/src/lib/facades/resource.facade.ts +38 -61
- package/templates/angular-bridge/src/lib/interceptors/bearer-auth.interceptor.ts +8 -12
- package/templates/angular-bridge/src/lib/interceptors/bridge-defaults.interceptor.ts +19 -11
- package/templates/angular-bridge/src/lib/interceptors/content-type.interceptor.ts +5 -17
- package/templates/angular-bridge/src/lib/interceptors/singleflight.interceptor.ts +28 -24
- package/templates/angular-bridge/src/lib/ports/realtime.port.ts +34 -53
- package/templates/angular-bridge/src/lib/ports/resource-repository.port.ts +31 -21
- package/templates/angular-bridge/src/lib/provide-bridge.ts +14 -38
- package/templates/angular-bridge/src/lib/tokens.ts +4 -15
- package/templates/angular-bridge/src/lib/utils/request-url.ts +4 -6
- package/templates/angular-bridge/src/lib/utils/url.ts +32 -9
- package/templates/angular-bridge/src/public-api.ts +0 -1
- package/templates/angular-bridge/tsconfig.lib.json +0 -3
- package/src/README.md +0 -9
- package/src/infrastructure/json.js +0 -71
- package/templates/angular-bridge/src/lib/bridge/rest/api-platform.adapter.ts +0 -82
- package/templates/angular-bridge/src/lib/bridge/sse/eventsource-wrapper.ts +0 -70
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-connections.ts +0 -255
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-payload.ts +0 -82
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-topic.mapper.ts +0 -48
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-url.builder.ts +0 -39
- package/templates/angular-bridge/src/lib/bridge/sse/mercure.adapter.ts +0 -358
- package/templates/angular-bridge/src/lib/bridge/sse/ref-count-topic.registry.ts +0 -36
- package/templates/angular-bridge/src/lib/bridge/sse/subscription-claims.ts +0 -76
- package/templates/angular-bridge/src/lib/utils/iri.ts +0 -6
|
@@ -1,358 +0,0 @@
|
|
|
1
|
-
import {inject, Injectable, OnDestroy, PLATFORM_ID} from '@angular/core';
|
|
2
|
-
import {defer, EMPTY, fromEvent, Observable, Subject} from 'rxjs';
|
|
3
|
-
import {auditTime, filter, finalize, map, takeUntil} from 'rxjs/operators';
|
|
4
|
-
import {
|
|
5
|
-
API_BASE_URL,
|
|
6
|
-
BRIDGE_LOGGER,
|
|
7
|
-
BRIDGE_MERCURE_WITH_CREDENTIALS,
|
|
8
|
-
MERCURE_CONNECTION_MODE,
|
|
9
|
-
MERCURE_HUB_URL,
|
|
10
|
-
MERCURE_MAX_URL_LENGTH,
|
|
11
|
-
MERCURE_TOPIC_MODE,
|
|
12
|
-
} from '../../tokens';
|
|
13
|
-
import {
|
|
14
|
-
RealtimeDiagnostics,
|
|
15
|
-
WatchConnectionOptions,
|
|
16
|
-
} from '../../bridge.types';
|
|
17
|
-
import {RealtimeDiagnosticsPort, RealtimeEvent, RealtimeStatus, SubscribeFilter} from '../../ports/realtime.port';
|
|
18
|
-
import {Iri, Item} from '../../ports/resource-repository.port';
|
|
19
|
-
import {isPlatformBrowser} from '@angular/common';
|
|
20
|
-
import {RefCountTopicRegistry} from './ref-count-topic.registry';
|
|
21
|
-
import {MercureTopicMapper} from './mercure-topic.mapper';
|
|
22
|
-
import {SubscriptionClaims, topicsKey} from './subscription-claims';
|
|
23
|
-
import {createPayloadTopicMatcher, parseMercurePayload} from './mercure-payload';
|
|
24
|
-
import {toIriList} from '../../utils/iri';
|
|
25
|
-
import {IncomingMessage, MercureConnectionManager} from './mercure-connections';
|
|
26
|
-
|
|
27
|
-
type SharedSubscriptionClaim = {
|
|
28
|
-
id: string;
|
|
29
|
-
topics: string[];
|
|
30
|
-
topicsKey: string;
|
|
31
|
-
stop$: Subject<void>;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
type DedicatedSubscriptionClaim = {
|
|
35
|
-
id: string;
|
|
36
|
-
groupId: string;
|
|
37
|
-
topics: string[];
|
|
38
|
-
topicsKey: string;
|
|
39
|
-
incoming: Subject<IncomingMessage>;
|
|
40
|
-
stop$: Subject<void>;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
type PreparedSubscription = {
|
|
44
|
-
iris: string[];
|
|
45
|
-
source$: Observable<IncomingMessage>;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
@Injectable({providedIn: 'root'})
|
|
49
|
-
export class MercureRealtimeAdapter implements RealtimeDiagnosticsPort, OnDestroy {
|
|
50
|
-
private readonly apiBase = inject(API_BASE_URL);
|
|
51
|
-
private readonly withCredentials = inject(BRIDGE_MERCURE_WITH_CREDENTIALS);
|
|
52
|
-
private readonly platformId = inject(PLATFORM_ID);
|
|
53
|
-
private readonly hubUrl = inject(MERCURE_HUB_URL, {optional: true}) ?? undefined;
|
|
54
|
-
private readonly topicMode = inject(MERCURE_TOPIC_MODE, {optional: true}) ?? 'url';
|
|
55
|
-
private readonly connectionMode = inject(MERCURE_CONNECTION_MODE, {optional: true}) ?? 'auto';
|
|
56
|
-
private readonly maxUrlLength = normalizeMaxUrlLength(
|
|
57
|
-
inject(MERCURE_MAX_URL_LENGTH, {optional: true}),
|
|
58
|
-
1900
|
|
59
|
-
);
|
|
60
|
-
private readonly logger = inject(BRIDGE_LOGGER, {optional: true}) ?? undefined;
|
|
61
|
-
|
|
62
|
-
private readonly destroy$ = new Subject<void>();
|
|
63
|
-
private readonly sharedRebuild$ = new Subject<void>();
|
|
64
|
-
private readonly incoming$ = new Subject<IncomingMessage>();
|
|
65
|
-
private shuttingDown = false;
|
|
66
|
-
private sequence = 0;
|
|
67
|
-
|
|
68
|
-
private readonly topicsRegistry = new RefCountTopicRegistry();
|
|
69
|
-
private readonly topicMapper = new MercureTopicMapper(this.apiBase, this.topicMode);
|
|
70
|
-
private readonly connectionManager = new MercureConnectionManager({
|
|
71
|
-
hubUrl: this.hubUrl,
|
|
72
|
-
withCredentials: this.withCredentials,
|
|
73
|
-
mode: this.connectionMode,
|
|
74
|
-
maxUrlLength: this.maxUrlLength,
|
|
75
|
-
logger: this.logger,
|
|
76
|
-
destroy$: this.destroy$,
|
|
77
|
-
nextId: (prefix) => `${prefix}-${++this.sequence}`,
|
|
78
|
-
});
|
|
79
|
-
private readonly sharedClaims = new SubscriptionClaims<SharedSubscriptionClaim>();
|
|
80
|
-
private readonly dedicatedClaims = new SubscriptionClaims<DedicatedSubscriptionClaim>();
|
|
81
|
-
|
|
82
|
-
constructor() {
|
|
83
|
-
this.sharedRebuild$.pipe(auditTime(10)).subscribe(() => this.rebuildSharedConnections());
|
|
84
|
-
|
|
85
|
-
if (isPlatformBrowser(this.platformId)) {
|
|
86
|
-
fromEvent<PageTransitionEvent>(window, 'pagehide')
|
|
87
|
-
.pipe(takeUntil(this.destroy$))
|
|
88
|
-
.subscribe(() => this.shutdownBeforeExit());
|
|
89
|
-
|
|
90
|
-
fromEvent<BeforeUnloadEvent>(window, 'beforeunload')
|
|
91
|
-
.pipe(takeUntil(this.destroy$))
|
|
92
|
-
.subscribe(() => this.shutdownBeforeExit());
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
status$(): Observable<RealtimeStatus> {
|
|
97
|
-
return this.connectionManager.status$;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
diagnostics$(): Observable<RealtimeDiagnostics> {
|
|
101
|
-
return this.connectionManager.diagnostics$;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
subscribe$<T>(
|
|
105
|
-
iris: Iri[],
|
|
106
|
-
subscribeFilter?: SubscribeFilter,
|
|
107
|
-
options?: WatchConnectionOptions
|
|
108
|
-
): Observable<RealtimeEvent<T>> {
|
|
109
|
-
return defer(() => {
|
|
110
|
-
const subscription = this.prepareSubscription(iris, options);
|
|
111
|
-
if (!subscription) return EMPTY;
|
|
112
|
-
|
|
113
|
-
const matchTopic = createPayloadTopicMatcher(
|
|
114
|
-
subscription.iris,
|
|
115
|
-
subscribeFilter,
|
|
116
|
-
this.topicMapper
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
return this.payloads$(subscription.source$).pipe(
|
|
120
|
-
map((payload) => {
|
|
121
|
-
const topic = matchTopic(payload);
|
|
122
|
-
if (!topic) return undefined;
|
|
123
|
-
|
|
124
|
-
const rawId = payload?.['@id'];
|
|
125
|
-
const iri = typeof rawId === 'string' ? rawId : topic;
|
|
126
|
-
return {topic, iri, data: payload as T} as RealtimeEvent<T>;
|
|
127
|
-
}),
|
|
128
|
-
filter((event): event is RealtimeEvent<T> => event !== undefined)
|
|
129
|
-
);
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
subscribeAll$<T>(iris: Iri[], options?: WatchConnectionOptions): Observable<RealtimeEvent<T>> {
|
|
134
|
-
return defer(() => {
|
|
135
|
-
const subscription = this.prepareSubscription(iris, options);
|
|
136
|
-
if (!subscription) return EMPTY;
|
|
137
|
-
|
|
138
|
-
return this.payloads$(subscription.source$).pipe(
|
|
139
|
-
map((payload) => {
|
|
140
|
-
const rawId = payload['@id'];
|
|
141
|
-
const iri = typeof rawId === 'string' ? rawId : '';
|
|
142
|
-
return {iri, data: payload as T} as RealtimeEvent<T>;
|
|
143
|
-
})
|
|
144
|
-
);
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
unsubscribe(iris: Iri[]): void {
|
|
149
|
-
const inputIris = toIriList(iris);
|
|
150
|
-
if (inputIris.length === 0) return;
|
|
151
|
-
|
|
152
|
-
const topics = uniqueStrings(inputIris.map((iri) => this.topicMapper.toTopic(iri)));
|
|
153
|
-
if (this.releaseOneClaimByTopics(topics)) return;
|
|
154
|
-
|
|
155
|
-
// Claims are the source of truth. Never drop shared topics blindly when claims still exist:
|
|
156
|
-
// it could disconnect another active consumer.
|
|
157
|
-
if (this.sharedClaims.size === 0 && this.dedicatedClaims.size === 0) {
|
|
158
|
-
this.topicsRegistry.removeAll(topics);
|
|
159
|
-
this.scheduleSharedRebuild();
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
this.logger?.warn?.('[Mercure] unsubscribe ignored: no matching active claim for topics', {topics});
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
shutdownBeforeExit(): void {
|
|
167
|
-
if (this.shuttingDown) return;
|
|
168
|
-
this.shuttingDown = true;
|
|
169
|
-
this.teardownAllConnections();
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
ngOnDestroy(): void {
|
|
173
|
-
this.teardownAllConnections();
|
|
174
|
-
this.connectionManager.complete();
|
|
175
|
-
this.incoming$.complete();
|
|
176
|
-
this.destroy$.next();
|
|
177
|
-
this.destroy$.complete();
|
|
178
|
-
this.sharedRebuild$.complete();
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
private prepareSubscription(
|
|
182
|
-
iris: Iri[],
|
|
183
|
-
options?: WatchConnectionOptions
|
|
184
|
-
): PreparedSubscription | undefined {
|
|
185
|
-
const inputIris = toIriList(iris);
|
|
186
|
-
if (inputIris.length === 0) return undefined;
|
|
187
|
-
|
|
188
|
-
if (!this.hubUrl) {
|
|
189
|
-
this.logger?.debug?.('[Mercure] hubUrl not configured → realtime disabled');
|
|
190
|
-
return undefined;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const topics = uniqueStrings(inputIris.map((iri) => this.topicMapper.toTopic(iri)));
|
|
194
|
-
return {
|
|
195
|
-
iris: inputIris,
|
|
196
|
-
source$: this.buildIncomingStream$(topics, options),
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
private payloads$(source$: Observable<IncomingMessage>): Observable<Item> {
|
|
201
|
-
return source$.pipe(
|
|
202
|
-
map((event) => parseMercurePayload(event.data, this.logger)),
|
|
203
|
-
filter((payload): payload is Item => payload !== undefined)
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
private buildIncomingStream$(topics: string[], options?: WatchConnectionOptions): Observable<IncomingMessage> {
|
|
208
|
-
const useDedicatedConnection = this.connectionMode === 'single' || options?.newConnection === true;
|
|
209
|
-
return useDedicatedConnection
|
|
210
|
-
? this.subscribeViaDedicatedConnections$(topics)
|
|
211
|
-
: this.subscribeViaSharedConnections$(topics);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
private subscribeViaSharedConnections$(topics: string[]): Observable<IncomingMessage> {
|
|
215
|
-
return defer(() => {
|
|
216
|
-
const uniqueTopics = uniqueStrings(topics);
|
|
217
|
-
if (uniqueTopics.length === 0) return EMPTY;
|
|
218
|
-
const claim = this.createSharedClaim(uniqueTopics);
|
|
219
|
-
|
|
220
|
-
return this.incoming$.pipe(
|
|
221
|
-
takeUntil(claim.stop$),
|
|
222
|
-
finalize(() => {
|
|
223
|
-
this.releaseSharedClaim(claim.id);
|
|
224
|
-
})
|
|
225
|
-
);
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
private subscribeViaDedicatedConnections$(topics: string[]): Observable<IncomingMessage> {
|
|
230
|
-
return defer(() => {
|
|
231
|
-
if (!this.hubUrl) return EMPTY;
|
|
232
|
-
|
|
233
|
-
const uniqueTopics = uniqueStrings(topics);
|
|
234
|
-
if (uniqueTopics.length === 0) return EMPTY;
|
|
235
|
-
|
|
236
|
-
const groupId = `dedicated-${++this.sequence}`;
|
|
237
|
-
const incoming = new Subject<IncomingMessage>();
|
|
238
|
-
const claim = this.createDedicatedClaim({groupId, topics: uniqueTopics, incoming});
|
|
239
|
-
|
|
240
|
-
try {
|
|
241
|
-
this.connectionManager.openDedicated(groupId, uniqueTopics, incoming);
|
|
242
|
-
} catch (error) {
|
|
243
|
-
this.logger?.error?.('[Mercure] dedicated connection setup failed:', error);
|
|
244
|
-
this.releaseDedicatedClaim(claim.id);
|
|
245
|
-
return EMPTY;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return incoming.asObservable().pipe(
|
|
249
|
-
takeUntil(claim.stop$),
|
|
250
|
-
finalize(() => {
|
|
251
|
-
this.releaseDedicatedClaim(claim.id);
|
|
252
|
-
})
|
|
253
|
-
);
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
private scheduleSharedRebuild(): void {
|
|
258
|
-
if (this.shuttingDown) return;
|
|
259
|
-
this.sharedRebuild$.next();
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
private rebuildSharedConnections(): void {
|
|
263
|
-
if (this.shuttingDown) return;
|
|
264
|
-
|
|
265
|
-
try {
|
|
266
|
-
if (!this.hubUrl || !this.topicsRegistry.hasAny()) {
|
|
267
|
-
this.connectionManager.closeShared();
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
this.connectionManager.reconcileShared(this.topicsRegistry.snapshot(), this.incoming$);
|
|
272
|
-
} catch (error) {
|
|
273
|
-
this.logger?.error?.('[Mercure] shared rebuild failed:', error);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
private createSharedClaim(topics: string[]): SharedSubscriptionClaim {
|
|
278
|
-
const normalizedTopics = uniqueStrings(topics);
|
|
279
|
-
const claim: SharedSubscriptionClaim = {
|
|
280
|
-
id: `shared-claim-${++this.sequence}`,
|
|
281
|
-
topics: normalizedTopics,
|
|
282
|
-
topicsKey: topicsKey(normalizedTopics),
|
|
283
|
-
stop$: new Subject<void>(),
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
this.sharedClaims.add(claim);
|
|
287
|
-
this.topicsRegistry.addAll(claim.topics);
|
|
288
|
-
this.scheduleSharedRebuild();
|
|
289
|
-
|
|
290
|
-
return claim;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
private createDedicatedClaim(params: {
|
|
294
|
-
groupId: string;
|
|
295
|
-
topics: string[];
|
|
296
|
-
incoming: Subject<IncomingMessage>;
|
|
297
|
-
}): DedicatedSubscriptionClaim {
|
|
298
|
-
const normalizedTopics = uniqueStrings(params.topics);
|
|
299
|
-
const claim: DedicatedSubscriptionClaim = {
|
|
300
|
-
id: `dedicated-claim-${++this.sequence}`,
|
|
301
|
-
groupId: params.groupId,
|
|
302
|
-
topics: normalizedTopics,
|
|
303
|
-
topicsKey: topicsKey(normalizedTopics),
|
|
304
|
-
incoming: params.incoming,
|
|
305
|
-
stop$: new Subject<void>(),
|
|
306
|
-
};
|
|
307
|
-
|
|
308
|
-
this.dedicatedClaims.add(claim);
|
|
309
|
-
return claim;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
private releaseOneClaimByTopics(topics: string[]): boolean {
|
|
313
|
-
const key = topicsKey(topics);
|
|
314
|
-
if (!key) return false;
|
|
315
|
-
|
|
316
|
-
if (this.connectionMode === 'single') {
|
|
317
|
-
return this.dedicatedClaims.stopNext(key) || this.sharedClaims.stopNext(key);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
return this.sharedClaims.stopNext(key) || this.dedicatedClaims.stopNext(key);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
private releaseSharedClaim(claimId: string): void {
|
|
324
|
-
const claim = this.sharedClaims.release(claimId);
|
|
325
|
-
if (!claim) return;
|
|
326
|
-
|
|
327
|
-
this.topicsRegistry.removeAll(claim.topics);
|
|
328
|
-
this.scheduleSharedRebuild();
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
private releaseDedicatedClaim(claimId: string): void {
|
|
332
|
-
const claim = this.dedicatedClaims.release(claimId);
|
|
333
|
-
if (!claim) return;
|
|
334
|
-
|
|
335
|
-
this.connectionManager.closeDedicated(claim.groupId);
|
|
336
|
-
claim.incoming.complete();
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
private teardownAllConnections(): void {
|
|
340
|
-
this.shuttingDown = true;
|
|
341
|
-
this.sharedClaims.stopAll();
|
|
342
|
-
this.dedicatedClaims.stopAll();
|
|
343
|
-
|
|
344
|
-
this.sharedClaims.clear();
|
|
345
|
-
this.dedicatedClaims.clear();
|
|
346
|
-
this.connectionManager.closeAll();
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function normalizeMaxUrlLength(value: number | null | undefined, fallback: number): number {
|
|
351
|
-
if (typeof value !== 'number' || !Number.isFinite(value)) return fallback;
|
|
352
|
-
const rounded = Math.floor(value);
|
|
353
|
-
return rounded >= 256 ? rounded : fallback;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function uniqueStrings(values: Iterable<string>): string[] {
|
|
357
|
-
return [...new Set(values)];
|
|
358
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
export class RefCountTopicRegistry {
|
|
2
|
-
private readonly refCounts = new Map<string, number>();
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Increments ref-count for each topic.
|
|
6
|
-
* Callers should pass unique topic strings (deduped).
|
|
7
|
-
*/
|
|
8
|
-
addAll(topics: Iterable<string>): void {
|
|
9
|
-
for (const topic of topics) {
|
|
10
|
-
this.refCounts.set(topic, (this.refCounts.get(topic) ?? 0) + 1);
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Decrements ref-count for each topic.
|
|
16
|
-
* Callers should pass unique topic strings (deduped).
|
|
17
|
-
*/
|
|
18
|
-
removeAll(topics: Iterable<string>): void {
|
|
19
|
-
for (const topic of topics) {
|
|
20
|
-
const next = (this.refCounts.get(topic) ?? 0) - 1;
|
|
21
|
-
if (next <= 0) {
|
|
22
|
-
this.refCounts.delete(topic);
|
|
23
|
-
} else {
|
|
24
|
-
this.refCounts.set(topic, next);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
hasAny(): boolean {
|
|
30
|
-
return this.refCounts.size > 0;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
snapshot(): ReadonlySet<string> {
|
|
34
|
-
return new Set(this.refCounts.keys());
|
|
35
|
-
}
|
|
36
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import {Subject} from 'rxjs';
|
|
2
|
-
|
|
3
|
-
export interface SubscriptionClaim {
|
|
4
|
-
id: string;
|
|
5
|
-
topicsKey: string;
|
|
6
|
-
stop$: Subject<void>;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/** Tracks active subscriptions and preserves FIFO `unsubscribe(topics)` semantics. */
|
|
10
|
-
export class SubscriptionClaims<T extends SubscriptionClaim> {
|
|
11
|
-
private readonly claims = new Map<string, T>();
|
|
12
|
-
private readonly queueByTopics = new Map<string, string[]>();
|
|
13
|
-
|
|
14
|
-
get size(): number {
|
|
15
|
-
return this.claims.size;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
add(claim: T): void {
|
|
19
|
-
this.claims.set(claim.id, claim);
|
|
20
|
-
const queue = this.queueByTopics.get(claim.topicsKey) ?? [];
|
|
21
|
-
queue.push(claim.id);
|
|
22
|
-
this.queueByTopics.set(claim.topicsKey, queue);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
release(claimId: string): T | undefined {
|
|
26
|
-
const claim = this.claims.get(claimId);
|
|
27
|
-
if (!claim) return undefined;
|
|
28
|
-
|
|
29
|
-
this.claims.delete(claimId);
|
|
30
|
-
this.removeFromQueue(claim);
|
|
31
|
-
return claim;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
stopNext(topicsKey: string): boolean {
|
|
35
|
-
const queue = this.queueByTopics.get(topicsKey);
|
|
36
|
-
while (queue?.length) {
|
|
37
|
-
const claimId = queue[0];
|
|
38
|
-
if (this.stop(claimId)) return true;
|
|
39
|
-
queue.shift();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
this.queueByTopics.delete(topicsKey);
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
stopAll(): void {
|
|
47
|
-
for (const claimId of [...this.claims.keys()]) this.stop(claimId);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
clear(): void {
|
|
51
|
-
this.claims.clear();
|
|
52
|
-
this.queueByTopics.clear();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
private stop(claimId: string): boolean {
|
|
56
|
-
const claim = this.claims.get(claimId);
|
|
57
|
-
if (!claim) return false;
|
|
58
|
-
|
|
59
|
-
claim.stop$.next();
|
|
60
|
-
claim.stop$.complete();
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
private removeFromQueue(claim: T): void {
|
|
65
|
-
const queue = this.queueByTopics.get(claim.topicsKey);
|
|
66
|
-
if (!queue) return;
|
|
67
|
-
|
|
68
|
-
const remaining = queue.filter((claimId) => claimId !== claim.id);
|
|
69
|
-
if (remaining.length > 0) this.queueByTopics.set(claim.topicsKey, remaining);
|
|
70
|
-
else this.queueByTopics.delete(claim.topicsKey);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function topicsKey(topics: Iterable<string>): string {
|
|
75
|
-
return [...new Set(topics)].sort().join('|');
|
|
76
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import {Iri} from '../ports/resource-repository.port';
|
|
2
|
-
|
|
3
|
-
export function toIriList(input: Iri | Iri[]): string[] {
|
|
4
|
-
const values = Array.isArray(input) ? input : [input];
|
|
5
|
-
return values.filter((value): value is string => typeof value === 'string' && value.length > 0);
|
|
6
|
-
}
|