@navios/di-react 0.1.1 → 0.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/CHANGELOG.md +39 -0
- package/README.md +609 -28
- package/lib/index.d.mts +297 -18
- package/lib/index.d.mts.map +1 -0
- package/lib/index.d.ts +297 -18
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +536 -335
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +533 -334
- package/lib/index.mjs.map +1 -1
- package/package.json +3 -3
- package/project.json +2 -2
- package/src/hooks/__tests__/use-service.spec.mts +1 -1
- package/src/hooks/index.mts +8 -2
- package/src/hooks/use-container.mts +47 -2
- package/src/hooks/use-invalidate.mts +3 -3
- package/src/hooks/use-optional-service.mts +59 -34
- package/src/hooks/use-scope.mts +66 -5
- package/src/hooks/use-service.mts +44 -18
- package/src/hooks/use-suspense-service.mts +48 -29
- package/src/providers/__tests__/scope-provider.spec.mts +84 -1
- package/src/providers/context.mts +11 -1
- package/src/providers/index.mts +2 -2
- package/src/providers/scope-provider.mts +34 -22
- package/{tsup.config.mts → tsdown.config.mts} +4 -3
- package/lib/_tsup-dts-rollup.d.mts +0 -304
- package/lib/_tsup-dts-rollup.d.ts +0 -304
package/lib/index.js
CHANGED
|
@@ -1,394 +1,595 @@
|
|
|
1
|
-
|
|
1
|
+
let react = require("react");
|
|
2
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
//#region src/providers/context.mts
|
|
5
|
+
/**
|
|
6
|
+
* Context for the root Container.
|
|
7
|
+
* This is set by ContainerProvider and provides the base container.
|
|
8
|
+
*/
|
|
9
|
+
const ContainerContext = (0, react.createContext)(null);
|
|
10
|
+
/**
|
|
11
|
+
* Context for the current ScopedContainer (if inside a ScopeProvider).
|
|
12
|
+
* This allows nested components to access request-scoped services.
|
|
13
|
+
*/
|
|
14
|
+
const ScopedContainerContext = (0, react.createContext)(null);
|
|
5
15
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
function ContainerProvider({
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/providers/container-provider.mts
|
|
18
|
+
function ContainerProvider({ container, children }) {
|
|
19
|
+
return (0, react_jsx_runtime.jsx)(ContainerContext.Provider, {
|
|
20
|
+
value: container,
|
|
21
|
+
children
|
|
22
|
+
});
|
|
13
23
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/providers/scope-provider.mts
|
|
27
|
+
/**
|
|
28
|
+
* ScopeProvider creates a new request scope for dependency injection.
|
|
29
|
+
*
|
|
30
|
+
* Services with `scope: 'Request'` will be instantiated once per ScopeProvider
|
|
31
|
+
* and shared among all components within that provider.
|
|
32
|
+
*
|
|
33
|
+
* This is useful for:
|
|
34
|
+
* - Table rows that need isolated state
|
|
35
|
+
* - Modal dialogs with their own service instances
|
|
36
|
+
* - Multi-tenant scenarios
|
|
37
|
+
* - Any case where you need isolated service instances
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* // Each row gets its own RowStateService instance
|
|
42
|
+
* {rows.map(row => (
|
|
43
|
+
* <ScopeProvider key={row.id} scopeId={row.id} metadata={{ rowData: row }}>
|
|
44
|
+
* <TableRow />
|
|
45
|
+
* </ScopeProvider>
|
|
46
|
+
* ))}
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
function ScopeProvider({ scopeId, metadata, priority = 100, children }) {
|
|
50
|
+
const container = (0, react.useContext)(ContainerContext);
|
|
51
|
+
if (!container) throw new Error("ScopeProvider must be used within a ContainerProvider");
|
|
52
|
+
const generatedId = (0, react.useId)();
|
|
53
|
+
const effectiveScopeId = scopeId ?? generatedId;
|
|
54
|
+
const scopedContainerRef = (0, react.useRef)(null);
|
|
55
|
+
if (!scopedContainerRef.current) {
|
|
56
|
+
if (!container.hasActiveRequest(effectiveScopeId)) scopedContainerRef.current = container.beginRequest(effectiveScopeId, metadata, priority);
|
|
57
|
+
}
|
|
58
|
+
(0, react.useEffect)(() => {
|
|
59
|
+
const scopedContainer = scopedContainerRef.current;
|
|
60
|
+
return () => {
|
|
61
|
+
if (scopedContainer) scopedContainer.endRequest();
|
|
62
|
+
};
|
|
63
|
+
}, []);
|
|
64
|
+
if (!scopedContainerRef.current) return null;
|
|
65
|
+
return (0, react_jsx_runtime.jsx)(ScopedContainerContext.Provider, {
|
|
66
|
+
value: scopedContainerRef.current,
|
|
67
|
+
children
|
|
68
|
+
});
|
|
20
69
|
}
|
|
21
70
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/hooks/use-container.mts
|
|
73
|
+
/**
|
|
74
|
+
* Hook to get the current container (ScopedContainer if inside ScopeProvider, otherwise Container).
|
|
75
|
+
*
|
|
76
|
+
* This is the primary hook for accessing the DI container. It automatically
|
|
77
|
+
* returns the correct container based on context:
|
|
78
|
+
* - Inside a ScopeProvider: returns the ScopedContainer for request-scoped services
|
|
79
|
+
* - Outside a ScopeProvider: returns the root Container
|
|
80
|
+
*
|
|
81
|
+
* @returns The current container (ScopedContainer or Container)
|
|
82
|
+
*/
|
|
83
|
+
function useContainer() {
|
|
84
|
+
const scopedContainer = (0, react.useContext)(ScopedContainerContext);
|
|
85
|
+
const container = (0, react.useContext)(ContainerContext);
|
|
86
|
+
if (scopedContainer) return scopedContainer;
|
|
87
|
+
if (!container) throw new Error("useContainer must be used within a ContainerProvider");
|
|
88
|
+
return container;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Hook to get the root Container, regardless of whether we're inside a ScopeProvider.
|
|
92
|
+
*
|
|
93
|
+
* Use this when you need access to the root container specifically,
|
|
94
|
+
* for example to create new request scopes programmatically.
|
|
95
|
+
*
|
|
96
|
+
* @returns The root Container
|
|
97
|
+
*/
|
|
98
|
+
function useRootContainer() {
|
|
99
|
+
const container = (0, react.useContext)(ContainerContext);
|
|
100
|
+
if (!container) throw new Error("useRootContainer must be used within a ContainerProvider");
|
|
101
|
+
return container;
|
|
47
102
|
}
|
|
103
|
+
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/hooks/use-service.mts
|
|
48
106
|
function serviceReducer(state, action) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
107
|
+
switch (action.type) {
|
|
108
|
+
case "loading": return { status: "loading" };
|
|
109
|
+
case "success": return {
|
|
110
|
+
status: "success",
|
|
111
|
+
data: action.data
|
|
112
|
+
};
|
|
113
|
+
case "error": return {
|
|
114
|
+
status: "error",
|
|
115
|
+
error: action.error
|
|
116
|
+
};
|
|
117
|
+
case "reset": return { status: "idle" };
|
|
118
|
+
default: return state;
|
|
119
|
+
}
|
|
61
120
|
}
|
|
62
121
|
function useService(token, args) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
122
|
+
const container = useContainer();
|
|
123
|
+
const serviceLocator = useRootContainer().getServiceLocator();
|
|
124
|
+
const initialSyncInstanceRef = (0, react.useRef)(void 0);
|
|
125
|
+
const isFirstRenderRef = (0, react.useRef)(true);
|
|
126
|
+
if (isFirstRenderRef.current) {
|
|
127
|
+
initialSyncInstanceRef.current = container.tryGetSync(token, args);
|
|
128
|
+
isFirstRenderRef.current = false;
|
|
129
|
+
}
|
|
130
|
+
const [state, dispatch] = (0, react.useReducer)(serviceReducer, initialSyncInstanceRef.current ? {
|
|
131
|
+
status: "success",
|
|
132
|
+
data: initialSyncInstanceRef.current
|
|
133
|
+
} : { status: "idle" });
|
|
134
|
+
const instanceNameRef = (0, react.useRef)(null);
|
|
135
|
+
const [refetchCounter, setRefetchCounter] = (0, react.useState)(0);
|
|
136
|
+
if (process.env.NODE_ENV === "development") {
|
|
137
|
+
const argsRef = (0, react.useRef)(args);
|
|
138
|
+
(0, react.useEffect)(() => {
|
|
139
|
+
if (argsRef.current !== args) {
|
|
140
|
+
if (JSON.stringify(argsRef.current) === JSON.stringify(args)) console.log(`WARNING: useService called with args that look the same but are different instances: ${JSON.stringify(argsRef.current)} !== ${JSON.stringify(args)}!
|
|
75
141
|
This is likely because you are using not memoized value that is not stable.
|
|
76
142
|
Please use a memoized value or use a different approach to pass the args.
|
|
77
143
|
Example:
|
|
78
144
|
const args = useMemo(() => ({ userId: '123' }), [])
|
|
79
145
|
return useService(UserToken, args)
|
|
80
146
|
`);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
147
|
+
argsRef.current = args;
|
|
148
|
+
}
|
|
149
|
+
}, [args]);
|
|
150
|
+
}
|
|
151
|
+
(0, react.useEffect)(() => {
|
|
152
|
+
const eventBus = serviceLocator.getEventBus();
|
|
153
|
+
let unsubscribe;
|
|
154
|
+
let isMounted = true;
|
|
155
|
+
const fetchAndSubscribe = async () => {
|
|
156
|
+
try {
|
|
157
|
+
const instance = await container.get(token, args);
|
|
158
|
+
if (!isMounted) return;
|
|
159
|
+
const instanceName = serviceLocator.getInstanceIdentifier(token, args);
|
|
160
|
+
instanceNameRef.current = instanceName;
|
|
161
|
+
dispatch({
|
|
162
|
+
type: "success",
|
|
163
|
+
data: instance
|
|
164
|
+
});
|
|
165
|
+
unsubscribe = eventBus.on(instanceName, "destroy", () => {
|
|
166
|
+
if (isMounted) {
|
|
167
|
+
dispatch({ type: "loading" });
|
|
168
|
+
fetchAndSubscribe();
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (isMounted) dispatch({
|
|
173
|
+
type: "error",
|
|
174
|
+
error
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
if (initialSyncInstanceRef.current && refetchCounter === 0) {
|
|
179
|
+
const instanceName = serviceLocator.getInstanceIdentifier(token, args);
|
|
180
|
+
instanceNameRef.current = instanceName;
|
|
181
|
+
unsubscribe = eventBus.on(instanceName, "destroy", () => {
|
|
182
|
+
if (isMounted) {
|
|
183
|
+
dispatch({ type: "loading" });
|
|
184
|
+
fetchAndSubscribe();
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
} else {
|
|
188
|
+
dispatch({ type: "loading" });
|
|
189
|
+
fetchAndSubscribe();
|
|
190
|
+
}
|
|
191
|
+
return () => {
|
|
192
|
+
isMounted = false;
|
|
193
|
+
unsubscribe?.();
|
|
194
|
+
};
|
|
195
|
+
}, [
|
|
196
|
+
container,
|
|
197
|
+
serviceLocator,
|
|
198
|
+
token,
|
|
199
|
+
args,
|
|
200
|
+
refetchCounter
|
|
201
|
+
]);
|
|
202
|
+
const refetch = (0, react.useCallback)(() => {
|
|
203
|
+
setRefetchCounter((c) => c + 1);
|
|
204
|
+
}, []);
|
|
205
|
+
return {
|
|
206
|
+
data: state.status === "success" ? state.data : void 0,
|
|
207
|
+
error: state.status === "error" ? state.error : void 0,
|
|
208
|
+
isLoading: state.status === "loading",
|
|
209
|
+
isSuccess: state.status === "success",
|
|
210
|
+
isError: state.status === "error",
|
|
211
|
+
refetch
|
|
212
|
+
};
|
|
140
213
|
}
|
|
141
|
-
|
|
214
|
+
|
|
215
|
+
//#endregion
|
|
216
|
+
//#region src/hooks/use-suspense-service.mts
|
|
217
|
+
const cacheMap = /* @__PURE__ */ new WeakMap();
|
|
142
218
|
function getCacheKey(token, args) {
|
|
143
|
-
|
|
144
|
-
return `${tokenId}:${JSON.stringify(args ?? null)}`;
|
|
219
|
+
return `${typeof token === "function" ? token.name : token.id || token.token?.id || String(token)}:${JSON.stringify(args ?? null)}`;
|
|
145
220
|
}
|
|
146
221
|
function getCache(container) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
222
|
+
let cache = cacheMap.get(container);
|
|
223
|
+
if (!cache) {
|
|
224
|
+
cache = /* @__PURE__ */ new Map();
|
|
225
|
+
cacheMap.set(container, cache);
|
|
226
|
+
}
|
|
227
|
+
return cache;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Sets up invalidation subscription for a cache entry if not already subscribed.
|
|
231
|
+
* When the service is destroyed, clears the cache and notifies subscribers.
|
|
232
|
+
*/
|
|
233
|
+
function setupInvalidationSubscription(entry, serviceLocator) {
|
|
234
|
+
if (entry.unsubscribe || !entry.instanceName) return;
|
|
235
|
+
entry.unsubscribe = serviceLocator.getEventBus().on(entry.instanceName, "destroy", () => {
|
|
236
|
+
entry.result = void 0;
|
|
237
|
+
entry.error = void 0;
|
|
238
|
+
entry.status = "pending";
|
|
239
|
+
entry.promise = null;
|
|
240
|
+
entry.subscribers.forEach((callback) => callback());
|
|
241
|
+
});
|
|
153
242
|
}
|
|
154
243
|
function useSuspenseService(token, args) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
console.log(`WARNING: useService called with args that look the same but are different instances: ${JSON.stringify(argsRef.current)} !== ${JSON.stringify(args)}!
|
|
244
|
+
const container = useContainer();
|
|
245
|
+
const serviceLocator = useRootContainer().getServiceLocator();
|
|
246
|
+
const cache = getCache(container);
|
|
247
|
+
const cacheKey = getCacheKey(token, args);
|
|
248
|
+
const entryRef = (0, react.useRef)(null);
|
|
249
|
+
if (process.env.NODE_ENV === "development") {
|
|
250
|
+
const argsRef = (0, react.useRef)(args);
|
|
251
|
+
(0, react.useEffect)(() => {
|
|
252
|
+
if (argsRef.current !== args) {
|
|
253
|
+
if (JSON.stringify(argsRef.current) === JSON.stringify(args)) console.log(`WARNING: useService called with args that look the same but are different instances: ${JSON.stringify(argsRef.current)} !== ${JSON.stringify(args)}!
|
|
166
254
|
This is likely because you are using not memoized value that is not stable.
|
|
167
255
|
Please use a memoized value or use a different approach to pass the args.
|
|
168
256
|
Example:
|
|
169
257
|
const args = useMemo(() => ({ userId: '123' }), [])
|
|
170
258
|
return useService(UserToken, args)
|
|
171
259
|
`);
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
[entry]
|
|
235
|
-
);
|
|
236
|
-
const getSnapshot = react.useCallback(() => {
|
|
237
|
-
return `${entry.status}:${entry.version}`;
|
|
238
|
-
}, [entry]);
|
|
239
|
-
react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
240
|
-
react.useEffect(() => {
|
|
241
|
-
return () => {
|
|
242
|
-
};
|
|
243
|
-
}, []);
|
|
244
|
-
if (entry.status === "pending" && !entry.promise) {
|
|
245
|
-
fetchService();
|
|
246
|
-
}
|
|
247
|
-
if (entry.status === "pending") {
|
|
248
|
-
throw entry.promise;
|
|
249
|
-
}
|
|
250
|
-
if (entry.status === "rejected") {
|
|
251
|
-
throw entry.error;
|
|
252
|
-
}
|
|
253
|
-
return entry.result;
|
|
260
|
+
argsRef.current = args;
|
|
261
|
+
}
|
|
262
|
+
}, [args]);
|
|
263
|
+
}
|
|
264
|
+
if (!cache.has(cacheKey)) {
|
|
265
|
+
const syncInstance = container.tryGetSync(token, args);
|
|
266
|
+
const entry$1 = {
|
|
267
|
+
promise: null,
|
|
268
|
+
result: syncInstance ?? void 0,
|
|
269
|
+
error: void 0,
|
|
270
|
+
status: syncInstance ? "resolved" : "pending",
|
|
271
|
+
version: 0,
|
|
272
|
+
subscribers: /* @__PURE__ */ new Set(),
|
|
273
|
+
instanceName: syncInstance ? serviceLocator.getInstanceIdentifier(token, args) : null,
|
|
274
|
+
unsubscribe: void 0
|
|
275
|
+
};
|
|
276
|
+
cache.set(cacheKey, entry$1);
|
|
277
|
+
}
|
|
278
|
+
const entry = cache.get(cacheKey);
|
|
279
|
+
entryRef.current = entry;
|
|
280
|
+
const fetchService = (0, react.useCallback)(() => {
|
|
281
|
+
const currentEntry = entryRef.current;
|
|
282
|
+
if (!currentEntry) return;
|
|
283
|
+
currentEntry.status = "pending";
|
|
284
|
+
currentEntry.version++;
|
|
285
|
+
currentEntry.promise = container.get(token, args).then((instance) => {
|
|
286
|
+
currentEntry.result = instance;
|
|
287
|
+
currentEntry.status = "resolved";
|
|
288
|
+
currentEntry.instanceName = serviceLocator.getInstanceIdentifier(token, args);
|
|
289
|
+
setupInvalidationSubscription(currentEntry, serviceLocator);
|
|
290
|
+
currentEntry.subscribers.forEach((callback) => callback());
|
|
291
|
+
return instance;
|
|
292
|
+
}).catch((error) => {
|
|
293
|
+
currentEntry.error = error;
|
|
294
|
+
currentEntry.status = "rejected";
|
|
295
|
+
throw error;
|
|
296
|
+
});
|
|
297
|
+
return currentEntry.promise;
|
|
298
|
+
}, [
|
|
299
|
+
container,
|
|
300
|
+
serviceLocator,
|
|
301
|
+
token,
|
|
302
|
+
args
|
|
303
|
+
]);
|
|
304
|
+
const subscribe = (0, react.useCallback)((callback) => {
|
|
305
|
+
entry.subscribers.add(callback);
|
|
306
|
+
return () => {
|
|
307
|
+
entry.subscribers.delete(callback);
|
|
308
|
+
};
|
|
309
|
+
}, [entry]);
|
|
310
|
+
const getSnapshot = (0, react.useCallback)(() => {
|
|
311
|
+
return `${entry.status}:${entry.version}`;
|
|
312
|
+
}, [entry]);
|
|
313
|
+
(0, react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
|
|
314
|
+
(0, react.useEffect)(() => {
|
|
315
|
+
const currentEntry = entryRef.current;
|
|
316
|
+
if (currentEntry && currentEntry.status === "resolved" && currentEntry.instanceName && !currentEntry.unsubscribe) setupInvalidationSubscription(currentEntry, serviceLocator);
|
|
317
|
+
}, [serviceLocator, entry]);
|
|
318
|
+
if (entry.status === "pending" && !entry.promise) fetchService();
|
|
319
|
+
if (entry.status === "pending") throw entry.promise;
|
|
320
|
+
if (entry.status === "rejected") throw entry.error;
|
|
321
|
+
return entry.result;
|
|
254
322
|
}
|
|
323
|
+
|
|
324
|
+
//#endregion
|
|
325
|
+
//#region src/hooks/use-optional-service.mts
|
|
255
326
|
function optionalServiceReducer(state, action) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
327
|
+
switch (action.type) {
|
|
328
|
+
case "loading": return { status: "loading" };
|
|
329
|
+
case "success": return {
|
|
330
|
+
status: "success",
|
|
331
|
+
data: action.data
|
|
332
|
+
};
|
|
333
|
+
case "not-found": return { status: "not-found" };
|
|
334
|
+
case "error": return {
|
|
335
|
+
status: "error",
|
|
336
|
+
error: action.error
|
|
337
|
+
};
|
|
338
|
+
case "reset": return { status: "idle" };
|
|
339
|
+
default: return state;
|
|
340
|
+
}
|
|
270
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* Hook to optionally load a service from the DI container.
|
|
344
|
+
*
|
|
345
|
+
* Unlike useService, this hook does NOT throw an error if the service is not registered.
|
|
346
|
+
* Instead, it returns `isNotFound: true` when the service doesn't exist.
|
|
347
|
+
*
|
|
348
|
+
* This is useful for:
|
|
349
|
+
* - Optional dependencies that may or may not be configured
|
|
350
|
+
* - Feature flags where a service might not be available
|
|
351
|
+
* - Plugins or extensions that are conditionally registered
|
|
352
|
+
*
|
|
353
|
+
* @example
|
|
354
|
+
* ```tsx
|
|
355
|
+
* function Analytics() {
|
|
356
|
+
* const { data: analytics, isNotFound } = useOptionalService(AnalyticsService)
|
|
357
|
+
*
|
|
358
|
+
* if (isNotFound) {
|
|
359
|
+
* // Analytics service not configured, skip tracking
|
|
360
|
+
* return null
|
|
361
|
+
* }
|
|
362
|
+
*
|
|
363
|
+
* return <AnalyticsTracker service={analytics} />
|
|
364
|
+
* }
|
|
365
|
+
* ```
|
|
366
|
+
*/
|
|
271
367
|
function useOptionalService(token, args) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
368
|
+
const container = useContainer();
|
|
369
|
+
const serviceLocator = useRootContainer().getServiceLocator();
|
|
370
|
+
const initialSyncInstanceRef = (0, react.useRef)(void 0);
|
|
371
|
+
const isFirstRenderRef = (0, react.useRef)(true);
|
|
372
|
+
if (isFirstRenderRef.current) {
|
|
373
|
+
try {
|
|
374
|
+
initialSyncInstanceRef.current = container.tryGetSync(token, args);
|
|
375
|
+
} catch {}
|
|
376
|
+
isFirstRenderRef.current = false;
|
|
377
|
+
}
|
|
378
|
+
const [state, dispatch] = (0, react.useReducer)(optionalServiceReducer, initialSyncInstanceRef.current ? {
|
|
379
|
+
status: "success",
|
|
380
|
+
data: initialSyncInstanceRef.current
|
|
381
|
+
} : { status: "idle" });
|
|
382
|
+
const instanceNameRef = (0, react.useRef)(null);
|
|
383
|
+
if (process.env.NODE_ENV === "development") {
|
|
384
|
+
const argsRef = (0, react.useRef)(args);
|
|
385
|
+
(0, react.useEffect)(() => {
|
|
386
|
+
if (argsRef.current !== args) {
|
|
387
|
+
if (JSON.stringify(argsRef.current) === JSON.stringify(args)) console.log(`WARNING: useOptionalService called with args that look the same but are different instances: ${JSON.stringify(argsRef.current)} !== ${JSON.stringify(args)}!
|
|
282
388
|
This is likely because you are using not memoized value that is not stable.
|
|
283
389
|
Please use a memoized value or use a different approach to pass the args.
|
|
284
390
|
Example:
|
|
285
391
|
const args = useMemo(() => ({ userId: '123' }), [])
|
|
286
392
|
return useOptionalService(UserToken, args)
|
|
287
393
|
`);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
394
|
+
argsRef.current = args;
|
|
395
|
+
}
|
|
396
|
+
}, [args]);
|
|
397
|
+
}
|
|
398
|
+
const fetchService = (0, react.useCallback)(async () => {
|
|
399
|
+
dispatch({ type: "loading" });
|
|
400
|
+
try {
|
|
401
|
+
const instance = await container.get(token, args);
|
|
402
|
+
instanceNameRef.current = serviceLocator.getInstanceIdentifier(token, args);
|
|
403
|
+
dispatch({
|
|
404
|
+
type: "success",
|
|
405
|
+
data: instance
|
|
406
|
+
});
|
|
407
|
+
} catch (error) {
|
|
408
|
+
const err = error;
|
|
409
|
+
const errorMessage = err.message?.toLowerCase() ?? "";
|
|
410
|
+
if (errorMessage.includes("not found") || errorMessage.includes("not registered") || errorMessage.includes("no provider")) dispatch({ type: "not-found" });
|
|
411
|
+
else dispatch({
|
|
412
|
+
type: "error",
|
|
413
|
+
error: err
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}, [
|
|
417
|
+
container,
|
|
418
|
+
serviceLocator,
|
|
419
|
+
token,
|
|
420
|
+
args
|
|
421
|
+
]);
|
|
422
|
+
(0, react.useEffect)(() => {
|
|
423
|
+
const eventBus = serviceLocator.getEventBus();
|
|
424
|
+
let unsubscribe;
|
|
425
|
+
if (initialSyncInstanceRef.current) {
|
|
426
|
+
instanceNameRef.current = serviceLocator.getInstanceIdentifier(token, args);
|
|
427
|
+
unsubscribe = eventBus.on(instanceNameRef.current, "destroy", () => {
|
|
428
|
+
fetchService();
|
|
429
|
+
});
|
|
430
|
+
} else {
|
|
431
|
+
fetchService();
|
|
432
|
+
const setupSubscription = () => {
|
|
433
|
+
if (instanceNameRef.current) unsubscribe = eventBus.on(instanceNameRef.current, "destroy", () => {
|
|
434
|
+
fetchService();
|
|
435
|
+
});
|
|
436
|
+
};
|
|
437
|
+
const timeoutId = setTimeout(setupSubscription, 10);
|
|
438
|
+
return () => {
|
|
439
|
+
clearTimeout(timeoutId);
|
|
440
|
+
unsubscribe?.();
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
return () => {
|
|
444
|
+
unsubscribe?.();
|
|
445
|
+
};
|
|
446
|
+
}, [
|
|
447
|
+
fetchService,
|
|
448
|
+
serviceLocator,
|
|
449
|
+
token,
|
|
450
|
+
args
|
|
451
|
+
]);
|
|
452
|
+
return {
|
|
453
|
+
data: state.status === "success" ? state.data : void 0,
|
|
454
|
+
error: state.status === "error" ? state.error : void 0,
|
|
455
|
+
isLoading: state.status === "loading",
|
|
456
|
+
isSuccess: state.status === "success",
|
|
457
|
+
isNotFound: state.status === "not-found",
|
|
458
|
+
isError: state.status === "error",
|
|
459
|
+
refetch: fetchService
|
|
460
|
+
};
|
|
350
461
|
}
|
|
462
|
+
|
|
463
|
+
//#endregion
|
|
464
|
+
//#region src/hooks/use-invalidate.mts
|
|
351
465
|
function useInvalidate(token, args) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
466
|
+
const serviceLocator = useRootContainer().getServiceLocator();
|
|
467
|
+
return (0, react.useCallback)(async () => {
|
|
468
|
+
const instanceName = serviceLocator.getInstanceIdentifier(token, args);
|
|
469
|
+
await serviceLocator.invalidate(instanceName);
|
|
470
|
+
}, [
|
|
471
|
+
serviceLocator,
|
|
472
|
+
token,
|
|
473
|
+
args
|
|
474
|
+
]);
|
|
358
475
|
}
|
|
476
|
+
/**
|
|
477
|
+
* Hook that returns a function to invalidate a service instance directly.
|
|
478
|
+
*
|
|
479
|
+
* This is useful when you have the service instance and want to invalidate it
|
|
480
|
+
* without knowing its token.
|
|
481
|
+
*
|
|
482
|
+
* @example
|
|
483
|
+
* ```tsx
|
|
484
|
+
* function UserProfile() {
|
|
485
|
+
* const { data: user } = useService(UserService)
|
|
486
|
+
* const invalidateInstance = useInvalidateInstance()
|
|
487
|
+
*
|
|
488
|
+
* const handleRefresh = () => {
|
|
489
|
+
* if (user) {
|
|
490
|
+
* invalidateInstance(user)
|
|
491
|
+
* }
|
|
492
|
+
* }
|
|
493
|
+
*
|
|
494
|
+
* return (
|
|
495
|
+
* <div>
|
|
496
|
+
* <span>{user?.name}</span>
|
|
497
|
+
* <button onClick={handleRefresh}>Refresh</button>
|
|
498
|
+
* </div>
|
|
499
|
+
* )
|
|
500
|
+
* }
|
|
501
|
+
* ```
|
|
502
|
+
*/
|
|
359
503
|
function useInvalidateInstance() {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
},
|
|
365
|
-
[container]
|
|
366
|
-
);
|
|
504
|
+
const container = useContainer();
|
|
505
|
+
return (0, react.useCallback)(async (instance) => {
|
|
506
|
+
await container.invalidate(instance);
|
|
507
|
+
}, [container]);
|
|
367
508
|
}
|
|
509
|
+
|
|
510
|
+
//#endregion
|
|
511
|
+
//#region src/hooks/use-scope.mts
|
|
512
|
+
/**
|
|
513
|
+
* Hook to get the current scope ID.
|
|
514
|
+
* Returns null if not inside a ScopeProvider.
|
|
515
|
+
*/
|
|
368
516
|
function useScope() {
|
|
369
|
-
|
|
517
|
+
return (0, react.useContext)(ScopedContainerContext)?.getRequestId() ?? null;
|
|
370
518
|
}
|
|
519
|
+
/**
|
|
520
|
+
* Hook to get the current scope ID, throwing if not inside a ScopeProvider.
|
|
521
|
+
* Use this when your component requires a scope to function correctly.
|
|
522
|
+
*/
|
|
371
523
|
function useScopeOrThrow() {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
524
|
+
const scopeId = useScope();
|
|
525
|
+
if (scopeId === null) throw new Error("useScopeOrThrow must be used within a ScopeProvider. Wrap your component tree with <ScopeProvider> to create a request scope.");
|
|
526
|
+
return scopeId;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Hook to get the current ScopedContainer.
|
|
530
|
+
* Returns null if not inside a ScopeProvider.
|
|
531
|
+
*
|
|
532
|
+
* Use this to access scope metadata or other ScopedContainer methods.
|
|
533
|
+
*
|
|
534
|
+
* @example
|
|
535
|
+
* ```tsx
|
|
536
|
+
* function TableRow() {
|
|
537
|
+
* const scope = useScopedContainer()
|
|
538
|
+
* const rowData = scope?.getMetadata('rowData')
|
|
539
|
+
* // ...
|
|
540
|
+
* }
|
|
541
|
+
* ```
|
|
542
|
+
*/
|
|
543
|
+
function useScopedContainer() {
|
|
544
|
+
return (0, react.useContext)(ScopedContainerContext);
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Hook to get the current ScopedContainer, throwing if not inside a ScopeProvider.
|
|
548
|
+
* Use this when your component requires a scope to function correctly.
|
|
549
|
+
*/
|
|
550
|
+
function useScopedContainerOrThrow() {
|
|
551
|
+
const scopedContainer = useScopedContainer();
|
|
552
|
+
if (scopedContainer === null) throw new Error("useScopedContainerOrThrow must be used within a ScopeProvider. Wrap your component tree with <ScopeProvider> to create a request scope.");
|
|
553
|
+
return scopedContainer;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Hook to get metadata from the current scope.
|
|
557
|
+
* Returns undefined if not inside a ScopeProvider or if the key doesn't exist.
|
|
558
|
+
*
|
|
559
|
+
* @example
|
|
560
|
+
* ```tsx
|
|
561
|
+
* // In parent component:
|
|
562
|
+
* <ScopeProvider metadata={{ userId: '123', theme: 'dark' }}>
|
|
563
|
+
* <ChildComponent />
|
|
564
|
+
* </ScopeProvider>
|
|
565
|
+
*
|
|
566
|
+
* // In child component:
|
|
567
|
+
* function ChildComponent() {
|
|
568
|
+
* const userId = useScopeMetadata<string>('userId')
|
|
569
|
+
* const theme = useScopeMetadata<'light' | 'dark'>('theme')
|
|
570
|
+
* // ...
|
|
571
|
+
* }
|
|
572
|
+
* ```
|
|
573
|
+
*/
|
|
574
|
+
function useScopeMetadata(key) {
|
|
575
|
+
return (0, react.useContext)(ScopedContainerContext)?.getMetadata(key);
|
|
379
576
|
}
|
|
380
577
|
|
|
578
|
+
//#endregion
|
|
381
579
|
exports.ContainerContext = ContainerContext;
|
|
382
580
|
exports.ContainerProvider = ContainerProvider;
|
|
383
|
-
exports.ScopeContext = ScopeContext;
|
|
384
581
|
exports.ScopeProvider = ScopeProvider;
|
|
582
|
+
exports.ScopedContainerContext = ScopedContainerContext;
|
|
385
583
|
exports.useContainer = useContainer;
|
|
386
584
|
exports.useInvalidate = useInvalidate;
|
|
387
585
|
exports.useInvalidateInstance = useInvalidateInstance;
|
|
388
586
|
exports.useOptionalService = useOptionalService;
|
|
587
|
+
exports.useRootContainer = useRootContainer;
|
|
389
588
|
exports.useScope = useScope;
|
|
589
|
+
exports.useScopeMetadata = useScopeMetadata;
|
|
390
590
|
exports.useScopeOrThrow = useScopeOrThrow;
|
|
591
|
+
exports.useScopedContainer = useScopedContainer;
|
|
592
|
+
exports.useScopedContainerOrThrow = useScopedContainerOrThrow;
|
|
391
593
|
exports.useService = useService;
|
|
392
594
|
exports.useSuspenseService = useSuspenseService;
|
|
393
|
-
//# sourceMappingURL=index.js.map
|
|
394
595
|
//# sourceMappingURL=index.js.map
|