@mfeflow/storage 0.0.0-stage → 0.1.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 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,39 @@
1
- # Temporary Holding Version
1
+ # @mfeflow/storage
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Browser storage namespaced per micro-frontend. Keys are stored as `<mfe>:<key>`, so two remotes cannot overwrite each other. It falls back to memory when Web Storage is missing (SSR, sandboxed iframes), keeps tabs in sync and has a React hook.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pnpm add @mfeflow/storage
9
+ ```
10
+
11
+ `react` (19) is an optional peer dependency, needed only for `@mfeflow/storage/react`.
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { createStorage } from '@mfeflow/storage';
17
+
18
+ const storage = createStorage('cart'); // 'local' by default; { area: 'session' | 'memory' } also works
19
+ storage.set('items', ['a', 'b']);
20
+ const items = storage.get('items', [] as string[], Array.isArray);
21
+ const stop = storage.subscribe('items', (value) => console.log(value)); // this tab and other tabs
22
+ ```
23
+
24
+ ```tsx
25
+ import { useStoredState } from '@mfeflow/storage/react';
26
+
27
+ const [open, setOpen] = useStoredState(storage, 'drawer-open', false);
28
+ ```
29
+
30
+ ## Module Federation sharing
31
+
32
+ Bucket: **stateless**. Its listeners live on a page global, so separate copies in different remotes still see each other's writes and no copy is required to be the only one. `@mfeflow/rsbuild` shares it as a singleton by default anyway, which keeps one copy on the page; sharing is not required for correctness.
33
+
34
+ ## Links
35
+
36
+ - [mfeflow repository](https://github.com/mfeflow/mfeflow) and its README
37
+ - [Documentation](https://mfeflow.github.io/mfeflow/)
38
+
39
+ MIT licensed.
@@ -0,0 +1,36 @@
1
+ /** Where values live. `memory` is used automatically when no Web Storage exists (SSR, sandboxed iframes). */
2
+ export type StorageArea = 'local' | 'session' | 'memory';
3
+ export interface CreateStorageOptions {
4
+ /** Default `local`. */
5
+ area?: StorageArea;
6
+ /** Use this Storage instead of the browser's (tests, custom adapters). */
7
+ backend?: Storage;
8
+ }
9
+ /** Called with the new value (already parsed), or `undefined` when the key was removed. */
10
+ export type Listener = (value: unknown) => void;
11
+ /** Narrows an untrusted stored value. */
12
+ export type Guard<T> = (value: unknown) => value is T;
13
+ export interface MfeStorage {
14
+ /** The micro-frontend this storage belongs to. Keys are stored as `<mfe>:<key>`. */
15
+ readonly mfe: string;
16
+ /** The stored value, or `undefined`. Returns the same reference until the value changes. */
17
+ read(key: string): unknown;
18
+ /** The stored value if `guard` accepts it, otherwise `fallback`. */
19
+ get<T>(key: string, fallback: T, guard?: Guard<T>): T;
20
+ set(key: string, value: unknown): void;
21
+ remove(key: string): void;
22
+ /** Keys owned by this micro-frontend, without the prefix. */
23
+ keys(): string[];
24
+ /** Called on changes from this tab and from other tabs. Returns an unsubscribe function. */
25
+ subscribe(key: string, listener: Listener): () => void;
26
+ }
27
+ type Bus = WeakMap<Storage, Map<string, Set<Listener>>>;
28
+ declare global {
29
+ var mfeflowStorageBus: Bus | undefined;
30
+ }
31
+ /**
32
+ * Storage for one micro-frontend. Every key is prefixed with the MFE's name,
33
+ * so two remotes can both use `theme` or `filters` without overwriting each other.
34
+ */
35
+ export declare function createStorage(mfe: string, options?: CreateStorageOptions): MfeStorage;
36
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,131 @@
1
+ const MFE_NAME = /^[a-z0-9][a-z0-9_-]*$/i;
2
+ class MemoryStorage {
3
+ #map = new Map();
4
+ get length() {
5
+ return this.#map.size;
6
+ }
7
+ clear() {
8
+ this.#map.clear();
9
+ }
10
+ getItem(key) {
11
+ return this.#map.get(key) ?? null;
12
+ }
13
+ key(index) {
14
+ return [
15
+ ...this.#map.keys()
16
+ ][index] ?? null;
17
+ }
18
+ removeItem(key) {
19
+ this.#map.delete(key);
20
+ }
21
+ setItem(key, value) {
22
+ this.#map.set(key, value);
23
+ }
24
+ }
25
+ function browserStorage(area) {
26
+ if ('memory' === area || "u" < typeof window) return;
27
+ try {
28
+ const s = 'session' === area ? window.sessionStorage : window.localStorage;
29
+ const probe = '__mfeflow_probe__';
30
+ s.setItem(probe, probe);
31
+ s.removeItem(probe);
32
+ return s;
33
+ } catch {
34
+ return;
35
+ }
36
+ }
37
+ function listenersFor(backend, fullKey) {
38
+ globalThis.mfeflowStorageBus ??= new WeakMap();
39
+ const bus = globalThis.mfeflowStorageBus;
40
+ let byKey = bus.get(backend);
41
+ if (!byKey) {
42
+ byKey = new Map();
43
+ bus.set(backend, byKey);
44
+ }
45
+ let set = byKey.get(fullKey);
46
+ if (!set) {
47
+ set = new Set();
48
+ byKey.set(fullKey, set);
49
+ }
50
+ return set;
51
+ }
52
+ function parse(raw) {
53
+ if (null === raw) return;
54
+ try {
55
+ const value = JSON.parse(raw);
56
+ return value;
57
+ } catch {
58
+ return;
59
+ }
60
+ }
61
+ function createStorage(mfe, options = {}) {
62
+ if (!MFE_NAME.test(mfe)) throw new Error(`createStorage: invalid micro-frontend name "${mfe}" (use letters, digits, - and _)`);
63
+ const area = options.area ?? 'local';
64
+ const backend = options.backend ?? browserStorage(area) ?? new MemoryStorage();
65
+ const prefix = `${mfe}:`;
66
+ const cache = new Map();
67
+ let subscriptions = 0;
68
+ const notify = (key, value)=>{
69
+ for (const l of listenersFor(backend, prefix + key))l(value);
70
+ };
71
+ const onStorageEvent = (e)=>{
72
+ if (e.storageArea !== backend || null === e.key || !e.key.startsWith(prefix)) return;
73
+ notify(e.key.slice(prefix.length), parse(e.newValue));
74
+ };
75
+ const read = (key)=>{
76
+ const raw = backend.getItem(prefix + key);
77
+ const hit = cache.get(key);
78
+ if (hit?.raw === raw) return hit.value;
79
+ const value = parse(raw);
80
+ cache.set(key, {
81
+ raw,
82
+ value
83
+ });
84
+ return value;
85
+ };
86
+ return {
87
+ mfe,
88
+ read,
89
+ get (key, fallback, guard) {
90
+ const value = read(key);
91
+ if (void 0 === value) return fallback;
92
+ if (guard) return guard(value) ? value : fallback;
93
+ if (typeof value !== typeof fallback || Array.isArray(value) !== Array.isArray(fallback)) return fallback;
94
+ return value;
95
+ },
96
+ set (key, value) {
97
+ try {
98
+ backend.setItem(prefix + key, JSON.stringify(value));
99
+ } catch {}
100
+ notify(key, value);
101
+ },
102
+ remove (key) {
103
+ backend.removeItem(prefix + key);
104
+ notify(key, void 0);
105
+ },
106
+ keys () {
107
+ const out = [];
108
+ for(let i = 0; i < backend.length; i++){
109
+ const k = backend.key(i);
110
+ if (k?.startsWith(prefix)) out.push(k.slice(prefix.length));
111
+ }
112
+ return out;
113
+ },
114
+ subscribe (key, listener) {
115
+ const set = listenersFor(backend, prefix + key);
116
+ const l = (value)=>{
117
+ listener(value);
118
+ };
119
+ set.add(l);
120
+ if (0 === subscriptions++ && "u" > typeof window) window.addEventListener('storage', onStorageEvent);
121
+ let active = true;
122
+ return ()=>{
123
+ if (!active) return;
124
+ active = false;
125
+ set.delete(l);
126
+ if (0 === --subscriptions && "u" > typeof window) window.removeEventListener('storage', onStorageEvent);
127
+ };
128
+ }
129
+ };
130
+ }
131
+ export { createStorage };
@@ -0,0 +1,8 @@
1
+ import type { Guard, MfeStorage } from './index.js';
2
+ type Next<T> = T | ((prev: T) => T);
3
+ /**
4
+ * React state backed by an `MfeStorage` key. Stays in sync across components,
5
+ * remotes using the same storage, and browser tabs.
6
+ */
7
+ export declare function useStoredState<T>(storage: MfeStorage, key: string, initial: T, guard?: Guard<T>): [T, (next: Next<T>) => void];
8
+ export {};
package/dist/react.js ADDED
@@ -0,0 +1,31 @@
1
+ import { useCallback, useSyncExternalStore } from "react";
2
+ function isUpdater(next) {
3
+ return 'function' == typeof next;
4
+ }
5
+ function useStoredState(storage, key, initial, guard) {
6
+ const subscribe = useCallback((onChange)=>storage.subscribe(key, onChange), [
7
+ storage,
8
+ key
9
+ ]);
10
+ const getSnapshot = useCallback(()=>storage.read(key), [
11
+ storage,
12
+ key
13
+ ]);
14
+ const getServerSnapshot = useCallback(()=>void 0, []);
15
+ useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
16
+ const value = storage.get(key, initial, guard);
17
+ const set = useCallback((next)=>{
18
+ const resolved = isUpdater(next) ? next(storage.get(key, initial, guard)) : next;
19
+ storage.set(key, resolved);
20
+ }, [
21
+ storage,
22
+ key,
23
+ initial,
24
+ guard
25
+ ]);
26
+ return [
27
+ value,
28
+ set
29
+ ];
30
+ }
31
+ export { useStoredState };
package/package.json CHANGED
@@ -1,6 +1,75 @@
1
1
  {
2
2
  "name": "@mfeflow/storage",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Browser storage namespaced per micro-frontend, SSR-safe, with cross-tab sync and a React hook.",
5
+ "keywords": [
6
+ "micro-frontend",
7
+ "mfe",
8
+ "module-federation",
9
+ "localstorage",
10
+ "react",
11
+ "hook",
12
+ "rsbuild"
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/storage"
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
+ },
37
+ "files": [
38
+ "dist"
39
+ ],
40
+ "engines": {
41
+ "node": ">=22.12"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public",
45
+ "provenance": true
46
+ },
47
+ "peerDependencies": {
48
+ "react": "^19.0.0"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "react": {
52
+ "optional": true
53
+ }
54
+ },
55
+ "devDependencies": {
56
+ "@rsbuild/plugin-react": "2.1.1",
57
+ "@rslib/core": "1.0.5",
58
+ "@testing-library/dom": "10.4.2",
59
+ "@testing-library/react": "16.3.3",
60
+ "@types/node": "26.6.6",
61
+ "@types/react": "19.3.0",
62
+ "@types/react-dom": "19.3.0",
63
+ "jsdom": "30.1.3",
64
+ "react": "19.3.1",
65
+ "react-dom": "19.3.1",
66
+ "typescript": "7.0.2"
67
+ },
68
+ "mfeflow": {
69
+ "bucket": "stateless"
70
+ },
71
+ "scripts": {
72
+ "build": "rslib build",
73
+ "typecheck": "tsc --noEmit"
74
+ }
6
75
  }