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