@mfeflow/event-bus 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +72 -2
- package/dist/bus.d.ts +34 -0
- package/dist/bus.js +118 -0
- package/dist/channel.d.ts +10 -0
- package/dist/channel.js +33 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +1 -0
- package/dist/react.d.ts +7 -0
- package/dist/react.js +18 -0
- package/dist/registry.d.ts +31 -0
- package/dist/registry.js +42 -0
- package/dist/standard-schema.d.ts +28 -0
- package/dist/standard-schema.js +12 -0
- package/dist/warn.d.ts +5 -0
- package/dist/warn.js +11 -0
- package/package.json +78 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sanjeev Sharma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,73 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @mfeflow/event-bus
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A typed event bus for micro-frontends on one page. Remotes publish and subscribe by event name; they never import each other. Event names are versioned (`cart.item-added.v1`), payload types come from a map each contract adds to, and late subscribers can be given the last payload.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add @mfeflow/event-bus
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`react` (19) is an optional peer dependency, needed only for `@mfeflow/event-bus/react`. In a project made with mfeflow, `mfeflow add event-bus` does all of this and creates the contracts package.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
// packages/contracts: the types, shared by every remote
|
|
17
|
+
export interface Events {
|
|
18
|
+
'cart.item-added.v1': { sku: string; qty: number };
|
|
19
|
+
'cart.cleared.v1': undefined;
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// each app, once
|
|
25
|
+
import type { Events } from '@acme/contracts/events';
|
|
26
|
+
|
|
27
|
+
declare module '@mfeflow/event-bus' {
|
|
28
|
+
interface EventMap extends Events {}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// catalog
|
|
34
|
+
import { publish } from '@mfeflow/event-bus';
|
|
35
|
+
publish('cart.item-added.v1', { sku: 'A1', qty: 1 });
|
|
36
|
+
|
|
37
|
+
// cart
|
|
38
|
+
import { useEvent } from '@mfeflow/event-bus/react';
|
|
39
|
+
useEvent('cart.item-added.v1', ({ sku, qty }) => addLine(sku, qty));
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
A wrong name, a wrong payload or a missing payload fails typecheck. A handler that throws is logged and does not stop the others.
|
|
43
|
+
|
|
44
|
+
### Replay for late subscribers
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { configureEvent } from '@mfeflow/event-bus';
|
|
48
|
+
configureEvent('session.changed.v1', { replay: true });
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
A remote that mounts after the event was published gets the last payload when it subscribes. Off by default: most events are things that happened, not state. Under React StrictMode a replayed event reaches the handler once per mount, so keep that handler idempotent.
|
|
52
|
+
|
|
53
|
+
### Validating payloads
|
|
54
|
+
|
|
55
|
+
Nothing validates in the core bundle. Pass any function that returns the payload or throws:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { fromStandardSchema } from '@mfeflow/event-bus/standard-schema';
|
|
59
|
+
configureEvent('cart.item-added.v1', { validate: fromStandardSchema(itemAddedSchema) });
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`fromStandardSchema` takes a Zod 3.24+, Valibot 1+ or ArkType 2+ schema (the [Standard Schema](https://standardschema.dev) interface, copied in, so it is not a dependency). A rejected payload reaches nobody, `publish` returns `false`, and the log names the event and the schema's message, never the payload.
|
|
63
|
+
|
|
64
|
+
## Module Federation sharing
|
|
65
|
+
|
|
66
|
+
Bucket: **singleton**. Share `@mfeflow/event-bus` and `@mfeflow/event-bus/react` (`@mfeflow/rsbuild` does). The registry also lives on `globalThis`, so a second copy of the same version still reaches the same subscribers (and warns in development that two copies are loaded). A copy that finds a registry from an incompatible version keeps its own, warns in development, and talks to other copies through `window` events.
|
|
67
|
+
|
|
68
|
+
## Links
|
|
69
|
+
|
|
70
|
+
- [mfeflow repository](https://github.com/mfeflow/mfeflow) and its README
|
|
71
|
+
- [Documentation](https://mfeflow.github.io/mfeflow/)
|
|
72
|
+
|
|
73
|
+
MIT licensed.
|
package/dist/bus.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { EventMap } from './index.js';
|
|
2
|
+
/** The names an event can be published under. */
|
|
3
|
+
export type EventName = Extract<keyof EventMap, string>;
|
|
4
|
+
export type Handler<K extends EventName> = (payload: EventMap[K]) => void;
|
|
5
|
+
/** `[]` for an event whose payload is `undefined`, `[payload]` for the rest. */
|
|
6
|
+
export type PayloadArgs<K extends EventName> = [EventMap[K]] extends [undefined] ? [payload?: undefined] : [payload: EventMap[K]];
|
|
7
|
+
export interface EventConfig<T> {
|
|
8
|
+
/** Give each new subscriber the last payload right away. Off by default: most events are things that happened, not state. */
|
|
9
|
+
replay?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Runs on every publish. Return the payload (parsed, if you like) or throw to reject it: a
|
|
12
|
+
* rejected payload reaches nobody. No validator ships here; `@mfeflow/event-bus/standard-schema`
|
|
13
|
+
* turns a Zod, Valibot or ArkType schema into one.
|
|
14
|
+
*/
|
|
15
|
+
validate?: (payload: unknown) => T;
|
|
16
|
+
}
|
|
17
|
+
export interface SubscribeOptions {
|
|
18
|
+
/** Skip the replayed payload of an event that replays. Default true. */
|
|
19
|
+
replay?: boolean | undefined;
|
|
20
|
+
}
|
|
21
|
+
export declare function isEventName(name: string): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Tells every subscriber, in every remote on the page. Returns false when the event's validator
|
|
24
|
+
* rejected the payload (nothing was sent, and the reason is logged without the payload).
|
|
25
|
+
*/
|
|
26
|
+
export declare function publish<K extends EventName>(name: K, ...args: PayloadArgs<K>): boolean;
|
|
27
|
+
/** Calls `handler` for every `name` published from now on. Returns the function that stops it. */
|
|
28
|
+
export declare function subscribe<K extends EventName>(name: K, handler: Handler<K>, options?: SubscribeOptions): () => void;
|
|
29
|
+
/** Sets how an event behaves. Safe to call from every remote that cares; later calls change only what they name. */
|
|
30
|
+
export declare function configureEvent<K extends EventName>(name: K, config: EventConfig<EventMap[K]>): void;
|
|
31
|
+
/** How many handlers are subscribed to `name`; for tests and the dev overlay. */
|
|
32
|
+
export declare function listenerCount(name: string): number;
|
|
33
|
+
/** Only for tests: forgets every subscriber, replayed payload and setting. */
|
|
34
|
+
export declare function resetEventBusForTests(): void;
|
package/dist/bus.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { broadcast, listen, stopListening } from "./channel.js";
|
|
2
|
+
import { registry, resetRegistry } from "./registry.js";
|
|
3
|
+
import { resetWarningsForTests, warnOnce } from "./warn.js";
|
|
4
|
+
const NAME = /^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+\.v[1-9][0-9]*$/;
|
|
5
|
+
function isEventName(name) {
|
|
6
|
+
return NAME.test(name);
|
|
7
|
+
}
|
|
8
|
+
function checkName(name) {
|
|
9
|
+
if (!NAME.test(name)) warnOnce(`"${name}" is not a versioned event name; use <domain>.<event>.v<major>, like cart.item-added.v1`);
|
|
10
|
+
}
|
|
11
|
+
function reportHandlerError(name, error) {
|
|
12
|
+
console.error(`[mfeflow/event-bus] a handler for "${name}" threw`, error);
|
|
13
|
+
}
|
|
14
|
+
function call(name, handler, payload) {
|
|
15
|
+
try {
|
|
16
|
+
handler(payload);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
reportHandlerError(name, error);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function deliver(r, name, payload) {
|
|
22
|
+
const set = r.listeners.get(name);
|
|
23
|
+
if (!set) return;
|
|
24
|
+
for (const subscription of Array.from(set))if (set.has(subscription)) call(name, subscription.handler, payload);
|
|
25
|
+
}
|
|
26
|
+
function remember(r, name, payload) {
|
|
27
|
+
if (r.config.get(name)?.replay) r.last.set(name, payload);
|
|
28
|
+
}
|
|
29
|
+
const DELIVERED_KEPT = 64;
|
|
30
|
+
const origin = Math.random().toString(36).slice(2);
|
|
31
|
+
let sent = 0;
|
|
32
|
+
function alreadyDelivered(r, id) {
|
|
33
|
+
if (r.delivered.has(id)) return true;
|
|
34
|
+
r.delivered.add(id);
|
|
35
|
+
if (r.delivered.size > DELIVERED_KEPT) {
|
|
36
|
+
const oldest = r.delivered.values().next();
|
|
37
|
+
if (!oldest.done) r.delivered.delete(oldest.value);
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
function checked(r, name, payload) {
|
|
42
|
+
const validate = r.config.get(name)?.validate;
|
|
43
|
+
if (!validate) return {
|
|
44
|
+
payload
|
|
45
|
+
};
|
|
46
|
+
try {
|
|
47
|
+
return {
|
|
48
|
+
payload: validate(payload)
|
|
49
|
+
};
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return {
|
|
52
|
+
reason: error instanceof Error ? error.message : 'invalid payload'
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function receive(id, name, payload) {
|
|
57
|
+
const r = registry();
|
|
58
|
+
if (alreadyDelivered(r, id)) return;
|
|
59
|
+
const result = checked(r, name, payload);
|
|
60
|
+
if ('reason' in result) return void console.error(`[mfeflow/event-bus] "${name}" was not delivered: ${result.reason}`);
|
|
61
|
+
remember(r, name, result.payload);
|
|
62
|
+
deliver(r, name, result.payload);
|
|
63
|
+
}
|
|
64
|
+
function publish(name, ...args) {
|
|
65
|
+
checkName(name);
|
|
66
|
+
const r = registry();
|
|
67
|
+
const result = checked(r, name, args[0]);
|
|
68
|
+
if ('reason' in result) {
|
|
69
|
+
console.error(`[mfeflow/event-bus] "${name}" was not sent: ${result.reason}`);
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const id = `${origin}:${String(sent++)}`;
|
|
73
|
+
alreadyDelivered(r, id);
|
|
74
|
+
remember(r, name, result.payload);
|
|
75
|
+
deliver(r, name, result.payload);
|
|
76
|
+
listen(receive);
|
|
77
|
+
broadcast(id, name, result.payload);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
function subscribe(name, handler, options = {}) {
|
|
81
|
+
checkName(name);
|
|
82
|
+
const r = registry();
|
|
83
|
+
listen(receive);
|
|
84
|
+
const subscription = {
|
|
85
|
+
handler: handler
|
|
86
|
+
};
|
|
87
|
+
const set = r.listeners.get(name) ?? new Set();
|
|
88
|
+
r.listeners.set(name, set);
|
|
89
|
+
set.add(subscription);
|
|
90
|
+
if (false !== options.replay && r.config.get(name)?.replay && r.last.has(name)) call(name, subscription.handler, r.last.get(name));
|
|
91
|
+
return ()=>{
|
|
92
|
+
set.delete(subscription);
|
|
93
|
+
if (0 === set.size && r.listeners.get(name) === set) r.listeners.delete(name);
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function configureEvent(name, config) {
|
|
97
|
+
checkName(name);
|
|
98
|
+
const r = registry();
|
|
99
|
+
const before = r.config.get(name) ?? {
|
|
100
|
+
replay: false,
|
|
101
|
+
validate: void 0
|
|
102
|
+
};
|
|
103
|
+
const next = {
|
|
104
|
+
replay: config.replay ?? before.replay,
|
|
105
|
+
validate: config.validate ?? before.validate
|
|
106
|
+
};
|
|
107
|
+
r.config.set(name, next);
|
|
108
|
+
if (!next.replay) r.last.delete(name);
|
|
109
|
+
}
|
|
110
|
+
function listenerCount(name) {
|
|
111
|
+
return registry().listeners.get(name)?.size ?? 0;
|
|
112
|
+
}
|
|
113
|
+
function resetEventBusForTests() {
|
|
114
|
+
stopListening();
|
|
115
|
+
resetRegistry();
|
|
116
|
+
resetWarningsForTests();
|
|
117
|
+
}
|
|
118
|
+
export { configureEvent, isEventName, listenerCount, publish, resetEventBusForTests, subscribe };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const CHANNEL_EVENT = "mfeflow:event-bus";
|
|
2
|
+
/**
|
|
3
|
+
* Tells the other copies of the bus on this page, which may keep their own registry. Copies that
|
|
4
|
+
* share a registry get it twice and drop the second by `id`. Does nothing without a window.
|
|
5
|
+
*/
|
|
6
|
+
export declare function broadcast(id: string, name: string, payload: unknown): void;
|
|
7
|
+
/** Hands each message to `deliver`. Listens once; a second call does nothing. */
|
|
8
|
+
export declare function listen(deliver: (id: string, name: string, payload: unknown) => void): void;
|
|
9
|
+
/** Only for tests. */
|
|
10
|
+
export declare function stopListening(): void;
|
package/dist/channel.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const CHANNEL_EVENT = 'mfeflow:event-bus';
|
|
2
|
+
function isMessage(value) {
|
|
3
|
+
return 'object' == typeof value && null !== value && 'string' == typeof Reflect.get(value, 'id') && 'string' == typeof Reflect.get(value, 'name');
|
|
4
|
+
}
|
|
5
|
+
let stop;
|
|
6
|
+
function broadcast(id, name, payload) {
|
|
7
|
+
if ("u" < typeof window) return;
|
|
8
|
+
const detail = {
|
|
9
|
+
id,
|
|
10
|
+
name,
|
|
11
|
+
payload
|
|
12
|
+
};
|
|
13
|
+
window.dispatchEvent(new CustomEvent(CHANNEL_EVENT, {
|
|
14
|
+
detail
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
function listen(deliver) {
|
|
18
|
+
if ("u" < typeof window || stop) return;
|
|
19
|
+
const on = (event)=>{
|
|
20
|
+
if (!(event instanceof CustomEvent)) return;
|
|
21
|
+
const detail = event.detail;
|
|
22
|
+
if (isMessage(detail)) deliver(detail.id, detail.name, detail.payload);
|
|
23
|
+
};
|
|
24
|
+
window.addEventListener(CHANNEL_EVENT, on);
|
|
25
|
+
stop = ()=>{
|
|
26
|
+
window.removeEventListener(CHANNEL_EVENT, on);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function stopListening() {
|
|
30
|
+
stop?.();
|
|
31
|
+
stop = void 0;
|
|
32
|
+
}
|
|
33
|
+
export { CHANNEL_EVENT, broadcast, listen, stopListening };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every event the platform knows, with its payload. Empty here: contracts add to it by
|
|
3
|
+
* declaration merging, so `publish('cart.item-added.v1', { sku })` is checked end to end.
|
|
4
|
+
*
|
|
5
|
+
* declare module '@mfeflow/event-bus' {
|
|
6
|
+
* interface EventMap {
|
|
7
|
+
* 'cart.item-added.v1': { sku: string };
|
|
8
|
+
* 'cart.cleared.v1': undefined;
|
|
9
|
+
* }
|
|
10
|
+
* }
|
|
11
|
+
*/
|
|
12
|
+
export interface EventMap {
|
|
13
|
+
}
|
|
14
|
+
export { configureEvent, isEventName, listenerCount, publish, resetEventBusForTests, subscribe, type EventConfig, type EventName, type Handler, type PayloadArgs, type SubscribeOptions, } from './bus.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { configureEvent, isEventName, listenerCount, publish, resetEventBusForTests, subscribe } from "./bus.js";
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type EventName, type Handler, type SubscribeOptions } from './bus.js';
|
|
2
|
+
/**
|
|
3
|
+
* Calls `handler` for each `name` while the component is mounted. The handler may change on every
|
|
4
|
+
* render without resubscribing. An event that replays calls it once on mount with the last payload,
|
|
5
|
+
* which under StrictMode happens once per mount, so keep a handler that replays idempotent.
|
|
6
|
+
*/
|
|
7
|
+
export declare function useEvent<K extends EventName>(name: K, handler: Handler<K>, options?: SubscribeOptions): void;
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { subscribe } from "./bus.js";
|
|
3
|
+
function useEvent(name, handler, options = {}) {
|
|
4
|
+
const latest = useRef(handler);
|
|
5
|
+
useEffect(()=>{
|
|
6
|
+
latest.current = handler;
|
|
7
|
+
});
|
|
8
|
+
const { replay } = options;
|
|
9
|
+
useEffect(()=>subscribe(name, (payload)=>{
|
|
10
|
+
latest.current(payload);
|
|
11
|
+
}, {
|
|
12
|
+
replay
|
|
13
|
+
}), [
|
|
14
|
+
name,
|
|
15
|
+
replay
|
|
16
|
+
]);
|
|
17
|
+
}
|
|
18
|
+
export { useEvent };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Bumped when the shape of the shared registry changes; copies that disagree stop sharing it. */
|
|
2
|
+
export declare const PROTOCOL = 1;
|
|
3
|
+
export type Listener = (payload: unknown) => void;
|
|
4
|
+
/** One subscription. The set holds these, not the functions, so subscribing a function twice is two subscriptions. */
|
|
5
|
+
export interface Subscription {
|
|
6
|
+
readonly handler: Listener;
|
|
7
|
+
}
|
|
8
|
+
export interface Config {
|
|
9
|
+
replay: boolean;
|
|
10
|
+
validate: ((payload: unknown) => unknown) | undefined;
|
|
11
|
+
}
|
|
12
|
+
export interface Registry {
|
|
13
|
+
protocol: number;
|
|
14
|
+
listeners: Map<string, Set<Subscription>>;
|
|
15
|
+
/** The last payload of each event that replays. */
|
|
16
|
+
last: Map<string, unknown>;
|
|
17
|
+
config: Map<string, Config>;
|
|
18
|
+
/** Ids of the latest events delivered, so an event that arrives twice (directly and over the window) is delivered once. */
|
|
19
|
+
delivered: Set<string>;
|
|
20
|
+
/** One entry per copy of this package that has used the registry. */
|
|
21
|
+
copies: Set<symbol>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The registry every copy of this package on the page shares, kept on `globalThis` so a second
|
|
25
|
+
* copy (a missing singleton share) still reaches the same subscribers. A copy that finds one it
|
|
26
|
+
* cannot read (another protocol) keeps its own and talks to the other copies through
|
|
27
|
+
* the window bridge in `./channel` only.
|
|
28
|
+
*/
|
|
29
|
+
export declare function registry(): Registry;
|
|
30
|
+
/** Only for tests: forgets every subscriber, replayed payload and setting, and this copy's isolation. */
|
|
31
|
+
export declare function resetRegistry(): void;
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { warnOnce } from "./warn.js";
|
|
2
|
+
const PROTOCOL = 1;
|
|
3
|
+
const KEY = Symbol.for('mfeflow.event-bus');
|
|
4
|
+
const COPY = Symbol('copy');
|
|
5
|
+
function create() {
|
|
6
|
+
return {
|
|
7
|
+
protocol: PROTOCOL,
|
|
8
|
+
listeners: new Map(),
|
|
9
|
+
last: new Map(),
|
|
10
|
+
config: new Map(),
|
|
11
|
+
delivered: new Set(),
|
|
12
|
+
copies: new Set()
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function isRegistry(value) {
|
|
16
|
+
return 'object' == typeof value && null !== value && Reflect.get(value, 'protocol') === PROTOCOL && Reflect.get(value, 'listeners') instanceof Map && Reflect.get(value, 'last') instanceof Map && Reflect.get(value, 'config') instanceof Map && Reflect.get(value, 'delivered') instanceof Set && Reflect.get(value, 'copies') instanceof Set;
|
|
17
|
+
}
|
|
18
|
+
let own;
|
|
19
|
+
function attach(shared) {
|
|
20
|
+
if (shared.copies.has(COPY)) return shared;
|
|
21
|
+
shared.copies.add(COPY);
|
|
22
|
+
if (shared.copies.size > 1) warnOnce('two copies of @mfeflow/event-bus are loaded on this page. They share their subscribers, so events still arrive, but a remote is bundling its own copy: share @mfeflow/event-bus as a singleton (@mfeflow/rsbuild does) and make sure the shell imports it.');
|
|
23
|
+
return shared;
|
|
24
|
+
}
|
|
25
|
+
function registry() {
|
|
26
|
+
if (own) return own;
|
|
27
|
+
const existing = Reflect.get(globalThis, KEY);
|
|
28
|
+
if (void 0 === existing) {
|
|
29
|
+
const fresh = create();
|
|
30
|
+
Reflect.set(globalThis, KEY, fresh);
|
|
31
|
+
return attach(fresh);
|
|
32
|
+
}
|
|
33
|
+
if (isRegistry(existing)) return attach(existing);
|
|
34
|
+
warnOnce('another copy of @mfeflow/event-bus on this page speaks a different protocol, so this copy cannot share its subscribers. It falls back to window events; share @mfeflow/event-bus as a singleton so one copy is loaded.');
|
|
35
|
+
own = create();
|
|
36
|
+
return own;
|
|
37
|
+
}
|
|
38
|
+
function resetRegistry() {
|
|
39
|
+
own = void 0;
|
|
40
|
+
Reflect.deleteProperty(globalThis, KEY);
|
|
41
|
+
}
|
|
42
|
+
export { PROTOCOL, registry, resetRegistry };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** The Standard Schema interface (https://standardschema.dev), copied here so no validator is a dependency. */
|
|
2
|
+
export interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
3
|
+
readonly '~standard': {
|
|
4
|
+
readonly version: 1;
|
|
5
|
+
readonly vendor: string;
|
|
6
|
+
readonly validate: (value: unknown) => StandardResult<Output> | Promise<StandardResult<Output>>;
|
|
7
|
+
readonly types?: {
|
|
8
|
+
readonly input: Input;
|
|
9
|
+
readonly output: Output;
|
|
10
|
+
} | undefined;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
type StandardResult<Output> = {
|
|
14
|
+
readonly value: Output;
|
|
15
|
+
readonly issues?: undefined;
|
|
16
|
+
} | {
|
|
17
|
+
readonly issues: readonly {
|
|
18
|
+
readonly message: string;
|
|
19
|
+
}[];
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Turns a Standard Schema (Zod 3.24+, Valibot 1+, ArkType 2+) into an event's `validate`. Failures
|
|
23
|
+
* list the schema's messages and never the payload, so a rejected event cannot leak what was in it.
|
|
24
|
+
*
|
|
25
|
+
* configureEvent('cart.item-added.v1', { validate: fromStandardSchema(itemAdded) });
|
|
26
|
+
*/
|
|
27
|
+
export declare function fromStandardSchema<Output>(schema: StandardSchemaV1<unknown, Output>): (payload: unknown) => Output;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
function fromStandardSchema(schema) {
|
|
2
|
+
return (payload)=>{
|
|
3
|
+
const result = schema['~standard'].validate(payload);
|
|
4
|
+
if (result instanceof Promise) {
|
|
5
|
+
result.catch(()=>void 0);
|
|
6
|
+
throw new TypeError('the schema validates asynchronously, and events are delivered synchronously');
|
|
7
|
+
}
|
|
8
|
+
if (result.issues) throw new TypeError(result.issues.map((issue)=>issue.message).join('; '));
|
|
9
|
+
return result.value;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export { fromStandardSchema };
|
package/dist/warn.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Dev builds warn about mistakes; production builds stay quiet. Bundlers replace `process.env.NODE_ENV`. */
|
|
2
|
+
export declare const isDev: () => boolean;
|
|
3
|
+
export declare function warnOnce(message: string): void;
|
|
4
|
+
/** Only for tests. */
|
|
5
|
+
export declare function resetWarningsForTests(): void;
|
package/dist/warn.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const seen = new Set();
|
|
2
|
+
const isDev = ()=>'production' !== process.env.NODE_ENV;
|
|
3
|
+
function warnOnce(message) {
|
|
4
|
+
if (!isDev() || seen.has(message)) return;
|
|
5
|
+
seen.add(message);
|
|
6
|
+
console.warn(`[mfeflow/event-bus] ${message}`);
|
|
7
|
+
}
|
|
8
|
+
function resetWarningsForTests() {
|
|
9
|
+
seen.clear();
|
|
10
|
+
}
|
|
11
|
+
export { isDev, resetWarningsForTests, warnOnce };
|
package/package.json
CHANGED
|
@@ -1,6 +1,81 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mfeflow/event-bus",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "A typed event bus for micro-frontends on one page: versioned event names, payload types merged by each remote, optional validation, replay for late subscribers, and a React hook.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"micro-frontend",
|
|
7
|
+
"mfe",
|
|
8
|
+
"module-federation",
|
|
9
|
+
"event-bus",
|
|
10
|
+
"pub-sub",
|
|
11
|
+
"typescript",
|
|
12
|
+
"react"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://mfeflow.github.io/mfeflow/",
|
|
15
|
+
"bugs": "https://github.com/mfeflow/mfeflow/issues",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/mfeflow/mfeflow.git",
|
|
19
|
+
"directory": "packages/event-bus"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "Sanjeev Sharma <speedsharma.ai@gmail.com>",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js",
|
|
29
|
+
"default": "./dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./react": {
|
|
32
|
+
"types": "./dist/react.d.ts",
|
|
33
|
+
"import": "./dist/react.js",
|
|
34
|
+
"default": "./dist/react.js"
|
|
35
|
+
},
|
|
36
|
+
"./standard-schema": {
|
|
37
|
+
"types": "./dist/standard-schema.d.ts",
|
|
38
|
+
"import": "./dist/standard-schema.js",
|
|
39
|
+
"default": "./dist/standard-schema.js"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist"
|
|
44
|
+
],
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=22.12"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public",
|
|
50
|
+
"provenance": true
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"react": "^19.0.0"
|
|
54
|
+
},
|
|
55
|
+
"peerDependenciesMeta": {
|
|
56
|
+
"react": {
|
|
57
|
+
"optional": true
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@mfeflow/contracts": "0.2.0",
|
|
62
|
+
"@rsbuild/plugin-react": "2.1.1",
|
|
63
|
+
"@rslib/core": "1.0.5",
|
|
64
|
+
"@testing-library/dom": "10.4.2",
|
|
65
|
+
"@testing-library/react": "16.3.3",
|
|
66
|
+
"@types/node": "26.6.6",
|
|
67
|
+
"@types/react": "19.3.0",
|
|
68
|
+
"@types/react-dom": "19.3.0",
|
|
69
|
+
"jsdom": "30.1.3",
|
|
70
|
+
"react": "19.3.1",
|
|
71
|
+
"react-dom": "19.3.1",
|
|
72
|
+
"typescript": "7.0.2"
|
|
73
|
+
},
|
|
74
|
+
"mfeflow": {
|
|
75
|
+
"bucket": "singleton"
|
|
76
|
+
},
|
|
77
|
+
"scripts": {
|
|
78
|
+
"build": "rslib build",
|
|
79
|
+
"typecheck": "tsc --noEmit"
|
|
80
|
+
}
|
|
6
81
|
}
|