@uxland/primary-shell 7.46.0 → 7.46.2

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.
@@ -2,6 +2,8 @@ import { HarmonixBroker } from '@uxland/harmonix';
2
2
  import { BrokerEvents } from './broker-events';
3
3
  export interface PrimariaBroker extends HarmonixBroker {
4
4
  events: BrokerEvents;
5
+ /** Internal: removes every registration (shell teardown only). */
6
+ _clear(): void;
5
7
  }
6
8
  export type { HarmonixBroker, IEvent, IEventClass, IRequest, IRequestClass, } from '@uxland/harmonix';
7
9
  export type { BrokerDisposableHandler } from '@uxland/harmonix';
@@ -16,9 +16,4 @@ export * from './api/interaction-service';
16
16
  export { ExitShell } from './features/exit/request';
17
17
  export type { ExitShellPayload } from './features/exit/request';
18
18
  export type { IUserInfo } from './features/get-user-info/model';
19
- export type { IActivityHistoryItem, IHistoryDataImporterContext, IHistoryDataImporterRules, } from '../../../plugins/activity-history/src/activity-history-item/domain/model';
20
- export type { InjectAsyncHistoryItemsPayload } from '../../../plugins/activity-history/src/activity-history-item/add/add-async-history-items/request';
21
- export type { AddHistoryItemPayload } from '../../../plugins/activity-history/src/activity-history-item/add/add-history-item/request';
22
- export type { AddHistoryItemsPayload } from '../../../plugins/activity-history/src/activity-history-item/add/add-history-items/request';
23
- export type { ActivityHistoryFilterType } from '../../../plugins/activity-history/src/activity-history-item/filter/model';
24
- export type { IActivityHistoryCustomFilterGroup, IActivityHistoryCustomFilter, } from '../../../plugins/activity-history/src/activity-history-item/filter/model';
19
+ export type { IActivityHistoryItem, IHistoryDataImporterContext, IHistoryDataImporterRules, InjectAsyncHistoryItemsPayload, AddHistoryItemPayload, AddHistoryItemsPayload, ActivityHistoryFilterType, IActivityHistoryCustomFilterGroup, IActivityHistoryCustomFilter, } from '@primaria/plugins-core';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uxland/primary-shell",
3
- "version": "7.46.0",
3
+ "version": "7.46.2",
4
4
  "description": "Primaria Shell",
5
5
  "author": "UXLand <dev@uxland.es>",
6
6
  "homepage": "https://github.com/uxland/harmonix/tree/app#readme",
@@ -42,7 +42,6 @@
42
42
  "inversify-inject-decorators": "^3.1.0",
43
43
  "jwt-decode": "^4.0.0",
44
44
  "lit": "^3.1.0",
45
- "mediatr-ts": "^1.2.1",
46
45
  "react": "^19.0.0",
47
46
  "react-dom": "^19.0.0",
48
47
  "reflect-metadata": "^0.2.2",
@@ -126,4 +126,248 @@ describe("Broker test", () => {
126
126
  });
127
127
  });
128
128
  });
129
+
130
+ describe("Disposal and re-registration", () => {
131
+ class MyRequest implements IRequest<string> {
132
+ constructor(public greeting: string) {}
133
+ }
134
+
135
+ it("disposing a request handler lets the same request be registered again (teardown + bootstrap)", () => {
136
+ const disposable = sut.registerRequest(MyRequest, () => "first");
137
+ disposable.dispose();
138
+ // Re-registering after dispose must NOT throw (the dispose-bug regression).
139
+ expect(() => sut.registerRequest(MyRequest, () => "second")).not.toThrow();
140
+ });
141
+
142
+ it("routes to the handler registered after a dispose/re-register cycle", async () => {
143
+ const first = vi.fn().mockResolvedValue("first");
144
+ const second = vi.fn().mockResolvedValue("second");
145
+ const disposable = sut.registerRequest(MyRequest, first);
146
+ disposable.dispose();
147
+ sut.registerRequest(MyRequest, second);
148
+
149
+ const result = await sut.send(new MyRequest("hi"));
150
+
151
+ expect(result).toBe("second");
152
+ expect(first).not.toHaveBeenCalled();
153
+ expect(second).toHaveBeenCalledWith({ greeting: "hi" });
154
+ });
155
+
156
+ it("sending a request with no handler registered throws", async () => {
157
+ await expect(sut.send(new MyRequest("hi"))).rejects.toThrow();
158
+ });
159
+
160
+ it("sending after the only handler was disposed throws", async () => {
161
+ const disposable = sut.registerRequest(MyRequest, () => "x");
162
+ disposable.dispose();
163
+ await expect(sut.send(new MyRequest("hi"))).rejects.toThrow();
164
+ });
165
+
166
+ it("a stale disposer does not drop a newer registration for the same request", async () => {
167
+ const first = vi.fn().mockResolvedValue("first");
168
+ const second = vi.fn().mockResolvedValue("second");
169
+ const firstDisposable = sut.registerRequest(MyRequest, first);
170
+ firstDisposable.dispose();
171
+ sut.registerRequest(MyRequest, second);
172
+ // Disposing the already-disposed first handler must not remove `second`.
173
+ firstDisposable.dispose();
174
+
175
+ const result = await sut.send(new MyRequest("hi"));
176
+ expect(result).toBe("second");
177
+ });
178
+ });
179
+
180
+ describe("Event subscription disposal", () => {
181
+ class MyEvent implements IEvent {
182
+ constructor(public greeting: string) {}
183
+ }
184
+
185
+ it("a disposed subscriber stops receiving events while the rest keep receiving them", async () => {
186
+ const kept = vi.fn();
187
+ const removed = vi.fn();
188
+ sut.subscribe(MyEvent, kept);
189
+ const disposable = sut.subscribe(MyEvent, removed);
190
+
191
+ disposable.dispose();
192
+ await sut.publish(new MyEvent("Hello"));
193
+
194
+ expect(kept).toHaveBeenCalledWith({ greeting: "Hello" });
195
+ expect(removed).not.toHaveBeenCalled();
196
+ });
197
+
198
+ it("publishing an event instance with no subscribers does not throw", () => {
199
+ expect(() => sut.publish(new MyEvent("Hello"))).not.toThrow();
200
+ });
201
+
202
+ it("the same handler can re-subscribe after disposing", async () => {
203
+ const handler = vi.fn();
204
+ const disposable = sut.subscribe("MyEvent", handler);
205
+ disposable.dispose();
206
+ sut.subscribe("MyEvent", handler);
207
+
208
+ await sut.publish("MyEvent", { greeting: "Hello" });
209
+
210
+ expect(handler).toHaveBeenCalledTimes(1);
211
+ expect(handler).toHaveBeenCalledWith({ greeting: "Hello" });
212
+ });
213
+ });
214
+
215
+ describe("_clear", () => {
216
+ class MyRequest implements IRequest<string> {}
217
+
218
+ it("removes all registrations so the registry can be reused cleanly", () => {
219
+ sut.registerRequest(MyRequest, () => "x");
220
+ sut._clear();
221
+ // After a clear, registering the same request again must not throw.
222
+ expect(() => sut.registerRequest(MyRequest, () => "y")).not.toThrow();
223
+ });
224
+ });
225
+
226
+ describe("Request handler semantics", () => {
227
+ class MyRequest implements IRequest<string> {
228
+ constructor(public greeting: string) {}
229
+ }
230
+
231
+ it("rejects when the handler throws synchronously", async () => {
232
+ sut.registerRequest(MyRequest, () => {
233
+ throw new Error("boom");
234
+ });
235
+ await expect(sut.send(new MyRequest("x"))).rejects.toThrow("boom");
236
+ });
237
+
238
+ it("rejects when the handler returns a rejected promise", async () => {
239
+ sut.registerRequest(MyRequest, async () => {
240
+ throw new Error("async boom");
241
+ });
242
+ await expect(sut.send(new MyRequest("x"))).rejects.toThrow("async boom");
243
+ });
244
+
245
+ it("resolves a synchronous (non-promise) handler return value", async () => {
246
+ sut.registerRequest(MyRequest, () => "sync-value");
247
+ await expect(sut.send(new MyRequest("x"))).resolves.toBe("sync-value");
248
+ });
249
+ });
250
+
251
+ describe("Publish semantics", () => {
252
+ it("resolves only after async subscribers have completed", async () => {
253
+ let completed = false;
254
+ sut.subscribe("evt", async () => {
255
+ await Promise.resolve();
256
+ completed = true;
257
+ });
258
+ await sut.publish("evt", {});
259
+ expect(completed).toBe(true);
260
+ });
261
+
262
+ it("isolates a throwing subscriber: the others are still notified and publish does not reject", async () => {
263
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
264
+ const before = vi.fn();
265
+ const after = vi.fn();
266
+ sut.subscribe("evt", before);
267
+ sut.subscribe("evt", () => {
268
+ throw new Error("bad subscriber");
269
+ });
270
+ sut.subscribe("evt", () => Promise.reject(new Error("bad async subscriber")));
271
+ sut.subscribe("evt", after);
272
+
273
+ await expect(sut.publish("evt", { a: 1 })).resolves.toBeUndefined();
274
+
275
+ expect(before).toHaveBeenCalledWith({ a: 1 });
276
+ expect(after).toHaveBeenCalledWith({ a: 1 });
277
+ expect(errorSpy).toHaveBeenCalledTimes(2);
278
+ errorSpy.mockRestore();
279
+ });
280
+
281
+ it("a subscriber added during dispatch is not invoked in the same round (snapshot)", async () => {
282
+ const added = vi.fn();
283
+ const adder = vi.fn(() => {
284
+ sut.subscribe("evt", added);
285
+ });
286
+ sut.subscribe("evt", adder);
287
+
288
+ await sut.publish("evt", {});
289
+ expect(adder).toHaveBeenCalledTimes(1);
290
+ expect(added).not.toHaveBeenCalled();
291
+
292
+ await sut.publish("evt", {});
293
+ expect(added).toHaveBeenCalledTimes(1);
294
+ });
295
+
296
+ it("a subscriber that disposes itself during dispatch is still called that round but not the next", async () => {
297
+ let disposable: ReturnType<typeof sut.subscribe>;
298
+ const handler = vi.fn(() => disposable.dispose());
299
+ disposable = sut.subscribe("evt", handler);
300
+
301
+ await sut.publish("evt", {});
302
+ await sut.publish("evt", {});
303
+ expect(handler).toHaveBeenCalledTimes(1);
304
+ });
305
+ });
306
+
307
+ describe("Namespace isolation", () => {
308
+ it("event class keys and event string keys are independent", async () => {
309
+ class Evt implements IEvent {}
310
+ const classHandler = vi.fn();
311
+ const stringHandler = vi.fn();
312
+ sut.subscribe(Evt, classHandler);
313
+ sut.subscribe("Evt", stringHandler);
314
+
315
+ await sut.publish(new Evt());
316
+ expect(classHandler).toHaveBeenCalledTimes(1);
317
+ expect(stringHandler).not.toHaveBeenCalled();
318
+
319
+ await sut.publish("Evt", {});
320
+ expect(stringHandler).toHaveBeenCalledTimes(1);
321
+ expect(classHandler).toHaveBeenCalledTimes(1);
322
+ });
323
+
324
+ it("the request registry and the event registry do not collide for the same name", async () => {
325
+ const requestHandler = vi.fn().mockResolvedValue("r");
326
+ const eventHandler = vi.fn();
327
+ sut.registerRequest("Shared", requestHandler);
328
+ sut.subscribe("Shared", eventHandler);
329
+
330
+ await sut.send("Shared", { v: 1 });
331
+ expect(requestHandler).toHaveBeenCalledWith({ v: 1 });
332
+ expect(eventHandler).not.toHaveBeenCalled();
333
+
334
+ await sut.publish("Shared", { v: 2 });
335
+ expect(eventHandler).toHaveBeenCalledWith({ v: 2 });
336
+ expect(requestHandler).toHaveBeenCalledTimes(1);
337
+ });
338
+ });
339
+
340
+ describe("Constructor-identity keying", () => {
341
+ it("keys requests by constructor identity, not by class name (two same-named classes coexist)", async () => {
342
+ const makeDup = () => class Dup implements IRequest<string> {};
343
+ const A = makeDup();
344
+ const B = makeDup();
345
+ expect(A.name).toBe(B.name); // both named "Dup"
346
+
347
+ const handlerA = vi.fn().mockResolvedValue("A");
348
+ const handlerB = vi.fn().mockResolvedValue("B");
349
+ sut.registerRequest(A, handlerA);
350
+ // The old name-based registry would have thrown here; identity keying must not.
351
+ expect(() => sut.registerRequest(B, handlerB)).not.toThrow();
352
+
353
+ await expect(sut.send(new A())).resolves.toBe("A");
354
+ await expect(sut.send(new B())).resolves.toBe("B");
355
+ });
356
+ });
357
+
358
+ describe("Idempotent disposal", () => {
359
+ class MyRequest implements IRequest<string> {}
360
+
361
+ it("disposing a request handler twice is safe", () => {
362
+ const disposable = sut.registerRequest(MyRequest, () => "x");
363
+ disposable.dispose();
364
+ expect(() => disposable.dispose()).not.toThrow();
365
+ });
366
+
367
+ it("disposing an event subscription twice is safe", () => {
368
+ const disposable = sut.subscribe("evt", () => {});
369
+ disposable.dispose();
370
+ expect(() => disposable.dispose()).not.toThrow();
371
+ });
372
+ });
129
373
  });
@@ -1,170 +1,138 @@
1
1
  import { IEvent, IEventClass, IRequest, IRequestClass } from "@uxland/harmonix";
2
- import { Mediator, mediatorSettings, notificationHandler, requestHandler } from "mediatr-ts";
3
2
  import { BROKER_EVENTS } from "./broker-events";
4
- import { type PrimariaBroker, type BrokerDisposableHandler } from "./primaria-broker";
3
+ import type { BrokerDisposableHandler, PrimariaBroker } from "./primaria-broker";
4
+
5
+ /**
6
+ * In-process message broker.
7
+ *
8
+ * The whole plugin system runs in a single JS heap (plugins are ES modules,
9
+ * no postMessage / workers / serialization), so dispatch is a plain map lookup.
10
+ *
11
+ * A message is keyed either by its **class constructor** (typed, intra-plugin
12
+ * requests/events) or by a **string name** (decoupled, cross-plugin events).
13
+ * Both live in the same map: `send(new Foo())` keys on `instance.constructor`,
14
+ * which is the very class passed to `registerRequest(Foo, …)`, so identity
15
+ * matches without any reflection or dynamically generated classes.
16
+ *
17
+ * - Requests (request/response): exactly one handler per key.
18
+ * - Events (publish/subscribe): any number of subscribers per key.
19
+ *
20
+ * Handlers receive a shallow copy of the message data (`{ ...instance }` for a
21
+ * class, or the raw payload for the string API), matching the previous
22
+ * behaviour exactly.
23
+ */
24
+
25
+ // biome-ignore lint/complexity/noBannedTypes: a class constructor is a Function, used here as a map key
26
+ type MessageKey = Function | string;
27
+
28
+ // biome-ignore lint/suspicious/noExplicitAny: handlers are typed at the public-interface boundary
29
+ type Handler = (message: any) => unknown;
5
30
 
6
- type messageHandler = (payload: unknown) => unknown | Promise<unknown>;
7
-
8
- const disposableFactory = (handler: any) => {
9
- return {
10
- dispose: () => {
11
- mediatorSettings.resolver.remove(handler.name);
12
- const mappingIndex = mediatorSettings.dispatcher.notifications._mappings.findIndex(
13
- (mapping) => mapping.handler.name === handler.name,
14
- );
15
- mappingIndex > -1 &&
16
- mediatorSettings.dispatcher.notifications._mappings.splice(mappingIndex, 1);
17
- },
18
- };
19
- };
20
31
  class Broker implements PrimariaBroker {
21
32
  public readonly events = BROKER_EVENTS;
22
- private mediator: Mediator;
23
- constructor() {
24
- this.mediator = new Mediator();
25
- mediatorSettings.resolver.clear();
26
- }
27
- // biome-ignore lint/complexity/noBannedTypes: <explanation>
28
- private eventConstructorMap: Map<string, Function> = new Map();
29
33
 
30
- // biome-ignore lint/complexity/noBannedTypes: <explanation>
31
- private requestConstructorMap: Map<string, Function> = new Map();
34
+ /** One handler per request type (request/response). */
35
+ private readonly requestHandlers = new Map<MessageKey, Handler>();
36
+
37
+ /** Many subscribers per event type (publish/subscribe). */
38
+ private readonly eventSubscribers = new Map<MessageKey, Set<Handler>>();
39
+
40
+ // ---- Request / response ---------------------------------------------------
32
41
 
33
42
  send<TRequest extends IRequest<TResponse>, TResponse>(message: TRequest): Promise<TResponse>;
34
43
  send<TPayload, TResponse>(requestName: string, payload: TPayload): Promise<TResponse>;
35
- send<TResponse>(request: IRequest<TResponse> | string, payload?: unknown): Promise<TResponse> {
36
- const eventPayload =
37
- typeof request === "string"
38
- ? this.getRequest<TResponse>(request as string, payload)
39
- : request;
40
- return this.mediator.send(eventPayload);
41
- }
42
- publish<TEvent extends IEvent>(event: TEvent): Promise<void>;
43
- publish<TPayload>(eventName: string, payload: TPayload): Promise<void>;
44
- publish(event: string | IEvent, payload?: unknown): Promise<void> {
45
- const eventPayload =
46
- typeof event === "string" ? this.getEvent(event as string, payload) : event;
47
- const classConstructor =
48
- eventPayload.constructor || Object.getPrototypeOf(eventPayload).constructor;
49
- const handlerSubscriptions = mediatorSettings.dispatcher.notifications._mappings.filter(
50
- (m) => m.notification === classConstructor,
51
- );
52
- if (!handlerSubscriptions.length) return Promise.resolve();
53
- return this.mediator.publish(eventPayload);
54
- }
55
- subscribe<TEvent extends IEvent>(
56
- event: TEvent,
57
- handler: (message: TEvent) => void,
58
- ): BrokerDisposableHandler;
59
- subscribe<TPayload>(
60
- eventName: string,
61
- handler: (message: TPayload) => void,
62
- ): BrokerDisposableHandler;
63
- subscribe(event: string | IEventClass, handler: messageHandler): BrokerDisposableHandler {
64
- const eventType = typeof event === "function" ? event : this.getEventType(event as string);
65
- const wrappedHandler = createDynamicEventHandler(handler);
66
- notificationHandler(eventType)(wrappedHandler);
67
- return disposableFactory(wrappedHandler);
44
+ async send<TResponse>(request: IRequest<TResponse> | string, payload?: unknown): Promise<TResponse> {
45
+ const key: MessageKey = typeof request === "string" ? request : request.constructor;
46
+ const handler = this.requestHandlers.get(key);
47
+ if (!handler) {
48
+ const name = typeof request === "string" ? request : request.constructor?.name;
49
+ throw new Error(`Broker: no request handler registered for "${name}".`);
50
+ }
51
+ const message = typeof request === "string" ? { ...(payload as object) } : { ...request };
52
+ return (await handler(message)) as TResponse;
68
53
  }
54
+
69
55
  registerRequest<TRequest extends IRequest<TResponse>, TResponse>(
70
- request: TRequest,
71
- handler: (message: TRequest) => TResponse,
56
+ request: IRequestClass<TRequest>,
57
+ handler: (message: TRequest) => TResponse | Promise<TResponse>,
72
58
  ): BrokerDisposableHandler;
73
59
  registerRequest<TPayload, TResponse>(
74
60
  requestName: string,
75
- handler: (message: TPayload) => TResponse,
61
+ handler: (message: TPayload) => TResponse | Promise<TResponse>,
76
62
  ): BrokerDisposableHandler;
77
- registerRequest(
78
- request: string | IRequestClass<unknown>,
79
- handler: messageHandler,
80
- ): BrokerDisposableHandler {
81
- const requestType =
82
- typeof request === "function" ? request : this.getRequestType(request as string);
83
- const wrappedHandler = createDynamicRequestHandler(handler);
84
- requestHandler(requestType)(wrappedHandler);
85
- return disposableFactory(wrappedHandler);
86
- }
87
-
88
- private getEvent(eventName: string, payload: unknown): IEvent {
89
- const eventConstructor = this.getEventType(eventName);
90
- return new eventConstructor(payload) as IEvent;
63
+ registerRequest(request: IRequestClass<unknown> | string, handler: Handler): BrokerDisposableHandler {
64
+ if (this.requestHandlers.has(request)) {
65
+ const name = typeof request === "string" ? request : request.name;
66
+ throw new Error(`Broker: a request handler is already registered for "${name}".`);
67
+ }
68
+ this.requestHandlers.set(request, handler);
69
+ return {
70
+ dispose: () => {
71
+ // Identity-guarded: a stale/duplicate dispose must not drop a handler
72
+ // registered later under the same key (after a teardown + bootstrap).
73
+ if (this.requestHandlers.get(request) === handler) {
74
+ this.requestHandlers.delete(request);
75
+ }
76
+ },
77
+ };
91
78
  }
92
79
 
93
- private getRequest<TResponse>(requestName: string, payload: unknown): IRequest<TResponse> {
94
- const requestConstructor = this.getRequestType(requestName);
95
- return new requestConstructor(payload) as IRequest<unknown>;
96
- }
80
+ // ---- Publish / subscribe --------------------------------------------------
97
81
 
98
- private getEventType(eventName: string): IEventClass {
99
- if (!this.eventConstructorMap.has(eventName)) {
100
- const clazz = createDynamicEventClass(eventName);
101
- this.eventConstructorMap.set(eventName, clazz);
102
- }
103
- return this.eventConstructorMap.get(eventName) as IEventClass;
82
+ publish<TEvent extends IEvent>(event: TEvent): Promise<void>;
83
+ publish<TPayload>(eventName: string, payload: TPayload): Promise<void>;
84
+ async publish(event: IEvent | string, payload?: unknown): Promise<void> {
85
+ const key: MessageKey = typeof event === "string" ? event : event.constructor;
86
+ const subscribers = this.eventSubscribers.get(key);
87
+ if (!subscribers || subscribers.size === 0) return;
88
+ const message = typeof event === "string" ? payload : { ...event };
89
+ const name = typeof event === "string" ? event : event.constructor?.name;
90
+ // Snapshot the set: a subscriber may subscribe/dispose while we iterate.
91
+ // Each subscriber is isolated: a throwing/rejecting subscriber is logged but
92
+ // never blocks the others nor rejects publish() (fire-and-forget event bus).
93
+ await Promise.all(
94
+ [...subscribers].map(async (handler) => {
95
+ try {
96
+ await handler(message);
97
+ } catch (error) {
98
+ console.error(`Broker: a subscriber for event "${name}" threw and was isolated.`, error);
99
+ }
100
+ }),
101
+ );
104
102
  }
105
103
 
106
- private getRequestType(requestName: string): IRequestClass<unknown> {
107
- if (!this.requestConstructorMap.has(requestName)) {
108
- const clazz = createDynamicRequestClass(requestName);
109
- this.requestConstructorMap.set(requestName, clazz);
104
+ subscribe<TEvent extends IEvent>(
105
+ event: IEventClass,
106
+ handler: (message: TEvent) => void | Promise<void>,
107
+ ): BrokerDisposableHandler;
108
+ subscribe<TPayload>(eventName: string, handler: (message: TPayload) => void | Promise<void>): BrokerDisposableHandler;
109
+ subscribe(event: IEventClass | string, handler: Handler): BrokerDisposableHandler {
110
+ let subscribers = this.eventSubscribers.get(event);
111
+ if (!subscribers) {
112
+ subscribers = new Set();
113
+ this.eventSubscribers.set(event, subscribers);
110
114
  }
111
- return this.requestConstructorMap.get(requestName) as IRequestClass<unknown>;
115
+ subscribers.add(handler);
116
+ return {
117
+ dispose: () => {
118
+ const current = this.eventSubscribers.get(event);
119
+ if (!current) return;
120
+ current.delete(handler);
121
+ if (current.size === 0) this.eventSubscribers.delete(event);
122
+ },
123
+ };
112
124
  }
113
- }
114
-
115
- const createDynamicEventClass = (eventName: string) => {
116
- return new Function(`return class Event_${eventName} {
117
- constructor(payload) {
118
- this.payload = payload;
119
- }
120
- }`)();
121
- };
122
-
123
- const createDynamicRequestClass = (requestName: string) => {
124
- return new Function(`return class Request_${requestName} {
125
- constructor(payload) {
126
- Object.assign(this, payload);
127
- }
128
- }`)();
129
- };
130
125
 
131
- const usedSuffixes = new Set();
126
+ // ---- Lifecycle ------------------------------------------------------------
132
127
 
133
- function generateUniqueRandomSuffix() {
134
- let suffix;
135
- do {
136
- suffix = Math.floor(Math.random() * 10000);
137
- } while (usedSuffixes.has(suffix));
138
-
139
- usedSuffixes.add(suffix);
140
- return suffix;
128
+ /**
129
+ * Removes every registration. Used on shell teardown so a re-initialization
130
+ * starts from a clean registry (the broker is a module-level singleton).
131
+ */
132
+ _clear(): void {
133
+ this.requestHandlers.clear();
134
+ this.eventSubscribers.clear();
135
+ }
141
136
  }
142
137
 
143
- const createDynamicRequestHandler = (handler: messageHandler) => {
144
- const className = `RequestHandler_${generateUniqueRandomSuffix()}`;
145
- return new Function(
146
- "handler",
147
- `return class ${className}{
148
- handle(notification){
149
- const handlerResult = handler({...notification});
150
- return handlerResult instanceof Promise ? handlerResult : Promise.resolve(handlerResult);
151
- }
152
- }`,
153
- )(handler);
154
- };
155
-
156
- const createDynamicEventHandler = (handler: messageHandler) => {
157
- const className = `EventHandler_${generateUniqueRandomSuffix()}`;
158
- return new Function(
159
- "handler",
160
- `return class ${className}{
161
- handle(notification){
162
- const payload = notification.payload !== undefined ? notification.payload : {...notification};
163
- const handlerResult = handler(payload);
164
- return handlerResult instanceof Promise ? handlerResult : Promise.resolve(handlerResult);
165
- }
166
- }`,
167
- )(handler);
168
- };
169
-
170
- export const createBroker = (): PrimariaBroker => new Broker();
138
+ export const createBroker = (): PrimariaBroker => new Broker();
@@ -3,6 +3,8 @@ import { type BrokerEvents } from "./broker-events";
3
3
 
4
4
  export interface PrimariaBroker extends HarmonixBroker {
5
5
  events: BrokerEvents;
6
+ /** Internal: removes every registration (shell teardown only). */
7
+ _clear(): void;
6
8
  }
7
9
 
8
10
  export type {
@@ -1,4 +1,3 @@
1
- import { mediatorSettings } from "mediatr-ts";
2
1
  import { PrimariaApi } from "../api/api";
3
2
  import { container } from "../infrastructure/ioc/container";
4
3
  import { TYPES } from "../infrastructure/ioc/types";
@@ -18,5 +17,5 @@ export const disposeFeatures = (api: PrimariaApi) => {
18
17
  disposeExitShell();
19
18
  container.unbindAll();
20
19
  api.regionManager._destroy();
21
- mediatorSettings.resolver.clear();
20
+ api.broker._clear();
22
21
  };
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import "reflect-metadata";
2
- import fontsCSS from "./UI/fonts/fonts.css?inline";
3
2
  import dssCSS from "@salut/design-system-salut/css/main.css?inline";
3
+ import fontsCSS from "./UI/fonts/fonts.css?inline";
4
4
 
5
5
  // Inject CSS automatically
6
6
  if (typeof document !== "undefined") {
@@ -29,16 +29,16 @@ export * from "./api/interaction-service";
29
29
  export { ExitShell } from "./features/exit/request";
30
30
  export type { ExitShellPayload } from "./features/exit/request";
31
31
  export type { IUserInfo } from "./features/get-user-info/model";
32
+ // Contractes de la Història d'Activitat: la font viu a `@primaria/plugins-core`
33
+ // (abans el shell importava del codi font del plugin activity-history).
32
34
  export type {
33
35
  IActivityHistoryItem,
34
36
  IHistoryDataImporterContext,
35
37
  IHistoryDataImporterRules,
36
- } from "../../../plugins/activity-history/src/activity-history-item/domain/model";
37
- export type { InjectAsyncHistoryItemsPayload } from "../../../plugins/activity-history/src/activity-history-item/add/add-async-history-items/request";
38
- export type { AddHistoryItemPayload } from "../../../plugins/activity-history/src/activity-history-item/add/add-history-item/request";
39
- export type { AddHistoryItemsPayload } from "../../../plugins/activity-history/src/activity-history-item/add/add-history-items/request";
40
- export type { ActivityHistoryFilterType } from "../../../plugins/activity-history/src/activity-history-item/filter/model";
41
- export type {
38
+ InjectAsyncHistoryItemsPayload,
39
+ AddHistoryItemPayload,
40
+ AddHistoryItemsPayload,
41
+ ActivityHistoryFilterType,
42
42
  IActivityHistoryCustomFilterGroup,
43
43
  IActivityHistoryCustomFilter,
44
- } from "../../../plugins/activity-history/src/activity-history-item/filter/model";
44
+ } from "@primaria/plugins-core";