@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/lib/index.js CHANGED
@@ -1,394 +1,595 @@
1
- 'use strict';
1
+ let react = require("react");
2
+ let react_jsx_runtime = require("react/jsx-runtime");
2
3
 
3
- var react = require('react');
4
- var jsxRuntime = require('react/jsx-runtime');
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
- // src/providers/context.mts
7
- var ContainerContext = react.createContext(null);
8
- function ContainerProvider({
9
- container,
10
- children
11
- }) {
12
- return jsxRuntime.jsx(ContainerContext.Provider, { value: container, children });
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
- function useContainer() {
15
- const container = react.useContext(ContainerContext);
16
- if (!container) {
17
- throw new Error("useContainer must be used within a ContainerProvider");
18
- }
19
- return container;
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
- // src/providers/scope-provider.mts
23
- var ScopeContext = react.createContext(null);
24
- function ScopeProvider({
25
- scopeId,
26
- metadata,
27
- priority = 100,
28
- children
29
- }) {
30
- const container = useContainer();
31
- const generatedId = react.useId();
32
- const effectiveScopeId = scopeId ?? generatedId;
33
- const isInitializedRef = react.useRef(false);
34
- if (!isInitializedRef.current) {
35
- const existingContexts = container.getServiceLocator().getRequestContexts();
36
- if (!existingContexts.has(effectiveScopeId)) {
37
- container.beginRequest(effectiveScopeId, metadata, priority);
38
- }
39
- isInitializedRef.current = true;
40
- }
41
- react.useEffect(() => {
42
- return () => {
43
- void container.endRequest(effectiveScopeId);
44
- };
45
- }, [container, effectiveScopeId]);
46
- return jsxRuntime.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 = (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
- switch (action.type) {
50
- case "loading":
51
- return { status: "loading" };
52
- case "success":
53
- return { status: "success", data: action.data };
54
- case "error":
55
- return { status: "error", error: action.error };
56
- case "reset":
57
- return { status: "idle" };
58
- default:
59
- return state;
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
- const container = useContainer();
64
- const serviceLocator = container.getServiceLocator();
65
- const scopeId = react.useContext(ScopeContext);
66
- const [state, dispatch] = react.useReducer(serviceReducer, { status: "idle" });
67
- const instanceNameRef = react.useRef(null);
68
- const [refetchCounter, setRefetchCounter] = react.useState(0);
69
- {
70
- const argsRef = react.useRef(args);
71
- react.useEffect(() => {
72
- if (argsRef.current !== args) {
73
- if (JSON.stringify(argsRef.current) === JSON.stringify(args)) {
74
- console.log(`WARNING: useService called with args that look the same but are different instances: ${JSON.stringify(argsRef.current)} !== ${JSON.stringify(args)}!
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
- argsRef.current = args;
83
- }
84
- }, [args]);
85
- }
86
- react.useEffect(() => {
87
- const eventBus = serviceLocator.getEventBus();
88
- let unsubscribe;
89
- let isMounted = true;
90
- const fetchAndSubscribe = async () => {
91
- try {
92
- if (scopeId) {
93
- const requestContexts = serviceLocator.getRequestContexts();
94
- if (requestContexts.has(scopeId)) {
95
- container.setCurrentRequestContext(scopeId);
96
- }
97
- }
98
- const instance = await container.get(
99
- // @ts-expect-error - token is valid
100
- token,
101
- args
102
- );
103
- if (!isMounted) return;
104
- const instanceName = serviceLocator.getInstanceIdentifier(
105
- token,
106
- args
107
- );
108
- instanceNameRef.current = instanceName;
109
- dispatch({ type: "success", data: instance });
110
- unsubscribe = eventBus.on(instanceName, "destroy", () => {
111
- if (isMounted) {
112
- dispatch({ type: "loading" });
113
- void fetchAndSubscribe();
114
- }
115
- });
116
- } catch (error) {
117
- if (isMounted) {
118
- dispatch({ type: "error", error });
119
- }
120
- }
121
- };
122
- dispatch({ type: "loading" });
123
- void fetchAndSubscribe();
124
- return () => {
125
- isMounted = false;
126
- unsubscribe?.();
127
- };
128
- }, [container, serviceLocator, token, args, scopeId, refetchCounter]);
129
- const refetch = react.useCallback(() => {
130
- setRefetchCounter((c) => c + 1);
131
- }, []);
132
- return {
133
- data: state.status === "success" ? state.data : void 0,
134
- error: state.status === "error" ? state.error : void 0,
135
- isLoading: state.status === "loading",
136
- isSuccess: state.status === "success",
137
- isError: state.status === "error",
138
- refetch
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
- var cacheMap = /* @__PURE__ */ new WeakMap();
214
+
215
+ //#endregion
216
+ //#region src/hooks/use-suspense-service.mts
217
+ const cacheMap = /* @__PURE__ */ new WeakMap();
142
218
  function getCacheKey(token, args) {
143
- const tokenId = typeof token === "function" ? token.name : token.id || token.token?.id || String(token);
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
- let cache = cacheMap.get(container);
148
- if (!cache) {
149
- cache = /* @__PURE__ */ new Map();
150
- cacheMap.set(container, cache);
151
- }
152
- return cache;
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
- const container = useContainer();
156
- const serviceLocator = container.getServiceLocator();
157
- const cache = getCache(container);
158
- const cacheKey = getCacheKey(token, args);
159
- const entryRef = react.useRef(null);
160
- {
161
- const argsRef = react.useRef(args);
162
- react.useEffect(() => {
163
- if (argsRef.current !== args) {
164
- if (JSON.stringify(argsRef.current) === JSON.stringify(args)) {
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
- argsRef.current = args;
174
- }
175
- }, [args]);
176
- }
177
- if (!cache.has(cacheKey)) {
178
- const entry2 = {
179
- promise: null,
180
- result: void 0,
181
- error: void 0,
182
- status: "pending",
183
- version: 0,
184
- subscribers: /* @__PURE__ */ new Set(),
185
- instanceName: null,
186
- unsubscribe: void 0
187
- };
188
- cache.set(cacheKey, entry2);
189
- }
190
- const entry = cache.get(cacheKey);
191
- entryRef.current = entry;
192
- const fetchService = react.useCallback(() => {
193
- const currentEntry = entryRef.current;
194
- if (!currentEntry) return;
195
- currentEntry.status = "pending";
196
- currentEntry.version++;
197
- currentEntry.promise = container.get(token, args).then((instance) => {
198
- currentEntry.result = instance;
199
- currentEntry.status = "resolved";
200
- currentEntry.instanceName = serviceLocator.getInstanceIdentifier(
201
- token,
202
- args
203
- );
204
- if (!currentEntry.unsubscribe && currentEntry.instanceName) {
205
- const eventBus = serviceLocator.getEventBus();
206
- currentEntry.unsubscribe = eventBus.on(
207
- currentEntry.instanceName,
208
- "destroy",
209
- () => {
210
- currentEntry.result = void 0;
211
- currentEntry.error = void 0;
212
- currentEntry.status = "pending";
213
- currentEntry.promise = null;
214
- currentEntry.subscribers.forEach((callback) => callback());
215
- }
216
- );
217
- }
218
- currentEntry.subscribers.forEach((callback) => callback());
219
- return instance;
220
- }).catch((error) => {
221
- currentEntry.error = error;
222
- currentEntry.status = "rejected";
223
- throw error;
224
- });
225
- return currentEntry.promise;
226
- }, [container, serviceLocator, token, args]);
227
- const subscribe = react.useCallback(
228
- (callback) => {
229
- entry.subscribers.add(callback);
230
- return () => {
231
- entry.subscribers.delete(callback);
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
- switch (action.type) {
257
- case "loading":
258
- return { status: "loading" };
259
- case "success":
260
- return { status: "success", data: action.data };
261
- case "not-found":
262
- return { status: "not-found" };
263
- case "error":
264
- return { status: "error", error: action.error };
265
- case "reset":
266
- return { status: "idle" };
267
- default:
268
- return state;
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
- const container = useContainer();
273
- const serviceLocator = container.getServiceLocator();
274
- const [state, dispatch] = react.useReducer(optionalServiceReducer, { status: "idle" });
275
- const instanceNameRef = react.useRef(null);
276
- {
277
- const argsRef = react.useRef(args);
278
- react.useEffect(() => {
279
- if (argsRef.current !== args) {
280
- if (JSON.stringify(argsRef.current) === JSON.stringify(args)) {
281
- console.log(`WARNING: useOptionalService called with args that look the same but are different instances: ${JSON.stringify(argsRef.current)} !== ${JSON.stringify(args)}!
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
- argsRef.current = args;
290
- }
291
- }, [args]);
292
- }
293
- const fetchService = react.useCallback(async () => {
294
- dispatch({ type: "loading" });
295
- try {
296
- const [error, instance] = await serviceLocator.getInstance(
297
- token,
298
- args
299
- );
300
- if (error) {
301
- const errorMessage = error.message?.toLowerCase() ?? "";
302
- if (errorMessage.includes("not found") || errorMessage.includes("not registered") || errorMessage.includes("no provider")) {
303
- dispatch({ type: "not-found" });
304
- } else {
305
- dispatch({ type: "error", error });
306
- }
307
- return;
308
- }
309
- instanceNameRef.current = serviceLocator.getInstanceIdentifier(
310
- token,
311
- args
312
- );
313
- dispatch({ type: "success", data: instance });
314
- } catch (error) {
315
- const err = error;
316
- const errorMessage = err.message?.toLowerCase() ?? "";
317
- if (errorMessage.includes("not found") || errorMessage.includes("not registered") || errorMessage.includes("no provider")) {
318
- dispatch({ type: "not-found" });
319
- } else {
320
- dispatch({ type: "error", error: err });
321
- }
322
- }
323
- }, [serviceLocator, token, args]);
324
- react.useEffect(() => {
325
- const eventBus = serviceLocator.getEventBus();
326
- let unsubscribe;
327
- void fetchService();
328
- const setupSubscription = () => {
329
- if (instanceNameRef.current) {
330
- unsubscribe = eventBus.on(instanceNameRef.current, "destroy", () => {
331
- void fetchService();
332
- });
333
- }
334
- };
335
- const timeoutId = setTimeout(setupSubscription, 10);
336
- return () => {
337
- clearTimeout(timeoutId);
338
- unsubscribe?.();
339
- };
340
- }, [fetchService, serviceLocator]);
341
- return {
342
- data: state.status === "success" ? state.data : void 0,
343
- error: state.status === "error" ? state.error : void 0,
344
- isLoading: state.status === "loading",
345
- isSuccess: state.status === "success",
346
- isNotFound: state.status === "not-found",
347
- isError: state.status === "error",
348
- refetch: fetchService
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
- const container = useContainer();
353
- const serviceLocator = container.getServiceLocator();
354
- return react.useCallback(async () => {
355
- const instanceName = serviceLocator.getInstanceIdentifier(token, args);
356
- await serviceLocator.invalidate(instanceName);
357
- }, [serviceLocator, token, args]);
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
- const container = useContainer();
361
- return react.useCallback(
362
- async (instance) => {
363
- await container.invalidate(instance);
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
- return react.useContext(ScopeContext);
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
- const scope = useScope();
373
- if (scope === null) {
374
- throw new Error(
375
- "useScopeOrThrow must be used within a ScopeProvider. Wrap your component tree with <ScopeProvider> to create a request scope."
376
- );
377
- }
378
- return scope;
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