@navios/di-react 0.1.0 → 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,392 +1,580 @@
1
- import { createContext, createElement, useContext, useId, useRef, useEffect, useReducer, useState, useCallback, useSyncExternalStore } from 'react';
1
+ import { createContext, useCallback, useContext, useEffect, useId, useReducer, useRef, useState, useSyncExternalStore } from "react";
2
+ import { jsx } from "react/jsx-runtime";
2
3
 
3
- // src/providers/context.mts
4
- var ContainerContext = createContext(null);
5
- function ContainerProvider({
6
- container,
7
- children
8
- }) {
9
- return createElement(
10
- ContainerContext.Provider,
11
- { value: container },
12
- children
13
- );
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
+ });
14
23
  }
15
- function useContainer() {
16
- const container = useContext(ContainerContext);
17
- if (!container) {
18
- throw new Error("useContainer must be used within a ContainerProvider");
19
- }
20
- 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
+ });
21
69
  }
22
70
 
23
- // src/providers/scope-provider.mts
24
- var ScopeContext = createContext(null);
25
- function ScopeProvider({
26
- scopeId,
27
- metadata,
28
- priority = 100,
29
- children
30
- }) {
31
- const container = useContainer();
32
- const generatedId = useId();
33
- const effectiveScopeId = scopeId ?? generatedId;
34
- const isInitializedRef = useRef(false);
35
- if (!isInitializedRef.current) {
36
- const existingContexts = container.getServiceLocator().getRequestContexts();
37
- if (!existingContexts.has(effectiveScopeId)) {
38
- container.beginRequest(effectiveScopeId, metadata, priority);
39
- }
40
- isInitializedRef.current = true;
41
- }
42
- useEffect(() => {
43
- return () => {
44
- void container.endRequest(effectiveScopeId);
45
- };
46
- }, [container, effectiveScopeId]);
47
- return createElement(
48
- ScopeContext.Provider,
49
- { value: effectiveScopeId },
50
- children
51
- );
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;
52
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
53
106
  function serviceReducer(state, action) {
54
- switch (action.type) {
55
- case "loading":
56
- return { status: "loading" };
57
- case "success":
58
- return { status: "success", data: action.data };
59
- case "error":
60
- return { status: "error", error: action.error };
61
- case "reset":
62
- return { status: "idle" };
63
- default:
64
- return state;
65
- }
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
+ }
66
120
  }
67
121
  function useService(token, args) {
68
- const container = useContainer();
69
- const serviceLocator = container.getServiceLocator();
70
- const scopeId = useContext(ScopeContext);
71
- const [state, dispatch] = useReducer(serviceReducer, { status: "idle" });
72
- const instanceNameRef = useRef(null);
73
- const [refetchCounter, setRefetchCounter] = useState(0);
74
- {
75
- const argsRef = useRef(args);
76
- useEffect(() => {
77
- if (argsRef.current !== args) {
78
- if (JSON.stringify(argsRef.current) === JSON.stringify(args)) {
79
- 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)}!
80
141
  This is likely because you are using not memoized value that is not stable.
81
142
  Please use a memoized value or use a different approach to pass the args.
82
143
  Example:
83
144
  const args = useMemo(() => ({ userId: '123' }), [])
84
145
  return useService(UserToken, args)
85
146
  `);
86
- }
87
- argsRef.current = args;
88
- }
89
- }, [args]);
90
- }
91
- useEffect(() => {
92
- const eventBus = serviceLocator.getEventBus();
93
- let unsubscribe;
94
- let isMounted = true;
95
- const fetchAndSubscribe = async () => {
96
- try {
97
- if (scopeId) {
98
- const requestContexts = serviceLocator.getRequestContexts();
99
- if (requestContexts.has(scopeId)) {
100
- container.setCurrentRequestContext(scopeId);
101
- }
102
- }
103
- const instance = await container.get(
104
- // @ts-expect-error - token is valid
105
- token,
106
- args
107
- );
108
- if (!isMounted) return;
109
- const instanceName = serviceLocator.getInstanceIdentifier(
110
- token,
111
- args
112
- );
113
- instanceNameRef.current = instanceName;
114
- dispatch({ type: "success", data: instance });
115
- unsubscribe = eventBus.on(instanceName, "destroy", () => {
116
- if (isMounted) {
117
- dispatch({ type: "loading" });
118
- void fetchAndSubscribe();
119
- }
120
- });
121
- } catch (error) {
122
- if (isMounted) {
123
- dispatch({ type: "error", error });
124
- }
125
- }
126
- };
127
- dispatch({ type: "loading" });
128
- void fetchAndSubscribe();
129
- return () => {
130
- isMounted = false;
131
- unsubscribe?.();
132
- };
133
- }, [container, serviceLocator, token, args, scopeId, refetchCounter]);
134
- const refetch = useCallback(() => {
135
- setRefetchCounter((c) => c + 1);
136
- }, []);
137
- return {
138
- data: state.status === "success" ? state.data : void 0,
139
- error: state.status === "error" ? state.error : void 0,
140
- isLoading: state.status === "loading",
141
- isSuccess: state.status === "success",
142
- isError: state.status === "error",
143
- refetch
144
- };
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
+ };
145
213
  }
146
- var cacheMap = /* @__PURE__ */ new WeakMap();
214
+
215
+ //#endregion
216
+ //#region src/hooks/use-suspense-service.mts
217
+ const cacheMap = /* @__PURE__ */ new WeakMap();
147
218
  function getCacheKey(token, args) {
148
- const tokenId = typeof token === "function" ? token.name : token.id || token.token?.id || String(token);
149
- return `${tokenId}:${JSON.stringify(args ?? null)}`;
219
+ return `${typeof token === "function" ? token.name : token.id || token.token?.id || String(token)}:${JSON.stringify(args ?? null)}`;
150
220
  }
151
221
  function getCache(container) {
152
- let cache = cacheMap.get(container);
153
- if (!cache) {
154
- cache = /* @__PURE__ */ new Map();
155
- cacheMap.set(container, cache);
156
- }
157
- 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
+ });
158
242
  }
159
243
  function useSuspenseService(token, args) {
160
- const container = useContainer();
161
- const serviceLocator = container.getServiceLocator();
162
- const cache = getCache(container);
163
- const cacheKey = getCacheKey(token, args);
164
- const entryRef = useRef(null);
165
- {
166
- const argsRef = useRef(args);
167
- useEffect(() => {
168
- if (argsRef.current !== args) {
169
- if (JSON.stringify(argsRef.current) === JSON.stringify(args)) {
170
- 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)}!
171
254
  This is likely because you are using not memoized value that is not stable.
172
255
  Please use a memoized value or use a different approach to pass the args.
173
256
  Example:
174
257
  const args = useMemo(() => ({ userId: '123' }), [])
175
258
  return useService(UserToken, args)
176
259
  `);
177
- }
178
- argsRef.current = args;
179
- }
180
- }, [args]);
181
- }
182
- if (!cache.has(cacheKey)) {
183
- const entry2 = {
184
- promise: null,
185
- result: void 0,
186
- error: void 0,
187
- status: "pending",
188
- version: 0,
189
- subscribers: /* @__PURE__ */ new Set(),
190
- instanceName: null,
191
- unsubscribe: void 0
192
- };
193
- cache.set(cacheKey, entry2);
194
- }
195
- const entry = cache.get(cacheKey);
196
- entryRef.current = entry;
197
- const fetchService = useCallback(() => {
198
- const currentEntry = entryRef.current;
199
- if (!currentEntry) return;
200
- currentEntry.status = "pending";
201
- currentEntry.version++;
202
- currentEntry.promise = container.get(token, args).then((instance) => {
203
- currentEntry.result = instance;
204
- currentEntry.status = "resolved";
205
- currentEntry.instanceName = serviceLocator.getInstanceIdentifier(
206
- token,
207
- args
208
- );
209
- if (!currentEntry.unsubscribe && currentEntry.instanceName) {
210
- const eventBus = serviceLocator.getEventBus();
211
- currentEntry.unsubscribe = eventBus.on(
212
- currentEntry.instanceName,
213
- "destroy",
214
- () => {
215
- currentEntry.result = void 0;
216
- currentEntry.error = void 0;
217
- currentEntry.status = "pending";
218
- currentEntry.promise = null;
219
- currentEntry.subscribers.forEach((callback) => callback());
220
- }
221
- );
222
- }
223
- currentEntry.subscribers.forEach((callback) => callback());
224
- return instance;
225
- }).catch((error) => {
226
- currentEntry.error = error;
227
- currentEntry.status = "rejected";
228
- throw error;
229
- });
230
- return currentEntry.promise;
231
- }, [container, serviceLocator, token, args]);
232
- const subscribe = useCallback(
233
- (callback) => {
234
- entry.subscribers.add(callback);
235
- return () => {
236
- entry.subscribers.delete(callback);
237
- };
238
- },
239
- [entry]
240
- );
241
- const getSnapshot = useCallback(() => {
242
- return `${entry.status}:${entry.version}`;
243
- }, [entry]);
244
- useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
245
- useEffect(() => {
246
- return () => {
247
- if (entry.subscribers.size === 0) {
248
- entry.unsubscribe?.();
249
- cache.delete(cacheKey);
250
- }
251
- };
252
- }, [entry]);
253
- if (entry.status === "pending" && !entry.promise) {
254
- fetchService();
255
- }
256
- if (entry.status === "pending") {
257
- throw entry.promise;
258
- }
259
- if (entry.status === "rejected") {
260
- throw entry.error;
261
- }
262
- 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;
263
322
  }
323
+
324
+ //#endregion
325
+ //#region src/hooks/use-optional-service.mts
264
326
  function optionalServiceReducer(state, action) {
265
- switch (action.type) {
266
- case "loading":
267
- return { status: "loading" };
268
- case "success":
269
- return { status: "success", data: action.data };
270
- case "not-found":
271
- return { status: "not-found" };
272
- case "error":
273
- return { status: "error", error: action.error };
274
- case "reset":
275
- return { status: "idle" };
276
- default:
277
- return state;
278
- }
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
+ }
279
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
+ */
280
367
  function useOptionalService(token, args) {
281
- const container = useContainer();
282
- const serviceLocator = container.getServiceLocator();
283
- const [state, dispatch] = useReducer(optionalServiceReducer, { status: "idle" });
284
- const instanceNameRef = useRef(null);
285
- {
286
- const argsRef = useRef(args);
287
- useEffect(() => {
288
- if (argsRef.current !== args) {
289
- if (JSON.stringify(argsRef.current) === JSON.stringify(args)) {
290
- 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)}!
291
388
  This is likely because you are using not memoized value that is not stable.
292
389
  Please use a memoized value or use a different approach to pass the args.
293
390
  Example:
294
391
  const args = useMemo(() => ({ userId: '123' }), [])
295
392
  return useOptionalService(UserToken, args)
296
393
  `);
297
- }
298
- argsRef.current = args;
299
- }
300
- }, [args]);
301
- }
302
- const fetchService = useCallback(async () => {
303
- dispatch({ type: "loading" });
304
- try {
305
- const [error, instance] = await serviceLocator.getInstance(
306
- token,
307
- args
308
- );
309
- if (error) {
310
- const errorMessage = error.message?.toLowerCase() ?? "";
311
- if (errorMessage.includes("not found") || errorMessage.includes("not registered") || errorMessage.includes("no provider")) {
312
- dispatch({ type: "not-found" });
313
- } else {
314
- dispatch({ type: "error", error });
315
- }
316
- return;
317
- }
318
- instanceNameRef.current = serviceLocator.getInstanceIdentifier(
319
- token,
320
- args
321
- );
322
- dispatch({ type: "success", data: instance });
323
- } catch (error) {
324
- const err = error;
325
- const errorMessage = err.message?.toLowerCase() ?? "";
326
- if (errorMessage.includes("not found") || errorMessage.includes("not registered") || errorMessage.includes("no provider")) {
327
- dispatch({ type: "not-found" });
328
- } else {
329
- dispatch({ type: "error", error: err });
330
- }
331
- }
332
- }, [serviceLocator, token, args]);
333
- useEffect(() => {
334
- const eventBus = serviceLocator.getEventBus();
335
- let unsubscribe;
336
- void fetchService();
337
- const setupSubscription = () => {
338
- if (instanceNameRef.current) {
339
- unsubscribe = eventBus.on(instanceNameRef.current, "destroy", () => {
340
- void fetchService();
341
- });
342
- }
343
- };
344
- const timeoutId = setTimeout(setupSubscription, 10);
345
- return () => {
346
- clearTimeout(timeoutId);
347
- unsubscribe?.();
348
- };
349
- }, [fetchService, serviceLocator]);
350
- return {
351
- data: state.status === "success" ? state.data : void 0,
352
- error: state.status === "error" ? state.error : void 0,
353
- isLoading: state.status === "loading",
354
- isSuccess: state.status === "success",
355
- isNotFound: state.status === "not-found",
356
- isError: state.status === "error",
357
- refetch: fetchService
358
- };
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
+ };
359
461
  }
462
+
463
+ //#endregion
464
+ //#region src/hooks/use-invalidate.mts
360
465
  function useInvalidate(token, args) {
361
- const container = useContainer();
362
- const serviceLocator = container.getServiceLocator();
363
- return useCallback(async () => {
364
- const instanceName = serviceLocator.getInstanceIdentifier(token, args);
365
- await serviceLocator.invalidate(instanceName);
366
- }, [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
+ ]);
367
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
+ */
368
503
  function useInvalidateInstance() {
369
- const container = useContainer();
370
- return useCallback(
371
- async (instance) => {
372
- await container.invalidate(instance);
373
- },
374
- [container]
375
- );
504
+ const container = useContainer();
505
+ return useCallback(async (instance) => {
506
+ await container.invalidate(instance);
507
+ }, [container]);
376
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
+ */
377
516
  function useScope() {
378
- return useContext(ScopeContext);
517
+ return useContext(ScopedContainerContext)?.getRequestId() ?? null;
379
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
+ */
380
523
  function useScopeOrThrow() {
381
- const scope = useScope();
382
- if (scope === null) {
383
- throw new Error(
384
- "useScopeOrThrow must be used within a ScopeProvider. Wrap your component tree with <ScopeProvider> to create a request scope."
385
- );
386
- }
387
- 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);
388
576
  }
389
577
 
390
- export { ContainerContext, ContainerProvider, ScopeContext, ScopeProvider, useContainer, useInvalidate, useInvalidateInstance, useOptionalService, useScope, useScopeOrThrow, useService, useSuspenseService };
391
- //# sourceMappingURL=index.mjs.map
578
+ //#endregion
579
+ export { ContainerContext, ContainerProvider, ScopeProvider, ScopedContainerContext, useContainer, useInvalidate, useInvalidateInstance, useOptionalService, useRootContainer, useScope, useScopeMetadata, useScopeOrThrow, useScopedContainer, useScopedContainerOrThrow, useService, useSuspenseService };
392
580
  //# sourceMappingURL=index.mjs.map