@navios/di-react 0.1.1 → 0.2.1
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 +44 -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 +8 -8
- 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.mjs
CHANGED
|
@@ -1,381 +1,580 @@
|
|
|
1
|
-
import { createContext,
|
|
2
|
-
import { jsx } from
|
|
1
|
+
import { createContext, useCallback, useContext, useEffect, useId, useReducer, useRef, useState, useSyncExternalStore } from "react";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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 = 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 = createContext(null);
|
|
15
|
+
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/providers/container-provider.mts
|
|
18
|
+
function ContainerProvider({ container, children }) {
|
|
19
|
+
return jsx(ContainerContext.Provider, {
|
|
20
|
+
value: container,
|
|
21
|
+
children
|
|
22
|
+
});
|
|
11
23
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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 = useContext(ContainerContext);
|
|
51
|
+
if (!container) throw new Error("ScopeProvider must be used within a ContainerProvider");
|
|
52
|
+
const generatedId = useId();
|
|
53
|
+
const effectiveScopeId = scopeId ?? generatedId;
|
|
54
|
+
const scopedContainerRef = useRef(null);
|
|
55
|
+
if (!scopedContainerRef.current) {
|
|
56
|
+
if (!container.hasActiveRequest(effectiveScopeId)) scopedContainerRef.current = container.beginRequest(effectiveScopeId, metadata, priority);
|
|
57
|
+
}
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const scopedContainer = scopedContainerRef.current;
|
|
60
|
+
return () => {
|
|
61
|
+
if (scopedContainer) scopedContainer.endRequest();
|
|
62
|
+
};
|
|
63
|
+
}, []);
|
|
64
|
+
if (!scopedContainerRef.current) return null;
|
|
65
|
+
return jsx(ScopedContainerContext.Provider, {
|
|
66
|
+
value: scopedContainerRef.current,
|
|
67
|
+
children
|
|
68
|
+
});
|
|
18
69
|
}
|
|
19
70
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
useEffect(() => {
|
|
40
|
-
return () => {
|
|
41
|
-
void container.endRequest(effectiveScopeId);
|
|
42
|
-
};
|
|
43
|
-
}, [container, effectiveScopeId]);
|
|
44
|
-
return jsx(ScopeContext.Provider, { value: effectiveScopeId, children });
|
|
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 = useContext(ScopedContainerContext);
|
|
85
|
+
const container = useContext(ContainerContext);
|
|
86
|
+
if (scopedContainer) return scopedContainer;
|
|
87
|
+
if (!container) throw new Error("useContainer must be used within a ContainerProvider");
|
|
88
|
+
return container;
|
|
45
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 = useContext(ContainerContext);
|
|
100
|
+
if (!container) throw new Error("useRootContainer must be used within a ContainerProvider");
|
|
101
|
+
return container;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/hooks/use-service.mts
|
|
46
106
|
function serviceReducer(state, action) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
+
}
|
|
59
120
|
}
|
|
60
121
|
function useService(token, args) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
122
|
+
const container = useContainer();
|
|
123
|
+
const serviceLocator = useRootContainer().getServiceLocator();
|
|
124
|
+
const initialSyncInstanceRef = useRef(void 0);
|
|
125
|
+
const isFirstRenderRef = useRef(true);
|
|
126
|
+
if (isFirstRenderRef.current) {
|
|
127
|
+
initialSyncInstanceRef.current = container.tryGetSync(token, args);
|
|
128
|
+
isFirstRenderRef.current = false;
|
|
129
|
+
}
|
|
130
|
+
const [state, dispatch] = useReducer(serviceReducer, initialSyncInstanceRef.current ? {
|
|
131
|
+
status: "success",
|
|
132
|
+
data: initialSyncInstanceRef.current
|
|
133
|
+
} : { status: "idle" });
|
|
134
|
+
const instanceNameRef = useRef(null);
|
|
135
|
+
const [refetchCounter, setRefetchCounter] = useState(0);
|
|
136
|
+
{
|
|
137
|
+
const argsRef = useRef(args);
|
|
138
|
+
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)}!
|
|
73
141
|
This is likely because you are using not memoized value that is not stable.
|
|
74
142
|
Please use a memoized value or use a different approach to pass the args.
|
|
75
143
|
Example:
|
|
76
144
|
const args = useMemo(() => ({ userId: '123' }), [])
|
|
77
145
|
return useService(UserToken, args)
|
|
78
146
|
`);
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
147
|
+
argsRef.current = args;
|
|
148
|
+
}
|
|
149
|
+
}, [args]);
|
|
150
|
+
}
|
|
151
|
+
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 = 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
|
+
};
|
|
138
213
|
}
|
|
139
|
-
|
|
214
|
+
|
|
215
|
+
//#endregion
|
|
216
|
+
//#region src/hooks/use-suspense-service.mts
|
|
217
|
+
const cacheMap = /* @__PURE__ */ new WeakMap();
|
|
140
218
|
function getCacheKey(token, args) {
|
|
141
|
-
|
|
142
|
-
return `${tokenId}:${JSON.stringify(args ?? null)}`;
|
|
219
|
+
return `${typeof token === "function" ? token.name : token.id || token.token?.id || String(token)}:${JSON.stringify(args ?? null)}`;
|
|
143
220
|
}
|
|
144
221
|
function getCache(container) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
+
});
|
|
151
242
|
}
|
|
152
243
|
function useSuspenseService(token, args) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
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 = useRef(null);
|
|
249
|
+
{
|
|
250
|
+
const argsRef = useRef(args);
|
|
251
|
+
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)}!
|
|
164
254
|
This is likely because you are using not memoized value that is not stable.
|
|
165
255
|
Please use a memoized value or use a different approach to pass the args.
|
|
166
256
|
Example:
|
|
167
257
|
const args = useMemo(() => ({ userId: '123' }), [])
|
|
168
258
|
return useService(UserToken, args)
|
|
169
259
|
`);
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
[entry]
|
|
233
|
-
);
|
|
234
|
-
const getSnapshot = useCallback(() => {
|
|
235
|
-
return `${entry.status}:${entry.version}`;
|
|
236
|
-
}, [entry]);
|
|
237
|
-
useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
238
|
-
useEffect(() => {
|
|
239
|
-
return () => {
|
|
240
|
-
};
|
|
241
|
-
}, []);
|
|
242
|
-
if (entry.status === "pending" && !entry.promise) {
|
|
243
|
-
fetchService();
|
|
244
|
-
}
|
|
245
|
-
if (entry.status === "pending") {
|
|
246
|
-
throw entry.promise;
|
|
247
|
-
}
|
|
248
|
-
if (entry.status === "rejected") {
|
|
249
|
-
throw entry.error;
|
|
250
|
-
}
|
|
251
|
-
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 = 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 = useCallback((callback) => {
|
|
305
|
+
entry.subscribers.add(callback);
|
|
306
|
+
return () => {
|
|
307
|
+
entry.subscribers.delete(callback);
|
|
308
|
+
};
|
|
309
|
+
}, [entry]);
|
|
310
|
+
const getSnapshot = useCallback(() => {
|
|
311
|
+
return `${entry.status}:${entry.version}`;
|
|
312
|
+
}, [entry]);
|
|
313
|
+
useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
314
|
+
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;
|
|
252
322
|
}
|
|
323
|
+
|
|
324
|
+
//#endregion
|
|
325
|
+
//#region src/hooks/use-optional-service.mts
|
|
253
326
|
function optionalServiceReducer(state, action) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
+
}
|
|
268
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
|
+
*/
|
|
269
367
|
function useOptionalService(token, args) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
368
|
+
const container = useContainer();
|
|
369
|
+
const serviceLocator = useRootContainer().getServiceLocator();
|
|
370
|
+
const initialSyncInstanceRef = useRef(void 0);
|
|
371
|
+
const isFirstRenderRef = 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] = useReducer(optionalServiceReducer, initialSyncInstanceRef.current ? {
|
|
379
|
+
status: "success",
|
|
380
|
+
data: initialSyncInstanceRef.current
|
|
381
|
+
} : { status: "idle" });
|
|
382
|
+
const instanceNameRef = useRef(null);
|
|
383
|
+
{
|
|
384
|
+
const argsRef = useRef(args);
|
|
385
|
+
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)}!
|
|
280
388
|
This is likely because you are using not memoized value that is not stable.
|
|
281
389
|
Please use a memoized value or use a different approach to pass the args.
|
|
282
390
|
Example:
|
|
283
391
|
const args = useMemo(() => ({ userId: '123' }), [])
|
|
284
392
|
return useOptionalService(UserToken, args)
|
|
285
393
|
`);
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
394
|
+
argsRef.current = args;
|
|
395
|
+
}
|
|
396
|
+
}, [args]);
|
|
397
|
+
}
|
|
398
|
+
const fetchService = 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
|
+
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
|
+
};
|
|
348
461
|
}
|
|
462
|
+
|
|
463
|
+
//#endregion
|
|
464
|
+
//#region src/hooks/use-invalidate.mts
|
|
349
465
|
function useInvalidate(token, args) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
466
|
+
const serviceLocator = useRootContainer().getServiceLocator();
|
|
467
|
+
return useCallback(async () => {
|
|
468
|
+
const instanceName = serviceLocator.getInstanceIdentifier(token, args);
|
|
469
|
+
await serviceLocator.invalidate(instanceName);
|
|
470
|
+
}, [
|
|
471
|
+
serviceLocator,
|
|
472
|
+
token,
|
|
473
|
+
args
|
|
474
|
+
]);
|
|
356
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
|
+
*/
|
|
357
503
|
function useInvalidateInstance() {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
},
|
|
363
|
-
[container]
|
|
364
|
-
);
|
|
504
|
+
const container = useContainer();
|
|
505
|
+
return useCallback(async (instance) => {
|
|
506
|
+
await container.invalidate(instance);
|
|
507
|
+
}, [container]);
|
|
365
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
|
+
*/
|
|
366
516
|
function useScope() {
|
|
367
|
-
|
|
517
|
+
return useContext(ScopedContainerContext)?.getRequestId() ?? null;
|
|
368
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
|
+
*/
|
|
369
523
|
function useScopeOrThrow() {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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 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 useContext(ScopedContainerContext)?.getMetadata(key);
|
|
377
576
|
}
|
|
378
577
|
|
|
379
|
-
|
|
380
|
-
|
|
578
|
+
//#endregion
|
|
579
|
+
export { ContainerContext, ContainerProvider, ScopeProvider, ScopedContainerContext, useContainer, useInvalidate, useInvalidateInstance, useOptionalService, useRootContainer, useScope, useScopeMetadata, useScopeOrThrow, useScopedContainer, useScopedContainerOrThrow, useService, useSuspenseService };
|
|
381
580
|
//# sourceMappingURL=index.mjs.map
|