@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.
@@ -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, Token } from './common';
5
- import { dedupeEntries } from './dedupe-entries';
4
+ import { ContainerContext } from './common';
6
5
  import {
7
- formatToken,
8
- getProvideTokenWithClass,
9
- isClassProviderConfigEntryToken,
10
- isClassProviderConfigEntryUseClass,
11
- isProviderConfigEntryInheritable,
12
- isValueProviderConfigEntry,
13
- ProviderConfigEntries,
14
- ProviderConfigEntry,
15
- } from './entry-helpers';
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
- // Rethrow asynchronously for global error handler
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
+ }