@dever-labs/ngx-mfe-broker 0.1.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 ADDED
@@ -0,0 +1,64 @@
1
+ # NgxMfeBroker
2
+
3
+ This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 22.0.0.
4
+
5
+ ## Code scaffolding
6
+
7
+ Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
8
+
9
+ ```bash
10
+ ng generate component component-name
11
+ ```
12
+
13
+ For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
14
+
15
+ ```bash
16
+ ng generate --help
17
+ ```
18
+
19
+ ## Building
20
+
21
+ To build the library, run:
22
+
23
+ ```bash
24
+ ng build ngx-mfe-broker
25
+ ```
26
+
27
+ This command will compile your project, and the build artifacts will be placed in the `dist/` directory.
28
+
29
+ ### Publishing the Library
30
+
31
+ Once the project is built, you can publish your library by following these steps:
32
+
33
+ 1. Navigate to the `dist` directory:
34
+
35
+ ```bash
36
+ cd dist/ngx-mfe-broker
37
+ ```
38
+
39
+ 2. Run the `npm publish` command to publish your library to the npm registry:
40
+ ```bash
41
+ npm publish
42
+ ```
43
+
44
+ ## Running unit tests
45
+
46
+ To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
47
+
48
+ ```bash
49
+ ng test
50
+ ```
51
+
52
+ ## Running end-to-end tests
53
+
54
+ For end-to-end (e2e) testing, run:
55
+
56
+ ```bash
57
+ ng e2e
58
+ ```
59
+
60
+ Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
61
+
62
+ ## Additional Resources
63
+
64
+ For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
@@ -0,0 +1,229 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, inject, signal, effect, Injectable, makeEnvironmentProviders } from '@angular/core';
3
+
4
+ const CHANNEL_NAME$1 = '@dever-labs/ngx-mfe-broker:state';
5
+ /**
6
+ * Injection token used to supply the initial state shape and default values.
7
+ *
8
+ * Provide this via `provideNgxMfeBroker({ initialState: { ... } })`.
9
+ *
10
+ * @example
11
+ * provideNgxMfeBroker({ initialState: { theme: 'light', token: null } })
12
+ */
13
+ const NGX_MFE_INITIAL_STATE = new InjectionToken('@dever-labs/ngx-mfe-broker:initial-state', { factory: () => ({}) });
14
+ function serialize(value) {
15
+ return typeof value === 'string' ? value : JSON.stringify(value);
16
+ }
17
+ function deserialize(raw, fallback) {
18
+ if (typeof fallback === 'string')
19
+ return raw;
20
+ try {
21
+ return JSON.parse(raw);
22
+ }
23
+ catch {
24
+ return fallback;
25
+ }
26
+ }
27
+ function stateEqual(a, b) {
28
+ if (a === b)
29
+ return true;
30
+ return JSON.stringify(a) === JSON.stringify(b);
31
+ }
32
+ /**
33
+ * Generic typed state service for micro-frontends.
34
+ *
35
+ * State is backed by localStorage, reactive via Angular Signals, and
36
+ * synchronised across browser tabs via BroadcastChannel.
37
+ *
38
+ * Consumers define their own state shape by providing `NGX_MFE_INITIAL_STATE`.
39
+ * Use `get<T>(key)` / `set(key, value)` for type-safe access.
40
+ */
41
+ class MfeStateService {
42
+ signals = new Map();
43
+ defaults = new Map();
44
+ /**
45
+ * Value-based guard: stores the last value received from another tab for
46
+ * each key. The effect checks this map instead of a time-sensitive Set so
47
+ * the check happens at effect execution time — guaranteed after the signal
48
+ * update — rather than racing against queueMicrotask cleanup.
49
+ */
50
+ inboundValues = new Map();
51
+ storage = typeof localStorage !== 'undefined' ? localStorage : null;
52
+ channel = typeof BroadcastChannel !== 'undefined'
53
+ ? new BroadcastChannel(CHANNEL_NAME$1)
54
+ : null;
55
+ constructor() {
56
+ const initialState = inject(NGX_MFE_INITIAL_STATE);
57
+ for (const [key, defaultValue] of Object.entries(initialState)) {
58
+ this.defaults.set(key, defaultValue);
59
+ const raw = this.storage?.getItem(key) ?? null;
60
+ const value = raw !== null ? deserialize(raw, defaultValue) : defaultValue;
61
+ const s = signal(value, /* @ts-ignore */
62
+ ...(ngDevMode ? [{ debugName: "s" }] : /* istanbul ignore next */ []));
63
+ this.signals.set(key, s);
64
+ effect(() => {
65
+ const current = s();
66
+ this.storage?.setItem(key, serialize(current));
67
+ // Skip broadcast if this value arrived from another tab to prevent echo loops.
68
+ if (stateEqual(current, this.inboundValues.get(key))) {
69
+ this.inboundValues.delete(key);
70
+ return;
71
+ }
72
+ this.channel?.postMessage({ key, value: current });
73
+ });
74
+ }
75
+ this.channel?.addEventListener('message', ({ data }) => {
76
+ const s = this.signals.get(data.key);
77
+ if (!s)
78
+ return;
79
+ if (stateEqual(s(), data.value))
80
+ return;
81
+ // Record the inbound value before updating the signal. The effect will
82
+ // read inboundValues synchronously during its execution and clear the entry.
83
+ this.inboundValues.set(data.key, data.value);
84
+ s.set(data.value);
85
+ });
86
+ }
87
+ /** Get a typed Signal for the given key. Key must be registered in `NGX_MFE_INITIAL_STATE`. */
88
+ get(key) {
89
+ if (!this.signals.has(key)) {
90
+ throw new Error(`[ngx-mfe-broker] Unknown state key "${key}". ` +
91
+ `Register it in NGX_MFE_INITIAL_STATE via provideNgxMfeBroker({ initialState: { ${key}: <default> } }).`);
92
+ }
93
+ return this.signals.get(key);
94
+ }
95
+ /** Set a value — persists to localStorage and broadcasts cross-tab. */
96
+ set(key, value) {
97
+ this.get(key).set(value);
98
+ }
99
+ ngOnDestroy() {
100
+ this.channel?.close();
101
+ }
102
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: MfeStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
103
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: MfeStateService, providedIn: 'root' });
104
+ }
105
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: MfeStateService, decorators: [{
106
+ type: Injectable,
107
+ args: [{ providedIn: 'root' }]
108
+ }], ctorParameters: () => [] });
109
+
110
+ const CHANNEL_NAME = '@dever-labs/ngx-mfe-broker:config';
111
+ /**
112
+ * Generic cross-tab string key-value store backed by localStorage.
113
+ * Changes are broadcast to all other tabs/windows via BroadcastChannel.
114
+ */
115
+ class ConfigRepositoryService {
116
+ signals = new Map();
117
+ /** Tracks every key ever written so clear() only removes its own keys. */
118
+ ownedKeys = new Set();
119
+ storage = typeof localStorage !== 'undefined' ? localStorage : null;
120
+ channel = typeof BroadcastChannel !== 'undefined'
121
+ ? new BroadcastChannel(CHANNEL_NAME)
122
+ : null;
123
+ /**
124
+ * Value-based inbound guard: stores the last value received from another tab.
125
+ * `null` means the last inbound operation was a remove for that key.
126
+ * Checked synchronously in set()/remove() to prevent re-broadcasting received updates.
127
+ */
128
+ inboundValues = new Map();
129
+ constructor() {
130
+ this.channel?.addEventListener('message', ({ data }) => {
131
+ if (data.type === 'clear') {
132
+ // Iterate only the keys the sender owned — not all signals on this tab.
133
+ data.keys.forEach(k => {
134
+ this.inboundValues.set(k, null);
135
+ this.storage?.removeItem(k);
136
+ this.signals.get(k)?.set(null);
137
+ });
138
+ }
139
+ else if (data.type === 'remove') {
140
+ this.inboundValues.set(data.key, null);
141
+ this.storage?.removeItem(data.key);
142
+ this.signals.get(data.key)?.set(null);
143
+ }
144
+ else {
145
+ this.inboundValues.set(data.key, data.value);
146
+ this.storage?.setItem(data.key, data.value);
147
+ this.getWritable(data.key).set(data.value);
148
+ }
149
+ });
150
+ }
151
+ getSignal(key) {
152
+ return this.getWritable(key).asReadonly();
153
+ }
154
+ get(key) {
155
+ return this.getSignal(key)();
156
+ }
157
+ set(key, value) {
158
+ this.ownedKeys.add(key);
159
+ this.storage?.setItem(key, value);
160
+ this.getWritable(key).set(value);
161
+ if (this.inboundValues.get(key) === value) {
162
+ this.inboundValues.delete(key);
163
+ return;
164
+ }
165
+ this.channel?.postMessage({ type: 'set', key, value });
166
+ }
167
+ remove(key) {
168
+ this.ownedKeys.delete(key);
169
+ this.storage?.removeItem(key);
170
+ this.signals.get(key)?.set(null);
171
+ if (this.inboundValues.get(key) === null && this.inboundValues.has(key)) {
172
+ this.inboundValues.delete(key);
173
+ return;
174
+ }
175
+ this.channel?.postMessage({ type: 'remove', key });
176
+ }
177
+ /** Removes only keys written by this service — does not touch unrelated localStorage entries. */
178
+ clear() {
179
+ const keys = [...this.ownedKeys];
180
+ keys.forEach(k => {
181
+ this.storage?.removeItem(k);
182
+ this.signals.get(k)?.set(null);
183
+ });
184
+ this.ownedKeys.clear();
185
+ this.channel?.postMessage({ type: 'clear', keys });
186
+ }
187
+ ngOnDestroy() {
188
+ this.channel?.close();
189
+ }
190
+ getWritable(key) {
191
+ if (!this.signals.has(key)) {
192
+ this.signals.set(key, signal(this.storage?.getItem(key) ?? null));
193
+ }
194
+ return this.signals.get(key);
195
+ }
196
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ConfigRepositoryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
197
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ConfigRepositoryService, providedIn: 'root' });
198
+ }
199
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ConfigRepositoryService, decorators: [{
200
+ type: Injectable,
201
+ args: [{ providedIn: 'root' }]
202
+ }], ctorParameters: () => [] });
203
+
204
+ /**
205
+ * Call in your `app.config.ts` to configure `@dever-labs/ngx-mfe-broker`.
206
+ *
207
+ * @example
208
+ * export const appConfig: ApplicationConfig = {
209
+ * providers: [
210
+ * provideNgxMfeBroker({ initialState: { theme: 'light', token: null } }),
211
+ * ]
212
+ * };
213
+ */
214
+ function provideNgxMfeBroker(config) {
215
+ return makeEnvironmentProviders([
216
+ { provide: NGX_MFE_INITIAL_STATE, useValue: config.initialState },
217
+ ]);
218
+ }
219
+
220
+ /*
221
+ * Public API Surface of @dever-labs/ngx-mfe-broker
222
+ */
223
+
224
+ /**
225
+ * Generated bundle index. Do not edit.
226
+ */
227
+
228
+ export { ConfigRepositoryService, MfeStateService, NGX_MFE_INITIAL_STATE, provideNgxMfeBroker };
229
+ //# sourceMappingURL=dever-labs-ngx-mfe-broker.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dever-labs-ngx-mfe-broker.mjs","sources":["../../../projects/ngx-mfe-broker/src/lib/mfe-state.service.ts","../../../projects/ngx-mfe-broker/src/lib/config-repository.service.ts","../../../projects/ngx-mfe-broker/src/lib/provide-ngx-mfe-broker.ts","../../../projects/ngx-mfe-broker/src/public-api.ts","../../../projects/ngx-mfe-broker/src/dever-labs-ngx-mfe-broker.ts"],"sourcesContent":["import { effect, inject, Injectable, InjectionToken, OnDestroy, signal, WritableSignal } from '@angular/core';\n\nconst CHANNEL_NAME = '@dever-labs/ngx-mfe-broker:state';\n\n/**\n * Injection token used to supply the initial state shape and default values.\n *\n * Provide this via `provideNgxMfeBroker({ initialState: { ... } })`.\n *\n * @example\n * provideNgxMfeBroker({ initialState: { theme: 'light', token: null } })\n */\nexport const NGX_MFE_INITIAL_STATE = new InjectionToken<Record<string, unknown>>(\n '@dever-labs/ngx-mfe-broker:initial-state',\n { factory: () => ({}) },\n);\n\nfunction serialize(value: unknown): string {\n return typeof value === 'string' ? value : JSON.stringify(value);\n}\n\nfunction deserialize(raw: string, fallback: unknown): unknown {\n if (typeof fallback === 'string') return raw;\n try { return JSON.parse(raw); } catch { return fallback; }\n}\n\nfunction stateEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n return JSON.stringify(a) === JSON.stringify(b);\n}\n\ntype StateMessage = { key: string; value: unknown };\n\n/**\n * Generic typed state service for micro-frontends.\n *\n * State is backed by localStorage, reactive via Angular Signals, and\n * synchronised across browser tabs via BroadcastChannel.\n *\n * Consumers define their own state shape by providing `NGX_MFE_INITIAL_STATE`.\n * Use `get<T>(key)` / `set(key, value)` for type-safe access.\n */\n@Injectable({ providedIn: 'root' })\nexport class MfeStateService implements OnDestroy {\n private readonly signals = new Map<string, WritableSignal<unknown>>();\n private readonly defaults = new Map<string, unknown>();\n /**\n * Value-based guard: stores the last value received from another tab for\n * each key. The effect checks this map instead of a time-sensitive Set so\n * the check happens at effect execution time — guaranteed after the signal\n * update — rather than racing against queueMicrotask cleanup.\n */\n private readonly inboundValues = new Map<string, unknown>();\n\n private readonly storage: Storage | null = typeof localStorage !== 'undefined' ? localStorage : null;\n\n private readonly channel = typeof BroadcastChannel !== 'undefined'\n ? new BroadcastChannel(CHANNEL_NAME)\n : null;\n\n constructor() {\n const initialState = inject(NGX_MFE_INITIAL_STATE);\n\n for (const [key, defaultValue] of Object.entries(initialState)) {\n this.defaults.set(key, defaultValue);\n const raw = this.storage?.getItem(key) ?? null;\n const value = raw !== null ? deserialize(raw, defaultValue) : defaultValue;\n const s = signal(value);\n this.signals.set(key, s);\n\n effect(() => {\n const current = s();\n this.storage?.setItem(key, serialize(current));\n // Skip broadcast if this value arrived from another tab to prevent echo loops.\n if (stateEqual(current, this.inboundValues.get(key))) {\n this.inboundValues.delete(key);\n return;\n }\n this.channel?.postMessage({ key, value: current } satisfies StateMessage);\n });\n }\n\n this.channel?.addEventListener('message', ({ data }: MessageEvent<StateMessage>) => {\n const s = this.signals.get(data.key);\n if (!s) return;\n if (stateEqual(s(), data.value)) return;\n // Record the inbound value before updating the signal. The effect will\n // read inboundValues synchronously during its execution and clear the entry.\n this.inboundValues.set(data.key, data.value);\n s.set(data.value);\n });\n }\n\n /** Get a typed Signal for the given key. Key must be registered in `NGX_MFE_INITIAL_STATE`. */\n get<T>(key: string): WritableSignal<T> {\n if (!this.signals.has(key)) {\n throw new Error(\n `[ngx-mfe-broker] Unknown state key \"${key}\". ` +\n `Register it in NGX_MFE_INITIAL_STATE via provideNgxMfeBroker({ initialState: { ${key}: <default> } }).`,\n );\n }\n return this.signals.get(key) as WritableSignal<T>;\n }\n\n /** Set a value — persists to localStorage and broadcasts cross-tab. */\n set<T>(key: string, value: T): void {\n this.get<T>(key).set(value);\n }\n\n ngOnDestroy(): void {\n this.channel?.close();\n }\n}\n","import { Injectable, signal, Signal, WritableSignal, OnDestroy } from '@angular/core';\n\nconst CHANNEL_NAME = '@dever-labs/ngx-mfe-broker:config';\n\ntype ConfigMessage =\n | { type: 'set'; key: string; value: string }\n | { type: 'remove'; key: string }\n | { type: 'clear'; keys: string[] };\n\n/**\n * Generic cross-tab string key-value store backed by localStorage.\n * Changes are broadcast to all other tabs/windows via BroadcastChannel.\n */\n@Injectable({ providedIn: 'root' })\nexport class ConfigRepositoryService implements OnDestroy {\n private readonly signals = new Map<string, WritableSignal<string | null>>();\n /** Tracks every key ever written so clear() only removes its own keys. */\n private readonly ownedKeys = new Set<string>();\n\n private readonly storage: Storage | null = typeof localStorage !== 'undefined' ? localStorage : null;\n\n private readonly channel = typeof BroadcastChannel !== 'undefined'\n ? new BroadcastChannel(CHANNEL_NAME)\n : null;\n\n /**\n * Value-based inbound guard: stores the last value received from another tab.\n * `null` means the last inbound operation was a remove for that key.\n * Checked synchronously in set()/remove() to prevent re-broadcasting received updates.\n */\n private readonly inboundValues = new Map<string, string | null>();\n\n constructor() {\n this.channel?.addEventListener('message', ({ data }: MessageEvent<ConfigMessage>) => {\n if (data.type === 'clear') {\n // Iterate only the keys the sender owned — not all signals on this tab.\n data.keys.forEach(k => {\n this.inboundValues.set(k, null);\n this.storage?.removeItem(k);\n this.signals.get(k)?.set(null);\n });\n } else if (data.type === 'remove') {\n this.inboundValues.set(data.key, null);\n this.storage?.removeItem(data.key);\n this.signals.get(data.key)?.set(null);\n } else {\n this.inboundValues.set(data.key, data.value);\n this.storage?.setItem(data.key, data.value);\n this.getWritable(data.key).set(data.value);\n }\n });\n }\n\n getSignal(key: string): Signal<string | null> {\n return this.getWritable(key).asReadonly();\n }\n\n get(key: string): string | null {\n return this.getSignal(key)();\n }\n\n set(key: string, value: string): void {\n this.ownedKeys.add(key);\n this.storage?.setItem(key, value);\n this.getWritable(key).set(value);\n if (this.inboundValues.get(key) === value) {\n this.inboundValues.delete(key);\n return;\n }\n this.channel?.postMessage({ type: 'set', key, value } satisfies ConfigMessage);\n }\n\n remove(key: string): void {\n this.ownedKeys.delete(key);\n this.storage?.removeItem(key);\n this.signals.get(key)?.set(null);\n if (this.inboundValues.get(key) === null && this.inboundValues.has(key)) {\n this.inboundValues.delete(key);\n return;\n }\n this.channel?.postMessage({ type: 'remove', key } satisfies ConfigMessage);\n }\n\n /** Removes only keys written by this service — does not touch unrelated localStorage entries. */\n clear(): void {\n const keys = [...this.ownedKeys];\n keys.forEach(k => {\n this.storage?.removeItem(k);\n this.signals.get(k)?.set(null);\n });\n this.ownedKeys.clear();\n this.channel?.postMessage({ type: 'clear', keys } satisfies ConfigMessage);\n }\n\n ngOnDestroy(): void {\n this.channel?.close();\n }\n\n private getWritable(key: string): WritableSignal<string | null> {\n if (!this.signals.has(key)) {\n this.signals.set(key, signal(this.storage?.getItem(key) ?? null));\n }\n return this.signals.get(key)!;\n }\n}\n","import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';\nimport { NGX_MFE_INITIAL_STATE } from './mfe-state.service';\n\nexport interface NgxMfeBrokerConfig {\n /**\n * Initial state values with their default values.\n * Each key becomes a Signal backed by localStorage with cross-tab sync.\n *\n * @example\n * provideNgxMfeBroker({\n * initialState: { theme: 'light', token: null, users: [] }\n * })\n */\n initialState: Record<string, unknown>;\n}\n\n/**\n * Call in your `app.config.ts` to configure `@dever-labs/ngx-mfe-broker`.\n *\n * @example\n * export const appConfig: ApplicationConfig = {\n * providers: [\n * provideNgxMfeBroker({ initialState: { theme: 'light', token: null } }),\n * ]\n * };\n */\nexport function provideNgxMfeBroker(config: NgxMfeBrokerConfig): EnvironmentProviders {\n return makeEnvironmentProviders([\n { provide: NGX_MFE_INITIAL_STATE, useValue: config.initialState },\n ]);\n}\n","/*\n * Public API Surface of @dever-labs/ngx-mfe-broker\n */\n\nexport * from './lib/mfe-state.service';\nexport * from './lib/config-repository.service';\nexport * from './lib/provide-ngx-mfe-broker';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":["CHANNEL_NAME"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@dever-labs/ngx-mfe-broker",
3
+ "version": "0.1.1",
4
+ "description": "Angular Signals + BroadcastChannel state broker for micro-frontends. Synchronises state across MFEs and browser tabs via localStorage persistence and BroadcastChannel cross-tab sync.",
5
+ "keywords": [
6
+ "angular",
7
+ "micro-frontends",
8
+ "mfe",
9
+ "signals",
10
+ "broadcast-channel",
11
+ "state-management",
12
+ "native-federation",
13
+ "cross-tab",
14
+ "localStorage"
15
+ ],
16
+ "author": {
17
+ "name": "Dever Labs",
18
+ "url": "https://github.com/dever-labs"
19
+ },
20
+ "license": "MIT",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/dever-labs/ngx-mfe-broker.git"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/dever-labs/ngx-mfe-broker/issues"
27
+ },
28
+ "homepage": "https://github.com/dever-labs/ngx-mfe-broker#readme",
29
+ "engines": {
30
+ "node": ">=20.0.0",
31
+ "npm": ">=9.0.0"
32
+ },
33
+ "peerDependencies": {
34
+ "@angular/core": ">=22.0.0"
35
+ },
36
+ "peerDependenciesMeta": {
37
+ "@angular/core": {
38
+ "optional": false
39
+ }
40
+ },
41
+ "dependencies": {
42
+ "tslib": "^2.3.0"
43
+ },
44
+ "sideEffects": false,
45
+ "module": "fesm2022/dever-labs-ngx-mfe-broker.mjs",
46
+ "typings": "types/dever-labs-ngx-mfe-broker.d.ts",
47
+ "exports": {
48
+ "./package.json": {
49
+ "default": "./package.json"
50
+ },
51
+ ".": {
52
+ "types": "./types/dever-labs-ngx-mfe-broker.d.ts",
53
+ "default": "./fesm2022/dever-labs-ngx-mfe-broker.mjs"
54
+ }
55
+ },
56
+ "type": "module"
57
+ }
@@ -0,0 +1,98 @@
1
+ import * as i0 from '@angular/core';
2
+ import { OnDestroy, WritableSignal, InjectionToken, Signal, EnvironmentProviders } from '@angular/core';
3
+
4
+ /**
5
+ * Injection token used to supply the initial state shape and default values.
6
+ *
7
+ * Provide this via `provideNgxMfeBroker({ initialState: { ... } })`.
8
+ *
9
+ * @example
10
+ * provideNgxMfeBroker({ initialState: { theme: 'light', token: null } })
11
+ */
12
+ declare const NGX_MFE_INITIAL_STATE: InjectionToken<Record<string, unknown>>;
13
+ /**
14
+ * Generic typed state service for micro-frontends.
15
+ *
16
+ * State is backed by localStorage, reactive via Angular Signals, and
17
+ * synchronised across browser tabs via BroadcastChannel.
18
+ *
19
+ * Consumers define their own state shape by providing `NGX_MFE_INITIAL_STATE`.
20
+ * Use `get<T>(key)` / `set(key, value)` for type-safe access.
21
+ */
22
+ declare class MfeStateService implements OnDestroy {
23
+ private readonly signals;
24
+ private readonly defaults;
25
+ /**
26
+ * Value-based guard: stores the last value received from another tab for
27
+ * each key. The effect checks this map instead of a time-sensitive Set so
28
+ * the check happens at effect execution time — guaranteed after the signal
29
+ * update — rather than racing against queueMicrotask cleanup.
30
+ */
31
+ private readonly inboundValues;
32
+ private readonly storage;
33
+ private readonly channel;
34
+ constructor();
35
+ /** Get a typed Signal for the given key. Key must be registered in `NGX_MFE_INITIAL_STATE`. */
36
+ get<T>(key: string): WritableSignal<T>;
37
+ /** Set a value — persists to localStorage and broadcasts cross-tab. */
38
+ set<T>(key: string, value: T): void;
39
+ ngOnDestroy(): void;
40
+ static ɵfac: i0.ɵɵFactoryDeclaration<MfeStateService, never>;
41
+ static ɵprov: i0.ɵɵInjectableDeclaration<MfeStateService>;
42
+ }
43
+
44
+ /**
45
+ * Generic cross-tab string key-value store backed by localStorage.
46
+ * Changes are broadcast to all other tabs/windows via BroadcastChannel.
47
+ */
48
+ declare class ConfigRepositoryService implements OnDestroy {
49
+ private readonly signals;
50
+ /** Tracks every key ever written so clear() only removes its own keys. */
51
+ private readonly ownedKeys;
52
+ private readonly storage;
53
+ private readonly channel;
54
+ /**
55
+ * Value-based inbound guard: stores the last value received from another tab.
56
+ * `null` means the last inbound operation was a remove for that key.
57
+ * Checked synchronously in set()/remove() to prevent re-broadcasting received updates.
58
+ */
59
+ private readonly inboundValues;
60
+ constructor();
61
+ getSignal(key: string): Signal<string | null>;
62
+ get(key: string): string | null;
63
+ set(key: string, value: string): void;
64
+ remove(key: string): void;
65
+ /** Removes only keys written by this service — does not touch unrelated localStorage entries. */
66
+ clear(): void;
67
+ ngOnDestroy(): void;
68
+ private getWritable;
69
+ static ɵfac: i0.ɵɵFactoryDeclaration<ConfigRepositoryService, never>;
70
+ static ɵprov: i0.ɵɵInjectableDeclaration<ConfigRepositoryService>;
71
+ }
72
+
73
+ interface NgxMfeBrokerConfig {
74
+ /**
75
+ * Initial state values with their default values.
76
+ * Each key becomes a Signal backed by localStorage with cross-tab sync.
77
+ *
78
+ * @example
79
+ * provideNgxMfeBroker({
80
+ * initialState: { theme: 'light', token: null, users: [] }
81
+ * })
82
+ */
83
+ initialState: Record<string, unknown>;
84
+ }
85
+ /**
86
+ * Call in your `app.config.ts` to configure `@dever-labs/ngx-mfe-broker`.
87
+ *
88
+ * @example
89
+ * export const appConfig: ApplicationConfig = {
90
+ * providers: [
91
+ * provideNgxMfeBroker({ initialState: { theme: 'light', token: null } }),
92
+ * ]
93
+ * };
94
+ */
95
+ declare function provideNgxMfeBroker(config: NgxMfeBrokerConfig): EnvironmentProviders;
96
+
97
+ export { ConfigRepositoryService, MfeStateService, NGX_MFE_INITIAL_STATE, provideNgxMfeBroker };
98
+ export type { NgxMfeBrokerConfig };