@servicetitan/react-ioc 38.0.0 → 38.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/dist/container-config.d.ts +12 -0
- package/dist/container-config.d.ts.map +1 -0
- package/dist/container-config.js +85 -0
- package/dist/container-config.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +3 -82
- package/dist/provider.js.map +1 -1
- package/dist/store.js +10 -5
- package/dist/store.js.map +1 -1
- package/dist/use-local-stores.d.ts +7 -0
- package/dist/use-local-stores.d.ts.map +1 -0
- package/dist/use-local-stores.js +62 -0
- package/dist/use-local-stores.js.map +1 -0
- package/package.json +3 -3
- package/src/__tests__/container-config.test.ts +159 -0
- package/src/__tests__/use-local-stores.test.tsx +849 -0
- package/src/container-config.ts +125 -0
- package/src/index.ts +1 -0
- package/src/provider.tsx +13 -122
- package/src/use-local-stores.ts +86 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Container } from 'inversify';
|
|
2
|
+
import { Token } from './common';
|
|
3
|
+
import {
|
|
4
|
+
formatToken,
|
|
5
|
+
getProvideTokenWithClass,
|
|
6
|
+
isClassProviderConfigEntryToken,
|
|
7
|
+
isClassProviderConfigEntryUseClass,
|
|
8
|
+
isProviderConfigEntryInheritable,
|
|
9
|
+
isValueProviderConfigEntry,
|
|
10
|
+
ProviderConfigEntries,
|
|
11
|
+
ProviderConfigEntry,
|
|
12
|
+
} from './entry-helpers';
|
|
13
|
+
import { isStore, Store } from './store';
|
|
14
|
+
|
|
15
|
+
export function getStoreInstances(
|
|
16
|
+
container: Container,
|
|
17
|
+
{ singletons = [], instances = [] }: ProviderConfigEntries
|
|
18
|
+
): Store[] {
|
|
19
|
+
return [...singletons, ...instances].reduce((out, entry) => {
|
|
20
|
+
const [provide, useClass] = getProvideTokenWithClass(entry);
|
|
21
|
+
|
|
22
|
+
if (provide && useClass && container.isCurrentBound(provide) && isStore(useClass)) {
|
|
23
|
+
out.push(container.get(provide));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return out;
|
|
27
|
+
}, [] as Store[]);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const bindEntry =
|
|
31
|
+
(container: Container, checkInherit: boolean) => (entry: ProviderConfigEntry<any>) => {
|
|
32
|
+
if (
|
|
33
|
+
checkInherit &&
|
|
34
|
+
isProviderConfigEntryInheritable(entry) &&
|
|
35
|
+
entry.inherit &&
|
|
36
|
+
container.isBound(entry.provide)
|
|
37
|
+
) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (isClassProviderConfigEntryUseClass(entry)) {
|
|
42
|
+
return container.bind(entry.provide).to(entry.useClass);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (isClassProviderConfigEntryToken(entry)) {
|
|
46
|
+
return container.bind(entry.provide).to(entry.provide);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (isValueProviderConfigEntry(entry)) {
|
|
50
|
+
// using toDynamicValue to return BindingInSyntax
|
|
51
|
+
return container.bind(entry.provide).toDynamicValue(() => entry.useValue);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return container.bind(entry).to(entry);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const unbindEntry = (container: Container) => (entry: ProviderConfigEntry<any>) => {
|
|
58
|
+
if (isProviderConfigEntryInheritable(entry) && !container.isCurrentBound(entry.provide)) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (
|
|
62
|
+
isClassProviderConfigEntryUseClass(entry) ||
|
|
63
|
+
isValueProviderConfigEntry(entry) ||
|
|
64
|
+
isClassProviderConfigEntryToken(entry)
|
|
65
|
+
) {
|
|
66
|
+
return container.unbind(entry.provide);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return container.unbind(entry);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export function bindEntries(
|
|
73
|
+
container: Container,
|
|
74
|
+
{ singletons = [], instances = [] }: ProviderConfigEntries
|
|
75
|
+
) {
|
|
76
|
+
singletons.map(bindEntry(container, true)).forEach(binding => {
|
|
77
|
+
binding?.inSingletonScope();
|
|
78
|
+
});
|
|
79
|
+
instances.forEach(bindEntry(container, false));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function unbindEntries(
|
|
83
|
+
container: Container,
|
|
84
|
+
{ singletons = [], instances = [] }: ProviderConfigEntries
|
|
85
|
+
) {
|
|
86
|
+
[...singletons, ...instances].forEach(unbindEntry(container));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function initializeStores(stores: Store[], onError: (error: any) => void) {
|
|
90
|
+
const promises: Promise<void>[] = [];
|
|
91
|
+
|
|
92
|
+
for (const instance of stores) {
|
|
93
|
+
let promise;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
promise = instance.initialize?.();
|
|
97
|
+
} catch (e: any) {
|
|
98
|
+
onError(e);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (promise && typeof promise.catch === 'function') {
|
|
102
|
+
promises.push(promise.catch(onError));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return promises;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function disposeStores(stores: Store[]) {
|
|
110
|
+
for (const instance of stores) {
|
|
111
|
+
instance.dispose?.();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function formatDuplicatesMessage(source: string, duplicates: Token<any>[]): string {
|
|
116
|
+
const names = duplicates.map(formatToken).join(', ');
|
|
117
|
+
return `${source}: token listed more than once: ${names}. Remove the duplicate entries.`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Rethrow asynchronously so the error reaches the global error handler
|
|
121
|
+
export function rethrowAsync(e: any) {
|
|
122
|
+
setTimeout(() => {
|
|
123
|
+
throw e;
|
|
124
|
+
});
|
|
125
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ export { Provider } from './provider';
|
|
|
8
8
|
export type { ProviderProps } from './provider';
|
|
9
9
|
export { Store } from './store';
|
|
10
10
|
export { useDependencies, useOptionalDependencies } from './use-dependencies';
|
|
11
|
+
export { useLocalStores } from './use-local-stores';
|
|
11
12
|
|
|
12
13
|
export { Container, inject, injectable, optional, unmanaged } from 'inversify';
|
|
13
14
|
export type { interfaces } from 'inversify';
|
package/src/provider.tsx
CHANGED
|
@@ -1,125 +1,19 @@
|
|
|
1
1
|
import { Container } from 'inversify';
|
|
2
2
|
import { FC, PropsWithChildren, ReactNode, useContext, useEffect, useState } from 'react';
|
|
3
3
|
import { Await } from './await';
|
|
4
|
-
import { ContainerContext
|
|
5
|
-
import { dedupeEntries } from './dedupe-entries';
|
|
4
|
+
import { ContainerContext } from './common';
|
|
6
5
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} from './
|
|
6
|
+
bindEntries,
|
|
7
|
+
disposeStores,
|
|
8
|
+
formatDuplicatesMessage,
|
|
9
|
+
getStoreInstances,
|
|
10
|
+
initializeStores,
|
|
11
|
+
rethrowAsync,
|
|
12
|
+
unbindEntries,
|
|
13
|
+
} from './container-config';
|
|
14
|
+
import { dedupeEntries } from './dedupe-entries';
|
|
15
|
+
import { ProviderConfigEntries } from './entry-helpers';
|
|
16
16
|
import { defaultErrorFallback, defaultLoadingFallback } from './fallbacks';
|
|
17
|
-
import { isStore, Store } from './store';
|
|
18
|
-
|
|
19
|
-
function getStoreInstances(
|
|
20
|
-
container: Container,
|
|
21
|
-
{ singletons = [], instances = [] }: ProviderConfigEntries
|
|
22
|
-
): Store[] {
|
|
23
|
-
return [...singletons, ...instances].reduce((out, entry) => {
|
|
24
|
-
const [provide, useClass] = getProvideTokenWithClass(entry);
|
|
25
|
-
|
|
26
|
-
if (provide && useClass && container.isCurrentBound(provide) && isStore(useClass)) {
|
|
27
|
-
out.push(container.get(provide));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return out;
|
|
31
|
-
}, [] as Store[]);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const bindEntry =
|
|
35
|
-
(container: Container, checkInherit: boolean) => (entry: ProviderConfigEntry<any>) => {
|
|
36
|
-
if (
|
|
37
|
-
checkInherit &&
|
|
38
|
-
isProviderConfigEntryInheritable(entry) &&
|
|
39
|
-
entry.inherit &&
|
|
40
|
-
container.isBound(entry.provide)
|
|
41
|
-
) {
|
|
42
|
-
return undefined;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (isClassProviderConfigEntryUseClass(entry)) {
|
|
46
|
-
return container.bind(entry.provide).to(entry.useClass);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (isClassProviderConfigEntryToken(entry)) {
|
|
50
|
-
return container.bind(entry.provide).to(entry.provide);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (isValueProviderConfigEntry(entry)) {
|
|
54
|
-
// using toDynamicValue to return BindingInSyntax
|
|
55
|
-
return container.bind(entry.provide).toDynamicValue(() => entry.useValue);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return container.bind(entry).to(entry);
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const unbindEntry = (container: Container) => (entry: ProviderConfigEntry<any>) => {
|
|
62
|
-
if (isProviderConfigEntryInheritable(entry) && !container.isCurrentBound(entry.provide)) {
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
if (
|
|
66
|
-
isClassProviderConfigEntryUseClass(entry) ||
|
|
67
|
-
isValueProviderConfigEntry(entry) ||
|
|
68
|
-
isClassProviderConfigEntryToken(entry)
|
|
69
|
-
) {
|
|
70
|
-
return container.unbind(entry.provide);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return container.unbind(entry);
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
function bindEntries(
|
|
77
|
-
container: Container,
|
|
78
|
-
{ singletons = [], instances = [] }: ProviderConfigEntries
|
|
79
|
-
) {
|
|
80
|
-
singletons.map(bindEntry(container, true)).forEach(binding => {
|
|
81
|
-
binding?.inSingletonScope();
|
|
82
|
-
});
|
|
83
|
-
instances.forEach(bindEntry(container, false));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function unbindEntries(
|
|
87
|
-
container: Container,
|
|
88
|
-
{ singletons = [], instances = [] }: ProviderConfigEntries
|
|
89
|
-
) {
|
|
90
|
-
[...singletons, ...instances].forEach(unbindEntry(container));
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function initializeStores(stores: Store[], onError: (error: any) => void) {
|
|
94
|
-
const promises: Promise<void>[] = [];
|
|
95
|
-
|
|
96
|
-
for (const instance of stores) {
|
|
97
|
-
let promise;
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
promise = instance.initialize?.();
|
|
101
|
-
} catch (e: any) {
|
|
102
|
-
onError(e);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (promise && typeof promise.catch === 'function') {
|
|
106
|
-
promises.push(promise.catch(onError));
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return promises;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function disposeStores(stores: Store[]) {
|
|
114
|
-
for (const instance of stores) {
|
|
115
|
-
instance.dispose?.();
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function formatDuplicatesMessage(duplicates: Token<any>[]): string {
|
|
120
|
-
const names = duplicates.map(formatToken).join(', ');
|
|
121
|
-
return `Provider: token listed more than once: ${names}. Remove the duplicate entries.`;
|
|
122
|
-
}
|
|
123
17
|
|
|
124
18
|
export type ProviderProps = PropsWithChildren<
|
|
125
19
|
ProviderConfigEntries & {
|
|
@@ -147,16 +41,13 @@ export const Provider: FC<ProviderProps> = ({
|
|
|
147
41
|
|
|
148
42
|
const handleError = (e: any) => {
|
|
149
43
|
setError(true);
|
|
150
|
-
|
|
151
|
-
setTimeout(() => {
|
|
152
|
-
throw e;
|
|
153
|
-
});
|
|
44
|
+
rethrowAsync(e);
|
|
154
45
|
};
|
|
155
46
|
|
|
156
47
|
const config = dedupeEntries({ singletons, instances });
|
|
157
48
|
|
|
158
49
|
if (config.duplicates.length > 0) {
|
|
159
|
-
const message = formatDuplicatesMessage(config.duplicates);
|
|
50
|
+
const message = formatDuplicatesMessage('Provider', config.duplicates);
|
|
160
51
|
if (process.env.NODE_ENV !== 'production') {
|
|
161
52
|
handleError(new Error(message));
|
|
162
53
|
return () => {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Container, interfaces } from 'inversify';
|
|
2
|
+
import { useContext, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { ContainerContext } from './common';
|
|
4
|
+
import {
|
|
5
|
+
bindEntries,
|
|
6
|
+
disposeStores,
|
|
7
|
+
formatDuplicatesMessage,
|
|
8
|
+
getStoreInstances,
|
|
9
|
+
initializeStores,
|
|
10
|
+
rethrowAsync,
|
|
11
|
+
} from './container-config';
|
|
12
|
+
import { dedupeEntries } from './dedupe-entries';
|
|
13
|
+
import { Store } from './store';
|
|
14
|
+
|
|
15
|
+
type Newables<T extends any[]> = {
|
|
16
|
+
[P in keyof T]: interfaces.Newable<T[P]>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
interface LocalStores<T extends any[]> {
|
|
20
|
+
instances: T;
|
|
21
|
+
storeInstances: Store[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useLocalStores<T extends any[]>(...stores: Newables<T>): [T, boolean] {
|
|
25
|
+
const parentContainer = useContext(ContainerContext);
|
|
26
|
+
const ref = useRef<LocalStores<T> | null>(null);
|
|
27
|
+
|
|
28
|
+
/*
|
|
29
|
+
* Build the private container once, eagerly, so the instances are available
|
|
30
|
+
* synchronously on the first render. That is the reason this hook exists.
|
|
31
|
+
*/
|
|
32
|
+
if (ref.current === null) {
|
|
33
|
+
const container = new Container();
|
|
34
|
+
container.parent = parentContainer;
|
|
35
|
+
|
|
36
|
+
const config = dedupeEntries({ singletons: stores });
|
|
37
|
+
|
|
38
|
+
if (config.duplicates.length > 0) {
|
|
39
|
+
const message = formatDuplicatesMessage('useLocalStores', config.duplicates);
|
|
40
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
41
|
+
throw new Error(message);
|
|
42
|
+
}
|
|
43
|
+
// eslint-disable-next-line no-console
|
|
44
|
+
console.warn(message);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
bindEntries(container, config);
|
|
48
|
+
|
|
49
|
+
const storeInstances = getStoreInstances(container, config);
|
|
50
|
+
|
|
51
|
+
ref.current = {
|
|
52
|
+
instances: stores.map(store => container.get(store)) as T,
|
|
53
|
+
storeInstances,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const { instances, storeInstances } = ref.current;
|
|
58
|
+
const [isInitialized, setIsInitialized] = useState(false);
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
let cancelled = false;
|
|
62
|
+
|
|
63
|
+
const promises = initializeStores(storeInstances, e => {
|
|
64
|
+
rethrowAsync(e);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (promises.length === 0) {
|
|
68
|
+
setIsInitialized(true);
|
|
69
|
+
} else {
|
|
70
|
+
Promise.all(promises).then(() => {
|
|
71
|
+
if (!cancelled) {
|
|
72
|
+
setIsInitialized(true);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return () => {
|
|
78
|
+
cancelled = true;
|
|
79
|
+
disposeStores(storeInstances);
|
|
80
|
+
setIsInitialized(false);
|
|
81
|
+
};
|
|
82
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
return [instances, isInitialized];
|
|
86
|
+
}
|