Skip to content

Commit 47c6a0b

Browse files
Merge pull request #4 from HichemTab-tech/enhance-api
Enhance shared state management with improved API methods and error handling
2 parents 8f5f6f5 + ccff3c2 commit 47c6a0b

File tree

7 files changed

+123
-73
lines changed

7 files changed

+123
-73
lines changed

demo/FakeSharedEmitter.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ export const FakeSharedEmitter = (() => {
3636
onCompletion
3737
}
3838

39-
await fakeAwait(1000);
39+
//TODO add persistance
40+
console.log("start loading")
41+
await fakeAwait(2000);
42+
console.log("connected")
4043
callback.onCompletion?.();
4144

4245
const entry = listeners.get(key)!;

demo/app.tsx

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ window.sharedSubscriptionsApi = sharedSubscriptionsApi;
1616
window.sharedFunctionsApi = sharedFunctionsApi;
1717
window.sharedStatesApi = sharedStatesApi;
1818

19+
sharedStatesApi.set("x", 55);
20+
1921
const Comp1 = () => {
2022
const [x, setX] = useSharedState('x', 0);
2123
const handle = (by = 1) => {
@@ -42,8 +44,9 @@ const Comp1 = () => {
4244
);
4345
}
4446

45-
const Comp2 = () => {
46-
const {state, trigger, unsubscribe} = useSharedSubscription<string>('test-sub', (set, onError, onCompletion) => {
47+
const use = () => {
48+
return useSharedSubscription<string>('test-sub', (set, onError, onCompletion) => {
49+
4750
return FakeSharedEmitter.subscribe("x", (data: string) => {
4851
if (data === "do-error") {
4952
onError(new Error("Error"));
@@ -52,12 +55,17 @@ const Comp2 = () => {
5255
set(data);
5356
console.log("data loaded...", data);
5457
}, onError, onCompletion)
58+
5559
});
60+
}
61+
62+
const Comp2 = () => {
63+
const {state, trigger, unsubscribe} = use();
5664

5765

5866
return (
5967
<div>
60-
<h1 className="text-red-600">Comp2</h1>
68+
<h1 className="text-red-600">Comp2 - {state.isLoading && "loading"}</h1>
6169
<button onClick={() => trigger()}>subscribe</button>
6270
<button onClick={() => unsubscribe()}>unsubscribe</button>
6371
results: {state.data}
@@ -87,16 +95,17 @@ const App = () => {
8795
<button onClick={() => handle()}>Increment x: {x}</button>
8896
<br/>
8997
<Comp1/>
98+
<SharedStatesProvider scopeName="aaa">
99+
<Comp1/>
100+
{x > 1 && <Comp1/>}
101+
{x > 3 && <Comp2/>}
102+
{x > 6 && <Comp2/>}
103+
</SharedStatesProvider>
104+
90105
<SharedStatesProvider>
91106
<Comp1/>
92107
<Comp1/>
93-
<SharedStatesProvider>
94-
<Comp1/>
95-
<Comp1/>
96-
</SharedStatesProvider>
97108
</SharedStatesProvider>
98-
{x > 3 && <Comp2/>}
99-
{x > 6 && <Comp2/>}
100109
</div>
101110
);
102111
};

src/SharedData.ts

Lines changed: 88 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type {AFunction, DataMapValue, Prefix} from "./types";
22
import {useEffect} from "react";
3-
import {log} from "./lib/utils";
3+
import {ensureNonEmptyString, log} from "./lib/utils";
44

55

66
type SharedDataType<T> = DataMapValue & T;
@@ -79,7 +79,12 @@ export abstract class SharedData<T> {
7979
}
8080

8181
static prefix(key: string, prefix: Prefix) {
82-
return `${prefix}_${key}`;
82+
if (key.includes("//")) throw new Error("key cannot contain '//'");
83+
return `${prefix}//${key}`;
84+
}
85+
86+
static extractPrefix(mapKey: string) {
87+
return mapKey.split("//");
8388
}
8489

8590
useEffect(key: string, prefix: Prefix, unsub: (() => void)|null = null) {
@@ -95,11 +100,85 @@ export abstract class SharedData<T> {
95100
}
96101
}
97102

98-
export interface SharedApi<T> {
99-
get: <S extends string = string>(key: S, scopeName: Prefix) => T;
100-
set: <S extends string = string>(key: S, value: T, scopeName: Prefix) => void;
101-
clearAll: () => void;
102-
clear: (key: string, scopeName: Prefix) => void;
103-
has: (key: string, scopeName: Prefix) => boolean;
104-
getAll: () => Map<string, DataMapValue>;
103+
// noinspection JSUnusedGlobalSymbols
104+
export class SharedApi<T>{
105+
constructor(private sharedData: SharedData<T>) {}
106+
107+
/**
108+
* get a value from the shared data
109+
* @param key
110+
* @param scopeName
111+
*/
112+
get<S extends string = string>(key: S, scopeName: Prefix) {
113+
key = ensureNonEmptyString(key);
114+
const prefix: Prefix = scopeName || "_global";
115+
return this.sharedData.get(key, prefix) as T;
116+
}
117+
118+
/**
119+
* set a value in the shared data
120+
* @param key
121+
* @param value
122+
* @param scopeName
123+
*/
124+
set<S extends string = string>(key: S, value: T, scopeName: Prefix) {
125+
key = ensureNonEmptyString(key);
126+
const prefix: Prefix = scopeName || "_global";
127+
this.sharedData.setValue(key, prefix, value);
128+
}
129+
130+
/**
131+
* clear all values from the shared data
132+
*/
133+
clearAll() {
134+
this.sharedData.clearAll();
135+
}
136+
137+
/**
138+
* clear all values from the shared data in a scope
139+
* @param scopeName
140+
*/
141+
clearScope(scopeName?: Prefix) {
142+
const prefixToSearch: Prefix = scopeName || "_global";
143+
this.sharedData.data.forEach((_, key) => {
144+
const [prefix] = SharedData.extractPrefix(key);
145+
if (prefix === prefixToSearch) {
146+
this.sharedData.clear(key, prefix);
147+
return;
148+
}
149+
});
150+
}
151+
152+
/**
153+
* clear a value from the shared data
154+
* @param key
155+
* @param scopeName
156+
*/
157+
clear(key: string, scopeName: Prefix) {
158+
const prefix: Prefix = scopeName || "_global";
159+
this.sharedData.clear(key, prefix);
160+
}
161+
162+
/**
163+
* check if a value exists in the shared data
164+
* @param key
165+
* @param scopeName
166+
*/
167+
has(key: string, scopeName: Prefix = "_global") {
168+
const prefix: Prefix = scopeName || "_global";
169+
return Boolean(this.sharedData.has(key, prefix));
170+
}
171+
172+
/**
173+
* get all values from the shared data
174+
*/
175+
getAll() {
176+
const all: Record<string, Record<string, any>> = {};
177+
this.sharedData.data.forEach((value, key) => {
178+
const [prefix, keyWithoutPrefix] = SharedData.extractPrefix(key);
179+
all[prefix] = all[prefix] || {};
180+
all[prefix][keyWithoutPrefix] = value;
181+
});
182+
return all;
183+
}
105184
}

src/context/SharedStatesContext.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ interface SharedStatesProviderProps<T extends string = string> extends PropsWith
1212
}
1313

1414
export const SharedStatesProvider = <T extends string = string>({ children, scopeName }: SharedStatesProviderProps<T>) => {
15+
if (scopeName && scopeName.includes("//")) throw new Error("scopeName cannot contain '//'");
1516

1617
if (!scopeName) scopeName = useMemo(() => Math.random().toString(36).substring(2, 15) as NonNullable<SharedStatesProviderProps<T>['scopeName']>, []);
1718

src/hooks/use-shared-function.ts

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type {AFunction, Prefix} from "../types";
22
import {useMemo, useSyncExternalStore} from "react";
3-
import {type SharedApi, SharedData} from "../SharedData";
3+
import {SharedApi, SharedData} from "../SharedData";
44
import useShared from "./use-shared";
55
import {ensureNonEmptyString} from "../lib/utils";
66

@@ -32,7 +32,7 @@ class SharedFunctionsData extends SharedData<SharedFunctionsState<unknown>> {
3232
}
3333
}
3434

35-
export class SharedFunctionsApi implements SharedApi<SharedFunctionsState<unknown>>{
35+
export class SharedFunctionsApi extends SharedApi<SharedFunctionsState<unknown>>{
3636
get<T, S extends string = string>(key: S, scopeName: Prefix = "_global") {
3737
key = ensureNonEmptyString(key);
3838
const prefix: Prefix = scopeName || "_global";
@@ -43,26 +43,12 @@ export class SharedFunctionsApi implements SharedApi<SharedFunctionsState<unknow
4343
const prefix: Prefix = scopeName || "_global";
4444
sharedFunctionsData.setValue(key, prefix, fnState);
4545
}
46-
clearAll() {
47-
sharedFunctionsData.clearAll();
48-
}
49-
clear(key: string, scopeName: Prefix = "_global") {
50-
const prefix: Prefix = scopeName || "_global";
51-
sharedFunctionsData.clear(key, prefix);
52-
}
53-
has(key: string, scopeName: Prefix = "_global") {
54-
const prefix: Prefix = scopeName || "_global";
55-
return Boolean(sharedFunctionsData.has(key, prefix));
56-
}
57-
getAll() {
58-
return sharedFunctionsData.data;
59-
}
6046
}
6147

62-
export const sharedFunctionsApi = new SharedFunctionsApi();
63-
6448
const sharedFunctionsData = new SharedFunctionsData();
6549

50+
export const sharedFunctionsApi = new SharedFunctionsApi(sharedFunctionsData);
51+
6652
export const useSharedFunction = <T, Args extends unknown[], S extends string = string>(key: S, fn: AFunction<T, Args>, scopeName?: Prefix) => {
6753

6854
key = ensureNonEmptyString(key);

src/hooks/use-shared-state.ts

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {useMemo, useSyncExternalStore} from "react";
22
import type {AFunction, Prefix} from "../types";
3-
import {type SharedApi, SharedData} from "../SharedData";
3+
import {SharedApi, SharedData} from "../SharedData";
44
import useShared from "./use-shared";
55
import {ensureNonEmptyString} from "../lib/utils";
66

@@ -24,7 +24,7 @@ class SharedStatesData extends SharedData<{
2424
}
2525
}
2626

27-
class SharedStatesApi implements SharedApi<{
27+
class SharedStatesApi extends SharedApi<{
2828
value: unknown
2929
}>{
3030
get<T, S extends string = string>(key: S, scopeName: Prefix = "_global") {
@@ -37,26 +37,12 @@ class SharedStatesApi implements SharedApi<{
3737
const prefix: Prefix = scopeName || "_global";
3838
sharedStatesData.setValue(key, prefix, {value});
3939
}
40-
clearAll() {
41-
sharedStatesData.clearAll();
42-
}
43-
clear(key: string, scopeName: Prefix = "_global") {
44-
const prefix: Prefix = scopeName || "_global";
45-
sharedStatesData.clear(key, prefix);
46-
}
47-
has(key: string, scopeName: Prefix = "_global") {
48-
const prefix: Prefix = scopeName || "_global";
49-
return Boolean(sharedStatesData.has(key, prefix));
50-
}
51-
getAll() {
52-
return sharedStatesData.data;
53-
}
5440
}
5541

56-
export const sharedStatesApi = new SharedStatesApi();
57-
5842
const sharedStatesData = new SharedStatesData();
5943

44+
export const sharedStatesApi = new SharedStatesApi(sharedStatesData);
45+
6046

6147

6248
export const useSharedState = <T, S extends string = string>(key: S, value: T, scopeName?: Prefix) => {

src/hooks/use-shared-subscription.ts

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type {PotentialPromise, Prefix} from "../types";
22
import {useEffect, useMemo, useSyncExternalStore} from "react";
3-
import {type SharedApi, SharedData} from "../SharedData";
3+
import {SharedApi, SharedData} from "../SharedData";
44
import useShared from "./use-shared";
55
import {ensureNonEmptyString, log} from "../lib/utils";
66

@@ -68,7 +68,7 @@ class SharedSubscriptionsData extends SharedData<SharedSubscriptionsState<unknow
6868
}
6969
}
7070

71-
export class SharedSubscriptionsApi implements SharedApi<SharedSubscriptionsState<unknown>>{
71+
export class SharedSubscriptionsApi extends SharedApi<SharedSubscriptionsState<unknown>>{
7272
get<T, S extends string = string>(key: S, scopeName: Prefix = "_global") {
7373
key = ensureNonEmptyString(key);
7474
const prefix: Prefix = scopeName || "_global";
@@ -79,26 +79,12 @@ export class SharedSubscriptionsApi implements SharedApi<SharedSubscriptionsStat
7979
const prefix: Prefix = scopeName || "_global";
8080
sharedSubscriptionsData.setValue(key, prefix, fnState);
8181
}
82-
clearAll() {
83-
sharedSubscriptionsData.clearAll();
84-
}
85-
clear(key: string, scopeName: Prefix = "_global") {
86-
const prefix: Prefix = scopeName || "_global";
87-
sharedSubscriptionsData.clear(key, prefix);
88-
}
89-
has(key: string, scopeName: Prefix = "_global") {
90-
const prefix: Prefix = scopeName || "_global";
91-
return Boolean(sharedSubscriptionsData.has(key, prefix));
92-
}
93-
getAll() {
94-
return sharedSubscriptionsData.data;
95-
}
9682
}
9783

98-
export const sharedSubscriptionsApi = new SharedSubscriptionsApi();
99-
10084
const sharedSubscriptionsData = new SharedSubscriptionsData();
10185

86+
export const sharedSubscriptionsApi = new SharedSubscriptionsApi(sharedSubscriptionsData);
87+
10288
export const useSharedSubscription = <T, S extends string = string>(key: S, subscriber: Subscriber<T>, scopeName?: Prefix) => {
10389

10490
key = ensureNonEmptyString(key);

0 commit comments

Comments
 (0)